Advanced Internationalization with Rails

5,353 views
5,141 views

Published on

Published in: Technology
0 Comments
6 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
5,353
On SlideShare
0
From Embeds
0
Number of Embeds
44
Actions
Shares
0
Downloads
32
Comments
0
Likes
6
Embeds 0
No embeds

No notes for slide

Advanced Internationalization with Rails

  1. 1. Advanced Internationalization with Rails Clinton R. Nixon Viget Labs
  2. 2. 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.
  3. 3. The current state of Rails i18n Key-value lookup ActiveRecord support Interpolation Weak pluralization Localized formats Localized views (index.es.html.erb)
  4. 4. Key-value lookup en:    site_title:  The  Lost  Tourist    add_tourism_office:  Add  a  New  Tourism  Office    add_city:  Add  a  New  City es:    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)
  5. 5. 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
  6. 6. Interpolation es:    welcome_to:  "Bienvenido  a  {{city}}!" t(:welcome_to,  :city  =>  @city.name)
  7. 7. 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)
  8. 8. Localized formats es:    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
  9. 9. Localized views app/    views/        cities/            show.haml        #  used  by  default  locale            show.es.haml  #  used  by  :es  locale
  10. 10. Difficulties File-based key-value lookup is not user-editable. Model attributes are not translated. The Rails inflector is not multi-lingual.
  11. 11. Plugins to help Newsdesk Translate translatable_columns Globalize2
  12. 12. 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.subject end
  13. 13. Other Globalize2 features Locale 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
  14. 14. Translating models Globalize2 and translatable_models work well for this.
  15. 15. How to start building a backend The easiest way is to extend I18n::Backend::Simple. Key methods are: init_translations  (protected) store_translations translate localize available_locales
  16. 16. Example: DB-backed backend 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
  17. 17. Example: DB-backed backend 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
  18. 18. Storing the translations class  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    end end
  19. 19. Loading translations from DB def  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
  20. 20. Example: a bad idea module  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
  21. 21. 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.
  22. 22. Improving inflections Let’s make the Rails inflector multilingual! This will be incredibly invasive, full of monkey- patching, and quite dangerous.
  23. 23. Why? Generated forms: Formtastic, Typus %h2=  City.human_name.pluralize
  24. 24. Black magic 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
  25. 25. Black magic 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
  26. 26. Black magic 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
  27. 27. 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])$/,  'a1es'    inflect.plural  /í([sn])$/,  'i1es'    inflect.plural  /ó([sn])$/,  'o1es'    inflect.plural  /ú([sn])$/,  'u1es'    inflect.singular  /s$/,  ''    inflect.singular  /es$/,  ''    inflect.irregular('papá',  'papás')    inflect.irregular('mamá',  'mamás')    inflect.irregular('sofá',  'sofás') end
  28. 28. A minor problem "oficina  de  turismo".pluralize   #  =>  "oficina  de  turismos" "contacto  de  emergencia".pluralize #  =>  "contacto  de  emergencias"
  29. 29. Lambdas to the rescue def  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    result end
  30. 30. 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
  31. 31. 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"
  32. 32. 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
  33. 33. An interesting side-effect ActiveRecord::Schema.define  do    create_table  "oficinas_de_turismo"  do  |t|        t.string  "nombre"        t.string  "calle"    end end 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"    end end
  34. 34. Muchas gracias a todos! crnixon@gmail.com http://crnixon.org http://github.com/crnixon/traductor http://delicious.com/crnixon/conferencia.rails+i18n

×