Ruby on Rails internationalization (i18n) provides a comprehensive framework for building multilingual web applications through the Rails I18n API, which abstracts translation lookups, locale management, and formatting conventions into a consistent interface. Rails i18n stores translations in YAML files organized by locale under config/locales/, using dot-notation keys for hierarchical organization and the t() helper method for runtime string retrieval with interpolation support. The framework includes sophisticated pluralization handling following CLDR rules, date and time localization through l() helper, number and currency formatting via number_to_currency() helpers, and automatic validation message translation. Rails automatically detects user locale from URL parameters, session data, or HTTP headers through before_action filters or middleware. For database-backed content translation, gems like Globalize and Mobility extend ActiveRecord models with translatable attributes stored in separate translation tables. The I18n backend supports multiple storage strategies including YAML files, database-backed translations, and Redis caching for high-performance applications. This comprehensive guide covers Rails i18n from configuration through production deployment, including route localization, ActionMailer internationalization, JavaScript integration, testing strategies, and continuous localization workflows using modern translation management systems.
Rails I18n Architecture
Rails internationalization operates through the I18n module, a thin abstraction layer that provides a consistent API regardless of the backend storage mechanism used for translations.
The I18n framework consists of several components:
Public API: Methods like I18n.t(), I18n.l(), I18n.locale= exposed to application code
Backend: Storage and retrieval mechanism (Simple backend using YAML by default)
Locale Detection: Automatic or manual locale setting through I18n.locale
Fallbacks: Chain of locales to check when translation missing
Interpolation: Variable substitution in translation strings
Pluralization: Language-specific plural form selection
When you call I18n.t('hello'), Rails follows this resolution path:
- Check current locale (
I18n.locale) - Look up key in backend (e.g.,
en.helloin YAML files) - If not found, check fallback locale
- If still not found, return default or missing translation message
- Interpolate any variables
- Return final string
Rails loads all YAML files from config/locales/ at boot time, making translations immediately available. In development mode, Rails reloads YAML files when they change.
The framework uses "lazy lookup" in views, allowing t('.title') to automatically namespace translations based on the view path.
Initial Rails i18n Configuration
Configure internationalization in your Rails application.
Basic Setup
RUBY1# config/application.rb 2 3module MyApp 4 class Application < Rails::Application 5 # Default locale 6 config.i18n.default_locale = :en 7 8 # Available locales 9 config.i18n.available_locales = [:en, :es, :fr, :de] 10 11 # Fallback to default locale for missing translations 12 config.i18n.fallbacks = true 13 14 # Load locale files from subdirectories 15 config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')] 16 17 # Raise error for missing translations in development/test 18 config.i18n.raise_on_missing_translations = Rails.env.development? || Rails.env.test? 19 end 20end
Directory Structure
Organize locale files:
Terminalmkdir -p config/locales/{en,es,fr,de}
Resulting structure:
config/locales/
├── en/
│ ├── models.yml
│ ├── views.yml
│ └── mailers.yml
├── es/
│ ├── models.yml
│ └── views.yml
├── fr/
│ └── views.yml
├── de.yml
└── en.yml
Creating YAML Translation Files
YAML1# config/locales/en.yml 2 3en: 4 hello: "Hello world" 5 messages: 6 welcome: "Welcome to our application" 7 greeting: "Hello, %{name}!" 8 9 activerecord: 10 models: 11 user: "User" 12 product: "Product" 13 attributes: 14 user: 15 email: "Email Address" 16 password: "Password" 17 18 time: 19 formats: 20 short: "%b %d" 21 long: "%B %d, %Y"
Spanish translation:
YAML1# config/locales/es.yml 2 3es: 4 hello: "Hola mundo" 5 messages: 6 welcome: "Bienvenido a nuestra aplicación" 7 greeting: "¡Hola, %{name}!" 8 9 activerecord: 10 models: 11 user: "Usuario" 12 product: "Producto" 13 attributes: 14 user: 15 email: "Correo Electrónico" 16 password: "Contraseña" 17 18 time: 19 formats: 20 short: "%d %b" 21 long: "%d de %B de %Y"
Translation Helpers
Rails provides helper methods for translation in controllers and views.
Basic Translation
RUBY1# In controllers 2class WelcomeController < ApplicationController 3 def index 4 @message = I18n.t('messages.welcome') 5 # Or shorthand: t('messages.welcome') 6 7 @greeting = t('messages.greeting', name: current_user.name) 8 9 # With default fallback 10 @title = t('page.title', default: 'Default Title') 11 12 # With scope 13 @user_email = t('email', scope: 'activerecord.attributes.user') 14 end 15end
View Helpers
ERB1<%# app/views/welcome/index.html.erb %> 2 3<h1><%= t('messages.welcome') %></h1> 4 5<%# Lazy lookup (relative to view path) %> 6<p><%= t('.intro') %></p> 7<%# Looks up: views.welcome.index.intro %> 8 9<%# With interpolation %> 10<p><%= t('messages.greeting', name: @user.name) %></p> 11 12<%# HTML-safe translations %> 13<p><%= t('messages.html_content').html_safe %></p> 14 15<%# Or mark as safe in YAML %> 16<%# messages.html_content_html: "<strong>Bold text</strong>" %> 17<p><%= t('messages.html_content_html') %></p>
Scoped Translations
RUBY1# Grouping related translations 2I18n.t('activerecord.errors.messages.blank') 3 4# Using scope option 5I18n.t('blank', scope: [:activerecord, :errors, :messages]) 6 7# In views with lazy lookup 8<%# views/products/show.html.erb %> 9<h1><%= t('.title') %></h1> 10<%# Resolves to: views.products.show.title %>
Pluralization
Rails supports complex pluralization rules for different languages.
Basic Pluralization
YAML1# config/locales/en.yml 2 3en: 4 inbox: 5 zero: "You have no messages" 6 one: "You have one message" 7 other: "You have %{count} messages"
Usage:
RUBY1# In controller or view 2t('inbox', count: 0) # "You have no messages" 3t('inbox', count: 1) # "You have one message" 4t('inbox', count: 5) # "You have 5 messages"
Complex Plural Forms
Languages like Polish or Russian have more plural forms:
YAML1# config/locales/pl.yml 2 3pl: 4 inbox: 5 zero: "Nie masz wiadomości" 6 one: "Masz jedną wiadomość" 7 few: "Masz %{count} wiadomości" 8 many: "Masz %{count} wiadomości" 9 other: "Masz %{count} wiadomości"
Rails uses the TwitterCldr or rails-i18n gem for CLDR-compliant pluralization rules.
Custom Pluralization Logic
RUBY1# lib/custom_pluralization.rb 2 3module I18n 4 module Backend 5 class Simple 6 def pluralize(locale, entry, count) 7 return entry unless entry.is_a?(Hash) 8 9 # Custom logic for special cases 10 if count == 0 && entry.has_key?(:zero) 11 entry[:zero] 12 elsif count == 1 13 entry[:one] 14 else 15 entry[:other] 16 end 17 end 18 end 19 end 20end
Date, Time, and Number Localization
Rails provides extensive localization for formatting dates, times, numbers, and currency.
Date and Time Formatting
YAML1# config/locales/en.yml 2 3en: 4 date: 5 formats: 6 default: "%Y-%m-%d" 7 short: "%b %d" 8 long: "%B %d, %Y" 9 day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday] 10 month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December] 11 12 time: 13 formats: 14 default: "%a, %d %b %Y %H:%M:%S %z" 15 short: "%d %b %H:%M" 16 long: "%B %d, %Y %H:%M"
RUBY1# Usage in views 2<%= l(Date.today) %> # Uses default format 3<%= l(Date.today, format: :short) %> # "Feb 12" 4<%= l(Date.today, format: :long) %> # "February 12, 2026" 5 6<%= l(Time.now) %> 7<%= l(Time.now, format: :short) %>
Number Formatting
RUBY1# In views or controllers 2number_to_currency(1234.56) # "$1,234.56" 3number_to_percentage(85.5) # "85.5%" 4number_to_delimited(1234567) # "1,234,567" 5number_to_human(1234567) # "1.23 Million" 6number_to_human_size(1234567890) # "1.15 GB" 7 8# With locale-specific formatting 9I18n.with_locale(:de) do 10 number_to_currency(1234.56, unit: "€") # "1.234,56 €" 11end
Configure in locale files:
YAML1# config/locales/es.yml 2 3es: 4 number: 5 currency: 6 format: 7 unit: "€" 8 precision: 2 9 separator: "," 10 delimiter: "." 11 format: "%n %u" 12 format: 13 separator: "," 14 delimiter: "." 15 precision: 2
Locale Detection and Switching
Implement automatic locale detection and manual switching.
Locale Detection Filter
RUBY1# app/controllers/application_controller.rb 2 3class ApplicationController < ActionController::Base 4 around_action :switch_locale 5 6 private 7 8 def switch_locale(&action) 9 locale = extract_locale || I18n.default_locale 10 I18n.with_locale(locale, &action) 11 end 12 13 def extract_locale 14 # Priority: URL param > User preference > Session > HTTP header 15 locale_from_params || 16 locale_from_user || 17 locale_from_session || 18 locale_from_headers 19 end 20 21 def locale_from_params 22 parsed = params[:locale] 23 I18n.available_locales.map(&:to_s).include?(parsed) ? parsed : nil 24 end 25 26 def locale_from_user 27 current_user&.locale if user_signed_in? 28 end 29 30 def locale_from_session 31 session[:locale] 32 end 33 34 def locale_from_headers 35 request.env['HTTP_ACCEPT_LANGUAGE']&.scan(/^[a-z]{2}/)&.first 36 end 37 38 def default_url_options 39 { locale: I18n.locale } 40 end 41end
Locale Switching Controller
RUBY1# app/controllers/locales_controller.rb 2 3class LocalesController < ApplicationController 4 def update 5 locale = params[:locale] 6 7 if I18n.available_locales.map(&:to_s).include?(locale) 8 session[:locale] = locale 9 current_user.update(locale: locale) if user_signed_in? 10 11 redirect_back(fallback_location: root_path, notice: t('locale.switched')) 12 else 13 redirect_back(fallback_location: root_path, alert: t('locale.invalid')) 14 end 15 end 16end
Routes:
RUBY1# config/routes.rb 2 3Rails.application.routes.draw do 4 post '/locale/:locale', to: 'locales#update', as: :switch_locale 5 6 # Optionally scope all routes with locale 7 scope "(:locale)", locale: /#{I18n.available_locales.join("|")}/ do 8 root to: 'home#index' 9 resources :products 10 end 11end
Locale Switcher View
ERB1<%# app/views/shared/_locale_switcher.html.erb %> 2 3<%= form_with url: switch_locale_path(locale: :placeholder), method: :post do %> 4 <%= select_tag :locale, 5 options_for_select(I18n.available_locales.map { |l| [t("locales.#{l}"), l] }, I18n.locale), 6 onchange: "this.form.action = this.form.action.replace('placeholder', this.value); this.form.submit();" %> 7<% end %>
ActiveRecord Translation
Translate database-backed model content using Globalize or Mobility gems.
Using Globalize
Install:
RUBY# Gemfile gem 'globalize', '~> 6.3'
Terminalbundle install rails generate globalize:migration CreateProductTranslations product name:string description:text rails db:migrate
Configure model:
RUBY1# app/models/product.rb 2 3class Product < ApplicationRecord 4 translates :name, :description, fallbacks_for_empty_translations: true 5end
Usage:
RUBY1# Create with translations 2product = Product.create( 3 name: 'Laptop', 4 description: 'High-performance laptop' 5) 6 7I18n.with_locale(:es) do 8 product.name = 'Portátil' 9 product.description = 'Portátil de alto rendimiento' 10 product.save 11end 12 13# Retrieve translations 14I18n.with_locale(:en) { product.name } # "Laptop" 15I18n.with_locale(:es) { product.name } # "Portátil" 16 17# Query translated attributes 18Product.with_translations(:es).where("product_translations.name LIKE ?", "%Port%")
Using Mobility
Modern alternative with better performance:
RUBY# Gemfile gem 'mobility', '~> 1.2'
RUBY1# app/models/product.rb 2 3class Product < ApplicationRecord 4 extend Mobility 5 6 translates :name, type: :string 7 translates :description, type: :text 8end
Terminalrails generate mobility:translations product rails db:migrate
Usage:
RUBY1product = Product.create(name: 'Laptop', description: 'High-performance laptop') 2 3Mobility.with_locale(:es) do 4 product.name = 'Portátil' 5 product.save 6end 7 8product.name(locale: :es) # "Portátil" 9product.name(locale: :en) # "Laptop"
Route Localization
Translate URL paths for better SEO and UX.
Using route_translator Gem
RUBY# Gemfile gem 'route_translator'
RUBY1# config/routes.rb 2 3Rails.application.routes.draw do 4 localized do 5 resources :products 6 get 'about', to: 'pages#about' 7 end 8end
Translation file:
YAML1# config/locales/routes.es.yml 2 3es: 4 routes: 5 products: productos 6 about: acerca-de
Generated routes:
/en/products→/es/productos/en/about→/es/acerca-de
URL Helpers with Locale
RUBY1# Generate localized URLs 2products_path(locale: :es) # "/es/productos" 3product_path(@product, locale: :fr) # "/fr/produits/123" 4 5# In views 6<%= link_to t('menu.products'), products_path %>
ActionMailer Internationalization
Localize email content based on recipient preferences.
Mailer with I18n
RUBY1# app/mailers/user_mailer.rb 2 3class UserMailer < ApplicationMailer 4 def welcome_email(user) 5 @user = user 6 7 I18n.with_locale(user.locale || I18n.default_locale) do 8 mail( 9 to: user.email, 10 subject: t('mailers.user.welcome.subject', name: user.name) 11 ) 12 end 13 end 14end
Email template:
ERB1<%# app/views/user_mailer/welcome_email.html.erb %> 2 3<h1><%= t('.greeting', name: @user.name) %></h1> 4 5<p><%= t('.intro') %></p> 6 7<%= link_to t('.cta'), root_url(locale: @user.locale) %>
Translation file:
YAML1# config/locales/en.yml 2 3en: 4 mailers: 5 user: 6 welcome: 7 subject: "Welcome to MyApp, %{name}!" 8 9 user_mailer: 10 welcome_email: 11 greeting: "Hello, %{name}!" 12 intro: "Thanks for signing up." 13 cta: "Get Started"
JavaScript Internationalization
Expose Rails translations to frontend JavaScript.
Using i18n-js Gem
RUBY# Gemfile gem 'i18n-js', '~> 4.0'
Export translations:
JavaScript1// app/javascript/i18n.js 2 3import { I18n } from "i18n-js" 4import translations from "../locales/translations.json" 5 6const i18n = new I18n(translations) 7i18n.defaultLocale = "en" 8i18n.locale = document.documentElement.lang || "en" 9 10export default i18n
Usage:
JavaScript1import i18n from './i18n' 2 3console.log(i18n.t('messages.welcome')) 4console.log(i18n.t('messages.greeting', { name: 'Alice' })) 5console.log(i18n.l(new Date(), { format: 'short' }))
IntlPull YAML Sync
Integrate Rails with IntlPull for team translation workflows.
Setup
Install IntlPull CLI:
Terminalgem install intlpull # Or add to Gemfile gem 'intlpull'
Configure:
YAML1# .intlpull.yml 2 3project_id: your-project-id 4api_key: <%= ENV['INTLPULL_API_KEY'] %> 5 6sync: 7 format: yaml 8 source_locale: en 9 target_locales: 10 - es 11 - fr 12 - de 13 14 import: 15 path: config/locales/{locale}.yml 16 17 export: 18 path: config/locales/{locale}.yml
Sync Commands
Terminal1# Upload translations to IntlPull 2intlpull push --locale es 3 4# Download updated translations 5intlpull pull --locale es --all 6 7# Sync all locales 8intlpull sync
CI/CD Integration
YAML1# .github/workflows/translations.yml 2 3name: Sync Translations 4 5on: 6 schedule: 7 - cron: '0 2 * * *' 8 workflow_dispatch: 9 10jobs: 11 sync: 12 runs-on: ubuntu-latest 13 steps: 14 - uses: actions/checkout@v3 15 16 - uses: ruby/setup-ruby@v1 17 with: 18 ruby-version: '3.2' 19 bundler-cache: true 20 21 - name: Pull translations 22 env: 23 INTLPULL_API_KEY: ${{ secrets.INTLPULL_API_KEY }} 24 run: bundle exec intlpull pull --all 25 26 - name: Create PR 27 uses: peter-evans/create-pull-request@v5 28 with: 29 commit-message: 'chore: update translations' 30 title: 'Update translations from IntlPull'
Testing I18n
Ensure translations work correctly.
RSpec Tests
RUBY1# spec/features/i18n_spec.rb 2 3require 'rails_helper' 4 5RSpec.describe 'Internationalization', type: :feature do 6 it 'displays content in Spanish' do 7 visit root_path(locale: :es) 8 9 expect(page).to have_content('Bienvenido') 10 end 11 12 it 'switches locale via selector' do 13 visit root_path 14 15 select 'Español', from: 'locale' 16 17 expect(page).to have_current_path('/es') 18 expect(page).to have_content('Bienvenido') 19 end 20end
Translation Coverage Test
RUBY1# spec/lib/translation_coverage_spec.rb 2 3require 'rails_helper' 4 5RSpec.describe 'Translation coverage' do 6 it 'has translations for all supported locales' do 7 base_translations = I18n.t('', locale: :en) 8 9 [:es, :fr, :de].each do |locale| 10 locale_translations = I18n.t('', locale: locale) 11 12 expect(locale_translations.keys).to match_array(base_translations.keys) 13 end 14 end 15end
Best Practices
1. Use Lazy Lookup in Views
ERB1<%# ❌ Bad %> 2<%= t('views.products.show.title') %> 3 4<%# ✅ Good %> 5<%= t('.title') %>
2. Organize by Feature
config/locales/
├── models/
│ ├── en.yml
│ └── es.yml
├── views/
│ ├── en.yml
│ └── es.yml
└── mailers/
├── en.yml
└── es.yml
3. Use Raise on Missing Translations
RUBY# config/environments/development.rb config.i18n.raise_on_missing_translations = true
4. Avoid Inline Defaults
RUBY1# ❌ Bad - hides missing translations 2t('key', default: 'Hardcoded text') 3 4# ✅ Good - add to YAML 5t('key') # Raises error if missing in dev
Production Deployment
Pre-Deployment Checklist
- All YAML files valid syntax
- Translation coverage > 95%
- Fallback locale configured
- Missing translation errors disabled in production
- I18n backend configured (Redis for large apps)
- Translations loaded eagerly
Deployment Script
Terminal1#!/bin/bash 2 3# Precompile assets 4RAILS_ENV=production rails assets:precompile 5 6# Validate translations 7RAILS_ENV=production rails runner 'I18n.t("test")' 8 9# Run migrations 10RAILS_ENV=production rails db:migrate 11 12# Restart server 13systemctl restart puma
Frequently Asked Questions
Q: Should I use symbols or strings for locale keys?
A: Rails accepts both (:en or "en"), but symbols are conventional and slightly more performant. Use symbols in code, strings in YAML.
Q: How do I handle user-generated content translation?
A: Use Globalize or Mobility for ActiveRecord models. Store translations in separate tables with locale columns. Query with .with_translations(locale).
Q: Can I split YAML files by feature?
A: Yes, Rails loads all .yml files from config/locales/ and subdirectories. Organize by feature, model, or view namespace.
Q: How do I translate validation errors?
A: Rails automatically uses translations from activerecord.errors.models.{model}.attributes.{attribute}. Use rails-i18n gem for pre-translated errors.
Q: What's the performance impact of I18n?
A: Minimal with YAML backend. For high-traffic apps, use Redis backend or precompile translations to JavaScript for frontend-heavy apps.
Q: How do I handle RTL languages?
A: Detect RTL locales (:ar, :he) in layout and add dir="rtl". Use CSS logical properties. Consider rtl-css-js for automatic stylesheet flipping.
Q: Can I use Rails I18n with React/Vue?
A: Export translations to JSON via custom Rake task or use react-i18next with Rails API. IntlPull provides API endpoints for dynamic translation loading.
Q: How do I test email translations?
A: Use ActionMailer::TestHelper and set locale before calling mailer. Assert on email subject/body content with translated strings.
IntlPull streamlines Rails i18n workflows with automated YAML sync, collaborative translation editing, and version control for locale files. Whether building a new Rails application or internationalizing an existing project, proper i18n configuration ensures your application serves global audiences effectively. Start with Rails' built-in I18n API, integrate IntlPull for team collaboration, and follow best practices for maintainable multilingual Ruby applications.
