AI & review

A machine can draft nine languages. It cannot count in Polish.

Machine translation handles the boring 80% of a locale file better than a tired human does. The other 20% is placeholders, plural forms and context — and that is where the bugs that reach production come from.

Published Updated

What it is good at

Machine translation is genuinely good at the bulk of a locale file, and the bulk of a locale file is boring: Cancel, Save changes, Are you sure you want to delete this project?. Short, literal, thousands of precedents. A current model gets these right more consistently than a hurried human does, because it never gets bored on string 400.

It is reliably good at:

  • Repeat terminology, once you have told it the terms. The word your product uses for a branch should be the same word in every one of 900 strings, and that is a machine's strength, not a person's.
  • Filling a new language from an existing one. Going from nine languages to ten is where the time actually goes, and a first pass that is 90% right turns weeks into a review.
  • Keeping up with a moving source string. A reworded English sentence can be re-drafted in every language the moment it changes, which is the failure mode that costs the most and gets noticed the least.

What it is bad at is not vocabulary. It is everything around the words.

Placeholders and markup

A placeholder is not text and must survive byte-for-byte. Every i18n library has its own syntax, and a model that has seen all of them will occasionally hand you the wrong one:

what a bad draft looks like
// i18next / Transloco — the braces were "translated"
"items": "{{count}} Artikel"        ✓
"items": "{ { count } } Artikel"    ✗  renders literally

// the placeholder name itself was translated
"greeting": "Hallo {{name}}"        ✓
"greeting": "Hallo {{Name}}"        ✗  undefined at runtime

// vue-i18n takes single braces, not double
"items": "{count} Artikel"          ✓
"items": "{{count}} Artikel"        ✗  renders literally

// ICU — a lost type turns a number into a string
"due": "{days, plural, ...}"        ✓
"due": "{Tage, plural, ...}"        ✗  throws on parse

All four of those failures pass a spell check. Two of them render as literal text in production, one throws, and the last one — the translated placeholder name — is the one that reaches a customer, because it looks completely fine in a review UI.

The same applies to inline markup. If your strings contain <strong> or a component placeholder like i18next's <1>, a model will sometimes reorder the tags to suit the target language's word order — which is correct translation and broken code, unless your library supports it.

This is the one thing to automate first: a check that the set of placeholders in the translation equals the set in the source. It is twenty lines, it runs in CI, and it catches the whole class. What such a check has to compare is more than the braces, though: printf conversions, references to other keys and the structure inside an ICU plural all count.

Plurals are not one problem

English has two plural forms. That is a property of English, not of counting. CLDR defines six categories — zero, one, two, few, many, other — and languages use different subsets of them:

LanguageFormsWhich
Japanese, Chinese, Vietnamese1other
English, German, Spanish2one, other
Polish, Russian, Czech3–4one, few, many, other
Arabic6all of them

So translating a plural string is not translating text — it is producing a different number of strings than you started with. Ask a model for “the Polish translation of {count} items” and you get one sentence, in one form, which is wrong for three quarters of all numbers.

Whatever your library's plural syntax is, the AI has to be told the target language's categories and asked for all of them:

the shape the answer has to have
// One request, all of the target language's categories —
// not one sentence to be pluralised later.
{
  "locale": "pl",
  "key": "cart.items",
  "source": "{count} items",
  "forms": {
    "one":   "{count} przedmiot",
    "few":   "{count} przedmioty",
    "many":  "{count} przedmiotów",
    "other": "{count} przedmiotu"
  }
}

The same holds for grammatical gender, formality (German du against Sie, Japanese politeness levels) and any string where a name gets inserted mid-sentence. None of these are translation errors a spell check finds, and all of them are answered by a decision that belongs to you rather than to the model.

The missing context

A locale file hands a translator a sentence and nothing else. The classic example is Save: a verb on a button, a noun in a heading, and different words in most languages. So is Open, and every string that is one word long.

Two of those are worth writing down once rather than inferring every time — see context for translators, on the note and the character limit that travel with the key.

Three things you already have and are probably not passing:

  • The key.settings.billing.actions.save says far more than Save does. If your keys are meaningful — and this is the argument for making them meaningful — the path is free context.
  • The neighbours. The other strings in the same namespace, translated together rather than one request per string, keep a screen consistent with itself.
  • A note from whoever wrote it. One line — “button, destructive, appears in a confirm dialog” — removes most of the ambiguity a model would otherwise guess at. It also helps the human reviewer, which is the reason to write it even if you never automate anything.

And one thing to decide out loud: length. German runs roughly a third longer than English, and a model has no idea your button is 96 pixels wide. If a string lives somewhere that cannot grow, that is a constraint to state, not a surprise to discover in a screenshot.

Reviewing without reading twice

The point of a machine draft is to stop reading 900 strings. If the review is “someone fluent reads all of it”, you have paid for the translation twice and saved nothing. Sort by what it costs to be wrong instead:

  1. Always read: anything legal, anything about money, error messages, and every string in a destructive flow. Small in number, expensive to get wrong.
  2. Always check mechanically: placeholders, plural forms, markup, and length against a per-string budget. No human should be doing this by eye.
  3. Sample the rest. If a random 30 strings from a namespace are clean, the namespace is probably clean. If three are wrong, read the namespace.
  4. Never review a language nobody in the loop reads. A rubber-stamp review is worse than an honest “machine-translated, unreviewed” marker, because it destroys the marker's meaning for the languages you do review.

That last one is the reason a translation's state matters as much as its text. A file that cannot tell you which strings a human has seen cannot be reviewed incrementally — every release starts from zero.

Where it belongs in the loop

Put the machine at the start, never at the end. A draft that arrives before a human looks at the string saves the human the boring 80%; a draft that goes straight to production removes the only step that would have caught the placeholder it ate.

Concretely, a loop that works:

  1. A new source key is added on a branch, by whoever wrote the UI.
  2. Every target language gets a machine draft immediately, marked as a draft — visibly, in the tool and in the file's own state.
  3. The mechanical checks run. Failures go back to step 2 rather than to a person.
  4. A reviewer works through what is left, in the priority order above.
  5. Nothing reaches the production locale files without having passed step 3, whether or not it passed step 4 — and if it skipped step 4, that is recorded.

See translations on feature branches for the branch half of this, which is what makes step 5 enforceable rather than a habit.

Where Mergua fits

Mergua's AI translation is step 2 of that loop: a draft per language, marked as a draft, with the key path and the surrounding strings passed as context, and placeholders treated as part of the string rather than as words. Nothing it writes counts as reviewed until a person says so, and the review state travels with the string.

The features page has the editor and the review states; the review docs have the workflow.

Keep reading

Try it on one branch.

Upload the locale files you already have and see the whole loop — branch, translate, review, sync — on the free tier.

Create your account