From 2df634c836ac186a41417345e1da5bf8dbcdcc19 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Sun, 16 Mar 2025 11:20:03 -0400 Subject: [PATCH 01/12] Add script to find missing translations --- .roomodes | 3 +- knip.json | 3 +- scripts/find-missing-translations.js | 218 +++++++++++++++++++++++++++ 3 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 scripts/find-missing-translations.js diff --git a/.roomodes b/.roomodes index 8dc2492f64..10625f66f5 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\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: ar, ca, cs, de, en, es, fr, hi, hu, it, ja, ko, pl, pt, pt-BR, ru, tr, zh-CN, zh-TW", "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() From db618ce377fcaa65fe03c267da5b3fc7643951eb Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Sun, 16 Mar 2025 10:05:16 -0400 Subject: [PATCH 02/12] Localize the settings tab --- .../src/components/history/CopyButton.tsx | 1 + .../src/components/history/HistoryView.tsx | 19 +- .../history/__tests__/HistoryView.test.tsx | 23 +- .../components/settings/AdvancedSettings.tsx | 41 ++-- .../components/settings/ApiConfigManager.tsx | 50 +++-- .../src/components/settings/ApiOptions.tsx | 36 +-- .../settings/AutoApproveSettings.tsx | 87 +++++--- .../components/settings/BrowserSettings.tsx | 49 +++-- .../settings/CheckpointSettings.tsx | 9 +- .../settings/ContextManagementSettings.tsx | 26 +-- .../settings/ExperimentalFeature.tsx | 25 ++- .../settings/ExperimentalSettings.tsx | 4 +- .../src/components/settings/ModelInfoView.tsx | 38 ++-- .../settings/NotificationSettings.tsx | 16 +- .../src/components/settings/SectionHeader.tsx | 24 +- .../components/settings/SettingsFooter.tsx | 93 ++++---- .../src/components/settings/SettingsView.tsx | 35 ++- .../settings/TemperatureControl.tsx | 10 +- .../__tests__/ApiConfigManager.test.tsx | 77 ++++--- .../ContextManagementSettings.test.tsx | 18 +- .../settings/__tests__/SettingsView.test.tsx | 90 ++++---- .../src/i18n/__mocks__/TranslationContext.tsx | 105 +++++---- webview-ui/src/i18n/locales/ar/settings.json | 205 ++++++++++++++++++ webview-ui/src/i18n/locales/ca/settings.json | 205 ++++++++++++++++++ webview-ui/src/i18n/locales/cs/settings.json | 205 ++++++++++++++++++ webview-ui/src/i18n/locales/de/settings.json | 205 ++++++++++++++++++ webview-ui/src/i18n/locales/en/settings.json | 205 ++++++++++++++++++ webview-ui/src/i18n/locales/es/settings.json | 205 ++++++++++++++++++ webview-ui/src/i18n/locales/fr/settings.json | 205 ++++++++++++++++++ webview-ui/src/i18n/locales/hi/settings.json | 205 ++++++++++++++++++ webview-ui/src/i18n/locales/hu/settings.json | 205 ++++++++++++++++++ webview-ui/src/i18n/locales/it/settings.json | 205 ++++++++++++++++++ webview-ui/src/i18n/locales/ja/settings.json | 205 ++++++++++++++++++ webview-ui/src/i18n/locales/ko/settings.json | 205 ++++++++++++++++++ webview-ui/src/i18n/locales/pl/settings.json | 205 ++++++++++++++++++ .../src/i18n/locales/pt-BR/settings.json | 205 ++++++++++++++++++ webview-ui/src/i18n/locales/pt/settings.json | 205 ++++++++++++++++++ webview-ui/src/i18n/locales/ru/settings.json | 205 ++++++++++++++++++ webview-ui/src/i18n/locales/tr/settings.json | 205 ++++++++++++++++++ .../src/i18n/locales/zh-CN/settings.json | 205 ++++++++++++++++++ .../src/i18n/locales/zh-TW/settings.json | 205 ++++++++++++++++++ 41 files changed, 4376 insertions(+), 395 deletions(-) create mode 100644 webview-ui/src/i18n/locales/ar/settings.json create mode 100644 webview-ui/src/i18n/locales/ca/settings.json create mode 100644 webview-ui/src/i18n/locales/cs/settings.json create mode 100644 webview-ui/src/i18n/locales/de/settings.json create mode 100644 webview-ui/src/i18n/locales/en/settings.json create mode 100644 webview-ui/src/i18n/locales/es/settings.json create mode 100644 webview-ui/src/i18n/locales/fr/settings.json create mode 100644 webview-ui/src/i18n/locales/hi/settings.json create mode 100644 webview-ui/src/i18n/locales/hu/settings.json create mode 100644 webview-ui/src/i18n/locales/it/settings.json create mode 100644 webview-ui/src/i18n/locales/ja/settings.json create mode 100644 webview-ui/src/i18n/locales/ko/settings.json create mode 100644 webview-ui/src/i18n/locales/pl/settings.json create mode 100644 webview-ui/src/i18n/locales/pt-BR/settings.json create mode 100644 webview-ui/src/i18n/locales/pt/settings.json create mode 100644 webview-ui/src/i18n/locales/ru/settings.json create mode 100644 webview-ui/src/i18n/locales/tr/settings.json create mode 100644 webview-ui/src/i18n/locales/zh-CN/settings.json create mode 100644 webview-ui/src/i18n/locales/zh-TW/settings.json 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/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..93c1703f5a 100644 --- a/webview-ui/src/components/settings/ExperimentalFeature.tsx +++ b/webview-ui/src/components/settings/ExperimentalFeature.tsx @@ -1,4 +1,5 @@ import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" +import { useAppTranslation } from "@/i18n/TranslationContext" interface ExperimentalFeatureProps { name: string @@ -7,14 +8,18 @@ interface ExperimentalFeatureProps { onChange: (value: boolean) => void } -export const ExperimentalFeature = ({ name, description, enabled, onChange }: ExperimentalFeatureProps) => ( -
-
- ⚠️ - onChange(e.target.checked)}> - {name} - +export const ExperimentalFeature = ({ name, description, enabled, onChange }: ExperimentalFeatureProps) => { + const { t } = useAppTranslation() + + return ( +
+
+ {t("settings:experimental.warning")} + onChange(e.target.checked)}> + {name} + +
+

{description}

-

{description}

-
-) + ) +} diff --git a/webview-ui/src/components/settings/ExperimentalSettings.tsx b/webview-ui/src/components/settings/ExperimentalSettings.tsx index bbdfe47e89..f0d011dab4 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")}
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/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..4d430097f3 100644 --- a/webview-ui/src/components/settings/SettingsFooter.tsx +++ b/webview-ui/src/components/settings/SettingsFooter.tsx @@ -1,6 +1,7 @@ import { HTMLAttributes } from "react" +import { useAppTranslation } from "@/i18n/TranslationContext" -import { VSCodeButton, VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react" +import { VSCodeButton, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" import { vscode } from "@/utils/vscode" import { cn } from "@/lib/utils" @@ -18,56 +19,44 @@ export const SettingsFooter = ({ setTelemetrySetting, 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 ( +
+

{t("settings:footer.feedback")}

+

{t("settings:footer.version", { 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..b6fbed748a 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 @@ -286,7 +288,7 @@ const SettingsView = forwardRef(({ onDone },
-

Settings

+

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

{sections.map(({ id, icon: Icon, ref }) => (
@@ -322,7 +331,7 @@ const SettingsView = forwardRef(({ onDone },
-
Providers
+
{t("settings:sections.providers")}
@@ -449,14 +458,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: () =>