Getting it right

A translator handed one word alone is guessing, not translating

Half the strings in an interface are one word long, and a word on its own is ambiguous in a way a sentence is not. This is about the two things that resolve it — where the string appears and how much room it has — and where to write them so the person translating actually reads them.

Published Updated

One word is not a sentence

“Open” is a button, a state and an adjective. In German it is three different words, and a locale file gives no clue which one is meant — so the translator picks, and sooner or later the app says something grammatically flawless and wrong.

No review catches it either, because the reviewer is reading the same one word. The same trap sits under “Save”, “Order”, “Close”, “Right” and most strings that are one word long — which, in an interface, is most of the strings.

what the file says
{
  "actions.open": "Open",
  "actions.close": "Close"
}

Two words. A translator picks a
German verb — and the string is
on a status chip, not a button.
what they needed to know
actions.open

  “Status of a support ticket, shown on
   a chip beside the subject. Adjective,
   not a command.”

  max 12 characters — the chip does not
  wrap.

Two things resolve nearly all of it, and neither is a translation: where the string appears, and how much room it has.

A key name can only go so far

Developers answer this by naming keys carefully, and it works — right up to the point where it stops. It is worth being precise about where that point is, because the answer is not “name them better”.

A name has to stay a name. It is an identifier in code, it cannot be a sentence, and there are three things it structurally cannot carry:

  • Where the string appears. A button, a tooltip, a page title and an error banner want four different registers, and a key can only hint at one of them by convention.
  • Which sense is meant. Homographs are the whole problem, and a path prefix does not disambiguate a word from itself.
  • How much room there is. German runs long, and it runs long into a column measured against English. A name has no way to carry a number.

Meaningful keys are still worth it — a machine draft leans on the path as much as a person does. They are the floor, not the answer.

Why a code comment never arrives

The obvious alternative is a comment beside the string in the template, and it is free. It also never reaches the one person who needs it, for three reasons that have nothing to do with tooling:

  1. The translator is not in the repository. That is the point of inviting them. A comment in a template is visible to everybody who already has the answer and to nobody who needs it.
  2. It is attached to the call, not the key. The same key is used from three components and the comment sits next to one of them. The note belongs to the string, which is the only place it can be written once.
  3. Nothing keeps it honest. A stale comment beside a changed string is invisible. A note that travels with the key through the same push and the same download comes back out to be read, which is what gets it corrected.

Which is the argument for putting it in the file the strings live in — and ARB, the format Flutter uses, has a slot for exactly this.

Where Mergua fits

Two fields on the key, neither of them a translation: a context note and a character limit. Both live on the key rather than on any one language, and neither belongs to a branch — a note written on a feature branch is a fact about the key, so it applies everywhere immediately and discarding the branch does not take it away. A note that vanished with the branch it was written on would be worth less than no note, because nobody would trust it.

FieldIn ARBWhat it is for
Context note@key.descriptionA sentence or two in prose: where the string appears, which sense of the word is meant, what a placeholder already contains. Optional, and clearable.
Character limit@key.x-maxLengthThe room the string actually has. An advisory number, not a constraint — the editor shows a translation that runs over as over, and still saves it.

Beside the field, not in a document

Context is only worth writing if it is in front of the person at the moment they type. In the panel the note sits with the string and is edited in place; in the table and inline views a marker carries it, reachable by keyboard rather than by hover alone. Over the limit, the editor shows the length against it under the field — and even with no limit set, a translation that runs far longer than its source is flagged, which is usually right and occasionally just German.

The limit warns and does not refuse. A string that runs over is shown as over and still saves, for the same reason a placeholder deviation is reported rather than rejected: whoever is typing can see the problem, and a tool that blocks the save just gets a shorter, worse sentence pasted in from somewhere else.

The AI round reads both. Alongside the source string, the same key in your other languages and its neighbours in the same file, the prompt carries the note and the limit — so the sentence you wrote for the person reviewing the string is also the sentence the machine draft is working from, and a note is worth writing whichever of the two arrives first.

The limit gets one thing more, because a model does not hold a length reliably however firmly it is asked. What comes back is measured against the number, and a value that runs over is written as a draft rather than a finished translation — the same treatment a changed placeholder gets. Still nothing is refused: the string is there, in the cell, with the badge beside it.

Written where the string is written

The best moment to explain a string is the moment you add it, and that moment happens in an editor with the component open. So both fields round-trip through ARB: write them in the repository, push them with everything else, and they land on the keys the push was allowed to write.

src/assets/i18n/en.arb
{
  "actions.open": "Open",
  "@actions.open": {
    "description": "Status of a support ticket, on a chip
                    beside the subject. Adjective.",
    "x-maxLength": 12,
    "x-screenshot": "tickets-list.png"
  }
}
what the push reports
$ curl -sf https://api.mergua.com/v1/mergua.sh \
    | sh -s -- sync --arb --path src/assets/i18n

  metadata: 3 description(s), 1 max length(s),
            2 attribute set(s) kept

Two details worth having. Attributes Mergua has no field of its own for are kept exactly as they arrived and handed back on the next download, so a pipeline of your own that writes into the same file is not fighting for the space. And the flag is the whole opt-in: without it an ARB file is refused rather than read, which is what stops a stray one from being pushed by accident. The CLI reference has the flags and the CI jobs, and the manual covers the editor field by field.

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