Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ce93ca0e3 | |||
| ca519cacbe | |||
| d4b938fafb |
@@ -0,0 +1,17 @@
|
||||
# Attachment size localisation
|
||||
|
||||
3 November 2026. Isolated `codex/i18n-attachment-sizes` from published `ca519cacbec6ee8e385927ad31356dacc18b2f2d`. Ordinary localisation work is authorised independently of the pending A3 increment gate. This branch does not replace the newer A3 application tree.
|
||||
|
||||
`Attachments` and the frozen submitted-package attachment list displayed decimal points through `toFixed`, even with Norwegian UI text. They now use `Intl.NumberFormat` with the existing UI language. The attachment table keeps its existing 1024-based KB/MB threshold, zero KB decimals and one MB decimal. Frozen package labels keep one KB decimal. No stored file size, filename, snapshot, route, API, revision, download or generation language changes.
|
||||
|
||||
Verification:
|
||||
|
||||
- Red regression: two Norwegian-format cases failed, one English case passed; 58 cases were filtered, not executed. Native `attachment-size-red.json`.
|
||||
- Full affected suites: **61 passed**, zero failed/skipped, two suites; `attachment-size-tests.json`. Includes EN/NB MB labels and changing the UI locale on a retained frozen package without a write.
|
||||
- Typecheck, scoped ESLint for the four affected files, static-export build and `git diff --check`: PASSED.
|
||||
- AST-only graph refresh PASSED:18,702 nodes/46,195 edges/1,119 communities. Optional SQL parser is absent and143 files yielded no symbols; these are graph coverage limits. Generated graph stays local.
|
||||
- Independent static review found no actionable issues; verification reruns were not added to the61 cases.
|
||||
|
||||
Rollback reverts these four source/test changes; no database rollback is required. The published branch remains separate until its narrow delta is reconciled against the newer A3 baseline. Existing physical PDF metrics and visual acceptance are unaffected; no renderer source changed.
|
||||
|
||||
NOT RUN: new browser/device or physical PDF checks, complete European localisation, native-language review, backend/provider/live-model or release qualification. Existing tests and code remain responsible for file permissions and downloads; this display-only change adds no new access path. The 188 historical dynamic-call reviews remain accounted for and are not expanded by these three numeric-format regressions.
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"base": "ca519cacbec6ee8e385927ad31356dacc18b2f2d",
|
||||
"policy": "LF-normalised text SHA256",
|
||||
"files": {
|
||||
"job-tracker-ui/src/components/Attachments.tsx": "02081be8282cb3d1a525b336bededb8aabe561cc3b73e1b98e924f9ef945add1",
|
||||
"job-tracker-ui/src/components/ApplicationAssets.tsx": "effcee6cfa0a43ebe0d73a0da625410dc98a6580fddda225b843e5e00d4e45f6",
|
||||
"job-tracker-ui/src/attachments.test.tsx": "89bb3e277ae7be652073eec6d3b6188bcbeaea8b29b07478ff5b21654be8e7c1",
|
||||
"job-tracker-ui/src/application-assets.test.tsx": "980a346cb4452ac4b7e6b689a963e5a23fc6d77410e47e66a422f9cd9c704e7b",
|
||||
"docs/i18n/ATTACHMENT_SIZE_FORMATTING.md": "4576ccfc3de3b72cd3e5591c40f697b827d724b8f4fe588b54567dbe75252332",
|
||||
"docs/i18n/attachment-size-red.json": "872882cb7d2937bf1df7f7a89810158c5de831381f56c6ae8bb0263d26b84d8e",
|
||||
"docs/i18n/attachment-size-tests.json": "8b8188d432b7788f951cf2c67f790c6cd4d24553ed9accbeb702645b3d4b2c6a"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
# Current localisation continuation
|
||||
|
||||
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.
|
||||
|
||||
Register portability correction: schema 3 explicitly normalises CRLF to LF when hashing reviewed TS/TSX/JSON text and the original inventory; historicalSourceSha256 retains the original inventory's raw-hash meaning. This avoids treating Git checkout line-ending conversion as a source edit. Integration initially detected the raw inventory mismatch; original snapshot contents and historical source hashes were preserved, and both worktrees are revalidated with the declared encoding. This is evidence portability, not renewed runtime execution.
|
||||
|
||||
2 November 2026. Historical 188 dynamic-call census finite key-domain review COMPLETE; full I18N-00 / European locale release acceptance remains INCOMPLETE. Original source-bound snapshots are retained under 00/. Machine register schema 2 binds inventory indices, source hashes and selected owner dependencies; 108 original plus 80 supplemental calls cover all 188 indices without overlap. Static review does not establish all translator sensitivity/context, every visible/backend string, grammar/plural quality, runtime coverage or native review.
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Historical direct catalogue access review
|
||||
|
||||
3 November 2026. All four entries in the frozen 00/DIRECT_CATALOGUE_ACCESS.json are in I18nProvider.tsx. Reviewed actual current callers and source, not re-extracted as a claim of complete current coverage.
|
||||
|
||||
| Historical expression | Context and disposition |
|
||||
|---|---|
|
||||
| translations[l].settingsLanguageSaveFailed | Public static account-preference error copy, no runtime private value. Uses requested language, guarded by save sequence/current language before showing feedback. Deliberate provider-internal access; calling the context hook inside its owner is inappropriate. Retain. |
|
||||
| translations[l].retry | Static retry action label in the same guarded feedback; no interpolation/private data. Retain. |
|
||||
| translations[language][key] | Central typed message lookup inside t, passed to interpolation; all callers use this message owner. React callers render text; this review does not certify every sink. Retain. |
|
||||
| translations.en[key] | Deterministic English fallback for the central lookup. No country/document-language coupling. Retain. |
|
||||
|
||||
The provider's TranslationKey/UiLanguage contracts and catalogue completeness tests constrain these accesses; sequence guards prevent obsolete-save feedback/retries. No new catalogue bypass or runtime change was required. Existing provider/catalogue tests pass within the40-case document-date qualification; this is not native linguistic or full security acceptance.
|
||||
|
||||
Original inventory/hash/line records are immutable historical evidence. Current source identity is bound in document-date SOURCE_MANIFEST.json. This closes contextual classification of the four historical direct accesses only. Current full re-extraction, non-catalogue/backend literal inventory and every-string sensitivity/caller review remain incomplete. No runtime user content was exported or translated externally.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Document upload wall-clock date correction
|
||||
|
||||
3 November 2026. Independent ordinary application fix, branch `codex/i18n-document-dates` from published d4b938fa. Not integrated into A3 yet; no wholesale merge of this older integration-derived tree.
|
||||
|
||||
Reproduced: the document index accepted an ISO prefix and let JavaScript normalise invalid wall-clock components. `2026-02-30T09:15:00`, hour24, minute61 and trailing junk displayed plausible timestamps. Eight EN/NB regression cases failed before the fix; two valid fractional-second cases passed. These failures are retained in evidence/document-dates-red.json;21 unrelated cases were skipped in that focused red execution.
|
||||
|
||||
The existing uploaded-local branch now accepts the complete unzoned .NET-style timestamp (optional1–7 fractional digits) and round-trips all calendar/time fields before formatting. Invalid input uses the existing localised Date not recorded message and emits no fabricated time element. Valid wall-clock components remain unchanged across time zones using the existing UTC formatting carrier. The raw datetime attribute remains unchanged. No API date-kind semantics, server data, UTC-history formatting, document content, download behavior, revisions or navigation changes. No new locale key/dependency/schema.
|
||||
|
||||
Final validation: **40 tests /3 suites PASSED**, documents31 + provider6 + catalogue3. Tests include metadata filters/paging/history, source links, all seven resource kinds, download/focus/cancellation, ownership-safe read behavior, both languages, invalid dates and valid .NET fractional seconds. All document tests assert no POST/PUT/PATCH/DELETE. Typecheck, scoped zero-warning lint, production build and diff whitespace check PASSED. An intermediate command used a nonexistent catalogue-test filename:37 cases passed but that execution failed one suite; it is not counted as passing. Corrected final run uses translation-catalogue.test.ts. No fresh browser/backend/native/provider qualification.
|
||||
|
||||
Rollback: revert this bounded view/parser and test commit; no data rollback. Broader document UTC date validation, partial-date semantics outside this surface, DST/native reviews and full European locale architecture remain open. No release or production qualification inferred.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Mobile build metadata localisation
|
||||
|
||||
3 November 2026. Independent bounded UI-language fix on `codex/i18n-build-metadata`, based on accepted integration `d3d338376444b8abdcaf7200f682faa3daf1ad10`. The existing i18n worktree was reused; original codex/i18n-accessible-labels ref remains4172c23d. This branch does not include the newer A3 foundation/navigation increments and must not replace their tree wholesale.
|
||||
|
||||
Defect reproduced: in the Bokmål mobile account menu, the version was translated but the revision line remained literal `commit abc1234`, while the existing catalogue already provides `applicationVersionCommit` with `revisjon`. The new regression passed for English and failed for Bokmål before the fix.
|
||||
|
||||
The existing AppShell now uses the existing combined localised version/revision message in that menu, retaining the version-only fallback when no commit was supplied. Caller-controlled admin metadata visibility is unchanged. No new keys, enabled locales, provider/model operation, API, data model or physical document changes. Historical188 dynamic-call reviews are not reopened or recounted; AppShell is absent from that resolution register. This is a static literal correction, not complete European localisation.
|
||||
|
||||
Validation: 24 targeted tests in five suites PASSED (new EN/NB commit and version-only menu checks plus existing metadata/visibility, mobile readiness, catalogue and language-provider checks). Typecheck, scoped zero-warning ESLint and a fresh static production build PASSED. An initial test used an unsupported Testing Library `exact` role option; removed it, retaining exact string-name matching. The failed intermediate typecheck/build were superseded by the final successful run. Native Jest report: evidence/mobile-build-metadata/TESTS.json. No fresh browser/native-device, provider, backend or integrated security qualification is claimed for this separate branch.
|
||||
|
||||
Integration requirement: preserve pending navigation review artifact992608c7. When the next relevant implementation entry is authorised, port only this small AppShell hunk and its regression test onto the reconciled A3 branch, then run combined navigation/metadata/language checks and build as appropriate. Do not cherry-pick older branch application/theme files or alter the original frozen translation register. No main merge or deployment.
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"base": "d4b938fafbf10541b62134293cea8299f25c4031",
|
||||
"recordedAtUtc": "2026-11-03T08:50:34.776817+00:00",
|
||||
"policy": "LF-normalised SHA256 text",
|
||||
"files": {
|
||||
"job-tracker-ui/src/views/DocumentsPage.tsx": "2a3187938d7f61cc1d723bae5b784f09707713606b523db176d6df6d05a91c3d",
|
||||
"job-tracker-ui/src/documents-page.test.tsx": "abfa8c4bef73499c0546f7e9b8a091793b8c273f829b51159ae115fc54aa4ed3",
|
||||
"job-tracker-ui/src/i18n/I18nProvider.tsx": "d22b5d033d0e405ffb5d581c752d61a24056d1e496f5f6a2f1baecef500f2113",
|
||||
"docs/i18n/00/DIRECT_CATALOGUE_ACCESS.json": "c470c52c500d79f51c4cedd073c2a8474ae73b832e531df3c0c38369993cae3f",
|
||||
"docs/i18n/DOCUMENT_DATE_VALIDATION.md": "602cd31d84eed1e59ed000159bdc8b998b0dbdc5b7c965ad51831d26a16b31d6",
|
||||
"docs/i18n/DIRECT_CATALOGUE_REVIEW.md": "4eb88d2d1da2ed0cd52d49fc8dc7f11d1943d169c240e56ae3562d298bc6eb6a",
|
||||
"docs/i18n/evidence/document-dates-tests.json": "227187b5a02473f442c8600b434e5f0922fa63ba13bfb9355254b9a19b7d4e81",
|
||||
"docs/i18n/evidence/document-dates-red.json": "430ec89f685c2558d443677c596a12b587829a9a45e3f29dc03e042d3685a074"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"recordedAtUtc": "2026-11-03T07:52:44.745521+00:00",
|
||||
"base": "d3d338376444b8abdcaf7200f682faa3daf1ad10",
|
||||
"policy": "UTF8 text/JSON SHA-256 after LF normalisation",
|
||||
"sources": {
|
||||
"job-tracker-ui/src/layout/AppShell.tsx": "603afbe21d854ec4af7b17ec75a58cfdd65cc053e72d63769d20df7a81c7acc3",
|
||||
"job-tracker-ui/src/build-metadata-localisation.test.tsx": "065c5550a18f7b3c7dca9bbb26c5564a53a53eaf389e7c9695596fcbb2c06f6e",
|
||||
"job-tracker-ui/src/i18n/translations.ts": "ed99d129f8702181429e10ec02f65f734220409fcba4de51d0d77a0d60871ea4",
|
||||
"job-tracker-ui/package.json": "3745394e406cefa40d30a87e840bccb359be147bac6ffba736e2ae90898a8626",
|
||||
"job-tracker-ui/package-lock.json": "bf31e90d7af03e8af059db2aaf893925b9e8881a30a1688034ed88ef45d4fcab"
|
||||
},
|
||||
"testReport": "1c7fb362460f2475fb4a782e26b56d1ee112595c3883cd6294a5b7ac6d72dd15",
|
||||
"validation": {
|
||||
"tests": "24 passed,5 suites",
|
||||
"typecheck": "exit0",
|
||||
"scopedLint": "exit0",
|
||||
"productionBuild": "exit0"
|
||||
},
|
||||
"limits": [
|
||||
"no fresh browser/native acceptance",
|
||||
"not combined with pending A3 navigation review992608c7",
|
||||
"188 historical dynamic-call review remains unchanged"
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -104,6 +104,18 @@ function changeLanguage(language: UiLanguage) {
|
||||
act(() => { window.dispatchEvent(new StorageEvent("storage", { key: "uiLanguage", newValue: language })); });
|
||||
}
|
||||
|
||||
test('frozen attachment size follows UI language without changing stored package data', async () => {
|
||||
const attachment = { id: 81, fileName: 'recorded.pdf', fileSize: 1536, fileType: 'application/pdf' };
|
||||
const summary = { id: 91, version: 1, cvVariantId: 3, cvVariantName: 'Original CV', cvVariantVersion: 2, cvThemeId: 'nordic', hasCv: true, hasCoverLetter: true, createdAtUtc: '2026-07-19T10:00:00Z', attachments: [attachment] };
|
||||
routeGet({ submittedPackages: [summary] });
|
||||
render(<SubmittedPackagesSection jobId={7} />);
|
||||
expect(await screen.findByRole('button', { name: /recorded.pdf.*1.5 KB/ })).toBeInTheDocument();
|
||||
changeLanguage('nb');
|
||||
expect(screen.getByRole('button', { name: /recorded.pdf.*1,5 KB/ })).toBeInTheDocument();
|
||||
expect(summary.attachments[0]).toEqual(attachment);
|
||||
expect(mockedApi.post).not.toHaveBeenCalled(); expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (reason: unknown) => void;
|
||||
|
||||
@@ -22,6 +22,15 @@ const mockedApi = api as jest.Mocked<typeof api>;
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
test.each([['en', '1.5 MB'], ['nb', '1,5 MB']])('attachment size follows the UI locale (%s)', async (language, size) => {
|
||||
const previous = localStorage.getItem('uiLanguage');
|
||||
localStorage.setItem('uiLanguage', language);
|
||||
mockedApi.get.mockResolvedValueOnce({ data: [{ id: 8, fileName: 'recorded.pdf', fileType: 'application/pdf', fileSize: 1572864, uploadDate: '2026-11-03T10:00:00Z' }] } as any);
|
||||
const view = render(<ToastProvider><I18nProvider><ConfirmProvider><PromptProvider><Attachments jobId={42} /></PromptProvider></ConfirmProvider></I18nProvider></ToastProvider>);
|
||||
try { expect(await screen.findByText(size)).toBeInTheDocument(); expect(mockedApi.post).not.toHaveBeenCalled(); }
|
||||
finally { view.unmount(); if (previous === null) localStorage.removeItem('uiLanguage'); else localStorage.setItem('uiLanguage', previous); }
|
||||
});
|
||||
|
||||
test.each([
|
||||
['en', 'Preview: resume.pdf', 'Preview: resume.pdf'],
|
||||
['nb', 'Forhåndsvis: resume.pdf', 'Forhåndsvisning: resume.pdf'],
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from "react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen, within } from "@testing-library/react";
|
||||
import JobTrackerThemeProvider from "./ThemeProvider";
|
||||
import { I18nProvider } from "./i18n/I18nProvider";
|
||||
import AppShell from "./layout/AppShell";
|
||||
|
||||
jest.mock("@mui/material/useMediaQuery", () => ({ __esModule: true, default: () => true }));
|
||||
afterEach(() => localStorage.clear());
|
||||
|
||||
test.each([
|
||||
["en", "User", "commit abc1234", "abc1234"],
|
||||
["nb", "Bruker", "revisjon abc1234", "abc1234"],
|
||||
["en", "User", "Application version 2.4.1", undefined],
|
||||
["nb", "Bruker", "Applikasjonsversjon 2.4.1", undefined],
|
||||
] as const)("%s mobile account menu localises the supplied build metadata (%s)", (language, user, expected, commit) => {
|
||||
localStorage.setItem("uiLanguage", language);
|
||||
render(<JobTrackerThemeProvider preference="light" systemPrefersDark={false}><I18nProvider>
|
||||
<AppShell pageTitle="Page" breadcrumbs={[]} pathname="/dashboard" nav={[]} navBottom={[]}
|
||||
drawerOpen={false} onNavigate={jest.fn()} onToggleDrawer={jest.fn()} user={{ userName: "Sample" }}
|
||||
buildMetadata={{ version: "2.4.1", commitSha: commit }}><div>Sample content</div></AppShell>
|
||||
</I18nProvider></JobTrackerThemeProvider>);
|
||||
fireEvent.click(screen.getByRole("button", { name: user }));
|
||||
expect(screen.getByRole("menu")).toHaveTextContent(expected);
|
||||
expect(within(screen.getByRole("menu")).getByText(/2\.4\.1/)).toBeVisible();
|
||||
});
|
||||
@@ -357,7 +357,7 @@ export function SubmittedPackagesSection({ jobId, allowCapture = true }: { jobId
|
||||
</Button>
|
||||
{item.attachments.map((attachment) => (
|
||||
<Button key={attachment.id} size="small" variant="text" disabled={pending} startIcon={<DownloadOutlinedIcon />} onClick={() => void download(item, attachment)} sx={{ alignSelf: "flex-start", maxWidth: "100%", overflowWrap: "anywhere", textAlign: "left" }}>
|
||||
{attachment.fileName} · {(attachment.fileSize / 1024).toFixed(1)} KB
|
||||
{attachment.fileName} · {new Intl.NumberFormat(language === "nb" ? "nb-NO" : "en", { minimumFractionDigits: 1, maximumFractionDigits: 1 }).format(attachment.fileSize / 1024)} KB
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
@@ -47,11 +47,12 @@ interface AttachmentItem {
|
||||
useForAi: boolean;
|
||||
}
|
||||
|
||||
function fmtSize(n: number) {
|
||||
function fmtSize(n: number, language: string) {
|
||||
if (!n) return "";
|
||||
const kb = n / 1024;
|
||||
if (kb < 1024) return `${kb.toFixed(0)} KB`;
|
||||
return `${(kb / 1024).toFixed(1)} MB`;
|
||||
const digits = kb < 1024 ? 0 : 1;
|
||||
const value = new Intl.NumberFormat(language === "nb" ? "nb-NO" : "en", { minimumFractionDigits: digits, maximumFractionDigits: digits }).format(kb < 1024 ? kb : kb / 1024);
|
||||
return `${value} ${kb < 1024 ? "KB" : "MB"}`;
|
||||
}
|
||||
|
||||
function isImageType(t: string) {
|
||||
@@ -303,7 +304,7 @@ export default function Attachments({ jobId }: { jobId: number }) {
|
||||
</TableCell>
|
||||
<TableCell>{kind}</TableCell>
|
||||
<TableCell sx={{ color: "text.secondary" }}>{a.fileType ? a.fileType.replace("application/", "") : ""}</TableCell>
|
||||
<TableCell sx={{ color: "text.secondary" }}>{a.fileSize ? fmtSize(a.fileSize) : ""}</TableCell>
|
||||
<TableCell sx={{ color: "text.secondary" }}>{a.fileSize ? fmtSize(a.fileSize, language) : ""}</TableCell>
|
||||
<TableCell sx={{ color: "text.secondary" }}>{a.uploadDate ? new Date(a.uploadDate).toLocaleString(language === "nb" ? "nb-NO" : "en") : ""}</TableCell>
|
||||
<TableCell>
|
||||
<FormControl size="small" fullWidth>
|
||||
|
||||
@@ -38,6 +38,27 @@ function deferred<T>() {
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
describe.each(['en', 'nb'] as const)('%s uploaded wall-clock dates', locale => {
|
||||
test.each(['2026-02-30T09:15:00', '2026-02-12T24:15:00', '2026-02-12T09:61:00', '2026-02-12T09:15:00junk'])('does not invent a timestamp from %s', async recordedAt => {
|
||||
localStorage.setItem('uiLanguage', locale);
|
||||
mockedApi.get.mockResolvedValue({ data: page([item({ recordedAt, dateKind: 'uploaded-local' })]) } as never);
|
||||
renderPage();
|
||||
expect(await screen.findByText(copy('documentsDateUnknown', {}, locale))).toBeVisible();
|
||||
expect(document.querySelector('time')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('preserves fractional-second wall-clock metadata without timezone conversion', async () => {
|
||||
localStorage.setItem('uiLanguage', locale);
|
||||
const recordedAt = '2026-02-12T09:15:00.1234567';
|
||||
mockedApi.get.mockResolvedValue({ data: page([item({ recordedAt, dateKind: 'uploaded-local' })]) } as never);
|
||||
renderPage();
|
||||
await screen.findByText('Metadata CV ÆØÅ', { exact: true });
|
||||
const time = document.querySelector('time');
|
||||
expect(time).toHaveAttribute('datetime', recordedAt);
|
||||
expect(time).toHaveTextContent(new Intl.DateTimeFormat(locale === 'nb' ? 'nb-NO' : 'en-GB', { dateStyle: 'medium', timeStyle: 'short', timeZone: 'UTC' }).format(new Date('2026-02-12T09:15:00Z')));
|
||||
});
|
||||
});
|
||||
|
||||
function Probe() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -559,8 +559,7 @@ export default function AppShell({
|
||||
>
|
||||
{isMobile && buildVersion ? (
|
||||
<ListSubheader disableSticky role="presentation" sx={{ maxWidth: 280, py: 1, lineHeight: 1.5, overflowWrap: "anywhere" }}>
|
||||
<Typography variant="body2">{t("applicationVersion", { version: buildVersion })}</Typography>
|
||||
{buildCommit ? <Typography variant="caption">commit {buildCommit}</Typography> : null}
|
||||
<Typography variant="body2">{buildCommit ? t("applicationVersionCommit", { version: buildVersion, commit: buildCommit }) : t("applicationVersion", { version: buildVersion })}</Typography>
|
||||
</ListSubheader>
|
||||
) : null}
|
||||
<MenuItem
|
||||
|
||||
@@ -214,9 +214,16 @@ export default function DocumentsPage() {
|
||||
const label: TranslationKey = item.dateKind === "uploaded-local" ? "documentsUploadedLocal" : item.dateKind === "captured-utc" ? "documentsCaptured" : item.dateKind === "history-recorded-utc" ? "documentsHistoryRecorded" : "documentsUpdated";
|
||||
let value: Date | null = null;
|
||||
if (item.recordedAt && item.dateKind === "uploaded-local") {
|
||||
const local = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)/.exec(item.recordedAt);
|
||||
const local = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(?:\.\d{1,7})?$/.exec(item.recordedAt);
|
||||
// UTC is only a formatting carrier for the unzoned wall-clock components.
|
||||
if (local) value = new Date(Date.UTC(+local[1], +local[2] - 1, +local[3], +local[4], +local[5], +local[6]));
|
||||
if (local) {
|
||||
const parts = local.slice(1).map(Number);
|
||||
const candidate = new Date(0);
|
||||
candidate.setUTCFullYear(parts[0], parts[1] - 1, parts[2]);
|
||||
candidate.setUTCHours(parts[3], parts[4], parts[5], 0);
|
||||
// Date normalises impossible fields; show unknown rather than inventing a date.
|
||||
if ([candidate.getUTCFullYear(), candidate.getUTCMonth() + 1, candidate.getUTCDate(), candidate.getUTCHours(), candidate.getUTCMinutes(), candidate.getUTCSeconds()].every((part, index) => part === parts[index])) value = candidate;
|
||||
}
|
||||
} else if (item.recordedAt && ["updated-utc", "captured-utc", "history-recorded-utc"].includes(item.dateKind)) value = new Date(item.recordedAt);
|
||||
if (!value || Number.isNaN(value.valueOf())) return <Typography variant="body2" color="text.secondary">{t("documentsDateUnknown")}</Typography>;
|
||||
return <Box><Typography variant="caption" color="text.secondary">{t(label)}</Typography><Typography variant="body2" component="time" dateTime={item.recordedAt!} sx={{ display: "block" }}>{value.toLocaleString(locale, { dateStyle: "medium", timeStyle: "short", ...(item.dateKind === "uploaded-local" ? { timeZone: "UTC" } : {}) })}</Typography></Box>;
|
||||
|
||||
Reference in New Issue
Block a user