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:
{
"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:
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
- You open a pull request that adds or changes keys in your source locale file, for example
config/locales/en.yml. - 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.
- It commits the updated YAML files to the same PR, so the diff you review contains both the code and its translations.
- 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:
"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
Ready to try it?
Get setup in a couple of minutes. No credit card required.