Second extraction pass: the strings the codemod could not see

`i18n:coverage` reported 100% while a hundred-odd strings rendered
English in every language. It was not wrong about what it measured: it
reads JSX text, and none of these were JSX text. They were toast
arguments, `confirmDialog({ title, confirmLabel })` props, `title=` and
`aria-label=` attributes, and template literals — every one built from an
expression the codemod cannot read.

176 source strings and 15 plural sets now go through t() and plural(),
translated into all nine languages. Where English put a word in a slot,
the sentence is spelled out per branch instead: `Filter ${verb}` became
"Filter saved" and "Filter created", because which word agrees with what,
and where it sits, is not a property English gets to decide for everyone.
Counts that were `${n} message${n === 1 ? "" : "s"}` are plural() calls,
so Russian and Ukrainian get three forms and Japanese and Chinese get the
one they actually have.

Two of the catalogue's own conventions were worth learning the hard way.
Plural entries are keyed on the English *other* form, not `one` — `one`
is a form English happens to have and Japanese does not. And a constant
table holding English that is translated at the render site is fine: the
literal is a key, not a leak.

Which is what the new check encodes. `scripts/i18n-literals.mjs` accepts
a string that is wrapped where it is written or is a catalogue key
somewhere, and refuses one that is neither — a string no catalogue can
translate, however many languages ship. It found twenty more than my own
sweep had, including the stale-folder toast seen in production. It runs
as part of `npm run i18n:check`.

Also fixed: the catalogue is now awaited before the first paint. The
tree is rebuilt when a catalogue lands, so components recover on their
own, but a string computed in an effect does not — a toast fired in that
gap is emitted in English and stays English. The wait costs nothing
visible, since the session bootstrap already shows a spinner and English
resolves immediately.

And the Japanese agenda title loses a space Japanese does not use:
"{date} からの予定" was written with the English habit of spacing around
a placeholder.
This commit is contained in:
2026-08-31 14:14:30 -07:00
parent a4f7d386a6
commit a6863e98cc
52 changed files with 2101 additions and 191 deletions
+193 -1
View File
@@ -864,7 +864,184 @@ export const catalog: Catalog = {
"Error": "エラー",
"Saved {when}": "{when} に保存",
"Unsaved": "未保存",
"Agenda from {date}": "{date} からの予定",
"Agenda from {date}": "{date}からの予定",
// ── Toasts, dialogs and computed labels (second extraction pass) ───
"(no subject)": "(件名なし)",
"(skipped: {actions})": "(スキップ: {actions}",
"Activate": "有効にする",
"Add a contact or import a vCard file.": "連絡先を追加するか、vCard ファイルをインポートしてください。",
"Added to your calendar": "カレンダーに追加しました",
"All events in this calendar will be deleted.": "このカレンダーの予定はすべて削除されます。",
"All occurrences": "すべての繰り返し",
"An occurrence cannot be moved to another calendar on its own": "繰り返しの 1 件だけを別のカレンダーへ移動することはできません",
"Anything signed in with this password stops working immediately.": "このパスワードでサインインしているものは、ただちに使えなくなります。",
"App password revoked": "アプリパスワードを無効化しました",
"Applies to every date in the series.": "この繰り返しのすべての日付に適用されます。",
"Applies to this date only.": "この日付にのみ適用されます。",
"Apply to series": "繰り返し全体に適用",
"Applying filter to existing messages…": "既存のメールにフィルターを適用しています…",
"Attachments are still uploading": "添付ファイルをアップロード中です",
"Calendar saved": "カレンダーを保存しました",
"Categorised as {name}": "分類「{name}」を設定しました",
"Category cleared": "分類を解除しました",
"Change this event?": "この予定を変更しますか?",
"Choose a calendar": "カレンダーを選んでください",
"Choose an address book": "アドレス帳を選んでください",
"Colour updated": "色を変更しました",
"Contact created": "連絡先を作成しました",
"Contact deleted": "連絡先を削除しました",
"Contact saved": "連絡先を保存しました",
"Copied": "コピーしました",
"Copy of {title}": "{title} のコピー",
"Could not cancel: {error}": "取り消せませんでした: {error}",
"Could not copy": "コピーできませんでした",
"Could not copy the address": "アドレスをコピーできませんでした",
"Could not empty folder: {error}": "フォルダーを空にできませんでした: {error}",
"Could not load source: {error}": "ソースを読み込めませんでした: {error}",
"Could not mark as read: {error}": "既読にできませんでした: {error}",
"Could not save draft: {error}": "下書きを保存できませんでした: {error}",
"Could not save filter: {error}": "フィルターを保存できませんでした: {error}",
"Could not save filters: {error}": "フィルターを保存できませんでした: {error}",
"Could not send the receipt: {error}": "開封確認を送信できませんでした: {error}",
"Could not sign in.": "サインインできませんでした。",
"Could not store image: {error}": "画像を保存できませんでした: {error}",
"Could not update labels: {error}": "ラベルを更新できませんでした: {error}",
"Could not update the Scheduled folder: {error}": "「送信予約」フォルダーを更新できませんでした: {error}",
"Could not update: {error}": "更新できませんでした: {error}",
"Create": "作成",
"Create an address book first": "先にアドレス帳を作成してください",
"Custom colour removed": "カスタム色を解除しました",
"Deactivate": "無効にする",
"Delete failed: {error}": "削除できませんでした: {error}",
"Delete forever": "完全に削除",
"Delete script “{name}”?": "スクリプト「{name}」を削除しますか?",
"Delete this event?": "この予定を削除しますか?",
"Delete this identity?": "この差出人を削除しますか?",
"Delete {name}?": "{name} を削除しますか?",
"Deleted": "削除しました",
"Deletes the whole series. This cannot be undone.": "繰り返し全体を削除します。元に戻せません。",
"Did you forget the attachment?": "添付ファイルを忘れていませんか?",
"Discard": "破棄",
"Discard this draft?": "この下書きを破棄しますか?",
"Edit calendar": "カレンダーを編集",
"Edit event": "予定を編集",
"Edit identity": "差出人を編集",
"Edit template": "テンプレートを編集",
"End must be after start": "終了は開始より後にしてください",
"Event duplicated": "予定を複製しました",
"Everything inside it goes too.": "中身もすべて削除されます。",
"Filter created": "フィルターを作成しました",
"Filter created — it will run on new mail": "フィルターを作成しました。新着メールに適用されます",
"Filter saved": "フィルターを保存しました",
"Filter saved — it will run on new mail": "フィルターを保存しました。新着メールに適用されます",
"Filter saved, but applying it failed: {error}": "フィルターは保存しましたが、適用できませんでした: {error}",
"Filters saved": "フィルターを保存しました",
"Folder changed, but its filter rules could not be updated: {error}": "フォルダーは変更しましたが、そのフィルタールールを更新できませんでした: {error}",
"Folder created": "フォルダーを作成しました",
"Folder deleted": "フォルダーを削除しました",
"Folder name": "フォルダー名",
"Folder name (use / for a subfolder, e.g. Work/Invoices)": "フォルダー名(/ でサブフォルダー。例: 仕事/請求書)",
"Folder name (use / for subfolders, e.g. Work/Invoices)": "フォルダー名(/ でサブフォルダー。例: 仕事/請求書)",
"Folder “{name}” created": "フォルダー「{name}」を作成しました",
"Forwarded": "転送済み",
"Hand to the server, held until {when} (Ctrl+Enter)": "サーバーに渡し、{when} まで保留します (Ctrl+Enter)",
"Hide": "非表示",
"Hide quoted text": "引用文を隠す",
"Hide this from the compose picker": "作成時に表示しない",
"Identity saved": "差出人を保存しました",
"Images in signatures need the Files feature, which this account doesn't have.": "署名に画像を使うには「ファイル」機能が必要ですが、このアカウントにはありません。",
"Invalid address: {address}": "アドレスが正しくありません: {address}",
"Invalid username or password.": "ユーザー名またはパスワードが正しくありません。",
"It belongs to a change that was applied to this and all later occurrences, which the server will only edit as a whole. Apply to the entire series instead?": "これは、この回以降のすべての繰り返しに適用された変更の一部です。サーバーはその変更をまとめてしか編集できません。繰り返し全体に適用しますか?",
"Label name": "ラベル名",
"Larger than {size} MB limit": "上限の {size} MB を超えています",
"Message sent": "メールを送信しました",
"Move failed: {error}": "移動できませんでした: {error}",
"Move “{name}”": "「{name}」を移動",
"Moved": "移動しました",
"Network error. Please check your connection.": "ネットワークエラーです。接続を確認してください。",
"New all-day event on {date}": "{date} に終日の予定を作成",
"New event at {time}": "{time} に予定を作成",
"New identity": "新しい差出人",
"No Archive folder found. Create one named “Archive” first.": "アーカイブフォルダーが見つかりません。先に「Archive」という名前で作成してください。",
"No contacts yet": "連絡先がまだありません",
"No longer shared": "共有を解除しました",
"No matches": "一致するものがありません",
"No new mail in your inbox.": "受信トレイに新着メールはありません。",
"No results": "結果がありません",
"Nothing here": "ここには何もありません",
"Only Deleted Items and Junk Mail can be emptied.": "空にできるのはゴミ箱と迷惑メールだけです。",
"Please add at least one recipient": "宛先を 1 件以上入力してください",
"Please use an image under 512 KB for signatures.": "署名には 512 KB 未満の画像をお使いください。",
"Preview": "プレビュー",
"Read receipt sent": "開封確認を送信しました",
"Remove attachments that failed to upload first": "アップロードに失敗した添付ファイルを先に削除してください",
"Removed from calendar": "カレンダーから削除しました",
"Removes this date and leaves the rest of the series.": "この日付だけを削除し、繰り返しの残りは残します。",
"Rename address book": "アドレス帳の名前を変更",
"Rename category": "分類の名前を変更",
"Replied": "返信済み",
"Report spam (!)": "迷惑メールとして報告 (!)",
"Response sent": "回答を送信しました",
"Revoke “{name}”?": "「{name}」を無効化しますか?",
"Script has errors": "スクリプトにエラーがあります",
"Script is valid": "スクリプトは正しい形式です",
"Script name is required": "スクリプト名を入力してください",
"Script saved": "スクリプトを保存しました",
"Send (Ctrl+Enter)": "送信 (Ctrl+Enter)",
"Send anyway": "このまま送信",
"Send cancelled — the message is back in Drafts": "送信を取り消しました。メールは下書きに戻っています",
"Send failed: {error}": "送信できませんでした: {error}",
"Send invites": "招待を送信",
"Send scheduled for {when}": "{when} に送信を予約しました",
"Send without a subject?": "件名なしで送信しますか?",
"Share “{name}”": "「{name}」を共有",
"Sharing updated": "共有設定を更新しました",
"Show": "表示",
"Show quoted text": "引用文を表示",
"Show this in the compose picker": "作成時に表示する",
"Sign out other sessions?": "他のセッションをサインアウトしますか?",
"Sign out others": "他をサインアウト",
"Stop sharing “{name}”?": "「{name}」の共有を解除しますか?",
"Switch to {theme}": "{theme}に切り替え",
"Template": "テンプレート",
"Template name": "テンプレート名",
"The contacts in it go too.": "中の連絡先も削除されます。",
"The default identity is always offered when composing": "既定の差出人は作成時につねに表示されます",
"The new passwords don't match": "新しいパスワードが一致しません",
"The server scheduled this for {when}, not the time requested.": "サーバーは要求した時刻ではなく {when} に予約しました。",
"This date cannot be changed on its own": "この日付だけを変更することはできません",
"This occurrence": "この回のみ",
"Too many attempts. Please wait a few minutes and try again.": "試行回数が多すぎます。数分待ってからもう一度お試しください。",
"Try another search.": "別のことばで検索してみてください。",
"Try different keywords or filters.": "別のキーワードや絞り込みをお試しください。",
"Two-factor authentication is off": "2 段階認証はオフです",
"Undo window: {seconds}s": "取り消せる時間: {seconds} 秒",
"You're all caught up": "未読はありません",
"Your browser refused the request: {error}": "ブラウザーが要求を拒否しました: {error}",
"Your browser will ask whether to open mail links in ihasmail": "メールのリンクを ihasmail で開くかどうか、ブラウザーが確認します",
"Your message mentions an attachment, but nothing is attached.": "本文で添付ファイルに触れていますが、何も添付されていません。",
"event": "予定",
"Hide password": "パスワードを隠す",
"Show password": "パスワードを表示",
"Settings reset to defaults": "設定を既定に戻しました",
"Removed. Mail links will open in whatever your browser falls back to.": "解除しました。メールのリンクは、ブラウザーが代わりに使うアプリで開きます。",
"Edit rule": "ルールを編集",
"Address copied": "アドレスをコピーしました",
"Search or create label": "ラベルを検索または作成",
"New label name": "新しいラベルの名前",
"That folder no longer exists. Showing your inbox instead.": "そのフォルダーはもうありません。代わりに受信トレイを表示しています。",
"Labels updated": "ラベルを更新しました",
"Unsubscribe message prepared — just hit Send": "配信停止のメールを用意しました。あとは送信するだけです",
"Maximize": "最大化",
"Full screen": "全画面",
"Resize panes": "ペインの大きさを変更",
"QR code": "QR コード",
"Draft discarded": "下書きを破棄しました",
"Draft saved": "下書きを保存しました",
"Emptying folder…": "フォルダーを空にしています…",
"Nothing unread here": "ここに未読はありません",
},
plurals: {
/*
@@ -875,5 +1052,20 @@ export const catalog: Catalog = {
"{n} messages": { other: "{n} 通のメール" },
"{n} conversations": { other: "{n} 件のスレッド" },
"{n} selected": { other: "{n} 件を選択中" },
"{n} messages will be permanently deleted.": { other: "{n} 通のメールが完全に削除されます。" },
"Signed out {n} other sessions": { other: "他の {n} 件のセッションをサインアウトしました" },
"applied to {matched} of {n} messages": { other: "{n} 通中 {matched} 通に適用" },
"Move {n} messages to…": { other: "{n} 通のメールを移動…" },
"This permanently deletes the folder and its {n} messages.": { other: "フォルダーと、その中の {n} 通のメールを完全に削除します。" },
"Added {n} contacts": { other: "{n} 件の連絡先を追加しました" },
"{n} people will lose access. The contacts in it are not affected.": { other: "{n} 人がアクセスできなくなります。中の連絡先には影響しません。" },
"Imported {n} contacts": { other: "{n} 件の連絡先をインポートしました" },
"Saved for this date. {names} apply to the whole series and were left unchanged.": { other: "この日付に保存しました。{names} は繰り返し全体に適用されるため、変更していません。" },
"{n} people will lose access. Events in it are not affected.": { other: "{n} 人がアクセスできなくなります。中の予定には影響しません。" },
"{n} messages could not be deleted": { other: "{n} 通のメールを削除できませんでした" },
"({n} deleted first)": { other: "(先に {n} 通を削除)" },
"Marked {n} messages as read": { other: "{n} 通のメールを既読にしました" },
"in {n} folders": { other: "{n} 個のフォルダーで" },
"Deleted {n} messages": { other: "{n} 通のメールを削除しました" },
},
};