Top Banner
Advanced Internationalization with Rails Clinton R. Nixon Viget Labs
34

Advanced Internationalization with Rails

May 12, 2015

Download

Technology

Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: Advanced Internationalization with Rails

AdvancedInternationalization with Rails

Clinton R. NixonViget Labs

Page 2: Advanced Internationalization with Rails

Hola! Qué tal?

Me llamo Clinton R. Nixon. Soy programador de software.

Vivo en los Estados Unidos. Mi cuidad es de Durham en Carolina del Norte.

Hablo un poco de español. Entiendo un poco mas.

Me encanta Ruby y Rails.

Page 3: Advanced Internationalization with Rails

The current state of Rails i18n

Key-value lookup

ActiveRecord support

Interpolation

Weak pluralization

Localized formats

Localized views (index.es.html.erb)

Page 4: Advanced Internationalization with Rails

Key-value lookupen:    site_title:  The  Lost  Tourist    add_tourism_office:  Add  a  New  Tourism  Office    add_city:  Add  a  New  Cityes:    site_title:  El  Turista  que  se  Pierde    add_tourism_office:  Crear  una  Nueva  Oficina  de  Turismo    add_city:  Crear  una  Nueva  Cuidad

link_to(t(:site_title),  root_url)

Page 5: Advanced Internationalization with Rails

ActiveRecord support

es:    activerecord:        errors:            messages:                phone_number:  "no  se  puede  en  blanco  a  menos  que  el  número  de  teléfono  de  trabajo,  número  de  teléfono  móvil,  o  otro  número  de  teléfono"        models:            business:  Negocio            business_plan:  Plan  de  Negocios            business_process:  Función  Crítica        attributes:            pet:                name:  Nombre                species:  Especies                special_needs:  Este  Mascota  Tiene  Necesidades  Especiales  Médicas

Page 6: Advanced Internationalization with Rails

Interpolation

es:    welcome_to:  "Bienvenido  a  {{city}}!"

t(:welcome_to,  :city  =>  @city.name)

Page 7: Advanced Internationalization with Rails

Pluralization

es:    city_has_x_tourism_offices:          one:  "Hay  {{count}}  oficina  de  turismo  de  {{city}}."        other:  "Hay  {{count}}  oficinas  de  turismo  de  {{city}}."

t(:city_has_x_tourism_offices,  :city  =>  @city.name,  :count  =>  @city.tourism_offices.count)

Page 8: Advanced Internationalization with Rails

Localized formatses:    number:        format:            #  Sets  the  separator  between  the  units  (e.g.  1.0  /  2.0  ==  0.5)            separator:  ","              #  Delimits  thousands  (e.g.  1,000,000  is  a  million)            delimiter:  "."              #  Number  of  decimals  (1  with  a  precision  of  2  gives:  1.00)            precision:  3

       #  Used  in  number_to_currency()        currency:            format:                #  %u  is  the  currency  unit,  %n  the  number  (default:  $5.00)                format:  "%n  %u"                  unit:  "€"                  #  These  three  are  to  override  number.format  and  are  optional                separator:  ","                  delimiter:  "."                  precision:  2

Page 9: Advanced Internationalization with Rails

Localized views

app/    views/        cities/            show.haml        #  used  by  default  locale            show.es.haml  #  used  by  :es  locale

Page 10: Advanced Internationalization with Rails

Difficulties

File-based key-value lookup is not user-editable.

Model attributes are not translated.

The Rails inflector is not multi-lingual.

Page 11: Advanced Internationalization with Rails

Plugins to help

Newsdesk Translate

translatable_columns

Globalize2

Page 12: Advanced Internationalization with Rails

Model attribute translations

test  "returns  the  value  for  the  correct  locale"  do    post  =  Post.create  :subject  =>  'foo'    I18n.locale  =  'de'    post.subject  =  'bar'    post.save    I18n.locale  =  'en'    post  =  Post.first    assert_equal  'foo',  post.subject    I18n.locale  =  'de'    assert_equal  'bar',  post.subjectend

Page 13: Advanced Internationalization with Rails

Other Globalize2 featuresLocale fallbacks    I18n.fallbacks[:"es-­‐MX"]  #  =>  [:"es-­‐MX",  :es,  :"en-­‐US",  :en]

Custom pluralization    @backend.add_pluralizer  :cz,  lambda{|c|          c  ==  1  ?  :one  :  (2..4).include?(c)  ?  :few  :  :other      }

Metadata    rails  =  I18n.t  :rails    #  if  no  translation  can  be  found:    rails.locale                      #  =>  :en    rails.requested_locale  #  =>  :es      rails.fallback?                #  =>  true    rails.options                    #  returns  the  options  passed  to  #t    rails.plural_key              #  returns  the  plural_key  (e.g.  :one,  :other)    rails.original                  #  returns  the  original  translation

Page 14: Advanced Internationalization with Rails

Translating models

Globalize2 and translatable_modelswork well for this.

Page 15: Advanced Internationalization with Rails

How to start building a backendThe easiest way is to extend I18n::Backend::Simple.

Key methods are:

init_translations  (protected)store_translationstranslatelocalizeavailable_locales

Page 16: Advanced Internationalization with Rails

Example: DB-backed backendclass  SnippetBackend  <  I18n::Backend::Simple    #  These  are  the  only  supported  locales    LOCALES  =  [:en,  :es]          protected

   def  init_translations        load_translations(*I18n.load_path.flatten)        load_translations_from_database        @initialized  =  true    end    #  ...end

Page 17: Advanced Internationalization with Rails

class  SnippetBackend  <  I18n::Backend::Simple    #  These  are  the  only  supported  locales    LOCALES  =  [:en,  :es]          protected

   def  init_translations        load_translations(*I18n.load_path.flatten)        load_translations_from_database        @initialized  =  true    end    #  ...end

Example: DB-backed backend

Page 18: Advanced Internationalization with Rails

Storing the translationsclass  Snippet  <  ActiveRecord::Base    validates_presence_of  :name,  :en_text,  :es_text    validates_uniqueness_of  :name    validate  :name_must_not_be_subset_of_other_name

   private    def  name_must_not_be_subset_of_other_name        if  self.name  =~  /\./            snippets  =  Snippet.find(:all,                      :conditions  =>  ['name  LIKE  ?',  "#{self.name}%"])            snippets.reject!  {  |s|  s.id  ==  self.id  }  unless  self.new_record?

           unless  snippets.empty?                errors.add(:name,                      "must  not  be  a  subset  of  another  snippet  name")            end        end    endend

Page 19: Advanced Internationalization with Rails

Loading translations from DBdef  load_translations_from_database    data  =  {  }    LOCALES.each  {  |locale|  data[locale]  =  {}  }

   Snippet.all.each  do  |snippet|        path  =  snippet.name.split(".")        key  =  path.pop        current  =  {}        LOCALES.each  {  |locale|  current[locale]  =  data[locale]  }

       path.each  do  |group|            LOCALES.each  do  |locale|                  current[locale][group]  ||=  {}                current[locale]  =  current[locale][group]            end        end

       LOCALES.each  {  |locale|              current[locale][key]  =  snippet.read_attribute("#{locale}_text")  }                    end    data.each  {  |locale,  d|  merge_translations(locale,  d)  }end

Page 20: Advanced Internationalization with Rails

Example: a bad ideamodule  Traductor    class  GoogleBackend  <  I18n::Backend::Simple        def  translate(locale,  key,  options  =  {})            if  locale.to_s  ==  'en'                key            else                Translate.t(key,  'en',  locale.to_s)            end        end    end

Page 21: Advanced Internationalization with Rails

Translation data format{:en  =>  {      "pets"  =>  {  "canine"  =>  "dog",  "feline"  =>  "cat"  },    "greeting"  =>  "Hello!",    "farewell"  =>  "Goodbye!"  },  :es  =>  {      "pets"  =>  {  "canine"  =>  "perro",  "feline"  =>  "gato"  },      "greeting"  =>  "¡Hola!",      "farewell"  =>  "¡Hasta  luego!"  }  }

One could override merge_translations if you needed to bring in data in another format.

Page 22: Advanced Internationalization with Rails

Improving inflections

Let’s make the Rails inflector multilingual!

This will be incredibly invasive, full of monkey-patching, and quite dangerous.

Page 23: Advanced Internationalization with Rails

Why?

Generated forms: Formtastic, Typus%h2=  City.human_name.pluralize

Page 24: Advanced Internationalization with Rails

Black magicmodule  ActiveSupport::Inflector    def  inflections(locale  =  nil)        locale  ||=  I18n.locale        locale_class  =  ActiveSupport::Inflector.const_get(            "Inflections_#{locale}")  rescue  nil

       if  locale_class.nil?            ActiveSupport::Inflector.module_eval  %{                class  ActiveSupport::Inflector::Inflections_#{locale}  <  ActiveSupport::Inflector::Inflections                end            }            locale_class  =  ActiveSupport::Inflector.const_get(                "Inflections_#{locale}")        end

       block_given?  ?  yield  locale_class.instance  :  locale_class.instance    end

Page 25: Advanced Internationalization with Rails

Black magicmodule  ActiveSupport::Inflector    def  inflections(locale  =  nil)        locale  ||=  I18n.locale        locale_class  =  ActiveSupport::Inflector.const_get(            "Inflections_#{locale}")  rescue  nil

       if  locale_class.nil?            ActiveSupport::Inflector.module_eval  %{                class  ActiveSupport::Inflector::Inflections_#{locale}  <  ActiveSupport::Inflector::Inflections                end            }            locale_class  =  ActiveSupport::Inflector.const_get(                "Inflections_#{locale}")        end

       block_given?  ?  yield  locale_class.instance  :  locale_class.instance    end

Page 26: Advanced Internationalization with Rails

module  ActiveSupport::Inflector    def  inflections(locale  =  nil)        locale  ||=  I18n.locale        locale_class  =  ActiveSupport::Inflector.const_get(            "Inflections_#{locale}")  rescue  nil

       if  locale_class.nil?            ActiveSupport::Inflector.module_eval  %{                class  ActiveSupport::Inflector::Inflections_#{locale}  <  ActiveSupport::Inflector::Inflections                end            }            locale_class  =  ActiveSupport::Inflector.const_get(                "Inflections_#{locale}")        end

       block_given?  ?  yield  locale_class.instance  :  locale_class.instance    end

Black magic

Page 27: Advanced Internationalization with Rails

Success!ActiveSupport::Inflector.inflections(:es)  do  |inflect|    inflect.plural  /$/,  's'    inflect.plural  /([^aeioué])$/,  '\1es'    inflect.plural  /([aeiou]s)$/,  '\1'    inflect.plural  /z$/,  'ces'    inflect.plural  /á([sn])$/,  'a\1es'    inflect.plural  /í([sn])$/,  'i\1es'    inflect.plural  /ó([sn])$/,  'o\1es'    inflect.plural  /ú([sn])$/,  'u\1es'    inflect.singular  /s$/,  ''    inflect.singular  /es$/,  ''    inflect.irregular('papá',  'papás')    inflect.irregular('mamá',  'mamás')    inflect.irregular('sofá',  'sofás')end

Page 28: Advanced Internationalization with Rails

A minor problem

"oficina  de  turismo".pluralize  #  =>  "oficina  de  turismos"

"contacto  de  emergencia".pluralize#  =>  "contacto  de  emergencias"

Page 29: Advanced Internationalization with Rails

Lambdas to the rescuedef  pluralize(word,  locale  =  nil)    locale  ||=  I18n.locale    result  =  word.to_s.dup

   #  ...  elided  ...    inflections(locale).plurals.each  do  |(rule,  replacement)|        if  replacement.respond_to?(:call)            break  if  result.gsub!(rule,  &replacement)        else            break  if  result.gsub!(rule,  replacement)        end    end    resultend

Page 30: Advanced Internationalization with Rails

Lambdas to the rescue

ActiveSupport::Inflector.inflections(:es)  do  |inflect|    #  ...  elided  ...    inflect.plural(/^(\w+)\s(.+)$/,  lambda  {  |match|                                      head,  tail  =  match.split(/\s+/,  2)                                      "#{head.pluralize}  #{tail}"                                  })    #  ...  elided  ...    end

Page 31: Advanced Internationalization with Rails

Success!I18n.locale  =  :es

"oficina  de  turismo".pluralize  #  =>  "oficinas  de  turismo"

"contacto  de  emergencia".pluralize#  =>  "contactos  de  emergencia"

I18n.locale  =  :en

"emergency  contact".pluralize#  =>  "emergency  contacts"

Page 32: Advanced Internationalization with Rails

The inflector is used everywhere

Because of the wide-spread use of the inflector, we have to patch:ActiveSupport::ModelName#initialize

String#pluralize,  singularize

AS::Inflector#pluralize,  singularize,  tableize

ActiveRecord::Base.human_name,  undecorated_table_name

AR::Reflection::AssociationReflection#derive_class_name

ActionController::PolymorphicRoutes#build_named_route_call

Page 33: Advanced Internationalization with Rails

An interesting side-effect

ActiveRecord::Schema.define  do    create_table  "oficinas_de_turismo"  do  |t|        t.string  "nombre"        t.string  "calle"    endend

context  "En  español"  do    setup  {  I18n.default_locale  =  :es  }

   context  "OficinaDeTurismo"  do        should("use  the  right  table")  {              OficinaDeTurismo.table_name  }.equals  "oficinas_de_turismo"    endend