@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
256 lines (201 loc) • 7.25 kB
JavaScript
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;
}
}