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

Internationalization & Localization

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

j3

August 07, 2012
Tweet

More Decks by j3

Other Decks in Technology

Transcript

  1. +

  2. +

  3. 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!
  4. Why A Business Cares 7 ▪Localization provides a better UX

    ▪Better UX = More Customers ▪More Customers = More Profits*
  5. 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
  6. 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
  7. 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
  8. Structuring Keys 19 ▪Keep the keys short ▪Don’t just snake-case

    the text ▪Focus on core meaning ▪Screw reusability
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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!"
  16. Attribute & Model Names 32 en: activerecord: attributes: article: title:

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

    errors: models: article: attributes: title: blank: "The article needs a title!"
  18. 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."
  19. Challenges with User-Generated Content 37 ▪ Creating multiple translations ▪

    Managing the content import/export ▪ Keeping translations in sync
  20. 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
  21. 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"]
  22. 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
  23. 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
  24. module  LocaleSetter    module  Matcher        def  self.match(requested)

               (requested  &  I18n.available_locales).first        end    end end
  25. Why Use URL Parameters? 57 ▪Easy to switch locales for

    development / debugging ▪Copied URLs will maintain state for sharing*
  26. 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
  27. 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]
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. Normal Application Layout 72 <head> <title>AssetSample</title> <%= stylesheet_link_tag "application", :media

    => "all" %> <%= javascript_include_tag "application" %> <%= csrf_meta_tags %> </head>
  34. 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>
  35. Application Layout w/ I18n 74 client_1: stylesheet: "client1_stylesheet" javascript: "client1_javascript"

    client_2: stylesheet: "client2_stylesheet" javascript: "client2_javascript"
  36. 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
  37. Errors Partial 85 <%= if errors.any? %> <ul class='errors' >

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

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