Reference / Configuration
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 to one of these ids:
format | Files | Placeholders | Plurals | Missing target file | Native placeholder syntax |
|---|---|---|---|---|---|
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 compares.
The four JSON formats
All four read nested JSON objects of string leaves.
i18next-json
{{double-brace}} interpolation; plural from the CLDR key suffix (_, _, _, _, _, _). $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
Single-brace {name} and {0} tokens, names in any script ({número}). Plurals from a pipe in the value.
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. A value that fails to parse as ICU is reported invalid and skipped.
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_.
The single-brace fabrication guard
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
files.pattern is a literal path, never a glob: public/locales/{locale}/*.json looks for a file named *.json and fails with SOURCE_. A single namespace works as configured:
files: {
pattern: "public/locales/{locale}/common.json",
},One config per namespace
For several namespaces, write one config each and run them in turn:
verbatra translate --config verbatra.common.config.ts
verbatra translate --config verbatra.auth.config.tsOnly 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.
XLIFF
XLIFF 1.2 (file/body/trans-unit) and 2.0 (file/unit/segment), a flat list of units.
- Keys. The unit
id, thenresname, then a position that shifts when units move, so give every unit anid. A 2.0 unit with several segments gets<id>#<n>keys. A duplicate key fails withINVALID_.STRUCTURE - Source or translation. A document is the source when the locale matches its
source-language(srcLangin 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 (entoen-GB) must declaretarget-language(trgLang). - Missing values. A
<target>that is absent, empty, or in 1.2 markedstate="new"or"needs-translation"is missing, and so is a 2.0 target equal to its source in a segment markedstate="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.2neworneeds-translationor 2.0initialstate (with itssubState) becomestranslated.<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 withINVALID_naming the key; an unreadable source fails it without one.STRUCTURE verbatra extractappends source units with only a<source>; in 2.0 the key must be a valid NMTOKEN (u1#0is refused). - Inline markup. 1.2's
g,x,bx,ex,bpt,ept,ph,it,mrk, 2.0'sph,pc,sc,ec,mrk,sm,em,cp, and any{...}run are guarded placeholders in either version. On write only these (plus 1.2<sub>insidebpt,ept,ph,it) stay markup; anything else, such as<b>, becomes text. - Escaping. Text is read as text (
A & BisA & B) and escaped once on write; text exactly matching an allowed element, such as<x id="1"/>, is written as that element, and<!DOCTYPEand<!ENTITYas text. - Notes (the unit's
<notes>in 2.0) are read-only context. - Rejected input. A document with a DTD or entity declaration fails with
INVALID_.XML
YAML
A nested tree like the JSON formats, with i18next {{double-brace}} interpolation and the 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_, malformed syntax fails with INVALID_, and alias expansion is bounded.
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.descriptionis read-only context. - A write holds exactly its messages. A message left out (pruned, or rejected in review) is dropped with its
@keyblock;@@globals stay. @@localecomes first. A created target starts with it ingen-l10nspelling (pt_); 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, orBR flutter gen-l10nfails.
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), andplural,select,selectordinalandchoicearguments. Single-quote quoting is not interpreted, so'{0}'still counts as a placeholder.
Apple .strings
"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_; re-save it as UTF-8.STRUCTURE - Comments. A
/* ... */comment before an entry is read-only context;//comments are kept.\",\\,\n,\tand\Uescapes 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,
.lprojdirectory and.stringsdictincluded. - 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
.stringsdictcategory becomes<key>_(<category> photo_), round-tripping only the categories a locale has. Errors name the file and key; format key, variable name and value type survive a rewrite.count_ one - Layout.
{locale}.lproj/Localizable.stringsneeds no special locale style.
Xcode String Catalogs (.xcstrings)
One JSON catalogue holding every locale, Xcode 15's replacement for .strings.
- One file for all locales.
files.patternstill needs{locale}, but{locale}expands to an empty string, so every locale resolves to the same file ({locale}Localizable.xcstringsisLocalizable.xcstrings). Every writer is serialized on it, so locales run one at a time even with--concurrency. - Keys. A key with no
sourceLanguagelocalization uses the key text as source; a missing target localization counts as untranslated. - Plurals under
variations.pluralbecome<key>_, only for the categories present.<category> - Placeholders are printf, as in
.strings;%lldequals%d. shouldTranslate: falsekeys 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
res/values/strings.xml for the source and res/values-<qualifier>/strings.xml per target; set files.localeStyle: "android" (config).
- Entries.
<string name>is one entry;<plurals>becomeskey[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,\tand 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.
--pruneknows 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
msgid/msgstr entries keyed by msgid.
msgctxtjoins itsmsgidinto one key with U+E000; a source containing that character is rejected.- Plurals are keyed by index,
key[n], not by CLDR category.Plural-Formsis kept verbatim and only itsnpluralsbounds the indices; plural entries without it are rejected, asmsgfmt --checkdoes. - Flags and comments. A
#, fuzzyvalue 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.potwith emptymsgstrreads cleanly, but in a source catalog eachmsgstris the text to translate, so an empty one is skipped withSOURCE_.VALUE_ EMPTY - Missing target is created with a minimal header and, for plurals, a
Plural-Formssized to the highest index (seed from a real template for exact grammar). - Placeholders are printf,
%(name)sincluded. - Errors name the
msgidand line.--pruneknows nothing of plural indices, so it can delete target-only forms.
INI
One level of [section] headers over key=value lines.
- Keys. A key in
[home]ishome.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,\tdecoded, 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
<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, getsxml:space="preserve". - Not translated.
<data>withtypeormimetype, 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
AdapterErrornaming it. - Errors. Invalid XML, a root other than
<root>, and a DTD or entity are rejected.
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
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_; ngx-translate-json refuses flat dotted keys beside nested objects, and dotted keys inside nested objects, with MIXED_.
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
.ponames its locale inLanguage(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.
Formats from outside verbatra
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.
Inline markup in values
The markup parity check applies to every format, per tag:
xliffguards its own inline elements as placeholders; other HTML such as<b>is compared as markup.next-intl-jsonandarbtreat 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-xmlcompares decoded values, so<b>is<b>.- Every other format has no markup concept, so only this check sees it.