Follow-up to #347, where the message-list splitter's ariaLabel="Resize message list" turned out to have passed npm run i18n:check.
The gaps in the checker
Component props weren't checked.scripts/i18n-literals.mjs checked title, aria-label, placeholder and alt on JSX elements. The component-prop names (label, ariaLabel, message, hint, …) were only checked inside object literals, so <MenuItem label="…"> and <Popover ariaLabel="…"> passed.
A catalogue key excused a JSX literal. That exemption is for English held in a constant and translated where it renders (label: "About" … t(s.label)). A literal written directly into a JSX attribute has no such render site. No component here runs its props through t() (grepped), so a matching key means a translation exists that the string never reaches.
Nothing could fail. Both scripts exit non-zero only with --check, and npm run i18n:check passed it to neither.
All three are fixed. Deliberately reverting one fixed file makes npm run i18n:check exit 1.
What that found: 28 strings rendering English in every language
New keys (9), added to all nine catalogues:
Hide from list / Show in list (folder menu)
Stop trusting sender images / Always show images from sender
Collapse all / Expand all (thread menu)
Send now instead
Switch to plain text / Switch to rich text
The wording follows each catalogue's existing terms: Absender / remitente / expéditeur / 差出人, "Plain text", "Rich text (HTML)" and so on.
Already had keys, now wrapped (19): Report spam / Not spam (list, context menu, thread toolbar), Mark as read / unread, Add / Remove star, New contact, Choose a date, Choose a date and time. The contact editor's Save / Saving… buttons were wrapped too, on the same line, although the checker doesn't look at JSX children.
Translation
9 new keys × 9 catalogues. Each catalogue goes from 1414/1430 to 1425/1441 translated, and the fallback count stays at 16, so nothing new falls back to English. Extraction is 1160 wrapped, 0 remaining.
Not covered
English built with a template literal inside an attribute, e.g. aria-label={`Remove ${email}`}, can't be a catalogue key as written and isn't flagged. The remaining cases are listed in KNOWN-ISSUES: calendar and contacts sidebars, recipient chips, address menu, attachment titles, and the contact editor's Edit … title. They need interpolated keys, which is a separate change.
Checked
npm run i18n:check (exit 0, and exit 1 with a fix reverted), npm run typecheck -w web, npm run build -w web, npm test -w web (1215 passed). The translated labels have not been viewed in the browser. Every change is a string wrapped in a call, and typecheck confirms each file imports the wrapper it uses.
Follow-up to #347, where the message-list splitter's `ariaLabel="Resize message list"` turned out to have passed `npm run i18n:check`.
## The gaps in the checker
1. **Component props weren't checked.** `scripts/i18n-literals.mjs` checked `title`, `aria-label`, `placeholder` and `alt` on JSX elements. The component-prop names (`label`, `ariaLabel`, `message`, `hint`, …) were only checked inside object literals, so `<MenuItem label="…">` and `<Popover ariaLabel="…">` passed.
2. **A catalogue key excused a JSX literal.** That exemption is for English held in a constant and translated where it renders (`label: "About"` … `t(s.label)`). A literal written directly into a JSX attribute has no such render site. No component here runs its props through `t()` (grepped), so a matching key means a translation exists that the string never reaches.
3. **Nothing could fail.** Both scripts exit non-zero only with `--check`, and `npm run i18n:check` passed it to neither.
All three are fixed. Deliberately reverting one fixed file makes `npm run i18n:check` exit 1.
## What that found: 28 strings rendering English in every language
**New keys (9), added to all nine catalogues:**
- Hide from list / Show in list (folder menu)
- Stop trusting sender images / Always show images from sender
- Collapse all / Expand all (thread menu)
- Send now instead
- Switch to plain text / Switch to rich text
The wording follows each catalogue's existing terms: Absender / remitente / expéditeur / 差出人, "Plain text", "Rich text (HTML)" and so on.
**Already had keys, now wrapped (19):** Report spam / Not spam (list, context menu, thread toolbar), Mark as read / unread, Add / Remove star, New contact, Choose a date, Choose a date and time. The contact editor's **Save** / **Saving…** buttons were wrapped too, on the same line, although the checker doesn't look at JSX children.
## Translation
9 new keys × 9 catalogues. Each catalogue goes from 1414/1430 to 1425/1441 translated, and the fallback count stays at **16**, so nothing new falls back to English. Extraction is 1160 wrapped, 0 remaining.
## Not covered
English built with a template literal inside an attribute, e.g. ``aria-label={`Remove ${email}`}``, can't be a catalogue key as written and isn't flagged. The remaining cases are listed in KNOWN-ISSUES: calendar and contacts sidebars, recipient chips, address menu, attachment titles, and the contact editor's `Edit …` title. They need interpolated keys, which is a separate change.
## Checked
`npm run i18n:check` (exit 0, and exit 1 with a fix reverted), `npm run typecheck -w web`, `npm run build -w web`, `npm test -w web` (1215 passed). The translated labels have not been viewed in the browser. Every change is a string wrapped in a call, and typecheck confirms each file imports the wrapper it uses.
**Merged** 2026-09-14 as coffey-labs/ihasmail@aa57c597037e
<sub>Rebuilt from: 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.
Follow-up to #347, where the message-list splitter's
ariaLabel="Resize message list"turned out to have passednpm run i18n:check.The gaps in the checker
scripts/i18n-literals.mjscheckedtitle,aria-label,placeholderandalton JSX elements. The component-prop names (label,ariaLabel,message,hint, …) were only checked inside object literals, so<MenuItem label="…">and<Popover ariaLabel="…">passed.label: "About"…t(s.label)). A literal written directly into a JSX attribute has no such render site. No component here runs its props throught()(grepped), so a matching key means a translation exists that the string never reaches.--check, andnpm run i18n:checkpassed it to neither.All three are fixed. Deliberately reverting one fixed file makes
npm run i18n:checkexit 1.What that found: 28 strings rendering English in every language
New keys (9), added to all nine catalogues:
The wording follows each catalogue's existing terms: Absender / remitente / expéditeur / 差出人, "Plain text", "Rich text (HTML)" and so on.
Already had keys, now wrapped (19): Report spam / Not spam (list, context menu, thread toolbar), Mark as read / unread, Add / Remove star, New contact, Choose a date, Choose a date and time. The contact editor's Save / Saving… buttons were wrapped too, on the same line, although the checker doesn't look at JSX children.
Translation
9 new keys × 9 catalogues. Each catalogue goes from 1414/1430 to 1425/1441 translated, and the fallback count stays at 16, so nothing new falls back to English. Extraction is 1160 wrapped, 0 remaining.
Not covered
English built with a template literal inside an attribute, e.g.
aria-label={`Remove ${email}`}, can't be a catalogue key as written and isn't flagged. The remaining cases are listed in KNOWN-ISSUES: calendar and contacts sidebars, recipient chips, address menu, attachment titles, and the contact editor'sEdit …title. They need interpolated keys, which is a separate change.Checked
npm run i18n:check(exit 0, and exit 1 with a fix reverted),npm run typecheck -w web,npm run build -w web,npm test -w web(1215 passed). The translated labels have not been viewed in the browser. Every change is a string wrapped in a call, and typecheck confirms each file imports the wrapper it uses.Merged 2026-09-14 as coffey-labs/ihasmail@aa57c59703
Rebuilt from: git history, session transcript.