fix(i18n): localise admin system metric display

This commit is contained in:
cesnimda
2026-11-10 13:27:00 +01:00
parent af33e45542
commit 4c12f7691f
4 changed files with 26 additions and 13 deletions
+7
View File
@@ -0,0 +1,7 @@
# Admin System metric formatting
Bounded I18N-02 display fix on `codex/i18n-admin-metrics`, based on the isolated salary checkpoint `af33e45542aea1e18088c44308161284fdca47a7`. The same unlocalised storage-size and benchmark-percentage formatting was present at A3 `49e79875`.
Database storage sizes and CV benchmark summary and entry percentages now format using the current English or Bokmål UI locale. Storage retains the existing B/KB/MB boundaries and one fractional digit for KB and MB. Percentages use locale-specific spacing and whole-number precision. Input metrics, missing-value fallbacks, threshold colours and diagnostic operation paths remain intact.
The existing mounted Bokmål diagnostic test reproduced the missing `2,0 KB` display before the fix and now checks that display and the localised `72 %` benchmark result. `npm test -- --runInBand --runTestsByPath src/admin-system-page.test.tsx src/translation-catalogue.test.ts` passed 23 tests across two suites. Full frontend typecheck, scoped ESLint with zero warnings and the Next static export passed. No browser/device or native-language review and no actual diagnostic or benchmark execution.
+2
View File
@@ -1,5 +1,7 @@
# Current localisation continuation
Admin System database sizes and CV benchmark percentages now follow the EN/NB UI locale on the isolated admin-metrics branch. See [implementation and validation](ADMIN_METRIC_FORMATTING.md).
Mobile application cards now use the selected EN/NB locale and existing salary-period labels on the isolated mobile-salary branch. See [the bounded implementation and validation](MOBILE_SALARY_FORMATTING.md). Full I18N-02 surface coverage and European locale acceptance remain open.
3 November 2026 — independent document-date fix: invalid uploaded wall-clock dates/times/trailing junk now show the existing EN/NB unknown-date message instead of JavaScript rollover.40 tests/3 suites, typecheck, scoped lint and build pass; see DOCUMENT_DATE_VALIDATION.md. All four historical direct-catalogue accesses have contextual dispositions in DIRECT_CATALOGUE_REVIEW.md; current full extraction/backend literals/every-string triage remain open. No new locales or release approval. This branch is not yet integrated into A3 dashboard/application work.
@@ -472,6 +472,8 @@ describe('AdminSystemPage', () => {
expect(await screen.findByText(/Mistenkelig kontaktsted: san francisco, hobbies/i)).toBeTruthy();
expect(screen.getByText('Production')).toBeTruthy();
expect(screen.getByText(/Modell · qwen2.5:7b/i)).toBeTruthy();
expect(screen.getByText(/2,0 KB/)).toBeVisible();
expect(screen.getByText('72 %')).toBeVisible();
fireEvent.click(screen.getByRole('tab', { name: /innstillinger/i }));
expect(await screen.findByDisplayValue('Dette er en test-e-post fra systempanelet i Jobbjakt.')).toBeTruthy();
});
+15 -13
View File
@@ -167,11 +167,12 @@ type SystemStatus = {
ai: AiServiceMetrics;
};
function formatBytes(bytes?: number | null) {
function formatBytes(bytes: number | null | undefined, locale: string) {
if (bytes == null) return "-";
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
if (bytes < 1024) return `${bytes.toLocaleString(locale)} B`;
const options = { minimumFractionDigits: 1, maximumFractionDigits: 1 };
if (bytes < 1024 * 1024) return `${(bytes / 1024).toLocaleString(locale, options)} KB`;
return `${(bytes / (1024 * 1024)).toLocaleString(locale, options)} MB`;
}
function displayMetadata(value?: string | null) {
@@ -182,8 +183,8 @@ function formatDate(value?: string | null, locale?: string) {
return value ? new Date(value).toLocaleString(locale) : "-";
}
function formatPercent(value?: number | null) {
return typeof value === "number" ? `${Math.round(value * 100)}%` : "-";
function formatPercent(value: number | null | undefined, locale: string) {
return typeof value === "number" ? value.toLocaleString(locale, { style: "percent", maximumFractionDigits: 0 }) : "-";
}
function parseBenchmarkIndex(indexJson?: string | null): CvBenchmarkIndex | null {
@@ -225,6 +226,7 @@ const actionErrorKeys: Record<ActionKind, TranslationKey> = { save: "adminSystem
export default function AdminSystemPage() {
const { language, t } = useI18n();
const locale = language === "nb" ? "nb-NO" : "en";
const [tab, setTab] = useState(0);
const [status, setStatus] = useState<SystemStatus | null>(null);
const [emailSettings, setEmailSettings] = useState<EditableEmailSettings | null>(null);
@@ -495,7 +497,7 @@ export default function AdminSystemPage() {
<DetailRow label={t("adminSystemDataRoot")} value={status?.storage.dataRoot || "-"} />
<DetailRow label={t("adminSystemDbPath")} value={status?.storage.dbPath || "-"} />
<DetailRow label={t("adminSystemDbFileExists")} value={status?.storage.dbExists ? t("yes") : t("noWord")} />
<DetailRow label={t("adminSystemDbSize")} value={formatBytes(status?.storage.dbSizeBytes)} />
<DetailRow label={t("adminSystemDbSize")} value={formatBytes(status?.storage.dbSizeBytes, locale)} />
<DetailRow label={t("companies")} value={status?.storage.companyCount ?? 0} />
<DetailRow label={t("adminSystemJobs")} value={status?.storage.jobCount ?? 0} />
<DetailRow label={t("adminSystemDeletedJobs")} value={status?.storage.deletedCount ?? 0} />
@@ -612,9 +614,9 @@ export default function AdminSystemPage() {
<>
<Box sx={{ mt: 2, display: "grid", gridTemplateColumns: { xs: "1fr 1fr", md: "repeat(5, 1fr)" }, gap: 1.25 }}>
<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("adminBenchmarkCoverage")} value={formatPercent(benchmarkIndex.AverageCoverage, locale)} subtitle={t("adminBenchmarkCoverageHelp")} tone={benchmarkTone(benchmarkIndex.AverageCoverage)} />
<SummaryCard title={t("adminBenchmarkConfidence")} value={formatPercent(benchmarkIndex.AverageConfidence, locale)} subtitle={t("adminBenchmarkConfidenceHelp")} tone={benchmarkTone(benchmarkIndex.AverageConfidence)} />
<SummaryCard title={t("adminBenchmarkConsistency")} value={formatPercent(benchmarkIndex.AverageConsistency, locale)} 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>
@@ -638,9 +640,9 @@ export default function AdminSystemPage() {
<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={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)} />
<Chip size="small" label={t("adminBenchmarkCoverageValue", { value: formatPercent(entry.CoverageScore, locale) })} color={benchmarkTone(entry.CoverageScore)} />
<Chip size="small" label={t("adminBenchmarkConfidenceValue", { value: formatPercent(entry.ConfidenceScore, locale) })} color={benchmarkTone(entry.ConfidenceScore)} />
<Chip size="small" label={t("adminBenchmarkConsistencyValue", { value: formatPercent(entry.ConsistencyScore, locale) })} color={benchmarkTone(entry.ConsistencyScore)} />
</Box>
<Typography variant="caption" sx={{ display: "block", color: "text.secondary", mt: 0.75 }}>{entry.DiffSummary || "-"}</Typography>
</Box>