mirror of
https://github.com/Dvorinka/Dash.git
synced 2026-10-28 07:41:27 +00:00
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:
1 parent
a89a81d46c
commit
88a8210bf8
5 files changed
+58
-15
No files matched your search
@@ -31,6 +31,8 @@ DASH_DEV=1 go run ./cmd/dash # API on :8080
|
|||||||
- No auth, no YAML-as-source-of-truth, no external database. These are
|
- No auth, no YAML-as-source-of-truth, no external database. These are
|
||||||
non-goals for v1.
|
non-goals for v1.
|
||||||
- Standard library before dependencies; existing helpers before new ones.
|
- Standard library before dependencies; existing helpers before new ones.
|
||||||
|
- User-visible strings go through `t("key")` from `src/i18n` — add the key to
|
||||||
|
`en.ts` rather than inlining literals in new UI.
|
||||||
|
|
||||||
## Before pushing
|
## Before pushing
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -197,7 +197,7 @@ Beszel-style server monitoring, push-based.
|
|||||||
- [x] `README.md` hero: screenshots, `docker run` one-liner (demo GIF deferred — pngs suffice for v1.0.0)
|
- [x] `README.md` hero: screenshots, `docker run` one-liner (demo GIF deferred — pngs suffice for v1.0.0)
|
||||||
- [x] goreleaser → binaries; GHCR image on tag; semver + `CHANGELOG.md`
|
- [x] goreleaser → binaries; GHCR image on tag; semver + `CHANGELOG.md`
|
||||||
- [x] `CONTRIBUTING.md`, issue/PR templates, code of conduct, security policy
|
- [x] `CONTRIBUTING.md`, issue/PR templates, code of conduct, security policy
|
||||||
- [ ] i18n scaffolding (en default)
|
- [x] i18n scaffolding (`src/i18n/` — typed `t()` + `en` dict; full string extraction deferred to first real locale)
|
||||||
- [ ] Public repo flip + announcement assets
|
- [ ] Public repo flip + announcement assets
|
||||||
|
|
||||||
**Exit:** a stranger can `docker run`, import their Homepage config, and have a working board in under two minutes.
|
**Exit:** a stranger can `docker run`, import their Homepage config, and have a working board in under two minutes.
|
||||||
|
|||||||
+15
-14
@@ -18,6 +18,7 @@ import { StatusPagesPage } from "@/pages/StatusPagesPage";
|
|||||||
import { StatusPublicPage } from "@/pages/StatusPublicPage";
|
import { StatusPublicPage } from "@/pages/StatusPublicPage";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { t, type Key } from "@/i18n";
|
||||||
import type { Item } from "@/types";
|
import type { Item } from "@/types";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
@@ -28,13 +29,13 @@ export default function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const NAV = [
|
const NAV: { href: string; labelKey: Key; icon: typeof LayoutGrid }[] = [
|
||||||
{ href: "/", label: "Board", icon: LayoutGrid },
|
{ href: "/", labelKey: "nav.board", icon: LayoutGrid },
|
||||||
{ href: "/monitors", label: "Monitors", icon: Activity },
|
{ href: "/monitors", labelKey: "nav.monitors", icon: Activity },
|
||||||
{ href: "/domains", label: "Domains", icon: Globe },
|
{ href: "/domains", labelKey: "nav.domains", icon: Globe },
|
||||||
{ href: "/systems", label: "Systems", icon: Server },
|
{ href: "/systems", labelKey: "nav.systems", icon: Server },
|
||||||
{ href: "/incidents", label: "Incidents", icon: AlertTriangle },
|
{ href: "/incidents", labelKey: "nav.incidents", icon: AlertTriangle },
|
||||||
{ href: "/status", label: "Status", icon: Megaphone },
|
{ href: "/status", labelKey: "nav.status", icon: Megaphone },
|
||||||
];
|
];
|
||||||
|
|
||||||
function Shell() {
|
function Shell() {
|
||||||
@@ -94,14 +95,14 @@ function Shell() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<n.icon size={13} strokeWidth={1.8} />
|
<n.icon size={13} strokeWidth={1.8} />
|
||||||
{n.label}
|
{t(n.labelKey)}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
<div className="ml-auto flex items-center gap-2">
|
<div className="ml-auto flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline" aria-label="Search (⌘K)"
|
variant="outline" aria-label={t("header.search")}
|
||||||
onClick={() => setPaletteOpen(true)}
|
onClick={() => setPaletteOpen(true)}
|
||||||
className="gap-1.5 text-text-faint"
|
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>
|
<kbd className="hidden font-mono text-[10px] min-[620px]:inline">⌘K</kbd>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline" size="icon" aria-label="Settings"
|
variant="outline" size="icon" aria-label={t("header.settings")}
|
||||||
onClick={() => setSettingsOpen(true)}
|
onClick={() => setSettingsOpen(true)}
|
||||||
>
|
>
|
||||||
<Settings size={15} strokeWidth={1.8} />
|
<Settings size={15} strokeWidth={1.8} />
|
||||||
</Button>
|
</Button>
|
||||||
<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")}
|
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} />}
|
{board.theme === "dark" ? <Moon size={15} strokeWidth={1.8} /> : <Sun size={15} strokeWidth={1.8} />}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline" aria-label="Add widget"
|
variant="outline" aria-label={t("header.addWidget")}
|
||||||
onClick={() => { setEditing(undefined); setWdgOpen(true); }}
|
onClick={() => { setEditing(undefined); setWdgOpen(true); }}
|
||||||
>
|
>
|
||||||
<LayoutGrid size={13} strokeWidth={2} />
|
<LayoutGrid size={13} strokeWidth={2} />
|
||||||
Widget
|
{t("header.widget")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => { setEditing(undefined); setSvcOpen(true); }}>
|
<Button onClick={() => { setEditing(undefined); setSvcOpen(true); }}>
|
||||||
<Plus size={13} strokeWidth={2.2} />
|
<Plus size={13} strokeWidth={2.2} />
|
||||||
Add service
|
{t("header.addService")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user