# The Ruby i18n gem makes the translation backend swappable

> The i18n gem is the internationalisation and localisation library for Ruby, and its distinctive feature is that the store behind the translations can be replaced, from a plain hash to ActiveRecord to a key value store, with the API held constant across all of them. The most current part of it is the fibre safe configuration update, which exists because Ruby fibers inherit state they should not share.

**ruby-i18n/i18n** — Internationalization (i18n) library for Ruby

- Repository: https://github.com/ruby-i18n/i18n
- Stars: 1,034 · Forks: 421
- Language: Ruby
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/ruby-i18n-i18n

## The library most Ruby applications already depend on

The i18n gem is the internationalization and localisation solution for Ruby, and it is currently maintained by @radar. In practice most people meet it inside a Rails application without choosing it, because Rails itself depends on it for its own translations.

The supported versions are stated in two places, because there are two ways to use it. Within Rails, versions from 6.0 and up are supported, and the Rails guide is linked for an example of the usage. Without Rails, Ruby versions from 3.1 and up are supported.

The feature list is the summary of what a localisation library is expected to do, and it is worth reading as a checklist of the things that are usually forgotten. Translation and localisation are the base. Interpolation of values into translations is the mechanism for a sentence with a variable in it. Pluralization is CLDR compatible, which means the plural rules are the locale data rules rather than an approximation. Transliteration to ASCII is customizable, for locales that need a fallback representation. Flexible defaults, bulk lookup, lambdas as translation data, a custom key and scope separator, custom exception handlers, and an extensible architecture with a swappable backend complete the list.

The last item is the one that distinguishes this library, and it is the subject of its own section below. The rest is a well built, complete implementation of something most teams would write badly.

The licence is MIT, with the MIT-LICENSE file in the repository, and the project has been going a long time: the contributors list includes @radar, @carlosantoniodasilva, @josevalim, @knapo and @tigrish among others, and the recent releases are v1.15.0, v1.15.1 and v1.15.2, all in June 2026, with the last push on 2026-09-01 and the repository not archived.

## Using it without Rails

The plain Ruby path is short, and it is the path a non Rails application takes.

The gem goes into the Gemfile:

```ruby
gem 'i18n'
```

Then you configure the load path and the default locale:

```ruby
I18n.load_path += Dir[File.expand_path("config/locales") + "/*.yml"]
I18n.default_locale = :en # (note that `en` is already the default!)
```

The load path is a list of YAML files, and a translation file in the project might live at config/locales/en.yml and look like this:

```yml
en:
  test: "This is a test"
```

After that, a translation is fetched with the translate method, taking the key:

```ruby
I18n.t(:test)
```

and the locale is switched by setting the locale on the I18n module to a different value, after which the same key returns the other language:

```ruby
I18n.locale = :de
I18n.t(:test) # => "Dies ist ein Test"
```

Two things about that last snippet are worth noticing. The locale is a single global setting, which is the design that a web request thread or fiber is expected to set and reset around the request, and the comment on the default locale is a small kindness, because the default is already English and setting it explicitly is only needed to change it.

The structure of the configuration, a directory of YAML files added to a load path, is the part most projects copy from this library, whether or not they use the gem.

## Pluralization, fallbacks and the pluggable pieces

Beyond the basics, the library has a set of features that are opt in, and they are what separate a real localisation library from a string replacement helper.

Cache is the first, and it is the one you need for a view that renders a hundred translated strings. Pluralization is listed twice, once as a feature and once as a pluggable piece, and the second listing is more precise: pluralizers are lambdas stored as translation data, so the plural rules live with the translations rather than in a switch statement in your code. Locale fallbacks are RFC4647 compliant, with optional RFC4646 locale validation, which is what makes a request for a regional variant fall back sensibly. Gettext support is available, so an existing gettext catalogue can be read rather than rewritten. And translation metadata is supported, which is how a translation management service knows which strings are missing.

The pliability of the plural rules is the part with the most design thought behind it. Putting the pluralizer in the translation file means a translator or a locale maintainer decides how many forms a language has, rather than a developer maintaining a global table, and it means a language with two plural forms and a language with six both work without a special case in the library.

The exception handlers are worth a mention too, because the default behaviour of a missing translation is to raise, and in production a missing key usually should not take down a page. Custom exception handlers let a team log and fall back instead, and the feature list pairs that with flexible defaults, which is the other half of the same problem.

## Swapping the backend under the API

The architectural claim in the feature list, an extensible architecture with a swappable backend, is the one that most affects how you would use this in a large application, so it deserves the detail.

The default backend is the Simple one, which keeps translations in a hash built from the YAML files on the load path. The alternative backends listed are Chain, which composes other backends, ActiveRecord, optionally with the ActiveRecord::Missing and ActiveRecord::StoreProcs modules, and KeyValue, which uses active_support and json and cannot store procs.

That last caveat is the interesting one. If your translation data includes lambdas, and the pluggable pluralization feature is built on lambdas, then a KeyValue backend cannot hold them, so the two features are incompatible. The limitation is stated in the documentation rather than discovered at runtime, and the same applies to a database backend that stores translations as plain strings.

The ActiveRecord backend is the one most teams would want. It means translations live in the application's own database, so they are versioned with the schema, backed up with everything else, and can be edited by non developers through an admin screen without a file deploy. The optional modules are what make that practical: ActiveRecord::Missing records which keys were requested and not found, which is the raw material for a translation management workflow, and ActiveRecord::StoreProcs allows the stored values to be code rather than text.

The whole design is a bet that the API is the contract and the storage is an implementation detail. The test suite is built to enforce exactly that, which is the subject of a later section.

## Fibre safe configuration, which is the modern part

The configuration section on fibre safety is short and it is the part of the readme that dates this version of the library, because it deals with a problem that only exists in recent Ruby.

The rule is that values in a Fiber array are inherited by child fibers. To avoid sharing mutable config objects across fibers, you should prefer creating updated configs and setting them with the bang method:

```ruby
I18n.config.with(locale: :ja).set!
```

The bang method stores a frozen config in the current execution context. The documentation explains the two properties that follow: it keeps inherited state safe, and it lets updates happen by replacing config objects.

Why this matters is the shape of modern Ruby concurrency. In a Ruby application using fibers, particularly with the fiber scheduler, a request handler may run inside a fiber that inherited its configuration from the parent fiber. Under the old model, setting the locale mutates an object that the child fiber also sees, and two fibers handling requests in different locales will interfere with each other. Under the new model, each fiber holds a frozen config, and a change replaces it rather than editing it in place.

The consequence for a user is that the old idiom of assigning the locale may not be the right thing to do in a fibered application, and the readme is explicit that set! is the safer form. For a Rails application, which sets the locale around a request, that difference is invisible. For an application that drives many fibers, it is the difference between translations that are correct and translations that occasionally belong to another request.

## A test suite built to hold the API constant

The test section explains an unusual structure, and the reason given is the best argument for the design of the library as a whole.

The structure is unusual because it uses modules to reuse particular tests in different test cases. The reason is that the I18n API has to be enforced across various combinations of extensions. The example given is that the Simple backend alone needs to support the same API as any combination of feature and optimisation modules included to the Simple backend, and the way they test that is by reusing the same API definition, implemented as test methods, in test cases with different setups.

So the API enforcing test cases live in test/api, and the API definition test methods live in test/api/tests. All the other test cases, the ones in test/backend and test/core_ext and the rest, follow the usual test setup and are easy to read.

Running the tests is straightforward. You can run rake test or just rake, you can run any test file directly, for example ruby -Ilib:test test/api/simple_test.rb, and you can run all the tests against all the Gemfiles with ruby test/run_all.rb. That last one matters, because the gemfiles/ directory in the repository is the matrix of dependency combinations, and running the suite across all of them is how a library that supports a range of Ruby and Rails versions checks that it really does.

The repository layout is small and conventional for a gem: lib/ for the code, test/ for the tests, benchmark/ for the performance measurements, gemfiles/ for the dependency matrix, the gemspec, a Rakefile and a CHANGELOG.md. The additional documentation lives on the project wiki, including a Resources page with links and material that does not fit in a readme.

## Conclusion

The i18n gem suits a Ruby or Rails application that needs real localisation, with plural rules, fallbacks and transliteration rather than string substitution, and it suits a team that stores its translations somewhere other than YAML files. It is the wrong choice if you want translations held in a database with no further configuration, because the ActiveRecord backend is an alternative to the default and has to be selected and set up, and a poor match if your app predates Rails 6 or Ruby 3.1, since those are the stated floors. Before adopting, check the version your Gemfile resolves to, because the gem tracks Ruby and Rails generations, and read the fibre safe configuration section if your application uses fibers, since the locale setting has changed shape.

## FAQ

### How do I set up the Ruby i18n gem outside Rails?

Add gem 'i18n' to the Gemfile, add your locale files to the load path with I18n.load_path, and set the default locale with I18n.default_locale, where English is already the default. Translations are then read with I18n.t and the locale is switched by assigning I18n.locale.

### Which alternative backends can the i18n gem use?

Chain, which composes other backends, ActiveRecord, optionally with the ActiveRecord::Missing and ActiveRecord::StoreProcs modules, and KeyValue, which uses active_support and json and cannot store procs. The default Simple backend holds translations in a hash built from the YAML files on the load path.

### Why does the i18n gem recommend set! for configuration changes?

Values in a Fiber array are inherited by child fibers, so mutating a shared config across fibers lets two requests interfere with each other. Creating an updated config and setting it with set! stores a frozen config in the current execution context, so updates replace the object rather than editing it in place.

### What are the pluralization and fallback rules in the i18n gem?

Pluralization is CLDR compatible and implemented through lambda pluralizers stored as translation data, so the rules live with the translations. Locale fallbacks are RFC4647 compliant, with optional RFC4646 locale validation, and Gettext support and translation metadata are also available.

## Sources

- [Issues](https://github.com/ruby-i18n/i18n/issues)
- [License: MIT](https://github.com/ruby-i18n/i18n/blob/master/LICENSE)
- [README](https://github.com/ruby-i18n/i18n/blob/master/README.md)
- [Releases](https://github.com/ruby-i18n/i18n/releases)
- [ruby-i18n/i18n on GitHub](https://github.com/ruby-i18n/i18n)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ruby-i18n-i18n
