feat(localize): add types to localize

Co-authored-by: narzac <narzac@gmail.com>
This commit is contained in:
Joren Broekema 2020-08-03 15:43:13 +02:00
parent 74cbb10553
commit 09d9675963
57 changed files with 1119 additions and 446 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/localize': minor
---
Add types for localize package. JSDocs types in the .js files, TSC generates type definition files. Mixins type definition files are hand-typed.

View file

@ -0,0 +1,5 @@
---
'singleton-manager': patch
---
Added basic JSDocs types to SingletonManager, in order for localize to be able to be typed correctly.

View file

@ -45,4 +45,4 @@ jobs:
publish: yarn release publish: yarn release
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View file

@ -2,22 +2,36 @@ import MessageFormat from '@bundled-es-modules/message-format/MessageFormat.js';
import { LionSingleton } from '@lion/core'; import { LionSingleton } from '@lion/core';
import isLocalizeESModule from './isLocalizeESModule.js'; import isLocalizeESModule from './isLocalizeESModule.js';
/**
* @typedef {import('../types/LocalizeMixinTypes').NamespaceObject} NamespaceObject
*/
/** /**
* `LocalizeManager` manages your translations (includes loading) * `LocalizeManager` manages your translations (includes loading)
*/ */
export class LocalizeManager extends LionSingleton { export class LocalizeManager extends LionSingleton {
// eslint-disable-line no-unused-vars // eslint-disable-line no-unused-vars
constructor(params = {}) { constructor({ autoLoadOnLocaleChange = false, fallbackLocale = '' } = {}) {
super(params); super();
this._fakeExtendsEventTarget(); this.__delegationTarget = document.createDocumentFragment();
this._autoLoadOnLocaleChange = !!autoLoadOnLocaleChange;
this._fallbackLocale = fallbackLocale;
this._autoLoadOnLocaleChange = !!params.autoLoadOnLocaleChange; /** @type {Object.<string, Object.<string, Object>>} */
this._fallbackLocale = params.fallbackLocale;
this.__storage = {}; this.__storage = {};
/** @type {Map.<RegExp|string, function>} */
this.__namespacePatternsMap = new Map(); this.__namespacePatternsMap = new Map();
/** @type {Object.<string, function|null>} */
this.__namespaceLoadersCache = {}; this.__namespaceLoadersCache = {};
/** @type {Object.<string, Object.<string, Promise.<Object>>>} */
this.__namespaceLoaderPromisesCache = {}; this.__namespaceLoaderPromisesCache = {};
this.formatNumberOptions = { returnIfNaN: '' };
this.formatNumberOptions = {
returnIfNaN: '',
};
/** /**
* Via html[data-localize-lang], developers are allowed to set the initial locale, without * Via html[data-localize-lang], developers are allowed to set the initial locale, without
@ -73,18 +87,24 @@ export class LocalizeManager extends LionSingleton {
this._teardownHtmlLangAttributeObserver(); this._teardownHtmlLangAttributeObserver();
} }
// eslint-disable-next-line class-methods-use-this /**
* @returns {string}
*/
get locale() { get locale() {
if (this._supportExternalTranslationTools) { if (this._supportExternalTranslationTools) {
return this.__locale; return this.__locale || '';
} }
return document.documentElement.lang; return document.documentElement.lang;
} }
/**
* @param {string} value
*/
set locale(value) { set locale(value) {
/** @type {string} */
let oldLocale; let oldLocale;
if (this._supportExternalTranslationTools) { if (this._supportExternalTranslationTools) {
oldLocale = this.__locale; oldLocale = /** @type {string} */ (this.__locale);
this.__locale = value; this.__locale = value;
if (this._langAttrSetByTranslationTool === null) { if (this._langAttrSetByTranslationTool === null) {
this._setHtmlLangAttribute(value); this._setHtmlLangAttribute(value);
@ -101,12 +121,19 @@ export class LocalizeManager extends LionSingleton {
this._onLocaleChanged(value, oldLocale); this._onLocaleChanged(value, oldLocale);
} }
/**
* @param {string} locale
*/
_setHtmlLangAttribute(locale) { _setHtmlLangAttribute(locale) {
this._teardownHtmlLangAttributeObserver(); this._teardownHtmlLangAttributeObserver();
document.documentElement.lang = locale; document.documentElement.lang = locale;
this._setupHtmlLangAttributeObserver(); this._setupHtmlLangAttributeObserver();
} }
/**
* @param {string} value
* @throws {Error} Language only locales are not allowed(Use 'en-GB' instead of 'en')
*/
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
__handleLanguageOnly(value) { __handleLanguageOnly(value) {
throw new Error(` throw new Error(`
@ -116,6 +143,9 @@ export class LocalizeManager extends LionSingleton {
`); `);
} }
/**
* @returns {Promise.<Object>}
*/
get loadingComplete() { get loadingComplete() {
return Promise.all(Object.values(this.__namespaceLoaderPromisesCache[this.locale])); return Promise.all(Object.values(this.__namespaceLoaderPromisesCache[this.locale]));
} }
@ -127,6 +157,12 @@ export class LocalizeManager extends LionSingleton {
this.__namespaceLoaderPromisesCache = {}; this.__namespaceLoaderPromisesCache = {};
} }
/**
* @param {string} locale
* @param {string} namespace
* @param {object} data
* @throws {Error} Namespace can be added only once, for a given locale
*/
addData(locale, namespace, data) { addData(locale, namespace, data) {
if (this._isNamespaceInCache(locale, namespace)) { if (this._isNamespaceInCache(locale, namespace)) {
throw new Error( throw new Error(
@ -138,18 +174,41 @@ export class LocalizeManager extends LionSingleton {
this.__storage[locale][namespace] = data; this.__storage[locale][namespace] = data;
} }
/**
* @param {RegExp|string} pattern
* @param {function} loader
*/
setupNamespaceLoader(pattern, loader) { setupNamespaceLoader(pattern, loader) {
this.__namespacePatternsMap.set(pattern, loader); this.__namespacePatternsMap.set(pattern, loader);
} }
/**
* @param {NamespaceObject[]} namespaces
* @param {Object} [options]
* @param {string} [options.locale]
* @returns {Promise.<Object>}
*/
loadNamespaces(namespaces, { locale } = {}) { loadNamespaces(namespaces, { locale } = {}) {
return Promise.all(namespaces.map(namespace => this.loadNamespace(namespace, { locale }))); return Promise.all(
namespaces.map(
/** @param {NamespaceObject} namespace */
namespace => this.loadNamespace(namespace, { locale }),
),
);
} }
/**
* @param {NamespaceObject} namespaceObj
* @param {Object} [options]
* @param {string} [options.locale]
* @returns {Promise.<Object|void>}
*/
loadNamespace(namespaceObj, { locale = this.locale } = { locale: this.locale }) { loadNamespace(namespaceObj, { locale = this.locale } = { locale: this.locale }) {
const isDynamicImport = typeof namespaceObj === 'object'; const isDynamicImport = typeof namespaceObj === 'object';
const namespace = isDynamicImport ? Object.keys(namespaceObj)[0] : namespaceObj; const namespace = /** @type {string} */ (isDynamicImport
? Object.keys(namespaceObj)[0]
: namespaceObj);
if (this._isNamespaceInCache(locale, namespace)) { if (this._isNamespaceInCache(locale, namespace)) {
return Promise.resolve(); return Promise.resolve();
@ -163,6 +222,13 @@ export class LocalizeManager extends LionSingleton {
return this._loadNamespaceData(locale, namespaceObj, isDynamicImport, namespace); return this._loadNamespaceData(locale, namespaceObj, isDynamicImport, namespace);
} }
/**
* @param {string | string[]} keys
* @param {Object.<string,?>} [vars]
* @param {Object} [opts]
* @param {string} [opts.locale]
* @returns {string}
*/
msg(keys, vars, opts = {}) { msg(keys, vars, opts = {}) {
const locale = opts.locale ? opts.locale : this.locale; const locale = opts.locale ? opts.locale : this.locale;
const message = this._getMessageForKeys(keys, locale); const message = this._getMessageForKeys(keys, locale);
@ -186,7 +252,7 @@ export class LocalizeManager extends LionSingleton {
this._langAttrSetByTranslationTool = document.documentElement.lang; this._langAttrSetByTranslationTool = document.documentElement.lang;
} }
} else { } else {
this._onLocaleChanged(document.documentElement.lang, mutation.oldValue); this._onLocaleChanged(document.documentElement.lang, mutation.oldValue || '');
} }
}); });
}); });
@ -199,13 +265,23 @@ export class LocalizeManager extends LionSingleton {
} }
_teardownHtmlLangAttributeObserver() { _teardownHtmlLangAttributeObserver() {
this._htmlLangAttributeObserver.disconnect(); if (this._htmlLangAttributeObserver) {
this._htmlLangAttributeObserver.disconnect();
}
} }
/**
* @param {string} locale
* @param {string} namespace
*/
_isNamespaceInCache(locale, namespace) { _isNamespaceInCache(locale, namespace) {
return !!(this.__storage[locale] && this.__storage[locale][namespace]); return !!(this.__storage[locale] && this.__storage[locale][namespace]);
} }
/**
* @param {string} locale
* @param {string} namespace
*/
_getCachedNamespaceLoaderPromise(locale, namespace) { _getCachedNamespaceLoaderPromise(locale, namespace) {
if (this.__namespaceLoaderPromisesCache[locale]) { if (this.__namespaceLoaderPromisesCache[locale]) {
return this.__namespaceLoaderPromisesCache[locale][namespace]; return this.__namespaceLoaderPromisesCache[locale][namespace];
@ -213,22 +289,41 @@ export class LocalizeManager extends LionSingleton {
return null; return null;
} }
/**
* @param {string} locale
* @param {NamespaceObject} namespaceObj
* @param {boolean} isDynamicImport
* @param {string} namespace
* @returns {Promise.<Object|void>}
*/
_loadNamespaceData(locale, namespaceObj, isDynamicImport, namespace) { _loadNamespaceData(locale, namespaceObj, isDynamicImport, namespace) {
const loader = this._getNamespaceLoader(namespaceObj, isDynamicImport, namespace); const loader = this._getNamespaceLoader(namespaceObj, isDynamicImport, namespace);
const loaderPromise = this._getNamespaceLoaderPromise(loader, locale, namespace); const loaderPromise = this._getNamespaceLoaderPromise(loader, locale, namespace);
this._cacheNamespaceLoaderPromise(locale, namespace, loaderPromise); this._cacheNamespaceLoaderPromise(locale, namespace, loaderPromise);
return loaderPromise.then(obj => { return loaderPromise.then(
const data = isLocalizeESModule(obj) ? obj.default : obj; /**
this.addData(locale, namespace, data); * @param {Object} obj
}); * @param {Object} obj.default
*/
obj => {
const data = isLocalizeESModule(obj) ? obj.default : obj;
this.addData(locale, namespace, data);
},
);
} }
/**
* @param {NamespaceObject} namespaceObj
* @param {boolean} isDynamicImport
* @param {string} namespace
* @throws {Error} Namespace shall setup properly. Check loader!
*/
_getNamespaceLoader(namespaceObj, isDynamicImport, namespace) { _getNamespaceLoader(namespaceObj, isDynamicImport, namespace) {
let loader = this.__namespaceLoadersCache[namespace]; let loader = this.__namespaceLoadersCache[namespace];
if (!loader) { if (!loader) {
if (isDynamicImport) { if (isDynamicImport) {
loader = namespaceObj[namespace]; const _namespaceObj = /** @type {Object.<string,function>} */ (namespaceObj);
loader = _namespaceObj[namespace];
this.__namespaceLoadersCache[namespace] = loader; this.__namespaceLoadersCache[namespace] = loader;
} else { } else {
loader = this._lookupNamespaceLoader(namespace); loader = this._lookupNamespaceLoader(namespace);
@ -245,12 +340,20 @@ export class LocalizeManager extends LionSingleton {
return loader; return loader;
} }
/**
* @param {function} loader
* @param {string} locale
* @param {string} namespace
* @param {string} [fallbackLocale]
* @returns {Promise.<any>}
* @throws {Error} Data for namespace and (locale or fallback locale) could not be loaded.
*/
_getNamespaceLoaderPromise(loader, locale, namespace, fallbackLocale = this._fallbackLocale) { _getNamespaceLoaderPromise(loader, locale, namespace, fallbackLocale = this._fallbackLocale) {
return loader(locale, namespace).catch(() => { return loader(locale, namespace).catch(() => {
const lang = this._getLangFromLocale(locale); const lang = this._getLangFromLocale(locale);
return loader(lang, namespace).catch(() => { return loader(lang, namespace).catch(() => {
if (fallbackLocale) { if (fallbackLocale) {
return this._getNamespaceLoaderPromise(loader, fallbackLocale, namespace, false).catch( return this._getNamespaceLoaderPromise(loader, fallbackLocale, namespace, '').catch(
() => { () => {
const fallbackLang = this._getLangFromLocale(fallbackLocale); const fallbackLang = this._getLangFromLocale(fallbackLocale);
throw new Error( throw new Error(
@ -268,6 +371,11 @@ export class LocalizeManager extends LionSingleton {
}); });
} }
/**
* @param {string} locale
* @param {string} namespace
* @param {Promise.<Object>} promise
*/
_cacheNamespaceLoaderPromise(locale, namespace, promise) { _cacheNamespaceLoaderPromise(locale, namespace, promise) {
if (!this.__namespaceLoaderPromisesCache[locale]) { if (!this.__namespaceLoaderPromisesCache[locale]) {
this.__namespaceLoaderPromisesCache[locale] = {}; this.__namespaceLoaderPromisesCache[locale] = {};
@ -275,6 +383,10 @@ export class LocalizeManager extends LionSingleton {
this.__namespaceLoaderPromisesCache[locale][namespace] = promise; this.__namespaceLoaderPromisesCache[locale][namespace] = promise;
} }
/**
* @param {string} namespace
* @returns {function|null}
*/
_lookupNamespaceLoader(namespace) { _lookupNamespaceLoader(namespace) {
/* eslint-disable no-restricted-syntax */ /* eslint-disable no-restricted-syntax */
for (const [key, value] of this.__namespacePatternsMap) { for (const [key, value] of this.__namespacePatternsMap) {
@ -289,18 +401,45 @@ export class LocalizeManager extends LionSingleton {
/* eslint-enable no-restricted-syntax */ /* eslint-enable no-restricted-syntax */
} }
/**
* @param {string} locale
* @returns {string}
*/
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
_getLangFromLocale(locale) { _getLangFromLocale(locale) {
return locale.substring(0, 2); return locale.substring(0, 2);
} }
_fakeExtendsEventTarget() { /**
const delegate = document.createDocumentFragment(); * @param {string} type
['addEventListener', 'dispatchEvent', 'removeEventListener'].forEach(funcName => { * @param {EventListener} listener
this[funcName] = (...args) => delegate[funcName](...args); * @param {...Object} options
}); */
addEventListener(type, listener, ...options) {
this.__delegationTarget.addEventListener(type, listener, ...options);
} }
/**
* @param {string} type
* @param {EventListener} listener
* @param {...Object} options
*/
removeEventListener(type, listener, ...options) {
this.__delegationTarget.removeEventListener(type, listener, ...options);
}
/**
* @param {CustomEvent} event
*/
dispatchEvent(event) {
this.__delegationTarget.dispatchEvent(event);
}
/**
* @param {string} newLocale
* @param {string} oldLocale
* @returns {undefined}
*/
_onLocaleChanged(newLocale, oldLocale) { _onLocaleChanged(newLocale, oldLocale) {
if (newLocale === oldLocale) { if (newLocale === oldLocale) {
return; return;
@ -311,19 +450,34 @@ export class LocalizeManager extends LionSingleton {
this.dispatchEvent(new CustomEvent('localeChanged', { detail: { newLocale, oldLocale } })); this.dispatchEvent(new CustomEvent('localeChanged', { detail: { newLocale, oldLocale } }));
} }
/**
* @param {string} newLocale
* @param {string} oldLocale
* @returns {Promise.<Object>}
*/
_loadAllMissing(newLocale, oldLocale) { _loadAllMissing(newLocale, oldLocale) {
const oldLocaleNamespaces = this.__storage[oldLocale] || {}; const oldLocaleNamespaces = this.__storage[oldLocale] || {};
const newLocaleNamespaces = this.__storage[newLocale] || {}; const newLocaleNamespaces = this.__storage[newLocale] || {};
/** @type {Promise<Object|void>[]} */
const promises = []; const promises = [];
Object.keys(oldLocaleNamespaces).forEach(namespace => { Object.keys(oldLocaleNamespaces).forEach(namespace => {
const newNamespaceData = newLocaleNamespaces[namespace]; const newNamespaceData = newLocaleNamespaces[namespace];
if (!newNamespaceData) { if (!newNamespaceData) {
promises.push(this.loadNamespace(namespace, { locale: newLocale })); promises.push(
this.loadNamespace(namespace, {
locale: newLocale,
}),
);
} }
}); });
return Promise.all(promises); return Promise.all(promises);
} }
/**
* @param {string | string[]} keys
* @param {string} locale
* @returns {string | undefined}
*/
_getMessageForKeys(keys, locale) { _getMessageForKeys(keys, locale) {
if (typeof keys === 'string') { if (typeof keys === 'string') {
return this._getMessageForKey(keys, locale); return this._getMessageForKey(keys, locale);
@ -341,16 +495,33 @@ export class LocalizeManager extends LionSingleton {
return undefined; return undefined;
} }
/**
* @param {string | undefined} key
* @param {string} locale
* @returns {string}
* @throws {Error} `key`is missing namespace. The format for `key` is "namespace:name"
*
*/
_getMessageForKey(key, locale) { _getMessageForKey(key, locale) {
if (key.indexOf(':') === -1) { if (!key || key.indexOf(':') === -1) {
throw new Error( throw new Error(
`Namespace is missing in the key "${key}". The format for keys is "namespace:name".`, `Namespace is missing in the key "${key}". The format for keys is "namespace:name".`,
); );
} }
const [ns, namesString] = key.split(':'); const [ns, namesString] = key.split(':');
const namespaces = this.__storage[locale]; const namespaces = this.__storage[locale];
const messages = namespaces ? namespaces[ns] : null; const messages = namespaces ? namespaces[ns] : {};
const names = namesString.split('.'); const names = namesString.split('.');
return names.reduce((message, n) => (message ? message[n] : null), messages); const result = names.reduce(
/**
* @param {Object.<string, any> | string} message
* @param {string} name
* @returns {string}
*/
(message, name) => (typeof message === 'object' ? message[name] : message),
messages,
);
return String(result || '');
} }
} }

View file

@ -2,111 +2,154 @@ import { dedupeMixin, until, nothing } from '@lion/core';
import { localize } from './localize.js'; import { localize } from './localize.js';
/** /**
* # LocalizeMixin - for self managed templates * @typedef {import('../types/LocalizeMixinTypes').LocalizeMixin} LocalizeMixin
*
* @polymerMixin
* @mixinFunction
*/ */
export const LocalizeMixin = dedupeMixin(
superclass =>
// eslint-disable-next-line
class LocalizeMixin extends superclass {
static get localizeNamespaces() {
return [];
}
static get waitForLocalizeNamespaces() { /**
return true; * # LocalizeMixin - for self managed templates
} * @type {LocalizeMixin}
*/
const LocalizeMixinImplementation = superclass =>
// eslint-disable-next-line
class LocalizeMixin extends superclass {
/**
* @returns {Object.<string,function>[]}
*/
static get localizeNamespaces() {
return [];
}
constructor() { /**
super(); * @returns {boolean}
*/
static get waitForLocalizeNamespaces() {
return true;
}
this.__boundLocalizeOnLocaleChanged = (...args) => this.__localizeOnLocaleChanged(...args); constructor() {
super();
// should be loaded in advance this.__boundLocalizeOnLocaleChanged =
this.__localizeStartLoadingNamespaces(); /** @param {...Object} args */
(...args) => {
const event = /** @type {CustomEvent} */ (Array.from(args)[0]);
this.__localizeOnLocaleChanged(event);
};
// should be loaded in advance
this.__localizeStartLoadingNamespaces();
if (this.localizeNamespacesLoaded) {
this.localizeNamespacesLoaded.then(() => { this.localizeNamespacesLoaded.then(() => {
this.__localizeMessageSync = true; this.__localizeMessageSync = true;
}); });
} }
}
/** /**
* hook into LitElement to only render once all translations are loaded * hook into LitElement to only render once all translations are loaded
*/ * @returns {Promise.<void>}
async performUpdate() { */
if (this.constructor.waitForLocalizeNamespaces) { async performUpdate() {
await this.localizeNamespacesLoaded; if (Object.getPrototypeOf(this).constructor.waitForLocalizeNamespaces) {
} await this.localizeNamespacesLoaded;
super.performUpdate(); }
super.performUpdate();
}
connectedCallback() {
if (super.connectedCallback) {
super.connectedCallback();
} }
connectedCallback() { if (this.localizeNamespacesLoaded) {
if (super.connectedCallback) {
super.connectedCallback();
}
this.localizeNamespacesLoaded.then(() => this.onLocaleReady()); this.localizeNamespacesLoaded.then(() => this.onLocaleReady());
this.__localizeAddLocaleChangedListener(); }
this.__localizeAddLocaleChangedListener();
}
disconnectedCallback() {
if (super.disconnectedCallback) {
super.disconnectedCallback();
} }
disconnectedCallback() { this.__localizeRemoveLocaleChangedListener();
if (super.disconnectedCallback) { }
super.disconnectedCallback();
}
this.__localizeRemoveLocaleChangedListener(); /**
* @param {string | string[]} keys
* @param {Object.<string,?>} variables
* @param {Object} [options]
* @param {string} [options.locale]
* @return {string | function}
*/
msgLit(keys, variables, options) {
if (this.__localizeMessageSync) {
return localize.msg(keys, variables, options);
} }
msgLit(...args) { if (!this.localizeNamespacesLoaded) {
if (this.__localizeMessageSync) { return '';
return localize.msg(...args);
}
return until(
this.localizeNamespacesLoaded.then(() => localize.msg(...args)),
nothing,
);
} }
__getUniqueNamespaces() { return until(
const uniqueNamespaces = []; this.localizeNamespacesLoaded.then(() => localize.msg(keys, variables, options)),
nothing,
);
}
// IE11 does not support iterable in the constructor /**
const s = new Set(); * @returns {string[]}
this.constructor.localizeNamespaces.forEach(s.add.bind(s)); */
s.forEach(uniqueNamespace => { __getUniqueNamespaces() {
uniqueNamespaces.push(uniqueNamespace); /** @type {string[]} */
}); const uniqueNamespaces = [];
return uniqueNamespaces;
}
__localizeStartLoadingNamespaces() { // IE11 does not support iterable in the constructor
this.localizeNamespacesLoaded = localize.loadNamespaces(this.__getUniqueNamespaces()); const s = new Set();
} Object.getPrototypeOf(this).constructor.localizeNamespaces.forEach(s.add.bind(s));
s.forEach(uniqueNamespace => {
uniqueNamespaces.push(uniqueNamespace);
});
return uniqueNamespaces;
}
__localizeAddLocaleChangedListener() { __localizeStartLoadingNamespaces() {
localize.addEventListener('localeChanged', this.__boundLocalizeOnLocaleChanged); this.localizeNamespacesLoaded = localize.loadNamespaces(this.__getUniqueNamespaces());
} }
__localizeRemoveLocaleChangedListener() { __localizeAddLocaleChangedListener() {
localize.removeEventListener('localeChanged', this.__boundLocalizeOnLocaleChanged); localize.addEventListener('localeChanged', this.__boundLocalizeOnLocaleChanged);
} }
__localizeOnLocaleChanged(event) { __localizeRemoveLocaleChangedListener() {
this.onLocaleChanged(event.detail.newLocale, event.detail.oldLocale); localize.removeEventListener('localeChanged', this.__boundLocalizeOnLocaleChanged);
} }
onLocaleReady() { /**
this.onLocaleUpdated(); * @param {CustomEvent} event
} */
__localizeOnLocaleChanged(event) {
this.onLocaleChanged(event.detail.newLocale, event.detail.oldLocale);
}
onLocaleChanged() { onLocaleReady() {
this.localizeNamespacesLoaded = localize.loadNamespaces(this.__getUniqueNamespaces()); this.onLocaleUpdated();
this.onLocaleUpdated(); }
this.requestUpdate();
}
// eslint-disable-next-line class-methods-use-this /**
onLocaleUpdated() {} * @param {string} newLocale
}, * @param {string} oldLocale
); */
// eslint-disable-next-line no-unused-vars
onLocaleChanged(newLocale, oldLocale) {
this.__localizeStartLoadingNamespaces();
this.onLocaleUpdated();
this.requestUpdate();
}
// eslint-disable-next-line class-methods-use-this
onLocaleUpdated() {}
};
export const LocalizeMixin = dedupeMixin(LocalizeMixinImplementation);

View file

@ -4,8 +4,8 @@ import { pad } from './pad.js';
/** /**
* To add a leading zero to a single number * To add a leading zero to a single number
* *
* @param dateString * @param {string} dateString
* @returns {*} * @returns {string}
*/ */
export function addLeadingZero(dateString) { export function addLeadingZero(dateString) {
const dateParts = splitDate(dateString); const dateParts = splitDate(dateString);

View file

@ -3,8 +3,8 @@ import { trim } from './trim.js';
/** /**
* To clean date from added characters from IE * To clean date from added characters from IE
* *
* @param dateAsString * @param {string} dateAsString
* @returns {string|XML} * @returns {string}
*/ */
export function clean(dateAsString) { export function clean(dateAsString) {
// list of separators is from wikipedia https://www.wikiwand.com/en/Date_format_by_country // list of separators is from wikipedia https://www.wikiwand.com/en/Date_format_by_country

View file

@ -4,14 +4,31 @@ import { normalizeIntlDate } from './normalizeIntlDate.js';
/** /**
* Formats date based on locale and options * Formats date based on locale and options
* *
* @param date * @param {Date} date
* @param options * @param {Object} [options] Intl options are available
* @returns {*} * @param {string} [options.locale]
* @param {string} [options.localeMatcher]
* @param {string} [options.formatMatcher]
* @param {boolean}[options.hour12]
* @param {string} [options.numberingSystem]
* @param {string} [options.calendar]
* @param {string} [options.timeZone]
* @param {string} [options.timeZoneName]
* @param {string} [options.weekday]
* @param {string} [options.era]
* @param {string} [options.year]
* @param {string} [options.month]
* @param {string} [options.day]
* @param {string} [options.hour]
* @param {string} [options.minute]
* @param {string} [options.second]
* @returns {string}
*/ */
export function formatDate(date, options) { export function formatDate(date, options) {
if (!(date instanceof Date)) { if (!(date instanceof Date)) {
return ''; return '';
} }
/** @type {options} */
const formatOptions = options || {}; const formatOptions = options || {};
/** /**
* Set smart defaults if: * Set smart defaults if:

View file

@ -3,13 +3,28 @@ import { splitDate } from './splitDate.js';
/** /**
* To compute the localized date format * To compute the localized date format
*
* @returns {string} * @returns {string}
*/ */
export function getDateFormatBasedOnLocale() { export function getDateFormatBasedOnLocale() {
/**
*
* @param {ArrayLike.<string>} dateParts
* @returns {string[]}
*/
function computePositions(dateParts) { function computePositions(dateParts) {
/**
* @param {number} index
* @returns {string}
*/
function getPartByIndex(index) { function getPartByIndex(index) {
return { 2012: 'year', 12: 'month', 20: 'day' }[dateParts[index]]; /** @type {Object.<string,string>} */
const template = {
'2012': 'year',
'12': 'month',
'20': 'day',
};
const key = dateParts[index];
return template[key];
} }
return [1, 3, 5].map(getPartByIndex); return [1, 3, 5].map(getPartByIndex);
@ -28,6 +43,8 @@ export function getDateFormatBasedOnLocale() {
const dateParts = splitDate(formattedDate); const dateParts = splitDate(formattedDate);
const dateFormat = {}; const dateFormat = {};
dateFormat.positions = computePositions(dateParts); if (dateParts) {
dateFormat.positions = computePositions(dateParts);
}
return `${dateFormat.positions[0]}-${dateFormat.positions[1]}-${dateFormat.positions[2]}`; return `${dateFormat.positions[0]}-${dateFormat.positions[1]}-${dateFormat.positions[2]}`;
} }

View file

@ -1,12 +1,14 @@
import { normalizeIntlDate } from './normalizeIntlDate.js'; import { normalizeIntlDate } from './normalizeIntlDate.js';
/** @type {Object.<string, Object.<string,string[]>>} */
const monthsLocaleCache = {}; const monthsLocaleCache = {};
/** /**
* @desc Returns month names for locale * @desc Returns month names for locale
* @param {string} options.locale locale * @param {Object} [options]
* @param {string} [options.locale] locale
* @param {string} [options.style=long] long, short or narrow * @param {string} [options.style=long] long, short or narrow
* @returns {Array} like: ['January', 'February', ...etc]. * @returns {string[]} like: ['January', 'February', ...etc].
*/ */
export function getMonthNames({ locale, style = 'long' } = {}) { export function getMonthNames({ locale, style = 'long' } = {}) {
let months = monthsLocaleCache[locale] && monthsLocaleCache[locale][style]; let months = monthsLocaleCache[locale] && monthsLocaleCache[locale][style];

View file

@ -1,24 +1,32 @@
import { normalizeIntlDate } from './normalizeIntlDate.js'; import { normalizeIntlDate } from './normalizeIntlDate.js';
/** @type {Object.<string, Object.<string,string[]>>} */
const weekdayNamesCache = {}; const weekdayNamesCache = {};
/** /**
* @desc Return cached weekday names for locale for all styles ('long', 'short', 'narrow') * @desc Return cached weekday names for locale for all styles ('long', 'short', 'narrow')
* @param {string} locale locale * @param {string} locale locale
* @returns {Object} like { long: ['Sunday', 'Monday'...], short: ['Sun', ...], narrow: ['S', ...] } * @returns {Object.<string,string[]>} - like { long: ['Sunday', 'Monday'...], short: ['Sun', ...], narrow: ['S', ...] }
*/ */
function getCachedWeekdayNames(locale) { function getCachedWeekdayNames(locale) {
let weekdays = weekdayNamesCache[locale]; const cachedWeekdayNames = weekdayNamesCache[locale];
let weekdays;
if (weekdays) { if (cachedWeekdayNames) {
return weekdays; return cachedWeekdayNames;
} }
weekdayNamesCache[locale] = { long: [], short: [], narrow: [] }; weekdayNamesCache[locale] = {
long: [],
short: [],
narrow: [],
};
['long', 'short', 'narrow'].forEach(style => { ['long', 'short', 'narrow'].forEach(style => {
weekdays = weekdayNamesCache[locale][style]; weekdays = weekdayNamesCache[locale][style];
const formatter = new Intl.DateTimeFormat(locale, { weekday: style }); const formatter = new Intl.DateTimeFormat(locale, {
weekday: style,
});
const date = new Date('2019/04/07'); // start from Sunday const date = new Date('2019/04/07'); // start from Sunday
for (let i = 0; i < 7; i += 1) { for (let i = 0; i < 7; i += 1) {
@ -34,10 +42,11 @@ function getCachedWeekdayNames(locale) {
/** /**
* @desc Returns weekday names for locale * @desc Returns weekday names for locale
* @param {string} options.locale locale * @param {Object} [options]
* @param {string} [options.locale] locale
* @param {string} [options.style=long] long, short or narrow * @param {string} [options.style=long] long, short or narrow
* @param {number} [options.firstDayOfWeek=0] 0 (Sunday), 1 (Monday), etc... * @param {number} [options.firstDayOfWeek=0] 0 (Sunday), 1 (Monday), etc...
* @returns {Array} like: ['Sunday', 'Monday', 'Tuesday', ...etc]. * @returns {string[]} like: ['Sunday', 'Monday', 'Tuesday', ...etc].
*/ */
export function getWeekdayNames({ locale, style = 'long', firstDayOfWeek = 0 } = {}) { export function getWeekdayNames({ locale, style = 'long', firstDayOfWeek = 0 } = {}) {
const weekdays = getCachedWeekdayNames(locale)[style]; const weekdays = getCachedWeekdayNames(locale)[style];

View file

@ -1,8 +1,8 @@
/** /**
* @desc Makes suitable for date comparisons * @desc Makes suitable for date comparisons
* @param {Date} d * @param {Date} date
* @returns {Date} * @returns {Date}
*/ */
export function normalizeDateTime(d) { export function normalizeDateTime(date) {
return new Date(d.getFullYear(), d.getMonth(), d.getDate()); return new Date(date.getFullYear(), date.getMonth(), date.getDate());
} }

View file

@ -1,7 +1,7 @@
/** /**
* To filter out some added characters in IE * To filter out some added characters in IE
* *
* @param str * @param {string} str
* @returns {string} * @returns {string}
*/ */
export function normalizeIntlDate(str) { export function normalizeIntlDate(str) {

View file

@ -1,12 +1,11 @@
/** /**
* To get the absolute value of a number. * To get the absolute value of a number.
* *
* @param n * @param {string} n - number in string format
* @returns {string} * @returns {string}
*/ */
export function pad(n) { export function pad(n) {
const digitRegex = /^\d+$/; const digitRegex = /^\d+$/;
const v = digitRegex.test(n) ? Math.abs(n) : n; const v = digitRegex.test(String(n)) ? Math.abs(Number(n)) : n;
return String(v < 10 ? `0${v}` : v); return String(v < 10 ? `0${v}` : v);
} }

View file

@ -2,9 +2,14 @@ import { localize } from '../localize.js';
import { getDateFormatBasedOnLocale } from './getDateFormatBasedOnLocale.js'; import { getDateFormatBasedOnLocale } from './getDateFormatBasedOnLocale.js';
import { addLeadingZero } from './addLeadingZero.js'; import { addLeadingZero } from './addLeadingZero.js';
/**
* @param {function} fn
*/
const memoize = fn => { const memoize = fn => {
/** @type {Object.<any, any>} */
const cache = {}; const cache = {};
return parm => {
return /** @param {any} parm */ parm => {
const n = parm; const n = parm;
if (n in cache) { if (n in cache) {
return cache[n]; return cache[n];
@ -20,11 +25,11 @@ const memoizedGetDateFormatBasedOnLocale = memoize(getDateFormatBasedOnLocale);
/** /**
* To parse a date into the right format * To parse a date into the right format
* *
* @param date * @param {string} dateString
* @returns {Date} * @returns {Date | undefined}
*/ */
export function parseDate(date) { export function parseDate(dateString) {
const stringToParse = addLeadingZero(date); const stringToParse = addLeadingZero(dateString);
let parsedString; let parsedString;
switch (memoizedGetDateFormatBasedOnLocale(localize.locale)) { switch (memoizedGetDateFormatBasedOnLocale(localize.locale)) {
case 'day-month-year': case 'day-month-year':
@ -51,7 +56,7 @@ export function parseDate(date) {
const parsedDate = new Date(parsedString); const parsedDate = new Date(parsedString);
// Check if parsedDate is not `Invalid Date` // Check if parsedDate is not `Invalid Date`
// eslint-disable-next-line no-restricted-globals // eslint-disable-next-line no-restricted-globals
if (!isNaN(parsedDate)) { if (!isNaN(parsedDate.getTime())) {
return parsedDate; return parsedDate;
} }
return undefined; return undefined;

View file

@ -4,8 +4,8 @@ import { clean } from './clean.js';
/** /**
* To sanitize a date from IE11 handling * To sanitize a date from IE11 handling
* *
* @param date * @param {Date} date
* @returns {string|XML} * @returns {string}
*/ */
export function sanitizedDateTimeFormat(date) { export function sanitizedDateTimeFormat(date) {
const fDate = formatDate(date); const fDate = formatDate(date);

View file

@ -1,9 +1,9 @@
/** /**
* To split a date into days, months, years, etc * To split a date into days, months, years, etc
* *
* @param date * @param {string} dateAsString
* @returns {Array|{index: number, input: string}|*} * @returns {ArrayLike.<string> | null}
*/ */
export function splitDate(date) { export function splitDate(dateAsString) {
return date.match(/(\d{1,4})([^\d]+)(\d{1,4})([^\d]+)(\d{1,4})/); return dateAsString.match(/(\d{1,4})([^\d]+)(\d{1,4})([^\d]+)(\d{1,4})/);
} }

View file

@ -1,8 +1,8 @@
/** /**
* To trim the date * To trim the date
* *
* @param dateAsString * @param {string} dateAsString
* @returns {string|XML} * @returns {string}
*/ */
export function trim(dateAsString) { export function trim(dateAsString) {
return dateAsString.replace(/^[^\d]*/g, '').replace(/[^\d]*$/g, ''); return dateAsString.replace(/^[^\d]*/g, '').replace(/[^\d]*$/g, '');

View file

@ -1,3 +1,7 @@
/**
* @param {Object.<string, Object>} obj
* @returns {boolean}
*/
export default function isLocalizeESModule(obj) { export default function isLocalizeESModule(obj) {
return !!(obj && obj.default && typeof obj.default === 'object' && Object.keys(obj).length === 1); return !!(obj && obj.default && typeof obj.default === 'object' && Object.keys(obj).length === 1);
} }

View file

@ -1,6 +1,7 @@
import { singletonManager } from 'singleton-manager'; import { singletonManager } from 'singleton-manager';
import { LocalizeManager } from './LocalizeManager.js'; import { LocalizeManager } from './LocalizeManager.js';
/** @type {LocalizeManager} */
// eslint-disable-next-line import/no-mutable-exports // eslint-disable-next-line import/no-mutable-exports
export let localize = export let localize =
singletonManager.get('@lion/localize::localize::0.10.x') || singletonManager.get('@lion/localize::localize::0.10.x') ||
@ -9,6 +10,9 @@ export let localize =
fallbackLocale: 'en-GB', fallbackLocale: 'en-GB',
}); });
/**
* @param {LocalizeManager} newLocalize
*/
export function setLocalize(newLocalize) { export function setLocalize(newLocalize) {
localize.teardown(); localize.teardown();
localize = newLocalize; localize = newLocalize;

View file

@ -4,9 +4,8 @@ import { localize } from '../localize.js';
* When number is NaN we should return an empty string or returnIfNaN param * When number is NaN we should return an empty string or returnIfNaN param
* *
* @param {string} returnIfNaN * @param {string} returnIfNaN
* @returns {*} * @returns {string}
*/ */
export function emptyStringWhenNumberNan(returnIfNaN) { export function emptyStringWhenNumberNan(returnIfNaN) {
const stringToReturn = returnIfNaN || localize.formatNumberOptions.returnIfNaN; return returnIfNaN || localize.formatNumberOptions.returnIfNaN;
return stringToReturn;
} }

View file

@ -1,11 +1,13 @@
/** /**
* Add separators when they are not present * Add separators when they are not present
* *
* @param {Array} formattedParts * @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
* @param {FormatNumberPart[]} formattedParts
* @param {string} groupSeparator * @param {string} groupSeparator
* @returns {Array} * @returns {FormatNumberPart[]}
*/ */
export function forceAddGroupSeparators(formattedParts, groupSeparator) { export function forceAddGroupSeparators(formattedParts, groupSeparator) {
/** @type {FormatNumberPart[]} */
let concatArray = []; let concatArray = [];
let firstPart; let firstPart;
let integerPart; let integerPart;
@ -29,24 +31,41 @@ export function forceAddGroupSeparators(formattedParts, groupSeparator) {
numberPart += integerPart[0].value[i]; numberPart += integerPart[0].value[i];
// Create first grouping which is < 3 // Create first grouping which is < 3
if (numberPart.length === mod3 && firstGroup === false) { if (numberPart.length === mod3 && firstGroup === false) {
numberArray.push({ type: 'integer', value: numberPart }); numberArray.push({
type: 'integer',
value: numberPart,
});
if (numberOfDigits > 3) { if (numberOfDigits > 3) {
numberArray.push({ type: 'group', value: groupSeparator }); numberArray.push({
type: 'group',
value: groupSeparator,
});
} }
numberPart = ''; numberPart = '';
firstGroup = true; firstGroup = true;
// Create groupings of 3 // Create groupings of 3
} else if (numberPart.length === 3 && i < numberOfDigits - 1) { } else if (numberPart.length === 3 && i < numberOfDigits - 1) {
numberOfGroups += 1; numberOfGroups += 1;
numberArray.push({ type: 'integer', value: numberPart }); numberArray.push({
type: 'integer',
value: numberPart,
});
if (numberOfGroups !== groups) { if (numberOfGroups !== groups) {
numberArray.push({ type: 'group', value: groupSeparator }); numberArray.push({
type: 'group',
value: groupSeparator,
});
} }
numberPart = ''; numberPart = '';
} }
} }
numberArray.push({ type: 'integer', value: numberPart }); numberArray.push({
concatArray = firstPart.concat(numberArray, formattedParts); type: 'integer',
value: numberPart,
});
if (firstPart) {
concatArray = firstPart.concat(numberArray, formattedParts);
}
} }
return concatArray; return concatArray;
} }

View file

@ -1,8 +1,9 @@
/** /**
* For Dutch and Belgian amounts the currency should be at the end of the string * For Dutch and Belgian amounts the currency should be at the end of the string
* *
* @param {Array} formattedParts * @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
* @returns {Array} * @param {FormatNumberPart[]} formattedParts
* @returns {FormatNumberPart[]}
*/ */
export function forceCurrencyToEnd(formattedParts) { export function forceCurrencyToEnd(formattedParts) {
if (formattedParts[0].type === 'currency') { if (formattedParts[0].type === 'currency') {

View file

@ -1,19 +1,25 @@
export function forceENAUSymbols(formattedParts, options) { /** @type {Object.<string,string>} */
const CURRENCY_CODE_SYMBOL_MAP = {
EUR: '€',
USD: '$',
JPY: '¥',
};
/**
* Change the symbols for locale 'en-AU', due to bug in Chrome
*
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
* @param {FormatNumberPart[]} formattedParts
* @param {Object} [options]
* @param {string} [options.currency]
* @param {string} [options.currencyDisplay]
* @returns {FormatNumberPart[]}
*/
export function forceENAUSymbols(formattedParts, { currency, currencyDisplay } = {}) {
const result = formattedParts; const result = formattedParts;
const numberOfParts = result.length; if (formattedParts.length > 1 && currencyDisplay === 'symbol') {
// Change the symbols for locale 'en-AU', due to bug in Chrome if (Object.keys(CURRENCY_CODE_SYMBOL_MAP).includes(currency)) {
if (numberOfParts > 1 && options && options.currencyDisplay === 'symbol') { result[0].value = CURRENCY_CODE_SYMBOL_MAP[currency];
switch (options.currency) {
case 'EUR':
result[0].value = '€';
break;
case 'USD':
result[0].value = '$';
break;
case 'JPY':
result[0].value = '¥';
break;
/* no default */
} }
result[1].value = ''; result[1].value = '';
} }

View file

@ -1,10 +1,14 @@
import { normalSpaces } from './normalSpaces.js'; import { normalSpaces } from './normalSpaces.js';
/** /**
* @param {Array} formattedParts * Parts with forced "normal" spaces
* @return {Array} parts with forced "normal" spaces *
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
* @param {FormatNumberPart[]} formattedParts
* @returns {FormatNumberPart[]}
*/ */
export function forceNormalSpaces(formattedParts) { export function forceNormalSpaces(formattedParts) {
/** @type {FormatNumberPart[]} */
const result = []; const result = [];
formattedParts.forEach(part => { formattedParts.forEach(part => {
result.push({ result.push({

View file

@ -1,14 +1,20 @@
/** /**
* When in some locales there is no space between currency and amount it is added * When in some locales there is no space between currency and amount it is added
* *
* @param {Array} formattedParts * @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
* @param {Object} options * @param {FormatNumberPart[]} formattedParts
* @returns {*} * @param {Object} [options]
* @param {string} [options.currency]
* @param {string} [options.currencyDisplay]
* @returns {FormatNumberPart[]}
*/ */
export function forceSpaceBetweenCurrencyCodeAndNumber(formattedParts, options) { export function forceSpaceBetweenCurrencyCodeAndNumber(
formattedParts,
{ currency, currencyDisplay } = {},
) {
const numberOfParts = formattedParts.length; const numberOfParts = formattedParts.length;
const literalObject = { type: 'literal', value: ' ' }; const literalObject = { type: 'literal', value: ' ' };
if (numberOfParts > 1 && options && options.currency && options.currencyDisplay === 'code') { if (numberOfParts > 1 && currency && currencyDisplay === 'code') {
if (formattedParts[0].type === 'currency' && formattedParts[1].type !== 'literal') { if (formattedParts[0].type === 'currency' && formattedParts[1].type !== 'literal') {
// currency in front of a number: EUR 1.00 // currency in front of a number: EUR 1.00
formattedParts.splice(1, 0, literalObject); formattedParts.splice(1, 0, literalObject);

View file

@ -1,8 +1,10 @@
/** /**
* @desc Intl uses 0 as group separator for bg-BG locale. * @desc Intl uses 0 as group separator for bg-BG locale.
* This should be a ' ' * This should be a ' '
* @param {{type,value}[]} formattedParts *
* @returns {{type,value}[]} corrected formatted parts * @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
* @param {FormatNumberPart[]} formattedParts
* @returns {FormatNumberPart[]} corrected formatted parts
*/ */
export function forceSpaceInsteadOfZeroForGroup(formattedParts) { export function forceSpaceInsteadOfZeroForGroup(formattedParts) {
return formattedParts.map(p => { return formattedParts.map(p => {

View file

@ -1,7 +1,15 @@
export function forceTryCurrencyCode(formattedParts, options) { /**
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
* @param {FormatNumberPart[]} formattedParts
* @param {Object} [options]
* @param {string} [options.currency]
* @param {string} [options.currencyDisplay]
* @returns {FormatNumberPart[]}
*/
export function forceTryCurrencyCode(formattedParts, { currency, currencyDisplay } = {}) {
const result = formattedParts; const result = formattedParts;
// Chage the currencycode from TRY to TL, for Turkey // Change the currency code from TRY to TL, for Turkey
if (options.currency === 'TRY' && options.currencyDisplay === 'code') { if (currency === 'TRY' && currencyDisplay === 'code') {
if (result[0].value === 'TRY') { if (result[0].value === 'TRY') {
result[0].value = 'TL'; result[0].value = 'TL';
} }

View file

@ -1,13 +1,16 @@
export function forceYenSymbol(formattedParts, options) { /**
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
* @param {FormatNumberPart[]} formattedParts
* @param {Object} [options]
* @param {string} [options.currency]
* @param {string} [options.currencyDisplay]
* @returns {FormatNumberPart[]}
*/
export function forceYenSymbol(formattedParts, { currency, currencyDisplay } = {}) {
const result = formattedParts; const result = formattedParts;
const numberOfParts = result.length; const numberOfParts = result.length;
// Change the symbol from JPY to ¥, due to bug in Chrome // Change the symbol from JPY to ¥, due to bug in Chrome
if ( if (numberOfParts > 1 && currency === 'JPY' && currencyDisplay === 'symbol') {
numberOfParts > 1 &&
options &&
options.currency === 'JPY' &&
options.currencyDisplay === 'symbol'
) {
result[numberOfParts - 1].value = '¥'; result[numberOfParts - 1].value = '¥';
} }
return result; return result;

View file

@ -4,25 +4,41 @@ import { formatNumberToParts } from './formatNumberToParts.js';
/** /**
* Formats a number based on locale and options. It uses Intl for the formatting. * Formats a number based on locale and options. It uses Intl for the formatting.
* *
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
* @param {number} number Number to be formatted * @param {number} number Number to be formatted
* @param {Object} options Intl options are available extended by roundMode * @param {Object} [options] Intl options are available extended by roundMode and returnIfNaN
* @returns {*} Formatted number * @param {string} [options.roundMode]
* @param {string} [options.returnIfNaN]
* @param {string} [options.locale]
* @param {string} [options.localeMatcher]
* @param {string} [options.numberingSystem]
* @param {string} [options.style]
* @param {string} [options.currency]
* @param {string} [options.currencyDisplay]
* @param {boolean}[options.useGrouping]
* @param {number} [options.minimumIntegerDigits]
* @param {number} [options.minimumFractionDigits]
* @param {number} [options.maximumFractionDigits]
* @param {number} [options.minimumSignificantDigits]
* @param {number} [options.maximumSignificantDigits]
* @returns {string}
*/ */
export function formatNumber(number, options) { export function formatNumber(number, options = {}) {
if (number === undefined || number === null) return ''; if (number === undefined || number === null) return '';
const formattedToParts = formatNumberToParts(number, options); const formattedToParts = formatNumberToParts(number, options);
// If number is not a number // If number is not a number
if ( if (
formattedToParts === (options && options.returnIfNaN) || formattedToParts === options.returnIfNaN ||
formattedToParts === localize.formatNumberOptions.returnIfNaN formattedToParts === localize.formatNumberOptions.returnIfNaN
) { ) {
return formattedToParts; return /** @type {string} */ (formattedToParts);
} }
let printNumberOfParts = ''; let printNumberOfParts = '';
// update numberOfParts because there may be some parts added // update numberOfParts because there may be some parts added
const numberOfParts = formattedToParts && formattedToParts.length; const numberOfParts = formattedToParts && formattedToParts.length;
for (let i = 0; i < numberOfParts; i += 1) { for (let i = 0; i < numberOfParts; i += 1) {
printNumberOfParts += formattedToParts[i].value; const part = /** @type {FormatNumberPart} */ (formattedToParts[i]);
printNumberOfParts += part.value;
} }
return printNumberOfParts; return printNumberOfParts;
} }

View file

@ -9,11 +9,26 @@ import { roundNumber } from './roundNumber.js';
/** /**
* Splits a number up in parts for integer, fraction, group, literal, decimal and currency. * Splits a number up in parts for integer, fraction, group, literal, decimal and currency.
* *
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
* @param {number} number Number to split up * @param {number} number Number to split up
* @param {Object} options Intl options are available extended by roundMode * @param {Object} [options] Intl options are available extended by roundMode,returnIfNaN
* @returns {Array} Array with parts * @param {string} [options.roundMode]
* @param {string} [options.returnIfNaN]
* @param {string} [options.locale]
* @param {string} [options.localeMatcher]
* @param {string} [options.numberingSystem]
* @param {string} [options.style]
* @param {string} [options.currency]
* @param {string} [options.currencyDisplay]
* @param {boolean}[options.useGrouping]
* @param {number} [options.minimumIntegerDigits]
* @param {number} [options.minimumFractionDigits]
* @param {number} [options.maximumFractionDigits]
* @param {number} [options.minimumSignificantDigits]
* @param {number} [options.maximumSignificantDigits]
* @returns {string | FormatNumberPart[]} Array with parts or (an empty string or returnIfNaN if not a number)
*/ */
export function formatNumberToParts(number, options) { export function formatNumberToParts(number, options = {}) {
let parsedNumber = typeof number === 'string' ? parseFloat(number) : number; let parsedNumber = typeof number === 'string' ? parseFloat(number) : number;
const computedLocale = getLocale(options && options.locale); const computedLocale = getLocale(options && options.locale);
// when parsedNumber is not a number we should return an empty string or returnIfNaN // when parsedNumber is not a number we should return an empty string or returnIfNaN
@ -25,6 +40,7 @@ export function formatNumberToParts(number, options) {
parsedNumber = roundNumber(number, options.roundMode); parsedNumber = roundNumber(number, options.roundMode);
} }
let formattedParts = []; let formattedParts = [];
const formattedNumber = Intl.NumberFormat(computedLocale, options).format(parsedNumber); const formattedNumber = Intl.NumberFormat(computedLocale, options).format(parsedNumber);
const regexCurrency = /[.,\s0-9]/; const regexCurrency = /[.,\s0-9]/;
const regexMinusSign = /[-]/; // U+002D, Hyphen-Minus, &#45; const regexMinusSign = /[-]/; // U+002D, Hyphen-Minus, &#45;
@ -95,7 +111,7 @@ export function formatNumberToParts(number, options) {
if (numberPart) { if (numberPart) {
formattedParts.push({ type: 'fraction', value: numberPart }); formattedParts.push({ type: 'fraction', value: numberPart });
} }
// If there are no fractions but we reached the end write the numberpart as integer // If there are no fractions but we reached the end write the number part as integer
} else if (i === formattedNumber.length - 1 && numberPart) { } else if (i === formattedNumber.length - 1 && numberPart) {
formattedParts.push({ type: 'integer', value: numberPart }); formattedParts.push({ type: 'integer', value: numberPart });
} }

View file

@ -3,17 +3,18 @@ import { formatNumberToParts } from './formatNumberToParts.js';
/** /**
* Based on number, returns currency name like 'US dollar' * Based on number, returns currency name like 'US dollar'
* *
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
* @param {string} currencyIso iso code like USD * @param {string} currencyIso iso code like USD
* @param {Object} options Intl options are available extended by roundMode * @param {Object} options Intl options are available extended by roundMode
* @returns {string} currency name like 'US dollar' * @returns {string} currency name like 'US dollar'
*/ */
export function getCurrencyName(currencyIso, options) { export function getCurrencyName(currencyIso, options) {
const parts = formatNumberToParts(1, { const parts = /** @type {FormatNumberPart[]} */ (formatNumberToParts(1, {
...options, ...options,
style: 'currency', style: 'currency',
currency: currencyIso, currency: currencyIso,
currencyDisplay: 'name', currencyDisplay: 'name',
}); }));
const currencyName = parts const currencyName = parts
.filter(p => p.type === 'currency') .filter(p => p.type === 'currency')
.map(o => o.value) .map(o => o.value)

View file

@ -4,13 +4,13 @@ import { getLocale } from './getLocale.js';
* To get the decimal separator * To get the decimal separator
* *
* @param {string} locale To override the browser locale * @param {string} locale To override the browser locale
* @returns {Object} the separator * @returns {string} The separator
*/ */
export function getDecimalSeparator(locale) { export function getDecimalSeparator(locale) {
const computedLocale = getLocale(locale); const computedLocale = getLocale(locale);
const formattedNumber = Intl.NumberFormat(computedLocale, { const formattedNumber = Intl.NumberFormat(computedLocale, {
style: 'decimal', style: 'decimal',
minimumFractionDigits: 1, minimumFractionDigits: 1,
}).format('1'); }).format(1);
return formattedNumber[1]; return formattedNumber[1];
} }

View file

@ -4,14 +4,15 @@ import { formatNumberToParts } from './formatNumberToParts.js';
* @example * @example
* getFractionDigits('JOD'); // return 3 * getFractionDigits('JOD'); // return 3
* *
* @param {string} currency Currency code e.g. EUR * @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
* @return {number} fraction for the given currency * @param {string} [currency="EUR"] Currency code e.g. EUR
* @returns {number} fraction for the given currency
*/ */
export function getFractionDigits(currency = 'EUR') { export function getFractionDigits(currency = 'EUR') {
const parts = formatNumberToParts(123, { const parts = /** @type {FormatNumberPart[]} */ (formatNumberToParts(123, {
style: 'currency', style: 'currency',
currency, currency,
}); }));
const [fractionPart] = parts.filter(part => part.type === 'fraction'); const [fractionPart] = parts.filter(part => part.type === 'fraction');
return fractionPart ? fractionPart.value.length : 0; return fractionPart ? fractionPart.value.length : 0;
} }

View file

@ -2,16 +2,16 @@ import { getLocale } from './getLocale.js';
import { normalSpaces } from './normalSpaces.js'; import { normalSpaces } from './normalSpaces.js';
/** /**
* To get the group separator * Gets the group separator
* *
* @param {string} locale To override the browser locale * @param {string} [locale] To override the browser locale
* @returns {Object} the separator * @returns {string}
*/ */
export function getGroupSeparator(locale) { export function getGroupSeparator(locale) {
const computedLocale = getLocale(locale); const computedLocale = getLocale(locale);
const formattedNumber = Intl.NumberFormat(computedLocale, { const formattedNumber = Intl.NumberFormat(computedLocale, {
style: 'decimal', style: 'decimal',
minimumFractionDigits: 0, minimumFractionDigits: 0,
}).format('10000'); }).format(10000);
return normalSpaces(formattedNumber[2]); return normalSpaces(formattedNumber[2]);
} }

View file

@ -3,7 +3,7 @@ import { localize } from '../localize.js';
/** /**
* Gets the locale to use * Gets the locale to use
* *
* @param {string} locale Locale to override browser locale * @param {string} [locale] Locale to override browser locale
* @returns {string} * @returns {string}
*/ */
export function getLocale(locale) { export function getLocale(locale) {

View file

@ -1,6 +1,6 @@
/** /**
* @param {Array} value * @param {string} value
* @return {Array} value with forced "normal" space * @returns {string} value with forced "normal" space
*/ */
export function normalSpaces(value) { export function normalSpaces(value) {
// If non-breaking space (160) or narrow non-breaking space (8239) then return ' ' // If non-breaking space (160) or narrow non-breaking space (8239) then return ' '

View file

@ -1,13 +1,10 @@
/** /**
* Function that fixes currency label with locale options * For Turkey fixes currency label with locale options
* *
* @param {String} currency * @param {string} currency
* @param {string} locale * @param {string} locale
* @returns {string} currency * @returns {string}
*/ */
export function normalizeCurrencyLabel(currency, locale) { export function normalizeCurrencyLabel(currency, locale) {
if (currency === 'TRY' && locale === 'tr-TR') { return currency === 'TRY' && locale === 'tr-TR' ? 'TL' : currency;
return 'TL';
}
return currency;
} }

View file

@ -11,15 +11,19 @@ import { forceENAUSymbols } from './forceENAUSymbols.js';
/** /**
* Function with all fixes on localize * Function with all fixes on localize
* *
* @param {Array} formattedParts * @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
* @param {Object} options * @param {FormatNumberPart[]} formattedParts
* @param {Object} [options]
* @param {string} [options.style]
* @param {string} [options.currency]
* @param {string} [options.currencyDisplay]
* @param {string} _locale * @param {string} _locale
* @returns {*} * @returns {FormatNumberPart[]}
*/ */
export function normalizeIntl(formattedParts, options, _locale) { export function normalizeIntl(formattedParts, options = {}, _locale) {
let normalize = forceNormalSpaces(formattedParts, options); let normalize = forceNormalSpaces(formattedParts);
// Dutch and Belgian currency must be moved to end of number // Dutch and Belgian currency must be moved to end of number
if (options && options.style === 'currency') { if (options.style === 'currency') {
if (options.currencyDisplay === 'code' && _locale.slice(0, 2) === 'nl') { if (options.currencyDisplay === 'code' && _locale.slice(0, 2) === 'nl') {
normalize = forceCurrencyToEnd(normalize); normalize = forceCurrencyToEnd(normalize);
} }

View file

@ -3,7 +3,8 @@
* *
* @param {number} number * @param {number} number
* @param {string} roundMode * @param {string} roundMode
* @returns {*} * @throws {Error} roundMode can only be round|floor|ceiling
* @returns {number}
*/ */
export function roundNumber(number, roundMode) { export function roundNumber(number, roundMode) {
switch (roundMode) { switch (roundMode) {

View file

@ -1,11 +1,22 @@
/**
* @type {Object.<String, Object>}
*/
let fakeImports = {}; let fakeImports = {};
/**
* @param {string} path
* @param {Object} data
*/
export function setupFakeImport(path, data) { export function setupFakeImport(path, data) {
const fakeExports = { ...data }; const fakeExports = { ...data };
Object.defineProperty(fakeExports, '__esModule', { value: true }); Object.defineProperty(fakeExports, '__esModule', { value: true });
fakeImports[path] = fakeExports; fakeImports[path] = fakeExports;
} }
/**
* @param {string[]} namespaces
* @param {string[]} locales
*/
export function setupEmptyFakeImportsFor(namespaces, locales) { export function setupEmptyFakeImportsFor(namespaces, locales) {
namespaces.forEach(namespace => { namespaces.forEach(namespace => {
locales.forEach(locale => { locales.forEach(locale => {
@ -20,6 +31,11 @@ export function resetFakeImport() {
fakeImports = {}; fakeImports = {};
} }
/**
* @param {Object} result
* @param {Function} resolve
* @param {Function} reject
*/
function resolveOrReject(result, resolve, reject) { function resolveOrReject(result, resolve, reject) {
if (result) { if (result) {
resolve(result); resolve(result);
@ -28,6 +44,11 @@ function resolveOrReject(result, resolve, reject) {
} }
} }
/**
* @param {string} path
* @param {number} [ms=0]
* @returns {Promise.<Object>}
*/
export async function fakeImport(path, ms = 0) { export async function fakeImport(path, ms = 0) {
const result = fakeImports[path]; const result = fakeImports[path];
if (ms > 0) { if (ms > 0) {

View file

@ -1,17 +1,20 @@
import { expect, oneEvent, aTimeout } from '@open-wc/testing'; import { expect, oneEvent, aTimeout } from '@open-wc/testing';
import sinon from 'sinon'; import sinon from 'sinon';
// eslint-disable-next-line import/no-unresolved
import { fetchMock } from '@bundled-es-modules/fetch-mock'; import { fetchMock } from '@bundled-es-modules/fetch-mock';
import { setupFakeImport, resetFakeImport, fakeImport } from '../test-helpers.js'; import { setupFakeImport, resetFakeImport, fakeImport } from '../test-helpers.js';
import { LocalizeManager } from '../src/LocalizeManager.js'; import { LocalizeManager } from '../src/LocalizeManager.js';
// useful for IE11 where LTR and RTL symbols are put by Intl when rendering dates /**
* @param {string} str
* Useful for IE11 where LTR and RTL symbols are put by Intl when rendering dates
*/
function removeLtrRtl(str) { function removeLtrRtl(str) {
return str.replace(/(\u200E|\u200E)/g, ''); return str.replace(/(\u200E|\u200E)/g, '');
} }
describe('LocalizeManager', () => { describe('LocalizeManager', () => {
/** @type {LocalizeManager} */
let manager; let manager;
beforeEach(() => { beforeEach(() => {
@ -43,7 +46,10 @@ describe('LocalizeManager', () => {
it('has teardown() method removing all side effects', () => { it('has teardown() method removing all side effects', () => {
manager = new LocalizeManager(); manager = new LocalizeManager();
const disconnectObserverSpy = sinon.spy(manager._htmlLangAttributeObserver, 'disconnect'); const disconnectObserverSpy = sinon.spy(
manager._htmlLangAttributeObserver,
/** @type {never} */ ('disconnect'),
);
manager.teardown(); manager.teardown();
expect(disconnectObserverSpy.callCount).to.equal(1); expect(disconnectObserverSpy.callCount).to.equal(1);
}); });
@ -54,7 +60,11 @@ describe('LocalizeManager', () => {
setTimeout(() => { setTimeout(() => {
manager.locale = 'en-US'; manager.locale = 'en-US';
}); });
const event = await oneEvent(manager, 'localeChanged');
const event = await oneEvent(
/** @type {EventTarget} */ (/** @type {unknown} */ (manager)),
'localeChanged',
);
expect(event.detail.newLocale).to.equal('en-US'); expect(event.detail.newLocale).to.equal('en-US');
expect(event.detail.oldLocale).to.equal('en-GB'); expect(event.detail.oldLocale).to.equal('en-GB');
}); });
@ -62,6 +72,7 @@ describe('LocalizeManager', () => {
it('does not fire "localeChanged" event if it was set to the same locale', () => { it('does not fire "localeChanged" event if it was set to the same locale', () => {
manager = new LocalizeManager(); manager = new LocalizeManager();
const eventSpy = sinon.spy(); const eventSpy = sinon.spy();
manager.addEventListener('localeChanged', eventSpy); manager.addEventListener('localeChanged', eventSpy);
manager.locale = 'en-US'; manager.locale = 'en-US';
manager.locale = 'en-US'; manager.locale = 'en-US';
@ -127,6 +138,7 @@ describe('LocalizeManager', () => {
manager = new LocalizeManager(); manager = new LocalizeManager();
await manager.loadNamespace({ await manager.loadNamespace({
/** @param {string} locale */
'my-component': locale => fakeImport(`./my-component/${locale}.js`), 'my-component': locale => fakeImport(`./my-component/${locale}.js`),
}); });
@ -145,6 +157,7 @@ describe('LocalizeManager', () => {
await manager.loadNamespace( await manager.loadNamespace(
{ {
/** @param {string} locale */
'my-component': locale => fakeImport(`./my-component/${locale}.js`), 'my-component': locale => fakeImport(`./my-component/${locale}.js`),
}, },
{ locale: 'nl-NL' }, { locale: 'nl-NL' },
@ -164,8 +177,14 @@ describe('LocalizeManager', () => {
manager = new LocalizeManager(); manager = new LocalizeManager();
await manager.loadNamespaces([ await manager.loadNamespaces([
{ 'my-defaults': locale => fakeImport(`./my-defaults/${locale}.js`) }, {
{ 'my-send-button': locale => fakeImport(`./my-send-button/${locale}.js`) }, /** @param {string} locale */
'my-defaults': locale => fakeImport(`./my-defaults/${locale}.js`),
},
{
/** @param {string} locale */
'my-send-button': locale => fakeImport(`./my-send-button/${locale}.js`),
},
]); ]);
expect(manager.__storage).to.deep.equal({ expect(manager.__storage).to.deep.equal({
@ -185,8 +204,14 @@ describe('LocalizeManager', () => {
await manager.loadNamespaces( await manager.loadNamespaces(
[ [
{ 'my-defaults': locale => fakeImport(`./my-defaults/${locale}.js`) }, {
{ 'my-send-button': locale => fakeImport(`./my-send-button/${locale}.js`) }, /** @param {string} locale */
'my-defaults': locale => fakeImport(`./my-defaults/${locale}.js`),
},
{
/** @param {string} locale */
'my-send-button': locale => fakeImport(`./my-send-button/${locale}.js`),
},
], ],
{ locale: 'nl-NL' }, { locale: 'nl-NL' },
); );
@ -205,6 +230,7 @@ describe('LocalizeManager', () => {
manager = new LocalizeManager(); manager = new LocalizeManager();
await manager.loadNamespace({ await manager.loadNamespace({
/** @param {string} locale */
'my-component': locale => fakeImport(`./my-component/${locale}.js`), 'my-component': locale => fakeImport(`./my-component/${locale}.js`),
}); });
@ -220,6 +246,7 @@ describe('LocalizeManager', () => {
try { try {
await manager.loadNamespace({ await manager.loadNamespace({
/** @param {string} locale */
'my-component': locale => fakeImport(`./my-component/${locale}.js`), 'my-component': locale => fakeImport(`./my-component/${locale}.js`),
}); });
} catch (e) { } catch (e) {
@ -242,6 +269,7 @@ describe('LocalizeManager', () => {
setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Hello!' } }); setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Hello!' } });
await manager.loadNamespace({ await manager.loadNamespace({
/** @param {string} locale */
'my-component': locale => fakeImport(`./my-component/${locale}.js`), 'my-component': locale => fakeImport(`./my-component/${locale}.js`),
}); });
@ -259,6 +287,7 @@ describe('LocalizeManager', () => {
setupFakeImport('./my-component/en.js', { default: { greeting: 'Hello!' } }); setupFakeImport('./my-component/en.js', { default: { greeting: 'Hello!' } });
await manager.loadNamespace({ await manager.loadNamespace({
/** @param {string} locale */
'my-component': locale => fakeImport(`./my-component/${locale}.js`), 'my-component': locale => fakeImport(`./my-component/${locale}.js`),
}); });
@ -275,6 +304,7 @@ describe('LocalizeManager', () => {
try { try {
await manager.loadNamespace({ await manager.loadNamespace({
/** @param {string} locale */
'my-component': locale => fakeImport(`./my-component/${locale}.js`), 'my-component': locale => fakeImport(`./my-component/${locale}.js`),
}); });
} catch (e) { } catch (e) {
@ -315,10 +345,14 @@ describe('LocalizeManager', () => {
manager = new LocalizeManager(); manager = new LocalizeManager();
manager.setupNamespaceLoader('my-component', async locale => { manager.setupNamespaceLoader(
const response = await fetch(`./my-component/${locale}.json`); 'my-component',
return response.json(); /** @param {string} locale */
}); async locale => {
const response = await fetch(`./my-component/${locale}.json`);
return response.json();
},
);
await manager.loadNamespace('my-component'); await manager.loadNamespace('my-component');
@ -330,26 +364,43 @@ describe('LocalizeManager', () => {
}); });
it('loads multiple namespaces via loadNamespaces() using string routes', async () => { it('loads multiple namespaces via loadNamespaces() using string routes', async () => {
fetchMock.get('./my-defaults/en-GB.json', { submit: 'Submit' }); fetchMock.get('./my-defaults/en-GB.json', {
fetchMock.get('./my-send-button/en-GB.json', { submit: 'Send' }); submit: 'Submit',
});
fetchMock.get('./my-send-button/en-GB.json', {
submit: 'Send',
});
manager = new LocalizeManager(); manager = new LocalizeManager();
manager.setupNamespaceLoader('my-defaults', async locale => { manager.setupNamespaceLoader(
const response = await fetch(`./my-defaults/${locale}.json`); 'my-defaults',
return response.json(); /** @param {string} locale */
}); async locale => {
manager.setupNamespaceLoader('my-send-button', async locale => { const response = await fetch(`./my-defaults/${locale}.json`);
const response = await fetch(`./my-send-button/${locale}.json`); return response.json();
return response.json(); },
}); );
manager.setupNamespaceLoader(
'my-send-button',
/** @param {string} locale */
async locale => {
const response = await fetch(`./my-send-button/${locale}.json`);
return response.json();
},
);
await manager.loadNamespaces(['my-defaults', 'my-send-button']); await manager.loadNamespaces(['my-defaults', 'my-send-button']);
expect(manager.__storage).to.deep.equal({ expect(manager.__storage).to.deep.equal({
'en-GB': { 'en-GB': {
'my-send-button': { submit: 'Send' }, 'my-send-button': {
'my-defaults': { submit: 'Submit' }, submit: 'Send',
},
'my-defaults': {
submit: 'Submit',
},
}, },
}); });
}); });
@ -359,10 +410,17 @@ describe('LocalizeManager', () => {
manager = new LocalizeManager(); manager = new LocalizeManager();
manager.setupNamespaceLoader(/my-.+/, async (locale, namespace) => { manager.setupNamespaceLoader(
const response = await fetch(`./${namespace}/${locale}.json`); /my-.+/,
return response.json(); /**
}); * @param {string} locale
* @param {string} namespace
*/
async (locale, namespace) => {
const response = await fetch(`./${namespace}/${locale}.json`);
return response.json();
},
);
await manager.loadNamespace('my-component'); await manager.loadNamespace('my-component');
@ -379,10 +437,17 @@ describe('LocalizeManager', () => {
manager = new LocalizeManager(); manager = new LocalizeManager();
manager.setupNamespaceLoader(/my-.+/, async (locale, namespace) => { manager.setupNamespaceLoader(
const response = await fetch(`./${namespace}/${locale}.json`); /my-.+/,
return response.json(); /**
}); * @param {string} locale
* @param {string} namespace
*/
async (locale, namespace) => {
const response = await fetch(`./${namespace}/${locale}.json`);
return response.json();
},
);
await manager.loadNamespaces(['my-defaults', 'my-send-button']); await manager.loadNamespaces(['my-defaults', 'my-send-button']);
@ -403,6 +468,7 @@ describe('LocalizeManager', () => {
manager = new LocalizeManager({ autoLoadOnLocaleChange: true }); manager = new LocalizeManager({ autoLoadOnLocaleChange: true });
await manager.loadNamespace({ await manager.loadNamespace({
/** @param {string} locale */
'my-component': locale => fakeImport(`./my-component/${locale}.js`, 25), 'my-component': locale => fakeImport(`./my-component/${locale}.js`, 25),
}); });
@ -426,6 +492,7 @@ describe('LocalizeManager', () => {
manager = new LocalizeManager(); manager = new LocalizeManager();
manager.loadNamespace({ manager.loadNamespace({
/** @param {string} locale */
'my-component': locale => fakeImport(`./my-component/${locale}.js`, 25), 'my-component': locale => fakeImport(`./my-component/${locale}.js`, 25),
}); });
expect(manager.__storage).to.deep.equal({}); expect(manager.__storage).to.deep.equal({});
@ -468,6 +535,7 @@ describe('LocalizeManager', () => {
let called = 0; let called = 0;
await manager.loadNamespace({ await manager.loadNamespace({
/** @param {string} locale */
'my-component': locale => { 'my-component': locale => {
called += 1; called += 1;
return fakeImport(`./my-component/${locale}.js`); return fakeImport(`./my-component/${locale}.js`);
@ -527,7 +595,7 @@ describe('LocalizeManager', () => {
manager = new LocalizeManager(); manager = new LocalizeManager();
manager.addData('en-GB', 'my-ns', { greeting: 'Hi!' }); manager.addData('en-GB', 'my-ns', { greeting: 'Hi!' });
manager.addData('nl-NL', 'my-ns', { greeting: 'Hey!' }); manager.addData('nl-NL', 'my-ns', { greeting: 'Hey!' });
expect(manager.msg('my-ns:greeting', null, { locale: 'nl-NL' })).to.equal('Hey!'); expect(manager.msg('my-ns:greeting', undefined, { locale: 'nl-NL' })).to.equal('Hey!');
manager.reset(); manager.reset();
manager.addData('en-GB', 'my-ns', { greeting: 'Hi {name}!' }); manager.addData('en-GB', 'my-ns', { greeting: 'Hi {name}!' });
manager.addData('nl-NL', 'my-ns', { greeting: 'Hey {name}!' }); manager.addData('nl-NL', 'my-ns', { greeting: 'Hey {name}!' });
@ -561,6 +629,7 @@ describe('When supporting external translation tools like Google Translate', ()
let manager; let manager;
const originalLang = document.documentElement.lang; const originalLang = document.documentElement.lang;
/** @param {string} lang */
async function simulateGoogleTranslateOn(lang) { async function simulateGoogleTranslateOn(lang) {
document.documentElement.lang = lang; document.documentElement.lang = lang;
} }
@ -569,6 +638,10 @@ describe('When supporting external translation tools like Google Translate', ()
document.documentElement.lang = 'auto'; document.documentElement.lang = 'auto';
} }
/**
* @param {...*} [cfg]
* @returns {LocalizeManager}
*/
function getInstance(cfg) { function getInstance(cfg) {
LocalizeManager.resetInstance(); LocalizeManager.resetInstance();
return LocalizeManager.getInstance(cfg || {}); return LocalizeManager.getInstance(cfg || {});
@ -660,6 +733,7 @@ describe('When supporting external translation tools like Google Translate', ()
}); });
describe('[deprecated] When not supporting external translation tools like Google Translate', () => { describe('[deprecated] When not supporting external translation tools like Google Translate', () => {
/** @type {LocalizeManager} */
let manager; let manager;
beforeEach(() => { beforeEach(() => {
@ -677,16 +751,20 @@ describe('[deprecated] When not supporting external translation tools like Googl
}); });
it('initializes locale from <html> by default', () => { it('initializes locale from <html> by default', () => {
manager = new LocalizeManager({ supportExternalTranslationTools: false }); manager = new LocalizeManager({});
expect(manager.locale).to.equal('en-GB'); expect(manager.locale).to.equal('en-GB');
}); });
it('fires "localeChanged" event if locale was changed via <html lang> attribute', async () => { it('fires "localeChanged" event if locale was changed via <html lang> attribute', async () => {
manager = new LocalizeManager({ supportExternalTranslationTools: false }); manager = new LocalizeManager({});
setTimeout(() => { setTimeout(() => {
document.documentElement.lang = 'en-US'; document.documentElement.lang = 'en-US';
}); });
const event = await oneEvent(manager, 'localeChanged');
const event = await oneEvent(
/** @type {EventTarget} */ (/** @type {unknown} */ (manager)),
'localeChanged',
);
expect(event.detail.newLocale).to.equal('en-US'); expect(event.detail.newLocale).to.equal('en-US');
expect(event.detail.oldLocale).to.equal('en-GB'); expect(event.detail.oldLocale).to.equal('en-GB');
}); });
@ -696,11 +774,11 @@ describe('[deprecated] When not supporting external translation tools like Googl
setupFakeImport('./my-component/nl-NL.js', { default: { greeting: 'Hallo!' } }); setupFakeImport('./my-component/nl-NL.js', { default: { greeting: 'Hallo!' } });
manager = new LocalizeManager({ manager = new LocalizeManager({
supportExternalTranslationTools: false,
autoLoadOnLocaleChange: true, autoLoadOnLocaleChange: true,
}); });
await manager.loadNamespace({ await manager.loadNamespace({
/** @param {string} locale */
'my-component': locale => fakeImport(`./my-component/${locale}.js`, 25), 'my-component': locale => fakeImport(`./my-component/${locale}.js`, 25),
}); });
@ -709,7 +787,7 @@ describe('[deprecated] When not supporting external translation tools like Googl
}); });
document.documentElement.lang = 'nl-NL'; document.documentElement.lang = 'nl-NL';
await aTimeout(); // wait for mutation observer to be called await aTimeout(0); // wait for mutation observer to be called
await manager.loadingComplete; await manager.loadingComplete;
expect(manager.__storage).to.deep.equal({ expect(manager.__storage).to.deep.equal({

View file

@ -7,6 +7,7 @@ import {
fixtureSync, fixtureSync,
html, html,
nextFrame, nextFrame,
unsafeStatic,
} from '@open-wc/testing'; } from '@open-wc/testing';
import sinon from 'sinon'; import sinon from 'sinon';
import { localize } from '../src/localize.js'; import { localize } from '../src/localize.js';
@ -19,6 +20,10 @@ import {
setupFakeImport, setupFakeImport,
} from '../test-helpers.js'; } from '../test-helpers.js';
/**
* @typedef {import('../types/LocalizeMixinTypes').LocalizeMixin} LocalizeMixinHost
*/
describe('LocalizeMixin', () => { describe('LocalizeMixin', () => {
afterEach(() => { afterEach(() => {
resetFakeImport(); resetFakeImport();
@ -26,21 +31,26 @@ describe('LocalizeMixin', () => {
}); });
it('loads namespaces defined in "get localizeNamespaces()" when created before attached to DOM', async () => { it('loads namespaces defined in "get localizeNamespaces()" when created before attached to DOM', async () => {
const myElementNs = { 'my-element': locale => fakeImport(`./my-element/${locale}.js`) }; const myElementNs = {
/** @param {string} locale */
'my-element': locale => fakeImport(`./my-element/${locale}.js`),
};
class MyElement extends LocalizeMixin(class {}) { const tagString = defineCE(
static get localizeNamespaces() { // @ts-ignore
return [myElementNs, ...super.localizeNamespaces]; class MyElement extends LocalizeMixin(LitElement) {
} static get localizeNamespaces() {
return [myElementNs, ...super.localizeNamespaces];
requestUpdate() {} }
} },
);
const tag = unsafeStatic(tagString);
setupEmptyFakeImportsFor(['my-element'], ['en-GB']); setupEmptyFakeImportsFor(['my-element'], ['en-GB']);
const loadNamespaceSpy = sinon.spy(localize, 'loadNamespace'); const loadNamespaceSpy = sinon.spy(localize, 'loadNamespace');
await fixture(html`<${tag}></${tag}>`);
new MyElement(); // eslint-disable-line no-new
expect(loadNamespaceSpy.callCount).to.equal(1); expect(loadNamespaceSpy.callCount).to.equal(1);
expect(loadNamespaceSpy.calledWith(myElementNs)).to.be.true; expect(loadNamespaceSpy.calledWith(myElementNs)).to.be.true;
@ -48,27 +58,41 @@ describe('LocalizeMixin', () => {
}); });
it('ignores duplicates in "get localizeNamespaces()" chain', async () => { it('ignores duplicates in "get localizeNamespaces()" chain', async () => {
const defaultNs = { default: loc => fakeImport(`./default/${loc}.js`) }; const defaultNs = {
const parentElementNs = { 'parent-element': loc => fakeImport(`./parent-element/${loc}.js`) }; /** @param {string} loc */
const childElementNs = { 'child-element': loc => fakeImport(`./child-element/${loc}.js`) }; default: loc => fakeImport(`./default/${loc}.js`),
};
const parentElementNs = {
/** @param {string} loc */
'parent-element': loc => fakeImport(`./parent-element/${loc}.js`),
};
const childElementNs = {
/** @param {string} loc */
'child-element': loc => fakeImport(`./child-element/${loc}.js`),
};
class ParentElement extends LocalizeMixin(class {}) { // @ts-ignore
class ParentElement extends LocalizeMixin(LitElement) {
static get localizeNamespaces() { static get localizeNamespaces() {
return [parentElementNs, defaultNs, ...super.localizeNamespaces]; return [parentElementNs, defaultNs, ...super.localizeNamespaces];
} }
} }
class ChildElement extends LocalizeMixin(ParentElement) { const tagString = defineCE(
static get localizeNamespaces() { // @ts-ignore
return [childElementNs, defaultNs, ...super.localizeNamespaces]; class ChildElement extends LocalizeMixin(ParentElement) {
} static get localizeNamespaces() {
} return [childElementNs, defaultNs, ...super.localizeNamespaces];
}
},
);
const tag = unsafeStatic(tagString);
setupEmptyFakeImportsFor(['default', 'parent-element', 'child-element'], ['en-GB']); setupEmptyFakeImportsFor(['default', 'parent-element', 'child-element'], ['en-GB']);
const loadNamespaceSpy = sinon.spy(localize, 'loadNamespace'); const loadNamespaceSpy = sinon.spy(localize, 'loadNamespace');
new ChildElement(); // eslint-disable-line no-new await fixture(html`<${tag}></${tag}>`);
expect(loadNamespaceSpy.callCount).to.equal(3); expect(loadNamespaceSpy.callCount).to.equal(3);
expect(loadNamespaceSpy.calledWith(childElementNs)).to.be.true; expect(loadNamespaceSpy.calledWith(childElementNs)).to.be.true;
expect(loadNamespaceSpy.calledWith(defaultNs)).to.be.true; expect(loadNamespaceSpy.calledWith(defaultNs)).to.be.true;
@ -78,49 +102,56 @@ describe('LocalizeMixin', () => {
}); });
it('calls "onLocaleReady()" after namespaces were loaded for the first time (only if attached to DOM)', async () => { it('calls "onLocaleReady()" after namespaces were loaded for the first time (only if attached to DOM)', async () => {
const myElementNs = { 'my-element': locale => fakeImport(`./my-element/${locale}.js`) }; const myElementNs = {
/** @param {string} locale */
'my-element': locale => fakeImport(`./my-element/${locale}.js`),
};
class MyElement extends LocalizeMixin(class {}) { // @ts-ignore
class MyElement extends LocalizeMixin(LitElement) {
static get localizeNamespaces() { static get localizeNamespaces() {
return [myElementNs, ...super.localizeNamespaces]; return [myElementNs, ...super.localizeNamespaces];
} }
requestUpdate() {}
onLocaleReady() {} onLocaleReady() {}
} }
const tagString = defineCE(MyElement);
setupEmptyFakeImportsFor(['my-element'], ['en-GB']); setupEmptyFakeImportsFor(['my-element'], ['en-GB']);
const element = new MyElement(); const el = /** @type {MyElement} */ (document.createElement(tagString));
const onLocaleReadySpy = sinon.spy(element, 'onLocaleReady'); const wrapper = await fixture('<div></div>');
const onLocaleReadySpy = sinon.spy(el, 'onLocaleReady');
await localize.loadingComplete; await localize.loadingComplete;
expect(onLocaleReadySpy.callCount).to.equal(0); expect(onLocaleReadySpy.callCount).to.equal(0);
element.connectedCallback(); wrapper.appendChild(el);
await localize.loadingComplete; await localize.loadingComplete;
expect(onLocaleReadySpy.callCount).to.equal(1); expect(onLocaleReadySpy.callCount).to.equal(1);
}); });
it('calls "onLocaleChanged(newLocale, oldLocale)" after locale was changed (only if attached to DOM)', async () => { it('calls "onLocaleChanged(newLocale, oldLocale)" after locale was changed (only if attached to DOM)', async () => {
const myElementNs = { 'my-element': locale => fakeImport(`./my-element/${locale}.js`) }; const myElementNs = {
/** @param {string} locale */
'my-element': locale => fakeImport(`./my-element/${locale}.js`),
};
class MyElement extends LocalizeMixin(class {}) { // @ts-ignore
class MyOtherElement extends LocalizeMixin(LitElement) {
static get localizeNamespaces() { static get localizeNamespaces() {
return [myElementNs, ...super.localizeNamespaces]; return [myElementNs, ...super.localizeNamespaces];
} }
requestUpdate() {}
onLocaleChanged() {}
} }
const tagString = defineCE(MyOtherElement);
setupEmptyFakeImportsFor(['my-element'], ['en-GB', 'nl-NL', 'ru-RU']); setupEmptyFakeImportsFor(['my-element'], ['en-GB', 'nl-NL', 'ru-RU']);
const element = new MyElement(); const el = /** @type {MyOtherElement} */ (document.createElement(tagString));
const onLocaleChangedSpy = sinon.spy(element, 'onLocaleChanged'); const wrapper = await fixture('<div></div>');
const onLocaleChangedSpy = sinon.spy(el, 'onLocaleChanged');
await localize.loadingComplete; await localize.loadingComplete;
@ -128,33 +159,40 @@ describe('LocalizeMixin', () => {
await localize.loadingComplete; await localize.loadingComplete;
expect(onLocaleChangedSpy.callCount).to.equal(0); expect(onLocaleChangedSpy.callCount).to.equal(0);
element.connectedCallback(); wrapper.appendChild(el);
localize.locale = 'ru-RU'; localize.locale = 'ru-RU';
await localize.loadingComplete; await localize.loadingComplete;
expect(onLocaleChangedSpy.callCount).to.equal(1); expect(onLocaleChangedSpy.callCount).to.equal(1);
expect(onLocaleChangedSpy.calledWith('ru-RU', 'nl-NL')).to.be.true; // FIXME: Expected 0 arguments, but got 2. ts(2554) --> not sure why this sinon type is not working
// @ts-ignore
expect(onLocaleChangedSpy.calledWithExactly('ru-RU', 'nl-NL')).to.be.true;
}); });
it('calls "onLocaleUpdated()" after both "onLocaleReady()" and "onLocaleChanged()"', async () => { it('calls "onLocaleUpdated()" after both "onLocaleReady()" and "onLocaleChanged()"', async () => {
const myElementNs = { 'my-element': locale => fakeImport(`./my-element/${locale}.js`) }; const myElementNs = {
/** @param {string} locale */
'my-element': locale => fakeImport(`./my-element/${locale}.js`),
};
class MyElement extends LocalizeMixin(class {}) { // @ts-ignore
class MyElement extends LocalizeMixin(LitElement) {
static get localizeNamespaces() { static get localizeNamespaces() {
return [myElementNs, ...super.localizeNamespaces]; return [myElementNs, ...super.localizeNamespaces];
} }
requestUpdate() {}
onLocaleUpdated() {} onLocaleUpdated() {}
} }
const tagString = defineCE(MyElement);
setupEmptyFakeImportsFor(['my-element'], ['en-GB', 'nl-NL']); setupEmptyFakeImportsFor(['my-element'], ['en-GB', 'nl-NL']);
const el = new MyElement(); const el = /** @type {MyElement} */ (document.createElement(tagString));
const wrapper = await fixture('<div></div>');
const onLocaleUpdatedSpy = sinon.spy(el, 'onLocaleUpdated'); const onLocaleUpdatedSpy = sinon.spy(el, 'onLocaleUpdated');
el.connectedCallback(); wrapper.appendChild(el);
await el.localizeNamespacesLoaded; await el.localizeNamespacesLoaded;
expect(onLocaleUpdatedSpy.callCount).to.equal(1); expect(onLocaleUpdatedSpy.callCount).to.equal(1);
@ -162,8 +200,11 @@ describe('LocalizeMixin', () => {
expect(onLocaleUpdatedSpy.callCount).to.equal(2); expect(onLocaleUpdatedSpy.callCount).to.equal(2);
}); });
it('should have the localizeNamespacesLoaded avaliable within "onLocaleUpdated()"', async () => { it('should have the localizeNamespacesLoaded available within "onLocaleUpdated()"', async () => {
const myElementNs = { 'my-element': locale => fakeImport(`./my-element/${locale}.js`) }; const myElementNs = {
/** @param {string} locale */
'my-element': locale => fakeImport(`./my-element/${locale}.js`),
};
setupFakeImport('./my-element/en-GB.js', { setupFakeImport('./my-element/en-GB.js', {
default: { default: {
label: 'one', label: 'one',
@ -174,13 +215,13 @@ describe('LocalizeMixin', () => {
label: 'two', label: 'two',
}, },
}); });
class MyElement extends LocalizeMixin(class {}) {
// @ts-ignore
class MyElement extends LocalizeMixin(LitElement) {
static get localizeNamespaces() { static get localizeNamespaces() {
return [myElementNs, ...super.localizeNamespaces]; return [myElementNs, ...super.localizeNamespaces];
} }
requestUpdate() {}
async onLocaleUpdated() { async onLocaleUpdated() {
super.onLocaleUpdated(); super.onLocaleUpdated();
await this.localizeNamespacesLoaded; await this.localizeNamespacesLoaded;
@ -188,7 +229,8 @@ describe('LocalizeMixin', () => {
} }
} }
const el = new MyElement(); const tagString = defineCE(MyElement);
const el = /** @type {MyElement} */ (document.createElement(tagString));
el.connectedCallback(); el.connectedCallback();
await el.localizeNamespacesLoaded; await el.localizeNamespacesLoaded;
@ -201,19 +243,22 @@ describe('LocalizeMixin', () => {
}); });
it('calls "requestUpdate()" after locale was changed', async () => { it('calls "requestUpdate()" after locale was changed', async () => {
const myElementNs = { 'my-element': locale => fakeImport(`./my-element/${locale}.js`) }; const myElementNs = {
/** @param {string} locale */
'my-element': locale => fakeImport(`./my-element/${locale}.js`),
};
class MyElement extends LocalizeMixin(class {}) { // @ts-ignore
class MyElement extends LocalizeMixin(LitElement) {
static get localizeNamespaces() { static get localizeNamespaces() {
return [myElementNs, ...super.localizeNamespaces]; return [myElementNs, ...super.localizeNamespaces];
} }
requestUpdate() {}
} }
setupEmptyFakeImportsFor(['my-element'], ['en-GB']); setupEmptyFakeImportsFor(['my-element'], ['en-GB']);
const el = new MyElement(); const tagString = defineCE(MyElement);
const el = /** @type {MyElement} */ (document.createElement(tagString));
const updateSpy = sinon.spy(el, 'requestUpdate'); const updateSpy = sinon.spy(el, 'requestUpdate');
el.connectedCallback(); el.connectedCallback();
@ -224,23 +269,29 @@ describe('LocalizeMixin', () => {
}); });
it('has msgLit() which integrates with lit-html', async () => { it('has msgLit() which integrates with lit-html', async () => {
const myElementNs = { 'my-element': locale => fakeImport(`./my-element/${locale}.js`) }; const myElementNs = {
/** @param {string} locale */
'my-element': locale => fakeImport(`./my-element/${locale}.js`),
};
setupFakeImport('./my-element/en-GB.js', { setupFakeImport('./my-element/en-GB.js', {
default: { default: {
greeting: 'Hi!', greeting: 'Hi!',
}, },
}); });
class MyElement extends LocalizeMixin(class {}) { // @ts-ignore
class MyElement extends LocalizeMixin(LitElement) {
static get localizeNamespaces() { static get localizeNamespaces() {
return [myElementNs, ...super.localizeNamespaces]; return [myElementNs, ...super.localizeNamespaces];
} }
} }
const element = new MyElement(); const tagString = defineCE(MyElement);
const el = /** @type {MyElement} */ (document.createElement(tagString));
el.connectedCallback();
const lionLocalizeMessageSpy = sinon.spy(localize, 'msg'); const lionLocalizeMessageSpy = sinon.spy(localize, 'msg');
const messageDirective = element.msgLit('my-element:greeting'); const messageDirective = el.msgLit('my-element:greeting');
expect(lionLocalizeMessageSpy.callCount).to.equal(0); expect(lionLocalizeMessageSpy.callCount).to.equal(0);
expect(isDirective(messageDirective)).to.be.true; expect(isDirective(messageDirective)).to.be.true;
@ -250,7 +301,7 @@ describe('LocalizeMixin', () => {
expect(lionLocalizeMessageSpy.callCount).to.equal(1); expect(lionLocalizeMessageSpy.callCount).to.equal(1);
expect(lionLocalizeMessageSpy.calledWith('my-element:greeting')).to.be.true; expect(lionLocalizeMessageSpy.calledWith('my-element:greeting')).to.be.true;
const message = element.msgLit('my-element:greeting'); const message = el.msgLit('my-element:greeting');
expect(message).to.equal('Hi!'); expect(message).to.equal('Hi!');
expect(typeof message).to.equal('string'); expect(typeof message).to.equal('string');
expect(lionLocalizeMessageSpy.callCount).to.equal(2); expect(lionLocalizeMessageSpy.callCount).to.equal(2);
@ -259,22 +310,25 @@ describe('LocalizeMixin', () => {
lionLocalizeMessageSpy.restore(); lionLocalizeMessageSpy.restore();
}); });
it('has a Promise "localizeNamespacesLoaded" which resolves once tranlations are available', async () => { it('has a Promise "localizeNamespacesLoaded" which resolves once translations are available', async () => {
const myElementNs = { 'my-element': locale => fakeImport(`./my-element/${locale}.js`, 25) }; const myElementNs = {
/** @param {string} locale */
'my-element': locale => fakeImport(`./my-element/${locale}.js`, 25),
};
setupFakeImport('./my-element/en-GB.js', { setupFakeImport('./my-element/en-GB.js', {
default: { default: {
greeting: 'Hi!', greeting: 'Hi!',
}, },
}); });
class MyElement extends LocalizeMixin(class {}) { // @ts-ignore
class MyElement extends LocalizeMixin(LitElement) {
static get localizeNamespaces() { static get localizeNamespaces() {
return [myElementNs, ...super.localizeNamespaces]; return [myElementNs, ...super.localizeNamespaces];
} }
requestUpdate() {}
} }
const el = new MyElement(); const tagString = defineCE(MyElement);
const el = /** @type {MyElement} */ (document.createElement(tagString));
const messageDirective = el.msgLit('my-element:greeting'); const messageDirective = el.msgLit('my-element:greeting');
expect(isDirective(messageDirective)).to.be.true; expect(isDirective(messageDirective)).to.be.true;
@ -284,33 +338,46 @@ describe('LocalizeMixin', () => {
}); });
it('renders only once all translations have been loaded (if BaseElement supports it)', async () => { it('renders only once all translations have been loaded (if BaseElement supports it)', async () => {
const myElementNs = { 'my-element': locale => fakeImport(`./my-element/${locale}.js`, 25) }; const myElementNs = {
/** @param {string} locale */
'my-element': locale => fakeImport(`./my-element/${locale}.js`, 25),
};
setupFakeImport('./my-element/en-GB.js', { setupFakeImport('./my-element/en-GB.js', {
default: { default: {
greeting: 'Hi!', greeting: 'Hi!',
}, },
}); });
const tag = defineCE( // @ts-ignore
class extends LocalizeMixin(LitElement) { class MyLocalizedClass extends LocalizeMixin(LitElement) {
static get localizeNamespaces() { static get localizeNamespaces() {
return [myElementNs, ...super.localizeNamespaces]; return [myElementNs, ...super.localizeNamespaces];
} }
render() { render() {
return html`<p>${this.msgLit('my-element:greeting')}</p>`; return html`<p>${this.msgLit('my-element:greeting')}</p>`;
} }
}, }
);
const el = await fixtureSync(`<${tag}></${tag}>`); const tag = defineCE(MyLocalizedClass);
expect(el.shadowRoot.children.length).to.equal(0); const el = /** @type {MyLocalizedClass} */ (await fixtureSync(`<${tag}></${tag}>`));
await el.updateComplete; expect(el.shadowRoot).to.exist;
expect(el.shadowRoot.querySelector('p').innerText).to.equal('Hi!'); if (el.shadowRoot) {
expect(el.shadowRoot.children.length).to.equal(0);
await el.updateComplete;
const pTag = el.shadowRoot.querySelector('p');
expect(pTag).to.exist;
if (pTag) {
expect(pTag.innerText).to.equal('Hi!');
}
}
}); });
it('rerender on locale change once all translations are loaded (if BaseElement supports it)', async () => { it('re-render on locale change once all translations are loaded (if BaseElement supports it)', async () => {
const myElementNs = { 'my-element': locale => fakeImport(`./my-element/${locale}.js`, 25) }; const myElementNs = {
/** @param {string} locale */
'my-element': locale => fakeImport(`./my-element/${locale}.js`, 25),
};
setupFakeImport('./my-element/en-GB.js', { setupFakeImport('./my-element/en-GB.js', {
default: { default: {
greeting: 'Hi!', greeting: 'Hi!',
@ -322,57 +389,69 @@ describe('LocalizeMixin', () => {
}, },
}); });
const tag = defineCE( // @ts-ignore
class TestPromise extends LocalizeMixin(LitElement) { class MyLocalizedClass extends LocalizeMixin(LitElement) {
static get localizeNamespaces() { static get localizeNamespaces() {
return [myElementNs, ...super.localizeNamespaces]; return [myElementNs, ...super.localizeNamespaces];
} }
render() { render() {
return html`<p>${this.msgLit('my-element:greeting')}</p>`; return html`<p>${this.msgLit('my-element:greeting')}</p>`;
} }
}, }
);
const el = await fixture(`<${tag}></${tag}>`); const tag = defineCE(MyLocalizedClass);
const el = /** @type {MyLocalizedClass} */ (await fixture(`<${tag}></${tag}>`));
await el.updateComplete; await el.updateComplete;
expect(el.shadowRoot.querySelector('p').innerText).to.equal('Hi!');
localize.locale = 'en-US'; expect(el.shadowRoot).to.exist;
expect(el.shadowRoot.querySelector('p').innerText).to.equal('Hi!'); if (el.shadowRoot) {
await el.updateComplete; const p = /** @type {HTMLParagraphElement} */ (el.shadowRoot.querySelector('p'));
expect(el.shadowRoot.querySelector('p').innerText).to.equal('Howdy!'); expect(p.innerText).to.equal('Hi!');
localize.locale = 'en-US';
expect(p.innerText).to.equal('Hi!');
await el.updateComplete;
expect(p.innerText).to.equal('Howdy!');
}
}); });
it('it can still render async by setting "static get waitForLocalizeNamespaces() { return false; }" (if BaseElement supports it)', async () => { it('it can still render async by setting "static get waitForLocalizeNamespaces() { return false; }" (if BaseElement supports it)', async () => {
const myElementNs = { 'my-element': locale => fakeImport(`./my-element/${locale}.js`, 50) }; const myElementNs = {
/** @param {string} locale */
'my-element': locale => fakeImport(`./my-element/${locale}.js`, 50),
};
setupFakeImport('./my-element/en-GB.js', { setupFakeImport('./my-element/en-GB.js', {
default: { default: {
greeting: 'Hi!', greeting: 'Hi!',
}, },
}); });
const tag = defineCE( // @ts-ignore
class extends LocalizeMixin(LitElement) { class MyLocalizedClass extends LocalizeMixin(LitElement) {
static get waitForLocalizeNamespaces() { static get waitForLocalizeNamespaces() {
return false; return false;
} }
static get localizeNamespaces() { static get localizeNamespaces() {
return [myElementNs, ...super.localizeNamespaces]; return [myElementNs, ...super.localizeNamespaces];
} }
render() { render() {
return html`<p>${this.msgLit('my-element:greeting')}</p>`; return html`<p>${this.msgLit('my-element:greeting')}</p>`;
} }
}, }
);
const el = await fixture(`<${tag}></${tag}>`); const tag = defineCE(MyLocalizedClass);
const el = /** @type {MyLocalizedClass} */ (await fixture(`<${tag}></${tag}>`));
await el.updateComplete; await el.updateComplete;
expect(el.shadowRoot.querySelector('p').innerText).to.equal(''); expect(el.shadowRoot).to.exist;
await el.localizeNamespacesLoaded; if (el.shadowRoot) {
await el.updateComplete; const p = /** @type {HTMLParagraphElement} */ (el.shadowRoot.querySelector('p'));
expect(el.shadowRoot.querySelector('p').innerText).to.equal('Hi!'); expect(p.innerText).to.equal('');
await el.localizeNamespacesLoaded;
await el.updateComplete;
expect(p.innerText).to.equal('Hi!');
}
}); });
}); });

View file

@ -57,10 +57,10 @@ describe('formatDate', () => {
locale: 'en-US', locale: 'en-US',
}; };
localize.locale = 'hu-HU'; localize.locale = 'hu-HU';
let date = parseDate('2018-5-28'); let date = /** @type {Date} */ (parseDate('2018-5-28'));
expect(formatDate(date)).to.equal('2018. 05. 28.'); expect(formatDate(date)).to.equal('2018. 05. 28.');
date = parseDate('1970-11-3'); date = /** @type {Date} */ (parseDate('1970-11-3'));
expect(formatDate(date, options)).to.equal('Tuesday, November 03, 1970'); expect(formatDate(date, options)).to.equal('Tuesday, November 03, 1970');
}); });
@ -73,13 +73,13 @@ describe('formatDate', () => {
locale: 'en-US', locale: 'en-US',
}; };
localize.locale = 'bg-BG'; localize.locale = 'bg-BG';
let date = parseDate('29-12-2017'); let date = /** @type {Date} */ (parseDate('29-12-2017'));
expect(formatDate(date)).to.equal('29.12.2017 г.'); expect(formatDate(date)).to.equal('29.12.2017 г.');
date = parseDate('13-1-1940'); date = /** @type {Date} */ (parseDate('13-1-1940'));
expect(formatDate(date)).to.equal('13.01.1940 г.'); expect(formatDate(date)).to.equal('13.01.1940 г.');
date = parseDate('3-11-1970'); date = /** @type {Date} */ (parseDate('3-11-1970'));
expect(formatDate(date, options)).to.equal('Tuesday, November 03, 1970'); expect(formatDate(date, options)).to.equal('Tuesday, November 03, 1970');
}); });
@ -92,13 +92,13 @@ describe('formatDate', () => {
locale: 'en-US', locale: 'en-US',
}; };
localize.locale = 'en-US'; localize.locale = 'en-US';
let date = parseDate('12-29-1940'); let date = /** @type {Date} */ (parseDate('12-29-1940'));
expect(formatDate(date)).to.equal('12/29/1940'); expect(formatDate(date)).to.equal('12/29/1940');
date = parseDate('1-13-1940'); date = /** @type {Date} */ (parseDate('1-13-1940'));
expect(formatDate(date)).to.equal('01/13/1940'); expect(formatDate(date)).to.equal('01/13/1940');
date = parseDate('11-3-1970'); date = /** @type {Date} */ (parseDate('11-3-1970'));
expect(formatDate(date, options)).to.equal('Tuesday, November 03, 1970'); expect(formatDate(date, options)).to.equal('Tuesday, November 03, 1970');
}); });
@ -110,10 +110,10 @@ describe('formatDate', () => {
day: '2-digit', day: '2-digit',
locale: 'en-US', locale: 'en-US',
}; };
let parsedDate = parseDate('05.11.2017'); let parsedDate = /** @type {Date} */ (parseDate('05.11.2017'));
expect(formatDate(parsedDate, options)).to.equal('Sunday, November 05, 2017'); expect(formatDate(parsedDate, options)).to.equal('Sunday, November 05, 2017');
parsedDate = parseDate('01-01-1940'); parsedDate = /** @type {Date} */ (parseDate('01-01-1940'));
options = { options = {
weekday: 'long', weekday: 'long',
year: 'numeric', year: 'numeric',
@ -130,7 +130,7 @@ describe('formatDate', () => {
month: 'long', month: 'long',
day: '2-digit', day: '2-digit',
}; };
const parsedDate = parseDate('12.10.2019'); const parsedDate = /** @type {Date} */ (parseDate('12.10.2019'));
expect(formatDate(parsedDate, options)).to.equal('Saturday, 12 October'); expect(formatDate(parsedDate, options)).to.equal('Saturday, 12 October');
}); });
@ -140,7 +140,7 @@ describe('formatDate', () => {
year: 'numeric', year: 'numeric',
day: '2-digit', day: '2-digit',
}; };
const parsedDate = parseDate('12.10.2019'); const parsedDate = /** @type {Date} */ (parseDate('12.10.2019'));
expect(formatDate(parsedDate, options)).to.equal('Saturday 12 2019'); expect(formatDate(parsedDate, options)).to.equal('Saturday 12 2019');
}); });
@ -150,12 +150,13 @@ describe('formatDate', () => {
year: 'numeric', year: 'numeric',
month: 'long', month: 'long',
}; };
const parsedDate = parseDate('12.10.2019'); const parsedDate = /** @type {Date} */ (parseDate('12.10.2019'));
expect(formatDate(parsedDate, options)).to.equal('October 2019 Saturday'); expect(formatDate(parsedDate, options)).to.equal('October 2019 Saturday');
}); });
it('returns empty string when input is not a Date object', async () => { it('returns empty string when input is not a Date object', async () => {
const date = '1-1-2016'; const date = '1-1-2016';
// @ts-ignore tests what happens if you use a wrong type
expect(formatDate(date)).to.equal(''); expect(formatDate(date)).to.equal('');
}); });
}); });

View file

@ -2,6 +2,9 @@ import { expect } from '@open-wc/testing';
import { getMonthNames } from '../../src/date/getMonthNames.js'; import { getMonthNames } from '../../src/date/getMonthNames.js';
/**
* @param {TemplateStringsArray} strings
*/
function s(strings) { function s(strings) {
return strings[0].split(' '); return strings[0].split(' ');
} }

View file

@ -2,6 +2,9 @@ import { expect } from '@open-wc/testing';
import { getWeekdayNames } from '../../src/date/getWeekdayNames.js'; import { getWeekdayNames } from '../../src/date/getWeekdayNames.js';
/**
* @param {TemplateStringsArray} strings
*/
function s(strings) { function s(strings) {
return strings[0].split(' '); return strings[0].split(' ');
} }

View file

@ -3,9 +3,15 @@ import { localizeTearDown } from '../../test-helpers.js';
import { parseDate } from '../../src/date/parseDate.js'; import { parseDate } from '../../src/date/parseDate.js';
/**
*
* @param {Date | undefined} value
* @param {Date} date
*/
function equalsDate(value, date) { function equalsDate(value, date) {
return ( return (
Object.prototype.toString.call(value) === '[object Date]' && // is Date Object Object.prototype.toString.call(value) === '[object Date]' && // is Date Object
value &&
value.getDate() === date.getDate() && // day value.getDate() === date.getDate() && // day
value.getMonth() === date.getMonth() && // month value.getMonth() === date.getMonth() && // month
value.getFullYear() === date.getFullYear() // year value.getFullYear() === date.getFullYear() // year

View file

@ -27,6 +27,7 @@ describe('isLocalizeESModule', () => {
}); });
it('ignores if not an object', () => { it('ignores if not an object', () => {
// @ts-ignore passing a non-object is not allowed by ts, but we still want to test the outcome
expect(isLocalizeESModule(undefined)).to.equal(false); expect(isLocalizeESModule(undefined)).to.equal(false);
}); });
}); });

View file

@ -7,7 +7,7 @@ import { LocalizeManager } from '../src/LocalizeManager.js';
import { localize, setLocalize } from '../src/localize.js'; import { localize, setLocalize } from '../src/localize.js';
describe('localize', () => { describe('localize', () => {
// this is an importan mindset: // this is an important mindset:
// we don't test the singleton // we don't test the singleton
// we check that it is an instance of the right class // we check that it is an instance of the right class
// we test newly created instances of this class separately // we test newly created instances of this class separately
@ -26,9 +26,11 @@ describe('localize', () => {
const oldLocalizeTeardown = localize.teardown; const oldLocalizeTeardown = localize.teardown;
localize.teardown = sinon.spy(); localize.teardown = sinon.spy();
const newLocalize = { teardown: () => {} }; const newLocalize = /** @type {LocalizeManager} */ ({ teardown: () => {} });
setLocalize(newLocalize); setLocalize(newLocalize);
expect(localize).to.equal(newLocalize); expect(localize).to.equal(newLocalize);
// @ts-ignore since we're testing another reference to the same global instance
expect(oldLocalize.teardown.callCount).to.equal(1); expect(oldLocalize.teardown.callCount).to.equal(1);
setLocalize(oldLocalize); setLocalize(oldLocalize);

View file

@ -3,14 +3,22 @@ import { localize } from '../../src/localize.js';
import { formatNumber } from '../../src/number/formatNumber.js'; import { formatNumber } from '../../src/number/formatNumber.js';
import { localizeTearDown } from '../../test-helpers.js'; import { localizeTearDown } from '../../test-helpers.js';
const currencyCode = currency => ({ style: 'currency', currencyDisplay: 'code', currency }); const currencyCode = /** @param {string} currency */ currency => ({
const currencySymbol = currency => ({ style: 'currency', currencyDisplay: 'symbol', currency }); style: 'currency',
currencyDisplay: 'code',
currency,
});
const currencySymbol = /** @param {string} currency */ currency => ({
style: 'currency',
currencyDisplay: 'symbol',
currency,
});
describe('formatNumber', () => { describe('formatNumber', () => {
afterEach(localizeTearDown); afterEach(localizeTearDown);
it('displays the appropriate amount of decimal places based on currencies spec http://www.currency-iso.org/en/home/tables/table-a1.html', () => { it('displays the appropriate amount of decimal places based on currencies spec http://www.currency-iso.org/en/home/tables/table-a1.html', () => {
const clean = str => str.replace(/[a-zA-Z]+/g, '').trim(); const clean = /** @param {string} str */ str => str.replace(/[a-zA-Z]+/g, '').trim();
expect(clean(formatNumber(123456.789, currencyCode('JPY')))).to.equal('123,457'); expect(clean(formatNumber(123456.789, currencyCode('JPY')))).to.equal('123,457');
expect(clean(formatNumber(123456.789, currencyCode('EUR')))).to.equal('123,456.79'); expect(clean(formatNumber(123456.789, currencyCode('EUR')))).to.equal('123,456.79');
expect(clean(formatNumber(123456.789, currencyCode('BHD')))).to.equal('123,456.789'); expect(clean(formatNumber(123456.789, currencyCode('BHD')))).to.equal('123,456.789');
@ -54,11 +62,10 @@ describe('formatNumber', () => {
expect(formatNumber(-12.6, { roundMode: 'floor' })).to.equal('13'); expect(formatNumber(-12.6, { roundMode: 'floor' })).to.equal('13');
}); });
it('returns empty string when NaN', () => { it('returns empty string when passing wrong type', () => {
// @ts-ignore tests what happens if you pass wrong type
expect(formatNumber('foo')).to.equal(''); expect(formatNumber('foo')).to.equal('');
}); // @ts-ignore tests what happens if you pass wrong type
it('returns empty string when number is undefined', () => {
expect(formatNumber(undefined)).to.equal(''); expect(formatNumber(undefined)).to.equal('');
}); });
@ -66,12 +73,14 @@ describe('formatNumber', () => {
const savedReturnIfNaN = localize.formatNumberOptions.returnIfNaN; const savedReturnIfNaN = localize.formatNumberOptions.returnIfNaN;
localize.formatNumberOptions.returnIfNaN = '-'; localize.formatNumberOptions.returnIfNaN = '-';
// @ts-ignore
expect(formatNumber('foo')).to.equal('-'); expect(formatNumber('foo')).to.equal('-');
localize.formatNumberOptions.returnIfNaN = savedReturnIfNaN; localize.formatNumberOptions.returnIfNaN = savedReturnIfNaN;
}); });
it("can set what to returns when NaN via `returnIfNaN: 'foo'`", () => { it("can set what to returns when NaN via `returnIfNaN: 'foo'`", () => {
// @ts-ignore
expect(formatNumber('foo', { returnIfNaN: '-' })).to.equal('-'); expect(formatNumber('foo', { returnIfNaN: '-' })).to.equal('-');
}); });

View file

@ -1,18 +1,26 @@
import { expect } from '@open-wc/testing'; import { expect } from '@open-wc/testing';
import { localize } from '../../src/localize.js'; import { localize } from '../../src/localize.js';
import { localizeTearDown } from '../../test-helpers.js'; import { localizeTearDown } from '../../test-helpers.js';
import { formatNumberToParts } from '../../src/number/formatNumberToParts.js'; import { formatNumberToParts } from '../../src/number/formatNumberToParts.js';
const c = v => ({ type: 'currency', value: v }); const c = /** @param {string} v */ v => ({
const d = v => ({ type: 'decimal', value: v }); type: 'currency',
const i = v => ({ type: 'integer', value: v }); value: v,
const f = v => ({ type: 'fraction', value: v }); });
const g = v => ({ type: 'group', value: v }); const d = /** @param {string} v */ v => ({ type: 'decimal', value: v });
const l = v => ({ type: 'literal', value: v }); const i = /** @param {string} v */ v => ({ type: 'integer', value: v });
const f = /** @param {string} v */ v => ({ type: 'fraction', value: v });
const g = /** @param {string} v */ v => ({ type: 'group', value: v });
const l = /** @param {string} v */ v => ({ type: 'literal', value: v });
const m = { type: 'minusSign', value: '' }; const m = { type: 'minusSign', value: '' };
const stringifyParts = parts => parts.map(part => part.value).join(''); const stringifyParts =
/**
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
* @param {FormatNumberPart[]} parts
* @returns {string}
*/
parts => parts.map(part => part.value).join('');
describe('formatNumberToParts', () => { describe('formatNumberToParts', () => {
afterEach(localizeTearDown); afterEach(localizeTearDown);
@ -32,12 +40,14 @@ describe('formatNumberToParts', () => {
]; ];
specs.forEach(([locale, currency, amount, expectedResult]) => { specs.forEach(([locale, currency, amount, expectedResult]) => {
it(`formats ${locale} ${currency} ${amount} as "${stringifyParts(expectedResult)}"`, () => { it(`formats ${locale} ${currency} ${amount} as "${stringifyParts(
/** @type {FormatNumberPart[]} */ (expectedResult),
)}"`, () => {
expect( expect(
formatNumberToParts(amount, { formatNumberToParts(Number(amount), {
locale,
style: 'currency', style: 'currency',
currency, locale: String(locale),
currency: String(currency),
}), }),
).to.deep.equal(expectedResult); ).to.deep.equal(expectedResult);
}); });
@ -59,13 +69,15 @@ describe('formatNumberToParts', () => {
]; ];
specs.forEach(([locale, currency, amount, expectedResult]) => { specs.forEach(([locale, currency, amount, expectedResult]) => {
it(`formats ${locale} ${currency} ${amount} as "${stringifyParts(expectedResult)}"`, () => { it(`formats ${locale} ${currency} ${amount} as "${stringifyParts(
/** @type {FormatNumberPart[]} */ (expectedResult),
)}"`, () => {
expect( expect(
formatNumberToParts(amount, { formatNumberToParts(Number(amount), {
locale,
style: 'currency', style: 'currency',
currencyDisplay: 'code', currencyDisplay: 'code',
currency, locale: String(locale),
currency: String(currency),
}), }),
).to.deep.equal(expectedResult); ).to.deep.equal(expectedResult);
}); });
@ -74,6 +86,7 @@ describe('formatNumberToParts', () => {
describe("style: 'decimal'", () => { describe("style: 'decimal'", () => {
describe('no minimumFractionDigits', () => { describe('no minimumFractionDigits', () => {
/** @type {Array.<Array.<?>>} */
const specs = [ const specs = [
['en-GB', 3500, [i('3'), g(','), i('500')]], ['en-GB', 3500, [i('3'), g(','), i('500')]],
['en-GB', -3500, [m, i('3'), g(','), i('500')]], ['en-GB', -3500, [m, i('3'), g(','), i('500')]],
@ -88,10 +101,12 @@ describe('formatNumberToParts', () => {
]; ];
specs.forEach(([locale, amount, expectedResult]) => { specs.forEach(([locale, amount, expectedResult]) => {
it(`formats ${locale} ${amount} as "${stringifyParts(expectedResult)}"`, () => { it(`formats ${locale} ${amount} as "${stringifyParts(
/** @type {FormatNumberPart[]} */ (expectedResult),
)}"`, () => {
localize.locale = locale; localize.locale = locale;
expect( expect(
formatNumberToParts(amount, { formatNumberToParts(Number(amount), {
style: 'decimal', style: 'decimal',
}), }),
).to.deep.equal(expectedResult); ).to.deep.equal(expectedResult);
@ -100,6 +115,7 @@ describe('formatNumberToParts', () => {
}); });
describe('minimumFractionDigits: 2', () => { describe('minimumFractionDigits: 2', () => {
/** @type {Array.<Array.<?>>} */
const specs = [ const specs = [
['en-GB', 3500, [i('3'), g(','), i('500'), d('.'), f('00')]], ['en-GB', 3500, [i('3'), g(','), i('500'), d('.'), f('00')]],
['en-GB', -3500, [m, i('3'), g(','), i('500'), d('.'), f('00')]], ['en-GB', -3500, [m, i('3'), g(','), i('500'), d('.'), f('00')]],
@ -114,10 +130,12 @@ describe('formatNumberToParts', () => {
]; ];
specs.forEach(([locale, amount, expectedResult]) => { specs.forEach(([locale, amount, expectedResult]) => {
it(`formats ${locale} ${amount} as "${stringifyParts(expectedResult)}"`, () => { it(`formats ${locale} ${amount} as "${stringifyParts(
/** @type {FormatNumberPart[]} */ (expectedResult),
)}"`, () => {
localize.locale = locale; localize.locale = locale;
expect( expect(
formatNumberToParts(amount, { formatNumberToParts(Number(amount), {
style: 'decimal', style: 'decimal',
minimumFractionDigits: 2, minimumFractionDigits: 2,
}), }),
@ -142,12 +160,14 @@ describe('formatNumberToParts', () => {
]; ];
specs.forEach(([locale, amount, expectedResult]) => { specs.forEach(([locale, amount, expectedResult]) => {
it(`formats ${locale} ${amount} as "${stringifyParts(expectedResult)}"`, () => { it(`formats ${locale} ${amount} as "${stringifyParts(
/** @type {FormatNumberPart[]} */ (expectedResult),
)}"`, () => {
expect( expect(
formatNumberToParts(amount / 100, { formatNumberToParts(Number(amount) / 100, {
locale,
style: 'percent', style: 'percent',
minimumFractionDigits: 2, minimumFractionDigits: 2,
locale: String(locale),
}), }),
).to.deep.equal(expectedResult); ).to.deep.equal(expectedResult);
}); });

View file

@ -0,0 +1,52 @@
import { Constructor } from '@open-wc/dedupe-mixin';
export interface FormatNumberPart {
type: string;
value: string;
}
interface StringToFunctionMap {
[key: string]: Function;
}
export type NamespaceObject = StringToFunctionMap | string;
interface msgVariables {
[key: string]: unknown;
}
interface msgOptions {
locale?: string;
}
declare class LocalizeMixinHost {
// FIXME: return value type check doesn't seem to be `working!
static get localizeNamespaces(): StringToFunctionMap[];
static get waitForLocalizeNamespaces(): boolean;
public localizeNamespacesLoaded(): Promise<Object>;
/**
* Hook into LitElement to only render once all translations are loaded
*/
public performUpdate(): Promise<void>;
public onLocaleReady(): void;
public onLocaleChanged(): void;
public onLocaleUpdated(): void;
public connectedCallback(): void;
public disconnectedCallback(): void;
public msgLit(keys: string | string[], variables?: msgVariables, options?: msgOptions): void;
private __getUniqueNamespaces(): void;
private __localizeAddLocaleChangedListener(): void;
private __localizeRemoveLocaleChangedListener(): void;
private __localizeOnLocaleChanged(event: CustomEvent): void;
}
declare function LocalizeMixinImplementation<T extends Constructor<HTMLElement>>(
superclass: T,
): T & Constructor<LocalizeMixinHost> & typeof LocalizeMixinHost;
export type LocalizeMixin = typeof LocalizeMixinImplementation;

8
packages/localize/types/index.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
declare module '@bundled-es-modules/message-format/MessageFormat.js' {
let main: any;
export = main;
}
declare module '@bundled-es-modules/fetch-mock' {
export const fetchMock: any;
}

View file

@ -3,6 +3,11 @@ export class SingletonManagerClass {
this._map = new Map(); this._map = new Map();
} }
/**
* @param {string} key
* @param {any} value
* @throws {Error} Will throw if the key is already defined
*/
set(key, value) { set(key, value) {
if (this.has(key)) { if (this.has(key)) {
throw new Error(`The key "${key}" is already defined and can not be overridden.`); throw new Error(`The key "${key}" is already defined and can not be overridden.`);
@ -10,10 +15,17 @@ export class SingletonManagerClass {
this._map.set(key, value); this._map.set(key, value);
} }
/**
* @param {string} key
* @returns
*/
get(key) { get(key) {
return this._map.get(key); return this._map.get(key);
} }
/**
* @param {string} key
*/
has(key) { has(key) {
return this._map.has(key); return this._map.has(key);
} }

View file

@ -10,15 +10,24 @@
"strict": true, "strict": true,
"noImplicitThis": true, "noImplicitThis": true,
"alwaysStrict": true, "alwaysStrict": true,
"types": ["node", "mocha"], "types": ["node", "mocha", "sinon"],
"esModuleInterop": true "esModuleInterop": true
}, },
"include": ["packages/core/**/*.js", "packages/tabs/**/*.js"], "include": [
"packages/core/**/*.js",
"packages/tabs/**/*.js",
"packages/singleton-manager/**/*.js",
"packages/localize/**/*.js",
"packages/localize/**/*.ts"
],
"exclude": [ "exclude": [
"node_modules", "node_modules",
"**/node_modules/*", "**/node_modules/*",
"**/coverage/*", "**/coverage/*",
"**/dist/**/*", "**/dist/**/*",
"packages/**/test-helpers" "packages/**/test-helpers",
// ignore test/demos for singleton manager until overlays are typed as it's used in there
"packages/singleton-manager/demo/",
"packages/singleton-manager/test/"
] ]
} }

View file

@ -20,11 +20,13 @@ module.exports = {
sessionStartTimeout: 60000, sessionStartTimeout: 60000,
concurrency: 1, concurrency: 1,
coverageConfig: { coverageConfig: {
report: true,
reportDir: 'coverage',
threshold: { threshold: {
statements: 80, statements: 90,
branches: 70, branches: 70,
functions: 70, functions: 80,
lines: 80, lines: 90,
}, },
}, },
browsers: [ browsers: [

View file

@ -6,11 +6,13 @@ module.exports = {
sessionStartTimeout: 30000, sessionStartTimeout: 30000,
concurrency: 5, concurrency: 5,
coverageConfig: { coverageConfig: {
report: true,
reportDir: 'coverage',
threshold: { threshold: {
statements: 80, statements: 90,
branches: 70, branches: 65,
functions: 70, functions: 80,
lines: 80, lines: 90,
}, },
}, },
}; };