Introduction
One of the most under-appreciated features of Ruby on Rails is “Internationalization” (often shortened to “I18n”). Although it has been a feature of Rails since version 2.2, not a whole lot of applications make use of it. While this is understandable if an application is only ever intended to be used by speakers of one language, not using I18n from the beginning makes it time consuming to support additional languages if the need ever arises.
I’m not going to extoll the virtues of I18n for supporting multiple languages; there are already plenty of articles about that. Instead, I’m going to explain how I18n can be used to allow translations to be editable (or overridable) without needing to change the YAML locale files, and thus, needing to deploy the changes and restart the server.
Why would you want to do this? Maybe you want to store the bulk of your translations in the YAML locale files, but store custom ones in the database (either in addition to or to override the standard ones). Or maybe you need to make a certain set of strings on your application editable by admin users who don’t program or have access to the code or deployments. The latter isn’t really an intended use of I18n, sure, since it’s nothing to do with translations or multiple languages, but if that’s a requirement that you need to provide, then this is one way to achieve that. Whatever your reasons, let’s look at how to get it done.
Background
Active Support (a component of Rails) includes the i18n gem as a dependency. This is the gem that gives Rails the functionality to store, lookup, format and display values in multiple languages. The default backend for storing and looking up translations is called the “Simple” backend. This is the backend that supports the most common method of storing translations: in YAML (.yml) or Ruby (.rb) files (in Hashes, for the latter). The gem also provides another basic backend, called the “Key Value” backend. But what is more interesting is the “Chain” backend. The Chain backend allows you to chain multiple I18n backends together, and searches through them in order until it finds a translation for the specified key. For the possible use cases outlined previously, this means that you can use the “Simple” backend with YAML files to hold your base translations, and store custom ones in a relational database, accessible through Active Record. In other words, the application would look up the database first, and if there is no translation to be found, would fall back to the YAML files.
All this requires an additional backend to handle the database lookups. Such a backend exists, called the “Active Record” backend, but it’s not included in the I18n gem; you have to include it yourself. Once you have done that, all you have to do is configure Rails to chain together the Active Record and Simple backends and query them in that order. So, let’s get to the code.
Implementation
First, add the Active Record I18n backend gem to your bundle:
```ruby # Gemfile
Store custom I18n translations in the database.
gem ‘i18n-active_record’, require: ‘i18n/active_record’ ```
Then configure Rails with the backend chain and to load YAML files in subdirectories of config/locales
:
```ruby # config/application.rb
Add this line to the top of the file (after the require statements for the Railties), and the others inside the main Application class.
require ‘i18n/backend/active_record’
Set up a chain of I18n backends where the database is queried first for a translation for the key (via a Translation ActiveRecord model), and then fall back to the defaults in the YAML files.
config.i18n.backend = I18n::Backend::Chain.new(I18n::Backend::ActiveRecord.new, I18n::Backend::Simple.new)
Auto load all translations from config/locales/*/.yml (and sub-directories).
config.i18n.load_path += Dir[Rails.root.join(‘config’, ‘locales’, ‘*’, ‘.yml’)] ```
Generate a Translation model:
```ruby # app/models/translation.rb
class Translation < ActiveRecord::Base # You’ll probably want to put some validations in here. end ```
And use this migration to create the database table to back the model:
```ruby class CreateTranslations < ActiveRecord::Migration def change create_table :translations do |t| t.string :locale, default: ‘en’, null: false t.string :key, null: false, index: true t.text :value, null: false t.text :interpolations t.boolean :is_proc, default: false, null: false
t.timestamps null: false
end end end ```
Reference your translation in one of your views:
```erb # app/views/public/home.html.erb
<%= t('.title') %>
```
Now add a YAML file to hold the default value of the translation (you need to restart the server when you add a new locale file):
```yml # config/locales/views/public/home.en.yml
en: public: home: title: ‘YAML value!’ ```
Now open up the application in your browser and you will see the default string in the YAML file on the page. Now create a translation in the database to override the default one:
```ruby # Run in the console.
Translation.create!(key: ‘public.home.title’, value: ‘Database value!’) ```
Reload the page, and you should see the database value on the page instead. Deleting the translation from the database will revert the string on the page back to the default one from the YAML file.
Now that it’s all working, you might want to throw an admin section on top so that you don’t need to use the console to manage custom translations (if that’s something you want your admin users to be able to do). I recommend either RailsAdmin or Active Admin.
Conclusion
The only drawback to this that I can think of (assuming you only make this feature available to trusted users) is the additional database queries while rendering your pages. Depending on how many translations you have, you probably want to make use of the excellent Fragment Caching functionality of Rails to cache parts of your pages, and expire those caches when a translation has been created or updated (by, for example, including Translation.maximum(:updated_at).utc.to_s(:number)
in the cache key).
And that’s all there is to it. Combining the default Simple backend of I18n with the additional Active Record backend and configuring Rails to chain them together is an effective way to store the bulk of your translations in YAML files and yet allow them to be added to or overridden by your users without needing to give them the permissions and skills to update the code, commit the changes, and deploy them. Translations added and edited in the database instantly take precedence over those in your YAML files, giving your users an easier and faster way to manage custom translations and content.