Localize Your App

JSON i18n Translation: i18next, react-intl and vue-i18n Compared

JSON is the common on-disk shape for JavaScript i18n. That is why a search for “react native localization” lands on i18next, react-intl (FormatJS), and vue-i18n in the same breath. All three can live in .json files. They are not interchangeable. Each file is a message catalog for one runtime: different interpolation tokens, different plural machinery, and different rules for nesting. Copying a file from one library into another does not “just work.” The strings parse as JSON and then fail as messages.

This page is a side-by-side of those three catalogs: what the JSON actually contains, how interpolation and plurals are written, and what breaks when a file moves. React Native is called out at the end because the same JS catalogs run there, but locale detection does not.

Same JSON, three catalogs

A localization file is not a generic dictionary of UI copy. It is input to a formatter. i18next reads keys, optional nested objects, and plural suffixes, then interpolates with {{…}}. react-intl / FormatJS treats each value as an ICU MessageFormat string. vue-i18n, in its default format, interpolates with single-brace placeholders and encodes plurals as pipe-separated variants inside one string. Those are three grammars. A translator tool that “supports JSON” does not make the grammars compatible.

If you share one JSON tree across a React Native app, a Vue web app, and a FormatJS pipeline, you will rewrite plurals and placeholders at the boundary. The rest of this guide is that boundary, library by library.

i18next

i18next’s JSON format is documented as a feature set: interpolation, formatting, plurals, nesting, and objects/arrays. Values are strings (or nested objects/arrays of strings). The runtime looks up a key, optionally walks nested objects, then runs the translation function.

Interpolation

Keys inside a message are, by default, strings surrounded by curly brackets. In source files that is the {{key}} form. You pass values into t(); the library substitutes them. Escaping is on by default. Docs describe turning escaping off with a - marker or with escapeValue: false. Nesting guidance is explicit: keep the default interpolation.escapeValue: true, and sanitise every user-controlled string before it becomes a t() option—at minimum strip or escape " and \.

{
  "greeting": "Hello {{name}}"
}

Plural key suffixes (JSON v4)

i18next does not put every plural variant inside one string. It uses a key suffix convention and Intl.PluralRules to pick the key. Since v24 the Intl API is mandatory; there is no fallback to old JSON v3 plural handling.

The suffix convention: key for a singular form, key_other for the default plural, and key_zero, key_one, key_two, key_few, key_many for additional CLDR forms. You add one JSON key per plural category the language needs, using the category name as the suffix. You pass count as the interpolation variable; the library selects the right key. count also interpolates, so {{count}} prints the number.

In v4, suffixes follow the plural category: zero, one, two, few, many, other. Two-form languages use key_one + key_other. Multi-form languages use combinations such as key_one, key_few, key_many, key_other.

{
  "item_one": "{{count}} item",
  "item_other": "{{count}} items"
}

Namespaces and nesting

The JSON format docs treat objects and arrays as first-class catalog structure: you nest keys in objects rather than flattening every phrase into a single dotted string. Nesting as a translation feature (a message that pulls in another message) is a separate, documented function, with the escaping rules above. That is how i18next groups copy—nested JSON plus optional message nesting—not ICU blocks and not pipe lists. Do not assume another library will walk the same object tree or resolve nested messages the same way.

react-intl / FormatJS

FormatJS messages use ICU syntax, with a couple of enhancements common in formats such as Fluent. React Intl’s string formatting builds on ICU Message Formatting and uses ICU Message Syntax. react-intl is the FormatJS React integration; it is widely treated as the main ICU implementation in the JavaScript ecosystem, including number skeletons and nested expressions.

A value is placed with a {key} argument. Typed/formatted values use {key, type, format}. The intl-messageformat library takes the message plus input data and produces the formatted string. In React, default copy is declared as a Message Descriptor and passed to formatMessage (or FormattedMessage). description and defaultMessage must be string literals.

{
  "nameIntro": "My name is {name}",
  "itemCount": "{count, plural, one {# item} other {# items}}"
}

Plurals are ICU plural arguments, not sibling keys. An other clause is required for plural (and select). Selection is CLDR-based through the ICU implementation. Typical storage is a JSON map of id → message string, but the public docs emphasise descriptors and ICU syntax more than a single mandated on-disk layout. The payload that matters is the ICU string, not a v4-style suffix table.

vue-i18n

Vue I18n’s default message format is its own syntax, not ICU. Interpolation uses placeholders such as {count}. The message-format guide also documents literal interpolation (a string literal quoted with ') and interpolating message-format syntax with a % prefix.

Pipe pluralization

Plurals are not suffix keys and not ICU plural blocks by default. You define locale messages with a pipe | separator and list forms in that one string. Documented shapes:

  • Two forms: singular | plural
  • Three forms: zero | singular | plural (the first form is used for zero)
  • More forms: add more pipe-separated options

With two forms, the first is singular and the second plural. Count is passed into the translator ($tc() in older call patterns, or t() / useI18n’s t / the i18n-t component). Pipe strings still interpolate the number (for example {n}) from that count. In current usage the pipe format is driven through the standard t() function. Selection uses an internal choice index, with optional pluralizationRules / pluralRules when you need custom CLDR mappings.

{
  "item": "item | items",
  "itemWithZero": "no items | item | items"
}

A literal | inside copy is a real hazard: it is the plural delimiter. Escaping approaches discussed in the ecosystem include using $t instead of $tc, or a pipe unicode \u007C instead of a raw |.

ICU support

ICU is not the default parser. vue-i18n v10 can parse ICU at build time via the @intlify/unplugin-vue-i18n Vite plugin with icu: true. The <i18n-t> component injects Vue components into named argument slots in an ICU message. Older docs describe ICU-like behavior through a custom formatter that implements the Formatter interface. Project roadmap text has also pointed at ICU / Unicode message-format compatibility. Until ICU mode is on, nested ICU expressions and ICU-only features are not something the default formatter is documented to accept. For languages with complex CLDR categories, guides point at ICU MessageFormat or an @.plural modifier with explicit category mappings—not at i18next-style _few keys.

Comparison

Concern i18next react-intl / FormatJS vue-i18n (default)
Interpolation syntax {{key}} (curly-bracket keys; escaping configurable) ICU {key} and {key, type, format} — no double braces {count}-style placeholders, literal '…' interpolation, %-prefixed message-format interpolation
Plural handling Separate keys with CLDR suffixes (_zero, _one, _two, _few, _many, _other); Intl.PluralRules; count required ICU {count, plural, one {…} other {…}}; other required; CLDR via ICU Pipe-separated variants in one string (singular | plural, or zero | singular | plural); optional custom plural rules
Nesting Nested JSON objects/arrays plus documented message nesting; keep interpolation.escapeValue: true Nested ICU expressions are part of the ICU implementation Default pipe/format parser is not full ICU; nested ICU needs ICU mode / plugin

What breaks when a file moves

Unmodified transfer breaks plural selection and interpolation. The JSON remains valid. The catalog is not.

i18next JSON → react-intl ICU. Suffix keys (key_one, key_other, and the rest) are ordinary ids to FormatJS. They are not a plural paradigm. react-intl expects one id whose value is an ICU {count, plural, …} block. {{name}} is not ICU; it must become {name}. A file full of _other keys will render the wrong string or the raw key, and double-brace tokens will show up as literal text.

react-intl ICU → vue-i18n pipes. ICU plural blocks have to be rewritten as pipe-separated variants (zero | one | other in Vue’s own form list, not ICU keywords inside braces). Nested ICU, selectOrdinal, and rich ICU formatting are ICU-mode features; the default Vue I18n formatter is not documented to parse full ICU. Enable ICU (v10: @intlify/unplugin-vue-i18n with icu: true) or flatten those messages. Leaving ICU in a default catalog means pipes never fire and braces are treated as Vue’s simpler placeholders—or fail to parse.

vue-i18n pipes → i18next. One key with a | b | c must become multiple keys with CLDR suffixes (key_zero, key_one, key_other, …). i18next will not split on |. Placeholders may be {n} or {count} in Vue and must become {{count}} / {{value}} for i18next. Until you split keys and retokenize, count will not select a suffix and the pipe characters will appear in the UI.

Three plural mechanisms (suffix keys vs ICU blocks vs pipe strings) and three interpolation styles ({{}} vs {} vs Vue’s mix) are the whole migration cost. There is no documented lossless round-trip.

React Native

React Native runs JavaScript, so the catalogs above are the same files you would use on web—when the library is a JS/React library. In that family, i18next and react-intl are the ones that show up in React Native codebases. vue-i18n is Vue’s library; it is not the RN path.

Locale detection is the part that is not portable from web. A browser app can read the user language from web platform APIs and pass that into i18next or FormatJS. React Native has no browser. You still initialise the same library with a language tag, but that tag has to come from the OS/runtime you actually run on, then be fed into the i18n instance. Do not assume a web navigator-style lookup exists, and do not assume a JSON file named en.json will be picked because the device is English—the library only sees the locale you give it. After that, interpolation, v4 suffixes, and ICU strings behave as in the tables above. The catalog format does not change because the renderer is native; only how you discover the locale does.

Practical sequence for an RN app using this family: pick i18next or react-intl, keep JSON in that library’s grammar only, resolve the device locale yourself, pass it in, and never paste vue-i18n pipe strings or mixed @{{}} / ICU into the same file.

The JSON translation walkthrough covers doing a pass without breaking nesting, and the file formats guide compares JSON with the native mobile formats. For the wider decision see the complete app localization guide.

Frequently asked questions

Does i18next still fall back to JSON v3 plural keys if Intl is missing?

No. i18next resolves plurals with Intl.PluralRules, and since v24 the Intl API is mandatory—there is no fallback to old JSON v3 plural handling.

What interpolation syntax should i18next JSON use, and how is escaping configured?

Keys are strings surrounded by curly brackets, written as {{key}}. Escaping can be disabled with a - marker or escapeValue: false; nesting guidance is to keep interpolation.escapeValue: true and sanitise user-controlled t() options.

Does react-intl store ICU MessageFormat, or a custom JSON plural table?

It uses ICU Message Syntax (FormatJS messages are ICU, with some Fluent-like enhancements). Plurals are {count, plural, …} arguments with a required other clause, not i18next-style suffix keys.

How do I declare plurals in default vue-i18n JSON?

Put pipe-separated forms in one string: singular | plural for two forms, or zero | singular | plural for three (first form is zero). Pass count into t() / $tc() / i18n-t; do not split into _one / _other keys.

How do I turn on ICU parsing in vue-i18n v10?

Use the @intlify/unplugin-vue-i18n Vite plugin with icu: true so ICU is parsed at build time. The <i18n-t> component fills named argument slots in an ICU message. Default pipe format is not full ICU.

If I drop an i18next file into react-intl (or vue-i18n pipes into i18next), what must be rewritten?

Interpolation and plurals. {{name}} must become {name} for ICU; ICU plural blocks must become pipes or suffix keys; vue-i18n’s one-key pipe lists must be split into key_one / key_other (and other CLDR suffixes). Unmodified files break selection and substitution.

Part of Localization file formats .strings vs .xcstrings vs strings.xml vs .arb: Localization File Formats Explained →

Ship your app in 50 languages by tonight

Upload your localization file, review side by side, download ready-to-import files for every language. One-time credits from $9 — no subscription.

No subscription. Credits never expire.

Related guides