Skip to content
Novus Examples
3 min readNovus ExamplesGuideIntermediatev2026.09

Test Localization Pipelines With Real Catalogs

Arabic has six plural forms, Polish has four, and most i18n bugs are a missing one. Eleven formats, the plural rules that break them, and what RTL actually requires.

The bug is almost never the translation

It is the plural form nobody implemented, the format that stores plurals differently from the one you tested, the string concatenated from two fragments that reorder in the target language, or a layout that assumed text flows one way.

The localization catalogue is 161 fixtures across eleven formats, and it is organised around those failures rather than around languages.

Plural forms, which is the one to get right first

English has two plural forms. Most developers write code that assumes two, and it works until it reaches a language that does not.

  • Arabic has six: zero, one, two, few, many, other.
  • Polish has four, and which one applies depends on the last two digits, not on the magnitude.
  • Japanese and Chinese have one.

The catalogue carries the same plural message across formats and languages so you can test the same rule through whatever your stack actually uses:

  • ICU: icu-plural-select-english, icu-plural-arabic-rtl
  • Android: android-quantity-strings-en, -pl, -ar
  • Apple: apple-stringsdict-en, -pl, -ar
  • Flutter: flutter-arb-plurals-en, -pl, -ar
  • XLIFF: xliff-1-2-plurals-en-pl, xliff-2-0-plurals-en-pl

The test that matters is not "does it render". It is: give it a count of 0, 1, 2, 3, 5, 11, 22 and 101, and check every one against the language's actual rule. A pipeline that handles one and other will produce grammatically wrong text for the majority of counts in Polish and Arabic, and it will never throw.

plural-forms-en and the plural group exist so you can assert the rule itself rather than a rendering of it.

Eleven formats, because your pipeline probably touches three

gettext (.pot, .po, .mo), XLIFF 1.2 and 2.0, Apple .strings and .stringsdict, Android strings.xml, .NET .resx, Flutter .arb, i18next JSON, Fluent .ftl, ICU MessageFormat, and TMX.

Two distinctions catch people:

  • .po is source, .mo is compiled. gettext-fr-po and gettext-fr-mo are the same catalog in both states. A build that ships the .po and expects the runtime to read it produces a completely untranslated app with no error at all.
  • XLIFF 1.2 and 2.0 are structurally different, not a version bump. A parser written for one will find nothing in the other rather than failing loudly.

xliff-1-2-inline-en-de covers inline markup, which is where placeholders and formatting tags live and where translators most often break a string by moving or dropping a tag.

Right-to-left is a layout problem, not a font problem

i18next-json-arabic-rtl, i18next-json-hebrew-rtl, rtl-ui-pack-ar, rtl-ui-pack-he, rtl-ui-pack-ar-eg, plus fluent-ftl-ar and -he and icu-messageformat-ar and -he.

Translating to Arabic and leaving the layout alone gives you correct words in the wrong order visually: navigation on the wrong side, progress bars filling backwards, icons pointing the wrong way. The RTL UI packs exist to be dropped into a real interface rather than read, because the failures only appear once the strings are in a layout.

icu-select-hebrew-rtl pairs with icu-select-english for the select construct, which is how gender and other non-numeric variants are expressed and is routinely lost in translation round-trips.

Composite formats, where an argument order changes

dotnet-resx-composite-formats-en-us, -de-de, -ar-eg.

A string like {0} added {1} to {2} has an argument order that may legitimately differ in the target language. Code that builds the sentence by concatenation instead of by format string cannot express that at all, and the translation ends up either wrong or unnatural. These three are the same message in three languages where the natural order differs.

Encodings and scripts

fb2-p7-windows-1251-encoded lives in the ebooks set but the lesson carries: not every catalog is UTF-8. i18next-json-japanese and i18next-json-chinese cover CJK, where line breaking has no spaces to break on and a naive truncation cuts mid-word every time.

A test list that fits in an afternoon

  1. Every plural form for Polish and Arabic at counts 0, 1, 2, 3, 5, 11, 22, 101.
  2. The .po / .mo pair, asserting the runtime reads the compiled one.
  3. One XLIFF 1.2 and one 2.0 through the same importer.
  4. A composite-format string in a language where argument order changes.
  5. An RTL UI pack in a real layout, looking at direction rather than glyphs.
  6. A CJK string through whatever truncates text in your UI.

Each fixture is small and generated, so they belong in your own repository as regression inputs rather than being fetched at test time.

Continue this workflow

Try the workflow

Documentation and troubleshooting

Was this article helpful?

Found an error? Send a correction.