i18n: tighten 404 + global-error copy across all locales

This commit is contained in:
Marc-André Bombeck committed 2026-05-21 22:32:02 +02:00
1 parent 71bbd094d7
commit 2390438edc
9 files changed
+165 -177

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
/**
* v1.4.43 QoL (L1) — pin the tightened 404 / global-error copy so a
* future translator does not re-introduce the v1.4.27 marketing
* paragraph. The 404 page now ships with i18n keys (`notFound.*`)
* across all six locales; the global-error boundary remains static
* bilingual (no i18n provider available at that level).
*/
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { join } from "node:path";
const ROOT = join(__dirname, "../../..");
const LOCALES = ["en", "de", "fr", "es", "it", "pl"] as const;
interface Messages {
notFound: {
title: string;
backToDashboard: string;
};
}
function load(locale: string): Messages {
const raw = readFileSync(join(ROOT, "messages", `${locale}.json`), "utf8");
return JSON.parse(raw) as Messages;
}
describe("notFound copy", () => {
it.each(LOCALES)("%s: ships title + backToDashboard keys", (locale) => {
const messages = load(locale);
expect(messages.notFound.title.trim().length).toBeGreaterThan(0);
expect(messages.notFound.backToDashboard.trim().length).toBeGreaterThan(0);
});
it.each(LOCALES)(
"%s: title is tight — under 60 characters, ends with a period",
(locale) => {
const messages = load(locale);
// Tightened copy is a single short statement, e.g. "Diese Seite
// existiert nicht." The 60-char ceiling pins out a future return
// of the verbose marketing block.
expect(messages.notFound.title.length).toBeLessThanOrEqual(60);
expect(messages.notFound.title).toMatch(/\.$/);
},
);
it("de: matches the tightened copy exactly", () => {
const messages = load("de");
expect(messages.notFound.title).toBe("Diese Seite existiert nicht.");
});
it("en: matches the tightened copy exactly", () => {
const messages = load("en");
expect(messages.notFound.title).toBe("This page doesn't exist.");
});
});
describe("global-error bilingual copy", () => {
// The global-error boundary cannot reach the i18n provider (root
// layout has failed). The v1.4.43 audit asked for a bilingual
// lockup so a German user does not read pure English in a bad-
// state moment. Read the file and assert both languages appear.
const globalErrorPath = join(ROOT, "src/app/global-error.tsx");
const source = readFileSync(globalErrorPath, "utf8");
it("ships the German leading phrase next to the English fallback", () => {
expect(source).toContain("Etwas ist schiefgegangen");
expect(source).toContain("Something went wrong");
});
it("ships the German error subtitle next to the English fallback", () => {
expect(source).toContain("Ein kritischer Fehler ist aufgetreten");
// The JSX-formatter wraps the English fallback across two lines, so
// collapse all whitespace before searching.
const collapsed = source.replace(/\s+/g, " ");
expect(collapsed).toContain("A critical error occurred");
});
});
+17 -12
View File
@@ -3,9 +3,7 @@
/**
* Root-level error boundary. Renders when even the root layout itself fails,
* so it cannot rely on any providers (i18n, query, auth). Keep it static,
* self-contained, and bilingual: both English and German appear side by side
* so a German visitor never lands on an all-English last-resort screen, and
* an English visitor can still read it.
* in English, and self-contained.
*/
export default function GlobalError({
error,
@@ -30,7 +28,7 @@ export default function GlobalError({
try {
await navigator.clipboard.writeText(text);
} catch {
window.prompt("Details kopieren · Copy details", text);
window.prompt("Copy details", text);
}
}
@@ -56,14 +54,21 @@ export default function GlobalError({
}}
>
<div style={{ maxWidth: 560 }}>
{/*
v1.4.43 QoL — bilingual lockup so a German user landing
in this boundary doesn't read pure English in an already-
bad-state moment. The root layout has failed by the time
this fires, so no i18n provider is available; the static
DE/EN pairing covers the dominant audience without a
runtime cost. Copy tightened per the v1.4.43 audit (L1)
to drop the verbose secondary sentence.
*/}
<h1 style={{ fontSize: 20, marginBottom: 12 }}>
Etwas ist schiefgegangen · Something went wrong
Etwas ist schiefgegangen / Something went wrong
</h1>
<p style={{ color: "#d0d0d0", marginBottom: 16, fontSize: 14 }}>
Ein kritischer Fehler ist aufgetreten. Du kannst es erneut versuchen
oder die Details kopieren.
<br />A critical error occurred. You can retry or copy the details
for support.
Ein kritischer Fehler ist aufgetreten. / A critical error
occurred.
</p>
<pre
style={{
@@ -76,7 +81,7 @@ export default function GlobalError({
border: "1px solid #44475a",
}}
>
{error.message || "Unbekannter Fehler · Unknown error"}
{error.message || "Unknown error"}
</pre>
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
<button
@@ -96,7 +101,7 @@ export default function GlobalError({
fontWeight: 500,
}}
>
Erneut versuchen · Retry
Retry
</button>
<button
onClick={handleCopy}
@@ -111,7 +116,7 @@ export default function GlobalError({
fontSize: 14,
}}
>
Details kopieren · Copy details
Copy details
</button>
</div>
</div>
+11 -9
View File
@@ -13,14 +13,19 @@ import { getServerTranslator } from "@/lib/i18n/server-translator";
* application error and doesn't need the bug-report path. Just a
* branded splash with one link home.
*
* v1.4.43 H4 — the copy is locale-resolved server-side from the same
* cookie / Accept-Language path the rest of the app uses, so a German
* visitor no longer hits an English-only splash on a typo URL.
*
* Mobile-first: `min-h-dvh` follows the dynamic viewport so the
* panel anchors centred under iOS Safari's animated URL bar;
* `safe-area-inset-top` keeps the headline clear of the notch or
* Dynamic Island.
*
* v1.4.43 QoL — copy lifted into i18n keys (`notFound.title` /
* `notFound.backToDashboard`) and tightened. The previous
* marketing-flavoured paragraph ("The page you were looking for…
* Head back to the dashboard to pick up where you left off.") read
* heavier than the surface warrants; the single-line statement
* matches the existing voice of every other in-app error message.
* Translations cover all six locales so a German user no longer
* lands on English copy.
*/
export default async function NotFound() {
const locale = await resolveServerLocale();
@@ -39,17 +44,14 @@ export default async function NotFound() {
404
</p>
<h1 className="text-2xl font-bold tracking-tight md:text-3xl">
{t("errors.notFound.title")}
{t("notFound.title")}
</h1>
<p className="text-muted-foreground text-sm leading-relaxed">
{t("errors.notFound.body")}
</p>
</div>
<Link
href="/"
className="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex min-h-11 items-center justify-center rounded-md px-5 text-sm font-medium transition-colors"
>
{t("errors.notFound.backToDashboard")}
{t("notFound.backToDashboard")}
</Link>
</main>
);