Files
CyberSim-OS/src/js/core/i18n.js
T

189 lines
5.7 KiB
JavaScript

/**
* CyberSim OS - Internationalization & Localization Engine (I18n)
*
* Offline-first, deterministic string translation with robust fallback policies.
*/
// Fallback embedded English catalog for instant initialization & environments without fetch
const EMBEDDED_EN = {
"login.title": "CyberSim OS - Enterprise Workstation Login",
"login.subtitle": "Simulated Workplace Environment",
"login.welcome": "Welcome",
"login.instructions": "Enter your first name and select an available network to begin your scheduled shift.",
"login.firstName": "First Name",
"login.firstNamePlaceholder": "e.g. Chris",
"login.firstNameRequired": "Please enter your first name.",
"login.firstNameTooLong": "First name must be 50 characters or less.",
"login.language": "Language",
"login.availableNetworks": "Available Networks & Workplaces",
"login.networkUnavailable": "Unavailable in selected language",
"login.selectNetwork": "Select a workplace network to connect",
"login.connect": "Connect to Workplace",
"login.connecting": "Connecting...",
"login.disclaimer": "CyberSim OS is a simulated training environment. Never enter real passwords or sensitive credentials.",
"login.sessionInfo": "Simulated Endpoint \u2022 Zero-Trust Security Active",
"desktop.start": "Start",
"desktop.loading": "Loading scenario package...",
"desktop.shiftResponsibilities": "Shift Responsibilities",
"desktop.applications": "Applications",
"desktop.finishShift": "Finish Shift & View Assessment",
"desktop.networkProtected": "Network: Protected",
"desktop.securityAndNotifications": "Security & Notifications",
"apps.inlook": "Inlook Mail",
"apps.navigator": "Navigator",
"apps.files": "Files",
"apps.securityCenter": "Security Center",
"apps.docViewer": "Doc Viewer",
"apps.verifyCert": "Verify Cert",
"common.close": "Close",
"common.reload": "Reload",
"common.unknown": "Unknown"
};
export class I18nService {
constructor({ defaultLocale = 'en', enabledLocales = ['en', 'es'], basePath = 'locales' } = {}) {
this.defaultLocale = defaultLocale;
this.activeLocale = defaultLocale;
this.enabledLocales = [...enabledLocales];
this.basePath = basePath;
this.dictionaries = {
en: { ...EMBEDDED_EN }
};
this.listeners = new Set();
}
/**
* Register change listener for reactive UI updates
*/
onChange(fn) {
this.listeners.add(fn);
return () => this.listeners.delete(fn);
}
/**
* Notify all listeners of a locale switch
*/
_notify() {
this.listeners.forEach(fn => {
try {
fn(this.activeLocale);
} catch (err) {
console.error('I18n onChange listener error:', err);
}
});
}
/**
* Load locale bundle from JSON or dictionary object
*/
async loadLocale(locale) {
if (this.dictionaries[locale] && Object.keys(this.dictionaries[locale]).length > 10) {
return this.dictionaries[locale];
}
try {
if (typeof fetch !== 'undefined') {
const res = await fetch(`${this.basePath}/${locale}.json`);
if (res.ok) {
const data = await res.json();
this.dictionaries[locale] = { ...(this.dictionaries[locale] || {}), ...data };
return this.dictionaries[locale];
}
}
} catch (e) {
console.warn(`[I18n] Could not fetch ${this.basePath}/${locale}.json, checking local dictionary.`);
}
return this.dictionaries[locale] || null;
}
/**
* Set custom dictionary directly
*/
setDictionary(locale, dictionary) {
this.dictionaries[locale] = {
...(this.dictionaries[locale] || {}),
...dictionary
};
}
/**
* Change current active locale and preload dictionary
*/
async setLocale(locale) {
const target = this.enabledLocales.includes(locale) ? locale : this.defaultLocale;
if (!this.dictionaries[target]) {
await this.loadLocale(target);
}
this.activeLocale = target;
this._notify();
return this.activeLocale;
}
getLocale() {
return this.activeLocale;
}
getEnabledLocales() {
return [...this.enabledLocales];
}
/**
* Core translation method with deterministic fallback policy:
* 1. Active locale
* 2. Deployment default locale
* 3. English ('en')
* 4. Visible missing key identifier `[missing: key]`
*/
t(key, params = {}) {
if (!key || typeof key !== 'string') return '';
let text = null;
// 1. Active locale
if (this.dictionaries[this.activeLocale] && this.dictionaries[this.activeLocale][key] !== undefined) {
text = this.dictionaries[this.activeLocale][key];
}
// 2. Deployment default locale
else if (this.dictionaries[this.defaultLocale] && this.dictionaries[this.defaultLocale][key] !== undefined) {
text = this.dictionaries[this.defaultLocale][key];
}
// 3. English fallback
else if (this.dictionaries['en'] && this.dictionaries['en'][key] !== undefined) {
text = this.dictionaries['en'][key];
}
// 4. Missing key fallback
if (text === null || text === undefined) {
return `[missing: ${key}]`;
}
// Interpolate {param} or {{param}}
if (params && typeof params === 'object') {
return text.replace(/\{?\{([^{}]+)\}\}?/g, (match, paramName) => {
const trimmed = paramName.trim();
if (params[trimmed] !== undefined && params[trimmed] !== null) {
return String(params[trimmed]);
}
return match;
});
}
return text;
}
/**
* Helper to format language display names
*/
getLocaleDisplayName(locale) {
const names = {
en: 'English',
es: 'Español'
};
return names[locale] || locale.toUpperCase();
}
}
// Singleton instance for global OS usage
export const i18n = new I18nService();