Localize a Ruby on Rails app: complete guide with Localingos
Rails' i18n system (i18n gem, ships built in) is the gold standard for Ruby web app localization — YAML files, interpolation, pluralization, scoped lookups, all in the core framework. The gap is the translation workflow: who maintains es.yml, de.yml, fr.yml as English copy evolves? This guide pairs Rails' built-in i18n with Localingos to automate the translation step.
Step 1 — Install
npm install -g @localingos/cli
localingos login
The Rails i18n gem is included by default — no Gemfile changes needed.
Step 2 — Configure locales
config/application.rb:
module YourApp
class Application < Rails::Application
config.i18n.default_locale = :en
config.i18n.available_locales = [:en, :es, :de, :fr, :ja, :'pt-BR', :'zh-CN', :ko]
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.yml')]
config.i18n.fallbacks = true
end
end
fallbacks = true means missing keys silently fall through to English instead of raising — good for production safety.
Step 3 — Source of truth
config/locales/en.yml:
en:
welcome: "Welcome, %{name}"
cart:
zero: "Your cart is empty"
one: "1 item in your cart"
other: "%{count} items in your cart"
errors:
not_found: "We couldn't find what you were looking for"
Rails interpolation uses %{var} syntax; pluralization uses the zero/one/other keys under a scope.
Step 4 — Configure Localingos
localingos.json in project root:
{
"projectId": "your-project-id",
"source": {
"path": "config/locales/en.yml",
"locale": "en",
"format": "yaml"
},
"targets": {
"path": "config/locales/{locale}.yml",
"locales": ["es", "de", "fr", "ja", "pt-BR", "zh-CN", "ko"]
},
"placeholders": ["%{variable}"]
}
Two Rails-specific things:
format: "yaml"— Localingos parses Rails-style YAML (with the top-level locale key) and writes the target files with the correct top-level key per locale.%{var}placeholder syntax — make sure yourplaceholdersconfig matches Rails's interpolation pattern, otherwise validation will flag every variable.
localingos sync
All target .yml files appear in config/locales/. Commit them.
Step 5 — Use in views
<header>
<h1><%= t('welcome', name: current_user.name) %></h1>
<p><%= t('cart', count: cart.item_count) %></p>
</header>
Rails picks the right plural variant from your YAML based on the count: argument.
Step 6 — Use in controllers and models
class OrdersController < ApplicationController
def create
@order = Order.new(order_params)
if @order.save
flash[:notice] = t('orders.created')
redirect_to @order
else
flash[:alert] = t('orders.create_failed')
render :new
end
end
end
For ActiveRecord validation messages, Rails has dedicated YAML scopes:
en:
activerecord:
attributes:
user:
email: "Email"
errors:
models:
user:
attributes:
email:
blank: "is required"
taken: "is already in use"
Localingos preserves these nested scopes exactly when translating.
Step 7 — URL routing per locale
config/routes.rb:
Rails.application.routes.draw do
scope "(:locale)", locale: /es|de|fr|ja|pt-BR|zh-CN|ko/ do
resources :products
get 'pricing', to: 'pages#pricing'
get 'docs', to: 'pages#docs'
end
root 'pages#home'
end
ApplicationController:
class ApplicationController < ActionController::Base
before_action :set_locale
def set_locale
I18n.locale = params[:locale] || extract_locale_from_accept_language || I18n.default_locale
end
def default_url_options
{ locale: I18n.locale == I18n.default_locale ? nil : I18n.locale }
end
private
def extract_locale_from_accept_language
request.env['HTTP_ACCEPT_LANGUAGE']&.scan(/^[a-z]{2}/)&.first if I18n.available_locales.map(&:to_s).include?(request.env['HTTP_ACCEPT_LANGUAGE']&.scan(/^[a-z]{2}/)&.first)
end
end
Now /pricing is English, /es/pricing is Spanish. default_url_options ensures generated links preserve the current locale.
Step 8 — hreflang in layouts
app/views/layouts/application.html.erb:
<head>
<%= csrf_meta_tags %>
<link rel="canonical" href="<%= canonical_url %>">
<% I18n.available_locales.each do |loc| %>
<link rel="alternate" hreflang="<%= loc %>" href="<%= alternate_url_for(loc) %>">
<% end %>
<link rel="alternate" hreflang="x-default" href="<%= canonical_url %>">
</head>
Helper methods in application_helper.rb:
def canonical_url
url_for(only_path: false, locale: nil)
end
def alternate_url_for(locale)
url_for(only_path: false, locale: locale == I18n.default_locale ? nil : locale)
end
Step 9 — Automate sync in CI
# .github/workflows/i18n.yml
name: i18n-sync
on:
push: { branches: [main], paths: ['config/locales/en.yml'] }
jobs:
sync:
runs-on: ubuntu-latest
permissions: { contents: write, pull-requests: write }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm install -g @localingos/cli
- run: localingos sync
env: { LOCALINGOS_API_KEY: '${{ secrets.LOCALINGOS_API_KEY }}' }
- uses: peter-evans/create-pull-request@v6
with:
branch: i18n/auto-sync
title: 'chore(i18n): sync translations'
commit-message: 'chore(i18n): sync translations'
Production checklist
- YAML files in git, not generated at deploy time. Translations should be visible in PRs.
I18n.tvsI18n.t!. Uset!in tests to fail loudly on missing keys; usetin production for graceful fallback.- Locale-aware date/number formatting. Rails has
l(Time.now)for dates andnumber_to_currencyfor money — both use the active locale. - Mailer i18n.
class WelcomeMailer < ApplicationMailer— wrap mailer actions inI18n.with_locale(user.locale) { ... }so transactional emails go out in the recipient's language.
Wrap up
Rails' i18n + Localingos = a multilingual Rails app with locale-prefixed routes, hreflang, ActiveRecord error messages translated, and a CI-driven translation pipeline. The setup follows Rails conventions exactly — no monkey patches, no replacement libraries.
Free tier covers typical Rails monolith string corpora.