Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 05ae911

Browse files
feat(i18n): translate Ollama settings page
1 parent 3fc92d4 commit 05ae911

21 files changed

Lines changed: 1102 additions & 83 deletions

webview-ui/src/components/settings/providers/Ollama.tsx

Lines changed: 145 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -67,52 +67,135 @@ export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaPro
6767
[setApiConfigurationField],
6868
)
6969

70-
const onMessage = useCallback((event: MessageEvent) => {
71-
const message: ExtensionMessage = event.data
72-
73-
switch (message.type) {
74-
case "ollamaModels":
75-
{
76-
const newModels = message.ollamaModels ?? {}
77-
setOllamaModels(newModels)
70+
// Helper function to translate backend messages
71+
const translateMessage = useCallback(
72+
(msg: string): string => {
73+
if (!msg) return msg
74+
75+
// Successfully connected to Ollama at {url}
76+
const successMatch = msg.match(/^Successfully connected to Ollama at (.+)$/)
77+
if (successMatch) {
78+
return t("settings:providers.ollama.messages.connectionSuccess", { baseUrl: successMatch[1] })
79+
}
80+
81+
// Invalid URL: {url}
82+
const invalidUrlMatch = msg.match(/^Invalid URL: (.+)$/)
83+
if (invalidUrlMatch) {
84+
return t("settings:providers.ollama.messages.connectionInvalidUrl", { baseUrl: invalidUrlMatch[1] })
85+
}
86+
87+
// Cannot connect to Ollama at {url}. Make sure Ollama is running.
88+
const refusedMatch = msg.match(/^Cannot connect to Ollama at (.+)\. Make sure Ollama is running\.$/)
89+
if (refusedMatch) {
90+
return t("settings:providers.ollama.messages.connectionRefused", { baseUrl: refusedMatch[1] })
91+
}
92+
93+
// Connection to Ollama timed out. Check if the URL is correct and Ollama is accessible.
94+
if (msg.includes("Connection to Ollama timed out")) {
95+
return t("settings:providers.ollama.messages.connectionTimeout")
96+
}
97+
98+
// Network error connecting to Ollama. Check your network connection.
99+
if (msg.includes("Network error connecting to Ollama")) {
100+
return t("settings:providers.ollama.messages.connectionNetworkError")
101+
}
102+
103+
// Ollama returned error: {status} {statusText}
104+
const httpErrorMatch = msg.match(/^Ollama returned error: (\d+) (.+)$/)
105+
if (httpErrorMatch) {
106+
return t("settings:providers.ollama.messages.connectionHttpError", {
107+
status: httpErrorMatch[1],
108+
statusText: httpErrorMatch[2],
109+
})
110+
}
111+
112+
// Failed to connect: {error}
113+
const failedMatch = msg.match(/^Failed to connect: (.+)$/)
114+
if (failedMatch) {
115+
return t("settings:providers.ollama.messages.connectionFailed", { error: failedMatch[1] })
116+
}
117+
118+
// Error testing connection: {error}
119+
const testErrorMatch = msg.match(/^Error testing connection: (.+)$/)
120+
if (testErrorMatch) {
121+
return t("settings:providers.ollama.messages.connectionTestError", { error: testErrorMatch[1] })
122+
}
123+
124+
// Found {count} model(s) with tools support ({total} total)
125+
const refreshSuccessMatch = msg.match(/^Found (\d+) model\(s\) with tools support \((\d+) total\)$/)
126+
if (refreshSuccessMatch) {
127+
return t("settings:providers.ollama.messages.refreshSuccess", {
128+
count: refreshSuccessMatch[1],
129+
total: refreshSuccessMatch[2],
130+
})
131+
}
132+
133+
// No models found. Make sure Ollama is running and has models installed.
134+
if (msg === "No models found. Make sure Ollama is running and has models installed.") {
135+
return t("settings:providers.ollama.messages.refreshNoModels")
136+
}
137+
138+
// Failed to refresh models: {error}
139+
const refreshFailedMatch = msg.match(/^Failed to refresh models: (.+)$/)
140+
if (refreshFailedMatch) {
141+
return t("settings:providers.ollama.messages.refreshFailed", { error: refreshFailedMatch[1] })
142+
}
143+
144+
// Unknown message - return as is
145+
return msg
146+
},
147+
[t],
148+
)
149+
150+
const onMessage = useCallback(
151+
(event: MessageEvent) => {
152+
const message: ExtensionMessage = event.data
153+
154+
switch (message.type) {
155+
case "ollamaModels":
156+
{
157+
const newModels = message.ollamaModels ?? {}
158+
setOllamaModels(newModels)
159+
if (message.ollamaModelsWithTools) {
160+
setModelsWithTools(message.ollamaModelsWithTools)
161+
}
162+
setModelsWithoutTools(message.modelsWithoutTools ?? [])
163+
}
164+
break
165+
case "ollamaConnectionTestResult":
166+
setTestResult({
167+
success: message.success ?? false,
168+
message: translateMessage(message.message ?? "Unknown error"),
169+
durationMs: message.durationMs,
170+
})
171+
setTestingConnection(false)
172+
if (testResultTimerRef.current) {
173+
clearTimeout(testResultTimerRef.current)
174+
}
175+
testResultTimerRef.current = setTimeout(() => setTestResult(null), 5000)
176+
break
177+
case "ollamaModelsRefreshResult":
178+
setRefreshResult({
179+
success: message.success ?? false,
180+
message: translateMessage(message.message ?? "Unknown error"),
181+
durationMs: message.durationMs,
182+
})
183+
setRefreshingModels(false)
78184
if (message.ollamaModelsWithTools) {
79185
setModelsWithTools(message.ollamaModelsWithTools)
80186
}
81-
setModelsWithoutTools(message.modelsWithoutTools ?? [])
82-
}
83-
break
84-
case "ollamaConnectionTestResult":
85-
setTestResult({
86-
success: message.success ?? false,
87-
message: message.message ?? "Unknown error",
88-
durationMs: message.durationMs,
89-
})
90-
setTestingConnection(false)
91-
if (testResultTimerRef.current) {
92-
clearTimeout(testResultTimerRef.current)
93-
}
94-
testResultTimerRef.current = setTimeout(() => setTestResult(null), 5000)
95-
break
96-
case "ollamaModelsRefreshResult":
97-
setRefreshResult({
98-
success: message.success ?? false,
99-
message: message.message ?? "Unknown error",
100-
durationMs: message.durationMs,
101-
})
102-
setRefreshingModels(false)
103-
if (message.ollamaModelsWithTools) {
104-
setModelsWithTools(message.ollamaModelsWithTools)
105-
}
106-
if (message.modelsWithoutTools) {
107-
setModelsWithoutTools(message.modelsWithoutTools)
108-
}
109-
if (refreshResultTimerRef.current) {
110-
clearTimeout(refreshResultTimerRef.current)
111-
}
112-
refreshResultTimerRef.current = setTimeout(() => setRefreshResult(null), 5000)
113-
break
114-
}
115-
}, [])
187+
if (message.modelsWithoutTools) {
188+
setModelsWithoutTools(message.modelsWithoutTools)
189+
}
190+
if (refreshResultTimerRef.current) {
191+
clearTimeout(refreshResultTimerRef.current)
192+
}
193+
refreshResultTimerRef.current = setTimeout(() => setRefreshResult(null), 5000)
194+
break
195+
}
196+
},
197+
[translateMessage],
198+
)
116199

117200
const handleTestConnection = useCallback(() => {
118201
setTestingConnection(true)
@@ -201,7 +284,9 @@ export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaPro
201284
}`}>
202285
<div>{testResult.message}</div>
203286
{testResult.durationMs !== undefined && (
204-
<div className="text-xs mt-1 opacity-80">Completed in {testResult.durationMs}ms</div>
287+
<div className="text-xs mt-1 opacity-80">
288+
{t("settings:providers.ollama.completedIn", { duration: testResult.durationMs })}
289+
</div>
205290
)}
206291
</div>
207292
)}
@@ -239,7 +324,9 @@ export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaPro
239324
}`}>
240325
<div>{refreshResult.message}</div>
241326
{refreshResult.durationMs !== undefined && (
242-
<div className="text-xs mt-1 opacity-80">Completed in {refreshResult.durationMs}ms</div>
327+
<div className="text-xs mt-1 opacity-80">
328+
{t("settings:providers.ollama.completedIn", { duration: refreshResult.durationMs })}
329+
</div>
243330
)}
244331
</div>
245332
)}
@@ -274,20 +361,22 @@ export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaPro
274361
<thead>
275362
<tr className="border-b border-vscode-foreground/10">
276363
<th className="text-left py-2 px-3 font-medium text-vscode-foreground">
277-
Model Name
364+
{t("settings:providers.ollama.table.modelName")}
365+
</th>
366+
<th className="text-left py-2 px-3 font-medium text-vscode-foreground">
367+
{t("settings:providers.ollama.table.context")}
278368
</th>
279369
<th className="text-left py-2 px-3 font-medium text-vscode-foreground">
280-
Context
370+
{t("settings:providers.ollama.table.size")}
281371
</th>
282-
<th className="text-left py-2 px-3 font-medium text-vscode-foreground">Size</th>
283372
<th className="text-left py-2 px-3 font-medium text-vscode-foreground">
284-
Quantization
373+
{t("settings:providers.ollama.table.quantization")}
285374
</th>
286375
<th className="text-left py-2 px-3 font-medium text-vscode-foreground">
287-
Family
376+
{t("settings:providers.ollama.table.family")}
288377
</th>
289378
<th className="text-left py-2 px-3 font-medium text-vscode-foreground">
290-
Images
379+
{t("settings:providers.ollama.table.images")}
291380
</th>
292381
</tr>
293382
</thead>
@@ -297,10 +386,10 @@ export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaPro
297386
if (!bytes) return "-"
298387
const gb = bytes / (1024 * 1024 * 1024)
299388
if (gb >= 1) {
300-
return `${gb.toFixed(1)} GB`
389+
return `${gb.toFixed(1)} ${t("settings:providers.ollama.table.sizeFormatting.gb")}`
301390
}
302391
const mb = bytes / (1024 * 1024)
303-
return `${mb.toFixed(1)} MB`
392+
return `${mb.toFixed(1)} ${t("settings:providers.ollama.table.sizeFormatting.mb")}`
304393
}
305394
return (
306395
<tr
@@ -326,7 +415,9 @@ export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaPro
326415
{model.family || "-"}
327416
</td>
328417
<td className="py-2 px-3 text-vscode-descriptionForeground">
329-
{model.supportsImages ? "Yes" : "No"}
418+
{model.supportsImages
419+
? t("settings:providers.ollama.table.yes")
420+
: t("settings:providers.ollama.table.no")}
330421
</td>
331422
</tr>
332423
)

webview-ui/src/i18n/TranslationContext.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,28 @@ export const TranslationProvider: React.FC<{ children: ReactNode }> = ({ childre
3535
// Memoize the translation function to prevent unnecessary re-renders
3636
const translate = useCallback(
3737
(key: string, options?: Record<string, any>) => {
38-
return i18n.t(key, options)
38+
const result = i18n.t(key, options)
39+
// Safeguard: ensure we always return a string, not an object
40+
// This handles cases where plural objects might not be resolved correctly
41+
if (typeof result === "object" && result !== null) {
42+
// Type guard for plural object
43+
const pluralResult = result as Record<string, any>
44+
// If it's a plural object and we have a count, try to resolve it
45+
if (options?.count !== undefined && "one" in pluralResult && "other" in pluralResult) {
46+
const count = options.count
47+
// Use i18next's pluralization logic
48+
if (count === 1 && typeof pluralResult.one === "string") {
49+
return pluralResult.one
50+
}
51+
if (typeof pluralResult.other === "string") {
52+
return pluralResult.other
53+
}
54+
}
55+
// Fallback: return the key if we can't resolve it
56+
console.warn(`Translation key "${key}" returned an object instead of string:`, result)
57+
return key
58+
}
59+
return result as string
3960
},
4061
[i18n],
4162
)

webview-ui/src/i18n/locales/ca/settings.json

Lines changed: 53 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)