A missing translation key is not an error anywhere, which is why users see them
Every plural key in the mobile application was dead. The counts on the sync bar — writes pending, writes failed — had keys defined for the plural case, they looked correct in the file, and not one of them had ever rendered.
The translation library had moved to a different plural format two major versions earlier. The old suffix was no longer recognised, so the library fell back to the singular form and displayed it for every count. No warning. The keys were present, spelled correctly, and inert.
That is the shape of the whole problem. Nothing in a typical stack treats a missing or unused translation key as an error. The type checker does not — the key is a string. The build does not — the file is valid. The tests do not, unless somebody wrote one specifically. The library does not, because falling back is what a translation library is supposed to do when a locale is incomplete. Every layer is being helpful, and the sum of the help is that a user reads a raw identifier on screen and the application reports full health.
Forty-seven codes with no copy
The larger instance was on the server boundary.
The API returns error codes rather than prose, deliberately, so that clients localise. That is the right design — the server has no business knowing what language the reader speaks — and it creates an obligation the design does not enforce: every code the server can emit needs copy on every client.
A sweep of the codes the API actually emits against the web client’s translation file found forty-seven with nothing behind them. Not obscure ones, either — several were on paths a user hits in normal work. Those users were reading the identifier itself where a sentence should have been.
Nobody had done anything wrong in any single change. Each round of API work added a few new codes. Each round of client work added copy for the codes it knew about. The gap accumulated one release at a time, invisibly, because the failure renders as text and text looks like a rendering decision rather than a defect.
The corrected copy does two things rather than one: it says what happened, and it says what to do next. A message that only names the failure leaves the user with nowhere to go, and the support call is the same cost as no message at all.
The check that closes it
The fix that lasts is not the forty-seven strings. It is a script, committed and run as part of the routine checks, that does three things:
Resolve every literal key used in the client. Extract every translation call site, look each key up in the actual translation file through the actual library, and fail on anything that does not resolve. Not a static comparison of two lists — the library’s own lookup, because the library’s lookup is what runs in production, and the plural failure above was invisible to any comparison that did not go through it.
Evaluate every count-bearing key at zero, one and more than one. A key used with a count that has no plural form is a defect even though it resolves. Two of them were rendering “1 days” to users.
Compare the codes the API can emit against the codes the client has copy for. Both directions are informative. Missing copy is a user-visible defect. Copy for a code that no longer exists is dead weight and usually a sign the code was renamed on one side only.
The number the sweep reports is the useful artefact. Ninety-seven codes emitted by the API, none missing from the client, checked automatically rather than asserted in a status update. That is a number that means something. “We added the missing translations” is not.
Formatting is part of localisation
One more class of failure lives in the same area and does not look like translation at all.
A stock alert notification was rendering a quantity as it came off the wire — a decimal with four places of scale, because that is how the API sends quantities to avoid floating-point error. The message read like a database value, in the middle of a sentence written for a person.
The values interpolated into a translated string need the same locale-aware formatting as anything else on the screen: numbers through the number formatter, money through the money formatter, dates through the date formatter. It is easy to miss because the string itself is translated and looks finished. The parameters are the untranslated part.
A key that resolves is not the same as a message that reads correctly. The check can catch the first. Only somebody looking at the rendered output catches the second.
Why this is worth the effort before you have a second language
The whole discipline is easy to postpone when the product ships in one language. There is no translation yet, so what is the harm in a hardcoded string.
The harm is that the errors above are not translation errors. They are correctness errors that happen to live in the translation layer, and every one of them was user-visible in the only language the product supports. A raw key on screen is a bug in English. A quantity rendered with four decimal places is a bug in English. “1 days” is a bug in English.
Doing it properly from the start is not preparation for a future locale. It is the mechanism that makes user-facing text checkable at all, and the second language is a benefit that arrives later.
Rules
Resolve every key through the real library, not by comparing lists. Format changes and fallback behaviour are invisible to a static comparison, and fallback is exactly what hides the failure.
Test every count key at zero, one and more. Then again after any library upgrade, because plural handling is where translation libraries make breaking changes.
Sweep the server’s error codes against the client’s copy, both directions, as a committed check. The gap accumulates one release at a time and is invisible in any single review.
Format interpolated values. A translated sentence with a raw decimal in it is not localised.
Write copy that says what to do next, not only what went wrong.
The honest limit: none of this tells you whether the copy is any good. It tells you the message exists, resolves, and pluralises. Whether it helps the person reading it is a judgement, and it is still made by reading the screen.