81 lines
3.1 KiB
TypeScript
81 lines
3.1 KiB
TypeScript
import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
import { translations, TranslationKey, UiLanguage } from "./translations";
|
|
import { api } from "../api";
|
|
import { useOptionalToast } from "../toast";
|
|
|
|
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;
|
|
};
|
|
|
|
const I18nContext = createContext<Ctx | null>(null);
|
|
|
|
function interpolate(template: string, params?: TranslationParams) {
|
|
if (!params) return template;
|
|
return template.replace(/\{(\w+)\}/g, (_, key) => String(params[key] ?? `{${key}}`));
|
|
}
|
|
|
|
export function I18nProvider({ children }: { children: React.ReactNode }) {
|
|
const feedback = useOptionalToast();
|
|
const [language, setLanguageState] = useState<UiLanguage>(() => {
|
|
const raw = window.localStorage.getItem("uiLanguage");
|
|
return raw === "nb" || raw === "nb-NO" || raw === "no" ? "nb" : "en";
|
|
});
|
|
const currentLanguage = useRef(language);
|
|
const languageSaveSequence = useRef(0);
|
|
|
|
const applyLanguage = useCallback((l: UiLanguage) => {
|
|
currentLanguage.current = l;
|
|
setLanguageState(l);
|
|
window.localStorage.setItem("uiLanguage", l);
|
|
document.documentElement.lang = l === "nb" ? "nb-NO" : "en";
|
|
}, []);
|
|
|
|
const setLanguage = (l: UiLanguage) => {
|
|
applyLanguage(l);
|
|
const requestSequence = ++languageSaveSequence.current;
|
|
void api.put("/auth/preferences/language", { language: l === "nb" ? "nb-NO" : "en" }).catch(() => {
|
|
if (requestSequence !== languageSaveSequence.current || currentLanguage.current !== l) return;
|
|
feedback?.toast(translations[l].settingsLanguageSaveFailed, "error", {
|
|
label: translations[l].retry,
|
|
onClick: () => {
|
|
if (requestSequence === languageSaveSequence.current && currentLanguage.current === l) setLanguage(l);
|
|
},
|
|
});
|
|
});
|
|
};
|
|
|
|
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, hydrateLanguage, t }}>{children}</I18nContext.Provider>;
|
|
}
|
|
|
|
export function useI18n() {
|
|
const ctx = useContext(I18nContext);
|
|
if (!ctx) throw new Error("useI18n must be used within I18nProvider");
|
|
return ctx;
|
|
}
|
|
|
|
export function useOptionalI18n() {
|
|
return useContext(I18nContext);
|
|
}
|