Advanced Internationalization with Rails Clinton R. Nixon Viget Labs
May 12, 2015
AdvancedInternationalization with Rails
Clinton R. NixonViget Labs
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.
The current state of Rails i18n
Key-value lookup
ActiveRecord support
Interpolation
Weak pluralization
Localized formats
Localized views (index.es.html.erb)
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)
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
Interpolation
es: welcome_to: "Bienvenido a {{city}}!"
t(:welcome_to, :city => @city.name)
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)
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
Localized views
app/ views/ cities/ show.haml # used by default locale show.es.haml # used by :es locale
Difficulties
File-based key-value lookup is not user-editable.
Model attributes are not translated.
The Rails inflector is not multi-lingual.
Plugins to help
Newsdesk Translate
translatable_columns
Globalize2
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
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
Translating models
Globalize2 and translatable_modelswork well for this.
How to start building a backendThe easiest way is to extend I18n::Backend::Simple.
Key methods are:
init_translations (protected)store_translationstranslatelocalizeavailable_locales
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
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
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
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
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
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.
Improving inflections
Let’s make the Rails inflector multilingual!
This will be incredibly invasive, full of monkey-patching, and quite dangerous.
Why?
Generated forms: Formtastic, Typus%h2= City.human_name.pluralize
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
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
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
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
A minor problem
"oficina de turismo".pluralize # => "oficina de turismos"
"contacto de emergencia".pluralize# => "contacto de emergencias"
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
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
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"
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
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
Muchas graciasa todos!
[email protected]://crnixon.org
http://github.com/crnixon/traductorhttp://delicious.com/crnixon/conferencia.rails+i18n