diff --git a/.roo/rules-translate/001-general-rules.md b/.roo/rules-translate/001-general-rules.md index e27b9793e2..05b89d7e14 100644 --- a/.roo/rules-translate/001-general-rules.md +++ b/.roo/rules-translate/001-general-rules.md @@ -30,7 +30,10 @@ # 4. WEBVIEW UI LOCALIZATION (webview-ui/src/) - Located in webview-ui/src/i18n/locales/ -- Uses standard React i18next patterns with the useTranslation hook +- Use react-i18next's `useTranslation` and `Trans` (`import { Trans, useTranslation } from "react-i18next"`): + - Always pass the component's `t` to `Trans` (``); `Trans` does not follow language changes on its own (ESLint enforces this) + - Never use the global `t` from `i18next` (`i18next.t`); memoized output would keep showing the previous language after a switch (ESLint enforces this). Pass `t` to helpers instead, as `utils/format.ts` and `utils/validate.ts` do +- `useTranslation()` takes fully qualified keys (`t("chat:about")`); `useTranslation("chat")` also takes keys relative to that namespace (`t("about")`); fully qualified keys still work - All user interface strings should be internationalized - Always use the Trans component with named components for text with embedded components @@ -40,6 +43,7 @@ ``` diff --git a/.roo/skills/roo-translation/SKILL.md b/.roo/skills/roo-translation/SKILL.md index 82eab76fbf..e77b5b83fd 100644 --- a/.roo/skills/roo-translation/SKILL.md +++ b/.roo/skills/roo-translation/SKILL.md @@ -70,7 +70,10 @@ This guidance file is loaded at runtime and should be consulted for the latest b ## WebView UI Localization (webview-ui/src/) - Located in `webview-ui/src/i18n/locales/` -- Uses standard React i18next patterns with the `useTranslation` hook +- Use react-i18next's `useTranslation` and `Trans` (`import { Trans, useTranslation } from "react-i18next"`): + - Always pass the component's `t` to `Trans` (``); `Trans` does not follow language changes on its own (ESLint enforces this) + - Never use the global `t` from `i18next` (`i18next.t`); memoized output would keep showing the previous language after a switch (ESLint enforces this). Pass `t` to helpers instead, as `utils/format.ts` and `utils/validate.ts` do +- `useTranslation()` takes fully qualified keys (`t("chat:about")`); `useTranslation("chat")` also takes keys relative to that namespace (`t("about")`); fully qualified keys still work - All user interface strings should be internationalized - Always use the `Trans` component with named components for text with embedded components @@ -86,6 +89,7 @@ React component usage: ```tsx , diff --git a/webview-ui/eslint.config.mjs b/webview-ui/eslint.config.mjs index 11a49e9b41..39d61c739c 100644 --- a/webview-ui/eslint.config.mjs +++ b/webview-ui/eslint.config.mjs @@ -40,6 +40,40 @@ export default [ "no-case-declarations": "off", }, }, + { + // Translations must come from the component's `t` (react-i18next's `useTranslation`), which changes + // identity on a language switch; the global `t` gives React (and React Compiler caches) nothing to + // invalidate on, so memoized output would keep the previous language. + files: ["src/**/*.{ts,tsx}"], + ignores: ["src/i18n/**"], + rules: { + "no-restricted-imports": [ + "error", + { + paths: [ + { + name: "i18next", + importNames: ["t"], + message: + "Use t from react-i18next's useTranslation so memoized output follows language changes.", + }, + ], + }, + ], + "no-restricted-syntax": [ + "error", + { + selector: "MemberExpression[object.name='i18next'][property.name='t']", + message: "Use t from react-i18next's useTranslation so memoized output follows language changes.", + }, + { + selector: "JSXOpeningElement[name.name='Trans']:not(:has(JSXAttribute[name.name='t']))", + message: + "Pass t={t} from useTranslation: Trans does not subscribe to language changes, so a cached keeps the previous language.", + }, + ], + }, + }, { files: ["src/__mocks__/**/*.js"], rules: { diff --git a/webview-ui/playwright/AppProviders.tsx b/webview-ui/playwright/AppProviders.tsx index 906c34ec47..c9e668a17a 100644 --- a/webview-ui/playwright/AppProviders.tsx +++ b/webview-ui/playwright/AppProviders.tsx @@ -4,10 +4,8 @@ import { I18nextProvider } from "react-i18next" import { providerIdentifiers } from "@roo-code/types" import { ExtensionStateContextProvider } from "@/context/ExtensionStateContext" -import TranslationProvider from "@/i18n/TranslationContext" import i18next, { loadTranslations } from "@/i18n/setup" import { TooltipProvider } from "@/components/ui/tooltip" -import { TranslationContext as PlaywrightTranslationContext } from "@src/i18n/TranslationContext" loadTranslations() @@ -42,16 +40,11 @@ export function AppProviders({ children, initialState }: AppProvidersProps) { return ( - - i18next.t(key, options), i18n: i18next }}> - - -
{children}
-
-
-
-
+ + +
{children}
+
+
) diff --git a/webview-ui/playwright/TranslationContext.ts b/webview-ui/playwright/TranslationContext.ts deleted file mode 100644 index b2024e29d9..0000000000 --- a/webview-ui/playwright/TranslationContext.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createContext, useContext } from "react" - -type TranslationContextValue = { - t: (key: string, options?: Record) => string - i18n: unknown -} - -export const TranslationContext = createContext({ - t: (key: string) => key, - i18n: null, -}) - -export const useAppTranslation = () => useContext(TranslationContext) diff --git a/webview-ui/playwright/vite.config.ts b/webview-ui/playwright/vite.config.ts index f3649be72d..faae6282bc 100644 --- a/webview-ui/playwright/vite.config.ts +++ b/webview-ui/playwright/vite.config.ts @@ -48,10 +48,6 @@ export default defineConfig({ find: "@src/context/ExtensionStateContext", replacement: path.resolve(dirname, "ExtensionStateContext.tsx"), }, - { - find: "@src/i18n/TranslationContext", - replacement: path.resolve(dirname, "TranslationContext.ts"), - }, { find: "@", replacement: path.resolve(webviewRoot, "src") }, { find: "@src", replacement: path.resolve(webviewRoot, "src") }, { find: "@roo", replacement: path.resolve(webviewRoot, "../src/shared") }, diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 3e05ce64da..dd1dfa76b6 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -4,7 +4,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { type ExtensionMessage, TelemetryEventName } from "@roo-code/types" -import TranslationProvider from "./i18n/TranslationContext" +import "./i18n/setup" import { MarketplaceViewStateManager } from "./components/marketplace/MarketplaceViewStateManager" import { vscode } from "./utils/vscode" @@ -344,13 +344,11 @@ const queryClient = new QueryClient() const AppWithProviders = () => ( - - - - - - - + + + + + ) diff --git a/webview-ui/src/__tests__/App.spec.tsx b/webview-ui/src/__tests__/App.spec.tsx index 4b4cbcd5fe..a553c54426 100644 --- a/webview-ui/src/__tests__/App.spec.tsx +++ b/webview-ui/src/__tests__/App.spec.tsx @@ -114,36 +114,18 @@ vi.mock("react-i18next", () => { MockedComponent.displayName = `withTranslation(${Component.displayName || Component.name || "Component"})` return MockedComponent }, - Trans: ({ children }: { children: React.ReactNode }) => <>{children}, - useTranslation: () => { - return { - t: tFunction, - i18n: { - t: tFunction, - changeLanguage: vi.fn(() => Promise.resolve()), - }, - } - }, - initReactI18next: { - type: "3rdParty", - init: vi.fn(), - }, - } -}) - -// Mock TranslationProvider to pass through children -vi.mock("@src/i18n/TranslationContext", () => { - const tFunction = (key: string) => key - return { - __esModule: true, - default: ({ children }: { children: React.ReactNode }) => <>{children}, - useAppTranslation: () => ({ + useTranslation: () => ({ t: tFunction, i18n: { t: tFunction, changeLanguage: vi.fn(() => Promise.resolve()), }, }), + Trans: ({ children }: { children: React.ReactNode }) => <>{children}, + initReactI18next: { + type: "3rdParty", + init: vi.fn(), + }, } }) diff --git a/webview-ui/src/__tests__/FileChangesPanel.spec.tsx b/webview-ui/src/__tests__/FileChangesPanel.spec.tsx index 7b74a12bc2..75fd4ec266 100644 --- a/webview-ui/src/__tests__/FileChangesPanel.spec.tsx +++ b/webview-ui/src/__tests__/FileChangesPanel.spec.tsx @@ -2,7 +2,6 @@ import React from "react" import { act } from "@testing-library/react" import { fireEvent, render, screen } from "@/utils/test-utils" import type { ClineMessage } from "@roo-code/types" -import { TranslationProvider } from "@/i18n/__mocks__/TranslationContext" import FileChangesPanel from "../components/chat/FileChangesPanel" const mockPostMessage = vi.fn() @@ -69,11 +68,7 @@ function createFileEditMessage( } function renderPanel(messages: ClineMessage[] | undefined, taskId?: string) { - return render( - - - , - ) + return render() } describe("FileChangesPanel", () => { @@ -333,11 +328,7 @@ describe("FileChangesPanel", () => { expandRow() expect(requestsOfType("readOriginalContent")).toHaveLength(1) - rerender( - - - , - ) + rerender() fireEvent.click(screen.getByTestId("accordian-toggle")) const requests = requestsOfType("readOriginalContent") @@ -347,11 +338,7 @@ describe("FileChangesPanel", () => { it("does not send a duplicate request when switching A -> B -> A before the first response arrives", () => { const messages = [createEditWithOriginal({ originalContentLength: 5000 })] - const panel = (taskId: string) => ( - - - - ) + const panel = (taskId: string) => const { rerender } = renderPanel(messages, "task-A") expandRow() expect(requestsOfType("readOriginalContent")).toHaveLength(1) @@ -372,11 +359,7 @@ describe("FileChangesPanel", () => { it("requests again after a response that arrived for a task that is no longer current", () => { const messages = [createEditWithOriginal({ originalContentLength: 5000 })] - const panel = (taskId: string) => ( - - - - ) + const panel = (taskId: string) => const { rerender } = renderPanel(messages, "task-A") expandRow() @@ -397,11 +380,7 @@ describe("FileChangesPanel", () => { expandRow() mockPostMessage.mockClear() - rerender( - - - , - ) + rerender() expect(mockPostMessage).not.toHaveBeenCalled() }) @@ -417,11 +396,7 @@ describe("FileChangesPanel", () => { }) expect(requestsOfType("readOriginalContent")).toHaveLength(1) - rerender( - - - , - ) + rerender() fireEvent.click(screen.getByTestId("accordian-toggle")) respond({ type: "fileContent", fileContent: { path: "src/foo.ts", content: "new line\n" } }) diff --git a/webview-ui/src/components/chat/Announcement.tsx b/webview-ui/src/components/chat/Announcement.tsx index a1231505a1..8764610253 100644 --- a/webview-ui/src/components/chat/Announcement.tsx +++ b/webview-ui/src/components/chat/Announcement.tsx @@ -1,9 +1,8 @@ import { memo, type MouseEvent, type ReactNode, useState } from "react" -import { Trans } from "react-i18next" import { SiDiscord, SiReddit, SiX } from "react-icons/si" import { Package } from "@roo/package" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { Trans, useTranslation } from "react-i18next" import { vscode } from "@src/utils/vscode" import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@src/components/ui" import { EXTERNAL_LINKS } from "@src/constants/externalLinks" @@ -24,7 +23,7 @@ interface AnnouncementProps { */ const Announcement = ({ hideAnnouncement }: AnnouncementProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [open, setOpen] = useState(true) return ( @@ -72,7 +71,7 @@ const Announcement = ({ hideAnnouncement }: AnnouncementProps) => {
- }} /> + }} />
diff --git a/webview-ui/src/components/chat/ApiConfigSelector.tsx b/webview-ui/src/components/chat/ApiConfigSelector.tsx index ef5151a00d..8ef3cff405 100644 --- a/webview-ui/src/components/chat/ApiConfigSelector.tsx +++ b/webview-ui/src/components/chat/ApiConfigSelector.tsx @@ -5,7 +5,7 @@ import { cn } from "@/lib/utils" import { enabledSelectorTriggerClassName, selectorTriggerClassName } from "@/components/ui/selectorTriggerStyles" import { useRooPortal } from "@/components/ui/hooks/useRooPortal" import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { vscode } from "@/utils/vscode" import { Button } from "@/components/ui" @@ -38,7 +38,7 @@ export const ApiConfigSelector = ({ lockApiConfigAcrossModes, onToggleLockApiConfig, }: ApiConfigSelectorProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [open, setOpen] = useState(false) const [searchValue, setSearchValue] = useState("") const portalContainer = useRooPortal("roo-portal") diff --git a/webview-ui/src/components/chat/AutoApproveDropdown.tsx b/webview-ui/src/components/chat/AutoApproveDropdown.tsx index f0c64e52c1..f8cd1773f0 100644 --- a/webview-ui/src/components/chat/AutoApproveDropdown.tsx +++ b/webview-ui/src/components/chat/AutoApproveDropdown.tsx @@ -8,7 +8,7 @@ import { enabledSelectorTriggerClassName, selectorTriggerClassName } from "@/com import { useExtensionState } from "@/context/ExtensionStateContext" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { useAutoApprovalToggles } from "@/hooks/useAutoApprovalToggles" import { useAutoApprovalState } from "@/hooks/useAutoApprovalState" @@ -27,7 +27,7 @@ interface AutoApproveDropdownProps { export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }: AutoApproveDropdownProps) => { const [open, setOpen] = React.useState(false) const portalContainer = useRooPortal("roo-portal") - const { t } = useAppTranslation() + const { t } = useTranslation() const { autoApprovalEnabled, diff --git a/webview-ui/src/components/chat/AutoApprovedRequestLimitWarning.tsx b/webview-ui/src/components/chat/AutoApprovedRequestLimitWarning.tsx index c7744f1bfd..7ec39be361 100644 --- a/webview-ui/src/components/chat/AutoApprovedRequestLimitWarning.tsx +++ b/webview-ui/src/components/chat/AutoApprovedRequestLimitWarning.tsx @@ -1,5 +1,5 @@ import React, { memo, useState } from "react" -import { Trans } from "react-i18next" +import { Trans, useTranslation } from "react-i18next" import type { ClineMessage } from "@roo-code/types" @@ -11,6 +11,7 @@ type AutoApprovedRequestLimitWarningProps = { } export const AutoApprovedRequestLimitWarning = memo(({ message }: AutoApprovedRequestLimitWarningProps) => { + const { t } = useTranslation() const [buttonClicked, setButtonClicked] = useState(false) const { count, type = "requests" } = JSON.parse(message.text ?? "{}") @@ -34,7 +35,7 @@ export const AutoApprovedRequestLimitWarning = memo(({ message }: AutoApprovedRe
- +
@@ -48,7 +49,7 @@ export const AutoApprovedRequestLimitWarning = memo(({ message }: AutoApprovedRe justifyContent: "center", }}>
- +
diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 77ae4529c0..294fd4064c 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -1,6 +1,6 @@ import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react" import { useSize } from "react-use" -import { useTranslation, Trans } from "react-i18next" +import { Trans, useTranslation } from "react-i18next" import deepEqual from "fast-deep-equal" import { VSCodeBadge } from "@vscode/webview-ui-toolkit/react" @@ -561,12 +561,14 @@ export const ChatRowContent = ({ {tool.path ? ( }} values={{ query: tool.query, path: tool.path }} /> ) : ( }} values={{ query: tool.query }} @@ -780,6 +782,7 @@ export const ChatRowContent = ({ {message.type === "ask" ? ( ) : ( {tool.reason ? ( {tool.mode} }} values={{ mode: tool.mode, reason: tool.reason }} /> ) : ( {tool.mode} }} values={{ mode: tool.mode }} @@ -838,12 +844,14 @@ export const ChatRowContent = ({ <> {tool.reason ? ( {tool.mode} }} values={{ mode: tool.mode, reason: tool.reason }} /> ) : ( {tool.mode} }} values={{ mode: tool.mode }} @@ -886,6 +894,7 @@ export const ChatRowContent = ({ {tool.mode} }} values={{ mode: tool.mode }} diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 61419487cc..25e92f5779 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -11,7 +11,7 @@ import { Mode, getAllModes } from "@roo/modes" import { vscode } from "@src/utils/vscode" import { useExtensionState } from "@src/context/ExtensionStateContext" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { ContextMenuOptionType, getContextMenuOptions, @@ -89,7 +89,7 @@ export const ChatTextArea = forwardRef( // indexing, paste, drop, and the textarea value) is safe. const normalizedInputValue = typeof inputValue === "string" ? inputValue : "" - const { t } = useAppTranslation() + const { t } = useTranslation() const { filePaths, openedTabs, diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index bb690c1f03..f761fbac6e 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -32,7 +32,7 @@ import { ProfileValidator } from "@roo/ProfileValidator" import { getLatestTodo } from "@roo/todo" import { vscode } from "@src/utils/vscode" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { useExtensionState } from "@src/context/ExtensionStateContext" import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel" import RooHero from "@src/components/welcome/RooHero" @@ -81,7 +81,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction(undefined) const [enableButtons, setEnableButtons] = useState(false) - const [primaryButtonText, setPrimaryButtonText] = useState(undefined) - const [secondaryButtonText, setSecondaryButtonText] = useState(undefined) + // Translation keys rather than labels, so an open prompt follows a UI language change. + const [primaryButtonKey, setPrimaryButtonKey] = useState(undefined) + const [secondaryButtonKey, setSecondaryButtonKey] = useState(undefined) + const primaryButtonText = primaryButtonKey ? t(primaryButtonKey) : undefined + const secondaryButtonText = secondaryButtonKey ? t(secondaryButtonKey) : undefined const [_didClickCancel, setDidClickCancel] = useState(false) const virtuosoRef = useRef(null) const [expandedRows, setExpandedRows] = useState>({}) @@ -314,16 +317,16 @@ const ChatViewComponent: React.ForwardRefRenderFunction msg.ask === "completion_result" || msg.say === "completion_result", ) if (isCompletedSubtask) { - setPrimaryButtonText(t("chat:startNewTask.title")) - setSecondaryButtonText(undefined) + setPrimaryButtonKey("chat:startNewTask.title") + setSecondaryButtonKey(undefined) } else { - setPrimaryButtonText(t("chat:resumeTask.title")) - setSecondaryButtonText(t("chat:terminate.title")) + setPrimaryButtonKey("chat:resumeTask.title") + setSecondaryButtonKey("chat:terminate.title") } setDidClickCancel(false) // special case where we reset the cancel button state break @@ -446,8 +449,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction msg.ask === "completion_result" || msg.say === "completion_result", ) if (hasCompletionResult) { - setPrimaryButtonText(t("chat:startNewTask.title")) - setSecondaryButtonText(undefined) + setPrimaryButtonKey("chat:startNewTask.title") + setSecondaryButtonKey(undefined) } } - }, [clineAsk, currentTaskItem?.parentTaskId, messages, t]) + }, [clineAsk, currentTaskItem?.parentTaskId, messages]) useEffect(() => { if (messages.length === 0) { setSendingDisabled(false) setClineAsk(undefined) setEnableButtons(false) - setPrimaryButtonText(undefined) - setSecondaryButtonText(undefined) + setPrimaryButtonKey(undefined) + setSecondaryButtonKey(undefined) } }, [messages.length]) @@ -630,8 +633,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction @@ -1817,13 +1820,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction diff --git a/webview-ui/src/components/chat/CheckpointRestoreDialog.tsx b/webview-ui/src/components/chat/CheckpointRestoreDialog.tsx index 34a99c3afa..8171b00bbc 100644 --- a/webview-ui/src/components/chat/CheckpointRestoreDialog.tsx +++ b/webview-ui/src/components/chat/CheckpointRestoreDialog.tsx @@ -1,5 +1,5 @@ import React from "react" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { AlertDialog, AlertDialogAction, @@ -26,7 +26,7 @@ export const CheckpointRestoreDialog: React.FC = ( type, hasCheckpoint, }) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const isEdit = type === "edit" const title = isEdit ? t("common:confirmation.editMessage") : t("common:confirmation.deleteMessage") diff --git a/webview-ui/src/components/chat/CheckpointWarning.tsx b/webview-ui/src/components/chat/CheckpointWarning.tsx index 81675f4993..2b661fc254 100644 --- a/webview-ui/src/components/chat/CheckpointWarning.tsx +++ b/webview-ui/src/components/chat/CheckpointWarning.tsx @@ -1,5 +1,5 @@ import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" -import { Trans } from "react-i18next" +import { Trans, useTranslation } from "react-i18next" interface CheckpointWarningProps { warning: { @@ -9,6 +9,7 @@ interface CheckpointWarningProps { } export const CheckpointWarning = ({ warning }: CheckpointWarningProps) => { + const { t } = useTranslation() const settingsLink = ( { = ({ indexingStatus: externalIndexingStatus, }) => { const SECRET_PLACEHOLDER = "••••••••••••••••" - const { t } = useAppTranslation() + const { t } = useTranslation() const { codebaseIndexConfig, codebaseIndexModels, cwd, apiConfiguration, platform, arch } = useExtensionState() const [open, setOpen] = useState(false) const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = useState(false) @@ -641,7 +640,7 @@ export const CodeIndexPopover: React.FC = ({

{t("settings:codeIndex.title")}

- + = ({ results }) => { + const { t } = useTranslation() const [codebaseSearchResultsExpanded, setCodebaseSearchResultsExpanded] = useState(false) return ( @@ -22,6 +23,7 @@ const CodebaseSearchResultsDisplay: React.FC className="cursor-pointer flex items-center justify-between px-2 py-2 border bg-[var(--vscode-editor-background)] border-[var(--vscode-editorGroup-border)]"> { + const { t } = useTranslation() const { terminalShellIntegrationDisabled = false, collapseCommandOutputByDefault = DEFAULT_COLLAPSE_COMMAND_OUTPUT_BY_DEFAULT, @@ -197,7 +198,7 @@ export const CommandExecution = ({ executionId, text, icon, title, isDenied = fa {status?.status === "exited" && (

+ content={t("chat:commandExecution.exitStatus", { exitCode: status.exitCode })}>
{
, diff --git a/webview-ui/src/components/chat/ContextMenu.tsx b/webview-ui/src/components/chat/ContextMenu.tsx index 04ceb2eab6..92cce63e09 100644 --- a/webview-ui/src/components/chat/ContextMenu.tsx +++ b/webview-ui/src/components/chat/ContextMenu.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useMemo, useRef, useState } from "react" import { getIconForFilePath, getIconUrlByName, getIconForDirectoryPath } from "vscode-material-icons" -import { Trans } from "react-i18next" -import { t } from "i18next" +import { Trans, useTranslation } from "react-i18next" import { Settings } from "lucide-react" import type { ModeConfig, Command } from "@roo-code/types" @@ -45,6 +44,7 @@ const ContextMenu: React.FC = ({ commands = [], }) => { const [materialIconsBaseUri, setMaterialIconsBaseUri] = useState("") + const { t } = useTranslation() const menuRef = useRef(null) const filteredOptions = useMemo(() => { @@ -297,6 +297,7 @@ const ContextMenu: React.FC = ({

Slash Commands

{t("chat:tokenProgress.tokensUsed", { - used: formatLargeNumber(safeContextTokens), - total: formatLargeNumber(safeContextWindow), + used: formatLargeNumber(safeContextTokens, t), + total: formatLargeNumber(safeContextWindow, t), })}
{reservedForOutput > 0 && (
{t("chat:tokenProgress.reservedForResponse", { - amount: formatLargeNumber(reservedForOutput), + amount: formatLargeNumber(reservedForOutput, t), })}
)} {availableSize > 0 && (
{t("chat:tokenProgress.availableSpace", { - amount: formatLargeNumber(availableSize), + amount: formatLargeNumber(availableSize, t), })}
)} @@ -56,7 +56,7 @@ export const ContextWindowProgress = ({ contextWindow, contextTokens, maxTokens return ( <>
-
{formatLargeNumber(safeContextTokens)}
+
{formatLargeNumber(safeContextTokens, t)}
{/* Main progress bar container */} @@ -91,7 +91,7 @@ export const ContextWindowProgress = ({ contextWindow, contextTokens, maxTokens
-
{formatLargeNumber(safeContextWindow)}
+
{formatLargeNumber(safeContextWindow, t)}
) diff --git a/webview-ui/src/components/chat/FollowUpSuggest.tsx b/webview-ui/src/components/chat/FollowUpSuggest.tsx index 4fa6f9ab1d..6226285058 100644 --- a/webview-ui/src/components/chat/FollowUpSuggest.tsx +++ b/webview-ui/src/components/chat/FollowUpSuggest.tsx @@ -3,7 +3,7 @@ import { ClipboardCopy, Timer } from "lucide-react" import { Button, StandardTooltip } from "@/components/ui" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { useExtensionState } from "@src/context/ExtensionStateContext" import { getSuggestionMode, hasUsableAnswer, type SuggestionItem } from "@roo-code/types" import { cn } from "@/lib/utils" @@ -31,7 +31,7 @@ export const FollowUpSuggest = ({ const { autoApprovalEnabled, alwaysAllowFollowupQuestions, followupAutoApproveTimeoutMs } = useExtensionState() const [countdown, setCountdown] = useState(null) const [suggestionSelected, setSuggestionSelected] = useState(false) - const { t } = useAppTranslation() + const { t } = useTranslation() // The model may emit suggestions with missing or blank answers. Those render // as empty buttons, and clicking "Copy to input" on one pushes an undefined diff --git a/webview-ui/src/components/chat/IndexingStatusBadge.tsx b/webview-ui/src/components/chat/IndexingStatusBadge.tsx index 22dbe8f9d5..f0a7df24a0 100644 --- a/webview-ui/src/components/chat/IndexingStatusBadge.tsx +++ b/webview-ui/src/components/chat/IndexingStatusBadge.tsx @@ -5,7 +5,7 @@ import type { IndexingStatus, IndexingStatusUpdateMessage } from "@roo-code/type import { cn } from "@src/lib/utils" import { vscode } from "@src/utils/vscode" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { useExtensionState } from "@src/context/ExtensionStateContext" import { PopoverTrigger, StandardTooltip, Button } from "@src/components/ui" @@ -18,7 +18,7 @@ interface IndexingStatusBadgeProps { } export const IndexingStatusBadge: React.FC = ({ className }) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const { cwd } = useExtensionState() const [indexingStatus, setIndexingStatus] = useState({ diff --git a/webview-ui/src/components/chat/MessageModificationConfirmationDialog.tsx b/webview-ui/src/components/chat/MessageModificationConfirmationDialog.tsx index 883a34f3f3..f5e03e384a 100644 --- a/webview-ui/src/components/chat/MessageModificationConfirmationDialog.tsx +++ b/webview-ui/src/components/chat/MessageModificationConfirmationDialog.tsx @@ -1,5 +1,5 @@ import React from "react" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { AlertDialog, AlertDialogAction, @@ -24,7 +24,7 @@ export const MessageModificationConfirmationDialog: React.FC { - const { t } = useAppTranslation() + const { t } = useTranslation() const isEdit = type === "edit" const title = isEdit ? t("common:confirmation.editMessage") : t("common:confirmation.deleteMessage") diff --git a/webview-ui/src/components/chat/ModeSelector.tsx b/webview-ui/src/components/chat/ModeSelector.tsx index 9e6a5a3a76..a7e07a6402 100644 --- a/webview-ui/src/components/chat/ModeSelector.tsx +++ b/webview-ui/src/components/chat/ModeSelector.tsx @@ -11,7 +11,7 @@ import { telemetryClient } from "@/utils/TelemetryClient" import { cn } from "@/lib/utils" import { enabledSelectorTriggerClassName, selectorTriggerClassName } from "@/components/ui/selectorTriggerStyles" import { useExtensionState } from "@/context/ExtensionStateContext" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { useRooPortal } from "@/components/ui/hooks/useRooPortal" import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui" @@ -50,7 +50,7 @@ export const ModeSelector = ({ const lastNotifiedInvalidModeRef = React.useRef(null) const portalContainer = useRooPortal("roo-portal") const { hasOpenedModeSelector, setHasOpenedModeSelector } = useExtensionState() - const { t } = useAppTranslation() + const { t } = useTranslation() const trackModeSelectorOpened = React.useCallback(() => { // Track telemetry every time the mode selector is opened. diff --git a/webview-ui/src/components/chat/ModelSelector.tsx b/webview-ui/src/components/chat/ModelSelector.tsx index a42e4c9d2a..35f22718d5 100644 --- a/webview-ui/src/components/chat/ModelSelector.tsx +++ b/webview-ui/src/components/chat/ModelSelector.tsx @@ -16,7 +16,7 @@ import { useRooPortal } from "@/components/ui/hooks/useRooPortal" import { useRouterModels } from "@/components/ui/hooks/useRouterModels" import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel" import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { vscode } from "@/utils/vscode" import { useExtensionState } from "@/context/ExtensionStateContext" import { @@ -87,7 +87,7 @@ export const ModelSelector = ({ const [searchValue, setSearchValue] = React.useState("") const searchInputRef = React.useRef(null) const portalContainer = useRooPortal("roo-portal") - const { t } = useAppTranslation() + const { t } = useTranslation() const { organizationAllowList, currentApiConfigName } = useExtensionState() const providerRaw = apiConfiguration?.apiProvider || providerIdentifiers.openrouter diff --git a/webview-ui/src/components/chat/ProfileViolationWarning.tsx b/webview-ui/src/components/chat/ProfileViolationWarning.tsx index fdaaa1bb64..db39a97c76 100644 --- a/webview-ui/src/components/chat/ProfileViolationWarning.tsx +++ b/webview-ui/src/components/chat/ProfileViolationWarning.tsx @@ -1,8 +1,8 @@ import React from "react" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" const ProfileViolationWarning: React.FC = () => { - const { t } = useAppTranslation() + const { t } = useTranslation() return (
diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index c3d0594b99..bccfb22f42 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -234,8 +234,8 @@ const TaskHeader = ({ {t("chat:tokenProgress.tokensUsedLabel")} - {formatLargeNumber(contextTokens || 0)} /{" "} - {formatLargeNumber(contextWindow)} + {formatLargeNumber(contextTokens || 0, t)} /{" "} + {formatLargeNumber(contextWindow, t)} {reservedForOutput > 0 && ( @@ -244,7 +244,7 @@ const TaskHeader = ({ {t("chat:tokenProgress.reservedForResponseLabel")} - {formatLargeNumber(reservedForOutput)} + {formatLargeNumber(reservedForOutput, t)} )} @@ -254,7 +254,7 @@ const TaskHeader = ({ {t("chat:tokenProgress.availableSpaceLabel")} - {formatLargeNumber(availableSpace)} + {formatLargeNumber(availableSpace, t)} )} @@ -389,10 +389,10 @@ const TaskHeader = ({
{typeof tokensIn === "number" && tokensIn > 0 && ( - ↑ {formatLargeNumber(tokensIn)} + ↑ {formatLargeNumber(tokensIn, t)} )} {typeof tokensOut === "number" && tokensOut > 0 && ( - ↓ {formatLargeNumber(tokensOut)} + ↓ {formatLargeNumber(tokensOut, t)} )}
@@ -409,13 +409,13 @@ const TaskHeader = ({ {typeof cacheWrites === "number" && cacheWrites > 0 && ( <> - {formatLargeNumber(cacheWrites)} + {formatLargeNumber(cacheWrites, t)} )} {typeof cacheReads === "number" && cacheReads > 0 && ( <> - {formatLargeNumber(cacheReads)} + {formatLargeNumber(cacheReads, t)} )}
diff --git a/webview-ui/src/components/chat/TodoChangeDisplay.tsx b/webview-ui/src/components/chat/TodoChangeDisplay.tsx index 3904cd9c9f..d9c25ec04f 100644 --- a/webview-ui/src/components/chat/TodoChangeDisplay.tsx +++ b/webview-ui/src/components/chat/TodoChangeDisplay.tsx @@ -1,4 +1,4 @@ -import { t } from "i18next" +import { useTranslation } from "react-i18next" import { ArrowRight, Check, ListChecks, SquareDashed } from "lucide-react" type TodoStatus = "completed" | "in_progress" | "pending" @@ -26,6 +26,7 @@ function getTodoIcon(status: TodoStatus | null) { } export function TodoChangeDisplay({ previousTodos, newTodos }: TodoChangeDisplayProps) { + const { t } = useTranslation() const isInitialState = previousTodos.length === 0 // Determine which todos to display diff --git a/webview-ui/src/components/chat/TodoListDisplay.tsx b/webview-ui/src/components/chat/TodoListDisplay.tsx index f2dbbc4d80..17447c1551 100644 --- a/webview-ui/src/components/chat/TodoListDisplay.tsx +++ b/webview-ui/src/components/chat/TodoListDisplay.tsx @@ -1,5 +1,5 @@ import { cn } from "@/lib/utils" -import { t } from "i18next" +import { useTranslation } from "react-i18next" import { ArrowRight, Check, ListChecks, SquareDashed } from "lucide-react" import { useState, useRef, useMemo, useEffect } from "react" @@ -18,6 +18,7 @@ function getTodoIcon(status: TodoStatus | null) { export function TodoListDisplay({ todos }: { todos: any[] }) { const [isCollapsed, setIsCollapsed] = useState(true) + const { t } = useTranslation() const ulRef = useRef(null) const itemRefs = useRef<(HTMLLIElement | null)[]>([]) const scrollIndex = useMemo(() => { diff --git a/webview-ui/src/components/chat/WarningRow.tsx b/webview-ui/src/components/chat/WarningRow.tsx index 4f18f6a88b..60da153859 100644 --- a/webview-ui/src/components/chat/WarningRow.tsx +++ b/webview-ui/src/components/chat/WarningRow.tsx @@ -1,6 +1,6 @@ import React from "react" import { TriangleAlert, BookOpenText } from "lucide-react" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { vscode } from "@src/utils/vscode" export interface WarningRowProps { @@ -31,7 +31,7 @@ export interface WarningRowProps { * /> */ const WarningRow: React.FC = ({ title, message, docsURL, actionText, onAction }) => { - const { t } = useAppTranslation() + const { t } = useTranslation() return (
diff --git a/webview-ui/src/components/chat/WorktreeSelector.tsx b/webview-ui/src/components/chat/WorktreeSelector.tsx index 0908516d32..cb7294c47f 100644 --- a/webview-ui/src/components/chat/WorktreeSelector.tsx +++ b/webview-ui/src/components/chat/WorktreeSelector.tsx @@ -6,7 +6,7 @@ import type { Worktree, WorktreeListResponse } from "@roo-code/types" import { cn } from "@/lib/utils" import { useRooPortal } from "@/components/ui/hooks/useRooPortal" import { Popover, PopoverContent, PopoverTrigger, StandardTooltip, Button } from "@/components/ui" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { vscode } from "@/utils/vscode" import { CreateWorktreeModal } from "../worktrees/CreateWorktreeModal" @@ -18,7 +18,7 @@ interface WorktreeSelectorProps { } export const WorktreeSelector = ({ disabled = false }: WorktreeSelectorProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [open, setOpen] = useState(false) const [worktrees, setWorktrees] = useState([]) const [isGitRepo, setIsGitRepo] = useState(true) diff --git a/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx b/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx index cb5c6cf69c..a5628981f5 100644 --- a/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx @@ -19,17 +19,7 @@ vi.mock("@roo/package", () => ({ })) vi.mock("react-i18next", () => ({ - Trans: ({ i18nKey, components }: { i18nKey: string; components?: Record }) => { - if (i18nKey === "chat:announcement.support" && components?.githubLink) { - return React.cloneElement(components.githubLink, undefined, "GitHub") - } - - return {i18nKey} - }, -})) - -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ + useTranslation: () => ({ t: (key: string, options?: { version?: string }) => { const translations: Record = { "chat:announcement.release.heading": "What is new in Zoo Code v3.90.0", @@ -48,6 +38,13 @@ vi.mock("@src/i18n/TranslationContext", () => ({ return translations[key] ?? key }, }), + Trans: ({ i18nKey, components }: { i18nKey: string; components?: Record }) => { + if (i18nKey === "chat:announcement.support" && components?.githubLink) { + return React.cloneElement(components.githubLink, undefined, "GitHub") + } + + return {i18nKey} + }, })) describe("Announcement", () => { diff --git a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx index 420249b365..1d9731d66b 100644 --- a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx @@ -11,8 +11,8 @@ vi.mock("@/utils/vscode", () => ({ }, })) -vi.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/chat/__tests__/AutoApproveDropdown.spec.tsx b/webview-ui/src/components/chat/__tests__/AutoApproveDropdown.spec.tsx index 113a088138..b58eaa4411 100644 --- a/webview-ui/src/components/chat/__tests__/AutoApproveDropdown.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/AutoApproveDropdown.spec.tsx @@ -19,8 +19,8 @@ vi.mock("@/context/ExtensionStateContext", () => ({ }), })) -vi.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ t: (key: string) => key }), +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), })) vi.mock("@/hooks/useAutoApprovalToggles", () => ({ diff --git a/webview-ui/src/components/chat/__tests__/BatchFilePermission.spec.tsx b/webview-ui/src/components/chat/__tests__/BatchFilePermission.spec.tsx index 6b2a290c63..9ad0644dc2 100644 --- a/webview-ui/src/components/chat/__tests__/BatchFilePermission.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/BatchFilePermission.spec.tsx @@ -1,7 +1,5 @@ import { render, screen, fireEvent } from "@/utils/test-utils" -import { TranslationProvider } from "@/i18n/__mocks__/TranslationContext" - import { BatchFilePermission } from "../BatchFilePermission" const mockVscodePostMessage = vi.fn() @@ -45,13 +43,7 @@ describe("BatchFilePermission", () => { it("renders file list correctly", () => { render( - - - , + , ) // Check that all files are rendered @@ -67,9 +59,7 @@ describe("BatchFilePermission", () => { it("renders nothing when files array is empty", () => { const { container } = render( - - - , + , ) expect(container.firstChild).toBeNull() @@ -77,9 +67,7 @@ describe("BatchFilePermission", () => { it("renders nothing when onPermissionResponse is not provided", () => { const { container } = render( - - - , + , ) expect(container.firstChild).toBeNull() @@ -87,13 +75,7 @@ describe("BatchFilePermission", () => { it("opens file when clicking on file item", () => { render( - - - , + , ) // The onClick is on the ToolUseBlockHeader which contains the file path text @@ -123,13 +105,11 @@ describe("BatchFilePermission", () => { ] render( - - - , + , ) // Should render dot before the path @@ -139,20 +119,14 @@ describe("BatchFilePermission", () => { it("re-renders when timestamp changes", () => { const { rerender } = render( - - - , + , ) // Initial render expect(screen.getByText(/Button\.tsx/)).toBeInTheDocument() // Re-render with new timestamp - rerender( - - - , - ) + rerender() // Should still show files expect(screen.getByText(/Button\.tsx/)).toBeInTheDocument() @@ -160,13 +134,7 @@ describe("BatchFilePermission", () => { it("displays external link icon for all files", () => { render( - - - , + , ) // All files should have external link icons diff --git a/webview-ui/src/components/chat/__tests__/ChatView.language-switch.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatView.language-switch.spec.tsx new file mode 100644 index 0000000000..86a25cf509 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/ChatView.language-switch.spec.tsx @@ -0,0 +1,111 @@ +// pnpm --filter @roo-code/vscode-webview test src/components/chat/__tests__/ChatView.language-switch.spec.tsx + +import React from "react" +import { renderWithExtensionState, screen, waitFor, act } from "@/utils/test-utils" + +import i18next from "@src/i18n/setup" + +import ChatView, { ChatViewProps } from "../ChatView" + +interface ClineMessage { + type: "say" | "ask" + say?: string + ask?: string + ts: number + text?: string + partial?: boolean +} + +vi.mock("@src/utils/vscode", () => ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +vi.mock("use-sound", () => ({ + default: vi.fn().mockImplementation(() => [vi.fn()]), +})) + +vi.mock("../ChatRow", () => ({ + default: () => null, +})) + +vi.mock("../AutoApproveMenu", () => ({ + default: () => null, +})) + +vi.mock("react-virtuoso", () => ({ + Virtuoso: () => null, +})) + +vi.mock("../../common/VersionIndicator", () => ({ + default: vi.fn(() => null), +})) + +vi.mock("@src/components/welcome/RooTips", () => ({ + default: () => null, +})) + +vi.mock("@src/components/welcome/RooHero", () => ({ + default: () => null, +})) + +vi.mock("../../common/TelemetryBanner", () => ({ + default: () => null, +})) + +const commandAsk: ClineMessage[] = [ + { type: "say", say: "task", ts: 1, text: "Initial task" }, + { type: "ask", ask: "command", ts: 2, text: "echo hi", partial: false }, +] + +const postState = (state: Record) => + act(async () => { + window.postMessage( + { + type: "state", + state: { + version: "1.0.0", + clineMessages: commandAsk, + taskHistory: [], + shouldShowAnnouncement: false, + allowedCommands: [], + alwaysAllowExecute: false, + cloudIsAuthenticated: false, + telemetrySetting: "enabled", + ...state, + }, + }, + "*", + ) + }) + +const defaultProps: ChatViewProps = { + isHidden: false, + showAnnouncement: false, + hideAnnouncement: () => {}, +} + +describe("ChatView approval buttons on language change", () => { + beforeEach(async () => { + await i18next.changeLanguage("en") + }) + + it("relabels an open prompt's buttons in the new language", async () => { + renderWithExtensionState() + + await postState({ language: "en" }) + await waitFor(() => { + expect(screen.getByRole("button", { name: "Run" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Deny" })).toBeInTheDocument() + }) + + // Same messages, so only the language changes; the approval-label effect does not rerun. + await postState({ language: "es" }) + await waitFor(() => { + expect(screen.getByRole("button", { name: "Comando" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Rechazar" })).toBeInTheDocument() + }) + expect(screen.queryByRole("button", { name: "Run" })).not.toBeInTheDocument() + }) +}) diff --git a/webview-ui/src/components/chat/__tests__/CheckpointRestoreDialog.spec.tsx b/webview-ui/src/components/chat/__tests__/CheckpointRestoreDialog.spec.tsx index fc1cb4a77f..e1f718f832 100644 --- a/webview-ui/src/components/chat/__tests__/CheckpointRestoreDialog.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/CheckpointRestoreDialog.spec.tsx @@ -6,9 +6,9 @@ import { vi } from "vitest" import { CheckpointRestoreDialog } from "../CheckpointRestoreDialog" -// Mock the translation context -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +// Mock the translation hook +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => { const translations: Record = { "common:confirmation.deleteMessage": "Delete Message", diff --git a/webview-ui/src/components/chat/__tests__/CommandExecution.spec.tsx b/webview-ui/src/components/chat/__tests__/CommandExecution.spec.tsx index 910d903a43..0301181a2d 100644 --- a/webview-ui/src/components/chat/__tests__/CommandExecution.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/CommandExecution.spec.tsx @@ -5,6 +5,7 @@ import { render, screen, fireEvent, act } from "@testing-library/react" import { CommandExecution } from "../CommandExecution" import { ExtensionStateContext } from "../../../context/ExtensionStateContext" +import { TooltipProvider } from "../../ui/tooltip" // Mock dependencies vi.mock("react-use", () => ({ @@ -51,8 +52,11 @@ const mockExtensionState = { setDeniedCommands: vi.fn(), } +// The status tooltips need a TooltipProvider now that their labels resolve through useTranslation. const ExtensionStateWrapper = ({ children }: { children: React.ReactNode }) => ( - {children} + + {children} + ) describe("CommandExecution", () => { diff --git a/webview-ui/src/components/chat/__tests__/FollowUpSuggest.spec.tsx b/webview-ui/src/components/chat/__tests__/FollowUpSuggest.spec.tsx index 9f437d685b..b275946e33 100644 --- a/webview-ui/src/components/chat/__tests__/FollowUpSuggest.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/FollowUpSuggest.spec.tsx @@ -6,9 +6,8 @@ import type { SuggestionItem } from "@roo-code/types" import { FollowUpSuggest } from "../FollowUpSuggest" // Mock the translation hook -vi.mock("@src/i18n/TranslationContext", () => ({ - TranslationProvider: ({ children }: { children: React.ReactNode }) => children, - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string, options?: any) => { if (key === "chat:followUpSuggest.countdownDisplay" && options?.count !== undefined) { return `${options.count}s` diff --git a/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx b/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx index 25f92757f8..1c8acab519 100644 --- a/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx @@ -20,31 +20,27 @@ vi.mock("@/i18n/setup", () => ({ vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (key: string, params?: any) => { + // Remove namespace prefix if present + const cleanKey = key.includes(":") ? key.split(":")[1] : key + const translations: Record = { "indexingStatus.ready": "Index ready", - "indexingStatus.indexing": params?.progress !== undefined ? `Indexing ${params.progress}%` : "Indexing", + "indexingStatus.indexing": + params?.percentage !== undefined ? `Indexing ${params.percentage}%` : "Indexing", "indexingStatus.error": "Index error", "indexingStatus.indexed": "Indexed", "indexingStatus.tooltip.ready": "The codebase index is ready for use", "indexingStatus.tooltip.indexing": - params?.progress !== undefined - ? `Indexing in progress: ${params.progress}% complete` + params?.percentage !== undefined + ? `Indexing in progress: ${params.percentage}% complete` : "Indexing in progress", "indexingStatus.tooltip.error": "An error occurred during indexing", "indexingStatus.tooltip.indexed": "Codebase has been successfully indexed", "indexingStatus.tooltip.clickToSettings": "Click to open indexing settings", } - return translations[key] || key - }, - i18n: { - language: "en", - changeLanguage: vi.fn(), + return translations[cleanKey] || cleanKey }, }), - initReactI18next: { - type: "3rdParty", - init: vi.fn(), - }, Trans: ({ children }: { children: React.ReactNode }) => <>{children}, })) @@ -67,33 +63,6 @@ vi.mock("@/context/ExtensionStateContext", () => ({ ExtensionStateContextProvider: ({ children }: { children: React.ReactNode }) => <>{children}, })) -// Mock TranslationContext to provide t function directly -vi.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ - t: (key: string, params?: any) => { - // Remove namespace prefix if present - const cleanKey = key.includes(":") ? key.split(":")[1] : key - - const translations: Record = { - "indexingStatus.ready": "Index ready", - "indexingStatus.indexing": - params?.percentage !== undefined ? `Indexing ${params.percentage}%` : "Indexing", - "indexingStatus.error": "Index error", - "indexingStatus.indexed": "Indexed", - "indexingStatus.tooltip.ready": "The codebase index is ready for use", - "indexingStatus.tooltip.indexing": - params?.percentage !== undefined - ? `Indexing in progress: ${params.percentage}% complete` - : "Indexing in progress", - "indexingStatus.tooltip.error": "An error occurred during indexing", - "indexingStatus.tooltip.indexed": "Codebase has been successfully indexed", - "indexingStatus.tooltip.clickToSettings": "Click to open indexing settings", - } - return translations[cleanKey] || cleanKey - }, - }), -})) - describe("IndexingStatusBadge", () => { const renderComponent = (props = {}) => { return render() diff --git a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx index ac8c324efd..aaa3593758 100644 --- a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx @@ -19,8 +19,8 @@ vi.mock("@/context/ExtensionStateContext", () => ({ }), })) -vi.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx index d4dd381015..391225fa5e 100644 --- a/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx @@ -19,8 +19,8 @@ vi.mock("@/context/ExtensionStateContext", () => ({ useExtensionState: () => extensionState, })) -vi.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/chat/__tests__/ThemeSensitiveStatus.visual.fixture.tsx b/webview-ui/src/components/chat/__tests__/ThemeSensitiveStatus.visual.fixture.tsx index ed239f719c..22bc228b65 100644 --- a/webview-ui/src/components/chat/__tests__/ThemeSensitiveStatus.visual.fixture.tsx +++ b/webview-ui/src/components/chat/__tests__/ThemeSensitiveStatus.visual.fixture.tsx @@ -1,43 +1,48 @@ import React from "react" +import i18next from "@/i18n/setup" +import { I18nextProvider } from "react-i18next" + import McpToolRow from "../../mcp/McpToolRow" import { CommandPatternSelector } from "../CommandPatternSelector" export function ThemeSensitiveStatusFixture() { return ( -
-
-

- Command permissions -

- undefined} - onDenyPatternChange={() => undefined} - /> -
+ +
+
+

+ Command permissions +

+ undefined} + onDenyPatternChange={() => undefined} + /> +
-
-

- MCP tools -

- -
-
+
+

+ MCP tools +

+ +
+
+ ) } diff --git a/webview-ui/src/components/chat/__tests__/WarningRow.spec.tsx b/webview-ui/src/components/chat/__tests__/WarningRow.spec.tsx index 1bcf5d771a..d781a9a7fe 100644 --- a/webview-ui/src/components/chat/__tests__/WarningRow.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/WarningRow.spec.tsx @@ -10,9 +10,9 @@ vi.mock("@/utils/vscode", () => ({ }, })) -// Mock i18n TranslationContext -vi.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +// Mock the translation hook +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => { const map: Record = { "chat:apiRequest.errorMessage.docs": "Docs", diff --git a/webview-ui/src/components/chat/__tests__/WorktreeSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/WorktreeSelector.spec.tsx index 106e23ad0f..315f1ca6e2 100644 --- a/webview-ui/src/components/chat/__tests__/WorktreeSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/WorktreeSelector.spec.tsx @@ -12,8 +12,8 @@ vi.mock("@/utils/vscode", () => ({ }, })) -vi.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/common/CodeBlock.tsx b/webview-ui/src/components/common/CodeBlock.tsx index c9c5d85aa0..9ed99dc751 100644 --- a/webview-ui/src/components/common/CodeBlock.tsx +++ b/webview-ui/src/components/common/CodeBlock.tsx @@ -6,7 +6,7 @@ import type { BundledTheme, ShikiTransformer } from "shiki" import { toJsxRuntime } from "hast-util-to-jsx-runtime" import { Fragment, jsx, jsxs } from "react/jsx-runtime" import { ChevronDown, ChevronUp, Copy, Check } from "lucide-react" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { StandardTooltip } from "@/components/ui" export const CODE_BLOCK_BG_COLOR = @@ -194,7 +194,7 @@ const CodeBlock = memo( const preRef = useRef(null) const copyButtonWrapperRef = useRef(null) const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard() - const { t } = useAppTranslation() + const { t } = useTranslation() const isMountedRef = useRef(true) const buttonPositionTimeoutRef = useRef(null) const collapseTimeout1Ref = useRef(null) diff --git a/webview-ui/src/components/common/ImageViewer.tsx b/webview-ui/src/components/common/ImageViewer.tsx index 6c2832d050..56b1d66d0c 100644 --- a/webview-ui/src/components/common/ImageViewer.tsx +++ b/webview-ui/src/components/common/ImageViewer.tsx @@ -1,6 +1,6 @@ import { useState, useCallback } from "react" import { useCopyToClipboard } from "@src/utils/clipboard" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { vscode } from "@src/utils/vscode" import { MermaidActionButtons } from "./MermaidActionButtons" import { Modal } from "./Modal" @@ -35,7 +35,7 @@ export function ImageViewer({ const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 }) const [imageError, setImageError] = useState(null) const { copyWithFeedback } = useCopyToClipboard() - const { t } = useAppTranslation() + const { t } = useTranslation() /** * Opens a modal with the image for zooming diff --git a/webview-ui/src/components/common/MermaidActionButtons.tsx b/webview-ui/src/components/common/MermaidActionButtons.tsx index 79558b9b03..64da63d1af 100644 --- a/webview-ui/src/components/common/MermaidActionButtons.tsx +++ b/webview-ui/src/components/common/MermaidActionButtons.tsx @@ -1,5 +1,5 @@ import React from "react" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { IconButton } from "./IconButton" import { ZoomControls } from "./ZoomControls" import { StandardTooltip } from "@/components/ui" @@ -29,7 +29,7 @@ export const MermaidActionButtons: React.FC = ({ showZoomControls = false, zoomLevel, }) => { - const { t } = useAppTranslation() + const { t } = useTranslation() if (showZoomControls && onZoomOut && onZoomIn && zoomLevel !== undefined) { return ( diff --git a/webview-ui/src/components/common/MermaidBlock.tsx b/webview-ui/src/components/common/MermaidBlock.tsx index 346fe3c471..d673962cd2 100644 --- a/webview-ui/src/components/common/MermaidBlock.tsx +++ b/webview-ui/src/components/common/MermaidBlock.tsx @@ -3,7 +3,7 @@ import mermaid from "mermaid" import styled from "styled-components" import { useDebounceEffect } from "@src/utils/useDebounceEffect" import { vscode } from "@src/utils/vscode" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { useCopyToClipboard } from "@src/utils/clipboard" import CodeBlock from "./CodeBlock" import { MermaidButton } from "@/components/common/MermaidButton" @@ -21,7 +21,7 @@ export default function MermaidBlock({ code }: MermaidBlockProps) { const renderVersionRef = useRef(0) const theme = useMermaidTheme() const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard() - const { t } = useAppTranslation() + const { t } = useTranslation() // Whenever the source or host theme changes, invalidate in-flight rendering. useEffect(() => { diff --git a/webview-ui/src/components/common/MermaidButton.tsx b/webview-ui/src/components/common/MermaidButton.tsx index 8d77502c6d..d2297aa9f0 100644 --- a/webview-ui/src/components/common/MermaidButton.tsx +++ b/webview-ui/src/components/common/MermaidButton.tsx @@ -1,6 +1,6 @@ import { useState, useCallback } from "react" import { useCopyToClipboard } from "@src/utils/clipboard" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { vscode } from "@src/utils/vscode" import { MermaidActionButtons } from "./MermaidActionButtons" import { Modal } from "./Modal" @@ -29,7 +29,7 @@ export function MermaidButton({ containerRef, code, isLoading, svgToPng, childre const [isDragging, setIsDragging] = useState(false) const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 }) const { copyWithFeedback } = useCopyToClipboard() - const { t } = useAppTranslation() + const { t } = useTranslation() /** * Opens a modal with the diagram for zooming diff --git a/webview-ui/src/components/common/TelemetryBanner.tsx b/webview-ui/src/components/common/TelemetryBanner.tsx index 6ed058d8bf..6d5d84073f 100644 --- a/webview-ui/src/components/common/TelemetryBanner.tsx +++ b/webview-ui/src/components/common/TelemetryBanner.tsx @@ -1,14 +1,13 @@ import { memo } from "react" -import { Trans } from "react-i18next" import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react" import type { TelemetrySetting } from "@roo-code/types" import { vscode } from "@src/utils/vscode" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { Trans, useTranslation } from "react-i18next" const TelemetryBanner = () => { - const { t } = useAppTranslation() + const { t } = useTranslation() const handleAccept = () => { vscode.postMessage({ type: "telemetrySetting", text: "enabled" satisfies TelemetrySetting }) @@ -31,6 +30,7 @@ const TelemetryBanner = () => {
{t("welcome:telemetry.helpImprove")}
, diff --git a/webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx b/webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx index 1d6b0fc061..93eb58b934 100644 --- a/webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx +++ b/webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx @@ -5,9 +5,9 @@ import { render, screen, fireEvent, act, waitFor } from "@/utils/test-utils" import CodeBlock, { getCodeBlockTheme } from "../CodeBlock" import { getHighlighter } from "../../../utils/highlighter" -// Mock the translation context -vi.mock("../../../i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +// Mock the translation hook +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => { // Return fixed English strings for tests const translations: { [key: string]: string } = { diff --git a/webview-ui/src/components/common/__tests__/MermaidBlock.spec.tsx b/webview-ui/src/components/common/__tests__/MermaidBlock.spec.tsx index 428e3ae306..171bdcdf92 100644 --- a/webview-ui/src/components/common/__tests__/MermaidBlock.spec.tsx +++ b/webview-ui/src/components/common/__tests__/MermaidBlock.spec.tsx @@ -17,8 +17,8 @@ vi.mock("mermaid", () => ({ }, })) -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ t: (key: string) => key }), +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), })) vi.mock("@src/utils/clipboard", () => ({ diff --git a/webview-ui/src/components/common/__tests__/TelemetryBanner.spec.tsx b/webview-ui/src/components/common/__tests__/TelemetryBanner.spec.tsx index ddffaff20a..a8ecf634d2 100644 --- a/webview-ui/src/components/common/__tests__/TelemetryBanner.spec.tsx +++ b/webview-ui/src/components/common/__tests__/TelemetryBanner.spec.tsx @@ -10,8 +10,9 @@ vi.mock("@src/utils/vscode", () => ({ }, })) -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", async (importOriginal) => ({ + ...(await importOriginal()), + useTranslation: () => ({ t: (key: string) => { const translations: Record = { "welcome:telemetry.helpImprove": "Help Improve Zoo Code", diff --git a/webview-ui/src/components/common/__tests__/TelemetryBanner.visual.fixture.tsx b/webview-ui/src/components/common/__tests__/TelemetryBanner.visual.fixture.tsx index 0df2d5b226..e243cb51e0 100644 --- a/webview-ui/src/components/common/__tests__/TelemetryBanner.visual.fixture.tsx +++ b/webview-ui/src/components/common/__tests__/TelemetryBanner.visual.fixture.tsx @@ -1,18 +1,11 @@ import React from "react" import { I18nextProvider } from "react-i18next" -import { TranslationContext } from "@src/i18n/TranslationContext" import TelemetryBanner from "../TelemetryBanner" -import { visualTestI18n, visualTestTranslations } from "./TelemetryBanner.visual.i18n" +import { visualTestI18n } from "./TelemetryBanner.visual.i18n" export const TelemetryBannerFixture = () => ( - visualTestTranslations[key] ?? key, - i18n: null as unknown as typeof import("../../../i18n/setup").default, - }}> - - + ) diff --git a/webview-ui/src/components/common/__tests__/TelemetryBanner.visual.i18n.ts b/webview-ui/src/components/common/__tests__/TelemetryBanner.visual.i18n.ts index 95e7a8d1b6..ecf602d285 100644 --- a/webview-ui/src/components/common/__tests__/TelemetryBanner.visual.i18n.ts +++ b/webview-ui/src/components/common/__tests__/TelemetryBanner.visual.i18n.ts @@ -9,10 +9,9 @@ export const visualTestTranslations: Record = { "welcome:telemetry.decline": "Decline", } -// Trans reads from its own react-i18next instance rather than the useAppTranslation -// context, so it needs a real (if minimal) i18next init to resolve helpImproveMessage -// and the settingsLink interpolation instead of rendering nothing. init() returns a -// promise even for inline resources, so callers must await it before mounting. +// The fixture passes this instance to I18nextProvider; TelemetryBanner's useTranslation and Trans +// translate with it. init() returns a promise even for inline resources, so callers must await it +// before mounting. export const visualTestI18n = i18next.createInstance() export const visualTestI18nReady = visualTestI18n.use(initReactI18next).init({ diff --git a/webview-ui/src/components/history/BatchDeleteTaskDialog.tsx b/webview-ui/src/components/history/BatchDeleteTaskDialog.tsx index decc905315..0ae55114fe 100644 --- a/webview-ui/src/components/history/BatchDeleteTaskDialog.tsx +++ b/webview-ui/src/components/history/BatchDeleteTaskDialog.tsx @@ -1,5 +1,5 @@ import { useCallback } from "react" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { AlertDialog, AlertDialogAction, @@ -19,7 +19,7 @@ interface BatchDeleteTaskDialogProps extends AlertDialogProps { } export const BatchDeleteTaskDialog = ({ taskIds, ...props }: BatchDeleteTaskDialogProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const { onOpenChange } = props const onDelete = useCallback(() => { diff --git a/webview-ui/src/components/history/CopyButton.tsx b/webview-ui/src/components/history/CopyButton.tsx index 4243ff8d5a..c71e90c725 100644 --- a/webview-ui/src/components/history/CopyButton.tsx +++ b/webview-ui/src/components/history/CopyButton.tsx @@ -2,7 +2,7 @@ import { useCallback } from "react" import { useClipboard } from "@/components/ui/hooks" import { Button, StandardTooltip } from "@/components/ui" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { cn } from "@/lib/utils" type CopyButtonProps = { @@ -11,7 +11,7 @@ type CopyButtonProps = { export const CopyButton = ({ itemTask }: CopyButtonProps) => { const { isCopied, copy } = useClipboard() - const { t } = useAppTranslation() + const { t } = useTranslation() const onCopy = useCallback( (e: React.MouseEvent) => { diff --git a/webview-ui/src/components/history/DeleteButton.tsx b/webview-ui/src/components/history/DeleteButton.tsx index 8378887c70..775dc8ce81 100644 --- a/webview-ui/src/components/history/DeleteButton.tsx +++ b/webview-ui/src/components/history/DeleteButton.tsx @@ -1,7 +1,7 @@ import { useCallback } from "react" import { Button, StandardTooltip } from "@/components/ui" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { vscode } from "@/utils/vscode" type DeleteButtonProps = { @@ -10,7 +10,7 @@ type DeleteButtonProps = { } export const DeleteButton = ({ itemId, onDelete }: DeleteButtonProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const handleDeleteClick = useCallback( (e: React.MouseEvent) => { diff --git a/webview-ui/src/components/history/DeleteTaskDialog.tsx b/webview-ui/src/components/history/DeleteTaskDialog.tsx index 5ff93f4ed3..8fc19485d3 100644 --- a/webview-ui/src/components/history/DeleteTaskDialog.tsx +++ b/webview-ui/src/components/history/DeleteTaskDialog.tsx @@ -13,7 +13,7 @@ import { AlertDialogTitle, Button, } from "@/components/ui" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { vscode } from "@/utils/vscode" @@ -24,7 +24,7 @@ interface DeleteTaskDialogProps extends AlertDialogProps { } export const DeleteTaskDialog = ({ taskId, subtaskCount = 0, ...props }: DeleteTaskDialogProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [isEnterPressed] = useKeyPress("Enter") const { onOpenChange } = props diff --git a/webview-ui/src/components/history/ExportButton.tsx b/webview-ui/src/components/history/ExportButton.tsx index fabc8d3d15..da49a6b15d 100644 --- a/webview-ui/src/components/history/ExportButton.tsx +++ b/webview-ui/src/components/history/ExportButton.tsx @@ -1,10 +1,10 @@ import { vscode } from "@/utils/vscode" import { Button, StandardTooltip } from "@/components/ui" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { useCallback } from "react" export const ExportButton = ({ itemId }: { itemId: string }) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const handleExportClick = useCallback( (e: React.MouseEvent) => { diff --git a/webview-ui/src/components/history/HistoryPreview.tsx b/webview-ui/src/components/history/HistoryPreview.tsx index 70467c44fb..7543e99dc3 100644 --- a/webview-ui/src/components/history/HistoryPreview.tsx +++ b/webview-ui/src/components/history/HistoryPreview.tsx @@ -1,7 +1,7 @@ import { memo } from "react" import { vscode } from "@src/utils/vscode" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { useTaskSearch } from "./useTaskSearch" import { useGroupedTasks } from "./useGroupedTasks" @@ -10,7 +10,7 @@ import TaskGroupItem from "./TaskGroupItem" const HistoryPreview = () => { const { tasks, searchQuery } = useTaskSearch() const { groups, toggleExpand } = useGroupedTasks(tasks, searchQuery) - const { t } = useAppTranslation() + const { t } = useTranslation() const handleViewAllHistory = () => { vscode.postMessage({ type: "switchTab", tab: "history" }) diff --git a/webview-ui/src/components/history/HistoryView.tsx b/webview-ui/src/components/history/HistoryView.tsx index 54226ca40b..ab30436206 100644 --- a/webview-ui/src/components/history/HistoryView.tsx +++ b/webview-ui/src/components/history/HistoryView.tsx @@ -16,7 +16,7 @@ import { SelectValue, StandardTooltip, } from "@/components/ui" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { Tab, TabContent, TabHeader } from "../common/Tab" import { useTaskSearch } from "./useTaskSearch" @@ -42,7 +42,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => { showAllWorkspaces, setShowAllWorkspaces, } = useTaskSearch() - const { t } = useAppTranslation() + const { t } = useTranslation() // Use grouped tasks hook const { groups, flatTasks, toggleExpand, isSearchMode } = useGroupedTasks(tasks, searchQuery) diff --git a/webview-ui/src/components/history/RenameButton.tsx b/webview-ui/src/components/history/RenameButton.tsx index bd834525d2..c33535a7dc 100644 --- a/webview-ui/src/components/history/RenameButton.tsx +++ b/webview-ui/src/components/history/RenameButton.tsx @@ -1,12 +1,12 @@ import { Button, StandardTooltip } from "@/components/ui" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" type RenameButtonProps = { onRename: () => void } export const RenameButton = ({ onRename }: RenameButtonProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const label = t("history:renameTask") return ( diff --git a/webview-ui/src/components/history/SubtaskCollapsibleRow.tsx b/webview-ui/src/components/history/SubtaskCollapsibleRow.tsx index 6e4c74c952..da17f24573 100644 --- a/webview-ui/src/components/history/SubtaskCollapsibleRow.tsx +++ b/webview-ui/src/components/history/SubtaskCollapsibleRow.tsx @@ -1,6 +1,6 @@ import { memo } from "react" import { ChevronRight } from "lucide-react" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { cn } from "@/lib/utils" interface SubtaskCollapsibleRowProps { @@ -19,7 +19,7 @@ interface SubtaskCollapsibleRowProps { * Clicking this row toggles the visibility of the subtask list. */ const SubtaskCollapsibleRow = ({ count, isExpanded, onToggle, className }: SubtaskCollapsibleRowProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() if (count === 0) { return null diff --git a/webview-ui/src/components/history/TaskItemFooter.tsx b/webview-ui/src/components/history/TaskItemFooter.tsx index 5d75822f4c..1bfc35ee68 100644 --- a/webview-ui/src/components/history/TaskItemFooter.tsx +++ b/webview-ui/src/components/history/TaskItemFooter.tsx @@ -6,7 +6,7 @@ import { ExportButton } from "./ExportButton" import { DeleteButton } from "./DeleteButton" import { RenameButton } from "./RenameButton" import { StandardTooltip } from "../ui/standard-tooltip" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { Split } from "lucide-react" import { TaskStatusBadge } from "./TaskStatusBadge" @@ -27,7 +27,7 @@ const TaskItemFooter: React.FC = ({ onDelete, onRename, }) => { - const { t } = useAppTranslation() + const { t } = useTranslation() return (
@@ -49,7 +49,7 @@ const TaskItemFooter: React.FC = ({ )} {/* Datetime with time-ago format */} - {formatTimeAgo(item.ts)} + {formatTimeAgo(item.ts, t)} {/* Cost */} diff --git a/webview-ui/src/components/history/TaskStatusBadge.tsx b/webview-ui/src/components/history/TaskStatusBadge.tsx index 5278f1422e..1d8951c4d1 100644 --- a/webview-ui/src/components/history/TaskStatusBadge.tsx +++ b/webview-ui/src/components/history/TaskStatusBadge.tsx @@ -1,4 +1,4 @@ -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { cn } from "@/lib/utils" import { StandardTooltip } from "../ui" @@ -13,7 +13,7 @@ interface TaskStatusBadgeProps { * can be resumed rather than silently detached. See #559. */ export const TaskStatusBadge = ({ status, className }: TaskStatusBadgeProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const isInterrupted = status === "interrupted" const icon = isInterrupted ? "codicon-warning" : "codicon-sync" diff --git a/webview-ui/src/components/history/TaskTitleEditor.tsx b/webview-ui/src/components/history/TaskTitleEditor.tsx index c02e044311..67a10b0253 100644 --- a/webview-ui/src/components/history/TaskTitleEditor.tsx +++ b/webview-ui/src/components/history/TaskTitleEditor.tsx @@ -2,7 +2,7 @@ import { useCallback, useState } from "react" import type { HistoryItem } from "@roo-code/types" import { AutosizeTextarea } from "@/components/ui" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { cn } from "@/lib/utils" import { vscode } from "@/utils/vscode" @@ -24,7 +24,7 @@ const normalizeTitle = (text: string) => text.replace(/\s+/g, " ").trim() * It wraps instead of scrolling sideways so a long title stays readable at narrow widths. */ export const TaskTitleEditor = ({ item, onDone, className }: TaskTitleEditorProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [value, setValue] = useState(() => normalizeTitle(getTaskDisplayTitle(item))) const save = useCallback(() => { diff --git a/webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.spec.tsx b/webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.spec.tsx index bdcff23cdd..dc9bcd6765 100644 --- a/webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.spec.tsx +++ b/webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.spec.tsx @@ -6,8 +6,8 @@ import { BatchDeleteTaskDialog } from "../BatchDeleteTaskDialog" vi.mock("@/utils/vscode") -vi.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string, options?: Record) => { const translations: Record = { "history:deleteTasks": "Delete Tasks", diff --git a/webview-ui/src/components/history/__tests__/CopyButton.spec.tsx b/webview-ui/src/components/history/__tests__/CopyButton.spec.tsx index ac1b39859d..0e9a048ac8 100644 --- a/webview-ui/src/components/history/__tests__/CopyButton.spec.tsx +++ b/webview-ui/src/components/history/__tests__/CopyButton.spec.tsx @@ -5,8 +5,8 @@ import { useClipboard } from "@/components/ui/hooks" import { CopyButton } from "../CopyButton" vi.mock("@/components/ui/hooks") -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/history/__tests__/DeleteButton.spec.tsx b/webview-ui/src/components/history/__tests__/DeleteButton.spec.tsx index 19b333ab44..7dbca11059 100644 --- a/webview-ui/src/components/history/__tests__/DeleteButton.spec.tsx +++ b/webview-ui/src/components/history/__tests__/DeleteButton.spec.tsx @@ -2,8 +2,8 @@ import { render, screen, fireEvent } from "@/utils/test-utils" import { DeleteButton } from "../DeleteButton" -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/history/__tests__/DeleteTaskDialog.spec.tsx b/webview-ui/src/components/history/__tests__/DeleteTaskDialog.spec.tsx index 1a43dbb4c0..d4a3762151 100644 --- a/webview-ui/src/components/history/__tests__/DeleteTaskDialog.spec.tsx +++ b/webview-ui/src/components/history/__tests__/DeleteTaskDialog.spec.tsx @@ -6,8 +6,8 @@ import { DeleteTaskDialog } from "../DeleteTaskDialog" vi.mock("@/utils/vscode") -vi.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string, options?: Record) => { const translations: Record = { "history:deleteTask": "Delete Task", diff --git a/webview-ui/src/components/history/__tests__/ExportButton.spec.tsx b/webview-ui/src/components/history/__tests__/ExportButton.spec.tsx index 1dda83305c..70752d2a34 100644 --- a/webview-ui/src/components/history/__tests__/ExportButton.spec.tsx +++ b/webview-ui/src/components/history/__tests__/ExportButton.spec.tsx @@ -6,8 +6,8 @@ import { ExportButton } from "../ExportButton" vi.mock("@src/utils/vscode") -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/history/__tests__/HistoryView.spec.tsx b/webview-ui/src/components/history/__tests__/HistoryView.spec.tsx index 3079844aad..f8f1ac9a59 100644 --- a/webview-ui/src/components/history/__tests__/HistoryView.spec.tsx +++ b/webview-ui/src/components/history/__tests__/HistoryView.spec.tsx @@ -7,8 +7,8 @@ import HistoryView from "../HistoryView" vi.mock("@src/context/ExtensionStateContext") vi.mock("@src/utils/vscode") -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/history/__tests__/RenameButton.spec.tsx b/webview-ui/src/components/history/__tests__/RenameButton.spec.tsx index 2bc837ee12..873fbe6134 100644 --- a/webview-ui/src/components/history/__tests__/RenameButton.spec.tsx +++ b/webview-ui/src/components/history/__tests__/RenameButton.spec.tsx @@ -2,8 +2,8 @@ import { render, screen, fireEvent } from "@/utils/test-utils" import { RenameButton } from "../RenameButton" -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/history/__tests__/SubtaskRow.spec.tsx b/webview-ui/src/components/history/__tests__/SubtaskRow.spec.tsx index 4fb609081f..ef86b12b60 100644 --- a/webview-ui/src/components/history/__tests__/SubtaskRow.spec.tsx +++ b/webview-ui/src/components/history/__tests__/SubtaskRow.spec.tsx @@ -6,8 +6,8 @@ import SubtaskRow from "../SubtaskRow" import type { SubtaskTreeNode, DisplayHistoryItem } from "../types" vi.mock("@src/utils/vscode") -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string, options?: Record) => { if (key === "history:subtasks" && options?.count !== undefined) { return `${options.count} Subtask${options.count === 1 ? "" : "s"}` diff --git a/webview-ui/src/components/history/__tests__/TaskGroupItem.spec.tsx b/webview-ui/src/components/history/__tests__/TaskGroupItem.spec.tsx index b04fac6b54..99f55978d7 100644 --- a/webview-ui/src/components/history/__tests__/TaskGroupItem.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskGroupItem.spec.tsx @@ -4,8 +4,8 @@ import TaskGroupItem from "../TaskGroupItem" import type { TaskGroup, DisplayHistoryItem, SubtaskTreeNode } from "../types" vi.mock("@src/utils/vscode") -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string, options?: Record) => { if (key === "history:subtasks" && options?.count !== undefined) { return `${options.count} Subtask${options.count === 1 ? "" : "s"}` diff --git a/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx b/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx index 4fb4a82d72..5bc178cc88 100644 --- a/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx @@ -5,8 +5,8 @@ import { vscode } from "@src/utils/vscode" import TaskItem from "../TaskItem" vi.mock("@src/utils/vscode") -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx b/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx index 83047dc0b4..acea93e5fe 100644 --- a/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx @@ -2,8 +2,8 @@ import { render, screen } from "@/utils/test-utils" import TaskItemFooter from "../TaskItemFooter" -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/history/__tests__/TaskTitleEditor.spec.tsx b/webview-ui/src/components/history/__tests__/TaskTitleEditor.spec.tsx index 951dc85ac8..05d885b643 100644 --- a/webview-ui/src/components/history/__tests__/TaskTitleEditor.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskTitleEditor.spec.tsx @@ -6,8 +6,8 @@ import { TaskTitleEditor } from "../TaskTitleEditor" vi.mock("@src/utils/vscode") -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-dark.png b/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-dark.png index 2bff5fbfaf..3850a8dd8c 100644 Binary files a/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-dark.png and b/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-dark.png differ diff --git a/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-high-contrast-light.png b/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-high-contrast-light.png index 1174b66e5b..ebc45d8371 100644 Binary files a/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-high-contrast-light.png and b/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-high-contrast-light.png differ diff --git a/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-high-contrast.png b/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-high-contrast.png index 75cbf52222..7590760d69 100644 Binary files a/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-high-contrast.png and b/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-high-contrast.png differ diff --git a/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-light.png b/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-light.png index 7190f6b6c8..db93d20137 100644 Binary files a/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-light.png and b/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-light.png differ diff --git a/webview-ui/src/components/marketplace/IssueFooter.tsx b/webview-ui/src/components/marketplace/IssueFooter.tsx index a40cefe5ad..53181531c5 100644 --- a/webview-ui/src/components/marketplace/IssueFooter.tsx +++ b/webview-ui/src/components/marketplace/IssueFooter.tsx @@ -1,13 +1,14 @@ import React from "react" -import { Trans } from "react-i18next" +import { Trans, useTranslation } from "react-i18next" import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" import { EXTERNAL_LINKS } from "@/constants/externalLinks" export const IssueFooter: React.FC = () => { + const { t } = useTranslation() return (
- + Open a GitHub issue diff --git a/webview-ui/src/components/marketplace/MarketplaceListView.tsx b/webview-ui/src/components/marketplace/MarketplaceListView.tsx index c3c497ccfe..12249d8dc8 100644 --- a/webview-ui/src/components/marketplace/MarketplaceListView.tsx +++ b/webview-ui/src/components/marketplace/MarketplaceListView.tsx @@ -7,7 +7,7 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command import { X, ChevronsUpDown } from "lucide-react" import { MarketplaceItemCard } from "./components/MarketplaceItemCard" import { MarketplaceViewStateManager } from "./MarketplaceViewStateManager" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { useStateManager } from "./useStateManager" import { useExtensionState } from "@/context/ExtensionStateContext" import { IssueFooter } from "./IssueFooter" @@ -21,7 +21,7 @@ export interface MarketplaceListViewProps { export function MarketplaceListView({ stateManager, allTags, filteredTags, filterByType }: MarketplaceListViewProps) { const [state, manager] = useStateManager(stateManager) - const { t } = useAppTranslation() + const { t } = useTranslation() const { marketplaceInstalledMetadata, cloudUserInfo } = useExtensionState() const [isTagPopoverOpen, setIsTagPopoverOpen] = React.useState(false) const [tagSearch, setTagSearch] = React.useState("") diff --git a/webview-ui/src/components/marketplace/MarketplaceView.tsx b/webview-ui/src/components/marketplace/MarketplaceView.tsx index 0ab5430eec..b0efb6d3dc 100644 --- a/webview-ui/src/components/marketplace/MarketplaceView.tsx +++ b/webview-ui/src/components/marketplace/MarketplaceView.tsx @@ -4,7 +4,7 @@ import { ArrowLeft } from "lucide-react" import { Tab, TabContent, TabHeader } from "../common/Tab" import { MarketplaceViewStateManager } from "./MarketplaceViewStateManager" import { useStateManager } from "./useStateManager" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { vscode } from "@/utils/vscode" import { MarketplaceListView } from "./MarketplaceListView" import { cn } from "@/lib/utils" @@ -17,7 +17,7 @@ interface MarketplaceViewProps { targetTab?: "mcp" | "mode" } export function MarketplaceView({ stateManager, onDone, targetTab }: MarketplaceViewProps) { - const { t } = useAppTranslation() + const { t } = useTranslation() const [state, manager] = useStateManager(stateManager) const [hasReceivedInitialState, setHasReceivedInitialState] = useState(false) const extensionState = useContext(ExtensionStateContext) diff --git a/webview-ui/src/components/marketplace/__tests__/IssueFooter.spec.tsx b/webview-ui/src/components/marketplace/__tests__/IssueFooter.spec.tsx index ab1ed39860..b04139679c 100644 --- a/webview-ui/src/components/marketplace/__tests__/IssueFooter.spec.tsx +++ b/webview-ui/src/components/marketplace/__tests__/IssueFooter.spec.tsx @@ -14,6 +14,7 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({ })) vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), Trans: ({ children }: { children: React.ReactNode }) => <>{children}, })) diff --git a/webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx b/webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx index 02f94001d1..135fbcea76 100644 --- a/webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx +++ b/webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx @@ -9,8 +9,9 @@ import { ExtensionStateContextProvider } from "@/context/ExtensionStateContext" import { MarketplaceListView } from "../MarketplaceListView" import { ViewState } from "../MarketplaceViewStateManager" -vi.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", async (importOriginal) => ({ + ...(await importOriginal()), + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/marketplace/__tests__/MarketplaceView.spec.tsx b/webview-ui/src/components/marketplace/__tests__/MarketplaceView.spec.tsx index 57fc62ece0..50ecc76bd8 100644 --- a/webview-ui/src/components/marketplace/__tests__/MarketplaceView.spec.tsx +++ b/webview-ui/src/components/marketplace/__tests__/MarketplaceView.spec.tsx @@ -13,8 +13,9 @@ vi.mock("@/utils/vscode", () => ({ }, })) -vi.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", async (importOriginal) => ({ + ...(await importOriginal()), + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx b/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx index 7556630c7b..167916b414 100644 --- a/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx +++ b/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx @@ -1,7 +1,7 @@ import React, { useState, useMemo, useEffect } from "react" import { MarketplaceItem, McpParameter, McpInstallationMethod } from "@roo-code/types" import { vscode } from "@/utils/vscode" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { Dialog, DialogContent, @@ -27,7 +27,7 @@ export const MarketplaceInstallModal: React.FC = ( onClose, hasWorkspace, }) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [scope, setScope] = useState<"project" | "global">(hasWorkspace ? "project" : "global") const [selectedMethodIndex, setSelectedMethodIndex] = useState(0) const [parameterValues, setParameterValues] = useState>({}) diff --git a/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx b/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx index 3b8df52176..ec2d4f53e7 100644 --- a/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx +++ b/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx @@ -3,7 +3,7 @@ import { MarketplaceItem, TelemetryEventName } from "@roo-code/types" import { vscode } from "@/utils/vscode" import { telemetryClient } from "@/utils/TelemetryClient" import { ViewState } from "../MarketplaceViewStateManager" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { isValidUrl } from "../../../utils/url" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" @@ -36,7 +36,7 @@ interface MarketplaceItemCardProps { } export const MarketplaceItemCard: React.FC = ({ item, filters, setFilters, installed }) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const { cwd } = useExtensionState() const [showInstallModal, setShowInstallModal] = useState(false) const [showRemoveConfirm, setShowRemoveConfirm] = useState(false) @@ -172,7 +172,7 @@ export const MarketplaceItemCard: React.FC = ({ item, key={tag} content={ filters.tags.includes(tag) - ? t("marketplace:filters.tags.clear", { count: tag }) + ? t("marketplace:filters.tags.clear") : t("marketplace:filters.tags.clickToFilter") }>
- + { />
{

{t("prompts:globalCustomInstructions.title")}

- + { />
& { } export const About = ({ telemetrySetting, setTelemetrySetting, debug, setDebug, className, ...props }: AboutProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [rooHistoryImportProgress, setRooHistoryImportProgress] = useState(null) useEffect(() => { @@ -117,6 +116,7 @@ export const About = ({ telemetrySetting, setTelemetrySetting, debug, setDebug,

, @@ -160,6 +160,7 @@ export const About = ({ telemetrySetting, setTelemetrySetting, debug, setDebug, , diff --git a/webview-ui/src/components/settings/ApiConfigManager.tsx b/webview-ui/src/components/settings/ApiConfigManager.tsx index d57cd004a9..898d9a20d9 100644 --- a/webview-ui/src/components/settings/ApiConfigManager.tsx +++ b/webview-ui/src/components/settings/ApiConfigManager.tsx @@ -4,7 +4,7 @@ import { TriangleAlert } from "lucide-react" import type { ProviderSettingsEntry, OrganizationAllowList } from "@roo-code/types" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { type SearchableSelectOption, Button, @@ -35,7 +35,7 @@ const ApiConfigManager = ({ onRenameConfig, onUpsertConfig, }: ApiConfigManagerProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [isRenaming, setIsRenaming] = useState(false) const [isCreating, setIsCreating] = useState(false) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index ab7457915e..427fb6ed5f 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -33,7 +33,7 @@ import { isTemperatureControlSupported } from "./utils/temperatureSupport" import { vscode } from "@src/utils/vscode" import { validateApiConfigurationExcludingModelErrors, getModelValidationError } from "@src/utils/validate" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { useRouterModels } from "@src/components/ui/hooks/useRouterModels" import { useZooGatewayRouterModelsSync } from "@src/components/ui/hooks/useZooGatewayRouterModelsSync" import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel" @@ -129,7 +129,7 @@ const ApiOptions = ({ errorMessage, setErrorMessage, }: ApiOptionsProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const { organizationAllowList, openAiCodexIsAuthenticated, kimiCodeIsAuthenticated, kimiCodeOAuthState } = useExtensionState() @@ -308,11 +308,12 @@ const ApiOptions = ({ const apiValidationResult = validateApiConfigurationExcludingModelErrors( apiConfiguration, + t, routerModels, organizationAllowList, ) setErrorMessage(apiValidationResult) - }, [apiConfiguration, routerModels, organizationAllowList, setErrorMessage, isRetiredSelectedProvider]) + }, [apiConfiguration, t, routerModels, organizationAllowList, setErrorMessage, isRetiredSelectedProvider]) const onProviderChange = useCallback( (value: ProviderName) => { @@ -378,8 +379,8 @@ const ApiOptions = ({ ) const modelValidationError = useMemo(() => { - return getModelValidationError(apiConfiguration, routerModels, organizationAllowList) - }, [apiConfiguration, routerModels, organizationAllowList]) + return getModelValidationError(apiConfiguration, t, routerModels, organizationAllowList) + }, [apiConfiguration, t, routerModels, organizationAllowList]) const docs = useMemo(() => { const provider = PROVIDERS.find(({ value }) => value === selectedProvider) diff --git a/webview-ui/src/components/settings/AutoApproveSettings.tsx b/webview-ui/src/components/settings/AutoApproveSettings.tsx index 58034960fb..7161c0ae1f 100644 --- a/webview-ui/src/components/settings/AutoApproveSettings.tsx +++ b/webview-ui/src/components/settings/AutoApproveSettings.tsx @@ -1,9 +1,8 @@ import { FormEvent, HTMLAttributes, useState } from "react" import { X } from "lucide-react" -import { Trans } from "react-i18next" import { Package } from "@roo/package" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { Trans, useTranslation } from "react-i18next" import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" import { vscode } from "@/utils/vscode" import { Button, Input, Slider } from "@/components/ui" @@ -92,7 +91,7 @@ export const AutoApproveSettings = ({ setCachedStateField, ...props }: AutoApproveSettingsProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [commandInput, setCommandInput] = useState("") const [deniedCommandInput, setDeniedCommandInput] = useState("") const { autoApprovalEnabled, setAutoApprovalEnabled } = useExtensionState() @@ -145,6 +144,7 @@ export const AutoApproveSettings = ({

{t("settings:autoApprove.description")}

{ - const { t } = useAppTranslation() + const { t } = useTranslation() return (

diff --git a/webview-ui/src/components/settings/CheckpointSettings.tsx b/webview-ui/src/components/settings/CheckpointSettings.tsx index dd28f6615f..cb22b52e9c 100644 --- a/webview-ui/src/components/settings/CheckpointSettings.tsx +++ b/webview-ui/src/components/settings/CheckpointSettings.tsx @@ -1,7 +1,6 @@ import { HTMLAttributes } from "react" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { Trans, useTranslation } from "react-i18next" import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react" -import { Trans } from "react-i18next" import { buildDocLink } from "@src/utils/docLinks" import { Slider } from "@/components/ui" @@ -27,7 +26,7 @@ export const CheckpointSettings = ({ setCachedStateField, ...props }: CheckpointSettingsProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() return (
{t("settings:sections.checkpoints")} @@ -45,7 +44,7 @@ export const CheckpointSettings = ({ {t("settings:checkpoints.enable.label")}
- + diff --git a/webview-ui/src/components/settings/ConsecutiveMistakeLimitControl.tsx b/webview-ui/src/components/settings/ConsecutiveMistakeLimitControl.tsx index 633ff8be8d..2035ae5559 100644 --- a/webview-ui/src/components/settings/ConsecutiveMistakeLimitControl.tsx +++ b/webview-ui/src/components/settings/ConsecutiveMistakeLimitControl.tsx @@ -1,6 +1,6 @@ import { DEFAULT_CONSECUTIVE_MISTAKE_LIMIT } from "@roo-code/types" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { Slider } from "@/components/ui" @@ -10,7 +10,7 @@ interface ConsecutiveMistakeLimitControlProps { } export const ConsecutiveMistakeLimitControl = ({ value, onChange }: ConsecutiveMistakeLimitControlProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() return (
diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx index c3af315d99..68894ca805 100644 --- a/webview-ui/src/components/settings/ContextManagementSettings.tsx +++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx @@ -1,6 +1,6 @@ import { HTMLAttributes } from "react" import React from "react" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { VSCodeCheckbox, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react" import { ListChevronsDownUp } from "lucide-react" import { DEFAULT_DIFF_FUZZY_THRESHOLD } from "@roo-code/types" @@ -89,7 +89,7 @@ export const ContextManagementSettings = ({ className, ...props }: ContextManagementSettingsProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [selectedThresholdProfile, setSelectedThresholdProfile] = React.useState("default") // Helper function to get the CONDENSE prompt value diff --git a/webview-ui/src/components/settings/CreateRuleDialog.tsx b/webview-ui/src/components/settings/CreateRuleDialog.tsx index fb3b65b84f..bd122d25c9 100644 --- a/webview-ui/src/components/settings/CreateRuleDialog.tsx +++ b/webview-ui/src/components/settings/CreateRuleDialog.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useMemo, useState } from "react" import { getAllModes } from "@roo/modes" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { useExtensionState } from "@/context/ExtensionStateContext" import { Button, @@ -41,7 +41,7 @@ export const CreateRuleDialog: React.FC = ({ onRuleCreated, hasWorkspace, }) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const { customModes } = useExtensionState() const [name, setName] = useState("") diff --git a/webview-ui/src/components/settings/CreateSkillDialog.tsx b/webview-ui/src/components/settings/CreateSkillDialog.tsx index 3a8def14ee..cc1b3b9179 100644 --- a/webview-ui/src/components/settings/CreateSkillDialog.tsx +++ b/webview-ui/src/components/settings/CreateSkillDialog.tsx @@ -3,7 +3,7 @@ import { validateSkillName as validateSkillNameShared, SkillNameValidationError import { getAllModes } from "@roo/modes" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { useExtensionState } from "@/context/ExtensionStateContext" import { Button, @@ -74,7 +74,7 @@ export const CreateSkillDialog: React.FC = ({ onSkillCreated, hasWorkspace, }) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const { customModes } = useExtensionState() const [name, setName] = useState("") diff --git a/webview-ui/src/components/settings/CreateSlashCommandDialog.tsx b/webview-ui/src/components/settings/CreateSlashCommandDialog.tsx index 343db2db73..2c1de3f9bc 100644 --- a/webview-ui/src/components/settings/CreateSlashCommandDialog.tsx +++ b/webview-ui/src/components/settings/CreateSlashCommandDialog.tsx @@ -1,6 +1,6 @@ import React, { useState, useCallback } from "react" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { Button, Dialog, @@ -45,7 +45,7 @@ export const CreateSlashCommandDialog: React.FC = onCommandCreated, hasWorkspace, }) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [name, setName] = useState("") const [source, setSource] = useState<"global" | "project">(hasWorkspace ? "project" : "global") diff --git a/webview-ui/src/components/settings/CustomToolsSettings.tsx b/webview-ui/src/components/settings/CustomToolsSettings.tsx index 03bff7fc8a..6b7a7a5773 100644 --- a/webview-ui/src/components/settings/CustomToolsSettings.tsx +++ b/webview-ui/src/components/settings/CustomToolsSettings.tsx @@ -5,7 +5,7 @@ import { RefreshCw, Loader2, FileCode } from "lucide-react" import type { SerializedCustomToolDefinition } from "@roo-code/types" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { vscode } from "@/utils/vscode" @@ -31,7 +31,7 @@ interface CustomToolsSettingsProps { } export const CustomToolsSettings = ({ enabled, onChange }: CustomToolsSettingsProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [tools, setTools] = useState([]) const [isRefreshing, setIsRefreshing] = useState(false) const [refreshError, setRefreshError] = useState(null) diff --git a/webview-ui/src/components/settings/ExperimentalFeature.tsx b/webview-ui/src/components/settings/ExperimentalFeature.tsx index a96a00a426..63cab40d8e 100644 --- a/webview-ui/src/components/settings/ExperimentalFeature.tsx +++ b/webview-ui/src/components/settings/ExperimentalFeature.tsx @@ -1,5 +1,5 @@ import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" interface ExperimentalFeatureProps { enabled: boolean @@ -9,7 +9,7 @@ interface ExperimentalFeatureProps { } export const ExperimentalFeature = ({ enabled, onChange, experimentKey }: ExperimentalFeatureProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() // Generate translation keys based on experiment key const nameKey = experimentKey ? `settings:experimental.${experimentKey}.name` : "" diff --git a/webview-ui/src/components/settings/ExperimentalSettings.tsx b/webview-ui/src/components/settings/ExperimentalSettings.tsx index d5c55297ea..ba2f1ba6df 100644 --- a/webview-ui/src/components/settings/ExperimentalSettings.tsx +++ b/webview-ui/src/components/settings/ExperimentalSettings.tsx @@ -4,7 +4,7 @@ import type { Experiments, ImageGenerationProvider } from "@roo-code/types" import { EXPERIMENT_IDS, experimentConfigsMap } from "@roo/experiments" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { cn } from "@src/lib/utils" import { SetExperimentEnabled } from "./types" @@ -42,7 +42,7 @@ export const ExperimentalSettings = ({ className, ...props }: ExperimentalSettingsProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() return (
diff --git a/webview-ui/src/components/settings/FilePatternAllowlist.tsx b/webview-ui/src/components/settings/FilePatternAllowlist.tsx index 86774a79c0..b0fb126651 100644 --- a/webview-ui/src/components/settings/FilePatternAllowlist.tsx +++ b/webview-ui/src/components/settings/FilePatternAllowlist.tsx @@ -1,7 +1,7 @@ import type { FormEvent } from "react" import { VSCodeTextArea } from "@vscode/webview-ui-toolkit/react" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { SetCachedStateField } from "./types" import { SearchableSetting } from "./SearchableSetting" @@ -58,7 +58,7 @@ export const FilePatternAllowlist = ({ patterns, setCachedStateField, }: FilePatternAllowlistProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const label = t(`settings:autoApprove.allowlists.${translationKey}.label`) const headingId = `${settingId}-label` diff --git a/webview-ui/src/components/settings/ImageGenerationSettings.tsx b/webview-ui/src/components/settings/ImageGenerationSettings.tsx index fd54054b70..33fa6896dd 100644 --- a/webview-ui/src/components/settings/ImageGenerationSettings.tsx +++ b/webview-ui/src/components/settings/ImageGenerationSettings.tsx @@ -2,7 +2,7 @@ import { providerIdentifiers } from "@roo-code/types" import React, { useMemo } from "react" import { VSCodeCheckbox, VSCodeTextField, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react" import { IMAGE_GENERATION_MODELS, type ImageGenerationProvider, getImageGenerationProvider } from "@roo-code/types" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" interface ImageGenerationSettingsProps { enabled: boolean @@ -25,7 +25,7 @@ export const ImageGenerationSettings = ({ setOpenRouterImageApiKey, setImageGenerationSelectedModel, }: ImageGenerationSettingsProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() // Use shared utility for backwards compatibility logic const currentProvider = getImageGenerationProvider( diff --git a/webview-ui/src/components/settings/LanguageSettings.tsx b/webview-ui/src/components/settings/LanguageSettings.tsx index 26a807a498..43dcfd26c9 100644 --- a/webview-ui/src/components/settings/LanguageSettings.tsx +++ b/webview-ui/src/components/settings/LanguageSettings.tsx @@ -1,5 +1,5 @@ import { HTMLAttributes } from "react" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import type { Language } from "@roo-code/types" @@ -19,7 +19,7 @@ type LanguageSettingsProps = HTMLAttributes & { } export const LanguageSettings = ({ language, setCachedStateField, className, ...props }: LanguageSettingsProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() return (
diff --git a/webview-ui/src/components/settings/ModelInfoView.tsx b/webview-ui/src/components/settings/ModelInfoView.tsx index 1ae893eeb2..52133d5d15 100644 --- a/webview-ui/src/components/settings/ModelInfoView.tsx +++ b/webview-ui/src/components/settings/ModelInfoView.tsx @@ -5,7 +5,7 @@ import { providerIdentifiers } from "@roo-code/types/provider-identifiers" import { formatPrice } from "@src/utils/formatPrice" import { cn } from "@src/lib/utils" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { StandardTooltip } from "@src/components/ui" import { ModelDescriptionMarkdown } from "./ModelDescriptionMarkdown" @@ -47,7 +47,7 @@ export const ModelInfoView = ({ setIsDescriptionExpanded, hidePricing, }: ModelInfoViewProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() // Show tiered pricing table for OpenAI Native when model supports non-standard tiers const allowedTierNames = diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx index ecc3678a97..3e52f3d40d 100644 --- a/webview-ui/src/components/settings/ModelPicker.tsx +++ b/webview-ui/src/components/settings/ModelPicker.tsx @@ -1,6 +1,5 @@ import { useMemo, useState, useCallback, useEffect, useRef } from "react" import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" -import { Trans } from "react-i18next" import { ChevronsUpDown, Check, X, Info } from "lucide-react" import { @@ -11,7 +10,7 @@ import { providerIdentifiers, } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { Trans, useTranslation } from "react-i18next" import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel" import { filterModels } from "./utils/organizationFilters" import { cn } from "@src/lib/utils" @@ -95,7 +94,7 @@ export const ModelPicker = ({ displayTransform, onModelChange, }: ModelPickerProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [open, setOpen] = useState(false) const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false) @@ -316,6 +315,7 @@ export const ModelPicker = ({ {!hidePricing && apiConfiguration.apiProvider !== providerIdentifiers.mimo && (
, diff --git a/webview-ui/src/components/settings/NotificationSettings.tsx b/webview-ui/src/components/settings/NotificationSettings.tsx index 68683e65f4..cff9b74ed6 100644 --- a/webview-ui/src/components/settings/NotificationSettings.tsx +++ b/webview-ui/src/components/settings/NotificationSettings.tsx @@ -1,5 +1,5 @@ import { HTMLAttributes } from "react" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" import { SetCachedStateField } from "./types" @@ -24,7 +24,7 @@ export const NotificationSettings = ({ setCachedStateField, ...props }: NotificationSettingsProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() return (
{t("settings:sections.notifications")} diff --git a/webview-ui/src/components/settings/PromptsSettings.tsx b/webview-ui/src/components/settings/PromptsSettings.tsx index d6e0ba0f6b..e386d2295f 100644 --- a/webview-ui/src/components/settings/PromptsSettings.tsx +++ b/webview-ui/src/components/settings/PromptsSettings.tsx @@ -4,7 +4,7 @@ import { VSCodeTextArea, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react import { supportPrompt, SupportPromptType } from "@roo/support-prompt" import { vscode } from "@src/utils/vscode" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { useExtensionState } from "@src/context/ExtensionStateContext" import { Button, @@ -33,7 +33,7 @@ const PromptsSettings = ({ includeTaskHistoryInEnhance: propsIncludeTaskHistoryInEnhance, setIncludeTaskHistoryInEnhance: propsSetIncludeTaskHistoryInEnhance, }: PromptsSettingsProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const { listApiConfigMeta, enhancementApiConfigId, diff --git a/webview-ui/src/components/settings/R1FormatSetting.tsx b/webview-ui/src/components/settings/R1FormatSetting.tsx index 6418c949bd..4ad90cc14a 100644 --- a/webview-ui/src/components/settings/R1FormatSetting.tsx +++ b/webview-ui/src/components/settings/R1FormatSetting.tsx @@ -1,6 +1,6 @@ import { Checkbox } from "vscrui" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" interface R1FormatSettingProps { onChange: (value: boolean) => void @@ -8,7 +8,7 @@ interface R1FormatSettingProps { } export const R1FormatSetting = ({ onChange, openAiR1FormatEnabled }: R1FormatSettingProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() return (
diff --git a/webview-ui/src/components/settings/RateLimitSecondsControl.tsx b/webview-ui/src/components/settings/RateLimitSecondsControl.tsx index e66232d396..87dad7942f 100644 --- a/webview-ui/src/components/settings/RateLimitSecondsControl.tsx +++ b/webview-ui/src/components/settings/RateLimitSecondsControl.tsx @@ -1,4 +1,4 @@ -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { Slider } from "@/components/ui" @@ -8,7 +8,7 @@ interface RateLimitSecondsControlProps { } export const RateLimitSecondsControl = ({ value, onChange }: RateLimitSecondsControlProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() return (
diff --git a/webview-ui/src/components/settings/RulesSettings.tsx b/webview-ui/src/components/settings/RulesSettings.tsx index 028ca5c56c..6a06231800 100644 --- a/webview-ui/src/components/settings/RulesSettings.tsx +++ b/webview-ui/src/components/settings/RulesSettings.tsx @@ -3,7 +3,7 @@ import { Edit, Folder, Globe, Plus, ScrollText, Trash2 } from "lucide-react" import type { RuleMetadata } from "@roo-code/types" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { useExtensionState } from "@/context/ExtensionStateContext" import { AlertDialog, @@ -23,7 +23,7 @@ import { SectionHeader } from "./SectionHeader" import { CreateRuleDialog } from "./CreateRuleDialog" export const RulesSettings: React.FC = () => { - const { t } = useAppTranslation() + const { t } = useTranslation() const { cwd, rules: rawRules } = useExtensionState() const rules = useMemo(() => rawRules ?? [], [rawRules]) diff --git a/webview-ui/src/components/settings/SettingsSearchInput.tsx b/webview-ui/src/components/settings/SettingsSearchInput.tsx index d1b8566ef9..b422eabc69 100644 --- a/webview-ui/src/components/settings/SettingsSearchInput.tsx +++ b/webview-ui/src/components/settings/SettingsSearchInput.tsx @@ -2,7 +2,7 @@ import { useState, type RefObject } from "react" import { Search, X } from "lucide-react" import { cn } from "@/lib/utils" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { Input } from "@/components/ui" export interface SettingsSearchInputProps { @@ -22,7 +22,7 @@ export function SettingsSearchInput({ onKeyDown, inputRef, }: SettingsSearchInputProps) { - const { t } = useAppTranslation() + const { t } = useTranslation() const [isExpanded, setIsExpanded] = useState(false) const handleFocus = () => { diff --git a/webview-ui/src/components/settings/SettingsSearchResults.tsx b/webview-ui/src/components/settings/SettingsSearchResults.tsx index bf9d74aefa..3f182bf1c0 100644 --- a/webview-ui/src/components/settings/SettingsSearchResults.tsx +++ b/webview-ui/src/components/settings/SettingsSearchResults.tsx @@ -1,7 +1,7 @@ import { useMemo } from "react" import type { LucideIcon } from "lucide-react" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { cn } from "@/lib/utils" import { SectionName } from "./SettingsView" @@ -73,7 +73,7 @@ export function SettingsSearchResults({ sections, highlightedResultId, }: SettingsSearchResultsProps) { - const { t } = useAppTranslation() + const { t } = useTranslation() // Group results by section/tab const groupedResults = useMemo(() => { diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 6100a373e1..85284dc675 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -44,7 +44,7 @@ import { import { vscode } from "@src/utils/vscode" import { cn } from "@src/lib/utils" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { ExtensionStateContextType, useExtensionState } from "@src/context/ExtensionStateContext" import { AlertDialog, @@ -127,7 +127,7 @@ type SettingsViewProps = { } const SettingsView = forwardRef(({ onDone, targetSection }, ref) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const extensionState = useExtensionState() const { currentApiConfigName, listApiConfigMeta, uriScheme, settingsImportedAt, mode } = extensionState diff --git a/webview-ui/src/components/settings/SkillsSettings.tsx b/webview-ui/src/components/settings/SkillsSettings.tsx index bf81d0c6c7..1c03bb53d7 100644 --- a/webview-ui/src/components/settings/SkillsSettings.tsx +++ b/webview-ui/src/components/settings/SkillsSettings.tsx @@ -1,12 +1,11 @@ import React, { useState, useEffect, useMemo, useCallback } from "react" import { Plus, Globe, Folder, Edit, Trash2, Settings } from "lucide-react" -import { Trans } from "react-i18next" import type { SkillMetadata } from "@roo-code/types" import { getAllModes } from "@roo/modes" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { Trans, useTranslation } from "react-i18next" import { useExtensionState } from "@/context/ExtensionStateContext" import { AlertDialog, @@ -34,7 +33,7 @@ import { SectionHeader } from "./SectionHeader" import { CreateSkillDialog } from "./CreateSkillDialog" export const SkillsSettings: React.FC = () => { - const { t } = useAppTranslation() + const { t } = useTranslation() const { cwd, skills: rawSkills, customModes } = useExtensionState() const skills = useMemo(() => rawSkills ?? [], [rawSkills]) @@ -218,6 +217,7 @@ export const SkillsSettings: React.FC = () => {

{ {/* Fixed Footer */}

{ - const { t } = useAppTranslation() + const { t } = useTranslation() const { commands: rawCommands, cwd } = useExtensionState() const commands = useMemo(() => rawCommands ?? [], [rawCommands]) @@ -148,6 +147,7 @@ export const SlashCommandsSettings: React.FC = () => {

{ - const { t } = useAppTranslation() + const { t } = useTranslation() const [isCustomTemperature, setIsCustomTemperature] = useState(value !== undefined) const [inputValue, setInputValue] = useState(value) diff --git a/webview-ui/src/components/settings/TerminalSettings.tsx b/webview-ui/src/components/settings/TerminalSettings.tsx index ac7decdb2c..75ccafd43a 100644 --- a/webview-ui/src/components/settings/TerminalSettings.tsx +++ b/webview-ui/src/components/settings/TerminalSettings.tsx @@ -1,8 +1,7 @@ import { HTMLAttributes, useState, useCallback, useEffect } from "react" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { Trans, useTranslation } from "react-i18next" import { vscode } from "@/utils/vscode" import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react" -import { Trans } from "react-i18next" import { buildDocLink } from "@src/utils/docLinks" import { useEvent, useMount } from "react-use" import { Terminal } from "lucide-react" @@ -70,7 +69,7 @@ export const TerminalSettings = ({ className, ...props }: TerminalSettingsProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [inheritEnv, setInheritEnv] = useState(true) const [profileNames, setProfileNames] = useState([]) @@ -205,7 +204,7 @@ export const TerminalSettings = ({

- + - + {t("settings:terminal.inheritEnv.label")}
- +
- + {terminalCommandDelay ?? 0}ms
- +
- +
- + {t("settings:terminal.zshOhMy.label")}
- + {t("settings:terminal.zshP10k.label")}
- + {t("settings:terminal.zdotdir.label")}
- + { - const { t } = useAppTranslation() + const { t } = useTranslation() const { id: selectedModelId } = useSelectedModel(apiConfiguration) // Check if this is a Gemini 2.5 Pro model diff --git a/webview-ui/src/components/settings/TodoListSettingsControl.tsx b/webview-ui/src/components/settings/TodoListSettingsControl.tsx index 7a38ab3adc..44d9c95c9d 100644 --- a/webview-ui/src/components/settings/TodoListSettingsControl.tsx +++ b/webview-ui/src/components/settings/TodoListSettingsControl.tsx @@ -1,5 +1,5 @@ import React, { useCallback } from "react" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" interface TodoListSettingsControlProps { @@ -11,7 +11,7 @@ export const TodoListSettingsControl: React.FC = ( todoListEnabled = true, onChange, }) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const handleTodoListEnabledChange = useCallback( (e: any) => { diff --git a/webview-ui/src/components/settings/ToolRepetitionLimitControl.tsx b/webview-ui/src/components/settings/ToolRepetitionLimitControl.tsx index ae2984d4c0..fa54f3dc79 100644 --- a/webview-ui/src/components/settings/ToolRepetitionLimitControl.tsx +++ b/webview-ui/src/components/settings/ToolRepetitionLimitControl.tsx @@ -1,6 +1,6 @@ import { DEFAULT_TOOL_REPETITION_SOFT_LIMIT } from "@roo-code/types" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { Slider } from "@/components/ui" @@ -10,7 +10,7 @@ interface ToolRepetitionLimitControlProps { } export const ToolRepetitionLimitControl = ({ softValue, onSoftChange }: ToolRepetitionLimitControlProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const resolvedSoft = softValue ?? DEFAULT_TOOL_REPETITION_SOFT_LIMIT diff --git a/webview-ui/src/components/settings/UISettings.tsx b/webview-ui/src/components/settings/UISettings.tsx index 46353c26ad..4b98072d10 100644 --- a/webview-ui/src/components/settings/UISettings.tsx +++ b/webview-ui/src/components/settings/UISettings.tsx @@ -1,5 +1,5 @@ import { HTMLAttributes, useMemo } from "react" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" import { telemetryClient } from "@/utils/TelemetryClient" import { @@ -39,7 +39,7 @@ export const UISettings = ({ setCachedStateField, ...props }: UISettingsProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() // Detect platform for dynamic modifier key display const primaryMod = useMemo(() => { diff --git a/webview-ui/src/components/settings/Verbosity.tsx b/webview-ui/src/components/settings/Verbosity.tsx index ee612d66cf..67262a1059 100644 --- a/webview-ui/src/components/settings/Verbosity.tsx +++ b/webview-ui/src/components/settings/Verbosity.tsx @@ -1,6 +1,6 @@ import { type ProviderSettings, type ModelInfo, type VerbosityLevel, verbosityLevels } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui" interface VerbosityProps { @@ -10,7 +10,7 @@ interface VerbosityProps { } export const Verbosity = ({ apiConfiguration, setApiConfigurationField, modelInfo }: VerbosityProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() // For now, we'll show verbosity for all models, but this can be restricted later // based on model capabilities (e.g., only for GPT-5 models) diff --git a/webview-ui/src/components/settings/__tests__/About.spec.tsx b/webview-ui/src/components/settings/__tests__/About.spec.tsx index e9ca86be37..96dffc4f13 100644 --- a/webview-ui/src/components/settings/__tests__/About.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/About.spec.tsx @@ -1,7 +1,6 @@ import React from "react" import { act, fireEvent, render, screen } from "@/utils/test-utils" -import { TranslationProvider } from "@/i18n/__mocks__/TranslationContext" import { EXTERNAL_LINKS } from "@/constants/externalLinks" import { vscode } from "@/utils/vscode" @@ -35,22 +34,19 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({ ), })) -vi.mock("@/i18n/TranslationContext", () => { - const actual = vi.importActual("@/i18n/TranslationContext") - return { - ...actual, - useAppTranslation: () => ({ - t: (key: string, options?: Record) => { - if (!options) return key - let result = key - for (const [k, v] of Object.entries(options)) { - result = result.replace(`{{${k}}}`, String(v)) - } - return result - }, - }), - } -}) +vi.mock("react-i18next", async (importOriginal) => ({ + ...(await importOriginal()), + useTranslation: () => ({ + t: (key: string, options?: Record) => { + if (!options) return key + let result = key + for (const [k, v] of Object.entries(options)) { + result = result.replace(`{{${k}}}`, String(v)) + } + return result + }, + }), +})) vi.mock("@roo/package", () => ({ Package: { @@ -65,12 +61,7 @@ describe("About", () => { setTelemetrySetting: vi.fn(), } - const renderAbout = () => - render( - - - , - ) + const renderAbout = () => render() const dispatchImportProgress = async (progress: { status: "starting" | "copying" | "finished" | "failed" @@ -95,29 +86,17 @@ describe("About", () => { }) it("renders the About section header", () => { - render( - - - , - ) + render() expect(screen.getByText("settings:sections.about")).toBeInTheDocument() }) it("displays version information", () => { - render( - - - , - ) + render() expect(screen.getByText(/Version: 1\.0\.0/)).toBeInTheDocument() }) it("renders the bug report section with label and link text", () => { - render( - - - , - ) + render() expect(screen.getByText("settings:about.bugReport.label")).toBeInTheDocument() expect(screen.getByRole("link", { name: "settings:about.bugReport.link" })).toHaveAttribute( "href", @@ -126,11 +105,7 @@ describe("About", () => { }) it("renders the feature request section with label and link text", () => { - render( - - - , - ) + render() expect(screen.getByText("settings:about.featureRequest.label")).toBeInTheDocument() expect(screen.getByRole("link", { name: "settings:about.featureRequest.link" })).toHaveAttribute( "href", @@ -139,11 +114,7 @@ describe("About", () => { }) it("renders the security issue section with label and link text", () => { - render( - - - , - ) + render() expect(screen.getByText("settings:about.securityIssue.label")).toBeInTheDocument() expect(screen.getByRole("link", { name: "settings:about.securityIssue.link" })).toHaveAttribute( "href", @@ -152,41 +123,25 @@ describe("About", () => { }) it("shows the telemetry checkbox as checked when the setting is explicitly enabled", () => { - render( - - - , - ) + render() expect(screen.getByRole("checkbox", { name: /telemetry/i })).toBeChecked() }) it("shows the telemetry checkbox as checked when the setting is unset (disclosed opt-out default)", () => { - render( - - - , - ) + render() expect(screen.getByRole("checkbox", { name: /telemetry/i })).toBeChecked() }) it("does not show the telemetry checkbox as checked when the setting is disabled", () => { - render( - - - , - ) + render() expect(screen.getByRole("checkbox", { name: /telemetry/i })).not.toBeChecked() }) it("renders export, import, and reset buttons", () => { - render( - - - , - ) + render() expect(screen.getByText("settings:footer.settings.export")).toBeInTheDocument() expect(screen.getByText("settings:footer.settings.import")).toBeInTheDocument() expect(screen.getByText("settings:footer.settings.reset")).toBeInTheDocument() diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx index b68bee99e6..47b547e518 100644 --- a/webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx @@ -21,8 +21,9 @@ vi.mock("@src/context/ExtensionStateContext", () => ({ })) // Mock the translation hook -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", async (importOriginal) => ({ + ...(await importOriginal()), + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/settings/__tests__/AutoApproveSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/AutoApproveSettings.spec.tsx index 12090b42b5..90c8f023d7 100644 --- a/webview-ui/src/components/settings/__tests__/AutoApproveSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/AutoApproveSettings.spec.tsx @@ -61,8 +61,9 @@ vi.mock("@vscode/webview-ui-toolkit/react", async () => { return { ...toolkitMock, VSCodeCheckbox: VSCodeCheckboxHost } }) -vi.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ t: (key: string) => key }), +vi.mock("react-i18next", async (importOriginal) => ({ + ...(await importOriginal()), + useTranslation: () => ({ t: (key: string) => key }), })) // AutoApproveSettings reads a couple of live-state values that are genuinely diff --git a/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx b/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx index 91788e0200..e95ddd517c 100644 --- a/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx @@ -1,18 +1,12 @@ import { render, screen, fireEvent } from "@/utils/test-utils" -import { TranslationProvider } from "@/i18n/__mocks__/TranslationContext" - import { AutoApproveToggle, autoApproveSettingsConfig } from "../AutoApproveToggle" -vi.mock("@/i18n/TranslationContext", () => { - const actual = vi.importActual("@/i18n/TranslationContext") - return { - ...actual, - useAppTranslation: () => ({ - t: (key: string) => key, - }), - } -}) +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) describe("AutoApproveToggle", () => { const mockOnToggle = vi.fn() @@ -32,11 +26,7 @@ describe("AutoApproveToggle", () => { }) test("renders all toggle buttons with correct initial ARIA attributes", () => { - render( - - - , - ) + render() Object.values(autoApproveSettingsConfig).forEach((config) => { const button = screen.getByTestId(config.testId) @@ -47,11 +37,7 @@ describe("AutoApproveToggle", () => { }) test("calls onToggle with the correct key and value when a button is clicked", () => { - render( - - - , - ) + render() const writeToggleButton = screen.getByTestId(autoApproveSettingsConfig.alwaysAllowWrite.testId) fireEvent.click(writeToggleButton) @@ -66,21 +52,13 @@ describe("AutoApproveToggle", () => { }) test("updates aria-pressed attribute after toggle", () => { - const { rerender } = render( - - - , - ) + const { rerender } = render() const writeToggleButton = screen.getByTestId(autoApproveSettingsConfig.alwaysAllowWrite.testId) expect(writeToggleButton).toHaveAttribute("aria-pressed", "false") const updatedProps = { ...initialProps, alwaysAllowWrite: true } - rerender( - - - , - ) + rerender() expect(screen.getByTestId(autoApproveSettingsConfig.alwaysAllowWrite.testId)).toHaveAttribute( "aria-pressed", diff --git a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx index 10d5d0fe4f..07f3daee23 100644 --- a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx @@ -4,19 +4,6 @@ import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" import { ContextManagementSettings } from "../ContextManagementSettings" import { vscode } from "@/utils/vscode" -// Mock the translation hook -vi.mock("@/hooks/useAppTranslation", () => ({ - useAppTranslation: () => ({ - t: (key: string) => { - // Return specific translations for our test cases - if (key === "settings:contextManagement.diagnostics.maxMessages.unlimitedLabel") { - return "Unlimited" - } - return key - }, - }), -})) - // Mock the UI components vi.mock("@/components/ui", () => ({ ...vi.importActual("@/components/ui"), diff --git a/webview-ui/src/components/settings/__tests__/CreateRuleDialog.spec.tsx b/webview-ui/src/components/settings/__tests__/CreateRuleDialog.spec.tsx index f1684a2cdf..1610242f8e 100644 --- a/webview-ui/src/components/settings/__tests__/CreateRuleDialog.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/CreateRuleDialog.spec.tsx @@ -10,8 +10,8 @@ vi.mock("@/utils/vscode", () => ({ }, })) -vi.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/settings/__tests__/CreateSkillDialog.spec.tsx b/webview-ui/src/components/settings/__tests__/CreateSkillDialog.spec.tsx index 9b2087ddaa..9b777ecd5b 100644 --- a/webview-ui/src/components/settings/__tests__/CreateSkillDialog.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/CreateSkillDialog.spec.tsx @@ -12,8 +12,8 @@ vi.mock("@/utils/vscode", () => ({ })) // Mock the translation hook -vi.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/settings/__tests__/ExperimentalSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/ExperimentalSettings.spec.tsx index b31f87dc7e..bad98b36e3 100644 --- a/webview-ui/src/components/settings/__tests__/ExperimentalSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ExperimentalSettings.spec.tsx @@ -4,8 +4,8 @@ import { experimentDefault } from "@roo/experiments" import { ExperimentalSettings } from "../ExperimentalSettings" -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/settings/__tests__/ImageGenerationSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/ImageGenerationSettings.spec.tsx index 8fd27b961f..ea5ead7f81 100644 --- a/webview-ui/src/components/settings/__tests__/ImageGenerationSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ImageGenerationSettings.spec.tsx @@ -2,9 +2,9 @@ import { render, fireEvent } from "@testing-library/react" import { ImageGenerationSettings } from "../ImageGenerationSettings" -// Mock the translation context -vi.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +// Mock the translation hook +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/settings/__tests__/ModelInfoView.spec.tsx b/webview-ui/src/components/settings/__tests__/ModelInfoView.spec.tsx index ac87f06d7a..81dc9f7f11 100644 --- a/webview-ui/src/components/settings/__tests__/ModelInfoView.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ModelInfoView.spec.tsx @@ -4,8 +4,8 @@ import { render, screen, within } from "@/utils/test-utils" import { ModelInfoView } from "../ModelInfoView" -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => ({ "settings:modelInfo.imageSupportUnknown": "Image support not reported", diff --git a/webview-ui/src/components/settings/__tests__/ModelInfoView.visual.fixture.tsx b/webview-ui/src/components/settings/__tests__/ModelInfoView.visual.fixture.tsx index b007d4be69..978921da24 100644 --- a/webview-ui/src/components/settings/__tests__/ModelInfoView.visual.fixture.tsx +++ b/webview-ui/src/components/settings/__tests__/ModelInfoView.visual.fixture.tsx @@ -3,7 +3,9 @@ import React from "react" import { OpenAiServiceTier, type ModelInfo } from "@roo-code/types/model" import { providerIdentifiers } from "@roo-code/types/provider-identifiers" -import { TranslationContext } from "@src/i18n/TranslationContext" +import { I18nextProvider } from "react-i18next" + +import { createFixtureI18n } from "@src/i18n/__tests__/createFixtureI18n.visual.i18n" import { ModelInfoView } from "../ModelInfoView" const modelInfo: ModelInfo = { @@ -49,12 +51,10 @@ const translations: Record = { "settings:serviceTier.priority": "Priority", } +const i18n = createFixtureI18n(translations) + export const ModelInfoViewFixture = () => ( - translations[key] ?? key, - i18n: null as unknown as typeof import("../../../i18n/setup").default, - }}> +
( setIsDescriptionExpanded={() => {}} />
-
+ ) diff --git a/webview-ui/src/components/settings/__tests__/ModelPicker.deprecated.spec.tsx b/webview-ui/src/components/settings/__tests__/ModelPicker.deprecated.spec.tsx index 154f1cc1de..0d5e72af93 100644 --- a/webview-ui/src/components/settings/__tests__/ModelPicker.deprecated.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ModelPicker.deprecated.spec.tsx @@ -12,8 +12,9 @@ import { ModelPicker } from "../ModelPicker" import type { ModelInfo } from "@roo-code/types" // Mock the i18n module -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", async (importOriginal) => ({ + ...(await importOriginal()), + useTranslation: () => ({ t: (key: string, options?: any) => { // Handle specific translation keys if (key === "settings:validation.modelDeprecated") { diff --git a/webview-ui/src/components/settings/__tests__/PromptsSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/PromptsSettings.spec.tsx index d10e100f45..44b9e43ac0 100644 --- a/webview-ui/src/components/settings/__tests__/PromptsSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/PromptsSettings.spec.tsx @@ -11,8 +11,8 @@ vi.mock("@src/utils/vscode", () => ({ }, })) -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ t: (key: string) => key }), +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), })) // Exposed so tests can assert the context (uncontrolled) setter still fires. diff --git a/webview-ui/src/components/settings/__tests__/RulesSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/RulesSettings.spec.tsx index 52ee44a5c8..760ee812ee 100644 --- a/webview-ui/src/components/settings/__tests__/RulesSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/RulesSettings.spec.tsx @@ -12,8 +12,8 @@ vi.mock("@/utils/vscode", () => ({ }, })) -vi.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string, params?: any) => params?.mode ? `${key} ${params.mode}` : params?.name ? `${key} ${params.name}` : key, }), diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx index 6704aa1cf0..2d2f153aaf 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx @@ -32,9 +32,10 @@ vi.mock("@src/context/ExtensionStateContext", () => ({ const mockTranslate = vi.hoisted(() => (key: string) => key) -// Mock the translation context -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +// Mock the translation hook +vi.mock("react-i18next", async (importOriginal) => ({ + ...(await importOriginal()), + useTranslation: () => ({ t: mockTranslate, }), })) diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx index 1bc4ab1afb..276d835633 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx @@ -17,11 +17,9 @@ vi.mock("@src/context/ExtensionStateContext", () => ({ useExtensionState: vi.fn(), })) -// Mock the translation context -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ - t: (key: string) => key, - }), +// Mock the translation hook +vi.mock("react-i18next", async (importOriginal) => ({ + ...(await importOriginal()), useTranslation: () => ({ t: (key: string) => key, }), diff --git a/webview-ui/src/components/settings/__tests__/SkillsSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/SkillsSettings.spec.tsx index 67c820d8ce..9d84f8db56 100644 --- a/webview-ui/src/components/settings/__tests__/SkillsSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SkillsSettings.spec.tsx @@ -14,8 +14,9 @@ vi.mock("@/utils/vscode", () => ({ })) // Mock the translation hook -vi.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", async (importOriginal) => ({ + ...(await importOriginal()), + useTranslation: () => ({ t: (key: string, params?: any) => { if (params?.name) { return `${key} ${params.name}` diff --git a/webview-ui/src/components/settings/__tests__/SlashCommandsSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/SlashCommandsSettings.spec.tsx index 4533e54a5f..853f924b74 100644 --- a/webview-ui/src/components/settings/__tests__/SlashCommandsSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SlashCommandsSettings.spec.tsx @@ -14,8 +14,9 @@ vi.mock("@/utils/vscode", () => ({ })) // Mock the translation hook -vi.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", async (importOriginal) => ({ + ...(await importOriginal()), + useTranslation: () => ({ t: (key: string, params?: any) => { if (params?.name) { return `${key} ${params.name}` diff --git a/webview-ui/src/components/settings/__tests__/TerminalSettings.profile.spec.tsx b/webview-ui/src/components/settings/__tests__/TerminalSettings.profile.spec.tsx index d3fc05abdf..93b26f843d 100644 --- a/webview-ui/src/components/settings/__tests__/TerminalSettings.profile.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/TerminalSettings.profile.spec.tsx @@ -7,8 +7,9 @@ import { render, screen, fireEvent, act } from "@/utils/test-utils" import { TerminalSettings, DEFAULT_PROFILE_VALUE } from "../TerminalSettings" // Mock translation hook to echo keys -vi.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ t: (key: string) => key }), +vi.mock("react-i18next", async (importOriginal) => ({ + ...(await importOriginal()), + useTranslation: () => ({ t: (key: string) => key }), })) vi.mock("@src/utils/docLinks", () => ({ diff --git a/webview-ui/src/components/settings/__tests__/TodoListSettingsControl.spec.tsx b/webview-ui/src/components/settings/__tests__/TodoListSettingsControl.spec.tsx index 76f65d312f..664be77993 100644 --- a/webview-ui/src/components/settings/__tests__/TodoListSettingsControl.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/TodoListSettingsControl.spec.tsx @@ -3,8 +3,8 @@ import { render, screen, fireEvent } from "@testing-library/react" import { TodoListSettingsControl } from "../TodoListSettingsControl" // Mock the translation hook -vi.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => { const translations: Record = { "settings:advanced.todoList.label": "Enable todo list tool", diff --git a/webview-ui/src/components/settings/__tests__/ToolRepetitionLimitControl.spec.tsx b/webview-ui/src/components/settings/__tests__/ToolRepetitionLimitControl.spec.tsx index 4412bdec0d..9bab6bb7ed 100644 --- a/webview-ui/src/components/settings/__tests__/ToolRepetitionLimitControl.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ToolRepetitionLimitControl.spec.tsx @@ -5,8 +5,8 @@ import { render, screen, fireEvent } from "@testing-library/react" import { ToolRepetitionLimitControl } from "../ToolRepetitionLimitControl" // Mock the translation hook -vi.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => { const translations: Record = { "settings:providers.toolRepetitionSoftLimit.label": "Tool repetition soft limit", diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/opencode-go-temperature.png b/webview-ui/src/components/settings/__tests__/__screenshots__/opencode-go-temperature.png index e1828c9b6b..d2633a1f3b 100644 Binary files a/webview-ui/src/components/settings/__tests__/__screenshots__/opencode-go-temperature.png and b/webview-ui/src/components/settings/__tests__/__screenshots__/opencode-go-temperature.png differ diff --git a/webview-ui/src/components/settings/providers/Anthropic.tsx b/webview-ui/src/components/settings/providers/Anthropic.tsx index 6417e34b8b..bd69a881ff 100644 --- a/webview-ui/src/components/settings/providers/Anthropic.tsx +++ b/webview-ui/src/components/settings/providers/Anthropic.tsx @@ -4,7 +4,7 @@ import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import type { ProviderSettings } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel" @@ -17,7 +17,7 @@ type AnthropicProps = { } export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: AnthropicProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const selectedModel = useSelectedModel(apiConfiguration) const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl) diff --git a/webview-ui/src/components/settings/providers/Baseten.tsx b/webview-ui/src/components/settings/providers/Baseten.tsx index 2f4efd62c1..e836456788 100644 --- a/webview-ui/src/components/settings/providers/Baseten.tsx +++ b/webview-ui/src/components/settings/providers/Baseten.tsx @@ -3,7 +3,7 @@ import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import type { ProviderSettings } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { inputEventTransform } from "../transforms" @@ -15,7 +15,7 @@ type BasetenProps = { } export const Baseten = ({ apiConfiguration, setApiConfigurationField }: BasetenProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const handleInputChange = useCallback( ( diff --git a/webview-ui/src/components/settings/providers/Bedrock.tsx b/webview-ui/src/components/settings/providers/Bedrock.tsx index bbb540709b..b24fe732d2 100644 --- a/webview-ui/src/components/settings/providers/Bedrock.tsx +++ b/webview-ui/src/components/settings/providers/Bedrock.tsx @@ -13,7 +13,7 @@ import { resolveBedrockCustomArnBaseModelId, } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StandardTooltip } from "@src/components/ui" import { inputEventTransform, noTransform } from "../transforms" @@ -26,7 +26,7 @@ type BedrockProps = { } export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedModelInfo }: BedrockProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [awsEndpointSelected, setAwsEndpointSelected] = useState(!!apiConfiguration?.awsBedrockEndpointEnabled) // For a custom ARN, model capabilities come from the model the ARN points to. diff --git a/webview-ui/src/components/settings/providers/BedrockCustomArn.tsx b/webview-ui/src/components/settings/providers/BedrockCustomArn.tsx index b74a6c87a7..a6ee0e7b70 100644 --- a/webview-ui/src/components/settings/providers/BedrockCustomArn.tsx +++ b/webview-ui/src/components/settings/providers/BedrockCustomArn.tsx @@ -12,7 +12,7 @@ import { } from "@roo-code/types" import { validateBedrockArn } from "@src/utils/validate" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui" const baseModelIds = Object.keys(bedrockModels).sort((a, b) => a.localeCompare(b)) @@ -28,12 +28,14 @@ type BedrockCustomArnProps = { } export const BedrockCustomArn = ({ apiConfiguration, setApiConfigurationField }: BedrockCustomArnProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const validation = useMemo(() => { const { awsCustomArn, awsRegion } = apiConfiguration - return awsCustomArn ? validateBedrockArn(awsCustomArn, awsRegion) : { isValid: true, errorMessage: undefined } - }, [apiConfiguration]) + return awsCustomArn + ? validateBedrockArn(awsCustomArn, t, awsRegion) + : { isValid: true, errorMessage: undefined } + }, [apiConfiguration, t]) const baseModelId = resolveBedrockCustomArnBaseModelId(apiConfiguration.awsCustomArn, apiConfiguration.awsCustomArnBaseModelId) ?? diff --git a/webview-ui/src/components/settings/providers/DeepSeek.tsx b/webview-ui/src/components/settings/providers/DeepSeek.tsx index 6f0ac2d92a..e91d17ea23 100644 --- a/webview-ui/src/components/settings/providers/DeepSeek.tsx +++ b/webview-ui/src/components/settings/providers/DeepSeek.tsx @@ -3,7 +3,7 @@ import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import type { ProviderSettings } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { inputEventTransform } from "../transforms" @@ -14,7 +14,7 @@ type DeepSeekProps = { } export const DeepSeek = ({ apiConfiguration, setApiConfigurationField }: DeepSeekProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const handleInputChange = useCallback( ( diff --git a/webview-ui/src/components/settings/providers/Fireworks.tsx b/webview-ui/src/components/settings/providers/Fireworks.tsx index bb8eb8aa58..b881fc4581 100644 --- a/webview-ui/src/components/settings/providers/Fireworks.tsx +++ b/webview-ui/src/components/settings/providers/Fireworks.tsx @@ -3,7 +3,7 @@ import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import type { ProviderSettings } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { inputEventTransform } from "../transforms" @@ -14,7 +14,7 @@ type FireworksProps = { } export const Fireworks = ({ apiConfiguration, setApiConfigurationField }: FireworksProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const handleInputChange = useCallback( ( diff --git a/webview-ui/src/components/settings/providers/Friendli.tsx b/webview-ui/src/components/settings/providers/Friendli.tsx index 66f4c554ef..79a38cff54 100644 --- a/webview-ui/src/components/settings/providers/Friendli.tsx +++ b/webview-ui/src/components/settings/providers/Friendli.tsx @@ -3,7 +3,7 @@ import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import type { ProviderSettings } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { inputEventTransform } from "../transforms" @@ -18,7 +18,7 @@ type FriendliProps = { * Renders an API-key input and a "Get Friendli API Key" link when the key is empty. */ export const Friendli = ({ apiConfiguration, setApiConfigurationField }: FriendliProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const handleInputChange = useCallback( ( diff --git a/webview-ui/src/components/settings/providers/Gemini.tsx b/webview-ui/src/components/settings/providers/Gemini.tsx index aed016204f..0273670645 100644 --- a/webview-ui/src/components/settings/providers/Gemini.tsx +++ b/webview-ui/src/components/settings/providers/Gemini.tsx @@ -4,7 +4,7 @@ import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import type { ProviderSettings } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { inputEventTransform } from "../transforms" @@ -15,7 +15,7 @@ type GeminiProps = { } export const Gemini = ({ apiConfiguration, setApiConfigurationField }: GeminiProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [googleGeminiBaseUrlSelected, setGoogleGeminiBaseUrlSelected] = useState( !!apiConfiguration?.googleGeminiBaseUrl, diff --git a/webview-ui/src/components/settings/providers/IOIntelligence.tsx b/webview-ui/src/components/settings/providers/IOIntelligence.tsx index 07a3a1d8bc..dc21dfa097 100644 --- a/webview-ui/src/components/settings/providers/IOIntelligence.tsx +++ b/webview-ui/src/components/settings/providers/IOIntelligence.tsx @@ -12,7 +12,7 @@ import { } from "@roo-code/types" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { vscode } from "@src/utils/vscode" import { ModelPicker } from "../ModelPicker" @@ -35,7 +35,7 @@ export const IOIntelligence = ({ modelValidationError, simplifySettings, }: IOIntelligenceProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const handleInputChange = useCallback( ( diff --git a/webview-ui/src/components/settings/providers/Kenari.tsx b/webview-ui/src/components/settings/providers/Kenari.tsx index e8d2f5cdb9..dd1850c0d1 100644 --- a/webview-ui/src/components/settings/providers/Kenari.tsx +++ b/webview-ui/src/components/settings/providers/Kenari.tsx @@ -9,7 +9,7 @@ import { providerIdentifiers, } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { inputEventTransform } from "../transforms" @@ -32,7 +32,7 @@ export const Kenari = ({ modelValidationError, simplifySettings, }: KenariProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const handleInputChange = useCallback( ( diff --git a/webview-ui/src/components/settings/providers/KimiCode.tsx b/webview-ui/src/components/settings/providers/KimiCode.tsx index a0350c9471..5bdd712e01 100644 --- a/webview-ui/src/components/settings/providers/KimiCode.tsx +++ b/webview-ui/src/components/settings/providers/KimiCode.tsx @@ -11,7 +11,7 @@ import { RouterModelsMessageType, } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { useRouterModels } from "@src/components/ui/hooks/useRouterModels" import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui" import { vscode } from "@src/utils/vscode" @@ -36,7 +36,7 @@ export const KimiCode = ({ kimiCodeIsAuthenticated = false, kimiCodeOAuthState, }: KimiCodeProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const authMethod = apiConfiguration.kimiCodeAuthMethod ?? "oauth" const { data, refetch, isFetching } = useRouterModels({ provider: providerIdentifiers.kimiCode, diff --git a/webview-ui/src/components/settings/providers/LMStudio.tsx b/webview-ui/src/components/settings/providers/LMStudio.tsx index 4ae20b939f..6f73c0cad2 100644 --- a/webview-ui/src/components/settings/providers/LMStudio.tsx +++ b/webview-ui/src/components/settings/providers/LMStudio.tsx @@ -1,6 +1,5 @@ import { useCallback, useState, useMemo, useEffect, useRef } from "react" import { useEvent } from "react-use" -import { Trans } from "react-i18next" import { Checkbox } from "vscrui" import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" @@ -12,7 +11,7 @@ import { providerIdentifiers, } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { Trans, useTranslation } from "react-i18next" import { requestLmStudioModels } from "@src/components/ui/hooks/useLmStudioModels" import { useRouterModels } from "@src/components/ui/hooks/useRouterModels" @@ -25,7 +24,7 @@ type LMStudioProps = { } export const LMStudio = ({ apiConfiguration, setApiConfigurationField }: LMStudioProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [lmStudioModels, setLmStudioModels] = useState({}) const routerModels = useRouterModels({ provider: providerIdentifiers.lmstudio }) @@ -158,6 +157,7 @@ export const LMStudio = ({ apiConfiguration, setApiConfigurationField }: LMStudi )}
, diff --git a/webview-ui/src/components/settings/providers/LiteLLM.tsx b/webview-ui/src/components/settings/providers/LiteLLM.tsx index d7a12249e7..13acc74446 100644 --- a/webview-ui/src/components/settings/providers/LiteLLM.tsx +++ b/webview-ui/src/components/settings/providers/LiteLLM.tsx @@ -16,7 +16,7 @@ import { RouterName } from "@roo/api" import { vscode } from "@src/utils/vscode" import { useExtensionState } from "@src/context/ExtensionStateContext" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { Button } from "@src/components/ui" import { inputEventTransform } from "../transforms" @@ -44,7 +44,7 @@ export const LiteLLM = ({ modelValidationError, simplifySettings, }: LiteLLMProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const queryClient = useQueryClient() const { routerModels } = useExtensionState() const [refreshStatus, setRefreshStatus] = useState(RefreshStatus.Idle) diff --git a/webview-ui/src/components/settings/providers/Mimo.tsx b/webview-ui/src/components/settings/providers/Mimo.tsx index 9060b837d3..64aea0bcf3 100644 --- a/webview-ui/src/components/settings/providers/Mimo.tsx +++ b/webview-ui/src/components/settings/providers/Mimo.tsx @@ -3,7 +3,7 @@ import { VSCodeTextField, VSCodeDropdown, VSCodeOption } from "@vscode/webview-u import type { ProviderSettings } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { inputEventTransform } from "../transforms" @@ -15,7 +15,7 @@ type MimoProps = { } export const Mimo = ({ apiConfiguration, setApiConfigurationField }: MimoProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const handleInputChange = useCallback( ( diff --git a/webview-ui/src/components/settings/providers/MiniMax.tsx b/webview-ui/src/components/settings/providers/MiniMax.tsx index 4055be7d17..ee24f4e0ee 100644 --- a/webview-ui/src/components/settings/providers/MiniMax.tsx +++ b/webview-ui/src/components/settings/providers/MiniMax.tsx @@ -3,7 +3,7 @@ import { VSCodeTextField, VSCodeDropdown, VSCodeOption } from "@vscode/webview-u import type { ProviderSettings } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { inputEventTransform } from "../transforms" @@ -15,7 +15,7 @@ type MiniMaxProps = { } export const MiniMax = ({ apiConfiguration, setApiConfigurationField }: MiniMaxProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const handleInputChange = useCallback( ( diff --git a/webview-ui/src/components/settings/providers/Mistral.tsx b/webview-ui/src/components/settings/providers/Mistral.tsx index 84a0154a63..09498c3626 100644 --- a/webview-ui/src/components/settings/providers/Mistral.tsx +++ b/webview-ui/src/components/settings/providers/Mistral.tsx @@ -3,7 +3,7 @@ import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { type ProviderSettings, type RouterModels, mistralDefaultModelId } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { inputEventTransform } from "../transforms" @@ -16,7 +16,7 @@ type MistralProps = { } export const Mistral = ({ apiConfiguration, setApiConfigurationField }: MistralProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const handleInputChange = useCallback( ( diff --git a/webview-ui/src/components/settings/providers/Moonshot.tsx b/webview-ui/src/components/settings/providers/Moonshot.tsx index 6e7d5aac33..4629e5cd46 100644 --- a/webview-ui/src/components/settings/providers/Moonshot.tsx +++ b/webview-ui/src/components/settings/providers/Moonshot.tsx @@ -13,7 +13,7 @@ import { import { RouterName } from "@roo/api" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { useExtensionState } from "@src/context/ExtensionStateContext" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { vscode } from "@src/utils/vscode" @@ -37,7 +37,7 @@ enum RefreshStatus { } export const Moonshot = ({ apiConfiguration, setApiConfigurationField, simplifySettings }: MoonshotProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const { routerModels } = useExtensionState() const queryClient = useQueryClient() const [refreshStatus, setRefreshStatus] = useState(RefreshStatus.Idle) diff --git a/webview-ui/src/components/settings/providers/NanoGPT.tsx b/webview-ui/src/components/settings/providers/NanoGPT.tsx index 11e0005604..e7ec8bd27b 100644 --- a/webview-ui/src/components/settings/providers/NanoGPT.tsx +++ b/webview-ui/src/components/settings/providers/NanoGPT.tsx @@ -15,7 +15,7 @@ import { import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { vscode } from "@src/utils/vscode" import { ModelPicker } from "../ModelPicker" @@ -48,7 +48,7 @@ export const NanoGPT = ({ modelValidationError, simplifySettings, }: NanoGPTProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const routingPreference = apiConfiguration.nanoGptRoutingPreference ?? nanoGptDefaultRoutingPreference const handleInputChange = useCallback( diff --git a/webview-ui/src/components/settings/providers/Ollama.tsx b/webview-ui/src/components/settings/providers/Ollama.tsx index 80a8bf5904..20fa9dc38e 100644 --- a/webview-ui/src/components/settings/providers/Ollama.tsx +++ b/webview-ui/src/components/settings/providers/Ollama.tsx @@ -11,7 +11,7 @@ import { providerIdentifiers, } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { useRouterModels } from "@src/components/ui/hooks/useRouterModels" import { Button } from "@src/components/ui" import { vscode } from "@src/utils/vscode" @@ -33,7 +33,7 @@ enum RefreshStatus { } export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [ollamaModels, setOllamaModels] = useState({}) const [refreshStatus, setRefreshStatus] = useState(RefreshStatus.Idle) diff --git a/webview-ui/src/components/settings/providers/OpenAI.tsx b/webview-ui/src/components/settings/providers/OpenAI.tsx index 460019e75a..c23df2074d 100644 --- a/webview-ui/src/components/settings/providers/OpenAI.tsx +++ b/webview-ui/src/components/settings/providers/OpenAI.tsx @@ -4,7 +4,7 @@ import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { OpenAiServiceTier, type ModelInfo, type ProviderSettings } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StandardTooltip } from "@src/components/ui" @@ -18,7 +18,7 @@ type OpenAIProps = { } export const OpenAI = ({ apiConfiguration, setApiConfigurationField, selectedModelInfo }: OpenAIProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [openAiNativeBaseUrlSelected, setOpenAiNativeBaseUrlSelected] = useState( !!apiConfiguration?.openAiNativeBaseUrl, diff --git a/webview-ui/src/components/settings/providers/OpenAICodex.tsx b/webview-ui/src/components/settings/providers/OpenAICodex.tsx index ee2abf0e2b..b2d259a914 100644 --- a/webview-ui/src/components/settings/providers/OpenAICodex.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICodex.tsx @@ -7,7 +7,7 @@ import { openAiCodexModels, } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { Button } from "@src/components/ui" import { vscode } from "@src/utils/vscode" @@ -28,7 +28,7 @@ export const OpenAICodex: React.FC = ({ simplifySettings, openAiCodexIsAuthenticated = false, }) => { - const { t } = useAppTranslation() + const { t } = useTranslation() return (
diff --git a/webview-ui/src/components/settings/providers/OpenAICodexRateLimitDashboard.tsx b/webview-ui/src/components/settings/providers/OpenAICodexRateLimitDashboard.tsx index d9946d1cf6..323278cbe6 100644 --- a/webview-ui/src/components/settings/providers/OpenAICodexRateLimitDashboard.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICodexRateLimitDashboard.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useState } from "react" import type { OpenAiCodexRateLimitInfo } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { vscode } from "@src/utils/vscode" interface OpenAICodexRateLimitDashboardProps { @@ -85,7 +85,7 @@ const UsageProgressBar: React.FC<{ usedPercent: number; label?: string }> = ({ u } export const OpenAICodexRateLimitDashboard: React.FC = ({ isAuthenticated }) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [rateLimits, setRateLimits] = useState(null) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) diff --git a/webview-ui/src/components/settings/providers/OpenAICodexSpeedSelector.tsx b/webview-ui/src/components/settings/providers/OpenAICodexSpeedSelector.tsx index 09faf5cd8d..4ca1671ac3 100644 --- a/webview-ui/src/components/settings/providers/OpenAICodexSpeedSelector.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICodexSpeedSelector.tsx @@ -5,7 +5,7 @@ import { type OpenAiCodexServiceTier as OpenAiCodexServiceTierValue, } from "@roo-code/types/model" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StandardTooltip } from "@src/components/ui" interface OpenAICodexSpeedSelectorProps { @@ -14,7 +14,7 @@ interface OpenAICodexSpeedSelectorProps { } export const OpenAICodexSpeedSelector: React.FC = ({ value, onValueChange }) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const selectId = React.useId() return ( diff --git a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx index efb839a7fc..0f0ce940e3 100644 --- a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx @@ -17,7 +17,7 @@ import { parseOpenAiExtraBody, } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { Button, StandardTooltip } from "@src/components/ui" import { formatOpenAiExtraBodyValidationError } from "@src/utils/validate" @@ -46,7 +46,7 @@ export const OpenAICompatible = ({ modelValidationError, simplifySettings, }: OpenAICompatibleProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const isAzureOpenAi = isAzureOpenAiBaseUrl(apiConfiguration?.openAiBaseUrl, apiConfiguration?.openAiUseAzure) const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion) diff --git a/webview-ui/src/components/settings/providers/OpenCodeGo.tsx b/webview-ui/src/components/settings/providers/OpenCodeGo.tsx index 249e2de534..dad263ae22 100644 --- a/webview-ui/src/components/settings/providers/OpenCodeGo.tsx +++ b/webview-ui/src/components/settings/providers/OpenCodeGo.tsx @@ -14,7 +14,7 @@ import { import type { RouterName } from "@roo/api" import { vscode } from "@src/utils/vscode" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { Button } from "@src/components/ui" @@ -45,7 +45,7 @@ export const OpenCodeGo = ({ modelValidationError, simplifySettings, }: OpenCodeGoProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [refreshStatus, setRefreshStatus] = useState(RefreshStatus.Idle) const [refreshError, setRefreshError] = useState() const errorJustReceived = useRef(false) diff --git a/webview-ui/src/components/settings/providers/OpenRouter.tsx b/webview-ui/src/components/settings/providers/OpenRouter.tsx index 2dba8c8459..4204b1dce0 100644 --- a/webview-ui/src/components/settings/providers/OpenRouter.tsx +++ b/webview-ui/src/components/settings/providers/OpenRouter.tsx @@ -9,7 +9,7 @@ import { openRouterDefaultModelId, } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { getOpenRouterAuthUrl } from "@src/oauth/urls" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" @@ -38,7 +38,7 @@ export const OpenRouter = ({ organizationAllowList, modelValidationError, }: OpenRouterProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl) diff --git a/webview-ui/src/components/settings/providers/Poe.tsx b/webview-ui/src/components/settings/providers/Poe.tsx index ef4cc51d4f..847814e749 100644 --- a/webview-ui/src/components/settings/providers/Poe.tsx +++ b/webview-ui/src/components/settings/providers/Poe.tsx @@ -14,7 +14,7 @@ import { import { RouterName } from "@roo/api" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { useExtensionState } from "@src/context/ExtensionStateContext" import { vscode } from "@src/utils/vscode" @@ -46,7 +46,7 @@ export const Poe = ({ modelValidationError, simplifySettings, }: PoeProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const queryClient = useQueryClient() const { routerModels } = useExtensionState() const [refreshStatus, setRefreshStatus] = useState(RefreshStatus.Idle) diff --git a/webview-ui/src/components/settings/providers/Requesty.tsx b/webview-ui/src/components/settings/providers/Requesty.tsx index 4149dbc2c8..a021d2a909 100644 --- a/webview-ui/src/components/settings/providers/Requesty.tsx +++ b/webview-ui/src/components/settings/providers/Requesty.tsx @@ -11,7 +11,7 @@ import { } from "@roo-code/types" import { vscode } from "@src/utils/vscode" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { Button } from "@src/components/ui" import { inputEventTransform } from "../transforms" @@ -40,7 +40,7 @@ export const Requesty = ({ uriScheme, simplifySettings, }: RequestyProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [requestyEndpointSelected, setRequestyEndpointSelected] = useState(!!apiConfiguration.requestyBaseUrl) diff --git a/webview-ui/src/components/settings/providers/SambaNova.tsx b/webview-ui/src/components/settings/providers/SambaNova.tsx index 9202f8d06e..a4b34ff190 100644 --- a/webview-ui/src/components/settings/providers/SambaNova.tsx +++ b/webview-ui/src/components/settings/providers/SambaNova.tsx @@ -3,7 +3,7 @@ import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import type { ProviderSettings } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { inputEventTransform } from "../transforms" @@ -14,7 +14,7 @@ type SambaNovaProps = { } export const SambaNova = ({ apiConfiguration, setApiConfigurationField }: SambaNovaProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const handleInputChange = useCallback( ( diff --git a/webview-ui/src/components/settings/providers/Unbound.tsx b/webview-ui/src/components/settings/providers/Unbound.tsx index b3211f0f8f..2e9c644b32 100644 --- a/webview-ui/src/components/settings/providers/Unbound.tsx +++ b/webview-ui/src/components/settings/providers/Unbound.tsx @@ -11,7 +11,7 @@ import { } from "@roo-code/types" import { vscode } from "@src/utils/vscode" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { Button } from "@src/components/ui" import { inputEventTransform } from "../transforms" @@ -35,7 +35,7 @@ export const Unbound = ({ modelValidationError, simplifySettings, }: UnboundProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const handleInputChange = useCallback( ( diff --git a/webview-ui/src/components/settings/providers/VSCodeLM.tsx b/webview-ui/src/components/settings/providers/VSCodeLM.tsx index b14c38c9a7..8ea8778b8f 100644 --- a/webview-ui/src/components/settings/providers/VSCodeLM.tsx +++ b/webview-ui/src/components/settings/providers/VSCodeLM.tsx @@ -11,7 +11,7 @@ import { providerIdentifiers, } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { Button, StandardTooltip } from "@src/components/ui" import { vscode } from "@src/utils/vscode" import { useGitHubCopilotModels } from "@src/components/ui/hooks/useGitHubCopilotModels" @@ -31,7 +31,7 @@ type VSCodeLMProps = { const NO_MODELS: NonNullable = [] export const VSCodeLM = ({ apiConfiguration, setApiConfigurationField }: VSCodeLMProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const isCopilot = apiConfiguration.apiProvider === providerIdentifiers.githubCopilot // The legacy provider is fed by messages into local state; Copilot's list is the shared, extension-pushed one. diff --git a/webview-ui/src/components/settings/providers/VercelAiGateway.tsx b/webview-ui/src/components/settings/providers/VercelAiGateway.tsx index 0951f39c57..2d993ccc37 100644 --- a/webview-ui/src/components/settings/providers/VercelAiGateway.tsx +++ b/webview-ui/src/components/settings/providers/VercelAiGateway.tsx @@ -9,7 +9,7 @@ import { providerIdentifiers, } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { inputEventTransform } from "../transforms" @@ -32,7 +32,7 @@ export const VercelAiGateway = ({ modelValidationError, simplifySettings, }: VercelAiGatewayProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const handleInputChange = useCallback( ( diff --git a/webview-ui/src/components/settings/providers/Vertex.tsx b/webview-ui/src/components/settings/providers/Vertex.tsx index 32f833ffbf..4f77b4da19 100644 --- a/webview-ui/src/components/settings/providers/Vertex.tsx +++ b/webview-ui/src/components/settings/providers/Vertex.tsx @@ -1,11 +1,10 @@ import { useCallback, useMemo } from "react" -import { Trans } from "react-i18next" import { Checkbox } from "vscrui" import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { type ProviderSettings, VERTEX_REGIONS, VERTEX_1M_CONTEXT_MODEL_IDS, looksLikeFilePath } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { Trans, useTranslation } from "react-i18next" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui" import { inputEventTransform } from "../transforms" @@ -16,7 +15,7 @@ type VertexProps = { } export const Vertex = ({ apiConfiguration, setApiConfigurationField }: VertexProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() // Check if the selected model supports 1M context (supported Claude 4 models) const supports1MContextBeta = @@ -80,6 +79,7 @@ export const Vertex = ({ apiConfiguration, setApiConfigurationField }: VertexPro role="status" className="text-sm text-vscode-errorForeground"> , diff --git a/webview-ui/src/components/settings/providers/XAI.tsx b/webview-ui/src/components/settings/providers/XAI.tsx index 619f223901..b9fb04322c 100644 --- a/webview-ui/src/components/settings/providers/XAI.tsx +++ b/webview-ui/src/components/settings/providers/XAI.tsx @@ -3,7 +3,7 @@ import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import type { ProviderSettings } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { inputEventTransform } from "../transforms" @@ -14,7 +14,7 @@ type XAIProps = { } export const XAI = ({ apiConfiguration, setApiConfigurationField }: XAIProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const handleInputChange = useCallback( ( diff --git a/webview-ui/src/components/settings/providers/ZAi.tsx b/webview-ui/src/components/settings/providers/ZAi.tsx index 3dab8964ff..04114840a8 100644 --- a/webview-ui/src/components/settings/providers/ZAi.tsx +++ b/webview-ui/src/components/settings/providers/ZAi.tsx @@ -3,7 +3,7 @@ import { VSCodeTextField, VSCodeDropdown, VSCodeOption } from "@vscode/webview-u import { type ProviderSettings, zaiApiLineConfigs, zaiApiLineSchema } from "@roo-code/types" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { inputEventTransform } from "../transforms" @@ -15,7 +15,7 @@ type ZAiProps = { } export const ZAi = ({ apiConfiguration, setApiConfigurationField }: ZAiProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const handleInputChange = useCallback( ( diff --git a/webview-ui/src/components/settings/providers/ZooGateway.tsx b/webview-ui/src/components/settings/providers/ZooGateway.tsx index bd2b35274c..ee87e8d6e2 100644 --- a/webview-ui/src/components/settings/providers/ZooGateway.tsx +++ b/webview-ui/src/components/settings/providers/ZooGateway.tsx @@ -9,7 +9,7 @@ import { import { useExtensionState } from "@src/context/ExtensionStateContext" import { getZooCodeAuthUrl } from "@src/oauth/urls" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { ModelPicker } from "../ModelPicker" @@ -57,7 +57,7 @@ export const ZooGateway = ({ modelValidationError, simplifySettings, }: ZooGatewayProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const { zooCodeIsAuthenticated, zooCodeUserEmail, zooCodeUserName, zooCodeBaseUrl, uriScheme, deviceName } = useExtensionState() diff --git a/webview-ui/src/components/settings/providers/__tests__/Bedrock.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Bedrock.spec.tsx index 307696fdd9..298b740a1a 100644 --- a/webview-ui/src/components/settings/providers/__tests__/Bedrock.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/Bedrock.spec.tsx @@ -53,8 +53,8 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({ })) // Mock the translation hook -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/settings/providers/__tests__/Friendli.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Friendli.spec.tsx index 5ccab1c7bb..c7517c0a0d 100644 --- a/webview-ui/src/components/settings/providers/__tests__/Friendli.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/Friendli.spec.tsx @@ -5,8 +5,8 @@ import type { ProviderSettings } from "@roo-code/types" import { Friendli } from "../Friendli" -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/settings/providers/__tests__/Gemini.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Gemini.spec.tsx index 70c762dd35..062b56e7b9 100644 --- a/webview-ui/src/components/settings/providers/__tests__/Gemini.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/Gemini.spec.tsx @@ -20,8 +20,8 @@ vi.mock("vscrui", () => ({ ), })) -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ t: (key: string) => key }), +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), })) vi.mock("@src/components/common/VSCodeButtonLink", () => ({ diff --git a/webview-ui/src/components/settings/providers/__tests__/IOIntelligence.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/IOIntelligence.spec.tsx index ae6f7bc63a..fa660ae560 100644 --- a/webview-ui/src/components/settings/providers/__tests__/IOIntelligence.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/IOIntelligence.spec.tsx @@ -15,8 +15,8 @@ const { postMessageMock } = vi.hoisted(() => ({ postMessageMock: vi.fn() })) vi.mock("@src/utils/vscode", () => ({ vscode: { postMessage: postMessageMock } })) -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ t: (key: string) => key }), +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), })) vi.mock("@vscode/webview-ui-toolkit/react", () => ({ diff --git a/webview-ui/src/components/settings/providers/__tests__/Kenari.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Kenari.spec.tsx index e9fb7ce34c..a5e2baa4ff 100644 --- a/webview-ui/src/components/settings/providers/__tests__/Kenari.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/Kenari.spec.tsx @@ -14,8 +14,8 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({ ), })) -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ t: (key: string) => key }), +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), })) vi.mock("@src/components/common/VSCodeButtonLink", () => ({ diff --git a/webview-ui/src/components/settings/providers/__tests__/LiteLLM.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/LiteLLM.spec.tsx index c6cd779658..a4c13af26e 100644 --- a/webview-ui/src/components/settings/providers/__tests__/LiteLLM.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/LiteLLM.spec.tsx @@ -23,8 +23,8 @@ vi.mock("@src/context/ExtensionStateContext", () => ({ useExtensionState: mockUseExtensionState, })) -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ t: (key: string) => key }), +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), })) vi.mock("@vscode/webview-ui-toolkit/react", () => ({ diff --git a/webview-ui/src/components/settings/providers/__tests__/Moonshot.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Moonshot.spec.tsx index 5c5d1b41b9..ce7628e144 100644 --- a/webview-ui/src/components/settings/providers/__tests__/Moonshot.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/Moonshot.spec.tsx @@ -8,8 +8,8 @@ import { allRouterModelsProvider, providerIdentifiers, type ProviderSettings } f import { Moonshot } from "../Moonshot" // Mock the translation hook -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/settings/providers/__tests__/NanoGPT.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/NanoGPT.spec.tsx index 698fa90e29..78cde7b810 100644 --- a/webview-ui/src/components/settings/providers/__tests__/NanoGPT.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/NanoGPT.spec.tsx @@ -16,8 +16,8 @@ const { postMessageMock } = vi.hoisted(() => ({ postMessageMock: vi.fn() })) vi.mock("@src/utils/vscode", () => ({ vscode: { postMessage: postMessageMock } })) -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ t: (key: string) => key }), +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), })) vi.mock("@vscode/webview-ui-toolkit/react", () => ({ diff --git a/webview-ui/src/components/settings/providers/__tests__/Ollama.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Ollama.spec.tsx index a4e14b4d81..606779e7d0 100644 --- a/webview-ui/src/components/settings/providers/__tests__/Ollama.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/Ollama.spec.tsx @@ -37,8 +37,8 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({ })) // Mock the translation hook -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/settings/providers/__tests__/Ollama.visual.fixture.tsx b/webview-ui/src/components/settings/providers/__tests__/Ollama.visual.fixture.tsx index 49cfdeb783..7769676957 100644 --- a/webview-ui/src/components/settings/providers/__tests__/Ollama.visual.fixture.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/Ollama.visual.fixture.tsx @@ -1,12 +1,11 @@ /* v8 ignore file -- Playwright component fixture is covered by the visual test. */ import React, { useState } from "react" -import i18next from "i18next" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import { I18nextProvider } from "react-i18next" import { providerIdentifiers, type ProviderSettings } from "@roo-code/types" -import { TranslationContext as AppTranslationContext } from "@/i18n/TranslationContext" -import { TranslationContext as PlaywrightTranslationContext } from "@src/i18n/TranslationContext" +import { createFixtureI18n } from "@src/i18n/__tests__/createFixtureI18n.visual.i18n" import { TooltipProvider } from "@src/components/ui/tooltip" import { Ollama } from "../Ollama" import enSettings from "@/i18n/locales/en/settings.json" @@ -24,10 +23,7 @@ function flattenTranslations(obj: Record, prefix = "settings:") return result } -// An uninitialized instance is a valid `i18n` for the context. The stories read `t`. -const i18n = i18next.createInstance() - -const translations = flattenTranslations(enSettings as Record) +const i18n = createFixtureI18n(flattenTranslations(enSettings as Record)) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.ollama, @@ -45,19 +41,9 @@ const Providers = ({ children }: React.PropsWithChildren) => { ) return ( - translations[key] ?? key, - i18n, - }}> - translations[key] ?? key, - i18n, - }}> - {children} - - + + {children} + ) } diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenAI.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenAI.spec.tsx index 239c76932f..a32276dc6f 100644 --- a/webview-ui/src/components/settings/providers/__tests__/OpenAI.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/OpenAI.spec.tsx @@ -6,8 +6,8 @@ import { fireEvent, render, screen } from "@/utils/test-utils" import { OpenAI } from "../OpenAI" -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ t: (key: string) => key }), +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), })) vi.mock("vscrui", () => ({ diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.spec.tsx index d73120ada3..355a1ea1ea 100644 --- a/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.spec.tsx @@ -12,8 +12,8 @@ import { vscode } from "@src/utils/vscode" import { OpenAICodex } from "../OpenAICodex" -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => ({ "settings:openAiCodexSpeed.label": "Speed", diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.visual.fixture.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.visual.fixture.tsx index 2061f00614..cf96f0d377 100644 --- a/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.visual.fixture.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.visual.fixture.tsx @@ -3,7 +3,9 @@ import React from "react" import { OpenAiCodexServiceTier } from "@roo-code/types/model" -import { TranslationContext } from "@src/i18n/TranslationContext" +import { I18nextProvider } from "react-i18next" + +import { createFixtureI18n } from "@src/i18n/__tests__/createFixtureI18n.visual.i18n" import { TooltipProvider } from "@src/components/ui/tooltip" import { OpenAICodexSpeedSelector } from "../OpenAICodexSpeedSelector" @@ -16,17 +18,15 @@ const translations: Record = { "settings:openAiCodexSpeed.fast": "Fast (1.5x speed, increased usage)", } +const i18n = createFixtureI18n(translations) + export const OpenAICodexFixture = () => ( - translations[key] ?? key, - i18n: null as unknown as typeof import("../../../../i18n/setup").default, - }}> +
{}} /> {}} />
-
+ ) diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenAICodexRateLimitDashboard.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenAICodexRateLimitDashboard.spec.tsx index 68145afb84..eb8eae9641 100644 --- a/webview-ui/src/components/settings/providers/__tests__/OpenAICodexRateLimitDashboard.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/OpenAICodexRateLimitDashboard.spec.tsx @@ -2,8 +2,8 @@ import { render, screen, waitFor } from "@/utils/test-utils" import { OpenAICodexRateLimitDashboard } from "../OpenAICodexRateLimitDashboard" -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string, options?: Record) => { switch (key) { case "settings:providers.openAiCodexRateLimits.title": diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx index ce7337b67a..c65c16d965 100644 --- a/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx @@ -61,8 +61,8 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({ })) // Mock the translation hook -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.visual.fixture.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.visual.fixture.tsx index 9e0aeb71bc..990a2992f2 100644 --- a/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.visual.fixture.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.visual.fixture.tsx @@ -4,8 +4,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { providerIdentifiers, type ProviderSettings } from "@roo-code/types" -import { TranslationContext as AppTranslationContext } from "@/i18n/TranslationContext" -import { TranslationContext as PlaywrightTranslationContext } from "@src/i18n/TranslationContext" +import { I18nextProvider } from "react-i18next" + +import { createFixtureI18n } from "@src/i18n/__tests__/createFixtureI18n.visual.i18n" import { TooltipProvider } from "@src/components/ui/tooltip" import { OpenAICompatible } from "../OpenAICompatible" import enSettings from "@/i18n/locales/en/settings.json" @@ -23,7 +24,7 @@ function flattenTranslations(obj: Record, prefix = "settings:") return result } -const translations = flattenTranslations(enSettings as Record) +const i18n = createFixtureI18n(flattenTranslations(enSettings as Record)) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.openai, @@ -48,19 +49,9 @@ const Providers = ({ children }: React.PropsWithChildren) => { ) return ( - translations[key] ?? key, - i18n: null as unknown as typeof import("../../../../i18n/setup").default, - }}> - translations[key] ?? key, - i18n: null as unknown as typeof import("../../../../i18n/setup").default, - }}> - {children} - - + + {children} + ) } diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenCodeGo.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenCodeGo.spec.tsx index 9fcfd2f102..6a42603374 100644 --- a/webview-ui/src/components/settings/providers/__tests__/OpenCodeGo.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/OpenCodeGo.spec.tsx @@ -15,8 +15,8 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({ ), })) -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ t: (key: string) => key }), +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), })) vi.mock("@src/components/common/VSCodeButtonLink", () => ({ diff --git a/webview-ui/src/components/settings/providers/__tests__/Poe.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Poe.spec.tsx index 34e5d5c2d8..47da5d7a64 100644 --- a/webview-ui/src/components/settings/providers/__tests__/Poe.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/Poe.spec.tsx @@ -23,8 +23,8 @@ vi.mock("@src/context/ExtensionStateContext", () => ({ useExtensionState: mockUseExtensionState, })) -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ t: (key: string) => key }), +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), })) vi.mock("@vscode/webview-ui-toolkit/react", () => ({ diff --git a/webview-ui/src/components/settings/providers/__tests__/ProviderRouting.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/ProviderRouting.spec.tsx index d165ead136..077748cc38 100644 --- a/webview-ui/src/components/settings/providers/__tests__/ProviderRouting.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/ProviderRouting.spec.tsx @@ -21,8 +21,8 @@ const { modelPickerMock } = vi.hoisted(() => ({ modelPickerMock: vi.fn((_props: React.ComponentProps) => null), })) -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string, options?: { account?: string }) => key === "settings:providers.githubCopilot.signedIn" ? `Signed in as ${options?.account}` : key, }), diff --git a/webview-ui/src/components/settings/providers/__tests__/Requesty.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Requesty.spec.tsx index bb7d4e234f..52302c6bfd 100644 --- a/webview-ui/src/components/settings/providers/__tests__/Requesty.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/Requesty.spec.tsx @@ -6,8 +6,8 @@ import { vscode } from "@src/utils/vscode" import { Requesty } from "../Requesty" -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ t: (key: string) => key }), +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), })) vi.mock("@vscode/webview-ui-toolkit/react", () => ({ diff --git a/webview-ui/src/components/settings/providers/__tests__/Vertex.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Vertex.spec.tsx index 93b682b86d..a1e1cca4fc 100644 --- a/webview-ui/src/components/settings/providers/__tests__/Vertex.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/Vertex.spec.tsx @@ -23,16 +23,8 @@ vi.mock("vscrui", () => ({ ), })) -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ t: (key: string) => key }), -})) - -// The component uses for the path-shape warning so it can interpolate -// / elements. Resolve the i18n key against the real English -// resource so the test fails if the warning copy drops the "Google Cloud Key -// File Path" field name or the "GOOGLE_APPLICATION_CREDENTIALS" env var -// mention — those are the actual remediation hints users need. vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), Trans: ({ i18nKey }: { i18nKey: string }) => { // Keys are ":"; the spec only renders the // settings namespace so resolve against the imported English bundle. @@ -48,6 +40,12 @@ vi.mock("react-i18next", () => ({ }, })) +// The component uses for the path-shape warning so it can interpolate +// / elements. Resolve the i18n key against the real English +// resource so the test fails if the warning copy drops the "Google Cloud Key +// File Path" field name or the "GOOGLE_APPLICATION_CREDENTIALS" env var +// mention — those are the actual remediation hints users need. + vi.mock("@src/components/ui", () => ({ Select: ({ children, value, onValueChange }: any) => (
diff --git a/webview-ui/src/components/settings/providers/__tests__/ZooGateway.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/ZooGateway.spec.tsx index 4f063b57ba..46341fdcf8 100644 --- a/webview-ui/src/components/settings/providers/__tests__/ZooGateway.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/ZooGateway.spec.tsx @@ -5,8 +5,8 @@ import type { ModelInfo, ProviderSettings, RouterModels } from "@roo-code/types" import { ZooGateway, pickZooGatewayDefaultModelId } from "../ZooGateway" -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, }), })) diff --git a/webview-ui/src/components/welcome/RooTips.tsx b/webview-ui/src/components/welcome/RooTips.tsx index b180a53c26..ec61515f84 100644 --- a/webview-ui/src/components/welcome/RooTips.tsx +++ b/webview-ui/src/components/welcome/RooTips.tsx @@ -1,6 +1,5 @@ import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" -import { useTranslation } from "react-i18next" -import { Trans } from "react-i18next" +import { Trans, useTranslation } from "react-i18next" import { buildDocLink } from "@src/utils/docLinks" import { ReplaceAll, Users } from "lucide-react" @@ -26,7 +25,7 @@ const RooTips = () => { return (

- +

{tips.map((tip) => ( @@ -43,6 +42,7 @@ const RooTips = () => {

@@ -22,7 +20,7 @@ export function WelcomeLanding({ onGetStarted, onImportSettings }: WelcomeLandin

- +

diff --git a/webview-ui/src/components/welcome/WelcomeViewProvider.tsx b/webview-ui/src/components/welcome/WelcomeViewProvider.tsx index 3a36b3634e..9a77077cdb 100644 --- a/webview-ui/src/components/welcome/WelcomeViewProvider.tsx +++ b/webview-ui/src/components/welcome/WelcomeViewProvider.tsx @@ -1,5 +1,4 @@ import { useCallback, useState } from "react" -import { Trans } from "react-i18next" import { ArrowLeft, Brain } from "lucide-react" import { openRouterDefaultModelId, providerIdentifiers, type ProviderSettings } from "@roo-code/types" @@ -7,7 +6,8 @@ import { openRouterDefaultModelId, providerIdentifiers, type ProviderSettings } import { useExtensionState } from "@src/context/ExtensionStateContext" import { validateApiConfiguration } from "@src/utils/validate" import { vscode } from "@src/utils/vscode" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import type { TFunction } from "i18next" +import { Trans, useTranslation } from "react-i18next" import { Button } from "@src/components/ui" import ApiOptions from "../settings/ApiOptions" @@ -21,6 +21,7 @@ const DEFAULT_WELCOME_API_CONFIGURATION: ProviderSettings = { } const getWelcomeApiConfiguration = ( + t: TFunction, apiConfiguration?: ProviderSettings, zooCodeIsAuthenticated?: boolean, ): ProviderSettings => { @@ -30,7 +31,7 @@ const getWelcomeApiConfiguration = ( return DEFAULT_WELCOME_API_CONFIGURATION } - const validationError = validateApiConfiguration(apiConfiguration, undefined, undefined, zooCodeIsAuthenticated) + const validationError = validateApiConfiguration(apiConfiguration, t, undefined, undefined, zooCodeIsAuthenticated) if (validationError) { return DEFAULT_WELCOME_API_CONFIGURATION } @@ -41,12 +42,12 @@ const getWelcomeApiConfiguration = ( const WelcomeViewProvider = () => { const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme, zooCodeIsAuthenticated } = useExtensionState() - const { t } = useAppTranslation() + const { t } = useTranslation() const [errorMessage, setErrorMessage] = useState(undefined) const [showProviderSetup, setShowProviderSetup] = useState(false) const [welcomeApiConfiguration, setWelcomeApiConfiguration] = useState() const effectiveApiConfiguration = - welcomeApiConfiguration ?? getWelcomeApiConfiguration(apiConfiguration, zooCodeIsAuthenticated) + welcomeApiConfiguration ?? getWelcomeApiConfiguration(t, apiConfiguration, zooCodeIsAuthenticated) const setApiConfigurationFieldForApiOptions = useCallback( (field: K, value: ProviderSettings[K]) => { @@ -61,7 +62,7 @@ const WelcomeViewProvider = () => { const handleGetStarted = useCallback(() => { if (!showProviderSetup) { - const initialApiConfiguration = getWelcomeApiConfiguration(apiConfiguration, zooCodeIsAuthenticated) + const initialApiConfiguration = getWelcomeApiConfiguration(t, apiConfiguration, zooCodeIsAuthenticated) setWelcomeApiConfiguration(initialApiConfiguration) setApiConfiguration(initialApiConfiguration) @@ -70,7 +71,13 @@ const WelcomeViewProvider = () => { return } - const error = validateApiConfiguration(effectiveApiConfiguration, undefined, undefined, zooCodeIsAuthenticated) + const error = validateApiConfiguration( + effectiveApiConfiguration, + t, + undefined, + undefined, + zooCodeIsAuthenticated, + ) if (error) { setErrorMessage(error) @@ -90,6 +97,7 @@ const WelcomeViewProvider = () => { effectiveApiConfiguration, currentApiConfigName, zooCodeIsAuthenticated, + t, ]) if (!showProviderSetup) { @@ -108,7 +116,7 @@ const WelcomeViewProvider = () => {

{t("welcome:providerSignup.heading")}

- +

diff --git a/webview-ui/src/components/welcome/__tests__/WelcomeViewProvider.spec.tsx b/webview-ui/src/components/welcome/__tests__/WelcomeViewProvider.spec.tsx index c15c0608ec..e7e2ef49e8 100644 --- a/webview-ui/src/components/welcome/__tests__/WelcomeViewProvider.spec.tsx +++ b/webview-ui/src/components/welcome/__tests__/WelcomeViewProvider.spec.tsx @@ -64,11 +64,10 @@ vi.mock("@src/utils/vscode", () => ({ })) vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), Trans: ({ i18nKey, children }: any) => {children || i18nKey}, - initReactI18next: { - type: "3rdParty", - init: () => {}, - }, })) vi.mock("i18next", () => ({ @@ -77,12 +76,6 @@ vi.mock("i18next", () => ({ }, })) -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ - t: (key: string) => key, - }), -})) - // Mock buildDocLink vi.mock("@/utils/docLinks", () => ({ buildDocLink: (path: string, source: string) => `https://docs.zoocode.dev/${path}?utm_source=${source}`, diff --git a/webview-ui/src/components/worktrees/CreateWorktreeModal.tsx b/webview-ui/src/components/worktrees/CreateWorktreeModal.tsx index 84743c1650..0bd1c8f3bd 100644 --- a/webview-ui/src/components/worktrees/CreateWorktreeModal.tsx +++ b/webview-ui/src/components/worktrees/CreateWorktreeModal.tsx @@ -4,7 +4,7 @@ import prettyBytes from "pretty-bytes" import type { WorktreeDefaultsResponse, BranchInfo, WorktreeIncludeStatus } from "@roo-code/types" import { vscode } from "@/utils/vscode" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Button, Input } from "@/components/ui" import { SearchableSelect, type SearchableSelectOption } from "@/components/ui/searchable-select" import { CornerDownRight, Folder, FolderSearch, Info } from "lucide-react" @@ -22,7 +22,7 @@ export const CreateWorktreeModal = ({ openAfterCreate = false, onSuccess, }: CreateWorktreeModalProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() // Form state const [branchName, setBranchName] = useState("") diff --git a/webview-ui/src/components/worktrees/DeleteWorktreeModal.tsx b/webview-ui/src/components/worktrees/DeleteWorktreeModal.tsx index 9e3f4802d5..27fdd4c9ac 100644 --- a/webview-ui/src/components/worktrees/DeleteWorktreeModal.tsx +++ b/webview-ui/src/components/worktrees/DeleteWorktreeModal.tsx @@ -3,7 +3,7 @@ import { useState, useEffect, useCallback } from "react" import type { Worktree } from "@roo-code/types" import { vscode } from "@/utils/vscode" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Button, Checkbox } from "@/components/ui" import { Folder, GitBranch, TriangleAlert } from "lucide-react" @@ -15,7 +15,7 @@ interface DeleteWorktreeModalProps { } export const DeleteWorktreeModal = ({ open, onClose, worktree, onSuccess }: DeleteWorktreeModalProps) => { - const { t } = useAppTranslation() + const { t } = useTranslation() const [isDeleting, setIsDeleting] = useState(false) const [forceDeleteLocked, setForceDeleteLocked] = useState(false) diff --git a/webview-ui/src/components/worktrees/WorktreesView.tsx b/webview-ui/src/components/worktrees/WorktreesView.tsx index 1df762c207..346f3341e0 100644 --- a/webview-ui/src/components/worktrees/WorktreesView.tsx +++ b/webview-ui/src/components/worktrees/WorktreesView.tsx @@ -4,7 +4,7 @@ import type { Worktree, WorktreeListResponse, WorktreeIncludeStatus } from "@roo import { Badge, Button, StandardTooltip, ToggleSwitch } from "@/components/ui" import { useExtensionState } from "@/context/ExtensionStateContext" -import { useAppTranslation } from "@/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { vscode } from "@/utils/vscode" import { SectionHeader } from "../settings/SectionHeader" @@ -14,7 +14,7 @@ import { DeleteWorktreeModal } from "./DeleteWorktreeModal" import { Folder, GitBranch, Lock, Plus, SquareArrowOutUpRight, Trash } from "lucide-react" export const WorktreesView = () => { - const { t } = useAppTranslation() + const { t } = useTranslation() const { showWorktreesInHomeScreen, setShowWorktreesInHomeScreen } = useExtensionState() // State diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index c4fa00f479..d1e59efe06 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -35,6 +35,8 @@ import { experimentDefault } from "@roo/experiments" import { vscode } from "@src/utils/vscode" import { convertTextMateToHljs } from "@src/utils/textMateToHljs" +import i18next from "i18next" +import { applyLanguage } from "@src/i18n/language" export interface ExtensionStateContextType extends ExtensionState { historyPreviewCollapsed?: boolean // Add the new state property @@ -346,6 +348,7 @@ export const ExtensionStateContextProvider: React.FC<{ switch (message.type) { case "state": { const newState = message.state ?? {} + applyLanguage(i18next, newState.language) setState((prevState) => mergeExtensionState(prevState, newState)) setShowWelcome(!checkExistKey(newState.apiConfiguration, newState.zooCodeIsAuthenticated)) setDidHydrateState(true) diff --git a/webview-ui/src/hooks/useTooManyTools.ts b/webview-ui/src/hooks/useTooManyTools.ts index 75a3d834c6..f486098982 100644 --- a/webview-ui/src/hooks/useTooManyTools.ts +++ b/webview-ui/src/hooks/useTooManyTools.ts @@ -1,6 +1,6 @@ import { useMemo } from "react" import { useExtensionState } from "@src/context/ExtensionStateContext" -import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useTranslation } from "react-i18next" import { MAX_MCP_TOOLS_THRESHOLD, countEnabledMcpTools } from "@roo-code/types" export interface TooManyToolsInfo { @@ -31,7 +31,7 @@ export interface TooManyToolsInfo { * } */ export function useTooManyTools(): TooManyToolsInfo { - const { t } = useAppTranslation() + const { t } = useTranslation() const { mcpServers } = useExtensionState() const { enabledServerCount, enabledToolCount } = useMemo(() => countEnabledMcpTools(mcpServers), [mcpServers]) diff --git a/webview-ui/src/i18n/TranslationContext.tsx b/webview-ui/src/i18n/TranslationContext.tsx deleted file mode 100644 index f2950ae2f8..0000000000 --- a/webview-ui/src/i18n/TranslationContext.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React, { createContext, useContext, ReactNode, useEffect, useCallback } from "react" -import { useTranslation } from "react-i18next" -import i18next, { loadTranslations } from "./setup" -import { useExtensionState } from "@/context/ExtensionStateContext" - -// Create context for translations -export const TranslationContext = createContext<{ - t: (key: string, options?: Record) => string - i18n: typeof i18next -}>({ - t: (key: string) => key, - i18n: i18next, -}) - -// Translation provider component -const TranslationProvider: React.FC<{ children: ReactNode }> = ({ children }) => { - // Initialize with default configuration - const { i18n } = useTranslation() - // Get the extension state directly - it already contains all state properties - const extensionState = useExtensionState() - - // Load translations once when the component mounts - useEffect(() => { - try { - loadTranslations() - } catch (error) { - console.error("Failed to load translations:", error) - } - }, []) - - useEffect(() => { - i18n.changeLanguage(extensionState.language) - }, [i18n, extensionState.language]) - - // Memoize the translation function to prevent unnecessary re-renders - const translate = useCallback( - (key: string, options?: Record) => { - return i18n.t(key, options) - }, - [i18n], - ) - - return ( - - {children} - - ) -} - -// Custom hook for easy translations -export const useAppTranslation = () => useContext(TranslationContext) - -export default TranslationProvider diff --git a/webview-ui/src/i18n/__mocks__/TranslationContext.tsx b/webview-ui/src/i18n/__mocks__/TranslationContext.tsx deleted file mode 100644 index 3fdfcbdc2c..0000000000 --- a/webview-ui/src/i18n/__mocks__/TranslationContext.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React, { createContext, useContext, ReactNode } from "react" - -// Mock translation function that returns English text for settings or the key itself -const mockTranslate = (key: string, options?: Record): string => { - // Convert the key back to approximate English text for test purposes - if (key.startsWith("settings.")) { - // For specific keys the tests are looking for - if (key === "settings.notifications.sound.label") return "Enable sound effects" - if (key === "settings.autoApprove.execute.label") return "Always approve allowed execute operations" - if (key === "settings.autoApprove.execute.allowedCommands") return "Allowed Auto-Execute Commands" - if (key === "settings.autoApprove.execute.commandPlaceholder") return "Enter command prefix" - if (key === "settings.autoApprove.execute.addButton") return "Add" - if (key === "settings.common.save") return "Save" - if (key === "settings.contextManagement.terminal.label") return "Terminal output limit" - if (key === "settings.header.title") return "Settings" - - // Default handling of other keys - return key.split(".").pop() || key - } - - // For keys that contain variables - if (options) { - let result = key - Object.entries(options).forEach(([varName, value]) => { - result = result.replace(`{${varName}}`, String(value)) - }) - return result - } - - return key -} - -// Create mock context -export const TranslationContext = createContext<{ - t: (key: string, options?: Record) => string - i18n: any -}>({ - t: mockTranslate, - i18n: {}, -}) - -// Mock translation provider component -export const TranslationProvider: React.FC<{ children: ReactNode }> = ({ children }) => { - return ( - - {children} - - ) -} - -// Custom hook for translations -export const useAppTranslation = () => useContext(TranslationContext) - -export default TranslationProvider diff --git a/webview-ui/src/i18n/__tests__/TranslationContext.spec.tsx b/webview-ui/src/i18n/__tests__/TranslationContext.spec.tsx deleted file mode 100644 index cdf0c21acf..0000000000 --- a/webview-ui/src/i18n/__tests__/TranslationContext.spec.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { render } from "@/utils/test-utils" - -import TranslationProvider, { useAppTranslation } from "../TranslationContext" - -vi.mock("@/context/ExtensionStateContext", () => ({ - useExtensionState: () => ({ - language: "en", - }), -})) - -vi.mock("react-i18next", () => ({ - useTranslation: () => ({ - i18n: { - t: (key: string, options?: Record) => { - // Mock specific translations used in tests - if (key === "settings.autoApprove.title") return "Auto-Approve" - if (key === "notifications.error") { - return options?.message ? `Operation failed: ${options.message}` : "Operation failed" - } - return key - }, - changeLanguage: vi.fn(), - }, - }), -})) - -vi.mock("../setup", () => ({ - default: { - t: (key: string, options?: Record) => { - // Mock specific translations used in tests - if (key === "settings.autoApprove.title") return "Auto-Approve" - if (key === "notifications.error") { - return options?.message ? `Operation failed: ${options.message}` : "Operation failed" - } - return key - }, - changeLanguage: vi.fn(), - }, - loadTranslations: vi.fn(), -})) - -const TestComponent = () => { - const { t } = useAppTranslation() - return ( -
-

{t("settings.autoApprove.title")}

-

{t("notifications.error", { message: "Test error" })}

-
- ) -} - -describe("TranslationContext", () => { - it("should provide translations via context", () => { - const { getByTestId } = render( - - - , - ) - - // Check if translation is provided correctly - expect(getByTestId("translation-test")).toHaveTextContent("Auto-Approve") - }) - - it("should handle interpolation correctly", () => { - const { getByTestId } = render( - - - , - ) - - // Check if interpolation works - expect(getByTestId("translation-interpolation")).toHaveTextContent("Operation failed: Test error") - }) -}) diff --git a/webview-ui/src/i18n/__tests__/createFixtureI18n.visual.i18n.ts b/webview-ui/src/i18n/__tests__/createFixtureI18n.visual.i18n.ts new file mode 100644 index 0000000000..533ef78ea0 --- /dev/null +++ b/webview-ui/src/i18n/__tests__/createFixtureI18n.visual.i18n.ts @@ -0,0 +1,35 @@ +import i18next, { type i18n } from "i18next" + +/** + * An i18next instance for a visual fixture that serves exactly `translations`, keyed `"ns:dotted.key"`. + * Pass it to react-i18next's `I18nextProvider`. Missing keys render as the qualified key. + */ +export function createFixtureI18n(translations: Record): i18n { + const resources: Record> = {} + + for (const [qualifiedKey, value] of Object.entries(translations)) { + const [ns, key] = qualifiedKey.split(":") + const path = key.split(".") + let node = (resources[ns] ??= {}) + + for (const segment of path.slice(0, -1)) { + node = (node[segment] ??= {}) as Record + } + + node[path[path.length - 1]] = value + } + + const instance = i18next.createInstance() + + // Inline resources and `initAsync: false` make the instance ready before the fixture renders. + void instance.init({ + lng: "en", + resources: { en: resources }, + ns: Object.keys(resources), + initAsync: false, + appendNamespaceToMissingKey: true, + interpolation: { escapeValue: false }, + }) + + return instance +} diff --git a/webview-ui/src/i18n/__tests__/hydrationLanguage.spec.tsx b/webview-ui/src/i18n/__tests__/hydrationLanguage.spec.tsx new file mode 100644 index 0000000000..1e40c27ada --- /dev/null +++ b/webview-ui/src/i18n/__tests__/hydrationLanguage.spec.tsx @@ -0,0 +1,71 @@ +import { StrictMode, useMemo } from "react" +import { act, render, screen } from "@testing-library/react" +import { useTranslation } from "react-i18next" + +import { ExtensionStateContextProvider, useExtensionState } from "@/context/ExtensionStateContext" + +import i18next from "../setup" +import { applyLanguage } from "../language" + +// A `useTranslation` consumer that caches its output on `t`, as React Compiler does. Under StrictMode +// its `t` is reset to the first identity, so it only renders the hydrated language if that language is +// applied before the hydrating render. +const TipsLikeConsumer = () => { + const { t } = useTranslation("chat") + const label = useMemo(() => t("rooTips.customizableModes.title"), [t]) + return {label} +} + +const HydrationGate = () => { + const { didHydrateState } = useExtensionState() + return didHydrateState ? : null +} + +const postState = (state: Record) => + act(async () => { + window.dispatchEvent(new MessageEvent("message", { data: { type: "state", state } })) + }) + +describe("hydrated language", () => { + beforeEach(async () => { + await i18next.changeLanguage("en") + }) + + it("renders components mounted by hydration in the hydrated language under StrictMode", async () => { + render( + + + + + , + ) + + await postState({ language: "es" }) + + const tips = screen.getByTestId("tips") + expect(tips).toHaveTextContent("Modos personalizables") + expect(tips).not.toHaveTextContent("rooTips.customizableModes.title") + }) +}) + +describe("applyLanguage", () => { + beforeEach(async () => { + await i18next.changeLanguage("en") + }) + + it("switches to a different language", () => { + applyLanguage(i18next, "es") + expect(i18next.language).toBe("es") + }) + + it("does not re-emit languageChanged for the active language or a missing one", () => { + const onChanged = vi.fn() + i18next.on("languageChanged", onChanged) + + applyLanguage(i18next, "en") + applyLanguage(i18next, undefined) + + i18next.off("languageChanged", onChanged) + expect(onChanged).not.toHaveBeenCalled() + }) +}) diff --git a/webview-ui/src/i18n/__tests__/languageSwitch.spec.tsx b/webview-ui/src/i18n/__tests__/languageSwitch.spec.tsx new file mode 100644 index 0000000000..eb1394f681 --- /dev/null +++ b/webview-ui/src/i18n/__tests__/languageSwitch.spec.tsx @@ -0,0 +1,85 @@ +import { memo, useMemo } from "react" +import { act, render, renderHook, screen, waitFor } from "@testing-library/react" +import { Trans, useTranslation } from "react-i18next" + +import { ExtensionStateContextProvider } from "@/context/ExtensionStateContext" + +import i18next from "../setup" + +const postState = (state: Record) => + act(async () => { + window.dispatchEvent(new MessageEvent("message", { data: { type: "state", state } })) + }) + +// React Compiler (enabled in vite.config.ts, not in Vitest) caches render output and only +// recomputes it when a dependency changes. These consumers reproduce that caching by hand: +// a stale value here is exactly what the compiled webview showed on the Welcome page. + +const PlainConsumer = memo(function PlainConsumer() { + const { t } = useTranslation() + const label = useMemo(() => t("history:recentTasks"), [t]) + return {label} +}) + +const NamespacedConsumer = memo(function NamespacedConsumer() { + const { t } = useTranslation("chat") + const label = useMemo(() => t("rooTips.customizableModes.title"), [t]) + return {label} +}) + +const TransConsumer = memo(function TransConsumer() { + const { t } = useTranslation() + // The compiler caches `` like this; passing `t` makes the element depend on the language. + const element = useMemo(() => , [t]) + return {element} +}) + +describe("language switch without remounting", () => { + beforeEach(async () => { + await i18next.changeLanguage("en") + }) + + it("updates plain and namespaced useTranslation and Trans consumers from en to es", async () => { + render( + + + + + , + ) + + expect(screen.getByTestId("plain")).toHaveTextContent("Recent Tasks") + expect(screen.getByTestId("namespaced")).toHaveTextContent( + i18next.t("chat:rooTips.customizableModes.title", { lng: "en" }), + ) + expect(screen.getByTestId("trans")).toHaveTextContent("Zoo is a whole AI dev team in your editor") + + await postState({ language: "es" }) + + await waitFor(() => expect(screen.getByTestId("plain")).toHaveTextContent("Tareas")) + expect(screen.getByTestId("namespaced")).toHaveTextContent( + i18next.t("chat:rooTips.customizableModes.title", { lng: "es" }), + ) + expect(screen.getByTestId("trans")).toHaveTextContent(i18next.t("chat:about", { lng: "es" })) + }) + + it("keeps t stable across re-renders while the language is unchanged", () => { + const { result, rerender } = renderHook(() => useTranslation("chat").t) + const first = result.current + + rerender() + + expect(result.current).toBe(first) + }) + + it("translates fully qualified keys with a namespaced t", () => { + const Qualified = () => { + const { t } = useTranslation("chat") + return {t("history:recentTasks")} + } + + render() + + expect(screen.getByTestId("qualified")).toHaveTextContent("Recent Tasks") + }) +}) diff --git a/webview-ui/src/i18n/language.ts b/webview-ui/src/i18n/language.ts new file mode 100644 index 0000000000..3b5384b373 --- /dev/null +++ b/webview-ui/src/i18n/language.ts @@ -0,0 +1,15 @@ +import type { i18n } from "i18next" + +/** + * Switch `instance` to `language` unless it is already active. + * + * Call it before the state that carries the language is rendered (the "state" message handler), so + * components mounting in that commit render the new language instead of catching up afterwards. + * Catching up is not reliable: under StrictMode, react-i18next's `useTranslation` resets a freshly + * mounted component's `t` to its first identity, and React Compiler then serves the cached output. + */ +export function applyLanguage(instance: i18n, language: string | undefined) { + if (language && instance.language !== language) { + void instance.changeLanguage(language) + } +} diff --git a/webview-ui/src/i18n/setup.ts b/webview-ui/src/i18n/setup.ts index 678cdc1d49..607ebe65fe 100644 --- a/webview-ui/src/i18n/setup.ts +++ b/webview-ui/src/i18n/setup.ts @@ -29,7 +29,7 @@ Object.entries(localeFiles).forEach(([path, module]) => { console.log("Dynamically loaded translations:", Object.keys(translations)) // Initialize i18next for React -// This will be initialized with the VSCode language in TranslationProvider +// The language is switched by the "state" message handler in ExtensionStateContext (see applyLanguage) i18next.use(initReactI18next).init({ lng: "en", // Default language (will be overridden) fallbackLng: "en", @@ -51,4 +51,8 @@ export function loadTranslations() { }) } +// The locale files are bundled eagerly, so register them before the first render instead of +// in a mount effect, which ran after children had already rendered raw keys. +loadTranslations() + export default i18next diff --git a/webview-ui/src/utils/__tests__/format.spec.ts b/webview-ui/src/utils/__tests__/format.spec.ts index 2f633d364d..6ffa7399f5 100644 --- a/webview-ui/src/utils/__tests__/format.spec.ts +++ b/webview-ui/src/utils/__tests__/format.spec.ts @@ -1,59 +1,68 @@ +import type { TFunction } from "i18next" + import { formatLargeNumber, formatDate, formatTimeAgo } from "../format" // Mock i18next vi.mock("i18next", () => ({ - default: { - t: vi.fn((key: string, options?: any) => { - // Mock translations for testing - const translations: Record = { - "common:number_format.billion_suffix": "b", - "common:number_format.million_suffix": "m", - "common:number_format.thousand_suffix": "k", - "common:time_ago.just_now": "just now", - "common:time_ago.seconds_ago": "{{count}} seconds ago", - "common:time_ago.minute_ago": "a minute ago", - "common:time_ago.minutes_ago": "{{count}} minutes ago", - "common:time_ago.hour_ago": "an hour ago", - "common:time_ago.hours_ago": "{{count}} hours ago", - "common:time_ago.day_ago": "a day ago", - "common:time_ago.days_ago": "{{count}} days ago", - "common:time_ago.week_ago": "a week ago", - "common:time_ago.weeks_ago": "{{count}} weeks ago", - "common:time_ago.month_ago": "a month ago", - "common:time_ago.months_ago": "{{count}} months ago", - "common:time_ago.year_ago": "a year ago", - "common:time_ago.years_ago": "{{count}} years ago", - } - - let result = translations[key] || key - if (options?.count !== undefined) { - result = result.replace("{{count}}", options.count.toString()) - } - return result - }), - language: "en", - }, + default: { language: "en" }, })) +// Returns translation keys with interpolated values appended. +const t = ((key: string, options?: { count?: number }) => { + const translations: Record = { + "common:number_format.billion_suffix": "b", + "common:number_format.million_suffix": "m", + "common:number_format.thousand_suffix": "k", + "common:time_ago.just_now": "just now", + "common:time_ago.seconds_ago": "{{count}} seconds ago", + "common:time_ago.minute_ago": "a minute ago", + "common:time_ago.minutes_ago": "{{count}} minutes ago", + "common:time_ago.hour_ago": "an hour ago", + "common:time_ago.hours_ago": "{{count}} hours ago", + "common:time_ago.day_ago": "a day ago", + "common:time_ago.days_ago": "{{count}} days ago", + "common:time_ago.week_ago": "a week ago", + "common:time_ago.weeks_ago": "{{count}} weeks ago", + "common:time_ago.month_ago": "a month ago", + "common:time_ago.months_ago": "{{count}} months ago", + "common:time_ago.year_ago": "a year ago", + "common:time_ago.years_ago": "{{count}} years ago", + } + + let result = translations[key] || key + if (options?.count !== undefined) { + result = result.replace("{{count}}", options.count.toString()) + } + return result +}) as unknown as TFunction + describe("formatLargeNumber", () => { it("should format billions", () => { - expect(formatLargeNumber(1500000000)).toBe("1.5b") - expect(formatLargeNumber(2000000000)).toBe("2.0b") + expect(formatLargeNumber(1500000000, t)).toBe("1.5b") + expect(formatLargeNumber(2000000000, t)).toBe("2.0b") }) it("should format millions", () => { - expect(formatLargeNumber(1500000)).toBe("1.5m") - expect(formatLargeNumber(2000000)).toBe("2.0m") + expect(formatLargeNumber(1500000, t)).toBe("1.5m") + expect(formatLargeNumber(2000000, t)).toBe("2.0m") }) it("should format thousands", () => { - expect(formatLargeNumber(1500)).toBe("1.5k") - expect(formatLargeNumber(2000)).toBe("2.0k") + expect(formatLargeNumber(1500, t)).toBe("1.5k") + expect(formatLargeNumber(2000, t)).toBe("2.0k") }) it("should return string for small numbers", () => { - expect(formatLargeNumber(999)).toBe("999") - expect(formatLargeNumber(100)).toBe("100") + expect(formatLargeNumber(999, t)).toBe("999") + expect(formatLargeNumber(100, t)).toBe("100") + }) + + it("takes suffixes from the given t", () => { + const givenT = vi.fn((key: string) => (key === "common:number_format.thousand_suffix" ? " mil" : key)) + + // `TFunction` is branded; the mock only needs to answer a key lookup. + expect(formatLargeNumber(1500, givenT as unknown as TFunction)).toBe("1.5 mil") + expect(givenT).toHaveBeenCalledWith("common:number_format.thousand_suffix") }) }) @@ -83,71 +92,80 @@ describe("formatTimeAgo", () => { it('should return "just now" for very recent times', () => { const timestamp = new Date("2024-01-15T11:59:35").getTime() // 25 seconds ago - expect(formatTimeAgo(timestamp)).toBe("just now") + expect(formatTimeAgo(timestamp, t)).toBe("just now") }) it("should format seconds ago", () => { const timestamp = new Date("2024-01-15T11:59:15").getTime() // 45 seconds ago - expect(formatTimeAgo(timestamp)).toBe("45 seconds ago") + expect(formatTimeAgo(timestamp, t)).toBe("45 seconds ago") }) it("should format a minute ago", () => { const timestamp = new Date("2024-01-15T11:59:00").getTime() // 1 minute ago - expect(formatTimeAgo(timestamp)).toBe("a minute ago") + expect(formatTimeAgo(timestamp, t)).toBe("a minute ago") }) it("should format minutes ago", () => { const timestamp = new Date("2024-01-15T11:45:00").getTime() // 15 minutes ago - expect(formatTimeAgo(timestamp)).toBe("15 minutes ago") + expect(formatTimeAgo(timestamp, t)).toBe("15 minutes ago") }) it("should format an hour ago", () => { const timestamp = new Date("2024-01-15T11:00:00").getTime() // 1 hour ago - expect(formatTimeAgo(timestamp)).toBe("an hour ago") + expect(formatTimeAgo(timestamp, t)).toBe("an hour ago") }) it("should format hours ago", () => { const timestamp = new Date("2024-01-15T09:00:00").getTime() // 3 hours ago - expect(formatTimeAgo(timestamp)).toBe("3 hours ago") + expect(formatTimeAgo(timestamp, t)).toBe("3 hours ago") }) it("should format a day ago", () => { const timestamp = new Date("2024-01-14T12:00:00").getTime() // 1 day ago - expect(formatTimeAgo(timestamp)).toBe("a day ago") + expect(formatTimeAgo(timestamp, t)).toBe("a day ago") }) it("should format days ago", () => { const timestamp = new Date("2024-01-12T12:00:00").getTime() // 3 days ago - expect(formatTimeAgo(timestamp)).toBe("3 days ago") + expect(formatTimeAgo(timestamp, t)).toBe("3 days ago") }) it("should format a week ago", () => { const timestamp = new Date("2024-01-08T12:00:00").getTime() // 7 days ago - expect(formatTimeAgo(timestamp)).toBe("a week ago") + expect(formatTimeAgo(timestamp, t)).toBe("a week ago") }) it("should format weeks ago", () => { const timestamp = new Date("2024-01-01T12:00:00").getTime() // 14 days ago - expect(formatTimeAgo(timestamp)).toBe("2 weeks ago") + expect(formatTimeAgo(timestamp, t)).toBe("2 weeks ago") }) it("should format a month ago", () => { const timestamp = new Date("2023-12-15T12:00:00").getTime() // ~1 month ago - expect(formatTimeAgo(timestamp)).toBe("a month ago") + expect(formatTimeAgo(timestamp, t)).toBe("a month ago") }) it("should format months ago", () => { const timestamp = new Date("2023-10-15T12:00:00").getTime() // ~3 months ago - expect(formatTimeAgo(timestamp)).toBe("3 months ago") + expect(formatTimeAgo(timestamp, t)).toBe("3 months ago") }) it("should format a year ago", () => { const timestamp = new Date("2023-01-15T12:00:00").getTime() // 1 year ago - expect(formatTimeAgo(timestamp)).toBe("a year ago") + expect(formatTimeAgo(timestamp, t)).toBe("a year ago") }) it("should format years ago", () => { const timestamp = new Date("2021-01-15T12:00:00").getTime() // 3 years ago - expect(formatTimeAgo(timestamp)).toBe("3 years ago") + expect(formatTimeAgo(timestamp, t)).toBe("3 years ago") + }) + + it("takes translations from the given t", () => { + const givenT = vi.fn((key: string, options?: Record) => `es:${key}:${options?.count}`) + const timestamp = new Date("2024-01-15T09:00:00").getTime() // 3 hours ago + + // `TFunction` is branded; the mock only needs to answer a key lookup with `count`. + expect(formatTimeAgo(timestamp, givenT as unknown as TFunction)).toBe("es:common:time_ago.hours_ago:3") + expect(givenT).toHaveBeenCalledWith("common:time_ago.hours_ago", { count: 3 }) }) }) diff --git a/webview-ui/src/utils/__tests__/validate.spec.ts b/webview-ui/src/utils/__tests__/validate.spec.ts index bf25faf7f2..d4b9a4da2a 100644 --- a/webview-ui/src/utils/__tests__/validate.spec.ts +++ b/webview-ui/src/utils/__tests__/validate.spec.ts @@ -4,22 +4,7 @@ import { type OrganizationAllowList, type RouterModels, } from "@roo-code/types" - -// Mock i18next to return translation keys with interpolated values -vi.mock("i18next", () => ({ - default: { - t: (key: string, options?: Record) => { - if (options) { - let result = key - Object.entries(options).forEach(([k, v]) => { - result += ` ${k}=${v}` - }) - return result - } - return key - }, - }, -})) +import type { TFunction } from "i18next" import { formatOpenAiExtraBodyValidationError, @@ -29,6 +14,18 @@ import { validateBedrockArn, } from "../validate" +// Returns translation keys with interpolated values appended. +const t = ((key: string, options?: Record) => { + if (options) { + let result = key + Object.entries(options).forEach(([k, v]) => { + result += ` ${k}=${v}` + }) + return result + } + return key +}) as unknown as TFunction + describe("Model Validation Functions", () => { const mockRouterModels: RouterModels = { openrouter: { @@ -88,7 +85,7 @@ describe("Model Validation Functions", () => { openRouterModelId: "valid-model", } - const result = getModelValidationError(config, mockRouterModels, allowAllOrganization) + const result = getModelValidationError(config, t, mockRouterModels, allowAllOrganization) expect(result).toBeUndefined() }) @@ -98,7 +95,7 @@ describe("Model Validation Functions", () => { openRouterModelId: "invalid-model", } - const result = getModelValidationError(config, mockRouterModels, allowAllOrganization) + const result = getModelValidationError(config, t, mockRouterModels, allowAllOrganization) expect(result).toContain("settings:validation.modelAvailability") }) @@ -108,7 +105,7 @@ describe("Model Validation Functions", () => { openRouterModelId: "another-valid-model", } - const result = getModelValidationError(config, mockRouterModels, restrictiveOrganization) + const result = getModelValidationError(config, t, mockRouterModels, restrictiveOrganization) expect(result).toContain("model") }) @@ -118,7 +115,7 @@ describe("Model Validation Functions", () => { openAiModelId: "gpt-4", } - const result = getModelValidationError(config, undefined, allowAllOrganization) + const result = getModelValidationError(config, t, undefined, allowAllOrganization) expect(result).toBeUndefined() }) @@ -152,7 +149,7 @@ describe("Model Validation Functions", () => { }, } - const result = getModelValidationError(config, undefined, organizationAllowList) + const result = getModelValidationError(config, t, undefined, organizationAllowList) expect(result).toContain("settings:validation.modelNotAllowed") expect(result).toContain("model=blocked-model") @@ -164,7 +161,7 @@ describe("Model Validation Functions", () => { openRouterModelId: "", } - const result = getModelValidationError(config, mockRouterModels, allowAllOrganization) + const result = getModelValidationError(config, t, mockRouterModels, allowAllOrganization) expect(result).toBe("settings:validation.modelId") }) @@ -174,14 +171,14 @@ describe("Model Validation Functions", () => { // openRouterModelId is undefined } - const result = getModelValidationError(config, mockRouterModels, allowAllOrganization) + const result = getModelValidationError(config, t, mockRouterModels, allowAllOrganization) expect(result).toBe("settings:validation.modelId") }) }) describe("validateApiConfiguration for GitHub Copilot", () => { const validate = (vsCodeLmModelSelector: ProviderSettings["vsCodeLmModelSelector"]) => - validateApiConfiguration({ apiProvider: providerIdentifiers.githubCopilot, vsCodeLmModelSelector }) + validateApiConfiguration({ apiProvider: providerIdentifiers.githubCopilot, vsCodeLmModelSelector }, t) it.each([ ["a model id", { vendor: "copilot", id: "gpt-x" }], @@ -207,7 +204,12 @@ describe("Model Validation Functions", () => { openRouterModelId: "valid-model", } - const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization) + const result = validateApiConfigurationExcludingModelErrors( + config, + t, + mockRouterModels, + allowAllOrganization, + ) expect(result).toBeUndefined() }) @@ -218,7 +220,12 @@ describe("Model Validation Functions", () => { // Missing openRouterApiKey } - const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization) + const result = validateApiConfigurationExcludingModelErrors( + config, + t, + mockRouterModels, + allowAllOrganization, + ) expect(result).toBe("settings:validation.apiKey") }) @@ -229,7 +236,12 @@ describe("Model Validation Functions", () => { openRouterModelId: "invalid-model", // This should be ignored } - const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization) + const result = validateApiConfigurationExcludingModelErrors( + config, + t, + mockRouterModels, + allowAllOrganization, + ) expect(result).toBeUndefined() // Should not return model validation error }) @@ -242,6 +254,7 @@ describe("Model Validation Functions", () => { const result = validateApiConfigurationExcludingModelErrors( config, + t, mockRouterModels, restrictiveOrganization, ) @@ -258,7 +271,7 @@ describe("Model Validation Functions", () => { } expect( - validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization), + validateApiConfigurationExcludingModelErrors(config, t, mockRouterModels, allowAllOrganization), ).toBeUndefined() }) @@ -279,9 +292,9 @@ describe("Model Validation Functions", () => { openAiExtraBody, } - expect(validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization)).toBe( - expectedError, - ) + expect( + validateApiConfigurationExcludingModelErrors(config, t, mockRouterModels, allowAllOrganization), + ).toBe(expectedError) }) }) @@ -297,9 +310,9 @@ describe("Model Validation Functions", () => { it("accepts a valid Extra Body object through both validation entry points", () => { const config = createConfig(JSON.stringify({ metadata: { completion_window: "balanced" } })) - expect(validateApiConfiguration(config, mockRouterModels, allowAllOrganization)).toBeUndefined() + expect(validateApiConfiguration(config, t, mockRouterModels, allowAllOrganization)).toBeUndefined() expect( - validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization), + validateApiConfigurationExcludingModelErrors(config, t, mockRouterModels, allowAllOrganization), ).toBeUndefined() }) @@ -324,14 +337,14 @@ describe("Model Validation Functions", () => { ])("rejects %s with the localized error", (_name, openAiExtraBody, expectedError) => { const config = createConfig(openAiExtraBody) - expect(validateApiConfiguration(config, mockRouterModels, allowAllOrganization)).toBe(expectedError) + expect(validateApiConfiguration(config, t, mockRouterModels, allowAllOrganization)).toBe(expectedError) }) it("returns required-field errors before Extra Body errors", () => { const config = createConfig("not json") config.openAiApiKey = undefined - expect(validateApiConfiguration(config, mockRouterModels, allowAllOrganization)).toBe( + expect(validateApiConfiguration(config, t, mockRouterModels, allowAllOrganization)).toBe( "settings:validation.openAi", ) }) @@ -364,7 +377,7 @@ describe("Model Validation Functions", () => { openAiExtraBody: "not json", } - expect(validateApiConfiguration(config, mockRouterModels, allowAllOrganization)).toBeUndefined() + expect(validateApiConfiguration(config, t, mockRouterModels, allowAllOrganization)).toBeUndefined() }) }) @@ -376,7 +389,12 @@ describe("Model Validation Functions", () => { // Missing opencodeGoApiKey } - const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization) + const result = validateApiConfigurationExcludingModelErrors( + config, + t, + mockRouterModels, + allowAllOrganization, + ) expect(result).toBe("settings:validation.apiKey") }) @@ -387,7 +405,12 @@ describe("Model Validation Functions", () => { opencodeGoModelId: "glm-5.1", } - const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization) + const result = validateApiConfigurationExcludingModelErrors( + config, + t, + mockRouterModels, + allowAllOrganization, + ) expect(result).toBeUndefined() }) @@ -398,7 +421,7 @@ describe("Model Validation Functions", () => { // Missing opencodeGoModelId } - const result = getModelValidationError(config, mockRouterModels, allowAllOrganization) + const result = getModelValidationError(config, t, mockRouterModels, allowAllOrganization) expect(result).toBe("settings:validation.modelId") }) }) @@ -410,7 +433,12 @@ describe("Model Validation Functions", () => { // Missing kenariApiKey } - const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization) + const result = validateApiConfigurationExcludingModelErrors( + config, + t, + mockRouterModels, + allowAllOrganization, + ) expect(result).toBe("settings:validation.apiKey") }) @@ -421,7 +449,12 @@ describe("Model Validation Functions", () => { kenariModelId: "glm-5.1", } - const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization) + const result = validateApiConfigurationExcludingModelErrors( + config, + t, + mockRouterModels, + allowAllOrganization, + ) expect(result).toBeUndefined() }) @@ -432,7 +465,7 @@ describe("Model Validation Functions", () => { // Missing kenariModelId } - const result = getModelValidationError(config, mockRouterModels, allowAllOrganization) + const result = getModelValidationError(config, t, mockRouterModels, allowAllOrganization) expect(result).toBe("settings:validation.modelId") }) }) @@ -444,7 +477,7 @@ describe("Model Validation Functions", () => { ioIntelligenceModelId: "zai-org/GLM-4.6", } - expect(validateApiConfiguration(config, mockRouterModels)).toBe("settings:validation.apiKey") + expect(validateApiConfiguration(config, t, mockRouterModels)).toBe("settings:validation.apiKey") }) it("accepts an IO Intelligence API key and selected model", () => { @@ -454,7 +487,7 @@ describe("Model Validation Functions", () => { ioIntelligenceModelId: "zai-org/GLM-4.6", } - expect(validateApiConfiguration(config, mockRouterModels)).toBeUndefined() + expect(validateApiConfiguration(config, t, mockRouterModels)).toBeUndefined() }) }) @@ -465,7 +498,7 @@ describe("Model Validation Functions", () => { nanoGptModelId: "openai/model", } - expect(validateApiConfiguration(config, mockRouterModels)).toBe("settings:validation.apiKey") + expect(validateApiConfiguration(config, t, mockRouterModels)).toBe("settings:validation.apiKey") }) it("returns a modelId error when the NanoGPT model is missing", () => { @@ -474,7 +507,7 @@ describe("Model Validation Functions", () => { nanoGptApiKey: "valid-key", } - expect(validateApiConfiguration(config, mockRouterModels)).toBe("settings:validation.modelId") + expect(validateApiConfiguration(config, t, mockRouterModels)).toBe("settings:validation.modelId") }) it("accepts a NanoGPT API key and selected model", () => { @@ -484,7 +517,7 @@ describe("Model Validation Functions", () => { nanoGptModelId: "openai/model", } - expect(validateApiConfiguration(config, mockRouterModels)).toBeUndefined() + expect(validateApiConfiguration(config, t, mockRouterModels)).toBeUndefined() }) }) @@ -496,7 +529,12 @@ describe("Model Validation Functions", () => { // Missing friendliApiKey } - const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization) + const result = validateApiConfigurationExcludingModelErrors( + config, + t, + mockRouterModels, + allowAllOrganization, + ) expect(result).toBe("settings:validation.apiKey") }) @@ -507,7 +545,12 @@ describe("Model Validation Functions", () => { apiModelId: "zai-org/GLM-5.2", } - const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization) + const result = validateApiConfigurationExcludingModelErrors( + config, + t, + mockRouterModels, + allowAllOrganization, + ) expect(result).toBeUndefined() }) }) @@ -520,7 +563,7 @@ describe("Model Validation Functions", () => { zooGatewayModelId: "anthropic/claude-sonnet-4", } - const result = validateApiConfiguration(config, mockRouterModels, allowAllOrganization, false) + const result = validateApiConfiguration(config, t, mockRouterModels, allowAllOrganization, false) expect(result).toBe("settings:validation.zooGatewaySignIn") }) @@ -530,7 +573,7 @@ describe("Model Validation Functions", () => { zooGatewayModelId: "anthropic/claude-sonnet-4", } - const result = validateApiConfiguration(config, mockRouterModels, allowAllOrganization, true) + const result = validateApiConfiguration(config, t, mockRouterModels, allowAllOrganization, true) expect(result).toBeUndefined() }) @@ -541,7 +584,7 @@ describe("Model Validation Functions", () => { zooSessionToken: "zoo_ext_test_token", } - const result = validateApiConfiguration(config, mockRouterModels, allowAllOrganization, false) + const result = validateApiConfiguration(config, t, mockRouterModels, allowAllOrganization, false) expect(result).toBeUndefined() }) }) @@ -558,6 +601,7 @@ describe("Model Validation Functions", () => { const result = validateApiConfigurationExcludingModelErrors( config, + t, mockRouterModels, allowAllOrganization, ) @@ -573,6 +617,7 @@ describe("Model Validation Functions", () => { const result = validateApiConfigurationExcludingModelErrors( config, + t, mockRouterModels, allowAllOrganization, ) @@ -594,6 +639,7 @@ describe("Model Validation Functions", () => { const result = validateApiConfigurationExcludingModelErrors( config, + t, mockRouterModels, orgWithoutZooGateway, ) @@ -609,7 +655,12 @@ describe("Model Validation Functions", () => { kimiCodeAuthMethod: "oauth", } - const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization) + const result = validateApiConfigurationExcludingModelErrors( + config, + t, + mockRouterModels, + allowAllOrganization, + ) expect(result).toBeUndefined() }) @@ -618,7 +669,12 @@ describe("Model Validation Functions", () => { apiProvider: providerIdentifiers.kimiCode, } - const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization) + const result = validateApiConfigurationExcludingModelErrors( + config, + t, + mockRouterModels, + allowAllOrganization, + ) expect(result).toBeUndefined() }) @@ -628,7 +684,12 @@ describe("Model Validation Functions", () => { kimiCodeAuthMethod: "api-key", } - const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization) + const result = validateApiConfigurationExcludingModelErrors( + config, + t, + mockRouterModels, + allowAllOrganization, + ) expect(result).toBe("settings:validation.apiKey") }) @@ -639,7 +700,12 @@ describe("Model Validation Functions", () => { kimiCodeApiKey: "valid-key", } - const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization) + const result = validateApiConfigurationExcludingModelErrors( + config, + t, + mockRouterModels, + allowAllOrganization, + ) expect(result).toBeUndefined() }) }) @@ -650,6 +716,7 @@ describe("validateBedrockArn", () => { it("accepts standard AWS Bedrock ARNs", () => { const result = validateBedrockArn( "arn:aws:bedrock:us-west-2:123456789012:inference-profile/us.anthropic.claude-3-5-sonnet-v2", + t, ) expect(result.isValid).toBe(true) expect(result.arnRegion).toBe("us-west-2") @@ -659,6 +726,7 @@ describe("validateBedrockArn", () => { it("accepts AWS GovCloud ARNs", () => { const result = validateBedrockArn( "arn:aws-us-gov:bedrock:us-gov-west-1:123456789012:inference-profile/model", + t, ) expect(result.isValid).toBe(true) expect(result.arnRegion).toBe("us-gov-west-1") @@ -666,14 +734,14 @@ describe("validateBedrockArn", () => { }) it("accepts AWS China ARNs", () => { - const result = validateBedrockArn("arn:aws-cn:bedrock:cn-north-1:123456789012:inference-profile/model") + const result = validateBedrockArn("arn:aws-cn:bedrock:cn-north-1:123456789012:inference-profile/model", t) expect(result.isValid).toBe(true) expect(result.arnRegion).toBe("cn-north-1") expect(result.errorMessage).toBeUndefined() }) it("accepts SageMaker ARNs", () => { - const result = validateBedrockArn("arn:aws:sagemaker:us-east-1:123456789012:endpoint/my-endpoint") + const result = validateBedrockArn("arn:aws:sagemaker:us-east-1:123456789012:endpoint/my-endpoint", t) expect(result.isValid).toBe(true) expect(result.arnRegion).toBe("us-east-1") expect(result.errorMessage).toBeUndefined() @@ -681,7 +749,7 @@ describe("validateBedrockArn", () => { it("accepts non-standard ARN formats without validation errors", () => { // Users are advanced - trust their input - const result = validateBedrockArn("arn:custom:service:region:account:resource") + const result = validateBedrockArn("arn:custom:service:region:account:resource", t) expect(result.isValid).toBe(true) expect(result.arnRegion).toBe("region") expect(result.errorMessage).toBeUndefined() @@ -689,7 +757,7 @@ describe("validateBedrockArn", () => { it("accepts completely custom ARN strings", () => { // Even unusual formats should be accepted - const result = validateBedrockArn("some-custom-arn-format") + const result = validateBedrockArn("some-custom-arn-format", t) expect(result.isValid).toBe(true) // May not be able to extract region from non-standard format expect(result.errorMessage).toBeUndefined() @@ -700,6 +768,7 @@ describe("validateBedrockArn", () => { it("shows warning when ARN region differs from provided region", () => { const result = validateBedrockArn( "arn:aws:bedrock:us-west-2:123456789012:inference-profile/model", + t, "us-east-1", ) expect(result.isValid).toBe(true) // Still valid, just a warning @@ -711,6 +780,7 @@ describe("validateBedrockArn", () => { it("shows no warning when ARN region matches provided region", () => { const result = validateBedrockArn( "arn:aws:bedrock:us-west-2:123456789012:inference-profile/model", + t, "us-west-2", ) expect(result.isValid).toBe(true) @@ -719,14 +789,14 @@ describe("validateBedrockArn", () => { }) it("shows no warning when no region is provided to check against", () => { - const result = validateBedrockArn("arn:aws:bedrock:us-west-2:123456789012:inference-profile/model") + const result = validateBedrockArn("arn:aws:bedrock:us-west-2:123456789012:inference-profile/model", t) expect(result.isValid).toBe(true) expect(result.arnRegion).toBe("us-west-2") expect(result.errorMessage).toBeUndefined() }) it("shows no warning when region cannot be extracted from ARN", () => { - const result = validateBedrockArn("non-arn-format", "us-east-1") + const result = validateBedrockArn("non-arn-format", t, "us-east-1") expect(result.isValid).toBe(true) expect(result.arnRegion).toBeUndefined() expect(result.errorMessage).toBeUndefined() diff --git a/webview-ui/src/utils/format.ts b/webview-ui/src/utils/format.ts index cd3521aed2..d814cf93dd 100644 --- a/webview-ui/src/utils/format.ts +++ b/webview-ui/src/utils/format.ts @@ -1,14 +1,14 @@ -import i18next from "i18next" +import i18next, { type TFunction } from "i18next" -export function formatLargeNumber(num: number): string { +export function formatLargeNumber(num: number, t: TFunction): string { if (num >= 1e9) { - return (num / 1e9).toFixed(1) + i18next.t("common:number_format.billion_suffix") + return (num / 1e9).toFixed(1) + t("common:number_format.billion_suffix") } if (num >= 1e6) { - return (num / 1e6).toFixed(1) + i18next.t("common:number_format.million_suffix") + return (num / 1e6).toFixed(1) + t("common:number_format.million_suffix") } if (num >= 1e3) { - return (num / 1e3).toFixed(1) + i18next.t("common:number_format.thousand_suffix") + return (num / 1e3).toFixed(1) + t("common:number_format.thousand_suffix") } return num.toString() } @@ -29,7 +29,7 @@ export const formatDate = (timestamp: number) => { }) } -export const formatTimeAgo = (timestamp: number) => { +export const formatTimeAgo = (timestamp: number, t: TFunction) => { const now = Date.now() const diff = now - timestamp const seconds = Math.floor(diff / 1000) @@ -41,38 +41,26 @@ export const formatTimeAgo = (timestamp: number) => { const years = Math.floor(days / 365) if (years > 0) { - return years === 1 - ? i18next.t("common:time_ago.year_ago") - : i18next.t("common:time_ago.years_ago", { count: years }) + return years === 1 ? t("common:time_ago.year_ago") : t("common:time_ago.years_ago", { count: years }) } if (months > 0) { - return months === 1 - ? i18next.t("common:time_ago.month_ago") - : i18next.t("common:time_ago.months_ago", { count: months }) + return months === 1 ? t("common:time_ago.month_ago") : t("common:time_ago.months_ago", { count: months }) } if (weeks > 0) { - return weeks === 1 - ? i18next.t("common:time_ago.week_ago") - : i18next.t("common:time_ago.weeks_ago", { count: weeks }) + return weeks === 1 ? t("common:time_ago.week_ago") : t("common:time_ago.weeks_ago", { count: weeks }) } if (days > 0) { - return days === 1 - ? i18next.t("common:time_ago.day_ago") - : i18next.t("common:time_ago.days_ago", { count: days }) + return days === 1 ? t("common:time_ago.day_ago") : t("common:time_ago.days_ago", { count: days }) } if (hours > 0) { - return hours === 1 - ? i18next.t("common:time_ago.hour_ago") - : i18next.t("common:time_ago.hours_ago", { count: hours }) + return hours === 1 ? t("common:time_ago.hour_ago") : t("common:time_ago.hours_ago", { count: hours }) } if (minutes > 0) { - return minutes === 1 - ? i18next.t("common:time_ago.minute_ago") - : i18next.t("common:time_ago.minutes_ago", { count: minutes }) + return minutes === 1 ? t("common:time_ago.minute_ago") : t("common:time_ago.minutes_ago", { count: minutes }) } if (seconds > 30) { - return i18next.t("common:time_ago.seconds_ago", { count: seconds }) + return t("common:time_ago.seconds_ago", { count: seconds }) } - return i18next.t("common:time_ago.just_now") + return t("common:time_ago.just_now") } diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 9e4addf7b1..3e84a03c83 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -1,5 +1,4 @@ -import i18next from "i18next" - +import type { TFunction } from "i18next" import { type ProviderSettings, type OrganizationAllowList, @@ -33,27 +32,28 @@ export function formatOpenAiExtraBodyValidationError( return t(`settings:validation.openAiExtraBody.${result.reason}`) } -function validateOpenAiExtraBody(apiConfiguration: ProviderSettings): string | undefined { +function validateOpenAiExtraBody(apiConfiguration: ProviderSettings, t: TFunction): string | undefined { if (apiConfiguration.apiProvider !== providerIdentifiers.openai) { return undefined } - return formatOpenAiExtraBodyValidationError(parseOpenAiExtraBody(apiConfiguration.openAiExtraBody), i18next.t) + return formatOpenAiExtraBodyValidationError(parseOpenAiExtraBody(apiConfiguration.openAiExtraBody), t) } export function validateApiConfiguration( apiConfiguration: ProviderSettings, + t: TFunction, routerModels?: RouterModels, organizationAllowList?: OrganizationAllowList, zooCodeIsAuthenticated?: boolean, ): string | undefined { - const keysAndIdsPresentErrorMessage = validateModelsAndKeysProvided(apiConfiguration, zooCodeIsAuthenticated) + const keysAndIdsPresentErrorMessage = validateModelsAndKeysProvided(apiConfiguration, t, zooCodeIsAuthenticated) if (keysAndIdsPresentErrorMessage) { return keysAndIdsPresentErrorMessage } - const extraBodyError = validateOpenAiExtraBody(apiConfiguration) + const extraBodyError = validateOpenAiExtraBody(apiConfiguration, t) if (extraBodyError) { return extraBodyError @@ -61,6 +61,7 @@ export function validateApiConfiguration( const organizationAllowListError = validateProviderAgainstOrganizationSettings( apiConfiguration, + t, organizationAllowList, ) @@ -68,82 +69,83 @@ export function validateApiConfiguration( return organizationAllowListError.message } - return validateDynamicProviderModelId(apiConfiguration, routerModels) + return validateDynamicProviderModelId(apiConfiguration, t, routerModels) } function validateModelsAndKeysProvided( apiConfiguration: ProviderSettings, + t: TFunction, zooCodeIsAuthenticated?: boolean, ): string | undefined { switch (apiConfiguration.apiProvider) { case providerIdentifiers.openrouter: if (!apiConfiguration.openRouterApiKey) { - return i18next.t("settings:validation.apiKey") + return t("settings:validation.apiKey") } break case providerIdentifiers.requesty: if (!apiConfiguration.requestyApiKey) { - return i18next.t("settings:validation.apiKey") + return t("settings:validation.apiKey") } break case providerIdentifiers.unbound: if (!apiConfiguration.unboundApiKey) { - return i18next.t("settings:validation.apiKey") + return t("settings:validation.apiKey") } break case providerIdentifiers.litellm: if (!apiConfiguration.litellmApiKey) { - return i18next.t("settings:validation.apiKey") + return t("settings:validation.apiKey") } break case providerIdentifiers.anthropic: if (!apiConfiguration.apiKey) { - return i18next.t("settings:validation.apiKey") + return t("settings:validation.apiKey") } break case providerIdentifiers.bedrock: if (!apiConfiguration.awsRegion) { - return i18next.t("settings:validation.awsRegion") + return t("settings:validation.awsRegion") } break case providerIdentifiers.vertex: if (!apiConfiguration.vertexProjectId || !apiConfiguration.vertexRegion) { - return i18next.t("settings:validation.googleCloud") + return t("settings:validation.googleCloud") } break case providerIdentifiers.gemini: if (!apiConfiguration.geminiApiKey) { - return i18next.t("settings:validation.apiKey") + return t("settings:validation.apiKey") } break case providerIdentifiers.openaiNative: if (!apiConfiguration.openAiNativeApiKey) { - return i18next.t("settings:validation.apiKey") + return t("settings:validation.apiKey") } break case providerIdentifiers.mistral: if (!apiConfiguration.mistralApiKey) { - return i18next.t("settings:validation.apiKey") + return t("settings:validation.apiKey") } break case providerIdentifiers.openai: if (!apiConfiguration.openAiBaseUrl || !apiConfiguration.openAiApiKey || !apiConfiguration.openAiModelId) { - return i18next.t("settings:validation.openAi") + return t("settings:validation.openAi") } break case providerIdentifiers.ollama: if (!apiConfiguration.ollamaModelId) { - return i18next.t("settings:validation.modelId") + return t("settings:validation.modelId") } break case providerIdentifiers.lmstudio: if (!apiConfiguration.lmStudioModelId) { - return i18next.t("settings:validation.modelId") + return t("settings:validation.modelId") } break case providerIdentifiers.vscodeLm: if (!apiConfiguration.vsCodeLmModelSelector) { - return i18next.t("settings:validation.modelSelector") + return t("settings:validation.modelSelector") } break case providerIdentifiers.githubCopilot: @@ -151,62 +153,62 @@ function validateModelsAndKeysProvided( apiConfiguration.vsCodeLmModelSelector?.vendor !== githubCopilotLanguageModel.vendor || (!apiConfiguration.vsCodeLmModelSelector.id && !apiConfiguration.vsCodeLmModelSelector.family) ) { - return i18next.t("settings:validation.modelSelector") + return t("settings:validation.modelSelector") } break case providerIdentifiers.fireworks: if (!apiConfiguration.fireworksApiKey) { - return i18next.t("settings:validation.apiKey") + return t("settings:validation.apiKey") } break case providerIdentifiers.friendli: if (!apiConfiguration.friendliApiKey) { - return i18next.t("settings:validation.apiKey") + return t("settings:validation.apiKey") } break case providerIdentifiers.qwenCode: if (!apiConfiguration.qwenCodeOauthPath) { - return i18next.t("settings:validation.qwenCodeOauthPath") + return t("settings:validation.qwenCodeOauthPath") } break case providerIdentifiers.kimiCode: if ((apiConfiguration.kimiCodeAuthMethod ?? "oauth") === "api-key" && !apiConfiguration.kimiCodeApiKey) { - return i18next.t("settings:validation.apiKey") + return t("settings:validation.apiKey") } break case providerIdentifiers.vercelAiGateway: if (!apiConfiguration.vercelAiGatewayApiKey) { - return i18next.t("settings:validation.apiKey") + return t("settings:validation.apiKey") } break case providerIdentifiers.opencodeGo: if (!apiConfiguration.opencodeGoApiKey) { - return i18next.t("settings:validation.apiKey") + return t("settings:validation.apiKey") } break case providerIdentifiers.kenari: if (!apiConfiguration.kenariApiKey) { - return i18next.t("settings:validation.apiKey") + return t("settings:validation.apiKey") } break case providerIdentifiers.ioIntelligence: if (!apiConfiguration.ioIntelligenceApiKey) { - return i18next.t("settings:validation.apiKey") + return t("settings:validation.apiKey") } break case providerIdentifiers.nanogpt: if (!apiConfiguration.nanoGptApiKey) { - return i18next.t("settings:validation.apiKey") + return t("settings:validation.apiKey") } break case providerIdentifiers.zooGateway: if (!apiConfiguration.zooSessionToken && !zooCodeIsAuthenticated) { - return i18next.t("settings:validation.zooGatewaySignIn") + return t("settings:validation.zooGatewaySignIn") } break case providerIdentifiers.baseten: if (!apiConfiguration.basetenApiKey) { - return i18next.t("settings:validation.apiKey") + return t("settings:validation.apiKey") } break } @@ -221,6 +223,7 @@ type ValidationError = { function validateProviderAgainstOrganizationSettings( apiConfiguration: ProviderSettings, + t: TFunction, organizationAllowList?: OrganizationAllowList, ): ValidationError | undefined { if (organizationAllowList && !organizationAllowList.allowAll) { @@ -234,7 +237,7 @@ function validateProviderAgainstOrganizationSettings( if (!providerConfig) { return { - message: i18next.t("settings:validation.providerNotAllowed", { provider }), + message: t("settings:validation.providerNotAllowed", { provider }), code: "PROVIDER_NOT_ALLOWED", } } @@ -245,7 +248,7 @@ function validateProviderAgainstOrganizationSettings( if (modelId && !allowedModels.includes(modelId)) { return { - message: i18next.t("settings:validation.modelNotAllowed", { + message: t("settings:validation.modelNotAllowed", { model: modelId, provider, }), @@ -265,10 +268,11 @@ function validateProviderAgainstOrganizationSettings( * provide valid ARNs without restriction. See issue #10108. * * @param arn The ARN string to validate + * @param t Translates the region mismatch message * @param region Optional region to check against the ARN's region * @returns An object with validation results: { isValid, arnRegion, errorMessage } */ -export function validateBedrockArn(arn: string, region?: string) { +export function validateBedrockArn(arn: string, t: TFunction, region?: string) { // Try to extract region from ARN for region mismatch warning. // This is a permissive regex that attempts to find the region component // without enforcing strict ARN format validation. @@ -280,7 +284,7 @@ export function validateBedrockArn(arn: string, region?: string) { return { isValid: true, arnRegion, - errorMessage: i18next.t("settings:validation.arn.regionMismatch", { arnRegion, region }), + errorMessage: t("settings:validation.arn.regionMismatch", { arnRegion, region }), } } @@ -290,6 +294,7 @@ export function validateBedrockArn(arn: string, region?: string) { function validateDynamicProviderModelId( apiConfiguration: ProviderSettings, + t: TFunction, routerModels?: RouterModels, ): string | undefined { const provider = apiConfiguration.apiProvider ?? "" @@ -302,13 +307,13 @@ function validateDynamicProviderModelId( const modelId = getModelId(apiConfiguration) if (!modelId) { - return i18next.t("settings:validation.modelId") + return t("settings:validation.modelId") } const models = routerModels?.[provider] if (models && Object.keys(models).length > 1 && !Object.keys(models).includes(modelId)) { - return i18next.t("settings:validation.modelAvailability", { modelId }) + return t("settings:validation.modelAvailability", { modelId }) } return undefined @@ -320,6 +325,7 @@ function validateDynamicProviderModelId( */ export function getModelValidationError( apiConfiguration: ProviderSettings, + t: TFunction, routerModels?: RouterModels, organizationAllowList?: OrganizationAllowList, ): string | undefined { @@ -330,13 +336,13 @@ export function getModelValidationError( apiModelId: modelId || "", } - const orgError = validateProviderAgainstOrganizationSettings(configWithModelId, organizationAllowList) + const orgError = validateProviderAgainstOrganizationSettings(configWithModelId, t, organizationAllowList) if (orgError && orgError.code === "MODEL_NOT_ALLOWED") { return orgError.message } - return validateDynamicProviderModelId(configWithModelId, routerModels) + return validateDynamicProviderModelId(configWithModelId, t, routerModels) } /** @@ -351,18 +357,19 @@ export function getModelValidationError( */ export function validateApiConfigurationExcludingModelErrors( apiConfiguration: ProviderSettings, + t: TFunction, _routerModels?: RouterModels, // Keeping this for compatibility with the old function. organizationAllowList?: OrganizationAllowList, ): string | undefined { if (apiConfiguration.apiProvider !== providerIdentifiers.zooGateway) { - const keysAndIdsPresentErrorMessage = validateModelsAndKeysProvided(apiConfiguration) + const keysAndIdsPresentErrorMessage = validateModelsAndKeysProvided(apiConfiguration, t) if (keysAndIdsPresentErrorMessage) { return keysAndIdsPresentErrorMessage } } - const extraBodyError = validateOpenAiExtraBody(apiConfiguration) + const extraBodyError = validateOpenAiExtraBody(apiConfiguration, t) if (extraBodyError) { return extraBodyError @@ -370,6 +377,7 @@ export function validateApiConfigurationExcludingModelErrors( const organizationAllowListError = validateProviderAgainstOrganizationSettings( apiConfiguration, + t, organizationAllowList, )