This commit is contained in:
Matt Rubens 2025-03-11 11:12:48 -04:00
parent baa346dc46
commit 00bf91470b
46 changed files with 603 additions and 386 deletions

29
.roomodes Normal file
View file

@ -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"
}
]
}

View file

@ -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`

View file

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

View file

@ -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()
})

View file

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

View file

@ -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

View file

@ -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}

View file

@ -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,

View file

@ -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 () => {

View file

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

View file

@ -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")
})
})

View file

@ -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
src/shared/language.ts Normal file
View file

@ -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()}`)
}

View file

@ -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 },
)
}

View file

@ -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: [

View file

@ -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",

View file

@ -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"
}
}

View file

@ -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>
)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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",

View file

@ -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

View file

@ -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")
})
})

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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

View file

@ -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
}