The restored check reported 41 stale keys per catalogue. Ten of them were.
Why 31 were false
Strings held in a constant and translated where they render — t(b.description), t(group), t(c.label) — reach t() as a variable, so there is no literal at the call site for a static scan to find.
The script already chased two of those shapes (label: properties, objects named *_LABELS) with a comment about having cried wolf 33 times. The shapes kept coming:
Reported stale
Actually lives in
Go to Inbox, Archive and next, Send message, …
description: on keyboard bindings, via t(b.description)
Actions, Navigation, Conversation
group: on the same bindings
Day view, Week view, Month view, Agenda view
the calendar's view table
the five read-receipt refusals
the MDN reason map
Classic
palette.ts
Deleting any of them would have un-translated that text in nine languages. The folder names were a near miss of the same kind — tc("folder", "Inbox") builds a contextual key that appears nowhere as one literal — and only survived because the script already had a tc() case.
The fix
Chasing shapes one at a time is the wrong shape of fix. Stale detection now asks only "is this key still written down anywhere in the source" — any string literal counts.
That under-reports, deliberately. A missed stale key costs one line of dead translation; a false one costs the credibility of the check and every real finding after it. Which is exactly what happened — a report that was three-quarters noise is one nobody acts on, and these sat unread long enough to need their own commit.
Two sets, because there are two questions.wanted is what a catalogue owes and stays strict — coverage is measured against it, and widening it counted every CSS class and JMAP method name as an untranslated string (it read 29% while the two shared a set). seen is every literal, and answers only whether a key is still written down.
What was actually dead
Removed from all nine catalogues:
Availability on {date}, Import vCard, PDF
two settings hints, both replaced by rewordings that are still live
the Catppuccin palette description
Tuesday, Wednesday, Thursday, Friday — left behind when the week-start dropdown narrowed to Monday/Saturday/Sunday, and appearing since only in comments
Checks
npm run i18n:check — no stale section at all, nine locales clean, exit 0
coverage unchanged at 1269/1285 (99%) — none of the ten was ever owed, so removing them moves nothing
npm run typecheck — clean, both packages
npm test — 1168 web tests, 168 server tests, all passing
npm run build — clean
The report also prints the whole list now rather than the first 25. With the false positives gone it is short, and a truncated list of dead keys is one somebody has to run twice to act on.
The restored check reported **41 stale keys** per catalogue. **Ten of them were.**
## Why 31 were false
Strings held in a constant and translated where they render — `t(b.description)`, `t(group)`, `t(c.label)` — reach `t()` as a *variable*, so there is no literal at the call site for a static scan to find.
The script already chased two of those shapes (`label:` properties, objects named `*_LABELS`) with a comment about having cried wolf 33 times. The shapes kept coming:
| Reported stale | Actually lives in |
|---|---|
| `Go to Inbox`, `Archive and next`, `Send message`, … | `description:` on keyboard bindings, via `t(b.description)` |
| `Actions`, `Navigation`, `Conversation` | `group:` on the same bindings |
| `Day view`, `Week view`, `Month view`, `Agenda view` | the calendar's view table |
| the five read-receipt refusals | the MDN reason map |
| `Classic` | `palette.ts` |
Deleting any of them would have un-translated that text in nine languages. The folder names were a near miss of the same kind — `tc("folder", "Inbox")` builds a contextual key that appears nowhere as one literal — and only survived because the script already had a `tc()` case.
## The fix
Chasing shapes one at a time is the wrong shape of fix. Stale detection now asks only **"is this key still written down anywhere in the source"** — any string literal counts.
That under-reports, deliberately. A missed stale key costs one line of dead translation; a false one costs the credibility of the check and every real finding after it. Which is exactly what happened — a report that was three-quarters noise is one nobody acts on, and these sat unread long enough to need their own commit.
**Two sets, because there are two questions.** `wanted` is what a catalogue *owes* and stays strict — coverage is measured against it, and widening it counted every CSS class and JMAP method name as an untranslated string (it read **29%** while the two shared a set). `seen` is every literal, and answers only whether a key is still written down.
## What was actually dead
Removed from all nine catalogues:
- `Availability on {date}`, `Import vCard`, `PDF`
- two settings hints, both replaced by rewordings that are still live
- the Catppuccin palette description
- `Tuesday`, `Wednesday`, `Thursday`, `Friday` — left behind when the week-start dropdown narrowed to Monday/Saturday/Sunday, and appearing since only in comments
## Checks
- `npm run i18n:check` — **no stale section at all**, nine locales clean, exit 0
- coverage unchanged at **1269/1285 (99%)** — none of the ten was ever owed, so removing them moves nothing
- `npm run typecheck` — clean, both packages
- `npm test` — 1168 web tests, 168 server tests, all passing
- `npm run build` — clean
The report also prints the whole list now rather than the first 25. With the false positives gone it is short, and a truncated list of dead keys is one somebody has to run twice to act on.
**Merged** 2026-09-10 as coffey-labs/ihasmail@c3d2dc241819
<sub>Rebuilt from: GH Archive, git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The restored check reported 41 stale keys per catalogue. Ten of them were.
Why 31 were false
Strings held in a constant and translated where they render —
t(b.description),t(group),t(c.label)— reacht()as a variable, so there is no literal at the call site for a static scan to find.The script already chased two of those shapes (
label:properties, objects named*_LABELS) with a comment about having cried wolf 33 times. The shapes kept coming:Go to Inbox,Archive and next,Send message, …description:on keyboard bindings, viat(b.description)Actions,Navigation,Conversationgroup:on the same bindingsDay view,Week view,Month view,Agenda viewClassicpalette.tsDeleting any of them would have un-translated that text in nine languages. The folder names were a near miss of the same kind —
tc("folder", "Inbox")builds a contextual key that appears nowhere as one literal — and only survived because the script already had atc()case.The fix
Chasing shapes one at a time is the wrong shape of fix. Stale detection now asks only "is this key still written down anywhere in the source" — any string literal counts.
That under-reports, deliberately. A missed stale key costs one line of dead translation; a false one costs the credibility of the check and every real finding after it. Which is exactly what happened — a report that was three-quarters noise is one nobody acts on, and these sat unread long enough to need their own commit.
Two sets, because there are two questions.
wantedis what a catalogue owes and stays strict — coverage is measured against it, and widening it counted every CSS class and JMAP method name as an untranslated string (it read 29% while the two shared a set).seenis every literal, and answers only whether a key is still written down.What was actually dead
Removed from all nine catalogues:
Availability on {date},Import vCard,PDFTuesday,Wednesday,Thursday,Friday— left behind when the week-start dropdown narrowed to Monday/Saturday/Sunday, and appearing since only in commentsChecks
npm run i18n:check— no stale section at all, nine locales clean, exit 0npm run typecheck— clean, both packagesnpm test— 1168 web tests, 168 server tests, all passingnpm run build— cleanThe report also prints the whole list now rather than the first 25. With the false positives gone it is short, and a truncated list of dead keys is one somebody has to run twice to act on.
Merged 2026-09-10 as coffey-labs/ihasmail@c3d2dc2418
Rebuilt from: GH Archive, git history, session transcript.