Browse Source

Merge pull request #1584 from RooVetGit/i18n

I18n
Matt Rubens 10 months ago
parent
commit
0d81c0d801
56 changed files with 1988 additions and 789 deletions
  1. 29 0
      .roomodes
  2. 9 0
      src/__mocks__/vscode.js
  3. 2 4
      src/core/Cline.ts
  4. 1144 0
      src/core/prompts/__tests__/__snapshots__/system.test.ts.snap
  5. 33 40
      src/core/prompts/__tests__/custom-system-prompt.test.ts
  6. 4 4
      src/core/prompts/__tests__/sections.test.ts
  7. 154 113
      src/core/prompts/__tests__/system.test.ts
  8. 2 2
      src/core/prompts/sections/__tests__/custom-instructions.test.ts
  9. 4 3
      src/core/prompts/sections/custom-instructions.ts
  10. 11 5
      src/core/prompts/system.ts
  11. 5 39
      src/core/webview/ClineProvider.ts
  12. 4 13
      src/core/webview/__tests__/ClineProvider.test.ts
  13. 0 1
      src/exports/roo-code.d.ts
  14. 1 1
      src/shared/ExtensionMessage.ts
  15. 0 1
      src/shared/WebviewMessage.ts
  16. 19 0
      src/shared/__tests__/language.test.ts
  17. 2 3
      src/shared/__tests__/modes.test.ts
  18. 0 1
      src/shared/globalState.ts
  19. 14 0
      src/shared/language.ts
  20. 7 4
      src/shared/modes.ts
  21. 2 0
      webview-ui/.gitignore
  22. 7 1
      webview-ui/jest.config.cjs
  23. 130 333
      webview-ui/package-lock.json
  24. 4 1
      webview-ui/package.json
  25. 4 2
      webview-ui/src/App.tsx
  26. 47 0
      webview-ui/src/__mocks__/i18n/TranslationContext.tsx
  27. 62 0
      webview-ui/src/__mocks__/i18n/setup.ts
  28. 4 1
      webview-ui/src/components/chat/ChatView.tsx
  29. 79 211
      webview-ui/src/components/prompts/PromptsView.tsx
  30. 1 1
      webview-ui/src/components/ui/combobox-primitive.tsx
  31. 1 4
      webview-ui/src/context/ExtensionStateContext.tsx
  32. 0 1
      webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx
  33. 57 0
      webview-ui/src/i18n/TranslationContext.tsx
  34. 52 0
      webview-ui/src/i18n/__tests__/TranslationContext.test.tsx
  35. 0 0
      webview-ui/src/i18n/locales/ar/.gitkeep
  36. 0 0
      webview-ui/src/i18n/locales/ca/.gitkeep
  37. 0 0
      webview-ui/src/i18n/locales/cs/.gitkeep
  38. 0 0
      webview-ui/src/i18n/locales/de/.gitkeep
  39. 0 0
      webview-ui/src/i18n/locales/en/.gitkeep
  40. 3 0
      webview-ui/src/i18n/locales/en/chat.json
  41. 0 0
      webview-ui/src/i18n/locales/es/.gitkeep
  42. 0 0
      webview-ui/src/i18n/locales/fr/.gitkeep
  43. 0 0
      webview-ui/src/i18n/locales/hi/.gitkeep
  44. 0 0
      webview-ui/src/i18n/locales/hu/.gitkeep
  45. 0 0
      webview-ui/src/i18n/locales/it/.gitkeep
  46. 0 0
      webview-ui/src/i18n/locales/ja/.gitkeep
  47. 0 0
      webview-ui/src/i18n/locales/ko/.gitkeep
  48. 0 0
      webview-ui/src/i18n/locales/pl/.gitkeep
  49. 0 0
      webview-ui/src/i18n/locales/pt-BR/.gitkeep
  50. 0 0
      webview-ui/src/i18n/locales/pt/.gitkeep
  51. 0 0
      webview-ui/src/i18n/locales/ru/.gitkeep
  52. 0 0
      webview-ui/src/i18n/locales/tr/.gitkeep
  53. 0 0
      webview-ui/src/i18n/locales/zh-CN/.gitkeep
  54. 0 0
      webview-ui/src/i18n/locales/zh-TW/.gitkeep
  55. 54 0
      webview-ui/src/i18n/setup.ts
  56. 37 0
      webview-ui/src/i18n/test-utils.ts

+ 29 - 0
.roomodes

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

+ 9 - 0
src/__mocks__/vscode.js

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

+ 2 - 4
src/core/Cline.ts

@@ -70,6 +70,7 @@ import { truncateConversationIfNeeded } from "./sliding-window"
 import { ClineProvider } from "./webview/ClineProvider"
 import { detectCodeOmission } from "../integrations/editor/detect-omission"
 import { BrowserSession } from "../services/browser/BrowserSession"
+import { formatLanguage } from "../shared/language"
 import { McpHub } from "../services/mcp/McpHub"
 import crypto from "crypto"
 import { insertGroups } from "./diff/insert-groups"
@@ -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
+ 1144 - 0
src/core/prompts/__tests__/__snapshots__/system.test.ts.snap


+ 33 - 40
src/core/prompts/__tests__/custom-system-prompt.test.ts

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

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

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

+ 154 - 113
src/core/prompts/__tests__/system.test.ts

@@ -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 = []
-
-			// Add language preference if provided
-			if (options?.preferredLanguage) {
-				sections.push(
-					`Language Preference:\nYou should always speak and think in the ${options.preferredLanguage} language.`,
-				)
-			}
+// Mock the custom instructions
+jest.mock("../sections/custom-instructions", () => {
+	const addCustomInstructions = jest.fn()
+	return {
+		addCustomInstructions,
+		__setMockImplementation: (impl: any) => {
+			addCustomInstructions.mockImplementation(impl)
+		},
+	}
+})
 
-			// Add global instructions first
-			if (globalCustomInstructions?.trim()) {
-				sections.push(`Global Instructions:\n${globalCustomInstructions.trim()}`)
-			}
+// 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 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()
 	})

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

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

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

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

+ 11 - 5
src/core/prompts/system.ts

@@ -25,6 +25,7 @@ import {
 	addCustomInstructions,
 } from "./sections"
 import { loadSystemPromptFile } from "./sections/custom-system-prompt"
+import { formatLanguage } from "../../shared/language"
 
 async function generatePrompt(
 	context: vscode.ExtensionContext,
@@ -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,

+ 5 - 39
src/core/webview/ClineProvider.ts

@@ -26,6 +26,7 @@ import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage
 import { Mode, CustomModePrompts, PromptComponent, defaultModeSlug, ModeConfig } from "../../shared/modes"
 import { checkExistKey } from "../../shared/checkExistApiConfig"
 import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, ExperimentId } from "../../shared/experiments"
+import { formatLanguage } from "../../shared/language"
 import { downloadTask } from "../../integrations/misc/export-markdown"
 import { openFile, openImage } from "../../integrations/misc/open-file"
 import { selectImages } from "../../integrations/misc/process-images"
@@ -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,

+ 4 - 13
src/core/webview/__tests__/ClineProvider.test.ts

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

+ 0 - 1
src/exports/roo-code.d.ts

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

+ 1 - 1
src/shared/ExtensionMessage.ts

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

+ 0 - 1
src/shared/WebviewMessage.ts

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

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

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

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

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

+ 0 - 1
src/shared/globalState.ts

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

+ 14 - 0
src/shared/language.ts

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

+ 7 - 4
src/shared/modes.ts

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

+ 2 - 0
webview-ui/.gitignore

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

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

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

+ 130 - 333
webview-ui/package-lock.json

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

+ 4 - 1
webview-ui/package.json

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

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

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

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

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

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

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

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

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

+ 79 - 211
webview-ui/src/components/prompts/PromptsView.tsx

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

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

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

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

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

+ 0 - 1
webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx

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

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

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

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

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

+ 0 - 0
webview-ui/src/i18n/locales/ar/.gitkeep


+ 0 - 0
webview-ui/src/i18n/locales/ca/.gitkeep


+ 0 - 0
webview-ui/src/i18n/locales/cs/.gitkeep


+ 0 - 0
webview-ui/src/i18n/locales/de/.gitkeep


+ 0 - 0
webview-ui/src/i18n/locales/en/.gitkeep


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

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

+ 0 - 0
webview-ui/src/i18n/locales/es/.gitkeep


+ 0 - 0
webview-ui/src/i18n/locales/fr/.gitkeep


+ 0 - 0
webview-ui/src/i18n/locales/hi/.gitkeep


+ 0 - 0
webview-ui/src/i18n/locales/hu/.gitkeep


+ 0 - 0
webview-ui/src/i18n/locales/it/.gitkeep


+ 0 - 0
webview-ui/src/i18n/locales/ja/.gitkeep


+ 0 - 0
webview-ui/src/i18n/locales/ko/.gitkeep


+ 0 - 0
webview-ui/src/i18n/locales/pl/.gitkeep


+ 0 - 0
webview-ui/src/i18n/locales/pt-BR/.gitkeep


+ 0 - 0
webview-ui/src/i18n/locales/pt/.gitkeep


+ 0 - 0
webview-ui/src/i18n/locales/ru/.gitkeep


+ 0 - 0
webview-ui/src/i18n/locales/tr/.gitkeep


+ 0 - 0
webview-ui/src/i18n/locales/zh-CN/.gitkeep


+ 0 - 0
webview-ui/src/i18n/locales/zh-TW/.gitkeep


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

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

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

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

Some files were not shown because too many files changed in this diff