= ({
{t("settings:codeIndex.title")}
-
+
= ({ results }) => {
+ const { t } = useTranslation()
const [codebaseSearchResultsExpanded, setCodebaseSearchResultsExpanded] = useState(false)
return (
@@ -22,6 +23,7 @@ const CodebaseSearchResultsDisplay: React.FC
className="cursor-pointer flex items-center justify-between px-2 py-2 border bg-[var(--vscode-editor-background)] border-[var(--vscode-editorGroup-border)]">
{
+ const { t } = useTranslation()
const {
terminalShellIntegrationDisabled = false,
collapseCommandOutputByDefault = DEFAULT_COLLAPSE_COMMAND_OUTPUT_BY_DEFAULT,
@@ -197,7 +198,7 @@ export const CommandExecution = ({ executionId, text, icon, title, isDenied = fa
{status?.status === "exited" && (
+ content={t("chat:commandExecution.exitStatus", { exitCode: status.exitCode })}>
{
,
diff --git a/webview-ui/src/components/chat/ContextMenu.tsx b/webview-ui/src/components/chat/ContextMenu.tsx
index 04ceb2eab6..92cce63e09 100644
--- a/webview-ui/src/components/chat/ContextMenu.tsx
+++ b/webview-ui/src/components/chat/ContextMenu.tsx
@@ -1,7 +1,6 @@
import React, { useEffect, useMemo, useRef, useState } from "react"
import { getIconForFilePath, getIconUrlByName, getIconForDirectoryPath } from "vscode-material-icons"
-import { Trans } from "react-i18next"
-import { t } from "i18next"
+import { Trans, useTranslation } from "react-i18next"
import { Settings } from "lucide-react"
import type { ModeConfig, Command } from "@roo-code/types"
@@ -45,6 +44,7 @@ const ContextMenu: React.FC
= ({
commands = [],
}) => {
const [materialIconsBaseUri, setMaterialIconsBaseUri] = useState("")
+ const { t } = useTranslation()
const menuRef = useRef(null)
const filteredOptions = useMemo(() => {
@@ -297,6 +297,7 @@ const ContextMenu: React.FC = ({
Slash Commands
{t("chat:tokenProgress.tokensUsed", {
- used: formatLargeNumber(safeContextTokens),
- total: formatLargeNumber(safeContextWindow),
+ used: formatLargeNumber(safeContextTokens, t),
+ total: formatLargeNumber(safeContextWindow, t),
})}
{reservedForOutput > 0 && (
{t("chat:tokenProgress.reservedForResponse", {
- amount: formatLargeNumber(reservedForOutput),
+ amount: formatLargeNumber(reservedForOutput, t),
})}
)}
{availableSize > 0 && (
{t("chat:tokenProgress.availableSpace", {
- amount: formatLargeNumber(availableSize),
+ amount: formatLargeNumber(availableSize, t),
})}
)}
@@ -56,7 +56,7 @@ export const ContextWindowProgress = ({ contextWindow, contextTokens, maxTokens
return (
<>
-
{formatLargeNumber(safeContextTokens)}
+
{formatLargeNumber(safeContextTokens, t)}
{/* Main progress bar container */}
@@ -91,7 +91,7 @@ export const ContextWindowProgress = ({ contextWindow, contextTokens, maxTokens
- {formatLargeNumber(safeContextWindow)}
+ {formatLargeNumber(safeContextWindow, t)}
>
)
diff --git a/webview-ui/src/components/chat/FollowUpSuggest.tsx b/webview-ui/src/components/chat/FollowUpSuggest.tsx
index 4fa6f9ab1d..6226285058 100644
--- a/webview-ui/src/components/chat/FollowUpSuggest.tsx
+++ b/webview-ui/src/components/chat/FollowUpSuggest.tsx
@@ -3,7 +3,7 @@ import { ClipboardCopy, Timer } from "lucide-react"
import { Button, StandardTooltip } from "@/components/ui"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { getSuggestionMode, hasUsableAnswer, type SuggestionItem } from "@roo-code/types"
import { cn } from "@/lib/utils"
@@ -31,7 +31,7 @@ export const FollowUpSuggest = ({
const { autoApprovalEnabled, alwaysAllowFollowupQuestions, followupAutoApproveTimeoutMs } = useExtensionState()
const [countdown, setCountdown] = useState(null)
const [suggestionSelected, setSuggestionSelected] = useState(false)
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
// The model may emit suggestions with missing or blank answers. Those render
// as empty buttons, and clicking "Copy to input" on one pushes an undefined
diff --git a/webview-ui/src/components/chat/IndexingStatusBadge.tsx b/webview-ui/src/components/chat/IndexingStatusBadge.tsx
index 22dbe8f9d5..f0a7df24a0 100644
--- a/webview-ui/src/components/chat/IndexingStatusBadge.tsx
+++ b/webview-ui/src/components/chat/IndexingStatusBadge.tsx
@@ -5,7 +5,7 @@ import type { IndexingStatus, IndexingStatusUpdateMessage } from "@roo-code/type
import { cn } from "@src/lib/utils"
import { vscode } from "@src/utils/vscode"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { PopoverTrigger, StandardTooltip, Button } from "@src/components/ui"
@@ -18,7 +18,7 @@ interface IndexingStatusBadgeProps {
}
export const IndexingStatusBadge: React.FC = ({ className }) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const { cwd } = useExtensionState()
const [indexingStatus, setIndexingStatus] = useState({
diff --git a/webview-ui/src/components/chat/MessageModificationConfirmationDialog.tsx b/webview-ui/src/components/chat/MessageModificationConfirmationDialog.tsx
index 883a34f3f3..f5e03e384a 100644
--- a/webview-ui/src/components/chat/MessageModificationConfirmationDialog.tsx
+++ b/webview-ui/src/components/chat/MessageModificationConfirmationDialog.tsx
@@ -1,5 +1,5 @@
import React from "react"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import {
AlertDialog,
AlertDialogAction,
@@ -24,7 +24,7 @@ export const MessageModificationConfirmationDialog: React.FC {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const isEdit = type === "edit"
const title = isEdit ? t("common:confirmation.editMessage") : t("common:confirmation.deleteMessage")
diff --git a/webview-ui/src/components/chat/ModeSelector.tsx b/webview-ui/src/components/chat/ModeSelector.tsx
index 9e6a5a3a76..a7e07a6402 100644
--- a/webview-ui/src/components/chat/ModeSelector.tsx
+++ b/webview-ui/src/components/chat/ModeSelector.tsx
@@ -11,7 +11,7 @@ import { telemetryClient } from "@/utils/TelemetryClient"
import { cn } from "@/lib/utils"
import { enabledSelectorTriggerClassName, selectorTriggerClassName } from "@/components/ui/selectorTriggerStyles"
import { useExtensionState } from "@/context/ExtensionStateContext"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui"
@@ -50,7 +50,7 @@ export const ModeSelector = ({
const lastNotifiedInvalidModeRef = React.useRef(null)
const portalContainer = useRooPortal("roo-portal")
const { hasOpenedModeSelector, setHasOpenedModeSelector } = useExtensionState()
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const trackModeSelectorOpened = React.useCallback(() => {
// Track telemetry every time the mode selector is opened.
diff --git a/webview-ui/src/components/chat/ModelSelector.tsx b/webview-ui/src/components/chat/ModelSelector.tsx
index a42e4c9d2a..35f22718d5 100644
--- a/webview-ui/src/components/chat/ModelSelector.tsx
+++ b/webview-ui/src/components/chat/ModelSelector.tsx
@@ -16,7 +16,7 @@ import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
import { useRouterModels } from "@/components/ui/hooks/useRouterModels"
import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel"
import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { vscode } from "@/utils/vscode"
import { useExtensionState } from "@/context/ExtensionStateContext"
import {
@@ -87,7 +87,7 @@ export const ModelSelector = ({
const [searchValue, setSearchValue] = React.useState("")
const searchInputRef = React.useRef(null)
const portalContainer = useRooPortal("roo-portal")
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const { organizationAllowList, currentApiConfigName } = useExtensionState()
const providerRaw = apiConfiguration?.apiProvider || providerIdentifiers.openrouter
diff --git a/webview-ui/src/components/chat/ProfileViolationWarning.tsx b/webview-ui/src/components/chat/ProfileViolationWarning.tsx
index fdaaa1bb64..db39a97c76 100644
--- a/webview-ui/src/components/chat/ProfileViolationWarning.tsx
+++ b/webview-ui/src/components/chat/ProfileViolationWarning.tsx
@@ -1,8 +1,8 @@
import React from "react"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
const ProfileViolationWarning: React.FC = () => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
return (
diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx
index c3d0594b99..bccfb22f42 100644
--- a/webview-ui/src/components/chat/TaskHeader.tsx
+++ b/webview-ui/src/components/chat/TaskHeader.tsx
@@ -234,8 +234,8 @@ const TaskHeader = ({
{t("chat:tokenProgress.tokensUsedLabel")}
- {formatLargeNumber(contextTokens || 0)} /{" "}
- {formatLargeNumber(contextWindow)}
+ {formatLargeNumber(contextTokens || 0, t)} /{" "}
+ {formatLargeNumber(contextWindow, t)}
{reservedForOutput > 0 && (
@@ -244,7 +244,7 @@ const TaskHeader = ({
{t("chat:tokenProgress.reservedForResponseLabel")}
- {formatLargeNumber(reservedForOutput)}
+ {formatLargeNumber(reservedForOutput, t)}
)}
@@ -254,7 +254,7 @@ const TaskHeader = ({
{t("chat:tokenProgress.availableSpaceLabel")}
- {formatLargeNumber(availableSpace)}
+ {formatLargeNumber(availableSpace, t)}
)}
@@ -389,10 +389,10 @@ const TaskHeader = ({
{typeof tokensIn === "number" && tokensIn > 0 && (
- ↑ {formatLargeNumber(tokensIn)}
+ ↑ {formatLargeNumber(tokensIn, t)}
)}
{typeof tokensOut === "number" && tokensOut > 0 && (
- ↓ {formatLargeNumber(tokensOut)}
+ ↓ {formatLargeNumber(tokensOut, t)}
)}
@@ -409,13 +409,13 @@ const TaskHeader = ({
{typeof cacheWrites === "number" && cacheWrites > 0 && (
<>
-
{formatLargeNumber(cacheWrites)}
+
{formatLargeNumber(cacheWrites, t)}
>
)}
{typeof cacheReads === "number" && cacheReads > 0 && (
<>
-
{formatLargeNumber(cacheReads)}
+
{formatLargeNumber(cacheReads, t)}
>
)}
diff --git a/webview-ui/src/components/chat/TodoChangeDisplay.tsx b/webview-ui/src/components/chat/TodoChangeDisplay.tsx
index 3904cd9c9f..d9c25ec04f 100644
--- a/webview-ui/src/components/chat/TodoChangeDisplay.tsx
+++ b/webview-ui/src/components/chat/TodoChangeDisplay.tsx
@@ -1,4 +1,4 @@
-import { t } from "i18next"
+import { useTranslation } from "react-i18next"
import { ArrowRight, Check, ListChecks, SquareDashed } from "lucide-react"
type TodoStatus = "completed" | "in_progress" | "pending"
@@ -26,6 +26,7 @@ function getTodoIcon(status: TodoStatus | null) {
}
export function TodoChangeDisplay({ previousTodos, newTodos }: TodoChangeDisplayProps) {
+ const { t } = useTranslation()
const isInitialState = previousTodos.length === 0
// Determine which todos to display
diff --git a/webview-ui/src/components/chat/TodoListDisplay.tsx b/webview-ui/src/components/chat/TodoListDisplay.tsx
index f2dbbc4d80..17447c1551 100644
--- a/webview-ui/src/components/chat/TodoListDisplay.tsx
+++ b/webview-ui/src/components/chat/TodoListDisplay.tsx
@@ -1,5 +1,5 @@
import { cn } from "@/lib/utils"
-import { t } from "i18next"
+import { useTranslation } from "react-i18next"
import { ArrowRight, Check, ListChecks, SquareDashed } from "lucide-react"
import { useState, useRef, useMemo, useEffect } from "react"
@@ -18,6 +18,7 @@ function getTodoIcon(status: TodoStatus | null) {
export function TodoListDisplay({ todos }: { todos: any[] }) {
const [isCollapsed, setIsCollapsed] = useState(true)
+ const { t } = useTranslation()
const ulRef = useRef(null)
const itemRefs = useRef<(HTMLLIElement | null)[]>([])
const scrollIndex = useMemo(() => {
diff --git a/webview-ui/src/components/chat/WarningRow.tsx b/webview-ui/src/components/chat/WarningRow.tsx
index 4f18f6a88b..60da153859 100644
--- a/webview-ui/src/components/chat/WarningRow.tsx
+++ b/webview-ui/src/components/chat/WarningRow.tsx
@@ -1,6 +1,6 @@
import React from "react"
import { TriangleAlert, BookOpenText } from "lucide-react"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { vscode } from "@src/utils/vscode"
export interface WarningRowProps {
@@ -31,7 +31,7 @@ export interface WarningRowProps {
* />
*/
const WarningRow: React.FC = ({ title, message, docsURL, actionText, onAction }) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
return (
diff --git a/webview-ui/src/components/chat/WorktreeSelector.tsx b/webview-ui/src/components/chat/WorktreeSelector.tsx
index 0908516d32..cb7294c47f 100644
--- a/webview-ui/src/components/chat/WorktreeSelector.tsx
+++ b/webview-ui/src/components/chat/WorktreeSelector.tsx
@@ -6,7 +6,7 @@ import type { Worktree, WorktreeListResponse } from "@roo-code/types"
import { cn } from "@/lib/utils"
import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
import { Popover, PopoverContent, PopoverTrigger, StandardTooltip, Button } from "@/components/ui"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { vscode } from "@/utils/vscode"
import { CreateWorktreeModal } from "../worktrees/CreateWorktreeModal"
@@ -18,7 +18,7 @@ interface WorktreeSelectorProps {
}
export const WorktreeSelector = ({ disabled = false }: WorktreeSelectorProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [open, setOpen] = useState(false)
const [worktrees, setWorktrees] = useState
([])
const [isGitRepo, setIsGitRepo] = useState(true)
diff --git a/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx b/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx
index cb5c6cf69c..a5628981f5 100644
--- a/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx
+++ b/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx
@@ -19,17 +19,7 @@ vi.mock("@roo/package", () => ({
}))
vi.mock("react-i18next", () => ({
- Trans: ({ i18nKey, components }: { i18nKey: string; components?: Record }) => {
- if (i18nKey === "chat:announcement.support" && components?.githubLink) {
- return React.cloneElement(components.githubLink, undefined, "GitHub")
- }
-
- return {i18nKey}
- },
-}))
-
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+ useTranslation: () => ({
t: (key: string, options?: { version?: string }) => {
const translations: Record = {
"chat:announcement.release.heading": "What is new in Zoo Code v3.90.0",
@@ -48,6 +38,13 @@ vi.mock("@src/i18n/TranslationContext", () => ({
return translations[key] ?? key
},
}),
+ Trans: ({ i18nKey, components }: { i18nKey: string; components?: Record }) => {
+ if (i18nKey === "chat:announcement.support" && components?.githubLink) {
+ return React.cloneElement(components.githubLink, undefined, "GitHub")
+ }
+
+ return {i18nKey}
+ },
}))
describe("Announcement", () => {
diff --git a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx
index 420249b365..1d9731d66b 100644
--- a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx
+++ b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx
@@ -11,8 +11,8 @@ vi.mock("@/utils/vscode", () => ({
},
}))
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/chat/__tests__/AutoApproveDropdown.spec.tsx b/webview-ui/src/components/chat/__tests__/AutoApproveDropdown.spec.tsx
index 113a088138..b58eaa4411 100644
--- a/webview-ui/src/components/chat/__tests__/AutoApproveDropdown.spec.tsx
+++ b/webview-ui/src/components/chat/__tests__/AutoApproveDropdown.spec.tsx
@@ -19,8 +19,8 @@ vi.mock("@/context/ExtensionStateContext", () => ({
}),
}))
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({ t: (key: string) => key }),
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({ t: (key: string) => key }),
}))
vi.mock("@/hooks/useAutoApprovalToggles", () => ({
diff --git a/webview-ui/src/components/chat/__tests__/BatchFilePermission.spec.tsx b/webview-ui/src/components/chat/__tests__/BatchFilePermission.spec.tsx
index 6b2a290c63..9ad0644dc2 100644
--- a/webview-ui/src/components/chat/__tests__/BatchFilePermission.spec.tsx
+++ b/webview-ui/src/components/chat/__tests__/BatchFilePermission.spec.tsx
@@ -1,7 +1,5 @@
import { render, screen, fireEvent } from "@/utils/test-utils"
-import { TranslationProvider } from "@/i18n/__mocks__/TranslationContext"
-
import { BatchFilePermission } from "../BatchFilePermission"
const mockVscodePostMessage = vi.fn()
@@ -45,13 +43,7 @@ describe("BatchFilePermission", () => {
it("renders file list correctly", () => {
render(
-
-
- ,
+ ,
)
// Check that all files are rendered
@@ -67,9 +59,7 @@ describe("BatchFilePermission", () => {
it("renders nothing when files array is empty", () => {
const { container } = render(
-
-
- ,
+ ,
)
expect(container.firstChild).toBeNull()
@@ -77,9 +67,7 @@ describe("BatchFilePermission", () => {
it("renders nothing when onPermissionResponse is not provided", () => {
const { container } = render(
-
-
- ,
+ ,
)
expect(container.firstChild).toBeNull()
@@ -87,13 +75,7 @@ describe("BatchFilePermission", () => {
it("opens file when clicking on file item", () => {
render(
-
-
- ,
+ ,
)
// The onClick is on the ToolUseBlockHeader which contains the file path text
@@ -123,13 +105,11 @@ describe("BatchFilePermission", () => {
]
render(
-
-
- ,
+ ,
)
// Should render dot before the path
@@ -139,20 +119,14 @@ describe("BatchFilePermission", () => {
it("re-renders when timestamp changes", () => {
const { rerender } = render(
-
-
- ,
+ ,
)
// Initial render
expect(screen.getByText(/Button\.tsx/)).toBeInTheDocument()
// Re-render with new timestamp
- rerender(
-
-
- ,
- )
+ rerender( )
// Should still show files
expect(screen.getByText(/Button\.tsx/)).toBeInTheDocument()
@@ -160,13 +134,7 @@ describe("BatchFilePermission", () => {
it("displays external link icon for all files", () => {
render(
-
-
- ,
+ ,
)
// All files should have external link icons
diff --git a/webview-ui/src/components/chat/__tests__/ChatView.language-switch.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatView.language-switch.spec.tsx
new file mode 100644
index 0000000000..86a25cf509
--- /dev/null
+++ b/webview-ui/src/components/chat/__tests__/ChatView.language-switch.spec.tsx
@@ -0,0 +1,111 @@
+// pnpm --filter @roo-code/vscode-webview test src/components/chat/__tests__/ChatView.language-switch.spec.tsx
+
+import React from "react"
+import { renderWithExtensionState, screen, waitFor, act } from "@/utils/test-utils"
+
+import i18next from "@src/i18n/setup"
+
+import ChatView, { ChatViewProps } from "../ChatView"
+
+interface ClineMessage {
+ type: "say" | "ask"
+ say?: string
+ ask?: string
+ ts: number
+ text?: string
+ partial?: boolean
+}
+
+vi.mock("@src/utils/vscode", () => ({
+ vscode: {
+ postMessage: vi.fn(),
+ },
+}))
+
+vi.mock("use-sound", () => ({
+ default: vi.fn().mockImplementation(() => [vi.fn()]),
+}))
+
+vi.mock("../ChatRow", () => ({
+ default: () => null,
+}))
+
+vi.mock("../AutoApproveMenu", () => ({
+ default: () => null,
+}))
+
+vi.mock("react-virtuoso", () => ({
+ Virtuoso: () => null,
+}))
+
+vi.mock("../../common/VersionIndicator", () => ({
+ default: vi.fn(() => null),
+}))
+
+vi.mock("@src/components/welcome/RooTips", () => ({
+ default: () => null,
+}))
+
+vi.mock("@src/components/welcome/RooHero", () => ({
+ default: () => null,
+}))
+
+vi.mock("../../common/TelemetryBanner", () => ({
+ default: () => null,
+}))
+
+const commandAsk: ClineMessage[] = [
+ { type: "say", say: "task", ts: 1, text: "Initial task" },
+ { type: "ask", ask: "command", ts: 2, text: "echo hi", partial: false },
+]
+
+const postState = (state: Record) =>
+ act(async () => {
+ window.postMessage(
+ {
+ type: "state",
+ state: {
+ version: "1.0.0",
+ clineMessages: commandAsk,
+ taskHistory: [],
+ shouldShowAnnouncement: false,
+ allowedCommands: [],
+ alwaysAllowExecute: false,
+ cloudIsAuthenticated: false,
+ telemetrySetting: "enabled",
+ ...state,
+ },
+ },
+ "*",
+ )
+ })
+
+const defaultProps: ChatViewProps = {
+ isHidden: false,
+ showAnnouncement: false,
+ hideAnnouncement: () => {},
+}
+
+describe("ChatView approval buttons on language change", () => {
+ beforeEach(async () => {
+ await i18next.changeLanguage("en")
+ })
+
+ it("relabels an open prompt's buttons in the new language", async () => {
+ renderWithExtensionState( )
+
+ await postState({ language: "en" })
+ await waitFor(() => {
+ expect(screen.getByRole("button", { name: "Run" })).toBeInTheDocument()
+ expect(screen.getByRole("button", { name: "Deny" })).toBeInTheDocument()
+ })
+
+ // Same messages, so only the language changes; the approval-label effect does not rerun.
+ await postState({ language: "es" })
+ await waitFor(() => {
+ expect(screen.getByRole("button", { name: "Comando" })).toBeInTheDocument()
+ expect(screen.getByRole("button", { name: "Rechazar" })).toBeInTheDocument()
+ })
+ expect(screen.queryByRole("button", { name: "Run" })).not.toBeInTheDocument()
+ })
+})
diff --git a/webview-ui/src/components/chat/__tests__/CheckpointRestoreDialog.spec.tsx b/webview-ui/src/components/chat/__tests__/CheckpointRestoreDialog.spec.tsx
index fc1cb4a77f..e1f718f832 100644
--- a/webview-ui/src/components/chat/__tests__/CheckpointRestoreDialog.spec.tsx
+++ b/webview-ui/src/components/chat/__tests__/CheckpointRestoreDialog.spec.tsx
@@ -6,9 +6,9 @@ import { vi } from "vitest"
import { CheckpointRestoreDialog } from "../CheckpointRestoreDialog"
-// Mock the translation context
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+// Mock the translation hook
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => {
const translations: Record = {
"common:confirmation.deleteMessage": "Delete Message",
diff --git a/webview-ui/src/components/chat/__tests__/CommandExecution.spec.tsx b/webview-ui/src/components/chat/__tests__/CommandExecution.spec.tsx
index 910d903a43..0301181a2d 100644
--- a/webview-ui/src/components/chat/__tests__/CommandExecution.spec.tsx
+++ b/webview-ui/src/components/chat/__tests__/CommandExecution.spec.tsx
@@ -5,6 +5,7 @@ import { render, screen, fireEvent, act } from "@testing-library/react"
import { CommandExecution } from "../CommandExecution"
import { ExtensionStateContext } from "../../../context/ExtensionStateContext"
+import { TooltipProvider } from "../../ui/tooltip"
// Mock dependencies
vi.mock("react-use", () => ({
@@ -51,8 +52,11 @@ const mockExtensionState = {
setDeniedCommands: vi.fn(),
}
+// The status tooltips need a TooltipProvider now that their labels resolve through useTranslation.
const ExtensionStateWrapper = ({ children }: { children: React.ReactNode }) => (
- {children}
+
+ {children}
+
)
describe("CommandExecution", () => {
diff --git a/webview-ui/src/components/chat/__tests__/FollowUpSuggest.spec.tsx b/webview-ui/src/components/chat/__tests__/FollowUpSuggest.spec.tsx
index 9f437d685b..b275946e33 100644
--- a/webview-ui/src/components/chat/__tests__/FollowUpSuggest.spec.tsx
+++ b/webview-ui/src/components/chat/__tests__/FollowUpSuggest.spec.tsx
@@ -6,9 +6,8 @@ import type { SuggestionItem } from "@roo-code/types"
import { FollowUpSuggest } from "../FollowUpSuggest"
// Mock the translation hook
-vi.mock("@src/i18n/TranslationContext", () => ({
- TranslationProvider: ({ children }: { children: React.ReactNode }) => children,
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string, options?: any) => {
if (key === "chat:followUpSuggest.countdownDisplay" && options?.count !== undefined) {
return `${options.count}s`
diff --git a/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx b/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx
index 25f92757f8..1c8acab519 100644
--- a/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx
+++ b/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx
@@ -20,31 +20,27 @@ vi.mock("@/i18n/setup", () => ({
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string, params?: any) => {
+ // Remove namespace prefix if present
+ const cleanKey = key.includes(":") ? key.split(":")[1] : key
+
const translations: Record = {
"indexingStatus.ready": "Index ready",
- "indexingStatus.indexing": params?.progress !== undefined ? `Indexing ${params.progress}%` : "Indexing",
+ "indexingStatus.indexing":
+ params?.percentage !== undefined ? `Indexing ${params.percentage}%` : "Indexing",
"indexingStatus.error": "Index error",
"indexingStatus.indexed": "Indexed",
"indexingStatus.tooltip.ready": "The codebase index is ready for use",
"indexingStatus.tooltip.indexing":
- params?.progress !== undefined
- ? `Indexing in progress: ${params.progress}% complete`
+ params?.percentage !== undefined
+ ? `Indexing in progress: ${params.percentage}% complete`
: "Indexing in progress",
"indexingStatus.tooltip.error": "An error occurred during indexing",
"indexingStatus.tooltip.indexed": "Codebase has been successfully indexed",
"indexingStatus.tooltip.clickToSettings": "Click to open indexing settings",
}
- return translations[key] || key
- },
- i18n: {
- language: "en",
- changeLanguage: vi.fn(),
+ return translations[cleanKey] || cleanKey
},
}),
- initReactI18next: {
- type: "3rdParty",
- init: vi.fn(),
- },
Trans: ({ children }: { children: React.ReactNode }) => <>{children}>,
}))
@@ -67,33 +63,6 @@ vi.mock("@/context/ExtensionStateContext", () => ({
ExtensionStateContextProvider: ({ children }: { children: React.ReactNode }) => <>{children}>,
}))
-// Mock TranslationContext to provide t function directly
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
- t: (key: string, params?: any) => {
- // Remove namespace prefix if present
- const cleanKey = key.includes(":") ? key.split(":")[1] : key
-
- const translations: Record = {
- "indexingStatus.ready": "Index ready",
- "indexingStatus.indexing":
- params?.percentage !== undefined ? `Indexing ${params.percentage}%` : "Indexing",
- "indexingStatus.error": "Index error",
- "indexingStatus.indexed": "Indexed",
- "indexingStatus.tooltip.ready": "The codebase index is ready for use",
- "indexingStatus.tooltip.indexing":
- params?.percentage !== undefined
- ? `Indexing in progress: ${params.percentage}% complete`
- : "Indexing in progress",
- "indexingStatus.tooltip.error": "An error occurred during indexing",
- "indexingStatus.tooltip.indexed": "Codebase has been successfully indexed",
- "indexingStatus.tooltip.clickToSettings": "Click to open indexing settings",
- }
- return translations[cleanKey] || cleanKey
- },
- }),
-}))
-
describe("IndexingStatusBadge", () => {
const renderComponent = (props = {}) => {
return render( )
diff --git a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx
index ac8c324efd..aaa3593758 100644
--- a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx
+++ b/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx
@@ -19,8 +19,8 @@ vi.mock("@/context/ExtensionStateContext", () => ({
}),
}))
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx
index d4dd381015..391225fa5e 100644
--- a/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx
+++ b/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx
@@ -19,8 +19,8 @@ vi.mock("@/context/ExtensionStateContext", () => ({
useExtensionState: () => extensionState,
}))
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/chat/__tests__/ThemeSensitiveStatus.visual.fixture.tsx b/webview-ui/src/components/chat/__tests__/ThemeSensitiveStatus.visual.fixture.tsx
index ed239f719c..22bc228b65 100644
--- a/webview-ui/src/components/chat/__tests__/ThemeSensitiveStatus.visual.fixture.tsx
+++ b/webview-ui/src/components/chat/__tests__/ThemeSensitiveStatus.visual.fixture.tsx
@@ -1,43 +1,48 @@
import React from "react"
+import i18next from "@/i18n/setup"
+import { I18nextProvider } from "react-i18next"
+
import McpToolRow from "../../mcp/McpToolRow"
import { CommandPatternSelector } from "../CommandPatternSelector"
export function ThemeSensitiveStatusFixture() {
return (
-
-
-
- Command permissions
-
- undefined}
- onDenyPatternChange={() => undefined}
- />
-
+
+
+
+
+ Command permissions
+
+ undefined}
+ onDenyPatternChange={() => undefined}
+ />
+
-
-
+
+
+
)
}
diff --git a/webview-ui/src/components/chat/__tests__/WarningRow.spec.tsx b/webview-ui/src/components/chat/__tests__/WarningRow.spec.tsx
index 1bcf5d771a..d781a9a7fe 100644
--- a/webview-ui/src/components/chat/__tests__/WarningRow.spec.tsx
+++ b/webview-ui/src/components/chat/__tests__/WarningRow.spec.tsx
@@ -10,9 +10,9 @@ vi.mock("@/utils/vscode", () => ({
},
}))
-// Mock i18n TranslationContext
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+// Mock the translation hook
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => {
const map: Record = {
"chat:apiRequest.errorMessage.docs": "Docs",
diff --git a/webview-ui/src/components/chat/__tests__/WorktreeSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/WorktreeSelector.spec.tsx
index 106e23ad0f..315f1ca6e2 100644
--- a/webview-ui/src/components/chat/__tests__/WorktreeSelector.spec.tsx
+++ b/webview-ui/src/components/chat/__tests__/WorktreeSelector.spec.tsx
@@ -12,8 +12,8 @@ vi.mock("@/utils/vscode", () => ({
},
}))
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/common/CodeBlock.tsx b/webview-ui/src/components/common/CodeBlock.tsx
index c9c5d85aa0..9ed99dc751 100644
--- a/webview-ui/src/components/common/CodeBlock.tsx
+++ b/webview-ui/src/components/common/CodeBlock.tsx
@@ -6,7 +6,7 @@ import type { BundledTheme, ShikiTransformer } from "shiki"
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
import { Fragment, jsx, jsxs } from "react/jsx-runtime"
import { ChevronDown, ChevronUp, Copy, Check } from "lucide-react"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { StandardTooltip } from "@/components/ui"
export const CODE_BLOCK_BG_COLOR =
@@ -194,7 +194,7 @@ const CodeBlock = memo(
const preRef = useRef(null)
const copyButtonWrapperRef = useRef(null)
const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard()
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const isMountedRef = useRef(true)
const buttonPositionTimeoutRef = useRef(null)
const collapseTimeout1Ref = useRef(null)
diff --git a/webview-ui/src/components/common/ImageViewer.tsx b/webview-ui/src/components/common/ImageViewer.tsx
index 6c2832d050..56b1d66d0c 100644
--- a/webview-ui/src/components/common/ImageViewer.tsx
+++ b/webview-ui/src/components/common/ImageViewer.tsx
@@ -1,6 +1,6 @@
import { useState, useCallback } from "react"
import { useCopyToClipboard } from "@src/utils/clipboard"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { vscode } from "@src/utils/vscode"
import { MermaidActionButtons } from "./MermaidActionButtons"
import { Modal } from "./Modal"
@@ -35,7 +35,7 @@ export function ImageViewer({
const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 })
const [imageError, setImageError] = useState(null)
const { copyWithFeedback } = useCopyToClipboard()
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
/**
* Opens a modal with the image for zooming
diff --git a/webview-ui/src/components/common/MermaidActionButtons.tsx b/webview-ui/src/components/common/MermaidActionButtons.tsx
index 79558b9b03..64da63d1af 100644
--- a/webview-ui/src/components/common/MermaidActionButtons.tsx
+++ b/webview-ui/src/components/common/MermaidActionButtons.tsx
@@ -1,5 +1,5 @@
import React from "react"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { IconButton } from "./IconButton"
import { ZoomControls } from "./ZoomControls"
import { StandardTooltip } from "@/components/ui"
@@ -29,7 +29,7 @@ export const MermaidActionButtons: React.FC = ({
showZoomControls = false,
zoomLevel,
}) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
if (showZoomControls && onZoomOut && onZoomIn && zoomLevel !== undefined) {
return (
diff --git a/webview-ui/src/components/common/MermaidBlock.tsx b/webview-ui/src/components/common/MermaidBlock.tsx
index 346fe3c471..d673962cd2 100644
--- a/webview-ui/src/components/common/MermaidBlock.tsx
+++ b/webview-ui/src/components/common/MermaidBlock.tsx
@@ -3,7 +3,7 @@ import mermaid from "mermaid"
import styled from "styled-components"
import { useDebounceEffect } from "@src/utils/useDebounceEffect"
import { vscode } from "@src/utils/vscode"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { useCopyToClipboard } from "@src/utils/clipboard"
import CodeBlock from "./CodeBlock"
import { MermaidButton } from "@/components/common/MermaidButton"
@@ -21,7 +21,7 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
const renderVersionRef = useRef(0)
const theme = useMermaidTheme()
const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard()
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
// Whenever the source or host theme changes, invalidate in-flight rendering.
useEffect(() => {
diff --git a/webview-ui/src/components/common/MermaidButton.tsx b/webview-ui/src/components/common/MermaidButton.tsx
index 8d77502c6d..d2297aa9f0 100644
--- a/webview-ui/src/components/common/MermaidButton.tsx
+++ b/webview-ui/src/components/common/MermaidButton.tsx
@@ -1,6 +1,6 @@
import { useState, useCallback } from "react"
import { useCopyToClipboard } from "@src/utils/clipboard"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { vscode } from "@src/utils/vscode"
import { MermaidActionButtons } from "./MermaidActionButtons"
import { Modal } from "./Modal"
@@ -29,7 +29,7 @@ export function MermaidButton({ containerRef, code, isLoading, svgToPng, childre
const [isDragging, setIsDragging] = useState(false)
const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 })
const { copyWithFeedback } = useCopyToClipboard()
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
/**
* Opens a modal with the diagram for zooming
diff --git a/webview-ui/src/components/common/TelemetryBanner.tsx b/webview-ui/src/components/common/TelemetryBanner.tsx
index 6ed058d8bf..6d5d84073f 100644
--- a/webview-ui/src/components/common/TelemetryBanner.tsx
+++ b/webview-ui/src/components/common/TelemetryBanner.tsx
@@ -1,14 +1,13 @@
import { memo } from "react"
-import { Trans } from "react-i18next"
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import type { TelemetrySetting } from "@roo-code/types"
import { vscode } from "@src/utils/vscode"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { Trans, useTranslation } from "react-i18next"
const TelemetryBanner = () => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const handleAccept = () => {
vscode.postMessage({ type: "telemetrySetting", text: "enabled" satisfies TelemetrySetting })
@@ -31,6 +30,7 @@ const TelemetryBanner = () => {
{t("welcome:telemetry.helpImprove")}
,
diff --git a/webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx b/webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx
index 1d6b0fc061..93eb58b934 100644
--- a/webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx
+++ b/webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx
@@ -5,9 +5,9 @@ import { render, screen, fireEvent, act, waitFor } from "@/utils/test-utils"
import CodeBlock, { getCodeBlockTheme } from "../CodeBlock"
import { getHighlighter } from "../../../utils/highlighter"
-// Mock the translation context
-vi.mock("../../../i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+// Mock the translation hook
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => {
// Return fixed English strings for tests
const translations: { [key: string]: string } = {
diff --git a/webview-ui/src/components/common/__tests__/MermaidBlock.spec.tsx b/webview-ui/src/components/common/__tests__/MermaidBlock.spec.tsx
index 428e3ae306..171bdcdf92 100644
--- a/webview-ui/src/components/common/__tests__/MermaidBlock.spec.tsx
+++ b/webview-ui/src/components/common/__tests__/MermaidBlock.spec.tsx
@@ -17,8 +17,8 @@ vi.mock("mermaid", () => ({
},
}))
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({ t: (key: string) => key }),
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({ t: (key: string) => key }),
}))
vi.mock("@src/utils/clipboard", () => ({
diff --git a/webview-ui/src/components/common/__tests__/TelemetryBanner.spec.tsx b/webview-ui/src/components/common/__tests__/TelemetryBanner.spec.tsx
index ddffaff20a..a8ecf634d2 100644
--- a/webview-ui/src/components/common/__tests__/TelemetryBanner.spec.tsx
+++ b/webview-ui/src/components/common/__tests__/TelemetryBanner.spec.tsx
@@ -10,8 +10,9 @@ vi.mock("@src/utils/vscode", () => ({
},
}))
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", async (importOriginal) => ({
+ ...(await importOriginal
()),
+ useTranslation: () => ({
t: (key: string) => {
const translations: Record = {
"welcome:telemetry.helpImprove": "Help Improve Zoo Code",
diff --git a/webview-ui/src/components/common/__tests__/TelemetryBanner.visual.fixture.tsx b/webview-ui/src/components/common/__tests__/TelemetryBanner.visual.fixture.tsx
index 0df2d5b226..e243cb51e0 100644
--- a/webview-ui/src/components/common/__tests__/TelemetryBanner.visual.fixture.tsx
+++ b/webview-ui/src/components/common/__tests__/TelemetryBanner.visual.fixture.tsx
@@ -1,18 +1,11 @@
import React from "react"
import { I18nextProvider } from "react-i18next"
-import { TranslationContext } from "@src/i18n/TranslationContext"
import TelemetryBanner from "../TelemetryBanner"
-import { visualTestI18n, visualTestTranslations } from "./TelemetryBanner.visual.i18n"
+import { visualTestI18n } from "./TelemetryBanner.visual.i18n"
export const TelemetryBannerFixture = () => (
- visualTestTranslations[key] ?? key,
- i18n: null as unknown as typeof import("../../../i18n/setup").default,
- }}>
-
-
+
)
diff --git a/webview-ui/src/components/common/__tests__/TelemetryBanner.visual.i18n.ts b/webview-ui/src/components/common/__tests__/TelemetryBanner.visual.i18n.ts
index 95e7a8d1b6..ecf602d285 100644
--- a/webview-ui/src/components/common/__tests__/TelemetryBanner.visual.i18n.ts
+++ b/webview-ui/src/components/common/__tests__/TelemetryBanner.visual.i18n.ts
@@ -9,10 +9,9 @@ export const visualTestTranslations: Record = {
"welcome:telemetry.decline": "Decline",
}
-// Trans reads from its own react-i18next instance rather than the useAppTranslation
-// context, so it needs a real (if minimal) i18next init to resolve helpImproveMessage
-// and the settingsLink interpolation instead of rendering nothing. init() returns a
-// promise even for inline resources, so callers must await it before mounting.
+// The fixture passes this instance to I18nextProvider; TelemetryBanner's useTranslation and Trans
+// translate with it. init() returns a promise even for inline resources, so callers must await it
+// before mounting.
export const visualTestI18n = i18next.createInstance()
export const visualTestI18nReady = visualTestI18n.use(initReactI18next).init({
diff --git a/webview-ui/src/components/history/BatchDeleteTaskDialog.tsx b/webview-ui/src/components/history/BatchDeleteTaskDialog.tsx
index decc905315..0ae55114fe 100644
--- a/webview-ui/src/components/history/BatchDeleteTaskDialog.tsx
+++ b/webview-ui/src/components/history/BatchDeleteTaskDialog.tsx
@@ -1,5 +1,5 @@
import { useCallback } from "react"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import {
AlertDialog,
AlertDialogAction,
@@ -19,7 +19,7 @@ interface BatchDeleteTaskDialogProps extends AlertDialogProps {
}
export const BatchDeleteTaskDialog = ({ taskIds, ...props }: BatchDeleteTaskDialogProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const { onOpenChange } = props
const onDelete = useCallback(() => {
diff --git a/webview-ui/src/components/history/CopyButton.tsx b/webview-ui/src/components/history/CopyButton.tsx
index 4243ff8d5a..c71e90c725 100644
--- a/webview-ui/src/components/history/CopyButton.tsx
+++ b/webview-ui/src/components/history/CopyButton.tsx
@@ -2,7 +2,7 @@ import { useCallback } from "react"
import { useClipboard } from "@/components/ui/hooks"
import { Button, StandardTooltip } from "@/components/ui"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { cn } from "@/lib/utils"
type CopyButtonProps = {
@@ -11,7 +11,7 @@ type CopyButtonProps = {
export const CopyButton = ({ itemTask }: CopyButtonProps) => {
const { isCopied, copy } = useClipboard()
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const onCopy = useCallback(
(e: React.MouseEvent) => {
diff --git a/webview-ui/src/components/history/DeleteButton.tsx b/webview-ui/src/components/history/DeleteButton.tsx
index 8378887c70..775dc8ce81 100644
--- a/webview-ui/src/components/history/DeleteButton.tsx
+++ b/webview-ui/src/components/history/DeleteButton.tsx
@@ -1,7 +1,7 @@
import { useCallback } from "react"
import { Button, StandardTooltip } from "@/components/ui"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { vscode } from "@/utils/vscode"
type DeleteButtonProps = {
@@ -10,7 +10,7 @@ type DeleteButtonProps = {
}
export const DeleteButton = ({ itemId, onDelete }: DeleteButtonProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const handleDeleteClick = useCallback(
(e: React.MouseEvent) => {
diff --git a/webview-ui/src/components/history/DeleteTaskDialog.tsx b/webview-ui/src/components/history/DeleteTaskDialog.tsx
index 5ff93f4ed3..8fc19485d3 100644
--- a/webview-ui/src/components/history/DeleteTaskDialog.tsx
+++ b/webview-ui/src/components/history/DeleteTaskDialog.tsx
@@ -13,7 +13,7 @@ import {
AlertDialogTitle,
Button,
} from "@/components/ui"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { vscode } from "@/utils/vscode"
@@ -24,7 +24,7 @@ interface DeleteTaskDialogProps extends AlertDialogProps {
}
export const DeleteTaskDialog = ({ taskId, subtaskCount = 0, ...props }: DeleteTaskDialogProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [isEnterPressed] = useKeyPress("Enter")
const { onOpenChange } = props
diff --git a/webview-ui/src/components/history/ExportButton.tsx b/webview-ui/src/components/history/ExportButton.tsx
index fabc8d3d15..da49a6b15d 100644
--- a/webview-ui/src/components/history/ExportButton.tsx
+++ b/webview-ui/src/components/history/ExportButton.tsx
@@ -1,10 +1,10 @@
import { vscode } from "@/utils/vscode"
import { Button, StandardTooltip } from "@/components/ui"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { useCallback } from "react"
export const ExportButton = ({ itemId }: { itemId: string }) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const handleExportClick = useCallback(
(e: React.MouseEvent) => {
diff --git a/webview-ui/src/components/history/HistoryPreview.tsx b/webview-ui/src/components/history/HistoryPreview.tsx
index 70467c44fb..7543e99dc3 100644
--- a/webview-ui/src/components/history/HistoryPreview.tsx
+++ b/webview-ui/src/components/history/HistoryPreview.tsx
@@ -1,7 +1,7 @@
import { memo } from "react"
import { vscode } from "@src/utils/vscode"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { useTaskSearch } from "./useTaskSearch"
import { useGroupedTasks } from "./useGroupedTasks"
@@ -10,7 +10,7 @@ import TaskGroupItem from "./TaskGroupItem"
const HistoryPreview = () => {
const { tasks, searchQuery } = useTaskSearch()
const { groups, toggleExpand } = useGroupedTasks(tasks, searchQuery)
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const handleViewAllHistory = () => {
vscode.postMessage({ type: "switchTab", tab: "history" })
diff --git a/webview-ui/src/components/history/HistoryView.tsx b/webview-ui/src/components/history/HistoryView.tsx
index 54226ca40b..ab30436206 100644
--- a/webview-ui/src/components/history/HistoryView.tsx
+++ b/webview-ui/src/components/history/HistoryView.tsx
@@ -16,7 +16,7 @@ import {
SelectValue,
StandardTooltip,
} from "@/components/ui"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { Tab, TabContent, TabHeader } from "../common/Tab"
import { useTaskSearch } from "./useTaskSearch"
@@ -42,7 +42,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
showAllWorkspaces,
setShowAllWorkspaces,
} = useTaskSearch()
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
// Use grouped tasks hook
const { groups, flatTasks, toggleExpand, isSearchMode } = useGroupedTasks(tasks, searchQuery)
diff --git a/webview-ui/src/components/history/RenameButton.tsx b/webview-ui/src/components/history/RenameButton.tsx
index bd834525d2..c33535a7dc 100644
--- a/webview-ui/src/components/history/RenameButton.tsx
+++ b/webview-ui/src/components/history/RenameButton.tsx
@@ -1,12 +1,12 @@
import { Button, StandardTooltip } from "@/components/ui"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
type RenameButtonProps = {
onRename: () => void
}
export const RenameButton = ({ onRename }: RenameButtonProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const label = t("history:renameTask")
return (
diff --git a/webview-ui/src/components/history/SubtaskCollapsibleRow.tsx b/webview-ui/src/components/history/SubtaskCollapsibleRow.tsx
index 6e4c74c952..da17f24573 100644
--- a/webview-ui/src/components/history/SubtaskCollapsibleRow.tsx
+++ b/webview-ui/src/components/history/SubtaskCollapsibleRow.tsx
@@ -1,6 +1,6 @@
import { memo } from "react"
import { ChevronRight } from "lucide-react"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { cn } from "@/lib/utils"
interface SubtaskCollapsibleRowProps {
@@ -19,7 +19,7 @@ interface SubtaskCollapsibleRowProps {
* Clicking this row toggles the visibility of the subtask list.
*/
const SubtaskCollapsibleRow = ({ count, isExpanded, onToggle, className }: SubtaskCollapsibleRowProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
if (count === 0) {
return null
diff --git a/webview-ui/src/components/history/TaskItemFooter.tsx b/webview-ui/src/components/history/TaskItemFooter.tsx
index 5d75822f4c..1bfc35ee68 100644
--- a/webview-ui/src/components/history/TaskItemFooter.tsx
+++ b/webview-ui/src/components/history/TaskItemFooter.tsx
@@ -6,7 +6,7 @@ import { ExportButton } from "./ExportButton"
import { DeleteButton } from "./DeleteButton"
import { RenameButton } from "./RenameButton"
import { StandardTooltip } from "../ui/standard-tooltip"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { Split } from "lucide-react"
import { TaskStatusBadge } from "./TaskStatusBadge"
@@ -27,7 +27,7 @@ const TaskItemFooter: React.FC = ({
onDelete,
onRename,
}) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
return (
@@ -49,7 +49,7 @@ const TaskItemFooter: React.FC
= ({
)}
{/* Datetime with time-ago format */}
- {formatTimeAgo(item.ts)}
+ {formatTimeAgo(item.ts, t)}
{/* Cost */}
diff --git a/webview-ui/src/components/history/TaskStatusBadge.tsx b/webview-ui/src/components/history/TaskStatusBadge.tsx
index 5278f1422e..1d8951c4d1 100644
--- a/webview-ui/src/components/history/TaskStatusBadge.tsx
+++ b/webview-ui/src/components/history/TaskStatusBadge.tsx
@@ -1,4 +1,4 @@
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { cn } from "@/lib/utils"
import { StandardTooltip } from "../ui"
@@ -13,7 +13,7 @@ interface TaskStatusBadgeProps {
* can be resumed rather than silently detached. See #559.
*/
export const TaskStatusBadge = ({ status, className }: TaskStatusBadgeProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const isInterrupted = status === "interrupted"
const icon = isInterrupted ? "codicon-warning" : "codicon-sync"
diff --git a/webview-ui/src/components/history/TaskTitleEditor.tsx b/webview-ui/src/components/history/TaskTitleEditor.tsx
index c02e044311..67a10b0253 100644
--- a/webview-ui/src/components/history/TaskTitleEditor.tsx
+++ b/webview-ui/src/components/history/TaskTitleEditor.tsx
@@ -2,7 +2,7 @@ import { useCallback, useState } from "react"
import type { HistoryItem } from "@roo-code/types"
import { AutosizeTextarea } from "@/components/ui"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { cn } from "@/lib/utils"
import { vscode } from "@/utils/vscode"
@@ -24,7 +24,7 @@ const normalizeTitle = (text: string) => text.replace(/\s+/g, " ").trim()
* It wraps instead of scrolling sideways so a long title stays readable at narrow widths.
*/
export const TaskTitleEditor = ({ item, onDone, className }: TaskTitleEditorProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [value, setValue] = useState(() => normalizeTitle(getTaskDisplayTitle(item)))
const save = useCallback(() => {
diff --git a/webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.spec.tsx b/webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.spec.tsx
index bdcff23cdd..dc9bcd6765 100644
--- a/webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.spec.tsx
+++ b/webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.spec.tsx
@@ -6,8 +6,8 @@ import { BatchDeleteTaskDialog } from "../BatchDeleteTaskDialog"
vi.mock("@/utils/vscode")
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string, options?: Record) => {
const translations: Record = {
"history:deleteTasks": "Delete Tasks",
diff --git a/webview-ui/src/components/history/__tests__/CopyButton.spec.tsx b/webview-ui/src/components/history/__tests__/CopyButton.spec.tsx
index ac1b39859d..0e9a048ac8 100644
--- a/webview-ui/src/components/history/__tests__/CopyButton.spec.tsx
+++ b/webview-ui/src/components/history/__tests__/CopyButton.spec.tsx
@@ -5,8 +5,8 @@ import { useClipboard } from "@/components/ui/hooks"
import { CopyButton } from "../CopyButton"
vi.mock("@/components/ui/hooks")
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/history/__tests__/DeleteButton.spec.tsx b/webview-ui/src/components/history/__tests__/DeleteButton.spec.tsx
index 19b333ab44..7dbca11059 100644
--- a/webview-ui/src/components/history/__tests__/DeleteButton.spec.tsx
+++ b/webview-ui/src/components/history/__tests__/DeleteButton.spec.tsx
@@ -2,8 +2,8 @@ import { render, screen, fireEvent } from "@/utils/test-utils"
import { DeleteButton } from "../DeleteButton"
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/history/__tests__/DeleteTaskDialog.spec.tsx b/webview-ui/src/components/history/__tests__/DeleteTaskDialog.spec.tsx
index 1a43dbb4c0..d4a3762151 100644
--- a/webview-ui/src/components/history/__tests__/DeleteTaskDialog.spec.tsx
+++ b/webview-ui/src/components/history/__tests__/DeleteTaskDialog.spec.tsx
@@ -6,8 +6,8 @@ import { DeleteTaskDialog } from "../DeleteTaskDialog"
vi.mock("@/utils/vscode")
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string, options?: Record) => {
const translations: Record = {
"history:deleteTask": "Delete Task",
diff --git a/webview-ui/src/components/history/__tests__/ExportButton.spec.tsx b/webview-ui/src/components/history/__tests__/ExportButton.spec.tsx
index 1dda83305c..70752d2a34 100644
--- a/webview-ui/src/components/history/__tests__/ExportButton.spec.tsx
+++ b/webview-ui/src/components/history/__tests__/ExportButton.spec.tsx
@@ -6,8 +6,8 @@ import { ExportButton } from "../ExportButton"
vi.mock("@src/utils/vscode")
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/history/__tests__/HistoryView.spec.tsx b/webview-ui/src/components/history/__tests__/HistoryView.spec.tsx
index 3079844aad..f8f1ac9a59 100644
--- a/webview-ui/src/components/history/__tests__/HistoryView.spec.tsx
+++ b/webview-ui/src/components/history/__tests__/HistoryView.spec.tsx
@@ -7,8 +7,8 @@ import HistoryView from "../HistoryView"
vi.mock("@src/context/ExtensionStateContext")
vi.mock("@src/utils/vscode")
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/history/__tests__/RenameButton.spec.tsx b/webview-ui/src/components/history/__tests__/RenameButton.spec.tsx
index 2bc837ee12..873fbe6134 100644
--- a/webview-ui/src/components/history/__tests__/RenameButton.spec.tsx
+++ b/webview-ui/src/components/history/__tests__/RenameButton.spec.tsx
@@ -2,8 +2,8 @@ import { render, screen, fireEvent } from "@/utils/test-utils"
import { RenameButton } from "../RenameButton"
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/history/__tests__/SubtaskRow.spec.tsx b/webview-ui/src/components/history/__tests__/SubtaskRow.spec.tsx
index 4fb609081f..ef86b12b60 100644
--- a/webview-ui/src/components/history/__tests__/SubtaskRow.spec.tsx
+++ b/webview-ui/src/components/history/__tests__/SubtaskRow.spec.tsx
@@ -6,8 +6,8 @@ import SubtaskRow from "../SubtaskRow"
import type { SubtaskTreeNode, DisplayHistoryItem } from "../types"
vi.mock("@src/utils/vscode")
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string, options?: Record) => {
if (key === "history:subtasks" && options?.count !== undefined) {
return `${options.count} Subtask${options.count === 1 ? "" : "s"}`
diff --git a/webview-ui/src/components/history/__tests__/TaskGroupItem.spec.tsx b/webview-ui/src/components/history/__tests__/TaskGroupItem.spec.tsx
index b04fac6b54..99f55978d7 100644
--- a/webview-ui/src/components/history/__tests__/TaskGroupItem.spec.tsx
+++ b/webview-ui/src/components/history/__tests__/TaskGroupItem.spec.tsx
@@ -4,8 +4,8 @@ import TaskGroupItem from "../TaskGroupItem"
import type { TaskGroup, DisplayHistoryItem, SubtaskTreeNode } from "../types"
vi.mock("@src/utils/vscode")
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string, options?: Record) => {
if (key === "history:subtasks" && options?.count !== undefined) {
return `${options.count} Subtask${options.count === 1 ? "" : "s"}`
diff --git a/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx b/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx
index 4fb4a82d72..5bc178cc88 100644
--- a/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx
+++ b/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx
@@ -5,8 +5,8 @@ import { vscode } from "@src/utils/vscode"
import TaskItem from "../TaskItem"
vi.mock("@src/utils/vscode")
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx b/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx
index 83047dc0b4..acea93e5fe 100644
--- a/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx
+++ b/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx
@@ -2,8 +2,8 @@ import { render, screen } from "@/utils/test-utils"
import TaskItemFooter from "../TaskItemFooter"
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/history/__tests__/TaskTitleEditor.spec.tsx b/webview-ui/src/components/history/__tests__/TaskTitleEditor.spec.tsx
index 951dc85ac8..05d885b643 100644
--- a/webview-ui/src/components/history/__tests__/TaskTitleEditor.spec.tsx
+++ b/webview-ui/src/components/history/__tests__/TaskTitleEditor.spec.tsx
@@ -6,8 +6,8 @@ import { TaskTitleEditor } from "../TaskTitleEditor"
vi.mock("@src/utils/vscode")
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-dark.png b/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-dark.png
index 2bff5fbfaf..3850a8dd8c 100644
Binary files a/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-dark.png and b/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-dark.png differ
diff --git a/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-high-contrast-light.png b/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-high-contrast-light.png
index 1174b66e5b..ebc45d8371 100644
Binary files a/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-high-contrast-light.png and b/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-high-contrast-light.png differ
diff --git a/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-high-contrast.png b/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-high-contrast.png
index 75cbf52222..7590760d69 100644
Binary files a/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-high-contrast.png and b/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-high-contrast.png differ
diff --git a/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-light.png b/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-light.png
index 7190f6b6c8..db93d20137 100644
Binary files a/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-light.png and b/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-light.png differ
diff --git a/webview-ui/src/components/marketplace/IssueFooter.tsx b/webview-ui/src/components/marketplace/IssueFooter.tsx
index a40cefe5ad..53181531c5 100644
--- a/webview-ui/src/components/marketplace/IssueFooter.tsx
+++ b/webview-ui/src/components/marketplace/IssueFooter.tsx
@@ -1,13 +1,14 @@
import React from "react"
-import { Trans } from "react-i18next"
+import { Trans, useTranslation } from "react-i18next"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { EXTERNAL_LINKS } from "@/constants/externalLinks"
export const IssueFooter: React.FC = () => {
+ const { t } = useTranslation()
return (
-
+
Open a GitHub issue
diff --git a/webview-ui/src/components/marketplace/MarketplaceListView.tsx b/webview-ui/src/components/marketplace/MarketplaceListView.tsx
index c3c497ccfe..12249d8dc8 100644
--- a/webview-ui/src/components/marketplace/MarketplaceListView.tsx
+++ b/webview-ui/src/components/marketplace/MarketplaceListView.tsx
@@ -7,7 +7,7 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command
import { X, ChevronsUpDown } from "lucide-react"
import { MarketplaceItemCard } from "./components/MarketplaceItemCard"
import { MarketplaceViewStateManager } from "./MarketplaceViewStateManager"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { useStateManager } from "./useStateManager"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { IssueFooter } from "./IssueFooter"
@@ -21,7 +21,7 @@ export interface MarketplaceListViewProps {
export function MarketplaceListView({ stateManager, allTags, filteredTags, filterByType }: MarketplaceListViewProps) {
const [state, manager] = useStateManager(stateManager)
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const { marketplaceInstalledMetadata, cloudUserInfo } = useExtensionState()
const [isTagPopoverOpen, setIsTagPopoverOpen] = React.useState(false)
const [tagSearch, setTagSearch] = React.useState("")
diff --git a/webview-ui/src/components/marketplace/MarketplaceView.tsx b/webview-ui/src/components/marketplace/MarketplaceView.tsx
index 0ab5430eec..b0efb6d3dc 100644
--- a/webview-ui/src/components/marketplace/MarketplaceView.tsx
+++ b/webview-ui/src/components/marketplace/MarketplaceView.tsx
@@ -4,7 +4,7 @@ import { ArrowLeft } from "lucide-react"
import { Tab, TabContent, TabHeader } from "../common/Tab"
import { MarketplaceViewStateManager } from "./MarketplaceViewStateManager"
import { useStateManager } from "./useStateManager"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { vscode } from "@/utils/vscode"
import { MarketplaceListView } from "./MarketplaceListView"
import { cn } from "@/lib/utils"
@@ -17,7 +17,7 @@ interface MarketplaceViewProps {
targetTab?: "mcp" | "mode"
}
export function MarketplaceView({ stateManager, onDone, targetTab }: MarketplaceViewProps) {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [state, manager] = useStateManager(stateManager)
const [hasReceivedInitialState, setHasReceivedInitialState] = useState(false)
const extensionState = useContext(ExtensionStateContext)
diff --git a/webview-ui/src/components/marketplace/__tests__/IssueFooter.spec.tsx b/webview-ui/src/components/marketplace/__tests__/IssueFooter.spec.tsx
index ab1ed39860..b04139679c 100644
--- a/webview-ui/src/components/marketplace/__tests__/IssueFooter.spec.tsx
+++ b/webview-ui/src/components/marketplace/__tests__/IssueFooter.spec.tsx
@@ -14,6 +14,7 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({
}))
vi.mock("react-i18next", () => ({
+ useTranslation: () => ({ t: (key: string) => key }),
Trans: ({ children }: { children: React.ReactNode }) => <>{children}>,
}))
diff --git a/webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx b/webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx
index 02f94001d1..135fbcea76 100644
--- a/webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx
+++ b/webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx
@@ -9,8 +9,9 @@ import { ExtensionStateContextProvider } from "@/context/ExtensionStateContext"
import { MarketplaceListView } from "../MarketplaceListView"
import { ViewState } from "../MarketplaceViewStateManager"
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", async (importOriginal) => ({
+ ...(await importOriginal()),
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/marketplace/__tests__/MarketplaceView.spec.tsx b/webview-ui/src/components/marketplace/__tests__/MarketplaceView.spec.tsx
index 57fc62ece0..50ecc76bd8 100644
--- a/webview-ui/src/components/marketplace/__tests__/MarketplaceView.spec.tsx
+++ b/webview-ui/src/components/marketplace/__tests__/MarketplaceView.spec.tsx
@@ -13,8 +13,9 @@ vi.mock("@/utils/vscode", () => ({
},
}))
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", async (importOriginal) => ({
+ ...(await importOriginal()),
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx b/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx
index 7556630c7b..167916b414 100644
--- a/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx
+++ b/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx
@@ -1,7 +1,7 @@
import React, { useState, useMemo, useEffect } from "react"
import { MarketplaceItem, McpParameter, McpInstallationMethod } from "@roo-code/types"
import { vscode } from "@/utils/vscode"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import {
Dialog,
DialogContent,
@@ -27,7 +27,7 @@ export const MarketplaceInstallModal: React.FC = (
onClose,
hasWorkspace,
}) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [scope, setScope] = useState<"project" | "global">(hasWorkspace ? "project" : "global")
const [selectedMethodIndex, setSelectedMethodIndex] = useState(0)
const [parameterValues, setParameterValues] = useState>({})
diff --git a/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx b/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx
index 3b8df52176..ec2d4f53e7 100644
--- a/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx
+++ b/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx
@@ -3,7 +3,7 @@ import { MarketplaceItem, TelemetryEventName } from "@roo-code/types"
import { vscode } from "@/utils/vscode"
import { telemetryClient } from "@/utils/TelemetryClient"
import { ViewState } from "../MarketplaceViewStateManager"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { isValidUrl } from "../../../utils/url"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
@@ -36,7 +36,7 @@ interface MarketplaceItemCardProps {
}
export const MarketplaceItemCard: React.FC = ({ item, filters, setFilters, installed }) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const { cwd } = useExtensionState()
const [showInstallModal, setShowInstallModal] = useState(false)
const [showRemoveConfirm, setShowRemoveConfirm] = useState(false)
@@ -172,7 +172,7 @@ export const MarketplaceItemCard: React.FC = ({ item,
key={tag}
content={
filters.tags.includes(tag)
- ? t("marketplace:filters.tags.clear", { count: tag })
+ ? t("marketplace:filters.tags.clear")
: t("marketplace:filters.tags.clickToFilter")
}>
= ({ item, typeLabel }) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const handleOpenAuthorUrl = () => {
if (item.authorUrl && isValidUrl(item.authorUrl)) {
diff --git a/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal-optional-params.spec.tsx b/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal-optional-params.spec.tsx
index 29af469f80..d191e5c0a3 100644
--- a/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal-optional-params.spec.tsx
+++ b/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal-optional-params.spec.tsx
@@ -13,8 +13,8 @@ vi.mock("@/utils/vscode", () => ({
import { vscode } from "@/utils/vscode"
const mockPostMessage = vscode.postMessage as any
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string, params?: any) => {
// Simple mock translation
if (key === "marketplace:install.configuration") return "Configuration"
diff --git a/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal.spec.tsx b/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal.spec.tsx
index e586fbb9e1..424ace5e1d 100644
--- a/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal.spec.tsx
+++ b/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal.spec.tsx
@@ -15,8 +15,8 @@ import { vscode } from "@/utils/vscode"
const mockedVscode = vscode as any
// Mock the translation hook
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string, params?: any) => {
// Simple mock translation that returns the key with params
if (key === "marketplace:install.validationRequired") {
diff --git a/webview-ui/src/components/marketplace/components/__tests__/MarketplaceItemCard.spec.tsx b/webview-ui/src/components/marketplace/components/__tests__/MarketplaceItemCard.spec.tsx
index 4fdc685192..5493509e13 100644
--- a/webview-ui/src/components/marketplace/components/__tests__/MarketplaceItemCard.spec.tsx
+++ b/webview-ui/src/components/marketplace/components/__tests__/MarketplaceItemCard.spec.tsx
@@ -21,8 +21,8 @@ vi.mock("@/context/ExtensionStateContext", () => ({
}),
}))
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string, params?: any) => {
if (key === "marketplace:items.card.by") {
return `by ${params.author}`
diff --git a/webview-ui/src/components/mcp/McpEnabledToggle.tsx b/webview-ui/src/components/mcp/McpEnabledToggle.tsx
index 3e6e5cf9ff..dd3a45c2f0 100644
--- a/webview-ui/src/components/mcp/McpEnabledToggle.tsx
+++ b/webview-ui/src/components/mcp/McpEnabledToggle.tsx
@@ -2,7 +2,7 @@ import { FormEvent } from "react"
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@src/context/ExtensionStateContext"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { vscode } from "@src/utils/vscode"
interface McpEnabledToggleProps {
@@ -15,7 +15,7 @@ const McpEnabledToggle = ({
setMcpEnabled: propsSetMcpEnabled,
}: McpEnabledToggleProps = {}) => {
const { mcpEnabled: contextMcpEnabled, setMcpEnabled: contextSetMcpEnabled } = useExtensionState()
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
// When rendered inside SettingsView the value is buffered in `cachedState` and
// only persisted on Save. Fall back to live extension state when used uncontrolled.
diff --git a/webview-ui/src/components/mcp/McpToolRow.tsx b/webview-ui/src/components/mcp/McpToolRow.tsx
index 44f3c24f4c..ead95a2b15 100644
--- a/webview-ui/src/components/mcp/McpToolRow.tsx
+++ b/webview-ui/src/components/mcp/McpToolRow.tsx
@@ -2,7 +2,7 @@ import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import type { McpTool } from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { vscode } from "@src/utils/vscode"
import { StandardTooltip, ToggleSwitch } from "@/components/ui"
@@ -15,7 +15,7 @@ type McpToolRowProps = {
}
const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp, isInChatContext = false }: McpToolRowProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const isToolEnabled = tool.enabledForPrompt ?? true
const handleAlwaysAllowChange = () => {
diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx
index 72724d72e8..5bf2e8f8d9 100644
--- a/webview-ui/src/components/mcp/McpView.tsx
+++ b/webview-ui/src/components/mcp/McpView.tsx
@@ -1,12 +1,11 @@
import React, { useState } from "react"
-import { Trans } from "react-i18next"
import { VSCodeLink, VSCodePanels, VSCodePanelTab, VSCodePanelView } from "@vscode/webview-ui-toolkit/react"
import type { McpServer } from "@roo-code/types"
import { vscode } from "@src/utils/vscode"
import { useExtensionState } from "@src/context/ExtensionStateContext"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { Trans, useTranslation } from "react-i18next"
import { useTooManyTools } from "@src/hooks/useTooManyTools"
import {
Button,
@@ -40,7 +39,7 @@ const McpView = ({ mcpEnabled: propsMcpEnabled, setMcpEnabled }: McpViewProps =
// only persisted on Save. Fall back to live extension state when used uncontrolled.
const mcpEnabled = propsMcpEnabled ?? contextMcpEnabled
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const { isOverThreshold, title, message } = useTooManyTools()
return (
@@ -55,7 +54,7 @@ const McpView = ({ mcpEnabled: propsMcpEnabled, setMcpEnabled }: McpViewProps =
marginBottom: "10px",
marginTop: "5px",
}}>
-
+
@@ -184,7 +183,7 @@ const McpView = ({ mcpEnabled: propsMcpEnabled, setMcpEnabled }: McpViewProps =
}
const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowMcp?: boolean }) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [isExpanded, setIsExpanded] = useState(false)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const [timeoutValue, setTimeoutValue] = useState(() => {
diff --git a/webview-ui/src/components/mcp/__tests__/McpEnabledToggle.spec.tsx b/webview-ui/src/components/mcp/__tests__/McpEnabledToggle.spec.tsx
index f59efce971..065de323c3 100644
--- a/webview-ui/src/components/mcp/__tests__/McpEnabledToggle.spec.tsx
+++ b/webview-ui/src/components/mcp/__tests__/McpEnabledToggle.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 }),
}))
const contextSetMcpEnabled = vi.fn()
diff --git a/webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx b/webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx
index 156ecb0653..0356cb2c46 100644
--- a/webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx
+++ b/webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx
@@ -5,8 +5,8 @@ import { vscode } from "@src/utils/vscode"
import McpToolRow from "../McpToolRow"
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => {
const translations: Record = {
"mcp:tool.alwaysAllow": "Always allow",
diff --git a/webview-ui/src/components/mcp/__tests__/McpView.spec.tsx b/webview-ui/src/components/mcp/__tests__/McpView.spec.tsx
index 957797d1e9..a902277ab5 100644
--- a/webview-ui/src/components/mcp/__tests__/McpView.spec.tsx
+++ b/webview-ui/src/components/mcp/__tests__/McpView.spec.tsx
@@ -12,8 +12,9 @@ vi.mock("@src/context/ExtensionStateContext", () => ({
}),
}))
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({ t: (key: string) => key }),
+vi.mock("react-i18next", async (importOriginal) => ({
+ ...(await importOriginal()),
+ useTranslation: () => ({ t: (key: string) => key }),
}))
vi.mock("@src/hooks/useTooManyTools", () => ({
diff --git a/webview-ui/src/components/modes/DeleteModeDialog.tsx b/webview-ui/src/components/modes/DeleteModeDialog.tsx
index d801b3149b..0c5c88896f 100644
--- a/webview-ui/src/components/modes/DeleteModeDialog.tsx
+++ b/webview-ui/src/components/modes/DeleteModeDialog.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 @@ interface DeleteModeDialogProps {
}
export const DeleteModeDialog: React.FC = ({ open, onOpenChange, modeToDelete, onConfirm }) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
return (
diff --git a/webview-ui/src/components/modes/ModesView.tsx b/webview-ui/src/components/modes/ModesView.tsx
index d5512009a5..768cd166b7 100644
--- a/webview-ui/src/components/modes/ModesView.tsx
+++ b/webview-ui/src/components/modes/ModesView.tsx
@@ -7,7 +7,6 @@ import {
VSCodeLink,
VSCodeTextField,
} from "@vscode/webview-ui-toolkit/react"
-import { Trans } from "react-i18next"
import { ChevronDown, X, Upload, Download } from "lucide-react"
import { ModeConfig, GroupEntry, PromptComponent, ToolGroup, modeConfigSchema } from "@roo-code/types"
@@ -26,7 +25,7 @@ import { TOOL_GROUPS } from "@roo/tools"
import { vscode } from "@src/utils/vscode"
import { buildDocLink } from "@src/utils/docLinks"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { Trans, useTranslation } from "react-i18next"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { Section } from "@src/components/settings/Section"
import {
@@ -66,7 +65,7 @@ function getGroupName(group: GroupEntry): ToolGroup {
}
const ModesView = () => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const {
customModePrompts,
@@ -710,7 +709,7 @@ const ModesView = () => {
-
+
{
/>
{
{t("prompts:globalCustomInstructions.title")}
-
+
{
/>
& {
}
export const About = ({ telemetrySetting, setTelemetrySetting, debug, setDebug, className, ...props }: AboutProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [rooHistoryImportProgress, setRooHistoryImportProgress] = useState(null)
useEffect(() => {
@@ -117,6 +116,7 @@ export const About = ({ telemetrySetting, setTelemetrySetting, debug, setDebug,
,
@@ -160,6 +160,7 @@ export const About = ({ telemetrySetting, setTelemetrySetting, debug, setDebug,
,
diff --git a/webview-ui/src/components/settings/ApiConfigManager.tsx b/webview-ui/src/components/settings/ApiConfigManager.tsx
index d57cd004a9..898d9a20d9 100644
--- a/webview-ui/src/components/settings/ApiConfigManager.tsx
+++ b/webview-ui/src/components/settings/ApiConfigManager.tsx
@@ -4,7 +4,7 @@ import { TriangleAlert } from "lucide-react"
import type { ProviderSettingsEntry, OrganizationAllowList } from "@roo-code/types"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import {
type SearchableSelectOption,
Button,
@@ -35,7 +35,7 @@ const ApiConfigManager = ({
onRenameConfig,
onUpsertConfig,
}: ApiConfigManagerProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [isRenaming, setIsRenaming] = useState(false)
const [isCreating, setIsCreating] = useState(false)
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index ab7457915e..427fb6ed5f 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -33,7 +33,7 @@ import { isTemperatureControlSupported } from "./utils/temperatureSupport"
import { vscode } from "@src/utils/vscode"
import { validateApiConfigurationExcludingModelErrors, getModelValidationError } from "@src/utils/validate"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { useRouterModels } from "@src/components/ui/hooks/useRouterModels"
import { useZooGatewayRouterModelsSync } from "@src/components/ui/hooks/useZooGatewayRouterModelsSync"
import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel"
@@ -129,7 +129,7 @@ const ApiOptions = ({
errorMessage,
setErrorMessage,
}: ApiOptionsProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const { organizationAllowList, openAiCodexIsAuthenticated, kimiCodeIsAuthenticated, kimiCodeOAuthState } =
useExtensionState()
@@ -308,11 +308,12 @@ const ApiOptions = ({
const apiValidationResult = validateApiConfigurationExcludingModelErrors(
apiConfiguration,
+ t,
routerModels,
organizationAllowList,
)
setErrorMessage(apiValidationResult)
- }, [apiConfiguration, routerModels, organizationAllowList, setErrorMessage, isRetiredSelectedProvider])
+ }, [apiConfiguration, t, routerModels, organizationAllowList, setErrorMessage, isRetiredSelectedProvider])
const onProviderChange = useCallback(
(value: ProviderName) => {
@@ -378,8 +379,8 @@ const ApiOptions = ({
)
const modelValidationError = useMemo(() => {
- return getModelValidationError(apiConfiguration, routerModels, organizationAllowList)
- }, [apiConfiguration, routerModels, organizationAllowList])
+ return getModelValidationError(apiConfiguration, t, routerModels, organizationAllowList)
+ }, [apiConfiguration, t, routerModels, organizationAllowList])
const docs = useMemo(() => {
const provider = PROVIDERS.find(({ value }) => value === selectedProvider)
diff --git a/webview-ui/src/components/settings/AutoApproveSettings.tsx b/webview-ui/src/components/settings/AutoApproveSettings.tsx
index 58034960fb..7161c0ae1f 100644
--- a/webview-ui/src/components/settings/AutoApproveSettings.tsx
+++ b/webview-ui/src/components/settings/AutoApproveSettings.tsx
@@ -1,9 +1,8 @@
import { FormEvent, HTMLAttributes, useState } from "react"
import { X } from "lucide-react"
-import { Trans } from "react-i18next"
import { Package } from "@roo/package"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { Trans, useTranslation } from "react-i18next"
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { vscode } from "@/utils/vscode"
import { Button, Input, Slider } from "@/components/ui"
@@ -92,7 +91,7 @@ export const AutoApproveSettings = ({
setCachedStateField,
...props
}: AutoApproveSettingsProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [commandInput, setCommandInput] = useState("")
const [deniedCommandInput, setDeniedCommandInput] = useState("")
const { autoApprovalEnabled, setAutoApprovalEnabled } = useExtensionState()
@@ -145,6 +144,7 @@ export const AutoApproveSettings = ({
{t("settings:autoApprove.description")}
{
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
return (
diff --git a/webview-ui/src/components/settings/CheckpointSettings.tsx b/webview-ui/src/components/settings/CheckpointSettings.tsx
index dd28f6615f..cb22b52e9c 100644
--- a/webview-ui/src/components/settings/CheckpointSettings.tsx
+++ b/webview-ui/src/components/settings/CheckpointSettings.tsx
@@ -1,7 +1,6 @@
import { HTMLAttributes } from "react"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { Trans, useTranslation } from "react-i18next"
import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
-import { Trans } from "react-i18next"
import { buildDocLink } from "@src/utils/docLinks"
import { Slider } from "@/components/ui"
@@ -27,7 +26,7 @@ export const CheckpointSettings = ({
setCachedStateField,
...props
}: CheckpointSettingsProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
return (
{t("settings:sections.checkpoints")}
@@ -45,7 +44,7 @@ export const CheckpointSettings = ({
{t("settings:checkpoints.enable.label")}
-
+
diff --git a/webview-ui/src/components/settings/ConsecutiveMistakeLimitControl.tsx b/webview-ui/src/components/settings/ConsecutiveMistakeLimitControl.tsx
index 633ff8be8d..2035ae5559 100644
--- a/webview-ui/src/components/settings/ConsecutiveMistakeLimitControl.tsx
+++ b/webview-ui/src/components/settings/ConsecutiveMistakeLimitControl.tsx
@@ -1,6 +1,6 @@
import { DEFAULT_CONSECUTIVE_MISTAKE_LIMIT } from "@roo-code/types"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { Slider } from "@/components/ui"
@@ -10,7 +10,7 @@ interface ConsecutiveMistakeLimitControlProps {
}
export const ConsecutiveMistakeLimitControl = ({ value, onChange }: ConsecutiveMistakeLimitControlProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
return (
diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx
index c3af315d99..68894ca805 100644
--- a/webview-ui/src/components/settings/ContextManagementSettings.tsx
+++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx
@@ -1,6 +1,6 @@
import { HTMLAttributes } from "react"
import React from "react"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { VSCodeCheckbox, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"
import { ListChevronsDownUp } from "lucide-react"
import { DEFAULT_DIFF_FUZZY_THRESHOLD } from "@roo-code/types"
@@ -89,7 +89,7 @@ export const ContextManagementSettings = ({
className,
...props
}: ContextManagementSettingsProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [selectedThresholdProfile, setSelectedThresholdProfile] = React.useState
("default")
// Helper function to get the CONDENSE prompt value
diff --git a/webview-ui/src/components/settings/CreateRuleDialog.tsx b/webview-ui/src/components/settings/CreateRuleDialog.tsx
index fb3b65b84f..bd122d25c9 100644
--- a/webview-ui/src/components/settings/CreateRuleDialog.tsx
+++ b/webview-ui/src/components/settings/CreateRuleDialog.tsx
@@ -2,7 +2,7 @@ import React, { useCallback, useMemo, useState } from "react"
import { getAllModes } from "@roo/modes"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { useExtensionState } from "@/context/ExtensionStateContext"
import {
Button,
@@ -41,7 +41,7 @@ export const CreateRuleDialog: React.FC = ({
onRuleCreated,
hasWorkspace,
}) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const { customModes } = useExtensionState()
const [name, setName] = useState("")
diff --git a/webview-ui/src/components/settings/CreateSkillDialog.tsx b/webview-ui/src/components/settings/CreateSkillDialog.tsx
index 3a8def14ee..cc1b3b9179 100644
--- a/webview-ui/src/components/settings/CreateSkillDialog.tsx
+++ b/webview-ui/src/components/settings/CreateSkillDialog.tsx
@@ -3,7 +3,7 @@ import { validateSkillName as validateSkillNameShared, SkillNameValidationError
import { getAllModes } from "@roo/modes"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { useExtensionState } from "@/context/ExtensionStateContext"
import {
Button,
@@ -74,7 +74,7 @@ export const CreateSkillDialog: React.FC = ({
onSkillCreated,
hasWorkspace,
}) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const { customModes } = useExtensionState()
const [name, setName] = useState("")
diff --git a/webview-ui/src/components/settings/CreateSlashCommandDialog.tsx b/webview-ui/src/components/settings/CreateSlashCommandDialog.tsx
index 343db2db73..2c1de3f9bc 100644
--- a/webview-ui/src/components/settings/CreateSlashCommandDialog.tsx
+++ b/webview-ui/src/components/settings/CreateSlashCommandDialog.tsx
@@ -1,6 +1,6 @@
import React, { useState, useCallback } from "react"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import {
Button,
Dialog,
@@ -45,7 +45,7 @@ export const CreateSlashCommandDialog: React.FC =
onCommandCreated,
hasWorkspace,
}) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [name, setName] = useState("")
const [source, setSource] = useState<"global" | "project">(hasWorkspace ? "project" : "global")
diff --git a/webview-ui/src/components/settings/CustomToolsSettings.tsx b/webview-ui/src/components/settings/CustomToolsSettings.tsx
index 03bff7fc8a..6b7a7a5773 100644
--- a/webview-ui/src/components/settings/CustomToolsSettings.tsx
+++ b/webview-ui/src/components/settings/CustomToolsSettings.tsx
@@ -5,7 +5,7 @@ import { RefreshCw, Loader2, FileCode } from "lucide-react"
import type { SerializedCustomToolDefinition } from "@roo-code/types"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { vscode } from "@/utils/vscode"
@@ -31,7 +31,7 @@ interface CustomToolsSettingsProps {
}
export const CustomToolsSettings = ({ enabled, onChange }: CustomToolsSettingsProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [tools, setTools] = useState([])
const [isRefreshing, setIsRefreshing] = useState(false)
const [refreshError, setRefreshError] = useState(null)
diff --git a/webview-ui/src/components/settings/ExperimentalFeature.tsx b/webview-ui/src/components/settings/ExperimentalFeature.tsx
index a96a00a426..63cab40d8e 100644
--- a/webview-ui/src/components/settings/ExperimentalFeature.tsx
+++ b/webview-ui/src/components/settings/ExperimentalFeature.tsx
@@ -1,5 +1,5 @@
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
interface ExperimentalFeatureProps {
enabled: boolean
@@ -9,7 +9,7 @@ interface ExperimentalFeatureProps {
}
export const ExperimentalFeature = ({ enabled, onChange, experimentKey }: ExperimentalFeatureProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
// Generate translation keys based on experiment key
const nameKey = experimentKey ? `settings:experimental.${experimentKey}.name` : ""
diff --git a/webview-ui/src/components/settings/ExperimentalSettings.tsx b/webview-ui/src/components/settings/ExperimentalSettings.tsx
index d5c55297ea..ba2f1ba6df 100644
--- a/webview-ui/src/components/settings/ExperimentalSettings.tsx
+++ b/webview-ui/src/components/settings/ExperimentalSettings.tsx
@@ -4,7 +4,7 @@ import type { Experiments, ImageGenerationProvider } from "@roo-code/types"
import { EXPERIMENT_IDS, experimentConfigsMap } from "@roo/experiments"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { cn } from "@src/lib/utils"
import { SetExperimentEnabled } from "./types"
@@ -42,7 +42,7 @@ export const ExperimentalSettings = ({
className,
...props
}: ExperimentalSettingsProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
return (
diff --git a/webview-ui/src/components/settings/FilePatternAllowlist.tsx b/webview-ui/src/components/settings/FilePatternAllowlist.tsx
index 86774a79c0..b0fb126651 100644
--- a/webview-ui/src/components/settings/FilePatternAllowlist.tsx
+++ b/webview-ui/src/components/settings/FilePatternAllowlist.tsx
@@ -1,7 +1,7 @@
import type { FormEvent } from "react"
import { VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { SetCachedStateField } from "./types"
import { SearchableSetting } from "./SearchableSetting"
@@ -58,7 +58,7 @@ export const FilePatternAllowlist = ({
patterns,
setCachedStateField,
}: FilePatternAllowlistProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const label = t(`settings:autoApprove.allowlists.${translationKey}.label`)
const headingId = `${settingId}-label`
diff --git a/webview-ui/src/components/settings/ImageGenerationSettings.tsx b/webview-ui/src/components/settings/ImageGenerationSettings.tsx
index fd54054b70..33fa6896dd 100644
--- a/webview-ui/src/components/settings/ImageGenerationSettings.tsx
+++ b/webview-ui/src/components/settings/ImageGenerationSettings.tsx
@@ -2,7 +2,7 @@ import { providerIdentifiers } from "@roo-code/types"
import React, { useMemo } from "react"
import { VSCodeCheckbox, VSCodeTextField, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { IMAGE_GENERATION_MODELS, type ImageGenerationProvider, getImageGenerationProvider } from "@roo-code/types"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
interface ImageGenerationSettingsProps {
enabled: boolean
@@ -25,7 +25,7 @@ export const ImageGenerationSettings = ({
setOpenRouterImageApiKey,
setImageGenerationSelectedModel,
}: ImageGenerationSettingsProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
// Use shared utility for backwards compatibility logic
const currentProvider = getImageGenerationProvider(
diff --git a/webview-ui/src/components/settings/LanguageSettings.tsx b/webview-ui/src/components/settings/LanguageSettings.tsx
index 26a807a498..43dcfd26c9 100644
--- a/webview-ui/src/components/settings/LanguageSettings.tsx
+++ b/webview-ui/src/components/settings/LanguageSettings.tsx
@@ -1,5 +1,5 @@
import { HTMLAttributes } from "react"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import type { Language } from "@roo-code/types"
@@ -19,7 +19,7 @@ type LanguageSettingsProps = HTMLAttributes
& {
}
export const LanguageSettings = ({ language, setCachedStateField, className, ...props }: LanguageSettingsProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
return (
diff --git a/webview-ui/src/components/settings/ModelInfoView.tsx b/webview-ui/src/components/settings/ModelInfoView.tsx
index 1ae893eeb2..52133d5d15 100644
--- a/webview-ui/src/components/settings/ModelInfoView.tsx
+++ b/webview-ui/src/components/settings/ModelInfoView.tsx
@@ -5,7 +5,7 @@ import { providerIdentifiers } from "@roo-code/types/provider-identifiers"
import { formatPrice } from "@src/utils/formatPrice"
import { cn } from "@src/lib/utils"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { StandardTooltip } from "@src/components/ui"
import { ModelDescriptionMarkdown } from "./ModelDescriptionMarkdown"
@@ -47,7 +47,7 @@ export const ModelInfoView = ({
setIsDescriptionExpanded,
hidePricing,
}: ModelInfoViewProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
// Show tiered pricing table for OpenAI Native when model supports non-standard tiers
const allowedTierNames =
diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx
index ecc3678a97..3e52f3d40d 100644
--- a/webview-ui/src/components/settings/ModelPicker.tsx
+++ b/webview-ui/src/components/settings/ModelPicker.tsx
@@ -1,6 +1,5 @@
import { useMemo, useState, useCallback, useEffect, useRef } from "react"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
-import { Trans } from "react-i18next"
import { ChevronsUpDown, Check, X, Info } from "lucide-react"
import {
@@ -11,7 +10,7 @@ import {
providerIdentifiers,
} from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { Trans, useTranslation } from "react-i18next"
import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel"
import { filterModels } from "./utils/organizationFilters"
import { cn } from "@src/lib/utils"
@@ -95,7 +94,7 @@ export const ModelPicker = ({
displayTransform,
onModelChange,
}: ModelPickerProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [open, setOpen] = useState(false)
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
@@ -316,6 +315,7 @@ export const ModelPicker = ({
{!hidePricing && apiConfiguration.apiProvider !== providerIdentifiers.mimo && (
,
diff --git a/webview-ui/src/components/settings/NotificationSettings.tsx b/webview-ui/src/components/settings/NotificationSettings.tsx
index 68683e65f4..cff9b74ed6 100644
--- a/webview-ui/src/components/settings/NotificationSettings.tsx
+++ b/webview-ui/src/components/settings/NotificationSettings.tsx
@@ -1,5 +1,5 @@
import { HTMLAttributes } from "react"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { SetCachedStateField } from "./types"
@@ -24,7 +24,7 @@ export const NotificationSettings = ({
setCachedStateField,
...props
}: NotificationSettingsProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
return (
{t("settings:sections.notifications")}
diff --git a/webview-ui/src/components/settings/PromptsSettings.tsx b/webview-ui/src/components/settings/PromptsSettings.tsx
index d6e0ba0f6b..e386d2295f 100644
--- a/webview-ui/src/components/settings/PromptsSettings.tsx
+++ b/webview-ui/src/components/settings/PromptsSettings.tsx
@@ -4,7 +4,7 @@ import { VSCodeTextArea, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react
import { supportPrompt, SupportPromptType } from "@roo/support-prompt"
import { vscode } from "@src/utils/vscode"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import {
Button,
@@ -33,7 +33,7 @@ const PromptsSettings = ({
includeTaskHistoryInEnhance: propsIncludeTaskHistoryInEnhance,
setIncludeTaskHistoryInEnhance: propsSetIncludeTaskHistoryInEnhance,
}: PromptsSettingsProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const {
listApiConfigMeta,
enhancementApiConfigId,
diff --git a/webview-ui/src/components/settings/R1FormatSetting.tsx b/webview-ui/src/components/settings/R1FormatSetting.tsx
index 6418c949bd..4ad90cc14a 100644
--- a/webview-ui/src/components/settings/R1FormatSetting.tsx
+++ b/webview-ui/src/components/settings/R1FormatSetting.tsx
@@ -1,6 +1,6 @@
import { Checkbox } from "vscrui"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
interface R1FormatSettingProps {
onChange: (value: boolean) => void
@@ -8,7 +8,7 @@ interface R1FormatSettingProps {
}
export const R1FormatSetting = ({ onChange, openAiR1FormatEnabled }: R1FormatSettingProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
return (
diff --git a/webview-ui/src/components/settings/RateLimitSecondsControl.tsx b/webview-ui/src/components/settings/RateLimitSecondsControl.tsx
index e66232d396..87dad7942f 100644
--- a/webview-ui/src/components/settings/RateLimitSecondsControl.tsx
+++ b/webview-ui/src/components/settings/RateLimitSecondsControl.tsx
@@ -1,4 +1,4 @@
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { Slider } from "@/components/ui"
@@ -8,7 +8,7 @@ interface RateLimitSecondsControlProps {
}
export const RateLimitSecondsControl = ({ value, onChange }: RateLimitSecondsControlProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
return (
diff --git a/webview-ui/src/components/settings/RulesSettings.tsx b/webview-ui/src/components/settings/RulesSettings.tsx
index 028ca5c56c..6a06231800 100644
--- a/webview-ui/src/components/settings/RulesSettings.tsx
+++ b/webview-ui/src/components/settings/RulesSettings.tsx
@@ -3,7 +3,7 @@ import { Edit, Folder, Globe, Plus, ScrollText, Trash2 } from "lucide-react"
import type { RuleMetadata } from "@roo-code/types"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { useExtensionState } from "@/context/ExtensionStateContext"
import {
AlertDialog,
@@ -23,7 +23,7 @@ import { SectionHeader } from "./SectionHeader"
import { CreateRuleDialog } from "./CreateRuleDialog"
export const RulesSettings: React.FC = () => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const { cwd, rules: rawRules } = useExtensionState()
const rules = useMemo(() => rawRules ?? [], [rawRules])
diff --git a/webview-ui/src/components/settings/SettingsSearchInput.tsx b/webview-ui/src/components/settings/SettingsSearchInput.tsx
index d1b8566ef9..b422eabc69 100644
--- a/webview-ui/src/components/settings/SettingsSearchInput.tsx
+++ b/webview-ui/src/components/settings/SettingsSearchInput.tsx
@@ -2,7 +2,7 @@ import { useState, type RefObject } from "react"
import { Search, X } from "lucide-react"
import { cn } from "@/lib/utils"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { Input } from "@/components/ui"
export interface SettingsSearchInputProps {
@@ -22,7 +22,7 @@ export function SettingsSearchInput({
onKeyDown,
inputRef,
}: SettingsSearchInputProps) {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [isExpanded, setIsExpanded] = useState(false)
const handleFocus = () => {
diff --git a/webview-ui/src/components/settings/SettingsSearchResults.tsx b/webview-ui/src/components/settings/SettingsSearchResults.tsx
index bf9d74aefa..3f182bf1c0 100644
--- a/webview-ui/src/components/settings/SettingsSearchResults.tsx
+++ b/webview-ui/src/components/settings/SettingsSearchResults.tsx
@@ -1,7 +1,7 @@
import { useMemo } from "react"
import type { LucideIcon } from "lucide-react"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { cn } from "@/lib/utils"
import { SectionName } from "./SettingsView"
@@ -73,7 +73,7 @@ export function SettingsSearchResults({
sections,
highlightedResultId,
}: SettingsSearchResultsProps) {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
// Group results by section/tab
const groupedResults = useMemo(() => {
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx
index 6100a373e1..85284dc675 100644
--- a/webview-ui/src/components/settings/SettingsView.tsx
+++ b/webview-ui/src/components/settings/SettingsView.tsx
@@ -44,7 +44,7 @@ import {
import { vscode } from "@src/utils/vscode"
import { cn } from "@src/lib/utils"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { ExtensionStateContextType, useExtensionState } from "@src/context/ExtensionStateContext"
import {
AlertDialog,
@@ -127,7 +127,7 @@ type SettingsViewProps = {
}
const SettingsView = forwardRef
(({ onDone, targetSection }, ref) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const extensionState = useExtensionState()
const { currentApiConfigName, listApiConfigMeta, uriScheme, settingsImportedAt, mode } = extensionState
diff --git a/webview-ui/src/components/settings/SkillsSettings.tsx b/webview-ui/src/components/settings/SkillsSettings.tsx
index bf81d0c6c7..1c03bb53d7 100644
--- a/webview-ui/src/components/settings/SkillsSettings.tsx
+++ b/webview-ui/src/components/settings/SkillsSettings.tsx
@@ -1,12 +1,11 @@
import React, { useState, useEffect, useMemo, useCallback } from "react"
import { Plus, Globe, Folder, Edit, Trash2, Settings } from "lucide-react"
-import { Trans } from "react-i18next"
import type { SkillMetadata } from "@roo-code/types"
import { getAllModes } from "@roo/modes"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { Trans, useTranslation } from "react-i18next"
import { useExtensionState } from "@/context/ExtensionStateContext"
import {
AlertDialog,
@@ -34,7 +33,7 @@ import { SectionHeader } from "./SectionHeader"
import { CreateSkillDialog } from "./CreateSkillDialog"
export const SkillsSettings: React.FC = () => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const { cwd, skills: rawSkills, customModes } = useExtensionState()
const skills = useMemo(() => rawSkills ?? [], [rawSkills])
@@ -218,6 +217,7 @@ export const SkillsSettings: React.FC = () => {
{
{/* Fixed Footer */}
{
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const { commands: rawCommands, cwd } = useExtensionState()
const commands = useMemo(() => rawCommands ?? [], [rawCommands])
@@ -148,6 +147,7 @@ export const SlashCommandsSettings: React.FC = () => {
{
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [isCustomTemperature, setIsCustomTemperature] = useState(value !== undefined)
const [inputValue, setInputValue] = useState(value)
diff --git a/webview-ui/src/components/settings/TerminalSettings.tsx b/webview-ui/src/components/settings/TerminalSettings.tsx
index ac7decdb2c..75ccafd43a 100644
--- a/webview-ui/src/components/settings/TerminalSettings.tsx
+++ b/webview-ui/src/components/settings/TerminalSettings.tsx
@@ -1,8 +1,7 @@
import { HTMLAttributes, useState, useCallback, useEffect } from "react"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { Trans, useTranslation } from "react-i18next"
import { vscode } from "@/utils/vscode"
import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
-import { Trans } from "react-i18next"
import { buildDocLink } from "@src/utils/docLinks"
import { useEvent, useMount } from "react-use"
import { Terminal } from "lucide-react"
@@ -70,7 +69,7 @@ export const TerminalSettings = ({
className,
...props
}: TerminalSettingsProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [inheritEnv, setInheritEnv] = useState(true)
const [profileNames, setProfileNames] = useState([])
@@ -205,7 +204,7 @@ export const TerminalSettings = ({
-
+
-
+
{t("settings:terminal.inheritEnv.label")}
-
+
-
+
{terminalCommandDelay ?? 0}ms
-
+
-
+
-
+
{t("settings:terminal.zshOhMy.label")}
-
+
{t("settings:terminal.zshP10k.label")}
-
+
{t("settings:terminal.zdotdir.label")}
-
+
{
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const { id: selectedModelId } = useSelectedModel(apiConfiguration)
// Check if this is a Gemini 2.5 Pro model
diff --git a/webview-ui/src/components/settings/TodoListSettingsControl.tsx b/webview-ui/src/components/settings/TodoListSettingsControl.tsx
index 7a38ab3adc..44d9c95c9d 100644
--- a/webview-ui/src/components/settings/TodoListSettingsControl.tsx
+++ b/webview-ui/src/components/settings/TodoListSettingsControl.tsx
@@ -1,5 +1,5 @@
import React, { useCallback } from "react"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
interface TodoListSettingsControlProps {
@@ -11,7 +11,7 @@ export const TodoListSettingsControl: React.FC = (
todoListEnabled = true,
onChange,
}) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const handleTodoListEnabledChange = useCallback(
(e: any) => {
diff --git a/webview-ui/src/components/settings/ToolRepetitionLimitControl.tsx b/webview-ui/src/components/settings/ToolRepetitionLimitControl.tsx
index ae2984d4c0..fa54f3dc79 100644
--- a/webview-ui/src/components/settings/ToolRepetitionLimitControl.tsx
+++ b/webview-ui/src/components/settings/ToolRepetitionLimitControl.tsx
@@ -1,6 +1,6 @@
import { DEFAULT_TOOL_REPETITION_SOFT_LIMIT } from "@roo-code/types"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { Slider } from "@/components/ui"
@@ -10,7 +10,7 @@ interface ToolRepetitionLimitControlProps {
}
export const ToolRepetitionLimitControl = ({ softValue, onSoftChange }: ToolRepetitionLimitControlProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const resolvedSoft = softValue ?? DEFAULT_TOOL_REPETITION_SOFT_LIMIT
diff --git a/webview-ui/src/components/settings/UISettings.tsx b/webview-ui/src/components/settings/UISettings.tsx
index 46353c26ad..4b98072d10 100644
--- a/webview-ui/src/components/settings/UISettings.tsx
+++ b/webview-ui/src/components/settings/UISettings.tsx
@@ -1,5 +1,5 @@
import { HTMLAttributes, useMemo } from "react"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { telemetryClient } from "@/utils/TelemetryClient"
import {
@@ -39,7 +39,7 @@ export const UISettings = ({
setCachedStateField,
...props
}: UISettingsProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
// Detect platform for dynamic modifier key display
const primaryMod = useMemo(() => {
diff --git a/webview-ui/src/components/settings/Verbosity.tsx b/webview-ui/src/components/settings/Verbosity.tsx
index ee612d66cf..67262a1059 100644
--- a/webview-ui/src/components/settings/Verbosity.tsx
+++ b/webview-ui/src/components/settings/Verbosity.tsx
@@ -1,6 +1,6 @@
import { type ProviderSettings, type ModelInfo, type VerbosityLevel, verbosityLevels } from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
interface VerbosityProps {
@@ -10,7 +10,7 @@ interface VerbosityProps {
}
export const Verbosity = ({ apiConfiguration, setApiConfigurationField, modelInfo }: VerbosityProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
// For now, we'll show verbosity for all models, but this can be restricted later
// based on model capabilities (e.g., only for GPT-5 models)
diff --git a/webview-ui/src/components/settings/__tests__/About.spec.tsx b/webview-ui/src/components/settings/__tests__/About.spec.tsx
index e9ca86be37..96dffc4f13 100644
--- a/webview-ui/src/components/settings/__tests__/About.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/About.spec.tsx
@@ -1,7 +1,6 @@
import React from "react"
import { act, fireEvent, render, screen } from "@/utils/test-utils"
-import { TranslationProvider } from "@/i18n/__mocks__/TranslationContext"
import { EXTERNAL_LINKS } from "@/constants/externalLinks"
import { vscode } from "@/utils/vscode"
@@ -35,22 +34,19 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({
),
}))
-vi.mock("@/i18n/TranslationContext", () => {
- const actual = vi.importActual("@/i18n/TranslationContext")
- return {
- ...actual,
- useAppTranslation: () => ({
- t: (key: string, options?: Record) => {
- if (!options) return key
- let result = key
- for (const [k, v] of Object.entries(options)) {
- result = result.replace(`{{${k}}}`, String(v))
- }
- return result
- },
- }),
- }
-})
+vi.mock("react-i18next", async (importOriginal) => ({
+ ...(await importOriginal()),
+ useTranslation: () => ({
+ t: (key: string, options?: Record) => {
+ if (!options) return key
+ let result = key
+ for (const [k, v] of Object.entries(options)) {
+ result = result.replace(`{{${k}}}`, String(v))
+ }
+ return result
+ },
+ }),
+}))
vi.mock("@roo/package", () => ({
Package: {
@@ -65,12 +61,7 @@ describe("About", () => {
setTelemetrySetting: vi.fn(),
}
- const renderAbout = () =>
- render(
-
-
- ,
- )
+ const renderAbout = () => render( )
const dispatchImportProgress = async (progress: {
status: "starting" | "copying" | "finished" | "failed"
@@ -95,29 +86,17 @@ describe("About", () => {
})
it("renders the About section header", () => {
- render(
-
-
- ,
- )
+ render( )
expect(screen.getByText("settings:sections.about")).toBeInTheDocument()
})
it("displays version information", () => {
- render(
-
-
- ,
- )
+ render( )
expect(screen.getByText(/Version: 1\.0\.0/)).toBeInTheDocument()
})
it("renders the bug report section with label and link text", () => {
- render(
-
-
- ,
- )
+ render( )
expect(screen.getByText("settings:about.bugReport.label")).toBeInTheDocument()
expect(screen.getByRole("link", { name: "settings:about.bugReport.link" })).toHaveAttribute(
"href",
@@ -126,11 +105,7 @@ describe("About", () => {
})
it("renders the feature request section with label and link text", () => {
- render(
-
-
- ,
- )
+ render( )
expect(screen.getByText("settings:about.featureRequest.label")).toBeInTheDocument()
expect(screen.getByRole("link", { name: "settings:about.featureRequest.link" })).toHaveAttribute(
"href",
@@ -139,11 +114,7 @@ describe("About", () => {
})
it("renders the security issue section with label and link text", () => {
- render(
-
-
- ,
- )
+ render( )
expect(screen.getByText("settings:about.securityIssue.label")).toBeInTheDocument()
expect(screen.getByRole("link", { name: "settings:about.securityIssue.link" })).toHaveAttribute(
"href",
@@ -152,41 +123,25 @@ describe("About", () => {
})
it("shows the telemetry checkbox as checked when the setting is explicitly enabled", () => {
- render(
-
-
- ,
- )
+ render( )
expect(screen.getByRole("checkbox", { name: /telemetry/i })).toBeChecked()
})
it("shows the telemetry checkbox as checked when the setting is unset (disclosed opt-out default)", () => {
- render(
-
-
- ,
- )
+ render( )
expect(screen.getByRole("checkbox", { name: /telemetry/i })).toBeChecked()
})
it("does not show the telemetry checkbox as checked when the setting is disabled", () => {
- render(
-
-
- ,
- )
+ render( )
expect(screen.getByRole("checkbox", { name: /telemetry/i })).not.toBeChecked()
})
it("renders export, import, and reset buttons", () => {
- render(
-
-
- ,
- )
+ render( )
expect(screen.getByText("settings:footer.settings.export")).toBeInTheDocument()
expect(screen.getByText("settings:footer.settings.import")).toBeInTheDocument()
expect(screen.getByText("settings:footer.settings.reset")).toBeInTheDocument()
diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx
index b68bee99e6..47b547e518 100644
--- a/webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx
@@ -21,8 +21,9 @@ vi.mock("@src/context/ExtensionStateContext", () => ({
}))
// Mock the translation hook
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", async (importOriginal) => ({
+ ...(await importOriginal()),
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/settings/__tests__/AutoApproveSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/AutoApproveSettings.spec.tsx
index 12090b42b5..90c8f023d7 100644
--- a/webview-ui/src/components/settings/__tests__/AutoApproveSettings.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/AutoApproveSettings.spec.tsx
@@ -61,8 +61,9 @@ vi.mock("@vscode/webview-ui-toolkit/react", async () => {
return { ...toolkitMock, VSCodeCheckbox: VSCodeCheckboxHost }
})
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({ t: (key: string) => key }),
+vi.mock("react-i18next", async (importOriginal) => ({
+ ...(await importOriginal()),
+ useTranslation: () => ({ t: (key: string) => key }),
}))
// AutoApproveSettings reads a couple of live-state values that are genuinely
diff --git a/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx b/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx
index 91788e0200..e95ddd517c 100644
--- a/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx
@@ -1,18 +1,12 @@
import { render, screen, fireEvent } from "@/utils/test-utils"
-import { TranslationProvider } from "@/i18n/__mocks__/TranslationContext"
-
import { AutoApproveToggle, autoApproveSettingsConfig } from "../AutoApproveToggle"
-vi.mock("@/i18n/TranslationContext", () => {
- const actual = vi.importActual("@/i18n/TranslationContext")
- return {
- ...actual,
- useAppTranslation: () => ({
- t: (key: string) => key,
- }),
- }
-})
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
+ t: (key: string) => key,
+ }),
+}))
describe("AutoApproveToggle", () => {
const mockOnToggle = vi.fn()
@@ -32,11 +26,7 @@ describe("AutoApproveToggle", () => {
})
test("renders all toggle buttons with correct initial ARIA attributes", () => {
- render(
-
-
- ,
- )
+ render( )
Object.values(autoApproveSettingsConfig).forEach((config) => {
const button = screen.getByTestId(config.testId)
@@ -47,11 +37,7 @@ describe("AutoApproveToggle", () => {
})
test("calls onToggle with the correct key and value when a button is clicked", () => {
- render(
-
-
- ,
- )
+ render( )
const writeToggleButton = screen.getByTestId(autoApproveSettingsConfig.alwaysAllowWrite.testId)
fireEvent.click(writeToggleButton)
@@ -66,21 +52,13 @@ describe("AutoApproveToggle", () => {
})
test("updates aria-pressed attribute after toggle", () => {
- const { rerender } = render(
-
-
- ,
- )
+ const { rerender } = render( )
const writeToggleButton = screen.getByTestId(autoApproveSettingsConfig.alwaysAllowWrite.testId)
expect(writeToggleButton).toHaveAttribute("aria-pressed", "false")
const updatedProps = { ...initialProps, alwaysAllowWrite: true }
- rerender(
-
-
- ,
- )
+ rerender( )
expect(screen.getByTestId(autoApproveSettingsConfig.alwaysAllowWrite.testId)).toHaveAttribute(
"aria-pressed",
diff --git a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx
index 10d5d0fe4f..07f3daee23 100644
--- a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx
@@ -4,19 +4,6 @@ import { render, screen, fireEvent, waitFor } from "@/utils/test-utils"
import { ContextManagementSettings } from "../ContextManagementSettings"
import { vscode } from "@/utils/vscode"
-// Mock the translation hook
-vi.mock("@/hooks/useAppTranslation", () => ({
- useAppTranslation: () => ({
- t: (key: string) => {
- // Return specific translations for our test cases
- if (key === "settings:contextManagement.diagnostics.maxMessages.unlimitedLabel") {
- return "Unlimited"
- }
- return key
- },
- }),
-}))
-
// Mock the UI components
vi.mock("@/components/ui", () => ({
...vi.importActual("@/components/ui"),
diff --git a/webview-ui/src/components/settings/__tests__/CreateRuleDialog.spec.tsx b/webview-ui/src/components/settings/__tests__/CreateRuleDialog.spec.tsx
index f1684a2cdf..1610242f8e 100644
--- a/webview-ui/src/components/settings/__tests__/CreateRuleDialog.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/CreateRuleDialog.spec.tsx
@@ -10,8 +10,8 @@ vi.mock("@/utils/vscode", () => ({
},
}))
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/settings/__tests__/CreateSkillDialog.spec.tsx b/webview-ui/src/components/settings/__tests__/CreateSkillDialog.spec.tsx
index 9b2087ddaa..9b777ecd5b 100644
--- a/webview-ui/src/components/settings/__tests__/CreateSkillDialog.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/CreateSkillDialog.spec.tsx
@@ -12,8 +12,8 @@ vi.mock("@/utils/vscode", () => ({
}))
// Mock the translation hook
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/settings/__tests__/ExperimentalSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/ExperimentalSettings.spec.tsx
index b31f87dc7e..bad98b36e3 100644
--- a/webview-ui/src/components/settings/__tests__/ExperimentalSettings.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/ExperimentalSettings.spec.tsx
@@ -4,8 +4,8 @@ import { experimentDefault } from "@roo/experiments"
import { ExperimentalSettings } from "../ExperimentalSettings"
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/settings/__tests__/ImageGenerationSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/ImageGenerationSettings.spec.tsx
index 8fd27b961f..ea5ead7f81 100644
--- a/webview-ui/src/components/settings/__tests__/ImageGenerationSettings.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/ImageGenerationSettings.spec.tsx
@@ -2,9 +2,9 @@ import { render, fireEvent } from "@testing-library/react"
import { ImageGenerationSettings } from "../ImageGenerationSettings"
-// Mock the translation context
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+// Mock the translation hook
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/settings/__tests__/ModelInfoView.spec.tsx b/webview-ui/src/components/settings/__tests__/ModelInfoView.spec.tsx
index ac87f06d7a..81dc9f7f11 100644
--- a/webview-ui/src/components/settings/__tests__/ModelInfoView.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/ModelInfoView.spec.tsx
@@ -4,8 +4,8 @@ import { render, screen, within } from "@/utils/test-utils"
import { ModelInfoView } from "../ModelInfoView"
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) =>
({
"settings:modelInfo.imageSupportUnknown": "Image support not reported",
diff --git a/webview-ui/src/components/settings/__tests__/ModelInfoView.visual.fixture.tsx b/webview-ui/src/components/settings/__tests__/ModelInfoView.visual.fixture.tsx
index b007d4be69..978921da24 100644
--- a/webview-ui/src/components/settings/__tests__/ModelInfoView.visual.fixture.tsx
+++ b/webview-ui/src/components/settings/__tests__/ModelInfoView.visual.fixture.tsx
@@ -3,7 +3,9 @@ import React from "react"
import { OpenAiServiceTier, type ModelInfo } from "@roo-code/types/model"
import { providerIdentifiers } from "@roo-code/types/provider-identifiers"
-import { TranslationContext } from "@src/i18n/TranslationContext"
+import { I18nextProvider } from "react-i18next"
+
+import { createFixtureI18n } from "@src/i18n/__tests__/createFixtureI18n.visual.i18n"
import { ModelInfoView } from "../ModelInfoView"
const modelInfo: ModelInfo = {
@@ -49,12 +51,10 @@ const translations: Record = {
"settings:serviceTier.priority": "Priority",
}
+const i18n = createFixtureI18n(translations)
+
export const ModelInfoViewFixture = () => (
- translations[key] ?? key,
- i18n: null as unknown as typeof import("../../../i18n/setup").default,
- }}>
+
(
setIsDescriptionExpanded={() => {}}
/>
-
+
)
diff --git a/webview-ui/src/components/settings/__tests__/ModelPicker.deprecated.spec.tsx b/webview-ui/src/components/settings/__tests__/ModelPicker.deprecated.spec.tsx
index 154f1cc1de..0d5e72af93 100644
--- a/webview-ui/src/components/settings/__tests__/ModelPicker.deprecated.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/ModelPicker.deprecated.spec.tsx
@@ -12,8 +12,9 @@ import { ModelPicker } from "../ModelPicker"
import type { ModelInfo } from "@roo-code/types"
// Mock the i18n module
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", async (importOriginal) => ({
+ ...(await importOriginal()),
+ useTranslation: () => ({
t: (key: string, options?: any) => {
// Handle specific translation keys
if (key === "settings:validation.modelDeprecated") {
diff --git a/webview-ui/src/components/settings/__tests__/PromptsSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/PromptsSettings.spec.tsx
index d10e100f45..44b9e43ac0 100644
--- a/webview-ui/src/components/settings/__tests__/PromptsSettings.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/PromptsSettings.spec.tsx
@@ -11,8 +11,8 @@ vi.mock("@src/utils/vscode", () => ({
},
}))
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({ t: (key: string) => key }),
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({ t: (key: string) => key }),
}))
// Exposed so tests can assert the context (uncontrolled) setter still fires.
diff --git a/webview-ui/src/components/settings/__tests__/RulesSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/RulesSettings.spec.tsx
index 52ee44a5c8..760ee812ee 100644
--- a/webview-ui/src/components/settings/__tests__/RulesSettings.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/RulesSettings.spec.tsx
@@ -12,8 +12,8 @@ vi.mock("@/utils/vscode", () => ({
},
}))
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string, params?: any) =>
params?.mode ? `${key} ${params.mode}` : params?.name ? `${key} ${params.name}` : key,
}),
diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx
index 6704aa1cf0..2d2f153aaf 100644
--- a/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx
@@ -32,9 +32,10 @@ vi.mock("@src/context/ExtensionStateContext", () => ({
const mockTranslate = vi.hoisted(() => (key: string) => key)
-// Mock the translation context
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+// Mock the translation hook
+vi.mock("react-i18next", async (importOriginal) => ({
+ ...(await importOriginal()),
+ useTranslation: () => ({
t: mockTranslate,
}),
}))
diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx
index 1bc4ab1afb..276d835633 100644
--- a/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx
@@ -17,11 +17,9 @@ vi.mock("@src/context/ExtensionStateContext", () => ({
useExtensionState: vi.fn(),
}))
-// Mock the translation context
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
- t: (key: string) => key,
- }),
+// Mock the translation hook
+vi.mock("react-i18next", async (importOriginal) => ({
+ ...(await importOriginal()),
useTranslation: () => ({
t: (key: string) => key,
}),
diff --git a/webview-ui/src/components/settings/__tests__/SkillsSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/SkillsSettings.spec.tsx
index 67c820d8ce..9d84f8db56 100644
--- a/webview-ui/src/components/settings/__tests__/SkillsSettings.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/SkillsSettings.spec.tsx
@@ -14,8 +14,9 @@ vi.mock("@/utils/vscode", () => ({
}))
// Mock the translation hook
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", async (importOriginal) => ({
+ ...(await importOriginal()),
+ useTranslation: () => ({
t: (key: string, params?: any) => {
if (params?.name) {
return `${key} ${params.name}`
diff --git a/webview-ui/src/components/settings/__tests__/SlashCommandsSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/SlashCommandsSettings.spec.tsx
index 4533e54a5f..853f924b74 100644
--- a/webview-ui/src/components/settings/__tests__/SlashCommandsSettings.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/SlashCommandsSettings.spec.tsx
@@ -14,8 +14,9 @@ vi.mock("@/utils/vscode", () => ({
}))
// Mock the translation hook
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", async (importOriginal) => ({
+ ...(await importOriginal()),
+ useTranslation: () => ({
t: (key: string, params?: any) => {
if (params?.name) {
return `${key} ${params.name}`
diff --git a/webview-ui/src/components/settings/__tests__/TerminalSettings.profile.spec.tsx b/webview-ui/src/components/settings/__tests__/TerminalSettings.profile.spec.tsx
index d3fc05abdf..93b26f843d 100644
--- a/webview-ui/src/components/settings/__tests__/TerminalSettings.profile.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/TerminalSettings.profile.spec.tsx
@@ -7,8 +7,9 @@ import { render, screen, fireEvent, act } from "@/utils/test-utils"
import { TerminalSettings, DEFAULT_PROFILE_VALUE } from "../TerminalSettings"
// Mock translation hook to echo keys
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({ t: (key: string) => key }),
+vi.mock("react-i18next", async (importOriginal) => ({
+ ...(await importOriginal()),
+ useTranslation: () => ({ t: (key: string) => key }),
}))
vi.mock("@src/utils/docLinks", () => ({
diff --git a/webview-ui/src/components/settings/__tests__/TodoListSettingsControl.spec.tsx b/webview-ui/src/components/settings/__tests__/TodoListSettingsControl.spec.tsx
index 76f65d312f..664be77993 100644
--- a/webview-ui/src/components/settings/__tests__/TodoListSettingsControl.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/TodoListSettingsControl.spec.tsx
@@ -3,8 +3,8 @@ import { render, screen, fireEvent } from "@testing-library/react"
import { TodoListSettingsControl } from "../TodoListSettingsControl"
// Mock the translation hook
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => {
const translations: Record = {
"settings:advanced.todoList.label": "Enable todo list tool",
diff --git a/webview-ui/src/components/settings/__tests__/ToolRepetitionLimitControl.spec.tsx b/webview-ui/src/components/settings/__tests__/ToolRepetitionLimitControl.spec.tsx
index 4412bdec0d..9bab6bb7ed 100644
--- a/webview-ui/src/components/settings/__tests__/ToolRepetitionLimitControl.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/ToolRepetitionLimitControl.spec.tsx
@@ -5,8 +5,8 @@ import { render, screen, fireEvent } from "@testing-library/react"
import { ToolRepetitionLimitControl } from "../ToolRepetitionLimitControl"
// Mock the translation hook
-vi.mock("@/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => {
const translations: Record = {
"settings:providers.toolRepetitionSoftLimit.label": "Tool repetition soft limit",
diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/opencode-go-temperature.png b/webview-ui/src/components/settings/__tests__/__screenshots__/opencode-go-temperature.png
index e1828c9b6b..d2633a1f3b 100644
Binary files a/webview-ui/src/components/settings/__tests__/__screenshots__/opencode-go-temperature.png and b/webview-ui/src/components/settings/__tests__/__screenshots__/opencode-go-temperature.png differ
diff --git a/webview-ui/src/components/settings/providers/Anthropic.tsx b/webview-ui/src/components/settings/providers/Anthropic.tsx
index 6417e34b8b..bd69a881ff 100644
--- a/webview-ui/src/components/settings/providers/Anthropic.tsx
+++ b/webview-ui/src/components/settings/providers/Anthropic.tsx
@@ -4,7 +4,7 @@ import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import type { ProviderSettings } from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel"
@@ -17,7 +17,7 @@ type AnthropicProps = {
}
export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: AnthropicProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const selectedModel = useSelectedModel(apiConfiguration)
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
diff --git a/webview-ui/src/components/settings/providers/Baseten.tsx b/webview-ui/src/components/settings/providers/Baseten.tsx
index 2f4efd62c1..e836456788 100644
--- a/webview-ui/src/components/settings/providers/Baseten.tsx
+++ b/webview-ui/src/components/settings/providers/Baseten.tsx
@@ -3,7 +3,7 @@ import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import type { ProviderSettings } from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { inputEventTransform } from "../transforms"
@@ -15,7 +15,7 @@ type BasetenProps = {
}
export const Baseten = ({ apiConfiguration, setApiConfigurationField }: BasetenProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const handleInputChange = useCallback(
(
diff --git a/webview-ui/src/components/settings/providers/Bedrock.tsx b/webview-ui/src/components/settings/providers/Bedrock.tsx
index bbb540709b..b24fe732d2 100644
--- a/webview-ui/src/components/settings/providers/Bedrock.tsx
+++ b/webview-ui/src/components/settings/providers/Bedrock.tsx
@@ -13,7 +13,7 @@ import {
resolveBedrockCustomArnBaseModelId,
} from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StandardTooltip } from "@src/components/ui"
import { inputEventTransform, noTransform } from "../transforms"
@@ -26,7 +26,7 @@ type BedrockProps = {
}
export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedModelInfo }: BedrockProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [awsEndpointSelected, setAwsEndpointSelected] = useState(!!apiConfiguration?.awsBedrockEndpointEnabled)
// For a custom ARN, model capabilities come from the model the ARN points to.
diff --git a/webview-ui/src/components/settings/providers/BedrockCustomArn.tsx b/webview-ui/src/components/settings/providers/BedrockCustomArn.tsx
index b74a6c87a7..a6ee0e7b70 100644
--- a/webview-ui/src/components/settings/providers/BedrockCustomArn.tsx
+++ b/webview-ui/src/components/settings/providers/BedrockCustomArn.tsx
@@ -12,7 +12,7 @@ import {
} from "@roo-code/types"
import { validateBedrockArn } from "@src/utils/validate"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
const baseModelIds = Object.keys(bedrockModels).sort((a, b) => a.localeCompare(b))
@@ -28,12 +28,14 @@ type BedrockCustomArnProps = {
}
export const BedrockCustomArn = ({ apiConfiguration, setApiConfigurationField }: BedrockCustomArnProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const validation = useMemo(() => {
const { awsCustomArn, awsRegion } = apiConfiguration
- return awsCustomArn ? validateBedrockArn(awsCustomArn, awsRegion) : { isValid: true, errorMessage: undefined }
- }, [apiConfiguration])
+ return awsCustomArn
+ ? validateBedrockArn(awsCustomArn, t, awsRegion)
+ : { isValid: true, errorMessage: undefined }
+ }, [apiConfiguration, t])
const baseModelId =
resolveBedrockCustomArnBaseModelId(apiConfiguration.awsCustomArn, apiConfiguration.awsCustomArnBaseModelId) ??
diff --git a/webview-ui/src/components/settings/providers/DeepSeek.tsx b/webview-ui/src/components/settings/providers/DeepSeek.tsx
index 6f0ac2d92a..e91d17ea23 100644
--- a/webview-ui/src/components/settings/providers/DeepSeek.tsx
+++ b/webview-ui/src/components/settings/providers/DeepSeek.tsx
@@ -3,7 +3,7 @@ import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import type { ProviderSettings } from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { inputEventTransform } from "../transforms"
@@ -14,7 +14,7 @@ type DeepSeekProps = {
}
export const DeepSeek = ({ apiConfiguration, setApiConfigurationField }: DeepSeekProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const handleInputChange = useCallback(
(
diff --git a/webview-ui/src/components/settings/providers/Fireworks.tsx b/webview-ui/src/components/settings/providers/Fireworks.tsx
index bb8eb8aa58..b881fc4581 100644
--- a/webview-ui/src/components/settings/providers/Fireworks.tsx
+++ b/webview-ui/src/components/settings/providers/Fireworks.tsx
@@ -3,7 +3,7 @@ import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import type { ProviderSettings } from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { inputEventTransform } from "../transforms"
@@ -14,7 +14,7 @@ type FireworksProps = {
}
export const Fireworks = ({ apiConfiguration, setApiConfigurationField }: FireworksProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const handleInputChange = useCallback(
(
diff --git a/webview-ui/src/components/settings/providers/Friendli.tsx b/webview-ui/src/components/settings/providers/Friendli.tsx
index 66f4c554ef..79a38cff54 100644
--- a/webview-ui/src/components/settings/providers/Friendli.tsx
+++ b/webview-ui/src/components/settings/providers/Friendli.tsx
@@ -3,7 +3,7 @@ import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import type { ProviderSettings } from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { inputEventTransform } from "../transforms"
@@ -18,7 +18,7 @@ type FriendliProps = {
* Renders an API-key input and a "Get Friendli API Key" link when the key is empty.
*/
export const Friendli = ({ apiConfiguration, setApiConfigurationField }: FriendliProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const handleInputChange = useCallback(
(
diff --git a/webview-ui/src/components/settings/providers/Gemini.tsx b/webview-ui/src/components/settings/providers/Gemini.tsx
index aed016204f..0273670645 100644
--- a/webview-ui/src/components/settings/providers/Gemini.tsx
+++ b/webview-ui/src/components/settings/providers/Gemini.tsx
@@ -4,7 +4,7 @@ import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import type { ProviderSettings } from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { inputEventTransform } from "../transforms"
@@ -15,7 +15,7 @@ type GeminiProps = {
}
export const Gemini = ({ apiConfiguration, setApiConfigurationField }: GeminiProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [googleGeminiBaseUrlSelected, setGoogleGeminiBaseUrlSelected] = useState(
!!apiConfiguration?.googleGeminiBaseUrl,
diff --git a/webview-ui/src/components/settings/providers/IOIntelligence.tsx b/webview-ui/src/components/settings/providers/IOIntelligence.tsx
index 07a3a1d8bc..dc21dfa097 100644
--- a/webview-ui/src/components/settings/providers/IOIntelligence.tsx
+++ b/webview-ui/src/components/settings/providers/IOIntelligence.tsx
@@ -12,7 +12,7 @@ import {
} from "@roo-code/types"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { vscode } from "@src/utils/vscode"
import { ModelPicker } from "../ModelPicker"
@@ -35,7 +35,7 @@ export const IOIntelligence = ({
modelValidationError,
simplifySettings,
}: IOIntelligenceProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const handleInputChange = useCallback(
(
diff --git a/webview-ui/src/components/settings/providers/Kenari.tsx b/webview-ui/src/components/settings/providers/Kenari.tsx
index e8d2f5cdb9..dd1850c0d1 100644
--- a/webview-ui/src/components/settings/providers/Kenari.tsx
+++ b/webview-ui/src/components/settings/providers/Kenari.tsx
@@ -9,7 +9,7 @@ import {
providerIdentifiers,
} from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { inputEventTransform } from "../transforms"
@@ -32,7 +32,7 @@ export const Kenari = ({
modelValidationError,
simplifySettings,
}: KenariProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const handleInputChange = useCallback(
(
diff --git a/webview-ui/src/components/settings/providers/KimiCode.tsx b/webview-ui/src/components/settings/providers/KimiCode.tsx
index a0350c9471..5bdd712e01 100644
--- a/webview-ui/src/components/settings/providers/KimiCode.tsx
+++ b/webview-ui/src/components/settings/providers/KimiCode.tsx
@@ -11,7 +11,7 @@ import {
RouterModelsMessageType,
} from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { useRouterModels } from "@src/components/ui/hooks/useRouterModels"
import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
import { vscode } from "@src/utils/vscode"
@@ -36,7 +36,7 @@ export const KimiCode = ({
kimiCodeIsAuthenticated = false,
kimiCodeOAuthState,
}: KimiCodeProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const authMethod = apiConfiguration.kimiCodeAuthMethod ?? "oauth"
const { data, refetch, isFetching } = useRouterModels({
provider: providerIdentifiers.kimiCode,
diff --git a/webview-ui/src/components/settings/providers/LMStudio.tsx b/webview-ui/src/components/settings/providers/LMStudio.tsx
index 4ae20b939f..6f73c0cad2 100644
--- a/webview-ui/src/components/settings/providers/LMStudio.tsx
+++ b/webview-ui/src/components/settings/providers/LMStudio.tsx
@@ -1,6 +1,5 @@
import { useCallback, useState, useMemo, useEffect, useRef } from "react"
import { useEvent } from "react-use"
-import { Trans } from "react-i18next"
import { Checkbox } from "vscrui"
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
@@ -12,7 +11,7 @@ import {
providerIdentifiers,
} from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { Trans, useTranslation } from "react-i18next"
import { requestLmStudioModels } from "@src/components/ui/hooks/useLmStudioModels"
import { useRouterModels } from "@src/components/ui/hooks/useRouterModels"
@@ -25,7 +24,7 @@ type LMStudioProps = {
}
export const LMStudio = ({ apiConfiguration, setApiConfigurationField }: LMStudioProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [lmStudioModels, setLmStudioModels] = useState({})
const routerModels = useRouterModels({ provider: providerIdentifiers.lmstudio })
@@ -158,6 +157,7 @@ export const LMStudio = ({ apiConfiguration, setApiConfigurationField }: LMStudi
)}
,
diff --git a/webview-ui/src/components/settings/providers/LiteLLM.tsx b/webview-ui/src/components/settings/providers/LiteLLM.tsx
index d7a12249e7..13acc74446 100644
--- a/webview-ui/src/components/settings/providers/LiteLLM.tsx
+++ b/webview-ui/src/components/settings/providers/LiteLLM.tsx
@@ -16,7 +16,7 @@ import { RouterName } from "@roo/api"
import { vscode } from "@src/utils/vscode"
import { useExtensionState } from "@src/context/ExtensionStateContext"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { Button } from "@src/components/ui"
import { inputEventTransform } from "../transforms"
@@ -44,7 +44,7 @@ export const LiteLLM = ({
modelValidationError,
simplifySettings,
}: LiteLLMProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const queryClient = useQueryClient()
const { routerModels } = useExtensionState()
const [refreshStatus, setRefreshStatus] = useState(RefreshStatus.Idle)
diff --git a/webview-ui/src/components/settings/providers/Mimo.tsx b/webview-ui/src/components/settings/providers/Mimo.tsx
index 9060b837d3..64aea0bcf3 100644
--- a/webview-ui/src/components/settings/providers/Mimo.tsx
+++ b/webview-ui/src/components/settings/providers/Mimo.tsx
@@ -3,7 +3,7 @@ import { VSCodeTextField, VSCodeDropdown, VSCodeOption } from "@vscode/webview-u
import type { ProviderSettings } from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { inputEventTransform } from "../transforms"
@@ -15,7 +15,7 @@ type MimoProps = {
}
export const Mimo = ({ apiConfiguration, setApiConfigurationField }: MimoProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const handleInputChange = useCallback(
(
diff --git a/webview-ui/src/components/settings/providers/MiniMax.tsx b/webview-ui/src/components/settings/providers/MiniMax.tsx
index 4055be7d17..ee24f4e0ee 100644
--- a/webview-ui/src/components/settings/providers/MiniMax.tsx
+++ b/webview-ui/src/components/settings/providers/MiniMax.tsx
@@ -3,7 +3,7 @@ import { VSCodeTextField, VSCodeDropdown, VSCodeOption } from "@vscode/webview-u
import type { ProviderSettings } from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { inputEventTransform } from "../transforms"
@@ -15,7 +15,7 @@ type MiniMaxProps = {
}
export const MiniMax = ({ apiConfiguration, setApiConfigurationField }: MiniMaxProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const handleInputChange = useCallback(
(
diff --git a/webview-ui/src/components/settings/providers/Mistral.tsx b/webview-ui/src/components/settings/providers/Mistral.tsx
index 84a0154a63..09498c3626 100644
--- a/webview-ui/src/components/settings/providers/Mistral.tsx
+++ b/webview-ui/src/components/settings/providers/Mistral.tsx
@@ -3,7 +3,7 @@ import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { type ProviderSettings, type RouterModels, mistralDefaultModelId } from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { inputEventTransform } from "../transforms"
@@ -16,7 +16,7 @@ type MistralProps = {
}
export const Mistral = ({ apiConfiguration, setApiConfigurationField }: MistralProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const handleInputChange = useCallback(
(
diff --git a/webview-ui/src/components/settings/providers/Moonshot.tsx b/webview-ui/src/components/settings/providers/Moonshot.tsx
index 6e7d5aac33..4629e5cd46 100644
--- a/webview-ui/src/components/settings/providers/Moonshot.tsx
+++ b/webview-ui/src/components/settings/providers/Moonshot.tsx
@@ -13,7 +13,7 @@ import {
import { RouterName } from "@roo/api"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { vscode } from "@src/utils/vscode"
@@ -37,7 +37,7 @@ enum RefreshStatus {
}
export const Moonshot = ({ apiConfiguration, setApiConfigurationField, simplifySettings }: MoonshotProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const { routerModels } = useExtensionState()
const queryClient = useQueryClient()
const [refreshStatus, setRefreshStatus] = useState(RefreshStatus.Idle)
diff --git a/webview-ui/src/components/settings/providers/NanoGPT.tsx b/webview-ui/src/components/settings/providers/NanoGPT.tsx
index 11e0005604..e7ec8bd27b 100644
--- a/webview-ui/src/components/settings/providers/NanoGPT.tsx
+++ b/webview-ui/src/components/settings/providers/NanoGPT.tsx
@@ -15,7 +15,7 @@ import {
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { vscode } from "@src/utils/vscode"
import { ModelPicker } from "../ModelPicker"
@@ -48,7 +48,7 @@ export const NanoGPT = ({
modelValidationError,
simplifySettings,
}: NanoGPTProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const routingPreference = apiConfiguration.nanoGptRoutingPreference ?? nanoGptDefaultRoutingPreference
const handleInputChange = useCallback(
diff --git a/webview-ui/src/components/settings/providers/Ollama.tsx b/webview-ui/src/components/settings/providers/Ollama.tsx
index 80a8bf5904..20fa9dc38e 100644
--- a/webview-ui/src/components/settings/providers/Ollama.tsx
+++ b/webview-ui/src/components/settings/providers/Ollama.tsx
@@ -11,7 +11,7 @@ import {
providerIdentifiers,
} from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { useRouterModels } from "@src/components/ui/hooks/useRouterModels"
import { Button } from "@src/components/ui"
import { vscode } from "@src/utils/vscode"
@@ -33,7 +33,7 @@ enum RefreshStatus {
}
export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [ollamaModels, setOllamaModels] = useState({})
const [refreshStatus, setRefreshStatus] = useState(RefreshStatus.Idle)
diff --git a/webview-ui/src/components/settings/providers/OpenAI.tsx b/webview-ui/src/components/settings/providers/OpenAI.tsx
index 460019e75a..c23df2074d 100644
--- a/webview-ui/src/components/settings/providers/OpenAI.tsx
+++ b/webview-ui/src/components/settings/providers/OpenAI.tsx
@@ -4,7 +4,7 @@ import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { OpenAiServiceTier, type ModelInfo, type ProviderSettings } from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StandardTooltip } from "@src/components/ui"
@@ -18,7 +18,7 @@ type OpenAIProps = {
}
export const OpenAI = ({ apiConfiguration, setApiConfigurationField, selectedModelInfo }: OpenAIProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [openAiNativeBaseUrlSelected, setOpenAiNativeBaseUrlSelected] = useState(
!!apiConfiguration?.openAiNativeBaseUrl,
diff --git a/webview-ui/src/components/settings/providers/OpenAICodex.tsx b/webview-ui/src/components/settings/providers/OpenAICodex.tsx
index ee2abf0e2b..b2d259a914 100644
--- a/webview-ui/src/components/settings/providers/OpenAICodex.tsx
+++ b/webview-ui/src/components/settings/providers/OpenAICodex.tsx
@@ -7,7 +7,7 @@ import {
openAiCodexModels,
} from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { Button } from "@src/components/ui"
import { vscode } from "@src/utils/vscode"
@@ -28,7 +28,7 @@ export const OpenAICodex: React.FC = ({
simplifySettings,
openAiCodexIsAuthenticated = false,
}) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
return (
diff --git a/webview-ui/src/components/settings/providers/OpenAICodexRateLimitDashboard.tsx b/webview-ui/src/components/settings/providers/OpenAICodexRateLimitDashboard.tsx
index d9946d1cf6..323278cbe6 100644
--- a/webview-ui/src/components/settings/providers/OpenAICodexRateLimitDashboard.tsx
+++ b/webview-ui/src/components/settings/providers/OpenAICodexRateLimitDashboard.tsx
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from "react"
import type { OpenAiCodexRateLimitInfo } from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { vscode } from "@src/utils/vscode"
interface OpenAICodexRateLimitDashboardProps {
@@ -85,7 +85,7 @@ const UsageProgressBar: React.FC<{ usedPercent: number; label?: string }> = ({ u
}
export const OpenAICodexRateLimitDashboard: React.FC
= ({ isAuthenticated }) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [rateLimits, setRateLimits] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(null)
diff --git a/webview-ui/src/components/settings/providers/OpenAICodexSpeedSelector.tsx b/webview-ui/src/components/settings/providers/OpenAICodexSpeedSelector.tsx
index 09faf5cd8d..4ca1671ac3 100644
--- a/webview-ui/src/components/settings/providers/OpenAICodexSpeedSelector.tsx
+++ b/webview-ui/src/components/settings/providers/OpenAICodexSpeedSelector.tsx
@@ -5,7 +5,7 @@ import {
type OpenAiCodexServiceTier as OpenAiCodexServiceTierValue,
} from "@roo-code/types/model"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StandardTooltip } from "@src/components/ui"
interface OpenAICodexSpeedSelectorProps {
@@ -14,7 +14,7 @@ interface OpenAICodexSpeedSelectorProps {
}
export const OpenAICodexSpeedSelector: React.FC = ({ value, onValueChange }) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const selectId = React.useId()
return (
diff --git a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx
index efb839a7fc..0f0ce940e3 100644
--- a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx
+++ b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx
@@ -17,7 +17,7 @@ import {
parseOpenAiExtraBody,
} from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { Button, StandardTooltip } from "@src/components/ui"
import { formatOpenAiExtraBodyValidationError } from "@src/utils/validate"
@@ -46,7 +46,7 @@ export const OpenAICompatible = ({
modelValidationError,
simplifySettings,
}: OpenAICompatibleProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const isAzureOpenAi = isAzureOpenAiBaseUrl(apiConfiguration?.openAiBaseUrl, apiConfiguration?.openAiUseAzure)
const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion)
diff --git a/webview-ui/src/components/settings/providers/OpenCodeGo.tsx b/webview-ui/src/components/settings/providers/OpenCodeGo.tsx
index 249e2de534..dad263ae22 100644
--- a/webview-ui/src/components/settings/providers/OpenCodeGo.tsx
+++ b/webview-ui/src/components/settings/providers/OpenCodeGo.tsx
@@ -14,7 +14,7 @@ import {
import type { RouterName } from "@roo/api"
import { vscode } from "@src/utils/vscode"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { Button } from "@src/components/ui"
@@ -45,7 +45,7 @@ export const OpenCodeGo = ({
modelValidationError,
simplifySettings,
}: OpenCodeGoProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [refreshStatus, setRefreshStatus] = useState(RefreshStatus.Idle)
const [refreshError, setRefreshError] = useState()
const errorJustReceived = useRef(false)
diff --git a/webview-ui/src/components/settings/providers/OpenRouter.tsx b/webview-ui/src/components/settings/providers/OpenRouter.tsx
index 2dba8c8459..4204b1dce0 100644
--- a/webview-ui/src/components/settings/providers/OpenRouter.tsx
+++ b/webview-ui/src/components/settings/providers/OpenRouter.tsx
@@ -9,7 +9,7 @@ import {
openRouterDefaultModelId,
} from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { getOpenRouterAuthUrl } from "@src/oauth/urls"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
@@ -38,7 +38,7 @@ export const OpenRouter = ({
organizationAllowList,
modelValidationError,
}: OpenRouterProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl)
diff --git a/webview-ui/src/components/settings/providers/Poe.tsx b/webview-ui/src/components/settings/providers/Poe.tsx
index ef4cc51d4f..847814e749 100644
--- a/webview-ui/src/components/settings/providers/Poe.tsx
+++ b/webview-ui/src/components/settings/providers/Poe.tsx
@@ -14,7 +14,7 @@ import {
import { RouterName } from "@roo/api"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { vscode } from "@src/utils/vscode"
@@ -46,7 +46,7 @@ export const Poe = ({
modelValidationError,
simplifySettings,
}: PoeProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const queryClient = useQueryClient()
const { routerModels } = useExtensionState()
const [refreshStatus, setRefreshStatus] = useState(RefreshStatus.Idle)
diff --git a/webview-ui/src/components/settings/providers/Requesty.tsx b/webview-ui/src/components/settings/providers/Requesty.tsx
index 4149dbc2c8..a021d2a909 100644
--- a/webview-ui/src/components/settings/providers/Requesty.tsx
+++ b/webview-ui/src/components/settings/providers/Requesty.tsx
@@ -11,7 +11,7 @@ import {
} from "@roo-code/types"
import { vscode } from "@src/utils/vscode"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { Button } from "@src/components/ui"
import { inputEventTransform } from "../transforms"
@@ -40,7 +40,7 @@ export const Requesty = ({
uriScheme,
simplifySettings,
}: RequestyProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [requestyEndpointSelected, setRequestyEndpointSelected] = useState(!!apiConfiguration.requestyBaseUrl)
diff --git a/webview-ui/src/components/settings/providers/SambaNova.tsx b/webview-ui/src/components/settings/providers/SambaNova.tsx
index 9202f8d06e..a4b34ff190 100644
--- a/webview-ui/src/components/settings/providers/SambaNova.tsx
+++ b/webview-ui/src/components/settings/providers/SambaNova.tsx
@@ -3,7 +3,7 @@ import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import type { ProviderSettings } from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { inputEventTransform } from "../transforms"
@@ -14,7 +14,7 @@ type SambaNovaProps = {
}
export const SambaNova = ({ apiConfiguration, setApiConfigurationField }: SambaNovaProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const handleInputChange = useCallback(
(
diff --git a/webview-ui/src/components/settings/providers/Unbound.tsx b/webview-ui/src/components/settings/providers/Unbound.tsx
index b3211f0f8f..2e9c644b32 100644
--- a/webview-ui/src/components/settings/providers/Unbound.tsx
+++ b/webview-ui/src/components/settings/providers/Unbound.tsx
@@ -11,7 +11,7 @@ import {
} from "@roo-code/types"
import { vscode } from "@src/utils/vscode"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { Button } from "@src/components/ui"
import { inputEventTransform } from "../transforms"
@@ -35,7 +35,7 @@ export const Unbound = ({
modelValidationError,
simplifySettings,
}: UnboundProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const handleInputChange = useCallback(
(
diff --git a/webview-ui/src/components/settings/providers/VSCodeLM.tsx b/webview-ui/src/components/settings/providers/VSCodeLM.tsx
index b14c38c9a7..8ea8778b8f 100644
--- a/webview-ui/src/components/settings/providers/VSCodeLM.tsx
+++ b/webview-ui/src/components/settings/providers/VSCodeLM.tsx
@@ -11,7 +11,7 @@ import {
providerIdentifiers,
} from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { Button, StandardTooltip } from "@src/components/ui"
import { vscode } from "@src/utils/vscode"
import { useGitHubCopilotModels } from "@src/components/ui/hooks/useGitHubCopilotModels"
@@ -31,7 +31,7 @@ type VSCodeLMProps = {
const NO_MODELS: NonNullable = []
export const VSCodeLM = ({ apiConfiguration, setApiConfigurationField }: VSCodeLMProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const isCopilot = apiConfiguration.apiProvider === providerIdentifiers.githubCopilot
// The legacy provider is fed by messages into local state; Copilot's list is the shared, extension-pushed one.
diff --git a/webview-ui/src/components/settings/providers/VercelAiGateway.tsx b/webview-ui/src/components/settings/providers/VercelAiGateway.tsx
index 0951f39c57..2d993ccc37 100644
--- a/webview-ui/src/components/settings/providers/VercelAiGateway.tsx
+++ b/webview-ui/src/components/settings/providers/VercelAiGateway.tsx
@@ -9,7 +9,7 @@ import {
providerIdentifiers,
} from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { inputEventTransform } from "../transforms"
@@ -32,7 +32,7 @@ export const VercelAiGateway = ({
modelValidationError,
simplifySettings,
}: VercelAiGatewayProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const handleInputChange = useCallback(
(
diff --git a/webview-ui/src/components/settings/providers/Vertex.tsx b/webview-ui/src/components/settings/providers/Vertex.tsx
index 32f833ffbf..4f77b4da19 100644
--- a/webview-ui/src/components/settings/providers/Vertex.tsx
+++ b/webview-ui/src/components/settings/providers/Vertex.tsx
@@ -1,11 +1,10 @@
import { useCallback, useMemo } from "react"
-import { Trans } from "react-i18next"
import { Checkbox } from "vscrui"
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { type ProviderSettings, VERTEX_REGIONS, VERTEX_1M_CONTEXT_MODEL_IDS, looksLikeFilePath } from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { Trans, useTranslation } from "react-i18next"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
import { inputEventTransform } from "../transforms"
@@ -16,7 +15,7 @@ type VertexProps = {
}
export const Vertex = ({ apiConfiguration, setApiConfigurationField }: VertexProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
// Check if the selected model supports 1M context (supported Claude 4 models)
const supports1MContextBeta =
@@ -80,6 +79,7 @@ export const Vertex = ({ apiConfiguration, setApiConfigurationField }: VertexPro
role="status"
className="text-sm text-vscode-errorForeground">
,
diff --git a/webview-ui/src/components/settings/providers/XAI.tsx b/webview-ui/src/components/settings/providers/XAI.tsx
index 619f223901..b9fb04322c 100644
--- a/webview-ui/src/components/settings/providers/XAI.tsx
+++ b/webview-ui/src/components/settings/providers/XAI.tsx
@@ -3,7 +3,7 @@ import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import type { ProviderSettings } from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { inputEventTransform } from "../transforms"
@@ -14,7 +14,7 @@ type XAIProps = {
}
export const XAI = ({ apiConfiguration, setApiConfigurationField }: XAIProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const handleInputChange = useCallback(
(
diff --git a/webview-ui/src/components/settings/providers/ZAi.tsx b/webview-ui/src/components/settings/providers/ZAi.tsx
index 3dab8964ff..04114840a8 100644
--- a/webview-ui/src/components/settings/providers/ZAi.tsx
+++ b/webview-ui/src/components/settings/providers/ZAi.tsx
@@ -3,7 +3,7 @@ import { VSCodeTextField, VSCodeDropdown, VSCodeOption } from "@vscode/webview-u
import { type ProviderSettings, zaiApiLineConfigs, zaiApiLineSchema } from "@roo-code/types"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { inputEventTransform } from "../transforms"
@@ -15,7 +15,7 @@ type ZAiProps = {
}
export const ZAi = ({ apiConfiguration, setApiConfigurationField }: ZAiProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const handleInputChange = useCallback(
(
diff --git a/webview-ui/src/components/settings/providers/ZooGateway.tsx b/webview-ui/src/components/settings/providers/ZooGateway.tsx
index bd2b35274c..ee87e8d6e2 100644
--- a/webview-ui/src/components/settings/providers/ZooGateway.tsx
+++ b/webview-ui/src/components/settings/providers/ZooGateway.tsx
@@ -9,7 +9,7 @@ import {
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { getZooCodeAuthUrl } from "@src/oauth/urls"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { ModelPicker } from "../ModelPicker"
@@ -57,7 +57,7 @@ export const ZooGateway = ({
modelValidationError,
simplifySettings,
}: ZooGatewayProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const { zooCodeIsAuthenticated, zooCodeUserEmail, zooCodeUserName, zooCodeBaseUrl, uriScheme, deviceName } =
useExtensionState()
diff --git a/webview-ui/src/components/settings/providers/__tests__/Bedrock.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Bedrock.spec.tsx
index 307696fdd9..298b740a1a 100644
--- a/webview-ui/src/components/settings/providers/__tests__/Bedrock.spec.tsx
+++ b/webview-ui/src/components/settings/providers/__tests__/Bedrock.spec.tsx
@@ -53,8 +53,8 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({
}))
// Mock the translation hook
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/settings/providers/__tests__/Friendli.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Friendli.spec.tsx
index 5ccab1c7bb..c7517c0a0d 100644
--- a/webview-ui/src/components/settings/providers/__tests__/Friendli.spec.tsx
+++ b/webview-ui/src/components/settings/providers/__tests__/Friendli.spec.tsx
@@ -5,8 +5,8 @@ import type { ProviderSettings } from "@roo-code/types"
import { Friendli } from "../Friendli"
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/settings/providers/__tests__/Gemini.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Gemini.spec.tsx
index 70c762dd35..062b56e7b9 100644
--- a/webview-ui/src/components/settings/providers/__tests__/Gemini.spec.tsx
+++ b/webview-ui/src/components/settings/providers/__tests__/Gemini.spec.tsx
@@ -20,8 +20,8 @@ vi.mock("vscrui", () => ({
),
}))
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({ t: (key: string) => key }),
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({ t: (key: string) => key }),
}))
vi.mock("@src/components/common/VSCodeButtonLink", () => ({
diff --git a/webview-ui/src/components/settings/providers/__tests__/IOIntelligence.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/IOIntelligence.spec.tsx
index ae6f7bc63a..fa660ae560 100644
--- a/webview-ui/src/components/settings/providers/__tests__/IOIntelligence.spec.tsx
+++ b/webview-ui/src/components/settings/providers/__tests__/IOIntelligence.spec.tsx
@@ -15,8 +15,8 @@ const { postMessageMock } = vi.hoisted(() => ({ postMessageMock: vi.fn() }))
vi.mock("@src/utils/vscode", () => ({ vscode: { postMessage: postMessageMock } }))
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({ t: (key: string) => key }),
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({ t: (key: string) => key }),
}))
vi.mock("@vscode/webview-ui-toolkit/react", () => ({
diff --git a/webview-ui/src/components/settings/providers/__tests__/Kenari.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Kenari.spec.tsx
index e9fb7ce34c..a5e2baa4ff 100644
--- a/webview-ui/src/components/settings/providers/__tests__/Kenari.spec.tsx
+++ b/webview-ui/src/components/settings/providers/__tests__/Kenari.spec.tsx
@@ -14,8 +14,8 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({
),
}))
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({ t: (key: string) => key }),
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({ t: (key: string) => key }),
}))
vi.mock("@src/components/common/VSCodeButtonLink", () => ({
diff --git a/webview-ui/src/components/settings/providers/__tests__/LiteLLM.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/LiteLLM.spec.tsx
index c6cd779658..a4c13af26e 100644
--- a/webview-ui/src/components/settings/providers/__tests__/LiteLLM.spec.tsx
+++ b/webview-ui/src/components/settings/providers/__tests__/LiteLLM.spec.tsx
@@ -23,8 +23,8 @@ vi.mock("@src/context/ExtensionStateContext", () => ({
useExtensionState: mockUseExtensionState,
}))
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({ t: (key: string) => key }),
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({ t: (key: string) => key }),
}))
vi.mock("@vscode/webview-ui-toolkit/react", () => ({
diff --git a/webview-ui/src/components/settings/providers/__tests__/Moonshot.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Moonshot.spec.tsx
index 5c5d1b41b9..ce7628e144 100644
--- a/webview-ui/src/components/settings/providers/__tests__/Moonshot.spec.tsx
+++ b/webview-ui/src/components/settings/providers/__tests__/Moonshot.spec.tsx
@@ -8,8 +8,8 @@ import { allRouterModelsProvider, providerIdentifiers, type ProviderSettings } f
import { Moonshot } from "../Moonshot"
// Mock the translation hook
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/settings/providers/__tests__/NanoGPT.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/NanoGPT.spec.tsx
index 698fa90e29..78cde7b810 100644
--- a/webview-ui/src/components/settings/providers/__tests__/NanoGPT.spec.tsx
+++ b/webview-ui/src/components/settings/providers/__tests__/NanoGPT.spec.tsx
@@ -16,8 +16,8 @@ const { postMessageMock } = vi.hoisted(() => ({ postMessageMock: vi.fn() }))
vi.mock("@src/utils/vscode", () => ({ vscode: { postMessage: postMessageMock } }))
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({ t: (key: string) => key }),
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({ t: (key: string) => key }),
}))
vi.mock("@vscode/webview-ui-toolkit/react", () => ({
diff --git a/webview-ui/src/components/settings/providers/__tests__/Ollama.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Ollama.spec.tsx
index a4e14b4d81..606779e7d0 100644
--- a/webview-ui/src/components/settings/providers/__tests__/Ollama.spec.tsx
+++ b/webview-ui/src/components/settings/providers/__tests__/Ollama.spec.tsx
@@ -37,8 +37,8 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({
}))
// Mock the translation hook
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/settings/providers/__tests__/Ollama.visual.fixture.tsx b/webview-ui/src/components/settings/providers/__tests__/Ollama.visual.fixture.tsx
index 49cfdeb783..7769676957 100644
--- a/webview-ui/src/components/settings/providers/__tests__/Ollama.visual.fixture.tsx
+++ b/webview-ui/src/components/settings/providers/__tests__/Ollama.visual.fixture.tsx
@@ -1,12 +1,11 @@
/* v8 ignore file -- Playwright component fixture is covered by the visual test. */
import React, { useState } from "react"
-import i18next from "i18next"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
+import { I18nextProvider } from "react-i18next"
import { providerIdentifiers, type ProviderSettings } from "@roo-code/types"
-import { TranslationContext as AppTranslationContext } from "@/i18n/TranslationContext"
-import { TranslationContext as PlaywrightTranslationContext } from "@src/i18n/TranslationContext"
+import { createFixtureI18n } from "@src/i18n/__tests__/createFixtureI18n.visual.i18n"
import { TooltipProvider } from "@src/components/ui/tooltip"
import { Ollama } from "../Ollama"
import enSettings from "@/i18n/locales/en/settings.json"
@@ -24,10 +23,7 @@ function flattenTranslations(obj: Record, prefix = "settings:")
return result
}
-// An uninitialized instance is a valid `i18n` for the context. The stories read `t`.
-const i18n = i18next.createInstance()
-
-const translations = flattenTranslations(enSettings as Record)
+const i18n = createFixtureI18n(flattenTranslations(enSettings as Record))
const apiConfiguration: ProviderSettings = {
apiProvider: providerIdentifiers.ollama,
@@ -45,19 +41,9 @@ const Providers = ({ children }: React.PropsWithChildren) => {
)
return (
- translations[key] ?? key,
- i18n,
- }}>
- translations[key] ?? key,
- i18n,
- }}>
- {children}
-
-
+
+ {children}
+
)
}
diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenAI.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenAI.spec.tsx
index 239c76932f..a32276dc6f 100644
--- a/webview-ui/src/components/settings/providers/__tests__/OpenAI.spec.tsx
+++ b/webview-ui/src/components/settings/providers/__tests__/OpenAI.spec.tsx
@@ -6,8 +6,8 @@ import { fireEvent, render, screen } from "@/utils/test-utils"
import { OpenAI } from "../OpenAI"
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({ t: (key: string) => key }),
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({ t: (key: string) => key }),
}))
vi.mock("vscrui", () => ({
diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.spec.tsx
index d73120ada3..355a1ea1ea 100644
--- a/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.spec.tsx
+++ b/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.spec.tsx
@@ -12,8 +12,8 @@ import { vscode } from "@src/utils/vscode"
import { OpenAICodex } from "../OpenAICodex"
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) =>
({
"settings:openAiCodexSpeed.label": "Speed",
diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.visual.fixture.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.visual.fixture.tsx
index 2061f00614..cf96f0d377 100644
--- a/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.visual.fixture.tsx
+++ b/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.visual.fixture.tsx
@@ -3,7 +3,9 @@ import React from "react"
import { OpenAiCodexServiceTier } from "@roo-code/types/model"
-import { TranslationContext } from "@src/i18n/TranslationContext"
+import { I18nextProvider } from "react-i18next"
+
+import { createFixtureI18n } from "@src/i18n/__tests__/createFixtureI18n.visual.i18n"
import { TooltipProvider } from "@src/components/ui/tooltip"
import { OpenAICodexSpeedSelector } from "../OpenAICodexSpeedSelector"
@@ -16,17 +18,15 @@ const translations: Record = {
"settings:openAiCodexSpeed.fast": "Fast (1.5x speed, increased usage)",
}
+const i18n = createFixtureI18n(translations)
+
export const OpenAICodexFixture = () => (
- translations[key] ?? key,
- i18n: null as unknown as typeof import("../../../../i18n/setup").default,
- }}>
+
{}} />
{}} />
-
+
)
diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenAICodexRateLimitDashboard.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenAICodexRateLimitDashboard.spec.tsx
index 68145afb84..eb8eae9641 100644
--- a/webview-ui/src/components/settings/providers/__tests__/OpenAICodexRateLimitDashboard.spec.tsx
+++ b/webview-ui/src/components/settings/providers/__tests__/OpenAICodexRateLimitDashboard.spec.tsx
@@ -2,8 +2,8 @@ import { render, screen, waitFor } from "@/utils/test-utils"
import { OpenAICodexRateLimitDashboard } from "../OpenAICodexRateLimitDashboard"
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string, options?: Record) => {
switch (key) {
case "settings:providers.openAiCodexRateLimits.title":
diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx
index ce7337b67a..c65c16d965 100644
--- a/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx
+++ b/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx
@@ -61,8 +61,8 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({
}))
// Mock the translation hook
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.visual.fixture.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.visual.fixture.tsx
index 9e0aeb71bc..990a2992f2 100644
--- a/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.visual.fixture.tsx
+++ b/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.visual.fixture.tsx
@@ -4,8 +4,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { providerIdentifiers, type ProviderSettings } from "@roo-code/types"
-import { TranslationContext as AppTranslationContext } from "@/i18n/TranslationContext"
-import { TranslationContext as PlaywrightTranslationContext } from "@src/i18n/TranslationContext"
+import { I18nextProvider } from "react-i18next"
+
+import { createFixtureI18n } from "@src/i18n/__tests__/createFixtureI18n.visual.i18n"
import { TooltipProvider } from "@src/components/ui/tooltip"
import { OpenAICompatible } from "../OpenAICompatible"
import enSettings from "@/i18n/locales/en/settings.json"
@@ -23,7 +24,7 @@ function flattenTranslations(obj: Record, prefix = "settings:")
return result
}
-const translations = flattenTranslations(enSettings as Record)
+const i18n = createFixtureI18n(flattenTranslations(enSettings as Record))
const apiConfiguration: ProviderSettings = {
apiProvider: providerIdentifiers.openai,
@@ -48,19 +49,9 @@ const Providers = ({ children }: React.PropsWithChildren) => {
)
return (
- translations[key] ?? key,
- i18n: null as unknown as typeof import("../../../../i18n/setup").default,
- }}>
- translations[key] ?? key,
- i18n: null as unknown as typeof import("../../../../i18n/setup").default,
- }}>
- {children}
-
-
+
+ {children}
+
)
}
diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenCodeGo.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenCodeGo.spec.tsx
index 9fcfd2f102..6a42603374 100644
--- a/webview-ui/src/components/settings/providers/__tests__/OpenCodeGo.spec.tsx
+++ b/webview-ui/src/components/settings/providers/__tests__/OpenCodeGo.spec.tsx
@@ -15,8 +15,8 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({
),
}))
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({ t: (key: string) => key }),
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({ t: (key: string) => key }),
}))
vi.mock("@src/components/common/VSCodeButtonLink", () => ({
diff --git a/webview-ui/src/components/settings/providers/__tests__/Poe.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Poe.spec.tsx
index 34e5d5c2d8..47da5d7a64 100644
--- a/webview-ui/src/components/settings/providers/__tests__/Poe.spec.tsx
+++ b/webview-ui/src/components/settings/providers/__tests__/Poe.spec.tsx
@@ -23,8 +23,8 @@ vi.mock("@src/context/ExtensionStateContext", () => ({
useExtensionState: mockUseExtensionState,
}))
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({ t: (key: string) => key }),
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({ t: (key: string) => key }),
}))
vi.mock("@vscode/webview-ui-toolkit/react", () => ({
diff --git a/webview-ui/src/components/settings/providers/__tests__/ProviderRouting.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/ProviderRouting.spec.tsx
index d165ead136..077748cc38 100644
--- a/webview-ui/src/components/settings/providers/__tests__/ProviderRouting.spec.tsx
+++ b/webview-ui/src/components/settings/providers/__tests__/ProviderRouting.spec.tsx
@@ -21,8 +21,8 @@ const { modelPickerMock } = vi.hoisted(() => ({
modelPickerMock: vi.fn((_props: React.ComponentProps) => null),
}))
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string, options?: { account?: string }) =>
key === "settings:providers.githubCopilot.signedIn" ? `Signed in as ${options?.account}` : key,
}),
diff --git a/webview-ui/src/components/settings/providers/__tests__/Requesty.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Requesty.spec.tsx
index bb7d4e234f..52302c6bfd 100644
--- a/webview-ui/src/components/settings/providers/__tests__/Requesty.spec.tsx
+++ b/webview-ui/src/components/settings/providers/__tests__/Requesty.spec.tsx
@@ -6,8 +6,8 @@ import { vscode } from "@src/utils/vscode"
import { Requesty } from "../Requesty"
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({ t: (key: string) => key }),
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({ t: (key: string) => key }),
}))
vi.mock("@vscode/webview-ui-toolkit/react", () => ({
diff --git a/webview-ui/src/components/settings/providers/__tests__/Vertex.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Vertex.spec.tsx
index 93b682b86d..a1e1cca4fc 100644
--- a/webview-ui/src/components/settings/providers/__tests__/Vertex.spec.tsx
+++ b/webview-ui/src/components/settings/providers/__tests__/Vertex.spec.tsx
@@ -23,16 +23,8 @@ vi.mock("vscrui", () => ({
),
}))
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({ t: (key: string) => key }),
-}))
-
-// The component uses for the path-shape warning so it can interpolate
-// / elements. Resolve the i18n key against the real English
-// resource so the test fails if the warning copy drops the "Google Cloud Key
-// File Path" field name or the "GOOGLE_APPLICATION_CREDENTIALS" env var
-// mention — those are the actual remediation hints users need.
vi.mock("react-i18next", () => ({
+ useTranslation: () => ({ t: (key: string) => key }),
Trans: ({ i18nKey }: { i18nKey: string }) => {
// Keys are ":"; the spec only renders the
// settings namespace so resolve against the imported English bundle.
@@ -48,6 +40,12 @@ vi.mock("react-i18next", () => ({
},
}))
+// The component uses for the path-shape warning so it can interpolate
+// / elements. Resolve the i18n key against the real English
+// resource so the test fails if the warning copy drops the "Google Cloud Key
+// File Path" field name or the "GOOGLE_APPLICATION_CREDENTIALS" env var
+// mention — those are the actual remediation hints users need.
+
vi.mock("@src/components/ui", () => ({
Select: ({ children, value, onValueChange }: any) => (
diff --git a/webview-ui/src/components/settings/providers/__tests__/ZooGateway.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/ZooGateway.spec.tsx
index 4f063b57ba..46341fdcf8 100644
--- a/webview-ui/src/components/settings/providers/__tests__/ZooGateway.spec.tsx
+++ b/webview-ui/src/components/settings/providers/__tests__/ZooGateway.spec.tsx
@@ -5,8 +5,8 @@ import type { ModelInfo, ProviderSettings, RouterModels } from "@roo-code/types"
import { ZooGateway, pickZooGatewayDefaultModelId } from "../ZooGateway"
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
t: (key: string) => key,
}),
}))
diff --git a/webview-ui/src/components/welcome/RooTips.tsx b/webview-ui/src/components/welcome/RooTips.tsx
index b180a53c26..ec61515f84 100644
--- a/webview-ui/src/components/welcome/RooTips.tsx
+++ b/webview-ui/src/components/welcome/RooTips.tsx
@@ -1,6 +1,5 @@
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
-import { useTranslation } from "react-i18next"
-import { Trans } from "react-i18next"
+import { Trans, useTranslation } from "react-i18next"
import { buildDocLink } from "@src/utils/docLinks"
import { ReplaceAll, Users } from "lucide-react"
@@ -26,7 +25,7 @@ const RooTips = () => {
return (
-
+
{tips.map((tip) => (
@@ -43,6 +42,7 @@ const RooTips = () => {
@@ -22,7 +20,7 @@ export function WelcomeLanding({ onGetStarted, onImportSettings }: WelcomeLandin
diff --git a/webview-ui/src/components/welcome/WelcomeViewProvider.tsx b/webview-ui/src/components/welcome/WelcomeViewProvider.tsx
index 3a36b3634e..9a77077cdb 100644
--- a/webview-ui/src/components/welcome/WelcomeViewProvider.tsx
+++ b/webview-ui/src/components/welcome/WelcomeViewProvider.tsx
@@ -1,5 +1,4 @@
import { useCallback, useState } from "react"
-import { Trans } from "react-i18next"
import { ArrowLeft, Brain } from "lucide-react"
import { openRouterDefaultModelId, providerIdentifiers, type ProviderSettings } from "@roo-code/types"
@@ -7,7 +6,8 @@ import { openRouterDefaultModelId, providerIdentifiers, type ProviderSettings }
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { validateApiConfiguration } from "@src/utils/validate"
import { vscode } from "@src/utils/vscode"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import type { TFunction } from "i18next"
+import { Trans, useTranslation } from "react-i18next"
import { Button } from "@src/components/ui"
import ApiOptions from "../settings/ApiOptions"
@@ -21,6 +21,7 @@ const DEFAULT_WELCOME_API_CONFIGURATION: ProviderSettings = {
}
const getWelcomeApiConfiguration = (
+ t: TFunction,
apiConfiguration?: ProviderSettings,
zooCodeIsAuthenticated?: boolean,
): ProviderSettings => {
@@ -30,7 +31,7 @@ const getWelcomeApiConfiguration = (
return DEFAULT_WELCOME_API_CONFIGURATION
}
- const validationError = validateApiConfiguration(apiConfiguration, undefined, undefined, zooCodeIsAuthenticated)
+ const validationError = validateApiConfiguration(apiConfiguration, t, undefined, undefined, zooCodeIsAuthenticated)
if (validationError) {
return DEFAULT_WELCOME_API_CONFIGURATION
}
@@ -41,12 +42,12 @@ const getWelcomeApiConfiguration = (
const WelcomeViewProvider = () => {
const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme, zooCodeIsAuthenticated } =
useExtensionState()
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [errorMessage, setErrorMessage] = useState(undefined)
const [showProviderSetup, setShowProviderSetup] = useState(false)
const [welcomeApiConfiguration, setWelcomeApiConfiguration] = useState()
const effectiveApiConfiguration =
- welcomeApiConfiguration ?? getWelcomeApiConfiguration(apiConfiguration, zooCodeIsAuthenticated)
+ welcomeApiConfiguration ?? getWelcomeApiConfiguration(t, apiConfiguration, zooCodeIsAuthenticated)
const setApiConfigurationFieldForApiOptions = useCallback(
(field: K, value: ProviderSettings[K]) => {
@@ -61,7 +62,7 @@ const WelcomeViewProvider = () => {
const handleGetStarted = useCallback(() => {
if (!showProviderSetup) {
- const initialApiConfiguration = getWelcomeApiConfiguration(apiConfiguration, zooCodeIsAuthenticated)
+ const initialApiConfiguration = getWelcomeApiConfiguration(t, apiConfiguration, zooCodeIsAuthenticated)
setWelcomeApiConfiguration(initialApiConfiguration)
setApiConfiguration(initialApiConfiguration)
@@ -70,7 +71,13 @@ const WelcomeViewProvider = () => {
return
}
- const error = validateApiConfiguration(effectiveApiConfiguration, undefined, undefined, zooCodeIsAuthenticated)
+ const error = validateApiConfiguration(
+ effectiveApiConfiguration,
+ t,
+ undefined,
+ undefined,
+ zooCodeIsAuthenticated,
+ )
if (error) {
setErrorMessage(error)
@@ -90,6 +97,7 @@ const WelcomeViewProvider = () => {
effectiveApiConfiguration,
currentApiConfigName,
zooCodeIsAuthenticated,
+ t,
])
if (!showProviderSetup) {
@@ -108,7 +116,7 @@ const WelcomeViewProvider = () => {
{t("welcome:providerSignup.heading")}
-
+
diff --git a/webview-ui/src/components/welcome/__tests__/WelcomeViewProvider.spec.tsx b/webview-ui/src/components/welcome/__tests__/WelcomeViewProvider.spec.tsx
index c15c0608ec..e7e2ef49e8 100644
--- a/webview-ui/src/components/welcome/__tests__/WelcomeViewProvider.spec.tsx
+++ b/webview-ui/src/components/welcome/__tests__/WelcomeViewProvider.spec.tsx
@@ -64,11 +64,10 @@ vi.mock("@src/utils/vscode", () => ({
}))
vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
+ t: (key: string) => key,
+ }),
Trans: ({ i18nKey, children }: any) =>
{children || i18nKey} ,
- initReactI18next: {
- type: "3rdParty",
- init: () => {},
- },
}))
vi.mock("i18next", () => ({
@@ -77,12 +76,6 @@ vi.mock("i18next", () => ({
},
}))
-vi.mock("@src/i18n/TranslationContext", () => ({
- useAppTranslation: () => ({
- t: (key: string) => key,
- }),
-}))
-
// Mock buildDocLink
vi.mock("@/utils/docLinks", () => ({
buildDocLink: (path: string, source: string) => `https://docs.zoocode.dev/${path}?utm_source=${source}`,
diff --git a/webview-ui/src/components/worktrees/CreateWorktreeModal.tsx b/webview-ui/src/components/worktrees/CreateWorktreeModal.tsx
index 84743c1650..0bd1c8f3bd 100644
--- a/webview-ui/src/components/worktrees/CreateWorktreeModal.tsx
+++ b/webview-ui/src/components/worktrees/CreateWorktreeModal.tsx
@@ -4,7 +4,7 @@ import prettyBytes from "pretty-bytes"
import type { WorktreeDefaultsResponse, BranchInfo, WorktreeIncludeStatus } from "@roo-code/types"
import { vscode } from "@/utils/vscode"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Button, Input } from "@/components/ui"
import { SearchableSelect, type SearchableSelectOption } from "@/components/ui/searchable-select"
import { CornerDownRight, Folder, FolderSearch, Info } from "lucide-react"
@@ -22,7 +22,7 @@ export const CreateWorktreeModal = ({
openAfterCreate = false,
onSuccess,
}: CreateWorktreeModalProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
// Form state
const [branchName, setBranchName] = useState("")
diff --git a/webview-ui/src/components/worktrees/DeleteWorktreeModal.tsx b/webview-ui/src/components/worktrees/DeleteWorktreeModal.tsx
index 9e3f4802d5..27fdd4c9ac 100644
--- a/webview-ui/src/components/worktrees/DeleteWorktreeModal.tsx
+++ b/webview-ui/src/components/worktrees/DeleteWorktreeModal.tsx
@@ -3,7 +3,7 @@ import { useState, useEffect, useCallback } from "react"
import type { Worktree } from "@roo-code/types"
import { vscode } from "@/utils/vscode"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Button, Checkbox } from "@/components/ui"
import { Folder, GitBranch, TriangleAlert } from "lucide-react"
@@ -15,7 +15,7 @@ interface DeleteWorktreeModalProps {
}
export const DeleteWorktreeModal = ({ open, onClose, worktree, onSuccess }: DeleteWorktreeModalProps) => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const [isDeleting, setIsDeleting] = useState(false)
const [forceDeleteLocked, setForceDeleteLocked] = useState(false)
diff --git a/webview-ui/src/components/worktrees/WorktreesView.tsx b/webview-ui/src/components/worktrees/WorktreesView.tsx
index 1df762c207..346f3341e0 100644
--- a/webview-ui/src/components/worktrees/WorktreesView.tsx
+++ b/webview-ui/src/components/worktrees/WorktreesView.tsx
@@ -4,7 +4,7 @@ import type { Worktree, WorktreeListResponse, WorktreeIncludeStatus } from "@roo
import { Badge, Button, StandardTooltip, ToggleSwitch } from "@/components/ui"
import { useExtensionState } from "@/context/ExtensionStateContext"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { vscode } from "@/utils/vscode"
import { SectionHeader } from "../settings/SectionHeader"
@@ -14,7 +14,7 @@ import { DeleteWorktreeModal } from "./DeleteWorktreeModal"
import { Folder, GitBranch, Lock, Plus, SquareArrowOutUpRight, Trash } from "lucide-react"
export const WorktreesView = () => {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const { showWorktreesInHomeScreen, setShowWorktreesInHomeScreen } = useExtensionState()
// State
diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx
index c4fa00f479..d1e59efe06 100644
--- a/webview-ui/src/context/ExtensionStateContext.tsx
+++ b/webview-ui/src/context/ExtensionStateContext.tsx
@@ -35,6 +35,8 @@ import { experimentDefault } from "@roo/experiments"
import { vscode } from "@src/utils/vscode"
import { convertTextMateToHljs } from "@src/utils/textMateToHljs"
+import i18next from "i18next"
+import { applyLanguage } from "@src/i18n/language"
export interface ExtensionStateContextType extends ExtensionState {
historyPreviewCollapsed?: boolean // Add the new state property
@@ -346,6 +348,7 @@ export const ExtensionStateContextProvider: React.FC<{
switch (message.type) {
case "state": {
const newState = message.state ?? {}
+ applyLanguage(i18next, newState.language)
setState((prevState) => mergeExtensionState(prevState, newState))
setShowWelcome(!checkExistKey(newState.apiConfiguration, newState.zooCodeIsAuthenticated))
setDidHydrateState(true)
diff --git a/webview-ui/src/hooks/useTooManyTools.ts b/webview-ui/src/hooks/useTooManyTools.ts
index 75a3d834c6..f486098982 100644
--- a/webview-ui/src/hooks/useTooManyTools.ts
+++ b/webview-ui/src/hooks/useTooManyTools.ts
@@ -1,6 +1,6 @@
import { useMemo } from "react"
import { useExtensionState } from "@src/context/ExtensionStateContext"
-import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useTranslation } from "react-i18next"
import { MAX_MCP_TOOLS_THRESHOLD, countEnabledMcpTools } from "@roo-code/types"
export interface TooManyToolsInfo {
@@ -31,7 +31,7 @@ export interface TooManyToolsInfo {
* }
*/
export function useTooManyTools(): TooManyToolsInfo {
- const { t } = useAppTranslation()
+ const { t } = useTranslation()
const { mcpServers } = useExtensionState()
const { enabledServerCount, enabledToolCount } = useMemo(() => countEnabledMcpTools(mcpServers), [mcpServers])
diff --git a/webview-ui/src/i18n/TranslationContext.tsx b/webview-ui/src/i18n/TranslationContext.tsx
deleted file mode 100644
index f2950ae2f8..0000000000
--- a/webview-ui/src/i18n/TranslationContext.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import React, { createContext, useContext, ReactNode, useEffect, useCallback } from "react"
-import { useTranslation } from "react-i18next"
-import i18next, { loadTranslations } from "./setup"
-import { useExtensionState } from "@/context/ExtensionStateContext"
-
-// Create context for translations
-export const TranslationContext = createContext<{
- t: (key: string, options?: Record
) => string
- i18n: typeof i18next
-}>({
- t: (key: string) => key,
- i18n: i18next,
-})
-
-// Translation provider component
-const TranslationProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
- // Initialize with default configuration
- const { i18n } = useTranslation()
- // Get the extension state directly - it already contains all state properties
- const extensionState = useExtensionState()
-
- // Load translations once when the component mounts
- useEffect(() => {
- try {
- loadTranslations()
- } catch (error) {
- console.error("Failed to load translations:", error)
- }
- }, [])
-
- useEffect(() => {
- i18n.changeLanguage(extensionState.language)
- }, [i18n, extensionState.language])
-
- // Memoize the translation function to prevent unnecessary re-renders
- const translate = useCallback(
- (key: string, options?: Record) => {
- return i18n.t(key, options)
- },
- [i18n],
- )
-
- return (
-
- {children}
-
- )
-}
-
-// Custom hook for easy translations
-export const useAppTranslation = () => useContext(TranslationContext)
-
-export default TranslationProvider
diff --git a/webview-ui/src/i18n/__mocks__/TranslationContext.tsx b/webview-ui/src/i18n/__mocks__/TranslationContext.tsx
deleted file mode 100644
index 3fdfcbdc2c..0000000000
--- a/webview-ui/src/i18n/__mocks__/TranslationContext.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import React, { createContext, useContext, ReactNode } from "react"
-
-// Mock translation function that returns English text for settings or the key itself
-const mockTranslate = (key: string, options?: Record): string => {
- // Convert the key back to approximate English text for test purposes
- if (key.startsWith("settings.")) {
- // For specific keys the tests are looking for
- if (key === "settings.notifications.sound.label") return "Enable sound effects"
- if (key === "settings.autoApprove.execute.label") return "Always approve allowed execute operations"
- if (key === "settings.autoApprove.execute.allowedCommands") return "Allowed Auto-Execute Commands"
- if (key === "settings.autoApprove.execute.commandPlaceholder") return "Enter command prefix"
- if (key === "settings.autoApprove.execute.addButton") return "Add"
- if (key === "settings.common.save") return "Save"
- if (key === "settings.contextManagement.terminal.label") return "Terminal output limit"
- if (key === "settings.header.title") return "Settings"
-
- // Default handling of other keys
- return key.split(".").pop() || key
- }
-
- // For keys that contain variables
- if (options) {
- let result = key
- Object.entries(options).forEach(([varName, value]) => {
- result = result.replace(`{${varName}}`, String(value))
- })
- return result
- }
-
- return key
-}
-
-// Create mock context
-export const TranslationContext = createContext<{
- t: (key: string, options?: Record) => string
- i18n: any
-}>({
- t: mockTranslate,
- i18n: {},
-})
-
-// Mock translation provider component
-export const TranslationProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
- return (
-
- {children}
-
- )
-}
-
-// Custom hook for translations
-export const useAppTranslation = () => useContext(TranslationContext)
-
-export default TranslationProvider
diff --git a/webview-ui/src/i18n/__tests__/TranslationContext.spec.tsx b/webview-ui/src/i18n/__tests__/TranslationContext.spec.tsx
deleted file mode 100644
index cdf0c21acf..0000000000
--- a/webview-ui/src/i18n/__tests__/TranslationContext.spec.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import { render } from "@/utils/test-utils"
-
-import TranslationProvider, { useAppTranslation } from "../TranslationContext"
-
-vi.mock("@/context/ExtensionStateContext", () => ({
- useExtensionState: () => ({
- language: "en",
- }),
-}))
-
-vi.mock("react-i18next", () => ({
- useTranslation: () => ({
- i18n: {
- t: (key: string, options?: Record) => {
- // Mock specific translations used in tests
- if (key === "settings.autoApprove.title") return "Auto-Approve"
- if (key === "notifications.error") {
- return options?.message ? `Operation failed: ${options.message}` : "Operation failed"
- }
- return key
- },
- changeLanguage: vi.fn(),
- },
- }),
-}))
-
-vi.mock("../setup", () => ({
- default: {
- t: (key: string, options?: Record) => {
- // Mock specific translations used in tests
- if (key === "settings.autoApprove.title") return "Auto-Approve"
- if (key === "notifications.error") {
- return options?.message ? `Operation failed: ${options.message}` : "Operation failed"
- }
- return key
- },
- changeLanguage: vi.fn(),
- },
- loadTranslations: vi.fn(),
-}))
-
-const TestComponent = () => {
- const { t } = useAppTranslation()
- return (
-
-
{t("settings.autoApprove.title")}
-
{t("notifications.error", { message: "Test error" })}
-
- )
-}
-
-describe("TranslationContext", () => {
- it("should provide translations via context", () => {
- const { getByTestId } = render(
-
-
- ,
- )
-
- // Check if translation is provided correctly
- expect(getByTestId("translation-test")).toHaveTextContent("Auto-Approve")
- })
-
- it("should handle interpolation correctly", () => {
- const { getByTestId } = render(
-
-
- ,
- )
-
- // Check if interpolation works
- expect(getByTestId("translation-interpolation")).toHaveTextContent("Operation failed: Test error")
- })
-})
diff --git a/webview-ui/src/i18n/__tests__/createFixtureI18n.visual.i18n.ts b/webview-ui/src/i18n/__tests__/createFixtureI18n.visual.i18n.ts
new file mode 100644
index 0000000000..533ef78ea0
--- /dev/null
+++ b/webview-ui/src/i18n/__tests__/createFixtureI18n.visual.i18n.ts
@@ -0,0 +1,35 @@
+import i18next, { type i18n } from "i18next"
+
+/**
+ * An i18next instance for a visual fixture that serves exactly `translations`, keyed `"ns:dotted.key"`.
+ * Pass it to react-i18next's `I18nextProvider`. Missing keys render as the qualified key.
+ */
+export function createFixtureI18n(translations: Record): i18n {
+ const resources: Record> = {}
+
+ for (const [qualifiedKey, value] of Object.entries(translations)) {
+ const [ns, key] = qualifiedKey.split(":")
+ const path = key.split(".")
+ let node = (resources[ns] ??= {})
+
+ for (const segment of path.slice(0, -1)) {
+ node = (node[segment] ??= {}) as Record
+ }
+
+ node[path[path.length - 1]] = value
+ }
+
+ const instance = i18next.createInstance()
+
+ // Inline resources and `initAsync: false` make the instance ready before the fixture renders.
+ void instance.init({
+ lng: "en",
+ resources: { en: resources },
+ ns: Object.keys(resources),
+ initAsync: false,
+ appendNamespaceToMissingKey: true,
+ interpolation: { escapeValue: false },
+ })
+
+ return instance
+}
diff --git a/webview-ui/src/i18n/__tests__/hydrationLanguage.spec.tsx b/webview-ui/src/i18n/__tests__/hydrationLanguage.spec.tsx
new file mode 100644
index 0000000000..1e40c27ada
--- /dev/null
+++ b/webview-ui/src/i18n/__tests__/hydrationLanguage.spec.tsx
@@ -0,0 +1,71 @@
+import { StrictMode, useMemo } from "react"
+import { act, render, screen } from "@testing-library/react"
+import { useTranslation } from "react-i18next"
+
+import { ExtensionStateContextProvider, useExtensionState } from "@/context/ExtensionStateContext"
+
+import i18next from "../setup"
+import { applyLanguage } from "../language"
+
+// A `useTranslation` consumer that caches its output on `t`, as React Compiler does. Under StrictMode
+// its `t` is reset to the first identity, so it only renders the hydrated language if that language is
+// applied before the hydrating render.
+const TipsLikeConsumer = () => {
+ const { t } = useTranslation("chat")
+ const label = useMemo(() => t("rooTips.customizableModes.title"), [t])
+ return {label}
+}
+
+const HydrationGate = () => {
+ const { didHydrateState } = useExtensionState()
+ return didHydrateState ? : null
+}
+
+const postState = (state: Record) =>
+ act(async () => {
+ window.dispatchEvent(new MessageEvent("message", { data: { type: "state", state } }))
+ })
+
+describe("hydrated language", () => {
+ beforeEach(async () => {
+ await i18next.changeLanguage("en")
+ })
+
+ it("renders components mounted by hydration in the hydrated language under StrictMode", async () => {
+ render(
+
+
+
+
+ ,
+ )
+
+ await postState({ language: "es" })
+
+ const tips = screen.getByTestId("tips")
+ expect(tips).toHaveTextContent("Modos personalizables")
+ expect(tips).not.toHaveTextContent("rooTips.customizableModes.title")
+ })
+})
+
+describe("applyLanguage", () => {
+ beforeEach(async () => {
+ await i18next.changeLanguage("en")
+ })
+
+ it("switches to a different language", () => {
+ applyLanguage(i18next, "es")
+ expect(i18next.language).toBe("es")
+ })
+
+ it("does not re-emit languageChanged for the active language or a missing one", () => {
+ const onChanged = vi.fn()
+ i18next.on("languageChanged", onChanged)
+
+ applyLanguage(i18next, "en")
+ applyLanguage(i18next, undefined)
+
+ i18next.off("languageChanged", onChanged)
+ expect(onChanged).not.toHaveBeenCalled()
+ })
+})
diff --git a/webview-ui/src/i18n/__tests__/languageSwitch.spec.tsx b/webview-ui/src/i18n/__tests__/languageSwitch.spec.tsx
new file mode 100644
index 0000000000..eb1394f681
--- /dev/null
+++ b/webview-ui/src/i18n/__tests__/languageSwitch.spec.tsx
@@ -0,0 +1,85 @@
+import { memo, useMemo } from "react"
+import { act, render, renderHook, screen, waitFor } from "@testing-library/react"
+import { Trans, useTranslation } from "react-i18next"
+
+import { ExtensionStateContextProvider } from "@/context/ExtensionStateContext"
+
+import i18next from "../setup"
+
+const postState = (state: Record) =>
+ act(async () => {
+ window.dispatchEvent(new MessageEvent("message", { data: { type: "state", state } }))
+ })
+
+// React Compiler (enabled in vite.config.ts, not in Vitest) caches render output and only
+// recomputes it when a dependency changes. These consumers reproduce that caching by hand:
+// a stale value here is exactly what the compiled webview showed on the Welcome page.
+
+const PlainConsumer = memo(function PlainConsumer() {
+ const { t } = useTranslation()
+ const label = useMemo(() => t("history:recentTasks"), [t])
+ return {label}
+})
+
+const NamespacedConsumer = memo(function NamespacedConsumer() {
+ const { t } = useTranslation("chat")
+ const label = useMemo(() => t("rooTips.customizableModes.title"), [t])
+ return {label}
+})
+
+const TransConsumer = memo(function TransConsumer() {
+ const { t } = useTranslation()
+ // The compiler caches `` like this; passing `t` makes the element depend on the language.
+ const element = useMemo(() => , [t])
+ return {element}
+})
+
+describe("language switch without remounting", () => {
+ beforeEach(async () => {
+ await i18next.changeLanguage("en")
+ })
+
+ it("updates plain and namespaced useTranslation and Trans consumers from en to es", async () => {
+ render(
+
+
+
+
+ ,
+ )
+
+ expect(screen.getByTestId("plain")).toHaveTextContent("Recent Tasks")
+ expect(screen.getByTestId("namespaced")).toHaveTextContent(
+ i18next.t("chat:rooTips.customizableModes.title", { lng: "en" }),
+ )
+ expect(screen.getByTestId("trans")).toHaveTextContent("Zoo is a whole AI dev team in your editor")
+
+ await postState({ language: "es" })
+
+ await waitFor(() => expect(screen.getByTestId("plain")).toHaveTextContent("Tareas"))
+ expect(screen.getByTestId("namespaced")).toHaveTextContent(
+ i18next.t("chat:rooTips.customizableModes.title", { lng: "es" }),
+ )
+ expect(screen.getByTestId("trans")).toHaveTextContent(i18next.t("chat:about", { lng: "es" }))
+ })
+
+ it("keeps t stable across re-renders while the language is unchanged", () => {
+ const { result, rerender } = renderHook(() => useTranslation("chat").t)
+ const first = result.current
+
+ rerender()
+
+ expect(result.current).toBe(first)
+ })
+
+ it("translates fully qualified keys with a namespaced t", () => {
+ const Qualified = () => {
+ const { t } = useTranslation("chat")
+ return {t("history:recentTasks")}
+ }
+
+ render( )
+
+ expect(screen.getByTestId("qualified")).toHaveTextContent("Recent Tasks")
+ })
+})
diff --git a/webview-ui/src/i18n/language.ts b/webview-ui/src/i18n/language.ts
new file mode 100644
index 0000000000..3b5384b373
--- /dev/null
+++ b/webview-ui/src/i18n/language.ts
@@ -0,0 +1,15 @@
+import type { i18n } from "i18next"
+
+/**
+ * Switch `instance` to `language` unless it is already active.
+ *
+ * Call it before the state that carries the language is rendered (the "state" message handler), so
+ * components mounting in that commit render the new language instead of catching up afterwards.
+ * Catching up is not reliable: under StrictMode, react-i18next's `useTranslation` resets a freshly
+ * mounted component's `t` to its first identity, and React Compiler then serves the cached output.
+ */
+export function applyLanguage(instance: i18n, language: string | undefined) {
+ if (language && instance.language !== language) {
+ void instance.changeLanguage(language)
+ }
+}
diff --git a/webview-ui/src/i18n/setup.ts b/webview-ui/src/i18n/setup.ts
index 678cdc1d49..607ebe65fe 100644
--- a/webview-ui/src/i18n/setup.ts
+++ b/webview-ui/src/i18n/setup.ts
@@ -29,7 +29,7 @@ Object.entries(localeFiles).forEach(([path, module]) => {
console.log("Dynamically loaded translations:", Object.keys(translations))
// Initialize i18next for React
-// This will be initialized with the VSCode language in TranslationProvider
+// The language is switched by the "state" message handler in ExtensionStateContext (see applyLanguage)
i18next.use(initReactI18next).init({
lng: "en", // Default language (will be overridden)
fallbackLng: "en",
@@ -51,4 +51,8 @@ export function loadTranslations() {
})
}
+// The locale files are bundled eagerly, so register them before the first render instead of
+// in a mount effect, which ran after children had already rendered raw keys.
+loadTranslations()
+
export default i18next
diff --git a/webview-ui/src/utils/__tests__/format.spec.ts b/webview-ui/src/utils/__tests__/format.spec.ts
index 2f633d364d..6ffa7399f5 100644
--- a/webview-ui/src/utils/__tests__/format.spec.ts
+++ b/webview-ui/src/utils/__tests__/format.spec.ts
@@ -1,59 +1,68 @@
+import type { TFunction } from "i18next"
+
import { formatLargeNumber, formatDate, formatTimeAgo } from "../format"
// Mock i18next
vi.mock("i18next", () => ({
- default: {
- t: vi.fn((key: string, options?: any) => {
- // Mock translations for testing
- const translations: Record = {
- "common:number_format.billion_suffix": "b",
- "common:number_format.million_suffix": "m",
- "common:number_format.thousand_suffix": "k",
- "common:time_ago.just_now": "just now",
- "common:time_ago.seconds_ago": "{{count}} seconds ago",
- "common:time_ago.minute_ago": "a minute ago",
- "common:time_ago.minutes_ago": "{{count}} minutes ago",
- "common:time_ago.hour_ago": "an hour ago",
- "common:time_ago.hours_ago": "{{count}} hours ago",
- "common:time_ago.day_ago": "a day ago",
- "common:time_ago.days_ago": "{{count}} days ago",
- "common:time_ago.week_ago": "a week ago",
- "common:time_ago.weeks_ago": "{{count}} weeks ago",
- "common:time_ago.month_ago": "a month ago",
- "common:time_ago.months_ago": "{{count}} months ago",
- "common:time_ago.year_ago": "a year ago",
- "common:time_ago.years_ago": "{{count}} years ago",
- }
-
- let result = translations[key] || key
- if (options?.count !== undefined) {
- result = result.replace("{{count}}", options.count.toString())
- }
- return result
- }),
- language: "en",
- },
+ default: { language: "en" },
}))
+// Returns translation keys with interpolated values appended.
+const t = ((key: string, options?: { count?: number }) => {
+ const translations: Record = {
+ "common:number_format.billion_suffix": "b",
+ "common:number_format.million_suffix": "m",
+ "common:number_format.thousand_suffix": "k",
+ "common:time_ago.just_now": "just now",
+ "common:time_ago.seconds_ago": "{{count}} seconds ago",
+ "common:time_ago.minute_ago": "a minute ago",
+ "common:time_ago.minutes_ago": "{{count}} minutes ago",
+ "common:time_ago.hour_ago": "an hour ago",
+ "common:time_ago.hours_ago": "{{count}} hours ago",
+ "common:time_ago.day_ago": "a day ago",
+ "common:time_ago.days_ago": "{{count}} days ago",
+ "common:time_ago.week_ago": "a week ago",
+ "common:time_ago.weeks_ago": "{{count}} weeks ago",
+ "common:time_ago.month_ago": "a month ago",
+ "common:time_ago.months_ago": "{{count}} months ago",
+ "common:time_ago.year_ago": "a year ago",
+ "common:time_ago.years_ago": "{{count}} years ago",
+ }
+
+ let result = translations[key] || key
+ if (options?.count !== undefined) {
+ result = result.replace("{{count}}", options.count.toString())
+ }
+ return result
+}) as unknown as TFunction
+
describe("formatLargeNumber", () => {
it("should format billions", () => {
- expect(formatLargeNumber(1500000000)).toBe("1.5b")
- expect(formatLargeNumber(2000000000)).toBe("2.0b")
+ expect(formatLargeNumber(1500000000, t)).toBe("1.5b")
+ expect(formatLargeNumber(2000000000, t)).toBe("2.0b")
})
it("should format millions", () => {
- expect(formatLargeNumber(1500000)).toBe("1.5m")
- expect(formatLargeNumber(2000000)).toBe("2.0m")
+ expect(formatLargeNumber(1500000, t)).toBe("1.5m")
+ expect(formatLargeNumber(2000000, t)).toBe("2.0m")
})
it("should format thousands", () => {
- expect(formatLargeNumber(1500)).toBe("1.5k")
- expect(formatLargeNumber(2000)).toBe("2.0k")
+ expect(formatLargeNumber(1500, t)).toBe("1.5k")
+ expect(formatLargeNumber(2000, t)).toBe("2.0k")
})
it("should return string for small numbers", () => {
- expect(formatLargeNumber(999)).toBe("999")
- expect(formatLargeNumber(100)).toBe("100")
+ expect(formatLargeNumber(999, t)).toBe("999")
+ expect(formatLargeNumber(100, t)).toBe("100")
+ })
+
+ it("takes suffixes from the given t", () => {
+ const givenT = vi.fn((key: string) => (key === "common:number_format.thousand_suffix" ? " mil" : key))
+
+ // `TFunction` is branded; the mock only needs to answer a key lookup.
+ expect(formatLargeNumber(1500, givenT as unknown as TFunction)).toBe("1.5 mil")
+ expect(givenT).toHaveBeenCalledWith("common:number_format.thousand_suffix")
})
})
@@ -83,71 +92,80 @@ describe("formatTimeAgo", () => {
it('should return "just now" for very recent times', () => {
const timestamp = new Date("2024-01-15T11:59:35").getTime() // 25 seconds ago
- expect(formatTimeAgo(timestamp)).toBe("just now")
+ expect(formatTimeAgo(timestamp, t)).toBe("just now")
})
it("should format seconds ago", () => {
const timestamp = new Date("2024-01-15T11:59:15").getTime() // 45 seconds ago
- expect(formatTimeAgo(timestamp)).toBe("45 seconds ago")
+ expect(formatTimeAgo(timestamp, t)).toBe("45 seconds ago")
})
it("should format a minute ago", () => {
const timestamp = new Date("2024-01-15T11:59:00").getTime() // 1 minute ago
- expect(formatTimeAgo(timestamp)).toBe("a minute ago")
+ expect(formatTimeAgo(timestamp, t)).toBe("a minute ago")
})
it("should format minutes ago", () => {
const timestamp = new Date("2024-01-15T11:45:00").getTime() // 15 minutes ago
- expect(formatTimeAgo(timestamp)).toBe("15 minutes ago")
+ expect(formatTimeAgo(timestamp, t)).toBe("15 minutes ago")
})
it("should format an hour ago", () => {
const timestamp = new Date("2024-01-15T11:00:00").getTime() // 1 hour ago
- expect(formatTimeAgo(timestamp)).toBe("an hour ago")
+ expect(formatTimeAgo(timestamp, t)).toBe("an hour ago")
})
it("should format hours ago", () => {
const timestamp = new Date("2024-01-15T09:00:00").getTime() // 3 hours ago
- expect(formatTimeAgo(timestamp)).toBe("3 hours ago")
+ expect(formatTimeAgo(timestamp, t)).toBe("3 hours ago")
})
it("should format a day ago", () => {
const timestamp = new Date("2024-01-14T12:00:00").getTime() // 1 day ago
- expect(formatTimeAgo(timestamp)).toBe("a day ago")
+ expect(formatTimeAgo(timestamp, t)).toBe("a day ago")
})
it("should format days ago", () => {
const timestamp = new Date("2024-01-12T12:00:00").getTime() // 3 days ago
- expect(formatTimeAgo(timestamp)).toBe("3 days ago")
+ expect(formatTimeAgo(timestamp, t)).toBe("3 days ago")
})
it("should format a week ago", () => {
const timestamp = new Date("2024-01-08T12:00:00").getTime() // 7 days ago
- expect(formatTimeAgo(timestamp)).toBe("a week ago")
+ expect(formatTimeAgo(timestamp, t)).toBe("a week ago")
})
it("should format weeks ago", () => {
const timestamp = new Date("2024-01-01T12:00:00").getTime() // 14 days ago
- expect(formatTimeAgo(timestamp)).toBe("2 weeks ago")
+ expect(formatTimeAgo(timestamp, t)).toBe("2 weeks ago")
})
it("should format a month ago", () => {
const timestamp = new Date("2023-12-15T12:00:00").getTime() // ~1 month ago
- expect(formatTimeAgo(timestamp)).toBe("a month ago")
+ expect(formatTimeAgo(timestamp, t)).toBe("a month ago")
})
it("should format months ago", () => {
const timestamp = new Date("2023-10-15T12:00:00").getTime() // ~3 months ago
- expect(formatTimeAgo(timestamp)).toBe("3 months ago")
+ expect(formatTimeAgo(timestamp, t)).toBe("3 months ago")
})
it("should format a year ago", () => {
const timestamp = new Date("2023-01-15T12:00:00").getTime() // 1 year ago
- expect(formatTimeAgo(timestamp)).toBe("a year ago")
+ expect(formatTimeAgo(timestamp, t)).toBe("a year ago")
})
it("should format years ago", () => {
const timestamp = new Date("2021-01-15T12:00:00").getTime() // 3 years ago
- expect(formatTimeAgo(timestamp)).toBe("3 years ago")
+ expect(formatTimeAgo(timestamp, t)).toBe("3 years ago")
+ })
+
+ it("takes translations from the given t", () => {
+ const givenT = vi.fn((key: string, options?: Record) => `es:${key}:${options?.count}`)
+ const timestamp = new Date("2024-01-15T09:00:00").getTime() // 3 hours ago
+
+ // `TFunction` is branded; the mock only needs to answer a key lookup with `count`.
+ expect(formatTimeAgo(timestamp, givenT as unknown as TFunction)).toBe("es:common:time_ago.hours_ago:3")
+ expect(givenT).toHaveBeenCalledWith("common:time_ago.hours_ago", { count: 3 })
})
})
diff --git a/webview-ui/src/utils/__tests__/validate.spec.ts b/webview-ui/src/utils/__tests__/validate.spec.ts
index bf25faf7f2..d4b9a4da2a 100644
--- a/webview-ui/src/utils/__tests__/validate.spec.ts
+++ b/webview-ui/src/utils/__tests__/validate.spec.ts
@@ -4,22 +4,7 @@ import {
type OrganizationAllowList,
type RouterModels,
} from "@roo-code/types"
-
-// Mock i18next to return translation keys with interpolated values
-vi.mock("i18next", () => ({
- default: {
- t: (key: string, options?: Record) => {
- if (options) {
- let result = key
- Object.entries(options).forEach(([k, v]) => {
- result += ` ${k}=${v}`
- })
- return result
- }
- return key
- },
- },
-}))
+import type { TFunction } from "i18next"
import {
formatOpenAiExtraBodyValidationError,
@@ -29,6 +14,18 @@ import {
validateBedrockArn,
} from "../validate"
+// Returns translation keys with interpolated values appended.
+const t = ((key: string, options?: Record) => {
+ if (options) {
+ let result = key
+ Object.entries(options).forEach(([k, v]) => {
+ result += ` ${k}=${v}`
+ })
+ return result
+ }
+ return key
+}) as unknown as TFunction
+
describe("Model Validation Functions", () => {
const mockRouterModels: RouterModels = {
openrouter: {
@@ -88,7 +85,7 @@ describe("Model Validation Functions", () => {
openRouterModelId: "valid-model",
}
- const result = getModelValidationError(config, mockRouterModels, allowAllOrganization)
+ const result = getModelValidationError(config, t, mockRouterModels, allowAllOrganization)
expect(result).toBeUndefined()
})
@@ -98,7 +95,7 @@ describe("Model Validation Functions", () => {
openRouterModelId: "invalid-model",
}
- const result = getModelValidationError(config, mockRouterModels, allowAllOrganization)
+ const result = getModelValidationError(config, t, mockRouterModels, allowAllOrganization)
expect(result).toContain("settings:validation.modelAvailability")
})
@@ -108,7 +105,7 @@ describe("Model Validation Functions", () => {
openRouterModelId: "another-valid-model",
}
- const result = getModelValidationError(config, mockRouterModels, restrictiveOrganization)
+ const result = getModelValidationError(config, t, mockRouterModels, restrictiveOrganization)
expect(result).toContain("model")
})
@@ -118,7 +115,7 @@ describe("Model Validation Functions", () => {
openAiModelId: "gpt-4",
}
- const result = getModelValidationError(config, undefined, allowAllOrganization)
+ const result = getModelValidationError(config, t, undefined, allowAllOrganization)
expect(result).toBeUndefined()
})
@@ -152,7 +149,7 @@ describe("Model Validation Functions", () => {
},
}
- const result = getModelValidationError(config, undefined, organizationAllowList)
+ const result = getModelValidationError(config, t, undefined, organizationAllowList)
expect(result).toContain("settings:validation.modelNotAllowed")
expect(result).toContain("model=blocked-model")
@@ -164,7 +161,7 @@ describe("Model Validation Functions", () => {
openRouterModelId: "",
}
- const result = getModelValidationError(config, mockRouterModels, allowAllOrganization)
+ const result = getModelValidationError(config, t, mockRouterModels, allowAllOrganization)
expect(result).toBe("settings:validation.modelId")
})
@@ -174,14 +171,14 @@ describe("Model Validation Functions", () => {
// openRouterModelId is undefined
}
- const result = getModelValidationError(config, mockRouterModels, allowAllOrganization)
+ const result = getModelValidationError(config, t, mockRouterModels, allowAllOrganization)
expect(result).toBe("settings:validation.modelId")
})
})
describe("validateApiConfiguration for GitHub Copilot", () => {
const validate = (vsCodeLmModelSelector: ProviderSettings["vsCodeLmModelSelector"]) =>
- validateApiConfiguration({ apiProvider: providerIdentifiers.githubCopilot, vsCodeLmModelSelector })
+ validateApiConfiguration({ apiProvider: providerIdentifiers.githubCopilot, vsCodeLmModelSelector }, t)
it.each([
["a model id", { vendor: "copilot", id: "gpt-x" }],
@@ -207,7 +204,12 @@ describe("Model Validation Functions", () => {
openRouterModelId: "valid-model",
}
- const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization)
+ const result = validateApiConfigurationExcludingModelErrors(
+ config,
+ t,
+ mockRouterModels,
+ allowAllOrganization,
+ )
expect(result).toBeUndefined()
})
@@ -218,7 +220,12 @@ describe("Model Validation Functions", () => {
// Missing openRouterApiKey
}
- const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization)
+ const result = validateApiConfigurationExcludingModelErrors(
+ config,
+ t,
+ mockRouterModels,
+ allowAllOrganization,
+ )
expect(result).toBe("settings:validation.apiKey")
})
@@ -229,7 +236,12 @@ describe("Model Validation Functions", () => {
openRouterModelId: "invalid-model", // This should be ignored
}
- const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization)
+ const result = validateApiConfigurationExcludingModelErrors(
+ config,
+ t,
+ mockRouterModels,
+ allowAllOrganization,
+ )
expect(result).toBeUndefined() // Should not return model validation error
})
@@ -242,6 +254,7 @@ describe("Model Validation Functions", () => {
const result = validateApiConfigurationExcludingModelErrors(
config,
+ t,
mockRouterModels,
restrictiveOrganization,
)
@@ -258,7 +271,7 @@ describe("Model Validation Functions", () => {
}
expect(
- validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization),
+ validateApiConfigurationExcludingModelErrors(config, t, mockRouterModels, allowAllOrganization),
).toBeUndefined()
})
@@ -279,9 +292,9 @@ describe("Model Validation Functions", () => {
openAiExtraBody,
}
- expect(validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization)).toBe(
- expectedError,
- )
+ expect(
+ validateApiConfigurationExcludingModelErrors(config, t, mockRouterModels, allowAllOrganization),
+ ).toBe(expectedError)
})
})
@@ -297,9 +310,9 @@ describe("Model Validation Functions", () => {
it("accepts a valid Extra Body object through both validation entry points", () => {
const config = createConfig(JSON.stringify({ metadata: { completion_window: "balanced" } }))
- expect(validateApiConfiguration(config, mockRouterModels, allowAllOrganization)).toBeUndefined()
+ expect(validateApiConfiguration(config, t, mockRouterModels, allowAllOrganization)).toBeUndefined()
expect(
- validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization),
+ validateApiConfigurationExcludingModelErrors(config, t, mockRouterModels, allowAllOrganization),
).toBeUndefined()
})
@@ -324,14 +337,14 @@ describe("Model Validation Functions", () => {
])("rejects %s with the localized error", (_name, openAiExtraBody, expectedError) => {
const config = createConfig(openAiExtraBody)
- expect(validateApiConfiguration(config, mockRouterModels, allowAllOrganization)).toBe(expectedError)
+ expect(validateApiConfiguration(config, t, mockRouterModels, allowAllOrganization)).toBe(expectedError)
})
it("returns required-field errors before Extra Body errors", () => {
const config = createConfig("not json")
config.openAiApiKey = undefined
- expect(validateApiConfiguration(config, mockRouterModels, allowAllOrganization)).toBe(
+ expect(validateApiConfiguration(config, t, mockRouterModels, allowAllOrganization)).toBe(
"settings:validation.openAi",
)
})
@@ -364,7 +377,7 @@ describe("Model Validation Functions", () => {
openAiExtraBody: "not json",
}
- expect(validateApiConfiguration(config, mockRouterModels, allowAllOrganization)).toBeUndefined()
+ expect(validateApiConfiguration(config, t, mockRouterModels, allowAllOrganization)).toBeUndefined()
})
})
@@ -376,7 +389,12 @@ describe("Model Validation Functions", () => {
// Missing opencodeGoApiKey
}
- const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization)
+ const result = validateApiConfigurationExcludingModelErrors(
+ config,
+ t,
+ mockRouterModels,
+ allowAllOrganization,
+ )
expect(result).toBe("settings:validation.apiKey")
})
@@ -387,7 +405,12 @@ describe("Model Validation Functions", () => {
opencodeGoModelId: "glm-5.1",
}
- const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization)
+ const result = validateApiConfigurationExcludingModelErrors(
+ config,
+ t,
+ mockRouterModels,
+ allowAllOrganization,
+ )
expect(result).toBeUndefined()
})
@@ -398,7 +421,7 @@ describe("Model Validation Functions", () => {
// Missing opencodeGoModelId
}
- const result = getModelValidationError(config, mockRouterModels, allowAllOrganization)
+ const result = getModelValidationError(config, t, mockRouterModels, allowAllOrganization)
expect(result).toBe("settings:validation.modelId")
})
})
@@ -410,7 +433,12 @@ describe("Model Validation Functions", () => {
// Missing kenariApiKey
}
- const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization)
+ const result = validateApiConfigurationExcludingModelErrors(
+ config,
+ t,
+ mockRouterModels,
+ allowAllOrganization,
+ )
expect(result).toBe("settings:validation.apiKey")
})
@@ -421,7 +449,12 @@ describe("Model Validation Functions", () => {
kenariModelId: "glm-5.1",
}
- const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization)
+ const result = validateApiConfigurationExcludingModelErrors(
+ config,
+ t,
+ mockRouterModels,
+ allowAllOrganization,
+ )
expect(result).toBeUndefined()
})
@@ -432,7 +465,7 @@ describe("Model Validation Functions", () => {
// Missing kenariModelId
}
- const result = getModelValidationError(config, mockRouterModels, allowAllOrganization)
+ const result = getModelValidationError(config, t, mockRouterModels, allowAllOrganization)
expect(result).toBe("settings:validation.modelId")
})
})
@@ -444,7 +477,7 @@ describe("Model Validation Functions", () => {
ioIntelligenceModelId: "zai-org/GLM-4.6",
}
- expect(validateApiConfiguration(config, mockRouterModels)).toBe("settings:validation.apiKey")
+ expect(validateApiConfiguration(config, t, mockRouterModels)).toBe("settings:validation.apiKey")
})
it("accepts an IO Intelligence API key and selected model", () => {
@@ -454,7 +487,7 @@ describe("Model Validation Functions", () => {
ioIntelligenceModelId: "zai-org/GLM-4.6",
}
- expect(validateApiConfiguration(config, mockRouterModels)).toBeUndefined()
+ expect(validateApiConfiguration(config, t, mockRouterModels)).toBeUndefined()
})
})
@@ -465,7 +498,7 @@ describe("Model Validation Functions", () => {
nanoGptModelId: "openai/model",
}
- expect(validateApiConfiguration(config, mockRouterModels)).toBe("settings:validation.apiKey")
+ expect(validateApiConfiguration(config, t, mockRouterModels)).toBe("settings:validation.apiKey")
})
it("returns a modelId error when the NanoGPT model is missing", () => {
@@ -474,7 +507,7 @@ describe("Model Validation Functions", () => {
nanoGptApiKey: "valid-key",
}
- expect(validateApiConfiguration(config, mockRouterModels)).toBe("settings:validation.modelId")
+ expect(validateApiConfiguration(config, t, mockRouterModels)).toBe("settings:validation.modelId")
})
it("accepts a NanoGPT API key and selected model", () => {
@@ -484,7 +517,7 @@ describe("Model Validation Functions", () => {
nanoGptModelId: "openai/model",
}
- expect(validateApiConfiguration(config, mockRouterModels)).toBeUndefined()
+ expect(validateApiConfiguration(config, t, mockRouterModels)).toBeUndefined()
})
})
@@ -496,7 +529,12 @@ describe("Model Validation Functions", () => {
// Missing friendliApiKey
}
- const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization)
+ const result = validateApiConfigurationExcludingModelErrors(
+ config,
+ t,
+ mockRouterModels,
+ allowAllOrganization,
+ )
expect(result).toBe("settings:validation.apiKey")
})
@@ -507,7 +545,12 @@ describe("Model Validation Functions", () => {
apiModelId: "zai-org/GLM-5.2",
}
- const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization)
+ const result = validateApiConfigurationExcludingModelErrors(
+ config,
+ t,
+ mockRouterModels,
+ allowAllOrganization,
+ )
expect(result).toBeUndefined()
})
})
@@ -520,7 +563,7 @@ describe("Model Validation Functions", () => {
zooGatewayModelId: "anthropic/claude-sonnet-4",
}
- const result = validateApiConfiguration(config, mockRouterModels, allowAllOrganization, false)
+ const result = validateApiConfiguration(config, t, mockRouterModels, allowAllOrganization, false)
expect(result).toBe("settings:validation.zooGatewaySignIn")
})
@@ -530,7 +573,7 @@ describe("Model Validation Functions", () => {
zooGatewayModelId: "anthropic/claude-sonnet-4",
}
- const result = validateApiConfiguration(config, mockRouterModels, allowAllOrganization, true)
+ const result = validateApiConfiguration(config, t, mockRouterModels, allowAllOrganization, true)
expect(result).toBeUndefined()
})
@@ -541,7 +584,7 @@ describe("Model Validation Functions", () => {
zooSessionToken: "zoo_ext_test_token",
}
- const result = validateApiConfiguration(config, mockRouterModels, allowAllOrganization, false)
+ const result = validateApiConfiguration(config, t, mockRouterModels, allowAllOrganization, false)
expect(result).toBeUndefined()
})
})
@@ -558,6 +601,7 @@ describe("Model Validation Functions", () => {
const result = validateApiConfigurationExcludingModelErrors(
config,
+ t,
mockRouterModels,
allowAllOrganization,
)
@@ -573,6 +617,7 @@ describe("Model Validation Functions", () => {
const result = validateApiConfigurationExcludingModelErrors(
config,
+ t,
mockRouterModels,
allowAllOrganization,
)
@@ -594,6 +639,7 @@ describe("Model Validation Functions", () => {
const result = validateApiConfigurationExcludingModelErrors(
config,
+ t,
mockRouterModels,
orgWithoutZooGateway,
)
@@ -609,7 +655,12 @@ describe("Model Validation Functions", () => {
kimiCodeAuthMethod: "oauth",
}
- const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization)
+ const result = validateApiConfigurationExcludingModelErrors(
+ config,
+ t,
+ mockRouterModels,
+ allowAllOrganization,
+ )
expect(result).toBeUndefined()
})
@@ -618,7 +669,12 @@ describe("Model Validation Functions", () => {
apiProvider: providerIdentifiers.kimiCode,
}
- const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization)
+ const result = validateApiConfigurationExcludingModelErrors(
+ config,
+ t,
+ mockRouterModels,
+ allowAllOrganization,
+ )
expect(result).toBeUndefined()
})
@@ -628,7 +684,12 @@ describe("Model Validation Functions", () => {
kimiCodeAuthMethod: "api-key",
}
- const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization)
+ const result = validateApiConfigurationExcludingModelErrors(
+ config,
+ t,
+ mockRouterModels,
+ allowAllOrganization,
+ )
expect(result).toBe("settings:validation.apiKey")
})
@@ -639,7 +700,12 @@ describe("Model Validation Functions", () => {
kimiCodeApiKey: "valid-key",
}
- const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization)
+ const result = validateApiConfigurationExcludingModelErrors(
+ config,
+ t,
+ mockRouterModels,
+ allowAllOrganization,
+ )
expect(result).toBeUndefined()
})
})
@@ -650,6 +716,7 @@ describe("validateBedrockArn", () => {
it("accepts standard AWS Bedrock ARNs", () => {
const result = validateBedrockArn(
"arn:aws:bedrock:us-west-2:123456789012:inference-profile/us.anthropic.claude-3-5-sonnet-v2",
+ t,
)
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("us-west-2")
@@ -659,6 +726,7 @@ describe("validateBedrockArn", () => {
it("accepts AWS GovCloud ARNs", () => {
const result = validateBedrockArn(
"arn:aws-us-gov:bedrock:us-gov-west-1:123456789012:inference-profile/model",
+ t,
)
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("us-gov-west-1")
@@ -666,14 +734,14 @@ describe("validateBedrockArn", () => {
})
it("accepts AWS China ARNs", () => {
- const result = validateBedrockArn("arn:aws-cn:bedrock:cn-north-1:123456789012:inference-profile/model")
+ const result = validateBedrockArn("arn:aws-cn:bedrock:cn-north-1:123456789012:inference-profile/model", t)
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("cn-north-1")
expect(result.errorMessage).toBeUndefined()
})
it("accepts SageMaker ARNs", () => {
- const result = validateBedrockArn("arn:aws:sagemaker:us-east-1:123456789012:endpoint/my-endpoint")
+ const result = validateBedrockArn("arn:aws:sagemaker:us-east-1:123456789012:endpoint/my-endpoint", t)
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("us-east-1")
expect(result.errorMessage).toBeUndefined()
@@ -681,7 +749,7 @@ describe("validateBedrockArn", () => {
it("accepts non-standard ARN formats without validation errors", () => {
// Users are advanced - trust their input
- const result = validateBedrockArn("arn:custom:service:region:account:resource")
+ const result = validateBedrockArn("arn:custom:service:region:account:resource", t)
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("region")
expect(result.errorMessage).toBeUndefined()
@@ -689,7 +757,7 @@ describe("validateBedrockArn", () => {
it("accepts completely custom ARN strings", () => {
// Even unusual formats should be accepted
- const result = validateBedrockArn("some-custom-arn-format")
+ const result = validateBedrockArn("some-custom-arn-format", t)
expect(result.isValid).toBe(true)
// May not be able to extract region from non-standard format
expect(result.errorMessage).toBeUndefined()
@@ -700,6 +768,7 @@ describe("validateBedrockArn", () => {
it("shows warning when ARN region differs from provided region", () => {
const result = validateBedrockArn(
"arn:aws:bedrock:us-west-2:123456789012:inference-profile/model",
+ t,
"us-east-1",
)
expect(result.isValid).toBe(true) // Still valid, just a warning
@@ -711,6 +780,7 @@ describe("validateBedrockArn", () => {
it("shows no warning when ARN region matches provided region", () => {
const result = validateBedrockArn(
"arn:aws:bedrock:us-west-2:123456789012:inference-profile/model",
+ t,
"us-west-2",
)
expect(result.isValid).toBe(true)
@@ -719,14 +789,14 @@ describe("validateBedrockArn", () => {
})
it("shows no warning when no region is provided to check against", () => {
- const result = validateBedrockArn("arn:aws:bedrock:us-west-2:123456789012:inference-profile/model")
+ const result = validateBedrockArn("arn:aws:bedrock:us-west-2:123456789012:inference-profile/model", t)
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBe("us-west-2")
expect(result.errorMessage).toBeUndefined()
})
it("shows no warning when region cannot be extracted from ARN", () => {
- const result = validateBedrockArn("non-arn-format", "us-east-1")
+ const result = validateBedrockArn("non-arn-format", t, "us-east-1")
expect(result.isValid).toBe(true)
expect(result.arnRegion).toBeUndefined()
expect(result.errorMessage).toBeUndefined()
diff --git a/webview-ui/src/utils/format.ts b/webview-ui/src/utils/format.ts
index cd3521aed2..d814cf93dd 100644
--- a/webview-ui/src/utils/format.ts
+++ b/webview-ui/src/utils/format.ts
@@ -1,14 +1,14 @@
-import i18next from "i18next"
+import i18next, { type TFunction } from "i18next"
-export function formatLargeNumber(num: number): string {
+export function formatLargeNumber(num: number, t: TFunction): string {
if (num >= 1e9) {
- return (num / 1e9).toFixed(1) + i18next.t("common:number_format.billion_suffix")
+ return (num / 1e9).toFixed(1) + t("common:number_format.billion_suffix")
}
if (num >= 1e6) {
- return (num / 1e6).toFixed(1) + i18next.t("common:number_format.million_suffix")
+ return (num / 1e6).toFixed(1) + t("common:number_format.million_suffix")
}
if (num >= 1e3) {
- return (num / 1e3).toFixed(1) + i18next.t("common:number_format.thousand_suffix")
+ return (num / 1e3).toFixed(1) + t("common:number_format.thousand_suffix")
}
return num.toString()
}
@@ -29,7 +29,7 @@ export const formatDate = (timestamp: number) => {
})
}
-export const formatTimeAgo = (timestamp: number) => {
+export const formatTimeAgo = (timestamp: number, t: TFunction) => {
const now = Date.now()
const diff = now - timestamp
const seconds = Math.floor(diff / 1000)
@@ -41,38 +41,26 @@ export const formatTimeAgo = (timestamp: number) => {
const years = Math.floor(days / 365)
if (years > 0) {
- return years === 1
- ? i18next.t("common:time_ago.year_ago")
- : i18next.t("common:time_ago.years_ago", { count: years })
+ return years === 1 ? t("common:time_ago.year_ago") : t("common:time_ago.years_ago", { count: years })
}
if (months > 0) {
- return months === 1
- ? i18next.t("common:time_ago.month_ago")
- : i18next.t("common:time_ago.months_ago", { count: months })
+ return months === 1 ? t("common:time_ago.month_ago") : t("common:time_ago.months_ago", { count: months })
}
if (weeks > 0) {
- return weeks === 1
- ? i18next.t("common:time_ago.week_ago")
- : i18next.t("common:time_ago.weeks_ago", { count: weeks })
+ return weeks === 1 ? t("common:time_ago.week_ago") : t("common:time_ago.weeks_ago", { count: weeks })
}
if (days > 0) {
- return days === 1
- ? i18next.t("common:time_ago.day_ago")
- : i18next.t("common:time_ago.days_ago", { count: days })
+ return days === 1 ? t("common:time_ago.day_ago") : t("common:time_ago.days_ago", { count: days })
}
if (hours > 0) {
- return hours === 1
- ? i18next.t("common:time_ago.hour_ago")
- : i18next.t("common:time_ago.hours_ago", { count: hours })
+ return hours === 1 ? t("common:time_ago.hour_ago") : t("common:time_ago.hours_ago", { count: hours })
}
if (minutes > 0) {
- return minutes === 1
- ? i18next.t("common:time_ago.minute_ago")
- : i18next.t("common:time_ago.minutes_ago", { count: minutes })
+ return minutes === 1 ? t("common:time_ago.minute_ago") : t("common:time_ago.minutes_ago", { count: minutes })
}
if (seconds > 30) {
- return i18next.t("common:time_ago.seconds_ago", { count: seconds })
+ return t("common:time_ago.seconds_ago", { count: seconds })
}
- return i18next.t("common:time_ago.just_now")
+ return t("common:time_ago.just_now")
}
diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts
index 9e4addf7b1..3e84a03c83 100644
--- a/webview-ui/src/utils/validate.ts
+++ b/webview-ui/src/utils/validate.ts
@@ -1,5 +1,4 @@
-import i18next from "i18next"
-
+import type { TFunction } from "i18next"
import {
type ProviderSettings,
type OrganizationAllowList,
@@ -33,27 +32,28 @@ export function formatOpenAiExtraBodyValidationError(
return t(`settings:validation.openAiExtraBody.${result.reason}`)
}
-function validateOpenAiExtraBody(apiConfiguration: ProviderSettings): string | undefined {
+function validateOpenAiExtraBody(apiConfiguration: ProviderSettings, t: TFunction): string | undefined {
if (apiConfiguration.apiProvider !== providerIdentifiers.openai) {
return undefined
}
- return formatOpenAiExtraBodyValidationError(parseOpenAiExtraBody(apiConfiguration.openAiExtraBody), i18next.t)
+ return formatOpenAiExtraBodyValidationError(parseOpenAiExtraBody(apiConfiguration.openAiExtraBody), t)
}
export function validateApiConfiguration(
apiConfiguration: ProviderSettings,
+ t: TFunction,
routerModels?: RouterModels,
organizationAllowList?: OrganizationAllowList,
zooCodeIsAuthenticated?: boolean,
): string | undefined {
- const keysAndIdsPresentErrorMessage = validateModelsAndKeysProvided(apiConfiguration, zooCodeIsAuthenticated)
+ const keysAndIdsPresentErrorMessage = validateModelsAndKeysProvided(apiConfiguration, t, zooCodeIsAuthenticated)
if (keysAndIdsPresentErrorMessage) {
return keysAndIdsPresentErrorMessage
}
- const extraBodyError = validateOpenAiExtraBody(apiConfiguration)
+ const extraBodyError = validateOpenAiExtraBody(apiConfiguration, t)
if (extraBodyError) {
return extraBodyError
@@ -61,6 +61,7 @@ export function validateApiConfiguration(
const organizationAllowListError = validateProviderAgainstOrganizationSettings(
apiConfiguration,
+ t,
organizationAllowList,
)
@@ -68,82 +69,83 @@ export function validateApiConfiguration(
return organizationAllowListError.message
}
- return validateDynamicProviderModelId(apiConfiguration, routerModels)
+ return validateDynamicProviderModelId(apiConfiguration, t, routerModels)
}
function validateModelsAndKeysProvided(
apiConfiguration: ProviderSettings,
+ t: TFunction,
zooCodeIsAuthenticated?: boolean,
): string | undefined {
switch (apiConfiguration.apiProvider) {
case providerIdentifiers.openrouter:
if (!apiConfiguration.openRouterApiKey) {
- return i18next.t("settings:validation.apiKey")
+ return t("settings:validation.apiKey")
}
break
case providerIdentifiers.requesty:
if (!apiConfiguration.requestyApiKey) {
- return i18next.t("settings:validation.apiKey")
+ return t("settings:validation.apiKey")
}
break
case providerIdentifiers.unbound:
if (!apiConfiguration.unboundApiKey) {
- return i18next.t("settings:validation.apiKey")
+ return t("settings:validation.apiKey")
}
break
case providerIdentifiers.litellm:
if (!apiConfiguration.litellmApiKey) {
- return i18next.t("settings:validation.apiKey")
+ return t("settings:validation.apiKey")
}
break
case providerIdentifiers.anthropic:
if (!apiConfiguration.apiKey) {
- return i18next.t("settings:validation.apiKey")
+ return t("settings:validation.apiKey")
}
break
case providerIdentifiers.bedrock:
if (!apiConfiguration.awsRegion) {
- return i18next.t("settings:validation.awsRegion")
+ return t("settings:validation.awsRegion")
}
break
case providerIdentifiers.vertex:
if (!apiConfiguration.vertexProjectId || !apiConfiguration.vertexRegion) {
- return i18next.t("settings:validation.googleCloud")
+ return t("settings:validation.googleCloud")
}
break
case providerIdentifiers.gemini:
if (!apiConfiguration.geminiApiKey) {
- return i18next.t("settings:validation.apiKey")
+ return t("settings:validation.apiKey")
}
break
case providerIdentifiers.openaiNative:
if (!apiConfiguration.openAiNativeApiKey) {
- return i18next.t("settings:validation.apiKey")
+ return t("settings:validation.apiKey")
}
break
case providerIdentifiers.mistral:
if (!apiConfiguration.mistralApiKey) {
- return i18next.t("settings:validation.apiKey")
+ return t("settings:validation.apiKey")
}
break
case providerIdentifiers.openai:
if (!apiConfiguration.openAiBaseUrl || !apiConfiguration.openAiApiKey || !apiConfiguration.openAiModelId) {
- return i18next.t("settings:validation.openAi")
+ return t("settings:validation.openAi")
}
break
case providerIdentifiers.ollama:
if (!apiConfiguration.ollamaModelId) {
- return i18next.t("settings:validation.modelId")
+ return t("settings:validation.modelId")
}
break
case providerIdentifiers.lmstudio:
if (!apiConfiguration.lmStudioModelId) {
- return i18next.t("settings:validation.modelId")
+ return t("settings:validation.modelId")
}
break
case providerIdentifiers.vscodeLm:
if (!apiConfiguration.vsCodeLmModelSelector) {
- return i18next.t("settings:validation.modelSelector")
+ return t("settings:validation.modelSelector")
}
break
case providerIdentifiers.githubCopilot:
@@ -151,62 +153,62 @@ function validateModelsAndKeysProvided(
apiConfiguration.vsCodeLmModelSelector?.vendor !== githubCopilotLanguageModel.vendor ||
(!apiConfiguration.vsCodeLmModelSelector.id && !apiConfiguration.vsCodeLmModelSelector.family)
) {
- return i18next.t("settings:validation.modelSelector")
+ return t("settings:validation.modelSelector")
}
break
case providerIdentifiers.fireworks:
if (!apiConfiguration.fireworksApiKey) {
- return i18next.t("settings:validation.apiKey")
+ return t("settings:validation.apiKey")
}
break
case providerIdentifiers.friendli:
if (!apiConfiguration.friendliApiKey) {
- return i18next.t("settings:validation.apiKey")
+ return t("settings:validation.apiKey")
}
break
case providerIdentifiers.qwenCode:
if (!apiConfiguration.qwenCodeOauthPath) {
- return i18next.t("settings:validation.qwenCodeOauthPath")
+ return t("settings:validation.qwenCodeOauthPath")
}
break
case providerIdentifiers.kimiCode:
if ((apiConfiguration.kimiCodeAuthMethod ?? "oauth") === "api-key" && !apiConfiguration.kimiCodeApiKey) {
- return i18next.t("settings:validation.apiKey")
+ return t("settings:validation.apiKey")
}
break
case providerIdentifiers.vercelAiGateway:
if (!apiConfiguration.vercelAiGatewayApiKey) {
- return i18next.t("settings:validation.apiKey")
+ return t("settings:validation.apiKey")
}
break
case providerIdentifiers.opencodeGo:
if (!apiConfiguration.opencodeGoApiKey) {
- return i18next.t("settings:validation.apiKey")
+ return t("settings:validation.apiKey")
}
break
case providerIdentifiers.kenari:
if (!apiConfiguration.kenariApiKey) {
- return i18next.t("settings:validation.apiKey")
+ return t("settings:validation.apiKey")
}
break
case providerIdentifiers.ioIntelligence:
if (!apiConfiguration.ioIntelligenceApiKey) {
- return i18next.t("settings:validation.apiKey")
+ return t("settings:validation.apiKey")
}
break
case providerIdentifiers.nanogpt:
if (!apiConfiguration.nanoGptApiKey) {
- return i18next.t("settings:validation.apiKey")
+ return t("settings:validation.apiKey")
}
break
case providerIdentifiers.zooGateway:
if (!apiConfiguration.zooSessionToken && !zooCodeIsAuthenticated) {
- return i18next.t("settings:validation.zooGatewaySignIn")
+ return t("settings:validation.zooGatewaySignIn")
}
break
case providerIdentifiers.baseten:
if (!apiConfiguration.basetenApiKey) {
- return i18next.t("settings:validation.apiKey")
+ return t("settings:validation.apiKey")
}
break
}
@@ -221,6 +223,7 @@ type ValidationError = {
function validateProviderAgainstOrganizationSettings(
apiConfiguration: ProviderSettings,
+ t: TFunction,
organizationAllowList?: OrganizationAllowList,
): ValidationError | undefined {
if (organizationAllowList && !organizationAllowList.allowAll) {
@@ -234,7 +237,7 @@ function validateProviderAgainstOrganizationSettings(
if (!providerConfig) {
return {
- message: i18next.t("settings:validation.providerNotAllowed", { provider }),
+ message: t("settings:validation.providerNotAllowed", { provider }),
code: "PROVIDER_NOT_ALLOWED",
}
}
@@ -245,7 +248,7 @@ function validateProviderAgainstOrganizationSettings(
if (modelId && !allowedModels.includes(modelId)) {
return {
- message: i18next.t("settings:validation.modelNotAllowed", {
+ message: t("settings:validation.modelNotAllowed", {
model: modelId,
provider,
}),
@@ -265,10 +268,11 @@ function validateProviderAgainstOrganizationSettings(
* provide valid ARNs without restriction. See issue #10108.
*
* @param arn The ARN string to validate
+ * @param t Translates the region mismatch message
* @param region Optional region to check against the ARN's region
* @returns An object with validation results: { isValid, arnRegion, errorMessage }
*/
-export function validateBedrockArn(arn: string, region?: string) {
+export function validateBedrockArn(arn: string, t: TFunction, region?: string) {
// Try to extract region from ARN for region mismatch warning.
// This is a permissive regex that attempts to find the region component
// without enforcing strict ARN format validation.
@@ -280,7 +284,7 @@ export function validateBedrockArn(arn: string, region?: string) {
return {
isValid: true,
arnRegion,
- errorMessage: i18next.t("settings:validation.arn.regionMismatch", { arnRegion, region }),
+ errorMessage: t("settings:validation.arn.regionMismatch", { arnRegion, region }),
}
}
@@ -290,6 +294,7 @@ export function validateBedrockArn(arn: string, region?: string) {
function validateDynamicProviderModelId(
apiConfiguration: ProviderSettings,
+ t: TFunction,
routerModels?: RouterModels,
): string | undefined {
const provider = apiConfiguration.apiProvider ?? ""
@@ -302,13 +307,13 @@ function validateDynamicProviderModelId(
const modelId = getModelId(apiConfiguration)
if (!modelId) {
- return i18next.t("settings:validation.modelId")
+ return t("settings:validation.modelId")
}
const models = routerModels?.[provider]
if (models && Object.keys(models).length > 1 && !Object.keys(models).includes(modelId)) {
- return i18next.t("settings:validation.modelAvailability", { modelId })
+ return t("settings:validation.modelAvailability", { modelId })
}
return undefined
@@ -320,6 +325,7 @@ function validateDynamicProviderModelId(
*/
export function getModelValidationError(
apiConfiguration: ProviderSettings,
+ t: TFunction,
routerModels?: RouterModels,
organizationAllowList?: OrganizationAllowList,
): string | undefined {
@@ -330,13 +336,13 @@ export function getModelValidationError(
apiModelId: modelId || "",
}
- const orgError = validateProviderAgainstOrganizationSettings(configWithModelId, organizationAllowList)
+ const orgError = validateProviderAgainstOrganizationSettings(configWithModelId, t, organizationAllowList)
if (orgError && orgError.code === "MODEL_NOT_ALLOWED") {
return orgError.message
}
- return validateDynamicProviderModelId(configWithModelId, routerModels)
+ return validateDynamicProviderModelId(configWithModelId, t, routerModels)
}
/**
@@ -351,18 +357,19 @@ export function getModelValidationError(
*/
export function validateApiConfigurationExcludingModelErrors(
apiConfiguration: ProviderSettings,
+ t: TFunction,
_routerModels?: RouterModels, // Keeping this for compatibility with the old function.
organizationAllowList?: OrganizationAllowList,
): string | undefined {
if (apiConfiguration.apiProvider !== providerIdentifiers.zooGateway) {
- const keysAndIdsPresentErrorMessage = validateModelsAndKeysProvided(apiConfiguration)
+ const keysAndIdsPresentErrorMessage = validateModelsAndKeysProvided(apiConfiguration, t)
if (keysAndIdsPresentErrorMessage) {
return keysAndIdsPresentErrorMessage
}
}
- const extraBodyError = validateOpenAiExtraBody(apiConfiguration)
+ const extraBodyError = validateOpenAiExtraBody(apiConfiguration, t)
if (extraBodyError) {
return extraBodyError
@@ -370,6 +377,7 @@ export function validateApiConfigurationExcludingModelErrors(
const organizationAllowListError = validateProviderAgainstOrganizationSettings(
apiConfiguration,
+ t,
organizationAllowList,
)