In this tutorial, you'll learn how to get started with Rails i18n (internationalization) to translate your application into multiple languages. We'll cover everything you need to know: working with translations, localizing dates and times, and switching locales. Along the way, we'll also touch on the importance of software localization for ensuring your app delivers a smooth experience for users across different regions.
To make things practical, we'll build a sample application and enhance it step by step. By the end, you'll have a solid understanding of Rails I18n and the confidence to implement it in real-world projects.
The source code for this tutorial can be found on GitHub.
As mentioned earlier, we’ll see all the relevant concepts in action. Let’s start by creating a new Rails application:
rails new LokaliseI18n
This tutorial uses Rails 8, but most of the concepts apply to earlier versions as well.
Generating static pages
Create a StaticPagesController with an index action for the main page::
rails g controller StaticPages index
Update the views/static_pages/index.html.erb file to include some sample content:
<h1>Welcome!</h1><p>We provide some fancy services to <em>good people</em>.</p>
Adding a feedback page
Let’s add a feedback page where users can share their (hopefully positive) opinions. Each piece of feedback will include the author's name and the message. To generate the necessary files, run:
rails g scaffold Feedback author message
We’re only interested in two actions:
new: Displays a form for posting feedback and lists all existing reviews.
create: Validates and saves the new feedback.
Modify the new action to fetch all feedback from the database, ordered by creation date:
Navigate to http://localhost:3000 to ensure everything is working correctly.
Now that we have a basic app up and running, let’s proceed to the main part—localizing our application!
A bit of Rails I18n configuration
Before we start translating, we need to decide which languages our application will support. You can pick any, but for this tutorial, we’ll use English (as the default) and French. Update the config/application.rb file to reflect this:
Also hook up a rails-i18n gem, which has locale data for different languages. For example, it has translated month names, pluralization rules, and other useful stuff.
To simplify things, we’ll use the rails-i18n gem. This gem provides locale data for various languages, such as translated month names, pluralization rules, and other helpful configurations.
Add the gem to your Gemfile:
# Gemfile# ... gem 'rails-i18n', '~> 8'
Then install the library:
bundle install
That’s it! Now we have everything set up to start adding translations.
Storing translations for Rails i18n
Now that everything is configured, let’s start translating the home page content.
Localized views
The simplest way to add translations is by using localized views. This involves creating separate view files for each supported language. The naming convention is:
index.LOCALE_CODE.html.erb
Here, LOCALE_CODE corresponds to the language code. For this demo, we’ll create two views:
index.en.html.erb for English
index.fr.html.erb for French
Add the localized content to these files:
<!-- views/static_pages/index.en.html.erb --><h1>Welcome!</h1><p>We provide some fancy services to <em>good people</em>.</p>
<!-- views/static_pages/index.fr.html.erb --><h1>Bienvenue !</h1><p>Nous offrons des services fantastiques aux <em>bonnes personnes</em>.</p>
Rails will automatically render the correct view based on the currently set locale. Handy, right?
However, while localized views work well for small projects or static pages, they aren’t always practical—especially when you have a lot of dynamic content. For that, we use translation files.
Translation files for Rails i18n
Instead of hardcoding text into views, Rails allows you to store translated strings in separate YAML files under the config/locales directory. This is the recommended approach for managing translations at scale.
Open the config/locales folder, and you’ll notice a default en.yml file with some sample data:
en: hello: "Hello world"
Here:
en is the top-level key, representing the language code.
The hello key maps to the translated string "Hello world". Just make sure to properly organize translation keys for your own convenience.
Let’s replace the sample data with a welcome message for the home page. Update en.yml like this:
# config/locales/en.ymlen: welcome: "Welcome!"
Now, create a new fr.yml file in the same directory to add the French translation:
# config/locales/fr.ymlfr: welcome: "Bienvenue !"
With these files, you’ve successfully created translations for your first string—great!
Performing simple translations with Rails I18n
Now that we’ve populated the YAML files with some data, let’s see how to display those translated strings in the views. It’s as simple as using the translate method (aliased as t). This method requires just one argument—the name of the translation key:
<!-- views/static_pages/index.html.erb --><h1><%= t 'welcome' %></h1>
When the page is rendered, Rails looks up the string that corresponds to the provided key and displays it. If the translation isn’t found, Rails will render the key itself (and format it into a more human-readable form).
Naming and organizing translation keys
Translation keys can be named almost anything, but it’s always a good idea to use meaningful names that describe their purpose. Additionally, it’s best to keep your keys well-organized—you can refer to best practices in the Translation keys: naming conventions and organizing blog post for more detailed guidelines.
Adding the second message
Let’s add the next string to both en.yml and fr.yml:
# config/locales/en.ymlen: welcome: "Welcome!" services_html: "We provide some fancy services to <em>good people</em>."
# config/locales/fr.ymlfr: welcome: "Bienvenue!" services_html: "Nous fournissons des services sophistiqués aux <em>bonnes personnes</em>."
Why the _html postfix?
You may have noticed the _html postfix on the translation key. Here’s why:
By default, Rails will escape any HTML tags, rendering them as plain text. Since we want the <em> tag to remain intact and be displayed as formatted HTML, we mark the string as "safe HTML" using this convention.
Using the t method again
Now, let’s render the second message in the view:
<!-- views/static_pages/index.html.erb --><!-- ... ---><p><%= t 'services_html' %></p>
And that’s it! Rails will display the proper translation values based on the current locale, and your HTML formatting will remain untouched.
More on translation keys and Rails i18n
Our homepage is now localized, but let’s pause and consider what we’ve done so far. While our translation keys have meaningful names, what happens as our application grows? For example, imagine we have 500+ messages—not an unrealistic number for even a small app. Larger websites can easily have thousands of translations.
If all the keys are stored directly under the en key with no further organization, this leads to two main problems:
Ensuring all keys have unique names becomes increasingly difficult.
It’s hard to locate related translations, such as those for a specific page or feature.
Grouping translation keys
To avoid these issues, it’s a good idea to group your keys logically. For example, you can nest your translations under arbitrary keys:
There’s no limit to the nesting levels (though it’s best to keep it reasonable). This also allows you to use identical key names in different groups without any conflicts.
Following the view folder structure
A useful approach is to mirror the folder structure of your views in your YAML files. This makes your translations easy to navigate and aligns naturally with Rails conventions. Let’s reorganize our existing translations like so:
English:
# config/locales/en.ymlen: static_pages: index: welcome: "Welcome!" services_html: "We provide some fancy services to <em>good people</em>."
And French:
# config/locales/fr.ymlfr: static_pages: index: welcome: "Bienvenue!" services_html: "Nous fournissons des services sophistiqués aux <em>bonnes personnes</em>."
Referencing translations
To access these nested keys, you provide the full path to the key in the t helper methods:
<!-- views/static_pages/index.html.erb --><h1><%= t 'static_pages.index.welcome' %></h1><p><%= t 'static_pages.index.services_html' %></p>
However, Rails provides a shortcut called lazy lookup. If you are referencing translations in a view or controller, and the keys are namespaced properly (following the folder structure), you can omit the full path. Instead, use a leading dot (.):
<!-- views/static_pages/index.html.erb --><h1><%= t '.welcome' %></h1><p><%= t '.services_html' %></p>
The leading dot tells Rails to look for the translation key relative to the current namespace—in this case, static_pages.index.
Translating the global menu
Next, let’s localize our global menu and namespace these translations properly:
Now let’s move on to the feedback page and localize the form.
Translating input labels
Rails makes it easy to translate model attributes. By providing the appropriate translations under the activerecord namespace, Rails will automatically pick them up when generating form labels.
But what about the corresponding error messages? How do we translate them? The rails-i18n gem includes localized error messages for many languages, so you don’t need to do anything extra for common validation errors.
For example, if you’re using the French locale, Rails will automatically render the appropriate error messages. If you want to customize these default messages further, refer to the official Rails I18n guide.
One problem with the form, however, is that the error messages subtitle (the one that says "N errors prohibited this feedback from being saved:") is not translated. Let's fix it now and also discuss pluralization.
Pluralization rules in Rails i18n
Since the feedback form may display one or more error messages, the word "error" in the subtitle needs to be pluralized correctly. In English, this is usually done by adding an "s" (e.g., error → errors), but in other languages, pluralization rules can be more complex.
Luckily, the rails-i18n gem handles all the pluralization rules for supported languages, so you don’t have to write them yourself.
English and French pluralization
For both English and French, there are just two pluralization cases:
one – Singular (e.g., "1 error").
other – Plural (e.g., "2 errors").
Here’s how you define them in the translation files:
English:
# config/locales/en.ymlen: global: forms: submit: "Submit" messages: errors: one: "One error prohibited this form from being saved:" other: "%{count} errors prohibited this form from being saved:"
French:
# config/locales/fr.ymlfr: global: forms: submit: "Soumettre" messages: errors: one: "Une erreur a interdit l'enregistrement de ce formulaire :" other: "%{count} erreurs ont empêché la sauvegarde de ce formulaire :"
Here, %{count} is used for interpolation—Rails inserts the value of count into the string wherever %{count} appears.
Handling complex pluralization rules with Rails i18n
If you’re working with a language that has more than two plural forms, you need to provide additional keys. For example, Russian has four plural forms:
Now let’s use these translations in the feedback form. Update the feedbacks/_form.html.erb partial like this:
<%= form_with(model: feedback) do |form| %> <% if feedback.errors.any? %> <div style="color: red"> <h2><%= t 'global.forms.messages.errors', count: feedback.errors.count %></h2> <ul> <% feedback.errors.each do |error| %> <li><%= error.full_message %></li> <% end %> </ul> </div> <% end %> <!-- ... other code ... --><% end %>
The t method fetches the correct translation based on the key and the count variable.
Rails selects the appropriate plural form (one, other, few, or many) based on the count.
The %{count} placeholder is dynamically replaced with the actual number of errors.
For example:
With 1 error, the English subtitle will read: "One error prohibited this form from being saved."
With 3 errors, it will display: "3 errors prohibited this form from being saved."
Working with date and time
Now let’s localize the _feedback.html.erb partial. We need to translate two pieces of text:
The date and time (the created_at field).
The "Posted by…" string, which includes the author’s name.
Translating "Posted by…" with interpolation
For the "Posted by…" string, we’ll use interpolation to include the author's name dynamically. Add the following translations to your YAML files:
# config/locales/en.ymlen: feedbacks: posted_by: "Posted by %{author}"
# config/locales/fr.ymlfr: feedbacks: posted_by: "Envoyé par %{author}"
Using translations in the view
Now, update the partial to include the t method for the "Posted by…" text:
<!-- views/feedbacks/_feedback.html.erb --><%= tag.article id: dom_id(feedback) do %> <em> <%= tag.time feedback.created_at, datetime: feedback.created_at %><br> <%= t 'feedbacks.posted_by', author: feedback.author %> </em> <p> <%= feedback.message %> </p> <hr><% end %>
Here, the t method fetches the appropriate translation and interpolates the author value dynamically.
Localizing date and time
To handle the created_at timestamp, we’ll use the localize method (aliased as l). This works similarly to Ruby’s strftime but automatically uses translated month and day names based on the current locale.
Now that our app is fully translated, there’s just one problem: we can’t change the locale. While it might sound minor, it’s actually a major usability issue—so let’s fix it!
We’ll implement the following solution:
URLs will include an optional :locale parameter, like: http://localhost:3000/en/some_page.
If this parameter is set and matches a supported locale, Rails will switch to that language.
If no parameter is provided or the locale isn’t supported, Rails will fall back to the default locale (English).
Sounds simple? Let’s dive into the code.
Routes
First, update the config/routes.rb file to include a locale scope:
# config/routes.rbscope "(:locale)", locale: /#{I18n.available_locales.join("|")}/ do # your routes here... resources :feedbacks root 'static_pages#index'end
Here:
The (:locale) scope makes the :locale parameter optional.
We validate the locale using a regular expression to ensure it matches one of the supported locales.
Adding a concern for locale handling
Next, create an internationalization concern to handle locale switching.
Include the concern in ApplicationController:
# controllers/application_controller.rbclass ApplicationController < ActionController::Base include Internationalizationend
Create this new concern:
# controllers/concerns/internationalization.rbmodule Internationalization extend ActiveSupport::Concern included do around_action :switch_locale private def switch_locale(&action) locale = locale_from_url || locale_from_headers || I18n.default_locale response.set_header 'Content-Language', locale I18n.with_locale(locale, &action) end def locale_from_url locale = params[:locale] return locale if I18n.available_locales.map(&:to_s).include?(locale) end def locale_from_headers header = request.env['HTTP_ACCEPT_LANGUAGE'] return if header.nil? locales = parse_header(header) return if locales.empty? detect_from_available(locales) end def parse_header(header) header.gsub(/\s+/, '').split(',').map do |tag| locale, quality = tag.split(/;q=/i) quality = quality ? quality.to_f : 1.0 [locale, quality] end.reject { |(locale, quality)| locale == '*' || quality.zero? } .sort_by { |(_, quality)| quality } .map(&:first) end def detect_from_available(locales) locales.reverse.find { |l| I18n.available_locales.any? { |al| match?(al, l) } } end def match?(str1, str2) str1.to_s.casecmp(str2.to_s).zero? end def default_url_options { locale: I18n.locale } end endend
How it works
switch_locale: Determines the locale by checking:\
• URL parameters (locale_from_url).
• Request headers (locale_from_headers).
• Defaults to I18n.default_locale if none are found.
default_url_options: Ensures all Rails URL helpers include the :locale parameter automatically.
The parse_header method handles the complexity of parsing the HTTP_ACCEPT_LANGUAGE header, which browsers send to indicate preferred languages.
Adding locale switching links
Now, let’s allow users to switch locales from the UI. Update the global menu in application.html.erb:
<!-- views/layouts/application.html.erb --><ul> <% I18n.available_locales.each do |locale| %> <li> <% if I18n.locale == locale %> <%= t(locale, scope: 'locales') %> <% else %> <%= link_to t(locale, scope: 'locales'), url_for(locale: locale) %> <% end %> </li> <% end %></ul>
Here:
I18n.available_locales provides a list of supported locales.
t(locale, scope: 'locales') fetches the translated name of the locale.
url_for(locale: locale) generates a URL with the updated :locale parameter.
• http://localhost:3000/en → App will render in English. • http://localhost:3000/en → App will render in English.
Switch between locales using the links in the navigation menu.
Rails will:
Remember the locale in URLs.
Automatically include the current locale in all generated links.
Fallback to the default locale if no valid :locale is provided.
Simplify your Rails i18n workflow with Lokalise
By now, you might be thinking that supporting multiple languages on a large website is a pain. Honestly? You’re right. While Rails makes it easier with namespaced keys and multiple YAML files, it’s still on you to ensure every key is translated across all locales.
Fortunately, there’s a better way: Lokalise—a platform that simplifies managing and editing localization files. Let’s walk through the setup.
Prefer a more Rails-like experience instead of relying on command-line interface? Enter the lokalise_rails gem, which integrates Lokalise directly into your app.
Installation
Add the gem to your Gemfile:
gem 'lokalise_rails'
Then run:
bundle installrails g lokalise_rails:install
Configuration
Update config/lokalise_rails.rb with your token and project ID:
require 'lokalise_rails'LokaliseRails::GlobalConfig.config do |c| c.api_token = ENV['LOKALISE_API_TOKEN'] c.project_id = ENV['LOKALISE_PROJECT_ID'] # ... other options ...end
Here you'll need to enter your Lokalise API and the project ID.
Advanced: Managing multiple directories in Rails i18n
If your app uses custom directories for translations, you can leverage the LokaliseManager gem to handle this programmatically. For example:
require 'rake'require 'lokalise_rails'require "#{LokaliseRails::Utils.root}/config/lokalise_rails"namespace :lokalise_custom do task :export do # importing from the default directory (./config/locales/) exporter = LokaliseManager.exporter({}, LokaliseRails::GlobalConfig) exporter.export! # importing from the custom directory exporter = LokaliseManager.exporter({locales_path: "#{Rails.root}/config/custom_locales"}, LokaliseRails::GlobalConfig) exporter.export! rescue StandardError => e abort e.inspect endend
This script uploads all YAML files from both config/locales and config/custom_locales.
Conclusion to Rails i18n
In this article, we’ve explored how to:
Set up Rails I18n for a multilingual app.
Use translation files and localized views.
Translate error messages, model attributes, and dynamic content.
Switch between locales with URL-based persistence.
Simplify your workflow with Lokalise and its tools.
While Rails I18n is powerful, tools like Lokalise make managing translations at scale a breeze. For further details, I recommend checking out the official Rails I18n guide.
Thank you for staying with me, and until next time!
Ilya is a lead of content/documentation/onboarding at Lokalise, an IT tutor and author, web developer, and ex-Microsoft/Cisco specialist. His primary programming languages are Ruby, JavaScript, Python, and Elixir. He enjoys coding, teaching people and learning new things. In his free time he writes educational posts, participates in OpenSource projects, goes in for sports and plays music.
Ilya is a lead of content/documentation/onboarding at Lokalise, an IT tutor and author, web developer, and ex-Microsoft/Cisco specialist. His primary programming languages are Ruby, JavaScript, Python, and Elixir. He enjoys coding, teaching people and learning new things. In his free time he writes educational posts, participates in OpenSource projects, goes in for sports and plays music.
An SRT file is a plain text file used to add subtitles to videos. It’s one of the simplest and most common formats out there. If you’ve ever turned on captions on a YouTube video, there’s a good chance it was using an SRT file behind the scenes. People use SRT files for all kinds of things: social media clips, online courses, interviews, films, you name it. They’re easy to make, easy to edit, and they work pretty much everywhere without hassle. In this post, we’ll
Libraries and frameworks to translate JavaScript apps
In our previous discussions, we explored localization strategies for backend frameworks like Rails and Phoenix. Today, we shift our focus to the front-end and talk about JavaScript translation and localization. The landscape here is packed with options, which makes many developers a
Syncing Lokalise translations with GitLab pipelines
In this guide, we’ll walk through building a fully automated translation pipeline using GitLab CI/CD and Lokalise. From upload to download, with tagging, version control, and merge requests. Here’s the high-level flow: Upload your source language files (e.g. English JSON files) to Lokalise from GitLab using a CI pipeline.Tag each uploaded key with your Git branch name. This helps keep translations isolated per feature or pull request