---
title: "Formats"
description: "Every i18n file format verbatra supports, how each one reads and writes its files, and what a write preserves so translated files diff cleanly."
type: "reference"
---
# Formats

Every i18n file format verbatra supports, how each one reads and writes its files, and what a write preserves so translated files diff cleanly.

A format adapter reads a locale file into one neutral shape and writes it back in the file's own shape. Set the `format` key of your [config](https://verbatra.kreitz-webdev.de/docs/config-file.md) to one of these ids:

| `format` | Files | Placeholders | Plurals | Missing target file | [Native placeholder syntax](https://verbatra.kreitz-webdev.de/docs/error-codes.md#foreign_placeholder_changed) <AvailableFrom version="0.12.0" /> |
| --- | --- | --- | --- | --- | --- |
| `i18next-json` | i18next JSON (`.json`) | `{{name}}`, `$t(...)` | CLDR key suffix | created | `{{name}}` |
| `vue-i18n-json` | vue-i18n JSON (`.json`) | `{name}`, `{0}` | pipe in the value | created | `{name}`, `%{name}`, `${name}` |
| `next-intl-json` | next-intl JSON (`.json`) | ICU arguments and tags | ICU `plural`, `selectordinal` | created | `{name}`, `%{name}`, `${name}` |
| `ngx-translate-json` | ngx-translate JSON (`.json`) | `{{name}}` | none | created | `{{name}}` |
| `xliff` | XLIFF 1.2 and 2.0 (`.xlf`, `.xliff`) | inline elements, `{...}` | none | refused | `{{name}}`, `{name}`, `%{name}`, `${name}` |
| `yaml` | YAML (`.yml`, `.yaml`) | `{{name}}` | none | created | `{{name}}` |
| `arb` | Flutter ARB (`.arb`) | ICU arguments and tags | ICU `plural`, `selectordinal` | created | `{name}`, `%{name}`, `${name}` |
| `properties` | Java/Spring properties (`.properties`) | MessageFormat `{0}`, `{count}` | none | created | `{name}`, `%{name}`, `${name}` |
| `apple-strings` | Apple `.strings`, with its `.stringsdict` | printf `%@`, `%d` | `.stringsdict` categories | created | printf `%s` |
| `apple-xcstrings` | Xcode String Catalogs (`.xcstrings`) | printf `%@`, `%lld` | `variations.plural` | refused | printf `%s` |
| `android-xml` | Android `strings.xml` | printf `%s`, `%1$s` | `<plurals>` quantities | created | printf `%s` |
| `gettext-po` | gettext (`.po`, `.pot`) | printf, `%(name)s` | `msgstr[n]` | created | printf `%s`, `%(name)s` |
| `ini` | INI (`.ini`) | `{name}`, `{0}` | none | created | `{name}`, `%{name}`, `${name}` |
| `resx` | .NET resources (`.resx`) | composite `{0}`, `{1:C}`, named `{name}` | none | created | `{{name}}`, `{name}`, `%{name}`, `${name}` |

Match the format to your i18n library: it decides which placeholders the [integrity check](https://verbatra.kreitz-webdev.de/docs/translation-safety.md) compares.

## The four JSON formats [#the-four-json-formats]

All four read nested JSON objects of string leaves.

### i18next-json [#i18next-json]

`{{double-brace}}` interpolation; plural from the CLDR key suffix (`_zero`, `_one`, `_two`, `_few`, `_many`, `_other`). `$t(common.foo)` and `$t(common.foo, { options })` nesting references are guarded as placeholders; only the default `$t(` prefix counts, without nested parentheses in the options.

### vue-i18n-json [#vue-i18n-json]

Single-brace `{name}` and `{0}` tokens, names in any script (`{número}`). Plurals from a pipe in the value.

### next-intl-json [#next-intl-json]

Values are ICU MessageFormat: argument and rich-text tag names are the placeholders, and the ICU body is kept verbatim except that each `plural` and `selectordinal` gets the [target language's CLDR arms](https://verbatra.kreitz-webdev.de/docs/translation-safety.md#plural-arms-per-target-language). A value that fails to parse as ICU is reported invalid and skipped.

### ngx-translate-json [#ngx-translate-json]

`{{double-brace}}` interpolation, no nesting references, plurals or ICU. A file may be flat (dotted keys) or nested, and each keeps its style on write; a new target is written nested. A file mixing both fails with `MIXED_STRUCTURE`.

### The single-brace fabrication guard [#the-single-brace-fabrication-guard]

<AvailableFrom version="0.9.0" />

Under `i18next-json`, `ngx-translate-json` and `yaml`, a `{name}` token is literal text a translation may reword, but one that never appeared in the source is rejected as fabricated. Only that direction is guarded: with custom single-brace `interpolation.prefix` and `suffix`, a dropped placeholder is not caught.

## Namespace layouts [#namespace-layouts]

[`files.pattern`](https://verbatra.kreitz-webdev.de/docs/config-file.md#locales-and-files) is a literal path, never a glob: `public/locales/{locale}/*.json` looks for a file named `*.json` and fails with `SOURCE_UNREADABLE`. A single namespace works as configured:

```ts title="verbatra.config.ts"
files: {
  pattern: "public/locales/{locale}/common.json",
},
```

### One config per namespace [#one-config-per-namespace]

For several namespaces, write one config each and run them in turn:

```bash
verbatra translate --config verbatra.common.config.ts
verbatra translate --config verbatra.auth.config.ts
```

<Callout type="warn" title="Only the namespace that ran last keeps change detection">
All configs in one directory share `verbatra.lock.json`, and each run replaces the locale's baselines with its own keys, so an edited string of an earlier namespace reads as in sync and is not retranslated. To retranslate it, delete the key (or the target file) so it counts as missing.
</Callout>

## XLIFF [#xliff]

XLIFF 1.2 (`file/body/trans-unit`) and 2.0 (`file/unit/segment`), a flat list of units.

- **Keys.** The unit `id`, then `resname`, then a position that shifts when units move, so give every unit an `id`. A 2.0 unit with several segments gets `<id>#<n>` keys. A duplicate key fails with `INVALID_STRUCTURE`.
- **Source or translation.** A document is the source when the locale matches its `source-language` (`srcLang` in 2.0; a bare language matches its regional forms) and it does not declare that locale as its target; anything else is a translation, except that a document declaring neither language is the source when it holds no `<target>`. A target sharing the source's language (`en` to `en-GB`) must declare `target-language` (`trgLang`).
- **Missing values.** A `<target>` that is absent, empty, or in 1.2 marked `state="new"` or `"needs-translation"` is missing, and so is a 2.0 target equal to its source in a segment marked `state="initial"`. The `<source>` text is never copied in.
- **Writes in place.** Each value goes into its `<target>`, created after `<source>` if absent, and a 1.2 `new` or `needs-translation` or 2.0 `initial` state (with its `subState`) becomes `translated`. `<source>`, attributes and `<note>` stay untouched.
- **Missing units are added** as a copy of the source unit (without its targets or 1.2 `<alt-trans>`) in the matching `<file>`. A value with no unit to go to fails the write with `INVALID_STRUCTURE` naming the key; an unreadable source fails it without one. `verbatra extract` appends source units with only a `<source>`; in 2.0 the key must be a valid NMTOKEN (`u1#0` is refused).
- **Inline markup.** 1.2's `g`, `x`, `bx`, `ex`, `bpt`, `ept`, `ph`, `it`, `mrk`, 2.0's `ph`, `pc`, `sc`, `ec`, `mrk`, `sm`, `em`, `cp`, and any `{...}` run are guarded placeholders in either version. On write only these (plus 1.2 `<sub>` inside `bpt`, `ept`, `ph`, `it`) stay markup; anything else, such as `<b>`, becomes text.
- **Escaping.** Text is read as text (`A &amp; B` is `A & B`) and escaped once on write; text exactly matching an allowed element, such as `&lt;x id="1"/&gt;`, is written as that element, and `<!DOCTYPE` and `<!ENTITY` as text.
- **Notes** (the unit's `<notes>` in 2.0) are read-only [context](https://verbatra.kreitz-webdev.de/docs/manual-translation.md).
- **Rejected input.** A document with a DTD or entity declaration fails with `INVALID_XML`.

## YAML [#yaml]

A nested tree like the JSON formats, with i18next `{{double-brace}}` interpolation and the [fabrication guard](#the-single-brace-fabrication-guard), detected by extension. Comments are not kept. Scalar non-string keys become strings (`1:` reads as `"1"`), a map or sequence used as a key fails with `INVALID_STRUCTURE`, malformed syntax fails with `INVALID_YAML`, and alias expansion is bounded.

## ARB [#arb]

Flutter's `.arb`: a flat object of messages with `@key` metadata and `@@` globals.

- **Messages are ICU**, handled exactly like `next-intl-json`.
- **Metadata** is never translated and is merged back in place; a corrupt existing target fails the write. `@key.description` is read-only context.
- **A write holds exactly its messages.** A message left out (pruned, or rejected in review) is dropped with its `@key` block; `@@` globals stay.
- **`@@locale` comes first.** A created target starts with it in `gen-l10n` spelling (`pt_BR`); an existing one keeps its value and moves to the top, and one is never added. Fix a value that disagrees with the file name by hand, or `flutter gen-l10n` fails.

## Properties [#properties]

Java and Spring `.properties`: flat key/value lines, keys kept verbatim.

- **Syntax** follows `java.util.Properties` (separators, comments, continuation lines, escapes). Files are read as UTF-8 and non-ASCII is written as `\uXXXX`.
- **Writes keep** key order, comments and blank lines, rewriting each existing line in place and appending new keys in source order. A duplicate key keeps its first position and last value.
- **Line endings follow the file**: any CRLF means all CRLF, CR-only stays CR, otherwise LF.
- **Placeholders are `java.text.MessageFormat`**: `{0}`, `{0,number}`, `{count}` (names in any script), and `plural`, `select`, `selectordinal` and `choice` arguments. Single-quote quoting is not interpreted, so `'{0}'` still counts as a placeholder.

## Apple .strings [#apple-strings]

<AvailableFrom version="0.10.0" />

`"key" = "value";` statements, with plurals in the sibling `.stringsdict` (same directory and base name), read and written together.

- **UTF-8 only.** A UTF-16 file fails with `INVALID_STRUCTURE`; re-save it as UTF-8.
- **Comments.** A `/* ... */` comment before an entry is read-only context; `//` comments are kept. `\"`, `\\`, `\n`, `\t` and `\U` escapes are decoded; on write only quote, backslash, newline and tab are escaped.
- **Writes keep** order, comments and blank lines; a removed key goes with its own comment. A missing target is created, `.lproj` directory and `.stringsdict` included.
- **Placeholders are printf**: `%@`, `%d`, `%1$@`, `%%`, with flags, width and length as decoration; a positional reorder is accepted. A bare `%` (`"50% off"`) is not one.
- **Plurals.** Each `.stringsdict` category becomes `<key>_<category>` (`photo_count_one`), round-tripping only the categories a locale has. Errors name the file and key; format key, variable name and value type survive a rewrite.
- **Layout.** `{locale}.lproj/Localizable.strings` needs no special locale style.

## Xcode String Catalogs (.xcstrings) [#xcode-string-catalogs-xcstrings]

<AvailableFrom version="0.10.0" />

One JSON catalogue holding every locale, Xcode 15's replacement for `.strings`.

- **One file for all locales.** `files.pattern` still needs `{locale}`, but `{locale}` expands to an empty string, so every locale resolves to the same file (`{locale}Localizable.xcstrings` is `Localizable.xcstrings`). Every writer is serialized on it, so locales run one at a time even with `--concurrency`.
- **Keys.** A key with no `sourceLanguage` localization uses the key text as source; a missing target localization counts as untranslated.
- **Plurals** under `variations.plural` become `<key>_<category>`, only for the categories present.
- **Placeholders are printf**, as in `.strings`; `%lld` equals `%d`.
- **`shouldTranslate: false`** keys are never sent or changed.
- **Writes patch the document.** Only touched localizations change, a new value gets `state: "translated"`, and the rest stays semantically identical, though the file is re-serialized as 2-space JSON. Create the catalogue in Xcode first.
- **Errors** name the file and, inside an entry, the key and locale.

## Android strings.xml [#android-stringsxml]

<AvailableFrom version="0.10.0" />

`res/values/strings.xml` for the source and `res/values-<qualifier>/strings.xml` per target; set `files.localeStyle: "android"` ([config](https://verbatra.kreitz-webdev.de/docs/config-file.md#locales-and-files)).

- **Entries.** `<string name>` is one entry; `<plurals>` becomes `key[quantity]` per `<item>`, only for the quantities present.
- **Skipped.** `translatable="false"` resources, `<string-array>`, and a `<string>` with inline elements (`<b>`, `<xliff:g>`, CDATA) are kept as they are; `formatted="false"` is translated normally.
- **Escaping.** `\'`, `\"`, `\n`, `\t` and a leading `\@` or `\?` are decoded and re-escaped on write.
- **Placeholders are printf**: `%s`, `%d`, `%1$s`, `%%`; a bare `%` is not one.
- **Writes** patch the file, delete removed keys, and create a missing file with its directory.
- **Limits.** A translated key that later becomes read-through in the source is removed from the target. `--prune` knows nothing of plural quantities, so it can delete target-only quantities.
- **Errors** name a root other than `<resources>`, an invalid name or quantity, or a duplicate key; a DTD or entity is rejected.

## gettext .po/.pot [#gettext-popot]

<AvailableFrom version="0.10.0" />

`msgid`/`msgstr` entries keyed by `msgid`.

- **`msgctxt`** joins its `msgid` into one key with U+E000; a source containing that character is rejected.
- **Plurals** are keyed by index, `key[n]`, not by CLDR category. `Plural-Forms` is kept verbatim and only its `nplurals` bounds the indices; plural entries without it are rejected, as `msgfmt --check` does.
- **Flags and comments.** A `#, fuzzy` value is read as current and the flag kept. `#.` comments are read-only context; `#:` and other flags are kept, `#~` blocks stay inert, and the header is never touched. A `.pot` with empty `msgstr` reads cleanly, but in a source catalog each `msgstr` is the text to translate, so an empty one is skipped with [`SOURCE_VALUE_EMPTY`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#source_value_empty).
- **Missing target** is created with a minimal header and, for plurals, a `Plural-Forms` sized to the highest index (seed from a real template for exact grammar).
- **Placeholders are printf**, `%(name)s` included.
- **Errors** name the `msgid` and line. `--prune` knows nothing of plural indices, so it can delete target-only forms.

## INI [#ini]

<AvailableFrom version="0.11.0" />

One level of `[section]` headers over `key=value` lines.

- **Keys.** A key in `[home]` is `home.title`; keys above the first section keep their bare name, and literal dots are escaped.
- **Writes** change only values, keeping spelling, spacing, comments and blank lines. A new key goes to the end of its section, a new section to the end of the file.
- **Values.** Double-quoted values are unquoted with `\\`, `\"`, `\n`, `\r`, `\t` decoded, and a value that needs it is written quoted. An inline `;` belongs to the value.
- **Placeholders** are `{name}` and `{0}`, names in any script; no plurals.
- **Errors.** A malformed header raises an `AdapterError`; a line without `=` is kept verbatim.

## .NET .resx [#net-resx]

<AvailableFrom version="0.11.0" />

`<data name><value>` elements under `<root>`, claimed by `.resx` only.

- **Writes patch values only**: everything else comes back unchanged, on the file's line terminator; a byte-order mark is dropped, and a new `<data>` element, or one whose value has surrounding whitespace, gets `xml:space="preserve"`.
- **Not translated.** `<data>` with `type` or `mimetype`, and names starting with `>>` or `$`, are kept as they are. A `<comment>` is read-only context.
- **Placeholders** are composite format items and named holes taken whole (`{0}`, `{1,-10}`, `{2:C}`, `{when:d}`), so a renamed hole is refused; `{{`, `}}` and stray braces are guarded too. No ICU check, no plurals.
- **Missing target** is created with the standard headers.
- **Refused keys.** A value the target holds as a typed, designer or markup entry, or a key that could not be read back, fails the write with an `AdapterError` naming it.
- **Errors.** Invalid XML, a root other than `<root>`, and a DTD or entity are rejected.

## Document key order [#document-key-order]

The JSON formats, YAML and ARB keep document order: integer-like keys such as `"404"` are not hoisted, new keys append in source order, and ARB metadata keeps its position. XLIFF updates targets in place, so its order never changes.

## Dotted keys and collisions [#dotted-keys-and-collisions]

In `i18next-json`, `vue-i18n-json` and `next-intl-json`, a literal dotted leaf such as `"foo.bar"` round-trips as written; `ngx-translate-json` reads it as a nested path. In the other formats, one path written both ways (`"foo.bar"` beside `"foo": { "bar": ... }`) fails with `INVALID_STRUCTURE`; `ngx-translate-json` refuses flat dotted keys beside nested objects, and dotted keys inside nested objects, with `MIXED_STRUCTURE`.

## How files are read and written [#how-files-are-read-and-written]

- **Limits.** Input size and nesting depth are capped (`INPUT_TOO_LARGE`, `MAX_DEPTH_EXCEEDED`).
- **Atomic writes.** A temporary file is renamed into place, never leaving half a file.
- **Layout.** The XML formats keep the line terminator and final newline and indent new elements like their neighbors. A new gettext entry follows a blank line; a created `.po` names its locale in `Language` (`pt_BR`).
- **Non-string leaves.** In the JSON formats, YAML and ARB, a number, boolean or null leaf is accepted but never translated, and a rewrite drops it; an array fails with `INVALID_STRUCTURE`.
- **Errors** are structured and secret-free; see [adapter error codes](https://verbatra.kreitz-webdev.de/docs/error-codes.md#adapter-error-codes).

## Formats from outside verbatra [#formats-from-outside-verbatra]

<AvailableFrom version="0.11.0" />

Another format can come from your own adapter with a `custom:` id such as `custom:toml`, run through the SDK as trusted code: see [Write a custom format adapter](https://verbatra.kreitz-webdev.de/docs/custom-format-adapters.md).

## Inline markup in values [#inline-markup-in-values]

<AvailableFrom version="0.11.0" />

The [markup parity check](https://verbatra.kreitz-webdev.de/docs/translation-safety.md#inline-markup-parity) applies to every format, per tag:

- `xliff` guards its own inline elements as placeholders; other HTML such as `<b>` is compared as markup.
- `next-intl-json` and `arb` treat a paired `<b>...</b>` as a placeholder and compare a self-closing `<br/>` as markup; an attribute-bearing tag is invalid ICU and refused.
- `android-xml` compares decoded values, so `&lt;b&gt;` is `<b>`.
- Every other format has no markup concept, so only this check sees it.
