feat(i18n): persist Bokmal preference globally
This commit is contained in:
@@ -100,7 +100,7 @@ function normalizeLanguage(value?: string | null) {
|
||||
const raw = (value || "").trim().toLowerCase();
|
||||
if (!raw) return "";
|
||||
if (["en", "eng", "english"].includes(raw)) return "en";
|
||||
if (["no", "nb", "nn", "norwegian", "norwegian bokmål", "bokmal", "bokmål"].includes(raw)) return "no";
|
||||
if (["no", "nb", "nb-no", "nn", "norwegian", "norwegian bokmål", "bokmal", "bokmål"].includes(raw)) return "nb";
|
||||
return raw;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { cvBuilderApi } from "../cvBuilder";
|
||||
import { aiWorkspaceApi } from "../aiWorkspace";
|
||||
import { useAccountPlan } from "../accountPlan";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
|
||||
// Phase 5.4 — Application Assets sections for the workspace.
|
||||
//
|
||||
@@ -417,10 +418,11 @@ const COVER_LETTER_ACTIONS = [
|
||||
|
||||
function CoverLetterAiAssistant({ jobId, currentText, onApply }: { jobId: number; currentText: string; onApply: (text: string, action: string) => void }) {
|
||||
const { canUseAi } = useAccountPlan();
|
||||
const { language: uiLanguage, t } = useI18n();
|
||||
const { data: cv, loading: loadingCv } = useAsset<ApplicationCv>(() => applicationAssetsApi.cv(jobId), [jobId]);
|
||||
const [action, setAction] = useState("generate");
|
||||
const [mode, setMode] = useState("professional");
|
||||
const [language, setLanguage] = useState<"en" | "nb-NO">("en");
|
||||
const [language, setLanguage] = useState<"en" | "nb-NO">(() => uiLanguage === "nb" ? "nb-NO" : "en");
|
||||
const [instructions, setInstructions] = useState("");
|
||||
const [suggestion, setSuggestion] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
@@ -449,17 +451,17 @@ function CoverLetterAiAssistant({ jobId, currentText, onApply }: { jobId: number
|
||||
const hasCv = !!cv?.attachedVariantId;
|
||||
return (
|
||||
<Shell
|
||||
title="AI writing assistant"
|
||||
subtitle="Uses this job and its linked CV. Suggestions never overwrite your document."
|
||||
title={t("coverAiTitle")}
|
||||
subtitle={t("coverAiSubtitle")}
|
||||
loading={loadingCv}
|
||||
error={null}
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
{!hasCv ? (
|
||||
<Alert severity="info">Select a CV before generating a tailored cover letter.</Alert>
|
||||
<Alert severity="info">{t("coverAiSelectCv")}</Alert>
|
||||
) : (
|
||||
<Alert severity="success" variant="outlined" sx={{ py: 0.5 }}>
|
||||
Using <strong>{cv?.attachedVariantName}</strong> and this application's full job advert and analysis.
|
||||
{t("coverAiUsing")} <strong>{cv?.attachedVariantName}</strong>
|
||||
</Alert>
|
||||
)}
|
||||
<Stack direction={{ xs: "column", sm: "row" }} spacing={1.5}>
|
||||
@@ -478,15 +480,15 @@ function CoverLetterAiAssistant({ jobId, currentText, onApply }: { jobId: number
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl size="small" sx={{ minWidth: 190 }}>
|
||||
<InputLabel>Document language</InputLabel>
|
||||
<Select label="Document language" value={language} onChange={(event) => setLanguage(event.target.value as "en" | "nb-NO")}>
|
||||
<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>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
<TextField
|
||||
label="Additional instructions"
|
||||
label={t("coverAiAdditionalInstructions")}
|
||||
placeholder="For example: Focus on my .NET experience and keep it concise."
|
||||
value={instructions}
|
||||
onChange={(event) => setInstructions(event.target.value)}
|
||||
@@ -508,17 +510,17 @@ function CoverLetterAiAssistant({ jobId, currentText, onApply }: { jobId: number
|
||||
{suggestion && (
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", lg: "1fr 1fr" }, gap: 1.5 }}>
|
||||
<Paper variant="outlined" sx={{ p: 2, minWidth: 0 }}>
|
||||
<Typography variant="overline" color="text.secondary">Current</Typography>
|
||||
<Typography variant="overline" color="text.secondary">{t("coverAiCurrent")}</Typography>
|
||||
<Typography variant="body2" sx={{ whiteSpace: "pre-wrap", overflowWrap: "anywhere" }}>
|
||||
{currentText || "No current draft"}
|
||||
</Typography>
|
||||
</Paper>
|
||||
<Paper variant="outlined" sx={{ p: 2, minWidth: 0, borderColor: "primary.main" }}>
|
||||
<Typography variant="overline" color="primary">Suggestion</Typography>
|
||||
<Typography variant="overline" color="primary">{t("coverAiSuggestion")}</Typography>
|
||||
<Typography variant="body2" sx={{ whiteSpace: "pre-wrap", overflowWrap: "anywhere" }}>{suggestion}</Typography>
|
||||
<Stack direction="row" spacing={1} sx={{ mt: 2 }}>
|
||||
<Button size="small" variant="contained" onClick={() => { onApply(suggestion, action); setSuggestion(""); }}>Apply to editor</Button>
|
||||
<Button size="small" onClick={() => setSuggestion("")}>Reject</Button>
|
||||
<Button size="small" variant="contained" onClick={() => { onApply(suggestion, action); setSuggestion(""); }}>{t("coverAiApply")}</Button>
|
||||
<Button size="small" onClick={() => setSuggestion("")}>{t("coverAiReject")}</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getApiErrorMessage } from "../api";
|
||||
import {
|
||||
CareerMatch, JobAnalysis, TIMELINE_CATEGORY_LABELS, Timeline, applicationIntelligenceApi,
|
||||
} from "../applicationWorkspace";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
|
||||
// Phase 5.3 — Application Intelligence sections for the workspace.
|
||||
//
|
||||
@@ -255,6 +256,7 @@ export function ApplicationAnalysis({ jobId }: { jobId: number }) {
|
||||
// ---------- Match ----------
|
||||
|
||||
export function ApplicationMatch({ jobId }: { jobId: number }) {
|
||||
const { t } = useI18n();
|
||||
const { data, error, loading } = useIntelligence<CareerMatch>(
|
||||
() => applicationIntelligenceApi.match(jobId),
|
||||
[jobId],
|
||||
@@ -262,17 +264,17 @@ export function ApplicationMatch({ jobId }: { jobId: number }) {
|
||||
|
||||
return (
|
||||
<SectionShell
|
||||
title="CV Match"
|
||||
title={t("workspaceCvMatch")}
|
||||
subtitle={data?.selectedCvName
|
||||
? `${data.selectedCvName} compared with this job advert.`
|
||||
: "Choose the CV intended for this application before comparing it with the advert."}
|
||||
? `${t("workspaceComparingCv")} ${data.selectedCvName}`
|
||||
: t("workspaceSelectCvMatch")}
|
||||
loading={loading}
|
||||
error={error}
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
{data && !data.hasSelectedCv ? (
|
||||
<Alert severity="info" sx={{ borderRadius: 2 }}>
|
||||
Select a CV on the CV tab first. The application will only analyse the document you explicitly link.
|
||||
{t("workspaceSelectCvFirst")}
|
||||
</Alert>
|
||||
) : data && !data.hasCareerProfile ? (
|
||||
<Alert severity="info" sx={{ borderRadius: 2 }}>
|
||||
|
||||
@@ -138,10 +138,10 @@ export default function SettingsView({
|
||||
labelId="language-label"
|
||||
value={language}
|
||||
label={t("settingsPreferredLanguage")}
|
||||
onChange={(e) => setLanguage(e.target.value as "en" | "no")}
|
||||
onChange={(e) => setLanguage(e.target.value as "en" | "nb")}
|
||||
>
|
||||
<MenuItem value="en">{t("settingsEnglish")}</MenuItem>
|
||||
<MenuItem value="no">{t("settingsNorwegian")}</MenuItem>
|
||||
<MenuItem value="nb">{t("settingsNorwegian")}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</SectionCard>
|
||||
|
||||
Reference in New Issue
Block a user