Slide 1

Slide 1 text

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!

Slide 5

Slide 5 text

A Dangerous Journey 5

Slide 6

Slide 6 text

Terms 6 ■i18n - Internationalization ■l10n - Localization

Slide 7

Slide 7 text

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

Slide 26

Slide 26 text

Automatic Lookup Paths 26 activerecord.errors.models.[model_name].attributes.[attribute_name] activerecord.errors.models.[model_name] activerecord.errors.messages errors.attributes.[attribute_name] errors.messages

Slide 27

Slide 27 text

Rails Guide’s Validation Keys 27

Slide 28

Slide 28 text

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

Slide 33

Slide 33 text

en: activerecord: attributes: article: title: "Title" comment: body: "Your Comment" errors: models: article: attributes: title: blank: "The article needs a title!"

Slide 34

Slide 34 text

The process of internationalization will improve your code, regardless of localization. 34

Slide 35

Slide 35 text

2. Localizing User Content 35

Slide 36

Slide 36 text

36 Static Dictionary Was Easy 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."

Slide 37

Slide 37 text

Challenges with User-Generated Content 37 ■ Creating multiple translations ■ Managing the content import/export ■ Keeping translations in sync

Slide 38

Slide 38 text

CopyCopter 38

Slide 39

Slide 39 text

Globalize3 39

Slide 40

Slide 40 text

Locale - localeapp.com 40

Slide 41

Slide 41 text

Step 3: Per-Request Locale 41

Slide 42

Slide 42 text

A Quick Story about BDD 42

Slide 43

Slide 43 text

github/jcasimir/locale_setter 43

Slide 44

Slide 44 text

Locale-Selection Strategies 44 1. Geolocation 2. Browser Preferences 3. URL Parameters 4. Account Preferences 5. Default

Slide 45

Slide 45 text

Geolocation 45

Slide 46

Slide 46 text

46 Browser Preferences

Slide 47

Slide 47 text

HTTP_ACCEPT_LANGUAGE 47

Slide 48

Slide 48 text

HTTP_ACCEPT_LANGUAGE 48 class ArticlesController < ApplicationController def show render :text => request.env['HTTP_ACCEPT_LANGUAGE'] end end

Slide 49

Slide 49 text

en;q=0.8 , , Accept-Language Formatting 49 en-US es;q=0.2

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

51 Parsing Accept-Language # In an imaginary controller context... accepts = request.env["HTTP_ACCEPT_LANGUAGE"] # => "en,en-US;q=0.8,es;q=0.2" accepts.downcase.scan(/([\w-]{2,})/).map(&:first) # => ["en", "en-us", "es"]

Slide 52

Slide 52 text

module  LocaleSetter    module  HTTP        def  self.for(accept_language)            LocaleSetter::Matcher.match(AcceptLanguageParser.                                                      parse(accept_language))        end        module  AcceptLanguageParser            LOCALE_SEPARATOR  =  ','            WEIGHT_SEPARATOR  =  ';'            def  self.parse(accept_language)                locale_fragments  =  accept_language.split(LOCALE_SEPARATOR)                weighted_fragments  =  locale_fragments.                                map{|f|  f.split(WEIGHT_SEPARATOR)}                sorted_fragments  =  weighted_fragments.sort_by{|f|  -­‐f.last.to_f  }                sorted_fragments.map{|locale,  weight|  locale}            end        end    end end

Slide 53

Slide 53 text

53 Querying Available Locales I18n.available_locales # => [:en, :"en-us", :es, :"es-ar"]

Slide 54

Slide 54 text

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

Slide 59

Slide 59 text

Danger! 59 symbols = Symbol.all_symbols # => [...] I18n.locale = "garbage_from_a_user" # => "garbage_from_a_user" I18n.locale # => :garbage_from_a_user Symbol.all_symbols - symbols # => [:garbage_from_a_user]

Slide 60

Slide 60 text

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

Slide 67

Slide 67 text

Composite Strategy 67

Slide 68

Slide 68 text

Lookup Chain 68 1.URL parameter 2.User Preference 3.Browser 4.Default

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

github/jcasimir/locale_setter 70

Slide 71

Slide 71 text

Bonus Round: White-Boxing & I18n 71

Slide 72

Slide 72 text

Normal Application Layout 72 AssetSample <%= stylesheet_link_tag "application", :media => "all" %> <%= javascript_include_tag "application" %> <%= csrf_meta_tags %>

Slide 73

Slide 73 text

Application Layout w/ I18n 73 AssetSample <%= stylesheet_link_tag t("stylesheet"), :media => "all" %> <%= javascript_include_tag t("javascript") %> <%= csrf_meta_tags %>

Slide 74

Slide 74 text

Application Layout w/ I18n 74 client_1: stylesheet: "client1_stylesheet" javascript: "client1_javascript" client_2: stylesheet: "client2_stylesheet" javascript: "client2_javascript"

Slide 75

Slide 75 text

!!! $$$ PROFIT $$$ !!! 75

Slide 76

Slide 76 text

Recap 76

Slide 77

Slide 77 text

Step 1: Internationalize 77

Slide 78

Slide 78 text

Step 2: Localize Content 78

Slide 79

Slide 79 text

Step 3: Determine Locale 79

Slide 80

Slide 80 text

Hack I18n Beyond Translations 80

Slide 81

Slide 81 text

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