From 78dcab4255a8df6e9c5bbbfdc8d89f4c519308f2 Mon Sep 17 00:00:00 2001 From: jiaminghua Date: Sat, 4 Apr 2026 23:04:13 +0800 Subject: [PATCH 1/3] 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 --- frontend/app/aipanel/aipanel-contextmenu.ts | 11 +- frontend/app/aipanel/aipanelheader.tsx | 12 +- frontend/app/app.tsx | 2 + frontend/app/block/blockframe-header.tsx | 22 +- frontend/app/i18n/index.ts | 27 + frontend/app/i18n/locales/en.json | 73 ++ frontend/app/i18n/locales/zh-CN.json | 73 ++ frontend/app/modals/about.tsx | 20 +- frontend/app/modals/conntypeahead.tsx | 26 +- frontend/app/modals/modal.tsx | 6 +- frontend/app/tab/tabcontextmenu.ts | 9 +- frontend/app/view/waveconfig/waveconfig.tsx | 19 +- package-lock.json | 889 ++------------------ package.json | 2 + 14 files changed, 333 insertions(+), 858 deletions(-) create mode 100644 frontend/app/i18n/index.ts create mode 100644 frontend/app/i18n/locales/en.json create mode 100644 frontend/app/i18n/locales/zh-CN.json diff --git a/frontend/app/aipanel/aipanel-contextmenu.ts b/frontend/app/aipanel/aipanel-contextmenu.ts index 4e78389198..c8068376ea 100644 --- a/frontend/app/aipanel/aipanel-contextmenu.ts +++ b/frontend/app/aipanel/aipanel-contextmenu.ts @@ -7,8 +7,11 @@ import { isDev } from "@/app/store/global"; import { globalStore } from "@/app/store/jotaiStore"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; +import i18n from "@/app/i18n/index"; import { WaveAIModel } from "./waveai-model"; +const t = i18n.t.bind(i18n); + export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boolean): Promise { e.preventDefault(); e.stopPropagation(); @@ -27,7 +30,7 @@ export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boo } menu.push({ - label: "New Chat", + label: t("app.newChat"), click: () => { model.clearChat(); }, @@ -121,14 +124,14 @@ export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boo } menu.push({ - label: "Max Output Tokens", + label: t("app.maxOutputTokens"), submenu: maxTokensSubmenu, }); menu.push({ type: "separator" }); menu.push({ - label: "Configure Modes", + label: t("app.configureModes"), click: () => { RpcApi.RecordTEventCommand( TabRpcClient, @@ -148,7 +151,7 @@ export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boo menu.push({ type: "separator" }); menu.push({ - label: "Hide Wave AI", + label: t("app.hideWaveAI"), click: () => { model.closeWaveAIPanel(); }, diff --git a/frontend/app/aipanel/aipanelheader.tsx b/frontend/app/aipanel/aipanelheader.tsx index da54f6c9e9..76345ad070 100644 --- a/frontend/app/aipanel/aipanelheader.tsx +++ b/frontend/app/aipanel/aipanelheader.tsx @@ -4,9 +4,11 @@ import { handleWaveAIContextMenu } from "@/app/aipanel/aipanel-contextmenu"; import { useAtomValue } from "jotai"; import { memo } from "react"; +import { useTranslation } from "react-i18next"; import { WaveAIModel } from "./waveai-model"; export const AIPanelHeader = memo(() => { + const { t } = useTranslation(); const model = WaveAIModel.getInstance(); const widgetAccess = useAtomValue(model.widgetAccessAtom); const inBuilder = model.inBuilder; @@ -32,8 +34,8 @@ export const AIPanelHeader = memo(() => {
{!inBuilder && (
- Context - Widget Context + {t("app.context")} + {t("app.widgetContext")}
@@ -65,7 +67,7 @@ export const AIPanelHeader = memo(() => { diff --git a/frontend/app/app.tsx b/frontend/app/app.tsx index e9c70a35df..205d34f270 100644 --- a/frontend/app/app.tsx +++ b/frontend/app/app.tsx @@ -1,6 +1,8 @@ // Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import "./i18n/index"; + import { clearBadgesForBlockOnFocus, clearBadgesForTabOnFocus, diff --git a/frontend/app/block/blockframe-header.tsx b/frontend/app/block/blockframe-header.tsx index a70f323e71..7af5d4a115 100644 --- a/frontend/app/block/blockframe-header.tsx +++ b/frontend/app/block/blockframe-header.tsx @@ -28,6 +28,7 @@ import * as util from "@/util/util"; import { cn, makeIconClass } from "@/util/util"; import * as jotai from "jotai"; import * as React from "react"; +import { useTranslation } from "react-i18next"; import { BlockEnv } from "./blockenv"; import { BlockFrameProps } from "./blocktypes"; @@ -40,17 +41,18 @@ function handleHeaderContextMenu( ) { e.preventDefault(); e.stopPropagation(); + const t = window.__waveI18n.t; const magnified = globalStore.get(nodeModel.isMagnified); const menu: ContextMenuItem[] = [ { - label: magnified ? "Un-Magnify Block" : "Magnify Block", + label: magnified ? t("app.unMagnifyBlock") : t("app.magnifyBlock"), click: () => { nodeModel.toggleMagnify(); }, }, { type: "separator" }, { - label: "Copy BlockId", + label: t("app.copyBlockId"), click: () => { navigator.clipboard.writeText(blockId); }, @@ -61,7 +63,7 @@ function handleHeaderContextMenu( menu.push( { type: "separator" }, { - label: "Close Block", + label: t("app.closeBlock"), click: () => uxCloseBlock(blockId), } ); @@ -76,6 +78,7 @@ type HeaderTextElemsProps = { }; const HeaderTextElems = React.memo(({ viewModel, blockId, preview, error }: HeaderTextElemsProps) => { + const { t } = useTranslation(); const waveEnv = useWaveEnv(); const frameTextAtom = waveEnv.getBlockMetaKeyAtom(blockId, "frame:text"); const frameText = jotai.useAtomValue(frameTextAtom); @@ -102,7 +105,7 @@ const HeaderTextElems = React.memo(({ viewModel, blockId, preview, error }: Head
); @@ -119,6 +122,7 @@ type HeaderEndIconsProps = { }; const HeaderEndIcons = React.memo(({ viewModel, nodeModel, blockId }: HeaderEndIconsProps) => { + const { t } = useTranslation(); const blockEnv = useWaveEnv(); const endIconButtons = util.useAtomValueSafe(viewModel?.endIconButtons); const magnified = jotai.useAtomValue(nodeModel.isMagnified); @@ -136,7 +140,7 @@ const HeaderEndIcons = React.memo(({ viewModel, nodeModel, blockId }: HeaderEndI const splitHorizontalDecl: IconButtonDecl = { elemtype: "iconbutton", icon: "columns", - title: "Split Horizontally", + title: t("app.splitHorizontally"), click: (e) => { e.stopPropagation(); const blockAtom = WOS.getWaveObjectAtom(WOS.makeORef("block", blockId)); @@ -150,7 +154,7 @@ const HeaderEndIcons = React.memo(({ viewModel, nodeModel, blockId }: HeaderEndI const splitVerticalDecl: IconButtonDecl = { elemtype: "iconbutton", icon: "grip-lines", - title: "Split Vertically", + title: t("app.splitVertically"), click: (e) => { e.stopPropagation(); const blockAtom = WOS.getWaveObjectAtom(WOS.makeORef("block", blockId)); @@ -167,7 +171,7 @@ const HeaderEndIcons = React.memo(({ viewModel, nodeModel, blockId }: HeaderEndI const settingsDecl: IconButtonDecl = { elemtype: "iconbutton", icon: "cog", - title: "Settings", + title: t("app.settings"), click: (e) => handleHeaderContextMenu(e, blockId, viewModel, nodeModel, blockEnv), }; endIconsElem.push(); @@ -175,7 +179,7 @@ const HeaderEndIcons = React.memo(({ viewModel, nodeModel, blockId }: HeaderEndI const addToLayoutDecl: IconButtonDecl = { elemtype: "iconbutton", icon: "circle-plus", - title: "Add to Layout", + title: t("app.addToLayout"), click: () => { nodeModel.addEphemeralNodeToLayout(); }, @@ -198,7 +202,7 @@ const HeaderEndIcons = React.memo(({ viewModel, nodeModel, blockId }: HeaderEndI const closeDecl: IconButtonDecl = { elemtype: "iconbutton", icon: "xmark-large", - title: "Close", + title: t("app.close"), click: () => uxCloseBlock(nodeModel.blockId), }; endIconsElem.push(); diff --git a/frontend/app/i18n/index.ts b/frontend/app/i18n/index.ts new file mode 100644 index 0000000000..e80321c836 --- /dev/null +++ b/frontend/app/i18n/index.ts @@ -0,0 +1,27 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; + +import en from "./locales/en.json"; +import zhCN from "./locales/zh-CN.json"; + +i18n.use(initReactI18next).init({ + resources: { + en: { translation: en }, + "zh-CN": { translation: zhCN }, + }, + lng: "zh-CN", // default language + fallbackLng: "en", + interpolation: { + escapeValue: false, + }, +}); + +export default i18n; + +// Expose a global t function for use in non-React contexts (e.g. event handlers, menus) +declare global { + interface Window { + __waveI18n: { t: typeof i18n.t }; + } +} +window.__waveI18n = { t: i18n.t.bind(i18n) }; \ No newline at end of file diff --git a/frontend/app/i18n/locales/en.json b/frontend/app/i18n/locales/en.json new file mode 100644 index 0000000000..76453e87ae --- /dev/null +++ b/frontend/app/i18n/locales/en.json @@ -0,0 +1,73 @@ +{ + "app.tabBarPosition": "Tab Bar Position", + "app.top": "Top", + "app.left": "Left", + "app.renameTab": "Rename Tab", + "app.copyTabId": "Copy TabId", + "app.flagTab": "Flag Tab", + "app.none": "None", + "app.green": "Green", + "app.teal": "Teal", + "app.blue": "Blue", + "app.purple": "Purple", + "app.red": "Red", + "app.orange": "Orange", + "app.yellow": "Yellow", + "app.backgrounds": "Backgrounds", + "app.default": "Default", + "app.closeTab": "Close Tab", + "app.magnifyBlock": "Magnify Block", + "app.unMagnifyBlock": "Un-Magnify Block", + "app.copyBlockId": "Copy BlockId", + "app.closeBlock": "Close Block", + "app.addToLayout": "Add to Layout", + "app.splitHorizontally": "Split Horizontally", + "app.splitVertically": "Split Vertically", + "app.settings": "Settings", + "app.close": "Close", + "app.connectTo": "Connect to (username@host)...", + "app.local": "Local", + "app.remote": "Remote", + "app.editConnections": "Edit Connections", + "app.reconnectTo": "Reconnect to {{connection}}", + "app.disconnect": "Disconnect {{connection}}", + "app.newConnection": "{{name}} (New Connection)", + "app.gitBash": "Git Bash", + "app.waveAI": "Wave AI", + "app.context": "Context", + "app.widgetContext": "Widget Context", + "app.widgetAccess": "Widget Access {{state}}", + "app.on": "ON", + "app.off": "OFF", + "app.moreOptions": "More options", + "app.newChat": "New Chat", + "app.maxOutputTokens": "Max Output Tokens", + "app.configureModes": "Configure Modes", + "app.hideWaveAI": "Hide Wave AI", + "app.about": "About", + "app.waveTerminal": "Wave Terminal", + "app.version": "Version", + "app.configFiles": "Config Files", + "app.connections": "Connections", + "app.themes": "Themes", + "app.keybindings": "Keybindings", + "app.errorRenderingViewHeader": "Error Rendering View Header: {{error}}", + "app.cancel": "Cancel", + "app.ok": "Ok", + "app.aboutDescription": "Open-Source AI-Integrated Terminal", + "app.aboutTagline": "Built for Seamless Workflows", + "app.clientVersion": "Client Version", + "app.updateChannel": "Update Channel", + "app.github": "GitHub", + "app.website": "Website", + "app.openSource": "Open Source", + "app.sponsor": "Sponsor", + "app.viewDocumentation": "View documentation", + "app.visual": "Visual", + "app.rawJson": "Raw JSON", + "app.saving": "Saving...", + "app.save": "Save", + "app.unsavedChanges": "Unsaved changes", + "app.loading": "Loading...", + "app.configError": "Config Error" +} diff --git a/frontend/app/i18n/locales/zh-CN.json b/frontend/app/i18n/locales/zh-CN.json new file mode 100644 index 0000000000..9273003927 --- /dev/null +++ b/frontend/app/i18n/locales/zh-CN.json @@ -0,0 +1,73 @@ +{ + "app.tabBarPosition": "标签页栏位置", + "app.top": "顶部", + "app.left": "左侧", + "app.renameTab": "重命名标签页", + "app.copyTabId": "复制标签页ID", + "app.flagTab": "标记标签页", + "app.none": "无", + "app.green": "绿色", + "app.teal": "青色", + "app.blue": "蓝色", + "app.purple": "紫色", + "app.red": "红色", + "app.orange": "橙色", + "app.yellow": "黄色", + "app.backgrounds": "背景", + "app.default": "默认", + "app.closeTab": "关闭标签页", + "app.magnifyBlock": "放大区块", + "app.unMagnifyBlock": "取消放大区块", + "app.copyBlockId": "复制区块ID", + "app.closeBlock": "关闭区块", + "app.addToLayout": "添加到布局", + "app.splitHorizontally": "水平拆分", + "app.splitVertically": "垂直拆分", + "app.settings": "设置", + "app.close": "关闭", + "app.connectTo": "连接到 (用户名@主机)...", + "app.local": "本地", + "app.remote": "远程", + "app.editConnections": "编辑连接", + "app.reconnectTo": "重新连接到 {{connection}}", + "app.disconnect": "断开连接 {{connection}}", + "app.newConnection": "{{name}} (新建连接)", + "app.gitBash": "Git Bash", + "app.waveAI": "Wave AI", + "app.context": "上下文", + "app.widgetContext": "小组件上下文", + "app.widgetAccess": "小组件访问 {{state}}", + "app.on": "开启", + "app.off": "关闭", + "app.moreOptions": "更多选项", + "app.newChat": "新建对话", + "app.maxOutputTokens": "最大输出令牌数", + "app.configureModes": "配置模式", + "app.hideWaveAI": "隐藏 Wave AI", + "app.about": "关于", + "app.waveTerminal": "Wave 终端", + "app.version": "版本", + "app.configFiles": "配置文件", + "app.connections": "连接", + "app.themes": "主题", + "app.keybindings": "快捷键", + "app.errorRenderingViewHeader": "渲染视图标题错误:{{error}}", + "app.cancel": "取消", + "app.ok": "确定", + "app.aboutDescription": "开源 AI 集成终端", + "app.aboutTagline": "为流畅工作流而生", + "app.clientVersion": "客户端版本", + "app.updateChannel": "更新频道", + "app.github": "GitHub", + "app.website": "官网", + "app.openSource": "开源", + "app.sponsor": "赞助", + "app.viewDocumentation": "查看文档", + "app.visual": "可视化", + "app.rawJson": "原始 JSON", + "app.saving": "保存中...", + "app.save": "保存", + "app.unsavedChanges": "未保存的更改", + "app.loading": "加载中...", + "app.configError": "配置错误" +} diff --git a/frontend/app/modals/about.tsx b/frontend/app/modals/about.tsx index 08c0e2210e..a26f9d3cdc 100644 --- a/frontend/app/modals/about.tsx +++ b/frontend/app/modals/about.tsx @@ -9,6 +9,7 @@ import { TabRpcClient } from "@/app/store/wshrpcutil"; import { isDev } from "@/util/isdev"; import { fireAndForget } from "@/util/util"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { getApi } from "../store/global"; import { Modal } from "./modal"; @@ -19,6 +20,7 @@ interface AboutModalVProps { } const AboutModalV = ({ versionString, updaterChannel, onClose }: AboutModalVProps) => { + const { t } = useTranslation(); const currentDate = new Date(); return ( @@ -27,17 +29,17 @@ const AboutModalV = ({ versionString, updaterChannel, onClose }: AboutModalVProp
-
Wave Terminal
+
{t("app.waveTerminal")}
- Open-Source AI-Integrated Terminal + {t("app.aboutDescription")}
- Built for Seamless Workflows + {t("app.aboutTagline")}
- Client Version {versionString} + {t("app.clientVersion")} {versionString}
- Update Channel: {updaterChannel} + {t("app.updateChannel")}: {updaterChannel}
diff --git a/frontend/app/modals/conntypeahead.tsx b/frontend/app/modals/conntypeahead.tsx index 95cf831e24..645da694f1 100644 --- a/frontend/app/modals/conntypeahead.tsx +++ b/frontend/app/modals/conntypeahead.tsx @@ -1,6 +1,7 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { useTranslation } from "react-i18next"; import { computeConnColorNum } from "@/app/block/blockutil"; import { TypeAheadModal } from "@/app/modals/typeaheadmodal"; import { ConnectionsModel } from "@/app/store/connections-model"; @@ -119,6 +120,7 @@ function getReconnectItem( blockId: string, changeConnModalAtom: jotai.PrimitiveAtom ): SuggestionConnectionItem | null { + const t = window.__waveI18n.t; if (connSelected != "" || (connStatus.status != "disconnected" && connStatus.status != "error")) { return null; } @@ -126,7 +128,7 @@ function getReconnectItem( status: "connected", icon: "arrow-right-arrow-left", iconColor: "var(--grey-text-color)", - label: `Reconnect to ${connStatus.connection}`, + label: t("app.reconnectTo", { connection: connStatus.connection }), value: "", onSelect: async (_: string) => { globalStore.set(changeConnModalAtom, false); @@ -151,6 +153,7 @@ function getLocalSuggestions( filterOutNowsh: boolean, hasGitBash: boolean ): SuggestionConnectionScope | null { + const t = window.__waveI18n.t; const wslFiltered = filterConnections(connList, connSelected, fullConfig, filterOutNowsh); const wslSuggestionItems = createWslSuggestionItems(wslFiltered, connection, connStatusMap); const localSuggestionItem = createFilteredLocalSuggestionItem(localName, connection, connSelected); @@ -162,7 +165,7 @@ function getLocalSuggestions( icon: "laptop", iconColor: "var(--grey-text-color)", value: "local:gitbash", - label: "Git Bash", + label: t("app.gitBash"), current: connection === "local:gitbash", }); } @@ -173,7 +176,7 @@ function getLocalSuggestions( return null; } const localSuggestions: SuggestionConnectionScope = { - headerText: "Local", + headerText: t("app.local"), items: sortedSuggestionItems, }; return localSuggestions; @@ -187,6 +190,7 @@ function getRemoteSuggestions( fullConfig: FullConfigType, filterOutNowsh: boolean ): SuggestionConnectionScope | null { + const t = window.__waveI18n.t; const filtered = filterConnections(connList, connSelected, fullConfig, filterOutNowsh); const suggestionItems = createRemoteSuggestionItems(filtered, connection, connStatusMap); const sortedSuggestionItems = sortConnSuggestionItems(suggestionItems, fullConfig); @@ -194,7 +198,7 @@ function getRemoteSuggestions( return null; } const remoteSuggestions: SuggestionConnectionScope = { - headerText: "Remote", + headerText: t("app.remote"), items: sortedSuggestionItems, }; return remoteSuggestions; @@ -205,6 +209,7 @@ function getDisconnectItem( connStatusMap: Map, changeConnModalAtom: jotai.PrimitiveAtom ): SuggestionConnectionItem | null { + const t = window.__waveI18n.t; if (util.isLocalConnName(connection)) { return null; } @@ -216,7 +221,7 @@ function getDisconnectItem( status: "connected", icon: "xmark", iconColor: "var(--grey-text-color)", - label: `Disconnect ${connStatus.connection}`, + label: t("app.disconnect", { connection: connStatus.connection }), value: "", onSelect: async (_: string) => { globalStore.set(changeConnModalAtom, false); @@ -231,6 +236,7 @@ function getConnectionsEditItem( changeConnModalAtom: jotai.PrimitiveAtom, connSelected: string ): SuggestionConnectionItem | null { + const t = window.__waveI18n.t; if (connSelected != "") { return null; } @@ -238,8 +244,8 @@ function getConnectionsEditItem( status: "disconnected", icon: "gear", iconColor: "var(--grey-text-color)", - value: "Edit Connections", - label: "Edit Connections", + value: t("app.editConnections"), + label: t("app.editConnections"), onSelect: () => { util.fireAndForget(async () => { globalStore.set(changeConnModalAtom, false); @@ -270,11 +276,12 @@ function getNewConnectionSuggestionItem( // with the exact name already exists return null; } + const t = window.__waveI18n.t; const newConnectionSuggestion: SuggestionConnectionItem = { status: "connected", icon: "plus", iconColor: "var(--grey-text-color)", - label: `${connSelected} (New Connection)`, + label: t("app.newConnection", { name: connSelected }), value: "", onSelect: (_: string) => { changeConnection(connSelected); @@ -300,6 +307,7 @@ const ChangeConnectionBlockModal = React.memo( changeConnModalAtom: jotai.PrimitiveAtom; nodeModel: NodeModel; }) => { + const { t } = useTranslation(); const [connSelected, setConnSelected] = React.useState(""); const changeConnModalOpen = jotai.useAtomValue(changeConnModalAtom); const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); @@ -486,7 +494,7 @@ const ChangeConnectionBlockModal = React.memo( onKeyDown={(e) => keyutil.keydownWrapper(handleTypeAheadKeyDown)(e)} onChange={(current: string) => setConnSelected(current)} value={connSelected} - label="Connect to (username@host)..." + label={t("app.connectTo")} onClickBackdrop={() => globalStore.set(changeConnModalAtom, false)} /> ); diff --git a/frontend/app/modals/modal.tsx b/frontend/app/modals/modal.tsx index 5733ec2608..e403f39553 100644 --- a/frontend/app/modals/modal.tsx +++ b/frontend/app/modals/modal.tsx @@ -6,6 +6,7 @@ import { cn } from "@/util/util"; import clsx from "clsx"; import { forwardRef } from "react"; import ReactDOM from "react-dom"; +import { useTranslation } from "react-i18next"; import "./modal.scss"; @@ -97,16 +98,17 @@ const ModalFooter = ({ okDisabled, cancelDisabled, }: ModalFooterProps) => { + const { t } = useTranslation(); return (
{onCancel && ( )} {onOk && ( )}
diff --git a/frontend/app/tab/tabcontextmenu.ts b/frontend/app/tab/tabcontextmenu.ts index bc87302d4c..c583bdb2cd 100644 --- a/frontend/app/tab/tabcontextmenu.ts +++ b/frontend/app/tab/tabcontextmenu.ts @@ -1,6 +1,7 @@ // Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import i18n from "@/app/i18n"; import { getOrefMetaKeyAtom, globalStore, recordTEvent } from "@/app/store/global"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { fireAndForget } from "@/util/util"; @@ -17,23 +18,25 @@ const FlagColors: { label: string; value: string }[] = [ { label: "Yellow", value: "#FFE900" }, ]; +const t = i18n.t.bind(i18n); + export function buildTabBarContextMenu(env: TabEnv): ContextMenuItem[] { const currentTabBar = globalStore.get(env.getSettingsKeyAtom("app:tabbar")) ?? "top"; const tabBarSubmenu: ContextMenuItem[] = [ { - label: "Top", + label: t("app.top"), type: "checkbox", checked: currentTabBar === "top", click: () => fireAndForget(() => env.rpc.SetConfigCommand(TabRpcClient, { "app:tabbar": "top" })), }, { - label: "Left", + label: t("app.left"), type: "checkbox", checked: currentTabBar === "left", click: () => fireAndForget(() => env.rpc.SetConfigCommand(TabRpcClient, { "app:tabbar": "left" })), }, ]; - return [{ label: "Tab Bar Position", type: "submenu", submenu: tabBarSubmenu }]; + return [{ label: t("app.tabBarPosition"), type: "submenu", submenu: tabBarSubmenu }]; } export function buildTabContextMenu( diff --git a/frontend/app/view/waveconfig/waveconfig.tsx b/frontend/app/view/waveconfig/waveconfig.tsx index ca06295bfc..a29f83720b 100644 --- a/frontend/app/view/waveconfig/waveconfig.tsx +++ b/frontend/app/view/waveconfig/waveconfig.tsx @@ -13,12 +13,14 @@ import { cn } from "@/util/util"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; import type * as MonacoTypes from "monaco-editor"; import { memo, useCallback, useEffect } from "react"; +import { useTranslation } from "react-i18next"; interface ConfigSidebarProps { model: WaveConfigViewModel; } const ConfigSidebar = memo(({ model }: ConfigSidebarProps) => { + const { t } = useTranslation(); const selectedFile = useAtomValue(model.selectedFileAtom); const setIsMenuOpen = useSetAtom(model.isMenuOpenAtom); const configFiles = model.getConfigFiles(); @@ -35,7 +37,7 @@ const ConfigSidebar = memo(({ model }: ConfigSidebarProps) => { return (
- Config Files + {t("app.configFiles")} @@ -245,7 +248,7 @@ const WaveConfigView = memo(({ blockId, model }: ViewComponentProps - Visual + {t("app.visual")} {/* No guard needed: visual tab saves changes immediately via RPC */}
)} @@ -286,7 +289,7 @@ const WaveConfigView = memo(({ blockId, model }: ViewComponentProps {isLoading ? (
- Loading... + {t("app.loading")}
) : selectedFile.visualComponent && (!selectedFile.hasJsonView || activeTab === "visual") ? ( @@ -314,7 +317,7 @@ const WaveConfigView = memo(({ blockId, model }: ViewComponentProps {configErrors.map((cerr, i) => (
- Config Error: + {t("app.configError")}: {cerr.file}: {cerr.err}
))} diff --git a/package-lock.json b/package-lock.json index 9211ad86f4..1dabb7bdc5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "fast-average-color": "^9.5.0", "htl": "^0.3.1", "html-to-image": "^1.11.13", + "i18next": "^26.0.3", "immer": "^10.1.1", "jotai": "2.9.3", "mermaid": "^11.12.3", @@ -55,6 +56,7 @@ "react-dnd-html5-backend": "^16.0.1", "react-dom": "^19.2.0", "react-frame-component": "^5.2.7", + "react-i18next": "^17.0.2", "react-markdown": "^9.0.3", "react-resizable-panels": "^3.0.6", "react-zoom-pan-pinch": "^3.7.0", @@ -2217,9 +2219,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -4826,40 +4828,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@electron/windows-sign": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", - "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "cross-dirname": "^0.1.0", - "debug": "^4.3.4", - "fs-extra": "^11.1.1", - "minimist": "^1.2.8", - "postject": "^1.0.0-alpha.6" - }, - "bin": { - "electron-windows-sign": "bin/electron-windows-sign.js" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -5576,468 +5544,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", - "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", - "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", - "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", - "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", - "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", - "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", - "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", - "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", - "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", - "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", - "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", - "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", - "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", - "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", - "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", - "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", - "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", - "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", - "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/runtime": "^1.4.4" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", - "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", - "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", - "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -13190,15 +12696,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cross-dirname": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", - "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -14877,19 +14374,6 @@ "node": ">=14.0.0" } }, - "node_modules/electron-builder-squirrel-windows": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", - "integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "app-builder-lib": "26.8.1", - "builder-util": "26.8.1", - "electron-winstaller": "5.4.0" - } - }, "node_modules/electron-builder/node_modules/builder-util-runtime": { "version": "9.5.1", "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", @@ -15047,66 +14531,6 @@ } } }, - "node_modules/electron-winstaller": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", - "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@electron/asar": "^3.2.1", - "debug": "^4.1.1", - "fs-extra": "^7.0.1", - "lodash": "^4.17.21", - "temp": "^0.9.0" - }, - "engines": { - "node": ">=8.0.0" - }, - "optionalDependencies": { - "@electron/windows-sign": "^1.1.2" - } - }, - "node_modules/electron-winstaller/node_modules/fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/electron-winstaller/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "license": "MIT", - "peer": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-winstaller/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/electron/node_modules/@types/node": { "version": "24.10.13", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", @@ -17679,6 +17103,15 @@ "node": ">=14" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-tags": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", @@ -17946,6 +17379,37 @@ "node": ">=10.18" } }, + "node_modules/i18next": { + "version": "26.0.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.3.tgz", + "integrity": "sha512-1571kXINxHKY7LksWp8wP+zP0YqHSSpl/OW0Y0owFEf2H3s8gCAffWaZivcz14rMkOvn3R/psiQxVsR9t2Nafg==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/iconv-corefoundation": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", @@ -22592,20 +22056,6 @@ "node": ">= 18" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -25682,36 +25132,6 @@ "postcss": "^8.4.31" } }, - "node_modules/postject": { - "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", - "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "commander": "^9.4.0" - }, - "bin": { - "postject": "dist/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/postject/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": "^12.20.0 || >=14" - } - }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -26282,12 +25702,32 @@ "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/react-is": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", - "integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==", + "node_modules/react-i18next": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.2.tgz", + "integrity": "sha512-shBftH2vaTWK2Bsp7FiL+cevx3xFJlvFxmsDFQSrJc+6twHkP0tv/bGa01VVWzpreUVVwU+3Hev5iFqRg65RwA==", "license": "MIT", - "peer": true + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.0.1", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } }, "node_modules/react-json-view-lite": { "version": "2.5.0", @@ -28113,58 +27553,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/roarr": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", @@ -28364,6 +27752,7 @@ "version": "1.91.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.91.0.tgz", "integrity": "sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==", + "dev": true, "license": "MIT", "dependencies": { "chokidar": "^4.0.0", @@ -28512,13 +27901,6 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, - "node_modules/search-insights": { - "version": "2.17.3", - "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", - "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", - "license": "MIT", - "peer": true - }, "node_modules/section-matter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", @@ -28874,61 +28256,6 @@ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", "license": "MIT" }, - "node_modules/sharp": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", - "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.4", - "semver": "^7.7.2" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.3", - "@img/sharp-darwin-x64": "0.34.3", - "@img/sharp-libvips-darwin-arm64": "1.2.0", - "@img/sharp-libvips-darwin-x64": "1.2.0", - "@img/sharp-libvips-linux-arm": "1.2.0", - "@img/sharp-libvips-linux-arm64": "1.2.0", - "@img/sharp-libvips-linux-ppc64": "1.2.0", - "@img/sharp-libvips-linux-s390x": "1.2.0", - "@img/sharp-libvips-linux-x64": "1.2.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", - "@img/sharp-libvips-linuxmusl-x64": "1.2.0", - "@img/sharp-linux-arm": "0.34.3", - "@img/sharp-linux-arm64": "0.34.3", - "@img/sharp-linux-ppc64": "0.34.3", - "@img/sharp-linux-s390x": "0.34.3", - "@img/sharp-linux-x64": "0.34.3", - "@img/sharp-linuxmusl-arm64": "0.34.3", - "@img/sharp-linuxmusl-x64": "0.34.3", - "@img/sharp-wasm32": "0.34.3", - "@img/sharp-win32-arm64": "0.34.3", - "@img/sharp-win32-ia32": "0.34.3", - "@img/sharp-win32-x64": "0.34.3" - } - }, - "node_modules/sharp/node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -29805,46 +29132,6 @@ "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", "license": "MIT" }, - "node_modules/svgo": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz", - "integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "commander": "^11.1.0", - "css-select": "^5.1.0", - "css-tree": "^3.0.1", - "css-what": "^6.1.0", - "csso": "^5.0.5", - "picocolors": "^1.1.1", - "sax": "^1.4.1" - }, - "bin": { - "svgo": "bin/svgo.js" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" - } - }, - "node_modules/svgo/node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=16" - } - }, "node_modules/swr": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz", @@ -29962,21 +29249,6 @@ "node": ">=18" } }, - "node_modules/temp": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", - "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mkdirp": "^0.5.1", - "rimraf": "~2.6.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/temp-file": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", @@ -32343,6 +31615,15 @@ "node": ">=14.0.0" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/vscode-jsonrpc": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", @@ -33340,16 +32621,6 @@ "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==", "license": "MIT" }, - "node_modules/zod": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", - "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index fd4928b9be..695d2965c1 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "fast-average-color": "^9.5.0", "htl": "^0.3.1", "html-to-image": "^1.11.13", + "i18next": "^26.0.3", "immer": "^10.1.1", "jotai": "2.9.3", "mermaid": "^11.12.3", @@ -109,6 +110,7 @@ "react-dnd-html5-backend": "^16.0.1", "react-dom": "^19.2.0", "react-frame-component": "^5.2.7", + "react-i18next": "^17.0.2", "react-markdown": "^9.0.3", "react-resizable-panels": "^3.0.6", "react-zoom-pan-pinch": "^3.7.0", From 177c749d451aef8ae1cab874e757c1c58f00c716 Mon Sep 17 00:00:00 2001 From: jiaminghua Date: Sat, 4 Apr 2026 23:40:25 +0800 Subject: [PATCH 2/3] fix: address CodeRabbit review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Complete tab context menu localization (rename, copy ID, flag tab, backgrounds, close tab, flag colors) - Fix 'Ok' → 'OK' capitalization in en.json - Auto-detect system language instead of hardcoding zh-CN - Persist language preference to localStorage - Support language switching via i18n.changeLanguage() --- frontend/app/i18n/index.ts | 24 ++- frontend/app/i18n/locales/en.json | 2 +- frontend/app/tab/tabcontextmenu.ts | 16 +- package-lock.json | 226 ++++++++++++++++++++++++++++- 4 files changed, 255 insertions(+), 13 deletions(-) diff --git a/frontend/app/i18n/index.ts b/frontend/app/i18n/index.ts index e80321c836..f42739314f 100644 --- a/frontend/app/i18n/index.ts +++ b/frontend/app/i18n/index.ts @@ -4,24 +4,42 @@ import { initReactI18next } from "react-i18next"; import en from "./locales/en.json"; import zhCN from "./locales/zh-CN.json"; +// Detect system language and map to supported locale +function detectLanguage(): string { + const sysLang = navigator.language || "en"; + if (sysLang.startsWith("zh")) { + return "zh-CN"; + } + return "en"; +} + +// Allow override via localStorage +const savedLang = localStorage.getItem("wave:language"); +const initialLang = savedLang || detectLanguage(); + i18n.use(initReactI18next).init({ resources: { en: { translation: en }, "zh-CN": { translation: zhCN }, }, - lng: "zh-CN", // default language + lng: initialLang, fallbackLng: "en", interpolation: { escapeValue: false, }, }); +// Save language preference on change +i18n.on("languageChanged", (lng: string) => { + localStorage.setItem("wave:language", lng); +}); + export default i18n; // Expose a global t function for use in non-React contexts (e.g. event handlers, menus) declare global { interface Window { - __waveI18n: { t: typeof i18n.t }; + __waveI18n: { t: typeof i18n.t; changeLanguage: typeof i18n.changeLanguage }; } } -window.__waveI18n = { t: i18n.t.bind(i18n) }; \ No newline at end of file +window.__waveI18n = { t: i18n.t.bind(i18n), changeLanguage: i18n.changeLanguage.bind(i18n) }; diff --git a/frontend/app/i18n/locales/en.json b/frontend/app/i18n/locales/en.json index 76453e87ae..defd0d04de 100644 --- a/frontend/app/i18n/locales/en.json +++ b/frontend/app/i18n/locales/en.json @@ -53,7 +53,7 @@ "app.keybindings": "Keybindings", "app.errorRenderingViewHeader": "Error Rendering View Header: {{error}}", "app.cancel": "Cancel", - "app.ok": "Ok", + "app.ok": "OK", "app.aboutDescription": "Open-Source AI-Integrated Terminal", "app.aboutTagline": "Built for Seamless Workflows", "app.clientVersion": "Client Version", diff --git a/frontend/app/tab/tabcontextmenu.ts b/frontend/app/tab/tabcontextmenu.ts index c583bdb2cd..d393fc98d8 100644 --- a/frontend/app/tab/tabcontextmenu.ts +++ b/frontend/app/tab/tabcontextmenu.ts @@ -47,9 +47,9 @@ export function buildTabContextMenu( ): ContextMenuItem[] { const menu: ContextMenuItem[] = []; menu.push( - { label: "Rename Tab", click: () => renameRef.current?.() }, + { label: t("app.renameTab"), click: () => renameRef.current?.() }, { - label: "Copy TabId", + label: t("app.copyTabId"), click: () => fireAndForget(() => navigator.clipboard.writeText(id)), }, { type: "separator" } @@ -58,7 +58,7 @@ export function buildTabContextMenu( const currentFlagColor = globalStore.get(getOrefMetaKeyAtom(tabORef, "tab:flagcolor")) ?? null; const flagSubmenu: ContextMenuItem[] = [ { - label: "None", + label: t("app.none"), type: "checkbox", checked: currentFlagColor == null, click: () => @@ -67,7 +67,7 @@ export function buildTabContextMenu( ), }, ...FlagColors.map((fc) => ({ - label: fc.label, + label: t("app." + fc.label.toLowerCase()), type: "checkbox" as const, checked: currentFlagColor === fc.value, click: () => @@ -76,7 +76,7 @@ export function buildTabContextMenu( ), })), ]; - menu.push({ label: "Flag Tab", type: "submenu", submenu: flagSubmenu }, { type: "separator" }); + menu.push({ label: t("app.flagTab"), type: "submenu", submenu: flagSubmenu }, { type: "separator" }); const fullConfig = globalStore.get(env.atoms.fullConfigAtom); const backgrounds = fullConfig?.backgrounds ?? {}; const bgKeys = Object.keys(backgrounds).filter((k) => backgrounds[k] != null); @@ -89,7 +89,7 @@ export function buildTabContextMenu( const submenu: ContextMenuItem[] = []; const oref = makeORef("tab", id); submenu.push({ - label: "Default", + label: t("app.default"), click: () => fireAndForget(async () => { await env.rpc.SetMetaCommand(TabRpcClient, { @@ -115,9 +115,9 @@ export function buildTabContextMenu( }), }); } - menu.push({ label: "Backgrounds", type: "submenu", submenu }, { type: "separator" }); + menu.push({ label: t("app.backgrounds"), type: "submenu", submenu }, { type: "separator" }); } menu.push(...buildTabBarContextMenu(env), { type: "separator" }); - menu.push({ label: "Close Tab", click: () => onClose(null) }); + menu.push({ label: t("app.closeTab"), click: () => onClose(null) }); return menu; } diff --git a/package-lock.json b/package-lock.json index 1dabb7bdc5..e38377ee5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4828,6 +4828,28 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -12696,6 +12718,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -14374,6 +14405,19 @@ "node": ">=14.0.0" } }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", + "integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "electron-winstaller": "5.4.0" + } + }, "node_modules/electron-builder/node_modules/builder-util-runtime": { "version": "9.5.1", "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", @@ -14531,6 +14575,66 @@ } } }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron-winstaller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-winstaller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/electron/node_modules/@types/node": { "version": "24.10.13", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", @@ -22056,6 +22160,20 @@ "node": ">= 18" } }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -25132,6 +25250,36 @@ "postcss": "^8.4.31" } }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -27553,6 +27701,58 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/roarr": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", @@ -27752,7 +27952,6 @@ "version": "1.91.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.91.0.tgz", "integrity": "sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==", - "dev": true, "license": "MIT", "dependencies": { "chokidar": "^4.0.0", @@ -29249,6 +29448,21 @@ "node": ">=18" } }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/temp-file": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", @@ -32621,6 +32835,16 @@ "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==", "license": "MIT" }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", From f79aa6278bfb6e30d9868a5aa58605edf83df26c Mon Sep 17 00:00:00 2001 From: jiaminghua Date: Sat, 4 Apr 2026 23:42:25 +0800 Subject: [PATCH 3/3] fix: resolve remaining CodeRabbit review issues - Translate 'deprecated' label in config sidebar - Translate 'Save (shortcut)' tooltip with i18n interpolation - Fix modal footer default label logic (null check instead of ||) - Fix connectionsEditItem value to empty string for consistency - Add app.deprecated and app.saveWithShortcut translation keys --- frontend/app/i18n/locales/en.json | 6 ++++-- frontend/app/i18n/locales/zh-CN.json | 6 ++++-- frontend/app/modals/conntypeahead.tsx | 2 +- frontend/app/modals/modal.tsx | 8 ++++---- frontend/app/view/waveconfig/waveconfig.tsx | 4 ++-- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/frontend/app/i18n/locales/en.json b/frontend/app/i18n/locales/en.json index defd0d04de..2256f1b7a8 100644 --- a/frontend/app/i18n/locales/en.json +++ b/frontend/app/i18n/locales/en.json @@ -69,5 +69,7 @@ "app.save": "Save", "app.unsavedChanges": "Unsaved changes", "app.loading": "Loading...", - "app.configError": "Config Error" -} + "app.configError": "Config Error", + "app.deprecated": "Deprecated", + "app.saveWithShortcut": "Save ({{shortcut}})" +} \ No newline at end of file diff --git a/frontend/app/i18n/locales/zh-CN.json b/frontend/app/i18n/locales/zh-CN.json index 9273003927..87ad67edd0 100644 --- a/frontend/app/i18n/locales/zh-CN.json +++ b/frontend/app/i18n/locales/zh-CN.json @@ -69,5 +69,7 @@ "app.save": "保存", "app.unsavedChanges": "未保存的更改", "app.loading": "加载中...", - "app.configError": "配置错误" -} + "app.configError": "配置错误", + "app.deprecated": "已弃用", + "app.saveWithShortcut": "保存 ({{shortcut}})" +} \ No newline at end of file diff --git a/frontend/app/modals/conntypeahead.tsx b/frontend/app/modals/conntypeahead.tsx index 645da694f1..2df3831e0d 100644 --- a/frontend/app/modals/conntypeahead.tsx +++ b/frontend/app/modals/conntypeahead.tsx @@ -244,7 +244,7 @@ function getConnectionsEditItem( status: "disconnected", icon: "gear", iconColor: "var(--grey-text-color)", - value: t("app.editConnections"), + value: "", label: t("app.editConnections"), onSelect: () => { util.fireAndForget(async () => { diff --git a/frontend/app/modals/modal.tsx b/frontend/app/modals/modal.tsx index e403f39553..2f1f666b57 100644 --- a/frontend/app/modals/modal.tsx +++ b/frontend/app/modals/modal.tsx @@ -93,8 +93,8 @@ interface ModalFooterProps { const ModalFooter = ({ onCancel, onOk, - cancelLabel = "Cancel", - okLabel = "Ok", + cancelLabel, + okLabel, okDisabled, cancelDisabled, }: ModalFooterProps) => { @@ -103,12 +103,12 @@ const ModalFooter = ({
{onCancel && ( )} {onOk && ( )}
diff --git a/frontend/app/view/waveconfig/waveconfig.tsx b/frontend/app/view/waveconfig/waveconfig.tsx index a29f83720b..2d3345db7f 100644 --- a/frontend/app/view/waveconfig/waveconfig.tsx +++ b/frontend/app/view/waveconfig/waveconfig.tsx @@ -85,7 +85,7 @@ const ConfigSidebar = memo(({ model }: ConfigSidebarProps) => { : "text-muted-foreground/70 bg-secondary/30" }`} > - deprecated + {t("app.deprecated")} {configErrorFiles.has(file.path) && ( @@ -164,7 +164,7 @@ const WaveConfigView = memo(({ blockId, model }: ViewComponentProps window.removeEventListener("keydown", handleKeyDown); }, [hasChanges, isSaving, model]); - const saveTooltip = `Save (${model.saveShortcut})`; + const saveTooltip = t("app.saveWithShortcut", { shortcut: model.saveShortcut }); return (