fix(i18n): finish career activity copy

This commit is contained in:
cesnimda
2026-08-29 21:57:40 +02:00
parent a704c75440
commit 457f0601c0
13 changed files with 272 additions and 91 deletions
+31 -31
View File
@@ -153,7 +153,7 @@ export default function CareerProfilePage() {
// Retained so the shared JSX (copied from ProfilePage) reads identically; hardcoded for /career.
const careerOnly = true;
const { toast } = useToast();
const { t } = useI18n();
const { language, t } = useI18n();
const cvInputRef = useRef<HTMLInputElement | null>(null);
const avatarInputRef = useRef<HTMLInputElement | null>(null);
const [me, setMe] = useState<MeResponse | null>(null);
@@ -393,8 +393,8 @@ export default function CareerProfilePage() {
/> : null}
{loadError ? (
<Alert severity="error" sx={{ mb: 2, borderRadius: 2.5 }} action={<Button color="inherit" size="small" onClick={() => void loadProfile()}>Retry</Button>}>
Unable to load profile.
<Alert severity="error" sx={{ mb: 2, borderRadius: 2.5 }} action={<Button color="inherit" size="small" onClick={() => void loadProfile()}>{t("retry")}</Button>}>
{t("profileLoadFailed")}
<Typography variant="body2" sx={{ mt: 0.5 }}>{loadError}</Typography>
</Alert>
) : null}
@@ -468,12 +468,12 @@ export default function CareerProfilePage() {
<Box sx={{ display: workspaceSection === "import" ? "block" : "none" }}>
{!canUseAi && (
<Box sx={{ mb: 2 }}>
<ProFeatureNotice featureKey="career-ai" title="Build your Career Profile faster with Pro.">
Import, rebuild and improve CV content with AI. Manual profile editing remains available on Free.
<ProFeatureNotice featureKey="career-ai" title={t("profileProImportTitle")}>
{t("profileProImportBody")}
</ProFeatureNotice>
</Box>
)}
{profileDirty ? <Alert severity="warning" sx={{ mb: 2 }}>You have unsaved career edits. Save them before running actions that use the stored profile.</Alert> : null}
{profileDirty ? <Alert severity="warning" sx={{ mb: 2 }}>{t("profileUnsavedBeforeActions")}</Alert> : null}
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, flexWrap: "wrap", alignItems: "center", mb: 1.5 }}>
<Box>
<Typography variant="h6">{t("profileMasterCv")}</Typography>
@@ -497,7 +497,7 @@ export default function CareerProfilePage() {
try {
const res = await api.post<QueuedCvRunResponse>("/profile-cv/upload", formData, { headers: { "Content-Type": "multipart/form-data" } });
await loadExtractionRuns();
toast(`Queued CV upload (run ${res.data.extractionRunId}).`, "info");
toast(t("profileCvQueuedUpload", { id: res.data.extractionRunId }), "info");
} catch (e: any) {
toast(String(e?.response?.data || e?.message || t("profileCvUploadFailed")), "error");
} finally {
@@ -516,7 +516,7 @@ export default function CareerProfilePage() {
try {
const res = await api.post<QueuedCvRunResponse>("/profile-cv/rebuild");
await loadExtractionRuns();
toast(`Queued CV rebuild (run ${res.data.extractionRunId}).`, "info");
toast(t("profileCvQueuedRebuild", { id: res.data.extractionRunId }), "info");
} catch (e: any) {
toast(String(e?.response?.data || e?.message || t("profileCvRebuildFailed")), "error");
} finally {
@@ -534,7 +534,7 @@ export default function CareerProfilePage() {
try {
const res = await api.post<QueuedCvRunResponse>("/profile-cv/improve");
await loadExtractionRuns();
toast(`Queued CV improve run (run ${res.data.extractionRunId}).`, "info");
toast(t("profileCvQueuedImprove", { id: res.data.extractionRunId }), "info");
} catch (e: any) {
toast(String(e?.response?.data || e?.message || t("profileCvImproveFailed")), "error");
} finally {
@@ -552,7 +552,7 @@ export default function CareerProfilePage() {
try {
const res = await api.post<QueuedCvRunResponse>("/profile-cv/reprocess");
await loadExtractionRuns();
toast(`Queued CV reprocess run (run ${res.data.extractionRunId}).`, "info");
toast(t("profileCvQueuedReprocess", { id: res.data.extractionRunId }), "info");
} catch (e: any) {
toast(String(e?.response?.data || e?.message || t("profileCvReprocessFailed")), "error");
} finally {
@@ -617,7 +617,7 @@ export default function CareerProfilePage() {
</Box>
<Typography variant="body2" sx={{ color: "text.secondary" }}>{run.artifactFileName || t("profileCvNoStoredArtifact")}</Typography>
<Typography variant="caption" sx={{ color: "text.secondary", display: "block", mt: 0.75 }}>
{run.parserVersion} · {new Date(run.startedAtUtc).toLocaleString()}
{run.parserVersion} · {new Date(run.startedAtUtc).toLocaleString(language === "nb" ? "nb-NO" : "en")}
</Typography>
{run.errorMessage ? (
<Typography variant="caption" sx={{ color: "error.main", display: "block", mt: 0.75 }}>
@@ -629,36 +629,36 @@ export default function CareerProfilePage() {
try {
await api.post(`/operations/${run.operation!.id}/cancel`);
await loadExtractionRuns();
toast("CV processing cancellation requested.", "info");
toast(t("profileCvCancelRequested"), "info");
} catch (error) {
toast(getApiErrorMessage(error, "Could not cancel CV processing."), "error");
toast(getApiErrorMessage(error, t("profileCvCancelFailed")), "error");
}
}}>Cancel processing</Button>
}}>{t("profileCvCancelProcessing")}</Button>
) : null}
{run.operation?.canRetry ? (
<Button size="small" color="inherit" sx={{ mt: 0.75 }} onClick={async () => {
try {
await api.post(`/operations/${run.operation!.id}/retry`);
await loadExtractionRuns();
toast("CV processing queued again.", "info");
toast(t("profileCvRetryQueued"), "info");
} catch (error) {
toast(getApiErrorMessage(error, "Could not retry CV processing."), "error");
toast(getApiErrorMessage(error, t("profileCvRetryFailed")), "error");
}
}}>Retry processing</Button>
}}>{t("profileCvRetryProcessing")}</Button>
) : null}
{run.status === "pending_review" ? (
<Box sx={{ mt: 1.25 }}>
{runDiffs[run.id] ? (
<>
<Typography variant="body2" sx={{ fontWeight: 700 }}>
{runDiffs[run.id].totalAdded} additions | {runDiffs[run.id].totalUpdated} updates
{runDiffs[run.id].totalLowConfidence ? ` | ${runDiffs[run.id].totalLowConfidence} need attention` : ""}
{t("profileCvDiffSummary", { added: runDiffs[run.id].totalAdded, updated: runDiffs[run.id].totalUpdated })}
{runDiffs[run.id].totalLowConfidence ? ` | ${t("profileCvNeedAttention", { count: runDiffs[run.id].totalLowConfidence })}` : ""}
</Typography>
<Typography variant="caption" sx={{ color: "text.secondary", display: "block", mt: 0.5 }}>
{runDiffs[run.id].categories
.filter((category) => category.added.length || category.updated.length)
.map((category) => `${category.category}: +${category.added.length} / ~${category.updated.length}`)
.join(" | ") || "No profile changes found"}
.join(" | ") || t("profileCvNoChangesFound")}
</Typography>
</>
) : <LinearProgress sx={{ my: 1 }} />}
@@ -672,12 +672,12 @@ export default function CareerProfilePage() {
? [...(current[run.id] ?? []), change.id]
: (current[run.id] ?? []).filter((id) => id !== change.id),
}))} />}
label={`Include low-confidence ${change.category}: ${change.label}`}
label={t("profileCvIncludeLowConfidence", { category: change.category, label: change.label })}
/>
))}
{profileDirty ? (
<Alert severity="warning" sx={{ mt: 1 }}>
Save your current career-profile edits before applying imported changes.
{t("profileCvSaveBeforeImport")}
</Alert>
) : null}
<Box sx={{ display: "flex", gap: 1, mt: 1, flexWrap: "wrap" }}>
@@ -687,22 +687,22 @@ export default function CareerProfilePage() {
await api.post(`/profile-cv/runs/${run.id}/accept`, { acceptedLowConfidenceIds: acceptedLowConfidenceIds[run.id] ?? [] });
setRunDiffs((current) => { const next = { ...current }; delete next[run.id]; return next; });
await Promise.all([loadProfile(), loadExtractionRuns(), loadVersions()]);
toast("CV changes merged into your career profile.", "success");
toast(t("profileCvChangesApplied"), "success");
} catch (error) {
toast(getApiErrorMessage(error, "Could not apply CV changes."), "error");
toast(getApiErrorMessage(error, t("profileCvApplyChangesFailed")), "error");
} finally { setReviewingRunId(null); }
}}>Apply changes</Button>
}}>{t("profileCvApplyChanges")}</Button>
<Button size="small" color="inherit" disabled={reviewingRunId !== null} onClick={async () => {
setReviewingRunId(run.id);
try {
await api.post(`/profile-cv/runs/${run.id}/discard`);
setRunDiffs((current) => { const next = { ...current }; delete next[run.id]; return next; });
await loadExtractionRuns();
toast("CV extraction discarded.", "info");
toast(t("profileCvExtractionDiscarded"), "info");
} catch (error) {
toast(getApiErrorMessage(error, "Could not discard CV extraction."), "error");
toast(getApiErrorMessage(error, t("profileCvDiscardFailed")), "error");
} finally { setReviewingRunId(null); }
}}>Discard</Button>
}}>{t("profileCvDiscard")}</Button>
</Box>
</Box>
) : null}
@@ -715,7 +715,7 @@ export default function CareerProfilePage() {
</Box>
<Box sx={{ gridColumn: "1 / -1", display: "flex", justifyContent: "flex-end", mt: 1 }}>
<Button size="small" variant="text" color="inherit" onClick={() => setShowAdvancedCvTools((v) => !v)}>
{showAdvancedCvTools ? "Hide advanced CV tools" : "Advanced CV tools"}
{showAdvancedCvTools ? t("profileCvHideAdvancedTools") : t("profileCvAdvancedTools")}
</Button>
</Box>
<Box sx={{ mt: 2, p: 1.5, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.paper", display: showAdvancedCvTools ? "block" : "none" }}>
@@ -794,7 +794,7 @@ export default function CareerProfilePage() {
</Box>
<Box sx={{ mt: 1, display: workspaceSection === "profile" ? "flex" : "none", justifyContent: "space-between", gap: 1, flexWrap: "wrap" }}>
<Typography variant="caption" sx={{ color: "text.secondary" }}>
{cvWordCount} words
{t("profileCvSectionWordCount", { count: cvWordCount })}
</Typography>
<Typography variant="caption" sx={{ color: "text.secondary" }}>
{t("profileCvPreferredUploads")}
@@ -803,7 +803,7 @@ export default function CareerProfilePage() {
</Box>
<Box sx={{ gridColumn: "1 / -1", display: workspaceSection === "profile" ? "flex" : "none", justifyContent: "flex-end", gap: 2, flexWrap: "wrap", alignItems: "center" }}>
{profileDirty ? <Chip size="small" color="warning" variant="outlined" label="Unsaved changes" /> : null}
{profileDirty ? <Chip size="small" color="warning" variant="outlined" label={t("assetsUnsavedChanges")} /> : null}
<Button
variant="contained"
disabled={!isLocal || loading}