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

Stop Fighting Ruby I18n: Message Format Is All ...

Stop Fighting Ruby I18n: Message Format Is All You Need

Originally presented at RubyConf 2026 in Las Vegas.

Avatar for Chris Fung

Chris Fung

July 14, 2026

More Decks by Chris Fung

Other Decks in Programming

Transcript

  1. • 👋 Chris Fung • 🧑💻 Sta ff Engineer at

    Gusto • 🐘 @[email protected] • 🦋 @aergonaut.com Hello, RubyConf !
  2. “[T]he Simple backend shipped with Active Support only does the

    ‘simplest thing that could possibly work’.” – Rails Guides
  3. Gem Overview • Drop-in replacement for the i18n gem’s built-in

    Simple backend • Allows using the ICU Message Format in Rails • Ecosystem compatibility as a fi rst-class goal: • Use your existing YAML translations via Chain backend • Use your existing date and number formats • Use translations and other data from gems such as rails-i18n
  4. Definitions • ICU — International Components for Unicode; originally a

    C/C++ library • Unicode — non-pro fi t org which maintains the emojis Unicode Standard • Message Format — ICU standard syntax for formatting user-visible text
  5. Java JS / TS Python .NET ICU4J Built-in support via

    co m .ib m .icu. text. M essageFor mat FormatJS intl- messageformat widely used in React Babel Support vai babel. m essages PyICU Native bindings ICU4N ICU APIs for .NET Cross-platform support
  6. String concatenation <div class="metadata"> <span><%= @ post.author % ></span> <span>Published

    <%= @ post.created_at % ></span> <span><%= @ post.read_t im e % ></span> </div>
  7. Vanilla I18n <div class="metadata"> <span><%= @ post.author % ></span> <span>

    <%= I18n.t(:published_at, date: @ post.created_at % > </span> <span><%= @ post.read_t im e % ></span> </div>
  8. Vanilla I18n <div class="metadata"> <span><%= @ post.author % ></span> <span>

    <%= I18n.t(:published_at, date: l( @ post.created_at)) % > </span> <span><%= @ post.read_t im e % ></span> </div>
  9. Vanilla I18n <div class="metadata"> <span><%= @ post.author % ></span> <span>

    <%= I18n.t(:published_at, date: l( @ post.created_at)) % > </span> <span><%= @ post.read_t im e % ></span> </div>
  10. Message Format <div class="metadata"> <span><%= @ post.author % ></span> <span>

    <%= I18n.t(:published_at, createdAt: @ post.created_at % > </span> <span><%= @ post.read_t im e % ></span> </div>
  11. International date formats Format English Japanese Full Thursday, April 23,

    2026 2026೥4݄23೔ʢՐ༵೔ʣ Long April 23, 2026 2026೥4݄23೔ Medium Apr 23, 2026 2026/04/23 Short 4/23/2026 2026/04/23
  12. Respecting user’s pronouns Monster A has invited you to his

    party… Monster B has invited you to her party… Monster C has invited you to their party…
  13. Concatenation with case <%= @ user.name % > has invited

    you to <%= case @ user.pronouns w hen Pronouns::HE_H I M % > his < % w hen Pronouns::SHE_HER % > her < % w hen Pronouns:: T HEY _T H E M % > their < % end % > party
  14. Vanilla I18n en: invited_his: " % {name} has invited you

    to his party" invited_her: " % {name} has invited you to her party" invited_their: " % {name} has invited you to their party"
  15. Vanilla I18n def invited_by(user) case user.pronouns w hen Pronouns::HE_H I

    M I18n.t('invited_his', name: user.name) w hen Pronouns::SHE_HER I18n.t('invited_her', name: user.name) w hen Pronouns:: T HEY _ T H EM I18n.t('invited_their', name: user.name) end end
  16. Vanilla I18n • This assumes there are always only 3

    pronouns • Locales that distinguish fewer pronouns will have duplicates for some • A locale that needs a new pronoun forces all the others to add it too • The helper function duplicates the pronouns’ semantics • 3 when branches re fl ect the 3 pronouns • A new pronoun requires a code change
  17. Message Format en: invited: | {pronouns, select, he_h im {{name}

    has invited you to his party} she_her {{name} has invited you to her party} other {{name} has invited you to their party}} YAML heredoc like case case branches “other” as fallback
  18. Message Format • No branching at the call site •

    One message contains all variants • Translators can change the {select} as needed • e.g. collapse everything to only other for a language without gendered pronouns
  19. Add -s Subtotal (<%= @ cart.items.count % > item<%= @

    cart.items.count > 1 ? "s" : nil % >)
  20. Vanilla I18n en: cart_subtotal: one: "Subtotal ( % {count} ite

    m )" other: "Subtotal ( % {count} items)"
  21. CLDR Plural Rules • English • N=1 — 1 item

    is in your cart • N=2 — 2 items are in your cart • Japanese • N=1 — Χʔτʹ1఺ೖ͍ͬͯ·͢ • N=2 — Χʔτʹ2఺ೖ͍ͬͯ·͢
  22. CLDR Plural Rules English French Japanese Irish one 1 other

    0, 2…n one 0, 1 other 2…n other 0…n one 1 two 2 few 3…6 many 7…10 other
  23. Vanilla I18n ga: cart_subtotal: one: "Fo-io m l án (

    % {count} m hír)" tw o: "Fo-io m l án ( % {count} m hír)" fe w : "Fo-io m l án ( % {count} m hír)" m any: "Fo-io m l án ( % {count} mí r)" other: "Fo-io m l án ( % {count} m í reanna)"
  24. Vanilla I18n ga: cart_subtotal: one: "Fo-io m l án (

    % {count} m hír)" tw o: "Fo-io m l án ( % {count} m hír)" fe w : "Fo-io m l án ( % {count} m hír)" m any: "Fo-io m l án ( % {count} mí r)" other: "Fo-io m l án ( % {count} m í reanna)"
  25. Vanilla I18n • Vanilla’s default pluralization support encodes grammatical information

    into the YAML structure • This means di ff erent languages may have di ff erent YAML structures • “Missing” or “unused” keys depend on the locale’s plural categories • Translation tools (i18n_tasks) also have to change the YAML structure in order to add/remove plural categories
  26. Message Format en: cart_subtotal: | {count, plural, one {Subtotal ({#}

    ite m )} other {Subtotal ({#} items)}} plural formatter plural categories # sign placeholder
  27. Message Format ga: cart_subtotal: | {count, plural, one {Fo-io m

    l án ({#} m hír)} t w o {Fo-io m l án ({#} m hír)} fe w {Fo-io m l án ({#} m hír)} m any {Fo-io m l án ({#} m í r)} other {Fo-io m l án ({#} m í reanna)}}
  28. Message Format • Plural categories are embedded within the message,

    via {plural} formatter • 1-to-1 correspondence of YAML keys between locales is restored • Missing and unused key detection is much easier • Machine translation tools can more easily add/remove plural categories
  29. I18n means adaptation • I18n is about making sure your

    app can work with multiple languages • That means fi nding places in your code that implicitly assume a particular language is being used • This is generally called “hard-coding” and usually is thought of in terms of text embedded in views
  30. Hard-coding is everywhere • Hard-coding is more than just English

    text in your ERB! • Hard-coding is anything that bakes linguistic assumptions into your app: • Hard-coded formats: l(date, format: : m ediu m ) • Hard-coded variation: branching calls to I18n.t • Hard-coded fi le structure: plural keys in YAML
  31. Variation belongs in messages • Your code is the same

    no matter the locale. What changes is the messages. • Default I18n still forces your code to account for some linguistic variation • Message Format’s richer syntax allows you to move variation into the messages themselves, keeping it out of your code