Finish extraction: 100%, and a coverage number worth believing #148

Closed
opened 2026-08-31 17:41:40 +00:00 by jcoffey-dev · 0 comments
Owner

Extraction complete: 596 strings wrapped, 0 remaining. Up from 78%.

The 143 the codemod refused were two different things

A third were phrases next to an icon<Plus /> New rule. The refusal rule was "has siblings", which is broader than the actual danger: what breaks a translation is a sibling that renders text, splitting a sentence into fragments nobody can reorder. An element beside a phrase doesn't. Narrowing the rule let the codemod take 73 more.

The rest were real sentences with values in them, rebuilt by hand as named placeholders — "Your active script “{name}” was written by hand", "Waiting on the server — goes out {when}." Named rather than positional, because translators move the parts around. Counted things go through plural() so Russian and Ukrainian get three forms rather than English's two.

One new primitive: tNode()

Open <code>mailto:</code> links in ihasmail has two obvious treatments and both are wrong. Splitting into two t() calls hands over fragments that can't be reordered. Dropping the <code> keeps the sentence whole but loses the monospace that said this is a literal.

tNode() keeps the sentence whole and makes the element a named hole in it:

tNode("Open {scheme} links here", { scheme: <code>mailto:</code> })

A translator sees one sentence with a hole and puts it where their language wants it. The German test asserts exactly that — the same call renders the code first when the catalogue says so.

The coverage number was lying

It counted text inside <code> and translate="no" as untranslated work, plus placeholders like "123456" and "+1 555 0100" — a one-time code and a phone format. None of those will ever be translated, so the report sat at 21 remaining when only 6 were real.

A number with an unreachable floor is something to argue with rather than act on. The tool now applies the same rules the codemod does, and reads 100% because that's true.

Verification

  • 447 web tests, 109 server tests, typecheck and build clean.
  • Browser sweep across 15 views — this is where the last bulk pass hid a bug the tests couldn't see. No entity leaks, no unfilled {placeholders} in rendered text, no raw t( leaking through. The nine "empty labels" it flagged on Settings are the toggle switches, which are text-free by design and labelled by their adjacent row.
  • Visual check confirms the earlier Language &amp; region fix, interpolation (Browser default (America/Phoenix)), and plural() (1 selected).

Still no user-visible change — English remains the only language in the picker.

What's next

The machinery is done. What's left is the part the memory from the last attempt is blunt about: a real catalogue, and a native speaker to read it. German first, per the roadmap order. Adding it to UI_LANGUAGES is the one line that makes any of this visible, and should be the last thing that happens, not the first.

Merged 2026-08-31 as coffey-labs/ihasmail@d0cbfc7870

Rebuilt from: git history, session transcript.

Extraction complete: **596 strings wrapped, 0 remaining.** Up from 78%. ## The 143 the codemod refused were two different things **A third were phrases next to an icon** — `<Plus /> New rule`. The refusal rule was "has siblings", which is broader than the actual danger: what breaks a translation is a sibling that renders *text*, splitting a sentence into fragments nobody can reorder. An element beside a phrase doesn't. Narrowing the rule let the codemod take 73 more. **The rest were real sentences with values in them**, rebuilt by hand as named placeholders — `"Your active script “{name}” was written by hand"`, `"Waiting on the server — goes out {when}."` Named rather than positional, because translators move the parts around. Counted things go through `plural()` so Russian and Ukrainian get three forms rather than English's two. ## One new primitive: `tNode()` `Open <code>mailto:</code> links in ihasmail` has two obvious treatments and both are wrong. Splitting into two `t()` calls hands over fragments that can't be reordered. Dropping the `<code>` keeps the sentence whole but loses the monospace that said *this is a literal*. `tNode()` keeps the sentence whole and makes the element a named hole in it: ```tsx tNode("Open {scheme} links here", { scheme: <code>mailto:</code> }) ``` A translator sees one sentence with a hole and puts it where their language wants it. The German test asserts exactly that — the same call renders the code *first* when the catalogue says so. ## The coverage number was lying It counted text inside `<code>` and `translate="no"` as untranslated work, plus placeholders like `"123456"` and `"+1 555 0100"` — a one-time code and a phone format. None of those will ever be translated, so the report sat at **21 remaining when only 6 were real**. A number with an unreachable floor is something to argue with rather than act on. The tool now applies the same rules the codemod does, and reads 100% because that's true. ## Verification - 447 web tests, 109 server tests, typecheck and build clean. - **Browser sweep across 15 views** — this is where the last bulk pass hid a bug the tests couldn't see. No entity leaks, no unfilled `{placeholders}` in rendered text, no raw `t(` leaking through. The nine "empty labels" it flagged on Settings are the toggle switches, which are text-free by design and labelled by their adjacent row. - Visual check confirms the earlier `Language &amp; region` fix, interpolation (`Browser default (America/Phoenix)`), and `plural()` (`1 selected`). Still no user-visible change — English remains the only language in the picker. ## What's next The machinery is done. What's left is the part the memory from the last attempt is blunt about: **a real catalogue, and a native speaker to read it.** German first, per the roadmap order. Adding it to `UI_LANGUAGES` is the one line that makes any of this visible, and should be the last thing that happens, not the first. **Merged** 2026-08-31 as coffey-labs/ihasmail@d0cbfc7870a7 <sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.