i18next
Localhero.ai is a lightweight TMS that lives in your pull requests. Your i18next JSON files stay in Git as the source of truth. When a pull request adds keys to your source language, the GitHub Action translates them and commits the results to the same PR. Code and translations are reviewed and merged together. Reviewers can edit translations in the browser; their edits are committed back to the branch. There is no SDK, runtime backend or CDN to add: i18next keeps loading the files it already loads.
Prerequisites
- An i18next project with JSON resource files. react-i18next, next-i18next and i18next on Node all read the same files.
- 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:
npx @localheroai/cli init
It recognizes an i18next project from the i18next dependency in package.json or an i18next.config.js or i18n.js file. It then proposes public/locales/ with the pattern **/*.json. If your files live elsewhere, it also checks src/locales, locales, src/i18n and i18n, or you can point it at any directory during init.
2. Check the generated config
init writes a localhero.json in your project root. For namespaced files under public/locales/ with English as the source:
localhero.json
{
"schemaVersion": "1.0",
"projectId": "your-project-id",
"sourceLocale": "en",
"outputLocales": ["de", "fr", "sv"],
"translationFiles": {
"paths": ["public/locales/"],
"pattern": "**/*.json"
}
}
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. To set it up by hand, this is the file:
.github/workflows/localhero-translate.yml
name: Localhero.ai - Automatic I18n translation
on:
pull_request:
paths:
- "public/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@v7
with:
ref: ${{ github.event.client_payload.branch || github.head_ref || github.ref_name }}
fetch-depth: 0
persist-credentials: false
- uses: localheroai/localhero-action@v1
with:
api-key: ${{ secrets.LOCALHERO_API_KEY }}
fetch-depth: 0 is important: the Action diffs against the base branch. A shallow checkout has nothing to compare. The paths: filter points at your locale directory; change it if yours is not public/locales/. All inputs are in the GitHub Actions guide.
How it works
- You open a pull request that adds keys to a source file, for example
public/locales/en/common.json. - The GitHub Action diffs the branch against its base and picks up the new keys. Keys that already have a translation are left alone.
- It translates them with your project's glossary, style guide and translation memory.
- It validates the output. Interpolation variables such as
{{name}}must match the source. Arrays and objects must keep their shape. A translation that fails is retranslated once. If it still fails, it never reaches your files. - It commits the updated JSON files to the same pull request. The diff you review holds both the code and its translations.
- Reviewers edit translations on Localhero.ai, no GitHub account needed. Sync to GitHub sends their edits back as a
Sync translationscommit on the same branch.
Nothing in your app has to change. Your i18n.init options, how you load resources (bundled imports or i18next-http-backend reading from /locales), and your t() calls stay as they are.
Rather not translate on pull requests? 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.
Your i18next files
Layouts
Any layout the glob matches works, including new namespace files added later:
| Layout | Example |
|---|---|
| One file per language | locales/en.json, locales/de.json |
| A folder per language, a file per namespace | public/locales/en/common.json, public/locales/de/common.json |
| A folder per namespace | locales/common/en.json, locales/common/de.json |
How the language of a file is worked out
The language is read from the filename or path: basename first, then a path segment, then an _ or - suffix, then the parent directory. That covers all three layouts above.
Matching expects lowercase xx or xx-XX codes such as pt-BR. A name like pt_BR.json still resolves when that language is configured, since configured languages are matched before the pattern. For anything else, set translationFiles.localeRegex.
Nested and flat keys
Nested objects and flat keys with dots in them (keySeparator: false) both work. Each file is written back in the shape it already has. Keys you did not touch keep their position.
Arrays, objects and other values
Arrays and objects you read with returnObjects: true are translated as one unit and must come back with the same length and keys. Source values that are numbers, booleans or empty strings are copied as they are.
Keys you never want translated
Use ignoreKeys for internal or admin-facing copy. It takes exact keys or a trailing wildcard:
localhero.json (excerpt)
"translationFiles": {
"paths": ["public/locales/"],
"pattern": "**/*.json",
"ignoreKeys": ["admin.*", "internal.debugBanner"]
}
To skip whole files instead of keys, use ignore with a glob.
Existing translations
Translations already in your files are kept. Only missing and empty ones are filled in. To bring an existing project in, see Migrating an existing project.
Questions we get
Do I need an i18next backend or SDK?
No. Localhero.ai works on the JSON files in your repository during CI. Nothing of ours runs in your app. The files you ship are plain i18next resources.
Will it translate my whole catalog on every pull request?
No, only new keys and keys missing a translation. On a pull request the diff is against the PR's base branch. Locally, --changed-only compares against main unless translationFiles.baseBranch says otherwise.
Does it work with next-i18next and react-i18next?
Yes. Both read the same JSON resources. init detects a next-i18next project and proposes public/locales/ for it too.
Can I translate some languages myself?
Yes. Turn off Auto-translate for that language in your project settings. Localhero.ai then writes nothing for it. The CLI reports those languages as skipped.
How do I stop it running on a specific pull request?
Add the skip-translation label. The Action also skips draft pull requests and its own commits. The details are in the GitHub Actions guide.
Further reading
- React: the same setup for next-intl, react-intl and vue-i18n
- Git workflow: what the translation commit contains, signed commits, and how it interacts with CI
- Localizing a React and Next.js app: a longer walkthrough with react-i18next
- Translation management systems for i18next: the i18next team's guide to choosing a TMS
Last updated
Klar til å prøve?
Kom i gang på noen minutter. Du trenger ikke kredittkort.