File formats
easyi18n handles ten localization file formats. Six work in both directions,
mo and csv are import only, and ios_strings and android_xml are
output only.
| Format | ID | Import | Output | Files written |
|---|---|---|---|---|
| ARB | arb | Yes | Yes | app_<locale>.arb |
| Flat JSON | json | Yes | Yes | <locale>.json |
| Nested JSON | nested_json | Yes | Yes | <locale>.json |
| Angular JSON | angular_json | Yes | Yes | messages.json, messages.<locale>.json |
| String Catalog | xcstrings | Yes | Yes | Localizable.xcstrings |
| Gettext PO | po | Yes | Yes | <locale>.po |
| Gettext MO | mo | Yes | No | |
| CSV | csv | Yes | No | |
| iOS strings | ios_strings | No | Yes | <locale>.lproj/Localizable.strings, Localizable.stringsdict |
| Android XML | android_xml | No | Yes | values/strings.xml, values-<qualifier>/strings.xml |
The ID is the value you write in the format setting of easyi18n.yaml,
pass to easyi18n formats add and send to
the output formats route of the REST API.
Where formats apply
File import in the dashboard accepts .arb, .json, .xcstrings, .po,
.mo and .csv. A .json file is classified by its content, in this order:
- A root with a
sourceLanguagestring and astringsobject is a String Catalog. - A root with exactly two members, a
localestring and atranslationsobject, is Angular JSON. - A root where any value is an object is nested JSON.
- Anything else is flat JSON.
A project enables at most two output formats, under Settings → Output or
with easyi18n formats add. The dashboard export and easyi18n pull write
those formats from a published version. pull writes the files of the
format in easyi18n.yaml into its output directory, with the names in
the table above. A String Catalog is merged into the file that catalog
names.
What every output shares
Key names
A key has one stored name, written with dots: home.title. Each format
writes it in the shape its platform expects.
| Format | Written as |
|---|---|
arb | camelCase: homeTitle |
android_xml | snake_case: home_title |
json, nested_json, angular_json | The spelling of the key in the file it was imported or synced from, if it has one; otherwise the stored name |
ios_strings, po | The stored name |
xcstrings | The base-language text, or the identifier the catalog used |
A stored name with no dots, such as authLoginButton, is split on its
capital letters for Android (auth_login_button) and the export reports a
warning, because the split is a guess.
Two keys that come out with the same name in a format block the export, and no file is written:
2 keys export to the same name "home_title" in Android strings.xml. Rename one.
In nested JSON the keys a and a.b collide the same way: a cannot be a
string and an object at once.
Locale codes in file names
| Language | arb | po | json, nested_json, angular_json, ios_strings | android_xml |
|---|---|---|---|---|
pt-br | app_pt_BR.arb | pt_BR.po | pt-BR | values-pt-rBR |
zh-tw | app_zh_TW.arb | zh_TW.po | zh-TW | values-zh-rTW |
zh-hant | app_zh_Hant.arb | zh_Hant.po | zh-Hant | values-b+zh+Hant |
The base language of an Android export goes to values/strings.xml and the
base language of an Angular export to messages.json.
Placeholders
Values hold placeholders as ICU arguments: Hello, {name}!. ARB, the three
JSON formats and PO write them unchanged. ios_strings, xcstrings and
android_xml convert each {name} to a positional printf specifier,
numbered by first appearance.
| Placeholder type | ios_strings, xcstrings | android_xml |
|---|---|---|
int | %1$d | %1$d |
double | %1$f | %1$f |
| Any other type, or none declared | %1$@ | %1$s |
A literal % in a converted value is doubled: {pct}% off becomes
%1$@%% off. A value that already contains printf specifiers, such as
%d items for %@, is written as it is. A value that mixes both styles,
such as {order} is %s, is also written as it is, and the ios_strings and
android_xml exports report a warning for it.
Plurals
A plural value holds one text per CLDR category (zero, one, two, few,
many, other) and can also hold exact matches such as =0.
- ARB and the JSON formats write one ICU string:
{count, plural, =0{No items} one{1 item} other{{count} items}}. Exact matches come first, then the categories in CLDR order. ios_strings,xcstrings,android_xmlandpouse the plural structure of their platform. None of them can express an exact match, so=0is left out and the export reports a warning.- A plural with no
othertext is left out of the file and reported as an error, in every format except PO. PO writes the forms it has and reports the empty slot. - Russian, Polish, Ukrainian and Belarusian are the exception, because
otheronly applies to fractions in those languages: a plural withone,fewandmanyis written, withotherempty and a warning.
A key with no translation in a language is absent from that language's file.
The samples below are the export of one project with three keys, English as the base language and Spanish as a target.
ARB
Application Resource Bundle, the format of Flutter gen-l10n.
{
"@@locale": "en",
"@@last_modified": "2026-10-11T23:48:57.641723Z",
"@@x-generator": "easyi18n",
"homeTitle": "Welcome",
"@homeTitle": {
"description": "Home screen title"
},
"greeting": "Hello, {name}!",
"@greeting": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"cartItems": "{count, plural, =0{No items} one{1 item} other{{count} items}}",
"@cartItems": {
"placeholders": {
"count": {
"type": "int"
}
}
}
}
{
"@@locale": "es",
"@@last_modified": "2026-10-11T23:48:57.641723Z",
"@@x-generator": "easyi18n",
"homeTitle": "Bienvenido",
"greeting": "¡Hola, {name}!",
"cartItems": "{count, plural, one{1 artículo} other{{count} artículos}}"
}
- The
@keyblocks, with the description and the typed placeholders, are written in the base-language file only. @@last_modifiedis the time the file was rendered.- On import,
@@entries are ignored, each@keyblock supplies the description and placeholders of its key, and a value that is an ICU plural becomes a plural with its forms. - On import, a value that is not a string and a value that is empty are skipped with a warning.
Flat JSON
One object per language, with one string per key.
{
"home.title": "Bienvenido",
"greeting": "¡Hola, {name}!",
"cart.items": "{count, plural, one{1 artículo} other{{count} artículos}}"
}
- Plurals are ICU strings. Your library needs ICU MessageFormat support to render them.
- The file has no place for descriptions or placeholder types, so an export drops them and an import creates keys without them.
- On import, a value that is an object, a number, a boolean or an array is skipped with a warning. Import a file with nested objects as nested JSON.
Nested JSON
The same content as flat JSON, with each dot in a key name opening an object.
{
"home": {
"title": "Bienvenido"
},
"greeting": "¡Hola, {name}!",
"cart": {
"items": "{count, plural, one{1 artículo} other{{count} artículos}}"
}
}
- The separator is always a dot. An underscore does not open an object.
- On import, objects collapse back into dotted names:
home.title. - On import, a leaf that is not a string is skipped with a warning.
- The limits of flat JSON on plurals, descriptions and placeholder types apply here too.
Angular JSON
The file ng extract-i18n --format=json writes and @angular/localize
loads.
{
"locale": "es",
"translations": {
"home.title": "Bienvenido",
"greeting": "¡Hola, {name}!",
"cart.items": "{count, plural, one {1 artículo} other {{count} artículos}}"
}
}
- The file is indented with two spaces and has no final newline, and a plural has a space before each body, as in the files Angular writes.
- On import, the root must be exactly
localeandtranslations. Each id undertranslationsbecomes a key in the spelling of the file, whether it is a custom@@idor the numeric id Angular derives from the source text. - On import,
{VAR_PLURAL, plural, …}becomes a plural with its forms, and{$INTERPOLATION}and the other{$…}tokens stay in the text. localenames the language of the file and never becomes a key.
String Catalog
The .xcstrings file of Xcode 15 and later: one file that holds every
language.
{
"sourceLanguage" : "en",
"strings" : {
"Hello, {name}!" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hello, %1$@!"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "¡Hola, %1$@!"
}
}
}
},
"Welcome" : {
"comment" : "Home screen title",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bienvenido"
}
}
}
}
},
"version" : "1.0"
}
The plural key of the sample project is written as "{count} items", with a
variations.plural object per language. It is left out above for length.
- The catalog key is the base-language text. The base language gets its own
localization only when placeholder conversion changed the text, as in
Hello, {name}!above. - An entry the catalog keys by an identifier, such as
NSCameraUsageDescriptioninInfoPlist.xcstrings, keeps that identifier as its catalog key, and its text goes in the base localization. - Two keys with the same base-language text share one catalog key. Only the first is written, and the export reports a warning.
- The export does not write
extractionStateorshouldTranslate. Xcode rebuilds them, andeasyi18n pullandsyncmerge into your local catalog and keep what it already has. - On import, an entry with
"shouldTranslate": falseis skipped. - On import, device variations are not supported: the value that applies to every device is imported, and a translation that has only device-specific values is skipped.
- On import, an entry that uses named substitutions is skipped.
- On import, a translation with plural variations whose source string is not a plural keeps one form only.
Gettext PO and MO
One .po catalog per language, keyed by the key name.
msgid ""
msgstr ""
"Project-Id-Version: easyi18n\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: es\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. Home screen title
msgid "home.title"
msgstr "Bienvenido"
msgid "greeting"
msgstr "¡Hola, {name}!"
msgid "cart.items"
msgid_plural "cart.items"
msgstr[0] "1 artículo"
msgstr[1] "{count} artículos"
msgidis the key name, not the source text, andmsgid_pluralrepeats it. Descriptions are written as#.comments.- Placeholders stay as
{name}. They are not converted to%s. - Each
msgstr[N]is filled from the CLDR category that the language'sPlural-Formsrule assigns to that position. A category outside the rule is dropped with a warning. - A position with no text is written as an empty
msgstr[N]and reported as an error: a compiled catalog shows an empty string for that form. - A language with no plural rule of its own in easyi18n gets the two-form rule and a warning.
- On import,
msgidis the key and#.comments are the description. Other comments are ignored, including#, fuzzy: a fuzzy entry is imported like any other. - On import,
msgctxtis not read. Two messages that differ only in their context have the same key, and the import reports a key collision. - On import, an entry with an empty
msgstris skipped. - On import, plural positions are assigned to categories by the rule of the
language in the
Language:header, not by thenpluralsof the file. A file with noLanguage:header is read with the two-form rule.
A compiled .mo catalog can be imported, with the same rules. easyi18n does
not write .mo files.
iOS strings
Localizable.strings for texts and Localizable.stringsdict for plurals,
inside one <locale>.lproj folder per language. Output only: easyi18n does
not import .strings files.
/* Home screen title */
"home.title" = "Bienvenido";
"greeting" = "¡Hola, %1$@!";
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>cart.items</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@count@</string>
<key>count</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>1 artículo</string>
<key>other</key>
<string>%d artículos</string>
</dict>
</dict>
</dict>
</plist>
- A language gets a
.stringsdictonly if it has at least one plural key. - In a plural, the counted placeholder becomes
%d, or%fwhen it is declared as a double. - A plural form that uses a second placeholder, as in
{count} files in {folder}, is written with a positional specifier numbered after the count (%d files in %2$@), and the export reports a warning. Format the string with the count as its first argument. - A plural form for a category the language does not have in CLDR is dropped with a warning.
Android XML
Android string resources. Output only: easyi18n does not import
strings.xml files.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Home screen title -->
<string name="home_title">Bienvenido</string>
<string name="greeting">¡Hola, %1$s!</string>
<plurals name="cart_items">
<item quantity="one">1 artículo</item>
<item quantity="other">%1$d artículos</item>
</plurals>
</resources>
- Descriptions are written as XML comments.
- Texts are escaped for XML and for Android resources:
Don't <b>is written asDon\'t <b>. - A plural form for a category the language does not have in CLDR is still written, and the export reports a warning.
CSV
A comma-separated file with one row per key, for one language. Import only.
key,value,description
home.title,Welcome,Home screen title
cart.items,"{count, plural, one{1 item} other{{count} items}}",
- The columns are the key, the value and an optional description, in that order. A value that contains a comma goes in double quotes.
- The first row is treated as a header only when its first two cells are
keyandvalue, in any letter case. Any other first row is imported as a key. - A value that is an ICU plural becomes a plural with its forms.
- A row with a value and no key is skipped with a warning, and so is a row with a key and an empty value. Blank rows are ignored.
- The file carries one language. You choose which one when you import it.