@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
724 lines • 22.2 kB
JavaScript
/**
* @fileoverview OrdoJS Core - Internationalization Manager
*/
/**
* Internationalization Manager for OrdoJS applications
*/
export class I18nManager {
config;
currentLocale;
translations = new Map();
localeConfigs = new Map();
numberFormatters = new Map();
dateFormatters = new Map();
currencyFormatters = new Map();
constructor(config = {}) {
this.config = {
defaultLocale: 'en',
supportedLocales: ['en'],
fallbackLocale: 'en',
enableRTL: true,
enableNumberFormatting: true,
enableDateFormatting: true,
enableCurrencyFormatting: true,
enablePluralization: true,
enableGenderSupport: true,
enableContextSupport: true,
translationPath: '/translations',
autoDetectLocale: true,
persistLocale: true,
...config
};
this.currentLocale = this.config.defaultLocale;
this.initialize();
}
/**
* Initialize the i18n manager
*/
async initialize() {
// Auto-detect locale if enabled
if (this.config.autoDetectLocale) {
this.currentLocale = this.detectLocale();
}
// Load persisted locale if enabled
if (this.config.persistLocale) {
const persistedLocale = this.getPersistedLocale();
if (persistedLocale && this.config.supportedLocales.includes(persistedLocale)) {
this.currentLocale = persistedLocale;
}
}
// Load default locale translations
await this.loadTranslations(this.currentLocale);
// Setup locale configurations
this.setupLocaleConfigs();
// Setup formatters
this.setupFormatters();
// Setup RTL support
if (this.config.enableRTL) {
this.setupRTLSupport();
}
}
/**
* Detect user's preferred locale
*/
detectLocale() {
// Check browser language
const browserLocale = navigator.language || navigator.languages?.[0];
if (browserLocale) {
const baseLocale = browserLocale.split('-')[0];
if (this.config.supportedLocales.includes(baseLocale)) {
return baseLocale;
}
}
// Check for exact match
if (browserLocale && this.config.supportedLocales.includes(browserLocale)) {
return browserLocale;
}
return this.config.defaultLocale;
}
/**
* Get persisted locale from storage
*/
getPersistedLocale() {
try {
return localStorage.getItem('ordojs-locale');
}
catch {
return null;
}
}
/**
* Set persisted locale in storage
*/
setPersistedLocale(locale) {
try {
localStorage.setItem('ordojs-locale', locale);
}
catch {
// Ignore storage errors
}
}
/**
* Load translations for a locale
*/
async loadTranslations(locale) {
try {
const response = await fetch(`${this.config.translationPath}/${locale}.json`);
if (response.ok) {
const translations = await response.json();
this.translations.set(locale, translations);
}
else {
// Load fallback translations
if (locale !== this.config.fallbackLocale) {
await this.loadTranslations(this.config.fallbackLocale);
}
}
}
catch (error) {
console.warn(`Failed to load translations for locale: ${locale}`, error);
// Load fallback translations
if (locale !== this.config.fallbackLocale) {
await this.loadTranslations(this.config.fallbackLocale);
}
}
}
/**
* Setup locale configurations
*/
setupLocaleConfigs() {
// English
this.localeConfigs.set('en', {
code: 'en',
name: 'English',
nativeName: 'English',
rtl: false,
numberFormat: { style: 'decimal' },
dateFormat: {
year: 'numeric',
month: 'long',
day: 'numeric'
},
currencyFormat: {
style: 'currency',
currency: 'USD'
},
pluralRules: {
one: 'one',
other: 'other'
},
genderRules: {
male: 'male',
female: 'female',
neutral: 'neutral'
}
});
// Spanish
this.localeConfigs.set('es', {
code: 'es',
name: 'Spanish',
nativeName: 'Español',
rtl: false,
numberFormat: { style: 'decimal' },
dateFormat: {
year: 'numeric',
month: 'long',
day: 'numeric'
},
currencyFormat: {
style: 'currency',
currency: 'EUR'
},
pluralRules: {
one: 'one',
other: 'other'
},
genderRules: {
male: 'male',
female: 'female',
neutral: 'neutral'
}
});
// Arabic
this.localeConfigs.set('ar', {
code: 'ar',
name: 'Arabic',
nativeName: 'العربية',
rtl: true,
numberFormat: { style: 'decimal' },
dateFormat: {
year: 'numeric',
month: 'long',
day: 'numeric'
},
currencyFormat: {
style: 'currency',
currency: 'SAR'
},
pluralRules: {
zero: 'zero',
one: 'one',
two: 'two',
few: 'few',
many: 'many',
other: 'other'
},
genderRules: {
male: 'male',
female: 'female',
neutral: 'neutral'
}
});
// French
this.localeConfigs.set('fr', {
code: 'fr',
name: 'French',
nativeName: 'Français',
rtl: false,
numberFormat: { style: 'decimal' },
dateFormat: {
year: 'numeric',
month: 'long',
day: 'numeric'
},
currencyFormat: {
style: 'currency',
currency: 'EUR'
},
pluralRules: {
one: 'one',
other: 'other'
},
genderRules: {
male: 'male',
female: 'female',
neutral: 'neutral'
}
});
// German
this.localeConfigs.set('de', {
code: 'de',
name: 'German',
nativeName: 'Deutsch',
rtl: false,
numberFormat: { style: 'decimal' },
dateFormat: {
year: 'numeric',
month: 'long',
day: 'numeric'
},
currencyFormat: {
style: 'currency',
currency: 'EUR'
},
pluralRules: {
one: 'one',
other: 'other'
},
genderRules: {
male: 'male',
female: 'female',
neutral: 'neutral'
}
});
// Chinese (Simplified)
this.localeConfigs.set('zh-CN', {
code: 'zh-CN',
name: 'Chinese (Simplified)',
nativeName: '简体中文',
rtl: false,
numberFormat: { style: 'decimal' },
dateFormat: {
year: 'numeric',
month: 'long',
day: 'numeric'
},
currencyFormat: {
style: 'currency',
currency: 'CNY'
},
pluralRules: {
other: 'other'
},
genderRules: {
male: 'male',
female: 'female',
neutral: 'neutral'
}
});
// Japanese
this.localeConfigs.set('ja', {
code: 'ja',
name: 'Japanese',
nativeName: '日本語',
rtl: false,
numberFormat: { style: 'decimal' },
dateFormat: {
year: 'numeric',
month: 'long',
day: 'numeric'
},
currencyFormat: {
style: 'currency',
currency: 'JPY'
},
pluralRules: {
other: 'other'
},
genderRules: {
male: 'male',
female: 'female',
neutral: 'neutral'
}
});
}
/**
* Setup formatters
*/
setupFormatters() {
this.config.supportedLocales.forEach(locale => {
const config = this.localeConfigs.get(locale);
if (config) {
// Number formatter
this.numberFormatters.set(locale, new Intl.NumberFormat(locale, config.numberFormat));
// Date formatter
this.dateFormatters.set(locale, new Intl.DateTimeFormat(locale, config.dateFormat));
// Currency formatter
this.currencyFormatters.set(locale, new Intl.NumberFormat(locale, config.currencyFormat));
}
});
}
/**
* Setup RTL support
*/
setupRTLSupport() {
const config = this.localeConfigs.get(this.currentLocale);
if (config?.rtl) {
document.documentElement.setAttribute('dir', 'rtl');
document.documentElement.setAttribute('lang', this.currentLocale);
}
else {
document.documentElement.setAttribute('dir', 'ltr');
document.documentElement.setAttribute('lang', this.currentLocale);
}
}
/**
* Translate a key with optional context
*/
translate(key, context = {}) {
const translation = this.getTranslation(key);
if (typeof translation === 'function') {
return translation(context);
}
if (typeof translation === 'string') {
return this.interpolate(translation, context);
}
// Return key if translation not found
return key;
}
/**
* Get translation for a key
*/
getTranslation(key) {
const currentTranslations = this.translations.get(this.currentLocale);
const fallbackTranslations = this.translations.get(this.config.fallbackLocale);
if (currentTranslations) {
const translation = this.getNestedTranslation(currentTranslations, key);
if (translation !== undefined) {
return translation;
}
}
if (fallbackTranslations && this.currentLocale !== this.config.fallbackLocale) {
const translation = this.getNestedTranslation(fallbackTranslations, key);
if (translation !== undefined) {
return translation;
}
}
return undefined;
}
/**
* Get nested translation from object
*/
getNestedTranslation(translations, key) {
const keys = key.split('.');
let current = translations;
for (const k of keys) {
if (current && typeof current === 'object' && k in current) {
current = current[k];
}
else {
return undefined;
}
}
return current;
}
/**
* Interpolate variables in translation string
*/
interpolate(text, context) {
return text.replace(/\{\{(\w+)\}\}/g, (match, key) => {
return context[key] !== undefined ? String(context[key]) : match;
});
}
/**
* Format number according to current locale
*/
formatNumber(value, options) {
const formatter = this.numberFormatters.get(this.currentLocale);
if (formatter) {
return formatter.format(value);
}
// Fallback to default formatting
return new Intl.NumberFormat(this.currentLocale, options).format(value);
}
/**
* Format date according to current locale
*/
formatDate(date, options) {
const formatter = this.dateFormatters.get(this.currentLocale);
if (formatter) {
return formatter.format(date);
}
// Fallback to default formatting
return new Intl.DateTimeFormat(this.currentLocale, options).format(date);
}
/**
* Format currency according to current locale
*/
formatCurrency(value, currency, options) {
const config = this.localeConfigs.get(this.currentLocale);
const currencyCode = currency || config?.currencyFormat.currency || 'USD';
const formatter = this.currencyFormatters.get(this.currentLocale);
if (formatter) {
return formatter.format(value);
}
// Fallback to default formatting
return new Intl.NumberFormat(this.currentLocale, {
style: 'currency',
currency: currencyCode,
...options
}).format(value);
}
/**
* Handle pluralization
*/
pluralize(key, count, context = {}) {
if (!this.config.enablePluralization) {
return this.translate(key, { ...context, count });
}
const config = this.localeConfigs.get(this.currentLocale);
const pluralRule = this.getPluralRule(count, config?.pluralRules);
const pluralKey = `${key}.${pluralRule}`;
const translation = this.translate(pluralKey, { ...context, count });
// If no plural translation found, fall back to base key
if (translation === pluralKey) {
return this.translate(key, { ...context, count });
}
return translation;
}
/**
* Get plural rule for count
*/
getPluralRule(count, rules) {
if (!rules) {
return count === 1 ? 'one' : 'other';
}
// English-like rules
if (rules.one && rules.other) {
return count === 1 ? 'one' : 'other';
}
// Arabic-like rules
if (rules.zero && rules.one && rules.two && rules.few && rules.many && rules.other) {
if (count === 0)
return 'zero';
if (count === 1)
return 'one';
if (count === 2)
return 'two';
if (count >= 3 && count <= 10)
return 'few';
if (count >= 11 && count <= 99)
return 'many';
return 'other';
}
// Chinese/Japanese-like rules
if (rules.other) {
return 'other';
}
return 'other';
}
/**
* Handle gender-specific translations
*/
genderize(key, gender, context = {}) {
if (!this.config.enableGenderSupport) {
return this.translate(key, { ...context, gender });
}
const genderKey = `${key}.${gender}`;
const translation = this.translate(genderKey, { ...context, gender });
// If no gender-specific translation found, fall back to base key
if (translation === genderKey) {
return this.translate(key, { ...context, gender });
}
return translation;
}
/**
* Change current locale
*/
async setLocale(locale) {
if (!this.config.supportedLocales.includes(locale)) {
throw new Error(`Unsupported locale: ${locale}`);
}
this.currentLocale = locale;
// Load translations if not already loaded
if (!this.translations.has(locale)) {
await this.loadTranslations(locale);
}
// Update RTL support
if (this.config.enableRTL) {
this.setupRTLSupport();
}
// Persist locale if enabled
if (this.config.persistLocale) {
this.setPersistedLocale(locale);
}
// Dispatch locale change event
window.dispatchEvent(new CustomEvent('localeChanged', { detail: { locale } }));
}
/**
* Get current locale
*/
getCurrentLocale() {
return this.currentLocale;
}
/**
* Get supported locales
*/
getSupportedLocales() {
return this.config.supportedLocales;
}
/**
* Get locale configuration
*/
getLocaleConfig(locale) {
return this.localeConfigs.get(locale);
}
/**
* Check if locale is RTL
*/
isRTL(locale) {
const targetLocale = locale || this.currentLocale;
const config = this.localeConfigs.get(targetLocale);
return config?.rtl || false;
}
/**
* Get locale name
*/
getLocaleName(locale) {
const config = this.localeConfigs.get(locale);
return config?.name || locale;
}
/**
* Get native locale name
*/
getNativeLocaleName(locale) {
const config = this.localeConfigs.get(locale);
return config?.nativeName || locale;
}
/**
* Add custom locale configuration
*/
addLocaleConfig(locale, config) {
this.localeConfigs.set(locale, config);
this.setupFormatters();
}
/**
* Add custom translations
*/
addTranslations(locale, translations) {
const existing = this.translations.get(locale) || {};
this.translations.set(locale, { ...existing, ...translations });
}
/**
* Create translation workflow
*/
createTranslationWorkflow() {
return new TranslationWorkflow(this);
}
/**
* Export translations for a locale
*/
exportTranslations(locale) {
return this.translations.get(locale) || {};
}
/**
* Import translations for a locale
*/
importTranslations(locale, translations) {
this.translations.set(locale, translations);
}
/**
* Get missing translations
*/
getMissingTranslations(locale, baseLocale = this.config.defaultLocale) {
const baseTranslations = this.translations.get(baseLocale) || {};
const targetTranslations = this.translations.get(locale) || {};
const missing = [];
this.findMissingKeys(baseTranslations, targetTranslations, '', missing);
return missing;
}
/**
* Find missing translation keys
*/
findMissingKeys(base, target, prefix, missing) {
for (const key in base) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (!(key in target)) {
missing.push(fullKey);
}
else if (typeof base[key] === 'object' && typeof target[key] === 'object') {
this.findMissingKeys(base[key], target[key], fullKey, missing);
}
}
}
}
/**
* Translation Workflow for managing translations
*/
export class TranslationWorkflow {
i18n;
constructor(i18n) {
this.i18n = i18n;
}
/**
* Extract translatable strings from code
*/
extractStrings(code) {
const strings = [];
const patterns = [
/translate\(['"`]([^'"`]+)['"`]/g,
/t\(['"`]([^'"`]+)['"`]/g,
/\{\{([^}]+)\}\}/g
];
patterns.forEach(pattern => {
let match;
while ((match = pattern.exec(code)) !== null) {
strings.push(match[1]);
}
});
return [...new Set(strings)];
}
/**
* Generate translation template
*/
generateTemplate(locale, strings) {
const template = {};
strings.forEach(str => {
const keys = str.split('.');
let current = template;
keys.forEach((key, index) => {
if (index === keys.length - 1) {
current[key] = str;
}
else {
current[key] = current[key] || {};
current = current[key];
}
});
});
return template;
}
/**
* Validate translations
*/
validateTranslations(locale) {
const errors = [];
const translations = this.i18n.exportTranslations(locale);
// Check for missing translations
const missing = this.i18n.getMissingTranslations(locale);
if (missing.length > 0) {
errors.push(`Missing translations: ${missing.join(', ')}`);
}
// Check for unused translations
const unused = this.findUnusedTranslations(locale);
if (unused.length > 0) {
errors.push(`Unused translations: ${unused.join(', ')}`);
}
return {
valid: errors.length === 0,
errors
};
}
/**
* Find unused translations
*/
findUnusedTranslations(locale) {
// This would require scanning the entire codebase
// For now, return empty array
return [];
}
/**
* Merge translations
*/
mergeTranslations(locale, newTranslations) {
const existing = this.i18n.exportTranslations(locale);
const merged = this.deepMerge(existing, newTranslations);
this.i18n.importTranslations(locale, merged);
}
/**
* Deep merge objects
*/
deepMerge(target, source) {
const result = { ...target };
for (const key in source) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
result[key] = this.deepMerge(target[key] || {}, source[key]);
}
else {
result[key] = source[key];
}
}
return result;
}
}
//# sourceMappingURL=i18n-manager.js.map