German, generated by AI and marked Beta until somebody signs it off
The first language, and the first one where the honest thing to say is not flattering: no native speaker has read it. That is stated in the app rather than in a commit nobody reads, because it is the fact a reader needs to judge what they are looking at. Somebody told a translation is unchecked forgives an odd sentence and reports it; somebody told it was reviewed reasonably concludes the product is sloppy. The setting carries a link straight to a report, which is the whole review process here. `beta` is a property of the language, not of the catalogue's completeness. A file can be word-for-word finished and still read like a machine wrote it, and that is what the flag marks. Removing it is a person's decision. Register is "Sie", consistently, and written down in the file so the next language and the next contributor inherit the decision rather than re-take it. Thunderbird and Outlook use it; ihasmail is as often a company's mail as somebody's own, where "du" from software the workplace deployed reads as presumptuous. Where a string can dodge the question it does, which is ordinary good German UI. The glossary at the top of the file fixes the vocabulary once -- Posteingang, Papierkorb, Entwürfe, archivieren -- because inconsistency reads as amateur far more than an imperfect word choice does. "Label" and "Spam" stay English, since translating them would name things no German mail client calls that. 766 of 781 strings. The fifteen left are product names, bare URLs and example addresses, which should stay English and now do. Two things this turned up that the earlier work had hidden: Labels defined as module-level constants -- the entire settings navigation, the theme cards, the swipe choices, the date formats, the sharing permissions -- are evaluated once, before any catalogue loads, so they could only ever be English. Nothing failed; the German build simply had an English sidebar. They are translated where they render now, which keeps the constant as data and makes its English text the key. And the codemod's narrowed rule, which let it take 73 more strings last time, was too broad after all: text stranded after an inline <a> or <strong> came through as sentence fragments -- ", and what a new account starts on." Eight of them, rebuilt with tNode so the sentence stays whole and the element is a named hole a translator can move. scripts/i18n-catalog-check.mjs is new and earned itself immediately: it found three keys invented that the code never asks for, which is the silent failure in a catalogue -- a translation that looks right, is never looked up, and renders English for ever. It also had to be taught about t(variable), because it cried wolf 33 times over the constants above, and a check that cries wolf gets switched off. Verified in the browser rather than only in tests, which is where the settings sidebar being English was visible and nowhere else.
This commit is contained in:
@@ -8,7 +8,7 @@ import { RuleDialog } from "../settings/RuleDialog";
|
||||
import { toast } from "@/ui/toast";
|
||||
import { Spinner } from "@/ui/misc";
|
||||
import { Dialog } from "@/ui/dialog";
|
||||
import { t } from "@/lib/i18n";
|
||||
import { t, tNode } from "@/lib/i18n";
|
||||
|
||||
/** "Filter messages like this…" — creates a Sieve rule seeded from a message, optionally applying it to the current folder. */
|
||||
export function FilterFromMessageDialog({ email, mailboxId, onClose }: { email: Email; mailboxId: Id | null; onClose: () => void }) {
|
||||
@@ -47,7 +47,7 @@ export function FilterFromMessageDialog({ email, mailboxId, onClose }: { email:
|
||||
{damage ? (
|
||||
<p>{t("Your filter script {damage}, so only part of it arrived. Adding a rule would write that part back over the whole thing. Reload the page and try again.", { damage })}</p>
|
||||
) : loaded ? (
|
||||
<p>{t("Your active Sieve script was written by hand, so rules can't be added automatically. Open")} <b>{t("Settings → Filters & rules")}</b> {t("to edit the script or switch to managed rules.")}</p>
|
||||
<p>{tNode("Your active Sieve script was written by hand, so rules can't be added automatically. Open {where} to edit the script or switch to managed rules.", { where: <b>{t("Settings → Filters & rules")}</b> })}</p>
|
||||
) : (
|
||||
<p>{t("Your filter script couldn't be read just now, so adding a rule would risk overwriting it. Reload the page and try again.")}</p>
|
||||
)}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { confirmDialog } from "@/ui/dialog";
|
||||
import { toast } from "@/ui/toast";
|
||||
import { isUnknownMailbox } from "@/lib/mailboxRoute";
|
||||
import { scheduledMailboxIdFrom, useScheduled } from "@/store/scheduled";
|
||||
import { t as translate } from "@/lib/i18n";
|
||||
import { plural, t as translate, tNode } from "@/lib/i18n";
|
||||
|
||||
export function MailView({ mailboxId, threadId, search }: { mailboxId?: string; threadId?: string; search?: boolean }) {
|
||||
const [, navigate] = useLocation();
|
||||
@@ -353,8 +353,8 @@ export function MailView({ mailboxId, threadId, search }: { mailboxId?: string;
|
||||
) : (
|
||||
<div className="no-thread">
|
||||
<img src="/img/logo.png" alt="" />
|
||||
<div>{list?.total ? `${list.total} conversation${list.total === 1 ? "" : "s"}` : "No conversation selected"}</div>
|
||||
<div className="hint">{translate("Select a conversation to read it here · Press")} <kbd className="kbd">?</kbd> {translate("for shortcuts")}</div>
|
||||
<div>{list?.total ? plural(list.total, { one: "{n} conversation", other: "{n} conversations" }) : translate("No conversation selected")}</div>
|
||||
<div className="hint">{tNode("Select a conversation to read it here · Press {key} for shortcuts", { key: <kbd className="kbd">?</kbd> })}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -143,7 +143,7 @@ export function MailboxTree() {
|
||||
if (id) void moveFolder(id, null);
|
||||
}}
|
||||
>
|
||||
<span>{draggingId && canDropOn(null) ? "Drop here for the top level" : "Folders"}</span>
|
||||
<span>{draggingId && canDropOn(null) ? t("Drop here for the top level") : t("Folders")}</span>
|
||||
<button className="icon-btn" title={t("New folder")} aria-label={t("New folder")} onClick={() => void createFolder(null)}>
|
||||
<Plus size={16} />
|
||||
</button>
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useScheduled } from "@/store/scheduled";
|
||||
import { formatScheduleTime } from "@/lib/schedule";
|
||||
import { mdnDecision, refusalText } from "@/lib/mdn";
|
||||
import { sendReadReceipt } from "@/store/mdn";
|
||||
import { t as translate } from "@/lib/i18n";
|
||||
import { t as translate, tNode } from "@/lib/i18n";
|
||||
|
||||
interface Props {
|
||||
email: Email;
|
||||
@@ -226,7 +226,7 @@ export const MessageView = memo(function MessageView({ email: e, expanded, wasUn
|
||||
|
||||
{translate("The sender asked for a read receipt.")}
|
||||
{receipt.redirected && (
|
||||
<> {translate("It would go to")} <strong>{receipt.to!.email}</strong>{translate(", which is not where the message came from.")}</>
|
||||
<>{tNode("It would go to {address}, which is not where the message came from.", { address: <strong className="notranslate" translate="no">{receipt.to!.email}</strong> })}</>
|
||||
)}
|
||||
</span>
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user