diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..610a563
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,30 @@
+{
+ "env": {
+ "browser": true,
+ "es6": true,
+ "node": true
+ },
+ "extends": "eslint:recommended",
+ "parserOptions": {
+ "ecmaVersion": 2018,
+ "sourceType": "module"
+ },
+ "rules": {
+ "indent": [
+ "error",
+ "tab"
+ ],
+ "linebreak-style": [
+ "error",
+ "unix"
+ ],
+ "quotes": [
+ "error",
+ "single"
+ ],
+ "semi": [
+ "error",
+ "always"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..937b84a
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+*.ts text eol=lf
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..07e6e47
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/node_modules
diff --git a/examples/sample.html b/examples/sample.html
new file mode 100644
index 0000000..bde8850
--- /dev/null
+++ b/examples/sample.html
@@ -0,0 +1,94 @@
+
+
+
+
+
+ i18n-js - Sample
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/i18n.js b/i18n.js
index f7941d1..d6e2bde 100644
--- a/i18n.js
+++ b/i18n.js
@@ -15,152 +15,493 @@
along with this program. If not, see .
*/
-let group = "i18n";
-let group_style = "font-weight: bold;";
-let text_style = "font-weight: inherit;";
+/*
+Dead Simple I18n Library
-class I18n {
- /** 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).
- */
- constructor(urlFormat) {
- console.log("%c[%s]%c Initializing ...", group_style, group, text_style);
- if (typeof(urlFormat) != "string") {
- throw "urlFormat must be of type string";
- }
- this.url = urlFormat;
- this.languages = {};
- this.languages.base = '';
- }
+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.
- _sanitizeLanguage(language) {
- if (typeof(language) != "string") {
- throw "language must be of type string";
- }
- return language.toLowerCase();
- }
+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.
- /** Load a new language.
- *
- * @param {string} language Name of the language file.
- * @param {boolean} isBaseLanguage Use this language as the new base language.
- * @returns {Promise}
- */
- load(language, isBaseLanguage) {
- 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);
+# 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.
- let req = new XMLHttpRequest();
- req.addEventListener("error", function (event) {
- console.log("%c[%s]%c Failed to load language '%s': ",
- group_style, group, text_style,
- language, event);
+# 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.
- });
- 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,
- language);
- try {
- self.languages[language] = JSON.parse(req.responseText);
- } catch (e) {
- console.log("%c[%s]%c Failed to load language '%s': ",
- group_style, group, text_style,
- language, e);
- reject(e);
- return;
- }
- resolve(true);
- console.log("%c[%s]%c Loaded language '%s'.",
- group_style, group, text_style,
- language);
- return;
- });
- req.open("GET", url);
- req.overrideMimeType("text/plain; charset=utf-8");
- req.send();
- });
- } else {
- console.log("%c[%s]%c Loaded language '%s'...",
- group_style, group, text_style, language);
- return new Promise((resolve, reject) => { resolve(true); });
- }
- }
-
- /** Translate a single string to any loaded language.
- *
- * @param {string} string String to translate
- * @param {string} language Language to translate to
- * @return {string} Translated string, or if failed the string plus the language appended.
- */
- translate(string, language) {
- language = this._sanitizeLanguage(language);
- if (typeof(string) != "string") {
- throw "string must be of type string";
- }
- 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;
- }
- }
-
- /** Automatically translate the entire page using the specified property on elements.
- *
- * @param {*} language Language to translate to
- * @param {*} property Property to search for (default 'data-i18n')
- */
- autoTranslate(language, property = 'data-i18n') {
- language = this._sanitizeLanguage(language);
- return new Promise((resolve, reject) => {
- console.debug("%c[%s]%c Starting automatic translation to language '%s'...",
- group_style, group, text_style,
- language);
- let els = document.querySelectorAll(`[${property}]`);
- for (let el of els) {
- let string = el.getAttribute(property);
- el.textContent = string;
- el.textContent = this.translate(string, language);
- }
- console.log("%c[%s]%c Translated to language '%s'.",
- group_style, group, text_style,
- language);
- });
+# 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.
+
+*/
+
+class I18n {
+ /** Create a new object, ready to be used.
+ *
+ * @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.
+ */
+ constructor(defaultLanguage, baseLanguageKey = '_base') {
+ this._sanitizeLanguage(defaultLanguage);
+ this._verifyKey(baseLanguageKey);
+
+ this.languages = new Map();
+ this.chains = new Map();
+ this.baseLanguage = defaultLanguage;
+ this.baseLanguageKey = baseLanguageKey;
+
+ this.dirtyTs = performance.now();
+ this.chainsTs = performance.now();
+ }
+
+ _verifyLanguageKnown(language) {
+ 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) {
+ try {
+ this._verifyKey(language);
+ } catch (e) {
+ throw 'language must be of type string';
+ }
+ 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.
+ *
+ * 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:
+ *
+ *
+ * @param {string} language
+ * @returns {array} Translation chain
+ */
+ _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();
+ 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])) {
+ 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.
+ if (typeof (baseLanguages) == 'string') {
+ baseLanguages = [baseLanguages];
+ } else if (!(baseLanguages instanceof Array)) {
+ continue;
+ }
+
+ for (let base of baseLanguages) {
+ base = this._sanitizeLanguage(base);
+ if (!chain.includes(base) && (this.languages.has(base))) {
+ chain.push(base);
+ }
+ }
+
+ // Append the global base languages if there are no other languages left.
+ if (pos == (chain.length - 1)) {
+ let baseLanguages = this.baseLanguage;
+ if (typeof(this.baseLanguage) == 'string') {
+ baseLanguages = [this.baseLanguage];
+ }
+ for (let base of baseLanguages) {
+ if (!chain.includes(base) && (this.languages.has(base))) {
+ chain.push(base);
+ }
+ }
+ }
+ }
+
+ // Store.
+ this.chains.set(language, chain);
+
+ // Return.
+ return chain;
+ }
+
+ /** Check if a language is known.
+ *
+ * @param {string} language Name of the language
+ * @returns {bool} true if known.
+ */
+ hasLanguage(language) {
+ language = this._sanitizeLanguage(language);
+ return this.languages.has(language);
+ }
+
+ /** Create a new language.
+ *
+ * @param {string} language Name of the language.
+ * @throws Exception on invalid parameters.
+ */
+ createLanguage(language) {
+ language = this._sanitizeLanguage(language);
+ this.languages.set(language, new Map());
+ 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);
+ this._verifyData(data);
+
+ return new Promise(async (resolve, reject) => {
+ try {
+ let json_data;
+
+ // Decode File, Blob and string to JSON object.
+ if ((data instanceof File) || (data instanceof Blob)) {
+ let freader = new FileReader();
+ await new Promise((resolve2, reject2) => {
+ freader.onload(() => {
+ 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 (let key in json_data) {
+ language_map.set(key, json_data[key]);
+ }
+
+ this.languages.set(language, language_map);
+ this.dirtyTs = performance.now();
+ resolve(language);
+ } catch (e) {
+ reject(e);
+ return;
+ }
+ });
+ }
+
+ /** 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) => {
+ 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.
+ *
+ * @param {string} language Name of the language.
+ * @throws Exception on invalid parameters and missing language.
+ */
+ destroyLanguage(language) {
+ language = this._sanitizeLanguage(language);
+ this._verifyLanguageKnown(language);
+
+ this.languages.delete(language);
+ this.dirtyTs = performance.now();
+ }
+
+ /** 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.
+ *
+ * @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.
+ */
+ 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;
+ }
+ }
+
+ return translated;
+ }
+
+ /** Automatically translate the entire page using the specified property on elements.
+ *
+ * @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.
+ */
+ domTranslate(language, property = 'data-i18n', resolver = undefined, applier = undefined) {
+ language = this._sanitizeLanguage(language);
+ if ((resolver != undefined) && (typeof (resolver) != 'function')) {
+ throw 'resolver must be a function';
+ } else if (resolver == undefined) {
+ let self = this;
+ resolver = (language, key, el) => {
+ return self._defaultResolver(language, key, el);
+ };
+ }
+ if ((applier != undefined) && (typeof (applier) != 'function')) {
+ throw 'applier must be a function';
+ } 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();
+ });
+ }
+}
+
+// Compatible with Node.js and Browsers
+if (typeof (module) != 'undefined') {
+ module.exports = exports = {
+ 'I18n': I18n
+ };
+}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..6e67e8c
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,928 @@
+{
+ "name": "i18n",
+ "version": "0.0.1",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz",
+ "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==",
+ "dev": true,
+ "requires": {
+ "@babel/highlight": "^7.0.0"
+ }
+ },
+ "@babel/highlight": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz",
+ "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==",
+ "dev": true,
+ "requires": {
+ "chalk": "^2.0.0",
+ "esutils": "^2.0.2",
+ "js-tokens": "^4.0.0"
+ }
+ },
+ "acorn": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.4.tgz",
+ "integrity": "sha512-VY4i5EKSKkofY2I+6QLTbTTN/UvEQPCo6eiwzzSaSWfpaDhOmStMCMod6wmuPciNq+XS0faCglFu2lHZpdHUtg==",
+ "dev": true
+ },
+ "acorn-jsx": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz",
+ "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==",
+ "dev": true
+ },
+ "ajv": {
+ "version": "6.5.5",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz",
+ "integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==",
+ "dev": true,
+ "requires": {
+ "fast-deep-equal": "^2.0.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "ansi-escapes": {
+ "version": "3.1.0",
+ "resolved": "http://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz",
+ "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==",
+ "dev": true
+ },
+ "ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "requires": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "dev": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "caller-path": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz",
+ "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=",
+ "dev": true,
+ "requires": {
+ "callsites": "^0.2.0"
+ }
+ },
+ "callsites": {
+ "version": "0.2.0",
+ "resolved": "http://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz",
+ "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=",
+ "dev": true
+ },
+ "chalk": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+ "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "chardet": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+ "dev": true
+ },
+ "circular-json": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz",
+ "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==",
+ "dev": true
+ },
+ "cli-cursor": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+ "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
+ "dev": true,
+ "requires": {
+ "restore-cursor": "^2.0.0"
+ }
+ },
+ "cli-width": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
+ "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
+ "dev": true
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "cross-spawn": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "dev": true,
+ "requires": {
+ "nice-try": "^1.0.4",
+ "path-key": "^2.0.1",
+ "semver": "^5.5.0",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ }
+ },
+ "debug": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz",
+ "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "deep-is": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
+ "dev": true
+ },
+ "doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "eslint": {
+ "version": "5.9.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.9.0.tgz",
+ "integrity": "sha512-g4KWpPdqN0nth+goDNICNXGfJF7nNnepthp46CAlJoJtC5K/cLu3NgCM3AHu1CkJ5Hzt9V0Y0PBAO6Ay/gGb+w==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.0.0",
+ "ajv": "^6.5.3",
+ "chalk": "^2.1.0",
+ "cross-spawn": "^6.0.5",
+ "debug": "^4.0.1",
+ "doctrine": "^2.1.0",
+ "eslint-scope": "^4.0.0",
+ "eslint-utils": "^1.3.1",
+ "eslint-visitor-keys": "^1.0.0",
+ "espree": "^4.0.0",
+ "esquery": "^1.0.1",
+ "esutils": "^2.0.2",
+ "file-entry-cache": "^2.0.0",
+ "functional-red-black-tree": "^1.0.1",
+ "glob": "^7.1.2",
+ "globals": "^11.7.0",
+ "ignore": "^4.0.6",
+ "imurmurhash": "^0.1.4",
+ "inquirer": "^6.1.0",
+ "is-resolvable": "^1.1.0",
+ "js-yaml": "^3.12.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.3.0",
+ "lodash": "^4.17.5",
+ "minimatch": "^3.0.4",
+ "mkdirp": "^0.5.1",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.8.2",
+ "path-is-inside": "^1.0.2",
+ "pluralize": "^7.0.0",
+ "progress": "^2.0.0",
+ "regexpp": "^2.0.1",
+ "require-uncached": "^1.0.3",
+ "semver": "^5.5.1",
+ "strip-ansi": "^4.0.0",
+ "strip-json-comments": "^2.0.1",
+ "table": "^5.0.2",
+ "text-table": "^0.2.0"
+ }
+ },
+ "eslint-scope": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.0.tgz",
+ "integrity": "sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA==",
+ "dev": true,
+ "requires": {
+ "esrecurse": "^4.1.0",
+ "estraverse": "^4.1.1"
+ }
+ },
+ "eslint-utils": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz",
+ "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==",
+ "dev": true
+ },
+ "eslint-visitor-keys": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
+ "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==",
+ "dev": true
+ },
+ "espree": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-4.1.0.tgz",
+ "integrity": "sha512-I5BycZW6FCVIub93TeVY1s7vjhP9CY6cXCznIRfiig7nRviKZYdRnj/sHEWC6A7WE9RDWOFq9+7OsWSYz8qv2w==",
+ "dev": true,
+ "requires": {
+ "acorn": "^6.0.2",
+ "acorn-jsx": "^5.0.0",
+ "eslint-visitor-keys": "^1.0.0"
+ }
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true
+ },
+ "esquery": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz",
+ "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==",
+ "dev": true,
+ "requires": {
+ "estraverse": "^4.0.0"
+ }
+ },
+ "esrecurse": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
+ "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+ "dev": true,
+ "requires": {
+ "estraverse": "^4.1.0"
+ }
+ },
+ "estraverse": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
+ "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
+ "dev": true
+ },
+ "esutils": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
+ "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
+ "dev": true
+ },
+ "external-editor": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz",
+ "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==",
+ "dev": true,
+ "requires": {
+ "chardet": "^0.7.0",
+ "iconv-lite": "^0.4.24",
+ "tmp": "^0.0.33"
+ }
+ },
+ "fast-deep-equal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+ "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
+ "dev": true
+ },
+ "fast-json-stable-stringify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
+ "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
+ "dev": true
+ },
+ "fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+ "dev": true
+ },
+ "figures": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
+ "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
+ "dev": true,
+ "requires": {
+ "escape-string-regexp": "^1.0.5"
+ }
+ },
+ "file-entry-cache": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz",
+ "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=",
+ "dev": true,
+ "requires": {
+ "flat-cache": "^1.2.1",
+ "object-assign": "^4.0.1"
+ }
+ },
+ "flat-cache": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz",
+ "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==",
+ "dev": true,
+ "requires": {
+ "circular-json": "^0.3.1",
+ "graceful-fs": "^4.1.2",
+ "rimraf": "~2.6.2",
+ "write": "^0.2.1"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "functional-red-black-tree": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
+ "dev": true
+ },
+ "glob": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+ "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "globals": {
+ "version": "11.9.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.9.0.tgz",
+ "integrity": "sha512-5cJVtyXWH8PiJPVLZzzoIizXx944O4OmRro5MWKx5fT4MgcN7OfaMutPeaTdJCCURwbWdhhcCWcKIffPnmTzBg==",
+ "dev": true
+ },
+ "graceful-fs": {
+ "version": "4.1.15",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz",
+ "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "ignore": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+ "dev": true
+ },
+ "imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+ "dev": true
+ },
+ "inquirer": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.0.tgz",
+ "integrity": "sha512-QIEQG4YyQ2UYZGDC4srMZ7BjHOmNk1lR2JQj5UknBapklm6WHA+VVH7N+sUdX3A7NeCfGF8o4X1S3Ao7nAcIeg==",
+ "dev": true,
+ "requires": {
+ "ansi-escapes": "^3.0.0",
+ "chalk": "^2.0.0",
+ "cli-cursor": "^2.1.0",
+ "cli-width": "^2.0.0",
+ "external-editor": "^3.0.0",
+ "figures": "^2.0.0",
+ "lodash": "^4.17.10",
+ "mute-stream": "0.0.7",
+ "run-async": "^2.2.0",
+ "rxjs": "^6.1.0",
+ "string-width": "^2.1.0",
+ "strip-ansi": "^4.0.0",
+ "through": "^2.3.6"
+ }
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "dev": true
+ },
+ "is-promise": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
+ "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
+ "dev": true
+ },
+ "is-resolvable": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz",
+ "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==",
+ "dev": true
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "js-yaml": {
+ "version": "3.12.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz",
+ "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==",
+ "dev": true,
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ }
+ },
+ "json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+ "dev": true
+ },
+ "levn": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+ "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2"
+ }
+ },
+ "lodash": {
+ "version": "4.17.11",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
+ "dev": true
+ },
+ "mimic-fn": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
+ "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
+ "dev": true
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "0.0.8",
+ "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+ "dev": true
+ },
+ "mkdirp": {
+ "version": "0.5.1",
+ "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+ "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+ "dev": true,
+ "requires": {
+ "minimist": "0.0.8"
+ }
+ },
+ "ms": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
+ "dev": true
+ },
+ "mute-stream": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
+ "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=",
+ "dev": true
+ },
+ "natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+ "dev": true
+ },
+ "nice-try": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+ "dev": true
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+ "dev": true
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "onetime": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
+ "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
+ "dev": true,
+ "requires": {
+ "mimic-fn": "^1.0.0"
+ }
+ },
+ "optionator": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
+ "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=",
+ "dev": true,
+ "requires": {
+ "deep-is": "~0.1.3",
+ "fast-levenshtein": "~2.0.4",
+ "levn": "~0.3.0",
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2",
+ "wordwrap": "~1.0.0"
+ }
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
+ "dev": true
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "path-is-inside": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+ "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
+ "dev": true
+ },
+ "path-key": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+ "dev": true
+ },
+ "pluralize": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz",
+ "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==",
+ "dev": true
+ },
+ "prelude-ls": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
+ "dev": true
+ },
+ "progress": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.1.tgz",
+ "integrity": "sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg==",
+ "dev": true
+ },
+ "punycode": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "dev": true
+ },
+ "regexpp": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
+ "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
+ "dev": true
+ },
+ "require-uncached": {
+ "version": "1.0.3",
+ "resolved": "http://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz",
+ "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=",
+ "dev": true,
+ "requires": {
+ "caller-path": "^0.1.0",
+ "resolve-from": "^1.0.0"
+ }
+ },
+ "resolve-from": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz",
+ "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=",
+ "dev": true
+ },
+ "restore-cursor": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
+ "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
+ "dev": true,
+ "requires": {
+ "onetime": "^2.0.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "rimraf": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
+ "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.0.5"
+ }
+ },
+ "run-async": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
+ "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=",
+ "dev": true,
+ "requires": {
+ "is-promise": "^2.1.0"
+ }
+ },
+ "rxjs": {
+ "version": "6.3.3",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz",
+ "integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.9.0"
+ }
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true
+ },
+ "semver": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
+ "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==",
+ "dev": true
+ },
+ "shebang-command": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+ "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+ "dev": true,
+ "requires": {
+ "shebang-regex": "^1.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+ "dev": true
+ },
+ "signal-exit": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
+ "dev": true
+ },
+ "slice-ansi": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz",
+ "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==",
+ "dev": true,
+ "requires": {
+ "is-fullwidth-code-point": "^2.0.0"
+ }
+ },
+ "sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "dev": true,
+ "requires": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ },
+ "strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "table": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/table/-/table-5.1.0.tgz",
+ "integrity": "sha512-e542in22ZLhD/fOIuXs/8yDZ9W61ltF8daM88rkRNtgTIct+vI2fTnAyu/Db2TCfEcI8i7mjZz6meLq0nW7TYg==",
+ "dev": true,
+ "requires": {
+ "ajv": "^6.5.3",
+ "lodash": "^4.17.10",
+ "slice-ansi": "1.0.0",
+ "string-width": "^2.1.1"
+ }
+ },
+ "text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+ "dev": true
+ },
+ "through": {
+ "version": "2.3.8",
+ "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+ "dev": true
+ },
+ "tmp": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+ "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+ "dev": true,
+ "requires": {
+ "os-tmpdir": "~1.0.2"
+ }
+ },
+ "tslib": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
+ "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==",
+ "dev": true
+ },
+ "type-check": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+ "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "~1.1.2"
+ }
+ },
+ "uri-js": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
+ "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
+ "dev": true,
+ "requires": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "wordwrap": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+ "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
+ "dev": true
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ },
+ "write": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz",
+ "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=",
+ "dev": true,
+ "requires": {
+ "mkdirp": "^0.5.1"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..12aa2bb
--- /dev/null
+++ b/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "i18n",
+ "version": "0.0.1",
+ "description": "Simple Internationalization provider for JavaScript",
+ "main": "i18n.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/Xaymar/i18n-js.git"
+ },
+ "keywords": [
+ "i18n",
+ "internationalization",
+ "translation",
+ "translate"
+ ],
+ "author": "Michael Fabian 'Xaymar' Dirks ",
+ "license": "AGPL-3.0",
+ "bugs": {
+ "url": "https://github.com/Xaymar/i18n-js/issues"
+ },
+ "homepage": "https://github.com/Xaymar/i18n-js#readme",
+ "devDependencies": {
+ "eslint": "^5.9.0"
+ }
+}
diff --git a/readme.md b/readme.md
index e563b44..fbb0b5b 100644
--- a/readme.md
+++ b/readme.md
@@ -1,6 +1,52 @@
# I18n (JavaScript)
+Very simple JavaScript library for handling translations. Supports Node.js, Electron and other JavaScript based code runners.
-Simple JavaScript library to load translation data in the browser and apply translations automatically to a full page.
+# Features:
+* Simple modern API with Promises and full Documentation.
+* Support for languages with more than one base language and per language base language overrides.
+* Cached translation chains for quick and recursion free key lookups.
+
+# Maintenance, Donations, etc.
+This codes maintenance is funded through [Patreon](https://www.patreon.com/Xaymar). If you think this code is useful to you, consider donating a few bucks.
# License
-Licensed under Affero GNU GPLv3. See license.txt for more information.
\ No newline at end of file
+Licensed under Affero GNU GPLv3. See license.txt for more information.
+
+# Documentation
+`i18n.js` is fully documented using JSDoc, a format IDEs should be able to display. If in any case your IDE is not showing this information, your first reference should be `i18n.js`, not this readme.
+
+### I18n
+The class that contains all the translation data.
+
+##### constructor(string defaultLanguage, string baseLanguageKey = '\_base')
+Creates a new I18n object when called with `new I18n('mylanguage')`. Will set up internal structures and variables for later use. Throws exceptions.
+
+##### bool hasLanguage(string language)
+Check if a language is known to this I18n object. Returns true if known. Throws exceptions.
+
+##### createLanguage(string language)
+Create a new empty language, or replaces a loaded one with an empty one. Throws exceptions.
+
+##### Promise(string) loadLanguage(string language, File|Blob|object|string data, string encoding = 'utf-8')
+Load a language from a File, Blob, object or string as JSON data, and store it as the language. Returns a Promise, which will resolve once it is done loading and parsing and the result will contain the loaded language name. Throws exceptions.
+
+##### Promise(string) saveLanguage(string language)
+Save a language to a string as JSON data. Returns a Promise which will resolve with the JSON data string once done. Modifying the language before the returned Promise is done can result in corrupt data. Throws exceptions.
+
+##### destroyLanguage(string language)
+Destroy a language, thus clearing any references to data in it. Throws exceptions.
+
+##### bool clearKey(string language, string key)
+Remove a key from a language. Returns true if the key was removed. Throws exceptions.
+
+##### bool setKey(string language, string key, any value, bool force = true)
+Set a key in a language to a new value, optionally overwriting the previous content (defaults to true). Returns true if successful. Throws exceptions.
+
+##### any getKey(string language, string key)
+Get the value of a key in a language. Returns the value of the key if known, otherwise throws an exception.
+
+##### string translate(string key, string language)
+Translate a given key to the language selected. Returns the translated string or the original key if it was not found. Throws exceptions.
+
+##### Promise domTranslate(string language, string property = 'data-i18n', function resolver = string function(string language, string key, Node element), function applier = bool function(string language, string key, string translated, Node element))
+Automatically translate all elements in the current DOM which have the attribute `property`. Returns a Promise which resolves when the translation is complete. Throws exceptions.