# General Translation Knowledge Base: How do I translate i18n JSON files?
URL: https://generaltranslation.com/en-GB/kb/translate-json-files.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: Translate only the string values in a JSON locale file. Keep nonplural keys, placeholders such as {{name}}, and tags unchanged, but expect plural keys to differ by language: Polish needs inbox_few and inbox_many. Use a format-aware tool or API, then parse and validate the output before review.
Category: Translation
Last updated: 2026-10-20
Authors: Kevin Wu, Jackie Chen
Published: 2026-10-20

This answer covers the locale files that i18n libraries such as i18next, react-intl, next-intl, and vue-i18n load, where each key maps to a UI string. Arbitrary data JSON (API responses, product catalogues) needs a different approach, because there you choose field by field what to translate.

## What JSON locale files look like

Most locale files are nested, grouping keys into objects as below. A flat file writes the same path as one dotted key, such as `"nav.home": "Home"`.

```json title="locales/en/common.json"
{
  "nav": { "home": "Home", "save": "Save" },
  "greeting": "Hello, {{name}}!",
  "inbox_one": "You have {{count}} new message",
  "inbox_other": "You have {{count}} new messages",
  "terms": "Read our <1>terms</1> before you continue."
}
```

The string syntax inside depends on the library, and a translation has to keep it intact:

* **i18next** interpolates with double braces (`{{name}}`) and stores plurals as separate keys with [suffixes](https://www.i18next.com/translation-function/plurals) such as `_one`, `_few`, `_many`, and `_other`, following each language&#39;s plural rules. react-i18next&#39;s `<Trans>` component adds numbered tags such as `<1>terms</1>`.
* **react-intl and next-intl** store ICU messages, where placeholders use single braces and plurals live inside the string: `{count, plural, one {# message} other {# messages}}`. (See [What is ICU message format?](/kb/icu-message-format)).
* **vue-i18n** uses single-brace placeholders and separates plural forms with a pipe: `no apples | one apple | {count} apples` ([vue-i18n pluralisation](https://vue-i18n.intlify.dev/guide/essentials/pluralization)).

How the keys are named matters too, because a translator often sees nothing but the key and the value. (See [How should I name i18n translation keys?](/kb/i18n-key-naming)).

## What goes wrong when you paste JSON into a translator

A general-purpose translator treats the file as text, so the structure is at risk:

* **Keys get translated.** `"nav"` becomes `"nawigacja"`, and the app can no longer find the string.
* **Placeholders change.** `{{name}}` becomes `{{nombre}}`, or a brace goes missing, and interpolation fails at run time.
* **Tags break.** `<1>terms</1>` gets translated, renumbered or dropped, so the link no longer wraps the right words.
* **The file stops being valid JSON.** Curly quotes, an unescaped `"` inside a translated string, or a trailing comma make the whole file fail to parse.
* **Plural keys are missing.** Translating `inbox_one` and `inbox_other` key by key leaves Polish without the `inbox_few` and `inbox_many` keys i18next needs, so &#39;5 messages&#39; has no correct Polish form to show. (See [What is ICU message format?](/kb/icu-message-format) for each language&#39;s plural categories.)
* **Short strings lose their meaning.** `"save": "Save"` in German is *speichern* (store a file) or *sparen* (spend less), and the key alone rarely says which.

## Ways to translate JSON locale files

| Approach                                                       | Keys and placeholders                                                                                                      | Plural forms                                                       | Context for short strings                                           | Keeping up with changes                                          |
| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------- | ---------------------------------------------------------------- |
| By hand, or a translator working in the file                   | Safe if the person is careful                                                                                              | The person adds them, if they know the rules                       | The person can ask                                                  | Manual for every change                                          |
| Online JSON translator (upload a file, download a translation) | Varies by tool; test with a placeholder-heavy file first                                                                   | Varies by tool                                                     | Varies by tool                                                      | Re-upload the file for each change                               |
| Script against the DeepL or Google Cloud Translation API       | You walk the JSON and send only values; DeepL&#39;s document API also accepts `.json` and protects placeholders by default | You generate the missing keys yourself                             | DeepL&#39;s `context` parameter; Google has none per string         | You rerun the script and diff the output                         |
| Prompt an LLM with the file                                    | Usually kept, but nothing guarantees it                                                                                    | Added if you ask and name the categories                           | Whatever you put in the prompt                                      | You rerun the prompt and validate every run                      |
| General Translation CLI                                        | Reads JSON structure and translates values                                                                                 | Translates existing entries; check target-language plural families | Per-key notes and length limits, shared Glossary and Custom Prompts | Reuses matching translations; generates updates before the build |
| Translation management system (TMS)                            | Parsed by format, so keys are safe                                                                                         | Depends on the TMS and file format                                 | Screenshots, comments, and descriptions per key                     | Sync files through the TMS&#39;s repository integration          |

Two of the scripted options need more detail. DeepL&#39;s [document API](https://developers.deepl.com/api-reference/document) lists `json` as a supported format and, by default, keeps placeholders such as `{stars}` or `{{userName}}` verbatim and protects ICU plural skeletons, translating only their branch text. Google&#39;s [document translation](https://docs.cloud.google.com/translate/docs/advanced/translate-documents) accepts Office files and PDFs but not JSON, and the consumer Google Translate site takes only `.docx`, `.pdf`, `.pptx`, and `.xlsx` uploads, so with Google you extract the string values and send them to the text API yourself.

Use a careful manual pass or DeepL&#39;s document API when you translate a small file once. Use a script or an LLM prompt when you&#39;ll own the validation and rerun it yourself. Use General Translation when you want generated and reviewed catalogues to ship with code changes while keeping your existing rendering library. Use a [translation management system](/kb/what-is-a-translation-management-system) when a team of translators works outside the codebase.

## How to translate JSON files with General Translation

When the files live in your repository and change with every release, a command in your build can keep them current. General Translation&#39;s CLI, `gt`, translates JSON locale files, including i18next and ICU-based catalogues. You can retain the library that renders them. Install it with `npm install gt --save-dev`, run [`npx gt init`](/docs/cli/reference/commands/init) to create `gt.config.json` and sign in, and point `files.json.include` at your source files. The `[locale]` placeholder tells the CLI where to read the source locale and where to write each target locale:

```json title="gt.config.json"
{
  "defaultLocale": "en",
  "locales": ["de"],
  "files": {
    "json": {
      "include": ["locales/[locale]/*.json"]
    }
  }
}
```

For an existing project, first run [`npx gt upload`](/docs/cli/reference/commands/upload) to import the source and reviewed translations.

[`npx gt translate`](/docs/cli/reference/commands/translate) then reads every file under `locales/en/` and writes German files to `locales/de/`, ready to review and commit; add `--dry-run` to list the files it found without calling the API. This example uses English and German, whose cardinal messages both use the `one` and `other` categories shown above. On later runs, matching translations can be reused.

If your `package.json` declares `i18next` or `next-intl`, the CLI preserves that library&#39;s string syntax, including ICU messages. (See the [JSON format reference](/docs/cli/reference/formats/json-files).) To resolve a short string such as &quot;Save&quot;, add a companion `common.metadata.json` file with the same keys and a `context` note or `maxChars` limit per string. (See [Keyed metadata](/docs/cli/reference/keyed-metadata).) For `nav.save`, use a note such as ‘Save the edited document to disk’, so the service has more than an ambiguous label. Glossary terms and Custom Prompts from your project&#39;s Context Groups apply as well.

Review generated wording in the [Dashboard editor](/docs/platform/dashboard/guides/reviewing-translations); use [`npx gt save-local`](/docs/cli/reference/commands/save-local) if reviewers edit downloaded files. The checks below validate message structure separately from that language review.

## Check the translated files before they ship

Validate the output with the same message format and locale rules your application uses:

* **Parse the JSON and compare nonplural keys.** Check nested key paths as well as top-level keys. Missing or renamed keys can break lookups even when the JSON parses. Handle arrays and non-string values according to your application&#39;s schema.
* **Check i18next plurals as families.** Group keys such as `inbox_one` and `inbox_other` by their base key, then check the target locale&#39;s categories. Polish needs `inbox_few` and `inbox_many` too; these are required additions, not unexpected keys. Japanese can use only `inbox_other`. i18next follows `Intl.PluralRules` and also allows a special `_zero` form; use the appropriate rules for cardinal or ordinal messages. (See [i18next plural rules](https://www.i18next.com/translation-function/plurals).)
* **Preserve placeholders and tags in every translated form.** Check newly added plural forms too. `{{name}}` must still refer to `name`, and numbered tags must still identify the intended components, even when their order changes.
* **Parse ICU messages and compare their arguments.** Use a parser such as the [FormatJS ICU message parser](https://formatjs.github.io/docs/icu-messageformat-parser/), rather than a regular expression for nested braces. Parsing alone is insufficient: changing the plural argument from `count` to `total` produces valid syntax but no longer matches the value the application supplies. Check argument names and types, rich-text tags, select cases, exact-number selectors such as `=0`, and plural offsets. Allow locale-specific plural categories; ICU&#39;s required `other` branch provides a fallback, so ICU messages need not contain every CLDR category.

Missing plural keys don&#39;t fail loudly. This Polish file has all four categories; a key-by-key translation would have stopped at `inbox_one` and `inbox_other`:

```json title="locales/pl/common.json"
{
  "inbox_one": "Masz {{count}} nową wiadomość",
  "inbox_few": "Masz {{count}} nowe wiadomości",
  "inbox_many": "Masz {{count}} nowych wiadomości",
  "inbox_other": "Masz {{count}} nowej wiadomości"
}
```

Loaded with i18next 26.5.3 and `fallbackLng: 'en'`, the key-by-key file falls back to English for every count that needs `few` or `many`, not to the Polish `other` form:

```text title="Output"
count  category  key-by-key file              complete file
1      one       Masz 1 nową wiadomość        Masz 1 nową wiadomość
2      few       You have 2 new messages      Masz 2 nowe wiadomości
5      many      You have 5 new messages      Masz 5 nowych wiadomości
22     few       You have 22 new messages     Masz 22 nowe wiadomości
1.5    other     Masz 1.5 nowej wiadomości    Masz 1.5 nowej wiadomości
```

The decimal prints as `1.5` because the value isn&#39;t number-formatted; format it for the locale if fractions can occur. Finally, load the messages through your application&#39;s i18n library and review them in context. Exercise counts that select each plural form, including zero and decimals where relevant. Syntax checks cannot establish whether a translation is accurate or whether its text fits the interface. Custom interpolation rules and vue-i18n&#39;s pipe syntax need checks adapted to those formats.

## Key takeaways

* Translate values while preserving message syntax. Nonplural keys stay unchanged; plural keys can differ by language.
* Plural categories differ by language: Polish needs `_few` and `_many` keys that an English source doesn&#39;t have.
* DeepL&#39;s document API accepts JSON and protects placeholders by default; Google&#39;s document translation doesn&#39;t accept JSON.
* To translate them with the `gt` CLI, set `files.json.include` to a `[locale]` pattern and run [`npx gt translate`](/docs/cli/reference/commands/translate); keyed metadata adds per-string context and length limits.
* Validate i18next plural families by target locale, and parse ICU messages instead of checking nested syntax with a regular expression.

## Related questions

- [What is ICU message format?](https://generaltranslation.com/en-GB/kb/icu-message-format.mdx): ICU message format is a syntax for one translatable string that holds variables, plurals, select choices, and ordinals, such as {count, plural, one {# file} other {# files}}. Each language keeps the CLDR plural categories it needs: English uses one and other, while Polish adds few and many.
- [How should I name i18n translation keys?](https://generaltranslation.com/en-GB/kb/i18n-key-naming.mdx): Name keys by where text appears and what it means, such as checkout.summary.title, not by its English wording. Give each distinct use its own key, even when the English matches: German translates the button Open as Öffnen and the status Open as Offen.
- [General Translation vs. i18next, react-intl, and next-intl: which i18n library should you use?](https://generaltranslation.com/en-GB/kb/general-translation-vs-i18n-libraries.mdx): Open-source libraries such as i18next, react-intl, and next-intl don't translate anything. They display translations you supply in a catalogue file for each language, written by you, your translators or a separate service. General Translation's libraries are open source too and do the same job, but General Translation also does the translating, using your code as context.
- [Can I do i18n without translation files?](https://generaltranslation.com/en-GB/kb/i18n-without-translation-files.mdx): Yes. General Translation's gt-react and gt-next libraries do React and Next.js i18n with no translation keys and no JSON file per language to maintain. Copy stays inline in JSX inside the `<T>` component, and the gt translate command generates each language's translations in your build, with context from your code. Locadex can keep them up to date through pull requests.
- [Is AI translation accurate enough for a production app?](https://generaltranslation.com/en-GB/kb/is-ai-translation-accurate.mdx): Often, for widely spoken languages, but only after it passes evaluation on your own languages, UI strings, and higher-risk content. In WMT25, Gemini 2.5 Pro matched or beat human reference translations in 10 of 16 language pairs, yet a document benchmark doesn't show whether short interface strings are ready to ship.

## Previous and next questions

- Previous question: [How do I support RTL (right-to-left) languages in CSS?](https://generaltranslation.com/en-GB/kb/right-to-left-css.mdx)
- Next question: [How do I translate Markdown and MDX without breaking syntax?](https://generaltranslation.com/en-GB/kb/translate-markdown-files.mdx)

## Sitemap

See the [Knowledge Base sitemap](https://generaltranslation.com/en-GB/kb/sitemap.md) for every article.
