UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

256 lines (201 loc) • 7.25 kB
import { parseTooltipString } from "../../view/tooltip/gml/parser/parseTooltipString.js"; import { assert } from "../assert.js"; import { Cache } from "../cache/Cache.js"; import { computeHashArray } from "../collection/array/computeHashArray.js"; import { isArrayEqualStrict } from "../collection/array/isArrayEqualStrict.js"; import ObservedString from "../model/ObservedString.js"; import { seedVariablesIntoTemplateString } from "../parser/seedVariablesIntoTemplateString.js"; import { STRING_TEMPLATE_VARIABLE_REGEX } from "../parser/STRING_TEMPLATE_VARIABLE_REGEX.js"; import { computeStringHash } from "../primitives/strings/computeStringHash.js"; import { string_compute_similarity } from "../primitives/strings/string_compute_similarity.js"; import { LanguageMetadata } from "./LanguageMetadata.js"; /** * Validation utility method * @param {string} template * @return {string} */ function validationMockSeed(template) { const result = template.replace(STRING_TEMPLATE_VARIABLE_REGEX, function (match, varName) { return "0"; }); return result; } const EMPTY_SEED = Object.freeze({}); const DEFAULT_LANGUAGE_METADATA = Object.freeze(new LanguageMetadata()); export class Localization { constructor() { /** * * @type {AssetManager|null} */ this.assetManager = null; this.json = {}; /** * @protected * @type {LanguageMetadata} */ this.language_metadata = DEFAULT_LANGUAGE_METADATA; /** * * @type {ObservedString} */ this.locale = new ObservedString(''); /** * In debug mode error messages are a lot more verbose and helpful * @type {boolean} */ this.debug = true; /** * @type {Cache<Array<string>, string>} * @private */ this.__failure_cache = new Cache({ maxWeight: 1024, keyHashFunction: (key) => computeHashArray(key, computeStringHash), keyEqualityFunction: isArrayEqualStrict }); } /** * Measured in characters per second * @deprecated use 'language_metadata' directly instead * @return {number} */ get reading_speed() { return this.language_metadata.reading_speed; } /** * Time required to read a piece of text, in seconds * @param {string} text * @returns {number} time in seconds */ estimateReadingTime(text) { assert.isString(text, 'text'); return text.length / this.reading_speed; } /** * * @param {AssetManager} am */ setAssetManager(am) { this.assetManager = am; } /** * @returns {boolean} * @param {function(key:string, error:*, original: string)} errorConsumer */ validate(errorConsumer) { let result = true; for (let key in this.json) { const value = this.json[key]; const seededValue = validationMockSeed(value); try { parseTooltipString(seededValue); } catch (e) { result = false; errorConsumer(key, e, value); } } return result; } /** * Request locale switch * Assumes a specific folder structure, each different locale is stored with its locale code as name, and there's also "languages.json" file expected to be in the folder * @param {string} locale * @param {string} path where to look for localization data * @returns {Promise} */ loadLocale(locale, path = 'data/database/text') { assert.isString(locale, 'locale'); assert.isString(path, 'path'); const am = this.assetManager; if (am === null) { throw new Error('AssetManager is not set'); } let _path = path.trim(); while (_path.endsWith('/') || _path.endsWith('\\')) { // remove trailing slash if present _path.slice(0, _path.length - 1); } const pLoadData = am.promise(`${_path}/${locale}.json`, "json") .then(asset => { const json = asset.create(); this.json = json; this.locale.set(locale); }, reason => { console.error(`Failed to load locale data for locale '${locale}' : ${reason}`); // reset data this.json = {}; }); const pLoadMetadata = am.promise(`${_path}/languages.json`, "json") .then(asset => { const languages_metadata = asset.create(); this.language_metadata = LanguageMetadata.fromJSON(languages_metadata[locale]); }, reason => { console.error(`Failed to load language metadata: ${reason}`); this.language_metadata = DEFAULT_LANGUAGE_METADATA; }); return Promise.all([pLoadData, pLoadMetadata]); } /** * * @param {number} value */ formatIntegerByThousands(value) { const formatter = new Intl.NumberFormat(this.locale.getValue(), { useGrouping: true }); return formatter.format(value); } /** * * @param {string} id * @param {Object} seed * @private */ __debugMissingKey(id, seed) { const locale = this.locale.getValue(); const seed_string = JSON.stringify(seed); const message = this.__failure_cache.getOrCompute([locale, id, seed_string], () => { //try to find similar keys const similarities = Object.keys(this.json).map(function (key) { const distance = string_compute_similarity(key, id); return { key, distance }; }); similarities.sort(function (a, b) { return a.distance - b.distance; }); const suggestions = similarities.slice(0, 3).map(p => p.key); return `No localization value for id='${id}', seed=${seed_string}, best approximate matches: ${suggestions.join(', ')}`; }); console.warn(message); } /** * Get a localized string by a key * @param {string} key * @param {object} [seed] * * @returns {string} */ getString(key, seed = EMPTY_SEED) { assert.isString(key, 'id'); const value = this.json[key]; if (value === undefined) { if (this.debug) { this.__debugMissingKey(key, seed) } //no value, provide substitute return `@${key}`; } //value needs to be seeded return seedVariablesIntoTemplateString(value, seed); } /** * Does a given key exist? * @param {string} key * @return {boolean} */ hasString(key) { return this.json[key] !== undefined; } }