feat(panel): i18n

This commit is contained in:
Lemon-miaow committed 2026-06-27 18:31:26 +08:00
1 parent a5a64821d8
commit dd2fc6fa82
47 files changed
+898 -231

No files matched your search

+23 -23
View File
@@ -1,5 +1,6 @@
import { Link } from "react-router-dom";
import { ServerCog, Boxes, ShieldCheck } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Card, CardContent } from "@/components/ui/card";
import { useTier } from "@/lib/tier";
@@ -7,33 +8,34 @@ import { useTier } from "@/lib/tier";
// content/server-admin surfaces. It deliberately states the four-power boundary
// so an admin never expects cluster-lifecycle levers here — those stay kubectl/CRD.
const LINKS = [
{
to: "/admin/servers",
icon: ServerCog,
title: "Servers",
body: "Create platform servers from the structured §15 form and manage the ones you operate.",
},
{
to: "/admin/images",
icon: Boxes,
title: "Images",
body: "The platform image whitelist — the value space the create form draws from.",
},
];
export function AdminHome() {
const { identity } = useTier();
const { t } = useTranslation("admin");
const LINKS = [
{
to: "/admin/servers",
icon: ServerCog,
title: t("servers"),
body: t("servers_desc"),
},
{
to: "/admin/images",
icon: Boxes,
title: t("images"),
body: t("images_desc"),
},
];
return (
<div className="mx-auto max-w-4xl space-y-6">
<div className="flex items-center gap-3">
<ShieldCheck className="h-6 w-6 text-primary" />
<div>
<h1 className="text-2xl font-semibold tracking-tight">Admin</h1>
<h1 className="text-2xl font-semibold tracking-tight">{t("title")}</h1>
<p className="text-sm text-muted-foreground">
Server &amp; content administration
{identity?.email ? ` · signed in as ${identity.email}` : ""}.
{t("subtitle")}
{identity?.email ? t("signed_in_as", { email: identity.email }) : ""}.
</p>
</div>
</div>
@@ -54,11 +56,9 @@ export function AdminHome() {
<Card className="border-dashed bg-transparent">
<CardContent className="p-4 text-xs text-muted-foreground">
Cluster scaling, RBAC, Secrets and control-plane lifecycle are{" "}
<span className="font-medium">kubectl / CRD operations</span> and are
intentionally not available from the panel — the four-power separation
(build / runtime / operator / app) is preserved. This is a window onto the
platform, not a lever for operator power.
{t("footer_pre")}
<span className="font-medium">{t("footer_kubectl")}</span>
{t("footer_post")}
</CardContent>
</Card>
</div>
+8 -8
View File
@@ -1,4 +1,5 @@
import { Boxes, CheckCircle2, CircleSlash } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Loading, ErrorState, EmptyState } from "@/components/States";
@@ -11,17 +12,16 @@ import { useAsync } from "@/lib/hooks";
// added, so this page surfaces the whitelist honestly and labels the missing
// mutation rather than shipping dead buttons.
export function ImageAdmin() {
const { t } = useTranslation("admin");
const { data, error, loading, reload } = useAsync(() => api.listImages(), []);
const images = data ?? [];
return (
<div className="mx-auto max-w-4xl space-y-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Images</h1>
<h1 className="text-2xl font-semibold tracking-tight">{t("images_title")}</h1>
<p className="text-sm text-muted-foreground">
The platform image whitelist. Only enabled images can back a new server.
Editing the whitelist from the Web is auditable (spec §14) — the mutation
routes are a pending admin-tier slice, so this view is read-only for now.
{t("images_subtitle")}
</p>
</div>
@@ -31,8 +31,8 @@ export function ImageAdmin() {
<ErrorState error={error} onRetry={reload} />
) : images.length === 0 ? (
<EmptyState
title="No images whitelisted"
hint="The whitelist is empty — a platform admin must add one (CLI for now)."
title={t("no_images_title")}
hint={t("no_images_hint")}
/>
) : (
<div className="space-y-2">
@@ -50,11 +50,11 @@ export function ImageAdmin() {
</div>
{img.enabled ? (
<span className="inline-flex shrink-0 items-center gap-1 text-xs text-primary">
<CheckCircle2 className="h-3.5 w-3.5" /> enabled
<CheckCircle2 className="h-3.5 w-3.5" /> {t("enabled")}
</span>
) : (
<span className="inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground">
<CircleSlash className="h-3.5 w-3.5" /> disabled
<CircleSlash className="h-3.5 w-3.5" /> {t("disabled")}
</span>
)}
</CardContent>
+9 -10
View File
@@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next";
import { Card, CardContent } from "@/components/ui/card";
import { ServerCard } from "@/components/ServerCard";
import { CreateServerDialog } from "@/components/CreateServerDialog";
@@ -17,16 +18,17 @@ import { useAsync, useConfig } from "@/lib/hooks";
export function ServerAdmin() {
const cfg = useConfig();
const { data, error, loading, reload } = useAsync(() => api.myServers(), []);
const { t } = useTranslation("admin");
const { t: ts } = useTranslation("servers");
const servers = data ?? [];
return (
<div className="mx-auto max-w-6xl space-y-6">
<div className="flex items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Servers</h1>
<h1 className="text-2xl font-semibold tracking-tight">{t("servers")}</h1>
<p className="text-sm text-muted-foreground">
Create platform servers from the structured form and manage the ones
you operate.
{t("server_admin_subtitle")}
</p>
</div>
{cfg && <CreateServerDialog cfg={cfg} onCreated={reload} />}
@@ -37,11 +39,11 @@ export function ServerAdmin() {
) : error ? (
<ErrorState error={error} onRetry={reload} />
) : !cfg ? (
<Loading label="Loading config…" />
<Loading label={ts("common:loading_config")} />
) : servers.length === 0 ? (
<EmptyState
title="No servers under your management yet"
hint="Use Create server to provision one from the vetted spec."
title={ts("no_servers_managed")}
hint={ts("no_servers_managed_hint")}
/>
) : (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
@@ -53,10 +55,7 @@ export function ServerAdmin() {
<Card className="border-dashed bg-transparent">
<CardContent className="p-4 text-xs text-muted-foreground">
Server creation goes through the structured form only — the platform maps
your choices onto a vetted Kubernetes spec. Raw cluster config (host
networking, host paths, arbitrary images, privileged pods) is never
expressible here (spec §15).
{ts("server_admin_footer")}
</CardContent>
</Card>
</div>