fix(i18n): use singular mail label counts in English and Bokmal

This commit is contained in:
cesnimda
2026-11-04 08:16:01 +01:00
parent 1881fdb224
commit cd116ab8ca
7 changed files with 42 additions and 5 deletions
@@ -0,0 +1,9 @@
# Mail label-count localisation
4 November 2026. Continue from published/independently verified1881fdb2, runtimea9208ab4. Actual EN/light320 and NB/dark1440 captures showed `1 labels` / `1 etiketter`. The only caller uses a plural catalogue key regardless of count.
Add a paired singular `emailInboxLabelCount` entry and select it only for1. Preserve the plural entry, counts, zero-count hiding, provider label text, metadata, actions and message context. Both calls use literal keys; this adds no dynamic translation call beyond the separately reviewed historical188/new public7. Generated content and stored language/data remain unchanged.
RED: two initial EN/NB cases failed;39 other inbox cases filtered. GREEN:44 cases/two suites PASSED (41 inbox including both new singular/plural/live-switch regressions, three catalogue cases). Locale switching updates1/2 labels without additional reads or mutations. This replaces the overlapping39 inbox total, not39+44. Types/full lint and scoped verifier lint passed.
Pending: source commit/export build and focused36-configuration label display run, source/export/evidence checks and publication. The existing144-state mail workflow qualification is reused for unchanged behavior; label-only mode explicitly skips document/draft/send workflows, expects one state/configuration and zero writes, and captures the actual chip in its viewport. No new provider/mailbox/model, native-language/human/device, final security or release acceptance is implied. Rollback reverts this label/catalogue/test delta; no database or dependency change.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -8,10 +8,11 @@ import assert from 'node:assert/strict';
const compiled = ts.transpileModule(fs.readFileSync(new URL('../src/i18n/translations.ts', import.meta.url), 'utf8'), { compilerOptions: { module: ts.ModuleKind.ESNext } }).outputText;
const { translations } = await import(`data:text/javascript;base64,${Buffer.from(compiled).toString('base64')}`);
const visualMatrix = process.env.A3_VISUAL_MATRIX === '1';
const mailOnly = process.env.A3_MAIL_FLAT === '1';
const labelOnly = process.env.A3_LABEL_COUNT === '1';
const mailOnly = process.env.A3_MAIL_FLAT === '1' || labelOnly;
const widths = process.env.A3_PILOT ? [320] : visualMatrix ? [320, 360, 390, 430, 768, 1024, 1280, 1440, 1920] : [320, 390, 768, 1440];
const expectedJourneys = widths.length * (process.env.A3_PILOT ? 1 : 4);
const out = path.resolve(import.meta.dirname, mailOnly ? '../../docs/ui-modernisation/visual-correction/evidence/mail-flat' : visualMatrix ? '../../docs/ui-modernisation/visual-correction/evidence/documents-inbox' : '../../docs/ui-modernisation/evidence/documents-inbox/browser'); fs.mkdirSync(out, { recursive: true });
const out = path.resolve(import.meta.dirname, labelOnly ? '../../docs/ui-modernisation/visual-correction/evidence/mail-labels' : mailOnly ? '../../docs/ui-modernisation/visual-correction/evidence/mail-flat' : visualMatrix ? '../../docs/ui-modernisation/visual-correction/evidence/documents-inbox' : '../../docs/ui-modernisation/evidence/documents-inbox/browser'); fs.mkdirSync(out, { recursive: true });
const report = { screens: [], journeys: [], errors: [], unexpectedWrites: [], mockedWrites: [], unavailableReads: [], blockedExternal: [] };
const browser = await chromium.launch();
try {
@@ -66,7 +67,13 @@ try {
const download = page.waitForEvent('download'); await docs.getByRole('button', { name: t('documentsDownloadAttachment'), exact: true }).click(); const file = await download; assert.equal(file.suggestedFilename(), document.name); assert.equal(fs.readFileSync(await file.path(), 'utf8'), 'Controlled certificate fixture');
assert.equal(await docs.getByRole('link', { name: t('documentsOpenSource'), exact: true }).getAttribute('href'), document.sourceHref); assert.equal(await docs.locator('time').getAttribute('datetime'), document.recordedAt); await scan('document-filtered', docs.getByText(document.name, { exact: true }));
}
await page.goto('http://localhost:4338/correspondence'); const messages = page.getByRole('region', { name: t('emailInboxLinkedMessages'), exact: true }); await messages.getByText(mail.subject, { exact: true }).waitFor(); await scan('inbox');
await page.goto('http://localhost:4338/correspondence'); const messages = page.getByRole('region', { name: t('emailInboxLinkedMessages'), exact: true }); await messages.getByText(mail.subject, { exact: true }).waitFor();
if (labelOnly) {
const label = messages.getByText(t('emailInboxLabelCount').replace('{count}', '1'), { exact: true }); await expect(label).toBeVisible();
await scan('label-count', label);
report.journeys.push({ width, language, mode, singularRecordedLabelCount: 'PASS', documentFilterFocusDownloadSource: 'NOT RUN', inboxDraftSendWorkflow: 'NOT RUN' }); await context.close(); continue;
}
await scan('inbox');
const initialMailReads = mailReads; await page.getByRole('button', { name: t('emailInboxJumpMessages'), exact: true }).press('Enter'); await expect(messages).toBeFocused(); await messages.getByRole('button', { name: t('emailInboxReturnFilters'), exact: true }).click(); await expect(page.getByRole('textbox', { name: t('correspondenceSearch'), exact: true })).toBeFocused(); assert.equal(mailReads, initialMailReads);
await messages.getByRole('button', { name: t('emailInboxViewMessage'), exact: true }).click(); const messageBody = messages.getByText('Please choose an interview time. <script>inert text</script>', { exact: true }); await messageBody.waitFor();
const article = messages.getByRole('article', { name: mail.subject, exact: true });
@@ -84,6 +91,6 @@ try {
assert.equal(sends, 1); await expect(compose.getByRole('textbox', { name: t('emailInboxMessage'), exact: true })).toHaveValue(draft.bodyText); await scan('uncertain-send', compose.getByRole('textbox', { name: t('emailInboxMessage'), exact: true }));
report.journeys.push({ width, language, mode, documentFilterFocusDownloadSource: mailOnly ? 'NOT RUN' : 'PASS', inboxFocusPlainTextReplySaveExplicitConfirmUncertainNoRetry: 'PASS' }); await context.close();
}
assert.equal(report.journeys.length, expectedJourneys); assert.equal(report.screens.length, report.journeys.length * (mailOnly ? 4 : 6)); assert.equal(report.mockedWrites.length, report.journeys.length * 2); assert.deepEqual(report.errors, []); assert.deepEqual(report.unexpectedWrites, []); assert.ok(report.blockedExternal.every(origin => origin === 'https://fonts.googleapis.com'));
assert.equal(report.journeys.length, expectedJourneys); assert.equal(report.screens.length, report.journeys.length * (labelOnly ? 1 : mailOnly ? 4 : 6)); assert.equal(report.mockedWrites.length, labelOnly ? 0 : report.journeys.length * 2); assert.deepEqual(report.errors, []); assert.deepEqual(report.unexpectedWrites, []); assert.ok(report.blockedExternal.every(origin => origin === 'https://fonts.googleapis.com'));
console.log(`PASS:${report.journeys.length} document/mail journeys,${report.screens.length} axe scans/captures,${report.mockedWrites.length} fixture-only save/send requests.`);
} finally { fs.writeFileSync(path.join(out, process.env.A3_PILOT ? 'PILOT.json' : 'BROWSER.json'), JSON.stringify({ ...report, limits: ['Synthetic metadata/binary/mail/provider fixtures; not live storage, mailbox, OAuth or delivery.', 'Browser covers draft save, explicit confirmation and controlled uncertain outcome only; other recovery/conflict paths remain targeted tests.', 'No real sending, native device, final integrated security or release acceptance.'] }, null, 2)); await browser.close(); }
@@ -118,6 +118,23 @@ function renderPage(initialPath = '/') {
}
describe('CorrespondenceInboxPage', () => {
test.each(['en', 'nb'] as const)('%s label counts keep singular/plural grammar through a live locale change', async language => {
const original = mockedApi.get.getMockImplementation();
mockedApi.get.mockImplementation((url: string, config?: any) => url === '/correspondence/page'
? Promise.resolve({ data: pageData([messageItem({ subject: 'One label', labelCount: 1 }), messageItem({ id: 2, subject: 'Two labels', labelCount: 2 })]) } as any)
: original!(url, config));
localStorage.setItem('uiLanguage', language); renderPage();
await screen.findByRole('article', { name: 'One label' });
const reads = mockedApi.get.mock.calls.length;
const assertLabels = (locale: UiLanguage) => {
expect(messageRow('One label').getByText(locale === 'en' ? '1 label' : '1 etikett', { exact: true })).toBeVisible();
expect(messageRow('Two labels').getByText(locale === 'en' ? '2 labels' : '2 etiketter', { exact: true })).toBeVisible();
};
assertLabels(language);
const other = language === 'en' ? 'nb' : 'en'; await changeLanguage(other); assertLabels(other);
expect(mockedApi.get).toHaveBeenCalledTimes(reads);
expect(mockedApi.post).not.toHaveBeenCalled(); expect(mockedApi.put).not.toHaveBeenCalled(); expect(mockedApi.delete).not.toHaveBeenCalled();
});
test.each(['en', 'nb'] as const)('%s opened message retains one application action in named context', async language => {
localStorage.setItem('uiLanguage', language); renderPage();
await screen.findByText('Interview invite', { exact: true });
+2
View File
@@ -453,6 +453,7 @@ export const translations = {
emailInboxUnknownRole: "Unknown role",
emailInboxThreadId: "Thread {id}",
emailInboxManualInternal: "Manual/internal",
emailInboxLabelCount: "{count} label",
emailInboxLabels: "{count} labels",
emailInboxAttachments: "{count} attachments",
emailInboxHideMessage: "Hide message",
@@ -3669,6 +3670,7 @@ export const translations = {
emailInboxUnknownRole: "Ukjent rolle",
emailInboxThreadId: "Tråd {id}",
emailInboxManualInternal: "Manuell/intern",
emailInboxLabelCount: "{count} etikett",
emailInboxLabels: "{count} etiketter",
emailInboxAttachments: "{count} vedlegg",
emailInboxHideMessage: "Skjul melding",
@@ -842,7 +842,7 @@ export default function CorrespondenceInboxPage() {
{item.direction ? <Chip size="small" label={t(item.direction === "inbound" ? "emailInboxInbound" : item.direction === "outbound" ? "emailInboxOutbound" : "emailInboxInternal")} variant="outlined" /> : null}
{item.provider ? <Chip size="small" label={item.provider === "microsoft" ? "Outlook" : item.provider === "gmail" ? "Gmail" : item.provider.toUpperCase()} variant="outlined" /> : null}
{item.externalThreadId ? <Chip size="small" label={t("emailInboxThreadId", { id: item.externalThreadId })} color="success" variant="outlined" /> : <Chip size="small" label={t("emailInboxManualInternal")} variant="outlined" />}
{item.labelCount > 0 ? <Chip size="small" label={t("emailInboxLabels", { count: item.labelCount })} variant="outlined" /> : null}
{item.labelCount > 0 ? <Chip size="small" label={item.labelCount === 1 ? t("emailInboxLabelCount", { count: item.labelCount }) : t("emailInboxLabels", { count: item.labelCount })} variant="outlined" /> : null}
{item.attachmentCount > 0 ? <Chip size="small" label={t(item.attachmentCount === 1 ? "emailInboxAttachmentCount" : "emailInboxAttachments", { count: item.attachmentCount })} variant="outlined" /> : null}
<Button size="small" variant="text" disabled={!!unlinkingThreadId} aria-expanded={selectedMessageId === item.id} aria-controls={selectedMessageId === item.id ? `${rowId}-${item.id}-message` : undefined} onClick={() => void showMessage(item)}>{selectedMessageId === item.id ? t("emailInboxHideMessage") : t("emailInboxViewMessage")}</Button>
{selectedMessageId !== item.id ? <Button size="small" variant="text" disabled={!!unlinkingThreadId} onClick={() => navigate(`/jobs/${item.jobApplicationId}?section=communication`, { state: { from: location.pathname + location.search + location.hash } })}>{t("emailInboxOpenJob")}</Button> : null}