mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
i18n: tighten 404 + global-error copy across all locales
This commit is contained in:
1 parent
71bbd094d7
commit
2390438edc
9 files changed
+165
-177
No files matched your search
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user