Matt Rubens 9 months ago
parent
commit
00bf91470b
46 changed files with 603 additions and 386 deletions
  1. 29 0
      .roomodes
  2. 2 1
      src/core/Cline.ts
  3. 1 1
      src/core/prompts/__tests__/sections.test.ts
  4. 5 5
      src/core/prompts/__tests__/system.test.ts
  5. 1 1
      src/core/prompts/sections/__tests__/custom-instructions.test.ts
  6. 3 5
      src/core/prompts/sections/custom-instructions.ts
  7. 3 2
      src/core/prompts/system.ts
  8. 4 1
      src/core/webview/ClineProvider.ts
  9. 2 2
      src/core/webview/__tests__/ClineProvider.test.ts
  10. 1 1
      src/shared/ExtensionMessage.ts
  11. 19 0
      src/shared/__tests__/language.test.ts
  12. 2 2
      src/shared/__tests__/modes.test.ts
  13. 14 0
      src/shared/language.ts
  14. 2 2
      src/shared/modes.ts
  15. 7 1
      webview-ui/jest.config.cjs
  16. 133 356
      webview-ui/package-lock.json
  17. 5 1
      webview-ui/package.json
  18. 4 2
      webview-ui/src/App.tsx
  19. 47 0
      webview-ui/src/__mocks__/i18n/TranslationContext.tsx
  20. 62 0
      webview-ui/src/__mocks__/i18n/setup.ts
  21. 4 1
      webview-ui/src/components/chat/ChatView.tsx
  22. 1 1
      webview-ui/src/components/ui/combobox-primitive.tsx
  23. 1 1
      webview-ui/src/context/ExtensionStateContext.tsx
  24. 51 0
      webview-ui/src/i18n/TranslationContext.tsx
  25. 52 0
      webview-ui/src/i18n/__tests__/TranslationContext.test.tsx
  26. 3 0
      webview-ui/src/i18n/locales/ar/chat.json
  27. 3 0
      webview-ui/src/i18n/locales/ca/chat.json
  28. 3 0
      webview-ui/src/i18n/locales/cs/chat.json
  29. 3 0
      webview-ui/src/i18n/locales/de/chat.json
  30. 3 0
      webview-ui/src/i18n/locales/en/chat.json
  31. 3 0
      webview-ui/src/i18n/locales/es/chat.json
  32. 3 0
      webview-ui/src/i18n/locales/fr/chat.json
  33. 3 0
      webview-ui/src/i18n/locales/hi/chat.json
  34. 3 0
      webview-ui/src/i18n/locales/hu/chat.json
  35. 3 0
      webview-ui/src/i18n/locales/it/chat.json
  36. 3 0
      webview-ui/src/i18n/locales/ja/chat.json
  37. 3 0
      webview-ui/src/i18n/locales/ko/chat.json
  38. 3 0
      webview-ui/src/i18n/locales/pl/chat.json
  39. 3 0
      webview-ui/src/i18n/locales/pt-br/chat.json
  40. 3 0
      webview-ui/src/i18n/locales/pt/chat.json
  41. 3 0
      webview-ui/src/i18n/locales/ru/chat.json
  42. 3 0
      webview-ui/src/i18n/locales/tr/chat.json
  43. 3 0
      webview-ui/src/i18n/locales/zh-cn/chat.json
  44. 3 0
      webview-ui/src/i18n/locales/zh-tw/chat.json
  45. 54 0
      webview-ui/src/i18n/setup.ts
  46. 37 0
      webview-ui/src/i18n/test-utils.ts

+ 29 - 0
.roomodes

@@ -0,0 +1,29 @@
+{
+  "customModes": [
+    {
+      "slug": "translator",
+      "name": "Translator",
+      "roleDefinition": "You are Roo, a linguistic specialist focused on translating and managing localization files. Your responsibility is to help maintain and update translation files for the application, ensuring consistency and accuracy across all language resources.",
+      "groups": [
+        "read",
+        ["edit", { "fileRegex": "src/i18n/locales/", "description": "Translation files only" }]
+      ],
+      "customInstructions": "When translating content:\n- Maintain consistent terminology across all translations\n- Respect the JSON structure of translation files\n- Consider context when translating UI strings\n- Watch for placeholders (like {{variable}}) and preserve them in translations\n- Be mindful of text length in UI elements when translating to languages that might require more characters\n- If you need context for a translation, use read_file to examine the components using these strings"
+    },
+    {
+      "slug": "test",
+      "name": "Test",
+      "roleDefinition": "You are Roo, a Jest testing specialist with deep expertise in:\n- Writing and maintaining Jest test suites\n- Test-driven development (TDD) practices\n- Mocking and stubbing with Jest\n- Integration testing strategies\n- TypeScript testing patterns\n- Code coverage analysis\n- Test performance optimization\n\nYour focus is on maintaining high test quality and coverage across the codebase, working primarily with:\n- Test files in __tests__ directories\n- Mock implementations in __mocks__\n- Test utilities and helpers\n- Jest configuration and setup\n\nYou ensure tests are:\n- Well-structured and maintainable\n- Following Jest best practices\n- Properly typed with TypeScript\n- Providing meaningful coverage\n- Using appropriate mocking strategies",
+      "groups": [
+        "read",
+        "browser",
+        "command",
+        ["edit", {
+          "fileRegex": "(__tests__/.*|__mocks__/.*|\\.test\\.(ts|tsx|js|jsx)$|/test/.*|jest\\.config\\.(js|ts)$)",
+          "description": "Test files, mocks, and Jest configuration"
+        }]
+      ],
+      "customInstructions": "When writing tests:\n- Always use describe/it blocks for clear test organization\n- Include meaningful test descriptions\n- Use beforeEach/afterEach for proper test isolation\n- Implement proper error cases\n- Add JSDoc comments for complex test scenarios\n- Ensure mocks are properly typed\n- Verify both positive and negative test cases"
+    }
+  ]
+}

+ 2 - 1
src/core/Cline.ts

@@ -70,6 +70,7 @@ import { truncateConversationIfNeeded } from "./sliding-window"
 import { ClineProvider } from "./webview/ClineProvider"
 import { detectCodeOmission } from "../integrations/editor/detect-omission"
 import { BrowserSession } from "../services/browser/BrowserSession"
+import { formatLanguage } from "../shared/language"
 import { McpHub } from "../services/mcp/McpHub"
 import crypto from "crypto"
 import { insertGroups } from "./diff/insert-groups"
@@ -3668,7 +3669,7 @@ export class Cline {
 		const modeDetails = await getFullModeDetails(currentMode, customModes, customModePrompts, {
 			cwd,
 			globalCustomInstructions,
-			vscodeLanguage: vscode.env.language,
+			language: formatLanguage(vscode.env.language),
 		})
 		details += `\n\n# Current Mode\n`
 		details += `<slug>${currentMode}</slug>\n`

+ 1 - 1
src/core/prompts/__tests__/sections.test.ts

@@ -9,7 +9,7 @@ describe("addCustomInstructions", () => {
 			"global instructions",
 			"/test/path",
 			"test-mode",
-			{ vscodeLanguage: "fr" },
+			{ language: "fr" },
 		)
 
 		expect(result).toContain("Language Preference:")

+ 5 - 5
src/core/prompts/__tests__/system.test.ts

@@ -37,14 +37,14 @@ __setMockImplementation(
 		globalCustomInstructions: string,
 		cwd: string,
 		mode: string,
-		options?: { vscodeLanguage?: string },
+		options?: { language?: string },
 	) => {
 		const sections = []
 
 		// Add language preference if provided
-		if (options?.vscodeLanguage) {
+		if (options?.language) {
 			sections.push(
-				`Language Preference:\nYou should always speak and think in the "${options.vscodeLanguage}" language.`,
+				`Language Preference:\nYou should always speak and think in the "${options.language}" language.`,
 			)
 		}
 
@@ -792,7 +792,7 @@ describe("addCustomInstructions", () => {
 
 	it("should include preferred language when provided", async () => {
 		const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug, {
-			vscodeLanguage: "es",
+			language: "es",
 		})
 		expect(instructions).toMatchSnapshot()
 	})
@@ -808,7 +808,7 @@ describe("addCustomInstructions", () => {
 			"",
 			"/test/path",
 			defaultModeSlug,
-			{ vscodeLanguage: "fr" },
+			{ language: "fr" },
 		)
 		expect(instructions).toMatchSnapshot()
 	})

+ 1 - 1
src/core/prompts/sections/__tests__/custom-instructions.test.ts

@@ -113,7 +113,7 @@ describe("addCustomInstructions", () => {
 			"global instructions",
 			"/fake/path",
 			"test-mode",
-			{ vscodeLanguage: "es" },
+			{ language: "es" },
 		)
 
 		expect(result).toContain("Language Preference:")

+ 3 - 5
src/core/prompts/sections/custom-instructions.ts

@@ -34,7 +34,7 @@ export async function addCustomInstructions(
 	globalCustomInstructions: string,
 	cwd: string,
 	mode: string,
-	options: { vscodeLanguage?: string; rooIgnoreInstructions?: string } = {},
+	options: { language?: string; rooIgnoreInstructions?: string } = {},
 ): Promise<string> {
 	const sections = []
 
@@ -46,10 +46,8 @@ export async function addCustomInstructions(
 	}
 
 	// Add language preference if provided
-	if (options.vscodeLanguage) {
-		sections.push(
-			`Language Preference:\nYou should always speak and think in the "${options.vscodeLanguage}" language.`,
-		)
+	if (options.language) {
+		sections.push(`Language Preference:\nYou should always speak and think in the "${options.language}" language.`)
 	}
 
 	// Add global instructions first

+ 3 - 2
src/core/prompts/system.ts

@@ -25,6 +25,7 @@ import {
 	addCustomInstructions,
 } from "./sections"
 import { loadSystemPromptFile } from "./sections/custom-system-prompt"
+import { formatLanguage } from "../../shared/language"
 
 async function generatePrompt(
 	context: vscode.ExtensionContext,
@@ -95,7 +96,7 @@ ${getSystemInfoSection(cwd, mode, customModeConfigs)}
 
 ${getObjectiveSection()}
 
-${await addCustomInstructions(promptComponent?.customInstructions || modeConfig.customInstructions || "", globalCustomInstructions || "", cwd, mode, { vscodeLanguage: vscode.env.language, rooIgnoreInstructions })}`
+${await addCustomInstructions(promptComponent?.customInstructions || modeConfig.customInstructions || "", globalCustomInstructions || "", cwd, mode, { language: formatLanguage(vscode.env.language), rooIgnoreInstructions })}`
 
 	return basePrompt
 }
@@ -144,7 +145,7 @@ export const SYSTEM_PROMPT = async (
 			globalCustomInstructions || "",
 			cwd,
 			mode,
-			{ vscodeLanguage: vscode.env.language, rooIgnoreInstructions },
+			{ language: formatLanguage(vscode.env.language), rooIgnoreInstructions },
 		)
 		// For file-based prompts, don't include the tool sections
 		return `${roleDefinition}

+ 4 - 1
src/core/webview/ClineProvider.ts

@@ -26,6 +26,7 @@ import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage
 import { Mode, CustomModePrompts, PromptComponent, defaultModeSlug, ModeConfig } from "../../shared/modes"
 import { checkExistKey } from "../../shared/checkExistApiConfig"
 import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, ExperimentId } from "../../shared/experiments"
+import { formatLanguage } from "../../shared/language"
 import { downloadTask } from "../../integrations/misc/export-markdown"
 import { openFile, openImage } from "../../integrations/misc/open-file"
 import { selectImages } from "../../integrations/misc/process-images"
@@ -2358,6 +2359,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			browserToolEnabled,
 			telemetrySetting,
 			showRooIgnoredFiles,
+			language,
 		} = await this.getState()
 		const telemetryKey = process.env.POSTHOG_API_KEY
 		const machineId = vscode.env.machineId
@@ -2422,6 +2424,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			telemetryKey,
 			machineId,
 			showRooIgnoredFiles: showRooIgnoredFiles ?? true,
+			language,
 		}
 	}
 
@@ -2556,7 +2559,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			terminalOutputLineLimit: stateValues.terminalOutputLineLimit ?? 500,
 			mode: stateValues.mode ?? defaultModeSlug,
 			// Pass the VSCode language code directly
-			vscodeLanguage: vscode.env.language,
+			language: formatLanguage(vscode.env.language),
 			mcpEnabled: stateValues.mcpEnabled ?? true,
 			enableMcpServerCreation: stateValues.enableMcpServerCreation ?? true,
 			alwaysApproveResubmit: stateValues.alwaysApproveResubmit ?? false,

+ 2 - 2
src/core/webview/__tests__/ClineProvider.test.ts

@@ -534,12 +534,12 @@ describe("ClineProvider", () => {
 		expect(state).toHaveProperty("writeDelayMs")
 	})
 
-	test("vscodeLanguage is set to VSCode language", async () => {
+	test("language is set to VSCode language", async () => {
 		// Mock VSCode language as Spanish
 		;(vscode.env as any).language = "es-ES"
 
 		const state = await provider.getState()
-		expect(state.vscodeLanguage).toBe("es-ES")
+		expect(state.language).toBe("es-ES")
 	})
 
 	test("diffEnabled defaults to true when not set", async () => {

+ 1 - 1
src/shared/ExtensionMessage.ts

@@ -131,7 +131,7 @@ export interface ExtensionState {
 	remoteBrowserHost?: string
 	remoteBrowserEnabled?: boolean
 	fuzzyMatchThreshold?: number
-	vscodeLanguage?: string
+	language?: string
 	writeDelayMs: number
 	terminalOutputLineLimit?: number
 	mcpEnabled: boolean

+ 19 - 0
src/shared/__tests__/language.test.ts

@@ -0,0 +1,19 @@
+import { formatLanguage } from "../language"
+
+describe("formatLanguage", () => {
+	it("should uppercase region code in locale string", () => {
+		expect(formatLanguage("en-us")).toBe("en-US")
+		expect(formatLanguage("fr-ca")).toBe("fr-CA")
+		expect(formatLanguage("de-de")).toBe("de-DE")
+	})
+
+	it("should return original string if no region code present", () => {
+		expect(formatLanguage("en")).toBe("en")
+		expect(formatLanguage("fr")).toBe("fr")
+	})
+
+	it("should handle empty or undefined input", () => {
+		expect(formatLanguage("")).toBe("en")
+		expect(formatLanguage(undefined as unknown as string)).toBe("en")
+	})
+})

+ 2 - 2
src/shared/__tests__/modes.test.ts

@@ -401,7 +401,7 @@ describe("FileRestrictionError", () => {
 			const options = {
 				cwd: "/test/path",
 				globalCustomInstructions: "Global instructions",
-				vscodeLanguage: "en",
+				language: "en",
 			}
 
 			await getFullModeDetails("debug", undefined, undefined, options)
@@ -411,7 +411,7 @@ describe("FileRestrictionError", () => {
 				"Global instructions",
 				"/test/path",
 				"debug",
-				{ vscodeLanguage: "en" },
+				{ language: "en" },
 			)
 		})
 

+ 14 - 0
src/shared/language.ts

@@ -0,0 +1,14 @@
+/**
+ * Formats a VSCode locale string to ensure the region code is uppercase.
+ * For example, transforms "en-us" to "en-US" or "fr-ca" to "fr-CA".
+ *
+ * @param vscodeLocale - The VSCode locale string to format (e.g., "en-us", "fr-ca")
+ * @returns The formatted locale string with uppercase region code
+ */
+export function formatLanguage(vscodeLocale: string): string {
+	if (!vscodeLocale) {
+		return "en" // Default to English if no locale is provided
+	}
+
+	return vscodeLocale.replace(/-(\w+)$/, (_, region) => `-${region.toUpperCase()}`)
+}

+ 2 - 2
src/shared/modes.ts

@@ -278,7 +278,7 @@ export async function getFullModeDetails(
 	options?: {
 		cwd?: string
 		globalCustomInstructions?: string
-		vscodeLanguage?: string
+		language?: string
 	},
 ): Promise<ModeConfig> {
 	// First get the base mode config from custom modes or built-in modes
@@ -298,7 +298,7 @@ export async function getFullModeDetails(
 			options.globalCustomInstructions || "",
 			options.cwd,
 			modeSlug,
-			{ vscodeLanguage: options.vscodeLanguage },
+			{ language: options.language },
 		)
 	}
 

+ 7 - 1
webview-ui/jest.config.cjs

@@ -4,7 +4,7 @@ module.exports = {
 	testEnvironment: "jsdom",
 	injectGlobals: true,
 	moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
-	transform: { "^.+\\.(ts|tsx)$": ["ts-jest", { tsconfig: { jsx: "react-jsx" } }] },
+	transform: { "^.+\\.(ts|tsx)$": ["ts-jest", { tsconfig: { jsx: "react-jsx", module: "ESNext" } }] },
 	testMatch: ["<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}", "<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"],
 	setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"],
 	moduleNameMapper: {
@@ -12,6 +12,12 @@ module.exports = {
 		"^vscrui$": "<rootDir>/src/__mocks__/vscrui.ts",
 		"^@vscode/webview-ui-toolkit/react$": "<rootDir>/src/__mocks__/@vscode/webview-ui-toolkit/react.ts",
 		"^@/(.*)$": "<rootDir>/src/$1",
+		"^src/i18n/setup$": "<rootDir>/src/__mocks__/i18n/setup.ts",
+		"^\\.\\./setup$": "<rootDir>/src/__mocks__/i18n/setup.ts",
+		"^\\./setup$": "<rootDir>/src/__mocks__/i18n/setup.ts",
+		"^src/i18n/TranslationContext$": "<rootDir>/src/__mocks__/i18n/TranslationContext.tsx",
+		"^\\.\\./TranslationContext$": "<rootDir>/src/__mocks__/i18n/TranslationContext.tsx",
+		"^\\./TranslationContext$": "<rootDir>/src/__mocks__/i18n/TranslationContext.tsx"
 	},
 	reporters: [["jest-simple-dot-reporter", {}]],
 	transformIgnorePatterns: [

+ 133 - 356
webview-ui/package-lock.json

@@ -21,6 +21,7 @@
 				"@radix-ui/react-slot": "^1.1.2",
 				"@radix-ui/react-tooltip": "^1.1.8",
 				"@tailwindcss/vite": "^4.0.0",
+				"@testing-library/dom": "^10.4.0",
 				"@vscode/webview-ui-toolkit": "^1.4.0",
 				"class-variance-authority": "^0.7.1",
 				"clsx": "^2.1.1",
@@ -28,11 +29,14 @@
 				"debounce": "^2.1.1",
 				"fast-deep-equal": "^3.1.3",
 				"fzf": "^0.5.2",
+				"i18next": "^24.2.2",
+				"i18next-http-backend": "^3.0.2",
 				"lucide-react": "^0.475.0",
 				"mermaid": "^11.4.1",
 				"posthog-js": "^1.227.2",
 				"react": "^18.3.1",
 				"react-dom": "^18.3.1",
+				"react-i18next": "^15.4.1",
 				"react-markdown": "^9.0.3",
 				"react-remark": "^2.1.0",
 				"react-textarea-autosize": "^8.5.3",
@@ -78,7 +82,7 @@
 				"storybook": "^8.5.6",
 				"storybook-dark-mode": "^4.0.2",
 				"ts-jest": "^29.2.5",
-				"typescript": "^4.9.5",
+				"typescript": "^5.4.5",
 				"vite": "6.0.11"
 			}
 		},
@@ -127,7 +131,6 @@
 			"version": "7.26.2",
 			"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
 			"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
-			"dev": true,
 			"license": "MIT",
 			"dependencies": {
 				"@babel/helper-validator-identifier": "^7.25.9",
@@ -445,7 +448,6 @@
 			"version": "7.25.9",
 			"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
 			"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
-			"dev": true,
 			"license": "MIT",
 			"engines": {
 				"node": ">=6.9.0"
@@ -6165,26 +6167,6 @@
 				"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta"
 			}
 		},
-		"node_modules/@storybook/instrumenter": {
-			"version": "8.5.6",
-			"resolved": "https://registry.npmjs.org/@storybook/instrumenter/-/instrumenter-8.5.6.tgz",
-			"integrity": "sha512-uMOOiq/9dFoFhSl3IxuQ+yq4lClkcRtEuB6cPzD/rVCmlh+i//VkHTqFCNrDvpVA21Lsy9NLmnxLHJpBGN3Avg==",
-			"dev": true,
-			"license": "MIT",
-			"optional": true,
-			"peer": true,
-			"dependencies": {
-				"@storybook/global": "^5.0.0",
-				"@vitest/utils": "^2.1.1"
-			},
-			"funding": {
-				"type": "opencollective",
-				"url": "https://opencollective.com/storybook"
-			},
-			"peerDependencies": {
-				"storybook": "^8.5.6"
-			}
-		},
 		"node_modules/@storybook/manager-api": {
 			"version": "8.5.6",
 			"resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-8.5.6.tgz",
@@ -6303,96 +6285,6 @@
 				}
 			}
 		},
-		"node_modules/@storybook/test": {
-			"version": "8.5.6",
-			"resolved": "https://registry.npmjs.org/@storybook/test/-/test-8.5.6.tgz",
-			"integrity": "sha512-U4HdyAcCwc/ictwq0HWKI6j2NAUggB9ENfyH3baEWaLEI+mp4pzQMuTnOIF9TvqU7K1D5UqOyfs/hlbFxUFysg==",
-			"dev": true,
-			"license": "MIT",
-			"optional": true,
-			"peer": true,
-			"dependencies": {
-				"@storybook/csf": "0.1.12",
-				"@storybook/global": "^5.0.0",
-				"@storybook/instrumenter": "8.5.6",
-				"@testing-library/dom": "10.4.0",
-				"@testing-library/jest-dom": "6.5.0",
-				"@testing-library/user-event": "14.5.2",
-				"@vitest/expect": "2.0.5",
-				"@vitest/spy": "2.0.5"
-			},
-			"funding": {
-				"type": "opencollective",
-				"url": "https://opencollective.com/storybook"
-			},
-			"peerDependencies": {
-				"storybook": "^8.5.6"
-			}
-		},
-		"node_modules/@storybook/test/node_modules/@testing-library/jest-dom": {
-			"version": "6.5.0",
-			"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz",
-			"integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==",
-			"dev": true,
-			"license": "MIT",
-			"optional": true,
-			"peer": true,
-			"dependencies": {
-				"@adobe/css-tools": "^4.4.0",
-				"aria-query": "^5.0.0",
-				"chalk": "^3.0.0",
-				"css.escape": "^1.5.1",
-				"dom-accessibility-api": "^0.6.3",
-				"lodash": "^4.17.21",
-				"redent": "^3.0.0"
-			},
-			"engines": {
-				"node": ">=14",
-				"npm": ">=6",
-				"yarn": ">=1"
-			}
-		},
-		"node_modules/@storybook/test/node_modules/@testing-library/user-event": {
-			"version": "14.5.2",
-			"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz",
-			"integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==",
-			"dev": true,
-			"license": "MIT",
-			"optional": true,
-			"peer": true,
-			"engines": {
-				"node": ">=12",
-				"npm": ">=6"
-			},
-			"peerDependencies": {
-				"@testing-library/dom": ">=7.21.4"
-			}
-		},
-		"node_modules/@storybook/test/node_modules/chalk": {
-			"version": "3.0.0",
-			"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
-			"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
-			"dev": true,
-			"license": "MIT",
-			"optional": true,
-			"peer": true,
-			"dependencies": {
-				"ansi-styles": "^4.1.0",
-				"supports-color": "^7.1.0"
-			},
-			"engines": {
-				"node": ">=8"
-			}
-		},
-		"node_modules/@storybook/test/node_modules/dom-accessibility-api": {
-			"version": "0.6.3",
-			"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
-			"integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
-			"dev": true,
-			"license": "MIT",
-			"optional": true,
-			"peer": true
-		},
 		"node_modules/@storybook/theming": {
 			"version": "8.5.6",
 			"resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.5.6.tgz",
@@ -6644,9 +6536,6 @@
 			"version": "10.4.0",
 			"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
 			"integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
-			"dev": true,
-			"license": "MIT",
-			"peer": true,
 			"dependencies": {
 				"@babel/code-frame": "^7.10.4",
 				"@babel/runtime": "^7.12.5",
@@ -6666,7 +6555,6 @@
 			"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz",
 			"integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==",
 			"dev": true,
-			"license": "MIT",
 			"dependencies": {
 				"@adobe/css-tools": "^4.4.0",
 				"aria-query": "^5.0.0",
@@ -6708,7 +6596,6 @@
 			"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.2.0.tgz",
 			"integrity": "sha512-2cSskAvA1QNtKc8Y9VJQRv0tm3hLVgxRGDB+KYhIaPQJ1I+RHbhIXcM+zClKXzMes/wshsMVzf4B9vS4IZpqDQ==",
 			"dev": true,
-			"license": "MIT",
 			"dependencies": {
 				"@babel/runtime": "^7.12.5"
 			},
@@ -6758,10 +6645,7 @@
 		"node_modules/@types/aria-query": {
 			"version": "5.0.4",
 			"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
-			"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
-			"dev": true,
-			"license": "MIT",
-			"peer": true
+			"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="
 		},
 		"node_modules/@types/babel__core": {
 			"version": "7.20.5",
@@ -7771,116 +7655,6 @@
 				"vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
 			}
 		},
-		"node_modules/@vitest/expect": {
-			"version": "2.0.5",
-			"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz",
-			"integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==",
-			"dev": true,
-			"license": "MIT",
-			"optional": true,
-			"peer": true,
-			"dependencies": {
-				"@vitest/spy": "2.0.5",
-				"@vitest/utils": "2.0.5",
-				"chai": "^5.1.1",
-				"tinyrainbow": "^1.2.0"
-			},
-			"funding": {
-				"url": "https://opencollective.com/vitest"
-			}
-		},
-		"node_modules/@vitest/expect/node_modules/@vitest/pretty-format": {
-			"version": "2.0.5",
-			"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz",
-			"integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==",
-			"dev": true,
-			"license": "MIT",
-			"optional": true,
-			"peer": true,
-			"dependencies": {
-				"tinyrainbow": "^1.2.0"
-			},
-			"funding": {
-				"url": "https://opencollective.com/vitest"
-			}
-		},
-		"node_modules/@vitest/expect/node_modules/@vitest/utils": {
-			"version": "2.0.5",
-			"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz",
-			"integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==",
-			"dev": true,
-			"license": "MIT",
-			"optional": true,
-			"peer": true,
-			"dependencies": {
-				"@vitest/pretty-format": "2.0.5",
-				"estree-walker": "^3.0.3",
-				"loupe": "^3.1.1",
-				"tinyrainbow": "^1.2.0"
-			},
-			"funding": {
-				"url": "https://opencollective.com/vitest"
-			}
-		},
-		"node_modules/@vitest/expect/node_modules/estree-walker": {
-			"version": "3.0.3",
-			"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
-			"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
-			"dev": true,
-			"license": "MIT",
-			"optional": true,
-			"peer": true,
-			"dependencies": {
-				"@types/estree": "^1.0.0"
-			}
-		},
-		"node_modules/@vitest/pretty-format": {
-			"version": "2.1.9",
-			"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz",
-			"integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==",
-			"dev": true,
-			"license": "MIT",
-			"optional": true,
-			"peer": true,
-			"dependencies": {
-				"tinyrainbow": "^1.2.0"
-			},
-			"funding": {
-				"url": "https://opencollective.com/vitest"
-			}
-		},
-		"node_modules/@vitest/spy": {
-			"version": "2.0.5",
-			"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz",
-			"integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==",
-			"dev": true,
-			"license": "MIT",
-			"optional": true,
-			"peer": true,
-			"dependencies": {
-				"tinyspy": "^3.0.0"
-			},
-			"funding": {
-				"url": "https://opencollective.com/vitest"
-			}
-		},
-		"node_modules/@vitest/utils": {
-			"version": "2.1.9",
-			"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz",
-			"integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==",
-			"dev": true,
-			"license": "MIT",
-			"optional": true,
-			"peer": true,
-			"dependencies": {
-				"@vitest/pretty-format": "2.1.9",
-				"loupe": "^3.1.2",
-				"tinyrainbow": "^1.2.0"
-			},
-			"funding": {
-				"url": "https://opencollective.com/vitest"
-			}
-		},
 		"node_modules/@vscode/webview-ui-toolkit": {
 			"version": "1.4.0",
 			"resolved": "https://registry.npmjs.org/@vscode/webview-ui-toolkit/-/webview-ui-toolkit-1.4.0.tgz",
@@ -8007,7 +7781,6 @@
 			"version": "5.0.1",
 			"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
 			"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
-			"dev": true,
 			"license": "MIT",
 			"engines": {
 				"node": ">=8"
@@ -8017,7 +7790,6 @@
 			"version": "4.3.0",
 			"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
 			"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-			"dev": true,
 			"license": "MIT",
 			"dependencies": {
 				"color-convert": "^2.0.1"
@@ -8066,7 +7838,6 @@
 			"version": "5.3.0",
 			"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
 			"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
-			"dev": true,
 			"license": "Apache-2.0",
 			"dependencies": {
 				"dequal": "^2.0.3"
@@ -8239,18 +8010,6 @@
 				"url": "https://github.com/sponsors/ljharb"
 			}
 		},
-		"node_modules/assertion-error": {
-			"version": "2.0.1",
-			"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
-			"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
-			"dev": true,
-			"license": "MIT",
-			"optional": true,
-			"peer": true,
-			"engines": {
-				"node": ">=12"
-			}
-		},
 		"node_modules/ast-types": {
 			"version": "0.16.1",
 			"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz",
@@ -8770,30 +8529,10 @@
 				"url": "https://github.com/sponsors/wooorm"
 			}
 		},
-		"node_modules/chai": {
-			"version": "5.2.0",
-			"resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz",
-			"integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==",
-			"dev": true,
-			"license": "MIT",
-			"optional": true,
-			"peer": true,
-			"dependencies": {
-				"assertion-error": "^2.0.1",
-				"check-error": "^2.1.1",
-				"deep-eql": "^5.0.1",
-				"loupe": "^3.1.0",
-				"pathval": "^2.0.0"
-			},
-			"engines": {
-				"node": ">=12"
-			}
-		},
 		"node_modules/chalk": {
 			"version": "4.1.2",
 			"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
 			"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-			"dev": true,
 			"license": "MIT",
 			"dependencies": {
 				"ansi-styles": "^4.1.0",
@@ -8856,18 +8595,6 @@
 				"url": "https://github.com/sponsors/wooorm"
 			}
 		},
-		"node_modules/check-error": {
-			"version": "2.1.1",
-			"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
-			"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
-			"dev": true,
-			"license": "MIT",
-			"optional": true,
-			"peer": true,
-			"engines": {
-				"node": ">= 16"
-			}
-		},
 		"node_modules/chevrotain": {
 			"version": "11.0.3",
 			"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
@@ -9405,7 +9132,6 @@
 			"version": "2.0.1",
 			"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
 			"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-			"dev": true,
 			"license": "MIT",
 			"dependencies": {
 				"color-name": "~1.1.4"
@@ -9418,7 +9144,6 @@
 			"version": "1.1.4",
 			"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
 			"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-			"dev": true,
 			"license": "MIT"
 		},
 		"node_modules/combined-stream": {
@@ -9568,6 +9293,14 @@
 				"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
 			}
 		},
+		"node_modules/cross-fetch": {
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
+			"integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
+			"dependencies": {
+				"node-fetch": "^2.6.12"
+			}
+		},
 		"node_modules/cross-spawn": {
 			"version": "7.0.6",
 			"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -10277,18 +10010,6 @@
 				}
 			}
 		},
-		"node_modules/deep-eql": {
-			"version": "5.0.2",
-			"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
-			"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
-			"dev": true,
-			"license": "MIT",
-			"optional": true,
-			"peer": true,
-			"engines": {
-				"node": ">=6"
-			}
-		},
 		"node_modules/deep-is": {
 			"version": "0.1.4",
 			"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -10459,10 +10180,7 @@
 		"node_modules/dom-accessibility-api": {
 			"version": "0.5.16",
 			"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
-			"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
-			"dev": true,
-			"license": "MIT",
-			"peer": true
+			"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="
 		},
 		"node_modules/domexception": {
 			"version": "4.0.0",
@@ -12709,7 +12427,6 @@
 			"version": "4.0.0",
 			"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
 			"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-			"dev": true,
 			"license": "MIT",
 			"engines": {
 				"node": ">=8"
@@ -13070,6 +12787,14 @@
 			"dev": true,
 			"license": "MIT"
 		},
+		"node_modules/html-parse-stringify": {
+			"version": "3.0.1",
+			"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
+			"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
+			"dependencies": {
+				"void-elements": "3.1.0"
+			}
+		},
 		"node_modules/html-url-attributes": {
 			"version": "3.0.1",
 			"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
@@ -13136,6 +12861,44 @@
 			"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
 			"license": "BSD-3-Clause"
 		},
+		"node_modules/i18next": {
+			"version": "24.2.2",
+			"resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.2.tgz",
+			"integrity": "sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ==",
+			"funding": [
+				{
+					"type": "individual",
+					"url": "https://locize.com"
+				},
+				{
+					"type": "individual",
+					"url": "https://locize.com/i18next.html"
+				},
+				{
+					"type": "individual",
+					"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+				}
+			],
+			"dependencies": {
+				"@babel/runtime": "^7.23.2"
+			},
+			"peerDependencies": {
+				"typescript": "^5"
+			},
+			"peerDependenciesMeta": {
+				"typescript": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/i18next-http-backend": {
+			"version": "3.0.2",
+			"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz",
+			"integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==",
+			"dependencies": {
+				"cross-fetch": "4.0.0"
+			}
+		},
 		"node_modules/iconv-lite": {
 			"version": "0.6.3",
 			"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -15602,15 +15365,6 @@
 				"loose-envify": "cli.js"
 			}
 		},
-		"node_modules/loupe": {
-			"version": "3.1.3",
-			"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz",
-			"integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==",
-			"dev": true,
-			"license": "MIT",
-			"optional": true,
-			"peer": true
-		},
 		"node_modules/lowlight": {
 			"version": "3.3.0",
 			"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
@@ -15649,9 +15403,6 @@
 			"version": "1.5.0",
 			"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
 			"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
-			"dev": true,
-			"license": "MIT",
-			"peer": true,
 			"bin": {
 				"lz-string": "bin/bin.js"
 			}
@@ -17793,6 +17544,44 @@
 			"dev": true,
 			"license": "MIT"
 		},
+		"node_modules/node-fetch": {
+			"version": "2.7.0",
+			"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+			"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+			"dependencies": {
+				"whatwg-url": "^5.0.0"
+			},
+			"engines": {
+				"node": "4.x || >=6.0.0"
+			},
+			"peerDependencies": {
+				"encoding": "^0.1.0"
+			},
+			"peerDependenciesMeta": {
+				"encoding": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/node-fetch/node_modules/tr46": {
+			"version": "0.0.3",
+			"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+			"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+		},
+		"node_modules/node-fetch/node_modules/webidl-conversions": {
+			"version": "3.0.1",
+			"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+			"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+		},
+		"node_modules/node-fetch/node_modules/whatwg-url": {
+			"version": "5.0.0",
+			"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+			"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+			"dependencies": {
+				"tr46": "~0.0.3",
+				"webidl-conversions": "^3.0.0"
+			}
+		},
 		"node_modules/node-int64": {
 			"version": "0.4.0",
 			"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -18252,18 +18041,6 @@
 			"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
 			"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="
 		},
-		"node_modules/pathval": {
-			"version": "2.0.0",
-			"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
-			"integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
-			"dev": true,
-			"license": "MIT",
-			"optional": true,
-			"peer": true,
-			"engines": {
-				"node": ">= 14.16"
-			}
-		},
 		"node_modules/picocolors": {
 			"version": "1.1.1",
 			"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -18489,7 +18266,6 @@
 			"version": "27.5.1",
 			"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
 			"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
-			"dev": true,
 			"license": "MIT",
 			"dependencies": {
 				"ansi-regex": "^5.0.1",
@@ -18504,7 +18280,6 @@
 			"version": "5.2.0",
 			"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
 			"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
-			"dev": true,
 			"license": "MIT",
 			"engines": {
 				"node": ">=10"
@@ -18725,11 +18500,31 @@
 				"react": "^18.3.1"
 			}
 		},
+		"node_modules/react-i18next": {
+			"version": "15.4.1",
+			"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.1.tgz",
+			"integrity": "sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw==",
+			"dependencies": {
+				"@babel/runtime": "^7.25.0",
+				"html-parse-stringify": "^3.0.1"
+			},
+			"peerDependencies": {
+				"i18next": ">= 23.2.3",
+				"react": ">= 16.8.0"
+			},
+			"peerDependenciesMeta": {
+				"react-dom": {
+					"optional": true
+				},
+				"react-native": {
+					"optional": true
+				}
+			}
+		},
 		"node_modules/react-is": {
 			"version": "17.0.2",
 			"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
 			"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
-			"dev": true,
 			"license": "MIT"
 		},
 		"node_modules/react-markdown": {
@@ -20671,7 +20466,6 @@
 			"version": "7.2.0",
 			"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
 			"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-			"dev": true,
 			"license": "MIT",
 			"dependencies": {
 				"has-flag": "^4.0.0"
@@ -20783,30 +20577,6 @@
 			"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
 			"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="
 		},
-		"node_modules/tinyrainbow": {
-			"version": "1.2.0",
-			"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz",
-			"integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==",
-			"dev": true,
-			"license": "MIT",
-			"optional": true,
-			"peer": true,
-			"engines": {
-				"node": ">=14.0.0"
-			}
-		},
-		"node_modules/tinyspy": {
-			"version": "3.0.2",
-			"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
-			"integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
-			"dev": true,
-			"license": "MIT",
-			"optional": true,
-			"peer": true,
-			"engines": {
-				"node": ">=14.0.0"
-			}
-		},
 		"node_modules/tmpl": {
 			"version": "1.0.5",
 			"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -21141,17 +20911,16 @@
 			}
 		},
 		"node_modules/typescript": {
-			"version": "4.9.5",
-			"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
-			"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
-			"dev": true,
-			"license": "Apache-2.0",
+			"version": "5.7.3",
+			"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
+			"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
+			"devOptional": true,
 			"bin": {
 				"tsc": "bin/tsc",
 				"tsserver": "bin/tsserver"
 			},
 			"engines": {
-				"node": ">=4.2.0"
+				"node": ">=14.17"
 			}
 		},
 		"node_modules/ufo": {
@@ -21742,6 +21511,14 @@
 				}
 			}
 		},
+		"node_modules/void-elements": {
+			"version": "3.1.0",
+			"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
+			"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
+			"engines": {
+				"node": ">=0.10.0"
+			}
+		},
 		"node_modules/vscode-jsonrpc": {
 			"version": "8.2.0",
 			"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",

+ 5 - 1
webview-ui/package.json

@@ -28,6 +28,7 @@
 		"@radix-ui/react-slot": "^1.1.2",
 		"@radix-ui/react-tooltip": "^1.1.8",
 		"@tailwindcss/vite": "^4.0.0",
+		"@testing-library/dom": "^10.4.0",
 		"@vscode/webview-ui-toolkit": "^1.4.0",
 		"class-variance-authority": "^0.7.1",
 		"clsx": "^2.1.1",
@@ -35,11 +36,14 @@
 		"debounce": "^2.1.1",
 		"fast-deep-equal": "^3.1.3",
 		"fzf": "^0.5.2",
+		"i18next": "^24.2.2",
+		"i18next-http-backend": "^3.0.2",
 		"lucide-react": "^0.475.0",
 		"mermaid": "^11.4.1",
 		"posthog-js": "^1.227.2",
 		"react": "^18.3.1",
 		"react-dom": "^18.3.1",
+		"react-i18next": "^15.4.1",
 		"react-markdown": "^9.0.3",
 		"react-remark": "^2.1.0",
 		"react-textarea-autosize": "^8.5.3",
@@ -85,7 +89,7 @@
 		"storybook": "^8.5.6",
 		"storybook-dark-mode": "^4.0.2",
 		"ts-jest": "^29.2.5",
-		"typescript": "^4.9.5",
+		"typescript": "^5.4.5",
 		"vite": "6.0.11"
 	}
 }

+ 4 - 2
webview-ui/src/App.tsx

@@ -1,8 +1,8 @@
 import { useCallback, useEffect, useRef, useState } from "react"
 import { useEvent } from "react-use"
-
 import { ExtensionMessage } from "../../src/shared/ExtensionMessage"
 import { ShowHumanRelayDialogMessage } from "../../src/shared/ExtensionMessage"
+import TranslationProvider from "./i18n/TranslationContext"
 
 import { vscode } from "./utils/vscode"
 import { telemetryClient } from "./utils/TelemetryClient"
@@ -131,7 +131,9 @@ const App = () => {
 
 const AppWithProviders = () => (
 	<ExtensionStateContextProvider>
-		<App />
+		<TranslationProvider>
+			<App />
+		</TranslationProvider>
 	</ExtensionStateContextProvider>
 )
 

+ 47 - 0
webview-ui/src/__mocks__/i18n/TranslationContext.tsx

@@ -0,0 +1,47 @@
+import React, { ReactNode } from "react"
+import i18next from "./setup"
+
+// Create a mock context
+export const TranslationContext = React.createContext<{
+	t: (key: string, options?: Record<string, any>) => string
+	i18n: typeof i18next
+}>({
+	t: (key: string, options?: Record<string, any>) => {
+		// Handle specific test cases
+		if (key === "settings.autoApprove.title") {
+			return "Auto-Approve"
+		}
+		if (key === "notifications.error" && options?.message) {
+			return `Operation failed: ${options.message}`
+		}
+		return key // Default fallback
+	},
+	i18n: i18next,
+})
+
+// Mock translation provider
+export const TranslationProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
+	return (
+		<TranslationContext.Provider
+			value={{
+				t: (key: string, options?: Record<string, any>) => {
+					// Handle specific test cases
+					if (key === "settings.autoApprove.title") {
+						return "Auto-Approve"
+					}
+					if (key === "notifications.error" && options?.message) {
+						return `Operation failed: ${options.message}`
+					}
+					return key // Default fallback
+				},
+				i18n: i18next,
+			}}>
+			{children}
+		</TranslationContext.Provider>
+	)
+}
+
+// Custom hook for easy translations
+export const useAppTranslation = () => React.useContext(TranslationContext)
+
+export default TranslationProvider

+ 62 - 0
webview-ui/src/__mocks__/i18n/setup.ts

@@ -0,0 +1,62 @@
+import i18next from "i18next"
+import { initReactI18next } from "react-i18next"
+
+// Mock translations for testing
+const translations: Record<string, Record<string, any>> = {
+	en: {
+		chat: {
+			greeting: "What can Roo do for you?",
+		},
+		settings: {
+			autoApprove: {
+				title: "Auto-Approve",
+			},
+		},
+		common: {
+			notifications: {
+				error: "Operation failed: {{message}}",
+			},
+		},
+	},
+	es: {
+		chat: {
+			greeting: "¿Qué puede hacer Roo por ti?",
+		},
+	},
+}
+
+// Initialize i18next for React
+i18next.use(initReactI18next).init({
+	lng: "en",
+	fallbackLng: "en",
+	debug: false,
+	interpolation: {
+		escapeValue: false,
+	},
+	resources: {
+		en: {
+			chat: translations.en.chat,
+			settings: translations.en.settings,
+			common: translations.en.common,
+		},
+		es: {
+			chat: translations.es.chat,
+		},
+	},
+})
+
+export function loadTranslations() {
+	// Translations are already loaded in the mock
+}
+
+export function addTranslation(language: string, namespace: string, resources: any) {
+	if (!translations[language]) {
+		translations[language] = {}
+	}
+	translations[language][namespace] = resources
+
+	// Also add to i18next
+	i18next.addResourceBundle(language, namespace, resources, true, true)
+}
+
+export default i18next

+ 4 - 1
webview-ui/src/components/chat/ChatView.tsx

@@ -30,6 +30,7 @@ import { AudioType } from "../../../../src/shared/WebviewMessage"
 import { validateCommand } from "../../utils/command-validation"
 import { getAllModes } from "../../../../src/shared/modes"
 import TelemetryBanner from "../common/TelemetryBanner"
+import { useAppTranslation } from "@/i18n/TranslationContext"
 
 interface ChatViewProps {
 	isHidden: boolean
@@ -66,6 +67,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
 		telemetrySetting,
 	} = useExtensionState()
 
+	const { t } = useAppTranslation()
+
 	//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
 	const task = useMemo(() => messages.at(0), [messages]) // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see Cline.abort)
 	const modifiedMessages = useMemo(() => combineApiRequests(combineCommandSequences(messages.slice(1))), [messages])
@@ -1100,7 +1103,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
 					{telemetrySetting === "unset" && <TelemetryBanner />}
 					{showAnnouncement && <Announcement version={version} hideAnnouncement={hideAnnouncement} />}
 					<div style={{ padding: "0 20px", flexShrink: 0 }}>
-						<h2>What can Roo do for you?</h2>
+						<h2>{t("chat:greeting")}</h2>
 						<p>
 							Thanks to the latest breakthroughs in agentic coding capabilities, I can handle complex
 							software development tasks step-by-step. With tools that let me create & edit files, explore

+ 1 - 1
webview-ui/src/components/ui/combobox-primitive.tsx

@@ -50,7 +50,7 @@ export type ComboboxType = "single" | "multiple"
 
 export interface ComboboxBaseProps
 	extends React.ComponentProps<typeof PopoverPrimitive.Root>,
-		Omit<React.ComponentProps<typeof CommandPrimitive>, "value" | "defaultValue" | "onValueChange"> {
+		Omit<React.ComponentProps<typeof CommandPrimitive>, "value" | "defaultValue" | "onValueChange" | "children"> {
 	type?: ComboboxType | undefined
 	inputValue?: string
 	defaultInputValue?: string

+ 1 - 1
webview-ui/src/context/ExtensionStateContext.tsx

@@ -116,7 +116,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		enableCheckpoints: true,
 		checkpointStorage: "task",
 		fuzzyMatchThreshold: 1.0,
-		vscodeLanguage: "en", // Default language code
+		language: "en", // Default language code
 		enableCustomModeCreation: true,
 		writeDelayMs: 1000,
 		browserViewportSize: "900x600",

+ 51 - 0
webview-ui/src/i18n/TranslationContext.tsx

@@ -0,0 +1,51 @@
+import React, { createContext, useContext, ReactNode, useEffect } 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, any>) => string
+	i18n: typeof i18next
+}>({
+	t: (key: string) => key,
+	i18n: i18next,
+})
+
+// Translation provider component
+export 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])
+
+	return (
+		<TranslationContext.Provider
+			value={{
+				t: (key: string, options?: Record<string, any>) => {
+					return i18n.t(key, options)
+				},
+				i18n,
+			}}>
+			{children}
+		</TranslationContext.Provider>
+	)
+}
+
+// Custom hook for easy translations
+export const useAppTranslation = () => useContext(TranslationContext)
+
+export default TranslationProvider

+ 52 - 0
webview-ui/src/i18n/__tests__/TranslationContext.test.tsx

@@ -0,0 +1,52 @@
+import React from "react"
+import { render } from "@testing-library/react"
+import "@testing-library/jest-dom"
+import TranslationProvider, { useAppTranslation } from "../TranslationContext"
+import { setupI18nForTests } from "../test-utils"
+
+// Mock the useExtensionState hook
+jest.mock("@/context/ExtensionStateContext", () => ({
+	useExtensionState: () => ({
+		language: "en",
+	}),
+}))
+
+// Mock component that uses the translation context
+const TestComponent = () => {
+	const { t } = useAppTranslation()
+	return (
+		<div>
+			<h1 data-testid="translation-test">{t("settings.autoApprove.title")}</h1>
+			<p data-testid="translation-interpolation">{t("notifications.error", { message: "Test error" })}</p>
+		</div>
+	)
+}
+
+describe("TranslationContext", () => {
+	beforeAll(() => {
+		// Initialize i18next with test translations
+		setupI18nForTests()
+	})
+
+	it("should provide translations via context", () => {
+		const { getByTestId } = render(
+			<TranslationProvider>
+				<TestComponent />
+			</TranslationProvider>,
+		)
+
+		// Check if translation is provided correctly
+		expect(getByTestId("translation-test")).toHaveTextContent("Auto-Approve")
+	})
+
+	it("should handle interpolation correctly", () => {
+		const { getByTestId } = render(
+			<TranslationProvider>
+				<TestComponent />
+			</TranslationProvider>,
+		)
+
+		// Check if interpolation works
+		expect(getByTestId("translation-interpolation")).toHaveTextContent("Operation failed: Test error")
+	})
+})

+ 3 - 0
webview-ui/src/i18n/locales/ar/chat.json

@@ -0,0 +1,3 @@
+{
+	"greeting": "ماذا يمكن أن يفعل Roo من أجلك؟"
+}

+ 3 - 0
webview-ui/src/i18n/locales/ca/chat.json

@@ -0,0 +1,3 @@
+{
+	"greeting": "Què pot fer Roo per tu?"
+}

+ 3 - 0
webview-ui/src/i18n/locales/cs/chat.json

@@ -0,0 +1,3 @@
+{
+	"greeting": "Co pro vás může Roo udělat?"
+}

+ 3 - 0
webview-ui/src/i18n/locales/de/chat.json

@@ -0,0 +1,3 @@
+{
+	"greeting": "Was kann Roo für dich tun?"
+}

+ 3 - 0
webview-ui/src/i18n/locales/en/chat.json

@@ -0,0 +1,3 @@
+{
+	"greeting": "What can Roo do for you?"
+}

+ 3 - 0
webview-ui/src/i18n/locales/es/chat.json

@@ -0,0 +1,3 @@
+{
+	"greeting": "¿Qué puedo hacer por ti?"
+}

+ 3 - 0
webview-ui/src/i18n/locales/fr/chat.json

@@ -0,0 +1,3 @@
+{
+	"greeting": "Que peut faire Roo pour vous ?"
+}

+ 3 - 0
webview-ui/src/i18n/locales/hi/chat.json

@@ -0,0 +1,3 @@
+{
+	"greeting": "Roo आपके लिए क्या कर सकता है?"
+}

+ 3 - 0
webview-ui/src/i18n/locales/hu/chat.json

@@ -0,0 +1,3 @@
+{
+	"greeting": "Mit tehet érted a Roo?"
+}

+ 3 - 0
webview-ui/src/i18n/locales/it/chat.json

@@ -0,0 +1,3 @@
+{
+	"greeting": "Cosa può fare Roo per te?"
+}

+ 3 - 0
webview-ui/src/i18n/locales/ja/chat.json

@@ -0,0 +1,3 @@
+{
+	"greeting": "Rooは何をお手伝いできますか?"
+}

+ 3 - 0
webview-ui/src/i18n/locales/ko/chat.json

@@ -0,0 +1,3 @@
+{
+	"greeting": "Roo가 당신을 위해 무엇을 할 수 있을까요?"
+}

+ 3 - 0
webview-ui/src/i18n/locales/pl/chat.json

@@ -0,0 +1,3 @@
+{
+	"greeting": "Co Roo może dla Ciebie zrobić?"
+}

+ 3 - 0
webview-ui/src/i18n/locales/pt-br/chat.json

@@ -0,0 +1,3 @@
+{
+	"greeting": "O que o Roo pode fazer por você?"
+}

+ 3 - 0
webview-ui/src/i18n/locales/pt/chat.json

@@ -0,0 +1,3 @@
+{
+	"greeting": "O que o Roo pode fazer por si?"
+}

+ 3 - 0
webview-ui/src/i18n/locales/ru/chat.json

@@ -0,0 +1,3 @@
+{
+	"greeting": "Что Roo может сделать для вас?"
+}

+ 3 - 0
webview-ui/src/i18n/locales/tr/chat.json

@@ -0,0 +1,3 @@
+{
+	"greeting": "Roo sizin için ne yapabilir?"
+}

+ 3 - 0
webview-ui/src/i18n/locales/zh-cn/chat.json

@@ -0,0 +1,3 @@
+{
+	"greeting": "Roo能为您做什么?"
+}

+ 3 - 0
webview-ui/src/i18n/locales/zh-tw/chat.json

@@ -0,0 +1,3 @@
+{
+	"greeting": "Roo能為您做什麼?"
+}

+ 54 - 0
webview-ui/src/i18n/setup.ts

@@ -0,0 +1,54 @@
+import i18next from "i18next"
+import { initReactI18next } from "react-i18next"
+
+// Build translations object
+const translations: Record<string, Record<string, any>> = {}
+
+// Dynamically load locale files
+const localeFiles = import.meta.glob("./locales/**/*.json", { eager: true })
+
+// Process all locale files
+Object.entries(localeFiles).forEach(([path, module]) => {
+	// Extract language and namespace from path
+	// Example path: './locales/en/common.json' -> language: 'en', namespace: 'common'
+	const match = path.match(/\.\/locales\/([^/]+)\/([^/]+)\.json/)
+
+	if (match) {
+		const [, language, namespace] = match
+
+		// Initialize language object if it doesn't exist
+		if (!translations[language]) {
+			translations[language] = {}
+		}
+
+		// Add namespace resources to language
+		translations[language][namespace] = (module as any).default || module
+	}
+})
+
+console.log("Dynamically loaded translations:", Object.keys(translations))
+
+// Initialize i18next for React
+// This will be initialized with the VSCode language in TranslationProvider
+i18next.use(initReactI18next).init({
+	lng: "en", // Default language (will be overridden)
+	fallbackLng: "en",
+	debug: false,
+	interpolation: {
+		escapeValue: false, // React already escapes by default
+	},
+})
+
+export function loadTranslations() {
+	Object.entries(translations).forEach(([lang, namespaces]) => {
+		try {
+			Object.entries(namespaces).forEach(([namespace, resources]) => {
+				i18next.addResourceBundle(lang, namespace, resources, true, true)
+			})
+		} catch (error) {
+			console.warn(`Could not load ${lang} translations:`, error)
+		}
+	})
+}
+
+export default i18next

+ 37 - 0
webview-ui/src/i18n/test-utils.ts

@@ -0,0 +1,37 @@
+import i18next from "i18next"
+import { initReactI18next } from "react-i18next"
+
+/**
+ * Sets up i18next for testing with pre-defined translations.
+ * Use this in test files to ensure consistent translation handling.
+ */
+export const setupI18nForTests = () => {
+	i18next.use(initReactI18next).init({
+		lng: "en",
+		fallbackLng: "en",
+		debug: false,
+		interpolation: {
+			escapeValue: false,
+		},
+		// Pre-define all translations needed for tests
+		resources: {
+			en: {
+				settings: {
+					autoApprove: {
+						title: "Auto-Approve",
+					},
+				},
+				common: {
+					notifications: {
+						error: "Operation failed: {{message}}",
+					},
+				},
+				chat: {
+					test: "Test",
+				},
+			},
+		},
+	})
+
+	return i18next
+}