You ask an AI to translate your app. It could be ChatGPT, or the coding agent in your editor. It writes the Ukrainian file, the Flutter build passes, and you ship.
Two of those strings are broken. Neither one stops the build, and you can't see either one unless you read Ukrainian.
Here are both, in English terms.
Bug 1: the total with no number
Your English string has a placeholder for the amount:
"checkoutTotal": "Total: {amount}"
The Ukrainian translation came back like this:
"checkoutTotal": "Разом до сплати"
It means "Total to pay". The {amount} is gone. flutter gen-l10n exits 0 and says nothing about it. The generated method still takes the amount as a parameter and never puts it in the text, so your Ukrainian users see a checkout total with no number.
Bug 2: "2 items" in the wrong grammar
English has two plural forms: 1 item, 2 items. Ukrainian has four, and the number decides which one you need. 2 to 4 take one form, 5 to 20 take another.
The AI translated the two forms English has, and stopped:
"itemsInCart": "{count, plural, one{{count} товар} other{{count} товарів}}"
So for 2 items the badge falls back to the only other form it has, the one for 5 to 20. It reads "2 товарів" where a Ukrainian speaker says "2 товари". In English terms, it's a badge that says "2 of items". The build passes.
You don't need Ukrainian to catch either one
Both bugs are structural. The English string has a placeholder and the translation doesn't. The language needs four plural forms and the file has two. Neither needs anyone to understand the words.
I build a free CLI that checks exactly this, called StringLane. Run it in the project folder:
$ npx @stringlane/cli check
flutter · arb · single-origin · base en
3 locales · 3 keys · 86% complete
de 4/4 complete
en 4/4 complete
uk 4/6 2 missing
3 errors · 0 warnings · 0 info
error placeholder_mismatch uk/checkoutTotal
error missing_key uk/itemsInCart[few]
error missing_key uk/itemsInCart[many]
That output is trimmed: each error also says in a sentence what is wrong. It exits 1, so the same line in CI stops the merge:
- run: npx @stringlane/cli check
It reads Flutter ARB, iOS String Catalogs and .strings, Android XML and i18next JSON. It needs no account and no key, and CI runs send no usage data.
If the AI is your coding agent
The CLI also runs as an MCP server, so an agent can translate through it and have each translation checked before it is written. Agents still write locale files directly sometimes, with their own edit tool or a quick script, and the server never sees those writes. So keep the check in CI either way. How that works per agent, and what each one can and can't guard, is in the full write-up.
What it won't tell you
Whether a translation is good. It checks structure: missing keys and plural forms, placeholders, ICU syntax. Whether "2 товари" is the right word for your badge still needs someone who understands Ukrainian.
Top comments (1)
Does placeholder matching work per ICU branch, or only over the whole message? A useful negative fixture would keep {amount} in one select branch but drop it from another. The message-wide placeholder set would still match while one checkout path loses the total.
I'd pair that with a valid translation that moves a shared placeholder outside the select, so the check doesn't require identical sentence structure. I haven't run StringLane; this is a parser-fixture suggestion for the structural boundary you describe, not a translation-quality check.