mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
fix(i18n): align PBS fields and omit duplicate English errors
This commit is contained in:
@@ -208,11 +208,16 @@ export function PBSPanel({ localOnly = false, view = "all" }: { localOnly?: bool
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{(["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 <div key={field} className="space-y-2">
|
||||
<Label htmlFor={`pbs-${field}`}>{t(`pbs.${field}`)}</Label>
|
||||
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 <div key={field} className="min-w-0 space-y-2">
|
||||
<Label htmlFor={`pbs-${field}`} className="block leading-5">{t(`pbs.${field}`)}</Label>
|
||||
<Input id={`pbs-${field}`} disabled={busy} type={secret ? "password" : field === "url" ? "url" : "text"}
|
||||
aria-describedby={help ? `pbs-${field}-help` : undefined}
|
||||
autoComplete={secret ? "new-password" : "off"} placeholder={field === "username" ? "root@pam" : undefined}
|
||||
value={form[field as keyof typeof form]} onChange={event => setForm({ ...form, [field]: event.target.value })} />
|
||||
{help && <p id={`pbs-${field}-help`} className="text-xs text-muted-foreground">{t(help)}</p>}
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
@@ -324,7 +329,7 @@ function PBSResultBadge({ status, running = false, tone }: { status?: string; ru
|
||||
const badge = <Badge variant="outline" className={`${tone} max-w-[16rem] truncate`}>
|
||||
{running ? t("pbs.running") : pbsResultText(status, t)}
|
||||
</Badge>
|
||||
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 <details className="max-w-xs">
|
||||
<summary className="cursor-pointer" aria-label={t("pbs.technicalDetails")}>{badge}</summary>
|
||||
<pre className="mt-1 whitespace-pre-wrap break-words text-xs text-muted-foreground">{status}</pre>
|
||||
|
||||
@@ -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 <div className={`min-w-0 space-y-1 ${className}`}>
|
||||
<p>{pbsErrorText(problem, t, fallback)}</p>
|
||||
{raw && <details className="text-xs">
|
||||
{raw && !pbsIsKnownError(problem) && <details className="text-xs">
|
||||
<summary className="cursor-pointer">{t("pbs.technicalDetails")}</summary>
|
||||
<pre className="mt-1 whitespace-pre-wrap break-words font-mono">{raw}</pre>
|
||||
</details>}
|
||||
|
||||
@@ -20,18 +20,26 @@ const ERROR_KEYS: Record<string, string> = {
|
||||
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 {
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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 <img> DIAGNOSTIC'});
|
||||
assert.ok(text(rendered).includes('RAW <img> 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');
|
||||
|
||||
Reference in New Issue
Block a user