feat(i18n): localise application workspace
This commit is contained in:
@@ -7,7 +7,7 @@ import {
|
||||
|
||||
import { getApiErrorMessage } from "../api";
|
||||
import {
|
||||
CareerMatch, JobAnalysis, TIMELINE_CATEGORY_LABELS, Timeline, applicationIntelligenceApi,
|
||||
CareerMatch, JobAnalysis, Timeline, applicationIntelligenceApi,
|
||||
} from "../applicationWorkspace";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
|
||||
@@ -110,6 +110,7 @@ function Bullets({ label, values }: { label: string; values: string[] }) {
|
||||
// ---------- Timeline ----------
|
||||
|
||||
export function ApplicationTimeline({ jobId }: { jobId: number }) {
|
||||
const { t } = useI18n();
|
||||
const [category, setCategory] = useState<string>("");
|
||||
const { data, error, loading } = useIntelligence<Timeline>(
|
||||
() => applicationIntelligenceApi.timeline(jobId, category || undefined),
|
||||
@@ -122,7 +123,7 @@ export function ApplicationTimeline({ jobId }: { jobId: number }) {
|
||||
<Stack spacing={2}>
|
||||
{hasEvents && (data?.milestones.length ?? 0) > 0 && (
|
||||
<Paper sx={{ p: 2, borderRadius: 3 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 800, mb: 1 }}>Milestones</Typography>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 800, mb: 1 }}>{t("intelligenceMilestones")}</Typography>
|
||||
<Stack spacing={0.75}>
|
||||
{(data?.milestones ?? []).map((m) => (
|
||||
<Stack key={m.id} direction="row" spacing={1} justifyContent="space-between" alignItems="baseline">
|
||||
@@ -137,12 +138,12 @@ export function ApplicationTimeline({ jobId }: { jobId: number }) {
|
||||
)}
|
||||
|
||||
<SectionShell
|
||||
title="Timeline"
|
||||
subtitle="Everything recorded for this application, newest first."
|
||||
title={t("intelligenceTimeline")}
|
||||
subtitle={t("intelligenceTimelineSubtitle")}
|
||||
loading={loading}
|
||||
error={error}
|
||||
empty={!hasEvents}
|
||||
emptyText="Nothing has happened yet. Activity appears here as you move the application along."
|
||||
emptyText={t("intelligenceTimelineEmpty")}
|
||||
>
|
||||
{/* SectionShell takes children as a prop, so this JSX is built before it decides whether to
|
||||
render it — every access has to tolerate a null `data`. */}
|
||||
@@ -153,20 +154,20 @@ export function ApplicationTimeline({ jobId }: { jobId: number }) {
|
||||
exclusive
|
||||
value={category}
|
||||
onChange={(_e, next) => setCategory(next ?? "")}
|
||||
aria-label="Filter timeline by category"
|
||||
aria-label={t("intelligenceTimelineFilter")}
|
||||
sx={{ flexWrap: "wrap" }}
|
||||
>
|
||||
<ToggleButton value="" aria-label="All events">All</ToggleButton>
|
||||
<ToggleButton value="" aria-label={t("intelligenceAllEvents")}>{t("intelligenceAll")}</ToggleButton>
|
||||
{(data?.categories ?? []).map((c) => (
|
||||
<ToggleButton key={c} value={c} aria-label={TIMELINE_CATEGORY_LABELS[c] ?? c}>
|
||||
{TIMELINE_CATEGORY_LABELS[c] ?? c}
|
||||
<ToggleButton key={c} value={c} aria-label={timelineCategoryLabel(t, c)}>
|
||||
{timelineCategoryLabel(t, c)}
|
||||
</ToggleButton>
|
||||
))}
|
||||
</ToggleButtonGroup>
|
||||
)}
|
||||
|
||||
{(data?.days.length ?? 0) === 0 ? (
|
||||
<Typography variant="body2" color="text.secondary">No events in this category.</Typography>
|
||||
<Typography variant="body2" color="text.secondary">{t("intelligenceNoCategoryEvents")}</Typography>
|
||||
) : (
|
||||
(data?.days ?? []).map((day) => (
|
||||
<Box key={day.date}>
|
||||
@@ -180,7 +181,7 @@ export function ApplicationTimeline({ jobId }: { jobId: number }) {
|
||||
<Typography variant="body2" sx={{ fontWeight: e.isMilestone ? 700 : 500 }}>
|
||||
{e.summary}
|
||||
</Typography>
|
||||
{e.isMilestone && <Chip size="small" label="Milestone" color="primary" variant="outlined" />}
|
||||
{e.isMilestone && <Chip size="small" label={t("intelligenceMilestone")} color="primary" variant="outlined" />}
|
||||
</Stack>
|
||||
{e.detail && (
|
||||
<Typography variant="caption" color="text.secondary">{e.detail}</Typography>
|
||||
@@ -200,6 +201,7 @@ export function ApplicationTimeline({ jobId }: { jobId: number }) {
|
||||
// ---------- Analysis ----------
|
||||
|
||||
export function ApplicationAnalysis({ jobId }: { jobId: number }) {
|
||||
const { t } = useI18n();
|
||||
const { data, error, loading } = useIntelligence<JobAnalysis>(
|
||||
() => applicationIntelligenceApi.analysis(jobId),
|
||||
[jobId],
|
||||
@@ -207,27 +209,26 @@ export function ApplicationAnalysis({ jobId }: { jobId: number }) {
|
||||
|
||||
const facts: [string, string | null][] = data
|
||||
? [
|
||||
["Role", data.role],
|
||||
["Company", data.company],
|
||||
["Location", data.location],
|
||||
["Employment type", data.employmentType],
|
||||
["Seniority", data.seniority],
|
||||
["Salary", data.salary],
|
||||
[t("intelligenceRole"), data.role],
|
||||
[t("company"), data.company],
|
||||
[t("location"), data.location],
|
||||
[t("intelligenceEmploymentType"), data.employmentType],
|
||||
[t("intelligenceSeniority"), data.seniority],
|
||||
[t("jobDetailsSalary"), data.salary],
|
||||
]
|
||||
: [];
|
||||
|
||||
return (
|
||||
<SectionShell
|
||||
title="Analysis"
|
||||
subtitle="Read straight from the advert — no AI, same answer every time."
|
||||
title={t("workspaceAnalysis")}
|
||||
subtitle={t("intelligenceAnalysisSubtitle")}
|
||||
loading={loading}
|
||||
error={error}
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
{data && !data.hasJobDescription && (
|
||||
<Alert severity="info" sx={{ borderRadius: 2 }}>
|
||||
No advert text saved yet. Paste it into the application to get requirements, technologies
|
||||
and interview topics.
|
||||
{t("intelligenceNoAdvert")}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@@ -242,12 +243,12 @@ export function ApplicationAnalysis({ jobId }: { jobId: number }) {
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Chips label="Technologies" values={data?.technologies ?? []} />
|
||||
<Chips label="Skills" values={data?.skills ?? []} />
|
||||
<Bullets label="Important requirements" values={data?.importantRequirements ?? []} />
|
||||
<Bullets label="Responsibilities" values={data?.responsibilities ?? []} />
|
||||
<Chips label="Likely interview topics" values={data?.interviewTopics ?? []} />
|
||||
<Bullets label="Not stated in the advert" values={data?.missingInformation ?? []} />
|
||||
<Chips label={t("intelligenceTechnologies")} values={data?.technologies ?? []} />
|
||||
<Chips label={t("intelligenceSkills")} values={data?.skills ?? []} />
|
||||
<Bullets label={t("intelligenceRequirements")} values={data?.importantRequirements ?? []} />
|
||||
<Bullets label={t("intelligenceResponsibilities")} values={data?.responsibilities ?? []} />
|
||||
<Chips label={t("intelligenceInterviewTopics")} values={data?.interviewTopics ?? []} />
|
||||
<Bullets label={t("intelligenceNotStated")} values={data?.missingInformation ?? []} />
|
||||
</Stack>
|
||||
</SectionShell>
|
||||
);
|
||||
@@ -278,7 +279,7 @@ export function ApplicationMatch({ jobId }: { jobId: number }) {
|
||||
</Alert>
|
||||
) : data && !data.hasCareerProfile ? (
|
||||
<Alert severity="info" sx={{ borderRadius: 2 }}>
|
||||
The linked CV cannot be matched until its career profile has content.
|
||||
{t("intelligenceCvProfileEmpty")}
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
@@ -290,24 +291,24 @@ export function ApplicationMatch({ jobId }: { jobId: number }) {
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={data?.score ?? 0}
|
||||
aria-label="Career match score"
|
||||
aria-label={t("intelligenceMatchScore")}
|
||||
sx={{ mt: 1, height: 8, borderRadius: 4 }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{data && !data.hasEnoughSignal && (
|
||||
<Alert severity="warning" sx={{ borderRadius: 2 }}>
|
||||
The advert is too short to score reliably. Paste the full text for a real match.
|
||||
{t("intelligenceAdvertTooShort")}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Chips label="Matched" values={data?.matchedSkills ?? []} color="success" />
|
||||
<Chips label="Missing" values={data?.missingSkills ?? []} color="warning" />
|
||||
<Chips label={t("intelligenceMatched")} values={data?.matchedSkills ?? []} color="success" />
|
||||
<Chips label={t("intelligenceMissing")} values={data?.missingSkills ?? []} color="warning" />
|
||||
|
||||
{(data?.relevantExperience.length ?? 0) > 0 && (
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 700 }}>
|
||||
Relevant experience
|
||||
{t("intelligenceRelevantExperience")}
|
||||
</Typography>
|
||||
<Stack spacing={1} sx={{ mt: 0.5 }}>
|
||||
{(data?.relevantExperience ?? []).map((e, i) => (
|
||||
@@ -326,7 +327,7 @@ export function ApplicationMatch({ jobId }: { jobId: number }) {
|
||||
{(data?.relevantProjects.length ?? 0) > 0 && (
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 700 }}>
|
||||
Relevant projects
|
||||
{t("intelligenceRelevantProjects")}
|
||||
</Typography>
|
||||
<Stack spacing={1} sx={{ mt: 0.5 }}>
|
||||
{(data?.relevantProjects ?? []).map((p, i) => (
|
||||
@@ -344,8 +345,19 @@ export function ApplicationMatch({ jobId }: { jobId: number }) {
|
||||
</>
|
||||
)}
|
||||
|
||||
<Bullets label="Suggestions" values={data?.suggestions ?? []} />
|
||||
<Bullets label={t("intelligenceSuggestions")} values={data?.suggestions ?? []} />
|
||||
</Stack>
|
||||
</SectionShell>
|
||||
);
|
||||
}
|
||||
|
||||
function timelineCategoryLabel(t: (key: any) => string, category: string): string {
|
||||
const keys: Record<string, any> = {
|
||||
lifecycle: "intelligenceCategoryLifecycle",
|
||||
stage: "intelligenceCategoryStage",
|
||||
"follow-up": "intelligenceCategoryFollowUp",
|
||||
communication: "intelligenceCategoryCommunication",
|
||||
ai: "intelligenceCategoryAi",
|
||||
};
|
||||
return keys[category] ? t(keys[category]) : category;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user