Qwen3 / public /i18n.js
Semnykcz's picture
Upload 17 files
fe77b2f verified
// Enhanced Internationalization (i18n) utility for the AI Chat application
class I18n {
constructor() {
this.currentLocale = 'en';
this.translations = {};
this.fallbackLocale = 'en';
this.loadedLocales = new Set();
this.cache = new Map();
this.observers = [];
this.pluralRules = new Map();
this.dateTimeFormats = new Map();
this.numberFormats = new Map();
}
// Load translations for a specific locale with caching
async loadLocale(locale) {
if (this.loadedLocales.has(locale)) {
return this.translations[locale];
}
try {
const response = await fetch(`../locales/${locale}.json`);
if (!response.ok) {
throw new Error(`Failed to load locale ${locale}: ${response.status}`);
}
const translations = await response.json();
this.translations[locale] = translations;
this.loadedLocales.add(locale);
// Cache commonly used translations
this.cacheTranslations(locale, translations);
// Setup locale-specific formatters
this.setupLocaleFormatters(locale);
return translations;
} catch (error) {
console.warn(`Could not load locale ${locale}:`, error);
if (locale !== this.fallbackLocale) {
return await this.loadLocale(this.fallbackLocale);
}
throw error;
}
}
// Cache frequently used translations
cacheTranslations(locale, translations) {
const commonKeys = [
'app.title',
'chat.placeholder',
'chat.send',
'ui.loading',
'ui.error',
'ui.retry'
];
commonKeys.forEach(key => {
const value = this.getNestedValue(translations, key);
if (value !== undefined) {
this.cache.set(`${locale}:${key}`, value);
}
});
}
// Setup locale-specific formatters
setupLocaleFormatters(locale) {
try {
// Date/time formatters
this.dateTimeFormats.set(locale, {
short: new Intl.DateTimeFormat(locale, {
timeStyle: 'short'
}),
medium: new Intl.DateTimeFormat(locale, {
dateStyle: 'medium',
timeStyle: 'short'
}),
relative: new Intl.RelativeTimeFormat(locale, {
numeric: 'auto'
})
});
// Number formatters
this.numberFormats.set(locale, {
decimal: new Intl.NumberFormat(locale),
percent: new Intl.NumberFormat(locale, {
style: 'percent'
}),
currency: new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD'
})
});
// Plural rules
this.pluralRules.set(locale, new Intl.PluralRules(locale));
} catch (error) {
console.warn(`Failed to setup formatters for ${locale}:`, error);
}
}
// Set the current locale with enhanced features
async setLocale(locale) {
const previousLocale = this.currentLocale;
if (!this.translations[locale]) {
await this.loadLocale(locale);
}
this.currentLocale = locale;
// Update document language
document.documentElement.lang = locale;
// Update direction for RTL languages
const rtlLanguages = ['ar', 'he', 'fa', 'ur'];
document.documentElement.dir = rtlLanguages.includes(locale) ? 'rtl' : 'ltr';
// Store preference
localStorage.setItem('i18n-locale', locale);
// Notify observers
this.notifyObservers(locale, previousLocale);
// Trigger global locale change event
document.dispatchEvent(new CustomEvent('localeChanged', {
detail: {
locale: this.currentLocale,
previousLocale
}
}));
}
// Enhanced translation with context and pluralization
t(key, params = {}) {
const cacheKey = `${this.currentLocale}:${key}`;
// Check cache first
if (this.cache.has(cacheKey) && Object.keys(params).length === 0) {
return this.cache.get(cacheKey);
}
let translation = this.getNestedValue(
this.translations[this.currentLocale],
key
);
if (translation === undefined) {
// Fallback to default locale
translation = this.getNestedValue(
this.translations[this.fallbackLocale],
key
);
if (translation === undefined) {
console.warn(`Translation missing for key: ${key}`);
return key;
}
}
// Handle pluralization
if (typeof translation === 'object' && params.count !== undefined) {
translation = this.handlePluralization(translation, params.count);
}
// Handle interpolation
const result = this.interpolate(translation, params);
// Cache result if no parameters
if (Object.keys(params).length === 0) {
this.cache.set(cacheKey, result);
}
return result;
}
// Handle pluralization rules
handlePluralization(translations, count) {
const rules = this.pluralRules.get(this.currentLocale);
if (!rules) return translations.other || '';
const rule = rules.select(count);
return translations[rule] ||
translations.other ||
translations.one ||
'';
}
// Enhanced interpolation with formatting
interpolate(text, params) {
if (typeof text !== 'string') return text;
return text.replace(/\{\{(\w+)(?::(\w+))?\}\}/g, (match, key, format) => {
const value = params[key];
if (value === undefined) return match;
// Apply formatting if specified
if (format) {
return this.formatValue(value, format);
}
return value;
});
}
// Format values based on type
formatValue(value, format) {
const formatters = this.numberFormats.get(this.currentLocale);
const dateFormatters = this.dateTimeFormats.get(this.currentLocale);
switch (format) {
case 'number':
return formatters?.decimal.format(value) || value;
case 'percent':
return formatters?.percent.format(value) || value;
case 'currency':
return formatters?.currency.format(value) || value;
case 'date':
return dateFormatters?.medium.format(new Date(value)) || value;
case 'time':
return dateFormatters?.short.format(new Date(value)) || value;
case 'relative':
const now = Date.now();
const diff = Math.floor((value - now) / 1000);
return dateFormatters?.relative.format(diff, 'second') || value;
case 'uppercase':
return String(value).toUpperCase();
case 'lowercase':
return String(value).toLowerCase();
case 'capitalize':
return String(value).charAt(0).toUpperCase() + String(value).slice(1);
default:
return value;
}
}
// Get nested value from object using dot notation
getNestedValue(obj, path) {
return path.split('.').reduce((current, key) => {
return current && current[key] !== undefined ? current[key] : undefined;
}, obj);
}
// Add observer for locale changes
addObserver(callback) {
this.observers.push(callback);
}
// Remove observer
removeObserver(callback) {
const index = this.observers.indexOf(callback);
if (index > -1) {
this.observers.splice(index, 1);
}
}
// Notify all observers of locale change
notifyObservers(newLocale, oldLocale) {
this.observers.forEach(callback => {
try {
callback(newLocale, oldLocale);
} catch (error) {
console.error('Error in i18n observer:', error);
}
});
}
// Get current locale
getCurrentLocale() {
return this.currentLocale;
}
// Get available locales
getAvailableLocales() {
return Array.from(this.loadedLocales);
}
// Get all loaded translations (for debugging)
getAllTranslations() {
return this.translations;
}
// Initialize the i18n system with enhanced detection
async init(locale = null) {
try {
// Detect locale in order of preference
const detectedLocale = locale ||
localStorage.getItem('i18n-locale') ||
this.detectBrowserLocale() ||
this.fallbackLocale;
await this.loadLocale(detectedLocale);
await this.setLocale(detectedLocale);
console.log(`i18n initialized with locale: ${detectedLocale}`);
return true;
} catch (error) {
console.error('Failed to initialize i18n:', error);
return false;
}
}
// Detect browser locale
detectBrowserLocale() {
if (navigator.languages && navigator.languages.length) {
// Check each preferred language
for (const lang of navigator.languages) {
const shortLang = lang.split('-')[0];
// Return first supported language
const supportedLocales = ['en', 'es', 'fr', 'de', 'cs'];
if (supportedLocales.includes(shortLang)) {
return shortLang;
}
}
}
return navigator.language?.split('-')[0] || 'en';
}
// Preload multiple locales
async preloadLocales(locales) {
const promises = locales.map(locale => this.loadLocale(locale));
await Promise.allSettled(promises);
}
// Clear cache
clearCache() {
this.cache.clear();
}
// Get memory usage statistics
getStats() {
return {
loadedLocales: this.loadedLocales.size,
cachedTranslations: this.cache.size,
observers: this.observers.length,
currentLocale: this.currentLocale,
memoryUsage: JSON.stringify(this.translations).length
};
}
}
// Enhanced translation helper functions
class TranslationHelpers {
static createKeyExtractor(prefix = '') {
return (key) => prefix ? `${prefix}.${key}` : key;
}
static createScopedTranslator(i18n, scope) {
return (key, params) => i18n.t(`${scope}.${key}`, params);
}
static validateTranslations(translations, requiredKeys) {
const missing = [];
requiredKeys.forEach(key => {
if (!this.hasKey(translations, key)) {
missing.push(key);
}
});
return missing;
}
static hasKey(obj, path) {
return path.split('.').reduce((current, key) => {
return current && current[key] !== undefined ? current[key] : undefined;
}, obj) !== undefined;
}
static flattenTranslations(obj, prefix = '') {
const flattened = {};
for (const key in obj) {
const newKey = prefix ? `${prefix}.${key}` : key;
if (typeof obj[key] === 'object' && obj[key] !== null) {
Object.assign(flattened, this.flattenTranslations(obj[key], newKey));
} else {
flattened[newKey] = obj[key];
}
}
return flattened;
}
}
// Create global instance
const i18n = new I18n();
// Helper function for easy access with enhanced features
function t(key, params = {}) {
return i18n.t(key, params);
}
// Additional helper functions
function tc(key, count, params = {}) {
return i18n.t(key, { ...params, count });
}
function td(key, date, params = {}) {
return i18n.t(key, { ...params, date });
}
function tn(key, number, params = {}) {
return i18n.t(key, { ...params, number });
}
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
I18n,
i18n,
t,
tc,
td,
tn,
TranslationHelpers
};
}
// Export to global scope for debugging
window.i18nDebug = {
i18n,
TranslationHelpers,
getStats: () => i18n.getStats(),
clearCache: () => i18n.clearCache(),
getAllTranslations: () => i18n.getAllTranslations()
};