// Top-level application container export default (Toolkit,_package)=>class App extends Toolkit.Component { // Instance fields #components; // Registered Toolkit.Components #locale; // Current display text dictionary #locales; // Registered display text dictionaries #title; // Application title ///////////////////////// Initialization Methods ////////////////////////// static { _package.App = { localize: (a,c)=>a.#localize(c), onCreate: (a,c)=>a.#onCreate(c), onDelete: (a,c)=>a.#onDelete(c) }; } constructor(overrides) { super(null, _package.override(overrides, { className: "tk-app" })); this.#components = new Set(); this.#locale = null; this.#locales = new Map(); this.#title = null; } /////////////////////////////// Properties //////////////////////////////// // Display text dictionary get locale() { return this.#locale?.get("id"); } set locale(value) { // Unset the locale if (value == null) { if (this.#locale == null) return; this.#locale = null; } // Change to a different locale else { value = value == null ? null : String(value); if (this.#locale?.get("id") == value) return; // Same locale specified if (!this.#locales.has(value)) throw new RangeError("No locale with ID " + value); this.#locale = this.#locales.get(value); } // Update all display text this.#localize(); } // Display title get title() { return this.#title; } set title(value) { if (value != null && !(value instanceof RegExp)) value = String(value); this.#title = value; this.#onLocalize(); } ///////////////////////////// Public Methods ////////////////////////////// // Register translation text addLocale(data) { // Error checking if (!(data instanceof Object)) throw new TypeError("Data must be an object."); // Working variables let locale = new Map(); let objects = [ [null,data] ]; // Process all objects while (objects.length != 0) { let object = objects.shift(); let prefix = object[0] ? object[0] + "." : ""; // Process all members of the object for (let entry of Object.entries(object[1])) { let key = prefix + entry[0]; let value = entry[1]; // Add the new object to he queue if (value instanceof Object) { objects.push([ key, entry[1] ]); continue; } // Error checking if (typeof(value) != "string") throw new TypeError("Non-string value encountered: "+key); // Register the localization value locale.set(key, new RegExp(value)); } } // Validate "id" let id = locale.get("id"); if (id == null) throw new Error("Locale does not contain \"id\" member."); // Register the locale this.#locales.set(id.source, locale); return id; } // Resolve the value of some display text translate(text, comp = null) { // Error checking if (comp != null) { if (!(comp instanceof Toolkit.Component)) throw new TypeError("Component must be a Toolkit.Component."); if (comp != this && comp.app != this) throw new RangeError("Compoment must belong to this App."); } // Nothing to resolve if (text == null) return null; // Input validation if (!(text instanceof RegExp)) text = String(text); // Working variables let locale = this.#locale; let ret = [ text ]; // Process all substitutions for (let x = 0; x < ret.length; x++) { let part = ret[x]; // Do not perform substitutions if (!(part instanceof RegExp)) continue; // Working variables part = part.source; // Locate the close of the innermost substitution let close = part.indexOf("}}"); if (close == -1) { if (part.indexOf("{{") != -1) throw new Error("Found {{ without matching }}."); ret[x] = part; continue; } // Locate the opening of the innermost substitution let open = part.substring(0, close).lastIndexOf("{{"); if (open == -1) throw new Error("Found }} without matching {{."); // Working variables let after = part.substring(close + 2); let before = part.substring(0, open); let key = part.substring(open + 2, close).trim(); let value = comp?.getSubstitution(key) ?? locale?.get(key) ?? "{{\u00d7" + key.toUpperCase() + "\u00d7}}"; let within = value instanceof RegExp ? value.source : value; // Compose the replacement text part = before + within + after; if (value instanceof RegExp) ret[x--] = new RegExp(part); else ret[x] = part; } return ret.join(""); } ///////////////////////////// Event Handlers ////////////////////////////// // Component created #onCreate(comp) { this.#components.add(comp); } // Component deleted #onDelete(comp) { this.#components.delete(comp); } // Configure display text #onLocalize() { document.title = this.translate(this.#title, this) ?? ""; } ///////////////////////////// Package Methods ///////////////////////////// // Update the display text for one or all components #localize(comp = null) { for (comp of (comp == null ? this.#components : [comp])) _package.Component.onLocalize(comp); } };