|
Documentation
EnterprisePlatform
PlatformAPIReact (MCP)CLIIntegrationsReact (Lingo Compiler)
Alpha
Guides
Changelog

Localization

  • Overview
  • Translation API
  • Web App Localization
  • Mobile App Localization
  • iOS with String Catalogs
  • Android with strings.xml
  • Emails Localization
  • Static Content (e.g. .md, .json)
  • Next.js with Markdoc
  • Rails with i18n

Workflows

  • Engine Setup with MCP
  • CI/CD

Ruby on Rails Localization with the i18n API

Max PrilutskiyMax Prilutskiy·Updated 1 day ago·5 min read

The Lingo.dev CLI translates Rails config/locales YAML files through a configured localization engine. Rails ships with the i18n API baked in – your app's translatable text lives in per-locale YAML files. Lingo.dev fits the existing pipeline without adding a runtime dependency.

This guide walks through localizing a Rails app end-to-end: configuring the CLI, organizing per-locale YAML files, switching locales at request time, and automating translations with GitHub Actions.

Demo repository

Clone or fork lingodotdev/ruby-on-rails-localization-example to follow along. The repository contains a working Rails app with config/locales YAML files, a Lingo.dev CLI configuration, and a GitHub Actions workflow.

How Rails Localization Works#

Rails reads translations from YAML files under config/locales/. Each file is keyed by a locale code at the root and contains nested keys that mirror the lookup paths your code uses with I18n.t.

LayerWhat lives thereExample file
UI stringsButtons, labels, flash messagesconfig/locales/en.yml
Mailer copySubjects and bodies for ActionMailerconfig/locales/mailers.en.yml
Model errorsValidation messages and attribute namesconfig/locales/activerecord.en.yml

The first key of every Rails YAML file is the locale code itself – en:, es:, fr:. The CLI's yaml-root-key bucket understands this shape: it strips the locale prefix before sending content to your localization engine, then writes a parallel file with the target locale code as the new root key. Nested keys, %{name} interpolation tokens, and CLDR plural categories (zero/one/two/few/many/other) are preserved.

Prerequisites#

1

Create a localization engine

Every CLI run sends content through a localization engine – the configuration that determines which LLM model, glossary, brand voice, and instructions apply. Create one in the Lingo.dev dashboard and generate an API key.

2

Verify Ruby and Rails

This guide targets Rails 7.2 or higher, which requires Ruby 3.1 or higher. Check your versions:

bash
ruby -v
rails -v
3

Verify Node.js

The CLI requires Node.js 18 or higher:

bash
node -v
4

Set up Rails i18n

This guide assumes your app already stores translations in config/locales/*.yml. If you have hardcoded strings in views or controllers, extract them into t() calls first. For example, replace:

erb
<h1>Welcome</h1>

with:

erb
<h1><%= t(".welcome") %></h1>

then add the matching key to config/locales/en.yml. See Rails internationalization guide for the full migration steps.

Organize Translation Files#

Rails auto-loads every *.yml file under config/locales/. Keep the source locale next to its translated siblings so the directory acts as the single source of truth:

text
config/locales/
  en.yml          # Source locale
  es.yml          # Generated by Lingo.dev
  fr.yml
  de.yml

A typical en.yml mixes plain strings, nested namespaces, %{name} interpolation, and pluralization:

yaml
en:
  hello: "Hello"
  home:
    welcome: "Welcome, %{name}!"
    cta: "Get started"
  notifications:
    unread:
      zero: "No unread notifications"
      one: "1 unread notification"
      other: "%{count} unread notifications"
  errors:
    messages:
      blank: "can't be blank"

Configure the CLI#

Create an i18n.json file in your project root. Declare a yaml-root-key bucket pointing at the locale files:

json
{
  "$schema": "https://lingo.dev/schema/i18n.json",
  "version": "1.15",
  "locale": {
    "source": "en",
    "targets": ["es", "fr", "de"]
  },
  "buckets": {
    "yaml-root-key": {
      "include": ["config/locales/[locale].yml"]
    }
  }
}

The [locale] placeholder resolves to each configured locale code. With source: "en", the CLI reads config/locales/en.yml and writes translated files to config/locales/es.yml, config/locales/fr.yml, and config/locales/de.yml.

Rails happily loads mailers.en.yml, pages.en.yml, and so on alongside en.yml. Add additional patterns to the include array of your yaml-root-key bucket:

json
{
  "buckets": {
    "yaml-root-key": {
      "include": [
        "config/locales/[locale].yml",
        "config/locales/mailers.[locale].yml",
        "config/locales/pages.[locale].yml"
      ]
    }
  }
}

Configure Rails for Multiple Locales#

Tell Rails which locales are available and which one to use as the default. In config/application.rb:

ruby
module YourApp
  class Application < Rails::Application
    config.i18n.available_locales = [:en, :es, :fr, :de]
    config.i18n.default_locale = :en
    config.i18n.fallbacks = [:en]
  end
end

Pick the request locale in ApplicationController from a URL parameter or the Accept-Language header:

ruby
class ApplicationController < ActionController::Base
  around_action :switch_locale

  private

  def switch_locale(&action)
    locale = params[:locale] || http_accept_locale || I18n.default_locale
    I18n.with_locale(locale, &action)
  end

  def http_accept_locale
    header = request.headers["Accept-Language"].to_s
    header.scan(/[a-z]{2}/).find { |l| I18n.available_locales.map(&:to_s).include?(l) }
  end

  def default_url_options
    { locale: I18n.locale }
  end
end

Render Translations in Views#

Use t and l helpers in ERB templates. A leading dot in the key resolves against the current view path, keeping translation keys colocated with the templates that use them:

erb
<h1><%= t(".welcome", name: @user_name) %></h1>
<p><%= t("notifications.unread", count: @unread_count) %></p>
<%= link_to t(".cta"), signup_path, class: "btn-primary" %>

Add a locale switcher to your layout:

erb
<nav>
  <% I18n.available_locales.each do |locale| %>
    <%= link_to locale.upcase, url_for(locale: locale) %>
  <% end %>
</nav>

Translate Locally#

Set your API key and run the CLI:

bash
export LINGO_API_KEY="your-api-key"
npx lingo.dev@latest run

The CLI reads every file matching your bucket patterns, identifies untranslated entries using the lockfile, translates the delta through your localization engine, and writes results into each target locale file. The locale root key, nested namespaces, %{name}-style interpolation tokens, and plural categories are preserved – only translatable text changes.

To target a specific locale during development:

bash
npx lingo.dev@latest run --target-locale es

Restart the Rails server after the first translation run so the new YAML files load:

bash
bin/rails server

Visit /es to see the Spanish output.

Plurals#

Rails uses CLDR plural categories – zero, one, two, few, many, other. Pass a count: argument to I18n.t and Rails picks the matching key:

ruby
t("notifications.unread", count: 0)   # => "No unread notifications"
t("notifications.unread", count: 1)   # => "1 unread notification"
t("notifications.unread", count: 12)  # => "12 unread notifications"

The CLI translates each plural variant in place. If your target locale needs more categories than English's one/other, define them in your source en.yml.

Automate with GitHub Actions#

Add a workflow file at .github/workflows/translate.yml to translate on every push:

Translations commit directly to main – zero friction, ideal for small teams:

yaml
name: Translate
on:
  push:
    branches: [main]
permissions:
  contents: write
jobs:
  translate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Lingo.dev
        uses: lingodotdev/lingo.dev@main
        with:
          api-key: ${{ secrets.LINGODOTDEV_API_KEY }}

Store your API key as LINGODOTDEV_API_KEY in Settings > Secrets and variables > Actions in your GitHub repository.

Verify Before Deploy#

Use the --frozen flag as a deployment gate to ensure no untranslated content ships to production. The CLI exits with a non-zero status if any entries need translation:

bash
npx lingo.dev@latest run --frozen

Add this as a separate CI step before your asset precompile or container build:

yaml
- name: Verify translations
  run: npx lingo.dev@latest run --frozen
- name: Precompile assets
  run: bundle exec rails assets:precompile

Next Steps#

Static Content Localization
Markdown, MDX, JSON, YAML, and more bucket types
Web App Localization
UI string patterns across common web frameworks
CI/CD Workflows
GitHub Actions, GitLab CI, Bitbucket Pipelines patterns
Glossaries
Lock brand names and technical terms from translation

Was this page helpful?