fix(i18n): use singular CV entry and page captions

This commit is contained in:
cesnimda
2026-11-04 13:16:44 +01:00
parent 7e2bf20187
commit 122956b6a3
3 changed files with 44 additions and 4 deletions
@@ -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: <CvBuilderEditor /> },
{ path: '/career/builder', element: <div>CV list destination</div> },
], { initialEntries: [`/career/builder/${id}`] });
const tree = (
<I18nProvider>
{extra}
<ToastProvider>
<ConfirmProvider>
<PromptProvider>
@@ -85,6 +86,38 @@ function routeGet(onVariant: () => Promise<any>, outline: any = { sections: [] }
});
}
function SwitchCountLanguage() {
const { language, setLocalLanguage } = useI18n();
return <button onClick={() => setLocalLanguage(language === 'en' ? 'nb' : 'en')}>Switch count language</button>;
}
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, <SwitchCountLanguage />);
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, <SwitchCountLanguage />);
const frame = await screen.findByTitle(translations[language].cvEditorPreviewTitle);
await waitFor(() => expect(frame).toHaveAttribute('srcdoc', expect.stringContaining('<p>cv</p>')));
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' }));
+4
View File
@@ -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",
+5 -2
View File
@@ -711,7 +711,9 @@ export default function CvBuilderEditor() {
<Typography variant="caption" color="text.secondary">{t("cvEditorPreviewScale", { percent: Math.round(zoom * 100) })}</Typography>
</Box>
<Typography variant="caption" color="text.secondary" role="status">
{previewBusy ? t("cvEditorUpdatingPreview") : previewReady ? t("cvEditorPageCount", { count: pages }) : null}
{previewBusy ? t("cvEditorUpdatingPreview") : previewReady ? (
pages === 1 ? t("cvEditorSinglePageCount", { count: pages }) : t("cvEditorPageCount", { count: pages })
) : null}
</Typography>
{!!html && pages > 1 && <TextField select size="small" disabled={!previewReady} label={t("cvEditorGoToPage")} value="" sx={{ minWidth: 140 }} onChange={(event) => {
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({
</IconButton>
</Stack>
<Typography variant="caption" color="text.secondary" sx={{ display: "block", px: 1.5, pb: 1 }}>
{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")}
</Typography>
<Collapse in={expanded} unmountOnExit>
<Box ref={contentRef} sx={{ py: 1.25 }}>