Formats
JSON
A flat key-to-string document per locale, in the shape i18next and react-intl load, for any runtime that reads its own catalog.
What you get
Everything the editor does, through the files you already ship.
AI drafts, a person verifies
Every language is drafted against your glossary and each key's length limit. A draft never ships until someone has verified it.
Five checks on every save
Placeholders, length, terminology, consistency and missing translations are flagged on the row they concern, not in production.
Everything is an HTTP call
Project tokens carry read, push and manage scopes, so a CI token that pushes files cannot delete keys.
The HTTP APIRuns in CI
Push the source language on merge and pull every language before the release build, on any CI that can run a shell.
CI pipelines
Set it up
From nothing to every language.
Step 1: The file
Flat keys with dots in the name, one object per locale. Plurals use i18next's
_oneand_othersuffixes, placeholders its{{name}}form, so the file loads without a transform.locales/de.json{ "cart.items_one": "1 Artikel", "cart.items_other": "{{count}} Artikel", "onboarding.location_permission": "Standort & Wetter für {{app}} erlauben", "paywall.subscribe_cta": "Jetzt abonnieren" }Step 2: Load it with i18next
The keys are flat, so turn the key separator off; otherwise i18next reads
cart.itemsas a path into nested objects. The plural suffixes and{{count}}are i18next's own conventions and need no configuration.i18n.tsimport i18next from "i18next"; import de from "./locales/de.json"; import en from "./locales/en.json"; await i18next.init({ lng: "de", keySeparator: false, resources: { de: { translation: de }, en: { translation: en } }, }); i18next.t("cart.items", { count: 3 }); // 3 ArtikelStep 3: Install the CLI and log in
Homebrew or the one-line installer, then a token issued per project under Settings, Integrations. The read scope pulls; push writes source files and values; manage creates and deletes keys.
shellbrew install anylocale/tap/anylocale anylocale login --project my-appStep 4: Push the source file
The push is additive: keys missing from the file stay in place, so a truncated file cannot empty a project. Descriptions in the file become translator notes.
shellcurl -fsS -X POST "https://anylocale.com/api/loc/push?project=my-app&format=json" \ -H "Authorization: Bearer $ANYLOCALE_TOKEN" \ --data-binary @locales/en.jsonStep 5: Pull one locale
One file in the json format, written to the path you name. Repeat per locale, or script it over the project's locale list.
shellanylocale pull --project my-app --locale de \ --format json --out locales/de.jsonStep 6: Or every locale the project has
The locales endpoint lists them, so no build script carries a hardcoded list.
shellfor locale in $(curl -fsS "https://anylocale.com/api/loc/locales?project=my-app&plain=1" \ -H "Authorization: Bearer $ANYLOCALE_TOKEN" | awk '$1 == "locale" { print $2 }'); do anylocale pull --project my-app --locale $locale --format json --out locales/$locale.json done
Good to know
- A key is read as a plural only when its _other form and at least one other category are both present, so a key that merely ends in _one stays a plain string.
- JSON has no comment channel: descriptions live in the project and are shown in the editor, not in the file.
- Every pull carries the current text of every key, verified or not. Only over-the-air delivery is limited to verified strings.
Questions
- Nested or flat?
- Flat. A nested object is refused with the key that nests, because a nested {"a":{"b":..}} and a flat "a.b" cannot both round-trip. Keep the dots in the key names and turn the key separator off in the loader.
- Which runtimes is this for?
- Any that loads its own catalog per locale: i18next in a web app, react-intl, a backend, a game, a desktop app. For a next-intl app use the next-intl format instead, which keeps ICU plurals intact.next-intl
- How do plurals and placeholders look in the file?
- A plural is a set of keys with the CLDR category as suffix,
cart.items_oneandcart.items_other, and a placeholder is{{name}}; i18next reads both as they are. A key that merely ends in _one stays a plain string unless its _other sibling exists too.
Start with the file your app already ships.
After checkout, import one strings file and read the first drafts within minutes.
Nothing to connect and nothing to install. The CLI is optional.