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 your placeholders config 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.t vs I18n.t!. Use t! in tests to fail loudly on missing keys; use t in production for graceful fallback.
  • Locale-aware date/number formatting. Rails has l(Time.now) for dates and number_to_currency for money — both use the active locale.
  • Mailer i18n. class WelcomeMailer < ApplicationMailer — wrap mailer actions in I18n.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.