Follow-up to #348, which left English built with template literals as the one known gap.
Checker
scripts/i18n-literals.mjs now flags a template literal in a UI attribute or prop (the same positions #348 covered) when there are words between its values. Neither the catalogue-key exemption nor the sentence-shape test applies: a template can't be a key as written, and ${name} — shared by ${owner} starts with a value, not a capital letter. A template that is only punctuation around values, like ${name} (${size}), isn't reported. With any one site reverted, npm run i18n:check exits 1.
Fixed: 12 sites
Where
Was
Key
Quota bar title
`${used} of ${total} used`
{used} of {total} used
Address menu ariaLabel
`Actions for ${email}`
Actions for {address}
Folder unread count title
`${own} here, ${n} in subfolders`
{here} here, {inSubfolders} in subfolders
Recipient chip remove
`Remove ${email}`
Remove {address} (existing)
Contact editor title
`Edit ${name}`
Edit {name}
Shared calendar / address book
`${name} — shared by ${owner}`
{name} — shared by {owner}
Available calendar / address book
`${name} — from ${owner}`
{name} — from {owner}
Date / time field labels
`${label} (date)`, "Date"
{label} (date), {label} (time), Date, Time (existing)
The free/busy bar showed the raw JMAP value (confirmed, tentative, unavailable) in every language, English included. It now maps through BUSY_LABEL.
The last two rows aren't the checker's finds. The attachment fallback is a lower-case string inside a template, and the busy status had no words outside its values. Both were found while going through the list.
Translation
11 new keys × 9 catalogues:{used} of {total} used, Actions for {address}, {here} here, {inSubfolders} in subfolders, Edit {name}, {name} — shared by {owner}, {name} — from {owner}, {label} (date), {label} (time), {status}: {start} – {end}, Unavailable, Attachment.
Each catalogue goes from 1425/1441 to 1436/1452 translated, and the fallback count stays at 16. The wording follows each catalogue's existing terms (Unterordner / sous-dossiers / вложенные папки, Freigegeben, …). French gets its space before the colon, and Japanese and Chinese use full-width punctuation. The Japanese unread-count string leaves out a counter, because the count can be messages (通) or conversations (件).
Checked
npm run i18n:check passes, and exits 1 with a site reverted.
npm run typecheck -w web, npm run build -w web and npm test -w web (1215 passed) all pass.
A throwaway Vitest run loaded the German and Japanese catalogues and rendered four of the new keys with values. None came back with a placeholder or English left in.
Not viewed in the browser. These are tooltips and accessible names.
Follow-up to #348, which left English built with template literals as the one known gap.
## Checker
`scripts/i18n-literals.mjs` now flags a template literal in a UI attribute or prop (the same positions #348 covered) when there are words between its values. Neither the catalogue-key exemption nor the sentence-shape test applies: a template can't be a key as written, and `${name} — shared by ${owner}` starts with a value, not a capital letter. A template that is only punctuation around values, like `${name} (${size})`, isn't reported. With any one site reverted, `npm run i18n:check` exits 1.
## Fixed: 12 sites
| Where | Was | Key |
|---|---|---|
| Quota bar title | `` `${used} of ${total} used` `` | `{used} of {total} used` |
| Address menu `ariaLabel` | `` `Actions for ${email}` `` | `Actions for {address}` |
| Folder unread count title | `` `${own} here, ${n} in subfolders` `` | `{here} here, {inSubfolders} in subfolders` |
| Recipient chip remove | `` `Remove ${email}` `` | `Remove {address}` (existing) |
| Contact editor title | `` `Edit ${name}` `` | `Edit {name}` |
| Shared calendar / address book | `` `${name} — shared by ${owner}` `` | `{name} — shared by {owner}` |
| Available calendar / address book | `` `${name} — from ${owner}` `` | `{name} — from {owner}` |
| Date / time field labels | `` `${label} (date)` ``, `"Date"` | `{label} (date)`, `{label} (time)`, `Date`, `Time` (existing) |
| Attachment title fallback | `"attachment"` | `Attachment` |
| Free/busy bar | `` `${b.busyStatus}: …` `` | `{status}: {start} – {end}`, with Busy / Tentative (existing) / Unavailable |
The free/busy bar showed the raw JMAP value (`confirmed`, `tentative`, `unavailable`) in every language, English included. It now maps through `BUSY_LABEL`.
The last two rows aren't the checker's finds. The attachment fallback is a lower-case string inside a template, and the busy status had no words outside its values. Both were found while going through the list.
## Translation
**11 new keys × 9 catalogues:** `{used} of {total} used`, `Actions for {address}`, `{here} here, {inSubfolders} in subfolders`, `Edit {name}`, `{name} — shared by {owner}`, `{name} — from {owner}`, `{label} (date)`, `{label} (time)`, `{status}: {start} – {end}`, `Unavailable`, `Attachment`.
Each catalogue goes from 1425/1441 to 1436/1452 translated, and the fallback count stays at **16**. The wording follows each catalogue's existing terms (Unterordner / sous-dossiers / вложенные папки, Freigegeben, …). French gets its space before the colon, and Japanese and Chinese use full-width punctuation. The Japanese unread-count string leaves out a counter, because the count can be messages (通) or conversations (件).
## Checked
- `npm run i18n:check` passes, and exits 1 with a site reverted.
- `npm run typecheck -w web`, `npm run build -w web` and `npm test -w web` (1215 passed) all pass.
- A throwaway Vitest run loaded the German and Japanese catalogues and rendered four of the new keys with values. None came back with a placeholder or English left in.
- Not viewed in the browser. These are tooltips and accessible names.
**Merged** 2026-09-14 as coffey-labs/ihasmail@7940a15a4363
<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 #348, which left English built with template literals as the one known gap.
Checker
scripts/i18n-literals.mjsnow flags a template literal in a UI attribute or prop (the same positions #348 covered) when there are words between its values. Neither the catalogue-key exemption nor the sentence-shape test applies: a template can't be a key as written, and${name} — shared by ${owner}starts with a value, not a capital letter. A template that is only punctuation around values, like${name} (${size}), isn't reported. With any one site reverted,npm run i18n:checkexits 1.Fixed: 12 sites
`${used} of ${total} used`{used} of {total} usedariaLabel`Actions for ${email}`Actions for {address}`${own} here, ${n} in subfolders`{here} here, {inSubfolders} in subfolders`Remove ${email}`Remove {address}(existing)`Edit ${name}`Edit {name}`${name} — shared by ${owner}`{name} — shared by {owner}`${name} — from ${owner}`{name} — from {owner}`${label} (date)`,"Date"{label} (date),{label} (time),Date,Time(existing)"attachment"Attachment`${b.busyStatus}: …`{status}: {start} – {end}, with Busy / Tentative (existing) / UnavailableThe free/busy bar showed the raw JMAP value (
confirmed,tentative,unavailable) in every language, English included. It now maps throughBUSY_LABEL.The last two rows aren't the checker's finds. The attachment fallback is a lower-case string inside a template, and the busy status had no words outside its values. Both were found while going through the list.
Translation
11 new keys × 9 catalogues:
{used} of {total} used,Actions for {address},{here} here, {inSubfolders} in subfolders,Edit {name},{name} — shared by {owner},{name} — from {owner},{label} (date),{label} (time),{status}: {start} – {end},Unavailable,Attachment.Each catalogue goes from 1425/1441 to 1436/1452 translated, and the fallback count stays at 16. The wording follows each catalogue's existing terms (Unterordner / sous-dossiers / вложенные папки, Freigegeben, …). French gets its space before the colon, and Japanese and Chinese use full-width punctuation. The Japanese unread-count string leaves out a counter, because the count can be messages (通) or conversations (件).
Checked
npm run i18n:checkpasses, and exits 1 with a site reverted.npm run typecheck -w web,npm run build -w webandnpm test -w web(1215 passed) all pass.Merged 2026-09-14 as coffey-labs/ihasmail@7940a15a43
Rebuilt from: git history, session transcript.