2019-11-03 11:37:41 +01:00
"use strict" ;
2018-11-29 18:48:43 +01:00
2018-10-31 23:37:41 +01:00
/*
Internationalization Class for JavaScript
2019-11-03 12:49:30 +01:00
Copyright (C) 2018-2019 Michael Fabian 'Xaymar' Dirks <info@xaymar.com>
2018-10-31 23:37:41 +01:00
*/
2018-11-29 00:27:33 +01:00
/*
Dead Simple I18n Library
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.
*/
2019-11-03 11:37:41 +01:00
/**
*
2018-12-07 06:57:01 +01:00
* # Events
* Events are called with multiple parameters, as described below. They can
* be used for a variety of things, for example to react to failures or
* other errors that are otherwise unrecoverable.
2019-11-03 11:37:41 +01:00
*
2018-12-07 06:57:01 +01:00
* ## Event: missingKey
* Called if a key is found to be missing in a language.
* @param {string} key Key that was determined to be missing.
* @param {string} language Language that key was not found in.
2019-11-03 11:37:41 +01:00
*
2018-12-07 06:57:01 +01:00
* ## Event: missingLanguage
* Called if a language is found to be missing.
* @param {string} language Language that was determined to be missing.
2019-11-03 11:37:41 +01:00
*
2018-12-07 06:57:01 +01:00
* ## Event: change
* Called whenever a change is made to the content of the object.
2019-11-03 11:37:41 +01:00
*
2018-12-07 06:57:01 +01:00
*/
2018-10-31 23:37:41 +01:00
class I18n {
2018-11-29 02:36:26 +01:00
/** Create a new object, ready to be used.
2019-11-03 12:49:30 +01:00
*
* @param {string} defaultLanguage 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.
*/
2019-11-03 11:37:41 +01:00
constructor ( defaultLanguage , baseLanguageKey = "_base" ) {
2018-11-29 03:00:33 +01:00
this . _sanitizeLanguage ( defaultLanguage ) ;
2018-11-29 02:36:26 +01:00
this . _verifyKey ( baseLanguageKey ) ;
this . languages = new Map ( ) ;
this . chains = new Map ( ) ;
2018-11-29 03:00:33 +01:00
this . baseLanguage = defaultLanguage ;
2018-11-29 02:36:26 +01:00
this . baseLanguageKey = baseLanguageKey ;
2018-12-07 06:57:01 +01:00
this . events = {
2019-11-03 11:37:41 +01:00
missingkey : new Map ( ) ,
missinglanguage : new Map ( ) ,
change : new Map ( )
2018-12-07 06:57:01 +01:00
} ;
2018-11-29 02:36:26 +01:00
this . dirtyTs = performance . now ( ) ;
this . chainsTs = performance . now ( ) ;
}
2018-11-29 19:05:12 +01:00
/** Update the global base language.
2019-11-03 12:49:30 +01:00
*
* @param {string} language
*/
2018-11-29 19:05:12 +01:00
setBaseLanguage ( language ) {
this . _sanitizeLanguage ( language ) ;
this . baseLanguage = language ;
this . dirtyTs = performance . now ( ) ;
2018-12-07 06:57:01 +01:00
// Event: onchange
2019-11-03 11:37:41 +01:00
this . _callEvent ( "change" ) ;
2018-11-29 19:05:12 +01:00
}
/** Retrieve the global base language.
2019-11-03 12:49:30 +01:00
*
* @return {string} name of base language.
*/
2018-11-29 19:05:12 +01:00
getBaseLanguage ( ) {
return this . baseLanguage ;
}
2018-11-29 02:36:26 +01:00
/** Check if a language is known.
2019-11-03 12:49:30 +01:00
*
* @param {string} language Name of the language
* @returns {bool} true if known.
*/
2018-11-29 02:36:26 +01:00
hasLanguage ( language ) {
language = this . _sanitizeLanguage ( language ) ;
return this . languages . has ( language ) ;
}
2018-11-29 01:19:21 +01:00
2018-11-29 02:36:26 +01:00
/** Create a new language.
2019-11-03 12:49:30 +01:00
*
* @param {string} language Name of the language.
* @throws Exception on invalid parameters.
*/
2018-11-29 02:36:26 +01:00
createLanguage ( language ) {
language = this . _sanitizeLanguage ( language ) ;
this . languages . set ( language , new Map ( ) ) ;
this . dirtyTs = performance . now ( ) ;
2018-12-07 06:57:01 +01:00
// Event: onchange
2019-11-03 11:37:41 +01:00
this . _callEvent ( "change" ) ;
2018-11-29 02:36:26 +01:00
}
2018-11-29 00:27:33 +01:00
2018-11-29 02:36:26 +01:00
/** Load a new language.
2019-11-03 12:49:30 +01:00
*
* @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.
*/
2019-11-03 11:37:41 +01:00
loadLanguage ( language , data , encoding = "utf-8" ) {
2018-11-29 02:36:26 +01:00
// Verify input.
language = this . _sanitizeLanguage ( language ) ;
this . _verifyData ( data ) ;
2018-11-29 18:48:43 +01:00
return new Promise ( ( resolve , reject ) => {
// Decode data from various forms.
let decodePromise ;
2019-11-03 11:37:41 +01:00
if ( data instanceof File || data instanceof Blob ) {
2018-11-29 18:48:43 +01:00
decodePromise = new Promise ( ( fileResolve , fileReject ) => {
2018-11-29 02:36:26 +01:00
let freader = new FileReader ( ) ;
2018-11-29 18:48:43 +01:00
freader . onload ( ( ) => {
fileResolve ( JSON . parse ( freader . result ) ) ;
2018-11-29 02:36:26 +01:00
} ) ;
2019-11-03 11:37:41 +01:00
freader . onabort ( ev => {
2018-11-29 18:48:43 +01:00
fileReject ( ev ) ;
} ) ;
2019-11-03 11:37:41 +01:00
freader . onerror ( ev => {
2018-11-29 18:48:43 +01:00
fileReject ( ev ) ;
} ) ;
freader . readAsText ( data , encoding ) ;
} ) ;
2019-11-03 11:37:41 +01:00
} else if ( typeof data == "string" ) {
2018-11-29 18:48:43 +01:00
decodePromise = new Promise ( ( parseResolve , parseReject ) => {
parseReject ;
parseResolve ( JSON . parse ( data ) ) ;
} ) ;
2019-11-03 11:37:41 +01:00
} else if ( typeof data == "object" ) {
2018-11-29 18:48:43 +01:00
decodePromise = new Promise ( ( passResolve , passReject ) => {
passReject ;
passResolve ( data ) ;
} ) ;
} else {
2019-11-03 11:37:41 +01:00
reject ( "invalid data" ) ;
2018-11-29 18:48:43 +01:00
}
2018-11-29 02:36:26 +01:00
2018-11-29 18:48:43 +01:00
// Load Data
2019-11-03 11:37:41 +01:00
decodePromise . then (
result => {
let language _map = new Map ( ) ;
for ( let key in result ) {
language _map . set ( key , result [ key ] ) ;
}
2018-11-29 02:36:26 +01:00
2019-11-03 11:37:41 +01:00
this . languages . set ( language , language _map ) ;
this . dirtyTs = performance . now ( ) ;
2018-12-07 06:57:01 +01:00
2019-11-03 11:37:41 +01:00
// Event: onchange
this . _callEvent ( "change" ) ;
2018-12-07 06:57:01 +01:00
2019-11-03 11:37:41 +01:00
resolve ( language ) ;
} ,
reason => {
reject ( reason ) ;
}
) ;
2018-11-29 02:36:26 +01:00
} ) ;
}
/** Save a language.
2019-11-03 12:49:30 +01:00
*
* @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.
*/
2018-11-29 02:36:26 +01:00
saveLanguage ( language ) {
language = this . _sanitizeLanguage ( language ) ;
this . _verifyLanguageKnown ( language ) ;
return new Promise ( ( resolve , reject ) => {
reject ;
this . _verifyLanguageKnown ( language ) ;
let language _data = { } ;
let language _map = this . languages . get ( language ) ;
language _map . forEach ( ( value , key , map ) => {
map ;
language _data [ key ] = value ;
} ) ;
let json _data = JSON . stringify ( language _data ) ;
resolve ( json _data ) ;
} ) ;
}
/** Destroy/unload a language.
2019-11-03 12:49:30 +01:00
*
* @param {string} language Name of the language.
* @throws Exception on invalid parameters and missing language.
*/
2018-11-29 02:36:26 +01:00
destroyLanguage ( language ) {
language = this . _sanitizeLanguage ( language ) ;
this . _verifyLanguageKnown ( language ) ;
2018-11-29 00:27:33 +01:00
2018-11-29 02:36:26 +01:00
this . languages . delete ( language ) ;
this . dirtyTs = performance . now ( ) ;
2018-12-07 06:57:01 +01:00
// Event: onchange
2019-11-03 11:37:41 +01:00
this . _callEvent ( "change" ) ;
2018-11-29 02:36:26 +01:00
}
2018-11-29 00:27:33 +01:00
2018-11-29 02:36:26 +01:00
/** Clear a key from a language.
2019-11-03 12:49:30 +01:00
*
* @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.
*/
2018-11-29 02:36:26 +01:00
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 ( ) ;
}
2018-12-07 06:57:01 +01:00
// Event: onchange
2019-11-03 11:37:41 +01:00
this . _callEvent ( "change" ) ;
2018-12-07 06:57:01 +01:00
2018-11-29 02:36:26 +01:00
return true ;
}
2018-12-07 06:57:01 +01:00
/** Set a key in a language.
2019-11-03 12:49:30 +01:00
*
* @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.
*/
2018-11-29 02:36:26 +01:00
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 ) ;
2019-11-03 11:37:41 +01:00
if ( language _map . has ( key ) && ! force ) {
2018-11-29 02:36:26 +01:00
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 ( ) ;
}
2018-12-07 06:57:01 +01:00
// Event: onchange
2019-11-03 11:37:41 +01:00
this . _callEvent ( "change" ) ;
2018-12-07 06:57:01 +01:00
2018-11-29 02:36:26 +01:00
return true ;
}
2018-12-07 06:57:01 +01:00
/** Get a key in a language.
2019-11-03 12:49:30 +01:00
*
* @param {string} language
* @param {string} key
* @return {*} the value
* @throws Exception on invalid parameters, missing language and missing key.
*/
2018-11-29 02:36:26 +01:00
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 ) ;
}
2018-10-31 23:37:41 +01:00
2018-12-07 06:57:01 +01:00
/** Hook into an event.
2019-11-03 12:49:30 +01:00
*
* @param {string} event Event to hook into.
* @param {function} callback Callback to call.
* @return {string} Id of the event (for unhooking).
*/
2018-12-07 06:57:01 +01:00
hook ( event , callback ) {
2019-11-03 11:37:41 +01:00
if ( typeof event != "string" ) {
throw "event must be a string" ;
2018-12-07 06:57:01 +01:00
}
event = event . toLowerCase ( ) ;
2019-11-03 11:37:41 +01:00
if ( typeof callback != "function" ) {
throw "callback must be a function" ;
2018-12-07 06:57:01 +01:00
}
if ( this . events [ event ] == undefined ) {
2019-11-03 11:37:41 +01:00
throw "event is unknown" ;
2018-12-07 06:57:01 +01:00
}
let uid = this . _uuid ( ) ;
this . events [ event ] . set ( uid , callback ) ;
return uid ;
}
unhook ( event , callbackid ) {
2019-11-03 11:37:41 +01:00
if ( typeof event != "string" ) {
throw "event must be a string" ;
2018-12-07 06:57:01 +01:00
}
event = event . toLowerCase ( ) ;
2019-11-03 11:37:41 +01:00
if ( typeof event != "number" ) {
throw "callbackid must be a number" ;
2018-12-07 06:57:01 +01:00
}
if ( this . events [ event ] == undefined ) {
2019-11-03 11:37:41 +01:00
throw "event is unknown" ;
2018-12-07 06:57:01 +01:00
}
if ( ! this . events [ event ] . has ( callbackid ) ) {
return false ;
}
this . events [ event ] . remove ( callbackid ) ;
return true ;
}
2018-11-29 02:36:26 +01:00
/** Translate a single string to any loaded language.
2019-11-03 12:49:30 +01:00
*
* @param {string} key String to translate
* @param {string} language Language to translate to
* @return {string} Translated string, or if failed the string plus the language appended.
* @throws Exception on invalid parameters.
*/
2018-11-29 02:36:26 +01:00
translate ( key , language ) {
// Verify and sanitize input.
language = this . _sanitizeLanguage ( language ) ;
this . _verifyKey ( key ) ;
// Translate using the translation chain.
let chain = this . _cacheChain ( language ) ;
let translated = key ;
for ( let language of chain ) {
let languageMap = this . languages . get ( language ) ;
if ( languageMap . has ( key ) ) {
translated = languageMap . get ( key ) ;
break ;
2018-12-07 06:57:01 +01:00
} else {
// Event: onMissingKey
2019-11-03 11:37:41 +01:00
this . _callEvent ( "missingKey" , key , language ) ;
2018-11-29 02:36:26 +01:00
}
}
return translated ;
}
/** Automatically translate the entire page using the specified property on elements.
2019-11-03 12:49:30 +01:00
*
* @param {string} language Language to translate to
* @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.
*/
2019-11-03 11:37:41 +01:00
domTranslate (
language ,
property = "data-i18n" ,
resolver = undefined ,
applier = undefined
) {
2018-11-29 02:36:26 +01:00
language = this . _sanitizeLanguage ( language ) ;
2019-11-03 11:37:41 +01:00
if ( resolver != undefined && typeof resolver != "function" ) {
throw "resolver must be a function" ;
2018-11-29 02:36:26 +01:00
} else if ( resolver == undefined ) {
let self = this ;
resolver = ( language , key , el ) => {
return self . _defaultResolver ( language , key , el ) ;
} ;
}
2019-11-03 11:37:41 +01:00
if ( applier != undefined && typeof applier != "function" ) {
throw "applier must be a function" ;
2018-11-29 02:36:26 +01:00
} else if ( applier == undefined ) {
let self = this ;
applier = ( language , key , translation , el ) => {
return self . _defaultApplier ( language , key , translation , el ) ;
} ;
}
return new Promise ( ( resolve , reject ) => {
let els = document . querySelectorAll ( ` [ ${ property } ] ` ) ;
for ( let el of els ) {
let key = el . getAttribute ( property ) ;
if ( ! applier ( language , key , resolver ( language , key , el ) , el ) ) {
reject ( ) ;
return ;
}
}
resolve ( ) ;
} ) ;
}
2018-12-07 06:57:01 +01:00
2018-11-29 18:58:06 +01:00
// Private Functions
_verifyLanguageKnown ( language ) {
if ( ! this . languages . has ( language ) ) {
2019-11-03 11:37:41 +01:00
throw "language unknown" ;
2018-11-29 18:58:06 +01:00
}
}
_verifyKey ( key ) {
2019-11-03 11:37:41 +01:00
if ( typeof key == "string" ) {
2018-11-29 18:58:06 +01:00
return ;
}
2019-11-03 11:37:41 +01:00
throw "key must be of type string" ;
2018-11-29 18:58:06 +01:00
}
_verifyKeyKnown ( language , key ) {
this . _verifyLanguageKnown ( language ) ;
this . _verifyKey ( key ) ;
if ( ! this . languages . get ( language ) . has ( key ) ) {
2019-11-03 11:37:41 +01:00
throw "key unknown in language" ;
2018-11-29 18:58:06 +01:00
}
}
_verifyData ( data ) {
2019-11-03 11:37:41 +01:00
if ( typeof data == "string" ) {
2018-11-29 18:58:06 +01:00
return ;
}
2019-11-03 11:37:41 +01:00
if ( typeof data == "object" ) {
2018-11-29 18:58:06 +01:00
return ;
}
if ( data instanceof File ) {
return ;
}
if ( data instanceof Blob ) {
return ;
}
2019-11-03 11:37:41 +01:00
throw "data must be of type string, object, File or Blob" ;
2018-11-29 18:58:06 +01:00
}
_sanitizeLanguage ( language ) {
try {
this . _verifyKey ( language ) ;
} catch ( e ) {
2019-11-03 11:37:41 +01:00
throw "language must be of type string" ;
2018-11-29 18:58:06 +01:00
}
return language . toLowerCase ( ) ;
}
_defaultResolver ( language , key , element ) {
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.
2019-11-03 12:49:30 +01:00
*
* Creates and returns a language chain required for translation, avoiding
* recursive loops that never end in the process. The chain will not
* 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:
* de-BE +- de-DE - en-GB - en-US
* +- en-GB - en-US
* +- de-AU - de-DE - en-GB - en-US
* +- en-US
* Result: [de-Be, de-DE, en-GB, de-AU, en-US]
* Explanation: de-BE depends on [de-DE, en-GB, de-AU, en-US], resolving
* de-DE returns en-GB (if loaded), which we already have. en-GB resolves
* to en-US, which we also already have. de-AU resolves to de-DE, also
* known. And finally en-US resolves to nothing as the global base language.
*
* @param {string} language
* @returns {array} Translation chain
*/
2018-11-29 18:58:06 +01:00
_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 . chains . has ( language ) ) {
// Yes, return the cached chain and go back to translation.
return this . chains . get ( language ) ;
}
// Create a new chain without relying on our own function.
let chain = new Array ( ) ;
2018-12-07 06:57:01 +01:00
let missing = new Array ( ) ;
2018-11-29 18:58:06 +01:00
chain . push ( 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 ] ) ) {
2018-12-07 06:57:01 +01:00
missing . push ( chain [ pos ] ) ;
2018-11-29 18:58:06 +01:00
continue ;
}
let languageMap = this . languages . get ( chain [ pos ] ) ;
// Check if there is a base language override.
if ( ! languageMap . has ( this . baseLanguageKey ) ) {
continue ;
}
let baseLanguages = languageMap . get ( this . baseLanguageKey ) ;
// Convert to array for for...in.
2019-11-03 11:37:41 +01:00
if ( typeof baseLanguages == "string" ) {
2018-11-29 18:58:06 +01:00
baseLanguages = [ baseLanguages ] ;
} else if ( ! ( baseLanguages instanceof Array ) ) {
continue ;
}
for ( let base of baseLanguages ) {
base = this . _sanitizeLanguage ( base ) ;
2019-11-03 11:37:41 +01:00
if ( ! chain . includes ( base ) && this . languages . has ( base ) ) {
2018-11-29 18:58:06 +01:00
chain . push ( base ) ;
2019-11-03 11:37:41 +01:00
} else if ( ! missing . includes ( base ) && ! this . languages . has ( base ) ) {
2018-12-07 06:57:01 +01:00
missing . push ( base ) ;
2018-11-29 18:58:06 +01:00
}
}
2018-12-07 06:57:01 +01:00
2018-11-29 18:58:06 +01:00
// Append the global base languages if there are no other languages left.
2019-11-03 11:37:41 +01:00
if ( pos == chain . length - 1 ) {
2018-11-29 18:58:06 +01:00
let baseLanguages = this . baseLanguage ;
2019-11-03 11:37:41 +01:00
if ( typeof this . baseLanguage == "string" ) {
2018-11-29 18:58:06 +01:00
baseLanguages = [ this . baseLanguage ] ;
}
for ( let base of baseLanguages ) {
2019-11-03 11:37:41 +01:00
if ( ! chain . includes ( base ) && this . languages . has ( base ) ) {
2018-11-29 18:58:06 +01:00
chain . push ( base ) ;
2019-11-03 11:37:41 +01:00
} else if ( ! missing . includes ( base ) && ! this . languages . has ( base ) ) {
2018-12-07 06:57:01 +01:00
missing . push ( base ) ;
2018-11-29 18:58:06 +01:00
}
}
}
}
2018-12-07 06:57:01 +01:00
// Trigger event for all missing languages
for ( let missingLanguage of missing ) {
// Event: onMissingLanguage
2019-11-03 11:37:41 +01:00
this . _callEvent ( "missingLanguage" , missingLanguage ) ;
2018-12-07 06:57:01 +01:00
}
2018-11-29 18:58:06 +01:00
// Store.
this . chains . set ( language , chain ) ;
// Return.
return chain ;
}
2018-12-07 06:57:01 +01:00
/** Generate a UUID compliant string
2019-11-03 12:49:30 +01:00
*
* Source: https://stackoverflow.com/a/21963136
*/
2018-12-07 06:57:01 +01:00
_uuid ( ) {
2019-11-03 11:37:41 +01:00
const lut = [ ] ;
for ( var i = 0 ; i < 256 ; i ++ ) {
lut [ i ] = ( i < 16 ? "0" : "" ) + i . toString ( 16 ) ;
}
var d0 = ( Math . random ( ) * 0xffffffff ) | 0 ;
var d1 = ( Math . random ( ) * 0xffffffff ) | 0 ;
var d2 = ( Math . random ( ) * 0xffffffff ) | 0 ;
var d3 = ( Math . random ( ) * 0xffffffff ) | 0 ;
return (
lut [ d0 & 0xff ] +
lut [ ( d0 >> 8 ) & 0xff ] +
lut [ ( d0 >> 16 ) & 0xff ] +
lut [ ( d0 >> 24 ) & 0xff ] +
"-" +
lut [ d1 & 0xff ] +
lut [ ( d1 >> 8 ) & 0xff ] +
"-" +
lut [ ( ( d1 >> 16 ) & 0x0f ) | 0x40 ] +
lut [ ( d1 >> 24 ) & 0xff ] +
"-" +
lut [ ( d2 & 0x3f ) | 0x80 ] +
lut [ ( d2 >> 8 ) & 0xff ] +
"-" +
lut [ ( d2 >> 16 ) & 0xff ] +
lut [ ( d2 >> 24 ) & 0xff ] +
lut [ d3 & 0xff ] +
lut [ ( d3 >> 8 ) & 0xff ] +
lut [ ( d3 >> 16 ) & 0xff ] +
lut [ ( d3 >> 24 ) & 0xff ]
) ;
2018-12-07 06:57:01 +01:00
}
_callEvent ( name ) {
2019-11-03 11:37:41 +01:00
if ( typeof name != "string" ) {
throw "name must be a string" ;
2018-12-07 06:57:01 +01:00
}
name = name . toLowerCase ( ) ;
if ( this . events [ name ] == undefined ) {
2019-11-03 11:37:41 +01:00
throw "invalid event call" ;
2018-12-07 06:57:01 +01:00
}
if ( this . events [ name ] . size == 0 ) {
return ;
}
let args = Array . prototype . slice . call ( arguments , 1 ) ;
this . events [ name ] . forEach ( ( value , key , map ) => {
2019-11-03 11:37:41 +01:00
key ;
map ;
if ( typeof value != "function" ) {
2018-12-07 06:57:01 +01:00
return ;
}
try {
value . apply ( null , args ) ;
} catch ( e ) {
e ;
}
} ) ;
}
2018-10-31 23:37:41 +01:00
}
2018-11-29 01:05:41 +01:00
// Compatible with Node.js and Browsers
2019-11-03 11:37:41 +01:00
if ( typeof module != "undefined" ) {
2018-11-29 02:36:26 +01:00
module . exports = exports = {
2019-11-03 11:37:41 +01:00
I18n : I18n
2018-11-29 02:36:26 +01:00
} ;
2018-11-29 01:05:41 +01:00
}