154 lines
5.1 KiB
TypeScript
154 lines
5.1 KiB
TypeScript
import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
|
|
import { Language, TranslationKeys, loadLanguage, fetchAvailableLanguageCodes, I18nCodeInfo } from '../../locales';
|
|
import { getUserDataCache } from '../../utils/userCache';
|
|
|
|
|
|
export type { Language };
|
|
|
|
type TranslateParams = Record<string, string | number | boolean | null | undefined>;
|
|
|
|
interface LanguageContextType {
|
|
currentLanguage: Language;
|
|
setLanguage: (language: Language) => void;
|
|
t: (key: string, params?: TranslateParams) => string;
|
|
isLoading: boolean;
|
|
reloadLanguage: () => Promise<void>;
|
|
availableLanguages: I18nCodeInfo[];
|
|
refreshAvailableLanguages: () => Promise<void>;
|
|
}
|
|
|
|
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
|
|
|
interface LanguageProviderProps {
|
|
children: ReactNode;
|
|
}
|
|
|
|
export const LanguageProvider: React.FC<LanguageProviderProps> = ({ children }) => {
|
|
const [currentLanguage, setCurrentLanguage] = useState<Language>('de');
|
|
const [translations, setTranslations] = useState<TranslationKeys>({});
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [availableLanguages, setAvailableLanguages] = useState<I18nCodeInfo[]>([]);
|
|
|
|
const loadAndSetLanguage = async (language: Language) => {
|
|
setIsLoading(true);
|
|
try {
|
|
const targetKeys = await loadLanguage(language);
|
|
setTranslations(targetKeys);
|
|
setCurrentLanguage(language);
|
|
} catch (error) {
|
|
console.error('Failed to load language:', error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const initializeLanguage = async () => {
|
|
let initialLanguage: Language = 'de';
|
|
|
|
const userData = getUserDataCache();
|
|
if (userData?.language && String(userData.language).trim()) {
|
|
initialLanguage = String(userData.language).trim() as Language;
|
|
await loadAndSetLanguage(initialLanguage);
|
|
return;
|
|
}
|
|
|
|
const browserLang = navigator.language.split('-')[0] as Language;
|
|
try {
|
|
const codes = await fetchAvailableLanguageCodes();
|
|
const codeSet = new Set(codes.map((c) => c.code));
|
|
if (codeSet.has(browserLang) && browserLang !== 'xx') {
|
|
initialLanguage = browserLang;
|
|
}
|
|
} catch {
|
|
// keep default
|
|
}
|
|
|
|
await loadAndSetLanguage(initialLanguage);
|
|
};
|
|
|
|
initializeLanguage();
|
|
|
|
const handleUserUpdate = async () => {
|
|
const userData = getUserDataCache();
|
|
if (userData?.language && String(userData.language).trim()) {
|
|
const userLanguage = String(userData.language).trim() as Language;
|
|
if (userLanguage !== currentLanguage) {
|
|
loadAndSetLanguage(userLanguage);
|
|
}
|
|
}
|
|
try {
|
|
const list = await fetchAvailableLanguageCodes();
|
|
setAvailableLanguages(list.filter((l) => l.code !== 'xx'));
|
|
} catch {
|
|
// silent
|
|
}
|
|
};
|
|
|
|
window.addEventListener('userInfoUpdated', handleUserUpdate);
|
|
|
|
return () => {
|
|
window.removeEventListener('userInfoUpdated', handleUserUpdate);
|
|
};
|
|
}, []);
|
|
|
|
const setLanguage = async (language: Language) => {
|
|
await loadAndSetLanguage(language);
|
|
};
|
|
|
|
const reloadLanguage = async () => {
|
|
await loadAndSetLanguage(currentLanguage);
|
|
};
|
|
|
|
const refreshAvailableLanguages = useCallback(async () => {
|
|
try {
|
|
const list = await fetchAvailableLanguageCodes();
|
|
setAvailableLanguages(list.filter((l) => l.code !== 'xx'));
|
|
} catch (e) {
|
|
console.error('Failed to load language codes:', e);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
refreshAvailableLanguages();
|
|
}, [refreshAvailableLanguages]);
|
|
|
|
const _applyParams = (template: string, params?: TranslateParams): string => {
|
|
if (!params) return template;
|
|
let out = template;
|
|
for (const [paramKey, rawVal] of Object.entries(params)) {
|
|
if (rawVal === undefined || rawVal === null) continue;
|
|
out = out.split(`{${paramKey}}`).join(String(rawVal));
|
|
}
|
|
return out;
|
|
};
|
|
|
|
const t = (key: string, params?: TranslateParams): string => {
|
|
const resolved = translations[key] ?? `[${key}]`;
|
|
return _applyParams(resolved, params);
|
|
};
|
|
|
|
const contextValue: LanguageContextType = {
|
|
currentLanguage,
|
|
setLanguage,
|
|
t,
|
|
isLoading,
|
|
reloadLanguage,
|
|
availableLanguages,
|
|
refreshAvailableLanguages,
|
|
};
|
|
|
|
return (
|
|
<LanguageContext.Provider value={contextValue}>
|
|
{children}
|
|
</LanguageContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useLanguage = (): LanguageContextType => {
|
|
const context = useContext(LanguageContext);
|
|
if (!context) {
|
|
throw new Error('useLanguage must be used within a LanguageProvider');
|
|
}
|
|
return context;
|
|
};
|