From 3bb792f1dc07978ab8828d03c8dd760f689899b5 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Fri, 28 Aug 2026 20:35:20 +0200 Subject: [PATCH] feat(i18n): localise career workspace overview --- .../src/career-workspace-page.test.tsx | 28 +++++-- job-tracker-ui/src/i18n/translations.ts | 80 +++++++++++++++++++ .../views/career/CareerWorkspaceOverview.tsx | 58 +++++++------- 3 files changed, 132 insertions(+), 34 deletions(-) diff --git a/job-tracker-ui/src/career-workspace-page.test.tsx b/job-tracker-ui/src/career-workspace-page.test.tsx index a3dac27..1d6f61c 100644 --- a/job-tracker-ui/src/career-workspace-page.test.tsx +++ b/job-tracker-ui/src/career-workspace-page.test.tsx @@ -4,6 +4,7 @@ import { render, screen, waitFor } from "@testing-library/react"; import { cvBuilderApi } from "./cvBuilder"; import CareerWorkspaceOverview, { CareerWorkspaceImportRun } from "./views/career/CareerWorkspaceOverview"; +import { I18nProvider } from "./i18n/I18nProvider"; jest.mock("./cvBuilder", () => ({ cvBuilderApi: { list: jest.fn() }, @@ -25,20 +26,24 @@ function renderOverview({ loadError?: string | null; } = {}) { return render( - , + + + , ); } beforeEach(() => { + window.localStorage.removeItem("uiLanguage"); mockedList.mockResolvedValue([]); }); afterEach(() => { + window.localStorage.removeItem("uiLanguage"); jest.clearAllMocks(); }); @@ -55,6 +60,17 @@ test("first-run workspace presents concise profile, import, general CV and job-s expect(await screen.findByText("No CV documents yet.")).toBeInTheDocument(); }); +test("career workspace overview and import state render in Bokmål", async () => { + window.localStorage.setItem("uiLanguage", "nb"); + renderOverview({ runs: [{ id: 7, status: "pending_review" }] }); + + expect(screen.getByRole("heading", { name: "Karriereområde", level: 1 })).toBeInTheDocument(); + expect(screen.getAllByText("Se gjennom importert CV", { selector: "a" })).toHaveLength(2); + expect(screen.getByText("Én import venter på godkjenning.")).toBeInTheDocument(); + expect(screen.getByRole("navigation", { name: "Handlinger i karriereområdet" })).toBeInTheDocument(); + expect(await screen.findByText("Ingen CV-dokumenter ennå.")).toBeInTheDocument(); +}); + test("returning workspace shows missing profile information and recent general and job-specific CVs", async () => { mockedList.mockResolvedValue([ { id: 8, name: "Backend CV", themeId: "modern", publicSlug: "backend", isPublic: false, version: 2, jobApplicationId: 42, updatedAtUtc: "2026-08-08T10:00:00Z" }, diff --git a/job-tracker-ui/src/i18n/translations.ts b/job-tracker-ui/src/i18n/translations.ts index 5ceda24..00fb911 100644 --- a/job-tracker-ui/src/i18n/translations.ts +++ b/job-tracker-ui/src/i18n/translations.ts @@ -427,6 +427,46 @@ export const translations = { cvAiGrammar: "Fix grammar", cvAiImpact: "Add measurable impact", cvAiBullets: "Suggest bullet points", + careerOverviewTitle: "Career Workspace", + careerOverviewSubtitle: "Choose what you want to work on next.", + careerOverviewOpenBuilder: "Open CV Builder", + careerOverviewFirstRun: "Start by adding career information manually or importing a CV.", + careerOverviewUnavailable: "Workspace status is unavailable. The profile editor below can retry the request.", + careerOverviewProfileStatus: "Profile status", + careerOverviewCompleteness: "Profile completeness", + careerOverviewCompletenessLabel: "Career profile completeness", + careerOverviewAddNext: "Add next: {items}", + careerOverviewImproveProfile: "Improve profile", + careerOverviewCvImport: "CV import", + careerOverviewReviewImport: "Review imported CV", + careerOverviewReviewOne: "1 import is waiting for your approval.", + careerOverviewReviewMany: "{count} imports are waiting for your approval.", + careerOverviewViewProcessing: "View CV processing", + careerOverviewProcessingOne: "1 CV import is processing.", + careerOverviewProcessingMany: "{count} CV imports are processing.", + careerOverviewResolveImport: "Resolve CV import", + careerOverviewFailedOne: "1 CV import needs attention.", + careerOverviewFailedMany: "{count} CV imports need attention.", + careerOverviewImportCv: "Import a CV", + careerOverviewImportComplete: "Your latest CV import is complete.", + careerOverviewNoImport: "No CV has been imported yet.", + careerOverviewActions: "Career workspace actions", + careerOverviewCareerProfile: "Career profile", + careerOverviewCareerProfileBody: "Add or improve the facts used in every CV.", + careerOverviewEditProfile: "Edit career profile", + careerOverviewImportBody: "Extract information, then review every proposed change before applying it.", + careerOverviewGeneralCv: "General CV", + careerOverviewGeneralCvBody: "Create and manage reusable CV documents from your profile.", + careerOverviewCreateGeneralCv: "Create a general CV", + careerOverviewJobCv: "Job-specific CV", + careerOverviewJobCvBody: "Open a saved job to tailor and attach a CV for that application.", + careerOverviewChooseJob: "Choose a job", + careerOverviewRecentCvs: "Recent CVs", + careerOverviewViewAll: "View all CVs", + careerOverviewRecentFailed: "Recent CVs could not be loaded. Open the CV Builder to try again.", + careerOverviewNoCvs: "No CV documents yet.", + careerOverviewUpdated: "Updated {date}", + careerOverviewJobSpecific: "Job-specific", richTextBold: "Bold", richTextBoldPlaceholder: "bold text", richTextItalic: "Italic", @@ -2053,6 +2093,46 @@ export const translations = { cvAiGrammar: "Rett grammatikk", cvAiImpact: "Legg til målbar effekt", cvAiBullets: "Foreslå punkter", + careerOverviewTitle: "Karriereområde", + careerOverviewSubtitle: "Velg hva du vil arbeide med videre.", + careerOverviewOpenBuilder: "Åpne CV-bygger", + careerOverviewFirstRun: "Start med å legge til karriereinformasjon manuelt eller importere en CV.", + careerOverviewUnavailable: "Status for karriereområdet er utilgjengelig. Profilredigeringen nedenfor kan prøve forespørselen på nytt.", + careerOverviewProfileStatus: "Profilstatus", + careerOverviewCompleteness: "Hvor komplett profilen er", + careerOverviewCompletenessLabel: "Hvor komplett karriereprofilen er", + careerOverviewAddNext: "Legg til neste: {items}", + careerOverviewImproveProfile: "Forbedre profil", + careerOverviewCvImport: "CV-import", + careerOverviewReviewImport: "Se gjennom importert CV", + careerOverviewReviewOne: "Én import venter på godkjenning.", + careerOverviewReviewMany: "{count} importer venter på godkjenning.", + careerOverviewViewProcessing: "Vis CV-behandling", + careerOverviewProcessingOne: "Én CV-import behandles.", + careerOverviewProcessingMany: "{count} CV-importer behandles.", + careerOverviewResolveImport: "Løs problem med CV-import", + careerOverviewFailedOne: "Én CV-import trenger oppmerksomhet.", + careerOverviewFailedMany: "{count} CV-importer trenger oppmerksomhet.", + careerOverviewImportCv: "Importer en CV", + careerOverviewImportComplete: "Den siste CV-importen er fullført.", + careerOverviewNoImport: "Ingen CV er importert ennå.", + careerOverviewActions: "Handlinger i karriereområdet", + careerOverviewCareerProfile: "Karriereprofil", + careerOverviewCareerProfileBody: "Legg til eller forbedre fakta som brukes i alle CV-er.", + careerOverviewEditProfile: "Rediger karriereprofil", + careerOverviewImportBody: "Trekk ut informasjon, og se deretter gjennom alle foreslåtte endringer før de brukes.", + careerOverviewGeneralCv: "Generell CV", + careerOverviewGeneralCvBody: "Opprett og administrer gjenbrukbare CV-dokumenter fra profilen din.", + careerOverviewCreateGeneralCv: "Opprett en generell CV", + careerOverviewJobCv: "Jobbspesifikk CV", + careerOverviewJobCvBody: "Åpne en lagret jobb for å tilpasse og koble en CV til søknaden.", + careerOverviewChooseJob: "Velg en jobb", + careerOverviewRecentCvs: "Nylige CV-er", + careerOverviewViewAll: "Vis alle CV-er", + careerOverviewRecentFailed: "Nylige CV-er kunne ikke lastes. Åpne CV-byggeren for å prøve igjen.", + careerOverviewNoCvs: "Ingen CV-dokumenter ennå.", + careerOverviewUpdated: "Oppdatert {date}", + careerOverviewJobSpecific: "Jobbspesifikk", richTextBold: "Fet", richTextBoldPlaceholder: "fet tekst", richTextItalic: "Kursiv", diff --git a/job-tracker-ui/src/views/career/CareerWorkspaceOverview.tsx b/job-tracker-ui/src/views/career/CareerWorkspaceOverview.tsx index 367cde8..05f6767 100644 --- a/job-tracker-ui/src/views/career/CareerWorkspaceOverview.tsx +++ b/job-tracker-ui/src/views/career/CareerWorkspaceOverview.tsx @@ -8,6 +8,7 @@ import WorkOutlineIcon from "@mui/icons-material/WorkOutline"; import { CvVariantSummary, cvBuilderApi } from "../../cvBuilder"; import type { UserOperation } from "../../types"; +import { useI18n } from "../../i18n/I18nProvider"; export type CareerWorkspaceCompleteness = { percent: number; @@ -54,6 +55,7 @@ export default function CareerWorkspaceOverview({ completeness, runs, loading, l loading: boolean; loadError: string | null; }) { + const { language, t } = useI18n(); const [recentCvs, setRecentCvs] = useState([]); const [recentLoading, setRecentLoading] = useState(true); const [recentError, setRecentError] = useState(false); @@ -82,11 +84,11 @@ export default function CareerWorkspaceOverview({ completeness, runs, loading, l const review = runs.filter((run) => run.status === "pending_review"); const active = runs.filter(isActive); const failed = runs.filter((run) => run.status === "failed" || run.operation?.status === "failed"); - if (review.length > 0) return { severity: "warning" as const, label: "Review imported CV", message: `${review.length} import${review.length === 1 ? " is" : "s are"} waiting for your approval.` }; - if (active.length > 0) return { severity: "info" as const, label: "View CV processing", message: `${active.length} CV import${active.length === 1 ? " is" : "s are"} processing.` }; - if (failed.length > 0) return { severity: "error" as const, label: "Resolve CV import", message: `${failed.length} CV import${failed.length === 1 ? " needs" : "s need"} attention.` }; - return { severity: "success" as const, label: "Import a CV", message: runs.length > 0 ? "Your latest CV import is complete." : "No CV has been imported yet." }; - }, [runs]); + if (review.length > 0) return { severity: "warning" as const, label: t("careerOverviewReviewImport"), message: t(review.length === 1 ? "careerOverviewReviewOne" : "careerOverviewReviewMany", { count: review.length }) }; + if (active.length > 0) return { severity: "info" as const, label: t("careerOverviewViewProcessing"), message: t(active.length === 1 ? "careerOverviewProcessingOne" : "careerOverviewProcessingMany", { count: active.length }) }; + if (failed.length > 0) return { severity: "error" as const, label: t("careerOverviewResolveImport"), message: t(failed.length === 1 ? "careerOverviewFailedOne" : "careerOverviewFailedMany", { count: failed.length }) }; + return { severity: "success" as const, label: t("careerOverviewImportCv"), message: t(runs.length > 0 ? "careerOverviewImportComplete" : "careerOverviewNoImport") }; + }, [language, runs, t]); const isFirstRun = !loading && !recentLoading && !loadError && (completeness?.percent ?? 0) === 0 && runs.length === 0 && recentCvs.length === 0; @@ -95,65 +97,65 @@ export default function CareerWorkspaceOverview({ completeness, runs, loading, l - Career Workspace - Choose what you want to work on next. + {t("careerOverviewTitle")} + {t("careerOverviewSubtitle")} - + - {isFirstRun ? Start by adding career information manually or importing a CV. : null} - {loadError ? Workspace status is unavailable. The profile editor below can retry the request. : null} + {isFirstRun ? {t("careerOverviewFirstRun")} : null} + {loadError ? {t("careerOverviewUnavailable")} : null} - + - Profile completeness + {t("careerOverviewCompleteness")} {loading ? : {completeness?.percent ?? 0}%} - + {!loading && completeness?.missing.length ? ( - Add next: {completeness.missing.slice(0, 3).join(", ")} + {t("careerOverviewAddNext", { items: completeness.missing.slice(0, 3).join(", ") })} ) : null} - + - CV import + {t("careerOverviewCvImport")} {importState.message} - - } /> - } /> - } /> - } /> + + } /> + } /> + } /> + } /> - Recent CVs - + {t("careerOverviewRecentCvs")} + {recentLoading ? ( ) : recentError ? ( - Recent CVs could not be loaded. Open the CV Builder to try again. + {t("careerOverviewRecentFailed")} ) : recentCvs.length === 0 ? ( - No CV documents yet. + {t("careerOverviewNoCvs")} ) : ( {recentCvs.map((cv) => ( {cv.name} - Updated {new Date(cv.updatedAtUtc).toLocaleDateString()} + {t("careerOverviewUpdated", { date: new Date(cv.updatedAtUtc).toLocaleDateString(language === "nb" ? "nb-NO" : "en-GB") })} - {cv.jobApplicationId ? : null} - + {cv.jobApplicationId ? : null} + ))}