A language ships on an audit and a report row, not on a native read-through
Context
ADR-0004 made English the base, made
completeness a merge gate, and left one promise outside both: that a translation would be read by
somebody who speaks the language before it shipped. scripts/translation-gate.py states it in its
own docstring — “A language ships on a native speaker’s read-through, and no script stands in for
that” — and points at a section of translator-brief.md that was never
written.
PLAN.md then said languages are not a phase of their own, and that adding a third is
an opt-in that gets a phase file the day it happens. This is that day, and it arrives with seven
languages rather than one: Czech, German, Spanish, French, Italian, Brazilian Portuguese and
Ukrainian, the same set as the reference repository next door.
Seven native reviewers were not available, and no plausible way of finding them was. Recruiting
strangers to proofread a screen dimmer for free was considered and rejected as a plan rather than
overlooked. So the promise as written had exactly one outcome: seven finished, mechanically green
drafts sitting in translations/ indefinitely, and seven markets reading an app in English while the
drafts rot against every English string added after them.
Decision
The seven languages ship without a native read-through. What replaces it is not a weaker version of the same check — it is the half of that check which does not need a native speaker, done properly, plus a channel for the half that does.
Why this is not simply lowering the bar
The read-through was doing two jobs at once, and they are not equally dangerous.
- Fluency. Wrong register, a stilted phrase, a compound a native speaker would break differently. The cost of getting this wrong is embarrassment; it is visible to every user of that language, it is cheap for any one of them to report, and nothing about it is irreversible — a string is a one-line edit and a release.
-
The three rules that outrank fluency. They are written out as
translator-brief.md§2 and they are this app’s, not craft advice:- The escape hatch stays findable.
CLAUDE.md’s first house rule is that the shade must never trap the user. Some of this copy is the only thing standing between a person and a screen they cannot read their way out of, and it makes a safety claim — that there is a Stop button outside the app, or that there is not. A translation that softens “the only way to stop dimming is this screen” into “you can also stop it here” is not a style problem. - No health claims. Play’s App content declaration was answered health-No. No eye strain, no sleep, no blue light, no filter — and the schedule and warmth copy are the two places a translator would most reasonably reach for them, because that is what apps of this shape usually say.
- Never state what the app cannot know. Gloam cannot read back the autostart grant or the battery exemption, so that copy names where to look and promises no outcome. A translation that tightens “this is the setting to look at” into “turn this on and the shade will come back” invents a guarantee the platform never gave.
- The escape hatch stays findable.
The load-bearing observation is that only the first job needs a native speaker. The second is a bounded checklist against a fixed list of resources: does this string still say the escape hatch is the only one, does it claim something about the body, does it promise something the app cannot see. Answering it needs the brief and the string, not fluency — and it was answerable, so it was answered.
What was actually done instead
Four things, all before the locales went into locales_config.xml, and all recorded in
phase-6.md:
- A forbidden-claim scan across all 99 × 8 resources, for the health and filter vocabulary of
each language rather than of English. Eighteen candidates, all eighteen morphological false
positives — Polish
oczyinside Samoczynne, Czechočiinside Samočinné, Ukrainianсонinside сонця andочіinside щоночі. Zero rule violations, and the negative result is worth as much as the method: a substring scan over an inflecting language is mostly noise, and knowing that is what makes the clean read believable. - An escape-hatch audit over the copy rule 1 covers, in all eight: every locale’s
dim_notification_warning_bodykeeps the only and names the screen, and every locale’s warning title names the notification’s button using the same word the notification’s action uses. - Two cross-string invariants turned into a test. The pairing in 2 is exactly the failure no
build could see — both halves fluent, naming two different things — so
TranslationTestnow asserts that a string quoting another by name still contains it, forshade_notification_stopinsidedim_notification_warning_titleandsettings_restartinsideschedule_rom_note. It was confirmed to fail when deliberately mispaired, because a silently skipping test is worse than none. - A report row under the language picker, wired to the existing support hand-off with a
Gloam #languagesubject of its own, and the resolved locale stamped into every report’s fact block so nobody has to say which language they are reading. This is the substantive change: the read-through was a gate before shipping, and what replaces it is a channel after.
Alternatives
- Waiting. Seven complete drafts unshipped for an unbounded time, for a review with no plausible source. Rejected; it is the status quo with extra steps.
- Shipping a subset. The languages differ in how far they are from English, not in review risk, and each shipped language pays the same completeness cost at the gate anyway. No principled line divides them.
- Paid review. Not rejected on principle. It stays available for any language that reports enough trouble to earn it, which is a better trigger than buying all seven up front on the guess that they need it.
- A
translations-pendingallowlist, so an English string could merge ahead of its translations. Rejected: the translate once, not twice argument for it assumed a round of review would reword the copy afterwards. Without that round the drafting is one pass, so the argument loses its premise. - Machine back-translation as the gate itself. It cannot see register, so it is not a substitute for a person — only for the part of a person’s job that was never about being a person. The mechanical part of it is items 1–3 above.
Consequences
- A language can now be wrong in the field, and that is accepted. The owner of this repo accepts that users will report language bugs. The exposure is bounded by the audit: what can be wrong is register and idiom, not what the app claims about the user’s body or about the way out of the shade.
- The completeness gate now demands eight translations for every future English string, with no review cycle behind it. That is the real ongoing cost of this decision and it falls on every branch that adds copy.
- Fluency findings arrive as ordinary bug reports and are fixed as ordinary strings. The one thing a report must not be allowed to do is talk the app back across the three rules — a plausible-sounding suggestion is still checked against the brief, because the reporter has the fluency and the brief has the reasoning.
- Clipping is fixed by shrinking the label, never by shortening the string. With no reviewer,
prefer the fix that needs no vocabulary judgement. German and Ukrainian are the long ones here;
dim_column_floorand the auto-off chips are where it will show first. - This changes ADR-0004’s promise, not its rule. Every user-visible string is still a resource in
every locale, translated from English only, and
TranslationTeststill keeps them level. What is retracted is read end to end by a native speaker before it ships — replaced by audited against the three rules before it ships, and reported on by the people who read it. A tenth language takes the same path. translator-brief.md§§1, 2 and 5 stop being placeholders. The audit is answerable only against a written brief, so the brief is now part of the mechanism rather than a courtesy to a future translator: §2 is the checklist item 1 and 2 above were run against.