Translate English built from template literals in attributes

i18n-literals now flags a template literal in a UI attribute or prop when
there are words between its values: `Remove ${email}`, `${name} — shared by
${owner}`. It cannot be a catalogue key as written, so neither the key
exemption nor the sentence-shape test applies. A template that is only
punctuation around values, like `${name} (${size})`, is left alone.

The twelve it found are now keys with placeholders: the quota bar title,
the address menu's label, a folder's subfolder unread count, a recipient
chip's remove button, the contact editor's title, shared and available
calendars and address books, the date and time fields' labels, the
attachment title's fallback name, and the free/busy bar. The bar showed
the raw JMAP busyStatus ("confirmed") and now says Busy, Tentative or
Unavailable.

11 new keys in all nine catalogues. Remove {address}, Date, Time, Busy and
Tentative already existed.
This commit is contained in:
2026-09-14 08:26:34 -07:00
parent aa57c59703
commit fecae2acd3
21 changed files with 144 additions and 17 deletions
+1 -1
View File
@@ -40,7 +40,7 @@ export function useAddressMenu() {
const node: ReactNode = (
<>
{menu && (
<Popover anchor={menu.anchor} onClose={close} width={230} ariaLabel={`Actions for ${menu.address.email}`}>
<Popover anchor={menu.anchor} onClose={close} width={230} ariaLabel={t("Actions for {address}", { address: menu.address.email })}>
<div className="menu-title truncate">{formatAddress(menu.address)}</div>
{contacts.available && (
known ? (
+1 -1
View File
@@ -369,7 +369,7 @@ function FolderRow({ mailbox: m, label, depth, hasChildren, open, hiddenUnread,
</span>
<span className="folder-icon" style={tint ? ({ "--folder-color": tint } as React.CSSProperties) : undefined}>{icon}</span>
<span className="nav-label">{label}</span>
{count > 0 && <span className="nav-count" title={hiddenUnread ? `${own} here, ${hiddenUnread} in subfolders` : undefined}>{count > 9999 ? "9999+" : count}</span>}
{count > 0 && <span className="nav-count" title={hiddenUnread ? t("{here} here, {inSubfolders} in subfolders", { here: own, inSubfolders: hiddenUnread }) : undefined}>{count > 9999 ? "9999+" : count}</span>}
{count > 0 && <span className="nav-dot" />}
<button
className="icon-btn nav-more"
+1 -1
View File
@@ -888,7 +888,7 @@ function AttachmentList({ attachments, accountId, email }: { attachments: EmailB
const url = a.blobId ? client.downloadUrl(accountId, a.blobId, a.name ?? "attachment", a.type) : "#";
const inlineUrl = a.blobId ? client.downloadUrl(accountId, a.blobId, a.name ?? "attachment", a.type, true) : "#";
return (
<a key={a.blobId ?? i} className="attachment" href={url} download={a.name ?? undefined} title={`${a.name ?? "attachment"} (${formatSize(a.size)})`} onClick={(ev) => { if (viewable(a)) { ev.preventDefault(); setPreview(a); } }}>
<a key={a.blobId ?? i} className="attachment" href={url} download={a.name ?? undefined} title={`${a.name ?? translate("Attachment")} (${formatSize(a.size)})`} onClick={(ev) => { if (viewable(a)) { ev.preventDefault(); setPreview(a); } }}>
<span className="att-icon">{a.type.startsWith("image/") && a.type !== "image/svg+xml" && a.blobId ? <img src={inlineUrl} alt="" loading="lazy" /> : attachmentIcon(a.type, a.name)}</span>
<span className="att-text">
<span className="att-name">{a.name ?? "(unnamed)"}</span>