Merge pull request #1584 from RooVetGit/i18n

I18n
This commit is contained in:
Matt Rubens 2025-03-12 11:38:00 -04:00 • committed by GitHub
commit 0d81c0d801
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 2280 additions and 790 deletions

29
.roomodes Normal file
View file

@ -0,0 +1,29 @@
{
"customModes": [
{
"slug": "translate",
"name": "Translate",
"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

@ -1,4 +1,13 @@
const vscode = {
env: {
language: "en", // Default language for tests
appName: "Visual Studio Code Test",
appHost: "desktop",
appRoot: "/test/path",
machineId: "test-machine-id",
sessionId: "test-session-id",
shell: "/bin/zsh",
},
window: {
showInformationMessage: jest.fn(),
showErrorMessage: jest.fn(),

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"
@ -1102,7 +1103,6 @@ export class Cline {
browserViewportSize,
mode,
customModePrompts,
preferredLanguage,
experiments,
enableMcpServerCreation,
browserToolEnabled,
@ -1124,7 +1124,6 @@ export class Cline {
customModePrompts,
customModes,
this.customInstructions,
preferredLanguage,
this.diffEnabled,
experiments,
enableMcpServerCreation,
@ -3665,13 +3664,12 @@ export class Cline {
customModePrompts,
experiments = {} as Record<ExperimentId, boolean>,
customInstructions: globalCustomInstructions,
preferredLanguage,
} = (await this.providerRef.deref()?.getState()) ?? {}
const currentMode = mode ?? defaultModeSlug
const modeDetails = await getFullModeDetails(currentMode, customModes, customModePrompts, {
cwd,
globalCustomInstructions,
preferredLanguage,
language: formatLanguage(vscode.env.language),
})
details += `\n\n# Current Mode\n`
details += `<slug>${currentMode}</slug>\n`

File diff suppressed because it is too large Load diff

View file

@ -46,8 +46,6 @@ const mockContext = {
} as unknown as vscode.ExtensionContext
describe("File-Based Custom System Prompt", () => {
const experiments = {}
beforeEach(() => {
// Reset mocks before each test
jest.clearAllMocks()
@ -66,18 +64,17 @@ describe("File-Based Custom System Prompt", () => {
const prompt = await SYSTEM_PROMPT(
mockContext,
"test/path", // Using a relative path without leading slash
false,
undefined,
undefined,
undefined,
defaultModeSlug,
customModePrompts,
undefined,
undefined,
undefined,
undefined,
experiments,
true,
false, // supportsComputerUse
undefined, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug, // mode
customModePrompts, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // diffEnabled
undefined, // experiments
true, // enableMcpServerCreation
)
// Should contain default sections
@ -101,18 +98,17 @@ describe("File-Based Custom System Prompt", () => {
const prompt = await SYSTEM_PROMPT(
mockContext,
"test/path", // Using a relative path without leading slash
false,
undefined,
undefined,
undefined,
defaultModeSlug,
undefined,
undefined,
undefined,
undefined,
undefined,
experiments,
true,
false, // supportsComputerUse
undefined, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug, // mode
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // diffEnabled
undefined, // experiments
true, // enableMcpServerCreation
)
// Should contain role definition and file-based system prompt
@ -120,7 +116,6 @@ describe("File-Based Custom System Prompt", () => {
expect(prompt).toContain(fileCustomSystemPrompt)
// Should not contain any of the default sections
expect(prompt).not.toContain("TOOL USE")
expect(prompt).not.toContain("CAPABILITIES")
expect(prompt).not.toContain("MODES")
})
@ -146,18 +141,17 @@ describe("File-Based Custom System Prompt", () => {
const prompt = await SYSTEM_PROMPT(
mockContext,
"test/path", // Using a relative path without leading slash
false,
undefined,
undefined,
undefined,
defaultModeSlug,
customModePrompts,
undefined,
undefined,
undefined,
undefined,
experiments,
true,
false, // supportsComputerUse
undefined, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug, // mode
customModePrompts, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // diffEnabled
undefined, // experiments
true, // enableMcpServerCreation
)
// Should contain custom role definition and file-based system prompt
@ -165,7 +159,6 @@ describe("File-Based Custom System Prompt", () => {
expect(prompt).toContain(fileCustomSystemPrompt)
// Should not contain any of the default sections
expect(prompt).not.toContain("TOOL USE")
expect(prompt).not.toContain("CAPABILITIES")
expect(prompt).not.toContain("MODES")
})

View file

@ -3,20 +3,20 @@ import { getCapabilitiesSection } from "../sections/capabilities"
import { DiffStrategy, DiffResult } from "../../diff/types"
describe("addCustomInstructions", () => {
test("adds preferred language to custom instructions", async () => {
test("adds vscode language to custom instructions", async () => {
const result = await addCustomInstructions(
"mode instructions",
"global instructions",
"/test/path",
"test-mode",
{ preferredLanguage: "French" },
{ language: "fr" },
)
expect(result).toContain("Language Preference:")
expect(result).toContain("You should always speak and think in the French language")
expect(result).toContain('You should always speak and think in the "fr" language')
})
test("works without preferred language", async () => {
test("works without vscode language", async () => {
const result = await addCustomInstructions(
"mode instructions",
"global instructions",

View file

@ -6,7 +6,7 @@ import { SearchReplaceDiffStrategy } from "../../../core/diff/strategies/search-
import * as vscode from "vscode"
import fs from "fs/promises"
import os from "os"
import { defaultModeSlug, modes } from "../../../shared/modes"
import { defaultModeSlug, modes, Mode, isToolAllowedForMode } from "../../../shared/modes"
// Import path utils to get access to toPosix string extension
import "../../../utils/path"
import { addCustomInstructions } from "../sections/custom-instructions"
@ -18,46 +18,63 @@ jest.mock("../sections/modes", () => ({
getModesSection: jest.fn().mockImplementation(async () => `====\n\nMODES\n\n- Test modes section`),
}))
jest.mock("../sections/custom-instructions", () => ({
addCustomInstructions: jest
.fn()
.mockImplementation(async (modeCustomInstructions, globalCustomInstructions, cwd, mode, options) => {
const sections = []
// Mock the custom instructions
jest.mock("../sections/custom-instructions", () => {
const addCustomInstructions = jest.fn()
return {
addCustomInstructions,
__setMockImplementation: (impl: any) => {
addCustomInstructions.mockImplementation(impl)
},
}
})
// Add language preference if provided
if (options?.preferredLanguage) {
sections.push(
`Language Preference:\nYou should always speak and think in the ${options.preferredLanguage} language.`,
)
}
// Set up default mock implementation
const { __setMockImplementation } = jest.requireMock("../sections/custom-instructions")
__setMockImplementation(
async (
modeCustomInstructions: string,
globalCustomInstructions: string,
cwd: string,
mode: string,
options?: { language?: string },
) => {
const sections = []
// Add global instructions first
if (globalCustomInstructions?.trim()) {
sections.push(`Global Instructions:\n${globalCustomInstructions.trim()}`)
}
// Add language preference if provided
if (options?.language) {
sections.push(
`Language Preference:\nYou should always speak and think in the "${options.language}" language.`,
)
}
// Add mode-specific instructions after
if (modeCustomInstructions?.trim()) {
sections.push(`Mode-specific Instructions:\n${modeCustomInstructions}`)
}
// Add global instructions first
if (globalCustomInstructions?.trim()) {
sections.push(`Global Instructions:\n${globalCustomInstructions.trim()}`)
}
// Add rules
const rules = []
if (mode) {
rules.push(`# Rules from .clinerules-${mode}:\nMock mode-specific rules`)
}
rules.push(`# Rules from .clinerules:\nMock generic rules`)
// Add mode-specific instructions after
if (modeCustomInstructions?.trim()) {
sections.push(`Mode-specific Instructions:\n${modeCustomInstructions}`)
}
if (rules.length > 0) {
sections.push(`Rules:\n${rules.join("\n")}`)
}
// Add rules
const rules = []
if (mode) {
rules.push(`# Rules from .clinerules-${mode}:\nMock mode-specific rules`)
}
rules.push(`# Rules from .clinerules:\nMock generic rules`)
const joinedSections = sections.join("\n\n")
return joinedSections
? `\n====\n\nUSER'S CUSTOM INSTRUCTIONS\n\nThe following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.\n\n${joinedSections}`
: ""
}),
}))
if (rules.length > 0) {
sections.push(`Rules:\n${rules.join("\n")}`)
}
const joinedSections = sections.join("\n\n")
return joinedSections
? `\n====\n\nUSER'S CUSTOM INSTRUCTIONS\n\nThe following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.\n\n${joinedSections}`
: ""
},
)
// Mock environment-specific values for consistent tests
jest.mock("os", () => ({
@ -69,6 +86,13 @@ jest.mock("default-shell", () => "/bin/zsh")
jest.mock("os-name", () => () => "Linux")
// Mock vscode language
jest.mock("vscode", () => ({
env: {
language: "en",
},
}))
jest.mock("../../../utils/shell", () => ({
getShell: () => "/bin/zsh",
}))
@ -126,7 +150,7 @@ const createMockMcpHub = (): McpHub =>
describe("SYSTEM_PROMPT", () => {
let mockMcpHub: McpHub
let experiments: Record<string, boolean>
let experiments: Record<string, boolean> | undefined
beforeAll(() => {
// Ensure fs mock is properly initialized
@ -146,6 +170,10 @@ describe("SYSTEM_PROMPT", () => {
"/mock/mcp/path",
]
dirs.forEach((dir) => mockFs._mockDirectories.add(dir))
})
beforeEach(() => {
// Reset experiments before each test to ensure they're disabled by default
experiments = {
[EXPERIMENT_IDS.SEARCH_AND_REPLACE]: false,
[EXPERIMENT_IDS.INSERT_BLOCK]: false,
@ -175,7 +203,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
@ -196,7 +223,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
@ -219,7 +245,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
@ -240,7 +265,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
@ -261,7 +285,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
@ -282,7 +305,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // preferredLanguage
true, // diffEnabled
experiments,
true, // enableMcpServerCreation
@ -304,7 +326,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // preferredLanguage
false, // diffEnabled
experiments,
true, // enableMcpServerCreation
@ -326,7 +347,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
@ -336,7 +356,11 @@ describe("SYSTEM_PROMPT", () => {
expect(prompt).toMatchSnapshot()
})
it("should include preferred language in custom instructions", async () => {
it("should include vscode language in custom instructions", async () => {
// Mock vscode.env.language
const vscode = jest.requireMock("vscode")
vscode.env = { language: "es" }
const prompt = await SYSTEM_PROMPT(
mockContext,
"/test/path",
@ -348,14 +372,16 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
"Spanish", // preferredLanguage
undefined, // diffEnabled
experiments,
undefined, // experiments
true, // enableMcpServerCreation
)
expect(prompt).toContain("Language Preference:")
expect(prompt).toContain("You should always speak and think in the Spanish language")
expect(prompt).toContain('You should always speak and think in the "es" language')
// Reset mock
vscode.env = { language: "en" }
})
it("should include custom mode role definition at top and instructions at bottom", async () => {
@ -381,7 +407,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
customModes, // customModes
"Global instructions", // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
@ -409,18 +434,17 @@ describe("SYSTEM_PROMPT", () => {
const prompt = await SYSTEM_PROMPT(
mockContext,
"/test/path",
false,
undefined,
undefined,
undefined,
defaultModeSlug,
customModePrompts,
undefined,
undefined,
undefined,
undefined,
experiments,
true, // enableMcpServerCreation
false, // supportsComputerUse
undefined, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug as Mode, // mode
customModePrompts, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // diffEnabled
undefined, // experiments
false, // enableMcpServerCreation
)
// Role definition from promptComponent should be at the top
@ -440,18 +464,17 @@ describe("SYSTEM_PROMPT", () => {
const prompt = await SYSTEM_PROMPT(
mockContext,
"/test/path",
false,
undefined,
undefined,
undefined,
defaultModeSlug,
customModePrompts,
undefined,
undefined,
undefined,
undefined,
experiments,
true, // enableMcpServerCreation
false, // supportsComputerUse
undefined, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug as Mode, // mode
customModePrompts, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // diffEnabled
undefined, // experiments
false, // enableMcpServerCreation
)
// Should use the default mode's role definition
@ -460,6 +483,15 @@ describe("SYSTEM_PROMPT", () => {
describe("experimental tools", () => {
it("should disable experimental tools by default", async () => {
// Set experiments to explicitly disable experimental tools
const experimentsConfig = {
[EXPERIMENT_IDS.SEARCH_AND_REPLACE]: false,
[EXPERIMENT_IDS.INSERT_BLOCK]: false,
}
// Reset experiments
experiments = experimentsConfig
const prompt = await SYSTEM_PROMPT(
mockContext,
"/test/path",
@ -471,23 +503,29 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments, // experiments - undefined should disable all experimental tools
experimentsConfig, // Explicitly disable experimental tools
true, // enableMcpServerCreation
)
// Verify experimental tools are not included in the prompt
expect(prompt).not.toContain(EXPERIMENT_IDS.SEARCH_AND_REPLACE)
expect(prompt).not.toContain(EXPERIMENT_IDS.INSERT_BLOCK)
// Check that experimental tool sections are not included
const toolSections = prompt.split("\n## ").slice(1)
const toolNames = toolSections.map((section) => section.split("\n")[0].trim())
expect(toolNames).not.toContain("search_and_replace")
expect(toolNames).not.toContain("insert_content")
expect(prompt).toMatchSnapshot()
})
it("should enable experimental tools when explicitly enabled", async () => {
const experiments = {
// Set experiments for testing experimental features
const experimentsEnabled = {
[EXPERIMENT_IDS.SEARCH_AND_REPLACE]: true,
[EXPERIMENT_IDS.INSERT_BLOCK]: true,
}
// Reset default experiments
experiments = undefined
const prompt = await SYSTEM_PROMPT(
mockContext,
"/test/path",
@ -499,23 +537,31 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
experimentsEnabled, // Use the enabled experiments
true, // enableMcpServerCreation
)
// Get all tool sections
const toolSections = prompt.split("## ").slice(1) // Split by section headers and remove first non-tool part
const toolNames = toolSections.map((section) => section.split("\n")[0].trim())
// Verify experimental tools are included in the prompt when enabled
expect(prompt).toContain(EXPERIMENT_IDS.SEARCH_AND_REPLACE)
expect(prompt).toContain(EXPERIMENT_IDS.INSERT_BLOCK)
expect(toolNames).toContain("search_and_replace")
expect(toolNames).toContain("insert_content")
expect(prompt).toMatchSnapshot()
})
it("should selectively enable experimental tools", async () => {
const experiments = {
// Set experiments for testing selective enabling
const experimentsSelective = {
[EXPERIMENT_IDS.SEARCH_AND_REPLACE]: true,
[EXPERIMENT_IDS.INSERT_BLOCK]: false,
}
// Reset default experiments
experiments = undefined
const prompt = await SYSTEM_PROMPT(
mockContext,
"/test/path",
@ -527,15 +573,19 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
experimentsSelective, // Use the selective experiments
true, // enableMcpServerCreation
)
// Get all tool sections
const toolSections = prompt.split("## ").slice(1) // Split by section headers and remove first non-tool part
const toolNames = toolSections.map((section) => section.split("\n")[0].trim())
// Verify only enabled experimental tools are included
expect(prompt).toContain(EXPERIMENT_IDS.SEARCH_AND_REPLACE)
expect(prompt).not.toContain(EXPERIMENT_IDS.INSERT_BLOCK)
expect(toolNames).toContain("search_and_replace")
expect(toolNames).not.toContain("insert_content")
expect(prompt).toMatchSnapshot()
})
it("should list all available editing tools in base instruction", async () => {
@ -555,9 +605,8 @@ describe("SYSTEM_PROMPT", () => {
undefined,
undefined,
undefined,
undefined,
true, // diffEnabled
experiments,
experiments, // experiments
true, // enableMcpServerCreation
)
@ -567,7 +616,6 @@ describe("SYSTEM_PROMPT", () => {
expect(prompt).toContain("insert_content (for adding lines to existing files)")
expect(prompt).toContain("search_and_replace (for finding and replacing individual pieces of text)")
})
it("should provide detailed instructions for each enabled tool", async () => {
const experiments = {
[EXPERIMENT_IDS.SEARCH_AND_REPLACE]: true,
@ -585,8 +633,7 @@ describe("SYSTEM_PROMPT", () => {
undefined,
undefined,
undefined,
undefined,
true,
true, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
@ -606,7 +653,7 @@ describe("SYSTEM_PROMPT", () => {
})
describe("addCustomInstructions", () => {
let experiments: Record<string, boolean>
let experiments: Record<string, boolean> | undefined
beforeAll(() => {
// Ensure fs mock is properly initialized
const mockFs = jest.requireMock("fs/promises")
@ -619,10 +666,8 @@ describe("addCustomInstructions", () => {
throw new Error(`ENOENT: no such file or directory, mkdir '${path}'`)
})
experiments = {
[EXPERIMENT_IDS.SEARCH_AND_REPLACE]: false,
[EXPERIMENT_IDS.INSERT_BLOCK]: false,
}
// Initialize experiments as undefined by default
experiments = undefined
})
beforeEach(() => {
@ -640,10 +685,9 @@ describe("addCustomInstructions", () => {
"architect", // mode
undefined, // customModePrompts
undefined, // customModes
undefined,
undefined,
undefined,
experiments,
undefined, // globalCustomInstructions
undefined, // diffEnabled
undefined, // experiments
true, // enableMcpServerCreation
)
@ -661,10 +705,9 @@ describe("addCustomInstructions", () => {
"ask", // mode
undefined, // customModePrompts
undefined, // customModes
undefined,
undefined,
undefined,
experiments,
undefined, // globalCustomInstructions
undefined, // diffEnabled
undefined, // experiments
true, // enableMcpServerCreation
)
@ -685,9 +728,8 @@ describe("addCustomInstructions", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
undefined, // experiments
true, // enableMcpServerCreation
)
@ -709,9 +751,8 @@ describe("addCustomInstructions", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
undefined, // experiments
false, // enableMcpServerCreation
)
@ -751,7 +792,7 @@ describe("addCustomInstructions", () => {
it("should include preferred language when provided", async () => {
const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug, {
preferredLanguage: "Spanish",
language: "es",
})
expect(instructions).toMatchSnapshot()
})
@ -767,7 +808,7 @@ describe("addCustomInstructions", () => {
"",
"/test/path",
defaultModeSlug,
{ preferredLanguage: "French" },
{ language: "fr" },
)
expect(instructions).toMatchSnapshot()
})

View file

@ -113,11 +113,11 @@ describe("addCustomInstructions", () => {
"global instructions",
"/fake/path",
"test-mode",
{ preferredLanguage: "Spanish" },
{ language: "es" },
)
expect(result).toContain("Language Preference:")
expect(result).toContain("Spanish")
expect(result).toContain("es")
expect(result).toContain("Global Instructions:\nglobal instructions")
expect(result).toContain("Mode-specific Instructions:\nmode instructions")
expect(result).toContain("Rules from .clinerules-test-mode:\nmode specific rules")

View file

@ -1,5 +1,6 @@
import fs from "fs/promises"
import path from "path"
import * as vscode from "vscode"
async function safeReadFile(filePath: string): Promise<string> {
try {
@ -33,7 +34,7 @@ export async function addCustomInstructions(
globalCustomInstructions: string,
cwd: string,
mode: string,
options: { preferredLanguage?: string; rooIgnoreInstructions?: string } = {},
options: { language?: string; rooIgnoreInstructions?: string } = {},
): Promise<string> {
const sections = []
@ -45,9 +46,9 @@ export async function addCustomInstructions(
}
// Add language preference if provided
if (options.preferredLanguage) {
if (options.language) {
sections.push(
`Language Preference:\nYou should always speak and think in the ${options.preferredLanguage} language.`,
`Language Preference:\nYou should always speak and think in the "${options.language}" language unless the user gives you instructions below to do otherwise.`,
)
}

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,
@ -37,7 +38,6 @@ async function generatePrompt(
promptComponent?: PromptComponent,
customModeConfigs?: ModeConfig[],
globalCustomInstructions?: string,
preferredLanguage?: string,
diffEnabled?: boolean,
experiments?: Record<string, boolean>,
enableMcpServerCreation?: boolean,
@ -90,7 +90,7 @@ ${getSystemInfoSection(cwd, mode, customModeConfigs)}
${getObjectiveSection()}
${await addCustomInstructions(promptComponent?.customInstructions || modeConfig.customInstructions || "", globalCustomInstructions || "", cwd, mode, { preferredLanguage, rooIgnoreInstructions })}`
${await addCustomInstructions(promptComponent?.customInstructions || modeConfig.customInstructions || "", globalCustomInstructions || "", cwd, mode, { language: formatLanguage(vscode.env.language), rooIgnoreInstructions })}`
return basePrompt
}
@ -106,7 +106,6 @@ export const SYSTEM_PROMPT = async (
customModePrompts?: CustomModePrompts,
customModes?: ModeConfig[],
globalCustomInstructions?: string,
preferredLanguage?: string,
diffEnabled?: boolean,
experiments?: Record<string, boolean>,
enableMcpServerCreation?: boolean,
@ -135,11 +134,19 @@ export const SYSTEM_PROMPT = async (
// If a file-based custom system prompt exists, use it
if (fileCustomSystemPrompt) {
const roleDefinition = promptComponent?.roleDefinition || currentMode.roleDefinition
const customInstructions = await addCustomInstructions(
promptComponent?.customInstructions || currentMode.customInstructions || "",
globalCustomInstructions || "",
cwd,
mode,
{ language: formatLanguage(vscode.env.language), rooIgnoreInstructions },
)
// For file-based prompts, don't include the tool sections
return `${roleDefinition}
${fileCustomSystemPrompt}
${await addCustomInstructions(promptComponent?.customInstructions || currentMode.customInstructions || "", globalCustomInstructions || "", cwd, mode, { preferredLanguage, rooIgnoreInstructions })}`
${customInstructions}`
}
// If diff is disabled, don't pass the diffStrategy
@ -156,7 +163,6 @@ ${await addCustomInstructions(promptComponent?.customInstructions || currentMode
promptComponent,
customModes,
globalCustomInstructions,
preferredLanguage,
diffEnabled,
experiments,
enableMcpServerCreation,

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"
@ -1389,10 +1390,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("rateLimitSeconds", message.value ?? 0)
await this.postStateToWebview()
break
case "preferredLanguage":
await this.updateGlobalState("preferredLanguage", message.text)
await this.postStateToWebview()
break
case "writeDelayMs":
await this.updateGlobalState("writeDelayMs", message.value)
await this.postStateToWebview()
@ -1930,7 +1927,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
apiConfiguration,
customModePrompts,
customInstructions,
preferredLanguage,
browserViewportSize,
diffEnabled,
mcpEnabled,
@ -1968,7 +1964,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
customModePrompts,
customModes,
customInstructions,
preferredLanguage,
diffEnabled,
experiments,
enableMcpServerCreation,
@ -2344,7 +2339,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
screenshotQuality,
remoteBrowserHost,
remoteBrowserEnabled,
preferredLanguage,
writeDelayMs,
terminalOutputLineLimit,
fuzzyMatchThreshold,
@ -2365,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
@ -2404,7 +2399,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
screenshotQuality: screenshotQuality ?? 75,
remoteBrowserHost,
remoteBrowserEnabled: remoteBrowserEnabled ?? false,
preferredLanguage: preferredLanguage ?? "English",
writeDelayMs: writeDelayMs ?? 1000,
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
@ -2430,6 +2424,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
telemetryKey,
machineId,
showRooIgnoredFiles: showRooIgnoredFiles ?? true,
language,
}
}
@ -2563,37 +2558,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
writeDelayMs: stateValues.writeDelayMs ?? 1000,
terminalOutputLineLimit: stateValues.terminalOutputLineLimit ?? 500,
mode: stateValues.mode ?? defaultModeSlug,
preferredLanguage:
stateValues.preferredLanguage ??
(() => {
// Get VSCode's locale setting
const vscodeLang = vscode.env.language
// Map VSCode locale to our supported languages
const langMap: { [key: string]: string } = {
en: "English",
ar: "Arabic",
"pt-br": "Brazilian Portuguese",
ca: "Catalan",
cs: "Czech",
fr: "French",
de: "German",
hi: "Hindi",
hu: "Hungarian",
it: "Italian",
ja: "Japanese",
ko: "Korean",
pl: "Polish",
pt: "Portuguese",
ru: "Russian",
zh: "Simplified Chinese",
"zh-cn": "Simplified Chinese",
es: "Spanish",
"zh-tw": "Traditional Chinese",
tr: "Turkish",
}
// Return mapped language or default to English
return langMap[vscodeLang] ?? langMap[vscodeLang.split("-")[0]] ?? "English"
})(),
// Pass the VSCode language code directly
language: formatLanguage(vscode.env.language),
mcpEnabled: stateValues.mcpEnabled ?? true,
enableMcpServerCreation: stateValues.enableMcpServerCreation ?? true,
alwaysApproveResubmit: stateValues.alwaysApproveResubmit ?? false,

View file

@ -420,7 +420,6 @@ describe("ClineProvider", () => {
const mockState: ExtensionState = {
version: "1.0.0",
preferredLanguage: "English",
clineMessages: [],
taskHistory: [],
shouldShowAnnouncement: false,
@ -535,20 +534,12 @@ describe("ClineProvider", () => {
expect(state).toHaveProperty("writeDelayMs")
})
test("preferredLanguage defaults to VSCode language when not set", 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.preferredLanguage).toBe("Spanish")
})
test("preferredLanguage defaults to English for unsupported VSCode language", async () => {
// Mock VSCode language as an unsupported language
;(vscode.env as any).language = "unsupported-LANG"
const state = await provider.getState()
expect(state.preferredLanguage).toBe("English")
expect(state.language).toBe("es-ES")
})
test("diffEnabled defaults to true when not set", async () => {
@ -1216,7 +1207,7 @@ describe("ClineProvider", () => {
expect(callArgs[4]).toHaveProperty("getToolDescription") // diffStrategy
expect(callArgs[5]).toBe("900x600") // browserViewportSize
expect(callArgs[6]).toBe("code") // mode
expect(callArgs[11]).toBe(true) // diffEnabled
expect(callArgs[10]).toBe(true) // diffEnabled
// Run the test again to verify it's consistent
await handler({ type: "getSystemPrompt", mode: "code" })
@ -1274,7 +1265,7 @@ describe("ClineProvider", () => {
expect(callArgs[4]).toHaveProperty("getToolDescription") // diffStrategy
expect(callArgs[5]).toBe("900x600") // browserViewportSize
expect(callArgs[6]).toBe("code") // mode
expect(callArgs[11]).toBe(false) // diffEnabled should be false
expect(callArgs[10]).toBe(false) // diffEnabled should be false
})
test("uses correct mode-specific instructions when mode is specified", async () => {

View file

@ -164,7 +164,6 @@ export type GlobalStateKey =
| "screenshotQuality"
| "remoteBrowserHost"
| "fuzzyMatchThreshold"
| "preferredLanguage" // Language setting for Cline's communication
| "writeDelayMs"
| "terminalOutputLineLimit"
| "mcpEnabled"

View file

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

View file

@ -64,7 +64,6 @@ export interface WebviewMessage {
| "toggleMcpServer"
| "updateMcpTimeout"
| "fuzzyMatchThreshold"
| "preferredLanguage"
| "writeDelayMs"
| "enhancePrompt"
| "enhancedPrompt"

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

@ -6,7 +6,6 @@ jest.mock("../../core/prompts/sections/custom-instructions", () => ({
}))
import { isToolAllowedForMode, FileRestrictionError, ModeConfig, getFullModeDetails, modes } from "../modes"
import * as vscode from "vscode"
import { addCustomInstructions } from "../../core/prompts/sections/custom-instructions"
describe("isToolAllowedForMode", () => {
@ -402,7 +401,7 @@ describe("FileRestrictionError", () => {
const options = {
cwd: "/test/path",
globalCustomInstructions: "Global instructions",
preferredLanguage: "en",
language: "en",
}
await getFullModeDetails("debug", undefined, undefined, options)
@ -412,7 +411,7 @@ describe("FileRestrictionError", () => {
"Global instructions",
"/test/path",
"debug",
{ preferredLanguage: "en" },
{ language: "en" },
)
})

View file

@ -82,7 +82,6 @@ export const GLOBAL_STATE_KEYS = [
"screenshotQuality",
"remoteBrowserHost",
"fuzzyMatchThreshold",
"preferredLanguage", // Language setting for Cline's communication.
"writeDelayMs",
"terminalOutputLineLimit",
"mcpEnabled",

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

@ -195,10 +195,13 @@ export function isToolAllowedForMode(
}
// Check tool requirements if any exist
if (toolRequirements && tool in toolRequirements) {
if (!toolRequirements[tool]) {
if (toolRequirements && typeof toolRequirements === "object") {
if (tool in toolRequirements && !toolRequirements[tool]) {
return false
}
} else if (toolRequirements === false) {
// If toolRequirements is a boolean false, all tools are disabled
return false
}
const mode = getModeBySlug(modeSlug, customModes)
@ -275,7 +278,7 @@ export async function getFullModeDetails(
options?: {
cwd?: string
globalCustomInstructions?: string
preferredLanguage?: string
language?: string
},
): Promise<ModeConfig> {
// First get the base mode config from custom modes or built-in modes
@ -295,7 +298,7 @@ export async function getFullModeDetails(
options.globalCustomInstructions || "",
options.cwd,
modeSlug,
{ preferredLanguage: options.preferredLanguage },
{ language: options.language },
)
}

View file

@ -23,3 +23,5 @@ yarn-debug.log*
yarn-error.log*
*storybook.log
tsconfig.tsbuildinfo

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

@ -28,11 +28,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 +81,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"
}
},
@ -6165,26 +6168,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 +6286,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",
@ -6645,7 +6538,6 @@
"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",
@ -6666,7 +6558,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 +6599,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"
},
@ -6760,7 +6650,6 @@
"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
},
"node_modules/@types/babel__core": {
@ -7771,116 +7660,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",
@ -8239,18 +8018,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,25 +8537,6 @@
"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",
@ -8856,18 +8604,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",
@ -9568,6 +9304,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 +10021,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",
@ -10461,7 +10193,6 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/domexception": {
@ -13070,6 +12801,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 +12875,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 +15379,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",
@ -15650,7 +15418,6 @@
"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 +17560,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 +18057,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",
@ -18725,6 +18518,27 @@
"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",
@ -20783,30 +20597,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 +20931,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 +21531,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

@ -35,11 +35,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 +88,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

@ -9,19 +9,6 @@ import {
VSCodeRadioGroup,
VSCodeRadio,
} from "@vscode/webview-ui-toolkit/react"
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"
import {
Button,
Command,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui"
import { cn } from "@/lib/utils"
import { useExtensionState } from "../../context/ExtensionStateContext"
import {
Mode,
@ -43,6 +30,7 @@ import {
import { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups"
import { vscode } from "../../utils/vscode"
import { Tab, TabContent, TabHeader } from "../common/Tab"
import i18next from "i18next"
// Get all available groups that should show in prompts view
const availableGroups = (Object.keys(TOOL_GROUPS) as ToolGroup[]).filter((group) => !TOOL_GROUPS[group].alwaysAvailable)
@ -69,8 +57,6 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
mode,
customInstructions,
setCustomInstructions,
preferredLanguage,
setPreferredLanguage,
customModes,
enableCustomModeCreation,
setEnableCustomModeCreation,
@ -82,9 +68,6 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
const [testPrompt, setTestPrompt] = useState("")
const [isEnhancing, setIsEnhancing] = useState(false)
const [isDialogOpen, setIsDialogOpen] = useState(false)
const [open, setOpen] = useState(false)
const [isCustomLanguage, setIsCustomLanguage] = useState(false)
const [customLanguage, setCustomLanguage] = useState("")
const [selectedPromptContent, setSelectedPromptContent] = useState("")
const [selectedPromptTitle, setSelectedPromptTitle] = useState("")
const [isToolsEditMode, setIsToolsEditMode] = useState(false)
@ -427,136 +410,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</TabHeader>
<TabContent>
<div className="pb-5 border-b border-vscode-input-border">
<div className="mb-5">
<div className="font-bold mb-1">Preferred Language</div>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="combobox"
role="combobox"
aria-expanded={open}
className="w-full justify-between">
{preferredLanguage ?? "Select language..."}
<CaretSortIcon className="opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent align="start" className="p-0">
<Command>
<CommandInput placeholder="Search language..." className="h-9" />
<CommandList>
<CommandGroup>
{[
{ value: "English", label: "English" },
{ value: "Arabic", label: "Arabic - العربية" },
{
value: "Brazilian Portuguese",
label: "Portuguese - Português (Brasil)",
},
{ value: "Catalan", label: "Catalan - Català" },
{ value: "Czech", label: "Czech - Čeština" },
{ value: "French", label: "French - Français" },
{ value: "German", label: "German - Deutsch" },
{ value: "Hindi", label: "Hindi - हिन्दी" },
{ value: "Hungarian", label: "Hungarian - Magyar" },
{ value: "Italian", label: "Italian - Italiano" },
{ value: "Japanese", label: "Japanese - 日本語" },
{ value: "Korean", label: "Korean - 한국어" },
{ value: "Polish", label: "Polish - Polski" },
{ value: "Portuguese", label: "Portuguese - Português (Portugal)" },
{ value: "Russian", label: "Russian - Русский" },
{ value: "Simplified Chinese", label: "Simplified Chinese - 简体中文" },
{ value: "Spanish", label: "Spanish - Español" },
{
value: "Traditional Chinese",
label: "Traditional Chinese - 繁體中文",
},
{ value: "Turkish", label: "Turkish - Türkçe" },
].map((language) => (
<CommandItem
key={language.value}
value={language.value}
onSelect={(value) => {
setPreferredLanguage(value)
vscode.postMessage({
type: "preferredLanguage",
text: value,
})
setOpen(false)
}}>
{language.label}
<CheckIcon
className={cn(
"ml-auto",
preferredLanguage === language.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
<div className="border-t border-[var(--vscode-input-border)]">
<button
className="w-full px-2 py-1.5 text-sm text-left hover:bg-[var(--vscode-list-hoverBackground)]"
onClick={() => {
setIsCustomLanguage(true)
setOpen(false)
}}>
+ Choose another language
</button>
</div>
</PopoverContent>
</Popover>
<p className="text-xs mt-1.5 text-vscode-descriptionForeground">
Select the language that Roo should use for communication.
</p>
</div>
<div className="font-bold mb-1">Custom Instructions for All Modes</div>
<div className="text-sm text-vscode-descriptionForeground mb-2">
These instructions apply to all modes. They provide a base set of behaviors that can be enhanced
by mode-specific instructions below.
</div>
<VSCodeTextArea
value={customInstructions ?? ""}
onChange={(e) => {
const value =
(e as CustomEvent)?.detail?.target?.value ||
((e as any).target as HTMLTextAreaElement).value
setCustomInstructions(value || undefined)
vscode.postMessage({
type: "customInstructions",
text: value.trim() || undefined,
})
}}
rows={4}
resize="vertical"
className="w-full"
data-testid="global-custom-instructions-textarea"
/>
<div className="text-xs text-vscode-descriptionForeground mt-1.5 mb-10">
Instructions can also be loaded from{" "}
<span
className="text-vscode-textLink-foreground cursor-pointer underline"
onClick={() =>
vscode.postMessage({
type: "openFile",
text: "./.clinerules",
values: {
create: true,
content: "",
},
})
}>
.clinerules
</span>{" "}
in your workspace.
</div>
</div>
<div className="mt-5">
<div>
<div onClick={(e) => e.stopPropagation()} className="flex justify-between items-center mb-3">
<h3 className="text-vscode-foreground m-0">Modes</h3>
<div className="flex gap-2">
@ -987,8 +841,37 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</VSCodeButton>
</div>
{/*
NOTE: This setting is placed in PromptsView rather than SettingsView since it
directly affects the functionality related to modes and custom mode creation,
which are managed in this component. This is an intentional deviation from
the standard pattern described in cline_docs/settings.md.
*/}
<div className="mt-12">
<VSCodeCheckbox
checked={enableCustomModeCreation ?? true}
onChange={(e: any) => {
// Just update the local state through React context
// The React context will update the global state
setEnableCustomModeCreation(e.target.checked)
}}>
<span style={{ fontWeight: "500" }}>Enable Custom Mode Creation Through Prompts</span>
</VSCodeCheckbox>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
When enabled, Roo allows you to create custom modes using prompts like ‘Make me a custom
mode that…’. Disabling this reduces your system prompt by about 700 tokens when this feature
isn’t needed. When disabled you can still manually create custom modes using the + button
above or by editing the related config JSON.
</p>
</div>
{/* Custom System Prompt Disclosure */}
<div className="mb-3 mt-12">
<div className="mt-12">
<button
onClick={() => setIsSystemPromptDisclosureOpen(!isSystemPromptDisclosureOpen)}
className="flex items-center text-xs text-vscode-foreground hover:text-vscode-textLink-foreground focus:outline-none"
@ -1025,34 +908,54 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div>
)}
</div>
</div>
{/*
NOTE: This setting is placed in PromptsView rather than SettingsView since it
directly affects the functionality related to modes and custom mode creation,
which are managed in this component. This is an intentional deviation from
the standard pattern described in cline_docs/settings.md.
*/}
<div className="mb-4 mt-4">
<VSCodeCheckbox
checked={enableCustomModeCreation ?? true}
onChange={(e: any) => {
// Just update the local state through React context
// The React context will update the global state
setEnableCustomModeCreation(e.target.checked)
}}>
<span style={{ fontWeight: "500" }}>Enable Custom Mode Creation Through Prompts</span>
</VSCodeCheckbox>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
When enabled, Roo allows you to create custom modes using prompts like ‘Make me a custom
mode that…’. Disabling this reduces your system prompt by about 700 tokens when this feature
isn’t needed. When disabled you can still manually create custom modes using the + button
above or by editing the related config JSON.
</p>
<div className="pb-5 border-b border-vscode-input-border">
<h3 style={{ color: "var(--vscode-foreground)", marginBottom: "12px" }}>
Custom Instructions for All Modes
</h3>
<div className="text-sm text-vscode-descriptionForeground mb-2">
These instructions apply to all modes. They provide a base set of behaviors that can be enhanced
by mode-specific instructions below.
<br />
If you would like Roo to think and speak in a different language than your editor display
language ({i18next.language}), you can specify it here.
</div>
<VSCodeTextArea
value={customInstructions ?? ""}
onChange={(e) => {
const value =
(e as CustomEvent)?.detail?.target?.value ||
((e as any).target as HTMLTextAreaElement).value
setCustomInstructions(value || undefined)
vscode.postMessage({
type: "customInstructions",
text: value.trim() || undefined,
})
}}
rows={4}
resize="vertical"
className="w-full"
data-testid="global-custom-instructions-textarea"
/>
<div className="text-xs text-vscode-descriptionForeground mt-1.5 mb-10">
Instructions can also be loaded from{" "}
<span
className="text-vscode-textLink-foreground cursor-pointer underline"
onClick={() =>
vscode.postMessage({
type: "openFile",
text: "./.clinerules",
values: {
create: true,
content: "",
},
})
}>
.clinerules
</span>{" "}
in your workspace.
</div>
</div>
@ -1509,41 +1412,6 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div>
</div>
)}
{isCustomLanguage && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-[var(--vscode-editor-background)] p-6 rounded-lg w-96">
<h3 className="text-lg font-semibold mb-4">Add Custom Language</h3>
<input
type="text"
className="w-full p-2 mb-4 bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border border-[var(--vscode-input-border)] rounded"
placeholder="Enter language name"
value={customLanguage}
onChange={(e) => setCustomLanguage(e.target.value)}
/>
<div className="flex justify-end gap-2">
<Button variant="secondary" onClick={() => setIsCustomLanguage(false)}>
Cancel
</Button>
<Button
onClick={() => {
if (customLanguage.trim()) {
setPreferredLanguage(customLanguage.trim())
vscode.postMessage({
type: "preferredLanguage",
text: customLanguage.trim(),
})
setIsCustomLanguage(false)
setCustomLanguage("")
}
}}
disabled={!customLanguage.trim()}>
Add
</Button>
</div>
</div>
</div>
)}
</Tab>
)
}

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

@ -39,8 +39,6 @@ export interface ExtensionStateContextType extends ExtensionState {
setEnableCheckpoints: (value: boolean) => void
setBrowserViewportSize: (value: string) => void
setFuzzyMatchThreshold: (value: number) => void
preferredLanguage: string
setPreferredLanguage: (value: string) => void
setWriteDelayMs: (value: number) => void
screenshotQuality?: number
setScreenshotQuality: (value: number) => void
@ -118,7 +116,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
enableCheckpoints: true,
checkpointStorage: "task",
fuzzyMatchThreshold: 1.0,
preferredLanguage: "English",
language: "en", // Default language code
enableCustomModeCreation: true,
writeDelayMs: 1000,
browserViewportSize: "900x600",
@ -260,7 +258,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setBrowserViewportSize: (value: string) =>
setState((prevState) => ({ ...prevState, browserViewportSize: value })),
setFuzzyMatchThreshold: (value) => setState((prevState) => ({ ...prevState, fuzzyMatchThreshold: value })),
setPreferredLanguage: (value) => setState((prevState) => ({ ...prevState, preferredLanguage: value })),
setWriteDelayMs: (value) => setState((prevState) => ({ ...prevState, writeDelayMs: value })),
setScreenshotQuality: (value) => setState((prevState) => ({ ...prevState, screenshotQuality: value })),
setTerminalOutputLineLimit: (value) =>

View file

@ -109,7 +109,6 @@ describe("mergeExtensionState", () => {
shouldShowAnnouncement: false,
enableCheckpoints: true,
checkpointStorage: "task",
preferredLanguage: "English",
writeDelayMs: 1000,
requestDelaySeconds: 5,
rateLimitSeconds: 0,

View file

@ -0,0 +1,57 @@
import React, { createContext, useContext, ReactNode, useEffect, useCallback } from "react"
import { useTranslation } from "react-i18next"
import i18next, { loadTranslations } from "./setup"
import { useExtensionState } from "@/context/ExtensionStateContext"
// Create context for translations
export const TranslationContext = createContext<{
t: (key: string, options?: Record<string, 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])
// Memoize the translation function to prevent unnecessary re-renders
const translate = useCallback(
(key: string, options?: Record<string, any>) => {
return i18n.t(key, options)
},
[i18n],
)
return (
<TranslationContext.Provider
value={{
t: translate,
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

View file

View file

View file

View file

View file

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

View file

View file

View file

View file

View file

View file

View file

View file

View file

View file

View file

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
}