feat: i18n seam — typed t() + en dictionary

Nav and header wired as proof. Full string extraction deferred until a
second locale exists; CONTRIBUTING records the convention.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-21 19:45:46 +02:00
1 parent a89a81d46c
commit 88a8210bf8
5 files changed
+58 -15

No files matched your search

+15 -14
View File
@@ -18,6 +18,7 @@ import { StatusPagesPage } from "@/pages/StatusPagesPage";
import { StatusPublicPage } from "@/pages/StatusPublicPage";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { t, type Key } from "@/i18n";
import type { Item } from "@/types";
export default function App() {
@@ -28,13 +29,13 @@ export default function App() {
);
}
const NAV = [
{ href: "/", label: "Board", icon: LayoutGrid },
{ href: "/monitors", label: "Monitors", icon: Activity },
{ href: "/domains", label: "Domains", icon: Globe },
{ href: "/systems", label: "Systems", icon: Server },
{ href: "/incidents", label: "Incidents", icon: AlertTriangle },
{ href: "/status", label: "Status", icon: Megaphone },
const NAV: { href: string; labelKey: Key; icon: typeof LayoutGrid }[] = [
{ href: "/", labelKey: "nav.board", icon: LayoutGrid },
{ href: "/monitors", labelKey: "nav.monitors", icon: Activity },
{ href: "/domains", labelKey: "nav.domains", icon: Globe },
{ href: "/systems", labelKey: "nav.systems", icon: Server },
{ href: "/incidents", labelKey: "nav.incidents", icon: AlertTriangle },
{ href: "/status", labelKey: "nav.status", icon: Megaphone },
];
function Shell() {
@@ -94,14 +95,14 @@ function Shell() {
)}
>
<n.icon size={13} strokeWidth={1.8} />
{n.label}
{t(n.labelKey)}
</Link>
);
})}
</nav>
<div className="ml-auto flex items-center gap-2">
<Button
variant="outline" aria-label="Search (⌘K)"
variant="outline" aria-label={t("header.search")}
onClick={() => setPaletteOpen(true)}
className="gap-1.5 text-text-faint"
>
@@ -109,27 +110,27 @@ function Shell() {
<kbd className="hidden font-mono text-[10px] min-[620px]:inline">⌘K</kbd>
</Button>
<Button
variant="outline" size="icon" aria-label="Settings"
variant="outline" size="icon" aria-label={t("header.settings")}
onClick={() => setSettingsOpen(true)}
>
<Settings size={15} strokeWidth={1.8} />
</Button>
<Button
variant="outline" size="icon" aria-label="Toggle theme"
variant="outline" size="icon" aria-label={t("header.theme")}
onClick={() => void board.setSetting("theme", board.theme === "dark" ? "light" : "dark")}
>
{board.theme === "dark" ? <Moon size={15} strokeWidth={1.8} /> : <Sun size={15} strokeWidth={1.8} />}
</Button>
<Button
variant="outline" aria-label="Add widget"
variant="outline" aria-label={t("header.addWidget")}
onClick={() => { setEditing(undefined); setWdgOpen(true); }}
>
<LayoutGrid size={13} strokeWidth={2} />
Widget
{t("header.widget")}
</Button>
<Button onClick={() => { setEditing(undefined); setSvcOpen(true); }}>
<Plus size={13} strokeWidth={2.2} />
Add service
{t("header.addService")}
</Button>
</div>
</header>
+27
View File
@@ -0,0 +1,27 @@
// English source strings. Flat keys, dot-separated namespaces.
// Add a locale by copying this file — keys must match exactly.
export const en = {
"nav.board": "Board",
"nav.monitors": "Monitors",
"nav.domains": "Domains",
"nav.systems": "Systems",
"nav.incidents": "Incidents",
"nav.status": "Status",
"header.search": "Search (⌘K)",
"header.settings": "Settings",
"header.theme": "Toggle theme",
"header.addWidget": "Add widget",
"header.addService": "Add service",
"header.widget": "Widget",
"common.add": "Add",
"common.edit": "Edit",
"common.delete": "Delete",
"common.cancel": "Cancel",
"common.save": "Save",
"common.test": "Test",
"common.loading": "Loading…",
} as const;
export type Key = keyof typeof en;
+13
View File
@@ -0,0 +1,13 @@
import { en, type Key } from "@/i18n/en";
export type { Key } from "@/i18n/en";
// i18n seam: dictionaries are plain objects keyed by dotted names. `t` is a
// typed lookup that falls back to the key itself, so a missing translation
// renders the key — loud in dev, never a crash. Locales beyond `en` plug in
// here later (settings key + lazy import); nothing else changes.
const dict: Record<Key, string> = en;
export function t(key: Key): string {
return dict[key] ?? key;
}