Formats
next-intl
The messages file a next-intl app loads, with ICU plural syntax kept intact in both directions.
What you get
Everything the editor does, through the files you already ship.
Placeholders round-trip
%@,%1$sand{count}come back as they went in, and a translation that drops or alters one is flagged before it is verified.Pull a subset by tag
Tag keys by feature or platform and pull only those, so one project can serve several apps without shipping each other's strings.
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.
Runs 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 messages file
Nested by namespace, the way
useTranslations("cart")addresses it. A plural is one ICU message with acountargument;{app}is a named argument.messages/de.json{ "cart": { "items": "{count, plural, one {1 Artikel} other {{count} Artikel}}" }, "onboarding": { "location_permission": "Standort & Wetter für {app} erlauben" }, "paywall": { "subscribe_cta": "Jetzt abonnieren" } }Step 2: Read it in a component
Nothing changes in the app. next-intl resolves the plural from the count and fills the arguments.
app/cart/page.tsximport { useTranslations } from "next-intl"; export default function CartPage({ count }: { count: number }) { const t = useTranslations("cart"); return <h1>{t("items", { count })}</h1>; }Step 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=next-intl" \ -H "Authorization: Bearer $ANYLOCALE_TOKEN" \ --data-binary @messages/en.jsonStep 5: Pull one locale
One file in the next-intl 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 next-intl --out messages/de.jsonStep 6: Pull only the keys the web app uses
A project shared with a mobile app can tag its web keys and pull that subset alone.
shellanylocale pull --project my-app --locale de --format next-intl \ --tag web --out messages/de.json
Good to know
- A plural is refused on push, with the key named, when its argument is not called count, when it has no other form, or when it uses =0, offset or selectordinal. Refusing beats storing it as a plain string.
- Inside a plural form an unquoted # is the count. ICU's '#' escape is kept as written.
- A tag that no key carries is refused with the list of tags that exist, never answered with an empty file.
- Every pull carries the current text of every key, verified or not. Only over-the-air delivery is limited to verified strings.
Questions
- Is the ICU syntax preserved?
- Yes. Plural and select syntax is stored as written and returned as written, and the placeholder check flags a translation that loses a {count} or an argument name.
- Can the web app share a project with the mobile apps?
- Yes. Tag the keys the web app uses and pull with --tag web, so the messages file carries only those and the mobile apps never ship the web's strings.
- What if I pull a tag that no key carries?
- The pull is refused with the list of tags that exist. It is never answered with an empty file, so a typo in CI cannot blank the messages of a release.
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.