fix(i18n): localize application workflows

Use stable checklist and interview keys for system copy while preserving user-authored content. Localize creation-tab errors, language selectors, match bands, dates, and selected-CV context.
This commit is contained in:
cesnimda
2026-08-29 16:51:59 +02:00
parent 9edcbfc5de
commit a704c75440
11 changed files with 173 additions and 37 deletions
@@ -233,6 +233,18 @@ test("warns before section navigation would discard application edits", async ()
test("renders shared workspace navigation and progress in Norwegian Bokmål", async () => {
window.localStorage.setItem("uiLanguage", "nb");
mockedApi.get.mockImplementation((url: string) => {
if (url === "/jobapplications/42/workspace") return Promise.resolve({ data: {
...overview,
nextStep: {
key: "prepare-cv",
label: "Prepare a CV for this role",
reason: "Attach a CV variant tailored to this application.",
section: "cv",
},
} } as any);
return Promise.resolve({ data: [] } as any);
});
renderTable("/jobs/42");
expect(await screen.findByRole("tab", { name: "Oversikt" })).toHaveAttribute("aria-selected", "true");
@@ -240,6 +252,7 @@ test("renders shared workspace navigation and progress in Norwegian Bokmål", as
expect(screen.getByRole("tab", { name: "Søknadsbrev" })).toBeInTheDocument();
expect(await screen.findByLabelText(/søknadsprogresjon/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /tilbake til søknader/i })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Forbered en CV for denne stillingen" })).toBeInTheDocument();
});
test("hydrates list filters, sort and page from a shareable URL", async () => {
@@ -89,6 +89,11 @@ export const WORKSPACE_SECTIONS: { key: WorkspaceSectionKey; label: string }[] =
{ key: "interview", label: "Interview Prep" },
];
export function checklistSystemTranslationToken(systemKey?: string | null): string | null {
if (!systemKey || systemKey.startsWith("learning:")) return null;
return systemKey.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
}
const LEGACY_WORKSPACE_SECTIONS: Record<string, WorkspaceSectionKey> = {
match: "analysis",
"job-details": "overview",
@@ -26,6 +26,7 @@ import { useI18n } from "../i18n/I18nProvider";
// is application-specific by nature. docs/architecture/application-workspace.md.
function useAsset<T>(load: () => Promise<T>, deps: React.DependencyList) {
const { t } = useI18n();
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
@@ -44,7 +45,7 @@ function useAsset<T>(load: () => Promise<T>, deps: React.DependencyList) {
}
})
.catch((err) => {
if (!cancelled) setError(getApiErrorMessage(err, "Could not load this section."));
if (!cancelled) setError(getApiErrorMessage(err, t("assetsLoadFailed")));
})
.finally(() => {
if (!cancelled) setLoading(false);
@@ -52,7 +53,7 @@ function useAsset<T>(load: () => Promise<T>, deps: React.DependencyList) {
return () => {
cancelled = true;
};
}, [run]);
}, [run, t]);
useEffect(() => reload(), [reload]);
@@ -98,7 +99,7 @@ export function ApplicationCvSection({ jobId }: { jobId: number }) {
setData(await applicationAssetsApi.attachVariant(jobId, variantId));
setError(null);
} catch (err) {
setError(getApiErrorMessage(err, "Could not change the attached CV."));
setError(getApiErrorMessage(err, t("assetsAttachCvFailed")));
} finally {
setBusy(false);
}
@@ -110,11 +111,11 @@ export function ApplicationCvSection({ jobId }: { jobId: number }) {
if (!data?.attachedVariantId) return;
setBusy(true);
try {
const copy = await cvBuilderApi.duplicate(data.attachedVariantId, `${data.attachedVariantName || "CV"} — tailored copy`);
const copy = await cvBuilderApi.duplicate(data.attachedVariantId, t("assetsTailoredCopyName", { name: data.attachedVariantName || "CV" }));
setData(await applicationAssetsApi.attachVariant(jobId, copy.id));
window.location.assign(`/career/builder/${copy.id}`);
} catch (err) {
setError(getApiErrorMessage(err, "Could not create a tailored CV copy."));
setError(getApiErrorMessage(err, t("assetsDuplicateCvFailed")));
} finally {
setBusy(false);
}
@@ -308,7 +309,7 @@ export function ApplicationCoverLetterSection({ jobId, onDirtyChange }: { jobId:
setDraftAiAction(null);
setError(null);
} catch (err) {
setError(getApiErrorMessage(err, "Could not save the cover letter."));
setError(getApiErrorMessage(err, t("assetsCoverSaveFailed")));
} finally {
setBusy(false);
}
@@ -322,7 +323,7 @@ export function ApplicationCoverLetterSection({ jobId, onDirtyChange }: { jobId:
setDraftAiAction(null);
setError(null);
} catch (err) {
setError(getApiErrorMessage(err, "Could not restore that version."));
setError(getApiErrorMessage(err, t("assetsCoverRestoreFailed")));
} finally {
setBusy(false);
}
@@ -394,7 +395,7 @@ export function ApplicationCoverLetterSection({ jobId, onDirtyChange }: { jobId:
{v.isCurrent && <Chip size="small" color="primary" variant="outlined" label={t("assetsCurrent")} />}
</Stack>
<Typography variant="caption" color="text.secondary">
{new Date(v.createdAtUtc).toLocaleString()} · {t("assetsCharacterCount", { count: v.length })}
{new Date(v.createdAtUtc).toLocaleString(language === "nb" ? "nb-NO" : "en")} · {t("assetsCharacterCount", { count: v.length })}
</Typography>
</Box>
{!v.isCurrent && (
@@ -458,7 +459,7 @@ function CoverLetterAiAssistant({ jobId, currentText, onApply }: { jobId: number
});
setSuggestion(result.result.text?.trim() ?? "");
} catch (err) {
setError(getApiErrorMessage(err, "Could not generate a cover-letter suggestion."));
setError(getApiErrorMessage(err, t("coverAiGenerateFailed")));
} finally {
setBusy(false);
}
@@ -499,8 +500,8 @@ function CoverLetterAiAssistant({ jobId, currentText, onApply }: { jobId: number
<FormControl size="small" sx={{ minWidth: 190 }}>
<InputLabel>{t("coverAiDocumentLanguage")}</InputLabel>
<Select label={t("coverAiDocumentLanguage")} value={language} onChange={(event) => setLanguage(event.target.value as "en" | "nb-NO")}>
<MenuItem value="en">English</MenuItem>
<MenuItem value="nb-NO">Norsk bokmål</MenuItem>
<MenuItem value="en">{t("languageEnglish")}</MenuItem>
<MenuItem value="nb-NO">{t("languageNorwegianBokmal")}</MenuItem>
</Select>
</FormControl>
</Stack>
@@ -598,7 +599,7 @@ export function ApplicationPackageDraftsSection({
setError(null);
onSaved?.();
} catch (err) {
setError(getApiErrorMessage(err, "Could not save the application drafts."));
setError(getApiErrorMessage(err, t("assetsDraftsSaveFailed")));
} finally {
setBusy(false);
}
@@ -11,7 +11,7 @@ import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
import { getApiErrorMessage } from "../api";
import { useI18n } from "../i18n/I18nProvider";
import {
CHECKLIST_CATEGORIES, Checklist, ChecklistItem, applicationChecklistApi,
CHECKLIST_CATEGORIES, Checklist, ChecklistItem, applicationChecklistApi, checklistSystemTranslationToken,
} from "../applicationWorkspace";
// Phase 5 Milestone 2 — the application checklist.
@@ -214,8 +214,8 @@ function checklistCategoryLabel(t: (key: any, vars?: Record<string, string | num
}
function checklistItemDisplay(t: (key: any, vars?: Record<string, string | number>) => string, item: ChecklistItem) {
if (!item.systemKey || item.systemKey.startsWith("learning:")) return { title: item.title, description: item.description };
const token = item.systemKey.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
const token = checklistSystemTranslationToken(item.systemKey);
if (!token) return { title: item.title, description: item.description };
return {
title: t(`checklistItem_${token}`),
description: t(`checklistItem_${token}Description`),
@@ -21,6 +21,7 @@ import { useI18n } from "../i18n/I18nProvider";
// One loader for all three sections: same fetch/loading/empty/error shape, so the sections stay
// consistent and each one is just its own rendering.
function useIntelligence<T>(load: () => Promise<T>, deps: React.DependencyList) {
const { t } = useI18n();
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
@@ -38,7 +39,7 @@ function useIntelligence<T>(load: () => Promise<T>, deps: React.DependencyList)
setError(null);
})
.catch((err) => {
if (!cancelled) setError(getApiErrorMessage(err, "Could not load this section."));
if (!cancelled) setError(getApiErrorMessage(err, t("intelligenceLoadFailed")));
})
.finally(() => {
if (!cancelled) setLoading(false);
@@ -46,7 +47,7 @@ function useIntelligence<T>(load: () => Promise<T>, deps: React.DependencyList)
return () => {
cancelled = true;
};
}, [run]);
}, [run, t]);
return { data, error, loading };
}
@@ -110,7 +111,7 @@ function Bullets({ label, values }: { label: string; values: string[] }) {
// ---------- Timeline ----------
export function ApplicationTimeline({ jobId }: { jobId: number }) {
const { t } = useI18n();
const { language, t } = useI18n();
const [category, setCategory] = useState<string>("");
const { data, error, loading } = useIntelligence<Timeline>(
() => applicationIntelligenceApi.timeline(jobId, category || undefined),
@@ -129,7 +130,7 @@ export function ApplicationTimeline({ jobId }: { jobId: number }) {
<Stack key={m.id} direction="row" spacing={1} justifyContent="space-between" alignItems="baseline">
<Typography variant="body2" sx={{ fontWeight: 600 }}>{m.summary}</Typography>
<Typography variant="caption" color="text.secondary">
{new Date(m.at).toLocaleDateString()}
{new Date(m.at).toLocaleDateString(language === "nb" ? "nb-NO" : "en")}
</Typography>
</Stack>
))}
@@ -267,7 +268,7 @@ export function ApplicationMatch({ jobId }: { jobId: number }) {
<SectionShell
title={t("workspaceCvMatch")}
subtitle={data?.selectedCvName
? `${t("workspaceComparingCv")} ${data.selectedCvName}`
? t("workspaceComparingCv", { name: data.selectedCvName })
: t("workspaceSelectCvMatch")}
loading={loading}
error={error}
@@ -286,7 +287,7 @@ export function ApplicationMatch({ jobId }: { jobId: number }) {
<Box>
<Stack direction="row" justifyContent="space-between" alignItems="baseline">
<Typography variant="h4" sx={{ fontWeight: 900 }}>{data?.score}%</Typography>
<Chip size="small" label={data?.band} variant="outlined" />
<Chip size="small" label={matchBandLabel(t, data?.band)} variant="outlined" />
</Stack>
<LinearProgress
variant="determinate"
@@ -369,3 +370,10 @@ function timelineCategoryLabel(t: (key: any) => string, category: string): strin
};
return keys[category] ? t(keys[category]) : category;
}
function matchBandLabel(t: (key: any) => string, band?: string): string {
if (!band) return t("matchScoreBand_Unknown");
return ["Strong", "Partial", "Low", "Unknown"].includes(band)
? t(`matchScoreBand_${band}`)
: band;
}
+23 -11
View File
@@ -61,11 +61,11 @@ export function ApplicationInterviewPrep({ jobId }: { jobId: number }) {
setBoard(await interviewPrepApi.get(jobId));
setError(null);
} catch (err) {
setError(getApiErrorMessage(err, "Could not load interview preparation."));
setError(getApiErrorMessage(err, t("interviewPrepLoadFailed")));
} finally {
setLoading(false);
}
}, [jobId]);
}, [jobId, t]);
useEffect(() => {
load();
@@ -77,7 +77,7 @@ export function ApplicationInterviewPrep({ jobId }: { jobId: number }) {
await run();
await load();
} catch (err) {
setError(getApiErrorMessage(err, "Could not update interview preparation."));
setError(getApiErrorMessage(err, t("interviewPrepUpdateFailed")));
} finally {
setBusy(false);
}
@@ -133,7 +133,7 @@ export function ApplicationInterviewPrep({ jobId }: { jobId: number }) {
(board?.groups ?? []).map((group) => (
<Box key={group.category}>
<Typography variant="caption" sx={{ fontWeight: 800, textTransform: "uppercase", letterSpacing: ".06em", color: "text.secondary" }}>
{group.label}
{interviewCategoryLabel(t, group.category)}
</Typography>
<Stack sx={{ mt: 0.5 }}>
{group.items.map((item) => (
@@ -163,7 +163,7 @@ export function ApplicationInterviewPrep({ jobId }: { jobId: number }) {
sx={{ minWidth: { sm: 200 } }}
>
{INTERVIEW_PREP_CATEGORIES.map((c) => (
<MenuItem key={c.key} value={c.key}>{c.label}</MenuItem>
<MenuItem key={c.key} value={c.key}>{interviewCategoryLabel(t, c.key)}</MenuItem>
))}
</TextField>
<TextField
@@ -271,8 +271,8 @@ function InterviewAiAssistant({ jobId, onAccepted }: { jobId: number; onAccepted
{INTERVIEW_AI_FOCUS.map((item) => <MenuItem key={item.key} value={item.key}>{t(item.labelKey)}</MenuItem>)}
</TextField>
<TextField select size="small" label={t("coverAiDocumentLanguage")} value={language} onChange={(event) => setLanguage(event.target.value as "en" | "nb-NO")} sx={{ minWidth: 190 }}>
<MenuItem value="en">English</MenuItem>
<MenuItem value="nb-NO">Norsk bokmål</MenuItem>
<MenuItem value="en">{t("languageEnglish")}</MenuItem>
<MenuItem value="nb-NO">{t("languageNorwegianBokmal")}</MenuItem>
</TextField>
</Stack>
<TextField multiline minRows={2} fullWidth label={t("coverAiAdditionalInstructions")} value={instructions} onChange={(event) => setInstructions(event.target.value)} />
@@ -317,7 +317,7 @@ function PrepRow({ jobId, item, busy, onChanged, onError }: {
setDraft(null);
onChanged();
} catch (err) {
onError(getApiErrorMessage(err, "Could not save this answer."));
onError(getApiErrorMessage(err, t("interviewPrepAnswerSaveFailed")));
} finally {
setSaving(false);
}
@@ -409,11 +409,11 @@ export function ApplicationFollowUp({ jobId }: { jobId: number }) {
setAction(result.nextAction ?? "");
setError(null);
} catch (err) {
setError(getApiErrorMessage(err, "Could not load follow-up."));
setError(getApiErrorMessage(err, t("interviewFollowUpLoadFailed")));
} finally {
setLoading(false);
}
}, [jobId]);
}, [jobId, t]);
useEffect(() => {
load();
@@ -426,7 +426,7 @@ export function ApplicationFollowUp({ jobId }: { jobId: number }) {
setData(result);
setError(null);
} catch (err) {
setError(getApiErrorMessage(err, "Could not save the follow-up."));
setError(getApiErrorMessage(err, t("interviewFollowUpSaveFailed")));
} finally {
setBusy(false);
}
@@ -476,3 +476,15 @@ export function ApplicationFollowUp({ jobId }: { jobId: number }) {
</Shell>
);
}
function interviewCategoryLabel(t: (key: any) => string, category: string) {
const keys: Record<string, string> = {
"company-research": "interviewCategoryCompanyResearch",
technical: "interviewCategoryTechnical",
behavioural: "interviewCategoryBehavioural",
star: "interviewCategoryStar",
question: "interviewCategoryQuestions",
note: "interviewCategoryNotes",
};
return keys[category] ? t(keys[category]) : category;
}
+44
View File
@@ -79,6 +79,8 @@ export const translations = {
noWord: "No",
createAndAddAnother: "Create & add another",
loading: "Loading...",
languageEnglish: "English",
languageNorwegianBokmal: "Norsk bokmål",
discoverJobs: "Discover jobs",
discoverJobsSubtitle: "Search official job-board feeds and save opportunities to your tracker.",
jobDetails: "Job details",
@@ -103,6 +105,15 @@ export const translations = {
interviewPrepCategory: "Category",
interviewPrepAddLabel: "Add a question, topic or note",
interviewPrepAdd: "Add",
interviewPrepLoadFailed: "Could not load interview preparation.",
interviewPrepUpdateFailed: "Could not update interview preparation.",
interviewPrepAnswerSaveFailed: "Could not save this answer.",
interviewCategoryCompanyResearch: "Company research",
interviewCategoryTechnical: "Technical preparation",
interviewCategoryBehavioural: "Behavioural questions",
interviewCategoryStar: "STAR examples",
interviewCategoryQuestions: "Questions to ask them",
interviewCategoryNotes: "Notes",
interviewAiTitle: "AI interview coach",
interviewAiSubtitle: "Uses the linked CV, job advert and application analysis. Suggestions never overwrite your notes.",
interviewAiUsing: "Using",
@@ -134,7 +145,10 @@ export const translations = {
interviewFollowUpOpenTasks: "{count} open follow-up tasks on the checklist.",
interviewFollowUpDate: "Follow up on",
interviewFollowUpEmpty: "No follow-up scheduled. Applications without one can go quiet.",
interviewFollowUpLoadFailed: "Could not load follow-up.",
interviewFollowUpSaveFailed: "Could not save the follow-up.",
intelligenceMilestones: "Milestones",
intelligenceLoadFailed: "Could not load this section.",
intelligenceTimeline: "Timeline",
intelligenceTimelineSubtitle: "Everything recorded for this application, newest first.",
intelligenceTimelineEmpty: "Nothing has happened yet. Activity appears here as the application moves forward.",
@@ -170,6 +184,10 @@ export const translations = {
intelligenceRelevantProjects: "Relevant projects",
intelligenceSuggestions: "Suggestions",
assetsCvSubtitle: "The CV variant used for this application. Variants tailor the master career profile without duplicating it.",
assetsLoadFailed: "Could not load this section.",
assetsAttachCvFailed: "Could not change the attached CV.",
assetsDuplicateCvFailed: "Could not create a tailored CV copy.",
assetsTailoredCopyName: "{name} — tailored copy",
assetsNoCvVariants: "No CV variants yet. Build one in the CV builder; it starts from the master career profile, so your history stays reusable.",
assetsAttachedCv: "Attached CV variant",
assetsAttachedCvHelp: "Changing this only changes which CV the application uses. The CV itself is untouched.",
@@ -189,6 +207,10 @@ export const translations = {
assetsCoverPlaceholder: "Write it yourself, start from the template, or generate a tailored draft.",
assetsDiscardChanges: "Discard changes",
assetsStartTemplate: "Start from template",
assetsCoverSaveFailed: "Could not save the cover letter.",
assetsCoverRestoreFailed: "Could not restore that version.",
assetsDraftsSaveFailed: "Could not save the application drafts.",
coverAiGenerateFailed: "Could not generate a cover-letter suggestion.",
cvEditorBack: "Back to CVs",
cvEditorName: "CV name",
cvEditorNameRequired: "Enter a name before saving.",
@@ -1865,6 +1887,8 @@ export const translations = {
noWord: "Nei",
createAndAddAnother: "Opprett og legg til en til",
loading: "Laster...",
languageEnglish: "Engelsk",
languageNorwegianBokmal: "Norsk bokmål",
discoverJobs: "Finn jobber",
discoverJobsSubtitle: "Søk i offisielle jobbportaler og lagre muligheter i oversikten din.",
jobDetails: "Jobbdetaljer",
@@ -1889,6 +1913,15 @@ export const translations = {
interviewPrepCategory: "Kategori",
interviewPrepAddLabel: "Legg til spørsmål, tema eller notat",
interviewPrepAdd: "Legg til",
interviewPrepLoadFailed: "Kunne ikke laste intervjuforberedelsen.",
interviewPrepUpdateFailed: "Kunne ikke oppdatere intervjuforberedelsen.",
interviewPrepAnswerSaveFailed: "Kunne ikke lagre dette svaret.",
interviewCategoryCompanyResearch: "Selskapsresearch",
interviewCategoryTechnical: "Teknisk forberedelse",
interviewCategoryBehavioural: "Atferdsbaserte spørsmål",
interviewCategoryStar: "STAR-eksempler",
interviewCategoryQuestions: "Spørsmål du kan stille",
interviewCategoryNotes: "Notater",
interviewAiTitle: "AI-intervjutrening",
interviewAiSubtitle: "Bruker tilknyttet CV, stillingsannonse og søknadsanalyse. Forslag overskriver aldri notatene dine.",
interviewAiUsing: "Bruker",
@@ -1920,7 +1953,10 @@ export const translations = {
interviewFollowUpOpenTasks: "{count} åpne oppfølgingsoppgaver i sjekklisten.",
interviewFollowUpDate: "Følg opp den",
interviewFollowUpEmpty: "Ingen oppfølging er planlagt. Søknader uten oppfølging kan bli stille.",
interviewFollowUpLoadFailed: "Kunne ikke laste oppfølgingen.",
interviewFollowUpSaveFailed: "Kunne ikke lagre oppfølgingen.",
intelligenceMilestones: "Milepæler",
intelligenceLoadFailed: "Kunne ikke laste denne delen.",
intelligenceTimeline: "Tidslinje",
intelligenceTimelineSubtitle: "Alt som er registrert for søknaden, nyeste først.",
intelligenceTimelineEmpty: "Ingenting har skjedd ennå. Aktivitet vises her når søknaden går videre.",
@@ -1956,6 +1992,10 @@ export const translations = {
intelligenceRelevantProjects: "Relevante prosjekter",
intelligenceSuggestions: "Forslag",
assetsCvSubtitle: "CV-varianten som brukes for denne søknaden. Varianter tilpasser karriereprofilen uten å duplisere den.",
assetsLoadFailed: "Kunne ikke laste denne delen.",
assetsAttachCvFailed: "Kunne ikke endre den tilknyttede CV-en.",
assetsDuplicateCvFailed: "Kunne ikke opprette en tilpasset CV-kopi.",
assetsTailoredCopyName: "{name} — tilpasset kopi",
assetsNoCvVariants: "Ingen CV-varianter ennå. Bygg en i CV-byggeren; den starter fra karriereprofilen, slik at historikken kan gjenbrukes.",
assetsAttachedCv: "Tilknyttet CV-variant",
assetsAttachedCvHelp: "Dette endrer bare hvilken CV søknaden bruker. Selve CV-en forblir urørt.",
@@ -1975,6 +2015,10 @@ export const translations = {
assetsCoverPlaceholder: "Skriv selv, start fra malen eller generer et tilpasset utkast.",
assetsDiscardChanges: "Forkast endringer",
assetsStartTemplate: "Start fra mal",
assetsCoverSaveFailed: "Kunne ikke lagre søknadsbrevet.",
assetsCoverRestoreFailed: "Kunne ikke gjenopprette denne versjonen.",
assetsDraftsSaveFailed: "Kunne ikke lagre søknadsutkastene.",
coverAiGenerateFailed: "Kunne ikke generere et forslag til søknadsbrev.",
cvEditorBack: "Tilbake til CV-er",
cvEditorName: "CV-navn",
cvEditorNameRequired: "Skriv inn et navn før du lagrer.",
+15 -1
View File
@@ -57,7 +57,21 @@ function routeGet(overrides: Record<string, any> = {}) {
});
}
beforeEach(() => jest.clearAllMocks());
beforeEach(() => {
jest.clearAllMocks();
window.localStorage.setItem("uiLanguage", "en");
});
test("localizes interview categories while preserving user-authored preparation", async () => {
window.localStorage.setItem("uiLanguage", "nb");
routeGet();
render(<ApplicationInterviewPrep jobId={7} />);
expect((await screen.findAllByText("Selskapsresearch")).length).toBeGreaterThan(0);
expect(screen.getAllByText("Atferdsbaserte spørsmål").length).toBeGreaterThan(0);
expect(screen.getByText("Funding history")).toBeInTheDocument();
});
test("prep renders grouped items with progress and marks AI-sourced ones", async () => {
routeGet();
@@ -35,7 +35,8 @@ import { PIPELINE_STATUSES, normalizeStatus, statusLabel, statusTone } from "../
import { useI18n } from "../i18n/I18nProvider";
import { useConfirm } from "../confirm";
import {
WORKSPACE_SECTIONS, WorkspaceOverview, WorkspaceSectionKey, applicationWorkspaceApi, workspaceSection,
WORKSPACE_SECTIONS, WorkspaceOverview, WorkspaceSectionKey, applicationWorkspaceApi,
checklistSystemTranslationToken, workspaceSection,
} from "../applicationWorkspace";
// Phase 5 Milestone 1 — the dedicated Application Workspace.
@@ -315,16 +316,24 @@ function OverviewSection({ overview, onGo, onReload, onEdit }: {
return <Stack spacing={2}>{[0, 1].map((i) => <Skeleton key={i} variant="rounded" height={120} />)}</Stack>;
}
const nextStepToken = checklistSystemTranslationToken(overview.nextStep?.key);
const nextStepLabel = overview.nextStep
? nextStepToken ? t(`checklistItem_${nextStepToken}` as any) : overview.nextStep.label
: null;
const nextStepReason = overview.nextStep
? nextStepToken ? t(`checklistItem_${nextStepToken}Description` as any) : overview.nextStep.reason
: null;
return (
<Stack spacing={2}>
{overview.nextStep ? (
<Paper sx={{ p: 2.5, borderRadius: 3, borderLeft: "4px solid", borderLeftColor: "primary.main" }}>
<Typography variant="overline" color="text.secondary">{t("workspaceNextAction")}</Typography>
<Typography variant="h6" sx={{ fontWeight: 800 }}>{overview.nextStep.label}</Typography>
<Typography color="text.secondary" sx={{ mb: 1.5 }}>{overview.nextStep.reason}</Typography>
<Typography variant="h6" sx={{ fontWeight: 800 }}>{nextStepLabel}</Typography>
<Typography color="text.secondary" sx={{ mb: 1.5 }}>{nextStepReason}</Typography>
<Button variant="contained" endIcon={<ArrowForwardIcon />}
onClick={() => overview.nextStep?.section && onGo(overview.nextStep.section as WorkspaceSectionKey)}>
{overview.nextStep.label}
{nextStepLabel}
</Button>
</Paper>
) : (