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$s and {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.

  1. Step 1: The messages file

    Nested by namespace, the way useTranslations("cart") addresses it. A plural is one ICU message with a count argument; {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"
      }
    }
  2. 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.tsx
    import { useTranslations } from "next-intl";
    
    export default function CartPage({ count }: { count: number }) {
      const t = useTranslations("cart");
      return <h1>{t("items", { count })}</h1>;
    }
  3. 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.

    shell
    brew install anylocale/tap/anylocale
    anylocale login --project my-app
  4. Step 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.

    shell
    curl -fsS -X POST "https://anylocale.com/api/loc/push?project=my-app&format=next-intl" \
        -H "Authorization: Bearer $ANYLOCALE_TOKEN" \
        --data-binary @messages/en.json
  5. Step 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.

    shell
    anylocale pull --project my-app --locale de \
        --format next-intl --out messages/de.json
  6. Step 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.

    shell
    anylocale 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.