generated from Labyricorn/labyricorn-project-template
Implement CyberSim Phase 2.5: Localization, Branding, and Immersive Login
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* 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();
|
||||
Reference in New Issue
Block a user