Merge pull request #248 from Coffey-Labs/i18n-render-untranslated-lists

Translate three lists the code was rendering raw
This commit is contained in:
Coffey Labs
2026-09-03 14:00:54 -07:00
committed by GitHub
4 changed files with 24 additions and 6 deletions
+11 -1
View File
@@ -23,10 +23,20 @@ export interface PaletteMeta {
name: string;
/** Shown in Settings and in NOTICE; who to credit and under what. */
credit?: string;
/**
* Whether the name is a word rather than a name.
*
* Five of these six are proper names -- ihasmail, Dracula, Gruvbox, Rosé
* Pine, Tokyo Night -- and are rendered translate="no" so a page translator
* leaves them alone. "Classic" is not a name, it is an adjective describing
* the theme, and a German reader should see "Klassisch". Reported by a
* native speaker reviewing the German catalogue (#247).
*/
translatable?: boolean;
}
export const PALETTES: PaletteMeta[] = [
{ id: "default", name: "Classic" },
{ id: "default", name: "Classic", translatable: true },
{ id: "ihasmail", name: "ihasmail" },
{ id: "dracula", name: "Dracula", credit: "Dracula Theme (MIT) — dark: Dracula, light: Alucard" },
{ id: "gruvbox", name: "Gruvbox", credit: "gruvbox by morhetz (MIT)" },
@@ -75,7 +75,9 @@ export function AppearanceSettings() {
return (
<button key={p.id} className={`theme-card ${s.palette === p.id ? "active" : ""}`} onClick={() => update({ palette: p.id })}>
<div className="preview" style={{ background: PALETTE_PREVIEW[p.id][shown] || PALETTE_PREVIEW[p.id].dark }} />
<span className="notranslate" translate="no">{p.name}</span>
{p.translatable
? <span>{translate(p.name)}</span>
: <span className="notranslate" translate="no">{p.name}</span>}
</button>
);
})}
+2 -2
View File
@@ -60,7 +60,7 @@ export function RuleDialog({ rule, onClose, onSave, applyMailbox, applyByDefault
else if (v === "address") setTest(i, { type: "address", header: "from", part: "domain", op: "is", value: "" });
else setTest(i, { type: "header", header: v === "__custom__" ? "" : v, op: "contains", value: "" });
}}>
{HEADER_CHOICES.map((h) => <option key={h.value} value={h.value}>{h.label}</option>)}
{HEADER_CHOICES.map((h) => <option key={h.value} value={h.value}>{translate(h.label)}</option>)}
<option value="address">{translate("Sender domain")}</option>
<option value="size">{translate("Message size")}</option>
<option value="body">{translate("Body text")}</option>
@@ -75,7 +75,7 @@ export function RuleDialog({ rule, onClose, onSave, applyMailbox, applyByDefault
<select className="select" value={t.op} onChange={(e) => setTest(i, { ...t, op: e.target.value as "contains" | "notcontains" })}><option value="contains">{translate("contains")}</option><option value="notcontains">{translate("does not contain")}</option></select>
) : t.type === "true" ? <span /> : (
<select className="select" value={t.op} onChange={(e) => setTest(i, { ...t, op: e.target.value as SieveTest extends { op: infer O } ? O : never })}>
{HEADER_OPS.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
{HEADER_OPS.map((o) => <option key={o.value} value={o.value}>{translate(o.label)}</option>)}
</select>
)}
{t.type === "size" ? (
+8 -2
View File
@@ -21,9 +21,15 @@ export function ShortcutsSettings() {
<div className="shortcut-grid">
{groups.map(([group, items]) => (
<div key={group}>
<h3>{group}</h3>
{/* Group names and descriptions are registered in English at the
call sites -- see views/Shortcuts.tsx -- because the binding
table is data, not markup, and the English is the catalogue
key. Translating at render keeps the registration simple and
means a binding added anywhere is translatable without the
registrar knowing about i18n. */}
<h3>{t(group)}</h3>
{items.map((b) => (
<div key={b.keys} className="shortcut-row"><span>{b.description}</span><Kbd keys={b.keys} /></div>
<div key={b.keys} className="shortcut-row"><span>{t(b.description)}</span><Kbd keys={b.keys} /></div>
))}
</div>
))}