UNPKG

kawkab-frontend

Version:

Kawkab frontend is a frontend library for the Kawkab framework

352 lines (351 loc) 12.4 kB
import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; // Global storage for resources and language (used for SSR) let globalResources = null; let globalDefaultLang = 'en'; let globalCurrentLang = null; // Event system for language changes let languageChangeListeners = []; export function onLanguageChange(callback) { languageChangeListeners.push(callback); // Return unsubscribe function return () => { languageChangeListeners = languageChangeListeners.filter(cb => cb !== callback); }; } // Notify all listeners about language change function notifyLanguageChange(lng) { languageChangeListeners.forEach(callback => { try { callback(lng); } catch (error) { console.warn('Error in language change listener:', error); } }); // Trigger a custom event to notify components about language change if (typeof window !== 'undefined') { const event = new CustomEvent('languageChanged', { detail: { language: lng } }); window.dispatchEvent(event); } } // Read cookie by name (browser only) function getCookie(name) { if (typeof document !== 'undefined') { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop()?.split(';').shift() || null; } return null; } // Write cookie (browser only) function setCookie(name, value, days = 365) { if (typeof document !== 'undefined') { const expires = new Date(Date.now() + days * 864e5).toUTCString(); document.cookie = `${name}=${value}; expires=${expires}; path=/`; } } // Save selected language in cookie and localStorage (browser) or in memory (SSR) function saveLanguage(lng) { if (typeof document !== 'undefined') { setCookie('i18next', lng); localStorage.setItem('i18nextLng', lng); } else { globalCurrentLang = lng; } } // Initialize i18n with resources and language export function initI18n(resources, defaultLang = 'en', currentLang, cookieHeader) { globalResources = resources; globalDefaultLang = defaultLang; // In SSR, use language from cookies first let lng = currentLang; if (typeof window === 'undefined' && cookieHeader) { const detectedLang = detectLanguageFromCookies(cookieHeader); if (detectedLang !== defaultLang) { lng = detectedLang; } } // If language is not specified, use getCurrentLanguage lng = lng ?? getCurrentLanguage(); if (typeof window === 'undefined') { globalCurrentLang = lng; } else { // In browser, update globalCurrentLang when language changes globalCurrentLang = lng; } if (!i18n.isInitialized) { i18n .use(LanguageDetector) .use(initReactI18next) .init({ resources, lng, fallbackLng: defaultLang, interpolation: { escapeValue: false }, detection: typeof window !== 'undefined' ? { order: ['cookie', 'localStorage', 'querystring', 'navigator', 'htmlTag'], caches: ['cookie', 'localStorage'], } : undefined, initImmediate: false, }); // Add listener to update globalCurrentLang when language changes if (typeof window !== 'undefined') { // Initialize the current language properly globalCurrentLang = lng; i18n.on('languageChanged', (lng) => { // Only notify if language actually changed if (globalCurrentLang !== lng) { globalCurrentLang = lng; // Notify listeners about language change notifyLanguageChange(lng); } }); } } else { // If i18n is already initialized, update resources i18n.addResourceBundle(lng, 'translation', resources[lng]?.translation, true, true); } return i18n; } // Change language and persist it export function changeLanguage(lng) { // If language is already the same, don't do anything const currentLang = getCurrentLanguage(); if (currentLang === lng) { return Promise.resolve(); } saveLanguage(lng); globalCurrentLang = lng; // Notify listeners about language change notifyLanguageChange(lng); if (typeof window === 'undefined') return Promise.resolve(); return i18n.isInitialized ? i18n.changeLanguage(lng) : Promise.resolve(); } // Get current language (from memory, cookie, or i18n) export function getCurrentLanguage() { // In SSR, return language stored in memory if (typeof window === 'undefined') { return globalCurrentLang ?? globalDefaultLang; } // In browser, check cookies first const cookieLang = getCookie('i18next') || getCookie('i18nextLng'); if (cookieLang && globalResources?.[cookieLang]) { return cookieLang; } // Then check localStorage const storedLang = localStorage.getItem('i18nextLng'); if (storedLang && globalResources?.[storedLang]) { return storedLang; } // Finally, get from i18n instance return i18n.language ?? globalDefaultLang; } // Detect language from cookie header (SSR) export function detectLanguageFromCookies(cookieHeader) { if (!cookieHeader) return globalDefaultLang; const cookies = cookieHeader.split(';').reduce((acc, c) => { const [k, v] = c.trim().split('='); if (k && v) acc[k] = decodeURIComponent(v); return acc; }, {}); const savedLang = cookies['i18next'] || cookies['i18nextLng']; if (savedLang && globalResources?.[savedLang]) { globalCurrentLang = savedLang; return savedLang; } return globalDefaultLang; } // Helper function to initialize default language from request (for SSR frameworks) export function initLanguageFromRequest(resources, defaultLang = 'en', request) { globalResources = resources; globalDefaultLang = defaultLang; if (request?.headers?.cookie) { const langFromCookie = detectLanguageFromCookies(request.headers.cookie); if (langFromCookie !== defaultLang) { return langFromCookie; } } if (request?.headers?.['accept-language']) { const langFromHeader = detectLanguageFromHeader(request.headers['accept-language']); if (langFromHeader !== defaultLang) { return langFromHeader; } } return defaultLang; } // Detect language from Accept-Language HTTP header (SSR) export function detectLanguageFromHeader(acceptLanguage) { if (!acceptLanguage) return globalDefaultLang; const languages = acceptLanguage .split(',') .map(part => part.trim().split(';')[0].split('-')[0]); for (const lang of languages) { if (globalResources?.[lang]) { globalCurrentLang = lang; return lang; } } return globalDefaultLang; } // Get text direction (ltr or rtl) based on language export function getLanguageDirection(language) { const rtlLangs = ['ar', 'arc', 'ckb', 'dv', 'fa', 'he', 'ps', 'ur', 'yi']; return rtlLangs.includes(language.split('-')[0]) ? 'rtl' : 'ltr'; } // Set current language manually (SSR) export function setCurrentLanguage(lng) { if (typeof window === 'undefined') globalCurrentLang = lng; } // Set language on hydration to ensure server and client match export function hydrateLanguage(callback) { if (typeof window === 'undefined') return; // Read language from storage const cookieLang = getCookie('i18next') || getCookie('i18nextLng'); const storedLang = localStorage.getItem('i18nextLng'); const currentLang = cookieLang || storedLang; if (currentLang && globalResources?.[currentLang]) { globalCurrentLang = currentLang; if (i18n.isInitialized) { // Only change language if it's actually different if (i18n.language !== currentLang) { i18n.changeLanguage(currentLang).then(() => { callback?.(); }); } else { // Language is already correct, just run callback callback?.(); } } else { // If i18n is not initialized yet, wait a bit setTimeout(() => { if (i18n.isInitialized && i18n.language !== currentLang) { i18n.changeLanguage(currentLang).then(() => { callback?.(); }); } else { callback?.(); } }, 100); } } else { callback?.(); } } // Access the raw i18n instance export function getI18nInstance() { return i18n; } // Helper function to get nested property safely function getNestedProperty(obj, path) { if (!obj || typeof obj !== 'object') return undefined; const keys = path.split('.'); let current = obj; for (const key of keys) { if (current === null || current === undefined || typeof current !== 'object') { return undefined; } current = current[key]; } return current; } // Translate key outside React components (safe for SSR and fallback) export function translate(key, fallback, options) { // In SSR, use globalResources directly if (typeof window === 'undefined') { const lang = globalCurrentLang ?? globalDefaultLang; const translationObj = globalResources?.[lang]?.translation; if (translationObj) { const result = getNestedProperty(translationObj, key); if (result && typeof result === 'string') { return result; } } return fallback ?? key; } // In browser, check i18n first if (i18n.isInitialized) { try { const result = i18n.t(key, options); // If result is different from key, it's a valid translation if (result && result !== key) { return result; } } catch (error) { console.warn('i18n.t error:', error); } } // Fallback: use globalResources if i18n is not initialized const currentLang = getCurrentLanguage(); const translationObj = globalResources?.[currentLang]?.translation; if (translationObj) { const result = getNestedProperty(translationObj, key); if (result && typeof result === 'string') { return result; } } // If translation not found, try default language if (currentLang !== globalDefaultLang) { const defaultTranslationObj = globalResources?.[globalDefaultLang]?.translation; if (defaultTranslationObj) { const result = getNestedProperty(defaultTranslationObj, key); if (result && typeof result === 'string') { return result; } } } return fallback ?? key; } // Set language in document head (useful for SEO) export function updateDocumentLanguage(lang) { if (typeof document === 'undefined') return; const currentLang = lang || getCurrentLanguage(); const direction = getLanguageDirection(currentLang); // Update lang attribute in html tag document.documentElement.lang = currentLang; document.documentElement.dir = direction; // Update body direction document.body.dir = direction; // Update meta tag if exists const metaLang = document.querySelector('meta[name="language"]'); if (metaLang) { metaLang.setAttribute('content', currentLang); } } // Helper function for debugging export function getI18nDebugInfo() { const isSSR = typeof window === 'undefined'; return { isInitialized: i18n.isInitialized, currentLanguage: getCurrentLanguage(), globalCurrentLang, globalDefaultLang, isSSR, cookies: isSSR ? null : (getCookie('i18next') || getCookie('i18nextLng')), localStorage: isSSR ? null : localStorage.getItem('i18nextLng'), }; }