Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Internationalization & Localization

3158320fc892baab27674fa756788ae2?s=47 j3
August 07, 2012

Internationalization & Localization

As presented at Argentina RubyConf 2012. Screencast (with built-in, low quality mic) at http://dl.dropbox.com/u/69001/i18n%20argentina%20rubyconf.mov

3158320fc892baab27674fa756788ae2?s=128

j3

August 07, 2012
Tweet

Transcript

  1. Internationalization & Localization Stop making that face. 1 Jeff Casimir

    / @j3
  2. +

  3. +

  4. 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!
  5. A Dangerous Journey 5

  6. Terms 6 ▪i18n - Internationalization ▪l10n - Localization

  7. Why A Business Cares 7 ▪Localization provides a better UX

    ▪Better UX = More Customers ▪More Customers = More Profits*
  8. But what’s in it for me?

  9. Why Developers Care 9 Better Code

  10. Roadmap to Multi-Language Support 1. Internationalize 2. Localize 3. Determine

    Locale 10
  11. Step 1: Internationalize 11

  12. 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
  13. 13 Do you like MAGIC?

  14. Copyedit Commits

  15. 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
  16. Lookup Dictionary 16

  17. 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
  18. t and I18n.t 18 t 'article.deleted' t 'comment.status.approved' I18n.t 'comment.created'

  19. Structuring Keys 19 ▪Keep the keys short ▪Don’t just snake-case

    the text ▪Focus on core meaning ▪Screw reusability
  20. 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
  21. Draper 21

  22. 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
  23. 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
  24. Automatic Lookups with ActiveRecord

  25. 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
  26. 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

  27. Rails Guide’s Validation Keys 27

  28. Validation Message Lookup 28 en: activerecord: errors: models: article: attributes:

    title: blank: "The article needs a title!"
  29. 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
  30. 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
  31. Aut-o-matic! 31 a = Article.new(:body => "boo") # => #<Article

    id: nil, title: nil, body: "boo"...> a.save # => false a.errors.first.inspect # => "[:title, \"The article needs a title!\"]" a.errors.first.last # => "The article needs a title!"
  32. Attribute & Model Names 32 en: activerecord: attributes: article: title:

    "Title" comment: body: "Your Comment" label and other form helpers use them automatically
  33. en: activerecord: attributes: article: title: "Title" comment: body: "Your Comment"

    errors: models: article: attributes: title: blank: "The article needs a title!"
  34. The process of internationalization will improve your code, regardless of

    localization. 34
  35. 2. Localizing User Content 35

  36. 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."
  37. Challenges with User-Generated Content 37 ▪ Creating multiple translations ▪

    Managing the content import/export ▪ Keeping translations in sync
  38. CopyCopter 38

  39. Globalize3 39

  40. Locale - localeapp.com 40

  41. Step 3: Per-Request Locale 41

  42. A Quick Story about BDD 42

  43. github/jcasimir/locale_setter 43

  44. Locale-Selection Strategies 44 1. Geolocation 2. Browser Preferences 3. URL

    Parameters 4. Account Preferences 5. Default
  45. Geolocation 45

  46. 46 Browser Preferences

  47. HTTP_ACCEPT_LANGUAGE 47

  48. HTTP_ACCEPT_LANGUAGE 48 class ArticlesController < ApplicationController def show render :text

    => request.env['HTTP_ACCEPT_LANGUAGE'] end end
  49. en;q=0.8 , , Accept-Language Formatting 49 en-US es;q=0.2

  50. 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
  51. 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"]
  52. 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
  53. 53 Querying Available Locales I18n.available_locales # => [:en, :"en-us", :es,

    :"es-ar"]
  54. 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
  55. module  LocaleSetter    module  Matcher        def  self.match(requested)

               (requested  &  I18n.available_locales).first        end    end end
  56. URL Parameters 56

  57. Why Use URL Parameters? 57 ▪Easy to switch locales for

    development / debugging ▪Copied URLs will maintain state for sharing*
  58. 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
  59. 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]
  60. 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
  61. 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
  62. The Link Problem 62

  63. 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
  64. User Preferences 64

  65. Store in the DB 65 rails generate migration \ add_locale_to_users

    locale:string
  66. 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
  67. Composite Strategy 67

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

  69. 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
  70. github/jcasimir/locale_setter 70

  71. Bonus Round: White-Boxing & I18n 71

  72. Normal Application Layout 72 <head> <title>AssetSample</title> <%= stylesheet_link_tag "application", :media

    => "all" %> <%= javascript_include_tag "application" %> <%= csrf_meta_tags %> </head>
  73. Application Layout w/ I18n 73 <head> <title>AssetSample</title> <%= stylesheet_link_tag t("stylesheet"),

    :media => "all" %> <%= javascript_include_tag t("javascript") %> <%= csrf_meta_tags %> </head>
  74. Application Layout w/ I18n 74 client_1: stylesheet: "client1_stylesheet" javascript: "client1_javascript"

    client_2: stylesheet: "client2_stylesheet" javascript: "client2_javascript"
  75. !!! $$$ PROFIT $$$ !!! 75

  76. Recap 76

  77. Step 1: Internationalize 77

  78. Step 2: Localize Content 78

  79. Step 3: Determine Locale 79

  80. Hack I18n Beyond Translations 80

  81. Internationalization & Localization 81 Jeff Casimir / @j3

  82. Scraps 82

  83. Localize Functional Text 83

  84. 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
  85. Errors Partial 85 <%= if errors.any? %> <ul class='errors' >

    <% errors.each do |e| <li><%= e.last %></li> <% end %> </ul> <% end %>
  86. 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
  87. Interpolation 87 en: generic: deleted: "%{subject} has been deleted." error:

    "%{subject} failed to save." t 'generic.deleted', :subject => "Comment" WAT?!
  88. 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')
  89. 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