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..f42739314f --- /dev/null +++ b/frontend/app/i18n/index.ts @@ -0,0 +1,45 @@ +import i18n from "i18next"; +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: 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; changeLanguage: typeof i18n.changeLanguage }; + } +} +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 new file mode 100644 index 0000000000..2256f1b7a8 --- /dev/null +++ b/frontend/app/i18n/locales/en.json @@ -0,0 +1,75 @@ +{ + "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", + "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 new file mode 100644 index 0000000000..87ad67edd0 --- /dev/null +++ b/frontend/app/i18n/locales/zh-CN.json @@ -0,0 +1,75 @@ +{ + "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": "配置错误", + "app.deprecated": "已弃用", + "app.saveWithShortcut": "保存 ({{shortcut}})" +} \ No newline at end of file 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..2df3831e0d 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: "", + 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..2f1f666b57 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"; @@ -92,21 +93,22 @@ interface ModalFooterProps { const ModalFooter = ({ onCancel, onOk, - cancelLabel = "Cancel", - okLabel = "Ok", + cancelLabel, + okLabel, 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..d393fc98d8 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( @@ -44,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" } @@ -55,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: () => @@ -64,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: () => @@ -73,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); @@ -86,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, { @@ -112,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/frontend/app/view/waveconfig/waveconfig.tsx b/frontend/app/view/waveconfig/waveconfig.tsx index ca06295bfc..2d3345db7f 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..e38377ee5a 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" @@ -4848,18 +4850,6 @@ "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 +5566,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", @@ -17679,6 +17207,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 +17483,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", @@ -26282,12 +25850,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", @@ -28512,13 +28100,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 +28455,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 +29331,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", @@ -32343,6 +31829,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", @@ -33341,9 +32836,9 @@ "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==", + "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": { 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",