Skip to content

Commit 78dcab4

Browse files
jiaminghuajiaminghua
authored andcommitted
feat: add i18n framework with Chinese (zh-CN) localization
- Add react-i18next + i18next for internationalization support - Create English and Simplified Chinese translation files (73 keys) - Localize core UI components: - Connection modal (connect, disconnect, local/remote sections) - Tab context menu (rename, close, flag, backgrounds, tab bar position) - Block frame header (split, magnify, settings, close) - AI panel header and context menu (widget context, new chat, configure modes) - Settings/config page (config files, visual/raw JSON, save, errors) - About modal (description, version, links) - Modal footer (OK/Cancel buttons) - Expose global t() function via window.__waveI18n for non-React contexts - Default language set to zh-CN with English fallback
1 parent 388b4c9 commit 78dcab4

File tree

14 files changed

+333
-858
lines changed

14 files changed

+333
-858
lines changed

frontend/app/aipanel/aipanel-contextmenu.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ import { isDev } from "@/app/store/global";
77
import { globalStore } from "@/app/store/jotaiStore";
88
import { RpcApi } from "@/app/store/wshclientapi";
99
import { TabRpcClient } from "@/app/store/wshrpcutil";
10+
import i18n from "@/app/i18n/index";
1011
import { WaveAIModel } from "./waveai-model";
1112

13+
const t = i18n.t.bind(i18n);
14+
1215
export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boolean): Promise<void> {
1316
e.preventDefault();
1417
e.stopPropagation();
@@ -27,7 +30,7 @@ export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boo
2730
}
2831

2932
menu.push({
30-
label: "New Chat",
33+
label: t("app.newChat"),
3134
click: () => {
3235
model.clearChat();
3336
},
@@ -121,14 +124,14 @@ export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boo
121124
}
122125

123126
menu.push({
124-
label: "Max Output Tokens",
127+
label: t("app.maxOutputTokens"),
125128
submenu: maxTokensSubmenu,
126129
});
127130

128131
menu.push({ type: "separator" });
129132

130133
menu.push({
131-
label: "Configure Modes",
134+
label: t("app.configureModes"),
132135
click: () => {
133136
RpcApi.RecordTEventCommand(
134137
TabRpcClient,
@@ -148,7 +151,7 @@ export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boo
148151
menu.push({ type: "separator" });
149152

150153
menu.push({
151-
label: "Hide Wave AI",
154+
label: t("app.hideWaveAI"),
152155
click: () => {
153156
model.closeWaveAIPanel();
154157
},

frontend/app/aipanel/aipanelheader.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
import { handleWaveAIContextMenu } from "@/app/aipanel/aipanel-contextmenu";
55
import { useAtomValue } from "jotai";
66
import { memo } from "react";
7+
import { useTranslation } from "react-i18next";
78
import { WaveAIModel } from "./waveai-model";
89

910
export const AIPanelHeader = memo(() => {
11+
const { t } = useTranslation();
1012
const model = WaveAIModel.getInstance();
1113
const widgetAccess = useAtomValue(model.widgetAccessAtom);
1214
const inBuilder = model.inBuilder;
@@ -32,8 +34,8 @@ export const AIPanelHeader = memo(() => {
3234
<div className="flex items-center flex-shrink-0 whitespace-nowrap">
3335
{!inBuilder && (
3436
<div className="flex items-center text-sm whitespace-nowrap">
35-
<span className="text-gray-300 @xs:hidden mr-1 text-[12px]">Context</span>
36-
<span className="text-gray-300 hidden @xs:inline mr-2 text-[12px]">Widget Context</span>
37+
<span className="text-gray-300 @xs:hidden mr-1 text-[12px]">{t("app.context")}</span>
38+
<span className="text-gray-300 hidden @xs:inline mr-2 text-[12px]">{t("app.widgetContext")}</span>
3739
<button
3840
onClick={() => {
3941
model.setWidgetAccess(!widgetAccess);
@@ -44,7 +46,7 @@ export const AIPanelHeader = memo(() => {
4446
className={`relative inline-flex h-6 w-14 items-center rounded-full transition-colors cursor-pointer ${
4547
widgetAccess ? "bg-accent-600" : "bg-zinc-600"
4648
}`}
47-
title={`Widget Access ${widgetAccess ? "ON" : "OFF"}`}
49+
title={t("app.widgetAccess", { state: widgetAccess ? t("app.on") : t("app.off") })}
4850
>
4951
<span
5052
className={`absolute inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
@@ -56,7 +58,7 @@ export const AIPanelHeader = memo(() => {
5658
widgetAccess ? "ml-2.5 mr-6 text-left" : "ml-6 mr-1 text-right"
5759
}`}
5860
>
59-
{widgetAccess ? "ON" : "OFF"}
61+
{widgetAccess ? t("app.on") : t("app.off")}
6062
</span>
6163
</button>
6264
</div>
@@ -65,7 +67,7 @@ export const AIPanelHeader = memo(() => {
6567
<button
6668
onClick={handleKebabClick}
6769
className="text-gray-400 hover:text-white cursor-pointer transition-colors p-1 rounded flex-shrink-0 ml-2 focus:outline-none"
68-
title="More options"
70+
title={t("app.moreOptions")}
6971
>
7072
<i className="fa fa-ellipsis-vertical"></i>
7173
</button>

frontend/app/app.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright 2026, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
import "./i18n/index";
5+
46
import {
57
clearBadgesForBlockOnFocus,
68
clearBadgesForTabOnFocus,

frontend/app/block/blockframe-header.tsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import * as util from "@/util/util";
2828
import { cn, makeIconClass } from "@/util/util";
2929
import * as jotai from "jotai";
3030
import * as React from "react";
31+
import { useTranslation } from "react-i18next";
3132
import { BlockEnv } from "./blockenv";
3233
import { BlockFrameProps } from "./blocktypes";
3334

@@ -40,17 +41,18 @@ function handleHeaderContextMenu(
4041
) {
4142
e.preventDefault();
4243
e.stopPropagation();
44+
const t = window.__waveI18n.t;
4345
const magnified = globalStore.get(nodeModel.isMagnified);
4446
const menu: ContextMenuItem[] = [
4547
{
46-
label: magnified ? "Un-Magnify Block" : "Magnify Block",
48+
label: magnified ? t("app.unMagnifyBlock") : t("app.magnifyBlock"),
4749
click: () => {
4850
nodeModel.toggleMagnify();
4951
},
5052
},
5153
{ type: "separator" },
5254
{
53-
label: "Copy BlockId",
55+
label: t("app.copyBlockId"),
5456
click: () => {
5557
navigator.clipboard.writeText(blockId);
5658
},
@@ -61,7 +63,7 @@ function handleHeaderContextMenu(
6163
menu.push(
6264
{ type: "separator" },
6365
{
64-
label: "Close Block",
66+
label: t("app.closeBlock"),
6567
click: () => uxCloseBlock(blockId),
6668
}
6769
);
@@ -76,6 +78,7 @@ type HeaderTextElemsProps = {
7678
};
7779

7880
const HeaderTextElems = React.memo(({ viewModel, blockId, preview, error }: HeaderTextElemsProps) => {
81+
const { t } = useTranslation();
7982
const waveEnv = useWaveEnv<BlockEnv>();
8083
const frameTextAtom = waveEnv.getBlockMetaKeyAtom(blockId, "frame:text");
8184
const frameText = jotai.useAtomValue(frameTextAtom);
@@ -102,7 +105,7 @@ const HeaderTextElems = React.memo(({ viewModel, blockId, preview, error }: Head
102105
<div className="iconbutton disabled" key="controller-status" onClick={copyHeaderErr}>
103106
<i
104107
className="fa-sharp fa-solid fa-triangle-exclamation"
105-
title={"Error Rendering View Header: " + error.message}
108+
title={t("app.errorRenderingViewHeader", { error: error.message })}
106109
/>
107110
</div>
108111
);
@@ -119,6 +122,7 @@ type HeaderEndIconsProps = {
119122
};
120123

121124
const HeaderEndIcons = React.memo(({ viewModel, nodeModel, blockId }: HeaderEndIconsProps) => {
125+
const { t } = useTranslation();
122126
const blockEnv = useWaveEnv<BlockEnv>();
123127
const endIconButtons = util.useAtomValueSafe(viewModel?.endIconButtons);
124128
const magnified = jotai.useAtomValue(nodeModel.isMagnified);
@@ -136,7 +140,7 @@ const HeaderEndIcons = React.memo(({ viewModel, nodeModel, blockId }: HeaderEndI
136140
const splitHorizontalDecl: IconButtonDecl = {
137141
elemtype: "iconbutton",
138142
icon: "columns",
139-
title: "Split Horizontally",
143+
title: t("app.splitHorizontally"),
140144
click: (e) => {
141145
e.stopPropagation();
142146
const blockAtom = WOS.getWaveObjectAtom<Block>(WOS.makeORef("block", blockId));
@@ -150,7 +154,7 @@ const HeaderEndIcons = React.memo(({ viewModel, nodeModel, blockId }: HeaderEndI
150154
const splitVerticalDecl: IconButtonDecl = {
151155
elemtype: "iconbutton",
152156
icon: "grip-lines",
153-
title: "Split Vertically",
157+
title: t("app.splitVertically"),
154158
click: (e) => {
155159
e.stopPropagation();
156160
const blockAtom = WOS.getWaveObjectAtom<Block>(WOS.makeORef("block", blockId));
@@ -167,15 +171,15 @@ const HeaderEndIcons = React.memo(({ viewModel, nodeModel, blockId }: HeaderEndI
167171
const settingsDecl: IconButtonDecl = {
168172
elemtype: "iconbutton",
169173
icon: "cog",
170-
title: "Settings",
174+
title: t("app.settings"),
171175
click: (e) => handleHeaderContextMenu(e, blockId, viewModel, nodeModel, blockEnv),
172176
};
173177
endIconsElem.push(<IconButton key="settings" decl={settingsDecl} className="block-frame-settings" />);
174178
if (ephemeral) {
175179
const addToLayoutDecl: IconButtonDecl = {
176180
elemtype: "iconbutton",
177181
icon: "circle-plus",
178-
title: "Add to Layout",
182+
title: t("app.addToLayout"),
179183
click: () => {
180184
nodeModel.addEphemeralNodeToLayout();
181185
},
@@ -198,7 +202,7 @@ const HeaderEndIcons = React.memo(({ viewModel, nodeModel, blockId }: HeaderEndI
198202
const closeDecl: IconButtonDecl = {
199203
elemtype: "iconbutton",
200204
icon: "xmark-large",
201-
title: "Close",
205+
title: t("app.close"),
202206
click: () => uxCloseBlock(nodeModel.blockId),
203207
};
204208
endIconsElem.push(<IconButton key="close" decl={closeDecl} className="block-frame-default-close" />);

frontend/app/i18n/index.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import i18n from "i18next";
2+
import { initReactI18next } from "react-i18next";
3+
4+
import en from "./locales/en.json";
5+
import zhCN from "./locales/zh-CN.json";
6+
7+
i18n.use(initReactI18next).init({
8+
resources: {
9+
en: { translation: en },
10+
"zh-CN": { translation: zhCN },
11+
},
12+
lng: "zh-CN", // default language
13+
fallbackLng: "en",
14+
interpolation: {
15+
escapeValue: false,
16+
},
17+
});
18+
19+
export default i18n;
20+
21+
// Expose a global t function for use in non-React contexts (e.g. event handlers, menus)
22+
declare global {
23+
interface Window {
24+
__waveI18n: { t: typeof i18n.t };
25+
}
26+
}
27+
window.__waveI18n = { t: i18n.t.bind(i18n) };

frontend/app/i18n/locales/en.json

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
{
2+
"app.tabBarPosition": "Tab Bar Position",
3+
"app.top": "Top",
4+
"app.left": "Left",
5+
"app.renameTab": "Rename Tab",
6+
"app.copyTabId": "Copy TabId",
7+
"app.flagTab": "Flag Tab",
8+
"app.none": "None",
9+
"app.green": "Green",
10+
"app.teal": "Teal",
11+
"app.blue": "Blue",
12+
"app.purple": "Purple",
13+
"app.red": "Red",
14+
"app.orange": "Orange",
15+
"app.yellow": "Yellow",
16+
"app.backgrounds": "Backgrounds",
17+
"app.default": "Default",
18+
"app.closeTab": "Close Tab",
19+
"app.magnifyBlock": "Magnify Block",
20+
"app.unMagnifyBlock": "Un-Magnify Block",
21+
"app.copyBlockId": "Copy BlockId",
22+
"app.closeBlock": "Close Block",
23+
"app.addToLayout": "Add to Layout",
24+
"app.splitHorizontally": "Split Horizontally",
25+
"app.splitVertically": "Split Vertically",
26+
"app.settings": "Settings",
27+
"app.close": "Close",
28+
"app.connectTo": "Connect to (username@host)...",
29+
"app.local": "Local",
30+
"app.remote": "Remote",
31+
"app.editConnections": "Edit Connections",
32+
"app.reconnectTo": "Reconnect to {{connection}}",
33+
"app.disconnect": "Disconnect {{connection}}",
34+
"app.newConnection": "{{name}} (New Connection)",
35+
"app.gitBash": "Git Bash",
36+
"app.waveAI": "Wave AI",
37+
"app.context": "Context",
38+
"app.widgetContext": "Widget Context",
39+
"app.widgetAccess": "Widget Access {{state}}",
40+
"app.on": "ON",
41+
"app.off": "OFF",
42+
"app.moreOptions": "More options",
43+
"app.newChat": "New Chat",
44+
"app.maxOutputTokens": "Max Output Tokens",
45+
"app.configureModes": "Configure Modes",
46+
"app.hideWaveAI": "Hide Wave AI",
47+
"app.about": "About",
48+
"app.waveTerminal": "Wave Terminal",
49+
"app.version": "Version",
50+
"app.configFiles": "Config Files",
51+
"app.connections": "Connections",
52+
"app.themes": "Themes",
53+
"app.keybindings": "Keybindings",
54+
"app.errorRenderingViewHeader": "Error Rendering View Header: {{error}}",
55+
"app.cancel": "Cancel",
56+
"app.ok": "Ok",
57+
"app.aboutDescription": "Open-Source AI-Integrated Terminal",
58+
"app.aboutTagline": "Built for Seamless Workflows",
59+
"app.clientVersion": "Client Version",
60+
"app.updateChannel": "Update Channel",
61+
"app.github": "GitHub",
62+
"app.website": "Website",
63+
"app.openSource": "Open Source",
64+
"app.sponsor": "Sponsor",
65+
"app.viewDocumentation": "View documentation",
66+
"app.visual": "Visual",
67+
"app.rawJson": "Raw JSON",
68+
"app.saving": "Saving...",
69+
"app.save": "Save",
70+
"app.unsavedChanges": "Unsaved changes",
71+
"app.loading": "Loading...",
72+
"app.configError": "Config Error"
73+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
{
2+
"app.tabBarPosition": "标签页栏位置",
3+
"app.top": "顶部",
4+
"app.left": "左侧",
5+
"app.renameTab": "重命名标签页",
6+
"app.copyTabId": "复制标签页ID",
7+
"app.flagTab": "标记标签页",
8+
"app.none": "",
9+
"app.green": "绿色",
10+
"app.teal": "青色",
11+
"app.blue": "蓝色",
12+
"app.purple": "紫色",
13+
"app.red": "红色",
14+
"app.orange": "橙色",
15+
"app.yellow": "黄色",
16+
"app.backgrounds": "背景",
17+
"app.default": "默认",
18+
"app.closeTab": "关闭标签页",
19+
"app.magnifyBlock": "放大区块",
20+
"app.unMagnifyBlock": "取消放大区块",
21+
"app.copyBlockId": "复制区块ID",
22+
"app.closeBlock": "关闭区块",
23+
"app.addToLayout": "添加到布局",
24+
"app.splitHorizontally": "水平拆分",
25+
"app.splitVertically": "垂直拆分",
26+
"app.settings": "设置",
27+
"app.close": "关闭",
28+
"app.connectTo": "连接到 (用户名@主机)...",
29+
"app.local": "本地",
30+
"app.remote": "远程",
31+
"app.editConnections": "编辑连接",
32+
"app.reconnectTo": "重新连接到 {{connection}}",
33+
"app.disconnect": "断开连接 {{connection}}",
34+
"app.newConnection": "{{name}} (新建连接)",
35+
"app.gitBash": "Git Bash",
36+
"app.waveAI": "Wave AI",
37+
"app.context": "上下文",
38+
"app.widgetContext": "小组件上下文",
39+
"app.widgetAccess": "小组件访问 {{state}}",
40+
"app.on": "开启",
41+
"app.off": "关闭",
42+
"app.moreOptions": "更多选项",
43+
"app.newChat": "新建对话",
44+
"app.maxOutputTokens": "最大输出令牌数",
45+
"app.configureModes": "配置模式",
46+
"app.hideWaveAI": "隐藏 Wave AI",
47+
"app.about": "关于",
48+
"app.waveTerminal": "Wave 终端",
49+
"app.version": "版本",
50+
"app.configFiles": "配置文件",
51+
"app.connections": "连接",
52+
"app.themes": "主题",
53+
"app.keybindings": "快捷键",
54+
"app.errorRenderingViewHeader": "渲染视图标题错误:{{error}}",
55+
"app.cancel": "取消",
56+
"app.ok": "确定",
57+
"app.aboutDescription": "开源 AI 集成终端",
58+
"app.aboutTagline": "为流畅工作流而生",
59+
"app.clientVersion": "客户端版本",
60+
"app.updateChannel": "更新频道",
61+
"app.github": "GitHub",
62+
"app.website": "官网",
63+
"app.openSource": "开源",
64+
"app.sponsor": "赞助",
65+
"app.viewDocumentation": "查看文档",
66+
"app.visual": "可视化",
67+
"app.rawJson": "原始 JSON",
68+
"app.saving": "保存中...",
69+
"app.save": "保存",
70+
"app.unsavedChanges": "未保存的更改",
71+
"app.loading": "加载中...",
72+
"app.configError": "配置错误"
73+
}

0 commit comments

Comments
 (0)