LangSync · learn

What is i18n JSON?

i18n JSON is the de-facto format for storing software UI translations as plain JSON files, with locale codes as filenames and key paths as identifiers. Most modern web and mobile i18n libraries read it natively, which is why it has quietly won as the default for new projects.

Used by i18next, react-intl, FormatJS, Vue I18n, ngx-translate Four common dialects (flat, nested, ICU, i18next) Native to every modern web + mobile i18n library
Definition

The 30-second version

i18n JSON is a plain JSON file (one per locale) where each key identifies a translatable string and each value is the translation in that locale. A typical project ships one file per language — en.json, de.json, cs.json — and the i18n library swaps the active file based on the user's locale.

The smallest possible example:

{
  "checkout.title": "Checkout",
  "checkout.cta": "Pay now"
}

That's the whole format. Everything else — nested objects, ICU plural rules, namespaces, fallback chains — is a dialect layered on top.

History

Why JSON won over .po, .yml, and .xliff

Three reasons, roughly in order of importance.

JavaScript ate the frontend. When i18next (2011) and react-intl (2014) standardised on JSON for browser bundles, every framework that followed (Vue I18n, ngx-translate, FormatJS, Solid, Svelte i18n) inherited the choice. JSON parses natively in every JavaScript runtime; YAML and XLIFF need a library. For a frontend i18n library, JSON is the lowest- friction format that ships.

Mobile followed. When Apple introduced String Catalogs (.xcstrings) in 2023, the underlying storage was JSON. Flutter's .arb files are JSON. Android's strings.xml is still XML, but every "modern Android" stack — Compose, KMP, Jetpack — has shipped JSON-based alternatives.

It's diffable. Translators rarely touch JSON directly, but engineers review translation changes in pull requests, and JSON diffs cleanly in every PR viewer. .po diffs are noisy; .xliff diffs are nearly unreadable; YAML diffs are merge-conflict-prone because of indentation.

The dialects

Four common dialects you'll actually meet

"i18n JSON" isn't one format — it's a family. The four dialects below cover ~95% of what you'll see in real projects.

Flat (dotted keys)

One level deep. Keys carry their hierarchy in the string itself with dots or colons. Simple, predictable, no library-specific syntax.

{
  "checkout.title": "Checkout",
  "checkout.cta": "Pay now",
  "checkout.error.declined": "Card declined"
}

Use when: you want the most portable format. Every i18n library can read flat JSON. No nested-object parsing logic, no library-specific escaping. Best for teams that want freedom to swap libraries later.

Nested

Same data, expressed as nested objects. The dot becomes structural.

{
  "checkout": {
    "title": "Checkout",
    "cta": "Pay now",
    "error": {
      "declined": "Card declined"
    }
  }
}

Use when: your i18n library prefers it (Vue I18n, react-intl, and ngx-translate all default to nested). The diff cost is slightly higher than flat (a renamed key touches multiple lines), but readability in large files is better.

ICU MessageFormat

Same dialect of JSON (flat or nested), but the values carry ICU MessageFormat syntax for pluralization, gender, and number formatting.

{
  "cart.items": "{count, plural, =0 {Empty cart} one {# item} other {# items}}",
  "user.greeting": "{gender, select, female {Welcome back} male {Welcome back} other {Welcome back}}, {name}"
}

Use when: you need correct pluralization across languages with complex plural rules (Russian, Arabic, Polish, Czech all have plural categories beyond one/other). Pair with formatjs or messageformat.

i18next-style

Specific to the i18next library: keys can use _one / _other suffixes for plurals, _male / _female for context, and namespaces via separate files. Interpolation uses {{name}} syntax.

{
  "cart_one": "{{count}} item in cart",
  "cart_other": "{{count}} items in cart",
  "greeting": "Hello, {{name}}!"
}

Use when: your stack is committed to i18next. The format is more ergonomic than ICU for simple plurals but locks you to one library's conventions.

Pitfalls

Where i18n JSON runs out of road

JSON is fine for the storage layer, but four problems show up consistently in production teams.

No comments. JSON doesn't support comments, so translator context — "this string appears on the checkout button" — has to live somewhere else: a sibling _context key, a separate YAML file, or screenshots in your TMS. Some teams use JSON5 or JSONC for source files and strip comments at build time.

Pluralization is a separate problem. Flat JSON gives you a key → string mapping. It doesn't tell you that Russian has four plural categories (one, few, many, other) or that Arabic has six. Either layer ICU MessageFormat on top, or accept that the i18n library handles plural rules for you — but the JSON file alone is not enough.

Interpolation syntax is library-specific. {name}, {{name}}, %name%, <name/> — every library picks one, and the JSON file binds you to that choice. If you switch from react-intl ({name}) to i18next ({{name}}), every value needs a transform.

Missing-key behaviour is yours to define. What happens when the user's locale doesn't have a translation? Fall back to the source language? Show the key path? Show an empty string? The JSON doesn't tell you; the i18n library does — and the choice matters more than you'd expect for QA.

In LangSync

How LangSync stores it

LangSync treats i18n JSON as the source of truth. Each namespace maps to a directory in your repo:

i18n/
  web/
    en.json
    de.json
    cs.json
  mobile/
    en.json
    de.json

In v1, LangSync supports flat JSON only{"checkout.title": "Checkout"} with dotted keys at the top level. Nested objects, ICU MessageFormat as a dialect distinction, and i18next-style suffix plurals (cart_one / cart_other) are on the roadmap but not shipped today. If your project already uses one of those shapes, you'll either flatten on import or wait for the format to land.

The reason to start with flat is that it's the most portable shape across i18n libraries — no nested-object parsing logic, no library- specific escaping, smallest diff cost. The format field in .langsync.json is designed to accept additional dialects without a breaking change once they ship.

On placeholders and plurals: LangSync's translation prompt instructs the AI to maintain formatting and special characters, which preserves interpolation tokens like {name} or {{name}} in practice. ICU {count, plural, …} blocks are preserved when present in the source, but the AI does not proactively generate ICU plural structure for languages with extra plural categories (Russian's one / few / many / other, Arabic's six, Czech's three). If correctness across complex plural rules matters for your project, structure source strings as ICU MessageFormat upfront rather than expecting the AI to invent the block shape. See the ICU MessageFormat guide for the syntax and the per-language plural categories you'll need.

FAQ

Common questions about i18n JSON

Should I use flat or nested JSON?
Flat is more portable across libraries; nested is easier to read in large files. If you have fewer than ~500 keys, flat keeps things simple. Beyond that, nested wins on human ergonomics — but pick whichever your i18n library defaults to and stick with it.
Do I need ICU MessageFormat?
Only if you target languages with plural categories beyond one / other (Russian, Arabic, Polish, Czech, etc.) AND you have strings with counts. For a UI without dynamic counts, plain JSON is enough. For "5 items in cart," you need either ICU or a library-specific plural convention.
How do I add translator context if JSON has no comments?
Three options. (1) Use a sibling _context key — "checkout.cta": "Pay now" plus "checkout.cta._context": "Primary button on the checkout page". (2) Use a separate context YAML file mapped by key. (3) Attach screenshots in your TMS (LangSync, Crowdin, Phrase, etc. all support this). Option 3 is the most reliable because translators see the rendered string, not just text.
Why not use `.po` (gettext)?
.po is mature and has excellent tooling for desktop and server-side i18n (PHP, Python, Ruby ecosystems still use it heavily). For new frontend or mobile projects, the diff cost, the need for a .po parser at runtime, and the lack of native JS support push teams toward JSON. If you're already on .po and it works, there's no urgent reason to switch.
What about XLIFF?
XLIFF is the industry standard for exchanging translations between TMSes and agencies — it carries metadata (translator, approval state, alternative translations) that JSON doesn't. Most teams use JSON for storage and export to XLIFF when sending strings to an external translation vendor. LangSync and most modern TMSes can import XLIFF and store the data as JSON internally.
Can I mix dialects in one project?
You can, but you shouldn't. Mixing flat and nested JSON in the same namespace breaks every key-lookup helper in your codebase, and translators get confused about which format to write into. Pick one per namespace; if you need different shapes for different surfaces (web vs mobile), give them separate namespaces.
Ready when you are

Try LangSync in 60 seconds

First 1,000 strings free, no credit card, no sales call.

Free tier 1,000 strings, no card
EU-hosted Every plan, by default
No SDK Keep your i18n library
No training On your strings
// tick. tick. tick.
0 ticks since founding