Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions frontend/app/aipanel/aipanel-contextmenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
e.preventDefault();
e.stopPropagation();
Expand All @@ -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();
},
Expand Down Expand Up @@ -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,
Expand All @@ -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();
},
Expand Down
12 changes: 7 additions & 5 deletions frontend/app/aipanel/aipanelheader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,8 +34,8 @@ export const AIPanelHeader = memo(() => {
<div className="flex items-center flex-shrink-0 whitespace-nowrap">
{!inBuilder && (
<div className="flex items-center text-sm whitespace-nowrap">
<span className="text-gray-300 @xs:hidden mr-1 text-[12px]">Context</span>
<span className="text-gray-300 hidden @xs:inline mr-2 text-[12px]">Widget Context</span>
<span className="text-gray-300 @xs:hidden mr-1 text-[12px]">{t("app.context")}</span>
<span className="text-gray-300 hidden @xs:inline mr-2 text-[12px]">{t("app.widgetContext")}</span>
<button
onClick={() => {
model.setWidgetAccess(!widgetAccess);
Expand All @@ -44,7 +46,7 @@ export const AIPanelHeader = memo(() => {
className={`relative inline-flex h-6 w-14 items-center rounded-full transition-colors cursor-pointer ${
widgetAccess ? "bg-accent-600" : "bg-zinc-600"
}`}
title={`Widget Access ${widgetAccess ? "ON" : "OFF"}`}
title={t("app.widgetAccess", { state: widgetAccess ? t("app.on") : t("app.off") })}
>
<span
className={`absolute inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
Expand All @@ -56,7 +58,7 @@ export const AIPanelHeader = memo(() => {
widgetAccess ? "ml-2.5 mr-6 text-left" : "ml-6 mr-1 text-right"
}`}
>
{widgetAccess ? "ON" : "OFF"}
{widgetAccess ? t("app.on") : t("app.off")}
</span>
</button>
</div>
Expand All @@ -65,7 +67,7 @@ export const AIPanelHeader = memo(() => {
<button
onClick={handleKebabClick}
className="text-gray-400 hover:text-white cursor-pointer transition-colors p-1 rounded flex-shrink-0 ml-2 focus:outline-none"
title="More options"
title={t("app.moreOptions")}
>
<i className="fa fa-ellipsis-vertical"></i>
</button>
Expand Down
2 changes: 2 additions & 0 deletions frontend/app/app.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

import "./i18n/index";

import {
clearBadgesForBlockOnFocus,
clearBadgesForTabOnFocus,
Expand Down
22 changes: 13 additions & 9 deletions frontend/app/block/blockframe-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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);
},
Expand All @@ -61,7 +63,7 @@ function handleHeaderContextMenu(
menu.push(
{ type: "separator" },
{
label: "Close Block",
label: t("app.closeBlock"),
click: () => uxCloseBlock(blockId),
}
);
Expand All @@ -76,6 +78,7 @@ type HeaderTextElemsProps = {
};

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

const HeaderEndIcons = React.memo(({ viewModel, nodeModel, blockId }: HeaderEndIconsProps) => {
const { t } = useTranslation();
const blockEnv = useWaveEnv<BlockEnv>();
const endIconButtons = util.useAtomValueSafe(viewModel?.endIconButtons);
const magnified = jotai.useAtomValue(nodeModel.isMagnified);
Expand All @@ -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<Block>(WOS.makeORef("block", blockId));
Expand All @@ -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<Block>(WOS.makeORef("block", blockId));
Expand All @@ -167,15 +171,15 @@ 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(<IconButton key="settings" decl={settingsDecl} className="block-frame-settings" />);
if (ephemeral) {
const addToLayoutDecl: IconButtonDecl = {
elemtype: "iconbutton",
icon: "circle-plus",
title: "Add to Layout",
title: t("app.addToLayout"),
click: () => {
nodeModel.addEphemeralNodeToLayout();
},
Expand All @@ -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(<IconButton key="close" decl={closeDecl} className="block-frame-default-close" />);
Expand Down
45 changes: 45 additions & 0 deletions frontend/app/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -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) };
75 changes: 75 additions & 0 deletions frontend/app/i18n/locales/en.json
Original file line number Diff line number Diff line change
@@ -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}})"
}
Loading