Skip to content

Commit 57f91fd

Browse files
authored
Merge pull request #1699 from RooVetGit/i18n_settings
Localize the settings tab
2 parents 85814d9 + db618ce commit 57f91fd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+4378
-397
lines changed

webview-ui/src/components/history/CopyButton.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const CopyButton = ({ itemTask }: CopyButtonProps) => {
2727
size="icon"
2828
title={t("history:copyPrompt")}
2929
onClick={onCopy}
30+
data-testid="copy-prompt-button"
3031
className="opacity-50 hover:opacity-100">
3132
<span className={cn("codicon scale-80", { "codicon-check": isCopied, "codicon-copy": !isCopied })} />
3233
</Button>

webview-ui/src/components/history/HistoryView.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
3939
style={{ width: "100%" }}
4040
placeholder={t("history:searchPlaceholder")}
4141
value={searchQuery}
42+
data-testid="history-search-input"
4243
onInput={(e) => {
4344
const newValue = (e.target as HTMLInputElement)?.value
4445
setSearchQuery(newValue)
@@ -72,13 +73,22 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
7273
value={sortOption}
7374
role="radiogroup"
7475
onChange={(e) => setSortOption((e.target as HTMLInputElement).value as SortOption)}>
75-
<VSCodeRadio value="newest">{t("history:newest")}</VSCodeRadio>
76-
<VSCodeRadio value="oldest">{t("history:oldest")}</VSCodeRadio>
77-
<VSCodeRadio value="mostExpensive">{t("history:mostExpensive")}</VSCodeRadio>
78-
<VSCodeRadio value="mostTokens">{t("history:mostTokens")}</VSCodeRadio>
76+
<VSCodeRadio value="newest" data-testid="radio-newest">
77+
{t("history:newest")}
78+
</VSCodeRadio>
79+
<VSCodeRadio value="oldest" data-testid="radio-oldest">
80+
{t("history:oldest")}
81+
</VSCodeRadio>
82+
<VSCodeRadio value="mostExpensive" data-testid="radio-most-expensive">
83+
{t("history:mostExpensive")}
84+
</VSCodeRadio>
85+
<VSCodeRadio value="mostTokens" data-testid="radio-most-tokens">
86+
{t("history:mostTokens")}
87+
</VSCodeRadio>
7988
<VSCodeRadio
8089
value="mostRelevant"
8190
disabled={!searchQuery}
91+
data-testid="radio-most-relevant"
8292
style={{ opacity: searchQuery ? 1 : 0.5 }}>
8393
{t("history:mostRelevant")}
8494
</VSCodeRadio>
@@ -135,6 +145,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
135145
variant="ghost"
136146
size="sm"
137147
title={t("history:deleteTaskTitle")}
148+
data-testid="delete-task-button"
138149
onClick={(e) => {
139150
e.stopPropagation()
140151

webview-ui/src/components/history/__tests__/HistoryView.test.tsx

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ describe("HistoryView", () => {
7575
render(<HistoryView onDone={onDone} />)
7676

7777
// Get search input and radio group
78-
const searchInput = screen.getByPlaceholderText("Fuzzy search history...")
78+
const searchInput = screen.getByTestId("history-search-input")
7979
const radioGroup = screen.getByRole("radiogroup")
8080

8181
// Type in search
@@ -85,7 +85,7 @@ describe("HistoryView", () => {
8585
jest.advanceTimersByTime(100)
8686

8787
// Check if sort option automatically changes to "Most Relevant"
88-
const mostRelevantRadio = within(radioGroup).getByLabelText("Most Relevant")
88+
const mostRelevantRadio = within(radioGroup).getByTestId("radio-most-relevant")
8989
expect(mostRelevantRadio).not.toBeDisabled()
9090

9191
// Click the radio button
@@ -95,7 +95,7 @@ describe("HistoryView", () => {
9595
jest.advanceTimersByTime(100)
9696

9797
// Verify radio button is checked
98-
const updatedRadio = within(radioGroup).getByRole("radio", { name: "Most Relevant", checked: true })
98+
const updatedRadio = within(radioGroup).getByTestId("radio-most-relevant")
9999
expect(updatedRadio).toBeInTheDocument()
100100
})
101101

@@ -106,21 +106,18 @@ describe("HistoryView", () => {
106106
const radioGroup = screen.getByRole("radiogroup")
107107

108108
// Test changing sort options
109-
const oldestRadio = within(radioGroup).getByLabelText("Oldest")
109+
const oldestRadio = within(radioGroup).getByTestId("radio-oldest")
110110
fireEvent.click(oldestRadio)
111111

112112
// Wait for oldest radio to be checked
113-
const checkedOldestRadio = await within(radioGroup).findByRole("radio", { name: "Oldest", checked: true })
113+
const checkedOldestRadio = within(radioGroup).getByTestId("radio-oldest")
114114
expect(checkedOldestRadio).toBeInTheDocument()
115115

116-
const mostExpensiveRadio = within(radioGroup).getByLabelText("Most Expensive")
116+
const mostExpensiveRadio = within(radioGroup).getByTestId("radio-most-expensive")
117117
fireEvent.click(mostExpensiveRadio)
118118

119119
// Wait for most expensive radio to be checked
120-
const checkedExpensiveRadio = await within(radioGroup).findByRole("radio", {
121-
name: "Most Expensive",
122-
checked: true,
123-
})
120+
const checkedExpensiveRadio = within(radioGroup).getByTestId("radio-most-expensive")
124121
expect(checkedExpensiveRadio).toBeInTheDocument()
125122
})
126123

@@ -148,7 +145,7 @@ describe("HistoryView", () => {
148145
fireEvent.mouseEnter(taskContainer)
149146

150147
// Click delete button to open confirmation dialog
151-
const deleteButton = within(taskContainer).getByTitle("Delete Task (Shift + Click to skip confirmation)")
148+
const deleteButton = within(taskContainer).getByTestId("delete-task-button")
152149
fireEvent.click(deleteButton)
153150

154151
// Verify dialog is shown
@@ -175,7 +172,7 @@ describe("HistoryView", () => {
175172
fireEvent.mouseEnter(taskContainer)
176173

177174
// Shift-click delete button
178-
const deleteButton = within(taskContainer).getByTitle("Delete Task (Shift + Click to skip confirmation)")
175+
const deleteButton = within(taskContainer).getByTestId("delete-task-button")
179176
fireEvent.click(deleteButton, { shiftKey: true })
180177

181178
// Verify no dialog is shown
@@ -203,7 +200,7 @@ describe("HistoryView", () => {
203200
const taskContainer = screen.getByTestId("virtuoso-item-1")
204201
fireEvent.mouseEnter(taskContainer)
205202

206-
const copyButton = within(taskContainer).getByTitle("Copy Prompt")
203+
const copyButton = within(taskContainer).getByTestId("copy-prompt-button")
207204

208205
// Click the copy button and wait for clipboard operation
209206
await act(async () => {

webview-ui/src/components/settings/AdvancedSettings.tsx

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { HTMLAttributes } from "react"
2+
import { useAppTranslation } from "@/i18n/TranslationContext"
23
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
34
import { Cog } from "lucide-react"
45

@@ -29,19 +30,20 @@ export const AdvancedSettings = ({
2930
className,
3031
...props
3132
}: AdvancedSettingsProps) => {
33+
const { t } = useAppTranslation()
3234
return (
3335
<div className={cn("flex flex-col gap-2", className)} {...props}>
3436
<SectionHeader>
3537
<div className="flex items-center gap-2">
3638
<Cog className="w-4" />
37-
<div>Advanced</div>
39+
<div>{t("settings:sections.advanced")}</div>
3840
</div>
3941
</SectionHeader>
4042

4143
<Section>
4244
<div>
4345
<div className="flex flex-col gap-2">
44-
<span className="font-medium">Rate limit</span>
46+
<span className="font-medium">{t("settings:advanced.rateLimit.label")}</span>
4547
<div className="flex items-center gap-2">
4648
<input
4749
type="range"
@@ -55,7 +57,9 @@ export const AdvancedSettings = ({
5557
<span style={{ ...sliderLabelStyle }}>{rateLimitSeconds}s</span>
5658
</div>
5759
</div>
58-
<p className="text-vscode-descriptionForeground text-sm mt-0">Minimum time between API requests.</p>
60+
<p className="text-vscode-descriptionForeground text-sm mt-0">
61+
{t("settings:advanced.rateLimit.description")}
62+
</p>
5963
</div>
6064

6165
<div>
@@ -69,16 +73,15 @@ export const AdvancedSettings = ({
6973
setExperimentEnabled(EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE, false)
7074
}
7175
}}>
72-
<span className="font-medium">Enable editing through diffs</span>
76+
<span className="font-medium">{t("settings:advanced.diff.label")}</span>
7377
</VSCodeCheckbox>
7478
<p className="text-vscode-descriptionForeground text-sm mt-0">
75-
When enabled, Roo will be able to edit files more quickly and will automatically reject
76-
truncated full-file writes. Works best with the latest Claude 3.7 Sonnet model.
79+
{t("settings:advanced.diff.description")}
7780
</p>
7881
{diffEnabled && (
7982
<div className="flex flex-col gap-2 mt-3 mb-2 pl-3 border-l-2 border-vscode-button-background">
8083
<div className="flex flex-col gap-2">
81-
<span className="font-medium">Diff strategy</span>
84+
<span className="font-medium">{t("settings:advanced.diff.strategy.label")}</span>
8285
<select
8386
value={
8487
experiments[EXPERIMENT_IDS.DIFF_STRATEGY]
@@ -101,25 +104,31 @@ export const AdvancedSettings = ({
101104
}
102105
}}
103106
className="p-2 rounded w-full bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border outline-none focus:border-vscode-focusBorder">
104-
<option value="standard">Standard (Single block)</option>
105-
<option value="multiBlock">Experimental: Multi-block diff</option>
106-
<option value="unified">Experimental: Unified diff</option>
107+
<option value="standard">
108+
{t("settings:advanced.diff.strategy.options.standard")}
109+
</option>
110+
<option value="multiBlock">
111+
{t("settings:advanced.diff.strategy.options.multiBlock")}
112+
</option>
113+
<option value="unified">
114+
{t("settings:advanced.diff.strategy.options.unified")}
115+
</option>
107116
</select>
108117
</div>
109118

110119
{/* Description for selected strategy */}
111120
<p className="text-vscode-descriptionForeground text-sm mt-1">
112121
{!experiments[EXPERIMENT_IDS.DIFF_STRATEGY] &&
113122
!experiments[EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE] &&
114-
"Standard diff strategy applies changes to a single code block at a time."}
123+
t("settings:advanced.diff.strategy.descriptions.standard")}
115124
{experiments[EXPERIMENT_IDS.DIFF_STRATEGY] &&
116-
"Unified diff strategy takes multiple approaches to applying diffs and chooses the best approach."}
125+
t("settings:advanced.diff.strategy.descriptions.unified")}
117126
{experiments[EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE] &&
118-
"Multi-block diff strategy allows updating multiple code blocks in a file in one request."}
127+
t("settings:advanced.diff.strategy.descriptions.multiBlock")}
119128
</p>
120129

121130
{/* Match precision slider */}
122-
<span className="font-medium mt-3">Match precision</span>
131+
<span className="font-medium mt-3">{t("settings:advanced.diff.matchPrecision.label")}</span>
123132
<div className="flex items-center gap-2">
124133
<input
125134
type="range"
@@ -137,9 +146,7 @@ export const AdvancedSettings = ({
137146
</span>
138147
</div>
139148
<p className="text-vscode-descriptionForeground text-sm mt-0">
140-
This slider controls how precisely code sections must match when applying diffs. Lower
141-
values allow more flexible matching but increase the risk of incorrect replacements. Use
142-
values below 100% with extreme caution.
149+
{t("settings:advanced.diff.matchPrecision.description")}
143150
</p>
144151
</div>
145152
)}

webview-ui/src/components/settings/ApiConfigManager.tsx

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
22
import { memo, useEffect, useRef, useState } from "react"
3+
import { useAppTranslation } from "@/i18n/TranslationContext"
34
import { ApiConfigMeta } from "../../../../src/shared/ExtensionMessage"
45
import { Dropdown } from "vscrui"
56
import type { DropdownOption } from "vscrui"
@@ -23,6 +24,7 @@ const ApiConfigManager = ({
2324
onRenameConfig,
2425
onUpsertConfig,
2526
}: ApiConfigManagerProps) => {
27+
const { t } = useAppTranslation()
2628
const [isRenaming, setIsRenaming] = useState(false)
2729
const [isCreating, setIsCreating] = useState(false)
2830
const [inputValue, setInputValue] = useState("")
@@ -33,18 +35,18 @@ const ApiConfigManager = ({
3335

3436
const validateName = (name: string, isNewProfile: boolean): string | null => {
3537
const trimmed = name.trim()
36-
if (!trimmed) return "Name cannot be empty"
38+
if (!trimmed) return t("settings:providers.nameEmpty")
3739

3840
const nameExists = listApiConfigMeta?.some((config) => config.name.toLowerCase() === trimmed.toLowerCase())
3941

4042
// For new profiles, any existing name is invalid
4143
if (isNewProfile && nameExists) {
42-
return "A profile with this name already exists"
44+
return t("settings:providers.nameExists")
4345
}
4446

4547
// For rename, only block if trying to rename to a different existing profile
4648
if (!isNewProfile && nameExists && trimmed.toLowerCase() !== currentApiConfigName?.toLowerCase()) {
47-
return "A profile with this name already exists"
49+
return t("settings:providers.nameExists")
4850
}
4951

5052
return null
@@ -144,7 +146,7 @@ const ApiConfigManager = ({
144146
return (
145147
<div className="flex flex-col gap-1">
146148
<label htmlFor="config-profile">
147-
<span className="font-medium">Configuration Profile</span>
149+
<span className="font-medium">{t("settings:providers.configProfile")}</span>
148150
</label>
149151

150152
{isRenaming ? (
@@ -160,7 +162,7 @@ const ApiConfigManager = ({
160162
setInputValue(target.target.value)
161163
setError(null)
162164
}}
163-
placeholder="Enter new name"
165+
placeholder={t("settings:providers.enterNewName")}
164166
style={{ flexGrow: 1 }}
165167
onKeyDown={(e: unknown) => {
166168
const event = e as { key: string }
@@ -175,7 +177,8 @@ const ApiConfigManager = ({
175177
appearance="icon"
176178
disabled={!inputValue.trim()}
177179
onClick={handleSave}
178-
title="Save"
180+
title={t("settings:common.save")}
181+
data-testid="save-rename-button"
179182
style={{
180183
padding: 0,
181184
margin: 0,
@@ -188,7 +191,8 @@ const ApiConfigManager = ({
188191
<VSCodeButton
189192
appearance="icon"
190193
onClick={handleCancel}
191-
title="Cancel"
194+
title={t("settings:common.cancel")}
195+
data-testid="cancel-rename-button"
192196
style={{
193197
padding: 0,
194198
margin: 0,
@@ -224,7 +228,8 @@ const ApiConfigManager = ({
224228
<VSCodeButton
225229
appearance="icon"
226230
onClick={handleAdd}
227-
title="Add profile"
231+
title={t("settings:providers.addProfile")}
232+
data-testid="add-profile-button"
228233
style={{
229234
padding: 0,
230235
margin: 0,
@@ -239,7 +244,8 @@ const ApiConfigManager = ({
239244
<VSCodeButton
240245
appearance="icon"
241246
onClick={handleStartRename}
242-
title="Rename profile"
247+
title={t("settings:providers.renameProfile")}
248+
data-testid="rename-profile-button"
243249
style={{
244250
padding: 0,
245251
margin: 0,
@@ -252,7 +258,12 @@ const ApiConfigManager = ({
252258
<VSCodeButton
253259
appearance="icon"
254260
onClick={handleDelete}
255-
title={isOnlyProfile ? "Cannot delete the only profile" : "Delete profile"}
261+
title={
262+
isOnlyProfile
263+
? t("settings:providers.cannotDeleteOnlyProfile")
264+
: t("settings:providers.deleteProfile")
265+
}
266+
data-testid="delete-profile-button"
256267
disabled={isOnlyProfile}
257268
style={{
258269
padding: 0,
@@ -272,7 +283,7 @@ const ApiConfigManager = ({
272283
margin: "5px 0 12px",
273284
color: "var(--vscode-descriptionForeground)",
274285
}}>
275-
Save different API configurations to quickly switch between providers and settings.
286+
{t("settings:providers.description")}
276287
</p>
277288
</>
278289
)}
@@ -290,7 +301,7 @@ const ApiConfigManager = ({
290301
}}
291302
aria-labelledby="new-profile-title">
292303
<DialogContent className="p-4 max-w-sm">
293-
<DialogTitle>New Configuration Profile</DialogTitle>
304+
<DialogTitle>{t("settings:providers.newProfile")}</DialogTitle>
294305
<Input
295306
ref={newProfileInputRef}
296307
value={newProfileName}
@@ -299,7 +310,8 @@ const ApiConfigManager = ({
299310
setNewProfileName(target.target.value)
300311
setError(null)
301312
}}
302-
placeholder="Enter profile name"
313+
placeholder={t("settings:providers.enterProfileName")}
314+
data-testid="new-profile-input"
303315
style={{ width: "100%" }}
304316
onKeyDown={(e: unknown) => {
305317
const event = e as { key: string }
@@ -316,11 +328,15 @@ const ApiConfigManager = ({
316328
</p>
317329
)}
318330
<div className="flex justify-end gap-2 mt-4">
319-
<Button variant="secondary" onClick={resetCreateState}>
320-
Cancel
331+
<Button variant="secondary" onClick={resetCreateState} data-testid="cancel-new-profile-button">
332+
{t("settings:common.cancel")}
321333
</Button>
322-
<Button variant="default" disabled={!newProfileName.trim()} onClick={handleNewProfileSave}>
323-
Create Profile
334+
<Button
335+
variant="default"
336+
disabled={!newProfileName.trim()}
337+
onClick={handleNewProfileSave}
338+
data-testid="create-profile-button">
339+
{t("settings:providers.createProfile")}
324340
</Button>
325341
</div>
326342
</DialogContent>

0 commit comments

Comments
 (0)