fix(i18n): use singular mail label counts in English and Bokmal
This commit is contained in:
@@ -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 });
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user