From aed43511d56039d489b8af7ce88ab4cf8acc6957 Mon Sep 17 00:00:00 2001 From: Stefan Meinecke Date: Tue, 28 Apr 2026 20:42:31 +0200 Subject: [PATCH 1/4] fix(dashboard): translate error messages in Windsurf login flows when i18n keys exist All Windsurf login error display paths (single/batch/import) now attempt I18n.t() on error strings before rendering. If the error message is a known i18n key (I18n.t(err) !== err), use the translated version; otherwise fall back to the raw error string. Covers toast notifications, status text, result cards, and batch table cells. --- src/dashboard/index.html | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/dashboard/index.html b/src/dashboard/index.html index 942b256e..a6c37def 100644 --- a/src/dashboard/index.html +++ b/src/dashboard/index.html @@ -2705,7 +2705,8 @@

控制台登录

} catch (err) { status.style.color = '#ef4444'; status.textContent = I18n.t('oauth.status.loginFailed', { label, error: err.message }); - this.toast(I18n.t('toast.loginFailed', { label, error: err.message }), 'error'); + const translatedErr = I18n.t(err.message) !== err.message ? I18n.t(err.message) : err.message; + this.toast(I18n.t('toast.loginFailed', { label, error: translatedErr }), 'error'); } finally { if (btn) btn.disabled = false; } @@ -2784,7 +2785,7 @@

控制台登录

${I18n.t('loginResult.fail')}
-

${this.esc(r.error || I18n.t('error.unknown'))}

${this.getWindsurfLoginFailActions(r)}
`); +

${this.esc((r.error && I18n.t(r.error) !== r.error) ? I18n.t(r.error) : (r.error || I18n.t('error.unknown')))}

${this.getWindsurfLoginFailActions(r)}
`); }, renderBatchWindsurfLoginResult(results, autoAdd) { @@ -2808,7 +2809,7 @@

控制台登录

${r.success ? I18n.t('batch.success') : I18n.t('batch.fail')} ${r.success ? `${this.esc(r.apiKey_masked || (r.apiKey ? r.apiKey.slice(0, 16) + '...' : '-'))}${r.account ? ` · ${I18n.t('table.header.account')} ${this.esc(r.account.id)}` : ''}` - : `${this.esc(r.error || I18n.t('error.unknown'))}`} + : `${this.esc((r.error && I18n.t(r.error) !== r.error) ? I18n.t(r.error) : (r.error || I18n.t('error.unknown')))}`} `).join('')} @@ -2880,11 +2881,11 @@

控制台登录

} else { entry.status = 'error: ' + (r.error || 'unknown'); this.renderSingleWindsurfLoginResult(r, autoAdd); - this.toast(r.error || I18n.t('toast.loginFailed'), 'error'); + this.toast((r.error && I18n.t(r.error) !== r.error) ? I18n.t(r.error) : (r.error || I18n.t('toast.loginFailed')), 'error'); } } catch (err) { entry.status = 'error: ' + err.message; - this.toast(err.message, 'error'); + this.toast(I18n.t(err.message) !== err.message ? I18n.t(err.message) : err.message, 'error'); } this.pushLoginHistory(entry); @@ -2916,8 +2917,9 @@

控制台登录

${I18n.t('loginResult.fail')}
-

${this.esc(r.error || I18n.t('error.unknown'))}

`); - return this.toast(r.error || I18n.t('batch.importFailed'), 'error'); +

${this.esc((r.error && I18n.t(r.error) !== r.error) ? I18n.t(r.error) : (r.error || I18n.t('error.unknown')))}

`); + const translatedErr = r.error && I18n.t(r.error) !== r.error ? I18n.t(r.error) : r.error; + return this.toast(translatedErr || I18n.t('batch.importFailed'), 'error'); } this.renderBatchWindsurfLoginResult(r.results, autoAdd); @@ -2938,8 +2940,9 @@

控制台登录

${I18n.t('loginResult.fail')}
-

${this.esc(err.message)}

`); - this.toast(err.message, 'error'); +

${this.esc(I18n.t(err.message) !== err.message ? I18n.t(err.message) : err.message)}

`); + const translatedErr = I18n.t(err.message) !== err.message ? I18n.t(err.message) : err.message; + this.toast(translatedErr, 'error'); } finally { btn.disabled = false; btn.innerHTML = ' ' + I18n.t('action.batchImport'); From 4dd7b9e96d877adf36237d356460bad80e225d93 Mon Sep 17 00:00:00 2001 From: Stefan Meinecke Date: Tue, 28 Apr 2026 20:52:40 +0200 Subject: [PATCH 2/4] =?UTF-8?q?fix(dashboard):=20improve=20i18n=20UX=20?= =?UTF-8?q?=E2=80=94=20full-word=20lang=20toggle=20+=20auto-refresh=20pane?= =?UTF-8?q?ls=20+=20error.*=20namespace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Language toggle button now shows "中文"/"English" full words instead of "中"/"EN" abbreviations, with dynamic title tooltip ("Switch to English" / "切换到中文"). toggleLang() made async and triggers refreshActivePanelI18n() to reload the active panel's content after locale switch, so users see translated UI immediately without manual navigation. --- src/dashboard/index.html | 54 +++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/src/dashboard/index.html b/src/dashboard/index.html index a6c37def..20033ade 100644 --- a/src/dashboard/index.html +++ b/src/dashboard/index.html @@ -1521,8 +1521,8 @@ `); return; } + const errKey = r.error ? `error.${r.error}` : null; + const errMsg = (errKey && I18n.t(errKey) !== errKey) ? I18n.t(errKey) : (r.error || I18n.t('error.unknown')); this.showWindsurfLoginResult(`
${I18n.t('loginResult.fail')}
-

${this.esc((r.error && I18n.t(r.error) !== r.error) ? I18n.t(r.error) : (r.error || I18n.t('error.unknown')))}

${this.getWindsurfLoginFailActions(r)}
`); +

${this.esc(errMsg)}

${this.getWindsurfLoginFailActions(r)}
`); }, renderBatchWindsurfLoginResult(results, autoAdd) { @@ -2809,7 +2829,7 @@

控制台登录

${r.success ? I18n.t('batch.success') : I18n.t('batch.fail')} ${r.success ? `${this.esc(r.apiKey_masked || (r.apiKey ? r.apiKey.slice(0, 16) + '...' : '-'))}${r.account ? ` · ${I18n.t('table.header.account')} ${this.esc(r.account.id)}` : ''}` - : `${this.esc((r.error && I18n.t(r.error) !== r.error) ? I18n.t(r.error) : (r.error || I18n.t('error.unknown')))}`} + : (() => { const errKey = r.error ? `error.${r.error}` : null; return `${this.esc((errKey && I18n.t(errKey) !== errKey) ? I18n.t(errKey) : (r.error || I18n.t('error.unknown')))}`; })()} `).join('')} @@ -2881,11 +2901,13 @@

控制台登录

} else { entry.status = 'error: ' + (r.error || 'unknown'); this.renderSingleWindsurfLoginResult(r, autoAdd); - this.toast((r.error && I18n.t(r.error) !== r.error) ? I18n.t(r.error) : (r.error || I18n.t('toast.loginFailed')), 'error'); + const errKey = r.error ? `error.${r.error}` : null; + this.toast((errKey && I18n.t(errKey) !== errKey) ? I18n.t(errKey) : (r.error || I18n.t('toast.loginFailed')), 'error'); } } catch (err) { entry.status = 'error: ' + err.message; - this.toast(I18n.t(err.message) !== err.message ? I18n.t(err.message) : err.message, 'error'); + const errKey = err.message ? `error.${err.message}` : null; + this.toast((errKey && I18n.t(errKey) !== errKey) ? I18n.t(errKey) : err.message, 'error'); } this.pushLoginHistory(entry); @@ -2913,12 +2935,13 @@

控制台登录

try { const r = await this.api('POST', '/batch-import', { text: input, autoAdd }); if (!r.success || !Array.isArray(r.results)) { + const errKey = r.error ? `error.${r.error}` : null; this.showWindsurfLoginResult(`
${I18n.t('loginResult.fail')}
-

${this.esc((r.error && I18n.t(r.error) !== r.error) ? I18n.t(r.error) : (r.error || I18n.t('error.unknown')))}

`); - const translatedErr = r.error && I18n.t(r.error) !== r.error ? I18n.t(r.error) : r.error; +

${this.esc((errKey && I18n.t(errKey) !== errKey) ? I18n.t(errKey) : (r.error || I18n.t('error.unknown')))}

`); + const translatedErr = errKey && I18n.t(errKey) !== errKey ? I18n.t(errKey) : r.error; return this.toast(translatedErr || I18n.t('batch.importFailed'), 'error'); } @@ -2936,12 +2959,13 @@

控制台登录

document.getElementById('wl-batch-input').value = ''; } } catch (err) { + const errKey = err.message ? `error.${err.message}` : null; this.showWindsurfLoginResult(`
${I18n.t('loginResult.fail')}
-

${this.esc(I18n.t(err.message) !== err.message ? I18n.t(err.message) : err.message)}

`); - const translatedErr = I18n.t(err.message) !== err.message ? I18n.t(err.message) : err.message; +

${this.esc((errKey && I18n.t(errKey) !== errKey) ? I18n.t(errKey) : err.message)}

`); + const translatedErr = (errKey && I18n.t(errKey) !== errKey) ? I18n.t(errKey) : err.message; this.toast(translatedErr, 'error'); } finally { btn.disabled = false; @@ -2959,7 +2983,7 @@

控制台登录

${this.esc(h.email)} ${ok ? I18n.t('batch.success') : I18n.t('batch.fail')} - ${!ok ? `
${this.esc((h.status||'').replace('error: ',''))}
` : ''} + ${!ok ? (() => { const errCode = (h.status||'').replace('error: ',''); const errKey = `error.${errCode}`; const errMsg = I18n.t(errKey) !== errKey ? I18n.t(errKey) : errCode; return `
${this.esc(errMsg)}
`; })() : ''} ${this.esc(h.proxy || '-')} From 94509b938ff1cebfd61eac1c5355f6439d1e23ba Mon Sep 17 00:00:00 2001 From: Stefan Meinecke Date: Tue, 28 Apr 2026 21:03:49 +0200 Subject: [PATCH 3/4] fix(i18n): add ERR_PROXY_PRIVATE_HOST translation + rename net-safety error codes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dashboard i18n now includes ERR_PROXY_PRIVATE_HOST ("Private/local host is not allowed" / "内网/本地主机不允许"). net-safety.js error codes renamed from ERR_PRIVATE_HOST/ERR_PRIVATE_IP to ERR_PROXY_PRIVATE_HOST/ERR_PROXY_PRIVATE_IP for consistency with error.* namespace convention. --- src/dashboard/i18n/en.json | 1 + src/dashboard/i18n/zh-CN.json | 1 + src/net-safety.js | 6 +++--- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/dashboard/i18n/en.json b/src/dashboard/i18n/en.json index 21d72627..d7ad044e 100644 --- a/src/dashboard/i18n/en.json +++ b/src/dashboard/i18n/en.json @@ -862,6 +862,7 @@ "ERR_IDTOKEN_REQUIRED": "ID token is required", "ERR_PROXY_PRIVATE_IP": "Proxy cannot point to private/local address", "ERR_PROXY_HTTP_ERROR": "Proxy returned HTTP error", + "ERR_PROXY_PRIVATE_HOST": "Private/local host is not allowed", "ERR_CONNECTION_FAILED": "Connection failed", "ERR_TIMEOUT": "Timeout (10s)", "ERR_TLS_TUNNEL_ERROR": "TLS tunnel established but returned abnormal content", diff --git a/src/dashboard/i18n/zh-CN.json b/src/dashboard/i18n/zh-CN.json index 2e966cc5..24f19173 100644 --- a/src/dashboard/i18n/zh-CN.json +++ b/src/dashboard/i18n/zh-CN.json @@ -862,6 +862,7 @@ "ERR_IDTOKEN_REQUIRED": "缺少 idToken", "ERR_PROXY_PRIVATE_IP": "代理地址不能指向内网/本机", "ERR_PROXY_HTTP_ERROR": "代理返回 HTTP 错误", + "ERR_PROXY_PRIVATE_HOST": "内网/本地主机不允许", "ERR_CONNECTION_FAILED": "连接失败", "ERR_TIMEOUT": "超时(10s)", "ERR_TLS_TUNNEL_ERROR": "TLS 隧道建立但返回内容异常", diff --git a/src/net-safety.js b/src/net-safety.js index 3332fc13..4984bc7c 100644 --- a/src/net-safety.js +++ b/src/net-safety.js @@ -88,9 +88,9 @@ export function isPrivateIp(address) { export async function resolvePublicAddresses(hostname, lookupFn = dnsLookup) { const host = String(hostname || '').replace(/^\[|\]$/g, ''); - if (!host || host.toLowerCase() === 'localhost') throw new Error('ERR_PRIVATE_HOST'); + if (!host || host.toLowerCase() === 'localhost') throw new Error('ERR_PROXY_PRIVATE_HOST'); if (net.isIP(host)) { - if (isPrivateIp(host)) throw new Error('ERR_PRIVATE_IP'); + if (isPrivateIp(host)) throw new Error('ERR_PROXY_PRIVATE_IP'); return [{ address: host, family: net.isIP(host) }]; } const result = await new Promise((resolve, reject) => { @@ -98,7 +98,7 @@ export async function resolvePublicAddresses(hostname, lookupFn = dnsLookup) { }); const addrs = Array.isArray(result) ? result : [result]; for (const a of addrs) { - if (isPrivateIp(a.address)) throw new Error('ERR_PRIVATE_IP'); + if (isPrivateIp(a.address)) throw new Error('ERR_PROXY_PRIVATE_IP'); } return addrs; } From 112e3da2ad4f3943e29d869bae013543e0ba8740 Mon Sep 17 00:00:00 2001 From: Stefan Meinecke Date: Tue, 28 Apr 2026 21:09:10 +0200 Subject: [PATCH 4/4] test(ssrf): update error code assertion to match ERR_PROXY_PRIVATE_IP rename Follows the error code rename from ERR_PRIVATE_IP to ERR_PROXY_PRIVATE_IP introduced in 94509b9 (net-safety.js error codes standardized to error.* namespace convention). Also removes trailing blank lines. --- test/ssrf.test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/ssrf.test.js b/test/ssrf.test.js index f4b5f8eb..3c87c418 100644 --- a/test/ssrf.test.js +++ b/test/ssrf.test.js @@ -20,7 +20,7 @@ describe('SSRF private address detection', () => { it('rejects hostnames after DNS resolution to private IPs', async () => { const lookup = (host, opts, cb) => cb(null, [{ address: '127.0.0.1', family: 4 }]); - await assert.rejects(() => resolvePublicAddresses('evil.example', lookup), /ERR_PRIVATE_IP/); + await assert.rejects(() => resolvePublicAddresses('evil.example', lookup), /ERR_PROXY_PRIVATE_IP/); }); it('rejects oversized generic data URLs', () => { @@ -28,4 +28,3 @@ describe('SSRF private address detection', () => { assert.throws(() => parseGenericDataUrl(tooLarge), /Data URL exceeds/); }); }); -