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

  1. You open a pull request that adds keys to a source file, for example public/locales/en/common.json.
  2. The GitHub Action diffs the branch against its base and picks up the new keys. Keys that already have a translation are left alone.
  3. It translates them with your project's glossary, style guide and translation memory.
  4. 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.
  5. It commits the updated JSON files to the same pull request. The diff you review holds both the code and its translations.
  6. Reviewers edit translations on Localhero.ai, no GitHub account needed. Sync to GitHub sends their edits back as a Sync translations commit 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

Last updated

Klar til å prøve?

Kom i gang på noen minutter. Du trenger ikke kredittkort.