diff --git a/webview-ui/src/components/history/CopyButton.tsx b/webview-ui/src/components/history/CopyButton.tsx index 17964cc37e7..c11624ab8c4 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 c82fd0b92a3..64528cb0ea6 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 a88a42ad2ef..4470e5d02dd 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 e25366331e8..e217537ab63 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 59d5d54f782..b7c76689300 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 49756796427..2262df28f71 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 fa3b9138323..6987ba4a03b 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 d5f4c33f9a0..de17de8dd5c 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 e06bfc513af..93c1703f5a8 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 bbdfe47e893..f0d011dab4a 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 8e9564525fc..603c5a5fe5b 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 1fba9dd412e..4466bc339b7 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 709052cea84..ee120639c9a 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 fba7d363c98..4d430097f3c 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 1cdc1d00fa4..b6fbed748a7 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 7502c3d1f3e..816ec7c8f5a 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 92a6a1cd036..81431db2f76 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 25e7fa3e50d..a9708d260eb 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 5e5defec598..95b3bb8fa55 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: () =>