From 00bf91470bd90f444cdfac8b00efdd41fbccdf26 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 11 Mar 2025 11:12:48 -0400 Subject: [PATCH] Add i18n --- .roomodes | 29 ++ src/core/Cline.ts | 3 +- src/core/prompts/__tests__/sections.test.ts | 2 +- src/core/prompts/__tests__/system.test.ts | 10 +- .../__tests__/custom-instructions.test.ts | 2 +- .../prompts/sections/custom-instructions.ts | 8 +- src/core/prompts/system.ts | 5 +- src/core/webview/ClineProvider.ts | 5 +- .../webview/__tests__/ClineProvider.test.ts | 4 +- src/shared/ExtensionMessage.ts | 2 +- src/shared/__tests__/language.test.ts | 19 + src/shared/__tests__/modes.test.ts | 4 +- src/shared/language.ts | 14 + src/shared/modes.ts | 4 +- webview-ui/jest.config.cjs | 8 +- webview-ui/package-lock.json | 489 +++++------------- webview-ui/package.json | 6 +- webview-ui/src/App.tsx | 6 +- .../src/__mocks__/i18n/TranslationContext.tsx | 47 ++ webview-ui/src/__mocks__/i18n/setup.ts | 62 +++ webview-ui/src/components/chat/ChatView.tsx | 5 +- .../src/components/ui/combobox-primitive.tsx | 2 +- .../src/context/ExtensionStateContext.tsx | 2 +- webview-ui/src/i18n/TranslationContext.tsx | 51 ++ .../__tests__/TranslationContext.test.tsx | 52 ++ webview-ui/src/i18n/locales/ar/chat.json | 3 + webview-ui/src/i18n/locales/ca/chat.json | 3 + webview-ui/src/i18n/locales/cs/chat.json | 3 + webview-ui/src/i18n/locales/de/chat.json | 3 + webview-ui/src/i18n/locales/en/chat.json | 3 + webview-ui/src/i18n/locales/es/chat.json | 3 + webview-ui/src/i18n/locales/fr/chat.json | 3 + webview-ui/src/i18n/locales/hi/chat.json | 3 + webview-ui/src/i18n/locales/hu/chat.json | 3 + webview-ui/src/i18n/locales/it/chat.json | 3 + webview-ui/src/i18n/locales/ja/chat.json | 3 + webview-ui/src/i18n/locales/ko/chat.json | 3 + webview-ui/src/i18n/locales/pl/chat.json | 3 + webview-ui/src/i18n/locales/pt-br/chat.json | 3 + webview-ui/src/i18n/locales/pt/chat.json | 3 + webview-ui/src/i18n/locales/ru/chat.json | 3 + webview-ui/src/i18n/locales/tr/chat.json | 3 + webview-ui/src/i18n/locales/zh-cn/chat.json | 3 + webview-ui/src/i18n/locales/zh-tw/chat.json | 3 + webview-ui/src/i18n/setup.ts | 54 ++ webview-ui/src/i18n/test-utils.ts | 37 ++ 46 files changed, 603 insertions(+), 386 deletions(-) create mode 100644 .roomodes create mode 100644 src/shared/__tests__/language.test.ts create mode 100644 src/shared/language.ts create mode 100644 webview-ui/src/__mocks__/i18n/TranslationContext.tsx create mode 100644 webview-ui/src/__mocks__/i18n/setup.ts create mode 100644 webview-ui/src/i18n/TranslationContext.tsx create mode 100644 webview-ui/src/i18n/__tests__/TranslationContext.test.tsx create mode 100644 webview-ui/src/i18n/locales/ar/chat.json create mode 100644 webview-ui/src/i18n/locales/ca/chat.json create mode 100644 webview-ui/src/i18n/locales/cs/chat.json create mode 100644 webview-ui/src/i18n/locales/de/chat.json create mode 100644 webview-ui/src/i18n/locales/en/chat.json create mode 100644 webview-ui/src/i18n/locales/es/chat.json create mode 100644 webview-ui/src/i18n/locales/fr/chat.json create mode 100644 webview-ui/src/i18n/locales/hi/chat.json create mode 100644 webview-ui/src/i18n/locales/hu/chat.json create mode 100644 webview-ui/src/i18n/locales/it/chat.json create mode 100644 webview-ui/src/i18n/locales/ja/chat.json create mode 100644 webview-ui/src/i18n/locales/ko/chat.json create mode 100644 webview-ui/src/i18n/locales/pl/chat.json create mode 100644 webview-ui/src/i18n/locales/pt-br/chat.json create mode 100644 webview-ui/src/i18n/locales/pt/chat.json create mode 100644 webview-ui/src/i18n/locales/ru/chat.json create mode 100644 webview-ui/src/i18n/locales/tr/chat.json create mode 100644 webview-ui/src/i18n/locales/zh-cn/chat.json create mode 100644 webview-ui/src/i18n/locales/zh-tw/chat.json create mode 100644 webview-ui/src/i18n/setup.ts create mode 100644 webview-ui/src/i18n/test-utils.ts diff --git a/.roomodes b/.roomodes new file mode 100644 index 0000000000..0ccbb47432 --- /dev/null +++ b/.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" + } + ] +} \ No newline at end of file diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 4f406dcd97..28bdab1ed0 100644 --- a/src/core/Cline.ts +++ b/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 += `${currentMode}\n` diff --git a/src/core/prompts/__tests__/sections.test.ts b/src/core/prompts/__tests__/sections.test.ts index 4c43a1ba56..630da9064b 100644 --- a/src/core/prompts/__tests__/sections.test.ts +++ b/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:") diff --git a/src/core/prompts/__tests__/system.test.ts b/src/core/prompts/__tests__/system.test.ts index 38f297dcd0..9750d13b3c 100644 --- a/src/core/prompts/__tests__/system.test.ts +++ b/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() }) diff --git a/src/core/prompts/sections/__tests__/custom-instructions.test.ts b/src/core/prompts/sections/__tests__/custom-instructions.test.ts index 0d30aaea9a..1922a22efc 100644 --- a/src/core/prompts/sections/__tests__/custom-instructions.test.ts +++ b/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:") diff --git a/src/core/prompts/sections/custom-instructions.ts b/src/core/prompts/sections/custom-instructions.ts index 251f2ad6cb..b50219d563 100644 --- a/src/core/prompts/sections/custom-instructions.ts +++ b/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 { 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 diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 4504cd5c01..a6220126e1 100644 --- a/src/core/prompts/system.ts +++ b/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} diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 05d9f0ba89..70b21f87bb 100644 --- a/src/core/webview/ClineProvider.ts +++ b/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, diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index c35016c0a6..d2afd5fcdc 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/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 () => { diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 6bf436cd5d..7a4152f239 100644 --- a/src/shared/ExtensionMessage.ts +++ b/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 diff --git a/src/shared/__tests__/language.test.ts b/src/shared/__tests__/language.test.ts new file mode 100644 index 0000000000..5536d0a1f5 --- /dev/null +++ b/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") + }) +}) diff --git a/src/shared/__tests__/modes.test.ts b/src/shared/__tests__/modes.test.ts index ce082f4730..5abcb8a8b3 100644 --- a/src/shared/__tests__/modes.test.ts +++ b/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" }, ) }) diff --git a/src/shared/language.ts b/src/shared/language.ts new file mode 100644 index 0000000000..b5cb4228b5 --- /dev/null +++ b/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()}`) +} diff --git a/src/shared/modes.ts b/src/shared/modes.ts index 15944c615d..6fd57206bf 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -278,7 +278,7 @@ export async function getFullModeDetails( options?: { cwd?: string globalCustomInstructions?: string - vscodeLanguage?: string + language?: string }, ): Promise { // 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 }, ) } diff --git a/webview-ui/jest.config.cjs b/webview-ui/jest.config.cjs index 6ee94dda39..be5b6aa1be 100644 --- a/webview-ui/jest.config.cjs +++ b/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: ["/src/**/__tests__/**/*.{js,jsx,ts,tsx}", "/src/**/*.{spec,test}.{js,jsx,ts,tsx}"], setupFilesAfterEnv: ["/src/setupTests.ts"], moduleNameMapper: { @@ -12,6 +12,12 @@ module.exports = { "^vscrui$": "/src/__mocks__/vscrui.ts", "^@vscode/webview-ui-toolkit/react$": "/src/__mocks__/@vscode/webview-ui-toolkit/react.ts", "^@/(.*)$": "/src/$1", + "^src/i18n/setup$": "/src/__mocks__/i18n/setup.ts", + "^\\.\\./setup$": "/src/__mocks__/i18n/setup.ts", + "^\\./setup$": "/src/__mocks__/i18n/setup.ts", + "^src/i18n/TranslationContext$": "/src/__mocks__/i18n/TranslationContext.tsx", + "^\\.\\./TranslationContext$": "/src/__mocks__/i18n/TranslationContext.tsx", + "^\\./TranslationContext$": "/src/__mocks__/i18n/TranslationContext.tsx" }, reporters: [["jest-simple-dot-reporter", {}]], transformIgnorePatterns: [ diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index 97960851c9..38aecc9d09 100644 --- a/webview-ui/package-lock.json +++ b/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", diff --git a/webview-ui/package.json b/webview-ui/package.json index 589ccbf674..29dad1893a 100644 --- a/webview-ui/package.json +++ b/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" } } diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 389f5709fc..9ce93b9a0e 100644 --- a/webview-ui/src/App.tsx +++ b/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 = () => ( - + + + ) diff --git a/webview-ui/src/__mocks__/i18n/TranslationContext.tsx b/webview-ui/src/__mocks__/i18n/TranslationContext.tsx new file mode 100644 index 0000000000..1b838923bc --- /dev/null +++ b/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 + i18n: typeof i18next +}>({ + t: (key: string, options?: Record) => { + // 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 ( + ) => { + // 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} + + ) +} + +// Custom hook for easy translations +export const useAppTranslation = () => React.useContext(TranslationContext) + +export default TranslationProvider diff --git a/webview-ui/src/__mocks__/i18n/setup.ts b/webview-ui/src/__mocks__/i18n/setup.ts new file mode 100644 index 0000000000..6ba0cf7e38 --- /dev/null +++ b/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> = { + 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 diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 09dcdd3ca1..46958e738d 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/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" && } {showAnnouncement && }
-

What can Roo do for you?

+

{t("chat:greeting")}

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 diff --git a/webview-ui/src/components/ui/combobox-primitive.tsx b/webview-ui/src/components/ui/combobox-primitive.tsx index 13bad87aba..b2c3104a35 100644 --- a/webview-ui/src/components/ui/combobox-primitive.tsx +++ b/webview-ui/src/components/ui/combobox-primitive.tsx @@ -50,7 +50,7 @@ export type ComboboxType = "single" | "multiple" export interface ComboboxBaseProps extends React.ComponentProps, - Omit, "value" | "defaultValue" | "onValueChange"> { + Omit, "value" | "defaultValue" | "onValueChange" | "children"> { type?: ComboboxType | undefined inputValue?: string defaultInputValue?: string diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index f7292e2721..d79f263e83 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/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", diff --git a/webview-ui/src/i18n/TranslationContext.tsx b/webview-ui/src/i18n/TranslationContext.tsx new file mode 100644 index 0000000000..e6115dae6a --- /dev/null +++ b/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 + 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 ( + ) => { + return i18n.t(key, options) + }, + i18n, + }}> + {children} + + ) +} + +// Custom hook for easy translations +export const useAppTranslation = () => useContext(TranslationContext) + +export default TranslationProvider diff --git a/webview-ui/src/i18n/__tests__/TranslationContext.test.tsx b/webview-ui/src/i18n/__tests__/TranslationContext.test.tsx new file mode 100644 index 0000000000..0b4dbf8238 --- /dev/null +++ b/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 ( +

+

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

+

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

+
+ ) +} + +describe("TranslationContext", () => { + beforeAll(() => { + // Initialize i18next with test translations + setupI18nForTests() + }) + + 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/locales/ar/chat.json b/webview-ui/src/i18n/locales/ar/chat.json new file mode 100644 index 0000000000..af09e7cf5e --- /dev/null +++ b/webview-ui/src/i18n/locales/ar/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "ماذا يمكن أن يفعل Roo من أجلك؟" +} diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json new file mode 100644 index 0000000000..9f9d819936 --- /dev/null +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Què pot fer Roo per tu?" +} diff --git a/webview-ui/src/i18n/locales/cs/chat.json b/webview-ui/src/i18n/locales/cs/chat.json new file mode 100644 index 0000000000..989655bf4b --- /dev/null +++ b/webview-ui/src/i18n/locales/cs/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Co pro vás může Roo udělat?" +} diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json new file mode 100644 index 0000000000..9047746c50 --- /dev/null +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Was kann Roo für dich tun?" +} diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json new file mode 100644 index 0000000000..1b742bb11f --- /dev/null +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "What can Roo do for you?" +} diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json new file mode 100644 index 0000000000..34b857fba7 --- /dev/null +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "¿Qué puedo hacer por ti?" +} diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json new file mode 100644 index 0000000000..8a2e696594 --- /dev/null +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Que peut faire Roo pour vous ?" +} diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json new file mode 100644 index 0000000000..5212c2026c --- /dev/null +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Roo आपके लिए क्या कर सकता है?" +} diff --git a/webview-ui/src/i18n/locales/hu/chat.json b/webview-ui/src/i18n/locales/hu/chat.json new file mode 100644 index 0000000000..f57221300c --- /dev/null +++ b/webview-ui/src/i18n/locales/hu/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Mit tehet érted a Roo?" +} diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json new file mode 100644 index 0000000000..6ab48932bc --- /dev/null +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Cosa può fare Roo per te?" +} diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json new file mode 100644 index 0000000000..d372814cee --- /dev/null +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Rooは何をお手伝いできますか?" +} diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json new file mode 100644 index 0000000000..a96e6d17a3 --- /dev/null +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Roo가 당신을 위해 무엇을 할 수 있을까요?" +} diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json new file mode 100644 index 0000000000..b21e0047ed --- /dev/null +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Co Roo może dla Ciebie zrobić?" +} diff --git a/webview-ui/src/i18n/locales/pt-br/chat.json b/webview-ui/src/i18n/locales/pt-br/chat.json new file mode 100644 index 0000000000..9fcd1ad1e7 --- /dev/null +++ b/webview-ui/src/i18n/locales/pt-br/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "O que o Roo pode fazer por você?" +} diff --git a/webview-ui/src/i18n/locales/pt/chat.json b/webview-ui/src/i18n/locales/pt/chat.json new file mode 100644 index 0000000000..66bef0dff9 --- /dev/null +++ b/webview-ui/src/i18n/locales/pt/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "O que o Roo pode fazer por si?" +} diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json new file mode 100644 index 0000000000..b01532b168 --- /dev/null +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Что Roo может сделать для вас?" +} diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json new file mode 100644 index 0000000000..8c160b2b77 --- /dev/null +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Roo sizin için ne yapabilir?" +} diff --git a/webview-ui/src/i18n/locales/zh-cn/chat.json b/webview-ui/src/i18n/locales/zh-cn/chat.json new file mode 100644 index 0000000000..4e17fd3ca0 --- /dev/null +++ b/webview-ui/src/i18n/locales/zh-cn/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Roo能为您做什么?" +} diff --git a/webview-ui/src/i18n/locales/zh-tw/chat.json b/webview-ui/src/i18n/locales/zh-tw/chat.json new file mode 100644 index 0000000000..13724748a9 --- /dev/null +++ b/webview-ui/src/i18n/locales/zh-tw/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Roo能為您做什麼?" +} diff --git a/webview-ui/src/i18n/setup.ts b/webview-ui/src/i18n/setup.ts new file mode 100644 index 0000000000..678cdc1d49 --- /dev/null +++ b/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> = {} + +// 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 diff --git a/webview-ui/src/i18n/test-utils.ts b/webview-ui/src/i18n/test-utils.ts new file mode 100644 index 0000000000..9abd4d9e06 --- /dev/null +++ b/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 +}