Skip to content

Commit ecbb8a7

Browse files
author
sam
committed
Add Web internationalization foundation
1 parent 09411f6 commit ecbb8a7

93 files changed

Lines changed: 3075 additions & 1487 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/web/package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
"dependencies": {
1313
"@agents-core-web/agents-client": "workspace:*",
1414
"@tanstack/react-table": "^8.21.3",
15+
"i18next": "^26.4.2",
1516
"lucide-react": "^1.22.0",
1617
"react": "^19.2.7",
1718
"react-dom": "^19.2.7",
19+
"react-i18next": "^17.0.15",
1820
"react-markdown": "^10.1.0",
1921
"uplot": "^1.6.32"
2022
},

‎apps/web/src/App.tsx‎

Lines changed: 55 additions & 52 deletions
Large diffs are not rendered by default.

‎apps/web/src/components/ConnectionModal.tsx‎

Lines changed: 62 additions & 90 deletions
Large diffs are not rendered by default.

‎apps/web/src/components/CreateMenu.tsx‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
Plus,
66
} from "lucide-react";
77
import { useEffect, useRef, useState, type KeyboardEvent } from "react";
8+
import { useTranslation } from "react-i18next";
89

910
interface CreateMenuActions {
1011
canCreateAgent: boolean;
@@ -19,24 +20,26 @@ export function CreateMenuContent({
1920
onCreateAgent,
2021
onStartSession,
2122
}: CreateMenuActions) {
23+
const { t } = useTranslation();
2224
return (
23-
<div className="create-menu-panel" role="menu" aria-label="Create">
25+
<div className="create-menu-panel" role="menu" aria-label={t("actions.create")}>
2426
<button type="button" role="menuitem" onClick={onCreateAgent} disabled={!canCreateAgent}>
2527
<Bot size={16} strokeWidth={1.5} aria-hidden="true" />
26-
<span><strong>Agent</strong><small>Save a reusable Agent definition</small></span>
28+
<span><strong>{t("createMenu.agent")}</strong><small>{t("createMenu.agentDescription")}</small></span>
2729
</button>
2830
<button type="button" role="menuitem" onClick={onStartSession} disabled={!canStartSession}>
2931
<MessageSquare size={16} strokeWidth={1.5} aria-hidden="true" />
3032
<span>
31-
<strong>Start Session</strong>
32-
<small>{canStartSession ? "Choose a Session-compatible saved Agent" : "Requires a loaded Session-compatible Agent"}</small>
33+
<strong>{t("createMenu.session")}</strong>
34+
<small>{canStartSession ? t("createMenu.sessionDescription") : t("createMenu.sessionDisabledDescription")}</small>
3335
</span>
3436
</button>
3537
</div>
3638
);
3739
}
3840

3941
export function CreateMenu(actions: CreateMenuActions) {
42+
const { t } = useTranslation();
4043
const [open, setOpen] = useState(false);
4144
const rootRef = useRef<HTMLDivElement>(null);
4245
const triggerRef = useRef<HTMLButtonElement>(null);
@@ -99,7 +102,7 @@ export function CreateMenu(actions: CreateMenuActions) {
99102
}}
100103
>
101104
<Plus size={14} strokeWidth={1.8} aria-hidden="true" />
102-
Create
105+
{t("actions.create")}
103106
<ChevronDown size={13} strokeWidth={1.5} aria-hidden="true" />
104107
</button>
105108
{open ? (

‎apps/web/src/components/ErrorState.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { AlertTriangle, RefreshCw } from "lucide-react";
22
import type { ReactNode } from "react";
3+
import { useTranslation } from "react-i18next";
34

45
interface ErrorStateProps {
56
title: string;
@@ -21,6 +22,7 @@ export function ErrorState({
2122
action,
2223
className,
2324
}: ErrorStateProps) {
25+
const { t } = useTranslation();
2426
return (
2527
<div className={["error-state", className].filter(Boolean).join(" ")}>
2628
<div className="error-state-heading">
@@ -37,7 +39,7 @@ export function ErrorState({
3739
{onRetry ? (
3840
<button className="button outline" type="button" onClick={onRetry}>
3941
<RefreshCw size={14} strokeWidth={1.5} aria-hidden="true" />
40-
Retry
42+
{t("actions.retry")}
4143
</button>
4244
) : null}
4345
{action}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Languages } from "lucide-react";
2+
import { useTranslation } from "react-i18next";
3+
4+
import { setLanguage, type SupportedLanguage } from "../i18n";
5+
6+
export function LanguageMenu() {
7+
const { i18n, t } = useTranslation("navigation");
8+
const language: SupportedLanguage = i18n.resolvedLanguage?.startsWith("zh") ? "zh-CN" : "en";
9+
const options: Array<{ value: SupportedLanguage; shortLabel: string; label: string }> = [
10+
{ value: "en", shortLabel: "EN", label: t("english") },
11+
{ value: "zh-CN", shortLabel: "中", label: t("chinese") },
12+
];
13+
14+
return (
15+
<div className="language-menu" role="group" aria-label={t("language")}>
16+
<Languages size={14} strokeWidth={1.5} aria-hidden="true" />
17+
{options.map((option) => (
18+
<button
19+
className={language === option.value ? "active" : ""}
20+
key={option.value}
21+
type="button"
22+
aria-pressed={language === option.value}
23+
aria-label={option.label}
24+
title={option.label}
25+
onClick={() => void setLanguage(option.value)}
26+
>
27+
{option.shortLabel}
28+
</button>
29+
))}
30+
</div>
31+
);
32+
}

‎apps/web/src/components/Modal.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { X } from "lucide-react";
22
import { useEffect, useId, useRef, useState, type PropsWithChildren, type ReactNode } from "react";
3+
import { useTranslation } from "react-i18next";
34

45
interface ModalProps extends PropsWithChildren {
56
open: boolean;
@@ -9,6 +10,7 @@ interface ModalProps extends PropsWithChildren {
910
}
1011

1112
export function Modal({ open, title, footer, onClose, children }: ModalProps) {
13+
const { t } = useTranslation();
1214
const dialogRef = useRef<HTMLElement>(null);
1315
const onCloseRef = useRef(onClose);
1416
const previousFocusRef = useRef<HTMLElement | null>(null);
@@ -115,7 +117,7 @@ export function Modal({ open, title, footer, onClose, children }: ModalProps) {
115117
>
116118
<header className="modal-header">
117119
<h2 id={titleId}>{title}</h2>
118-
<button className="icon-button" type="button" onClick={onClose} aria-label="Close dialog">
120+
<button className="icon-button" type="button" onClick={onClose} aria-label={t("actions.closeDialog")}>
119121
<X size={14} strokeWidth={1.5} />
120122
</button>
121123
</header>

‎apps/web/src/components/ProductNavigation.tsx‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { Bot, LayoutDashboard, Layers3, MessageSquare, Vault, type LucideIcon } from "lucide-react";
2+
import { useTranslation } from "react-i18next";
23

34
export type ProductView = "dashboard" | "agents" | "sessions" | "vaults" | "templates";
45

5-
const productViews: Array<{ id: ProductView; label: string; icon: LucideIcon }> = [
6-
{ id: "dashboard", label: "Dashboard", icon: LayoutDashboard },
7-
{ id: "agents", label: "Agents", icon: Bot },
8-
{ id: "sessions", label: "Sessions", icon: MessageSquare },
9-
{ id: "templates", label: "Templates", icon: Layers3 },
10-
{ id: "vaults", label: "Vaults", icon: Vault },
6+
const productViews: Array<{ id: ProductView; label: "dashboard" | "agents" | "sessions" | "templates" | "vaults"; icon: LucideIcon }> = [
7+
{ id: "dashboard", label: "dashboard", icon: LayoutDashboard },
8+
{ id: "agents", label: "agents", icon: Bot },
9+
{ id: "sessions", label: "sessions", icon: MessageSquare },
10+
{ id: "templates", label: "templates", icon: Layers3 },
11+
{ id: "vaults", label: "vaults", icon: Vault },
1112
];
1213

1314
export function ProductNavigation({
@@ -21,9 +22,10 @@ export function ProductNavigation({
2122
showVaults?: boolean;
2223
showTemplates?: boolean;
2324
}) {
25+
const { t } = useTranslation("navigation");
2426
return (
25-
<nav className="main-nav product-navigation" aria-label="Agents product">
26-
<p className="nav-label">Workspace</p>
27+
<nav className="main-nav product-navigation" aria-label={t("productLabel")}>
28+
<p className="nav-label">{t("workspace")}</p>
2729
{productViews.filter((item) => (item.id !== "vaults" || showVaults) && (item.id !== "templates" || showTemplates)).map((item) => {
2830
const Icon = item.icon;
2931
return (
@@ -32,11 +34,11 @@ export function ProductNavigation({
3234
className={active === item.id ? "active" : ""}
3335
key={item.id}
3436
onClick={() => onSelect(item.id)}
35-
aria-label={item.label}
37+
aria-label={t(item.label)}
3638
aria-current={active === item.id ? "page" : undefined}
3739
>
3840
<Icon size={15} strokeWidth={1.5} aria-hidden="true" />
39-
<span>{item.label}</span>
41+
<span>{t(item.label)}</span>
4042
</button>
4143
);
4244
})}
Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { Layers3, Server } from "lucide-react";
2-
import { useLocale } from "../lib/LocaleProvider";
2+
import { useTranslation } from "react-i18next";
33
export type SystemView = "system" | "sandbox";
44
export function SystemNavigation({ active, onSelect }: { active: SystemView | null; onSelect: (view: SystemView) => void }) {
5-
const { t, locale, setLocale } = useLocale();
6-
return <nav className="main-nav" aria-label={t("System navigation")} lang={locale}>
7-
<p className="nav-label">{t("System")}</p>
8-
{([{ id: "system", label: t("System"), icon: Layers3 }, { id: "sandbox", label: t("Hosted Sandbox Manager"), icon: Server }] as const).map(({ id, label, icon: Icon }) => (
9-
<button key={id} type="button" className={active === id ? "active" : ""} onClick={() => onSelect(id)} aria-label={label} aria-current={active === id ? "page" : undefined}>
10-
<Icon size={15} strokeWidth={1.5} aria-hidden="true" /><span>{label}</span>
5+
const { t } = useTranslation("navigation");
6+
return <nav className="main-nav" aria-label={t("systemNavigation")}>
7+
<p className="nav-label">{t("system")}</p>
8+
{([{ id: "system", label: "system", icon: Layers3 }, { id: "sandbox", label: "sandbox", icon: Server }] as const).map(({ id, label, icon: Icon }) => (
9+
<button key={id} type="button" className={active === id ? "active" : ""} onClick={() => onSelect(id)} aria-label={t(label)} aria-current={active === id ? "page" : undefined}>
10+
<Icon size={15} strokeWidth={1.5} aria-hidden="true" /><span>{t(label)}</span>
1111
</button>
1212
))}
13-
<label className="field"><span>Language / 语言</span><select aria-label="Language / 语言" value={locale} onChange={(event) => setLocale(event.target.value === "zh" ? "zh" : "en")}><option value="en">English</option><option value="zh">中文</option></select></label>
1413
</nav>;
1514
}

‎apps/web/src/components/ThemeMenu.tsx‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import { Moon, Sun } from "lucide-react";
2+
import { useTranslation } from "react-i18next";
23

34
import { useTheme, type ResolvedTheme } from "../lib/theme";
45

56
export function ThemeMenu() {
67
const { resolvedTheme, setPreference } = useTheme();
8+
const { t } = useTranslation("navigation");
79
const segments: Array<{ value: ResolvedTheme; Icon: typeof Sun; label: string }> = [
8-
{ value: "light", Icon: Sun, label: "Light theme" },
9-
{ value: "dark", Icon: Moon, label: "Dark theme" },
10+
{ value: "light", Icon: Sun, label: t("lightTheme") },
11+
{ value: "dark", Icon: Moon, label: t("darkTheme") },
1012
];
1113

1214
return (
13-
<div className="theme-menu" role="group" aria-label="Theme">
15+
<div className="theme-menu" role="group" aria-label={t("theme")}>
1416
{segments.map(({ value, Icon, label }) => {
1517
const pressed = resolvedTheme === value;
1618
return (

0 commit comments

Comments
 (0)