From f9e36f0b0aed236a2dd1fa204e1e26cd2823106d Mon Sep 17 00:00:00 2001 From: VAIO73 <50487331+Vaso73@users.noreply.github.com> Date: Tue, 20 Oct 2026 08:13:34 +0000 Subject: [PATCH] fix(i18n): align PBS fields and omit duplicate English errors --- AppImage/components/pbs-panel.tsx | 11 ++++++++--- AppImage/components/pbs-problem-message.tsx | 7 ++++--- AppImage/lib/pbs-messages.ts | 20 ++++++++++++++------ AppImage/messages/en/common.json | 15 ++++++++++----- AppImage/messages/sk/common.json | 15 ++++++++++----- tests/test_pbs_i18n.cjs | 20 ++++++++++++++++++++ 6 files changed, 66 insertions(+), 22 deletions(-) diff --git a/AppImage/components/pbs-panel.tsx b/AppImage/components/pbs-panel.tsx index 5dff026e..b18a3e12 100644 --- a/AppImage/components/pbs-panel.tsx +++ b/AppImage/components/pbs-panel.tsx @@ -208,11 +208,16 @@ export function PBSPanel({ localOnly = false, view = "all" }: { localOnly?: bool
{(["name", "url", ...(method === "password" && !form.id ? ["username", "password"] : ["token_id", "token_secret"]), "fingerprint"] as const).map(field => { const secret = field === "token_secret" || field === "password" - return
- + const help = field === "url" ? "pbs.urlHelp" : field === "username" ? "pbs.usernameHelp" + : field === "token_id" ? "pbs.tokenIdHelp" : field === "token_secret" + ? form.id ? "pbs.tokenSecretKeepHelp" : "pbs.tokenSecretHelp" : undefined + return
+ setForm({ ...form, [field]: event.target.value })} /> + {help &&

{t(help)}

}
})}
@@ -324,7 +329,7 @@ function PBSResultBadge({ status, running = false, tone }: { status?: string; ru const badge = {running ? t("pbs.running") : pbsResultText(status, t)} - if (running || !status || /^ok$/i.test(status)) return badge + if (running || !status || status === "unknown" || /^ok$/i.test(status) || /^WARNINGS:\s*\d+\s*$/i.test(status)) return badge return
{badge}
{status}
diff --git a/AppImage/components/pbs-problem-message.tsx b/AppImage/components/pbs-problem-message.tsx index 459701c4..f8286da9 100644 --- a/AppImage/components/pbs-problem-message.tsx +++ b/AppImage/components/pbs-problem-message.tsx @@ -1,16 +1,17 @@ "use client" -import { pbsErrorText } from "../lib/pbs-messages" +import { pbsErrorText, pbsIsKnownError } from "../lib/pbs-messages" import { useT } from "../lib/i18n/provider" -// Keep PBS's original diagnostic available without making it the UI message. +// Known application messages are already explained in the active language. +// Preserve unrecognized external diagnostics as escaped technical details. export function PBSProblemMessage({ problem, raw, fallback, className = "" }: { problem?: unknown; raw?: string; fallback?: string; className?: string }) { const t = useT() return

{pbsErrorText(problem, t, fallback)}

- {raw &&
+ {raw && !pbsIsKnownError(problem) &&
{t("pbs.technicalDetails")}
{raw}
} diff --git a/AppImage/lib/pbs-messages.ts b/AppImage/lib/pbs-messages.ts index 6ce6faa3..6314d317 100644 --- a/AppImage/lib/pbs-messages.ts +++ b/AppImage/lib/pbs-messages.ts @@ -20,18 +20,26 @@ const ERROR_KEYS: Record = { pbs_datastore_offline: "pbs.errors.datastoreOffline", pbs_datastore_unavailable: "pbs.errors.datastoreUnavailable", } -// Accept structured codes from fetchApi, partial status or a datastore. Legacy -// English errors remain diagnostics; never interpret arbitrary text as a key. -export function pbsErrorText(error: unknown, t: Translate, fallback = "pbs.loadFailed"): string { +// Accept structured codes, never arbitrary error text as a translation key. +function pbsErrorTranslation(error: unknown): { key: string; params?: { count: number } } | null { const body = error && typeof error === "object" && "body" in error ? error.body : error - if (!body || typeof body !== "object" || !("code" in body) || typeof body.code !== "string") return t(fallback) + if (!body || typeof body !== "object" || !("code" in body) || typeof body.code !== "string") return null if (body.code === "pbs_connection_limit") { const params = "params" in body ? body.params : null const count = params && typeof params === "object" && "count" in params ? params.count : null return typeof count === "number" && Number.isSafeInteger(count) && count > 0 - ? t("pbs.errors.connectionLimit", { count }) : t(fallback) + ? { key: "pbs.errors.connectionLimit", params: { count } } : null } - return Object.prototype.hasOwnProperty.call(ERROR_KEYS, body.code) ? t(ERROR_KEYS[body.code]) : t(fallback) + return Object.prototype.hasOwnProperty.call(ERROR_KEYS, body.code) ? { key: ERROR_KEYS[body.code] } : null +} + +export function pbsIsKnownError(error: unknown): boolean { + return pbsErrorTranslation(error) !== null +} + +export function pbsErrorText(error: unknown, t: Translate, fallback = "pbs.loadFailed"): string { + const translated = pbsErrorTranslation(error) + return translated ? t(translated.key, translated.params) : t(fallback) } export function pbsDiagnostic(error: unknown): string | undefined { diff --git a/AppImage/messages/en/common.json b/AppImage/messages/en/common.json index f4a3b71c..7bc7b31c 100644 --- a/AppImage/messages/en/common.json +++ b/AppImage/messages/en/common.json @@ -14,7 +14,7 @@ "signInWith": "Sign in with", "methodPassword": "User and password", "methodToken": "API token", - "username": "User (user@realm)", + "username": "User", "password": "Password", "passwordHelp": "The password is used once to create a read-only API token (Audit role) for this Monitor. Only that token is saved, never the password. Enable Monitor authentication first.", "cancel": "Cancel", @@ -27,10 +27,15 @@ "editConnection": "Connection to edit", "add": "Add a new connection", "name": "Name", - "url": "HTTPS URL (include :8007 for the native PBS port)", - "token_id": "API token ID (user@realm!token)", - "token_secret": "API token secret (leave empty to keep the stored secret)", - "fingerprint": "SHA256 certificate fingerprint (optional)", + "url": "HTTPS address", + "urlHelp": "Include :8007 for the default PBS port, for example https://pbs.example:8007.", + "usernameHelp": "Use user@realm, for example root@pam.", + "tokenIdHelp": "Use user@realm!token, for example monitor@pbs!proxmenux.", + "tokenSecretHelp": "Enter the secret of this API token.", + "tokenSecretKeepHelp": "Leave empty to keep the saved token secret.", + "token_id": "API token ID", + "token_secret": "API token secret", + "fingerprint": "SHA256 fingerprint (optional)", "tlsHelp": "Leave the fingerprint empty for normal CA and hostname verification. For a self-signed certificate, copy its SHA256 fingerprint from the PBS console or another trusted channel. The pin is checked before sending credentials.", "saved": "PBS connection verified and saved.", "saveFailed": "Could not save the PBS connection.", diff --git a/AppImage/messages/sk/common.json b/AppImage/messages/sk/common.json index 801b456e..418bf19b 100644 --- a/AppImage/messages/sk/common.json +++ b/AppImage/messages/sk/common.json @@ -7310,7 +7310,7 @@ "signInWith": "Prihlásiť sa pomocou", "methodPassword": "Používateľ a heslo", "methodToken": "API token", - "username": "Používateľ (user@realm)", + "username": "Používateľ", "password": "Heslo", "passwordHelp": "Heslo sa použije iba raz na vytvorenie API tokenu iba na čítanie s rolou Audit pre tento Monitor. Uloží sa iba token, heslo sa neukladá. Najprv zapni overovanie v Monitore.", "cancel": "Zrušiť", @@ -7323,10 +7323,15 @@ "editConnection": "Pripojenie na úpravu", "add": "Pridať nové pripojenie", "name": "Názov", - "url": "Adresa HTTPS (pre štandardný port PBS uveď :8007)", - "token_id": "ID API tokenu (user@realm!token)", - "token_secret": "Tajný kľúč API tokenu (prázdne pole zachová uložený kľúč)", - "fingerprint": "Odtlačok SHA256 certifikátu (voliteľný)", + "url": "Adresa HTTPS", + "urlHelp": "Štandardný port PBS je 8007, napríklad https://pbs.example:8007.", + "usernameHelp": "Použi meno a overovaciu oblasť oddelené znakom @, napríklad root@pam.", + "tokenIdHelp": "Použi formát meno@oblasť!token, napríklad monitor@pbs!proxmenux.", + "tokenSecretHelp": "Zadaj tajný kľúč k tomuto API tokenu.", + "tokenSecretKeepHelp": "Prázdne pole zachová uložený tajný kľúč tokenu.", + "token_id": "ID API tokenu", + "token_secret": "Tajný kľúč API tokenu", + "fingerprint": "Odtlačok SHA256 (voliteľný)", "tlsHelp": "Pri bežnom overovaní certifikačnej autority a názvu hostiteľa nechaj odtlačok prázdny. Pri certifikáte s vlastným podpisom skopíruj odtlačok SHA256 z konzoly PBS alebo iného dôveryhodného zdroja. Kontroluje sa ešte pred odoslaním prihlasovacích údajov.", "saved": "Pripojenie PBS bolo overené a uložené.", "saveFailed": "Nepodarilo sa uložiť pripojenie PBS.", diff --git a/tests/test_pbs_i18n.cjs b/tests/test_pbs_i18n.cjs index 1712b9d8..93d92605 100644 --- a/tests/test_pbs_i18n.cjs +++ b/tests/test_pbs_i18n.cjs @@ -97,11 +97,28 @@ async function main() { assert.equal(messages.pbsTaskType('garbage_collection',t),t('pbs.taskTypes.garbageCollection')); assert.equal(messages.pbsTaskType('future',t),t('pbs.taskTypes.other',{type:'future'})); assert.equal(messages.pbsServiceState('constructor',t),t('pbs.serviceStates.unknown')); + assert.equal(t('pbs.url'),locale==='sk'?'Adresa HTTPS':'HTTPS address'); + const diagnosticFixture=fixture(locale),problem=diagnosticFixture.load('AppImage/components/pbs-problem-message.tsx').PBSProblemMessage; + for(const code of codes){ + if(code==='pbs_unknown')continue; + const err=failure(code);err.body.params={count:32}; + const rendered=problem({problem:err,raw:'RAW DIAGNOSTIC'}); + assert.ok(!text(rendered).includes('RAW DIAGNOSTIC'),'Known '+code+' must not repeat an English application message'); + assert.ok(!flatten(rendered).some(n=>n.type==='details')); + } + for(const code of ['future','__proto__','constructor']){ + const rendered=problem({problem:failure(code),raw:'RAW DIAGNOSTIC'}); + assert.ok(text(rendered).includes('RAW DIAGNOSTIC'));assert.ok(flatten(rendered).some(n=>n.type==='details')); + } const unknown=fixture(locale);unknown.status.tasks=[{upid:'unknown',endtime:1700000000,status:'unknown'}];unknown.status.sync=[{id:'unknown','last-run-state':'unknown'}]; unknown.render();unknown.render(); const unknownBadges=unknown.nodes().filter(n=>n.type==='Badge'&&text(n)===t('pbs.states.unknown')); assert.equal(unknownBadges.length,2);assert.ok(unknownBadges.every(n=>!n.props.className.includes('red-'))); + unknown.status.tasks=[{upid:'warn',endtime:1700000000,status:'WARNINGS: 3'},{upid:'raw',endtime:1700000000,status:'RAW EXTERNAL TASK ERROR'}];unknown.status.sync=[];unknown.render(); + const taskDetails=unknown.nodes().filter(n=>n.type==='details');assert.equal(taskDetails.length,1);assert.ok(text(taskDetails[0]).includes('RAW EXTERNAL TASK ERROR')); const f=fixture(locale);f.render();f.render();f.button(t('pbs.manage')).props.onClick();f.render(); + assert.equal(f.input('url').props['aria-describedby'],'pbs-url-help'); + assert.ok(f.text().includes(t('pbs.urlHelp')));assert.ok(f.text().includes(t('pbs.usernameHelp'))); f.input('name').props.onChange({target:{value:'Draft'}});f.render(); f.input('password').props.onChange({target:{value:'fake-one-time-password'}});f.render(); f.button(t('pbs.cancel')).props.onClick();f.render();assert.equal(f.calls.length,0); @@ -111,8 +128,11 @@ async function main() { assert.ok(f.text().includes(t('pbs.errors.invalidUrl')));assert.equal(f.input('url').props.value,'http://bad.invalid'); assert.equal(f.calls[0].path,'/api/pbs/servers/login');assert.ok(!Object.hasOwn(JSON.parse(f.calls[0].body),'token_secret')); f.button(t('pbs.methodToken')).props.onClick();f.render();await f.button(t('pbs.save')).props.onClick();f.render(); + assert.ok(f.text().includes(t('pbs.tokenSecretHelp')));assert.ok(!f.text().includes(t('pbs.tokenSecretKeepHelp'))); + assert.equal(f.input('token_secret').props['aria-describedby'],'pbs-token_secret-help'); assert.equal(f.calls[1].path,'/api/pbs/servers');assert.ok(!Object.hasOwn(JSON.parse(f.calls[1].body),'password')); f.select(t('pbs.editConnection')).props.onValueChange('saved');f.render();await f.button(t('pbs.remove')).props.onClick();f.render(); + assert.ok(f.text().includes(t('pbs.tokenSecretKeepHelp')));assert.ok(!f.text().includes(t('pbs.tokenSecretHelp'))); assert.deepEqual(f.confirmations,[t('pbs.removeConfirm')]);assert.equal(f.calls.length,2,'Declined remove has no DELETE'); const remove=fixture(locale,{confirm:true});remove.render();remove.render();remove.button(t('pbs.manage')).props.onClick();remove.render();remove.select(t('pbs.editConnection')).props.onValueChange('saved');remove.render(); await remove.button(t('pbs.remove')).props.onClick();remove.render();assert.equal(remove.calls.length,1);assert.equal(remove.calls[0].method,'DELETE');