Internationalization on Rails: Unpacking the Rails I18n Toolkit


Summarized using AI

Internationalization on Rails: Unpacking the Rails I18n Toolkit

Chris Fung • July 10, 2025 • Philadelphia, PA • Talk

Introduction

This talk, delivered by Chris Fung at RailsConf 2025, explores the topic of internationalization (I18n) and localization (L10n) in Ruby on Rails applications. The session demystifies these concepts, highlights their importance beyond large enterprises, and provides practical tools and techniques for implementing multilingual and localized user experiences in Rails apps.

Key Points

  • Definitions and Rationale
    • Localization (L10n): The process of adapting content for specific locales, combining language, region, and script.
    • Internationalization (I18n): Designing software so it can be easily localized.
    • English alone does not suffice—even in the US, a significant portion of residents speak languages other than English.
  • First Steps in Localization
    • Externalizing UI Text: Replace hard-coded strings in views and controllers with translation keys, enabling dynamic text rendering.
    • Using Rails Translate Helper: Use translate (or t) to fetch text for the current locale from YAML files in config/locales.
  • Selecting and Managing Locales
    • Identify commonly spoken languages in your target market using sources like Ethnologue or US Census data.
    • Convert languages into standardized locale codes, preferably following the IETF tag format (used by Rails).
  • Translation Strategies
    • Hire professional language service providers (LSPs) for high-quality translations or use machine translation via APIs for a DIY approach.
    • The i18n-tasks gem assists in managing and automating translation workflows, including integration with machine translation providers.
  • Advanced Localization Topics
    • Dates, Numbers, and Data Formatting: Different languages and regions use distinct formats. Use Rails' localize helper and supply locale-specific formats in YAML files.
    • Leveraging Unicode CLDR Data: Tap into the Unicode Consortium’s Common Locale Data Repository (CLDR) to get culturally-correct formats for dates, numbers, and more.
    • The twitter-cldr gem allows programmatic access to CLDR data to generate correct format strings for different locales.
    • Handling Plurals: Different languages have complex pluralization rules. Externalize plural logic and use pluralization keys (one, other, etc.) in YAML files.
    • The rails-i18n gem includes pluralization rules for many languages, directly usable by Rails.
    • Proper List Formatting: Languages differ in how they join list items (e.g., different punctuation or connector words). Use Rails’ to_sentence method for locale-aware lists, leveraging data from rails-i18n.
  • Practical Examples
    • Common Rails code snippets demonstrated key concepts (e.g., translating flash messages, formatting dates, pluralizing comments, listing tags).
    • Showed how to set up and configure supporting gems for translation and locale-specific formatting.

Conclusion and Takeaways

  • Internationalization and localization are crucial for reaching broader and more diverse user bases, even for small teams or startups.
  • Rails includes robust built-in support for I18n and L10n, and the ecosystem provides powerful gems (i18n-tasks, twitter-cldr, rails-i18n).
  • With these tools, developers do not need extensive linguistic or localization expertise—Rails makes it practical and accessible to build applications that feel natural to users in any language or region.
  • The speaker encourages attendees to leverage these tools and practices to make their applications inclusive and accessible, offering further resources and being available for questions.

Internationalization on Rails: Unpacking the Rails I18n Toolkit
Chris Fung • Philadelphia, PA • Talk

Date: July 10, 2025
Published: July 23, 2025
Announced: unknown

Building an app that's "just in English" might feel sufficient, but what if your next user prefers Spanish, German, or Chinese? Localization and internationalization are often an afterthought for many developers, if they are thought of at all. But it’s not only big, global companies that can benefit from them. They’re essential for startups and solo developers, too. In this talk, I’ll demystify internationalization and show you how Rails makes it easy to bake internationalization into your app from the start. You’ll learn how to use tools built into Rails and from gems in the wider ecosystem to easily support multilingual users, and make your app available to more people, wherever they are!

RailsConf 2025

00:00:17.199 Yeah. Hello everybody. Um, I just wanted
00:00:19.439 to start off by saying thank you to the
00:00:21.279 Rails Comp program committee for giving
00:00:23.119 me the opportunity here to give this
00:00:25.199 give this talk on this topic. Um it's
00:00:27.359 one that I find really interesting and
00:00:29.439 uh really fun to learn about. Um so
00:00:31.760 without further ado, this is unpacking
00:00:34.160 the Rails ITN toolkit.
00:00:37.520 Um so yeah, so first up, my name is
00:00:39.120 Chris Fun as uh as I was so introduced.
00:00:41.520 Um I've been working with Ruby and Rails
00:00:43.280 for over 10 years and currently I am a
00:00:45.840 staff engineer at a startup called
00:00:47.600 Binty.
00:00:49.280 And just really quick, just to give a
00:00:50.719 rundown of what Binty is, we are a
00:00:52.719 missiondriven SAS company that believes
00:00:54.800 every child deserves the care and
00:00:56.559 support of a loving family. Over 550
00:00:59.680 agencies use Binty in 36 states, serving
00:01:03.359 46% of children in care nationwide. Um,
00:01:06.640 over 85,000 families have used Benty to
00:01:09.040 become foster or adoptive parents. And
00:01:11.760 over 12,000 social workers use Binty and
00:01:14.479 benefit from up to 40% time savings
00:01:16.479 every day.
00:01:19.520 So this talk is about localization. But
00:01:22.080 um to start off with what is
00:01:23.680 localization and uh why should you care
00:01:26.000 about it?
00:01:28.720 Well, localization means to adapt
00:01:30.960 content from one local into another. A
00:01:33.600 classic example of this would be say you
00:01:35.600 have a Rails app like an online store or
00:01:38.400 a CMS where all of the text is written
00:01:41.280 in English and the process of adapting
00:01:43.840 that text into French or German or
00:01:46.880 Japanese for example would be called
00:01:48.799 localization.
00:01:50.320 Um you might also see localization
00:01:52.640 written as L10N. This is called a numer
00:01:56.720 where the 10 stands for the 10 letters
00:01:58.960 between the L and N of localization.
00:02:03.200 And so in that previous definition, I
00:02:04.960 specifically said localization is not
00:02:07.040 about adapting between languages, but
00:02:09.360 between local. And a local is a
00:02:12.239 combination of a language and a region.
00:02:14.480 Um, and potentially also a script.
00:02:17.040 They're often represented as codes such
00:02:19.440 as en fr--ca
00:02:22.800 or z-honty-h.
00:02:26.879 And the important bit here is that a
00:02:28.800 local doesn't represent just a language
00:02:31.360 because languages can be spoken by
00:02:33.440 different people all around the world in
00:02:35.920 different situations, different regions,
00:02:38.160 like all over the place. And these
00:02:40.080 regional variations, they affect how the
00:02:42.160 text and the data in your applications
00:02:44.000 need to be represented.
00:02:47.440 And so the last term I want to define
00:02:48.959 before we really get started is
00:02:50.480 internationalization, also called IEN.
00:02:54.720 And so if localization is the process of
00:02:57.200 adapting content from one local into
00:02:59.440 another, then internationalization is
00:03:01.920 the process of designing software so it
00:03:03.920 can be easily localized. There are a lot
00:03:06.959 of little pitfalls and blind spots that
00:03:09.360 could trip you up when it comes to
00:03:10.720 internationalization.
00:03:12.239 But luckily um Rails provides us with a
00:03:14.640 very good set of tools built right into
00:03:16.640 the framework that are very easy to use
00:03:18.959 and mostly just stay out of your way.
00:03:23.760 So, that's all well and good, right? But
00:03:26.159 why should you care? Uh, don't most
00:03:29.120 people speak English, right? Isn't
00:03:31.040 English like good enough to get to most
00:03:32.799 people? What benefits do you actually
00:03:34.879 gain from localization?
00:03:38.640 Well, first off, give me a show of
00:03:40.879 hands. Who here speaks English as a
00:03:43.120 second language?
00:03:45.440 Okay. Okay.
00:03:48.159 How about who knows someone who speaks
00:03:50.159 English as a second language?
00:03:52.319 Yeah, right. That's pretty much
00:03:53.360 everybody, right?
00:03:55.360 So, worldwide there are about 1.5
00:03:58.000 billion English speakers, but the total
00:04:00.000 world population is actually closer to 8
00:04:02.239 billion. And that means there's 6.5
00:04:05.040 billion people out there for whom
00:04:07.120 English is not their first language.
00:04:10.560 And even in the US, according to the US
00:04:12.959 census, there are 21.7% of residents who
00:04:16.799 speak a language other than English at
00:04:18.639 home.
00:04:20.959 And 8.2% of US residents say they speak
00:04:24.240 English less than very well.
00:04:27.600 And so that's all to say uh no, English
00:04:30.720 is not enough. You might default to
00:04:32.720 English and it's a it's a pretty good
00:04:34.639 place to start, but um even in the US,
00:04:37.280 English is not enough if you want to
00:04:38.800 reach the most people, especially people
00:04:41.280 who may have been underserved or
00:04:43.120 excluded from technology due to their
00:04:45.199 language.
00:04:47.840 Okay, great. So, let's say I've
00:04:49.600 convinced you, you've decided you want
00:04:51.600 to localize. Where do you start?
00:04:54.560 And luckily, again, Rails makes this
00:04:56.320 very easy. With just a small toolkit of
00:04:58.720 built-in utilities and a few gems from
00:05:00.800 the community, Rails lets us easily go
00:05:02.960 from a monolingual app to one that can
00:05:05.280 support however many languages we want
00:05:07.520 without needing to become an expert in
00:05:09.440 software localization, languages, or
00:05:11.680 linguistics. So, in this talk, we're
00:05:13.840 going to look at how you can use these
00:05:15.120 tools to translate your UI text, adapt
00:05:18.160 your UI to different data formatting
00:05:20.080 patterns, um, and even support different
00:05:22.160 rules for constructing plurals and
00:05:24.000 lists. And all at the end, we'll have an
00:05:26.639 app that feels just as natural in any
00:05:29.600 language as it does in English.
00:05:32.800 Okay, part one, translate your text. So,
00:05:35.680 the first step to translating your text
00:05:37.919 is actually to externalize your text.
00:05:40.400 And there's a lot more to externalizing
00:05:42.000 than what I'm going to be able to cover
00:05:43.280 here. Uh but the basic idea is you need
00:05:46.080 to look for hard-coded text anywhere in
00:05:48.560 your application that it might be shown
00:05:50.560 to a user.
00:05:52.639 And so let's take for example this
00:05:54.240 excerpt from a hypothetical view. It's
00:05:56.720 got a H1 tag says create post and then
00:05:59.520 there's a paragraph tag that says write
00:06:01.360 your post below. And I hope it's pretty
00:06:03.440 clear that since the text here is in
00:06:05.759 English, uh, the view can only ever
00:06:07.919 render in English because that's just
00:06:10.160 how it's written. If we wanted to
00:06:11.759 display the text in another in another
00:06:13.600 language, we we can't.
00:06:19.600 And so this brings us to the first of
00:06:21.440 Rails IN utilities, a little helper
00:06:24.400 called translate. This is available in
00:06:26.560 controllers and views and has a pretty
00:06:28.240 simple signature. First, it takes a key.
00:06:31.280 So this is an identifier like greeting
00:06:33.600 or errors not found. Um and then it
00:06:37.039 takes a bunch of different keyword
00:06:38.160 arguments and the these are used for a
00:06:39.680 ve a lot of different purposes but
00:06:41.440 mostly for variables as we'll see later
00:06:43.600 on.
00:06:45.680 So translate's purpose is to let us
00:06:47.520 dynamically render text in a different
00:06:49.280 language at runtime. It uses the first
00:06:51.759 argument the translation key to look up
00:06:54.400 what string to return based on the
00:06:56.160 current local. And if we change the
00:06:58.319 local for example from English to French
00:07:01.280 then we also change where translate will
00:07:03.520 look up the string. So instead of hello
00:07:05.840 world we get bonjour lemon.
00:07:10.080 So if we go back to the example from
00:07:11.520 before
00:07:13.840 we can replace the hardcoded text with
00:07:16.000 calls to translate. And here I'm just
00:07:18.080 using t which is an alias for brevity.
00:07:20.880 Um now the template no longer contains
00:07:22.720 the hard-coded text. Instead, it'll be
00:07:25.039 fetched at runtime from the current
00:07:26.560 local.
00:07:29.360 But of course, we do still also have to
00:07:31.360 provide the text for translated display.
00:07:33.840 And Rails does this with YAML files
00:07:35.919 which are conventionally placed in the
00:07:37.840 config/locals
00:07:39.360 directory. Um, since we only have
00:07:41.360 English text for now, we'll still place
00:07:43.120 these two strings we extracted from the
00:07:44.800 template into en.yaml. Encal code for
00:07:48.880 English.
00:07:52.479 And externalization doesn't just apply
00:07:54.319 to views, however, it applies anywhere a
00:07:57.120 string that might be shown to the user
00:07:59.120 originates. So another big example is in
00:08:02.080 controllers like flash messages for
00:08:04.080 example.
00:08:06.960 And since it's ex since the translate
00:08:09.360 method is also available here, we can
00:08:11.039 just replace the the text with translate
00:08:13.440 just like we did in the view.
00:08:18.000 Okay. So once we've externalized all the
00:08:19.840 text, the next step is we need to choose
00:08:21.759 target loces. And you may already know
00:08:25.039 what loc you want to target. But if you
00:08:27.599 don't, a good place to start is by
00:08:29.599 asking yourself who do you who do you
00:08:31.680 want to reach with your application.
00:08:34.640 Um if you're a global product, uh one
00:08:36.959 way to start is by looking at what
00:08:38.560 regions you want to target. So you can
00:08:40.560 use a site like this one. This is ethnol
00:08:42.399 log. Um to research what languages are
00:08:45.040 most spoken in those regions. And this
00:08:47.279 could give you a good idea of which
00:08:48.880 languages would help make your app
00:08:50.320 available to the most people.
00:08:53.519 If your main market is in the US, on the
00:08:55.600 other hand, like it is for us at Binty,
00:08:57.600 um you could look at the US census data
00:08:59.360 instead. If you go to data.sensus.gov,
00:09:03.040 you can browse all sorts of different
00:09:04.560 tables with data about education,
00:09:07.200 employment, health, and language. Um,
00:09:10.399 specifically here I have highlighted the
00:09:12.399 language spoken at home data set which
00:09:14.800 can give you a pretty good indication of
00:09:16.560 what languages people speak besides
00:09:18.800 English.
00:09:23.200 Okay. So once we know which languages we
00:09:25.360 want to target, we have to convert those
00:09:27.120 languages into local codes. And like I
00:09:29.600 mentioned in the beginning, a local code
00:09:31.839 is just a standardized identifier to
00:09:33.839 refer to the language. Um, the easiest
00:09:36.080 way to do this is actually just look
00:09:37.680 look up the language on Wikipedia.
00:09:39.519 Normally, you'll see the local code
00:09:41.040 there in the sidebar. Um, but you could
00:09:42.959 also use a tool like the one I have
00:09:44.640 linked here to look up the language um
00:09:46.640 and its its correct tag. Um, there are
00:09:50.320 also a few different formats for your
00:09:52.800 your local codes that you can follow.
00:09:55.120 Uh, but if you're unsure if like it
00:09:57.040 doesn't really matter to you, I
00:09:58.480 recommend sticking with the IEF language
00:10:00.959 tag format, which uses dashes here. Um,
00:10:04.160 this is the one that Rails uses
00:10:05.519 internally. um and it's the one that's
00:10:07.760 used by all the gems that I'm going to
00:10:09.120 mention today.
00:10:12.160 Okay. So, once we know the local we want
00:10:14.240 to target, now we can translate the
00:10:15.920 text.
00:10:17.839 And here again, there are two options.
00:10:20.160 On the one hand, you could hire a
00:10:22.560 language services provider, also called
00:10:24.880 an LSP. These are big organizations that
00:10:28.079 provide services like translation, proof
00:10:30.480 reading, QA, etc. Um, they also may be
00:10:34.160 able to provide translators who
00:10:36.079 specialize in specific regions or
00:10:38.560 specialize in specific domains. And so
00:10:41.360 they can provide more accurate or
00:10:43.360 culturally appropriate translations for
00:10:45.360 your text. Um, if you're a larger
00:10:48.079 organization, this might be a good
00:10:49.600 option for you to consider. But on the
00:10:52.399 other hand, there's also a DIY route
00:10:54.399 where we can use machine translation
00:10:56.000 instead. And it's a lot easier to get
00:10:58.480 going because all you need is an API
00:11:00.480 key. But of course, the caveat is that
00:11:02.720 while machine translation and AI
00:11:04.959 translation have improved a lot over the
00:11:06.959 years, they're still imperfect and they
00:11:09.120 can still struggle. Um, so it's
00:11:11.040 important that you're able to give the
00:11:12.320 AI as much context as possible about how
00:11:14.959 your text is used so it can generate
00:11:17.040 like a good translation.
00:11:20.240 Okay, but let's say we want to go down
00:11:22.800 number two and go the DIY route. So, how
00:11:24.959 do we do that?
00:11:26.880 And this brings us to the first
00:11:28.240 community gem that I'm going to mention
00:11:29.680 today. um has a somewhat generic
00:11:32.720 sounding name, i8N tasks, but what it
00:11:35.760 really is is a bunch is a CLI with a
00:11:38.240 bunch of different commands that help
00:11:39.920 you manage the loces in your app.
00:11:44.240 Um so it's just a gem. So all we have to
00:11:46.320 do is add it to the gem file and bundle
00:11:48.399 install. Um then we got to choose a
00:11:50.800 translation provider. So like Google
00:11:52.560 translate for example. Um IN task uses
00:11:57.120 environment variables. So, we put the
00:11:58.800 API key into the appropriate variable.
00:12:02.320 Um, and then finally, we can run the
00:12:04.240 translate-missing task. This will take
00:12:07.120 the source local, so English for in our
00:12:09.279 example, um, and then our list of target
00:12:11.760 loces, and translate them using using
00:12:14.720 Google Translate. And so once we've done
00:12:17.120 that, if you look in your config local f
00:12:19.600 directory, you'll see a bunch of new
00:12:21.920 YAML files for each of your target
00:12:24.399 languages with the machine translated
00:12:26.320 text.
00:12:28.320 And congrats, right? Your app is
00:12:30.480 translated. You're all done.
00:12:34.639 Well, uh, no. Unfortunately, uh,
00:12:36.959 fortunately, you're you're not done.
00:12:40.480 Part two, advanced localization.
00:12:44.639 And that's because localization is about
00:12:47.600 a lot more than just text. Especially in
00:12:50.240 software today, there's all sorts of
00:12:52.240 data um besides text that also needs to
00:12:55.200 be considered when you're trying to
00:12:56.800 provide a localized experience for your
00:12:59.120 users.
00:13:01.839 So, I want to start off just talking
00:13:03.440 about dates. Um a lot of what I'm going
00:13:05.360 to cover here is also applicable to
00:13:08.160 other kinds of data like numbers or
00:13:10.160 currency. Um but just to keep it short,
00:13:12.240 I just want to focus on dates for now.
00:13:16.160 So, here's another view. Um, we want to
00:13:18.800 add a list showing some metadata about
00:13:21.839 our posts. Um, we specifically want to
00:13:25.120 show when the post was posted. And since
00:13:27.680 we're using Rails, we can um, the post
00:13:30.800 model has a created timestamp. And one
00:13:33.600 way we could display that is by using
00:13:35.200 the strif function just to format the
00:13:37.519 timestamp. This would end up showing
00:13:39.360 something like July 10th, 2025.
00:13:42.880 Uh, but there's a problem here. Like in
00:13:45.279 the previous example, we can see some
00:13:46.959 hard-coded English text. So, let's
00:13:49.360 externalize that.
00:13:53.760 And we also need to include the
00:13:55.519 formatted date in addition to just the
00:13:58.480 static text. So, like Iuded before,
00:14:01.920 translate lets us pass variables into
00:14:03.760 the translation. So, we can pass the
00:14:05.839 formatted date as a variable called
00:14:08.160 date.
00:14:11.279 And then when we define the string in.l,
00:14:13.680 AML we can include the variable with
00:14:15.839 curly brackets. Uh this variables are
00:14:18.560 really important whenever a translation
00:14:20.160 needs to include some sort of dynamic
00:14:21.920 data because the word order of a phrase
00:14:24.720 in a different language may may differ
00:14:27.040 than your source language and you may
00:14:28.880 have to move the parts around.
00:14:32.160 For example, like in Japanese, the date
00:14:34.320 would actually come first in this
00:14:35.839 phrase. And so using the variable lets
00:14:38.399 um the translators move the parts around
00:14:40.480 into their appropriate place for the for
00:14:42.480 the language's grammar.
00:14:44.720 So this gets us pretty far, but there's
00:14:47.680 still another problem.
00:14:50.000 So this is website yall should be really
00:14:52.160 familiar with at this point. Um this is
00:14:54.639 how we write a date in American English.
00:14:59.040 But the same date in the UK would be
00:15:01.120 written like this. Uh the date the day
00:15:04.639 comes first. uh before the month if if
00:15:07.199 that's unclear.
00:15:09.600 Um and in Japanese the format is even
00:15:12.000 more different. So how do we account for
00:15:14.240 this?
00:15:16.399 Well, just like with text, Rails
00:15:18.240 provides us with a helper method to
00:15:19.920 handle data called localize.
00:15:23.440 Um
00:15:25.199 it takes the object to be localized
00:15:27.600 which is something like a date, a time
00:15:29.600 or a number.
00:15:32.320 And then it can also take some options
00:15:34.000 which are generally used to choose a
00:15:36.320 specific format other than the default
00:15:39.040 uh the default format for that data
00:15:40.880 type.
00:15:43.600 So if we go back to the previous
00:15:44.800 example, we can replace surf time with
00:15:47.680 localize instead.
00:15:50.880 And now the date format can change based
00:15:52.800 on the current local. Just like how
00:15:54.560 translate would use en.yaml to look up
00:15:57.199 translations, localize will use the YAML
00:16:00.000 file to look up the formats.
00:16:04.320 But of course we have to provide the
00:16:05.600 formats. So let's go back to.yaml and we
00:16:08.560 can enter in a default format for dates.
00:16:11.680 There's also a possibility to define
00:16:13.519 other named formats if you want like a
00:16:16.639 long format or a short format or for
00:16:19.120 some other specific purpose.
00:16:22.160 But this leaves us with a question.
00:16:26.880 How do we know what the data format
00:16:28.800 should be for another language? What
00:16:31.839 should we fill in here for Japanese?
00:16:37.040 And so to find this data, we can turn to
00:16:39.279 Unicode. And you may have heard of
00:16:41.680 Unicode and the Unicode Consortium as
00:16:44.399 the ones who are responsible for
00:16:46.079 deciding what emojis make it into your
00:16:48.079 devices every year. Um, but as it turns
00:16:51.040 out, uh, Unicode is about a lot more
00:16:53.120 than just emojis. Um, one of their the
00:16:56.160 goals of their project is to enable
00:16:58.079 software to be used by anyone in any
00:17:00.720 language anywhere in the world.
00:17:04.480 And so to further that goal, uh, Unicode
00:17:07.280 hosts the common local data repository
00:17:10.240 or CLLDR. CLLDR is this huge collection
00:17:13.839 of building blocks for software to
00:17:15.679 support different languages. Um, it
00:17:17.760 includes things like translations of
00:17:19.760 language and country names, rules for
00:17:22.240 how to sort lists in different scripts,
00:17:24.959 preferences for calendars and, uh, week
00:17:27.919 systems, and more importantly for this
00:17:30.640 part of this talk, uh, formatting rules
00:17:32.880 for many data types, including numbers,
00:17:34.960 currencies, and dates.
00:17:38.799 So for dates in particular, CLLDR
00:17:41.280 provides us with a handful of standard
00:17:43.039 formats for long, medium, and short
00:17:46.080 dates. Um, and here on screen, this is
00:17:49.679 just a small sample of what the long
00:17:51.919 format looks like in the CLLDR data. On
00:17:54.799 the right, you have the format string,
00:17:57.039 which is written in CLLDR's um, special
00:18:00.240 formatting DSL. Um, and then on the
00:18:02.960 left, you have the list of local that
00:18:05.440 the format applies to.
00:18:08.240 So that's that's all great, but how do
00:18:10.320 we use this data in Rails?
00:18:13.600 And once again, there's a gem for that.
00:18:15.760 Uh there's actually a couple different
00:18:17.360 gems that clean up and reexport the
00:18:19.520 CLLDR data, but the one I want to focus
00:18:21.520 on here is this one called Twitter CLDR.
00:18:24.799 I think Twitter is like a social network
00:18:27.440 or something. Um, at its core, what
00:18:31.760 Twitter CLDR does is it uses the CLLDR
00:18:34.480 data to format different data types into
00:18:37.200 their localized equivalents, but it also
00:18:40.160 provides a very nice interface into the
00:18:42.000 CLLDR directly, which lets us access
00:18:44.400 that data and transform it into a format
00:18:46.799 that works better with Rails. So, let's
00:18:49.440 take a quick look at how we might do
00:18:51.039 that.
00:18:53.760 So here's a little script demonstrating
00:18:55.679 how we can use Twitter CLDR's date data
00:18:59.120 reader uh to programmatically access
00:19:01.600 data from the CLLDR. So first we set the
00:19:04.480 local to English, then we set the date
00:19:07.120 style to long. Um and then when we make
00:19:10.240 a new data reader object, we can call
00:19:12.320 pattern on it. And then we get the
00:19:14.480 format for the requested local which for
00:19:17.280 English in this example is 4 m d, y.
00:19:25.120 And then we can repeat that for any
00:19:26.720 other local such as Japanese here and we
00:19:29.039 get the format back.
00:19:32.960 But you might have noticed and I kind of
00:19:35.120 alluded to this as well. Um the data
00:19:37.520 formats that we get back from those past
00:19:39.200 two slides are slightly different than
00:19:41.120 the one that we saw at the beginning of
00:19:42.799 this section. Um and that's because
00:19:44.480 CLLDR it uses its own formatting
00:19:47.039 language when it's specifying these
00:19:48.799 formats. But Rails expects us to use
00:19:52.000 Ruby's surfime language. So that means
00:19:54.480 they're not directly compatible. Uh, but
00:19:56.960 converting between them is actually
00:19:58.559 pretty easy thanks to um a map such as
00:20:03.039 this one which I copied out of the
00:20:04.640 Shopify uhworldwide gem.
00:20:09.919 So then if we combine all that stuff
00:20:11.840 together, we take the map from the
00:20:13.360 previous slide and the formats that we
00:20:15.520 got from the scripts earlier, we can
00:20:17.280 finally generate the date format for
00:20:19.120 Japanese. Um, and then we can use all
00:20:21.679 that to repeat this process for any
00:20:23.520 other language that we want to target.
00:20:27.760 So with those formats loaded, we can see
00:20:30.080 that localize will pick them up
00:20:31.679 automatically to generate the correct
00:20:33.600 format string in English
00:20:37.039 or in Japanese. So neat.
00:20:42.240 Okay. Um let's talk about plurals.
00:20:47.840 So let's add another list item to our
00:20:49.679 view. We want to show how many comments
00:20:51.919 this post has. Uh, we've all probably
00:20:54.480 done the thing where we just stick a
00:20:56.720 parentheses s onto the end of the word
00:20:59.200 if it has maybe has to be plural.
00:21:02.559 Um, and maybe you've even done something
00:21:04.720 a little more clever like this where you
00:21:06.799 conditionally add the s if the count is
00:21:09.440 more than one. Um, there's a couple
00:21:12.480 problems here. So, first off, of course,
00:21:14.799 we have hard-coded text. There's the
00:21:16.799 word comment in this view. Uh, but
00:21:19.440 there's also some hard-coded logic.
00:21:22.720 uh specifically the handling of that
00:21:24.559 plural s. And although English is
00:21:27.520 generally pretty regular when it comes
00:21:29.280 to plurals, there are many examples of
00:21:31.280 words that have no plural like sheep or
00:21:35.280 maybe they have an irregular plural like
00:21:37.360 mice. So how do we account for this in
00:21:40.240 localization?
00:21:43.120 And once again though, luckily Rails has
00:21:45.840 support for plurals as well baked right
00:21:47.840 in. So we start by externalizing the
00:21:50.320 string as before, but then we also pass
00:21:52.720 this special variable called count with
00:21:55.280 the value being the number of comments
00:21:57.039 or the number of things that we're
00:21:58.320 counting.
00:22:02.559 And then when we go to the local file to
00:22:04.960 write the translations, instead of
00:22:06.320 writing the strings directly, we supply
00:22:08.640 these two special subkeys. Uh for one we
00:22:13.039 write the message as if there was only
00:22:14.640 one item and then for other we write the
00:22:17.679 message in the plural case. And these
00:22:19.760 keys are what signal to Rails that this
00:22:21.919 this translation it has pluralization
00:22:24.080 logic. So it's going to look at the
00:22:26.240 count to choose which variant to use.
00:22:32.080 But once again how do we handle other
00:22:34.720 languages? English is relatively simple
00:22:37.840 as we just described, but other
00:22:39.679 languages are different. Chinese and
00:22:42.080 Japanese, for example, they generally
00:22:44.240 don't have special plural forms. Uh,
00:22:46.799 languages like Hebrew, for example, have
00:22:49.280 a special form for just two items. And
00:22:52.400 other languages like Russian, they have
00:22:54.320 even more categories. So, how do we know
00:22:57.120 which categories a language needs and
00:23:00.320 how do we ch how we're supposed to
00:23:01.840 choose between them?
00:23:04.159 So once again we can turn to CLLDR.
00:23:07.120 CLLDR contains all of this data on the
00:23:09.200 plural categories used by every language
00:23:11.360 it covers and it also includes the rules
00:23:13.919 for how to choose between uh which form
00:23:17.280 for a given count. So here on on screen
00:23:20.559 it's this is the data for Irish which
00:23:23.760 uses a total of five different plural
00:23:26.000 categories.
00:23:30.559 So how do we how do we use this data?
00:23:32.320 How do we get into Rails? And once
00:23:33.919 again, there's a community gem for that.
00:23:36.000 This one is called Rails ITN. This is
00:23:38.720 another very generic name, but um what
00:23:42.000 it is is like a it's a combination of a
00:23:45.120 bunch of different local data that is uh
00:23:48.080 just really easy to plug into your Rails
00:23:50.240 application.
00:23:53.679 One thing it does include is the
00:23:55.200 pluralization rules for many languages
00:23:57.520 coded as Ruby classes that are directly
00:23:59.840 usable by um Rails's I8NN. So after you
00:24:04.080 install the gem, it will allow translate
00:24:06.480 to look up the appropriate pluralization
00:24:08.559 algorithm for the current local. The
00:24:11.679 major caveat here of course is that you
00:24:13.679 still need to provide the actual
00:24:15.600 translations for the different
00:24:16.960 categories in all of the languages you
00:24:19.279 want to support.
00:24:21.760 So here's an example of what that might
00:24:23.600 look like. Um on the left we have
00:24:26.159 English like from the previous slides
00:24:29.120 and then on the right we have Russian.
00:24:31.360 So we know that Russian in addition to
00:24:34.480 one and other it also uses few and many.
00:24:37.760 So when we make the translations, we
00:24:39.760 have to make sure that the Russian
00:24:41.679 includes few and many variants for this
00:24:44.799 this specific key in addition to the one
00:24:48.000 and other.
00:24:51.200 And with that data um input into our
00:24:54.480 local files, when we run the code, we
00:24:56.720 can see that the phrase gets correctly
00:24:58.400 pluralized based on the local. Neat.
00:25:09.440 Okay, finally let's talk about lists
00:25:16.000 to our post metadata. Post might have
00:25:19.039 many tags. So, we're just going to list
00:25:21.200 them all by joining the tags together
00:25:22.880 with a comma.
00:25:26.159 Of course, can't have hard-coded text.
00:25:28.400 So, let's externalize and apply
00:25:30.240 translate. Uh, but there's one piece of
00:25:33.520 hard-coded text left here. Can Can
00:25:36.240 anybody anybody spot it?
00:25:39.760 Right. Yeah. Yeah. It's the It's the
00:25:41.360 comma.
00:25:44.320 So, different languages may use
00:25:46.640 different punctuation, especially if
00:25:48.799 they use a different script than
00:25:50.400 English. My favorite example of this is
00:25:52.720 Japanese, uh, which uses this very
00:25:55.520 special looking comma on screen here.
00:25:58.240 And no, that is not a space inside the
00:26:00.960 quotes. That is padding that is built
00:26:03.520 directly into the comma character.
00:26:07.120 Um, additionally, languages may have
00:26:09.360 different rules about how punctuation is
00:26:11.279 used, such as whether a punctuation mark
00:26:14.000 is used to join two items of a list or
00:26:16.960 whether there is a special connector
00:26:18.960 word that's used instead. Um, this data
00:26:22.320 is actually already loaded inside of
00:26:24.320 RailsN, so we don't need to do anything
00:26:26.320 else to bring it in. But how do we how
00:26:29.039 do we put it to work?
00:26:32.480 So going back to our example, we can
00:26:35.520 replace join
00:26:38.400 with two sentence. Two sentence is an
00:26:41.520 array extension provided by Rails. Uh
00:26:43.919 but you can also call it as a helper if
00:26:45.600 you don't want to use the the monkey
00:26:47.360 patch. It simply joins the items of the
00:26:50.320 array by looking up the appropriate word
00:26:52.240 connectors from the local YAML.
00:26:56.960 And this is just an example showing two
00:26:59.039 sentence in action. So we can see just
00:27:01.200 by changing the the local uh two
00:27:04.080 sentence will look up the correct
00:27:05.520 punctuation marks and connectors to join
00:27:08.159 the array together. So we can see in um
00:27:11.279 Japanese here, Japanese actually doesn't
00:27:13.200 use a connector word at the end. It just
00:27:15.919 uses punctuation all throughout. Whereas
00:27:18.240 we can see in English we get the commas
00:27:20.720 and then we get a special connector word
00:27:22.400 for the last item.
00:27:26.799 Okay. Uh wrapping up. So to recap, so by
00:27:30.880 this point we've got a pretty robustly
00:27:32.960 internationalized app. We've
00:27:34.799 externalized all of the hard-coded text
00:27:37.039 and all of the data formats. We have
00:27:39.520 machine translated the local files and
00:27:42.080 we can easily update those translations
00:27:44.080 as the source text changes. And we're
00:27:46.960 leveraging data formats, plural rules,
00:27:49.600 list connectors, all from the standard
00:27:51.600 CLDR data. And to get all of this, we
00:27:54.559 only had to use four small gems. I8NN,
00:27:57.840 which is bundled with Rails by default,
00:28:00.640 and the three other gems that I
00:28:02.480 mentioned mentioned here. IN tasks,
00:28:05.200 Twitter CLDR, and Rails IN.
00:28:10.399 So if you have one takeaway from this
00:28:12.240 talk, I hope it's this. Rails makes
00:28:14.559 internationalization very easy. You
00:28:17.120 don't have to become an expert in
00:28:18.720 software localization, languages,
00:28:21.200 linguistics, or the Unicode standard.
00:28:23.840 All of this knowledge is already
00:28:25.520 collected for you and right there
00:28:27.919 available at your fingertips in this
00:28:29.919 very easy to consume, very easy to use
00:28:32.399 package.
00:28:35.120 So that's it. That's that's all I have.
00:28:37.039 Thank you and good luck on your
00:28:38.799 localization journey.
00:28:45.679 These are my uh links and credits. Um
00:28:48.240 and yeah, if you want to ask me ask me
00:28:50.240 questions, I'll stick around by the by
00:28:52.159 the stage. Um feel free to come up and
00:28:54.000 and talk to me.
Explore all talks recorded at RailsConf 2025
Ben Sheldon
Sam Poder
Rhiannon Payne
Joe Masilotti
Josh Puetz
Wade Winningham
Irina Nazarova
Tess Griffin
+77