IntlPull
Tutorial
13 min read

Ruby on Rails i18n: Complete Localization Guide for 2026

Master Rails internationalization with I18n API, YAML locale files, t() helper, pluralization, ActiveRecord translations, and testing. Complete Ruby i18n guide.

IntlPull Team
IntlPull Team
Feb 12, 2026
On this page
Summary

Master Rails internationalization with I18n API, YAML locale files, t() helper, pluralization, ActiveRecord translations, and testing. Complete Ruby i18n guide.

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:

  1. Check current locale (I18n.locale)
  2. Look up key in backend (e.g., en.hello in YAML files)
  3. If not found, check fallback locale
  4. If still not found, return default or missing translation message
  5. Interpolate any variables
  6. 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

RUBY
1# 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:

Terminal
mkdir -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

YAML
1# 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:

YAML
1# 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

RUBY
1# 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

ERB
1<%# 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

RUBY
1# 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

YAML
1# 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:

RUBY
1# 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:

YAML
1# 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

RUBY
1# 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

YAML
1# 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"
RUBY
1# 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

RUBY
1# 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:

YAML
1# 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

RUBY
1# 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

RUBY
1# 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:

RUBY
1# 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

ERB
1<%# 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'
Terminal
bundle install
rails generate globalize:migration CreateProductTranslations product name:string description:text
rails db:migrate

Configure model:

RUBY
1# app/models/product.rb
2
3class Product < ApplicationRecord
4  translates :name, :description, fallbacks_for_empty_translations: true
5end

Usage:

RUBY
1# 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'
RUBY
1# app/models/product.rb
2
3class Product < ApplicationRecord
4  extend Mobility
5  
6  translates :name, type: :string
7  translates :description, type: :text
8end
Terminal
rails generate mobility:translations product
rails db:migrate

Usage:

RUBY
1product = 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'
RUBY
1# 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:

YAML
1# 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

RUBY
1# 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

RUBY
1# 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:

ERB
1<%# 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:

YAML
1# 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:

JavaScript
1// 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:

JavaScript
1import 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:

Terminal
gem install intlpull
# Or add to Gemfile
gem 'intlpull'

Configure:

YAML
1# .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

Terminal
1# 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

YAML
1# .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

RUBY
1# 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

RUBY
1# 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

ERB
1<%# ❌ 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

RUBY
1# ❌ 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

Terminal
1#!/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.

Tags
rails
ruby
i18n
localization
yaml
backend
web-framework
IntlPull Team
IntlPull Team
Engineering

Building tools to help teams ship products globally. Follow us for more insights on localization and i18n.