kawkab-frontend
Version:
Kawkab frontend is a frontend library for the Kawkab framework
352 lines (351 loc) • 12.4 kB
JavaScript
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'),
};
}