i18n: Massive rework to modernize features
* Added support for multiple base languages. * Added support for per-language base languages. * Added caching for language translation chain. * Changed language functionality to allow creating, loading, saving and destroying a language. * Added functionality to set, get and clear keys for a language. * Added functionality to override resolver and applier for domTranslate (previously autoTranslate).
This commit is contained in:
committed by
Michael Fabian Dirks
parent
a3604e7a22
commit
44cf17a348
@@ -15,152 +15,536 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let group = "i18n";
|
/*
|
||||||
let group_style = "font-weight: bold;";
|
Dead Simple I18n Library
|
||||||
let text_style = "font-weight: inherit;";
|
|
||||||
|
Languages are stored in a Map() to allow for any kind of name to be used. In
|
||||||
|
addition to that there is a global base language, and each language can
|
||||||
|
specify an override base language which will be used only if that language
|
||||||
|
is actually loaded during translation. The base language can be an array
|
||||||
|
instead of a string, in which each language is tested in order of appearance.
|
||||||
|
|
||||||
|
The key can only be a string, while the Value is allowed to be anything. In
|
||||||
|
case you want to implement pluralization in your translation layer, you would
|
||||||
|
be doing it with an object value or similar. This creates a simple structure
|
||||||
|
that can be used for various kinds of translations, and is as fast as the
|
||||||
|
underlying Map implementation.
|
||||||
|
|
||||||
|
# Example: Evaluation of the language chain [en-US < en-GB < de-DE < de-BE]
|
||||||
|
Searching for the key will first happen in de-BE, then in de-DE, then in en-GB
|
||||||
|
and finally in en-US. If de-BE, de-DE or en-GB is not loaded, that specific
|
||||||
|
lookup step skips straight to the global base language, in this case en-US. If
|
||||||
|
en-US is not loaded, the original i18n key is returned instead.
|
||||||
|
|
||||||
|
# Example: Evaluation of the language chain [en-US < en-GB < de-DE, en-GB < de-BE]
|
||||||
|
Checks first happen in de-BE, if de-BE does not have the key checks move on to
|
||||||
|
de-DE. If de-DE is not loaded, en-GB is instead checked. If de-DE does not
|
||||||
|
have the key, checks progress to en-GB. If en-GB does not have the key, checks
|
||||||
|
progress to en-US. If any of them are not loaded or does not have the key, the
|
||||||
|
next language in the base languages is checked, until the base languages are
|
||||||
|
exhausted, in which case the i18n key is returned.
|
||||||
|
|
||||||
|
# Structure
|
||||||
|
Map{
|
||||||
|
languageName: Map{
|
||||||
|
// Base language override (optional)
|
||||||
|
_base: {string},
|
||||||
|
// Key Value storage for lookup
|
||||||
|
{string key}: {any value},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Internal behavior
|
||||||
|
The library will build a chain of languages to check in a loop for, to avoid
|
||||||
|
costly recursive calls that aren't well protected against loops in base
|
||||||
|
language definitions (i.e. de-DE depends on en-GB, en-GB depends on de-DE).
|
||||||
|
This cache is refreshed when first attempting to translate to that language,
|
||||||
|
and flagged as dirty every time a language is loaded or a base language is
|
||||||
|
changed.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
let group = 'i18n';
|
||||||
|
let group_style = 'font-weight: bold;';
|
||||||
|
let text_style = 'font-weight: inherit;';
|
||||||
|
|
||||||
class I18n {
|
class I18n {
|
||||||
/** Create a new object, ready to be used.
|
/** Create a new object, ready to be used.
|
||||||
*
|
*
|
||||||
* @param {string} urlFormat URL from which to asynchronously load translation data from in the format 'url/file{0}.extension' ({0} is replaced by the lowercase language name).
|
* @param {string} language Initial base language to base all translations on.
|
||||||
|
* @param {string} baseLanguageKey Key to use for base language overrides. (Default = _base)
|
||||||
|
* @throws Exception on invalid parameters.
|
||||||
*/
|
*/
|
||||||
constructor(urlFormat) {
|
constructor(language, baseLanguageKey = '_base') {
|
||||||
console.log("%c[%s]%c Initializing ...", group_style, group, text_style);
|
this._sanitizeLanguage(language);
|
||||||
if (typeof(urlFormat) != "string") {
|
this._verifyKey(baseLanguageKey);
|
||||||
throw "urlFormat must be of type string";
|
|
||||||
|
console.log('%c[%s]%c Initializing ...', group_style, group, text_style);
|
||||||
|
|
||||||
|
this.languages = new Map();
|
||||||
|
this.chains = new Map();
|
||||||
|
this.baseLanguage = language;
|
||||||
|
this.baseLanguageKey = baseLanguageKey;
|
||||||
|
|
||||||
|
this.dirtyTs = performance.now();
|
||||||
|
this.chainsTs = performance.now();
|
||||||
}
|
}
|
||||||
this.url = urlFormat;
|
|
||||||
this.languages = {};
|
_verifyLanguageKnown(language) {
|
||||||
this.languages.base = '';
|
if (!this.languages.has(language)) {
|
||||||
|
throw 'language unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_verifyKey(key) {
|
||||||
|
if (typeof (key) == 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw 'key must be of type string';
|
||||||
|
}
|
||||||
|
|
||||||
|
_verifyKeyKnown(language, key) {
|
||||||
|
this._verifyLanguageKnown(language);
|
||||||
|
this._verifyKey(key);
|
||||||
|
|
||||||
|
if (!this.languages.get(language).has(key)) {
|
||||||
|
throw 'key unknown in language';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_verifyData(data) {
|
||||||
|
if (typeof (data) == 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof (data) == 'object') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data instanceof File) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data instanceof Blob) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw 'data must be of type string, object, File or Blob';
|
||||||
}
|
}
|
||||||
|
|
||||||
_sanitizeLanguage(language) {
|
_sanitizeLanguage(language) {
|
||||||
if (typeof(language) != "string") {
|
try {
|
||||||
throw "language must be of type string";
|
this._verifyKey(language);
|
||||||
|
} catch (e) {
|
||||||
|
throw 'language must be of type string';
|
||||||
}
|
}
|
||||||
return language.toLowerCase();
|
return language.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Load a new language.
|
_defaultResolver(language, key, element) {
|
||||||
|
return this.translate(key, language);
|
||||||
|
}
|
||||||
|
|
||||||
|
_defaultApplier(language, key, translation, element) {
|
||||||
|
try {
|
||||||
|
element.textContent = translation;
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Caches the necessary language chain for translation.
|
||||||
*
|
*
|
||||||
* @param {string} language Name of the language file.
|
* Creates and returns a language chain required for translation, avoiding
|
||||||
* @param {boolean} isBaseLanguage Use this language as the new base language.
|
* recursive loops that never end in the process. The chain will not
|
||||||
* @returns {Promise}
|
* evalute the entire branch first, instead it will check all branches
|
||||||
|
* and then list the childs of those branches. This is an expensive
|
||||||
|
* operation and should only be done once on every language update.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* de-DE +- en-GB - en-US - de-DE (- en-GB, en-US, ... [recursive])
|
||||||
|
* \- en-US - de-DE (- en-GB, en-US, ... [recursive])
|
||||||
|
* Result: [de-DE, en-GB, en-US]
|
||||||
|
* Explanation: de-DE depends on both en-GB and en-US, and will check both
|
||||||
|
* before considering the next branch. Since all dependencies are resolved
|
||||||
|
* early, a recursive lookup is prevented here.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param {string} language
|
||||||
|
* @returns {array} Translation chain
|
||||||
*/
|
*/
|
||||||
load(language, isBaseLanguage) {
|
_cacheChain(language) {
|
||||||
|
// This caches a chain so that we do not have to rebuild this every
|
||||||
|
// lookup. It is important that there are no recursive loops in this
|
||||||
|
// code, which means we can't rely on this function to work until the
|
||||||
|
// cache is completed.
|
||||||
|
|
||||||
|
// Have there been changes to the timestamp?
|
||||||
|
if (this.chainsTs < this.dirtyTs) {
|
||||||
|
// If yes, clear all chains for rebuilding.
|
||||||
|
this.chainsTs = this.dirtyTs;
|
||||||
|
this.chains.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we have a chain cached?
|
||||||
|
if (this.languageChains.has(language)) {
|
||||||
|
// Yes, return the cached chain and go back to translation.
|
||||||
|
return this.languageChains.get(language);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new chain without relying on our own function.
|
||||||
|
let chain = [language]; // Chains always contain the language itself.
|
||||||
|
|
||||||
|
// Now we walk through the chain manually, modifying it as we go.
|
||||||
|
for (let pos = 0; pos < chain.length; pos++) {
|
||||||
|
// Check if the language is loaded, if not skip it.
|
||||||
|
if (!this.languages.has(chain[pos])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let languageMap = this.languages.get(chain[pos]);
|
||||||
|
|
||||||
|
// Check if there is a base language override.
|
||||||
|
if (!languageMap.has(this.baseLanguageKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If yes, walk it.
|
||||||
|
let baseLanguages = languageMap.get(this.baseLanguageKey);
|
||||||
|
if (typeof (baseLanguages) == 'string') {
|
||||||
|
baseLanguages = [baseLanguages];
|
||||||
|
} else if (typeof (baseLanguages) == 'array') {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (baseLanguage in baseLanguages) {
|
||||||
|
if (!chain.includes(baseLanguage)) {
|
||||||
|
chain = chain.push(baseLanguage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are now through with the actual language chain, so we now have to check for the base language chain.
|
||||||
|
// The logic for this is identical to the above.
|
||||||
|
let baseChain = this.baseLanguage;
|
||||||
|
if (typeof (this.baseLanguage) == 'string') {
|
||||||
|
baseChain = [baseChain];
|
||||||
|
}
|
||||||
|
for (let pos = 0; pos < baseChain.length; pos++) {
|
||||||
|
// Check if the language is loaded, if not skip it.
|
||||||
|
if (!this.languages.has(baseChain[pos])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let languageMap = this.languages.get(baseChain[pos]);
|
||||||
|
|
||||||
|
// Check if there is a base language override.
|
||||||
|
if (!languageMap.has(this.baseLanguageKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If yes, walk it.
|
||||||
|
let baseLanguages = languageMap.get(this.baseLanguageKey);
|
||||||
|
if (typeof (baseLanguages) == 'string') {
|
||||||
|
baseLanguages = [baseLanguages];
|
||||||
|
} else if (typeof (baseLanguages) == 'array') {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (baseLanguage in baseLanguages) {
|
||||||
|
if ((!chain.includes(baseLanguage)) && (!baseChain.includes(baseLanguage))) {
|
||||||
|
baseChain = baseChain.push(baseLanguage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concat normal walk and base chain walk.
|
||||||
|
chain = chain.concat(baseChain);
|
||||||
|
|
||||||
|
// Store.
|
||||||
|
this.chains.set(language, chain);
|
||||||
|
|
||||||
|
// Return.
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a new language.
|
||||||
|
*
|
||||||
|
* @param {string} language Name of the language.
|
||||||
|
* @throws Exception on invalid parameters.
|
||||||
|
*/
|
||||||
|
createLanguage(language) {
|
||||||
language = this._sanitizeLanguage(language);
|
language = this._sanitizeLanguage(language);
|
||||||
if (isBaseLanguage == true) {
|
|
||||||
this.languages.base = language;
|
|
||||||
}
|
|
||||||
if (this.languages[language] == undefined) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let self = this;
|
|
||||||
let url = this.url.replace('{0}', language);
|
|
||||||
console.debug("%c[%s]%c Loading language '%s' from url '%s'...",
|
|
||||||
group_style, group, text_style,
|
|
||||||
language, url);
|
|
||||||
|
|
||||||
let req = new XMLHttpRequest();
|
console.debug('%c[%s]%c Creating language "%s"...',
|
||||||
req.addEventListener("error", function (event) {
|
|
||||||
console.log("%c[%s]%c Failed to load language '%s': ",
|
|
||||||
group_style, group, text_style,
|
|
||||||
language, event);
|
|
||||||
|
|
||||||
});
|
|
||||||
req.addEventListener("progress", function (event) {
|
|
||||||
let prc = event.loaded / event.total;
|
|
||||||
if (event.loaded == event.total) {
|
|
||||||
prc = 100.0;
|
|
||||||
}
|
|
||||||
if (event.total == 0) {
|
|
||||||
prc = 0.0;
|
|
||||||
}
|
|
||||||
console.debug("%c[%s]%c Loading language '%s'... [%6.2f%%]",
|
|
||||||
group_style, group, text_style,
|
|
||||||
language, prc);
|
|
||||||
});
|
|
||||||
req.addEventListener("load", function (event) {
|
|
||||||
console.debug("%c[%s]%c Parsing language '%s'...",
|
|
||||||
group_style, group, text_style,
|
group_style, group, text_style,
|
||||||
language);
|
language);
|
||||||
|
this.languages.set(language, new Map());
|
||||||
|
console.debug('%c[%s]%c Created language "%s".',
|
||||||
|
group_style, group, text_style,
|
||||||
|
language);
|
||||||
|
this.dirtyTs = performance.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Load a new language.
|
||||||
|
*
|
||||||
|
* @param {string} language Name of the language.
|
||||||
|
* @param {File/Blob/object/string} data Data containing a JSON representation of the language.
|
||||||
|
* @param {string} encoding (Optional) Encoding to use when reading File or Blob.
|
||||||
|
* @returns {Promise}
|
||||||
|
* @throws Exception on invalid parameters or invalid data.
|
||||||
|
*/
|
||||||
|
loadLanguage(language, data, encoding = 'utf-8') {
|
||||||
|
// Verify input.
|
||||||
|
language = this._sanitizeLanguage(language);
|
||||||
|
_verifyData(data);
|
||||||
|
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
console.debug('%c[%s]%c Loading language "%s"...',
|
||||||
|
group_style, group, text_style,
|
||||||
|
language);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
self.languages[language] = JSON.parse(req.responseText);
|
let json_data;
|
||||||
|
|
||||||
|
// Decode File, Blob and string to JSON object.
|
||||||
|
if ((data instanceof File) || (data instanceof Blob)) {
|
||||||
|
await new Promise((resolve2, reject2) => {
|
||||||
|
let freader = new FileReader();
|
||||||
|
freader.onload((ev) => {
|
||||||
|
resolve2();
|
||||||
|
});
|
||||||
|
freader.onabort((ev) => {
|
||||||
|
reject2(ev);
|
||||||
|
});
|
||||||
|
freader.onerror((ev) => {
|
||||||
|
reject2(ev);
|
||||||
|
});
|
||||||
|
freader.readAsText(data, encoding);
|
||||||
|
});
|
||||||
|
json_data = JSON.parse(freader.result);
|
||||||
|
} else if (typeof (data) == 'string') {
|
||||||
|
json_data = JSON.parse(data);
|
||||||
|
} else if (typeof (data) == 'object') {
|
||||||
|
json_data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
let language_map = new Map();
|
||||||
|
for (key in json_data) {
|
||||||
|
language_map.set(key, json_data[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.languages.set(language, language_map);
|
||||||
|
this.dirtyTs = performance.now();
|
||||||
|
|
||||||
|
console.debug('%c[%s]%c Loaded language "%s".',
|
||||||
|
group_style, group, text_style, language);
|
||||||
|
resolve(language);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("%c[%s]%c Failed to load language '%s': ",
|
console.error('%c[%s]%c Failed to load language "%s": %o',
|
||||||
group_style, group, text_style,
|
group_style, group, text_style,
|
||||||
language, e);
|
language, e);
|
||||||
reject(e);
|
reject(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resolve(true);
|
});
|
||||||
console.log("%c[%s]%c Loaded language '%s'.",
|
}
|
||||||
|
|
||||||
|
/** Save a language.
|
||||||
|
*
|
||||||
|
* @param {string} language Name of the language.
|
||||||
|
* @returns {Promise} Promise that eventually returns the JSON data of the language.
|
||||||
|
* @throws Exception on invalid parameters and missing language.
|
||||||
|
*/
|
||||||
|
saveLanguage(language) {
|
||||||
|
language = this._sanitizeLanguage(language);
|
||||||
|
this._verifyLanguageKnown(language);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
console.debug('%c[%s]%c Saving language "%s"...',
|
||||||
group_style, group, text_style,
|
group_style, group, text_style,
|
||||||
language);
|
language);
|
||||||
return;
|
|
||||||
|
this._verifyLanguageKnown(language);
|
||||||
|
|
||||||
|
let language_data = {};
|
||||||
|
let language_map = this.languages.get(language);
|
||||||
|
language_map.forEach((value, key, map) => {
|
||||||
|
language_data[key] = value;
|
||||||
});
|
});
|
||||||
req.open("GET", url);
|
let json_data = JSON.stringify(language_data);
|
||||||
req.overrideMimeType("text/plain; charset=utf-8");
|
|
||||||
req.send();
|
console.debug('%c[%s]%c Saved language "%s".',
|
||||||
|
group_style, group, text_style,
|
||||||
|
language);
|
||||||
|
resolve(json_data);
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
console.log("%c[%s]%c Loaded language '%s'...",
|
|
||||||
group_style, group, text_style, language);
|
|
||||||
return new Promise((resolve, reject) => { resolve(true); });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Destroy/unload a language.
|
||||||
|
*
|
||||||
|
* @param {string} language Name of the language.
|
||||||
|
* @throws Exception on invalid parameters and missing language.
|
||||||
|
*/
|
||||||
|
destroyLanguage(language) {
|
||||||
|
language = this._sanitizeLanguage(language);
|
||||||
|
this._verifyLanguageKnown(language);
|
||||||
|
|
||||||
|
console.debug('%c[%s]%c Destroying language "%s"...',
|
||||||
|
group_style, group, text_style,
|
||||||
|
language);
|
||||||
|
|
||||||
|
this.languages.delete(language);
|
||||||
|
this.dirtyTs = performance.now();
|
||||||
|
|
||||||
|
console.debug('%c[%s]%c Destroyed language "%s".',
|
||||||
|
group_style, group, text_style,
|
||||||
|
language);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clear a key from a language.
|
||||||
|
*
|
||||||
|
* @param {string} language Language to edit.
|
||||||
|
* @param {string} key Key to clear.
|
||||||
|
* @return {bool} true on success.
|
||||||
|
* @throws Exception on invalid parameters and missing language.
|
||||||
|
*/
|
||||||
|
clearKey(language, key) {
|
||||||
|
// Verify and sanitize input.
|
||||||
|
language = this._sanitizeLanguage(language);
|
||||||
|
this._verifyKey(key);
|
||||||
|
|
||||||
|
// Check if language exists.
|
||||||
|
this._verifyLanguageKnown(language);
|
||||||
|
|
||||||
|
// Delete key if exists.
|
||||||
|
let language_map = this.languages.get(language);
|
||||||
|
if (!language_map.has(key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
language_map.delete(key);
|
||||||
|
|
||||||
|
// If the key was the base language key, set dirty timestamp.
|
||||||
|
if (key == this.baseLanguageKey) {
|
||||||
|
this.dirtyTs = performance.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set a key in a language
|
||||||
|
*
|
||||||
|
* @param {string} language Language to edit.
|
||||||
|
* @param {string} key Key to set.
|
||||||
|
* @param {*} value New value to set.
|
||||||
|
* @param {bool} force Force the update if the key exists. (Default = true)
|
||||||
|
* @return {bool} true on success.
|
||||||
|
* @throws Exception on invalid parameters and missing language.
|
||||||
|
*/
|
||||||
|
setKey(language, key, value, force = true) {
|
||||||
|
// Verify and sanitize input.
|
||||||
|
language = this._sanitizeLanguage(language);
|
||||||
|
this._verifyKey(key);
|
||||||
|
|
||||||
|
// Check if language exists.
|
||||||
|
this._verifyLanguageKnown(language);
|
||||||
|
|
||||||
|
// Set key.
|
||||||
|
let language_map = this.languages.get(language);
|
||||||
|
if ((language_map.has(key)) && !force) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
language_map.set(key, value);
|
||||||
|
|
||||||
|
// If the key was the base language key, set dirty timestamp.
|
||||||
|
if (key == this.baseLanguageKey) {
|
||||||
|
this.dirtyTs = performance.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get a key in a language
|
||||||
|
*
|
||||||
|
* @param {string} language
|
||||||
|
* @param {string} key
|
||||||
|
* @return {*} the value
|
||||||
|
* @throws Exception on invalid parameters, missing language and missing key.
|
||||||
|
*/
|
||||||
|
getKey(language, key) {
|
||||||
|
// Verify and sanitize input.
|
||||||
|
language = this._sanitizeLanguage(language);
|
||||||
|
this._verifyKeyKnown(language, key);
|
||||||
|
|
||||||
|
// Get Key
|
||||||
|
let language_map = this.languages.get(language);
|
||||||
|
return language_map.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Translate a single string to any loaded language.
|
/** Translate a single string to any loaded language.
|
||||||
*
|
*
|
||||||
* @param {string} string String to translate
|
* @param {string} key String to translate
|
||||||
* @param {string} language Language to translate to
|
* @param {string} language Language to translate to
|
||||||
* @return {string} Translated string, or if failed the string plus the language appended.
|
* @return {string} Translated string, or if failed the string plus the language appended.
|
||||||
|
* @throws Exception on invalid parameters.
|
||||||
*/
|
*/
|
||||||
translate(string, language) {
|
translate(key, language) {
|
||||||
|
// Verify and sanitize input.
|
||||||
language = this._sanitizeLanguage(language);
|
language = this._sanitizeLanguage(language);
|
||||||
if (typeof(string) != "string") {
|
this._verifyKey(key);
|
||||||
throw "string must be of type string";
|
|
||||||
|
// Translate using the translation chain.
|
||||||
|
let chain = this._cacheChain(language);
|
||||||
|
let translated = key;
|
||||||
|
for (language in chain) {
|
||||||
|
let languageMap = this.languages.get(language);
|
||||||
|
if (languageMap.has(key)) {
|
||||||
|
translated = languageMap.get(key);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
let str = this.languages[language][string];
|
|
||||||
if (str == undefined) {
|
|
||||||
let str = this.languages[this.languages.base][string];
|
|
||||||
if (str == undefined) {
|
|
||||||
console.error("%c[%s]%c Language '%s' and base language '%s' contain no translation for '%s'.",
|
|
||||||
group_style, group, text_style,
|
|
||||||
language, this.languages.base, string
|
|
||||||
)
|
|
||||||
return string + language;
|
|
||||||
} else {
|
|
||||||
console.debug("%c[%s]%c Language '%s' contains no translation for '%s', falling back to base language '%s'.",
|
|
||||||
group_style, group, text_style,
|
|
||||||
language, string, this.languages.base
|
|
||||||
)
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return str;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return translated;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Automatically translate the entire page using the specified property on elements.
|
/** Automatically translate the entire page using the specified property on elements.
|
||||||
*
|
*
|
||||||
* @param {*} language Language to translate to
|
* @param {string} language Language to translate to
|
||||||
* @param {*} property Property to search for (default 'data-i18n')
|
* @param {string} property Property to search for (default 'data-i18n')
|
||||||
|
* @param {function} resolver Function to call to find the proper translation, called with parameters ({string}language, {string}key, {Node}element) and must return a string.
|
||||||
|
* @param {function} applier Function to call to apply the proper translation, called with parameters ({string}language, {string}key, {string}translation, {Node}element) and must return a boolean.
|
||||||
|
* @returns {Promise} resolved when successful, rejected if applier returns false.
|
||||||
|
* @throws Exception on invalid parameters.
|
||||||
*/
|
*/
|
||||||
autoTranslate(language, property = 'data-i18n') {
|
domTranslate(language, property = 'data-i18n', resolver = this._defaultResolver, applier = this._defaultApplier) {
|
||||||
language = this._sanitizeLanguage(language);
|
language = this._sanitizeLanguage(language);
|
||||||
|
if (typeof (resolver) != 'function') {
|
||||||
|
throw 'resolver must be a function';
|
||||||
|
}
|
||||||
|
if (typeof (applier) != 'function') {
|
||||||
|
throw 'applier must be a function';
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
console.debug("%c[%s]%c Starting automatic translation to language '%s'...",
|
console.debug('%c[%s]%c Starting DOM translation to language "%s" using property "%s"...',
|
||||||
group_style, group, text_style,
|
group_style, group, text_style,
|
||||||
language);
|
language, property);
|
||||||
|
|
||||||
let els = document.querySelectorAll(`[${property}]`);
|
let els = document.querySelectorAll(`[${property}]`);
|
||||||
for (let el of els) {
|
for (let el of els) {
|
||||||
let string = el.getAttribute(property);
|
let key = el.getAttribute(property);
|
||||||
el.textContent = string;
|
if (!applier(language, key, resolver(language, key, el), el)) {
|
||||||
el.textContent = this.translate(string, language);
|
reject();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
console.log("%c[%s]%c Translated to language '%s'.",
|
}
|
||||||
|
console.log('%c[%s]%c Translated to language "%s".',
|
||||||
group_style, group, text_style,
|
group_style, group, text_style,
|
||||||
language);
|
language);
|
||||||
|
resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user