From 6f06052bcdde6fa1430e9a45fb8b652341211f06 Mon Sep 17 00:00:00 2001 From: rrewll Date: Sun, 25 Oct 2026 20:12:10 +0300 Subject: [PATCH 1/3] fix(ui): make UI language change consistent (#2185) --- .roo/rules-translate/001-general-rules.md | 6 +- .roo/skills/roo-translation/SKILL.md | 6 +- webview-ui/eslint.config.mjs | 34 +++ webview-ui/playwright/AppProviders.tsx | 17 +- webview-ui/playwright/TranslationContext.ts | 13 -- webview-ui/playwright/vite.config.ts | 4 - webview-ui/src/App.tsx | 14 +- webview-ui/src/__tests__/App.spec.tsx | 30 +-- .../src/__tests__/FileChangesPanel.spec.tsx | 37 +--- .../src/components/chat/Announcement.tsx | 7 +- .../src/components/chat/ApiConfigSelector.tsx | 4 +- .../components/chat/AutoApproveDropdown.tsx | 4 +- .../chat/AutoApprovedRequestLimitWarning.tsx | 9 +- webview-ui/src/components/chat/ChatRow.tsx | 11 +- .../src/components/chat/ChatTextArea.tsx | 4 +- webview-ui/src/components/chat/ChatView.tsx | 4 +- .../chat/CheckpointRestoreDialog.tsx | 4 +- .../src/components/chat/CheckpointWarning.tsx | 4 +- .../src/components/chat/CodeIndexPopover.tsx | 7 +- .../chat/CodebaseSearchResultsDisplay.tsx | 4 +- .../src/components/chat/CommandExecution.tsx | 5 +- .../components/chat/CommandExecutionError.tsx | 3 +- .../src/components/chat/ContextMenu.tsx | 5 +- .../components/chat/ContextWindowProgress.tsx | 12 +- .../src/components/chat/FollowUpSuggest.tsx | 4 +- .../components/chat/IndexingStatusBadge.tsx | 4 +- .../MessageModificationConfirmationDialog.tsx | 4 +- .../src/components/chat/ModeSelector.tsx | 4 +- .../src/components/chat/ModelSelector.tsx | 4 +- .../chat/ProfileViolationWarning.tsx | 4 +- webview-ui/src/components/chat/TaskHeader.tsx | 16 +- .../src/components/chat/TodoChangeDisplay.tsx | 3 +- .../src/components/chat/TodoListDisplay.tsx | 3 +- webview-ui/src/components/chat/WarningRow.tsx | 4 +- .../src/components/chat/WorktreeSelector.tsx | 4 +- .../chat/__tests__/Announcement.spec.tsx | 19 +- .../chat/__tests__/ApiConfigSelector.spec.tsx | 4 +- .../__tests__/AutoApproveDropdown.spec.tsx | 4 +- .../__tests__/BatchFilePermission.spec.tsx | 56 ++--- .../CheckpointRestoreDialog.spec.tsx | 6 +- .../chat/__tests__/CommandExecution.spec.tsx | 6 +- .../chat/__tests__/FollowUpSuggest.spec.tsx | 5 +- .../__tests__/IndexingStatusBadge.spec.tsx | 47 +---- .../chat/__tests__/ModeSelector.spec.tsx | 4 +- .../chat/__tests__/ModelSelector.spec.tsx | 4 +- .../ThemeSensitiveStatus.visual.fixture.tsx | 71 ++++--- .../chat/__tests__/WarningRow.spec.tsx | 6 +- .../chat/__tests__/WorktreeSelector.spec.tsx | 4 +- .../src/components/common/CodeBlock.tsx | 4 +- .../src/components/common/ImageViewer.tsx | 4 +- .../common/MermaidActionButtons.tsx | 4 +- .../src/components/common/MermaidBlock.tsx | 4 +- .../src/components/common/MermaidButton.tsx | 4 +- .../src/components/common/TelemetryBanner.tsx | 6 +- .../common/__tests__/CodeBlock.spec.tsx | 6 +- .../common/__tests__/MermaidBlock.spec.tsx | 4 +- .../common/__tests__/TelemetryBanner.spec.tsx | 5 +- .../TelemetryBanner.visual.fixture.tsx | 11 +- .../__tests__/TelemetryBanner.visual.i18n.ts | 7 +- .../history/BatchDeleteTaskDialog.tsx | 4 +- .../src/components/history/CopyButton.tsx | 4 +- .../src/components/history/DeleteButton.tsx | 4 +- .../components/history/DeleteTaskDialog.tsx | 4 +- .../src/components/history/ExportButton.tsx | 4 +- .../src/components/history/HistoryPreview.tsx | 4 +- .../src/components/history/HistoryView.tsx | 4 +- .../src/components/history/RenameButton.tsx | 4 +- .../history/SubtaskCollapsibleRow.tsx | 4 +- .../src/components/history/TaskItemFooter.tsx | 6 +- .../components/history/TaskStatusBadge.tsx | 4 +- .../components/history/TaskTitleEditor.tsx | 4 +- .../__tests__/BatchDeleteTaskDialog.spec.tsx | 4 +- .../history/__tests__/CopyButton.spec.tsx | 4 +- .../history/__tests__/DeleteButton.spec.tsx | 4 +- .../__tests__/DeleteTaskDialog.spec.tsx | 4 +- .../history/__tests__/ExportButton.spec.tsx | 4 +- .../history/__tests__/HistoryView.spec.tsx | 4 +- .../history/__tests__/RenameButton.spec.tsx | 4 +- .../history/__tests__/SubtaskRow.spec.tsx | 4 +- .../history/__tests__/TaskGroupItem.spec.tsx | 4 +- .../history/__tests__/TaskItem.spec.tsx | 4 +- .../history/__tests__/TaskItemFooter.spec.tsx | 4 +- .../__tests__/TaskTitleEditor.spec.tsx | 4 +- .../components/marketplace/IssueFooter.tsx | 5 +- .../marketplace/MarketplaceListView.tsx | 4 +- .../marketplace/MarketplaceView.tsx | 4 +- .../__tests__/IssueFooter.spec.tsx | 1 + .../__tests__/MarketplaceListView.spec.tsx | 5 +- .../__tests__/MarketplaceView.spec.tsx | 5 +- .../components/MarketplaceInstallModal.tsx | 4 +- .../components/MarketplaceItemCard.tsx | 8 +- ...placeInstallModal-optional-params.spec.tsx | 4 +- .../MarketplaceInstallModal.spec.tsx | 4 +- .../__tests__/MarketplaceItemCard.spec.tsx | 4 +- .../src/components/mcp/McpEnabledToggle.tsx | 4 +- webview-ui/src/components/mcp/McpToolRow.tsx | 4 +- webview-ui/src/components/mcp/McpView.tsx | 9 +- .../mcp/__tests__/McpEnabledToggle.spec.tsx | 4 +- .../mcp/__tests__/McpToolRow.spec.tsx | 4 +- .../components/mcp/__tests__/McpView.spec.tsx | 5 +- .../src/components/modes/DeleteModeDialog.tsx | 4 +- webview-ui/src/components/modes/ModesView.tsx | 11 +- webview-ui/src/components/settings/About.tsx | 7 +- .../components/settings/ApiConfigManager.tsx | 4 +- .../src/components/settings/ApiOptions.tsx | 11 +- .../settings/AutoApproveSettings.tsx | 6 +- .../components/settings/AutoApproveToggle.tsx | 4 +- .../settings/CheckpointSettings.tsx | 7 +- .../ConsecutiveMistakeLimitControl.tsx | 4 +- .../settings/ContextManagementSettings.tsx | 4 +- .../components/settings/CreateRuleDialog.tsx | 4 +- .../components/settings/CreateSkillDialog.tsx | 4 +- .../settings/CreateSlashCommandDialog.tsx | 4 +- .../settings/CustomToolsSettings.tsx | 4 +- .../settings/ExperimentalFeature.tsx | 4 +- .../settings/ExperimentalSettings.tsx | 4 +- .../settings/FilePatternAllowlist.tsx | 4 +- .../settings/ImageGenerationSettings.tsx | 4 +- .../components/settings/LanguageSettings.tsx | 4 +- .../src/components/settings/ModelInfoView.tsx | 4 +- .../src/components/settings/ModelPicker.tsx | 6 +- .../settings/NotificationSettings.tsx | 4 +- .../components/settings/PromptsSettings.tsx | 4 +- .../components/settings/R1FormatSetting.tsx | 4 +- .../settings/RateLimitSecondsControl.tsx | 4 +- .../src/components/settings/RulesSettings.tsx | 4 +- .../settings/SettingsSearchInput.tsx | 4 +- .../settings/SettingsSearchResults.tsx | 4 +- .../src/components/settings/SettingsView.tsx | 4 +- .../components/settings/SkillsSettings.tsx | 7 +- .../settings/SlashCommandsSettings.tsx | 6 +- .../settings/TemperatureControl.tsx | 4 +- .../components/settings/TerminalSettings.tsx | 25 ++- .../components/settings/ThinkingBudget.tsx | 4 +- .../settings/TodoListSettingsControl.tsx | 4 +- .../settings/ToolRepetitionLimitControl.tsx | 4 +- .../src/components/settings/UISettings.tsx | 4 +- .../src/components/settings/Verbosity.tsx | 4 +- .../settings/__tests__/About.spec.tsx | 91 +++------ .../ApiOptions.provider-filtering.spec.tsx | 5 +- .../__tests__/AutoApproveSettings.spec.tsx | 5 +- .../__tests__/AutoApproveToggle.spec.tsx | 40 +--- .../ContextManagementSettings.spec.tsx | 13 -- .../__tests__/CreateRuleDialog.spec.tsx | 4 +- .../__tests__/CreateSkillDialog.spec.tsx | 4 +- .../__tests__/ExperimentalSettings.spec.tsx | 4 +- .../ImageGenerationSettings.spec.tsx | 6 +- .../settings/__tests__/ModelInfoView.spec.tsx | 4 +- .../ModelInfoView.visual.fixture.tsx | 14 +- .../__tests__/ModelPicker.deprecated.spec.tsx | 5 +- .../__tests__/PromptsSettings.spec.tsx | 4 +- .../settings/__tests__/RulesSettings.spec.tsx | 4 +- .../SettingsView.change-detection.spec.tsx | 7 +- .../SettingsView.unsaved-changes.spec.tsx | 8 +- .../__tests__/SkillsSettings.spec.tsx | 5 +- .../__tests__/SlashCommandsSettings.spec.tsx | 5 +- .../TerminalSettings.profile.spec.tsx | 5 +- .../TodoListSettingsControl.spec.tsx | 4 +- .../ToolRepetitionLimitControl.spec.tsx | 4 +- .../settings/providers/Anthropic.tsx | 4 +- .../components/settings/providers/Baseten.tsx | 4 +- .../components/settings/providers/Bedrock.tsx | 4 +- .../settings/providers/BedrockCustomArn.tsx | 10 +- .../settings/providers/DeepSeek.tsx | 4 +- .../settings/providers/Fireworks.tsx | 4 +- .../settings/providers/Friendli.tsx | 4 +- .../components/settings/providers/Gemini.tsx | 4 +- .../settings/providers/IOIntelligence.tsx | 4 +- .../components/settings/providers/Kenari.tsx | 4 +- .../settings/providers/KimiCode.tsx | 4 +- .../settings/providers/LMStudio.tsx | 6 +- .../components/settings/providers/LiteLLM.tsx | 4 +- .../components/settings/providers/Mimo.tsx | 4 +- .../components/settings/providers/MiniMax.tsx | 4 +- .../components/settings/providers/Mistral.tsx | 4 +- .../settings/providers/Moonshot.tsx | 4 +- .../components/settings/providers/NanoGPT.tsx | 4 +- .../components/settings/providers/Ollama.tsx | 4 +- .../components/settings/providers/OpenAI.tsx | 4 +- .../settings/providers/OpenAICodex.tsx | 4 +- .../OpenAICodexRateLimitDashboard.tsx | 4 +- .../providers/OpenAICodexSpeedSelector.tsx | 4 +- .../settings/providers/OpenAICompatible.tsx | 4 +- .../settings/providers/OpenCodeGo.tsx | 4 +- .../settings/providers/OpenRouter.tsx | 4 +- .../src/components/settings/providers/Poe.tsx | 4 +- .../settings/providers/Requesty.tsx | 4 +- .../settings/providers/SambaNova.tsx | 4 +- .../components/settings/providers/Unbound.tsx | 4 +- .../settings/providers/VSCodeLM.tsx | 4 +- .../settings/providers/VercelAiGateway.tsx | 4 +- .../components/settings/providers/Vertex.tsx | 6 +- .../src/components/settings/providers/XAI.tsx | 4 +- .../src/components/settings/providers/ZAi.tsx | 4 +- .../settings/providers/ZooGateway.tsx | 4 +- .../providers/__tests__/Bedrock.spec.tsx | 4 +- .../providers/__tests__/Friendli.spec.tsx | 4 +- .../providers/__tests__/Gemini.spec.tsx | 4 +- .../__tests__/IOIntelligence.spec.tsx | 4 +- .../providers/__tests__/Kenari.spec.tsx | 4 +- .../providers/__tests__/LiteLLM.spec.tsx | 4 +- .../providers/__tests__/Moonshot.spec.tsx | 4 +- .../providers/__tests__/NanoGPT.spec.tsx | 4 +- .../providers/__tests__/Ollama.spec.tsx | 4 +- .../__tests__/Ollama.visual.fixture.tsx | 26 +-- .../providers/__tests__/OpenAI.spec.tsx | 4 +- .../providers/__tests__/OpenAICodex.spec.tsx | 4 +- .../__tests__/OpenAICodex.visual.fixture.tsx | 14 +- .../OpenAICodexRateLimitDashboard.spec.tsx | 4 +- .../__tests__/OpenAICompatible.spec.tsx | 4 +- .../OpenAICompatible.visual.fixture.tsx | 23 +-- .../providers/__tests__/OpenCodeGo.spec.tsx | 4 +- .../settings/providers/__tests__/Poe.spec.tsx | 4 +- .../__tests__/ProviderRouting.spec.tsx | 4 +- .../providers/__tests__/Requesty.spec.tsx | 4 +- .../providers/__tests__/Vertex.spec.tsx | 16 +- .../providers/__tests__/ZooGateway.spec.tsx | 4 +- webview-ui/src/components/welcome/RooTips.tsx | 6 +- .../src/components/welcome/WelcomeLanding.tsx | 8 +- .../welcome/WelcomeViewProvider.tsx | 24 ++- .../__tests__/WelcomeViewProvider.spec.tsx | 13 +- .../worktrees/CreateWorktreeModal.tsx | 4 +- .../worktrees/DeleteWorktreeModal.tsx | 4 +- .../components/worktrees/WorktreesView.tsx | 4 +- .../src/context/ExtensionStateContext.tsx | 3 + webview-ui/src/hooks/useTooManyTools.ts | 4 +- webview-ui/src/i18n/TranslationContext.tsx | 57 ------ .../src/i18n/__mocks__/TranslationContext.tsx | 58 ------ .../__tests__/TranslationContext.spec.tsx | 74 ------- .../createFixtureI18n.visual.i18n.ts | 35 ++++ .../i18n/__tests__/hydrationLanguage.spec.tsx | 71 +++++++ .../i18n/__tests__/languageSwitch.spec.tsx | 85 ++++++++ webview-ui/src/i18n/language.ts | 15 ++ webview-ui/src/i18n/setup.ts | 6 +- webview-ui/src/utils/__tests__/format.spec.ts | 120 ++++++----- .../src/utils/__tests__/validate.spec.ts | 193 ++++++++++++------ webview-ui/src/utils/format.ts | 41 ++-- webview-ui/src/utils/validate.ts | 96 +++++---- 238 files changed, 1191 insertions(+), 1251 deletions(-) delete mode 100644 webview-ui/playwright/TranslationContext.ts delete mode 100644 webview-ui/src/i18n/TranslationContext.tsx delete mode 100644 webview-ui/src/i18n/__mocks__/TranslationContext.tsx delete mode 100644 webview-ui/src/i18n/__tests__/TranslationContext.spec.tsx create mode 100644 webview-ui/src/i18n/__tests__/createFixtureI18n.visual.i18n.ts create mode 100644 webview-ui/src/i18n/__tests__/hydrationLanguage.spec.tsx create mode 100644 webview-ui/src/i18n/__tests__/languageSwitch.spec.tsx create mode 100644 webview-ui/src/i18n/language.ts 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..3ec30873bc 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 = ( 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__/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/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/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..31962c5b29 --- /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" }) + + expect(screen.getByTestId("tips")).toHaveTextContent( + i18next.t("chat:rooTips.customizableModes.title", { lng: "es" }), + ) + }) +}) + +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..161a09e40f 100644 --- a/webview-ui/src/utils/__tests__/format.spec.ts +++ b/webview-ui/src/utils/__tests__/format.spec.ts @@ -2,58 +2,64 @@ 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" }, })) +// Stands in for the `t` a component gets from `useTranslation`. +const t = (key: string, options?: Record) => { + 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 +} + 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)) + + expect(formatLargeNumber(1500, givenT)).toBe("1.5 mil") + expect(givenT).toHaveBeenCalledWith("common:number_format.thousand_suffix") }) }) @@ -83,71 +89,79 @@ 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 + + expect(formatTimeAgo(timestamp, givenT)).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..b1bf7abad2 100644 --- a/webview-ui/src/utils/__tests__/validate.spec.ts +++ b/webview-ui/src/utils/__tests__/validate.spec.ts @@ -5,22 +5,6 @@ import { 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 { formatOpenAiExtraBodyValidationError, getModelValidationError, @@ -29,6 +13,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 +} + describe("Model Validation Functions", () => { const mockRouterModels: RouterModels = { openrouter: { @@ -88,7 +84,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 +94,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 +104,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 +114,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 +148,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 +160,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 +170,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 +203,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 +219,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 +235,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 +253,7 @@ describe("Model Validation Functions", () => { const result = validateApiConfigurationExcludingModelErrors( config, + t, mockRouterModels, restrictiveOrganization, ) @@ -258,7 +270,7 @@ describe("Model Validation Functions", () => { } expect( - validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization), + validateApiConfigurationExcludingModelErrors(config, t, mockRouterModels, allowAllOrganization), ).toBeUndefined() }) @@ -279,9 +291,9 @@ describe("Model Validation Functions", () => { openAiExtraBody, } - expect(validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization)).toBe( - expectedError, - ) + expect( + validateApiConfigurationExcludingModelErrors(config, t, mockRouterModels, allowAllOrganization), + ).toBe(expectedError) }) }) @@ -297,9 +309,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 +336,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 +376,7 @@ describe("Model Validation Functions", () => { openAiExtraBody: "not json", } - expect(validateApiConfiguration(config, mockRouterModels, allowAllOrganization)).toBeUndefined() + expect(validateApiConfiguration(config, t, mockRouterModels, allowAllOrganization)).toBeUndefined() }) }) @@ -376,7 +388,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 +404,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 +420,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 +432,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 +448,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 +464,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 +476,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 +486,7 @@ describe("Model Validation Functions", () => { ioIntelligenceModelId: "zai-org/GLM-4.6", } - expect(validateApiConfiguration(config, mockRouterModels)).toBeUndefined() + expect(validateApiConfiguration(config, t, mockRouterModels)).toBeUndefined() }) }) @@ -465,7 +497,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 +506,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 +516,7 @@ describe("Model Validation Functions", () => { nanoGptModelId: "openai/model", } - expect(validateApiConfiguration(config, mockRouterModels)).toBeUndefined() + expect(validateApiConfiguration(config, t, mockRouterModels)).toBeUndefined() }) }) @@ -496,7 +528,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 +544,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 +562,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 +572,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 +583,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 +600,7 @@ describe("Model Validation Functions", () => { const result = validateApiConfigurationExcludingModelErrors( config, + t, mockRouterModels, allowAllOrganization, ) @@ -573,6 +616,7 @@ describe("Model Validation Functions", () => { const result = validateApiConfigurationExcludingModelErrors( config, + t, mockRouterModels, allowAllOrganization, ) @@ -594,6 +638,7 @@ describe("Model Validation Functions", () => { const result = validateApiConfigurationExcludingModelErrors( config, + t, mockRouterModels, orgWithoutZooGateway, ) @@ -609,7 +654,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 +668,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 +683,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 +699,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 +715,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 +725,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 +733,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 +748,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 +756,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 +767,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 +779,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 +788,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..14f70d0b64 100644 --- a/webview-ui/src/utils/format.ts +++ b/webview-ui/src/utils/format.ts @@ -1,14 +1,17 @@ import i18next from "i18next" -export function formatLargeNumber(num: number): string { +// Structural rather than i18next's branded `TFunction`, so test doubles can stand in for `t`. +type Translate = (key: string, options?: Record) => string + +export function formatLargeNumber(num: number, t: Translate): 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 +32,7 @@ export const formatDate = (timestamp: number) => { }) } -export const formatTimeAgo = (timestamp: number) => { +export const formatTimeAgo = (timestamp: number, t: Translate) => { const now = Date.now() const diff = now - timestamp const seconds = Math.floor(diff / 1000) @@ -41,38 +44,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..c2c4181dde 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -1,5 +1,3 @@ -import i18next from "i18next" - import { type ProviderSettings, type OrganizationAllowList, @@ -11,6 +9,9 @@ import { githubCopilotLanguageModel, } from "@roo-code/types" +// Structural rather than i18next's branded `TFunction`, so test doubles can stand in for `t`. +type Translate = (key: string, options?: Record) => string + type OpenAiExtraBodyParseResult = ReturnType type OpenAiExtraBodyTranslationKey = `settings:validation.openAiExtraBody.${ "invalidJson" | "objectRequired" | "reservedKeys"}` @@ -33,27 +34,28 @@ export function formatOpenAiExtraBodyValidationError( return t(`settings:validation.openAiExtraBody.${result.reason}`) } -function validateOpenAiExtraBody(apiConfiguration: ProviderSettings): string | undefined { +function validateOpenAiExtraBody(apiConfiguration: ProviderSettings, t: Translate): 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: Translate, 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 +63,7 @@ export function validateApiConfiguration( const organizationAllowListError = validateProviderAgainstOrganizationSettings( apiConfiguration, + t, organizationAllowList, ) @@ -68,82 +71,83 @@ export function validateApiConfiguration( return organizationAllowListError.message } - return validateDynamicProviderModelId(apiConfiguration, routerModels) + return validateDynamicProviderModelId(apiConfiguration, t, routerModels) } function validateModelsAndKeysProvided( apiConfiguration: ProviderSettings, + t: Translate, 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 +155,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 +225,7 @@ type ValidationError = { function validateProviderAgainstOrganizationSettings( apiConfiguration: ProviderSettings, + t: Translate, organizationAllowList?: OrganizationAllowList, ): ValidationError | undefined { if (organizationAllowList && !organizationAllowList.allowAll) { @@ -234,7 +239,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 +250,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 +270,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: Translate, 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 +286,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 +296,7 @@ export function validateBedrockArn(arn: string, region?: string) { function validateDynamicProviderModelId( apiConfiguration: ProviderSettings, + t: Translate, routerModels?: RouterModels, ): string | undefined { const provider = apiConfiguration.apiProvider ?? "" @@ -302,13 +309,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 +327,7 @@ function validateDynamicProviderModelId( */ export function getModelValidationError( apiConfiguration: ProviderSettings, + t: Translate, routerModels?: RouterModels, organizationAllowList?: OrganizationAllowList, ): string | undefined { @@ -330,13 +338,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 +359,19 @@ export function getModelValidationError( */ export function validateApiConfigurationExcludingModelErrors( apiConfiguration: ProviderSettings, + t: Translate, _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 +379,7 @@ export function validateApiConfigurationExcludingModelErrors( const organizationAllowListError = validateProviderAgainstOrganizationSettings( apiConfiguration, + t, organizationAllowList, ) From b1bf9878a52a1229d4140d4ef40522dec3096d74 Mon Sep 17 00:00:00 2001 From: rrewll Date: Mon, 26 Oct 2026 12:55:48 +0300 Subject: [PATCH 2/3] test(webview): refresh baselines added before #2163 landed #2122 (task rename) and #2160 (OpenCode Go temperature) added these baselines against the mocked toolkit; #2163 switched the gallery to the real VS Code toolkit without regenerating them. Rendered in the CI Playwright image (v1.62.1-noble). Co-Authored-By: Claude Opus 5.5 --- .../history-rename-editor-dark.png | Bin 18446 -> 18141 bytes ...tory-rename-editor-high-contrast-light.png | Bin 18263 -> 17951 bytes .../history-rename-editor-high-contrast.png | Bin 18434 -> 18060 bytes .../history-rename-editor-light.png | Bin 18536 -> 18186 bytes .../opencode-go-temperature.png | Bin 25005 -> 25615 bytes 5 files changed, 0 insertions(+), 0 deletions(-) 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 2bff5fbfafc180d5f2540e8a2becd186ab958445..3850a8dd8c49a34729906be70be608b1e52de004 100644 GIT binary patch literal 18141 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 bObyB2;h5=9<;w@JK-84Am5QGH6a0Sx+Ou|I 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 1174b66e5b34c1d2015c937aa9826c3fbca88dff..ebc45d83713758cc5d4a9ac599d75a30ba6023d3 100644 GIT binary patch literal 17951 zcmeIaXH=7Iob4S{!I2_-ZI5di^F5b3=np?5+P zK~P#~ArL|nX$geh+qpPr=EGU*oOx&Fta(4pH^oTq+~t4m{oDIWh_lpaSps&v|1oAgT?eQa+XX5LSw2F~7?_pX6*~Z|Dfpxq^^&&I<^s!LpJLv&`U0XVo*77)* zERwv!ujMydXi|69vMW(aM0VBJZ^lEVr6D)>Cj(?K1AiP+#N5aN`Es?2;oE}~5|HQb z#!o`p|E56xxfH+!xvS)K{I&UC4v?ZV|DG2;GiXeTj*kBH={I+|WBs-Tol>1_>$g1> z%j>__bJu_Q2S*sYpbO;r%e}2-g2G{K;Dwz2ag$ol<*ZnfovBz;Y1M_A#rgyZ$K;v# zxxSpky;XvR0c%<-|0KP}W3m2Vhqms|^ev>+tTh5xSihrixJ?)=He0M+?aDW*bf@-X zl+=fZQ8sc$^+OM*lMc6WCR6xQm-3^4KfXpbqYQMr>66BEJb*o{G$=;T7#+2vO|M40zE zOgtMgH`J-`Sh!kRT5Ps^RC!rWopO#)T%GQOzdKkj^cPd;N>Na_cTZcZQHrkqmqXS@ zr^HxZ&dzj#<3O%XlT^#WHo-p`O~qKI_*Q&@ziZf~*CdGBGoL=)NAYW7k=R@7(@OE( zh;!%-gF>O^n%$yd^1Jh;oT5oG9s?)(YCIhC6UFVxcoZ!?BSSUva{i;#TLfBRgTrOT ztVScpx~v*WNW^z#AUOqmx-`6&V9T6`j3p{pOdHwU!pGT#ydCq0Uvhf`p9FS*Z7HczFng9B$ zdu_7nfyei^r*=0M^j6MXdhpoRZ-1Sn63(fSEGu>O>Lb*e7|D1Nl}p6Jxuo54r}Mq( z)vE1=tNWW{rY^s3N*~_ivT{uE+nHOLYBwsgN>l0`Ez^CJsigk&Y0Z2|dz<{0%d$gE zo=Ke{CST-=9AWY!xx{^eytd{hn3?@AFo6}y(cHr*jU)_#(WMwG~+Lua7OV@9q z_ye)`r#hA=?^&w(Lb60hPqc$T*ByL*1b2NL5A8h`lz~>e;S#1 zVP2L6mHP*~i;=P`KMgFC)>qnv1&q|^Pvo9TJ4~>3L08>;`X$kwY)vQZTA!dV)-4rOLYt9Zh`{@p9IWfermm5(fpaU5hm`>h zeiw&^+m9BTsjgOGyJIZKk^JI6-k&m-AO>xY`8RZt%JZbP&iPLSaoBi$d(9y2BADpf zCB@GamSXfZ-Du>KNgY}v?tyY}&ME|j=OMjH-JXBLbh*1^`c%w>VTMqsE)5JEl6rX( zJTlgO35j=eZfmN6>;BRGoo$lCKb8JgOzT&7NA~Tx@^C-Pa4re?O^R!I>XO|4T3>>w zwXP2W+ueEkMedp(6_eB9g454`W)tRj!Nyu{W6%t(Bbq<==Ldrm8%z`Hre_k#Eu|}X zZ2vj=3lr;QHy?Ix?!DJxSi2Z={!7FS*rV)-*0vadPVvxx2$8pE^|NYkv96kp;8C0O z5nLN#Up&&8Z{ZwK#F;5b(G1KFiyrs=iL59 zu-u5z1B{9e&Y<$SqFB!*O(`CBigb>)o|-o-3dgChy2!0sZbR&aWn&E%I5p<68S6E$ z-`@7wBGGwycM0BcexGv2GT~Evq z@t_+|eZaWJB;bE%k@cc-l1zP;f0*ndB#3us2f(A~m82pd;ZCPme}M*3J!pSYo0*7G zb$Iq$x-qxu0$=L;vu&hi(};8W!-l&arpa)}Z*Gbojx(LjZOy;@UVPQqfZP}xyZOd_ zw)>HHzjc5~$>iRnvz>p?)6B+lWf6}KC6NA~+v(yTg%qgRpj{Ji8G3QrK8`k5kxq?_ z(x>|_g|l-*R5flF-YORe<2PMb=^qzYppsLuDls@}_|Xi#72rlIdeJ1in5D8&V&2Zy z86nU~kXe+S$|&%}$ff&5#Q&5&jToX=-8xQ?73da^ZA0*M;4R=FgHE<0GP zE-`KJZRao zXIP{I5!18uWC7wdT-Zc4-7=FNoSv7~J{hS{@bs9?l)!r!J9u<7rQibj8TsucZ4Qhn zmX7$B1=Z7>$QyS@C-qeljKpSU-?-g4JlJf2+v2jje*WzKtkcx)mgIIUi)u8Z=YP06 z1~;YQvrITfGRJ}o1ZQt{i<2L&my;SU_GW1!FD#)$7+|l}5p@Z$C)g7MyRfMu>dSJ@ zP)8}BEvFyFdYAdXA3Jue*5>Ylv6%I%SFb`mbY9-LapR5=nflH6jtN-0{n zjfK$J@g@Ftbh74Dk5W!%=3`}LuY!Jt+((P39|g^Un@CLKLe;^qn(TUu&UmTTsAEWi z>TMbRJuzN9DdF^A^e|RVQE7o5B1G(i`f!ZN-K2reulJRFotVDqBuF}^+gwNb>z--hf-?MVqZK(;PS zE;|25l-K_*&!&qMXz|I(eqe0Q9-f7y+Wd9;v^OokQnq1Y9{dv;tjp|&FEt?FLZFj}WKKI4Zg_#+THolxQJkoAJEr#Ca-Mr_2;6w0VbUm-`!M8B5 zw6ruoZx_JzaL9*+4m8Rpm1sLZSe)Ig{~R}%XHaSZ7Atl^!#^v!=M1bW>7g}10Yybc zy?OJ7{ER8vNJ&YFOUx!Se7)PK!mj5L5N)tZsy6Bg&|AsD(Oeb_L5vjq8kNKH^LD z;c|Vio&sLah^>T$OWb%N9VTYP&s?s{z`?mG+I{#N>W0a zSUKJA`?y#$);jM^`sAY?gO(QcJ3q1lbf>W1SyXf7ul(k%>y8hh&n<2nF74b-V|KR3 zNW)ZfCG|t7RV&Et+~)qeXK>tBdwaW4l~Wp;jXyD_h<~*6a#{FG1Q$-)p*!_W1>zab zW&rcfx-&u1gezB*)I#o{@#W@Z^4L6*k@!(QGl8iZxcjnzefEv4Bk={TKAyirbB)=` zjG**D%QPX5L%nikhWYj9;KKa#@POkd&ny{Y{(;X*$_pSOjbsqc3q$Ahd9dG2Z(@f+ z9_XCQ{;7YLUsU5U-g+ut%(fq;GawMJQTZrAxu!}&FPWeBNX`B&fhr48Zc2P{=MoR1 zKR$+j8cP?O_6#K`C0jyFDyg1sFlkop{Q^a!)zF!}Ox3kL9g$F}lbcti3dRRcy}fli ztKs{|YB6N5dYh9HDk8?bv$?1MbZe=KR~qvUkoxWsicw((8+o(P?ob`E*b!TpB+e|= zq{tf1#Vic%!A5Dw{0J*C(m2yfyxJpL^iSilEE7e=07NrQU*nWb^6Znc)UX1R-TQI5 z-4bb-vmuzE$X-njIa`ezHXp3wmwg@+c96qthiehjta6X!e3Mu_q4SUfN?XsYcFCP9 z1@4!`?FLgH1+sc zgU>RQn6gzRTm$DOBnwzmW|v?Z39-toIq~={%{4c-Z|G{EM5d|=*5bp8rJuOI=%b8` zFf%R9Xx9^*3q6*N`+%Xms`PFTtfQJicm5_`m>sRFA$O|_=)F#m6+@imK|Fn2oTdNH z@E5a2smlVnoLOe7URTThggKs+47F(!p2~>7%3KegEUT#s`(aoeaHncFOWB`I8X>O` zS3(t9iH+UQy3>@HO6z7WkP6Vib99r4igLNXN0Y3(;s444q?oB+Y1Y(YH&1d-a)ds= zKe0?_%SNUDNAK6K7h8!PI$EZ2CVQNL%cdF4h{j0xEp?j;^)*EJ&0(xyWP3ZGnF+>vc zoSOoCb=NO-($ksFgc*n=-!h5eUzoyl(fIW728cFe(A&432cFNJjO}=vuM1)7o z_Vd@ex;?gY^K5TnNos2vY&iuhF82N7|GM<1@8Z6rrsiM;uGE&4l+m<)ae{>=`EB{r zmpGo3j}_mcg@*4P*!QS%GZNM?l9coZ`$Q)w*Rbke^vVX)t%0}7rjeZN7cNmD@Fr^J zT6^*yb+lovdO!WLrt_q-_g{_qQNBY8^*(#}J}uhdcLpqWX!#ex|NicArOFR-oeMsexAljkwd-JF+t!DYT+Ct=eZS)AmgM;G1$KAqzOPX%0&9!h zL-G&S>Ud+fO75F+vGbYPc_S!By_<=LOO^ui_;!Z*o)=rRzC&NLNfmS7D{qTgxMCzL z>ICS!qhI&d^$?P;;q{kMKk5n(VH@Hf><4q3i+!IvIg-D`Rrz{63;S5&;kMc-;Sa!$ zg%lqjpSbwOw>QV9TYEB8{deYax}rJ~rB<^O99*zp0WUxT+dfj`!F&H@Gw|$5W_;JL zn~}?`&+`A`6Koo=AB~H~*OamNa1RGpP|D^ih8>Q4|H(l1-o8GbF*{1teIcQOd3!og z#iMoJi$kTYd$~c4flM;3y#LEd`=Me0nJ+ZFcTZWpYi{Pv4z4H%s;^<9q% zY`ZWleN8B>|Luyi-I2H*#lmrp4g2l@TyP-WqTV%w1MscRc-I}kcsioWz{)C@MAGKb zJJPhL9EN|Wct_ed=wzr=`)}E_w6RaH>?HQq&ey>TP4=5u6>67TxXSkc{s{^WUL2{} z1dfBWbwKbx5OMkQ(}sx6%FH}^2>>hGUj&T+t;w!AlCJ|mM-)W$*ZX=49yuy<6R;+~ zCnt?69g;qOUIoKF@{)*38aodUvd~0sWCxG}CPqfN<>m_l`gz@Lcoge?U^=)CxIZKf z=AojZdpxi89Y*l*$gLk7rt_5Q(x_`u>0wEYO<@ZTKqs6(%> zpmhP)AOX4P2Mhu!S4W98Gs!AAC&`@*fYd;y+GoRZ8|wtX`gF8zNJ@GFHHOyavDerC z;y!EQ(3jm=*l@7ZCA0V}QS!d9u$GqIW-n)*JlX{OhvJGka0Cv%K%<_&Z~^rX+8IES z@xsqK7SUw(cwtpIAtVKN$=qNn+yIsUp~Tqdj>GT&4xqdGWKZ9o;-Py zN}?_~up!EfI(*~?-e&tONn5Lu%Sr3Z1CJSX2vko8(w zCz%%BmvS0^$Z})hkrK?78_`%0m9d=tCXM{WOeL_|mQ)a78&ASzI_H`%RDk;l9R}qc z{xSAQTFL~Re!u=*&6xVYNnCHsMg@{jW&<6u(8N!NCiKdv2J<=#hh9$lZS5&laNfwv zlAjL8>03V(AV;GS#?C?FdMr5SRd5H$6o_aOzjDsrDhwC19gyAr^EvxT1F(XkC!qU4 zT2xW7I2Tvf@|6*fF*r9(2xw2@)&NfX`E$>OauT)t2(@)d)9%XQ&#E{=@?|GKh$MT{ zrRpq2f1}0#d3>~Ztq#*AyV|k4*f7@51FkjEX%nqNRx&gsZ_lJGnye0*ManIGJ8sxr z09HnG{R4nPxWw(8j5T67OnvTOyQXM=jbLuSROr9|6nQ1-C5@**B1ZfGU^!l|Kj(O; z?ZORtEYV~+^jg(;z-dFEWrn_=X#VwVwa&u)$CnxP{oI&%wC`BtI|rXRXl10?uyd03 zs4GEkN6fltuJuLJJB(ZR_x#FH?EJt-{d^}tGp)F7-t$|+-{;!&7{n{;Pvp9ag8yLnAL2_ zJkd~KxyO?*GuYy+XKhv!vpKlIOfS>SkC3f```$tKt7*?;FY3%=S#$Z2R||G}oP$QH zXOeMvrKTs#Ih>FB!Uk}cd+s1=@hRVuJVr5`jXtY;JL@#)4*x44unK>QEV7U2Ttbg} z&G40%#|3B&-)STI0}qm|l8-fnKQtTz$O`%shyfUvj+}>i@iq8$BUJd=X@IACjC5pR znFYqxy|hbl^4X*O5rnm8>vS>YkKWsLo1=OvKq41_&w=Ks9^OY^pM&=vpf2+Jmt~nq z?B_~gF;iL=z&G5#f1i_!zMRhpPBZ@3=6>`D|FW}P*fB&9RVCOvrtJ3y|9 zio<+z>wCD2n!H;+Tq2u9rjuR)gNN2;spxHSU-tNTD~()ki%aaq1O`qPoNsmaQH-C7 zZ#s}OkD8=O)C)Mn25s)pUTIs}bN+7F-$)2BTCJ6mwEw5VZXhqt<^q(>P&_BmqOL^u zN6<-po9imKs0j+28=z>A7nM^GG}Mdyob1xZ$3I-2Dy8IjmBlpaD)x9rllFGuu>m{w z$#kFoD+38_z8%1B2`tQg{Rs$Ra762JxOJeXAnr0rPkZ+#?Wab4SXT|aTuL&L(>%h~ zL_#4x3XCT9vDON-IilLi7}J*@_gv{|gMnE6$Z(n6knRht;Ug}@xa9uc+44JZKHEc! z@RJpxwjKktIe{UfX3t*QwTHJ|4W)*aiN3nJy5L4D^Y~jcwT*MwxU@OsdCNp#CJ6S^{i(E7>U$Is98Ei2qd!wSUh!op?WUc4wX|eNj=Ej(La}_5?VmD z(%|KQa-QZ8@>%OqC1#4u6^fD5*vpDP8M?kcZyNq2MhZRr^&mC$v}rq2?&U zWWK^Z2IU%Z=F$^;{j`SITAV+fk?q4GD2ThB){_;FfQD!;Qj`qc)N^`=rFCGXQZLroe*g) z*rkHI1EQ&Ih|?BNJO9qMAX#w68fp_B+soLe_L{VgS6M`Cug(ZOKh4a16p2{(WzP|H ziVyp=g5?i$e<`u3uP)25>`RRKlx~UW?xz=tTb_(JTZ@nefd~(x)%DjWr9&4r->azX zHXk_mh_dh1(mk?mAqImaf3X?zKFPpM+MQ>xd-H=ONoRr(x?NzALK5YX@F zhB=tlbz|j9uSM&uYQI^OX%eE^KPQVtL@Rz0ta7%cc~7r!;<6uiL$8Ys@E{fnTlYbV z(=Sc>Nn%UKn(96IpSR1$h_+2F(cc%P$i;Phoiygo7wd|#v+o@(^WA+?w@p!5EgvyA zp-R}GPN+J${WW-^cw30@S{COuo8jCbYe|K zAHs9)#0z`$biqq!`BOBlQPP;9mp?yVwfV~?tNLN)6~CoMCNVn7)ZPM3o5n^JFr#_i z;p4CsLUn7p8VYEU*=OSX{OUARmQJ|mk7BHKT}|Qx45zN@Xrj5TdY?>bnIKWDSKP*y z3U`U!Ub0h-j^(7prt}n-8ToP#V_%`Rm%*yyf5Z`Wb7S+f6SqHp;ylMGE-lsfq+84t zsp0I;J*-_zHNihV>{eop5$}+lLuTA}B=>Rp*~H9#sz3Yi_-@GU9{Tjl1`B2+P1zu};Nba61oj%DcbJ>PGyI`rA)t-9k2{dukg6?{nJA5!ac^r!WLx@zwU* z!7;@eaIWFk!s^_^qm6Zf*KL>f&Gh|!BK5>(AB1)NU0oGiQeL+~94Z_;KTza;X&6>s z%(9itJu%kQ33!?h*5_7wd1fRZ5?8+&>4`gZZcSqZY`0<;&;Ru=?gw*SzUbW;E5Woc z+aP0u)KD)S#k!Qi95VbBD^5xD)@;@ce&Axc?HvuiQYF{3{Fik3{l%%j23_S`B+E z?X8i#rW+PVth4Avpn>1v{-*!lvg*vN4{&eSRX%KOO-A}s>Qhty*;$)A3)wS($yUt} zQg|<2sG8XVD}xVX-A0Y51& z08H78ocG38^QrP0pe>Yfe(vt#kPZxmkxOtgJ>Mr zpFdg>4YlQkjH`;uI@8nBqrbm+@dBXqd$wiWm5!$HS*-nq^XEgyl;K4s&trBy#(X`1 zFNUQ^A>9$6EKWG3jR`Tye@W}8tgG7A$+i}*%j~@eKuR2jcHS!=?#3J3}sLPk- z(6d)z3AA%?eVWkKt5@R<%XIrf=zhSJV8zE8{2S<5uE#oW;=_(Z;K>boS8ePCc42=k z%+2$G2aCz`TbqioG5yLI({bt2r5_U7q6v&hBFyYhFt6V|1@^H9#qA@*)2`h2I{t=D z&YzhX97AuW+J(|a(5=86CRmAA*bSwxjNP@B)-hG)IWLF{O2<+dAY8B;RnP$&QQT^( zIk945VuJr0L28D3-*ajFQEB+gvgTdvmZ0|x9Riow1I`&SgD(?o*!q1MWs&T&q6S^( zuTBe&AIkNrZet^=T1a<1pqVQq<2U=XY zZ38#e*Q*Gi6;f3tnO{4XodDF}#F+mfviN8+Bt%xZEs#-venx@t`_lcFZ}B%l+u%vQ zVR^s*;qchOG~~7YSzo^{3=CMq_Yn~rcG`Emh-m#SwC@&ygPJUJQ>_6E&@*f0uF-V=jU1h@9 zu&sQ*61`3Za{{D7;E_ZhoI{L6M@99dE1zz?yt}z*TJlQG$;k=KnGW7SIbig`>@hJh z0m1mlSkN>kH`fXrGcZx~nR?JI0!EFEon72$p;zFFtCbY3OtNE z--Avg@(rgBGOECV7=p@#i>XIXl$E|KWPC99$&Y+1{VeOyHt@?53K5$}t(E@%e!#io zdW|M>ch*1}MK*c!H95K9dj;^z7Ec#6Y`ICX_j3IT_4DI@>t@-yxs~m<2eIT+z$5}^ z0iYxz0L=UT;A4h_gn*luxep9DTdtA#k01YLV`Jmu!mR_#Fm1#E$}D0~>&CG$1wlcL z)_>|Nst|=Hz;%KT5jTSbbE^lGhX`JZg=&$P9RfL9Z64=-TIk1o)*X&63$>2PwJ>9>tbS^2oOoXL#*J48 zd)sXk5K7&?eOn+xZZj_&wb`dBqoIbk>bbVy^&=EYejb$sdNpwmOMigpDW;;Wd#Kb> zMqc~&uJLbAstQLB%n*p*pNiiZy2Z6S%lX`Y+- z;}?jM_~nS)ONNF0`X4c?sqYatai8?<$AEPecoyT3C8q248S^}59s4|H9*lU=F%ms{ z7V#3>tS<@X6`#%e^K}}DWRd+NEgm^Ms|wx42Gkux#5vT*_kO;-)*(WG1r;RpZ}h;@ zkY4T!sL$K#o)QFM<<)c%2>dragayKnWE9p6{jH{!^Sg43LovQ^7J9)S8#c0y8x7*T zb(7vrc`6qQQinSfxC6kIs!>#f9$aZ&9cDj0h8maKtS^cy(I;aKB_gM~jOB7kOv=&I zCO2y*sV=#B`aZboG`>fXxL@CV1ZvZ`B~10b9^2T{UcCfOnEEm+Na{=e=gzomfLDzG zSlhnEX)0lFW;lqC&WU(?trzw?0;lvlX40_mWEi?L{ya z1`8F&qbFN~D<8I=Z_iB%HiCVky^&okcm@VuHfaKLhU9wRbOD1Z!B4Y z7NsO+Nj3Z5D=7b8%b5R1J^6pvkOy5l7i%Q#JN6cwjY|7U&Pqa@R!$Xib!QnQCG3b^ z8W8M|GQomcp%m0*20>#EOMGgkWR^heNs|FD)EUCd-rP+vpDRCxZ9p3s%~a+7<0Me7 zMD6$73D7?a`R05%$>Sri0vxnEje_Of$n)OFPj(8wGxatsd(bLG#x z#&H`Zrt7e z1k=}dYa$pRaTgyIr{mvFP0q~B>xO0-+6;&a*g)cU&M?_ zkYaGNw;q|#H<+~>=2R3;M(b5aYy^z5j|-7{Ok^b2-c(+2^l{G6yIdZK9rIN)o4M%JaO+2%wBK&tR$W)riGf`A z;h>3q=>b#(18*clQM|5&A>$dpKO)AN7U0W^t8a({%LJ1UWCZfy_Qy)ItnY z99A8md~qJ%PwCZl=l=3w-F6o#BDt2osMi_kVS0Xfxu1uKlZs6_S0XNDxh3`Dk^z&# z*XW9?HxZTYHgNAAyj|gbjmP>dQDfJm0HLj&iT7^y7w3yC;Jir2`6l`9o~}=*v!!E| zcxf0!yJNHRIes0~y%bH2u`)S=t>X^~r$tlq`SqE%%E~8_Nw^oERTw^V&<8A}w{Bf! zu>IsZn)nf2l=^d(aeJ66WI94RISL^T@5x_r{ho(Jhj1epeW&8C`uP!CG{=hn(Wn@R z-dDUNEp?GBEGbjDG|4w-`)RYr^z$b_?})RD5B*-WY0gZqHnA6U*3VJyY1EB%k=JP@ za`TP9_upcyo5n76ejnaMrN8zx9MH}zonTc8xmXq%JrXoKb z-SIG@t=HX2qsD*_amin_vt>vQ}47V9?ozmPSv5EN#In9%!zHZ}%_ zhn_a*&5B^=r2=~!kSgLT4cc2C#je@9sf~@NaJ*7E!DKv`ljgAhsx1O(z;s*qXCmIJ zvX|QFZ3Jb9$}LY!*X=SVH{9%9Tr$fu>RVi42&X9U7uquEliFxq%yKoBfvRI#$(8)J zYdkvB0|F=C^`FV5SNju?8YV^F_A7oGw^Q~@nxvv9JJHSkUOSV5r3ec-soHAmceSF| zY&^ohR}~1E9!~VG54B}hz4Z;jc!hHjJvZOX!qucQTI?K% zl2-jo)8v*_M&~hSTG#Out>H9LE)gl;YMQ9Bb*|-p+m`(*!dwoebh_@As!v+x3*@0l zO~3ziU-X~;T1&@>{*FI>8i}9I(DhAQGZ;JTJ&?UyNxZsad-ChWw(;c-oSDH~)J((k z|H=Y8w%S!78)X#yjMsLql;c+qYE?&`bBf_=ROoWG2ebcMmWOB__ipSVAV*w8Y0LyI{*s@Sf0sZwO&czze>M|8toI?5 zRz9W%5!tp{>heWfOfDv*OHIFdp~3S_5rISzfn_9tx>Qa&zf{%w7n7L8jOoBn(o%ws zU2S)vRyq@JihIftx2C?@8;YOx8DGduk$V6uPf*VnOW166Z0qqhT956D?R&6#xW6u5 z{?of<%T2vN&~)2kw@Z$xA*J-GR6)H{$nIus`^sBinT)}bH8eWp2%u> z@Q}bpyk79n_usyMPnX+e&K4YU9Zn3Z5@qroU`+3%^WvGXsOuw^zYY3#GFf4_>g? zZmlTJJ^nL9&|CX3n*5czI5WHUeR+u0)gyoS6YiQX$|ZtInSQ~5j_1`HN(Pr>Ur2Iy7>Wjo#a;Hxi4?NX)wZ? zRUJC}?X?XJIivP&x^(k)7L5aoP$8l{onFUl>TxV4mt7HY_8wJ`C+CGRS@0j5Pq&Rx zT@6-GG+V}en&R5v%Z7X4GLW!8ca2hfRY*HFPP7pR@xjC|IX=P*boJtoC&l`&-BYlb zVbA55*n0Q^t2lVq_ORS`M=EYRf&hcDL!os(meJmwzsrMnP91XAptgq6uryRlD!oxv z!Ta!Pz;%c5>;UJvcMABuQ^PJ$sZJ$Uv9@-Z(v**MFX0N!ivW<8;eAN^aqLY5sN+#dx=z8zKxz^>|Q_aLYDY4LZ zB4b+R)#L7^(x!sFu^%lRb^dz+;tu;6hKGKdErYko)@65sEUP#lsAxY#i3P0j$MKq8 z_Q-7{UX#@xfo|$)KI>%3H*I3jln3^bKLaK}#2D;1C}hv6mg}KxC=ae#Eg$Y+v(pUX z6Di*_`zb~y>UZn|uaRyGb(F*(e{(29L-Y`eA>^ah>qvM`z%-7X7_&^kitmp=p7RV7{z~!NdgbU7~G4 zOIh0<1Cojv_cc?*k~Yu`DLY%;Sr(ml@V!gjF&d7ZFET*AjPl)BaLKJ`JTjyxs1HA{<0${x#P!8c=kjvUc@zwfM{{D!Gp+^n z=Q{IaHTvQ;l#ZM1I_!=V+UY*zNU^(oao|rBj#VrPN)wsfh-c~Fl!(1AE3uSLZ-rXE z%heo9UOrq6wFsZ$DiKq&s($LWfS@~Ic(6nHv_uPWU4gy!+Yx*)!P8#F9lZN7TSifrJC|hu5b93P$QYy`wi$Mcl;T#KConuH&3Xy_WAp z#S^$|yZHQYeR~dnuj^s5{Vbzx>6pAko<`plWKhjvezX55o7bb1cURYK_bPk{!eJh? z>e=YS($-;YMLu*k$M!?E=NiQ*ge%(W`#dr~;Y7GsW@kO+Sl!*ox>)0l`#9mf_ZiX6tqOzlRXa}zl>XnVt-}M%dG*%bbpQHMeIJ5CCt;6f8eglE z7vVRDv*-O_=c^xi@*Kpg##=fH7@r?Pj?e%d&hc!qyPnVCpGD4z{~ zb92;9)Sss)3+MUF%{NiA+DRQI_`}y)HK)KT!9pyS_n^^fY;#c!@fYz|$Z#1!R{z;* z{I5AL`asc8s_(0OO6~WlQ@iT_^CV0VQbyqk*9ll^?JQF^%lwo zl}jkXttD;!cJ#N-)7=azHtLK-u$-A2E$9k#yzRyFA^Y2{@MCMzo|@~D=~St|%r780#aNMNHUFHrXK&u_h2`99JvPN(%$Xg&*zSHpOD)ZsD5prWtU zA2~H4)Tz8PmeLT-vbICmDaiQOYe&1{V)j+Dt8`C5S}#KXb$=Ydi%qnXncI6BEfA+q zPe#12q&rOce?~Y%Ptk=ssdu%0xdX5A#r_wSy!X{dBswV;7Bw4~3CRN#Gy-am`ya63un>ba2Z%YX6n= z(Pb+dH2Tfx_wZWAlwQ^psd=(9H6k_O2|=iGHvHjbUl# z6M$@Bi(iWW>@W@o#|;3~1e%CB=?@+}_#$j-Vt9{V36Qv~K~!5tQIQic%8-5IcuSb6P;0A#)oH}(1 zgTVmUJ%~r)Mw2bF0^pAJ{)eG%Yyh|R6`QpJ9vACL1Cc_EH(D}Bwxp!w%Uy%?NerSe z8FZt10nV~{;G`NZ%sog!!=H8VAA* zP$7q*kMa%B?KyYu+-df^-NH4Xf}ZT5lWN;Wyv{${84q#>>!%}#sTzvI=!_oEGJ zf(RQK=vfElgye^wo>ewBJRfH?z#bw1IUavQexZ6fkjvDJsQnMUVrSC3`!$XVAWRpnNK>2 zZ7l~aoj7%P5n`ObPb|TU2Yc-RfWrRe+3YwGOR9Nqfvnaux6dZ3gqmn)7QG8G$t9r< zOy7Q~YS<*z$V}xV>TI!@nid=6T+2Vtm@*v-Nd12I{{B9(#;KCi?FAzLeCa&>0_G&* zZ=5VE3k#g^*w)P?gE6Se#*!eX1@^ij?d-Et2_VD@0d(0s|F)gb1zQDr2izxt_S0dy zwuxpHv&fIrSXBNTEcDR!!9CZc-8rk{txSM^fc%hj>W#12V9TXVE4CFD zA{~=?4O+Q#13+HR`Skiver{u#hQyIJXAd2il?QD=14G?&MC-g4P=aH2hVIzh z#9gqb+9v7c$}W-$Q_vjQm_q8X4_4((0R_>1Sae&j4!s;X2Rg^39~9*d__1>14y8M- zF>~>LYu)rwP3N7UIdt~)DP68DS3Tuio5JPm|H6Q`#%hu>?$38H%cQci*FF2^edLs#rU#verThY$F*!d#lW60maNZ&DYkj?b?33jS6#AzN@aEWKY-{KV z3V@do31#YsOCoNXiza6B*tCcOy{lB~qrD_ZgN5cbvlzwrt8VlHPJ^sX5NMgai44fa z)!Gd`fTw3Gq^asW!pJ)<`^UIGX7~AYOGd`%kYpWn9CA~~KBxU%@}Wz~G3!}1Z&&{= zMYm#&b7g?0K?U>^Q3D1h2$E0Ev3_IZDkWdf&u8Wa*p040WByM@ME`fx`TsLn{jYdC zPH{RX1j3kk^nC&U#=8Dr@z($ScK^w5NB9o{`TxFl|Ign?@!x0s-}k*8|9zhR%g)pE qA^3Ke!^6-qi5BpOlSlLVI7IB9P;xT=Afa_M1)X6 z6BSW<3z3de0ttlPOV09~@tiZxdB=Ij{k+eI_sh+23?%G?z1LoA&j0+)|Ju<;hPv!* zd~6U1gkA5}4HF3D$Q%T6^vQ{%;FX`DAuABbX^7qpO|yWEg)x=@u9@Afm8X%Bw*4in zk&+F1x?+B%_l#Hmfwtzi=Kl;WYkk(B@c90*Gslf@oEGa-&aFOu^vFAn6K~!y>x~QV z1#C7wW=lW)PkfkqG7tHYANX2ZSgNID$ z+MdUrIGk{Yd^pw24tamR=Ex`M;|h>_&;ON!iatz^m)q-FTW9#RUA~j@!Sk0^w_MY~ z=18|X`x?_wP8 z;hPST?>9~dm1@z$mM5F7JR0B(7^EvJbZ>Lax8J~{acA~tx&2b(9(}CNzuJ9_=n$3_ zvZ^=Xuk6&7L9@Zt{El(T3Y?wm&1Z6}^;@)zdS}WtQswq4CPunF=8S=op%=Tbn)O{b zGR}rBS6f?4$@I6y+ct!ZYBLTG?CtF{-%VyOS*nthm+C7_iw(EN{Ih-13{s^Fm)X}} znSJD0r5*QVKF-22;rWFfQ>nJMP=S=xH=zs^6cbgOzaY`oW!5EZ5DP_hwo@d8SH zV(0a#d~(Q2w-NL9c-75&Mn-RZ*r;SYUV43kaag_Mweq{=>C>k{Y+by`cRj@k9!&>+ zs-&%n@Fwe8?=F{3aM0GMPj|!Qvy+vj(jfyS#v|1poUE+9qhTX%)drcc4H~{_&>*P% z!i5VRtZ9zDJhlOv5k&^+@@e;pev6nS8Kr*w- z{;ShJbmIh!S5FBj8o4@lropl1WiI_iI=Z^pI3bmQ59HkUVy3egEUvE}wbZbMv~UgE zUuhM`*PBp;9gi(8V)2cZ4MC^~W_I6O!|CSGm%g`HSy{o4(EZ6*U+dr>AADghtm=BQ zuF|c6YzLQr_S`wbX<=bTk2dF+SF2!sSvXn+Ob%Nic-uPtj=7MpN zKYi4q>79^j(40%U>G{l7^fiP;j@+>Kczq!KOwNRnvvbL@oJ$2oMN|W7;&HfrTk3i3 z=U4B2_~+t9ZSHdPuXXvRnfF$SO?TA~D%qryrn9!%y1Im5z9re&6YWw>)(t@$drM7r zq##Gf*-nLt(vHxT79MUUVnERi^Cu-A_~jp`&^N$=C07QX!KyJd?u(pi+c|B@J`Ha4 z(b{;m-MJ@6j@bq;$~oL_%!J6DU&q&8E`DKE{!nL&J~mbjtxFt(p634ACNB7H_c-j5 z;1@6xi-X1T*5X!<(k6Ux+P`_nPbxe%Ln%o^89vW;Kr`@Qw~{EOtzQ9QOK-q)6Dqr`|L ziTS?5_VxLGQ~-?AsCdN7fH|%E=Ig(69XiiJzYuQPa1{=UBwObtA(gR0Qgf3d1Xl`I zlt1}h@%YTU``qcK7^X)H+4j0bu!)@`;ofE(pkc>L3-4(_So()YF0 z_+C0ka3jkSS1)UGnGc&>Ql&0;E#?7gy^Gfj%pU^>Od1K-`xOVXY`QJdNOpv2>*?vO z)F;m?M5j5>H`+oE_F9F*ox^1M#h#evj_&+=A-v83*Te{m!!fcU+n$txtJXC}O7>Li znIc>Odedy$g@Um5iXtD94CGD{2^JNOw#B_|u?Krw=?*%%yJIk=?^~KELp&}j+H59z zFQ!n6fVSiaaz+tJRfC6P$xbBxYni7nii_V?b>GmaOr^9v<7h0Y=d1*2WH{gO-1@uB zU!2~~m&r1SNop?Y)Gpcgz(BHYM1#0rg8hpgTxQo~i`N=&Ku7t`7ba7$-f@z(^n_$c zXV#|;NQz-XNc_eTBl{(;5g0x=4x=EAqMDu4ByJFCxV8FZz9A5nOk|aDvQ68+UnRv8 ze;01P+*Lwepk1|gsGCYVrLs0)lx;5a#zo>zhGMTTDYTe(W`k@;Zpsn|kp!J*bpUCF zn_~A7G8&=$8_fpxHk^;3T_K#~VtFBI@I1p7(d>}CbMM0sXm&_u^k@IB6RPFL`8ON3 z#)FX$lyOS^8C}k51EYnh7EV`ojf!i7xJB3eMa%IbHJvhuM}(B1)g`jA?VKPB+$BsuC}ixW~6CZ=hcVjpxV&IKeP1~Z_WWmlg*>;5Q58#Jll%yq~;CZB4?Xv!4!kpPPzvRDtc=0 zu-}XqD#N|P?ZY6;Jl@ZD$xbOSiHWd1w=p70Reg^Y63!mp8N&}b{Jb6fUr=L8zq*O$ zP3t-=5TjjzjNO{xp{6$@5J=%u1l$07{QoNOzD*>@f{6GwD~o>U_36nGgbgamKAv0L z{3_6AxH>x2Il^n|8xsr57LDBPGXoS|Mq;AuqwjA*mOxuzJv4Sk+Lma+?a!fjX8jODTf^RNkq{O5B9_nxDb#h zQgwT;%eoB@)cZn=PPHs}b!RFgY9GHx;H+!G2YS!6$$6pS*!NlnKmPvfFUP?fKnGFY zynTBegd-mw2Wv~qGzy1PM+&$~j;P5$Y3f_c=Hdv!`*S2KkP9_Cv*}jV?g#V5*_TB{ zU1zperr?XVK#m<_;n)W+CT>_Nd5qUbpA&^%;T}zcBn}f1b$*LCWBD8~jv^m9yytpy zEi5dCrI%#V)WaJ&wl_yS>>7dyjwj1{ug~=gBD_=`+PFmUo9HKlx$^F#H6VjiadXbx z&9LV{f*0M*Rzui@68h|9BI$*D)aAtCOlAMU`n*g zM6>$YF^%T2QjjG_N6A3Q#E9cgu(Ol&ldzc5bLY;z;1tHC14*w;8rFxwOF31RpHRl& z>6Gc|lAD8N-D-QKgI9*Qi}3ZUG3HPa?tEf$pXrQe&a1Y2D4E29A{B%xZqC;_Y45{> zFHkt9pax-$@f_553*VVZcFhgmqJy*V4Bk(>e`f{vE|AidsT#DttRi0+E|RQFulZ71 zYFgX{Ch~eBLB(xYR&@qM1qs>-IjmS|d{fQgArP5;TA^54*Lgw-dd{QzP}tX%tgY$O-UZ+`amrTAoS`yCe&oyUb4C-jih;W z@U=7qT@8#MZFt)D3*6&?abJ2Zg+5e4iB%Zyspu)+SQzRb3%l-%4UQGEWV!#Iq$zze z?Y?c;&WtO*!CY0q=N821F_?zRxREW5wH>qb4#>`R4p5LaX^EsJDR}KHRypAFs}% z&SUWRkt0WTy+Bcg98<(yt}`vs!rafiQzA1}GkZ!^6Xi{BVuaz@&B$101*unc;cgsy z17^JsO0x62^a28EjzNlZQdHe-Y)$h&d5Prb4SF%XAMT-^Dmg7b0%p%m?2E}p1PBE1 zi&&dCTVMVrwiSP?{43Fw`j7Z(Vg#FVzm$3GKD}<~rj8DJB5#34#%G&Y8Tgc~49Bk` zPUll*3A-?el_1Mz(!~Lg{sk39zsg-8+ z>B1Y-K)-#|R((6<4gR6|D3C7eL5oI+oy0w&inG2w_rUCgRO7Z*G8@Xmq?wF?i!T~Q)tX~+%zZcMW5Kk+ zGTS;6l#2KCt&Qjs$uIxG0v?(DQ&%TjtaadydTbl;i%Hdjv|7|zHhwE8h-kh^CDGDc zalOs17A*MF{^3u)nkGxyBpxFD(uKK4gF+=6)=gSBl+JBzbsA*-xdQ1TLs^X7?!&i- zLe-K7Xbm+Np3nU$*X>8MH`nqsChGh>%+23R{tl#@cG5v*7v$K$=1Cf(-}JL3z6a^k zvV-o}A|{>G7&H;oIMbf2AfEIA_RCW}M&2mlYo+sQUun^|-j`=C3P7%0zpA zvKh&G?$0b=LsELZzQs*Dp1@}%!qH2F) z$iH#o3H{LaKKK-Wt(LJ5546{~KV|ureFS-z1Crybc)sRfe_yh3`}a~4W4z(=_3K`v zHC{Z{o==ZKt_ZH~Sc$YJOEEyi7a%JryqB<&2NXa~Zlg*4Ad7DZDnq#2T*caUi8oc{eo)!STlSF)!*hdsZO@yfT&AB<8O?&}xM zdUc7Q?x*rNo9gIOB>Tw0k_Cv9zLqyNZ7!043m|DdYDn{#2-eHAglCw3`T5NPQC}YV zZt`%swNUtu?dE|DS&oy_?@8Z-FL7Tr?}qzN$c@p7^$DiduiX;0)Z!M8MkuSU{UqIu zb0aG*6e_bQYRs>FZgqQMF?^>*$P;(zutf&V`4J0i` z2ZvN)yYPbzeB*ALQmXplwmr!0hGH^d``dt?d;~{*dBOQ7xBI|EL_|6>l=?MUcMel& zOTqrgUnASnRStWbi~i$*6ga}BusJ8{>(?8t3jc$cK||7m>9xhKWZVl5L1}elZaCwx zYrfL+*XQl+0G2^;YT2hpN474tE~x&j*EGuA^Md))>mLtESJXTA=H=_gOWynNX}_f2 zbXn=IG)1r9AoF>)OH_Act203K;sw-WPbrHd=tVf7Y#jREmDsiAmqwc|9^TjF-&U-e zi5AUr9w_MrmGia0)#*PRMf*2ke{r0$k?*fsc=vTai-?#AqDfHh1Wv^&oN&hq3Xjyi zdw7l`5U291;<9Vrl$9Kt(tI4zcYYO&TAD+lRlJZY*C+rTjo+#=>(Rc42^p9j;|Fu6!h@-2Vho zmCF3fIc67>byCE+yqugVkL~R6LyK-42p8Z^FOu;S;OcNr#Svaob|EV*6#z4+g%Y!F zPc(*h_79|45|9848sI+#QZf|+mM6tbi{=97Ex;(2F%Fi(HwFYUK32tk$;+F%_!&b9 z#>U%Q$v^(NJYae^NzC+S`ybL0mc;iH@I3(YdWQ=16PVfgMp0y&-AqSEN946FkeJ5& zC(4X+pB06;Re@N}KK1N*s^8vHIAd?6RhWFOM`N{KkEyO}XI)4MFh#Qzp!=OKUlT1# zn?;=}x|AmC^6s0{{rmUF%QLqG3li)z&$+&y|3|J~@F&`BdZk=Y$g;CxHZsPl+T~RO zy%1astmRd7w3oF=6>~Ydh71Z2058Em`b3jmgkC!OIq6yukV*n%-V)@UjeCF(vu)vk zqTOEazngw05Zq*1=kjd2$L4&$u2;s}x4B6m*O(MgCHbOaYNw~CGcq#LJsN9+HmG2M zVQzj9vSG=ueq#>(3keV|B7Hdn{fm(BD7?MSZrEwn& z2mBB4SHiJDAlvpc>n|E+!fb-(3xgQ6*s|UBo#6i0*vhs#Tk)-hp{easgZvM2y{t@Q z)Ty6-)}o1jv`Cl%-{ajCz%@(>xluspY7dp27d(45uT5jhNZV|Us6Ra35bWCK0kYEY zKQM$3+ux2i2}n{U1Mu^2K80Cm{a}(nkn)^F{yXdRRIMA;80J1tZ)?JLr|eL3kS;@7 z{l@*@q=oU_hsk%B$I29a{pR<`d7$c?VY=Z7I$`7-q2-6)?~+oZ^^(i*1$mdg8|B#F zUQ6Q*Y+f>J;tOVl7|Y&kCj01{MiMz2Am#R5LDZ)mlzgq(qu0U9QiA+|;A1<8;TFfh zjHXllU(riW!9}nGgN|%!ZuwUb3}d;tn{KcEk{B$t$lm~r5Fm>Do9u9LYOVMyZZzMJ zoa^8~b4ecA%2?T~0B2R*MdM9XD;kH!b+6yqpvyTy=rch7-rxmgiA?74Ih$$sEm~{= zLT``!pd_g8ebCr?{&-1HW7kAF@&{P3$ZG+(fp|-F#%icK9}$W@NhGzFOR!mUW7rHb z^`T3?nCTkJXJ|B;hylAjtYQ4RRSrpQla}OGXEyhk@4l zZHspv+(|VYla)6MDQWEWAungC23J*)^XHiK?%pLwAU|-+R;@Kg$g;vQW_mMb{Y3_X z8D1jtRs4bRi{~swT&qPZ-KYB}&3~7CAQ_pD+;9r#`|UH^VeILerJ!5H-tm5eN-}kO zSj1D9q%~2BFvryNXdnXJgP8d6uwGNm+^d7%>*kBnJKodN%!=PYM+6b!H$Bd?Pk;g( z%gFg+w1Yo7_;_$sXQxE7R3T|E1u!m)p8o4^8$fX)7(c}?_-NdLG!?`K7DE*IZF&e# z<}VD=XHv5%l>MOr)lz=!Sgi0tiQU7uX}5~tCOu`1@r$7J9h=MZDm1Kie8?_w;7Ho` z^XLbl~oYiXmm{aIzHHa;K=^O zNy>`#Xgg9-ZT0)b*~%{otvc{3Sl3%>+w z@Zogn70cB3pnQE`bP3u4FGy#8>0E;mF6nwsk*BgiN8u?nGZW4NCiv~NTe9_Qf+I`~ z5uSe~4gB2X`y_fiChbX-FRLng&}gQlT@iUw5yxtWE;iLk_<-SZ>_~Z_wpM_B3A1Y- zi89hQD)xYiB+kS5;p)beXy})umNaIw9c1%kH1Fquh2c9>CMT(>C2qy+vZ3>T3y?d? z16Cd~x%&BSR&=W=`^?^HkcDm*8>${&=Q9vO8l}ZgxXd+y=3QX+TPjZ!K}n`ZAm4w0 ziN|`f29_mmF2I1)?`pxC#n$0ozC0UI9Umo2Fld{e_sF4O<|FHSeNV=xP3qs4UA}+b z=CN=qxap|#1L$knp78UfQ@!Y{n>8`+Yn%Q0v8UNj;rFQZZ3oH{&MAR?0(jSSM~h?) zS#yKv9PyjTQf1t*T|D;H8)T;Iv@4r22`_eOqTHrO`Ihd?o+zIeEF-QwRI8x}V_ed8 zg~c~UWhg9;hYuqz_xz3o`fn47(yQ{-7?Cn#Tyt@n4$Ge`%kxp3w7}L0N)S$7=^IIL zeb%PWph5m)7=?(V#>sZF8t~#PX?Rp`7rbH)nr-NUf}w{VPJVidJb^5%)hPHUY`K|5 zj*hZ(a}1)*VvNVz52%iqVd5?oD|ay0nIkg89Lo?p93E2Hl&j5P3^5(C2-b zn88xVvnn(4t&x&4G~$QNEThv!BmLL(P~vi?cdbEl^WxM`a}jKNyi~(_-=Es{omRtR zxf$6!0mMbEFE>S<_*jXKIII{w-kv`OF?Kt7PB>^T7vy5m23OfxgJ8=qcgxP&^pTG^ z-&fz6PH^OxyZP?jyN;;S>|v^r7TNxfX%eYR%D2b}*~{>Ql34oWRV7k+gK9I4t&^2O znT}smIJoBVC8ZVTr~mBcU44E1dntF5SJ@lx`aabO1Wp-hqtw`g_nwCO5$oj@z#5QpB@9m%Nr>9eg4ae1`38G?!jAQunuM4G)+_TZa*sp(X>{Hy<90X;vv zD990;W6EXmdKpcZ2ao7CC}AH~Evk9$6{;P$$b6H(YV!SO7sLMSxvaXwLJvx(eMr7< z|MxJ|=ux}Fdsu#Ze)B6Kk9oEonp1TvSXAY3fe|?fMquu-M74`*DUELaeUVNcS15Ls zjO$6|u_p8a`?^nOgzt4N6(=*(v_f84O^luXLI_B_U_A00)l%dT&=W5;YBn(7_xH$^ zmJopvCi7AE-~^6YmnRHakffL?9WJT=I&Oo}!m8 z1M*2YCu(>#qypg5Km7Y@IH(@f8T%DJNPxE$LbjK$+ke}x2?dcB)cbVECp(>$h+yCl z`1R`-kVE1j6+;zHc$>OGvGcsVyg=OtfzkV1VE)zSp?F z+Wz3n^8>)G0KUQlO+I)hU0y;_u_k0^)iO)L!!}jg0TAzxpr8{}_5*ab2!Xc+{JPW( zZDM9N@TZLlIEGJ`lI}wtv7w?U0??egWDoYhX< zSsfi6fWeyKZ9@STA11x#l`%V1@EEraZtv_QnOzUt4^Vx$I85LaQuza&X@j7YVqs=R zW`k}%5&i-b1uB*#z%e8x-N6O#Z7nf?I0s~X7O)%hju>SnC5xfq0=)!zw_#96PL%G0 z?i;5d?9uxqGTV(P6i`S&S%BnF2it-s zH86<)6}F1S5s|>2z!oY(_-(a={faj)OPggT7%+1P1c36(eZ1bnYpLe)pT5W_0HHQS z-$3AU0FA<+nIy#S&JG%zf6+J&RNhm=wel}xW8t)${=HsZp7UdM)yE)(w$~%9%0V4X zL2B;Q_|Eq|lYjN{r4ieU$jHjaLw%*aTA(O3%f(j3#>I67^T3VDfmko%ERU-qj z#$DsU^#V@vb}XM9*C=MrihwLUy);_825{Tw&!3^-Zh0a$%IaJzG$WYNLQ4ba1R*U# zt&p(;fqs^Ys{pMdE|)n+SC|N?K(4K?`;OhF98bIL%fkZER}C+|H!y6ziT-Y&r#DpT zk^r?hm}?g#HC{UjO$BYbRrefc`IAHp`rCJ4Y+BC(A|e0 zD*%OwV+6l@jV2?_S>A7PxNvpisw>RcPDk*p1U4!UGkOHV&m~&JJ&2UWuH`t8s&aF4 zp`WDcm$eQ87Jgo@hN?>_nCQO*%uGX!Q2NW zIEkQdb4x(UyWprXVCqj^rL_~`#<;AHrd&wRKN1P>RNEJ@u|Rc3F}s-q?QCJtD4=X35jyXsoTN?WG67<2CnXLlbmtxtrmB{}IQ2#|g*g^3{zynl+297*2!KX~@=cSAQ zMQ>?o>Fn$bR-`K9CDt8C1Y}RnQ~UV!^>wh420*w0-hkQoW=-uB}f`kKKNM)tF9`gq`g~azHYKTF^DzofugIeiOEk8|_{P3|?HgYEAvXyawk9f^ z?zY5mTU9+qhx;!MbAz{?KAm&Tt-mO?s<*7Sw--=#kCCdf!R+<<6c3wQ1_mUv0;ceA zMr7o1!HAvV-;y@Oj+E>9pe>!{pQ;+{2Wm=H3TwkUCAU-aEf`=CYeytG*E(OF4jV`Kg0 zoSdANS3KfA%6T&93J@C`Hm45H~{=Mf;&d@`!O!TcB z=Jt2i1cgh^FUI0Q^ry~w#uVWKZ&bIzxo-U81aW$Xpk7;FnIR&$6dqBXm4X7(mGrHX=Q|PWG12Oz7WzJX zNsB!p#NU}-DAWR`1~(mA&2#l$&aI8oIg-WT_&`{%{>B}%hvI2)3voAiS$eCt6HaJ= z7I8t}2Y+_l3#DXcF5XBQ?McjOB|W;|$USZpmo9}>5hGNgqeN^9U} zK$o@_Aeg9-SbTcbb&1C|ZKk?fXI!9Ot)%lSZC+)F2%QE;16E9CLPCP72RygzsNIh1 zW)a3ZDg-nnNOjJA1<~f1-9h;{+7}7vj}471e?$@RdNtFQ&7RY0Q|eyQ^P)8+cuYTw zx1v5FLuiVmJ;_Nip{Dy-T56i;EjLK9l^4{Vpa#}PVH)`$@a&A^`H4v!*C6RL=$oTo zmswTgygz`fOpM$HCN}Jq%{FtU@|fR%Im1Uv$fY-HDP=K_xyL|MN17k6xiTbrH>)&D z4?t7Wf;$OA>PVG=-*9D6D@$yqGG6aaLB~;%ZN4^dWRtZ?4-n_^T2`ipV1^1JN@-76 z*!c>42Yq_^ROdi=SpQmwq7|jyDaTzT%g4DM_T(!=Jo$beNGxFycylG;GiP$0a{KPh zgn$H^@o=cz{so^Dn>9#m3Uv{CJ-OP{2#{j|!Yf(#3;%}`SS%Je+`m+|qjY+YJZXB` zq~&?+(*#@iBewsmJ>tKt`u=lH+W+;$<2>fb~ zI|@fT{X0XDX*_M z0$*taG&z{wcasD1$tx@6Z05VvgD!$-H{GVrZ)SQr^2H0~7I=nGHS7{wun9k8)V4-N z`oOtsJ1JUp%zNQyIdF!M0E)}_4K?({;zQx-_gNs5nq~GaQ+f))+rM5Yqo}^Eg8nyj zbd*|5GF~MR`OuAijS48f#&1Wua%O}HUQhaRLl2->zryX0GeATfuEd9rhV3l^PbM&S zNn~Dt8n%=gp=}ADg`opbWU12$o@%^e1caTmZOm7sgduc#E>CGdjWx7>- z*5{1z8?zPX$u|CD0D6xX-sWj_0=DH0xv;RXF2>nW|35$2{z3H%72Wmb08nFE%cMj0 zw^wxBe3F+LtB3R2Z?z@&*N%yH(h&F7ZE0*z!_~g#D+~(y{a|G{^#;0O4f%H{zcy(` zWtz8sY;~e$pdSr)v^FO&4_bVz%=}_XZs`WQkojcnlqs)X@GI;5T0%H_GXAL$FwY+y zt+rIs8K@1MVeOV}uNyx|>j*D~8^Oc7qk7}z8_PZ3_?Ra>w}%)i6mBm6`l8@AT%e(@ z%_DlU{4%V}=8;InD8V7g`_N&xvJ4sanAg~gRCO_nAOA`(c(dKcaVM@_i*}p|CbHPf z>Hk&uJM<9)v0gG%Yw=K#w8iLPN}19^ z#-01M(*Cx84OhUqDYDBee?zchg#5g`#9_CQDpU+E5;PVSifzXmiY(m8HGP;e-7Yud z8FC@ZvHkV|cD8#Bi{F*baPvhTcNAx37=y2XrSruy5*Db4ZHSYD(JIm3A}fy_@02a8ocMe$1>Ik&YP}s>Rg3XsWdd^tGRul$3x9>vdYWry=bt?MA$@-exu`t5XA_>)! zPTytJv!n9giW=0F4)6KlC?{95w;px~qEt91j1W52ZQVcCIOasWt-06PDJ(lXNxXY| zjUxP}gra4hww@o&a8=hL$LZIpSvA+?#}3k_w?=$?11xlXes3yQ+nxRBYf9~Tr6s#@ zcx~xZDg1@6YjIH$j&6f>Bze#C4)N;xFutF|AA_0KZIANp+DJ`=JX&8@XDCFzsxw;} zX|#-54O=4%2q^?N4fg*%MEMUEVBLczI&!FHen^|?llGp-tVG}=23_Osws){{vdgv_ z6}`@wm&MyqVZ6B7eF}|tQ?Wa}CFsA@z3VuuRyZ{05{OrBz?1Thyz~ z42|&3$tlgB6p_PhXHVdU*X&wazBx5|2G=-V-g5mtT^#-b2@{fV-aH^D5!83Z4iAK< zgoUl~USq34HRL7tnn1*Ezum%sY=ZCHTv6tH*4E;1-l|}7RH&MSv6Hc2nmKVV=2|OC zEB_!w)hD|smVcK??4uwV{WJ5}7bKZ2rE&d!QyfW_kfM63L2=!PdVTBqXqeZncyoA` zP=gPhsx5WuB>5y(Y`I~N&*gQj0DrNP<5v_)@RJs3FHV7y80NPy$Aw^JsUGj6+4m2G z7@4$Qek6G>Iq#^%1=$a7j|x%BeI4@bt}Joeo~?KxUd*sT;o!H-dDp7q`G&IH+mv?J zAG#CUVeyvJndy7J`Z$S+v=9D_Tzhp%#T)^Io#Mnn@VEnN{cY%rIJ!Zbi3t{FPT20? zQJzYOM{=%_h2)Dlem!`qq*VJN?oqe3m-8oWeludwqQnw2{XqD~_tbMJMNzUvl^FcG zLXF*WJ>iAu;ajfo$&$8?!%Pfq;rs;8C8e1Z%))4f^;oFx=asXzNlVJ-w&?mDyANzl zHlT;+<=S>uwdQY3M|u2Sf322rc4>(+xBhu$=^!7uWY%W{TvbQ_l@%fp-a?G2;dsXXiB(+$SGEnV7eHrII5vTY9MRC!}EcAH!IXQG<^xfxVMWU-h<7Zy3N>?Uu_ zMCrpq?)b~LQ-3)#qe80o!ee<<^BkE3AKf&kt4p%=qQ7ygx~B59_D}EVy+yT$`-X$x zbfqoo$~KqOo)Iz+`_6P;VSnkN{pv6EaLo}vhD8~p<{y`Zgicw@#LpEr?7aCQ8WwWT z)}g?Mz#m@UO!`OGXpDQyG%mXP1kSwc$YGeiS`GXr+8kq@yU?mGI;Tr8MIlG8mF}-H zHsA)@75}*KDk=Y#dEUTh)5BbgyK9}|Ajfi);XjDq9Z+JzBodCP;>f?m4A!%ub+%<# zHE;W|lm=eRvsaZ*(|141VGnnnVHHVDD?cpG5RRP;*L+=nmtG*1I2F^xu#!Yv&b=aI z(G@LxmE?;1R@t_%wur$@hI)9{lQ)BoWDEN2n^ZV**|-GEJttPjjuv5=VAaKjXeB3ZwsvK;o+!ep&q?rH(WY&M`P^?28P*Y_CfH&QmMup%pUJ}xldt`Y6-nNVZa zPk)E6NwVhmFPIN?#%-x`KMi1_m9QW5`_<(6E5-&1z89&E>$ht zo7)9+*c7;VDfX!l%4ve<&Bj(<(UmO{}I7CyCWSIIv* zyVMTf#=o8T7Fbn4sv|ew38=C_22}~IM0fkaaB6*P+WaAFH~M3qI#e%p2CVI=25zaK zyjGj==r!9c*e+>lGi1|q>PRN$1?fbZOXST%I5+UN)y*upf56az5W{48Kd%Dls%1 z*t|Gfo<_!>PEVP4XHRFhj6`&N@30UIy)@AUqkL2R1J1Ucp14UaBzd!*x*nqhMP zL0BJ;{#e0aY}^(v2Uq<$yM0R9FchU5H|7>rU+!Vl>X;D8K&-mM%Fem=zSd*EK;|k%}<7%l@>ysEaU6@?iXuW2X;7TeZ zC^+~!)EOf3Z1OoiaX?+l&<~Q~Cc;3Ug?w_+`Lj<^e>eV)SK$eWp$zmtb}Wh+dcH7c zy|uiP{`$zI$Av>r^Lt6}7?<(&G3Q1Xsc>7tWGV!rQaYT}*Ldi%<9J1?g_WK=M!j<{ z33LTiUfZPx1Xshl<`Ef_jX3RFEs!SBq8rK(NV0C&sCmUZhTLLAW9Ys*SH zZq}M4f3KMN0lfQ$6(~3PHwdKjJMKCU%i0d{c4J?aF2)Ft#?k8Uum5g<%?04#N4Kr| zNVmB0)HfNamLrOVpnZ`M7!vpYvhflxdF+XdsH*B8hRzK&18%CQ?CfmQtKdyf#va<+ z`!D?TTAy3-z)PAI-2um>D|pbk5GE#lR1)?i+c(f-N6q)?l+(C$3=O*gLhD%lV|E%S z@dB=WP{s|SefBxiuQWzHdlvJtsmZ`oSjqd+KmTY*5sytuU4p@2YHE$3Dg)%lTrDk$ zZ4g-TclUUT^FQ2geg-%J*t;h&VFfr_DuAIcd}1z$h&TYa1rZ{8Rc>T??~ zgWV~)UN-yY(L#6c{v$2@F^64RMh4hb@{_xjMtt4#5M_WWfQ^JQLzziQ1jkN3!Ca2+ zJ3+Rx*>0mX#%oo3q=Cp<6wN&LHK24aK!pm?Kf+XP;?b3%bmq(%x<3}zl;Hk0cETNEMXLo=ZH%AMiw}fRZh`wNxp~0)f;V!gwhILa|!^b6HA!BmO@f_HB zWvV6HyEs-y79#@FKd@?zc02s~90h}xWq{rYaFl@E3Ln5Q)YK^A>#p={AgQdxySl)^ z^Wo3VfJS=W#UW6Z9?Rh}^0-0i*Ixweo3N7&P;F6Rp;Dn9*h^<1BMOD;ixr^%cFZvg7Ealfs(r^kpikfx{NVUj4mI?(KuT$q;Q`*PSsRWS46BVuG!)O-;r&0U(u zntOua1keXTV34;}BjsO4M;j+=E6i`@x>X8ymIq-#y++zi5W;wy>*ggyym@n4V(zgH zg>Ba%-SkK&Nyf3G1cA?@*Z})#nO!|aC0DZtZ7M35^j=OC^S2bW_Uw~>%_Rr%-3G7+ z6}I7T4bRc{kPyF=`J37Qy10AcgJV2QSh4{I&fNoAR$w;B{c-A?>Td}4pP2$StHdhm zfili7(tUM7S<1Hlp24nK=ti#Kw(=@@(9R2@*N-$u1&o0D2@d z&lDz}zlM_S*-g(SWqpisXZbYo`k9u4-O4j7@@;NsLsl_E?2YE) z3*o@D)w;B_gddNpBjsFy&;Ee9U90y==(EFh4 zRx*w9H#Tzytk$adZ=`R^+R>=(0ko;>VCnqOsPPr?b&~$zN6BCq&hbs#8n7b~tBp6x z8pNWw=h{MTup27Z8e{|2?a4CDW#3Gk9-nb>wcZwyPwZtTs_x zim>L9-{49drlOS~`Ygg&$qDrJI`=V{&YRy_KPC-1qtbU9y2MQ7F(o8-IfX0hF}rU) z^A(!~0mctI8nXb1xOm){*I>Rbr>dLa`@JV-C$Mwom#L}gmhfsQq2aQm8~$u?Z=Oz& z+OUb>-)^PEV9>>bwHP^LhQ@%+-T^nOowS3@-&D8Yd#@-)ZndlJEroK*;XQf&e zi?e{-b7$2PpQDHLc5o49E~QyDj!eb(d>wNva&tII$37%qKy5AzWfiy2=JO#>DYoRv zEC6;zo3>THUc5nd#lACTE#b3xN|6ODfO+)K^8!}N{?fXx#+Enx6$D-`=b{c${*`19 zE0ebvZA$2h)1$ zS7C^9lUk_9AA@uP0@*RgByw@*AmA~A;&E1 zXlngWKQG{aQO^JBlL!9&N&ngt8vZ>_{~o7*@nIhSo+|&ID*r!El{Ll@2!z2H;MD}* fA4Ifc%q4;K!>D-b;$!y7oQ`_KLxk4g>j 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 75cbf522227cd3739c82b02fee56c6907bd335ce..7590760d6974773a27ed44831d8b4eaf2495911c 100644 GIT binary patch literal 18060 zcmeIaXHXN~zb72^2P%r#5Cs7h5e)(&O}dKoUP3P_(h>qFC3Fx(1q2ib2uQCyF2@0_x|th%=6~CGro{a%E>w3^F5z-5~`!6%*@2j1c5-9 zRaKtoK_ExwAdsVhCys(ghWvb2Ads^V)hCY(JijcC9rrYVjW?|*oFK<{nazFddQ4N% z4+~1mn1lu)>LQ1_EZQN|aEMZQd>T@F=Q<-~@?sDN*Yskh5wO& zJa{|F2x&Sy3Yon8uRQ5AnU(=<|w zcz60rw18RJ?=r`JY(Ex;(}0WGccp12iF=p`XWVJ}2hkdP#~a>{GukAaJb7}s*iKtR zLnF{*#0_TGU*%w;(w zoD9;(@^mxHkd);R>qt>UMsG3*C}4KvTj#Nz9I(;OxB*faLa9BKwvspoAq!qGAv9F-kB<2 zqwwyG(Y4Zu+0bF}_i8ku>aE9}xh_Ayp+;Xe9b=J66AZKb9wmQZOr+wR`X8gAuv=PR z@+Jo)!~MDB_t#@&S)qpc#`SNRQ$2r$1|05A?5Ij02J$UP9vjz1Rdaso93!}fFtM&) z(uh~FA-%Hg_##Q1@b^iP@hWp1&iE{nLag|vb$uvTn_KbCcz5+?>lex5`#l{@s_it%Rg z*&=kWtx%NYQ+ji?arttue!r*s(Tn@!rQGH|Cd;o!&ueI&N>q;*FdHbak{38!at_cR zUC1_yT`*%HnwnByIGAWZAs4@#YIHE^4XmUx*77#T3r$2wjF&&H3+m2P=BLLNV>QsM zp~ODSpMZn4)Q1}th(STtu}=BJDk?rhFcNwWw`pC6e5U`Jfy9(e8 zjFu74em8$&vPrhw;>3qJp@*2vk$>WA_7wHQy`?vD1T89mx1!q`$(ENIh%@30o7H~1 zqTH7|?L-}ucw2%82^#R5pVr~JycL+uv%zWH#kA8oY)10#HApwDtOPZ-+itoB@_ghV zZ?#ZVc5(VS9wpD3{!XP_J`a-?Cw;SZ{lNvoDbB=**n`UTXAtwF3`DK>?xpk7@m2%8 zYVM5Dl>0E=tep%$8G|#3POO(Zs{w-vMlZX{*-r7b7KV({l_fZniCnVY22D96&$iYC zt`^yhGJ|Z|STPZw&Qo}bIskRuU$&uaS_SNvLhaF%Rx696>m6oTX$i-n$uVQ)3@(+8 z#bH*PaX&bd>hh!})Z)($_4|{x4{$Th6L9q0rys0Y#cvbE5Y#t{OYF(>b33$!f|`^q zYF=Z6r5KGUC9m?z1WgJu<5OOu5V_@i%a6D?TvT#j7=U%-xd_z}p`l(I!*=c2-O}sd zLobwd^waTRykBIz^coVz+px&~)<0wApxitj5us<3L>+$l;9e7LJM_$7g!J8#*~NL74gac#(B9K4ECGm#gN#j(h3JYsx-+E= zpLla5_{c}4oG{XZk2G; zjrv!`G3glfb|<=quMzgWVbGmkv4uI)yd38CE8gacRY3-wkLMkCMO3+}RI^XFjCGYb z=(j&jJiXsx7<87CQ#RmW@4KwiuEW_e>>c~sz$1wey32@$eA`h5ro}Omk@mP@Z!jZs z&uPpo=3M3i*N(9K!PeA{Dy|?4S1*MWPC0d^@@l?qI86cPaL~x%Y)@pY&DLEov6)#M zE;cK%Pmfj^gOFuo_E;`KJ-Iz6kHoM;zx5&i;0pULf@3}(yLywB@jcI|Af>2**pRMf z=w~-Ux~<_BNvPjgcw?__^IkA8p(N8tLHmi{sR^_c#9eof{g^73@8ta~m!wwD`uH{6<7|z9)%0ZU@gHlkBgiO^1x4;Y-K4LH*~%9*W39%I8M%%a2U5 zH`1p5Gq~!11z$fHY6iqyXMTvm`?>m2ybbWjJ3g{~$emm>nxH$f* z>8j)_g96Jj=jmu(b*&T`hf#yCPwN2Q1Ji%Eg1HsVkDB?B0RGq_-2Gh$9=}P87P0F9 z=U78yBRfRJ5*kE$HQoB@)2EnQno_=7E{z|q@Z7lJM5Yh?g$XG~N9#u`Tv;_{q{=#Z zD$D@J^WEEe8N~6h^(gc5$B!Sq=~QM;slK=&CIoBJaFI=TSeWV8r>{>Me!8J(P&vXR zDk@syG*;Q&-EC@;@#G{;oC07KKy%=nCnGviWJv%Z09?2bDLzwV+m6*gg4a+{`AV7l zlJ=q{8vCp?-+F*c>&t_58~hJ|FlJa-cay}CMa7WEj{A*zQsBYPyHKez(NTxgq(E zeojxeI)$Zwq1FvyY-|iLQb{3~_wL4$R5Jj$3x=H7GxGq$R=+FpTC*LqC_wvNnvPQ9 z6Bv~~TixY?^{$H-IV)idEwaP z&d0Fu77SkVr#^85bN`J`sWOliM4%VDbFd*lj(9h1d35h@2w*2H&8PkT| zdxUy_y42%_LN$Y_7Swd+dHu~I82*=#prF&i-ZoV?&&BW?)|2SdB_3vh%%?tZ9kq4m)UWPVyV5o# zX+{q@cDbVcc1C_w>s0pB=8u)XWf+0#@(xvbxz?bW3_~JM8e_djM=HV)6Omu3I;;m! z-6gHq(6D=oWzTif9e8@w`Hc!J@r3Xq%Wg*!^GN=ecPNo*%xa+lMvv*GnKH$=Wbw+| z=x)!90m8ba4xLcg6ywRYDtqh?3D%UE+2a*RMX`<~8u`xh$!5@qiMorzo-8e%;DkP# z_$Bs$2{qAl)CGA;w4Bureo+)T;s&;oN5tx5*7bs#1Gr$FBnxTLteZcLH4`rAGYBT8 z=^^@E|Wdp7{RfB!;%PB(^-3>oWWq>MyEuz zKMAR3)^u0#4XAnEwc#^og1Dh;{iOE;jb~?oHganv7bO}ncal24;8bB6B*iCeH$Xzt#5wK}|I**E@| znGYKrw^yf^il8V~Jk|?|hI4CqALt*Uq#KxQ z>*9zroS(VII+)AeS<7(Cb^pX9mI;M(X^FZ2N}ija3#7l|l=1BHB&?5w7fPx4JnJ1F zqcDMSs*KH+4|;78Lc_u;w`Y=+0Rs!lcxa%V=CL#aSK!Ax9&uXhvy>D~iF^NC9e=ts znl;g58lb0(9<6@aDI+IZ-D+g{`(mwHmBkb;`T~wP*b0~f#v5;lPgJuN7-}R|KEzJ5 z1~m58b!q67!eJyb?ZO~|I)KM@4fa2l&HFWOe$@R3U6gJtC9|Zjt;~ur+f7b#QD{sSv4A`~p%qt4wA8VWE-t`u26p^2w=kroJ zTC2+@Y6+m%b3JV`gT-TwtI0-64PmTHc3VH(Rb5lGGzkOYER$gglCC)@Nq1IMv|DMH zIWM#xRx-kpVD4;?i%ZYaKd#47KA!c+#Y$j9egZf4lG8u!@mQ3UJ2kKEh0k3Gf&a|$ zP0%^o>1u=Ovp*87A?ER0OM(9;4!AyYX=-S=0(mGk#n!>*E4npV7tFwP@y`Jb4ylE) zadX%BZm*v54|#h8@*srkKhPFVt{;y47SPRkx{VEiD!rMv+Qv#Uy}2@}7Akx8Lb1Qi zj~&?V!2Cihk2Eo{q<*Loc5!s5dMlu|1y;VrtbM2u>~qN2NuWE|V=o-v`VLInLEy#@?0@t@&?H_8^sBglZ_jSL8O_b&Hl>F!T0{MEXXnY!1e0^ z2~5cooF#vngyPO{ya;L-Qj)qjIiywA#F@yufaE^>LQ)BRO_Z>C!DAs=ZnICc;K1lD z@BHlgkWl>D;JG8u7?X=5pT$eW3zWCdvlF+DFhA<&C1aFHKrprno$AwU!Q@G^7b#U+xRj zRbc+O%sEz^=`szk2uF{Qc76r4Miw%^%eq&#Z^m2M;ZJFj$86d`Q0rWBv$@BmnwBS$ zzKMT8mfvnETnczuWK(mbvcn0IySK4U7D)BtnMQ(-_|)iu^Do%NS6wT2zayf((!)mX zkONp_HpQnyP+3v2p`l^=M?$avF2I@xfF6plxy~k|Iuga&vP6J5@&jxWaD)omw8nBc zS9P0fWvj7oY5v3m>bH2}l7Ok(Y)A6UtIDM^3ZU)wlXoMQ_I|RuvI+botp-gB8dp;f zm~n!YnV?%pIP`oMBqAx|V94MDiY5l|W+>1b8y{)QZT}8Da!l@UZw(x^K$G#@`ty@G z`YOD_F;N`p4Kx{!tmMz`{7kS98p{KarZQi8ztO?~KK%XrJoAcGa4Pd@r+fii_2Kdm z&ZrH{E5cLY zcW3J1RFr&*IC7-+^)Y%`A+h>kXF9YVJb7+HELHna)V?Db$_{3Jb8 zHxCt=leC!VI9wzPzA?aCdQ3#bUTqXpmsS%n*OgAI-dzSF zPMPASx)9j_pcqoS0*ma6)N&8p<3&Ig4Yzg#7jDTa%Z_pf?5%7~MNxblBAA35>elCb zt$bG$@jxSs>QIOGC*u&UuH)>zacHn=er%r+z0Ls>2l5@3*JR+Hp@cZ2` zkVQnZq*2S}HU54zNhL}?16zaFPBSyZgbzj#1;a@OT&2zvT&YIyvwWZ$TM_=k0~msDQLE_2kmasXd|Ty8ml=(=;W6@al`BY;BcyYn5KcLpw zKv8~sY?tuR1*LXTZJCuf6Cynr1i-Zbg8#C}^pRa{=JBZWd5*L0eUlaPmw!69aY}pS z{hT6=l2FcM0Xkj{DjO#Cc*K~717)xZkxcN4Uf?n}p~bju%ZcOofeU{Uw3jv@M8B8TZ^dH*WYAoiyK;0`?P zeKy&Xj2n5Vh08pk5sJH@bqfsKlIvX3U+h5tZ33Sa|&gwtkPK6$RASdsOe%GvB z*MN=SE>KDNd<02_lZ0_ScOU;TNiDGH&a>(e_KM)I|m?wU9=r@ZtDNYM_r z5s&!L@T2xu5$w7S7&F#irq)D*#|~K|{4#1@A=MTI`G-Ke7hM%)ViU5=7sI|x@!K%* zgAEh1^sieJ*9Wq^%gTznzNB9#1U-hS0`gIvQ%Tz@>XGZA}Iy zk1a*_safLt&^PI$23tlrLjlklMx{>}b=$C0*#033G!r{^*D_GOE!EI-in++!h|$}( zVLA){w0pj-el_gmup110l+EPXD+=biC5)4D0rn>bf7oQ@gT z1|n!g?#=+O=aMpCppl`nz5Cv@i}DcFJ!vS+#a$clcECrkG$vBZK_v!I?kO_)HlbX2%y>hYVtzX@yEx*W;lrNP zcY4lPz00{igT3W-4Hy?pu7qs@PR zXFgv)_p+3>fwL}enfI+QpX{=cGaENP>;%L6=FIgp&s#9|h2p8#=*gvaQ=$IJM~0#% zcv1Z-b_LXyp@mfV#KKqi%cdzJc9AKGD>9Ax%ZQbd^fGXXZ8lbru_|ufg{@G0~mefV?t6=rVRzyBfHO__|q&bgF5S^0_+8g zJB4j6%pz7$xk?m`u%;`3dzbSF(b;&ht1PhV#9(=U_Br%XDF~W^z1MeLF8LZ?xUgZUg zp3B!qI+2;zsY7imQl))#r3eSj>rh#P?$3u7Vny-ZzVqGg7^9~AdF8b?wx=kN?otd` z%=tU^T59HMySk$bLw(!b`d!*85pBQA`a*-)HKlj}*!enh`WVapy5?86VZZk8pZPj(^uN?K_!lsAi%k+{dm^`yo_6nb=ZhA1Mx zJ6k8PptE?mkaL=<~rZ}8=)vfsZlY!Bt9J2yfs(rtI zd?;?~SbzP_ho!8;qaBqknP&2v1=cXxEZiLVc8$h6zc;Z2WbD=To;mVxo^o-tE1!#eb@A@#YQo^DSj^>Or-o{G ze(QmPQKC~rXz(txOZJOStFJl@%>aaiK$8!>pY|rUIB(XhKWYZFjt$T{PZzU_@VFDW z%O-ixzi%JUyPS~u@beKu?9egcu)ejr%8fmviNjEr`OB2@upkb~Yeoskwd51R5J>L7 zWL&?I6Sgis0x1tZTs;GcOotu)zqY^sSDE%-H_}hamAAzSaBy<&4_QZLSDBQ;`>BHk z&hb`$^#0>qp0(zQb}3%A-;PJ6BmZ3scym&Sysj5Wv1^UR0KtzDLSU^sjskimFerNf z7>$)6O~;u5$pO)$vOHQrF9*gy*S)6?15qt0vb&j4a&X9`>8I|0aKMKH*f=F@7?^sT zz=J{nPo0dSje|4I0a$kgU}ghE5wJGX-UggLVAX>IZUPvLl`w?&#^PTi%WG{0ym<#I z)ihwT_BCIM=MCTl05{cPqM8hKYbaUu20RYz*^y_!KnCx_!NGxRf_G;q7Fd!i-4|@z z0+s}B-uz7C%8;W7Ke*pzJ-?=nksTd$3DfPK=GU?J^HV8D%_wP?= z3S_IHy;ocKnxeR!A=ms)75x&_&3FSH29R9{n2alMBM)jH2KbU56)>EkT4SMg6Ty;f zN;!Sy{&xaxsBmhgJt-BS7hcTy0cT<*9Ux1+8$uS920jab%Pc>z@4AK@1`0BORd}nK z1EBIUjqdMmBAj$05ywh3Z}9MleX?h1T>-F(ytmr+*BA$!_a&f}fFeAamvNbV$f*Qm z@ai2Z4$W^=z1qqzC4;n;kxlL3N_>49lI8P=k+{POFmBH`U>zfclWyABXlVQ(SddB* zEQ$ejoABRV0u%^{??&CpTY}hp0PNL6NbqeyW}loS%s+DQ*x@N+6ErQFS^sg0n->yRjkS7M>azh85OpL>URUF%PcWl?ciX47q=nrwVIW9vvm%97Yu*V%PA#N9Webf3&axz z7)r42r2O{#FPhUD!{Y#Dzr+oev6i9kAC$52=V(*r;{rGjLw*y#4}h9>I zXp%p8rcRrio2v^xzOWE3RGo*2mXdt%^4A+vUTVo@h>Ip;%m8R81@G69I(cpR9&e~G zVc&0|!`OrfKJ%u}UvWH9QX-g^wxT*EU1#$3vcs?rtRv%S-~#FRjET33HU0I^+W0meAFPfjK zJL4UCvO)4rKl z4H7JHJO)eVCHCD8y*WU?!{j*i=0v3A(=008=I;abpJ5%m!(!@(+lKi=hAf=XGl8qq zZ9a!oVMrGKDb$I6q)OGI=tawSWFRB@ks z#XYPkk+=1-RQ7l1=wRTQJ9lSftJSPRT}6fwA4yO6x=XHSJNy?8gDAtLEuRs!SEeYJALj4 z_o*@i3Uode?_Zkhc{r3AA@8nOrJBnyU395Wu@Le2@#89qc^uRDb%nPE`Nq22gw@eq z)F?0zOz#+j(#kpxlO;GjoB$a(U7Ic7?=4w-9gb$7sn4m1FY9LXSH@u!nXj}T4`&k^ zxSAPck$b9|x6FfZrTL$SY8y7mYtwf&O12UJ7ZFTevlW}pVt zK*^TbykV8(D@(p0rt@e|^9AID`Mo-PqCq=C?9<;W89dD`H@ewbxeJgF8frhC_|4!> zz5&d5A#4Ik9LxD$5`UZAr*9ZS!JA3gH~~wWPvi5X&jQ$xeMgMGQX|!#vCd=TK^MSC znWkfuT0w(BSHRQfsMDZV9Ibc&x|)W?pT2X@oPO;1*Jr_o9SI`Jomvs>nyiVbA7(Fn zG!+3sv%h`ik3@=^Ii6Jj2++EC56#P!7yyA9MU3Xn!MfT^ZdiFa59BA@&?~L5J*vqE zG@1cc-3zfG&4n={3^zj_?#y_&EF853+Fw7-@Pd-;rT&g_?O;f4Y3tHtnImbmAek95 zh(&;SrKDD6K0sio)YbbO>LDo887koDRbEw+wK z;J?v4X$a?j@Y1$6%lH`N-V6=->UZn;)gN==c7LUC&}GX(eCI;`Q}0otQD=1B7oO51 zwF{-8g(eD`@$c`ljTLF9%7N4WZ`(X+c&~1Z2fCXFGa@7%gJ|j8+sKRA&q$_64A zy>Lq-&#I>Sq}n|;K6f!azojf^+#3D}A#2W(QH48hSuIajG))INNErLw62qsN*jMGX zrWk%H!GI~}mtd?2j}gz2+RpJQyHm4F>pUKSyzA7z09^^7*IQ`8`KdriV0ce|Gl(1B z0NSblLT9pcZUKO_h<+?Ok?+=1keD_15YN1zuJT~QqvPXsckVwtV5wIDG}8xRgndKX z_C)tz-$PA`UUc)e_LWuiZq-zE?p4;iZW7tpww^vKpl^u34r^*ffaV8_SoJK3W zFv-@W;c1F#9L5+*R7K2Q8f7yPbY|LhK6_;#$5J>(4I?$;GZG)3;5qokSvs+=`6_!4 z(dUhYAnZX?w*xB401hM0QLRWfEopw5&+YcI?O!`qpIWP4*!pb&Yloq^B9D}h#*Tp0 z3&y1QZ|xRzHMCs(VOdF3ZU5#`fee10AQIvRw`}z{Bf{&hsBi2z$v7)&6b#yqOviKQ zU9r3yuZkdUBvm1`9hr7%?&=|N?%|0BYW>}EeyGxp{aX@?Vk22fgsTY$+xDEUS3Ip^ zzNc(It;?IIv^TtHi(Ww=hr8RGe=}!1!gXv?G}{BD0b)nsE=hgrQ-&>lAtV|pJIKN? zsMENL=uUnv|6w9l%(bzo!L{AIh-mqwE-ZWXOPF+Kdx|g060f$+_hSqrWkIaoG$6N} z@{m2~sEQ78mFbtw@R&Yo=D~f~G^j4VSUwqWgS9zEF?%jNbsrVEGs{yt!CobE^JYB{ z(a5CS`_OgMaKh)$??C6cyA}Z|MDct}d$o0^ntdM>efv~|lanFz`cKFG=C5%qQAS*{ zFYREQf4YZyts;!2oi?|urH2jLY>j+h=zfo+WF))Cb!U%o%l2N=w}0Q-S>SKB z*)+PQoWZoSB8_`%d)n);34LLf){_`(bz7b9cmTOHBc(ejI8L!ZZqiRPdS^bN`PH^o zL+Wt(+K@UKJ~vFSDR`SRE)-xU?bvEK=B==FgT(`5UjDFbicc%`0s0<%N4$oFLR=u& zCGxs9_sxwRvX_h!sz??Uf)cSE39`L&S19s(Ug&|*VR3U2<5pL$u)S zlXlU|97pGe|B`~f>Xk;0V0C274C`j!oNR^b3&U7@N$jgfitzA}*@fX8u~Yor>*6@} zl+A+=Yj+L@CT(`lr0hJ~YfL>D%D+^{%{}2c^P_@vC}%+%N*yo1 zE;_}4+^v!n*{mRkZJJHH6BZLniYCTBp~?h&><%$gcC^bYS_q#7>oM$1Gdqf~h+*WKD0iPH9uKb?sq0XGQ?#rloyqw>cEljbEgTcRbcQz_VWpMbhIlG4Y?BN_!QidMJp#0o9oC1MX20!<^Qe)Y`=VXs$FAz@cYf_GD90TnMT(etBpMF2L}x|+65EGvGJX`?>M%d78(#N zqid_{<-K+rS07Zkv>%z+EyG(a(l!Tutta)!f$f=L)dzpJwI9%{v%f9)!TCMJkTNfg zV2WjSZNH#HZ5P|Nr?xb`zY_%7hGeUTaE-5dUXELL5trD}<(t#-0ezXfbMPO zYeKPFPq=)o<5IfSQw8+){at&bZ#_L0U`6y?dc->4jXTp9euCfXKJiwx#Brg)dOt~D zWUpz+ep!K`T#YRo=XS^E3ri_m^(p6N`(=v``MQhamXoTUZX&D%x<{=g20l&5KON`` z@p-E?XGPith^IZ}mCwig1Y_%3X4$=JkC{p$SPkH!sSKsyQl^X{bKW}>`G&ya0|mBhe+Tcm#o!9u=Nj07Qdrj>*tyeu%chLme^P}c-eaN z(6()mX8_BTvO26e;@GkJS?Fznu4|@Vf(+=j7Mv`k_Ugm^z&PgzFW9QDR5;B?CA-cx zXIQb-f8GQOOo?GSJ_VYPIv9oBcJKs{U&&W4fsg=$c zku`oR!2u#XmwK~O&C@dZ=LoYFU)Fk!AnXTW_*GWRzI8O^p{6Ell!rN&5!2b*8|O_H|y6XkO2iIW%Z)g13b5E|Wx;x_K3CY^L@LJ<%Q17ygNev`lD^n3Bg^cLgT}Yu-UgpEq_1xGw|g#UG9bAV(gGv3MThgI z_J-G(BXUa){dUTFPtFDiTH7s^6BQV0nD_Q%Uan=L40MwT>iOg79SpUv-Tx*=HR5sR zgmY%TQL)*eBr^D5!_X#i(cUtvyQ8xAh-&Z5@{Nkzn1~9~E!jxL&#cSel@{NHMn!xXSGDo_ErV)ri6-? z@pwL52{fLgUFAyYbV?*O%<2_~`Fe<+1m;=kV{fJLQ;<5Z+0aUO)$RNxqV_nfRB07y7Bi?ox>RPr`kD) zxvjy!$FMJGxX`5ib9A)Mb3+7m9@sUHTigAZX45zjq$KN*Z>RG76L_lc_N7WUR8YlU z(8=ZN-F#BYFT@}aZi^em{@i}N2iB(d>SP@iM?-sOEuCWOArKi$WYW}Riz|;|V^W*Y z=Ly(z@|dvEfQ|?pHriux(kIGlPeScjZK<*!(p|*>!C$%?C2dQ1KN)OR`Q*jCE}u_C zMtX?r3R@P$Jj(4ajiRxAk2H4Yg(DZD!975i=@pFJ|Nf?+TmUn~NDG8$fNp0AWGA3^ zev#$)9bi#qDc6)NA8-)E#aLUK4OlZPzu}MAJ27#rAwUL5l@F*sX&8E*7f6-d=cC;u zZpg{`0|g(5WMY_e?ChpkgFB?FSFU&gmjTeW#>Pe`AQK_9Qswa0TI~md*Z-O*IE?jt zPj(CPBaiYKWHAU%yOvJ3#N+~w2eLL+^r*uzyWu9WNb0mqhrg7;T0 zX-@20=t>VB-}2d9)?78zJl{$No=8I9ZEz(E#MxN8fMB;l!Wb7mSB|`zaNj1(iP7a8;LR-3+`WrCj1Q&KHfi9pl~dJ<5{(4=_U&72&8^$e zj*Dz9W7x~xjJMDRV%SNLji3GTL6DS z?i=QO-5}`3b^@6k{B3Gk@yaHGUF>wA+e4Z;y0$e21kH8{wC>Dzs@fM#u%yy7DZQUL z(;x%`R7j1MODd7uCyRa!+$FXsT%SYfgOZ5imS5)Zh5m4N0jU=wB^-YNU)l@#!=dZf zncSS=;Z$?6MGyhbPBQ2Q;uQ(ZvJ`&;2$U3AuD!uppvR@~I^_IS$&U&!Hr1HdIm}Cs za&N-@F5c1muJqNuviAwzc$3`r{v40nEKV&lOj2#XYk*}$a5Nx zG*9y86m5avp_(=_M{i_fhI~siF)Q~XD{FR-S*MyiVIwoP@52k9`|x}7C66cIIX&W) zzkyHAP{Pjc{|-wyoa@e9a_q+F^Np#KR>_}n3H7wmCY0bx=5wnHJs^m5I&dLMO*Wa7 z)Td9exRUJY=TfY74}ab!b=7G99|7F64E~Q$l_gDVKKc6G(uKdvkJ9xb+#m2Dqqm@6 zN-wxWeK$wJ9o^E`B?&1X9-Hu%E$1EYaq3UImr`A^2~RH?DySzQOI}ZOQ+=Q4huEIc zWR2v4ka15e zWdWaS6^{$FnN$t=L^Tn&q}i_JuQy{XUuV z%dl4S^SwGj{(oTasDV`>Uq?9MaFBual3pIk0WAlbTH0eN)34cT?mRf8cRe3XHOEb5 z3eZR9Sq;de;KDK4QtgH2`ARtqA{3q;dt3K8xVHP+JgomqI(VS%wu`%M+LNi{9 zi_jk?8i42)YAWGmi7jY>Qpw$Sf>sQE!SVLbfTRR3MSSbhl;Z131UFFOLBinJxkgyb zb_8ZA(8i434@W({9sVJ=J*`AF21V{sAi=I;9AS5^m|ypFKR6aSat9vxfm2$1n#Gj5 zic9$ol53*Ec@61}J3#K&&~XC;er3@k3L#u?l`9njeLxa9B052r^_E%y&z`_b}3 zHPzDNA7&S=_@<%U&gDNDc}e#r-k(&knb=yH@}uQ>C<3#1FH+{L(e?8F$KCuHk;Q`d za;mZgpN@1UfSlUJ+d2P=%8d)#l+uUc_3EHMLG=`KZhHRNGc z>bj)wcis;+kI(2OwsWKbGiU(xsboe9m+n4xazISP9ia)8{9`rFI=n+2>|lflCCB3C zHV)+7v*BkQ=G1-G_N%`9GCP+Q=Kw0mJsoj)uz%||cGOSa>^Tad7gZy95)%1yu=42t z`-S-bP-^{`5(@vplkz6YtsxM`bANv?;6Hg`|NoQm{nwcOv%jP8U(52Z{ocfX&C`F) z)4%xBB>%rFm2G+OQ!Zir@&8s1qZD`AQvI(%8EvQDSt-j{8-5QTWd6@6klATjEs!bJbcck zVXpC#9bxO)Jyy&1u=Ouej3t#kplUL;{_MwmXbSdnC)3t z$kz-1%7e6m8Z4{*)*ij4mzHv=52Hzy_1&`Q&r*$Q<_o3Vs@s|TuryTa1u@CAt_%6` zhQZ5y_9q90x#m1`(T1n;yO|Zb*23B(jG2zkwq>%i({P(DDYcO6VOeYZjWEY z(vEit8DzICkg#+M!G0X5)1wr#QRKYsie*_m+1CTI+|{-maTYiU?DiZ_*CJBd){ z+?U-NCu*MZD&Mr&uGp@##8B^tpcpVMm<{Y;N9wHJ(hl-XWyN+jM10A z>)3Z=mjBOKowclYXyEpU@7uR;WqlvrYQPcGpiTqdY}<#*-T40gdxG**;rL+V2a|$x z95lx1aZS+fH>(V?b*QBiZ{VLI@H@lvp_POv_twuh*~KhOs{M+rWQ)HAZZGqN9u#f4 zkJr;IZKQ`Dc__R({d1-6co{;F9t+ygMhU}Whjgy>j(-3CUCRU;5=te(^RyD5y7mnp zF2!1sh}hr@*-@lh)D*8_+0cWziaf6LS;9OTF6cej z1j~aUb(WFwX#6G9Ojjz_Mo=#!%oKfwM?PrhBgfrJ?|J6q27!0J?r^+?DZ1uqW4NTt z5akWg$3nNGMA0Z96x_D}d1=qzgzeElzTi!Lr2gi$5b#baVb(}2n+8f4>NTT9SKej1 zxv{fUKGuQOTc%Vvkuaokw=rFXLnDRxJ?dgPg4Fil6@RNTH=;h&V(ZV~2wlIn!5}`x z6kLGZHYMhcO;b(aw(tx$dD!Af-o-!tc~RFMtvGZC=4hZa61iAeb4uip3$c$Lwx(TG zI&DiPq76}8$OU3=<6Wo~aoDuPAzDLbL=!%w5if?LqdJ~!h%ZWS8F?S^1uN)W;$3^!-ps}}lnB_M1w_Lxj0DX1C&eJ|d*~rGUVQ&S(9YNJ z<=OO5E2=TxqGAANo;ReaFZ)kD1XZ5POm5vQa~XcXg~3>%Snafrj}FUSM=%6I9wQEgW+R2inkdosV0o_WhpAu6ZgVta39(?}4|0d0t8KdzO^7o(bFR`}C)U3*w zv5p8Au@Z*H8_w_%iJ7C36=<90$oc*}DUZTK zM?XJrBJ>+8(fbzr!FpHN5Y zew-&@XWHs&V7_TcUc{g0Ns5R)s@$9a_k-Rw_2MF;->I-BS2K3u!p~CM7aD3*m3=}! z_dptKV?j8d{@cOsM%7qX_^DKbs+G(Di?K>KIp5`!NNrz_c+>}r$pB>+w8mJCy|1~F z>Qgo%4mJ}o(q6I>da$Mn>k%$T)m8pAmsW1%yF66dUQ-mQEcdCzv0u22vQK1TCO62Q z^B!5mUb@tI7vloGnh(oV^>n?S3N(OYSebivaiz4}h_5~ae>FmB&!&#}=g0o#!I-9iAG z^xPZQN8{el$RqR2K(;7fDs$43!F9u>(5l7=x{zXCa~8^{HHbB5jZfR|GdE{oo35G? zPFH&Lh$x_6u1J@Tfz!p~?<_1=QtO}7f=00!phwjw&E1Y(?@?kxZ%o~IC85!IvAg|q zX6l@p7B?JcR;u^q5u!1F!h+Q3fm9*VL@kb|{uMrLcoQ=LMGIF^hjx3R+rx&sayh&) zfu{{4SAG>Kov*a|BGen4P0<~vt#V$r6Qr$+YE0S&uy`;SoePc4T zUO!L~{x|X|&aTAnwLp510h|$uFRWetRb)fb9Ro|CxLY1e+^CL6c)^ehgoUB0Z1tGY z(Boi|mj=_#Z$p&uN7Q?xpx`$nMYZDzC5h@6rsng~jo)C#`$LGf+GVGbKF9njOHVVc(zro(kTT1{;|H^s)X6xvf2m&W%i1W1Lyh zQfQlIV*s=(8kuy_Ey-Gnku)FdAnD-l5rS& zDN_`6wGuLOgSUn=;M#(@AD&2)|6!7J`5kpUIQ~}Juu;=uiAsOG7uP(8aFAiP+Lzt_ zjg5CF4I91XSkny-n`F=;kpNCA6D4Q>($bUg7H%PDryAqe-xNh>OX=_U8b!r=F zsqb8Lu6%0UeaNb~=f2sm@)*QYGjE`lT<*^CT{+0FxiM1@+WNZ7by5Gv+UmaxF!G@p zx%Rx<;+Pf$vNN|4@S4(2vqm`$QKPTMAk1$5&y^cZt@GyoN?o3I^1$H|Xih-sEd0vy z-mPcgG%bZtgwyg?$ThrtM2fx|Qb|g+g>X)Vs z-P_NLSATWv=bx>6Hq(*#{P}Z_5wlz^j?OcGNreq}lgGFEax~+?bE7p#o?wB1c6y1E zbEwEhb^G)pxv=}wRi!VFj`vp{=>ryN^!5w)Z2f@F9(v{LpdtkOpG}HyR=1X30tH&k&(wKw#tMkB^fUs- zlP7Hu#cRp+Q^o}n78Qge03o`zy6P^<4TM@&lze!_^YEvv$*nO0@TbC4;5BRq$ocw8 z6Vw57H^&Mf$IXmK(P6Na#*O9OjahOq>vmhP%GUvhr+N&-#rC?SmIX7x$58WpS6EF! zvjH@C^5^^4=h-o8a2#$4#<}Rh16MVF{dE}{dUU{iQzPGfK!fLCdn~l@b%ob#(qW)J z8lP!(Uc|US(1P%FR-5iTn-MN}>yN4Sg~Ga{N>lWuvpk>EzD;4an?G_CyN<|+>ZaT; z0Q=q> zhKVW#EcU(EE`Mc?WqPGlHk^nR9b+yXb2Xk-t%rWU~sSu*&O;Z zBo1`3yM=9OC7Z^RU>Frw_Hs(&2iRJ=!KX_Njr5}U6h>o*hfw5@*r8-8H{;O9?Vw4zEVSG6y{1zsQ z!7n@ulV<~GyCKSMl>Hp3GfO$r= zO^+0EN807~JzvX;%RocA|kAlVIy7R-3dz?ExjX$_mxO}f|W!~uDV>exuGJ+H*P zxlaI_1n9!1zfdLg_ff+$o|)_@QsW19rVAI0L@32_2H9$VLo>uJD}Ptm)&}ir8Oj=m z!$&j+19sPK1?~uHA)kH`!6JIF=Uk03aQp#MA)HePK}lxQ)~c+sW3XDvYmO$W4JSI`cgNkJ$c zLP3KLfY>N1D(2~?Z|&8egN$!Yktl^{CSFKW`^uCKI?O(XaDP6$?8o}X<%`-& zoeqafrzg>uz@KGzeikK|B#uAD`-vLQY(D=a%~$Jpbu-_~>+&#R>VRul>g;m(_5+Lg z(X>max9-=tmDAo}Fr{Z(TwQxdstYL(`E$0}`lHJYMfu33`+*di=V;kbYT%m`|F>g? zQPB}Mrir7e3P=o2qoSYP#vq&CoB3T)#EtgmBA&oTT=dA%j|t7tgWs!fP>i39rqL!@ zcHJSDPRp~f{yi~ed3pHjqXQe+tvcz!wGHdIqt!dtxzs7US#LKBTfJ6XQ{@Mmt8SM) zdA-x@1L)7F^|PI6?U<(f4?p`Z&oyx0Y`1MmJ^nR7m7*jRF?{%Wx2(qeW@XptE_RH# z?*$3mg9WC=JLQR>Qw$c{d(C#a)OMmo$N+JG z=K=o&HN*EG(TF8}+OaJ&e@pj81G}aNbgq7>!Jv>K7YCs2oyk(VfJlKt*jWxln>oS# zi_Mpi!#xHLiQj-Rb;byEw?tnD)s>95l%0)3!waoIo8h_f+3>3DH?~`GSK+k;$yE>M zb|!^`ufY6E019%;)UzYuPO*KrPO41l)JvdaC0!{yZ<2WwLJkhM$9!Gm$yR|l;NBPu za*9%O^wus^tOww$#R$c$qQOteaa_v|02T?0b$nyCP#-CAG!%6TIw5RSLLAsg`(R%? zp1ho1D;^4jWa#+wGxv^l6D1nG+(gaGi0{ac;%=GN?k!+}7G~g+w{YX~S_E%p1W3yz z`(Iz50CprHg2J7Mj_`Peqn#}E7=F8)e5&Lwv-d zF<*1p6sKBJ1+dQ;TkYSRG0&fq-HXIy2vyU;Z2T-1SCw>-%5hntWENHv5(1T~^f{YdpzrB{5D7xU1$l8m!GE;a&&h zG8w$E=f+HDbTmM0sWy0%qD-J5IUjO9t}OMQ@2jy4*_~$J#(T3f2r)oKozAA+_s9gg zqd!->U+Gy#g1$(4$ev@3ly`ja{+1AWW4b->4lXtQf&cnw;0o03GJ^+C)af-~nW*o8 zQSo1EWJivHWsN!2SCD~z)clXmWoZBA(lB|EW1a}L?@9sE&Zn@qCM{@Py%^LeQjz1M zzVAv*a@pn64KL4-R@W?+N!a+&xIN>WC|(twt1boGBv9zViu4P{`W6_Ncf-dPIBNEn ztAIE1JsQ4e7S1Zd=2c^y6tvm9mTLauLww}FeJT!;hk$2p_LKkVFULF}QhL5`&i9j; zi4a-O-|tQ*H$4Gb{a)Ib|7 zUQU}fSGhKuC^=)`OGfs9pOD-CQ*73AU6=qXZk8#tseQRQN^A<)pW~1wEHS${uly>Y z`BO1dInF3%#ZP?O5DW-0C@=^LvC!ID02SU2l>S_`!F#k~A;TWe3pU?Oe&Bsz^> zpis!*d(pr5fYa;@F?yQ+Y`HI|9qi3bm-oy!Gcz+|Tg=c&->c30a<#GiZx&2pe4Mcu zhkJJ%-dcz3=8Qr(qk%Qm-@{s017%VN{2AahC&CmFlv5#Tj1lI5;hcgL4px5*O#q{s zQJzA+D{q&I*OTCh?8 zKs5Dt1SeF(CadZL0s#{asJo$?X0Z~gQoNj*X+_nuu#ugSodevy)_vlI9ZRVc55=e4 z)!ACA?8S#TM(UEc!BRBP8BT&uiUKIaDF^OF2P3lu zwXu9Y7osdW=aNz7;EufQJ3y!Zk5Ft>iTqwtL??+lI9moM1ONLn56xcNPf@MQOimmDXH%UPVvU3f?)Y@eZ4)stO?~{{m4PB2vg}#k z9#PI=a;e6o!r*4MSg{1$_MJ3czo5~u@>Iu-f?86KzR(f@13_Jo`LH?k@-c218>3hw z#|vmW6)<$nQs8Qp(eza+!LDl;2yC zJtJj#n)n{KaD|#$YTwLyC_{UjukOPlCc4Xt6my5HK&mA_S-f3R?LBWG2W=)rN-;G1 zIh^i0S72(KY!J~xD7$d(+}edx4uVb(;ofU*&#*W2pD>egKzLCX$=DA#2sGUtP*u$7 zQ49=gbb#T!&6nHP2BJkr!GIAs={-I+VVdwJQGk_iq!QV`n;%C~>d%1M+t520K6H5H zL|N#2r$NIL*5(4cyl}Berfe$UjAPFJHHPJP=6A{pu>>Wgf1b&FaQp0-g92IDBEk-J zHFvTHotSAF;pTdUjR$O=OIsno!Qe2F62O24$J74<`-kjAP zaSIb^ETC`rBGVM-65-4$By&1qM=MbReWTYjZ-^4W<)CppehP^|FGg_oCp|I>$j|gR zTERqD5U13m@FJ&W?6eh<1n!w9oPLMbj^L3s)>ZT}=r9=iFlp7C+ZC1eBIE(WqNy}T zhlS|S$FTU3tNASwrzyHhP zY%fkYp<^$>#@wydWK41BrP&rx9DW_$MdM}an?=DR$0@glz9QF}aCxd4$__{w(8Hvzq+LCK-xg2Wt=VW+#H<0fALl-zIU1olcY~(cu zI6Z1Edjr;Fs+&LF$RB%(va$ZSBXB-jJMZ%mwAs7#@yli>Nb~iXUy-|vo)*Ed+2gLk zCs|H5`|0PZ*gM<{E5rzQXM}ntveH#4%=dUSB|_?8Gi;iF>?=x zMOoNX4Od)0GvWjsS-M=w%I49hTXY|!)gR~Sq!K@Qg!!QvJSCn5TA-J1v=%Y!Tr=-A zz!wGgc;#%1C(_e*`LC2ug)A5i)BNcT{2p!{SQdF)7)i^g5WoSFnZQH z!xL1YeC`Ha#Odk^uVtc=liR94qDivg68Tr;v~E0(PxhL5^k;uzPX)%;G&}&JZ%S-J|A6-mtq!sb(Th|ZX3H0o5SV>g~n4|Q*1U|&^89a0jeU3IzdH+C& z5j^q5<6n^*h1Gd#D9qtog>?GCJ1fOxo-6acrr8V8?_UO3MO1&zkd-Kl{yF0A+`J1P z&AN82iMr&2I8L*gin9~?DgIQIIdkz(MJ`zUJ0iq`5*82<8#DV0W|IG|;K?yWq zK*IA3xLY3ku|kF*#yMn!9GLI$Q9vd(T;=PQDubLHD*(O? zm?SS|^*0wdHVGgxzAW+VU6ikSvg8ubZqtzZIu-h6YueKnc7Dg`8g&3%0irT5vwkqU z0K8GELa0?ccq00grLH4ptd&pA{A++*0{hwNEzp1M;CcbsrY?K2chHA^53m7 zV}L8`K3G!*ARqTsQ_K zEe}L(2yiu^@Ua>`jp3{yXEdx%LM;uKcgKk)o1P>9pFO}>2H=z=Nua(Z7eS5YVrE{a zcy$Y@qrO^Ik#L9m+wg0EmskZZg5-Yr{?nglT?WC_##D3E3Z-HLTsuAGl7Nnbt6roN zZK-x!oGA9aa-d!$@S6Le0VFGg9?7m$dx5kANKln|kZrs3JO|Syh{yz+#C;=Ym+&moH!5GVu-A*>!?gp(%T0x-Lmum@Ic&Od_cbA!v;CG(FaQ>uMYg%0T7uill0rb6)Y^b zje&bTuLzSBw;2i1XwM zV?D)PCs?4HDwA^!-T<%kc7%P?FtA?s1G6Hw?Vz@QVC$oss~R6^)XyXdXkrl1=s4%~jSu z-_)EOT2Hi3YfX_Z7JDGm}cT>`^LnJB=sCjd>)w$U@F1A$2HrWFXAK<}qg zbKpWI_2&Sy;x>C@e5dFCkvp6vI0e6=zPh~Dvi@v7weYz!Di33+%>p86f zb_eUA*&qfcqOEqGUF;UHksS&MZWb0I2+_fQ60Yq`0=}P!g}Y5PMO1!Z%(ClDVuVmQ zkBlf_s43#zJCnMQn$OE1alybq?mt}%Lpp&?4D*F!jYUnrJCJSN#d)GWZeTA(Gw$beYz z&hV**h6asUa6<*FPPVj9_(DJM^TJ1*QmzG`jql{SbTsAiL<46Gl0v~VOx*W-0L5y| z)PSpeMVNQXfx7{{efy+xv^i*B9`XVtuVf8Oux&n+Ij5I^2uQeVAGu(Q>W*aES{W<+ zGjOSww<}E!du5=3`+;8%1v9T`TguPcU3KESpD$NDe!p_GxK1D3$#HV~rKo7;*M*4T zgwS`EwuKAYu&Opi)F=WfXipL{78EguMKo1Pu~rY&dz3x}bUm9s0DLGzs98ykP}C~I zTF@Ld6plnKX|#U*dVAx{43xnkpr<^E_MTFLW~3{@`E}coG$#?k%g1|EnSBB6dldoTfjMej;sTjG&!)9LX1GLDbj*O^a zd})SQxM|D-f6s00Jhdlqn{yi?$>9l81#)p_BeK6MuLLM=i zD;{8ng*%tZ9v$twk;V;+ZS9LC&7{@rrePw-pRj-3mB|8kKL~VRVxJ` zBruU7bF`k<9IU%r|4io|ngQbK)^$OXXWaYNCBGk{`jI4?&q5d}-hgWrkF9SGhw~1FRLEl5@4Wpjtps8CH1O z3OsjRm;xfMiWXwrRlHs_bNOYMoE+Qvis+s3^9HFlFrnYo-T*QHXYPu@(t&SmlfY~*DOqq zyoN)zQ9jH0+3pwWak<1aAdP_P0g+`uvY1YjP3xWwQe@_Kd#BTk4obp*|MV`1Tr}*> z3CjtyP3=;zw}QN)O+%J?!(V&m`7#fW(AuvkaI$qD1(t~n5X{8>?XtS%_3%IH2TKEV ze3-8VSH`Z$!$=i$ltOi_m-5!qjtw-kkNf`0^*ijs2syOH~3#=V}vLlV9Q4}De|;@5m*xyGXP1;>h;sj{L%z>q7dlC}~h8Y^&!5f!l7vqLKXhIO-NYu6>tM9MJj zc}_MIRJfOLbd#m^e*_ez}m!{lL@LLoRDu)!C$! zn!OKwzmzu{EBbjMV(aU_vw&SsW#Y2Zto5>~V)t_6TM>ta!;pimBCoCafyXdU`GKNm zkp+%o7S*0>p1-r{_G^|$b4;(w_17D<&f;a(N8d%*P1EASy`7D&2)eYjo@WYL+h@bg z4^@A%i)?~nw7}ii^YK?$C)o6f>Pfl8stgNAHGx%iQ~;Oae^sQhm}itr$cJ5NeFM%Ui}msyxFU&u>_3fPW+HgCY<60=jr?;hRRfK zM`_v_W=j@$;?i*NJ8zk_-HQ8OOHh~fERI7G%Va~lyqQXC;@0^-Qrr)S(_oPtEu~*9 zJ$tl^Q86;wq%9Qdk=-iy<)q6d;9b&%1Yzlc8WnXgt32Ig1CcZGdSy-<-z}|EWP@89 z&hbH^DE#sKeE?NO>OXCLGi9#S#nQog@3?9}MDp#H zU$A20wZu1d#=66PwO5v9m!-XrVyq=qEh9pFciyPr`(x{kg!a6}psU5GyK%gnYmb9# z_@#or+z6Z@`br6}wtQ+F-cCJ4sws79Et7w~*Pi94Lq3RHRQ{bRfUVsd;p`Mk8f|0m zEO?u|`6 zk{A9I-v4-PUqj@ZrTL70gVk-{p2Gg^xxZzhVh&rY2Ho@m-EXR*6vxUisoP)3Y+5OG zp&i2v)bDY^2_8-GP8j#5zZ*KYzWemfZaRCI>|(N8t$xfikTOTsF-sF*A~=fYqt3T` zD9=B?LVXq{X@z?<*jh~To-#rocSz>Y+Zim5lQ{w@+k1fxG$=D{d74o@($Q*8<2Oms zy5NEPt%Bn`k-nUB4hEfhyd`Bu=X`d4<~#{q>4=p_?R_s=9@`KDpO*r6fXB+jc|$fn zBV@sl-kqXjlZ34dDAO3y#Xz6tGAk1He7(7#W-vKQn#-s0zkOiV$mLhN`-P8Xi{aeH z9cY=f>bd&zsj?MW&RmI&QM=dPTNfA4dmd~~hsN<$20+bx$I4a;2J+W~mp`CnnNkc7 zxl*K&qkrau{J(f#TmL2df%?&kxAHM{-;%m6Ezfnd9SJ@Ow3>D4^fRhNgUW_&kMjCa zOGE%+rP-wrF7%eXH63o(`zQ0d0!u&wD5(8Pc!OZ*`DuiS(qr#TtIX4SrAzK{}649xq1IaT@}pn>R7X^$V>z?BDy<- z_ko=$hVh%!*6n!A$d^hh`wI)a)pvtll6l++&yxfA&kr1}BuFmF2jj)1>+E12N*_u?tUYZrPbW$&+qc8%$F=cB;;=Wz^uxcxe>)yYX#%R9a;f ze1Fo*I6%rgB;~6mY&GSJXz5^I#Fw#%;oO@5nE+i9d z4hwlCvNh2xyt9aw%Q6Cb*VPI?@0P7Jle+|wjIj##pZ0~^MgsYL%Sx(v#uWtCUIvaX zWXY^`Uyfa%u8>@mPf$|ruc}S^d)bKi9q1wZ1dp=#bstmaiEJqlKle0IX6L6p{*PPIo{+2D-F04bP z))$@6p||~bOn2w0d=*!%-sgd=o0-=i_#I?U-7P#rbiy}z$J@O|mYR02H5S4HT?!2& z-de^dk>)NR=t)!OoHi4JmLL7xu*ThkpMOx1z8-lvJFxN~hnGCUaK0qeCob@%=0vpc zs@F4huf3pGQ;1T>nVuV)%>|zttnr><$7Mf{&);8I$kmK7h_BERP&}O6LwR8~WoLVp z-p%){eZcUQ-hMI~QrgnaQadnh^qN&-E$!(pet&#q^v5i6w%;MpQS|s;+St6{`Z2aDXXtPR}c0N_)_l?al)pzV>lTj>p)@4bZr_Jhf9Wf zzBwgxt?5}-CIIm z&4+_UR;)>&iLFu~TE=QLc3^D;zu>V3%vH z%~h0}NWCL5UGi~|H{aT@oc)1!9`&kR{ID^pFe8m+EWGSNrodFwqHs+{@=eE&T6IHL zh;%=+=4n67XpSAnuzs-@lRfIYM5L|RHJNr0kdcs(F_p;c-q(~f?^}qN`={)XwD#xq zJ1xcMBUWj7DBX~+oAKqG1w12Gooqa-VY=Xq&PIz+mAqf>(0OZrg=V0l%KS{sOV^=+ z)DWj$wAbGOXsCYyw?CPLCF|VZ3!R*0^{@9iI=K3J!);dUxUct@Th;q9MhAJ#jPmkY ze%+b57YT?y!h$1GMTDh&lG@-tw!NYbKdZ^wk-tH^VS>_p2DP6~<1!LOiT>$hwe0wa zbSrYb7(cFsCVe#iP`{>_N^^a1~E)R=8Uh5$bHCr9v(C+c)L9cvRi53suN9 zbgl?L*L|!zjLR*?{As&cypZXzk(ax#uSl!L@|RRlPB!ly+j!b>9nAw>6a?ZbbP_$p z{K7HgS!n7-A23#I-G!Ugb&s+_7AYkX86ovb&EL?fxJsVmJDus{yj4O6lc+gzQjQk{BIb5a*LT5S z^Ht~H;BUHDgzv~ZC(9$l?mBAj?EQ#t7NhJ)X@DaJQF}uRpFl>AsJp*Z?R63z^c881 zAHw>LTUb9`mkU*-uMdsJKgJZwA#8uW5-rafkaQJ3fq#7I3$*{!1E3QdWQhAd7)P!J z8O6z*d%)%#35aE~ihMIey(>ucfg$&6ixny@5PSg690H!k9Xj2aT>dzMvo)r96wtGG z34lU@5$u##XV0E(0g1k;n0eVB5G>raZDV0$D+@gi!GUKEkCeW;zu*0rFPNSZtGEw2 zQV}*TNC12W%q_498wr|zJEj`~WJStlDEkwV84nCOh*V^J{+JY$BR=c3RtN2R3S0&7 z2^8-waMF;_XVKdbs%c+W!70)YiYS33dK&sxbC0JUhSO_g57KI z8zF)$0)V<3f~Bd+757ci1~*mA0SulTy5cfa0<4=C^ZQ5tFV4&YQ*nFhb2+`b@Ms(g z1m!Xz2Rq>1Acr#fs>zmMEH+8f1(RqYUSgUhWZ7G&uIAhOL~xS_qW;j^yF^&6F>^Ao z_c-y6&71XRbq(*zLJj@%;aT?_`+m2_7lOcNPxAI{J-JH)&q36HL=HmAH%{tr`zTqB7>X;3e=~bx&r?B8;rPUvpV}=2}EZOPR>ezTvO*A zXpw~_86!!xD1KTT5&IPk1z4wjjdTamicH#%w@hw;VMgT`3Lk#$M28K*P8ib&4!T?g zj@S4AAWHAF&8e~r(twjF1S}^hFdy4M^Xad-R4n8B*?lQTgJ(k9-0)##Dt?9)d1@2a zcK~jM&Z%8f53I(>ROZFaN;DV~l40x~H++Ks1;*tnCCuUtsei@(A@j=md(u_PmZ~7} z_!6C@d+Ji?%#+h80A3Px%*vb-(~4)#!K#F7NK6Jb1?z+HO0a$FnCH0+ONuo&O65Ig zIvlT=5Iqe{aVFcZ`3sUx1GhR= z0E#u9$IYuD@mpE3Mz$^i5|t7e_{v%sTy1#nbJpw#YY)P~j|U@0)=7IX*ue&}5pqq5 zNVo2U!^|qe%)jfVmC+xkM`q=&skGw?k4@tNhy3*EqQC@;ZSJHF%Tb9&Y;c=3^-@W} zoA>yKm@7~z^f#Zk8C4Bq+3J@@RHiH1O!}$DlUVlw9U^*_;ssK0Fw&RZd5=ag7WQ;8 zGT_RRF#~-wNy^t+)QBw*`KwgBqsbWX6)4PMrpG(;5}n-1#($vcykvnQOuaNKMK~_^ zo})?Ep*g}Cl?x?-%jVeNZ~P$f^O^pmdCuhhq{QSgZQjrq<`Y=s()qu!ky{s*+r<$4 z30vi+=&vWIX@&-d`U(ciT+G2rItj8&TWZWvzmj^uv1FXB;eBihdH3~#?Y3n2KT8%7 zj5pmn*x};Z9cRT1bG7{Z2vd8Msz1Z!y3M(@Wp)bFg|4LM=(l+3hdJIBUT8+GRF0(} zPy|9k?RXz%ZcMdV9e6{rppqe0lSKUa}Cqpc|<(F%<13+@_}nfyHZ5}Pqz0#ln{guwPUc?MKHy(~Ie*P0#;{O!6%67Q z>di7#CVil_5+U0xF7v~<7Z+w+YOga{>a}DT%p44>$^WzdhiykKe$G2bn-1$3C3u>Z zf~Z-@9#hYdy+}cPaS%3aHf6s&HD!>?3t8>%N49B_papxvDr$uQVV< z-P_Er@eld)XbrbuZa)f+LmAZ>kvq8S@;$c?oZk=PV%*sd;(b-hYWKD`A>(6jttIFn zaTD7f|0iE^@Sluh|D|Bei p7x)|>o7Vju4gTl!NlT`M+<#RqyhVS77yJaGuA-xifAr6b{|6cAH>Cgo 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 7190f6b6c8d6a337d5f66e91caf729254b722e48..db93d20137096c1ec8d123d396cea4f8eb44dc58 100644 GIT binary patch literal 18186 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 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 e1828c9b6b5fb53abb5eed2e07eff05c3a8e2339..d2633a1f3ba97275540985a66ca757ee8034b9c9 100644 GIT binary patch literal 25615 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 From a73bdf6ddf934f97f68ca022ca6fa558ae33a276 Mon Sep 17 00:00:00 2001 From: rrewll Date: Mon, 26 Oct 2026 23:43:08 +0300 Subject: [PATCH 3/3] fix: fix coderabbit issues --- webview-ui/src/components/chat/ChatView.tsx | 137 +++++++++--------- .../ChatView.language-switch.spec.tsx | 111 ++++++++++++++ .../i18n/__tests__/hydrationLanguage.spec.tsx | 6 +- webview-ui/src/utils/__tests__/format.spec.ts | 14 +- .../src/utils/__tests__/validate.spec.ts | 5 +- webview-ui/src/utils/format.ts | 9 +- webview-ui/src/utils/validate.ts | 20 ++- 7 files changed, 208 insertions(+), 94 deletions(-) create mode 100644 webview-ui/src/components/chat/__tests__/ChatView.language-switch.spec.tsx diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 3ec30873bc..f761fbac6e 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -180,8 +180,11 @@ 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/__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/i18n/__tests__/hydrationLanguage.spec.tsx b/webview-ui/src/i18n/__tests__/hydrationLanguage.spec.tsx index 31962c5b29..1e40c27ada 100644 --- a/webview-ui/src/i18n/__tests__/hydrationLanguage.spec.tsx +++ b/webview-ui/src/i18n/__tests__/hydrationLanguage.spec.tsx @@ -42,9 +42,9 @@ describe("hydrated language", () => { await postState({ language: "es" }) - expect(screen.getByTestId("tips")).toHaveTextContent( - i18next.t("chat:rooTips.customizableModes.title", { lng: "es" }), - ) + const tips = screen.getByTestId("tips") + expect(tips).toHaveTextContent("Modos personalizables") + expect(tips).not.toHaveTextContent("rooTips.customizableModes.title") }) }) diff --git a/webview-ui/src/utils/__tests__/format.spec.ts b/webview-ui/src/utils/__tests__/format.spec.ts index 161a09e40f..6ffa7399f5 100644 --- a/webview-ui/src/utils/__tests__/format.spec.ts +++ b/webview-ui/src/utils/__tests__/format.spec.ts @@ -1,3 +1,5 @@ +import type { TFunction } from "i18next" + import { formatLargeNumber, formatDate, formatTimeAgo } from "../format" // Mock i18next @@ -5,8 +7,8 @@ vi.mock("i18next", () => ({ default: { language: "en" }, })) -// Stands in for the `t` a component gets from `useTranslation`. -const t = (key: string, options?: Record) => { +// 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", @@ -32,7 +34,7 @@ const t = (key: string, options?: Record) => { result = result.replace("{{count}}", options.count.toString()) } return result -} +}) as unknown as TFunction describe("formatLargeNumber", () => { it("should format billions", () => { @@ -58,7 +60,8 @@ describe("formatLargeNumber", () => { it("takes suffixes from the given t", () => { const givenT = vi.fn((key: string) => (key === "common:number_format.thousand_suffix" ? " mil" : key)) - expect(formatLargeNumber(1500, givenT)).toBe("1.5 mil") + // `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") }) }) @@ -161,7 +164,8 @@ describe("formatTimeAgo", () => { 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 - expect(formatTimeAgo(timestamp, givenT)).toBe("es:common:time_ago.hours_ago:3") + // `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 b1bf7abad2..d4b9a4da2a 100644 --- a/webview-ui/src/utils/__tests__/validate.spec.ts +++ b/webview-ui/src/utils/__tests__/validate.spec.ts @@ -4,6 +4,7 @@ import { type OrganizationAllowList, type RouterModels, } from "@roo-code/types" +import type { TFunction } from "i18next" import { formatOpenAiExtraBodyValidationError, @@ -14,7 +15,7 @@ import { } from "../validate" // Returns translation keys with interpolated values appended. -const t = (key: string, options?: Record) => { +const t = ((key: string, options?: Record) => { if (options) { let result = key Object.entries(options).forEach(([k, v]) => { @@ -23,7 +24,7 @@ const t = (key: string, options?: Record) => { return result } return key -} +}) as unknown as TFunction describe("Model Validation Functions", () => { const mockRouterModels: RouterModels = { diff --git a/webview-ui/src/utils/format.ts b/webview-ui/src/utils/format.ts index 14f70d0b64..d814cf93dd 100644 --- a/webview-ui/src/utils/format.ts +++ b/webview-ui/src/utils/format.ts @@ -1,9 +1,6 @@ -import i18next from "i18next" +import i18next, { type TFunction } from "i18next" -// Structural rather than i18next's branded `TFunction`, so test doubles can stand in for `t`. -type Translate = (key: string, options?: Record) => string - -export function formatLargeNumber(num: number, t: Translate): string { +export function formatLargeNumber(num: number, t: TFunction): string { if (num >= 1e9) { return (num / 1e9).toFixed(1) + t("common:number_format.billion_suffix") } @@ -32,7 +29,7 @@ export const formatDate = (timestamp: number) => { }) } -export const formatTimeAgo = (timestamp: number, t: Translate) => { +export const formatTimeAgo = (timestamp: number, t: TFunction) => { const now = Date.now() const diff = now - timestamp const seconds = Math.floor(diff / 1000) diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index c2c4181dde..3e84a03c83 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -1,3 +1,4 @@ +import type { TFunction } from "i18next" import { type ProviderSettings, type OrganizationAllowList, @@ -9,9 +10,6 @@ import { githubCopilotLanguageModel, } from "@roo-code/types" -// Structural rather than i18next's branded `TFunction`, so test doubles can stand in for `t`. -type Translate = (key: string, options?: Record) => string - type OpenAiExtraBodyParseResult = ReturnType type OpenAiExtraBodyTranslationKey = `settings:validation.openAiExtraBody.${ "invalidJson" | "objectRequired" | "reservedKeys"}` @@ -34,7 +32,7 @@ export function formatOpenAiExtraBodyValidationError( return t(`settings:validation.openAiExtraBody.${result.reason}`) } -function validateOpenAiExtraBody(apiConfiguration: ProviderSettings, t: Translate): string | undefined { +function validateOpenAiExtraBody(apiConfiguration: ProviderSettings, t: TFunction): string | undefined { if (apiConfiguration.apiProvider !== providerIdentifiers.openai) { return undefined } @@ -44,7 +42,7 @@ function validateOpenAiExtraBody(apiConfiguration: ProviderSettings, t: Translat export function validateApiConfiguration( apiConfiguration: ProviderSettings, - t: Translate, + t: TFunction, routerModels?: RouterModels, organizationAllowList?: OrganizationAllowList, zooCodeIsAuthenticated?: boolean, @@ -76,7 +74,7 @@ export function validateApiConfiguration( function validateModelsAndKeysProvided( apiConfiguration: ProviderSettings, - t: Translate, + t: TFunction, zooCodeIsAuthenticated?: boolean, ): string | undefined { switch (apiConfiguration.apiProvider) { @@ -225,7 +223,7 @@ type ValidationError = { function validateProviderAgainstOrganizationSettings( apiConfiguration: ProviderSettings, - t: Translate, + t: TFunction, organizationAllowList?: OrganizationAllowList, ): ValidationError | undefined { if (organizationAllowList && !organizationAllowList.allowAll) { @@ -274,7 +272,7 @@ function validateProviderAgainstOrganizationSettings( * @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, t: Translate, 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. @@ -296,7 +294,7 @@ export function validateBedrockArn(arn: string, t: Translate, region?: string) { function validateDynamicProviderModelId( apiConfiguration: ProviderSettings, - t: Translate, + t: TFunction, routerModels?: RouterModels, ): string | undefined { const provider = apiConfiguration.apiProvider ?? "" @@ -327,7 +325,7 @@ function validateDynamicProviderModelId( */ export function getModelValidationError( apiConfiguration: ProviderSettings, - t: Translate, + t: TFunction, routerModels?: RouterModels, organizationAllowList?: OrganizationAllowList, ): string | undefined { @@ -359,7 +357,7 @@ export function getModelValidationError( */ export function validateApiConfigurationExcludingModelErrors( apiConfiguration: ProviderSettings, - t: Translate, + t: TFunction, _routerModels?: RouterModels, // Keeping this for compatibility with the old function. organizationAllowList?: OrganizationAllowList, ): string | undefined {