Internationalization &
Localization
Stop making that face.
1
Jeff Casimir / @j3
Slide 2
Slide 2 text
+
Slide 3
Slide 3 text
+
Slide 4
Slide 4 text
Hooray, RubyConf Argentina!
You going to present in English?
People might not understand
my jokes in English.
In your Spanish, they might not
understand anything!
anything!
Why A Business Cares 7
■Localization provides a better UX
■Better UX = More Customers
■More Customers = More Profits*
Slide 8
Slide 8 text
But what’s
in it for me?
Slide 9
Slide 9 text
Why Developers Care 9
Better Code
Slide 10
Slide 10 text
Roadmap to
Multi-Language Support
1. Internationalize
2. Localize
3. Determine Locale
10
Slide 11
Slide 11 text
Step 1: Internationalize
11
Slide 12
Slide 12 text
12
An Average Controller Action
def create
@article = Article.new(params[:article])
if @article.save
flash[:notice] = "Article was created."
redirect_to articles_path
else
flash[:notice] = "The article could not be saved!"
render :new
end
end
Slide 13
Slide 13 text
13
Do you like MAGIC?
Slide 14
Slide 14 text
Copyedit
Commits
Slide 15
Slide 15 text
15
Magic Data in a Controller
def create
@article = Article.new(params[:article])
if @article.save
flash[:notice] = "Article was created."
redirect_to articles_path
else
flash[:notice] = "The article could not be saved!"
render :new
end
end
Slide 16
Slide 16 text
Lookup Dictionary
16
Slide 17
Slide 17 text
17
en:
article:
deleted: "Article has been deleted."
error: "Article failed to save."
created: "Article created successfully."
new: "New Article"
update: "Save Changes"
create: "Save Article"
comment:
created: "Comment posted, you're internet famous."
spam: "Burn your own face off, spammer."
create: "Post Comment"
status:
moderation: "Comment is pending moderation."
approved: "Comment approved."
A YAML Dictionary
Slide 18
Slide 18 text
t and I18n.t 18
t 'article.deleted'
t 'comment.status.approved'
I18n.t 'comment.created'
Slide 19
Slide 19 text
Structuring Keys 19
■Keep the keys short
■Don’t just snake-case the text
■Focus on core meaning
■Screw reusability
Slide 20
Slide 20 text
20
en:
article:
deleted: "Article has been deleted."
error: "Article failed to save."
created: "Article created successfully."
new: "New Article"
update: "Save Changes"
create: "Save Article"
comment:
created: "Comment posted, you're internet famous."
spam: "Burn your own face off, spammer."
create: "Post Comment"
status:
moderation: "Comment is pending moderation."
approved: "Comment approved."
Simple Keys, Simple Strings
Slide 21
Slide 21 text
Draper
21
Slide 22
Slide 22 text
22
Magic Data in a Controller
def create
@article = Article.new(params[:article])
if @article.save
flash[:notice] = "Article was created."
redirect_to articles_path
else
flash[:notice] = "The article could not be saved!"
render :new
end
end
Slide 23
Slide 23 text
23
Controller Without Magic
def create
@article = Article.new(params[:article])
if @article.save
flash[:notice] = t 'article.save.success'
redirect_to articles_path
else
flash[:notice] = t 'article.save.failure'
render :new
end
end
Slide 24
Slide 24 text
Automatic
Lookups with
ActiveRecord
Slide 25
Slide 25 text
25
Magic Data in a Model
class Article < ActiveRecord::Base
validates_length_of :body, :min => 100
:message => " must be at least 100 characters bro."
def self.recent
order('created_at DESC').limit(10)
end
def attribution
"written by #{author.full_name}"
end
end
Validation Message Lookup 28
en:
activerecord:
errors:
models:
article:
attributes:
title:
blank: "The article needs a title!"
Slide 29
Slide 29 text
29
Validation with a :message
class Article < ActiveRecord::Base
validates_presence_of :title,
:message => "The article needs a title."
def self.recent
order('created_at DESC').limit(10)
end
def attribution
"written by #{author.full_name}"
end
end
Slide 30
Slide 30 text
30
Validation Using Auto-Lookup
class Article < ActiveRecord::Base
validates_presence_of :title
def self.recent
order('created_at DESC').limit(10)
end
def attribution
"written by #{author.full_name}"
end
end
Slide 31
Slide 31 text
Aut-o-matic! 31
a = Article.new(:body => "boo")
# => #
a.save
# => false
a.errors.first.inspect
# => "[:title, \"The article needs a title!\"]"
a.errors.first.last
# => "The article needs a title!"
Slide 32
Slide 32 text
Attribute & Model Names 32
en:
activerecord:
attributes:
article:
title: "Title"
comment:
body: "Your Comment"
label and other form helpers use them automatically
Accept-Language Formatting 50
“US English for Full Experience”
“Generic English at 80% Quality”
“Generic Spanish at 20% Quality”
en;q=0.8
es;q=0.2
en-US
Matching Available Locales 54
■ Find the user’s accepted locales
■ Symbolize them
■ Compare with the supported locales
■ Find the first match in order of their preference
Slide 55
Slide 55 text
module
LocaleSetter
module
Matcher
def
self.match(requested)
(requested
&
I18n.available_locales).first
end
end
end
Slide 56
Slide 56 text
URL Parameters 56
Slide 57
Slide 57 text
Why Use URL Parameters? 57
■Easy to switch locales for
development / debugging
■Copied URLs will maintain
state for sharing*
Slide 58
Slide 58 text
Pulling it out of the URL 58
class ApplicationController < ActionController::Base
before_filter :set_locale
private
def set_locale
I18n.locale = params[:locale]
end
end
module
LocaleSetter
include
LocaleSetter::Rails
#...
def
from_params
if
respond_to?(:params)
&&
params[:locale]
LocaleSetter::Param.for(params[:locale])
end
end
end
module
LocaleSetter
module
Param
def
self.for(param)
LocaleSetter::Matcher.match([param])
end
end
end
Slide 61
Slide 61 text
module
LocaleSetter
module
Matcher
def
self.match(requested,
against
=
available)
matched
=
(sanitize(requested)
&
against).first
matched.to_sym
if
matched
end
def
self.available
I18n.available_locales.map(&:to_s)
end
#
sanitize
methods
are
just
for
case
&
whitespace
#
...
end
end
Slide 62
Slide 62 text
The Link Problem 62
Slide 63
Slide 63 text
Modifying default_url_options 63
module
LocaleSetter
module
Rails
def
default_url_options(options
=
{})
if
i18n.locale
==
i18n.default_locale
options
else
{:locale
=>
i18n.locale}.merge(options)
end
end
#
...
end
end
Slide 64
Slide 64 text
User Preferences
64
Slide 65
Slide 65 text
Store in the DB 65
rails generate migration \
add_locale_to_users locale:string
Slide 66
Slide 66 text
module
LocaleSetter
module
User
def
self.for(user)
if
user
&&
user.respond_to?(:locale)
&&
user.locale
&&
!user.locale.empty?
LocaleSetter::Matcher.match(user.locale)
end
end
end
end
module
LocaleSetter
#
...
def
from_user
if
respond_to?(:current_user)
&&
current_user
LocaleSetter::User.for(current_user)
end
end
end
module
LocaleSetter
include
LocaleSetter::Rails
def
self.included(controller)
if
controller.respond_to?(:before_filter)
controller.before_filter
:set_locale
end
end
def
set_locale
i18n.locale
=
from_params
||
from_user
||
from_http
||
i18n.default_locale
end
def
from_user
if
respond_to?(:current_user)
&&
current_user
LocaleSetter::User.for(current_user)
end
end
Internationalization &
Localization
81
Jeff Casimir / @j3
Slide 82
Slide 82 text
Scraps 82
Slide 83
Slide 83 text
Localize Functional Text
83
Slide 84
Slide 84 text
In the View Layer 84
<%= errors_for(@article) %>
module ApplicationHelper
def errors_for(subject)
if subject.errors
render(:partial => 'common/errors',
:locals => {:errors => subject.errors})
end
end
end
Slide 85
Slide 85 text
Errors Partial 85
<%= if errors.any? %>
<% errors.each do |e|
<%= e.last %>
<% end %>
<% end %>
Slide 86
Slide 86 text
86
No Magic
def create
@article = Article.new(params[:article])
if @article.save
flash[:notice] = t 'article.save.success'
redirect_to articles_path
else
render :new
end
end
Slide 87
Slide 87 text
Interpolation 87
en:
generic:
deleted: "%{subject} has been deleted."
error: "%{subject} failed to save."
t 'generic.deleted', :subject => "Comment"
WAT?!
Slide 88
Slide 88 text
Interpolation 88
en:
models:
comment: "Comment"
article: "Article"
generic:
deleted: "%{subject} has been deleted."
error: "%{subject} failed to save."
t 'generic.deleted', :subject => t('models.comment')
Slide 89
Slide 89 text
class ActiveRecord::Base
def self.translates(*args)
args.each do |method|
define_method method do
target = "#{method}_#{I18n.locale}"
respond_to?(target) ? send(target) : attribute(method)
end
end
end
end
class Article < ActiveRecord::Base
translates :title, :body
end