Skip to main content

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.

FormatIDImportOutputFiles written
ARBarbYesYesapp_<locale>.arb
Flat JSONjsonYesYes<locale>.json
Nested JSONnested_jsonYesYes<locale>.json
Angular JSONangular_jsonYesYesmessages.json, messages.<locale>.json
String CatalogxcstringsYesYesLocalizable.xcstrings
Gettext POpoYesYes<locale>.po
Gettext MOmoYesNo
CSVcsvYesNo
iOS stringsios_stringsNoYes<locale>.lproj/Localizable.strings, Localizable.stringsdict
Android XMLandroid_xmlNoYesvalues/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:

  1. A root with a sourceLanguage string and a strings object is a String Catalog.
  2. A root with exactly two members, a locale string and a translations object, is Angular JSON.
  3. A root where any value is an object is nested JSON.
  4. 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.

FormatWritten as
arbcamelCase: homeTitle
android_xmlsnake_case: home_title
json, nested_json, angular_jsonThe spelling of the key in the file it was imported or synced from, if it has one; otherwise the stored name
ios_strings, poThe stored name
xcstringsThe 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​

Languagearbpojson, nested_json, angular_json, ios_stringsandroid_xml
pt-brapp_pt_BR.arbpt_BR.popt-BRvalues-pt-rBR
zh-twapp_zh_TW.arbzh_TW.pozh-TWvalues-zh-rTW
zh-hantapp_zh_Hant.arbzh_Hant.pozh-Hantvalues-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 typeios_strings, xcstringsandroid_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_xml and po use the plural structure of their platform. None of them can express an exact match, so =0 is left out and the export reports a warning.
  • A plural with no other text 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 other only applies to fractions in those languages: a plural with one, few and many is written, with other empty 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.

app_en.arb
{
"@@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"
}
}
}
}
app_es.arb
{
"@@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 @key blocks, with the description and the typed placeholders, are written in the base-language file only.
  • @@last_modified is the time the file was rendered.
  • On import, @@ entries are ignored, each @key block 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.

es.json
{
"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.

es.json
{
"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.

messages.es.json
{
"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 locale and translations. Each id under translations becomes a key in the spelling of the file, whether it is a custom @@id or 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.
  • locale names 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.

Localizable.xcstrings
{
"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 NSCameraUsageDescription in InfoPlist.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 extractionState or shouldTranslate. Xcode rebuilds them, and easyi18n pull and sync merge into your local catalog and keep what it already has.
  • On import, an entry with "shouldTranslate": false is 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.

es.po
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"
  • msgid is the key name, not the source text, and msgid_plural repeats 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's Plural-Forms rule 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, msgid is the key and #. comments are the description. Other comments are ignored, including #, fuzzy: a fuzzy entry is imported like any other.
  • On import, msgctxt is 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 msgstr is skipped.
  • On import, plural positions are assigned to categories by the rule of the language in the Language: header, not by the nplurals of the file. A file with no Language: 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.

es.lproj/Localizable.strings
/* Home screen title */
"home.title" = "Bienvenido";
"greeting" = "¡Hola, %1$@!";
es.lproj/Localizable.stringsdict
<?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 .stringsdict only if it has at least one plural key.
  • In a plural, the counted placeholder becomes %d, or %f when 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.

values-es/strings.xml
<?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 as Don\'t &lt;b&gt;.
  • 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.

en.csv
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 key and value, 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.