The document discusses internationalization (i18n) in Ruby on Rails applications. It covers the current state of Rails i18n including key-value lookup, ActiveRecord support, interpolation, pluralization, localized formats and views. It also discusses difficulties with the current approach and plugins that can help like Globalize2. The document then discusses building custom i18n backends and improving Rails' inflection rules to support multiple languages.
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. The current state of Rails i18n
Key-value lookup
ActiveRecord support
Interpolation
Weak pluralization
Localized formats
Localized views (index.es.html.erb)
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. 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
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. 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. Localized views
app/
views/
cities/
show.haml # used by default locale
show.es.haml # used by :es locale
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. 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
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. 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. 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. 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. 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. 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. 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. Improving inflections
Let’s make the Rails inflector multilingual!
This will be incredibly invasive, full of monkey-
patching, and quite dangerous.
28. A minor problem
"oficina de turismo".pluralize
# => "oficina de turismos"
"contacto de emergencia".pluralize
# => "contacto de emergencias"
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. 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. 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. 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. 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. Muchas gracias
a todos!
crnixon@gmail.com
http://crnixon.org
http://github.com/crnixon/traductor
http://delicious.com/crnixon/conferencia.rails+i18n