Ruby on Rails

Localhero.ai translates Rails i18n YAML files on every pull request. When you add keys to config/locales, the GitHub Action translates the ones that are new or missing and commits the updated locale files back to the same PR, so you always have on-brand translations ready to deploy with each PR.

Prerequisites

  • A Rails app with locale files in config/locales/
  • Node 18 or later, to run the CLI through npx
  • A Localhero.ai API key

Get an API key from your account.

Setup

1. Initialize the project

Run the CLI in your project root. No install step, and it detects the Rails folder structure and setup config:

npx @localheroai/cli init

It proposes config/locales/ with a **/*.{yml,yaml} pattern, then asks for your source and target locales.

2. Check the generated config

init writes a localhero.json in your project root. A config can look like this:

localhero.json
{
  "schemaVersion": "1.0",
  "projectId": "your-project-id",
  "sourceLocale": "en",
  "outputLocales": ["sv", "nb", "de", "es"],
  "translationFiles": {
    "paths": ["config/locales/"],
    "pattern": "**/*.{yml,yaml}"
  }
}

Commit this file. Fields are documented in Project Setup.

3. Add the GitHub Action workflow

init offers to create the workflow for you. Add your API key as the repository secret LOCALHERO_API_KEY on GitHub and new keys get translated on every PR. It runs localheroai/localhero-action. To set it up by hand, this is the file, with the paths: filter set to the Rails locale directory:

.github/workflows/localhero-translate.yml
name: Localhero.ai - Automatic I18n translation

on:
  pull_request:
    paths:
      - "config/locales/**"
      - "localhero.json"
  repository_dispatch:
    types: [localhero-sync]
  workflow_dispatch:

concurrency:
  group: translate-${{ github.event.client_payload.branch || github.head_ref || github.run_id }}
  cancel-in-progress: true

jobs:
  translate:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v5
        with:
          ref: ${{ github.event.client_payload.branch || github.head_ref || github.ref_name }}
          fetch-depth: 0

      - uses: localheroai/localhero-action@v1
        with:
          api-key: ${{ secrets.LOCALHERO_API_KEY }}

fetch-depth: 0 is important: the Action diffs against the base branch, and a shallow checkout has nothing to compare. See the GitHub Actions guide for all inputs.

How it works

  1. You open a pull request that adds or changes keys in your source locale file, for example config/locales/en.yml.
  2. The GitHub Action diffs the branch against your base branch and translates only the keys that changed or are missing. Updated and removed keys are synced to the Localhero.ai backend, so the web interface stays in sync with your codebase and PRs.
  3. It commits the updated YAML files to the same PR, so the diff you review contains both the code and its translations.
  4. You review and edit the translations in the dashboard, and edits sync back to the branch.

Nothing in your app has to know Localhero.ai exists. The locale YAML file is the integration, so I18n.t calls and your view code stay as they are.

Rather not automatically translate on PRs? Skip the Action and run npx @localheroai/cli translate locally to fill in every missing key, then commit the result yourself. See the CLI reference.

Rails specifics

Split and nested locale files

Files split by concern, like devise.en.yml or en/models.yml, work without configuration. The **/*.{yml,yaml} pattern is recursive, and the locale is read from the filename or path: basename first, then a path segment, then an _ or - suffix, then the parent directory.

Matching expects lowercase xx or xx-XX codes. For anything else, set translationFiles.localeRegex.

Keys you never want translated

Use ignoreKeys for internal or admin-facing copy. It takes exact keys or a trailing wildcard, and applies on both translate and push:

localhero.json (excerpt)
"translationFiles": {
  "paths": ["config/locales/"],
  "pattern": "**/*.{yml,yaml}",
  "ignoreKeys": ["admin.*", "internal.debug_banner"]
}

To skip whole files instead of keys, use ignore with a glob, as in the config above.

Renaming and removing keys

In the pull request flow this takes care of itself: keys removed in the diff are reported to the backend along with the rest of the changes. The manual push command is more conservative and never deletes, so after renaming or removing keys outside a PR, run push --prune --force to make the remote side match your files.

Plurals

Translations come back in each target language's own CLDR plural categories rather than a copy of the source language's set. A language like Polish gets its extra plural forms even when your English source only has one and other.

One file holding several locales

Everything above assumes one locale per file. If your YAML files instead hold several locales as top-level keys, turn on translationFiles.multiLanguageFiles. Beta, YAML and JSON only. A file qualifies when every top-level key is a configured locale and at least two are present.

Questions we get

Can I translate some languages myself?

Yes. Turn off Auto-translate for that language in your project settings, and Localhero.ai writes nothing for it. The CLI reports those languages as skipped, and you keep them under your own process while the rest are translated automatically.

Will it translate my whole locale file on every PR?

No, only keys that changed or are missing. On a pull request the diff is against the PR's base branch. Locally, --changed-only compares against main unless translationFiles.baseBranch says otherwise.

How do I stop it running on a specific PR?

Add the skip-translation label. The Action also skips draft PRs and its own commits. The details are in the GitHub Actions guide.

Longer walkthrough with a real codebase: localizing a Ruby on Rails app.

Last updated

Redo att testa?

Kom igång på några minuter. Inget kreditkort behövs.