Skip to content

Commit 123945c

Browse files
deepracticexsclaude
andcommitted
feat(desktop): add i18n support and AGPL v3 license
- Integrate react-i18next for internationalization - Add zh-CN and en translations - Add language switcher in Settings page - Update license to AGPL-3.0-or-later across all packages Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f7230c4 commit 123945c

13 files changed

Lines changed: 940 additions & 26 deletions

File tree

LICENSE

Lines changed: 661 additions & 0 deletions
Large diffs are not rendered by default.

apps/desktop/electron/main.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ async function createWindow() {
2222
height: 1240,
2323
minWidth: 900,
2424
minHeight: 700,
25-
titleBarStyle: "hiddenInset", // macOS native title bar
26-
trafficLightPosition: { x: 12, y: 12 },
25+
titleBarStyle: "hidden", // macOS - traffic lights overlay on content
26+
trafficLightPosition: { x: 15, y: 14 },
2727
webPreferences: {
2828
preload: join(__dirname, "preload.js"),
2929
contextIsolation: true,

apps/desktop/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "@agentvm/desktop",
33
"version": "0.1.0",
44
"description": "AgentVM Desktop Client",
5+
"license": "AGPL-3.0-or-later",
56
"type": "module",
67
"main": "dist-electron/main.js",
78
"scripts": {
@@ -21,9 +22,12 @@
2122
"agentvm": "workspace:*",
2223
"class-variance-authority": "^0.7.1",
2324
"clsx": "^2.1.1",
25+
"i18next": "^25.7.4",
26+
"i18next-browser-languagedetector": "^8.2.0",
2427
"lucide-react": "^0.469.0",
2528
"react": "^18.3.1",
2629
"react-dom": "^18.3.1",
30+
"react-i18next": "^16.5.3",
2731
"react-resizable-panels": "^4.4.1",
2832
"tailwind-merge": "^2.6.0",
2933
"zustand": "^5.0.10"

apps/desktop/src/components/layout/ActivityBar.tsx

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
import { CircleUser, MessageSquare, Bot, Building2, Settings } from "lucide-react";
2+
import { useTranslation } from "react-i18next";
23
import { cn } from "@/lib/utils";
34
import { useAppStore, type ActiveTab } from "@/stores/app";
45

56
interface ActivityItem {
67
id: ActiveTab | "tenant";
78
icon: React.ComponentType<{ className?: string }>;
8-
label: string;
9+
labelKey: string;
910
position?: "top" | "bottom";
1011
}
1112

1213
const activities: ActivityItem[] = [
13-
{ id: "sessions", icon: MessageSquare, label: "对话", position: "top" },
14-
{ id: "agents", icon: Bot, label: "智能体", position: "top" },
15-
{ id: "tenant", icon: Building2, label: "切换租户", position: "bottom" },
16-
{ id: "settings", icon: Settings, label: "设置", position: "bottom" },
14+
{ id: "sessions", icon: MessageSquare, labelKey: "nav.sessions", position: "top" },
15+
{ id: "agents", icon: Bot, labelKey: "nav.agents", position: "top" },
16+
{ id: "tenant", icon: Building2, labelKey: "nav.switchTenant", position: "bottom" },
17+
{ id: "settings", icon: Settings, labelKey: "nav.settings", position: "bottom" },
1718
];
1819

1920
export function ActivityBar() {
21+
const { t } = useTranslation();
2022
const { activeTab, setActiveTab, currentTenant, openTenantSwitcher } = useAppStore();
2123

2224
const topActivities = activities.filter((a) => a.position === "top");
@@ -33,6 +35,7 @@ export function ActivityBar() {
3335
const renderButton = (item: ActivityItem) => {
3436
const Icon = item.icon;
3537
const isActive = item.id !== "tenant" && activeTab === item.id;
38+
const label = t(item.labelKey);
3639

3740
return (
3841
<button
@@ -44,28 +47,28 @@ export function ActivityBar() {
4447
? "bg-[var(--bg-tertiary)] text-[var(--text-primary)]"
4548
: "text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)]"
4649
)}
47-
title={item.label}
50+
title={label}
4851
>
49-
<Icon className="w-5 h-5" />
52+
<Icon className="w-6 h-6" />
5053

5154
{/* Tooltip */}
5255
<div className="absolute left-full ml-2 px-2 py-1 bg-[var(--text-primary)] text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-50">
53-
{item.label}
56+
{label}
5457
</div>
5558
</button>
5659
);
5760
};
5861

5962
return (
60-
<div className="h-full w-[54px] bg-[var(--bg-secondary)] border-r border-[var(--border-light)] flex flex-col items-center pt-12 pb-3 drag-region">
63+
<div className="h-full w-[76px] bg-[var(--bg-secondary)] border-r border-[var(--border-light)] flex flex-col items-center pb-3 drag-region" style={{ paddingTop: 50 }}>
6164
{/* Avatar */}
6265
<button
63-
className="w-9 h-9 rounded-full bg-gradient-to-br from-[var(--accent-primary)] to-orange-400 flex items-center justify-center text-white mb-4 group relative no-drag"
64-
title="用户"
66+
className="w-10 h-10 rounded-full bg-gradient-to-br from-[var(--accent-primary)] to-orange-400 flex items-center justify-center text-white mb-4 group relative no-drag"
67+
title={t("nav.user")}
6568
>
66-
<CircleUser className="w-5 h-5" />
69+
<CircleUser className="w-6 h-6" />
6770
<div className="absolute left-full ml-2 px-2 py-1 bg-[var(--text-primary)] text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-50">
68-
用户
71+
{t("nav.user")}
6972
</div>
7073
</button>
7174

apps/desktop/src/i18n.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import i18n from "i18next";
2+
import { initReactI18next } from "react-i18next";
3+
import LanguageDetector from "i18next-browser-languagedetector";
4+
5+
import zhCN from "./locales/zh-CN/common.json";
6+
import en from "./locales/en/common.json";
7+
8+
const resources = {
9+
"zh-CN": { translation: zhCN },
10+
en: { translation: en },
11+
};
12+
13+
i18n
14+
.use(LanguageDetector)
15+
.use(initReactI18next)
16+
.init({
17+
resources,
18+
fallbackLng: "zh-CN",
19+
interpolation: {
20+
escapeValue: false, // React already escapes
21+
},
22+
detection: {
23+
order: ["localStorage", "navigator"],
24+
caches: ["localStorage"],
25+
},
26+
});
27+
28+
export default i18n;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"app": {
3+
"name": "AgentVM"
4+
},
5+
"nav": {
6+
"user": "User",
7+
"sessions": "Sessions",
8+
"agents": "Agents",
9+
"switchTenant": "Switch Tenant",
10+
"settings": "Settings"
11+
},
12+
"sessions": {
13+
"title": "Sessions",
14+
"newChat": "New Chat",
15+
"today": "Today",
16+
"yesterday": "Yesterday",
17+
"earlier": "Earlier",
18+
"empty": "No sessions yet",
19+
"startNew": "Start a new conversation"
20+
},
21+
"agents": {
22+
"title": "Agents",
23+
"empty": "No agents yet",
24+
"create": "Create Agent"
25+
},
26+
"settings": {
27+
"title": "Settings",
28+
"general": "General",
29+
"storage": "Storage",
30+
"apiKeys": "API Keys",
31+
"language": "Language",
32+
"languageDesc": "Select display language",
33+
"theme": "Theme",
34+
"about": "About",
35+
"version": "Version"
36+
},
37+
"tenant": {
38+
"switch": "Switch Tenant",
39+
"current": "Current Tenant",
40+
"select": "Select Tenant",
41+
"create": "Create Tenant",
42+
"name": "Tenant Name",
43+
"cancel": "Cancel",
44+
"confirm": "Confirm"
45+
},
46+
"common": {
47+
"loading": "Loading...",
48+
"error": "Something went wrong",
49+
"retry": "Retry",
50+
"save": "Save",
51+
"cancel": "Cancel",
52+
"confirm": "Confirm",
53+
"delete": "Delete",
54+
"edit": "Edit",
55+
"search": "Search",
56+
"developingFeature": "Feature in development..."
57+
}
58+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"app": {
3+
"name": "AgentVM"
4+
},
5+
"nav": {
6+
"user": "用户",
7+
"sessions": "对话",
8+
"agents": "智能体",
9+
"switchTenant": "切换租户",
10+
"settings": "设置"
11+
},
12+
"sessions": {
13+
"title": "对话",
14+
"newChat": "新对话",
15+
"today": "今天",
16+
"yesterday": "昨天",
17+
"earlier": "更早",
18+
"empty": "暂无对话",
19+
"startNew": "开始新对话"
20+
},
21+
"agents": {
22+
"title": "智能体",
23+
"empty": "暂无智能体",
24+
"create": "创建智能体"
25+
},
26+
"settings": {
27+
"title": "设置",
28+
"general": "通用设置",
29+
"storage": "数据存储",
30+
"apiKeys": "API 密钥",
31+
"language": "语言",
32+
"languageDesc": "选择界面显示语言",
33+
"theme": "主题",
34+
"about": "关于",
35+
"version": "版本"
36+
},
37+
"tenant": {
38+
"switch": "切换租户",
39+
"current": "当前租户",
40+
"select": "选择租户",
41+
"create": "创建租户",
42+
"name": "租户名称",
43+
"cancel": "取消",
44+
"confirm": "确认"
45+
},
46+
"common": {
47+
"loading": "加载中...",
48+
"error": "出错了",
49+
"retry": "重试",
50+
"save": "保存",
51+
"cancel": "取消",
52+
"confirm": "确认",
53+
"delete": "删除",
54+
"edit": "编辑",
55+
"search": "搜索",
56+
"developingFeature": "功能开发中..."
57+
}
58+
}

apps/desktop/src/main.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from "react";
22
import ReactDOM from "react-dom/client";
33
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
44
import App from "./App";
5+
import "./i18n";
56
import "./styles/globals.css";
67

78
const queryClient = new QueryClient({

apps/desktop/src/pages/Settings.tsx

Lines changed: 94 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,111 @@
11
import { useState } from "react";
2-
import { Settings, Database, Key, Info } from "lucide-react";
2+
import { useTranslation } from "react-i18next";
3+
import { Settings, Database, Key, Info, ChevronDown } from "lucide-react";
34
import { cn } from "@/lib/utils";
45

56
type SettingsSection = "general" | "storage" | "api" | "about";
67

7-
const settingsSections = [
8-
{ id: "general" as SettingsSection, icon: Settings, label: "通用设置" },
9-
{ id: "storage" as SettingsSection, icon: Database, label: "数据存储" },
10-
{ id: "api" as SettingsSection, icon: Key, label: "API 密钥" },
11-
{ id: "about" as SettingsSection, icon: Info, label: "关于" },
8+
const languages = [
9+
{ code: "zh-CN", label: "简体中文" },
10+
{ code: "en", label: "English" },
1211
];
1312

1413
function SettingsPage() {
14+
const { t, i18n } = useTranslation();
1515
const [activeSection, setActiveSection] = useState<SettingsSection>("general");
16+
const [languageOpen, setLanguageOpen] = useState(false);
17+
18+
const settingsSections = [
19+
{ id: "general" as SettingsSection, icon: Settings, label: t("settings.general") },
20+
{ id: "storage" as SettingsSection, icon: Database, label: t("settings.storage", "数据存储") },
21+
{ id: "api" as SettingsSection, icon: Key, label: t("settings.apiKeys", "API 密钥") },
22+
{ id: "about" as SettingsSection, icon: Info, label: t("settings.about") },
23+
];
24+
25+
const currentLanguage = languages.find((l) => l.code === i18n.language) || languages[0];
26+
27+
const handleLanguageChange = (code: string) => {
28+
i18n.changeLanguage(code);
29+
setLanguageOpen(false);
30+
};
31+
32+
const renderGeneralSettings = () => (
33+
<div className="space-y-6">
34+
{/* Language Setting */}
35+
<div className="flex items-center justify-between">
36+
<div>
37+
<div className="text-sm font-medium text-[var(--text-primary)]">
38+
{t("settings.language")}
39+
</div>
40+
<div className="text-xs text-[var(--text-muted)] mt-0.5">
41+
{t("settings.languageDesc", "选择界面显示语言")}
42+
</div>
43+
</div>
44+
<div className="relative">
45+
<button
46+
onClick={() => setLanguageOpen(!languageOpen)}
47+
className="flex items-center gap-2 px-3 py-1.5 bg-[var(--bg-tertiary)] rounded-lg text-sm text-[var(--text-primary)] hover:bg-[var(--border-light)] transition-colors"
48+
>
49+
{currentLanguage.label}
50+
<ChevronDown className={cn("w-4 h-4 transition-transform", languageOpen && "rotate-180")} />
51+
</button>
52+
{languageOpen && (
53+
<div className="absolute right-0 mt-1 w-32 bg-[var(--bg-card)] border border-[var(--border-light)] rounded-lg shadow-lg overflow-hidden z-10">
54+
{languages.map((lang) => (
55+
<button
56+
key={lang.code}
57+
onClick={() => handleLanguageChange(lang.code)}
58+
className={cn(
59+
"w-full px-3 py-2 text-sm text-left hover:bg-[var(--bg-tertiary)] transition-colors",
60+
lang.code === i18n.language
61+
? "text-[var(--accent-primary)] bg-[var(--bg-tertiary)]"
62+
: "text-[var(--text-primary)]"
63+
)}
64+
>
65+
{lang.label}
66+
</button>
67+
))}
68+
</div>
69+
)}
70+
</div>
71+
</div>
72+
</div>
73+
);
74+
75+
const renderAboutSettings = () => (
76+
<div className="space-y-4">
77+
<div className="flex items-center justify-between">
78+
<span className="text-sm text-[var(--text-secondary)]">{t("settings.version")}</span>
79+
<span className="text-sm text-[var(--text-primary)]">0.1.0</span>
80+
</div>
81+
<div className="flex items-center justify-between">
82+
<span className="text-sm text-[var(--text-secondary)]">Electron</span>
83+
<span className="text-sm text-[var(--text-primary)]">35.x</span>
84+
</div>
85+
</div>
86+
);
87+
88+
const renderContent = () => {
89+
switch (activeSection) {
90+
case "general":
91+
return renderGeneralSettings();
92+
case "about":
93+
return renderAboutSettings();
94+
default:
95+
return (
96+
<p className="text-[var(--text-muted)]">
97+
{t("common.developingFeature", "功能开发中...")}
98+
</p>
99+
);
100+
}
101+
};
16102

17103
return (
18104
<div className="h-full flex">
19105
{/* Sidebar - Settings Nav */}
20106
<div className="w-[260px] bg-[var(--bg-secondary)] border-r border-[var(--border-light)] flex flex-col">
21107
<div className="p-3 border-b border-[var(--border-light)]">
22-
<h2 className="text-sm font-medium text-[var(--text-primary)]">设置</h2>
108+
<h2 className="text-sm font-medium text-[var(--text-primary)]">{t("settings.title")}</h2>
23109
</div>
24110

25111
<div className="flex-1 overflow-y-auto p-2">
@@ -53,7 +139,7 @@ function SettingsPage() {
53139
</h1>
54140

55141
<div className="bg-[var(--bg-card)] rounded-lg border border-[var(--border-light)] p-4">
56-
<p className="text-[var(--text-muted)]">设置内容开发中...</p>
142+
{renderContent()}
57143
</div>
58144
</div>
59145
</div>

0 commit comments

Comments
 (0)