Check component props for untranslated literals, and fail on a finding #348

Closed
opened 2026-09-14 15:03:10 +00:00 by jcoffey-dev · 0 comments
Owner

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@aa57c59703

Rebuilt from: git history, session transcript.

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.