Formats
The eight supported formats, how each one reads and writes its files, and the document key order guarantee.
verbatra works on your locale files through a format adapter. Each adapter reads a file into one format-neutral shape that the diff, hashing, and integrity checks run on, then writes it back in the file's original shape. You pick the format with the format field in your config. Eight are supported: four JSON flavors, plus XLIFF, YAML, ARB, and Java/Spring properties.
The four JSON formats
format | For | Placeholders | Plurals | ICU |
|---|---|---|---|---|
i18next-json | i18next | {{name}} and $t(...) nesting references | CLDR plural suffix on the key | no |
vue-i18n-json | vue-i18n | {name}, {0} | pipe in the value | no |
next-intl-json | next-intl | ICU argument and tag names | ICU plural or selectordinal | yes |
ngx-translate-json | ngx-translate | {{name}} | none | no |
All four read nested JSON objects of string leaves. Their differences are the message syntax:
- i18next uses
{{double-brace}}interpolation and decides plural from the CLDR suffix on the key (_zero,_one,_two,_few,_many,_other). It additionally extracts and guards$t()nesting references (which splice another key's content into the value), for example$t(common.foo)and$t(common.foo, { options }), as placeholders, so a translation that drops or alters one fails the integrity check. Two limitations: nested parentheses inside the$t()options are not supported, and only the default$t(prefix is recognized. - vue-i18n uses single-brace
{name}and{0}tokens and decides plural from a pipe in the value. - next-intl values are ICU MessageFormat: the placeholders are the ICU argument and rich-text tag names, plural follows an ICU plural or selectordinal argument, and the ICU body is kept verbatim through the pipeline. A value that fails to parse as ICU is reported as invalid and skipped, never thrown.
- ngx-translate shares i18next's
{{double-brace}}interpolation but has no$t()nesting and no built-in plural or ICU. Its files may be flat (dotted keys) or nested, and verbatra preserves whichever style the file uses on write; a new target file is written nested. Uniquely, a file that mixes flat dotted keys with nested objects fails the read withMIXED_STRUCTURE, since such a file is ambiguous rather than guessable.
XLIFF
The xliff format covers .xlf and .xliff files, XLIFF 1.2 (file/body/trans-unit) and 2.0 (file/unit/segment). Unlike the tree formats, an XLIFF document is a flat list of trans-units.
- Keys. Each entry is keyed by its trans-unit
id, falling back toresname. A unit with neither falls back to a positional key, which shifts when earlier units are added or removed, so give every unit a stableid. Two units resolving to the same key (typically a duplicateid) fail the read withINVALID_STRUCTURErather than silently keeping one. - Values. The value comes from
<target>when present, otherwise<source>. - Writes update targets in place. verbatra writes each value into its
<target>and leaves<source>, attributes, and<note>elements untouched, so the document round-trips. Because a flat key/value map cannot reconstruct an XLIFF document, the destination file must already exist: verbatra updates targets in a pre-seeded target file (the standard XLIFF workflow) and does not create a missing one; a missing destination fails withINVALID_STRUCTURE. - Inline markup. Inline placeholder elements (
x,g,bx,ex,ph,it,mrk) and single-brace{name}interpolation are extracted as placeholders and guarded across translation. On write, only those allow-listed elements (with their own minimal, non-executable attributes) survive as live markup in<target>; anything else in a translated value, including an unexpected element or a namespace mismatch, is written as plain text instead. - Notes as context. A trans-unit's
<note>(in 2.0, the unit's<notes><note>, shared by every segment in the unit) is read as developer context: it reaches the provider as disambiguation context and appears in theContextcolumn of an exported workbook. It is read-only and never written back.
YAML
The yaml format covers .yml and .yaml files: a nested tree in YAML syntax, the same shape as a nested JSON file, handled by the same tree pipeline. It assumes i18next-compatible {{double-brace}} interpolation and detects by extension only.
- YAML comments are not carried across a write, the same way JSON has no comment concept.
- Scalar non-string keys keep their string form (
1:reads as"1",true:as"true"). - A composite key (a map or sequence used as a mapping key) has no faithful string form, so the read fails with
INVALID_STRUCTUREinstead of silently collapsing it to text. - Malformed syntax is reported as
INVALID_YAML, and anchor-alias expansion is bounded, so a hostile document cannot blow up the parse.
ARB
The arb format covers Flutter's .arb files: JSON with a flat object of message keys alongside @-prefixed metadata (@key per-message metadata and @@-prefixed globals such as @@locale).
- Metadata is preserved, never translated.
@-prefixed keys are stripped before translation and merged back on write in their document position. A destination file that exists but is corrupt fails the write rather than silently erasing its metadata. - Messages are ICU. Placeholders, plural handling, and ICU validation work exactly as they do for
next-intl-json. - Descriptions become context. Each
@key.descriptionis read as developer context for that message: it reaches the provider as disambiguation context (never as text to translate) and appears in theContextcolumn of an exported workbook. It is read-only and never written back.
Properties
The properties format covers Java and Spring .properties files, detected by the .properties extension: a flat list of key/value lines. Keys are kept verbatim as flat keys, never split into a tree.
- Separators and comments. A key is separated from its value by
=,:, or whitespace, with any spaces around the separator ignored, matchingjava.util.Properties. A line whose first non-blank character is#or!is a comment. - Continuations and escapes. A line ending in a backslash continues onto the next. The standard escapes (
\t,\n,\r,\f,\\, an escaped separator or comment character) and\uXXXXare decoded on read. Input is read as UTF-8; on write, every non-ASCII character is emitted as an ASCII-safe\uXXXXescape, so the file still loads under a legacy ISO-8859-1 reader. - Order, comments, and blank lines are preserved. A write re-reads the destination and keeps its key order, comments, and blank lines: each existing key line is rewritten in place with its new value, and a key the file does not yet have is appended in source order. A duplicate key keeps its first position and takes the last value, matching
Properties.load. - Placeholders are MessageFormat. Values are read as
java.text.MessageFormat, so{0}, the typed form{0,number}, the styled form{0,number,integer}, and named arguments such as{count}are extracted and guarded across translation. Sub-message arguments (plural,select,selectordinal,choice) are recognized, so translating the branch text stays a match while dropping or renaming an argument does not. - Limitation: single quotes are not interpreted. MessageFormat single-quote quoting is not honored, so a quoted literal such as
'{0}'is still read as a placeholder. This is deliberate, so an ordinary apostrophe in translated text never swallows a following placeholder.
Document key order
Writes preserve your document's key order. The JSON family, YAML, and ARB round-trip keys exactly in document order:
- An integer-like key such as
"2","10", or"404"keeps its position instead of being hoisted to the front and re-sorted, so a file keyed by numeric ids, HTTP status codes, or years stays in its own order. - A key that a
translaterun adds to a target file appends after the target's existing keys, following the source document's order, rather than being inserted alphabetically. - ARB metadata blocks round-trip in their document position too.
XLIFF is unaffected: it updates existing <target> elements in place, so the document's order was never rebuilt in the first place.
Dotted keys and collisions
For i18next-json, vue-i18n-json, and next-intl-json, a literal dotted leaf (a key such as "foo.bar" used as a single leaf) round-trips losslessly: verbatra reads it and writes it back with its on-disk shape preserved, not re-nested into foo then bar. Real nested paths stay nested.
The one case that fails by design is a genuine collision, where one file expresses the same effective path both as a literal dotted leaf and as a real nested path (for example "foo.bar" alongside "foo": { "bar": ... }). That read fails with INVALID_STRUCTURE rather than guessing or corrupting data.
ngx-translate-json treats a dotted key as a nested path rather than a literal leaf, so there is no dotted-vs-literal ambiguity to preserve, but it gets the same collision safety: a dotted key whose path collides with a real nested path, including one being an ancestor of the other, fails with INVALID_STRUCTURE instead of silently overwriting a value.
How files are read and written
verbatra caps input size and nesting depth on read, and resists a file changing underneath it. It writes atomically: it writes to a temporary file, then renames it into place, so an interrupted write never leaves a half-finished locale file. When verbatra cannot handle a file, it raises a structured, secret-free error with a stable code rather than a raw one: INVALID_JSON, INVALID_YAML, or INVALID_XML for malformed syntax, INVALID_STRUCTURE for a parseable file of the wrong shape, MAX_DEPTH_EXCEEDED and INPUT_TOO_LARGE for the caps, and MIXED_STRUCTURE for ngx-translate's mixed-style case.
A tree-based locale file (the JSON family, YAML, and ARB) can carry a leaf that is not a string, for example a count: 5 or enabled: true value sitting next to the translatable keys. verbatra accepts these: a leaf can be a string, a number, a boolean, or null, and only a leaf of another type, such as an array, fails with INVALID_STRUCTURE. A non-string leaf is excluded from the translatable set: it is never translated, hashed, diffed, or checked for placeholders or ICU. Exclusion is not preservation: if verbatra later writes that same locale file, the write is rebuilt from the strings it manages, so a non-string leaf present at read time is not carried into the output. If such a value needs to survive a rewrite, keep it out of the files verbatra writes to. XLIFF trans-unit values and .properties values are always strings, so this does not apply there.
Why the format matters
The format tells verbatra two things it needs for a safe run. First, the placeholder syntax, so the placeholder integrity check knows what to compare before and after translation. Second, for the ICU formats, which values to validate, so an invalid ICU source key is skipped rather than sent in a broken state. The wrong format compares the wrong tokens, so match it to your i18n library: i18next-json for i18next, vue-i18n-json for vue-i18n, next-intl-json for next-intl, ngx-translate-json for ngx-translate, xliff for XLIFF files, yaml for YAML-based i18n, arb for Flutter, and properties for Java or Spring .properties files.