ui-nyla/src/providers/language/LanguageContext.tsx
2026-04-08 20:28:44 +02:00

183 lines
No EOL
6.8 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, paramsOrFallback?: TranslateParams | string) => 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 [deTranslations, setDeTranslations] = useState<TranslationKeys>({});
const [isLoading, setIsLoading] = useState(true);
const [availableLanguages, setAvailableLanguages] = useState<I18nCodeInfo[]>([]);
// Function to load and set a language
const loadAndSetLanguage = async (language: Language) => {
setIsLoading(true);
try {
const deKeys = await loadLanguage('de');
setDeTranslations(deKeys);
const targetKeys =
language === 'de' ? deKeys : await loadLanguage(language);
setTranslations(targetKeys);
setCurrentLanguage(language);
} catch (error) {
console.error('Failed to load language:', error);
// Keep current language and translations if loading fails
} finally {
setIsLoading(false);
}
};
// Load language from user profile on mount
useEffect(() => {
const initializeLanguage = async () => {
let initialLanguage: Language = 'de';
// Priority 1: Check if user data has language setting (ONLY source of truth!)
const userData = getUserDataCache();
if (userData?.language && String(userData.language).trim()) {
initialLanguage = String(userData.language).trim() as Language;
console.log('🌍 Using language from user profile (sessionStorage cache):', initialLanguage);
await loadAndSetLanguage(initialLanguage);
return;
}
// Priority 2: Detect browser language (fallback only if no user data)
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)) {
initialLanguage = browserLang;
console.log('🌍 Using browser language as fallback:', initialLanguage);
} else {
console.log('🌍 Using default language:', initialLanguage);
}
} catch {
console.log('🌍 Using default language:', initialLanguage);
}
await loadAndSetLanguage(initialLanguage);
};
initializeLanguage();
// Listen for user data updates to sync language
const handleUserUpdate = () => {
const userData = getUserDataCache();
if (userData?.language && String(userData.language).trim()) {
const userLanguage = String(userData.language).trim() as Language;
if (userLanguage !== currentLanguage) {
console.log('🔄 Syncing language with user data (sessionStorage cache):', userLanguage);
loadAndSetLanguage(userLanguage);
}
}
};
// Listen for user info update events
window.addEventListener('userInfoUpdated', handleUserUpdate);
return () => {
window.removeEventListener('userInfoUpdated', handleUserUpdate);
};
}, []);
const setLanguage = async (language: Language) => {
// Load the new language immediately for UI
await loadAndSetLanguage(language);
// IMPORTANT: This should ONLY be called after the backend profile is updated
// The settings component should:
// 1. Update backend user profile with new language
// 2. Refetch user data (which includes the new language)
// 3. Update sessionStorage cache with new data (via setUserDataCache)
// 4. Call this function to sync the UI
};
const reloadLanguage = async () => {
await loadAndSetLanguage(currentLanguage);
};
const refreshAvailableLanguages = useCallback(async () => {
try {
const list = await fetchAvailableLanguageCodes();
setAvailableLanguages(list);
} 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, paramsOrFallback?: TranslateParams | string): string => {
let params: TranslateParams | undefined;
if (typeof paramsOrFallback === 'string') {
params = undefined;
} else {
params = paramsOrFallback;
}
const resolved =
translations[key] ??
deTranslations[key] ??
(typeof paramsOrFallback === 'string' ? paramsOrFallback : undefined) ??
`[${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;
};