diff --git a/.roomodes b/.roomodes index 8dc2492f64..416e4b31e0 100644 --- a/.roomodes +++ b/.roomodes @@ -22,9 +22,10 @@ "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.", - "customInstructions": "When internationalizing and translating content:\n\n# Translation Style and Tone\n- Maintain a direct and concise style that mirrors the tone of the original text\n- Carefully account for colloquialisms and idiomatic expressions in both source and target languages\n- Aim for culturally relevant and meaningful translations rather than literal translations\n- Adapt the formality level to match the original content (whether formal or informal)\n- Preserve the personality and voice of the original content\n- Use natural-sounding language that feels native to speakers of the target language\n- Don't translate the word \"token\" as it means something specific in English that all languages will understand\n\n# Technical Implementation\n- Use namespaces to organize translations logically\n- Handle pluralization using i18next's built-in capabilities\n- Implement proper interpolation for variables using {{variable}} syntax\n- Don't include defaultValue. The `en` translations are the fallback.\n\n# Quality Assurance\n- Maintain consistent terminology across all translations\n- Respect the JSON structure of translation files\n- Watch for placeholders and preserve them in translations\n- Be mindful of text length in UI elements when translating to languages that might require more characters\n- Use context-aware translations when the same string has different meanings\n\n# Supported Languages\n- Localize all strings into the following locale files: ar, ca, cs, de, en, es, fr, hi, hu, it, ja, ko, pl, pt, pt-BR, ru, tr, zh-CN, zh-TW", + "customInstructions": "When internationalizing and translating content:\n\n# Translation Style and Tone\n- Maintain a direct and concise style that mirrors the tone of the original text\n- Carefully account for colloquialisms and idiomatic expressions in both source and target languages\n- Aim for culturally relevant and meaningful translations rather than literal translations\n- Adapt the formality level to match the original content (whether formal or informal)\n- Preserve the personality and voice of the original content\n- Use natural-sounding language that feels native to speakers of the target language\n- Don't translate the word \"token\" as it means something specific in English that all languages will understand\n\n# Technical Implementation\n- Use namespaces to organize translations logically\n- Handle pluralization using i18next's built-in capabilities\n- Implement proper interpolation for variables using {{variable}} syntax\n- Don't include defaultValue. The `en` translations are the fallback.\n- Always use apply_diff instead of write_to_file when editing existing translation files as it's much faster and more reliable\n- When using apply_diff, make sure to carefully identify the exact JSON structure to edit to avoid syntax errors\n- Always use the Trans component for text with embedded components\n\n# Quality Assurance\n- Maintain consistent terminology across all translations\n- Respect the JSON structure of translation files\n- Watch for placeholders and preserve them in translations\n- Be mindful of text length in UI elements when translating to languages that might require more characters\n- Use context-aware translations when the same string has different meanings\n- Always validate your translation work by running the missing translations script:\n ```\n node scripts/find-missing-translations.js\n ```\n- Before completing any translation task, ensure there are no missing translations by running the script with the target locale(s):\n ```\n node scripts/find-missing-translations.js --locale=\n ```\n- Address any missing translations identified by the script to ensure complete coverage across all locales\n\n# Supported Languages\n- Localize all strings into the following locale files: ca, de, en, es, fr, hi, it, ja, ko, pl, pt-BR, tr, vi, zh-CN, zh-TW\n- The translation files are under webview-ui/src/i18n/locales/", "groups": [ "read", + "command", [ "edit", { diff --git a/knip.json b/knip.json index 55b60e8210..ceb48b829c 100644 --- a/knip.json +++ b/knip.json @@ -16,7 +16,8 @@ "e2e/**", "src/activate/**", "src/exports/**", - "src/extension.ts" + "src/extension.ts", + "scripts/**" ], "workspaces": { "webview-ui": { diff --git a/scripts/find-missing-translations.js b/scripts/find-missing-translations.js new file mode 100644 index 0000000000..120d703ed0 --- /dev/null +++ b/scripts/find-missing-translations.js @@ -0,0 +1,218 @@ +/** + * Script to find missing translations in locale files + * + * Usage: + * node scripts/find-missing-translations.js [options] + * + * Options: + * --locale= Only check a specific locale (e.g. --locale=fr) + * --file= Only check a specific file (e.g. --file=chat.json) + * --help Show this help message + */ + +const fs = require("fs") +const path = require("path") + +// Process command line arguments +const args = process.argv.slice(2).reduce((acc, arg) => { + if (arg === "--help") { + acc.help = true + } else if (arg.startsWith("--locale=")) { + acc.locale = arg.split("=")[1] + } else if (arg.startsWith("--file=")) { + acc.file = arg.split("=")[1] + } + return acc +}, {}) + +// Show help if requested +if (args.help) { + console.log(` +Find Missing Translations + +A utility script to identify missing translations across locale files. +Compares non-English locale files to the English ones to find any missing keys. + +Usage: + node scripts/find-missing-translations.js [options] + +Options: + --locale= Only check a specific locale (e.g. --locale=fr) + --file= Only check a specific file (e.g. --file=chat.json) + --help Show this help message + +Output: + - Generates a report of missing translations + `) + process.exit(0) +} + +// Path to the locales directory +const LOCALES_DIR = path.join(__dirname, "../webview-ui/src/i18n/locales") + +// Recursively find all keys in an object +function findKeys(obj, parentKey = "") { + let keys = [] + + for (const [key, value] of Object.entries(obj)) { + const currentKey = parentKey ? `${parentKey}.${key}` : key + + if (typeof value === "object" && value !== null) { + // If value is an object, recurse + keys = [...keys, ...findKeys(value, currentKey)] + } else { + // If value is a primitive, add the key + keys.push(currentKey) + } + } + + return keys +} + +// Get value at a dotted path in an object +function getValueAtPath(obj, path) { + const parts = path.split(".") + let current = obj + + for (const part of parts) { + if (current === undefined || current === null) { + return undefined + } + current = current[part] + } + + return current +} + +// Main function to find missing translations +function findMissingTranslations() { + try { + // Get all locale directories (or filter to the specified locale) + const allLocales = fs.readdirSync(LOCALES_DIR).filter((item) => { + const stats = fs.statSync(path.join(LOCALES_DIR, item)) + return stats.isDirectory() && item !== "en" // Exclude English as it's our source + }) + + // Filter to the specified locale if provided + const locales = args.locale ? allLocales.filter((locale) => locale === args.locale) : allLocales + + if (args.locale && locales.length === 0) { + console.error(`Error: Locale '${args.locale}' not found in ${LOCALES_DIR}`) + process.exit(1) + } + + console.log(`Checking ${locales.length} non-English locale(s): ${locales.join(", ")}`) + + // Get all English JSON files + const englishDir = path.join(LOCALES_DIR, "en") + let englishFiles = fs.readdirSync(englishDir).filter((file) => file.endsWith(".json") && !file.startsWith(".")) + + // Filter to the specified file if provided + if (args.file) { + if (!englishFiles.includes(args.file)) { + console.error(`Error: File '${args.file}' not found in ${englishDir}`) + process.exit(1) + } + englishFiles = englishFiles.filter((file) => file === args.file) + } + + // Load file contents + const englishFileContents = englishFiles.map((file) => ({ + name: file, + content: JSON.parse(fs.readFileSync(path.join(englishDir, file), "utf8")), + })) + + console.log( + `Checking ${englishFileContents.length} translation file(s): ${englishFileContents.map((f) => f.name).join(", ")}`, + ) + + // Results object to store missing translations + const missingTranslations = {} + + // For each locale, check for missing translations + for (const locale of locales) { + missingTranslations[locale] = {} + + for (const { name, content: englishContent } of englishFileContents) { + const localeFilePath = path.join(LOCALES_DIR, locale, name) + + // Check if the file exists in the locale + if (!fs.existsSync(localeFilePath)) { + missingTranslations[locale][name] = { file: "File is missing entirely" } + continue + } + + // Load the locale file + const localeContent = JSON.parse(fs.readFileSync(localeFilePath, "utf8")) + + // Find all keys in the English file + const englishKeys = findKeys(englishContent) + + // Check for missing keys in the locale file + const missingKeys = [] + + for (const key of englishKeys) { + const englishValue = getValueAtPath(englishContent, key) + const localeValue = getValueAtPath(localeContent, key) + + if (localeValue === undefined) { + missingKeys.push({ + key, + englishValue, + }) + } + } + + if (missingKeys.length > 0) { + missingTranslations[locale][name] = missingKeys + } + } + } + + // Output results + let hasMissingTranslations = false + + console.log("\nMissing Translations Report:\n") + + for (const [locale, files] of Object.entries(missingTranslations)) { + if (Object.keys(files).length === 0) { + console.log(`✅ ${locale}: No missing translations`) + continue + } + + hasMissingTranslations = true + console.log(`📝 ${locale}:`) + + for (const [fileName, missingItems] of Object.entries(files)) { + if (missingItems.file) { + console.log(` - ${fileName}: ${missingItems.file}`) + continue + } + + console.log(` - ${fileName}: ${missingItems.length} missing translations`) + + for (const { key, englishValue } of missingItems) { + console.log(` ${key}: "${englishValue}"`) + } + } + + console.log("") + } + + if (!hasMissingTranslations) { + console.log("\n✅ All translations are complete!") + } else { + console.log("✏️ To add missing translations:") + console.log("1. Add the missing keys to the corresponding locale files") + console.log("2. Translate the English values to the appropriate language") + console.log("3. Run this script again to verify all translations are complete") + } + } catch (error) { + console.error("Error:", error.message) + console.error(error.stack) + process.exit(1) + } +} + +// Run the main function +findMissingTranslations() diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 05c7dd085e..326ec27467 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1549,6 +1549,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("browserToolEnabled", message.bool ?? true) await this.postStateToWebview() break + case "language": + await this.updateGlobalState("language", message.text) + await this.postStateToWebview() + break case "showRooIgnoredFiles": await this.updateGlobalState("showRooIgnoredFiles", message.bool ?? true) await this.postStateToWebview() @@ -2528,8 +2532,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { writeDelayMs: stateValues.writeDelayMs ?? 1000, terminalOutputLineLimit: stateValues.terminalOutputLineLimit ?? 500, mode: stateValues.mode ?? defaultModeSlug, - // Pass the VSCode language code directly - language: formatLanguage(vscode.env.language), + language: stateValues.language || formatLanguage(vscode.env.language), mcpEnabled: stateValues.mcpEnabled ?? true, enableMcpServerCreation: stateValues.enableMcpServerCreation ?? true, alwaysApproveResubmit: stateValues.alwaysApproveResubmit ?? false, @@ -2641,7 +2644,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { * like the current mode, API provider, etc. */ public async getTelemetryProperties(): Promise> { - const { mode, apiConfiguration } = await this.getState() + const { mode, apiConfiguration, language } = await this.getState() const appVersion = this.context.extension?.packageJSON?.version const vscodeVersion = vscode.version const platform = process.platform @@ -2656,6 +2659,11 @@ export class ClineProvider implements vscode.WebviewViewProvider { properties.appVersion = appVersion } + // Add language + if (language) { + properties.language = language + } + // Add current mode if (mode) { properties.mode = mode diff --git a/src/exports/roo-code.d.ts b/src/exports/roo-code.d.ts index 1c45dec84b..f9764b91a5 100644 --- a/src/exports/roo-code.d.ts +++ b/src/exports/roo-code.d.ts @@ -218,6 +218,7 @@ export type GlobalStateKey = | "telemetrySetting" | "showRooIgnoredFiles" | "remoteBrowserEnabled" + | "language" export type ConfigurationKey = GlobalStateKey | SecretKey diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index d3f316742b..3982cd5c16 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -108,6 +108,7 @@ export interface WebviewMessage { | "discoverBrowser" | "browserConnectionResult" | "remoteBrowserEnabled" + | "language" text?: string disabled?: boolean askResponse?: ClineAskResponse diff --git a/src/shared/__tests__/experiments.test.ts b/src/shared/__tests__/experiments.test.ts index 07566c8e9c..a192b260cf 100644 --- a/src/shared/__tests__/experiments.test.ts +++ b/src/shared/__tests__/experiments.test.ts @@ -5,9 +5,6 @@ describe("experiments", () => { it("is configured correctly", () => { expect(EXPERIMENT_IDS.POWER_STEERING).toBe("powerSteering") expect(experimentConfigsMap.POWER_STEERING).toMatchObject({ - name: 'Use experimental "power steering" mode', - description: - "When enabled, Roo will remind the model about the details of its current mode definition more frequently. This will lead to stronger adherence to role definitions and custom instructions, but will use more tokens per message.", enabled: false, }) }) diff --git a/src/shared/experiments.ts b/src/shared/experiments.ts index dd8ff84a8c..f4decc324c 100644 --- a/src/shared/experiments.ts +++ b/src/shared/experiments.ts @@ -10,8 +10,6 @@ export type ExperimentKey = keyof typeof EXPERIMENT_IDS export type ExperimentId = valueof export interface ExperimentConfig { - name: string - description: string enabled: boolean } @@ -19,34 +17,18 @@ type valueof = X[keyof X] export const experimentConfigsMap: Record = { DIFF_STRATEGY: { - name: "Use experimental unified diff strategy", - description: - "Enable the experimental unified diff strategy. This strategy might reduce the number of retries caused by model errors but may cause unexpected behavior or incorrect edits. Only enable if you understand the risks and are willing to carefully review all changes.", enabled: false, }, SEARCH_AND_REPLACE: { - name: "Use experimental search and replace tool", - description: - "Enable the experimental search and replace tool, allowing Roo to replace multiple instances of a search term in one request.", enabled: false, }, INSERT_BLOCK: { - name: "Use experimental insert content tool", - - description: - "Enable the experimental insert content tool, allowing Roo to insert content at specific line numbers without needing to create a diff.", enabled: false, }, POWER_STEERING: { - name: 'Use experimental "power steering" mode', - description: - "When enabled, Roo will remind the model about the details of its current mode definition more frequently. This will lead to stronger adherence to role definitions and custom instructions, but will use more tokens per message.", enabled: false, }, MULTI_SEARCH_AND_REPLACE: { - name: "Use experimental multi block diff tool", - description: - "When enabled, Roo will use multi block diff tool. This will try to update multiple code blocks in the file in one request.", enabled: false, }, } @@ -67,17 +49,4 @@ export const experiments = { }, } as const -// Expose experiment details for UI - pre-compute from map for better performance -export const experimentLabels = Object.fromEntries( - Object.entries(experimentConfigsMap).map(([_, config]) => [ - EXPERIMENT_IDS[_ as keyof typeof EXPERIMENT_IDS] as ExperimentId, - config.name, - ]), -) as Record - -export const experimentDescriptions = Object.fromEntries( - Object.entries(experimentConfigsMap).map(([_, config]) => [ - EXPERIMENT_IDS[_ as keyof typeof EXPERIMENT_IDS] as ExperimentId, - config.description, - ]), -) as Record +// No longer needed as we use translation keys directly in the UI diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts index cec7bdf9ce..c2b78f1ef7 100644 --- a/src/shared/globalState.ts +++ b/src/shared/globalState.ts @@ -116,6 +116,7 @@ export const GLOBAL_STATE_KEYS = [ "telemetrySetting", "showRooIgnoredFiles", "remoteBrowserEnabled", + "language", "maxWorkspaceFiles", ] as const diff --git a/src/shared/tool-groups.ts b/src/shared/tool-groups.ts index 50c7b80ca9..11d0513e47 100644 --- a/src/shared/tool-groups.ts +++ b/src/shared/tool-groups.ts @@ -66,12 +66,3 @@ export function getToolName(toolConfig: string | readonly [ToolName, ...any[]]): export function getToolOptions(toolConfig: string | readonly [ToolName, ...any[]]): any { return typeof toolConfig === "string" ? undefined : toolConfig[1] } - -// Display names for groups in UI -export const GROUP_DISPLAY_NAMES: Record = { - read: "Read Files", - edit: "Edit Files", - browser: "Use Browser", - command: "Run Commands", - mcp: "Use MCP", -} diff --git a/webview-ui/src/components/common/TelemetryBanner.tsx b/webview-ui/src/components/common/TelemetryBanner.tsx index e6f52ce2d6..ee3a993f34 100644 --- a/webview-ui/src/components/common/TelemetryBanner.tsx +++ b/webview-ui/src/components/common/TelemetryBanner.tsx @@ -45,13 +45,13 @@ const TelemetryBanner = () => { return (
- {t("common:telemetryTitle")} + {t("welcome:telemetry.title")}
- {t("common:anonymousTelemetry")} + {t("welcome:telemetry.anonymousTelemetry")}
- {t("common:changeSettings")}{" "} + {t("welcome:telemetry.changeSettings")}{" "} - {t("common:settings")} + {t("welcome:telemetry.settings")} .
@@ -59,10 +59,10 @@ const TelemetryBanner = () => {
- {t("common:allow")} + {t("welcome:telemetry.allow")} - {t("common:deny")} + {t("welcome:telemetry.deny")} diff --git a/webview-ui/src/components/history/CopyButton.tsx b/webview-ui/src/components/history/CopyButton.tsx index 17964cc37e..c11624ab8c 100644 --- a/webview-ui/src/components/history/CopyButton.tsx +++ b/webview-ui/src/components/history/CopyButton.tsx @@ -27,6 +27,7 @@ export const CopyButton = ({ itemTask }: CopyButtonProps) => { size="icon" title={t("history:copyPrompt")} onClick={onCopy} + data-testid="copy-prompt-button" className="opacity-50 hover:opacity-100"> diff --git a/webview-ui/src/components/history/HistoryView.tsx b/webview-ui/src/components/history/HistoryView.tsx index c82fd0b92a..64528cb0ea 100644 --- a/webview-ui/src/components/history/HistoryView.tsx +++ b/webview-ui/src/components/history/HistoryView.tsx @@ -39,6 +39,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => { style={{ width: "100%" }} placeholder={t("history:searchPlaceholder")} value={searchQuery} + data-testid="history-search-input" onInput={(e) => { const newValue = (e.target as HTMLInputElement)?.value setSearchQuery(newValue) @@ -72,13 +73,22 @@ const HistoryView = ({ onDone }: HistoryViewProps) => { value={sortOption} role="radiogroup" onChange={(e) => setSortOption((e.target as HTMLInputElement).value as SortOption)}> - {t("history:newest")} - {t("history:oldest")} - {t("history:mostExpensive")} - {t("history:mostTokens")} + + {t("history:newest")} + + + {t("history:oldest")} + + + {t("history:mostExpensive")} + + + {t("history:mostTokens")} + {t("history:mostRelevant")} @@ -135,6 +145,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => { variant="ghost" size="sm" title={t("history:deleteTaskTitle")} + data-testid="delete-task-button" onClick={(e) => { e.stopPropagation() diff --git a/webview-ui/src/components/history/__tests__/HistoryView.test.tsx b/webview-ui/src/components/history/__tests__/HistoryView.test.tsx index a88a42ad2e..4470e5d02d 100644 --- a/webview-ui/src/components/history/__tests__/HistoryView.test.tsx +++ b/webview-ui/src/components/history/__tests__/HistoryView.test.tsx @@ -75,7 +75,7 @@ describe("HistoryView", () => { render() // Get search input and radio group - const searchInput = screen.getByPlaceholderText("Fuzzy search history...") + const searchInput = screen.getByTestId("history-search-input") const radioGroup = screen.getByRole("radiogroup") // Type in search @@ -85,7 +85,7 @@ describe("HistoryView", () => { jest.advanceTimersByTime(100) // Check if sort option automatically changes to "Most Relevant" - const mostRelevantRadio = within(radioGroup).getByLabelText("Most Relevant") + const mostRelevantRadio = within(radioGroup).getByTestId("radio-most-relevant") expect(mostRelevantRadio).not.toBeDisabled() // Click the radio button @@ -95,7 +95,7 @@ describe("HistoryView", () => { jest.advanceTimersByTime(100) // Verify radio button is checked - const updatedRadio = within(radioGroup).getByRole("radio", { name: "Most Relevant", checked: true }) + const updatedRadio = within(radioGroup).getByTestId("radio-most-relevant") expect(updatedRadio).toBeInTheDocument() }) @@ -106,21 +106,18 @@ describe("HistoryView", () => { const radioGroup = screen.getByRole("radiogroup") // Test changing sort options - const oldestRadio = within(radioGroup).getByLabelText("Oldest") + const oldestRadio = within(radioGroup).getByTestId("radio-oldest") fireEvent.click(oldestRadio) // Wait for oldest radio to be checked - const checkedOldestRadio = await within(radioGroup).findByRole("radio", { name: "Oldest", checked: true }) + const checkedOldestRadio = within(radioGroup).getByTestId("radio-oldest") expect(checkedOldestRadio).toBeInTheDocument() - const mostExpensiveRadio = within(radioGroup).getByLabelText("Most Expensive") + const mostExpensiveRadio = within(radioGroup).getByTestId("radio-most-expensive") fireEvent.click(mostExpensiveRadio) // Wait for most expensive radio to be checked - const checkedExpensiveRadio = await within(radioGroup).findByRole("radio", { - name: "Most Expensive", - checked: true, - }) + const checkedExpensiveRadio = within(radioGroup).getByTestId("radio-most-expensive") expect(checkedExpensiveRadio).toBeInTheDocument() }) @@ -148,7 +145,7 @@ describe("HistoryView", () => { fireEvent.mouseEnter(taskContainer) // Click delete button to open confirmation dialog - const deleteButton = within(taskContainer).getByTitle("Delete Task (Shift + Click to skip confirmation)") + const deleteButton = within(taskContainer).getByTestId("delete-task-button") fireEvent.click(deleteButton) // Verify dialog is shown @@ -175,7 +172,7 @@ describe("HistoryView", () => { fireEvent.mouseEnter(taskContainer) // Shift-click delete button - const deleteButton = within(taskContainer).getByTitle("Delete Task (Shift + Click to skip confirmation)") + const deleteButton = within(taskContainer).getByTestId("delete-task-button") fireEvent.click(deleteButton, { shiftKey: true }) // Verify no dialog is shown @@ -203,7 +200,7 @@ describe("HistoryView", () => { const taskContainer = screen.getByTestId("virtuoso-item-1") fireEvent.mouseEnter(taskContainer) - const copyButton = within(taskContainer).getByTitle("Copy Prompt") + const copyButton = within(taskContainer).getByTestId("copy-prompt-button") // Click the copy button and wait for clipboard operation await act(async () => { diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx index 391a6c9dca..5588aaba02 100644 --- a/webview-ui/src/components/prompts/PromptsView.tsx +++ b/webview-ui/src/components/prompts/PromptsView.tsx @@ -22,7 +22,7 @@ import { import { CustomModeSchema } from "../../../../src/core/config/CustomModesSchema" import { supportPrompt, SupportPromptType } from "../../../../src/shared/support-prompt" -import { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups" +import { TOOL_GROUPS, ToolGroup } from "../../../../src/shared/tool-groups" import { vscode } from "../../utils/vscode" import { Tab, TabContent, TabHeader } from "../common/Tab" import i18next from "i18next" @@ -657,7 +657,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { checked={isGroupEnabled} onChange={handleGroupChange(group, Boolean(isCustomMode), customMode)} disabled={!isCustomMode}> - {GROUP_DISPLAY_NAMES[group]} + {t(`prompts:tools.toolNames.${group}`)} {group === "edit" && (
{t("prompts:tools.allowedFiles")}{" "} @@ -689,7 +689,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { return enabledGroups .map((group) => { const groupName = getGroupName(group) - const displayName = GROUP_DISPLAY_NAMES[groupName] + const displayName = t(`prompts:tools.toolNames.${groupName}`) if (Array.isArray(group) && group[1]?.fileRegex) { const description = group[1].description || `/${group[1].fileRegex}/` @@ -1247,7 +1247,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { ) } }}> - {GROUP_DISPLAY_NAMES[group]} + {t(`prompts:tools.toolNames.${group}`)} ))}
diff --git a/webview-ui/src/components/settings/AdvancedSettings.tsx b/webview-ui/src/components/settings/AdvancedSettings.tsx index e25366331e..e217537ab6 100644 --- a/webview-ui/src/components/settings/AdvancedSettings.tsx +++ b/webview-ui/src/components/settings/AdvancedSettings.tsx @@ -1,4 +1,5 @@ import { HTMLAttributes } from "react" +import { useAppTranslation } from "@/i18n/TranslationContext" import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" import { Cog } from "lucide-react" @@ -29,19 +30,20 @@ export const AdvancedSettings = ({ className, ...props }: AdvancedSettingsProps) => { + const { t } = useAppTranslation() return (
-
Advanced
+
{t("settings:sections.advanced")}
- Rate limit + {t("settings:advanced.rateLimit.label")}
{rateLimitSeconds}s
-

Minimum time between API requests.

+

+ {t("settings:advanced.rateLimit.description")} +

@@ -69,16 +73,15 @@ export const AdvancedSettings = ({ setExperimentEnabled(EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE, false) } }}> - Enable editing through diffs + {t("settings:advanced.diff.label")}

- When enabled, Roo will be able to edit files more quickly and will automatically reject - truncated full-file writes. Works best with the latest Claude 3.7 Sonnet model. + {t("settings:advanced.diff.description")}

{diffEnabled && (
- Diff strategy + {t("settings:advanced.diff.strategy.label")}
@@ -111,15 +120,15 @@ export const AdvancedSettings = ({

{!experiments[EXPERIMENT_IDS.DIFF_STRATEGY] && !experiments[EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE] && - "Standard diff strategy applies changes to a single code block at a time."} + t("settings:advanced.diff.strategy.descriptions.standard")} {experiments[EXPERIMENT_IDS.DIFF_STRATEGY] && - "Unified diff strategy takes multiple approaches to applying diffs and chooses the best approach."} + t("settings:advanced.diff.strategy.descriptions.unified")} {experiments[EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE] && - "Multi-block diff strategy allows updating multiple code blocks in a file in one request."} + t("settings:advanced.diff.strategy.descriptions.multiBlock")}

{/* Match precision slider */} - Match precision + {t("settings:advanced.diff.matchPrecision.label")}

- This slider controls how precisely code sections must match when applying diffs. Lower - values allow more flexible matching but increase the risk of incorrect replacements. Use - values below 100% with extreme caution. + {t("settings:advanced.diff.matchPrecision.description")}

)} diff --git a/webview-ui/src/components/settings/ApiConfigManager.tsx b/webview-ui/src/components/settings/ApiConfigManager.tsx index 59d5d54f78..b7c7668930 100644 --- a/webview-ui/src/components/settings/ApiConfigManager.tsx +++ b/webview-ui/src/components/settings/ApiConfigManager.tsx @@ -1,5 +1,6 @@ import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { memo, useEffect, useRef, useState } from "react" +import { useAppTranslation } from "@/i18n/TranslationContext" import { ApiConfigMeta } from "../../../../src/shared/ExtensionMessage" import { Dropdown } from "vscrui" import type { DropdownOption } from "vscrui" @@ -23,6 +24,7 @@ const ApiConfigManager = ({ onRenameConfig, onUpsertConfig, }: ApiConfigManagerProps) => { + const { t } = useAppTranslation() const [isRenaming, setIsRenaming] = useState(false) const [isCreating, setIsCreating] = useState(false) const [inputValue, setInputValue] = useState("") @@ -33,18 +35,18 @@ const ApiConfigManager = ({ const validateName = (name: string, isNewProfile: boolean): string | null => { const trimmed = name.trim() - if (!trimmed) return "Name cannot be empty" + if (!trimmed) return t("settings:providers.nameEmpty") const nameExists = listApiConfigMeta?.some((config) => config.name.toLowerCase() === trimmed.toLowerCase()) // For new profiles, any existing name is invalid if (isNewProfile && nameExists) { - return "A profile with this name already exists" + return t("settings:providers.nameExists") } // For rename, only block if trying to rename to a different existing profile if (!isNewProfile && nameExists && trimmed.toLowerCase() !== currentApiConfigName?.toLowerCase()) { - return "A profile with this name already exists" + return t("settings:providers.nameExists") } return null @@ -144,7 +146,7 @@ const ApiConfigManager = ({ return (
{isRenaming ? ( @@ -160,7 +162,7 @@ const ApiConfigManager = ({ setInputValue(target.target.value) setError(null) }} - placeholder="Enter new name" + placeholder={t("settings:providers.enterNewName")} style={{ flexGrow: 1 }} onKeyDown={(e: unknown) => { const event = e as { key: string } @@ -175,7 +177,8 @@ const ApiConfigManager = ({ appearance="icon" disabled={!inputValue.trim()} onClick={handleSave} - title="Save" + title={t("settings:common.save")} + data-testid="save-rename-button" style={{ padding: 0, margin: 0, @@ -188,7 +191,8 @@ const ApiConfigManager = ({ - Save different API configurations to quickly switch between providers and settings. + {t("settings:providers.description")}

)} @@ -290,7 +301,7 @@ const ApiConfigManager = ({ }} aria-labelledby="new-profile-title"> - New Configuration Profile + {t("settings:providers.newProfile")} { const event = e as { key: string } @@ -316,11 +328,15 @@ const ApiConfigManager = ({

)}
- -
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 4975679642..2262df28f7 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -1,4 +1,6 @@ import React, { memo, useCallback, useEffect, useMemo, useState } from "react" +import { useAppTranslation } from "@/i18n/TranslationContext" +import { Trans } from "react-i18next" import { useDebounce, useEvent } from "react-use" import { Checkbox, Dropdown, type DropdownOption } from "vscrui" import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" @@ -91,6 +93,7 @@ const ApiOptions = ({ errorMessage, setErrorMessage, }: ApiOptionsProps) => { + const { t } = useAppTranslation() const [ollamaModels, setOllamaModels] = useState([]) const [lmStudioModels, setLmStudioModels] = useState([]) const [vsCodeLmModels, setVsCodeLmModels] = useState([]) @@ -259,7 +262,7 @@ const ApiOptions = ({

- Adjust the WebP quality of browser screenshots. Higher values provide clearer - screenshots but increase token usage. + {t("settings:browser.screenshotQuality.description")}

@@ -183,11 +188,10 @@ export const BrowserSettings = ({ setCachedStateField("remoteBrowserHost", undefined) } }}> - Use remote browser connection + {t("settings:browser.remote.label")}

- Connect to a Chrome browser running with remote debugging enabled - (--remote-debugging-port=9222). + {t("settings:browser.remote.description")}

{remoteBrowserEnabled && ( @@ -201,13 +205,15 @@ export const BrowserSettings = ({ e.target.value || undefined, ) } - placeholder="Custom URL (e.g., http://localhost:9222)" + placeholder={t("settings:browser.remote.urlPlaceholder")} style={{ flexGrow: 1 }} /> - {testingConnection || discovering ? "Testing..." : "Test Connection"} + {testingConnection || discovering + ? t("settings:browser.remote.testingButton") + : t("settings:browser.remote.testButton")}
{testResult && ( @@ -221,10 +227,7 @@ export const BrowserSettings = ({
)}

- Enter the DevTools Protocol host address or - leave empty to auto-discover Chrome local instances. - The Test Connection button will try the custom URL if provided, or - auto-discover if the field is empty. + {t("settings:browser.remote.instructions")}

)} diff --git a/webview-ui/src/components/settings/CheckpointSettings.tsx b/webview-ui/src/components/settings/CheckpointSettings.tsx index fa3b913832..6987ba4a03 100644 --- a/webview-ui/src/components/settings/CheckpointSettings.tsx +++ b/webview-ui/src/components/settings/CheckpointSettings.tsx @@ -1,4 +1,5 @@ import { HTMLAttributes } from "react" +import { useAppTranslation } from "@/i18n/TranslationContext" import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" import { GitBranch } from "lucide-react" @@ -20,12 +21,13 @@ export const CheckpointSettings = ({ setCachedStateField, ...props }: CheckpointSettingsProps) => { + const { t } = useAppTranslation() return (
-
Checkpoints
+
{t("settings:sections.checkpoints")}
@@ -36,11 +38,10 @@ export const CheckpointSettings = ({ onChange={(e: any) => { setCachedStateField("enableCheckpoints", e.target.checked) }}> - Enable automatic checkpoints + {t("settings:checkpoints.enable.label")}

- When enabled, Roo will automatically create checkpoints during task execution, making it easy to - review changes or revert to earlier states. + {t("settings:checkpoints.enable.description")}

diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx index d5f4c33f9a..de17de8dd5 100644 --- a/webview-ui/src/components/settings/ContextManagementSettings.tsx +++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx @@ -1,4 +1,5 @@ import { HTMLAttributes } from "react" +import { useAppTranslation } from "@/i18n/TranslationContext" import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" import { Database } from "lucide-react" @@ -28,19 +29,20 @@ export const ContextManagementSettings = ({ className, ...props }: ContextManagementSettingsProps) => { + const { t } = useAppTranslation() return (
- +
-
Context Management
+
{t("settings:sections.contextManagement")}
- Terminal output limit + {t("settings:contextManagement.terminal.label")}

- Maximum number of lines to include in terminal output when executing commands. When exceeded - lines will be removed from the middle, saving tokens. + {t("settings:contextManagement.terminal.description")}

- Open tabs context limit + {t("settings:contextManagement.openTabs.label")}

- Maximum number of VSCode open tabs to include in context. Higher values provide more context but - increase token usage. + {t("settings:contextManagement.openTabs.description")}

- Workspace files context limit + {t("settings:contextManagement.workspaceFiles.label")}

- Maximum number of files to include in current working directory details. Higher values provide - more context but increase token usage. + {t("settings:contextManagement.workspaceFiles.description")}

@@ -116,11 +115,10 @@ export const ContextManagementSettings = ({ setCachedStateField("showRooIgnoredFiles", e.target.checked) }} data-testid="show-rooignored-files-checkbox"> - Show .rooignore'd files in lists and searches + {t("settings:contextManagement.rooignore.label")}

- When enabled, files matching patterns in .rooignore will be shown in lists with a lock symbol. - When disabled, these files will be completely hidden from file lists and searches. + {t("settings:contextManagement.rooignore.description")}

diff --git a/webview-ui/src/components/settings/ExperimentalFeature.tsx b/webview-ui/src/components/settings/ExperimentalFeature.tsx index e06bfc513a..e2abe0340b 100644 --- a/webview-ui/src/components/settings/ExperimentalFeature.tsx +++ b/webview-ui/src/components/settings/ExperimentalFeature.tsx @@ -1,20 +1,29 @@ import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" +import { useAppTranslation } from "@/i18n/TranslationContext" interface ExperimentalFeatureProps { - name: string - description: string enabled: boolean onChange: (value: boolean) => void + // Additional property to identify the experiment + experimentKey?: string } -export const ExperimentalFeature = ({ name, description, enabled, onChange }: ExperimentalFeatureProps) => ( -
-
- ⚠️ - onChange(e.target.checked)}> - {name} - +export const ExperimentalFeature = ({ enabled, onChange, experimentKey }: ExperimentalFeatureProps) => { + const { t } = useAppTranslation() + + // Generate translation keys based on experiment key + const nameKey = experimentKey ? `settings:experimental.${experimentKey}.name` : "" + const descriptionKey = experimentKey ? `settings:experimental.${experimentKey}.description` : "" + + return ( +
+
+ {t("settings:experimental.warning")} + onChange(e.target.checked)}> + {t(nameKey)} + +
+

{t(descriptionKey)}

-

{description}

-
-) + ) +} diff --git a/webview-ui/src/components/settings/ExperimentalSettings.tsx b/webview-ui/src/components/settings/ExperimentalSettings.tsx index bbdfe47e89..8700670b92 100644 --- a/webview-ui/src/components/settings/ExperimentalSettings.tsx +++ b/webview-ui/src/components/settings/ExperimentalSettings.tsx @@ -1,4 +1,5 @@ import { HTMLAttributes } from "react" +import { useAppTranslation } from "@/i18n/TranslationContext" import { FlaskConical } from "lucide-react" import { EXPERIMENT_IDS, experimentConfigsMap, ExperimentId } from "../../../../src/shared/experiments" @@ -25,12 +26,13 @@ export const ExperimentalSettings = ({ className, ...props }: ExperimentalSettingsProps) => { + const { t } = useAppTranslation() return (
-
Experimental Features
+
{t("settings:sections.experimental")}
@@ -40,7 +42,7 @@ export const ExperimentalSettings = ({ .map((config) => ( setExperimentEnabled(EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS], enabled) diff --git a/webview-ui/src/components/settings/ModelInfoView.tsx b/webview-ui/src/components/settings/ModelInfoView.tsx index 8e9564525f..603c5a5fe5 100644 --- a/webview-ui/src/components/settings/ModelInfoView.tsx +++ b/webview-ui/src/components/settings/ModelInfoView.tsx @@ -1,5 +1,6 @@ import { useMemo } from "react" import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" +import { useAppTranslation } from "@/i18n/TranslationContext" import { formatPrice } from "@/utils/formatPrice" import { cn } from "@/lib/utils" @@ -21,59 +22,64 @@ export const ModelInfoView = ({ isDescriptionExpanded, setIsDescriptionExpanded, }: ModelInfoViewProps) => { + const { t } = useAppTranslation() const isGemini = useMemo(() => Object.keys(geminiModels).includes(selectedModelId), [selectedModelId]) const infoItems = [ , , !isGemini && ( ), modelInfo.maxTokens !== undefined && modelInfo.maxTokens > 0 && ( <> - Max output: {modelInfo.maxTokens?.toLocaleString()} tokens + {t("settings:modelInfo.maxOutput")}:{" "} + {modelInfo.maxTokens?.toLocaleString()} tokens ), modelInfo.inputPrice !== undefined && modelInfo.inputPrice > 0 && ( <> - Input price: {formatPrice(modelInfo.inputPrice)} / 1M tokens + {t("settings:modelInfo.inputPrice")}:{" "} + {formatPrice(modelInfo.inputPrice)} / 1M tokens ), modelInfo.outputPrice !== undefined && modelInfo.outputPrice > 0 && ( <> - Output price: {formatPrice(modelInfo.outputPrice)} / 1M tokens + {t("settings:modelInfo.outputPrice")}:{" "} + {formatPrice(modelInfo.outputPrice)} / 1M tokens ), modelInfo.supportsPromptCache && modelInfo.cacheReadsPrice && ( <> - Cache reads price: {formatPrice(modelInfo.cacheReadsPrice || 0)} / - 1M tokens + {t("settings:modelInfo.cacheReadsPrice")}:{" "} + {formatPrice(modelInfo.cacheReadsPrice || 0)} / 1M tokens ), modelInfo.supportsPromptCache && modelInfo.cacheWritesPrice && ( <> - Cache writes price: {formatPrice(modelInfo.cacheWritesPrice || 0)}{" "} - / 1M tokens + {t("settings:modelInfo.cacheWritesPrice")}:{" "} + {formatPrice(modelInfo.cacheWritesPrice || 0)} / 1M tokens ), isGemini && ( - * Free up to {selectedModelId && selectedModelId.includes("flash") ? "15" : "2"} requests per minute. - After that, billing depends on prompt size.{" "} + {t("settings:modelInfo.gemini.freeRequests", { + count: selectedModelId && selectedModelId.includes("flash") ? 15 : 2, + })}{" "} - For more info, see pricing details. + {t("settings:modelInfo.gemini.pricingDetails")} ), diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx index 8a3e7f73dc..2da4a94cad 100644 --- a/webview-ui/src/components/settings/ModelPicker.tsx +++ b/webview-ui/src/components/settings/ModelPicker.tsx @@ -1,5 +1,6 @@ import { useMemo, useState, useCallback, useEffect, useRef } from "react" import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" +import { Trans } from "react-i18next" import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem } from "@/components/ui/combobox" @@ -100,15 +101,17 @@ export const ModelPicker = ({ modelInfo={selectedModelInfo} />
- The extension automatically fetches the latest list of models available on{" "} - - {serviceName} - - . If you're unsure which model to choose, Roo Code works best with{" "} - onSelect(defaultModelId)} className="text-sm"> - {defaultModelId}. - - You can also try searching "free" for no-cost options currently available. + , + defaultModelLink: onSelect(defaultModelId)} className="text-sm" />, + }} + values={{ + serviceName, + defaultModelId, + }} + />
) diff --git a/webview-ui/src/components/settings/NotificationSettings.tsx b/webview-ui/src/components/settings/NotificationSettings.tsx index 1fba9dd412..4466bc339b 100644 --- a/webview-ui/src/components/settings/NotificationSettings.tsx +++ b/webview-ui/src/components/settings/NotificationSettings.tsx @@ -1,4 +1,5 @@ import { HTMLAttributes } from "react" +import { useAppTranslation } from "@/i18n/TranslationContext" import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" import { Bell } from "lucide-react" @@ -18,12 +19,13 @@ export const NotificationSettings = ({ setCachedStateField, ...props }: NotificationSettingsProps) => { + const { t } = useAppTranslation() return (
-
Notifications
+
{t("settings:sections.notifications")}
@@ -31,11 +33,12 @@ export const NotificationSettings = ({
setCachedStateField("soundEnabled", e.target.checked)}> - Enable sound effects + onChange={(e: any) => setCachedStateField("soundEnabled", e.target.checked)} + data-testid="sound-enabled-checkbox"> + {t("settings:notifications.sound.label")}

- When enabled, Roo will play sound effects for notifications and events. + {t("settings:notifications.sound.description")}

{soundEnabled && (
setCachedStateField("soundVolume", parseFloat(e.target.value))} className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background" aria-label="Volume" + data-testid="sound-volume-slider" /> {((soundVolume ?? 0.5) * 100).toFixed(0)}%
-

Volume

+

+ {t("settings:notifications.sound.volumeLabel")} +

)}
diff --git a/webview-ui/src/components/settings/SectionHeader.tsx b/webview-ui/src/components/settings/SectionHeader.tsx index 709052cea8..ee120639c9 100644 --- a/webview-ui/src/components/settings/SectionHeader.tsx +++ b/webview-ui/src/components/settings/SectionHeader.tsx @@ -7,14 +7,16 @@ type SectionHeaderProps = HTMLAttributes & { description?: string } -export const SectionHeader = ({ description, children, className, ...props }: SectionHeaderProps) => ( -
-

{children}

- {description &&

{description}

} -
-) +export const SectionHeader = ({ description, children, className, ...props }: SectionHeaderProps) => { + return ( +
+

{children}

+ {description &&

{description}

} +
+ ) +} diff --git a/webview-ui/src/components/settings/SettingsFooter.tsx b/webview-ui/src/components/settings/SettingsFooter.tsx index fba7d363c9..ad8d3a3da7 100644 --- a/webview-ui/src/components/settings/SettingsFooter.tsx +++ b/webview-ui/src/components/settings/SettingsFooter.tsx @@ -1,73 +1,116 @@ import { HTMLAttributes } from "react" +import { useAppTranslation } from "@/i18n/TranslationContext" +import { Trans } from "react-i18next" +import { Globe } from "lucide-react" import { VSCodeButton, VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react" import { vscode } from "@/utils/vscode" import { cn } from "@/lib/utils" import { TelemetrySetting } from "../../../../src/shared/TelemetrySetting" +import { SetCachedStateField } from "./types" + +// Map of language codes to their display names +const LANGUAGES: Record = { + ca: "Català", + de: "Deutsch", + en: "English", + es: "Español", + fr: "Français", + hi: "हिन्दी", + it: "Italiano", + ja: "日本語", + ko: "한국어", + pl: "Polski", + "pt-BR": "Português", + tr: "Türkçe", + vi: "Tiếng Việt", + "zh-CN": "简体中文", + "zh-TW": "繁體中文", +} type SettingsFooterProps = HTMLAttributes & { version: string telemetrySetting: TelemetrySetting setTelemetrySetting: (setting: TelemetrySetting) => void + language: string + setCachedStateField: SetCachedStateField<"language"> } export const SettingsFooter = ({ version, telemetrySetting, setTelemetrySetting, + language, + setCachedStateField, className, ...props -}: SettingsFooterProps) => ( -
-

- If you have any questions or feedback, feel free to open an issue at{" "} - - github.com/RooVetGit/Roo-Code - {" "} - or join{" "} - - reddit.com/r/RooCode - -

-

Roo Code v{version}

-
-
- { - const checked = e.target.checked === true - setTelemetrySetting(checked ? "enabled" : "disabled") - }}> - Allow anonymous error and usage reporting - -

- Help improve Roo Code by sending anonymous usage data and error reports. No code, prompts, or - personal information is ever sent. See our{" "} - - privacy policy - {" "} - for more details. -

+}: SettingsFooterProps) => { + const { t } = useAppTranslation() + + return ( +
+

+ , + redditLink: , + discordLink: , + }} + /> +

+
+
+

Roo Code

+

v{version}

+
+
+ + +
+
+
+
+ { + const checked = e.target.checked === true + setTelemetrySetting(checked ? "enabled" : "disabled") + }}> + {t("settings:footer.telemetry.label")} + +

+ {t("settings:footer.telemetry.description")} +

+
+
+
+

{t("settings:footer.reset.description")}

+ vscode.postMessage({ type: "resetState" })} + appearance="secondary" + className="shrink-0"> + + {t("settings:footer.reset.button")} +
-
-

Reset all global state and secret storage in the extension.

- vscode.postMessage({ type: "resetState" })} - appearance="secondary" - className="shrink-0"> - - Reset - -
-
-) + ) +} diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 1cdc1d00fa..509ecd740f 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -1,4 +1,5 @@ import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react" +import { useAppTranslation } from "@/i18n/TranslationContext" import { Button as VSCodeButton } from "vscrui" import { CheckCheck, @@ -55,6 +56,7 @@ type SettingsViewProps = { } const SettingsView = forwardRef(({ onDone }, ref) => { + const { t } = useAppTranslation() const extensionState = useExtensionState() const { currentApiConfigName, listApiConfigMeta, uriScheme, version } = extensionState @@ -70,6 +72,7 @@ const SettingsView = forwardRef(({ onDone }, const { alwaysAllowReadOnly, allowedCommands, + language, alwaysAllowBrowser, alwaysAllowExecute, alwaysAllowMcp, @@ -173,6 +176,7 @@ const SettingsView = forwardRef(({ onDone }, const handleSubmit = () => { if (isSettingValid) { + vscode.postMessage({ type: "language", text: language }) vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly }) vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite }) vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute }) @@ -251,7 +255,16 @@ const SettingsView = forwardRef(({ onDone }, { id: "advanced", icon: Cog, ref: advancedRef }, { id: "experimental", icon: FlaskConical, ref: experimentalRef }, ], - [providersRef, autoApproveRef, browserRef, checkpointRef, notificationsRef, advancedRef, experimentalRef], + [ + providersRef, + autoApproveRef, + browserRef, + checkpointRef, + notificationsRef, + contextRef, + advancedRef, + experimentalRef, + ], ) const handleScroll = useCallback((e: React.UIEvent) => { @@ -286,7 +299,7 @@ const SettingsView = forwardRef(({ onDone },
-

Settings

+

{t("settings:header.title")}

{sections.map(({ id, icon: Icon, ref }) => (
@@ -322,7 +342,7 @@ const SettingsView = forwardRef(({ onDone },
-
Providers
+
{t("settings:sections.providers")}
@@ -441,6 +461,8 @@ const SettingsView = forwardRef(({ onDone }, version={version} telemetrySetting={telemetrySetting} setTelemetrySetting={setTelemetrySetting} + language={language || "en"} + setCachedStateField={setCachedStateField} /> @@ -449,14 +471,18 @@ const SettingsView = forwardRef(({ onDone }, - Unsaved Changes + {t("settings:unsavedChangesDialog.title")} - Do you want to discard changes and continue? + + {t("settings:unsavedChangesDialog.description")} + - onConfirmDialogResult(false)}>Cancel + onConfirmDialogResult(false)}> + {t("settings:unsavedChangesDialog.cancelButton")} + onConfirmDialogResult(true)}> - Discard changes + {t("settings:unsavedChangesDialog.discardButton")} diff --git a/webview-ui/src/components/settings/TemperatureControl.tsx b/webview-ui/src/components/settings/TemperatureControl.tsx index 7502c3d1f3..816ec7c8f5 100644 --- a/webview-ui/src/components/settings/TemperatureControl.tsx +++ b/webview-ui/src/components/settings/TemperatureControl.tsx @@ -1,5 +1,6 @@ import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" import { useEffect, useState } from "react" +import { useAppTranslation } from "@/i18n/TranslationContext" import { useDebounce } from "react-use" interface TemperatureControlProps { @@ -9,6 +10,7 @@ interface TemperatureControlProps { } export const TemperatureControl = ({ value, onChange, maxValue = 1 }: TemperatureControlProps) => { + const { t } = useAppTranslation() const [isCustomTemperature, setIsCustomTemperature] = useState(value !== undefined) const [inputValue, setInputValue] = useState(value) useDebounce(() => onChange(inputValue), 50, [onChange, inputValue]) @@ -33,11 +35,9 @@ export const TemperatureControl = ({ value, onChange, maxValue = 1 }: Temperatur setInputValue(value ?? 0) // Use the value from apiConfiguration, if set } }}> - Use custom temperature + {t("settings:temperature.useCustom")} -
- Controls randomness in the model's responses. -
+
{t("settings:temperature.description")}
{isCustomTemperature && ( @@ -60,7 +60,7 @@ export const TemperatureControl = ({ value, onChange, maxValue = 1 }: Temperatur {inputValue}

- Higher values make output more random, lower values make it more deterministic. + {t("settings:temperature.rangeDescription")}

)} diff --git a/webview-ui/src/components/settings/__tests__/ApiConfigManager.test.tsx b/webview-ui/src/components/settings/__tests__/ApiConfigManager.test.tsx index 92a6a1cd03..81431db2f7 100644 --- a/webview-ui/src/components/settings/__tests__/ApiConfigManager.test.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiConfigManager.test.tsx @@ -3,17 +3,18 @@ import ApiConfigManager from "../ApiConfigManager" // Mock VSCode components jest.mock("@vscode/webview-ui-toolkit/react", () => ({ - VSCodeButton: ({ children, onClick, title, disabled }: any) => ( - ), - VSCodeTextField: ({ value, onInput, placeholder, onKeyDown }: any) => ( + VSCodeTextField: ({ value, onInput, placeholder, onKeyDown, "data-testid": dataTestId }: any) => ( onInput(e)} placeholder={placeholder} onKeyDown={onKeyDown} + data-testid={dataTestId} ref={undefined} // Explicitly set ref to undefined to avoid warning /> ), @@ -44,6 +45,24 @@ jest.mock("@/components/ui/dialog", () => ({ DialogTitle: ({ children }: any) =>
{children}
, })) +// Mock UI components +jest.mock("@/components/ui", () => ({ + Button: ({ children, onClick, disabled, variant, "data-testid": dataTestId }: any) => ( + + ), + Input: ({ value, onInput, placeholder, onKeyDown, "data-testid": dataTestId }: any) => ( + onInput(e)} + placeholder={placeholder} + onKeyDown={onKeyDown} + data-testid={dataTestId} + /> + ), +})) + describe("ApiConfigManager", () => { const mockOnSelectConfig = jest.fn() const mockOnDeleteConfig = jest.fn() @@ -72,26 +91,26 @@ describe("ApiConfigManager", () => { it("opens new profile dialog when clicking add button", () => { render() - const addButton = screen.getByTitle("Add profile") + const addButton = screen.getByTestId("add-profile-button") fireEvent.click(addButton) expect(screen.getByTestId("dialog")).toBeVisible() - expect(screen.getByText("New Configuration Profile")).toBeInTheDocument() + expect(screen.getByTestId("dialog-title")).toHaveTextContent("settings:providers.newProfile") }) it("creates new profile with entered name", () => { render() // Open dialog - const addButton = screen.getByTitle("Add profile") + const addButton = screen.getByTestId("add-profile-button") fireEvent.click(addButton) // Enter new profile name - const input = screen.getByPlaceholderText("Enter profile name") + const input = screen.getByTestId("new-profile-input") fireEvent.input(input, { target: { value: "New Profile" } }) // Click create button - const createButton = screen.getByText("Create Profile") + const createButton = screen.getByText("settings:providers.createProfile") fireEvent.click(createButton) expect(mockOnUpsertConfig).toHaveBeenCalledWith("New Profile") @@ -101,21 +120,21 @@ describe("ApiConfigManager", () => { render() // Open dialog - const addButton = screen.getByTitle("Add profile") + const addButton = screen.getByTestId("add-profile-button") fireEvent.click(addButton) // Enter existing profile name - const input = screen.getByPlaceholderText("Enter profile name") + const input = screen.getByTestId("new-profile-input") fireEvent.input(input, { target: { value: "Default Config" } }) // Click create button to trigger validation - const createButton = screen.getByText("Create Profile") + const createButton = screen.getByText("settings:providers.createProfile") fireEvent.click(createButton) // Verify error message const dialogContent = getDialogContent() const errorMessage = within(dialogContent).getByTestId("error-message") - expect(errorMessage).toHaveTextContent("A profile with this name already exists") + expect(errorMessage).toHaveTextContent("settings:providers.nameExists") expect(mockOnUpsertConfig).not.toHaveBeenCalled() }) @@ -123,15 +142,15 @@ describe("ApiConfigManager", () => { render() // Open dialog - const addButton = screen.getByTitle("Add profile") + const addButton = screen.getByTestId("add-profile-button") fireEvent.click(addButton) // Enter empty name - const input = screen.getByPlaceholderText("Enter profile name") + const input = screen.getByTestId("new-profile-input") fireEvent.input(input, { target: { value: " " } }) // Verify create button is disabled - const createButton = screen.getByText("Create Profile") + const createButton = screen.getByText("settings:providers.createProfile") expect(createButton).toBeDisabled() expect(mockOnUpsertConfig).not.toHaveBeenCalled() }) @@ -140,7 +159,7 @@ describe("ApiConfigManager", () => { render() // Start rename - const renameButton = screen.getByTitle("Rename profile") + const renameButton = screen.getByTestId("rename-profile-button") fireEvent.click(renameButton) // Find input and enter new name @@ -148,7 +167,7 @@ describe("ApiConfigManager", () => { fireEvent.input(input, { target: { value: "New Name" } }) // Save - const saveButton = screen.getByTitle("Save") + const saveButton = screen.getByTestId("save-rename-button") fireEvent.click(saveButton) expect(mockOnRenameConfig).toHaveBeenCalledWith("Default Config", "New Name") @@ -158,7 +177,7 @@ describe("ApiConfigManager", () => { render() // Start rename - const renameButton = screen.getByTitle("Rename profile") + const renameButton = screen.getByTestId("rename-profile-button") fireEvent.click(renameButton) // Find input and enter existing name @@ -166,13 +185,13 @@ describe("ApiConfigManager", () => { fireEvent.input(input, { target: { value: "Another Config" } }) // Save to trigger validation - const saveButton = screen.getByTitle("Save") + const saveButton = screen.getByTestId("save-rename-button") fireEvent.click(saveButton) // Verify error message const renameForm = getRenameForm() const errorMessage = within(renameForm).getByTestId("error-message") - expect(errorMessage).toHaveTextContent("A profile with this name already exists") + expect(errorMessage).toHaveTextContent("settings:providers.nameExists") expect(mockOnRenameConfig).not.toHaveBeenCalled() }) @@ -180,7 +199,7 @@ describe("ApiConfigManager", () => { render() // Start rename - const renameButton = screen.getByTitle("Rename profile") + const renameButton = screen.getByTestId("rename-profile-button") fireEvent.click(renameButton) // Find input and enter empty name @@ -188,7 +207,7 @@ describe("ApiConfigManager", () => { fireEvent.input(input, { target: { value: " " } }) // Verify save button is disabled - const saveButton = screen.getByTitle("Save") + const saveButton = screen.getByTestId("save-rename-button") expect(saveButton).toBeDisabled() expect(mockOnRenameConfig).not.toHaveBeenCalled() }) @@ -205,7 +224,7 @@ describe("ApiConfigManager", () => { it("allows deleting the current config when not the only one", () => { render() - const deleteButton = screen.getByTitle("Delete profile") + const deleteButton = screen.getByTestId("delete-profile-button") expect(deleteButton).not.toBeDisabled() fireEvent.click(deleteButton) @@ -215,7 +234,7 @@ describe("ApiConfigManager", () => { it("disables delete button when only one config exists", () => { render() - const deleteButton = screen.getByTitle("Cannot delete the only profile") + const deleteButton = screen.getByTestId("delete-profile-button") expect(deleteButton).toHaveAttribute("disabled") }) @@ -223,7 +242,7 @@ describe("ApiConfigManager", () => { render() // Start rename - const renameButton = screen.getByTitle("Rename profile") + const renameButton = screen.getByTestId("rename-profile-button") fireEvent.click(renameButton) // Find input and enter new name @@ -231,7 +250,7 @@ describe("ApiConfigManager", () => { fireEvent.input(input, { target: { value: "New Name" } }) // Cancel - const cancelButton = screen.getByTitle("Cancel") + const cancelButton = screen.getByTestId("cancel-rename-button") fireEvent.click(cancelButton) // Verify rename was not called @@ -245,10 +264,10 @@ describe("ApiConfigManager", () => { render() // Open dialog - const addButton = screen.getByTitle("Add profile") + const addButton = screen.getByTestId("add-profile-button") fireEvent.click(addButton) - const input = screen.getByPlaceholderText("Enter profile name") + const input = screen.getByTestId("new-profile-input") // Test Enter key fireEvent.input(input, { target: { value: "New Profile" } }) @@ -264,7 +283,7 @@ describe("ApiConfigManager", () => { render() // Start rename - const renameButton = screen.getByTitle("Rename profile") + const renameButton = screen.getByTestId("rename-profile-button") fireEvent.click(renameButton) const input = screen.getByDisplayValue("Default Config") diff --git a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.test.tsx b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.test.tsx index 25e7fa3e50..a9708d260e 100644 --- a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.test.tsx +++ b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.test.tsx @@ -18,19 +18,23 @@ describe("ContextManagementSettings", () => { render() // Terminal output limit - expect(screen.getByText("Terminal output limit")).toBeInTheDocument() - expect(screen.getByTestId("terminal-output-limit-slider")).toHaveValue("500") + const terminalSlider = screen.getByTestId("terminal-output-limit-slider") + expect(terminalSlider).toBeInTheDocument() + expect(terminalSlider).toHaveValue("500") // Open tabs context limit - expect(screen.getByText("Open tabs context limit")).toBeInTheDocument() - expect(screen.getByTestId("open-tabs-limit-slider")).toHaveValue("20") + const openTabsSlider = screen.getByTestId("open-tabs-limit-slider") + expect(openTabsSlider).toBeInTheDocument() + expect(openTabsSlider).toHaveValue("20") // Workspace files limit - expect(screen.getByText("Workspace files context limit")).toBeInTheDocument() - expect(screen.getByTestId("workspace-files-limit-slider")).toHaveValue("200") + const workspaceFilesSlider = screen.getByTestId("workspace-files-limit-slider") + expect(workspaceFilesSlider).toBeInTheDocument() + expect(workspaceFilesSlider).toHaveValue("200") // Show .rooignore'd files - expect(screen.getByText("Show .rooignore'd files in lists and searches")).toBeInTheDocument() + const showRooIgnoredFilesCheckbox = screen.getByTestId("show-rooignored-files-checkbox") + expect(showRooIgnoredFilesCheckbox).toBeInTheDocument() expect(screen.getByTestId("show-rooignored-files-checkbox")).not.toBeChecked() }) diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx index 5e5defec59..95b3bb8fa5 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx @@ -40,33 +40,39 @@ jest.mock("../ApiConfigManager", () => ({ // Mock VSCode components jest.mock("@vscode/webview-ui-toolkit/react", () => ({ - VSCodeButton: ({ children, onClick, appearance }: any) => + VSCodeButton: ({ children, onClick, appearance, "data-testid": dataTestId }: any) => appearance === "icon" ? ( - ) : ( - ), - VSCodeCheckbox: ({ children, onChange, checked }: any) => ( + VSCodeCheckbox: ({ children, onChange, checked, "data-testid": dataTestId }: any) => ( ), - VSCodeTextField: ({ value, onInput, placeholder }: any) => ( + VSCodeTextField: ({ value, onInput, placeholder, "data-testid": dataTestId }: any) => ( onInput({ target: { value: e.target.value } })} placeholder={placeholder} + data-testid={dataTestId} /> ), VSCodeTextArea: () =>