fix(i18n): localize admin diagnostics

This commit is contained in:
cesnimda
2026-08-29 22:35:28 +02:00
parent 0ef51fed74
commit ee8eacf0de
4 changed files with 134 additions and 41 deletions
+41 -41
View File
@@ -166,8 +166,8 @@ function displayMetadata(value?: string | null) {
return value && value.trim().length > 0 ? value : "-";
}
function formatDate(value?: string | null) {
return value ? new Date(value).toLocaleString() : "-";
function formatDate(value?: string | null, locale?: string) {
return value ? new Date(value).toLocaleString(locale) : "-";
}
function formatPercent(value?: number | null) {
@@ -206,7 +206,7 @@ function DetailRow({ label, value }: { label: string; value: React.ReactNode })
}
export default function AdminSystemPage() {
const { t } = useI18n();
const { language, t } = useI18n();
const [tab, setTab] = useState(0);
const [status, setStatus] = useState<SystemStatus | null>(null);
const [emailSettings, setEmailSettings] = useState<EditableEmailSettings | null>(null);
@@ -237,7 +237,7 @@ export default function AdminSystemPage() {
setSmtpPassword("");
setClearPassword(false);
} catch (e: any) {
setError(getApiErrorMessage(e, "Failed to load system status."));
setError(getApiErrorMessage(e, t("adminSystemLoadFailed")));
setStatus(null);
setEmailSettings(null);
setBenchmarkStatus(null);
@@ -277,20 +277,20 @@ export default function AdminSystemPage() {
return benchmarkIndex.Entries.flatMap((entry) => {
const findings: Array<{ file: string; issue: string }> = [];
if (entry.ContactLocation && /(culture|education|arial|hobbies|cooperate|ag, ni|bold)/i.test(entry.ContactLocation)) {
findings.push({ file: entry.FileName, issue: `Suspicious contact location: ${entry.ContactLocation}` });
findings.push({ file: entry.FileName, issue: t("adminBenchmarkSuspiciousLocation", { value: entry.ContactLocation }) });
}
if (entry.FirstEducation && entry.FirstEducation.length > 120) {
findings.push({ file: entry.FileName, issue: "Education qualification looks over-captured." });
findings.push({ file: entry.FileName, issue: t("adminBenchmarkEducationOvercaptured") });
}
if ((entry.FirstJob ?? "").length > 120) {
findings.push({ file: entry.FileName, issue: "Work title looks over-captured." });
findings.push({ file: entry.FileName, issue: t("adminBenchmarkWorkTitleOvercaptured") });
}
if ((entry.QualificationLevels ?? []).includes("Other")) {
findings.push({ file: entry.FileName, issue: "Qualification level fell back to Other." });
findings.push({ file: entry.FileName, issue: t("adminBenchmarkQualificationOther") });
}
return findings;
}).slice(0, 10);
}, [benchmarkIndex]);
}, [benchmarkIndex, language]);
const sendTestEmail = async () => {
setSendingTestEmail(true);
@@ -302,7 +302,7 @@ export default function AdminSystemPage() {
});
setError(null);
} catch (e: any) {
setError(getApiErrorMessage(e, "Failed to send test email."));
setError(getApiErrorMessage(e, t("adminSystemTestEmailFailed")));
} finally {
setSendingTestEmail(false);
}
@@ -329,7 +329,7 @@ export default function AdminSystemPage() {
setClearPassword(false);
await load();
} catch (e: any) {
setError(getApiErrorMessage(e, "Failed to save email settings."));
setError(getApiErrorMessage(e, t("adminSystemEmailSaveFailed")));
} finally {
setSavingSettings(false);
}
@@ -352,7 +352,7 @@ export default function AdminSystemPage() {
await api.post("/admin/system/ai/probe");
await load();
} catch (e: any) {
setError(getApiErrorMessage(e, "Failed to run AI service probe."));
setError(getApiErrorMessage(e, t("adminSystemProbeFailed")));
} finally {
setRunningProbe(false);
}
@@ -382,12 +382,12 @@ export default function AdminSystemPage() {
<SummaryCard
title={t("adminSystemEnvironment")}
value={status?.environment ?? "-"}
subtitle={`Version ${displayMetadata(status?.version)} · Commit ${displayMetadata(status?.commitSha)}`}
subtitle={t("adminSystemVersionCommit", { version: displayMetadata(status?.version), commit: displayMetadata(status?.commitSha) })}
/>
<SummaryCard
title={t("adminSystemDatabase")}
value={status ? (status.database.canConnect ? t("adminSystemConnected") : t("adminSystemOffline")) : "-"}
subtitle={status ? `${status.database.provider} · ${status.database.target || "No target"}` : "-"}
subtitle={status ? `${status.database.provider} · ${status.database.target || t("adminSystemNoTarget")}` : "-"}
tone={dbTone}
/>
<SummaryCard
@@ -400,9 +400,9 @@ export default function AdminSystemPage() {
title={t("adminSystemSummarizer")}
value={status?.ai.healthy ? t("adminSystemHealthy") : t("adminSystemOffline")}
subtitle={status?.ai.probeLatencyMs != null
? `${status.ai.probeLatencyMs} ms probe · ${status.ai.device || "unknown device"}`
? t("adminSystemProbeDevice", { latency: status.ai.probeLatencyMs, device: status.ai.device || t("adminSystemUnknownDevice") })
: status?.ai.healthLatencyMs != null
? `${status.ai.healthLatencyMs} ms health · ${status.ai.device || "unknown device"}`
? t("adminSystemHealthDevice", { latency: status.ai.healthLatencyMs, device: status.ai.device || t("adminSystemUnknownDevice") })
: t("adminSystemNoLatencyData")}
tone={aiTone}
/>
@@ -468,13 +468,13 @@ export default function AdminSystemPage() {
<DetailRow label={t("adminSystemOllamaReachable")} value={status?.ai.ollamaReachable ? t("yes") : t("noWord")} />
<DetailRow label={t("adminSystemOllamaModel")} value={status?.ai.ollamaModel || "-"} />
<DetailRow label={t("adminSystemOllamaModelAvailable")} value={status?.ai.ollamaModelAvailable ? t("yes") : t("noWord")} />
<DetailRow label="Ollama version" value={status?.ai.ollamaVersion || "-"} />
<DetailRow label="Loaded models" value={status?.ai.ollamaLoadedCount ?? 0} />
<DetailRow label={t("adminSystemOllamaVersion")} value={status?.ai.ollamaVersion || "-"} />
<DetailRow label={t("adminSystemLoadedModels")} value={status?.ai.ollamaLoadedCount ?? 0} />
<DetailRow label={t("adminSystemHealthLatency")} value={status?.ai.healthLatencyMs != null ? `${status.ai.healthLatencyMs} ms` : "-"} />
<DetailRow label={t("adminSystemProbeLatency")} value={status?.ai.probeLatencyMs != null ? `${status.ai.probeLatencyMs} ms` : "-"} />
<DetailRow label={t("adminSystemLastProbe")} value={formatDate(status?.ai.lastProbeAt)} />
<DetailRow label={t("adminSystemLastSuccessfulProbe")} value={formatDate(status?.ai.lastProbeSuccessAt)} />
<DetailRow label={t("adminSystemLastSummarizationSuccess")} value={formatDate(status?.ai.lastSuccessAt)} />
<DetailRow label={t("adminSystemLastProbe")} value={formatDate(status?.ai.lastProbeAt, language === "nb" ? "nb-NO" : "en")} />
<DetailRow label={t("adminSystemLastSuccessfulProbe")} value={formatDate(status?.ai.lastProbeSuccessAt, language === "nb" ? "nb-NO" : "en")} />
<DetailRow label={t("adminSystemLastSummarizationSuccess")} value={formatDate(status?.ai.lastSuccessAt, language === "nb" ? "nb-NO" : "en")} />
</Stack>
</Paper>
</Box>
@@ -497,57 +497,57 @@ export default function AdminSystemPage() {
<Chip label={status?.auth.googleConfigured ? t("adminSystemGoogleReady") : t("adminSystemGoogleOff")} variant="outlined" size="small" />
<Chip label={status?.auth.gmailConfigured ? t("adminSystemGmailReady") : t("adminSystemGmailIncomplete")} variant="outlined" size="small" />
<Chip label={status?.ai.gpuAvailable ? t("adminSystemGpuVisible") : t("adminSystemCpuMode")} color={status?.ai.gpuAvailable ? "success" : "default"} size="small" />
<Chip label={status?.ai.ocrAvailable ? `OCR ${status.ai.ocrLanguages || "enabled"}` : t("adminSystemOcrUnavailable")} variant="outlined" size="small" />
<Chip label={status?.ai.ocrAvailable ? t("adminSystemOcrEnabled", { languages: status.ai.ocrLanguages || t("adminSystemEnabled") }) : t("adminSystemOcrUnavailable")} variant="outlined" size="small" />
{(status?.ai.ollamaInstalledModels ?? []).slice(0, 4).map((model) => (
<Chip key={model} label={`Model · ${model}`} variant="outlined" size="small" />
<Chip key={model} label={t("adminSystemModelName", { model })} variant="outlined" size="small" />
))}
{(status?.ai.ollamaLoadedModels ?? []).slice(0, 3).map((model) => (
<Chip key={`loaded-${model}`} label={`Loaded · ${model}`} color="primary" variant="outlined" size="small" />
<Chip key={`loaded-${model}`} label={t("adminSystemLoadedModel", { model })} color="primary" variant="outlined" size="small" />
))}
</Box>
</Paper>
<Paper sx={{ p: 2, borderRadius: 3 }}>
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>CV benchmark review</Typography>
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>{t("adminBenchmarkTitle")}</Typography>
<Stack spacing={0.75}>
<DetailRow label="Benchmark root" value={benchmarkStatus?.rootPath || "-"} />
<DetailRow label="Last benchmark update" value={formatDate(benchmarkStatus?.lastUpdatedAtUtc)} />
<DetailRow label="Corpus root" value={benchmarkIndex?.CorpusRoot || "-"} />
<DetailRow label={t("adminBenchmarkRoot")} value={benchmarkStatus?.rootPath || "-"} />
<DetailRow label={t("adminBenchmarkLastUpdate")} value={formatDate(benchmarkStatus?.lastUpdatedAtUtc, language === "nb" ? "nb-NO" : "en")} />
<DetailRow label={t("adminBenchmarkCorpusRoot")} value={benchmarkIndex?.CorpusRoot || "-"} />
</Stack>
{benchmarkIndex ? (
<>
<Box sx={{ mt: 2, display: "grid", gridTemplateColumns: { xs: "1fr 1fr", md: "repeat(5, 1fr)" }, gap: 1.25 }}>
<SummaryCard title="Files" value={String(benchmarkIndex.TotalFiles)} subtitle="Corpus inputs" />
<SummaryCard title="Coverage" value={formatPercent(benchmarkIndex.AverageCoverage)} subtitle="Structured field coverage" tone={benchmarkTone(benchmarkIndex.AverageCoverage)} />
<SummaryCard title="Confidence" value={formatPercent(benchmarkIndex.AverageConfidence)} subtitle="Field metadata confidence" tone={benchmarkTone(benchmarkIndex.AverageConfidence)} />
<SummaryCard title="Consistency" value={formatPercent(benchmarkIndex.AverageConsistency)} subtitle="Normalization consistency" tone={benchmarkTone(benchmarkIndex.AverageConsistency)} />
<SummaryCard title="Missing approved" value={String(benchmarkIndex.MissingApprovedFixtures)} subtitle="Needs fixture review" tone={benchmarkIndex.MissingApprovedFixtures > 0 ? "warning" : "success"} />
<SummaryCard title={t("adminBenchmarkFiles")} value={String(benchmarkIndex.TotalFiles)} subtitle={t("adminBenchmarkCorpusInputs")} />
<SummaryCard title={t("adminBenchmarkCoverage")} value={formatPercent(benchmarkIndex.AverageCoverage)} subtitle={t("adminBenchmarkCoverageHelp")} tone={benchmarkTone(benchmarkIndex.AverageCoverage)} />
<SummaryCard title={t("adminBenchmarkConfidence")} value={formatPercent(benchmarkIndex.AverageConfidence)} subtitle={t("adminBenchmarkConfidenceHelp")} tone={benchmarkTone(benchmarkIndex.AverageConfidence)} />
<SummaryCard title={t("adminBenchmarkConsistency")} value={formatPercent(benchmarkIndex.AverageConsistency)} subtitle={t("adminBenchmarkConsistencyHelp")} tone={benchmarkTone(benchmarkIndex.AverageConsistency)} />
<SummaryCard title={t("adminBenchmarkMissingApproved")} value={String(benchmarkIndex.MissingApprovedFixtures)} subtitle={t("adminBenchmarkNeedsReview")} tone={benchmarkIndex.MissingApprovedFixtures > 0 ? "warning" : "success"} />
</Box>
<Box sx={{ mt: 2.5, display: "grid", gridTemplateColumns: { xs: "1fr", xl: "1.1fr 0.9fr" }, gap: 2 }}>
<Paper variant="outlined" sx={{ p: 1.5, borderRadius: 3 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 900, mb: 1 }}>Top parser findings</Typography>
<Typography variant="subtitle1" sx={{ fontWeight: 900, mb: 1 }}>{t("adminBenchmarkTopFindings")}</Typography>
<Stack spacing={1}>
{benchmarkFindings.length > 0 ? benchmarkFindings.map((finding) => (
<Box key={`${finding.file}:${finding.issue}`} sx={{ p: 1.25, borderRadius: 2, backgroundColor: "background.default", border: "none", boxShadow: "0px 1px 2px 0px rgba(15,23,42,0.04), 0px 8px 24px -12px rgba(15,23,42,0.12)" }}>
<Typography variant="caption" sx={{ color: "text.secondary" }}>{finding.file}</Typography>
<Typography variant="body2">{finding.issue}</Typography>
</Box>
)) : <Typography variant="body2" sx={{ color: "text.secondary" }}>No standout benchmark anomalies in the current run.</Typography>}
)) : <Typography variant="body2" sx={{ color: "text.secondary" }}>{t("adminBenchmarkNoAnomalies")}</Typography>}
</Stack>
</Paper>
<Paper variant="outlined" sx={{ p: 1.5, borderRadius: 3 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 900, mb: 1 }}>Weakest files in current run</Typography>
<Typography variant="subtitle1" sx={{ fontWeight: 900, mb: 1 }}>{t("adminBenchmarkWeakest")}</Typography>
<Stack spacing={1}>
{weakestEntries.map((entry) => (
<Box key={entry.Slug} sx={{ p: 1.25, borderRadius: 2, backgroundColor: "background.default", border: "none", boxShadow: "0px 1px 2px 0px rgba(15,23,42,0.04), 0px 8px 24px -12px rgba(15,23,42,0.12)" }}>
<Typography variant="body2" sx={{ fontWeight: 800 }}>{entry.FileName}</Typography>
<Box sx={{ display: "flex", gap: 0.75, flexWrap: "wrap", mt: 0.75 }}>
<Chip size="small" label={`Coverage ${formatPercent(entry.CoverageScore)}`} color={benchmarkTone(entry.CoverageScore)} />
<Chip size="small" label={`Confidence ${formatPercent(entry.ConfidenceScore)}`} color={benchmarkTone(entry.ConfidenceScore)} />
<Chip size="small" label={`Consistency ${formatPercent(entry.ConsistencyScore)}`} color={benchmarkTone(entry.ConsistencyScore)} />
<Chip size="small" label={t("adminBenchmarkCoverageValue", { value: formatPercent(entry.CoverageScore) })} color={benchmarkTone(entry.CoverageScore)} />
<Chip size="small" label={t("adminBenchmarkConfidenceValue", { value: formatPercent(entry.ConfidenceScore) })} color={benchmarkTone(entry.ConfidenceScore)} />
<Chip size="small" label={t("adminBenchmarkConsistencyValue", { value: formatPercent(entry.ConsistencyScore) })} color={benchmarkTone(entry.ConsistencyScore)} />
</Box>
<Typography variant="caption" sx={{ display: "block", color: "text.secondary", mt: 0.75 }}>{entry.DiffSummary || "-"}</Typography>
</Box>
@@ -557,7 +557,7 @@ export default function AdminSystemPage() {
</Box>
<Box sx={{ mt: 2, p: 1.5, borderRadius: 2, backgroundColor: "background.default", border: "none", boxShadow: "0px 1px 2px 0px rgba(15,23,42,0.04), 0px 8px 24px -12px rgba(15,23,42,0.12)", maxHeight: 280, overflow: "auto" }}>
<Typography variant="subtitle2" sx={{ fontWeight: 800, mb: 1 }}>Latest markdown summary</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 800, mb: 1 }}>{t("adminBenchmarkLatestSummary")}</Typography>
<Typography variant="body2" sx={{ whiteSpace: "pre-wrap", fontFamily: "ui-monospace, SFMono-Regular, monospace" }}>
{benchmarkStatus?.reportMarkdown || "-"}
</Typography>
@@ -566,7 +566,7 @@ export default function AdminSystemPage() {
) : (
<Box sx={{ mt: 1.5, p: 1.5, borderRadius: 2, backgroundColor: "background.default", border: "none", boxShadow: "0px 1px 2px 0px rgba(15,23,42,0.04), 0px 8px 24px -12px rgba(15,23,42,0.12)" }}>
<Typography variant="body2" sx={{ whiteSpace: "pre-wrap", fontFamily: "ui-monospace, SFMono-Regular, monospace" }}>
{benchmarkStatus?.reportMarkdown || "Run scripts/run-cv-benchmark.sh to generate the latest corpus report and fixture candidates."}
{benchmarkStatus?.reportMarkdown || t("adminBenchmarkRunScript")}
</Typography>
</Box>
)}