feat(i18n): persist Bokmal preference globally
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
import React, { createContext, useContext, useState } from "react";
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from "react";
|
||||
import { translations, TranslationKey, UiLanguage } from "./translations";
|
||||
import { api } from "../api";
|
||||
|
||||
type TranslationParams = Record<string, string | number>;
|
||||
|
||||
type Ctx = {
|
||||
language: UiLanguage;
|
||||
setLanguage: (l: UiLanguage) => void;
|
||||
hydrateLanguage: (language?: string | null) => void;
|
||||
t: (key: TranslationKey, params?: TranslationParams) => string;
|
||||
};
|
||||
|
||||
@@ -19,17 +21,38 @@ function interpolate(template: string, params?: TranslationParams) {
|
||||
export function I18nProvider({ children }: { children: React.ReactNode }) {
|
||||
const [language, setLanguageState] = useState<UiLanguage>(() => {
|
||||
const raw = window.localStorage.getItem("uiLanguage");
|
||||
return raw === "no" ? "no" : "en";
|
||||
return raw === "nb" || raw === "nb-NO" || raw === "no" ? "nb" : "en";
|
||||
});
|
||||
|
||||
const setLanguage = (l: UiLanguage) => {
|
||||
const applyLanguage = useCallback((l: UiLanguage) => {
|
||||
setLanguageState(l);
|
||||
window.localStorage.setItem("uiLanguage", l);
|
||||
document.documentElement.lang = l === "nb" ? "nb-NO" : "en";
|
||||
}, []);
|
||||
|
||||
const setLanguage = (l: UiLanguage) => {
|
||||
applyLanguage(l);
|
||||
void api.put("/auth/preferences/language", { language: l === "nb" ? "nb-NO" : "en" }).catch(() => undefined);
|
||||
};
|
||||
|
||||
const hydrateLanguage = useCallback((value?: string | null) => {
|
||||
if (!value) return;
|
||||
applyLanguage(value === "nb" || value === "nb-NO" || value === "no" ? "nb" : "en");
|
||||
}, [applyLanguage]);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = language === "nb" ? "nb-NO" : "en";
|
||||
const onStorage = (event: StorageEvent) => {
|
||||
if (event.key !== "uiLanguage") return;
|
||||
applyLanguage(event.newValue === "nb" || event.newValue === "nb-NO" || event.newValue === "no" ? "nb" : "en");
|
||||
};
|
||||
window.addEventListener("storage", onStorage);
|
||||
return () => window.removeEventListener("storage", onStorage);
|
||||
}, [applyLanguage, language]);
|
||||
|
||||
const t = (key: TranslationKey, params?: TranslationParams) => interpolate(translations[language][key] ?? translations.en[key], params);
|
||||
|
||||
return <I18nContext.Provider value={{ language, setLanguage, t }}>{children}</I18nContext.Provider>;
|
||||
return <I18nContext.Provider value={{ language, setLanguage, hydrateLanguage, t }}>{children}</I18nContext.Provider>;
|
||||
}
|
||||
|
||||
export function useI18n() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type UiLanguage = "en" | "no";
|
||||
export type UiLanguage = "en" | "nb";
|
||||
|
||||
export const translations = {
|
||||
en: {
|
||||
@@ -1170,8 +1170,50 @@ export const translations = {
|
||||
rulesSaving: "Saving...",
|
||||
rulesSave: "Save Rules",
|
||||
rulesSaveFailed: "Failed to save rules.",
|
||||
workspace: "Workspace",
|
||||
workspaceBack: "Back to applications",
|
||||
workspaceOpenFull: "Open full-page workspace",
|
||||
workspaceSections: "Workspace sections",
|
||||
workspaceOverview: "Overview",
|
||||
workspaceAnalysis: "Analysis",
|
||||
workspaceCv: "CV",
|
||||
workspaceCoverLetter: "Cover Letter",
|
||||
workspaceInterviewPrep: "Interview Prep",
|
||||
workspaceInvalidLink: "This application link is invalid.",
|
||||
workspaceLoadFailed: "Could not open this application.",
|
||||
workspaceUnsavedTitle: "Unsaved application changes",
|
||||
workspaceUnsavedMessage: "Leaving this section will discard changes that have not been saved.",
|
||||
workspaceDiscardLeave: "Discard and leave",
|
||||
workspaceKeepEditing: "Keep editing",
|
||||
workspaceEditApplication: "Edit application",
|
||||
workspaceOpenAdvert: "Open original advert",
|
||||
workspaceProgress: "Application progress",
|
||||
workspaceNextAction: "Next recommended action",
|
||||
workspaceNothingOutstanding: "Nothing outstanding — this application is fully prepared.",
|
||||
workspaceRecentActivity: "Recent activity",
|
||||
workspaceRefresh: "Refresh",
|
||||
workspaceNoActivity: "No activity recorded yet.",
|
||||
workspaceJobDetails: "Job details",
|
||||
workspaceChecklist: "Next actions and checklist",
|
||||
workspaceActivityHistory: "Activity history",
|
||||
workspaceDocuments: "Documents",
|
||||
workspaceCommunication: "Communication",
|
||||
workspaceCvMatch: "CV Match",
|
||||
workspaceSelectCvMatch: "Choose the CV intended for this application before comparing it with the advert.",
|
||||
workspaceComparingCv: "{name} compared with this job advert.",
|
||||
workspaceSelectCvFirst: "Select a CV on the CV tab first. The application will only analyse the document you explicitly link.",
|
||||
coverAiTitle: "AI writing assistant",
|
||||
coverAiSubtitle: "Uses this job and its linked CV. Suggestions never overwrite your document.",
|
||||
coverAiSelectCv: "Select a CV before generating a tailored cover letter.",
|
||||
coverAiUsing: "Using {name} and this application's full job advert and analysis.",
|
||||
coverAiDocumentLanguage: "Document language",
|
||||
coverAiAdditionalInstructions: "Additional instructions",
|
||||
coverAiCurrent: "Current",
|
||||
coverAiSuggestion: "Suggestion",
|
||||
coverAiApply: "Apply to editor",
|
||||
coverAiReject: "Reject",
|
||||
},
|
||||
no: {
|
||||
nb: {
|
||||
appTitle: "Jobbjakt",
|
||||
appTagline: "Hold oversikt over jobbsøkingen",
|
||||
dashboard: "Dashboard",
|
||||
@@ -2340,6 +2382,48 @@ export const translations = {
|
||||
rulesSaving: "Lagrer...",
|
||||
rulesSave: "Lagre regler",
|
||||
rulesSaveFailed: "Kunne ikke lagre regler.",
|
||||
workspace: "Arbeidsområde",
|
||||
workspaceBack: "Tilbake til søknader",
|
||||
workspaceOpenFull: "Åpne arbeidsområdet på helside",
|
||||
workspaceSections: "Deler av arbeidsområdet",
|
||||
workspaceOverview: "Oversikt",
|
||||
workspaceAnalysis: "Analyse",
|
||||
workspaceCv: "CV",
|
||||
workspaceCoverLetter: "Søknadsbrev",
|
||||
workspaceInterviewPrep: "Intervjuforberedelse",
|
||||
workspaceInvalidLink: "Denne søknadslenken er ugyldig.",
|
||||
workspaceLoadFailed: "Kunne ikke åpne denne søknaden.",
|
||||
workspaceUnsavedTitle: "Ulagrede søknadsendringer",
|
||||
workspaceUnsavedMessage: "Hvis du forlater denne delen, forkastes endringer som ikke er lagret.",
|
||||
workspaceDiscardLeave: "Forkast og forlat",
|
||||
workspaceKeepEditing: "Fortsett å redigere",
|
||||
workspaceEditApplication: "Rediger søknad",
|
||||
workspaceOpenAdvert: "Åpne den opprinnelige annonsen",
|
||||
workspaceProgress: "Søknadsprogresjon",
|
||||
workspaceNextAction: "Neste anbefalte handling",
|
||||
workspaceNothingOutstanding: "Ingenting gjenstår — denne søknaden er ferdig forberedt.",
|
||||
workspaceRecentActivity: "Nylig aktivitet",
|
||||
workspaceRefresh: "Oppdater",
|
||||
workspaceNoActivity: "Ingen aktivitet er registrert ennå.",
|
||||
workspaceJobDetails: "Stillingsdetaljer",
|
||||
workspaceChecklist: "Neste handlinger og sjekkliste",
|
||||
workspaceActivityHistory: "Aktivitetshistorikk",
|
||||
workspaceDocuments: "Dokumenter",
|
||||
workspaceCommunication: "Kommunikasjon",
|
||||
workspaceCvMatch: "CV-samsvar",
|
||||
workspaceSelectCvMatch: "Velg CV-en som skal brukes i søknaden før den sammenlignes med annonsen.",
|
||||
workspaceComparingCv: "{name} sammenlignes med denne stillingsannonsen.",
|
||||
workspaceSelectCvFirst: "Velg først en CV under CV-fanen. Søknaden analyserer bare dokumentet du kobler til eksplisitt.",
|
||||
coverAiTitle: "AI-skriveassistent",
|
||||
coverAiSubtitle: "Bruker denne stillingen og den tilknyttede CV-en. Forslag overskriver aldri dokumentet ditt.",
|
||||
coverAiSelectCv: "Velg en CV før du genererer et skreddersydd søknadsbrev.",
|
||||
coverAiUsing: "Bruker {name} samt hele stillingsannonsen og analysen for denne søknaden.",
|
||||
coverAiDocumentLanguage: "Dokumentspråk",
|
||||
coverAiAdditionalInstructions: "Tilleggsinstruksjoner",
|
||||
coverAiCurrent: "Nåværende",
|
||||
coverAiSuggestion: "Forslag",
|
||||
coverAiApply: "Bruk i redigeringsfeltet",
|
||||
coverAiReject: "Avvis",
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user