From 072ac3b05a2dc7c05ac47a1d9096e6ecebc0e944 Mon Sep 17 00:00:00 2001 From: Chase Liang Date: Mon, 18 May 2026 11:25:57 +0800 Subject: [PATCH 1/7] Add Chinese support --- core/config/sharedConfig.ts | 1 + gui/package.json | 2 + .../AssistantOption.tsx | 6 +- .../AssistantOptions.tsx | 4 +- .../SelectedAssistantButton.tsx | 10 +- .../AssistantAndOrgListbox/index.tsx | 25 +- gui/src/components/FeedbackButtons.tsx | 6 +- gui/src/components/ModeSelect/ModeSelect.tsx | 28 +- .../StepContainer/ConversationSummary.tsx | 10 +- .../StepContainer/ResponseActions.tsx | 14 +- .../StepContainer/StepContainer.tsx | 4 +- .../StepContainerPreToolbar/ApplyActions.tsx | 23 +- .../CollapsibleContainer.tsx | 14 +- .../StepContainerPreToolbar/CopyButton.tsx | 7 +- .../CreateFileButton.tsx | 10 +- .../StepContainerPreToolbar/InsertButton.tsx | 8 +- gui/src/components/gui/CopyIconButton.tsx | 4 +- gui/src/components/gui/Shortcut.tsx | 41 +- .../components/mainInput/ContinueInputBox.tsx | 6 +- gui/src/components/mainInput/InputToolbar.tsx | 28 +- .../LumpToolbar/BlockSettingsTopToolbar.tsx | 26 +- .../Lump/LumpToolbar/EditToolbar.tsx | 7 +- .../Lump/LumpToolbar/GeneratingIndicator.tsx | 3 +- .../Lump/LumpToolbar/IsApplyingToolbar.tsx | 10 +- .../Lump/LumpToolbar/LumpToolbar.tsx | 5 +- .../LumpToolbar/PendingToolCallToolbar.tsx | 6 +- .../Lump/LumpToolbar/StreamingToolbar.tsx | 3 +- .../Lump/LumpToolbar/TtsActiveToolbar.tsx | 4 +- .../TipTapEditor/utils/editorConfig.ts | 5 +- .../belowMainInput/ThinkingBlockPeek.tsx | 19 +- .../components/modelSelection/ModelSelect.tsx | 17 +- gui/src/context/LocalStorage.tsx | 9 +- gui/src/locales/en.json | 396 ++++++++++++++++++ gui/src/locales/i18n.ts | 27 ++ gui/src/locales/zh.json | 396 ++++++++++++++++++ gui/src/main.tsx | 1 + .../pages/config/components/ModelRoleRow.tsx | 4 +- .../config/components/ToolPoliciesGroup.tsx | 10 +- .../config/components/ToolPolicyItem.tsx | 39 +- gui/src/pages/config/configTabs.tsx | 25 +- .../features/account/AccountDropdown.tsx | 8 +- .../features/keyboard/KeyboardShortcuts.tsx | 8 +- gui/src/pages/config/index.tsx | 4 +- .../pages/config/sections/ConfigsSection.tsx | 9 +- gui/src/pages/config/sections/DocsSection.tsx | 6 +- gui/src/pages/config/sections/HelpSection.tsx | 48 ++- .../sections/IndexingSettingsSection.tsx | 25 +- .../pages/config/sections/ModelsSection.tsx | 40 +- .../config/sections/OrganizationsSection.tsx | 13 +- .../pages/config/sections/RulesSection.tsx | 49 ++- .../pages/config/sections/ToolsSection.tsx | 63 +-- .../config/sections/UserSettingsSection.tsx | 155 +++++-- gui/src/pages/error.tsx | 16 +- gui/src/pages/gui/Chat.tsx | 4 +- gui/src/pages/gui/StreamError.tsx | 47 ++- .../gui/ToolCallDiv/GroupedToolCallHeader.tsx | 6 +- .../gui/ToolCallDiv/ToolCallStatusMessage.tsx | 32 +- .../ToolCallDiv/ToolTruncateHistoryIcon.tsx | 5 +- gui/src/pages/gui/ToolCallDiv/utils.tsx | 24 +- gui/src/pages/gui/index.tsx | 4 + gui/src/pages/stats.tsx | 44 +- gui/src/util/localStorage.ts | 1 + 62 files changed, 1509 insertions(+), 365 deletions(-) create mode 100644 gui/src/locales/en.json create mode 100644 gui/src/locales/i18n.ts create mode 100644 gui/src/locales/zh.json diff --git a/core/config/sharedConfig.ts b/core/config/sharedConfig.ts index 87306a2aed8..24e643864b6 100644 --- a/core/config/sharedConfig.ts +++ b/core/config/sharedConfig.ts @@ -28,6 +28,7 @@ export const sharedConfigSchema = z showSessionTabs: z.boolean(), codeBlockToolbarPosition: z.enum(["top", "bottom"]), fontSize: z.number(), + language: z.string(), codeWrap: z.boolean(), displayRawMarkdown: z.boolean(), showChatScrollbar: z.boolean(), diff --git a/gui/package.json b/gui/package.json index b5f58f91d39..dd21bd91582 100644 --- a/gui/package.json +++ b/gui/package.json @@ -48,6 +48,7 @@ "downshift": "^7.6.0", "escape-carriage": "^1.3.1", "handlebars": "^4.7.8", + "i18next": "^26.0.8", "lodash": "^4.17.23", "lowlight": "^3.3.0", "mermaid": "^11.10.0", @@ -58,6 +59,7 @@ "react-dom": "^18.2.0", "react-error-boundary": "^4.0.11", "react-hook-form": "^7.69.0", + "react-i18next": "^17.0.6", "react-intersection-observer": "^9.13.1", "react-markdown": "^9.0.1", "react-redux": "^8.0.5", diff --git a/gui/src/components/AssistantAndOrgListbox/AssistantOption.tsx b/gui/src/components/AssistantAndOrgListbox/AssistantOption.tsx index ec48c847417..7d920c05358 100644 --- a/gui/src/components/AssistantAndOrgListbox/AssistantOption.tsx +++ b/gui/src/components/AssistantAndOrgListbox/AssistantOption.tsx @@ -12,6 +12,7 @@ import { CONFIG_ROUTES } from "../../util/navigation"; import { ToolTip } from "../gui/Tooltip"; import { Button, ListboxOption, useFontSize } from "../ui"; import { AssistantIcon } from "./AssistantIcon"; +import { useTranslation } from "react-i18next"; interface AssistantOptionProps { profile: ProfileDescription; @@ -24,6 +25,7 @@ export function AssistantOption({ selected, onClick, }: AssistantOptionProps) { + const { t } = useTranslation(); const tinyFont = useFontSize(-4); const navigate = useNavigate(); const dispatch = useAppDispatch(); @@ -61,7 +63,9 @@ export function AssistantOption({ 0 ? "text-error" : ""}`} > - {profile.title} + {profile.title == "Local Config" + ? t("AssistantAndOrgListbox.LocalConfig") + : profile.title}
diff --git a/gui/src/components/AssistantAndOrgListbox/AssistantOptions.tsx b/gui/src/components/AssistantAndOrgListbox/AssistantOptions.tsx index 63fb604e240..7aadff3f8ef 100644 --- a/gui/src/components/AssistantAndOrgListbox/AssistantOptions.tsx +++ b/gui/src/components/AssistantAndOrgListbox/AssistantOptions.tsx @@ -1,5 +1,6 @@ import { useAuth } from "../../context/Auth"; import { AssistantOption } from "./AssistantOption"; +import { useTranslation } from "react-i18next"; interface AssistantOptionsProps { selectedProfileId: string | undefined; @@ -10,13 +11,14 @@ export function AssistantOptions({ selectedProfileId, onClose, }: AssistantOptionsProps) { + const { t } = useTranslation(); const { profiles } = useAuth(); return (
{profiles?.length === 0 ? (
- No config found + {t("AssistantAndOrgListbox.NoConfigFound")}
) : ( profiles?.map((profile, idx) => ( diff --git a/gui/src/components/AssistantAndOrgListbox/SelectedAssistantButton.tsx b/gui/src/components/AssistantAndOrgListbox/SelectedAssistantButton.tsx index 0344abc713a..061597d6b3b 100644 --- a/gui/src/components/AssistantAndOrgListbox/SelectedAssistantButton.tsx +++ b/gui/src/components/AssistantAndOrgListbox/SelectedAssistantButton.tsx @@ -6,6 +6,7 @@ import { fontSize } from "../../util"; import { cn } from "../../util/cn"; import { ListboxButton } from "../ui"; import { AssistantIcon } from "./AssistantIcon"; +import { useTranslation } from "react-i18next"; interface SelectedAssistantButtonProps { selectedProfile: ProfileDescription | null; @@ -16,6 +17,7 @@ export function SelectedAssistantButton({ selectedProfile, variant, }: SelectedAssistantButtonProps) { + const { t } = useTranslation(); const configLoading = useAppSelector((store) => store.config.loading); const isSidebar = variant === "sidebar"; @@ -34,7 +36,7 @@ export function SelectedAssistantButton({ >
{selectedProfile === null ? ( - "Set up config file" + t("AssistantAndOrgListbox.SetupConfigfile") ) : configLoading ? ( - Loading + {t("AssistantAndOrgListbox.Loading")} ) : ( <> @@ -51,7 +53,9 @@ export function SelectedAssistantButton({ )} )} diff --git a/gui/src/components/AssistantAndOrgListbox/index.tsx b/gui/src/components/AssistantAndOrgListbox/index.tsx index cf767c6c3fa..8c79249f45e 100644 --- a/gui/src/components/AssistantAndOrgListbox/index.tsx +++ b/gui/src/components/AssistantAndOrgListbox/index.tsx @@ -28,6 +28,7 @@ import { Divider } from "../ui/Divider"; import { AssistantOptions } from "./AssistantOptions"; import { OrganizationOptions } from "./OrganizationOptions"; import { SelectedAssistantButton } from "./SelectedAssistantButton"; +import { useTranslation } from "react-i18next"; export interface AssistantAndOrgListboxProps { variant: "lump" | "sidebar"; @@ -36,6 +37,7 @@ export interface AssistantAndOrgListboxProps { export function AssistantAndOrgListbox({ variant = "sidebar", }: AssistantAndOrgListboxProps) { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const navigate = useNavigate(); const listboxRef = useRef(null); @@ -155,7 +157,7 @@ export function AssistantAndOrgListbox({ >
- Configs + {t("AssistantAndOrgListbox.Configs")}
{session ? ( @@ -262,7 +268,9 @@ export function AssistantAndOrgListbox({ >
- Log out + + {t("AssistantAndOrgListbox.Logout")} +
) : ( @@ -278,7 +286,9 @@ export function AssistantAndOrgListbox({ >
- Log in + + {t("AssistantAndOrgListbox.Login")} +
)} @@ -291,7 +301,8 @@ export function AssistantAndOrgListbox({
- {getMetaKeyLabel()} ⇧ ' to toggle config + {getMetaKeyLabel()} ⇧ '{" "} + {t("AssistantAndOrgListbox.toggleconfig")}
diff --git a/gui/src/components/FeedbackButtons.tsx b/gui/src/components/FeedbackButtons.tsx index a942531c4bb..3d675a8ddd3 100644 --- a/gui/src/components/FeedbackButtons.tsx +++ b/gui/src/components/FeedbackButtons.tsx @@ -7,12 +7,14 @@ import { useContext, useState } from "react"; import { IdeMessengerContext } from "../context/IdeMessenger"; import { useAppSelector } from "../redux/hooks"; import HeaderButtonWithToolTip from "./gui/HeaderButtonWithToolTip"; +import { useTranslation } from "react-i18next"; export interface FeedbackButtonsProps { item: ChatHistoryItem; } export function FeedbackButtons({ item }: FeedbackButtonsProps) { + const { t } = useTranslation(); const [feedback, setFeedback] = useState(undefined); const ideMessenger = useContext(IdeMessengerContext); const sessionId = useAppSelector((store) => store.session.id); @@ -41,7 +43,7 @@ export function FeedbackButtons({ item }: FeedbackButtonsProps) { return ( <> sendFeedback(true)} > @@ -50,7 +52,7 @@ export function FeedbackButtons({ item }: FeedbackButtonsProps) { /> sendFeedback(false)} > diff --git a/gui/src/components/ModeSelect/ModeSelect.tsx b/gui/src/components/ModeSelect/ModeSelect.tsx index 8b69bca2065..1695df990b1 100644 --- a/gui/src/components/ModeSelect/ModeSelect.tsx +++ b/gui/src/components/ModeSelect/ModeSelect.tsx @@ -16,8 +16,10 @@ import { ToolTip } from "../gui/Tooltip"; import { useMainEditor } from "../mainInput/TipTapEditor"; import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from "../ui"; import { ModeIcon } from "./ModeIcon"; +import { useTranslation } from "react-i18next"; export function ModeSelect() { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const mode = useAppSelector((store) => store.session.mode); const selectedModel = useAppSelector(selectSelectedChatModel); @@ -112,12 +114,12 @@ export function ModeSelect() { {mode === "chat" - ? "Chat" + ? t("ModeSelect.Chat") : mode === "agent" - ? "Agent" + ? t("ModeSelect.Agent") : mode === "background" - ? "Background" - : "Plan"} + ? t("ModeSelect.Background") + : t("ModeSelect.Plan")}
- Chat + {t("ModeSelect.Chat")}
- Plan + {t("ModeSelect.Plan")} @@ -170,12 +172,12 @@ export function ModeSelect() {
- Agent + {t("ModeSelect.Agent")} @@ -193,12 +195,12 @@ export function ModeSelect() { >
- Background + {t("ModeSelect.Background")} @@ -212,7 +214,7 @@ export function ModeSelect() {
- {`${metaKeyLabel} . for next mode`} + {`${metaKeyLabel} . ` + t("ModeSelect.forNextMode")}
diff --git a/gui/src/components/StepContainer/ConversationSummary.tsx b/gui/src/components/StepContainer/ConversationSummary.tsx index 392b757da2c..091ed7c14cc 100644 --- a/gui/src/components/StepContainer/ConversationSummary.tsx +++ b/gui/src/components/StepContainer/ConversationSummary.tsx @@ -7,6 +7,7 @@ import { useDeleteCompaction } from "../../util/compactConversation"; import { AnimatedEllipsis } from "../AnimatedEllipsis"; import HeaderButtonWithToolTip from "../gui/HeaderButtonWithToolTip"; import StyledMarkdownPreview from "../StyledMarkdownPreview"; +import { useTranslation } from "react-i18next"; interface ConversationSummaryProps { item: ChatHistoryItem; @@ -14,6 +15,7 @@ interface ConversationSummaryProps { } export default function ConversationSummary(props: ConversationSummaryProps) { + const { t } = useTranslation(); const [open, setOpen] = useState(true); const isLoading = useAppSelector( (state) => state.session.compactionLoading[props.index] || false, @@ -30,7 +32,7 @@ export default function ConversationSummary(props: ConversationSummaryProps) {
- Generating conversation summary + {t("StepContainer.GeneratingSummary")}
@@ -51,9 +53,11 @@ export default function ConversationSummary(props: ConversationSummaryProps) { ) : ( )} - Conversation Summary + + {t("StepContainer.ConversationSummary")} + { e.stopPropagation(); deleteCompaction(props.index); diff --git a/gui/src/components/StepContainer/ResponseActions.tsx b/gui/src/components/StepContainer/ResponseActions.tsx index 0dc09a438be..80db115fd91 100644 --- a/gui/src/components/StepContainer/ResponseActions.tsx +++ b/gui/src/components/StepContainer/ResponseActions.tsx @@ -16,6 +16,7 @@ import { FeedbackButtons } from "../FeedbackButtons"; import { GenerateRuleDialog } from "../GenerateRuleDialog"; import { CopyIconButton } from "../gui/CopyIconButton"; import HeaderButtonWithToolTip from "../gui/HeaderButtonWithToolTip"; +import { useTranslation } from "react-i18next"; export interface ResponseActionsProps { isTruncated: boolean; @@ -34,6 +35,7 @@ export default function ResponseActions({ onDelete, isLast, }: ResponseActionsProps) { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const selectedModel = useAppSelector(selectSelectedChatModel); const contextPercentage = useAppSelector( @@ -65,8 +67,8 @@ export default function ResponseActions({ testId={`compact-button-${index}`} text={ showLabel - ? "Summarize conversation to reduce context length" - : "Compact conversation" + ? t("StepContainer.ResponseActions.SummarizeConversation") + : t("StepContainer.ResponseActions.CompactConversation") } tabIndex={-1} onClick={() => compactConversation(index)} @@ -79,7 +81,7 @@ export default function ResponseActions({ - Compact conversation + {t("StepContainer.ResponseActions.CompactConversation")} )}
@@ -88,7 +90,7 @@ export default function ResponseActions({ {isLast && ruleGenerationSupported && ( @@ -98,7 +100,7 @@ export default function ResponseActions({ {isTruncated && ( @@ -107,7 +109,7 @@ export default function ResponseActions({ diff --git a/gui/src/components/StepContainer/StepContainer.tsx b/gui/src/components/StepContainer/StepContainer.tsx index fc6981d0558..11c122068a6 100644 --- a/gui/src/components/StepContainer/StepContainer.tsx +++ b/gui/src/components/StepContainer/StepContainer.tsx @@ -10,6 +10,7 @@ import StyledMarkdownPreview from "../StyledMarkdownPreview"; import ConversationSummary from "./ConversationSummary"; import ResponseActions from "./ResponseActions"; import ThinkingIndicator from "./ThinkingIndicator"; +import { useTranslation } from "react-i18next"; interface StepContainerProps { item: ChatHistoryItem; @@ -19,6 +20,7 @@ interface StepContainerProps { } export default function StepContainer(props: StepContainerProps) { + const { t } = useTranslation(); const dispatch = useDispatch(); const [isTruncated, setIsTruncated] = useState(false); const isStreaming = useAppSelector((state) => state.session.isStreaming); @@ -125,7 +127,7 @@ export default function StepContainer(props: StepContainerProps) {
- Previous Conversation Compacted + {t("StepContainer.PreviousCompacted")}
diff --git a/gui/src/components/StyledMarkdownPreview/StepContainerPreToolbar/ApplyActions.tsx b/gui/src/components/StyledMarkdownPreview/StepContainerPreToolbar/ApplyActions.tsx index 66e5f97e1e5..655e45f42ee 100644 --- a/gui/src/components/StyledMarkdownPreview/StepContainerPreToolbar/ApplyActions.tsx +++ b/gui/src/components/StyledMarkdownPreview/StepContainerPreToolbar/ApplyActions.tsx @@ -5,6 +5,7 @@ import Spinner from "../../gui/Spinner"; import { ToolTip } from "../../gui/Tooltip"; import HoverItem from "../../mainInput/InputToolbar/HoverItem"; import { ToolbarButtonWithTooltip } from "./ToolbarButtonWithTooltip"; +import { useTranslation } from "react-i18next"; interface ApplyActionsProps { disableManualApply?: boolean; @@ -15,6 +16,8 @@ interface ApplyActionsProps { } export function ApplyActions(props: ApplyActionsProps) { + const { t } = useTranslation(); + function onClickReject() { props.onClickReject(); } @@ -24,7 +27,7 @@ export function ApplyActions(props: ApplyActionsProps) { return (
- Applying + {t("StepContainerPreToolbar.ApplyActions.applying")}
@@ -33,14 +36,16 @@ export function ApplyActions(props: ApplyActionsProps) { return (
- {`${props.applyState?.numDiffs === 1 ? "1 diff" : `${props.applyState?.numDiffs} diffs`}`} + {props.applyState?.numDiffs === 1 + ? t("StepContainerPreToolbar.ApplyActions.1Diff") + : `${props.applyState?.numDiffs} ${t("StepContainerPreToolbar.ApplyActions.diffs")}`}
@@ -48,7 +53,7 @@ export function ApplyActions(props: ApplyActionsProps) { @@ -62,7 +67,10 @@ export function ApplyActions(props: ApplyActionsProps) { } return ( - +
- Apply + + + {t("StepContainerPreToolbar.ApplyActions.apply")} +
diff --git a/gui/src/components/StyledMarkdownPreview/StepContainerPreToolbar/CollapsibleContainer.tsx b/gui/src/components/StyledMarkdownPreview/StepContainerPreToolbar/CollapsibleContainer.tsx index 464c4b4cf04..80b842c2cab 100644 --- a/gui/src/components/StyledMarkdownPreview/StepContainerPreToolbar/CollapsibleContainer.tsx +++ b/gui/src/components/StyledMarkdownPreview/StepContainerPreToolbar/CollapsibleContainer.tsx @@ -1,5 +1,6 @@ import { ChevronDownIcon } from "@heroicons/react/24/outline"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; interface CollapsibleContainerProps { children: React.ReactNode; @@ -14,6 +15,7 @@ export function CollapsibleContainer({ className = "", collapsible = false, }: CollapsibleContainerProps) { + const { t } = useTranslation(); const [isExpanded, setIsExpanded] = useState(false); // If not collapsible, just render children without any collapsible behavior @@ -33,7 +35,11 @@ export function CollapsibleContainer({ onClick={() => setIsExpanded(true)} className="group flex h-full cursor-pointer items-end justify-center pb-2" > - +
@@ -45,7 +51,11 @@ export function CollapsibleContainer({ onClick={() => setIsExpanded(false)} className="group mt-2 flex cursor-pointer justify-center" > - +
diff --git a/gui/src/components/StyledMarkdownPreview/StepContainerPreToolbar/CopyButton.tsx b/gui/src/components/StyledMarkdownPreview/StepContainerPreToolbar/CopyButton.tsx index 19ae2aa81e7..8984afcd51f 100644 --- a/gui/src/components/StyledMarkdownPreview/StepContainerPreToolbar/CopyButton.tsx +++ b/gui/src/components/StyledMarkdownPreview/StepContainerPreToolbar/CopyButton.tsx @@ -2,16 +2,21 @@ import { CheckIcon, ClipboardIcon } from "@heroicons/react/24/outline"; import useCopy from "../../../hooks/useCopy"; import { ToolTip } from "../../gui/Tooltip"; import HoverItem from "../../mainInput/InputToolbar/HoverItem"; +import { useTranslation } from "react-i18next"; interface CopyButtonProps { text: string; } export function CopyButton({ text }: CopyButtonProps) { + const { t } = useTranslation(); const { copyText, copied } = useCopy(text); return ( - +
void; } export function CreateFileButton({ onClick }: CreateFileButtonProps) { + const { t } = useTranslation(); + return ( - +
diff --git a/gui/src/components/StyledMarkdownPreview/StepContainerPreToolbar/InsertButton.tsx b/gui/src/components/StyledMarkdownPreview/StepContainerPreToolbar/InsertButton.tsx index 9fecb2fa34a..f7ab6284dec 100644 --- a/gui/src/components/StyledMarkdownPreview/StepContainerPreToolbar/InsertButton.tsx +++ b/gui/src/components/StyledMarkdownPreview/StepContainerPreToolbar/InsertButton.tsx @@ -1,6 +1,7 @@ import { ArrowLeftEndOnRectangleIcon } from "@heroicons/react/24/outline"; import { ToolTip } from "../../gui/Tooltip"; import HoverItem from "../../mainInput/InputToolbar/HoverItem"; +import { useTranslation } from "react-i18next"; /** * Button that inserts code at the current cursor position @@ -10,12 +11,17 @@ interface InsertButtonProps { } export function InsertButton({ onInsert }: InsertButtonProps) { + const { t } = useTranslation(); + return ( - +
string); @@ -17,6 +18,7 @@ export function CopyIconButton({ clipboardIconClassName = "h-4 w-4 text-gray-400", tooltipPlacement = "bottom", }: CopyIconButtonProps) { + const { t } = useTranslation(); const { copyText, copied } = useCopy(text); return ( @@ -24,7 +26,7 @@ export function CopyIconButton({ {copied ? ( diff --git a/gui/src/components/gui/Shortcut.tsx b/gui/src/components/gui/Shortcut.tsx index 33c7cd91dfb..ad5c6911379 100644 --- a/gui/src/components/gui/Shortcut.tsx +++ b/gui/src/components/gui/Shortcut.tsx @@ -7,6 +7,7 @@ import { getPlatform, } from "../../util"; import "./Shortcut.css"; +import { useTranslation } from "react-i18next"; interface ShortcutProps { children: string; @@ -18,25 +19,34 @@ const metaKeys = ["meta", "⌘", "ctrl", "cmd", "^"]; const altKeys = ["alt", "option", "opt", "⌥"]; const modifierKeys = [...metaKeys, ...altKeys]; -const getSpecialKeyMap = (platform: string): Record => ({ - uparrow: "UpArrow ↑", - downarrow: "DownArrow ↓", - leftarrow: "LeftArrow ←", - rightarrow: "RightArrow →", - enter: "Enter ⏎", - esc: "Esc", - backspace: platform === "mac" ? "Delete ⌫" : "Backspace ⌫", - delete: platform === "mac" ? "Delete ⌫" : "Backspace ⌫", - "⌫": platform === "mac" ? "Delete ⌫" : "Backspace ⌫", +const getSpecialKeyMap = ( + platform: string, + t: (key: string) => string, +): Record => ({ + uparrow: t("Shortcut.UpArrow ↑"), + downarrow: t("Shortcut.DownArrow ↓"), + leftarrow: t("Shortcut.LeftArrow ←"), + rightarrow: t("Shortcut.RightArrow →"), + enter: t("Shortcut.Enter ⏎"), + esc: t("Shortcut.Esc"), + backspace: + platform === "mac" ? t("Shortcut.Delete ⌫") : t("Shortcut.Backspace ⌫"), + delete: + platform === "mac" ? t("Shortcut.Delete ⌫") : t("Shortcut.Backspace ⌫"), + "⌫": platform === "mac" ? t("Shortcut.Delete ⌫") : t("Shortcut.Backspace ⌫"), }); -const parseShortcut = (shortcut: string, platform: string) => { +const parseShortcut = ( + shortcut: string, + platform: string, + t: (key: string) => string, +) => { if (!shortcut || typeof shortcut !== "string") { console.warn("Invalid shortcut provided:", shortcut); return []; } - const specialKeyMap = getSpecialKeyMap(platform); + const specialKeyMap = getSpecialKeyMap(platform, t); return shortcut .split(",") .map((combo) => @@ -61,12 +71,13 @@ const isSingleCharNonModifier = (key: string) => key && key.length === 1 && /^[a-zA-Z0-9]$/.test(key); const Shortcut: React.FC = ({ children }) => { + const { t } = useTranslation(); const platform = getPlatform(); if (!children || typeof children !== "string") { - return Error: Invalid shortcut key; + return {t("Shortcut.Error: Invalid shortcut key")}; } - const shortcuts = parseShortcut(children, platform); + const shortcuts = parseShortcut(children, platform, t); return ( <> @@ -89,7 +100,7 @@ const Shortcut: React.FC = ({ children }) => { : `${fontSize - 3}px`, }} > - {key || "?"} + {key || t("Shortcut.?")} {keyIndex < combo.length - 1 && ( + diff --git a/gui/src/components/mainInput/ContinueInputBox.tsx b/gui/src/components/mainInput/ContinueInputBox.tsx index c57dbd9d009..203d1f18bc4 100644 --- a/gui/src/components/mainInput/ContinueInputBox.tsx +++ b/gui/src/components/mainInput/ContinueInputBox.tsx @@ -15,6 +15,7 @@ import { GradientBorder } from "./GradientBorder"; import { ToolbarOptions } from "./InputToolbar"; import { Lump } from "./Lump"; import { TipTapEditor } from "./TipTapEditor"; +import { useTranslation } from "react-i18next"; interface ContinueInputBoxProps { isLastUserInput: boolean; @@ -53,6 +54,7 @@ const EDIT_ALLOWED_SLASH_COMMAND_SOURCES: SlashCommandSource[] = [ ]; function ContinueInputBox(props: ContinueInputBoxProps) { + const { t } = useTranslation(); const isStreaming = useAppSelector((state) => state.session.isStreaming); const availableSlashCommands = useAppSelector( selectSlashCommandComboBoxInputs, @@ -88,7 +90,7 @@ function ContinueInputBox(props: ContinueInputBoxProps) { }, [availableContextProviders, isInEdit]); const historyKey = isInEdit ? "edit" : "chat"; - const placeholder = isInEdit ? "Edit selected code" : undefined; + const placeholder = isInEdit ? t("ContinueInputBox.EditSelectedCode") : undefined; const toolbarOptions: ToolbarOptions = useMemo(() => { if (isInEdit) { @@ -98,7 +100,7 @@ function ContinueInputBox(props: ContinueInputBoxProps) { hideUseCodebase: true, hideSelectModel: false, enterText: - editModeState.applyState.status === "done" ? "Retry" : "Edit", + editModeState.applyState.status === "done" ? t("ContinueInputBox.Retry") : t("ContinueInputBox.Edit"), } as ToolbarOptions; } // Stable empty object to avoid re-renders from identity changes diff --git a/gui/src/components/mainInput/InputToolbar.tsx b/gui/src/components/mainInput/InputToolbar.tsx index 99a940b78b6..b9c8793a2e3 100644 --- a/gui/src/components/mainInput/InputToolbar.tsx +++ b/gui/src/components/mainInput/InputToolbar.tsx @@ -25,6 +25,7 @@ import { Button } from "../ui"; import { useFontSize } from "../ui/font"; import ContextStatus from "./ContextStatus"; import HoverItem from "./InputToolbar/HoverItem"; +import { useTranslation } from "react-i18next"; export interface ToolbarOptions { hideUseCodebase?: boolean; @@ -47,6 +48,7 @@ interface InputToolbarProps { } function InputToolbar(props: InputToolbarProps) { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const ideMessenger = useContext(IdeMessengerContext); const fileInputRef = useRef(null); @@ -85,13 +87,13 @@ function InputToolbar(props: InputToolbarProps) { >
{!isInEdit && ( - + )} - + @@ -116,7 +118,7 @@ function InputToolbar(props: InputToolbarProps) { }} /> - + ))} {props.toolbarOptions?.hideAddContext || ( - + @@ -153,8 +155,8 @@ function InputToolbar(props: InputToolbarProps) { place="top" content={ hasReasoningEnabled - ? "Disable model reasoning" - : "Enable model reasoning" + ? t("InputToolbar.DisableReasoning") + : t("InputToolbar.EnableReasoning") } > {hasReasoningEnabled ? ( @@ -196,13 +198,15 @@ function InputToolbar(props: InputToolbarProps) { place="top-end" content={`${ useActiveFile - ? "Send Without Active File" - : "Send With Active File" + ? t("InputToolbar.SendWithoutActiveFile") + : t("InputToolbar.SendWithActiveFile") } (${getMetaKeyLabel()}⏎)`} > {getMetaKeyLabel()}⏎{" "} - {useActiveFile ? "No active file" : "Active file"} + {useActiveFile + ? t("InputToolbar.NoActiveFile") + : t("InputToolbar.ActiveFile")} @@ -217,11 +221,11 @@ function InputToolbar(props: InputToolbarProps) { }} > - Esc to exit Edit + Esc {t("InputToolbar.ActiveFile")} )} - + diff --git a/gui/src/components/mainInput/Lump/LumpToolbar/BlockSettingsTopToolbar.tsx b/gui/src/components/mainInput/Lump/LumpToolbar/BlockSettingsTopToolbar.tsx index 2f022ea5a52..dcba86f9d09 100644 --- a/gui/src/components/mainInput/Lump/LumpToolbar/BlockSettingsTopToolbar.tsx +++ b/gui/src/components/mainInput/Lump/LumpToolbar/BlockSettingsTopToolbar.tsx @@ -22,8 +22,10 @@ import { useAuth } from "../../../../context/Auth"; import { useCreditStatus } from "../../../../hooks/useCredits"; import { CONFIG_ROUTES } from "../../../../util/navigation"; import { AssistantAndOrgListbox } from "../../../AssistantAndOrgListbox"; +import { useTranslation } from "react-i18next"; export function BlockSettingsTopToolbar() { + const { t } = useTranslation(); const navigate = useNavigate(); const dispatch = useAppDispatch(); const { selectedProfile } = useAuth(); @@ -77,7 +79,10 @@ export function BlockSettingsTopToolbar() {
{shouldShowError && ( - +
{isUsingFreeTrial && ( - + )} - + - + - + @@ -135,7 +146,10 @@ export function BlockSettingsTopToolbar() { )}
- +
diff --git a/gui/src/components/mainInput/Lump/LumpToolbar/EditToolbar.tsx b/gui/src/components/mainInput/Lump/LumpToolbar/EditToolbar.tsx index 8e36e64cf0b..d59c590cf9f 100644 --- a/gui/src/components/mainInput/Lump/LumpToolbar/EditToolbar.tsx +++ b/gui/src/components/mainInput/Lump/LumpToolbar/EditToolbar.tsx @@ -4,8 +4,10 @@ import { IdeMessengerContext } from "../../../../context/IdeMessenger"; import { useAppDispatch, useAppSelector } from "../../../../redux/hooks"; import { exitEdit } from "../../../../redux/thunks/edit"; import { getEditFilenameAndRangeText } from "../../util"; +import { useTranslation } from "react-i18next"; export function EditToolbar() { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const ideMessenger = useContext(IdeMessengerContext); const mode = useAppSelector((state) => state.session.mode); @@ -25,10 +27,11 @@ export function EditToolbar() { onClick={handleBackClick} > - Back to {mode.charAt(0).toUpperCase() + mode.slice(1)} + {t("Lump.EditToolbar.BackTo")}{" "} + {mode.charAt(0).toUpperCase() + mode.slice(1)} - Editing:{" "} + {t("Lump.EditToolbar.Editing")}{" "} {getEditFilenameAndRangeText(codeToEdit)} diff --git a/gui/src/components/mainInput/Lump/LumpToolbar/GeneratingIndicator.tsx b/gui/src/components/mainInput/Lump/LumpToolbar/GeneratingIndicator.tsx index 620c2aacb12..f6dca0831a1 100644 --- a/gui/src/components/mainInput/Lump/LumpToolbar/GeneratingIndicator.tsx +++ b/gui/src/components/mainInput/Lump/LumpToolbar/GeneratingIndicator.tsx @@ -1,7 +1,8 @@ import { AnimatedEllipsis } from "../../../AnimatedEllipsis"; +import i18n from "i18next"; export function GeneratingIndicator({ - text = "Generating", + text = i18n.t("Lump.GeneratingIndicator.Generating"), testId, }: { text?: string; diff --git a/gui/src/components/mainInput/Lump/LumpToolbar/IsApplyingToolbar.tsx b/gui/src/components/mainInput/Lump/LumpToolbar/IsApplyingToolbar.tsx index 74850077f95..a453345927d 100644 --- a/gui/src/components/mainInput/Lump/LumpToolbar/IsApplyingToolbar.tsx +++ b/gui/src/components/mainInput/Lump/LumpToolbar/IsApplyingToolbar.tsx @@ -4,15 +4,20 @@ import { useAppDispatch } from "../../../../redux/hooks"; import { cancelStream } from "../../../../redux/thunks/cancelStream"; import { getAltKeyLabel, getMetaKeyLabel } from "../../../../util"; import { GeneratingIndicator } from "./GeneratingIndicator"; +import { useTranslation } from "react-i18next"; export const IsApplyingToolbar = () => { + const { t } = useTranslation(); const ideMessenger = useContext(IdeMessengerContext); const dispatch = useAppDispatch(); const jetbrains = window.location.protocol === "jb-api:"; return (
- +
{ }} > {/* JetBrains overrides cmd+backspace, so we have to use another shortcut */} - {jetbrains ? getAltKeyLabel() : getMetaKeyLabel()} ⌫ Cancel + {jetbrains ? getAltKeyLabel() : getMetaKeyLabel()} ⌫{" "} + {t("Lump.IsApplyingToolbar.Cancel")}
); diff --git a/gui/src/components/mainInput/Lump/LumpToolbar/LumpToolbar.tsx b/gui/src/components/mainInput/Lump/LumpToolbar/LumpToolbar.tsx index 8d10cb8458e..ab20d06b7a3 100644 --- a/gui/src/components/mainInput/Lump/LumpToolbar/LumpToolbar.tsx +++ b/gui/src/components/mainInput/Lump/LumpToolbar/LumpToolbar.tsx @@ -21,6 +21,7 @@ import { PendingApplyStatesToolbar } from "./PendingApplyStatesToolbar"; import { PendingToolCallToolbar } from "./PendingToolCallToolbar"; import { StreamingToolbar } from "./StreamingToolbar"; import { TtsActiveToolbar } from "./TtsActiveToolbar"; +import { useTranslation } from "react-i18next"; // Keyboard shortcut detection utilities const isExecuteToolCallShortcut = (event: KeyboardEvent) => { @@ -47,6 +48,7 @@ const isTerminalCommand = (toolCallState: any) => { }; export function LumpToolbar() { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const ideMessenger = useContext(IdeMessengerContext); const ttsActive = useAppSelector((state) => state.ui.ttsActive); @@ -187,7 +189,8 @@ export function LumpToolbar() { // Only show terminal streaming for actual terminal commands if (hasRunningTerminalCommand) { const count = runningTerminalCalls.length; - const stopText = `Stop Terminal${count > 1 ? ` (${count})` : ""}`; + const stopText = + t("Lump.LumpToolbar.StopTerminal") + `${count > 1 ? ` (${count})` : ""}`; return ( ); diff --git a/gui/src/components/mainInput/Lump/LumpToolbar/PendingToolCallToolbar.tsx b/gui/src/components/mainInput/Lump/LumpToolbar/PendingToolCallToolbar.tsx index 7b08c6eaf96..bb70bbd06f6 100644 --- a/gui/src/components/mainInput/Lump/LumpToolbar/PendingToolCallToolbar.tsx +++ b/gui/src/components/mainInput/Lump/LumpToolbar/PendingToolCallToolbar.tsx @@ -5,6 +5,7 @@ import { cancelToolCallThunk } from "../../../../redux/thunks/cancelToolCall"; import { getAltKeyLabel, getMetaKeyLabel, isJetBrains } from "../../../../util"; import { Button } from "../../../ui"; import { useMainEditor } from "../../TipTapEditor"; +import { useTranslation } from "react-i18next"; export const generateToolCallButtonTestId = ( action: "accept" | "reject", @@ -14,6 +15,7 @@ export const generateToolCallButtonTestId = ( }; export function PendingToolCallToolbar() { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const jetbrains = isJetBrains(); const pendingToolCalls = useAppSelector(selectPendingToolCalls); @@ -63,7 +65,7 @@ export function PendingToolCallToolbar() { {jetbrains ? getAltKeyLabel() : getMetaKeyLabel()}⌫
)} - Reject + {t("Lump.PendingToolCallToolbar.Reject")}
diff --git a/gui/src/components/mainInput/Lump/LumpToolbar/StreamingToolbar.tsx b/gui/src/components/mainInput/Lump/LumpToolbar/StreamingToolbar.tsx index d6718716173..a2e696db2dc 100644 --- a/gui/src/components/mainInput/Lump/LumpToolbar/StreamingToolbar.tsx +++ b/gui/src/components/mainInput/Lump/LumpToolbar/StreamingToolbar.tsx @@ -1,5 +1,6 @@ import { getAltKeyLabel, getMetaKeyLabel, isJetBrains } from "../../../../util"; import { GeneratingIndicator } from "./GeneratingIndicator"; +import i18n from "i18next"; interface StreamingToolbarProps { onStop: () => void; @@ -8,7 +9,7 @@ interface StreamingToolbarProps { export function StreamingToolbar({ onStop, - displayText = "Stop", + displayText = i18n.t("Lump.StreamingToolbar.Stop"), }: StreamingToolbarProps) { const jetbrains = isJetBrains(); diff --git a/gui/src/components/mainInput/Lump/LumpToolbar/TtsActiveToolbar.tsx b/gui/src/components/mainInput/Lump/LumpToolbar/TtsActiveToolbar.tsx index 034bbf281d8..de17e6205f7 100644 --- a/gui/src/components/mainInput/Lump/LumpToolbar/TtsActiveToolbar.tsx +++ b/gui/src/components/mainInput/Lump/LumpToolbar/TtsActiveToolbar.tsx @@ -3,6 +3,7 @@ import styled from "styled-components"; import { IdeMessengerContext } from "../../../../context/IdeMessenger"; import { getFontSize } from "../../../../util"; import { GeneratingIndicator } from "./GeneratingIndicator"; +import { useTranslation } from "react-i18next"; const Container = styled.div` display: flex; @@ -19,6 +20,7 @@ const StopButton = styled.div` `; export function TtsActiveToolbar() { + const { t } = useTranslation(); const ideMessenger = useContext(IdeMessengerContext); return ( @@ -30,7 +32,7 @@ export function TtsActiveToolbar() { ideMessenger.post("tts/kill", undefined); }} > - ■ Stop TTS + ■ {t("Lump.TtsActiveToolbar.StopTTS")} ); diff --git a/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts b/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts index 7076fa60a6a..2779b65bd96 100644 --- a/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts +++ b/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts @@ -29,6 +29,7 @@ import { getSlashCommandDropdownOptions, } from "./getSuggestion"; import { handleImageFile } from "./imageUtils"; +import i18n from "i18next"; export function getPlaceholderText( placeholder: TipTapEditorProps["placeholder"], @@ -39,8 +40,8 @@ export function getPlaceholderText( } return historyLength === 0 - ? "Ask anything, '@' to add context" - : "Ask a follow-up"; + ? i18n.t("TipTapEditor.editorConfig.AskAnything") + : i18n.t("TipTapEditor.editorConfig.AskFollowup"); } /** diff --git a/gui/src/components/mainInput/belowMainInput/ThinkingBlockPeek.tsx b/gui/src/components/mainInput/belowMainInput/ThinkingBlockPeek.tsx index 113b3623289..f2d3e172164 100644 --- a/gui/src/components/mainInput/belowMainInput/ThinkingBlockPeek.tsx +++ b/gui/src/components/mainInput/belowMainInput/ThinkingBlockPeek.tsx @@ -8,6 +8,7 @@ import styled from "styled-components"; import { AnimatedEllipsis } from "../../AnimatedEllipsis"; import StyledMarkdownPreview from "../../StyledMarkdownPreview"; import { Button } from "../../ui"; +import { useTranslation } from "react-i18next"; const MarkdownWrapper = styled.div` & > div > *:first-child { @@ -33,6 +34,7 @@ function ThinkingBlockPeek({ inProgress, tokens, }: ThinkingBlockPeekProps) { + const { t } = useTranslation(); const [open, setOpen] = useState(false); const [startTime, setStartTime] = useState(null); const [elapsedTime, setElapsedTime] = useState(""); @@ -50,7 +52,8 @@ function ThinkingBlockPeek({ } else if (startTime) { const endTime = Date.now(); const diff = endTime - startTime; - const diffString = `${(diff / 1000).toFixed(1)}s`; + const diffString = + `${(diff / 1000).toFixed(1)}` + t("ThinkingBlockPeek.S"); setElapsedTime(diffString); } }, [inProgress]); @@ -69,14 +72,18 @@ function ThinkingBlockPeek({ > {inProgress ? ( - {redactedThinking ? "Redacted Thinking" : "Thinking"} + {redactedThinking + ? t("ThinkingBlockPeek.RedactedThinking") + : t("ThinkingBlockPeek.Thinking")} ) : redactedThinking ? ( - "Redacted Thinking" + t("ThinkingBlockPeek.RedactedThinking") ) : ( - "Thought" + - (elapsedTime ? ` for ${elapsedTime}` : "") + + t("ThinkingBlockPeek.Thought") + + (elapsedTime + ? t("ThinkingBlockPeek.ThoughtFor") + ` ${elapsedTime}` + : "") + (tokens ? ` (${tokens} tokens)` : "") )} {open ? ( @@ -94,7 +101,7 @@ function ThinkingBlockPeek({ > {redactedThinking ? (
- Thinking content redacted due to safety reasons. + {t("ThinkingBlockPeek.SafetyReasons")}
) : ( diff --git a/gui/src/components/modelSelection/ModelSelect.tsx b/gui/src/components/modelSelection/ModelSelect.tsx index b4159314f2d..58d3c2dc7ba 100644 --- a/gui/src/components/modelSelection/ModelSelect.tsx +++ b/gui/src/components/modelSelection/ModelSelect.tsx @@ -23,6 +23,7 @@ import { useFontSize, } from "../ui"; import { Divider } from "../ui/Divider"; +import { useTranslation } from "react-i18next"; interface ModelOptionProps { option: Option; @@ -118,6 +119,7 @@ function ModelOption({ } function ModelSelect() { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const navigate = useNavigate(); @@ -252,7 +254,7 @@ function ModelSelect() { className="text-description h-[18px] gap-1 border-none" > - {modelSelectTitle(selectedModel) || "Select model"} + {modelSelectTitle(selectedModel) || t("ModelSelect.SelectModel")}
- Models + + {t("ModelSelect.Models")} +
{mode === "chat" - ? "Tool disabled in chat mode" + ? t("ToolPolicyItem.ToolDisabledInChatMode") : !props.isGroupEnabled - ? "Group is turned off" - : "Tool disabled in plan mode"} + ? t("ToolPolicyItem.GroupIsTurnedOff") + : t("ToolPolicyItem.ToolDisabledInPlanMode")}
- Description: + + {t("ToolPolicyItem.Description")}: + {props.tool.function.description} {parameters ? ( <> - Arguments: + + {t("ToolPolicyItem.Arguments")}: + {parameters.map((param, idx) => (
{param[0]} {`(${param[1].type ?? "unknown"}):`} - {param[1].description ?? "No description"} + {param[1].description ?? t("ToolPolicyItem.NoDescription")}
))} diff --git a/gui/src/pages/config/configTabs.tsx b/gui/src/pages/config/configTabs.tsx index b4687f1e33c..7ddd97409ee 100644 --- a/gui/src/pages/config/configTabs.tsx +++ b/gui/src/pages/config/configTabs.tsx @@ -18,6 +18,7 @@ import { OrganizationsSection } from "./sections/OrganizationsSection"; import { RulesSection } from "./sections/RulesSection"; import { ToolsSection } from "./sections/ToolsSection"; import { UserSettingsSection } from "./sections/UserSettingsSection"; +import i18n from "../../locales/i18n"; interface TabOption { id: string; @@ -34,13 +35,13 @@ interface TabSection { className?: string; } -export const topTabSections: TabSection[] = [ +export const topTabSections = (): TabSection[] => [ { id: "top", tabs: [ { id: "back", - label: "Back", + label: i18n.t("configTabs.Back"), component:
, icon: , }, @@ -52,7 +53,7 @@ export const topTabSections: TabSection[] = [ tabs: [ { id: "models", - label: "Models", + label: i18n.t("configTabs.Models"), component: ( @@ -62,7 +63,7 @@ export const topTabSections: TabSection[] = [ }, { id: "rules", - label: "Rules", + label: i18n.t("configTabs.Rules"), component: ( @@ -72,7 +73,7 @@ export const topTabSections: TabSection[] = [ }, { id: "tools", - label: "Tools", + label: i18n.t("configTabs.Tools"), component: ( @@ -90,7 +91,7 @@ export const topTabSections: TabSection[] = [ tabs: [ { id: "configs", - label: "Configs", + label: i18n.t("configTabs.Configs"), component: ( @@ -100,7 +101,7 @@ export const topTabSections: TabSection[] = [ }, { id: "organizations", - label: "Organizations", + label: i18n.t("configTabs.Organizations"), component: ( @@ -118,7 +119,7 @@ export const topTabSections: TabSection[] = [ tabs: [ { id: "indexing", - label: "Indexing", + label: i18n.t("configTabs.Indexing"), component: ( @@ -132,13 +133,13 @@ export const topTabSections: TabSection[] = [ }, ]; -export const bottomTabSections: TabSection[] = [ +export const bottomTabSections = (): TabSection[] => [ { id: "bottom", tabs: [ { id: "settings", - label: "Settings", + label: i18n.t("configTabs.Settings"), component: ( @@ -148,7 +149,7 @@ export const bottomTabSections: TabSection[] = [ }, { id: "help", - label: "Help", + label: i18n.t("configTabs.Help"), component: ( @@ -163,7 +164,7 @@ export const bottomTabSections: TabSection[] = [ ]; export const getAllTabs = (): TabOption[] => { - return [...topTabSections, ...bottomTabSections].flatMap( + return [...topTabSections(), ...bottomTabSections()].flatMap( (section) => section.tabs, ); }; diff --git a/gui/src/pages/config/features/account/AccountDropdown.tsx b/gui/src/pages/config/features/account/AccountDropdown.tsx index 449988b8990..b91bcfc9548 100644 --- a/gui/src/pages/config/features/account/AccountDropdown.tsx +++ b/gui/src/pages/config/features/account/AccountDropdown.tsx @@ -17,8 +17,10 @@ import { import { Divider } from "../../../../components/ui/Divider"; import { useAuth } from "../../../../context/Auth"; import { IdeMessengerContext } from "../../../../context/IdeMessenger"; +import { useTranslation } from "react-i18next"; export function AccountDropdown() { + const { t } = useTranslation(); const { session, logout, login } = useAuth(); const ideMessenger = useContext(IdeMessengerContext); @@ -36,7 +38,7 @@ export function AccountDropdown() { > - Log in + {t("AccountDropdown.Login")} @@ -88,14 +90,14 @@ export function AccountDropdown() { >
- Manage Account + {t("AccountDropdown.ManageAccount")}
- Log out + {t("AccountDropdown.Logout")}
diff --git a/gui/src/pages/config/features/keyboard/KeyboardShortcuts.tsx b/gui/src/pages/config/features/keyboard/KeyboardShortcuts.tsx index 8c9bdd84b0a..7d729b6737d 100644 --- a/gui/src/pages/config/features/keyboard/KeyboardShortcuts.tsx +++ b/gui/src/pages/config/features/keyboard/KeyboardShortcuts.tsx @@ -1,6 +1,7 @@ import { useMemo } from "react"; import Shortcut from "../../../../components/gui/Shortcut"; import { isJetBrains } from "../../../../util"; +import { useTranslation } from "react-i18next"; interface KeyboardShortcutProps { shortcut: string; @@ -131,20 +132,23 @@ const jetbrainsShortcuts: Omit[] = [ ]; function KeyboardShortcuts() { + const { t } = useTranslation(); const shortcuts = useMemo(() => { return isJetBrains() ? jetbrainsShortcuts : vscodeShortcuts; }, []); return (
-

Keyboard shortcuts

+

+ {t("KeyboardShortcuts.KeyboardShortcuts")} +

{shortcuts.map((shortcut, i) => { return ( ); diff --git a/gui/src/pages/config/index.tsx b/gui/src/pages/config/index.tsx index d672b3d099c..77a5142fa28 100644 --- a/gui/src/pages/config/index.tsx +++ b/gui/src/pages/config/index.tsx @@ -35,7 +35,7 @@ function ConfigPage() { {/* Vertical Sidebar - full height */}
- {topTabSections.map((section, index) => ( + {topTabSections().map((section, index) => ( - {bottomTabSections.map((section) => ( + {bottomTabSections().map((section) => ( @@ -82,7 +83,7 @@ export function ConfigsSection() { )}
- + -

- Report the issue on GitHub or Discussions: -

+

{t("error.reportIssue")}

openUrl(GITHUB_LINK)} className="flex items-center justify-center space-x-2 rounded-lg px-4 py-2 text-base text-white" > - GitHub Issues + {" "} + {t("error.githubIssues")} openUrl(DISCUSSIONS_LINK)} className="flex items-center justify-center rounded-lg text-base" > - Discussions + {" "} + {t("error.discussions")}
diff --git a/gui/src/pages/gui/Chat.tsx b/gui/src/pages/gui/Chat.tsx index d37ff2efcef..7dd3e645f42 100644 --- a/gui/src/pages/gui/Chat.tsx +++ b/gui/src/pages/gui/Chat.tsx @@ -59,6 +59,7 @@ import { getLocalStorage, setLocalStorage } from "../../util/localStorage"; import { EmptyChatBody } from "./EmptyChatBody"; import { ExploreDialogWatcher } from "./ExploreDialogWatcher"; import { useAutoScroll } from "./useAutoScroll"; +import { useTranslation } from "react-i18next"; // Helper function to find the index of the latest conversation summary function findLatestSummaryIndex(history: ChatHistoryItem[]): number { @@ -106,6 +107,7 @@ function fallbackRender({ error, resetErrorBoundary }: any) { } export function Chat() { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const ideMessenger = useContext(IdeMessengerContext); const reduxStore = useStore(); @@ -499,7 +501,7 @@ export function Chat() { className="flex items-center gap-2" > - Last Session + {t("gui.Chat.LastSession")} )}
diff --git a/gui/src/pages/gui/StreamError.tsx b/gui/src/pages/gui/StreamError.tsx index 6717a623acd..68d359d1521 100644 --- a/gui/src/pages/gui/StreamError.tsx +++ b/gui/src/pages/gui/StreamError.tsx @@ -20,6 +20,7 @@ import { streamResponseThunk } from "../../redux/thunks/streamResponse"; import { isLocalProfile } from "../../util"; import { analyzeError } from "../../util/errorAnalysis"; import { OutOfCreditsDialog } from "./OutOfCreditsDialog"; +import { useTranslation } from "react-i18next"; interface StreamErrorProps { error: unknown; @@ -32,6 +33,7 @@ const StreamErrorDialog = ({ error }: StreamErrorProps) => { const selectedProfile = useAppSelector(selectSelectedProfile); const { session, refreshProfiles } = useAuth(); const { mainEditor } = useMainEditor(); + const { t } = useTranslation(); const { parsedError, @@ -62,7 +64,7 @@ const StreamErrorDialog = ({ error }: StreamErrorProps) => { onClick={() => ideMessenger.ide.openUrl(apiKeyUrl)} > - Check API key + {t("StreamError.CheckAPIKey")} ) : null; @@ -74,7 +76,7 @@ const StreamErrorDialog = ({ error }: StreamErrorProps) => { onClick={() => handleEditModel(selectedModel)} > - View config + {t("StreamError.ViewConfig")} ); @@ -116,7 +118,7 @@ const StreamErrorDialog = ({ error }: StreamErrorProps) => { }} > - Resubmit last message + {t("StreamError.ResubmitLastMessage")} ); @@ -133,10 +135,10 @@ const StreamErrorDialog = ({ error }: StreamErrorProps) => {

- There was an error handling the response from{" "} - {selectedModel?.title || "the model"}. + {t("StreamError.ThereWasAnError")}{" "} + {selectedModel?.title || t("StreamError.TheModel")}.

-

Please try to submit your message again.

+

{t("StreamError.PleaseTrySubmitAgain")}

{resubmitButton}
@@ -147,8 +149,7 @@ const StreamErrorDialog = ({ error }: StreamErrorProps) => { errorContent = (
- {`This might mean your ${modelTitle} usage has been rate limited - by ${providerName}.`} + {t("StreamError.MightBeRateLimited", { modelTitle, providerName })}
{checkKeysButton} @@ -161,10 +162,10 @@ const StreamErrorDialog = ({ error }: StreamErrorProps) => { if (statusCode === 404) { errorContent = (
- Likely causes: + {t("StreamError.LikelyCauses")}
  • - Invalid + {t("StreamError.Invalid")} apiBase {selectedModel && ( <> @@ -174,7 +175,7 @@ const StreamErrorDialog = ({ error }: StreamErrorProps) => { )}
  • - Model/deployment not found + {t("StreamError.ModelDeploymentNotFound")} {selectedModel && ( <> {` for: `} @@ -193,13 +194,13 @@ const StreamErrorDialog = ({ error }: StreamErrorProps) => {
    {session && selectedProfile && !isLocalProfile(selectedProfile) && (
    - {`If your hub secret values may have changed, refresh your agents`} + {t("StreamError.IfHubSecretValuesChanged")} - Refresh agent secrets + {t("StreamError.RefreshAgentSecrets")}
    )} - {`It's possible that your API key is invalid.`} + {t("StreamError.APIKeyMayBeInvalid")}
    {checkKeysButton} {configButton} @@ -211,7 +212,7 @@ const StreamErrorDialog = ({ error }: StreamErrorProps) => { if (statusCode === 403) { errorContent = (
    - {`Likely cause: not authorized to access the model deployment.`} + {t("StreamError.NotAuthorizedToAccessModel")}
    {checkKeysButton} {configButton} @@ -227,10 +228,10 @@ const StreamErrorDialog = ({ error }: StreamErrorProps) => { ) { errorContent = (
    - {`Most likely, the provider's server(s) are overloaded and streaming was interrupted. Try again later`} + {t("StreamError.ServerOverloadedTryAgain")} {selectedModel ? ( - {`Provider: `} + {t("StreamError.Provider")}:{" "} {selectedModel.underlyingProviderName} ) : null} @@ -250,7 +251,7 @@ const StreamErrorDialog = ({ error }: StreamErrorProps) => { onClick={() => ideMessenger.ide.openUrl(helpUrl)} > - View help documentation + {t("StreamError.ViewHelpDocumentation")} )} {apiKeyUrl && ( @@ -259,7 +260,7 @@ const StreamErrorDialog = ({ error }: StreamErrorProps) => { onClick={() => ideMessenger.ide.openUrl(apiKeyUrl)} > - Check API key + {t("StreamError.CheckAPIKey")} )} {configButton} @@ -272,7 +273,7 @@ const StreamErrorDialog = ({ error }: StreamErrorProps) => {
    {/* Concise error title */}

    - Error handling model response + {t("StreamError.ErrorHandlingModelResponse")}

    {errorContent} @@ -281,7 +282,7 @@ const StreamErrorDialog = ({ error }: StreamErrorProps) => { {message && (
    @@ -296,7 +297,7 @@ const StreamErrorDialog = ({ error }: StreamErrorProps) => { className="flex items-center" > - Copy output + {t("StreamError.CopyOutput")} { className="flex items-center" > - View Logs + {t("StreamError.ViewLogs")}
    diff --git a/gui/src/pages/gui/ToolCallDiv/GroupedToolCallHeader.tsx b/gui/src/pages/gui/ToolCallDiv/GroupedToolCallHeader.tsx index e3e8ffdcafb..d14922d06f7 100644 --- a/gui/src/pages/gui/ToolCallDiv/GroupedToolCallHeader.tsx +++ b/gui/src/pages/gui/ToolCallDiv/GroupedToolCallHeader.tsx @@ -2,6 +2,7 @@ import { FolderIcon } from "@heroicons/react/24/outline"; import { ToolCallState } from "core"; import { ToggleWithIcon } from "./ToggleWithIcon"; import { getGroupActionVerb } from "./utils"; +import { useTranslation } from "react-i18next"; interface GroupedToolCallHeaderProps { toolCallStates: ToolCallState[]; @@ -16,6 +17,7 @@ export function GroupedToolCallHeader({ open, onToggle, }: GroupedToolCallHeaderProps) { + const { t } = useTranslation(); return (
    {getGroupActionVerb(toolCallStates)} {activeCalls.length}{" "} - {activeCalls.length === 1 ? "action" : "actions"} + {activeCalls.length === 1 + ? t("GroupedToolCallHeader.action") + : t("GroupedToolCallHeader.actions")}
    ); diff --git a/gui/src/pages/gui/ToolCallDiv/ToolCallStatusMessage.tsx b/gui/src/pages/gui/ToolCallDiv/ToolCallStatusMessage.tsx index c633b0514c4..9495c2ce6bc 100644 --- a/gui/src/pages/gui/ToolCallDiv/ToolCallStatusMessage.tsx +++ b/gui/src/pages/gui/ToolCallDiv/ToolCallStatusMessage.tsx @@ -1,6 +1,7 @@ import { Tool, ToolCallState } from "core"; import Mustache from "mustache"; import { getStatusIntro } from "./utils"; +import { useTranslation } from "react-i18next"; interface ToolCallStatusMessageProps { tool: Tool | undefined; @@ -11,14 +12,27 @@ export function ToolCallStatusMessage({ tool, toolCallState, }: ToolCallStatusMessageProps) { - if (!tool) return "Agent tool use"; + const { t } = useTranslation(); + // Helper function to translate tool names, falling back to the original name if translation is missing. + const t_tool = function (name: string) { + const key = "ToolCallStatusMessage.tool." + name; + const val = t(key); + if (key === val) { + return name; + } + return val; + }; + + if (!tool) return t("ToolCallStatusMessage.AgentToolUse"); const toolName = tool.displayTitle ?? tool.function.name; - const defaultToolDescription = `${toolName} tool`; + const defaultToolDescription = t("ToolCallStatusMessage.ToolNameTool", { + toolName, + }); const futureMessage: string = tool.wouldLikeTo - ? Mustache.render(tool.wouldLikeTo, toolCallState.parsedArgs) - : `use the ${defaultToolDescription}`; + ? Mustache.render(t_tool(tool.wouldLikeTo), toolCallState.parsedArgs) + : t("ToolCallStatusMessage.UseThe", { defaultToolDescription }); // TODO go back and replace arg string values and tool names with tags // to make them more readable @@ -31,8 +45,8 @@ export function ToolCallStatusMessage({ (tool.isInstant && toolCallState.status === "calling") ) { message = tool.hasAlready - ? Mustache.render(tool.hasAlready, toolCallState.parsedArgs) - : `used the ${defaultToolDescription}`; + ? Mustache.render(t_tool(tool.hasAlready), toolCallState.parsedArgs) + : t("ToolCallStatusMessage.UsedThe", { defaultToolDescription }); } else { switch (toolCallState.status) { case "generating": @@ -43,8 +57,8 @@ export function ToolCallStatusMessage({ break; case "calling": message = tool.isCurrently - ? Mustache.render(tool.isCurrently, toolCallState.parsedArgs) - : `calling the ${defaultToolDescription}`; + ? Mustache.render(t_tool(tool.isCurrently), toolCallState.parsedArgs) + : t("ToolCallStatusMessage.CallingThe", { defaultToolDescription }); break; default: message = defaultToolDescription; @@ -56,7 +70,7 @@ export function ToolCallStatusMessage({ className="text-description line-clamp-4 min-w-0 break-words" data-testid="tool-call-title" > - {`Continue ${intro} ${message}`} + {t("ToolCallStatusMessage.ContinueIntroMessage", { intro, message })}
    ); } diff --git a/gui/src/pages/gui/ToolCallDiv/ToolTruncateHistoryIcon.tsx b/gui/src/pages/gui/ToolCallDiv/ToolTruncateHistoryIcon.tsx index dc5c2a03b72..2ea8ecd06bb 100644 --- a/gui/src/pages/gui/ToolCallDiv/ToolTruncateHistoryIcon.tsx +++ b/gui/src/pages/gui/ToolCallDiv/ToolTruncateHistoryIcon.tsx @@ -6,6 +6,7 @@ import { useMainEditor } from "../../../components/mainInput/TipTapEditor"; import { ToolbarButtonWithTooltip } from "../../../components/StyledMarkdownPreview/StepContainerPreToolbar/ToolbarButtonWithTooltip"; import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; import { truncateHistoryToMessage } from "../../../redux/slices/sessionSlice"; +import i18n from "../../../locales/i18n"; export function ToolTruncateHistoryIcon({ historyIndex, @@ -27,7 +28,9 @@ export function ToolTruncateHistoryIcon({ return ( { if (isStreaming) { return; diff --git a/gui/src/pages/gui/ToolCallDiv/utils.tsx b/gui/src/pages/gui/ToolCallDiv/utils.tsx index fe18123f461..acccb87d940 100644 --- a/gui/src/pages/gui/ToolCallDiv/utils.tsx +++ b/gui/src/pages/gui/ToolCallDiv/utils.tsx @@ -8,6 +8,7 @@ import { import { ComponentType, SVGProps } from "react"; import { vscButtonBackground } from "../../../components"; import Spinner from "../../../components/gui/Spinner"; +import i18n from "../../../locales/i18n"; // Helper function to determine the intro verb based on tool call status export function getStatusIntro( @@ -20,14 +21,14 @@ export function getStatusIntro( switch (status) { case "generating": - return "will"; + return i18n.t("ToolCallDiv.utils.will"); case "generated": - return "wants to"; + return i18n.t("ToolCallDiv.utils.wantsTo"); case "calling": - return "is"; + return i18n.t("ToolCallDiv.utils.is"); case "canceled": case "errored": - return "tried to"; + return i18n.t("ToolCallDiv.utils.triedTo"); default: return ""; } @@ -35,25 +36,26 @@ export function getStatusIntro( // Helper function to get the appropriate verb for group actions export function getGroupActionVerb(toolCallStates: ToolCallState[]): string { - if (toolCallStates.length === 0) return "Performing"; + if (toolCallStates.length === 0) + return i18n.t("ToolCallDiv.utils.performing"); // Get the most "active" status from all tool calls const statuses = toolCallStates.map((state) => state.status); // Priority order: calling > generating > generated > done > errored/canceled if (statuses.includes("calling")) { - return "Performing"; + return i18n.t("ToolCallDiv.utils.performing"); } else if (statuses.includes("generating")) { - return "Generating"; + return i18n.t("ToolCallDiv.utils.generating"); } else if (statuses.includes("generated")) { - return "Pending"; + return i18n.t("ToolCallDiv.utils.pending"); } else if (statuses.some((s) => s === "done")) { - return "Performed"; + return i18n.t("ToolCallDiv.utils.performed"); } else if (statuses.some((s) => s === "errored" || s === "canceled")) { - return "Attempted"; + return i18n.t("ToolCallDiv.utils.attempted"); } - return "Performing"; + return i18n.t("ToolCallDiv.utils.performing"); } type IconName = keyof typeof Icons; diff --git a/gui/src/pages/gui/index.tsx b/gui/src/pages/gui/index.tsx index 0550d27f258..8e7bb364305 100644 --- a/gui/src/pages/gui/index.tsx +++ b/gui/src/pages/gui/index.tsx @@ -1,7 +1,11 @@ import { History } from "../../components/History"; import { Chat } from "./Chat"; +import { useLocalStorage } from "../../context/LocalStorage"; +import i18n from "../../locales/i18n"; export default function GUI() { + const { language } = useLocalStorage(); + i18n.changeLanguage(language || "en"); return (