diff --git a/job-tracker-ui/src/cv-builder-deep-link.test.tsx b/job-tracker-ui/src/cv-builder-deep-link.test.tsx index 3706de7b..a4cfe016 100644 --- a/job-tracker-ui/src/cv-builder-deep-link.test.tsx +++ b/job-tracker-ui/src/cv-builder-deep-link.test.tsx @@ -4,7 +4,7 @@ import { act, fireEvent, render, screen, waitFor, within } from '@testing-librar import { createMemoryRouter, RouterProvider } from 'react-router-dom'; import CvBuilderEditor from './views/CvBuilderEditor'; -import { I18nProvider } from './i18n/I18nProvider'; +import { I18nProvider, useI18n } from './i18n/I18nProvider'; import { translations } from './i18n/translations'; import { ToastProvider } from './toast'; import { api } from './api'; @@ -41,13 +41,14 @@ const variant = { updatedAtUtc: '2026-07-19T10:00:00Z', }; -function renderAt(id: number, strict = false) { +function renderAt(id: number, strict = false, extra?: React.ReactNode) { const router = createMemoryRouter([ { path: '/career/builder/:id', element: }, { path: '/career/builder', element:
CV list destination
}, ], { initialEntries: [`/career/builder/${id}`] }); const tree = ( + {extra} @@ -85,6 +86,38 @@ function routeGet(onVariant: () => Promise, outline: any = { sections: [] } }); } +function SwitchCountLanguage() { + const { language, setLocalLanguage } = useI18n(); + return ; +} + +test.each((['en', 'nb'] as const).flatMap(language => ['profile', 'custom'].flatMap(source => [0, 1, 2].map(count => [language, source, count] as const))))('section count grammar: %s %s %s updates locale without saving', async (language, source, count) => { + localStorage.setItem('uiLanguage', language); + const items = Array.from({ length: count }, (_, i) => `Recorded item ${i}`); + const key = source === 'custom' ? 'custom:counted' : 'summary'; + const settings = { ...variant.settings, sectionsExplicit: true, sections: [{ key, title: 'Counted section' }], customSections: source === 'custom' ? [{ key: 'counted', title: 'Counted section', contentType: 'paragraphs', items }] : [] }; + routeGet(() => Promise.resolve({ data: { ...variant, settings } }), { sections: [{ key: 'summary', title: 'Recorded summary', kind: 'bullets', bullets: items, tags: [], entries: [] }] }); + renderAt(3, false, ); + const group = await screen.findByRole('group', { name: 'Counted section' }); + const label = (locale: 'en' | 'nb') => `${count} ${locale === 'en' ? count === 1 ? 'entry' : 'entries' : count === 1 ? 'oppføring' : 'oppføringer'}`; + expect(group).toHaveTextContent(label(language)); + fireEvent.click(screen.getByRole('button', { name: 'Switch count language' })); + expect(group).toHaveTextContent(label(language === 'en' ? 'nb' : 'en')); + expect(mockedApi.put).not.toHaveBeenCalled(); +}); + +test.each((['en', 'nb'] as const).flatMap(language => [1, 2].map(pages => [language, pages] as const)))('page count grammar: %s %s does not change document state', async (language, pages) => { + localStorage.setItem('uiLanguage', language); + routeGet(() => Promise.resolve({ data: variant })); renderAt(3, false, ); + const frame = await screen.findByTitle(translations[language].cvEditorPreviewTitle); + await waitFor(() => expect(frame).toHaveAttribute('srcdoc', expect.stringContaining('

cv

'))); + const document = (frame as HTMLIFrameElement).contentDocument!; + document.documentElement.dataset.cvPreviewReady = 'true'; document.documentElement.dataset.cvPreviewPages = String(pages); fireEvent.load(frame); + const label = (locale: 'en' | 'nb') => locale === 'en' ? `${pages}-page CV` : `CV på ${pages} ${pages === 1 ? 'side' : 'sider'}`; + await screen.findByText(label(language)); fireEvent.click(screen.getByRole('button', { name: 'Switch count language' })); await screen.findByText(label(language === 'en' ? 'nb' : 'en')); + expect(mockedApi.put).not.toHaveBeenCalled(); +}); + async function openContentTab() { await screen.findByDisplayValue('Backend CV'); fireEvent.click(screen.getByRole('tab', { name: 'Content' })); diff --git a/job-tracker-ui/src/i18n/translations.ts b/job-tracker-ui/src/i18n/translations.ts index 1230e169..bde6d07b 100644 --- a/job-tracker-ui/src/i18n/translations.ts +++ b/job-tracker-ui/src/i18n/translations.ts @@ -918,6 +918,7 @@ export const translations = { cvEditorSourceProfile: "Source: Career Profile", cvEditorReordered: "{item}: position {position} of {total}.", cvEditorSectionEntryCount: "{count} entries", + cvEditorSectionSingleEntry: "{count} entry", cvEditorHistoryScope: "These are CV settings revisions. Restoring uses your current Career Profile, including any later changes to inherited content. Recorded application packages preserve separate historical documents.", cvEditorGoToPage: "Go to page", assetsLetterWorkspace: "Letter workspace", @@ -1168,6 +1169,7 @@ export const translations = { cvEditorDocument: "Document", cvEditorPageSize: "Page size", cvEditorPageCount: "{count}-page CV", + cvEditorSinglePageCount: "{count}-page CV", cvEditorUsLetter: "US Letter", cvEditorDateLongExample: "January 2020", cvEditorDateShortExample: "Jan 2020", @@ -4136,6 +4138,7 @@ export const translations = { cvEditorSourceProfile: "Kilde: Karriereprofil", cvEditorReordered: "{item}: plassering {position} av {total}.", cvEditorSectionEntryCount: "{count} oppføringer", + cvEditorSectionSingleEntry: "{count} oppføring", cvEditorHistoryScope: "Dette er versjoner av CV-innstillinger. Gjenoppretting bruker gjeldende karriereprofil, også senere endringer i arvet innhold. Registrerte søknadspakker bevarer egne historiske dokumenter.", cvEditorGoToPage: "Gå til side", assetsLetterWorkspace: "Søknadsbrev", @@ -4386,6 +4389,7 @@ export const translations = { cvEditorDocument: "Dokument", cvEditorPageSize: "Sidestørrelse", cvEditorPageCount: "CV på {count} sider", + cvEditorSinglePageCount: "CV på {count} side", cvEditorUsLetter: "US Letter", cvEditorDateLongExample: "Januar 2020", cvEditorDateShortExample: "Jan. 2020", diff --git a/job-tracker-ui/src/views/CvBuilderEditor.tsx b/job-tracker-ui/src/views/CvBuilderEditor.tsx index df14c1b8..5e9b6ed4 100644 --- a/job-tracker-ui/src/views/CvBuilderEditor.tsx +++ b/job-tracker-ui/src/views/CvBuilderEditor.tsx @@ -711,7 +711,9 @@ export default function CvBuilderEditor() { {t("cvEditorPreviewScale", { percent: Math.round(zoom * 100) })} - {previewBusy ? t("cvEditorUpdatingPreview") : previewReady ? t("cvEditorPageCount", { count: pages }) : null} + {previewBusy ? t("cvEditorUpdatingPreview") : previewReady ? ( + pages === 1 ? t("cvEditorSinglePageCount", { count: pages }) : t("cvEditorPageCount", { count: pages }) + ) : null} {!!html && pages > 1 && { const pane = scrollRef.current; @@ -1286,6 +1288,7 @@ function SectionRow({ }); const sourceItems = outlineSection?.kind === "tags" ? outlineSection.tags : outlineSection?.bullets ?? []; const editableItems = row.items ?? sourceItems; + const entryCount = customSection?.items.length ?? (outlineSection?.kind === "entries" ? outlineSection.entries.length : editableItems.length); const customText = customSection?.items.join("\n\n") ?? ""; return ( @@ -1324,7 +1327,7 @@ function SectionRow({ - {t(row.hidden ? "cvEditorOmitted" : "cvEditorIncluded")} · {t("cvEditorSectionEntryCount", { count: customSection?.items.length ?? (outlineSection?.kind === "entries" ? outlineSection.entries.length : editableItems.length) })} · {t(customSection ? "cvEditorCvOnly" : "cvEditorSourceProfile")} + {t(row.hidden ? "cvEditorOmitted" : "cvEditorIncluded")} · {entryCount === 1 ? t("cvEditorSectionSingleEntry", { count: entryCount }) : t("cvEditorSectionEntryCount", { count: entryCount })} · {t(customSection ? "cvEditorCvOnly" : "cvEditorSourceProfile")}