fix(i18n): use singular CV entry and page captions
This commit is contained in:
@@ -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' }));
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
Reference in New Issue
Block a user