Merge pull request #834 from ing-bank/feat/type-localize
Feat/type-localize
This commit is contained in:
commit
d5517f2dee
57 changed files with 1119 additions and 446 deletions
5
.changeset/new-oranges-knock.md
Normal file
5
.changeset/new-oranges-knock.md
Normal 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.
|
||||
5
.changeset/pretty-mice-wonder.md
Normal file
5
.changeset/pretty-mice-wonder.md
Normal 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.
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -45,4 +45,4 @@ jobs:
|
|||
publish: yarn release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
|
|
|||
|
|
@ -2,22 +2,36 @@ import MessageFormat from '@bundled-es-modules/message-format/MessageFormat.js';
|
|||
import { LionSingleton } from '@lion/core';
|
||||
import isLocalizeESModule from './isLocalizeESModule.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('../types/LocalizeMixinTypes').NamespaceObject} NamespaceObject
|
||||
*/
|
||||
|
||||
/**
|
||||
* `LocalizeManager` manages your translations (includes loading)
|
||||
*/
|
||||
export class LocalizeManager extends LionSingleton {
|
||||
// eslint-disable-line no-unused-vars
|
||||
constructor(params = {}) {
|
||||
super(params);
|
||||
this._fakeExtendsEventTarget();
|
||||
constructor({ autoLoadOnLocaleChange = false, fallbackLocale = '' } = {}) {
|
||||
super();
|
||||
this.__delegationTarget = document.createDocumentFragment();
|
||||
this._autoLoadOnLocaleChange = !!autoLoadOnLocaleChange;
|
||||
this._fallbackLocale = fallbackLocale;
|
||||
|
||||
this._autoLoadOnLocaleChange = !!params.autoLoadOnLocaleChange;
|
||||
this._fallbackLocale = params.fallbackLocale;
|
||||
/** @type {Object.<string, Object.<string, Object>>} */
|
||||
this.__storage = {};
|
||||
|
||||
/** @type {Map.<RegExp|string, function>} */
|
||||
this.__namespacePatternsMap = new Map();
|
||||
|
||||
/** @type {Object.<string, function|null>} */
|
||||
this.__namespaceLoadersCache = {};
|
||||
|
||||
/** @type {Object.<string, Object.<string, Promise.<Object>>>} */
|
||||
this.__namespaceLoaderPromisesCache = {};
|
||||
this.formatNumberOptions = { returnIfNaN: '' };
|
||||
|
||||
this.formatNumberOptions = {
|
||||
returnIfNaN: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
get locale() {
|
||||
if (this._supportExternalTranslationTools) {
|
||||
return this.__locale;
|
||||
return this.__locale || '';
|
||||
}
|
||||
return document.documentElement.lang;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
set locale(value) {
|
||||
/** @type {string} */
|
||||
let oldLocale;
|
||||
if (this._supportExternalTranslationTools) {
|
||||
oldLocale = this.__locale;
|
||||
oldLocale = /** @type {string} */ (this.__locale);
|
||||
this.__locale = value;
|
||||
if (this._langAttrSetByTranslationTool === null) {
|
||||
this._setHtmlLangAttribute(value);
|
||||
|
|
@ -101,12 +121,19 @@ export class LocalizeManager extends LionSingleton {
|
|||
this._onLocaleChanged(value, oldLocale);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} locale
|
||||
*/
|
||||
_setHtmlLangAttribute(locale) {
|
||||
this._teardownHtmlLangAttributeObserver();
|
||||
document.documentElement.lang = locale;
|
||||
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
|
||||
__handleLanguageOnly(value) {
|
||||
throw new Error(`
|
||||
|
|
@ -116,6 +143,9 @@ export class LocalizeManager extends LionSingleton {
|
|||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise.<Object>}
|
||||
*/
|
||||
get loadingComplete() {
|
||||
return Promise.all(Object.values(this.__namespaceLoaderPromisesCache[this.locale]));
|
||||
}
|
||||
|
|
@ -127,6 +157,12 @@ export class LocalizeManager extends LionSingleton {
|
|||
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) {
|
||||
if (this._isNamespaceInCache(locale, namespace)) {
|
||||
throw new Error(
|
||||
|
|
@ -138,18 +174,41 @@ export class LocalizeManager extends LionSingleton {
|
|||
this.__storage[locale][namespace] = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RegExp|string} pattern
|
||||
* @param {function} loader
|
||||
*/
|
||||
setupNamespaceLoader(pattern, loader) {
|
||||
this.__namespacePatternsMap.set(pattern, loader);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NamespaceObject[]} namespaces
|
||||
* @param {Object} [options]
|
||||
* @param {string} [options.locale]
|
||||
* @returns {Promise.<Object>}
|
||||
*/
|
||||
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 }) {
|
||||
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)) {
|
||||
return Promise.resolve();
|
||||
|
|
@ -163,6 +222,13 @@ export class LocalizeManager extends LionSingleton {
|
|||
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 = {}) {
|
||||
const locale = opts.locale ? opts.locale : this.locale;
|
||||
const message = this._getMessageForKeys(keys, locale);
|
||||
|
|
@ -186,7 +252,7 @@ export class LocalizeManager extends LionSingleton {
|
|||
this._langAttrSetByTranslationTool = document.documentElement.lang;
|
||||
}
|
||||
} 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() {
|
||||
this._htmlLangAttributeObserver.disconnect();
|
||||
if (this._htmlLangAttributeObserver) {
|
||||
this._htmlLangAttributeObserver.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} locale
|
||||
* @param {string} namespace
|
||||
*/
|
||||
_isNamespaceInCache(locale, namespace) {
|
||||
return !!(this.__storage[locale] && this.__storage[locale][namespace]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} locale
|
||||
* @param {string} namespace
|
||||
*/
|
||||
_getCachedNamespaceLoaderPromise(locale, namespace) {
|
||||
if (this.__namespaceLoaderPromisesCache[locale]) {
|
||||
return this.__namespaceLoaderPromisesCache[locale][namespace];
|
||||
|
|
@ -213,22 +289,41 @@ export class LocalizeManager extends LionSingleton {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} locale
|
||||
* @param {NamespaceObject} namespaceObj
|
||||
* @param {boolean} isDynamicImport
|
||||
* @param {string} namespace
|
||||
* @returns {Promise.<Object|void>}
|
||||
*/
|
||||
_loadNamespaceData(locale, namespaceObj, isDynamicImport, namespace) {
|
||||
const loader = this._getNamespaceLoader(namespaceObj, isDynamicImport, namespace);
|
||||
const loaderPromise = this._getNamespaceLoaderPromise(loader, locale, namespace);
|
||||
this._cacheNamespaceLoaderPromise(locale, namespace, loaderPromise);
|
||||
return loaderPromise.then(obj => {
|
||||
const data = isLocalizeESModule(obj) ? obj.default : obj;
|
||||
this.addData(locale, namespace, data);
|
||||
});
|
||||
return loaderPromise.then(
|
||||
/**
|
||||
* @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) {
|
||||
let loader = this.__namespaceLoadersCache[namespace];
|
||||
|
||||
if (!loader) {
|
||||
if (isDynamicImport) {
|
||||
loader = namespaceObj[namespace];
|
||||
const _namespaceObj = /** @type {Object.<string,function>} */ (namespaceObj);
|
||||
loader = _namespaceObj[namespace];
|
||||
this.__namespaceLoadersCache[namespace] = loader;
|
||||
} else {
|
||||
loader = this._lookupNamespaceLoader(namespace);
|
||||
|
|
@ -245,12 +340,20 @@ export class LocalizeManager extends LionSingleton {
|
|||
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) {
|
||||
return loader(locale, namespace).catch(() => {
|
||||
const lang = this._getLangFromLocale(locale);
|
||||
return loader(lang, namespace).catch(() => {
|
||||
if (fallbackLocale) {
|
||||
return this._getNamespaceLoaderPromise(loader, fallbackLocale, namespace, false).catch(
|
||||
return this._getNamespaceLoaderPromise(loader, fallbackLocale, namespace, '').catch(
|
||||
() => {
|
||||
const fallbackLang = this._getLangFromLocale(fallbackLocale);
|
||||
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) {
|
||||
if (!this.__namespaceLoaderPromisesCache[locale]) {
|
||||
this.__namespaceLoaderPromisesCache[locale] = {};
|
||||
|
|
@ -275,6 +383,10 @@ export class LocalizeManager extends LionSingleton {
|
|||
this.__namespaceLoaderPromisesCache[locale][namespace] = promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} namespace
|
||||
* @returns {function|null}
|
||||
*/
|
||||
_lookupNamespaceLoader(namespace) {
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
for (const [key, value] of this.__namespacePatternsMap) {
|
||||
|
|
@ -289,18 +401,45 @@ export class LocalizeManager extends LionSingleton {
|
|||
/* eslint-enable no-restricted-syntax */
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} locale
|
||||
* @returns {string}
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_getLangFromLocale(locale) {
|
||||
return locale.substring(0, 2);
|
||||
}
|
||||
|
||||
_fakeExtendsEventTarget() {
|
||||
const delegate = document.createDocumentFragment();
|
||||
['addEventListener', 'dispatchEvent', 'removeEventListener'].forEach(funcName => {
|
||||
this[funcName] = (...args) => delegate[funcName](...args);
|
||||
});
|
||||
/**
|
||||
* @param {string} type
|
||||
* @param {EventListener} listener
|
||||
* @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) {
|
||||
if (newLocale === oldLocale) {
|
||||
return;
|
||||
|
|
@ -311,19 +450,34 @@ export class LocalizeManager extends LionSingleton {
|
|||
this.dispatchEvent(new CustomEvent('localeChanged', { detail: { newLocale, oldLocale } }));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} newLocale
|
||||
* @param {string} oldLocale
|
||||
* @returns {Promise.<Object>}
|
||||
*/
|
||||
_loadAllMissing(newLocale, oldLocale) {
|
||||
const oldLocaleNamespaces = this.__storage[oldLocale] || {};
|
||||
const newLocaleNamespaces = this.__storage[newLocale] || {};
|
||||
/** @type {Promise<Object|void>[]} */
|
||||
const promises = [];
|
||||
Object.keys(oldLocaleNamespaces).forEach(namespace => {
|
||||
const newNamespaceData = newLocaleNamespaces[namespace];
|
||||
if (!newNamespaceData) {
|
||||
promises.push(this.loadNamespace(namespace, { locale: newLocale }));
|
||||
promises.push(
|
||||
this.loadNamespace(namespace, {
|
||||
locale: newLocale,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | string[]} keys
|
||||
* @param {string} locale
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
_getMessageForKeys(keys, locale) {
|
||||
if (typeof keys === 'string') {
|
||||
return this._getMessageForKey(keys, locale);
|
||||
|
|
@ -341,16 +495,33 @@ export class LocalizeManager extends LionSingleton {
|
|||
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) {
|
||||
if (key.indexOf(':') === -1) {
|
||||
if (!key || key.indexOf(':') === -1) {
|
||||
throw new Error(
|
||||
`Namespace is missing in the key "${key}". The format for keys is "namespace:name".`,
|
||||
);
|
||||
}
|
||||
const [ns, namesString] = key.split(':');
|
||||
const namespaces = this.__storage[locale];
|
||||
const messages = namespaces ? namespaces[ns] : null;
|
||||
const messages = namespaces ? namespaces[ns] : {};
|
||||
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 || '');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,111 +2,154 @@ import { dedupeMixin, until, nothing } from '@lion/core';
|
|||
import { localize } from './localize.js';
|
||||
|
||||
/**
|
||||
* # LocalizeMixin - for self managed templates
|
||||
*
|
||||
* @polymerMixin
|
||||
* @mixinFunction
|
||||
* @typedef {import('../types/LocalizeMixinTypes').LocalizeMixin} LocalizeMixin
|
||||
*/
|
||||
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.__localizeStartLoadingNamespaces();
|
||||
this.__boundLocalizeOnLocaleChanged =
|
||||
/** @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.__localizeMessageSync = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* hook into LitElement to only render once all translations are loaded
|
||||
*/
|
||||
async performUpdate() {
|
||||
if (this.constructor.waitForLocalizeNamespaces) {
|
||||
await this.localizeNamespacesLoaded;
|
||||
}
|
||||
super.performUpdate();
|
||||
/**
|
||||
* hook into LitElement to only render once all translations are loaded
|
||||
* @returns {Promise.<void>}
|
||||
*/
|
||||
async performUpdate() {
|
||||
if (Object.getPrototypeOf(this).constructor.waitForLocalizeNamespaces) {
|
||||
await this.localizeNamespacesLoaded;
|
||||
}
|
||||
super.performUpdate();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (super.connectedCallback) {
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (super.connectedCallback) {
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
if (this.localizeNamespacesLoaded) {
|
||||
this.localizeNamespacesLoaded.then(() => this.onLocaleReady());
|
||||
this.__localizeAddLocaleChangedListener();
|
||||
}
|
||||
this.__localizeAddLocaleChangedListener();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (super.disconnectedCallback) {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (super.disconnectedCallback) {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
this.__localizeRemoveLocaleChangedListener();
|
||||
}
|
||||
|
||||
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.__localizeMessageSync) {
|
||||
return localize.msg(...args);
|
||||
}
|
||||
return until(
|
||||
this.localizeNamespacesLoaded.then(() => localize.msg(...args)),
|
||||
nothing,
|
||||
);
|
||||
if (!this.localizeNamespacesLoaded) {
|
||||
return '';
|
||||
}
|
||||
|
||||
__getUniqueNamespaces() {
|
||||
const uniqueNamespaces = [];
|
||||
return until(
|
||||
this.localizeNamespacesLoaded.then(() => localize.msg(keys, variables, options)),
|
||||
nothing,
|
||||
);
|
||||
}
|
||||
|
||||
// IE11 does not support iterable in the constructor
|
||||
const s = new Set();
|
||||
this.constructor.localizeNamespaces.forEach(s.add.bind(s));
|
||||
s.forEach(uniqueNamespace => {
|
||||
uniqueNamespaces.push(uniqueNamespace);
|
||||
});
|
||||
return uniqueNamespaces;
|
||||
}
|
||||
/**
|
||||
* @returns {string[]}
|
||||
*/
|
||||
__getUniqueNamespaces() {
|
||||
/** @type {string[]} */
|
||||
const uniqueNamespaces = [];
|
||||
|
||||
__localizeStartLoadingNamespaces() {
|
||||
this.localizeNamespacesLoaded = localize.loadNamespaces(this.__getUniqueNamespaces());
|
||||
}
|
||||
// IE11 does not support iterable in the constructor
|
||||
const s = new Set();
|
||||
Object.getPrototypeOf(this).constructor.localizeNamespaces.forEach(s.add.bind(s));
|
||||
s.forEach(uniqueNamespace => {
|
||||
uniqueNamespaces.push(uniqueNamespace);
|
||||
});
|
||||
return uniqueNamespaces;
|
||||
}
|
||||
|
||||
__localizeAddLocaleChangedListener() {
|
||||
localize.addEventListener('localeChanged', this.__boundLocalizeOnLocaleChanged);
|
||||
}
|
||||
__localizeStartLoadingNamespaces() {
|
||||
this.localizeNamespacesLoaded = localize.loadNamespaces(this.__getUniqueNamespaces());
|
||||
}
|
||||
|
||||
__localizeRemoveLocaleChangedListener() {
|
||||
localize.removeEventListener('localeChanged', this.__boundLocalizeOnLocaleChanged);
|
||||
}
|
||||
__localizeAddLocaleChangedListener() {
|
||||
localize.addEventListener('localeChanged', this.__boundLocalizeOnLocaleChanged);
|
||||
}
|
||||
|
||||
__localizeOnLocaleChanged(event) {
|
||||
this.onLocaleChanged(event.detail.newLocale, event.detail.oldLocale);
|
||||
}
|
||||
__localizeRemoveLocaleChangedListener() {
|
||||
localize.removeEventListener('localeChanged', this.__boundLocalizeOnLocaleChanged);
|
||||
}
|
||||
|
||||
onLocaleReady() {
|
||||
this.onLocaleUpdated();
|
||||
}
|
||||
/**
|
||||
* @param {CustomEvent} event
|
||||
*/
|
||||
__localizeOnLocaleChanged(event) {
|
||||
this.onLocaleChanged(event.detail.newLocale, event.detail.oldLocale);
|
||||
}
|
||||
|
||||
onLocaleChanged() {
|
||||
this.localizeNamespacesLoaded = localize.loadNamespaces(this.__getUniqueNamespaces());
|
||||
this.onLocaleUpdated();
|
||||
this.requestUpdate();
|
||||
}
|
||||
onLocaleReady() {
|
||||
this.onLocaleUpdated();
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import { pad } from './pad.js';
|
|||
/**
|
||||
* To add a leading zero to a single number
|
||||
*
|
||||
* @param dateString
|
||||
* @returns {*}
|
||||
* @param {string} dateString
|
||||
* @returns {string}
|
||||
*/
|
||||
export function addLeadingZero(dateString) {
|
||||
const dateParts = splitDate(dateString);
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { trim } from './trim.js';
|
|||
/**
|
||||
* To clean date from added characters from IE
|
||||
*
|
||||
* @param dateAsString
|
||||
* @returns {string|XML}
|
||||
* @param {string} dateAsString
|
||||
* @returns {string}
|
||||
*/
|
||||
export function clean(dateAsString) {
|
||||
// list of separators is from wikipedia https://www.wikiwand.com/en/Date_format_by_country
|
||||
|
|
|
|||
|
|
@ -4,14 +4,31 @@ import { normalizeIntlDate } from './normalizeIntlDate.js';
|
|||
/**
|
||||
* Formats date based on locale and options
|
||||
*
|
||||
* @param date
|
||||
* @param options
|
||||
* @returns {*}
|
||||
* @param {Date} date
|
||||
* @param {Object} [options] Intl options are available
|
||||
* @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) {
|
||||
if (!(date instanceof Date)) {
|
||||
return '';
|
||||
}
|
||||
/** @type {options} */
|
||||
const formatOptions = options || {};
|
||||
/**
|
||||
* Set smart defaults if:
|
||||
|
|
|
|||
|
|
@ -3,13 +3,28 @@ import { splitDate } from './splitDate.js';
|
|||
|
||||
/**
|
||||
* To compute the localized date format
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getDateFormatBasedOnLocale() {
|
||||
/**
|
||||
*
|
||||
* @param {ArrayLike.<string>} dateParts
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function computePositions(dateParts) {
|
||||
/**
|
||||
* @param {number} index
|
||||
* @returns {string}
|
||||
*/
|
||||
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);
|
||||
|
|
@ -28,6 +43,8 @@ export function getDateFormatBasedOnLocale() {
|
|||
const dateParts = splitDate(formattedDate);
|
||||
|
||||
const dateFormat = {};
|
||||
dateFormat.positions = computePositions(dateParts);
|
||||
if (dateParts) {
|
||||
dateFormat.positions = computePositions(dateParts);
|
||||
}
|
||||
return `${dateFormat.positions[0]}-${dateFormat.positions[1]}-${dateFormat.positions[2]}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { normalizeIntlDate } from './normalizeIntlDate.js';
|
||||
|
||||
/** @type {Object.<string, Object.<string,string[]>>} */
|
||||
const monthsLocaleCache = {};
|
||||
|
||||
/**
|
||||
* @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
|
||||
* @returns {Array} like: ['January', 'February', ...etc].
|
||||
* @returns {string[]} like: ['January', 'February', ...etc].
|
||||
*/
|
||||
export function getMonthNames({ locale, style = 'long' } = {}) {
|
||||
let months = monthsLocaleCache[locale] && monthsLocaleCache[locale][style];
|
||||
|
|
|
|||
|
|
@ -1,24 +1,32 @@
|
|||
import { normalizeIntlDate } from './normalizeIntlDate.js';
|
||||
|
||||
/** @type {Object.<string, Object.<string,string[]>>} */
|
||||
const weekdayNamesCache = {};
|
||||
|
||||
/**
|
||||
* @desc Return cached weekday names for locale for all styles ('long', 'short', 'narrow')
|
||||
* @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) {
|
||||
let weekdays = weekdayNamesCache[locale];
|
||||
const cachedWeekdayNames = weekdayNamesCache[locale];
|
||||
let weekdays;
|
||||
|
||||
if (weekdays) {
|
||||
return weekdays;
|
||||
if (cachedWeekdayNames) {
|
||||
return cachedWeekdayNames;
|
||||
}
|
||||
|
||||
weekdayNamesCache[locale] = { long: [], short: [], narrow: [] };
|
||||
weekdayNamesCache[locale] = {
|
||||
long: [],
|
||||
short: [],
|
||||
narrow: [],
|
||||
};
|
||||
|
||||
['long', 'short', 'narrow'].forEach(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
|
||||
for (let i = 0; i < 7; i += 1) {
|
||||
|
|
@ -34,10 +42,11 @@ function getCachedWeekdayNames(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 {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 } = {}) {
|
||||
const weekdays = getCachedWeekdayNames(locale)[style];
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
/**
|
||||
* @desc Makes suitable for date comparisons
|
||||
* @param {Date} d
|
||||
* @param {Date} date
|
||||
* @returns {Date}
|
||||
*/
|
||||
export function normalizeDateTime(d) {
|
||||
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
||||
export function normalizeDateTime(date) {
|
||||
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* To filter out some added characters in IE
|
||||
*
|
||||
* @param str
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
export function normalizeIntlDate(str) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
/**
|
||||
* To get the absolute value of a number.
|
||||
*
|
||||
* @param n
|
||||
* @param {string} n - number in string format
|
||||
* @returns {string}
|
||||
*/
|
||||
export function pad(n) {
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,14 @@ import { localize } from '../localize.js';
|
|||
import { getDateFormatBasedOnLocale } from './getDateFormatBasedOnLocale.js';
|
||||
import { addLeadingZero } from './addLeadingZero.js';
|
||||
|
||||
/**
|
||||
* @param {function} fn
|
||||
*/
|
||||
const memoize = fn => {
|
||||
/** @type {Object.<any, any>} */
|
||||
const cache = {};
|
||||
return parm => {
|
||||
|
||||
return /** @param {any} parm */ parm => {
|
||||
const n = parm;
|
||||
if (n in cache) {
|
||||
return cache[n];
|
||||
|
|
@ -20,11 +25,11 @@ const memoizedGetDateFormatBasedOnLocale = memoize(getDateFormatBasedOnLocale);
|
|||
/**
|
||||
* To parse a date into the right format
|
||||
*
|
||||
* @param date
|
||||
* @returns {Date}
|
||||
* @param {string} dateString
|
||||
* @returns {Date | undefined}
|
||||
*/
|
||||
export function parseDate(date) {
|
||||
const stringToParse = addLeadingZero(date);
|
||||
export function parseDate(dateString) {
|
||||
const stringToParse = addLeadingZero(dateString);
|
||||
let parsedString;
|
||||
switch (memoizedGetDateFormatBasedOnLocale(localize.locale)) {
|
||||
case 'day-month-year':
|
||||
|
|
@ -51,7 +56,7 @@ export function parseDate(date) {
|
|||
const parsedDate = new Date(parsedString);
|
||||
// Check if parsedDate is not `Invalid Date`
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
if (!isNaN(parsedDate)) {
|
||||
if (!isNaN(parsedDate.getTime())) {
|
||||
return parsedDate;
|
||||
}
|
||||
return undefined;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import { clean } from './clean.js';
|
|||
/**
|
||||
* To sanitize a date from IE11 handling
|
||||
*
|
||||
* @param date
|
||||
* @returns {string|XML}
|
||||
* @param {Date} date
|
||||
* @returns {string}
|
||||
*/
|
||||
export function sanitizedDateTimeFormat(date) {
|
||||
const fDate = formatDate(date);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/**
|
||||
* To split a date into days, months, years, etc
|
||||
*
|
||||
* @param date
|
||||
* @returns {Array|{index: number, input: string}|*}
|
||||
* @param {string} dateAsString
|
||||
* @returns {ArrayLike.<string> | null}
|
||||
*/
|
||||
export function splitDate(date) {
|
||||
return date.match(/(\d{1,4})([^\d]+)(\d{1,4})([^\d]+)(\d{1,4})/);
|
||||
export function splitDate(dateAsString) {
|
||||
return dateAsString.match(/(\d{1,4})([^\d]+)(\d{1,4})([^\d]+)(\d{1,4})/);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
/**
|
||||
* To trim the date
|
||||
*
|
||||
* @param dateAsString
|
||||
* @returns {string|XML}
|
||||
* @param {string} dateAsString
|
||||
* @returns {string}
|
||||
*/
|
||||
export function trim(dateAsString) {
|
||||
return dateAsString.replace(/^[^\d]*/g, '').replace(/[^\d]*$/g, '');
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
/**
|
||||
* @param {Object.<string, Object>} obj
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export default function isLocalizeESModule(obj) {
|
||||
return !!(obj && obj.default && typeof obj.default === 'object' && Object.keys(obj).length === 1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { singletonManager } from 'singleton-manager';
|
||||
import { LocalizeManager } from './LocalizeManager.js';
|
||||
|
||||
/** @type {LocalizeManager} */
|
||||
// eslint-disable-next-line import/no-mutable-exports
|
||||
export let localize =
|
||||
singletonManager.get('@lion/localize::localize::0.10.x') ||
|
||||
|
|
@ -9,6 +10,9 @@ export let localize =
|
|||
fallbackLocale: 'en-GB',
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {LocalizeManager} newLocalize
|
||||
*/
|
||||
export function setLocalize(newLocalize) {
|
||||
localize.teardown();
|
||||
localize = newLocalize;
|
||||
|
|
|
|||
|
|
@ -4,9 +4,8 @@ import { localize } from '../localize.js';
|
|||
* When number is NaN we should return an empty string or returnIfNaN param
|
||||
*
|
||||
* @param {string} returnIfNaN
|
||||
* @returns {*}
|
||||
* @returns {string}
|
||||
*/
|
||||
export function emptyStringWhenNumberNan(returnIfNaN) {
|
||||
const stringToReturn = returnIfNaN || localize.formatNumberOptions.returnIfNaN;
|
||||
return stringToReturn;
|
||||
return returnIfNaN || localize.formatNumberOptions.returnIfNaN;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
/**
|
||||
* Add separators when they are not present
|
||||
*
|
||||
* @param {Array} formattedParts
|
||||
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
||||
* @param {FormatNumberPart[]} formattedParts
|
||||
* @param {string} groupSeparator
|
||||
* @returns {Array}
|
||||
* @returns {FormatNumberPart[]}
|
||||
*/
|
||||
export function forceAddGroupSeparators(formattedParts, groupSeparator) {
|
||||
/** @type {FormatNumberPart[]} */
|
||||
let concatArray = [];
|
||||
let firstPart;
|
||||
let integerPart;
|
||||
|
|
@ -29,24 +31,41 @@ export function forceAddGroupSeparators(formattedParts, groupSeparator) {
|
|||
numberPart += integerPart[0].value[i];
|
||||
// Create first grouping which is < 3
|
||||
if (numberPart.length === mod3 && firstGroup === false) {
|
||||
numberArray.push({ type: 'integer', value: numberPart });
|
||||
numberArray.push({
|
||||
type: 'integer',
|
||||
value: numberPart,
|
||||
});
|
||||
if (numberOfDigits > 3) {
|
||||
numberArray.push({ type: 'group', value: groupSeparator });
|
||||
numberArray.push({
|
||||
type: 'group',
|
||||
value: groupSeparator,
|
||||
});
|
||||
}
|
||||
numberPart = '';
|
||||
firstGroup = true;
|
||||
// Create groupings of 3
|
||||
} else if (numberPart.length === 3 && i < numberOfDigits - 1) {
|
||||
numberOfGroups += 1;
|
||||
numberArray.push({ type: 'integer', value: numberPart });
|
||||
numberArray.push({
|
||||
type: 'integer',
|
||||
value: numberPart,
|
||||
});
|
||||
if (numberOfGroups !== groups) {
|
||||
numberArray.push({ type: 'group', value: groupSeparator });
|
||||
numberArray.push({
|
||||
type: 'group',
|
||||
value: groupSeparator,
|
||||
});
|
||||
}
|
||||
numberPart = '';
|
||||
}
|
||||
}
|
||||
numberArray.push({ type: 'integer', value: numberPart });
|
||||
concatArray = firstPart.concat(numberArray, formattedParts);
|
||||
numberArray.push({
|
||||
type: 'integer',
|
||||
value: numberPart,
|
||||
});
|
||||
if (firstPart) {
|
||||
concatArray = firstPart.concat(numberArray, formattedParts);
|
||||
}
|
||||
}
|
||||
return concatArray;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
/**
|
||||
* For Dutch and Belgian amounts the currency should be at the end of the string
|
||||
*
|
||||
* @param {Array} formattedParts
|
||||
* @returns {Array}
|
||||
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
||||
* @param {FormatNumberPart[]} formattedParts
|
||||
* @returns {FormatNumberPart[]}
|
||||
*/
|
||||
export function forceCurrencyToEnd(formattedParts) {
|
||||
if (formattedParts[0].type === 'currency') {
|
||||
|
|
|
|||
|
|
@ -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 numberOfParts = result.length;
|
||||
// Change the symbols for locale 'en-AU', due to bug in Chrome
|
||||
if (numberOfParts > 1 && options && options.currencyDisplay === 'symbol') {
|
||||
switch (options.currency) {
|
||||
case 'EUR':
|
||||
result[0].value = '€';
|
||||
break;
|
||||
case 'USD':
|
||||
result[0].value = '$';
|
||||
break;
|
||||
case 'JPY':
|
||||
result[0].value = '¥';
|
||||
break;
|
||||
/* no default */
|
||||
if (formattedParts.length > 1 && currencyDisplay === 'symbol') {
|
||||
if (Object.keys(CURRENCY_CODE_SYMBOL_MAP).includes(currency)) {
|
||||
result[0].value = CURRENCY_CODE_SYMBOL_MAP[currency];
|
||||
}
|
||||
result[1].value = '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import { normalSpaces } from './normalSpaces.js';
|
||||
|
||||
/**
|
||||
* @param {Array} formattedParts
|
||||
* @return {Array} parts with forced "normal" spaces
|
||||
* Parts with forced "normal" spaces
|
||||
*
|
||||
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
||||
* @param {FormatNumberPart[]} formattedParts
|
||||
* @returns {FormatNumberPart[]}
|
||||
*/
|
||||
export function forceNormalSpaces(formattedParts) {
|
||||
/** @type {FormatNumberPart[]} */
|
||||
const result = [];
|
||||
formattedParts.forEach(part => {
|
||||
result.push({
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
/**
|
||||
* When in some locales there is no space between currency and amount it is added
|
||||
*
|
||||
* @param {Array} formattedParts
|
||||
* @param {Object} options
|
||||
* @returns {*}
|
||||
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
||||
* @param {FormatNumberPart[]} formattedParts
|
||||
* @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 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') {
|
||||
// currency in front of a number: EUR 1.00
|
||||
formattedParts.splice(1, 0, literalObject);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
/**
|
||||
* @desc Intl uses 0 as group separator for bg-BG locale.
|
||||
* 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) {
|
||||
return formattedParts.map(p => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
// Chage the currencycode from TRY to TL, for Turkey
|
||||
if (options.currency === 'TRY' && options.currencyDisplay === 'code') {
|
||||
// Change the currency code from TRY to TL, for Turkey
|
||||
if (currency === 'TRY' && currencyDisplay === 'code') {
|
||||
if (result[0].value === 'TRY') {
|
||||
result[0].value = 'TL';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 numberOfParts = result.length;
|
||||
// Change the symbol from JPY to ¥, due to bug in Chrome
|
||||
if (
|
||||
numberOfParts > 1 &&
|
||||
options &&
|
||||
options.currency === 'JPY' &&
|
||||
options.currencyDisplay === 'symbol'
|
||||
) {
|
||||
if (numberOfParts > 1 && currency === 'JPY' && currencyDisplay === 'symbol') {
|
||||
result[numberOfParts - 1].value = '¥';
|
||||
}
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -4,25 +4,41 @@ import { formatNumberToParts } from './formatNumberToParts.js';
|
|||
/**
|
||||
* 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 {Object} options Intl options are available extended by roundMode
|
||||
* @returns {*} Formatted number
|
||||
* @param {Object} [options] Intl options are available extended by roundMode and returnIfNaN
|
||||
* @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 '';
|
||||
const formattedToParts = formatNumberToParts(number, options);
|
||||
// If number is not a number
|
||||
if (
|
||||
formattedToParts === (options && options.returnIfNaN) ||
|
||||
formattedToParts === options.returnIfNaN ||
|
||||
formattedToParts === localize.formatNumberOptions.returnIfNaN
|
||||
) {
|
||||
return formattedToParts;
|
||||
return /** @type {string} */ (formattedToParts);
|
||||
}
|
||||
let printNumberOfParts = '';
|
||||
// update numberOfParts because there may be some parts added
|
||||
const numberOfParts = formattedToParts && formattedToParts.length;
|
||||
for (let i = 0; i < numberOfParts; i += 1) {
|
||||
printNumberOfParts += formattedToParts[i].value;
|
||||
const part = /** @type {FormatNumberPart} */ (formattedToParts[i]);
|
||||
printNumberOfParts += part.value;
|
||||
}
|
||||
return printNumberOfParts;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,11 +9,26 @@ import { roundNumber } from './roundNumber.js';
|
|||
/**
|
||||
* 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 {Object} options Intl options are available extended by roundMode
|
||||
* @returns {Array} Array with parts
|
||||
* @param {Object} [options] Intl options are available extended by roundMode,returnIfNaN
|
||||
* @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;
|
||||
const computedLocale = getLocale(options && options.locale);
|
||||
// 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);
|
||||
}
|
||||
let formattedParts = [];
|
||||
|
||||
const formattedNumber = Intl.NumberFormat(computedLocale, options).format(parsedNumber);
|
||||
const regexCurrency = /[.,\s0-9]/;
|
||||
const regexMinusSign = /[-]/; // U+002D, Hyphen-Minus, -
|
||||
|
|
@ -95,7 +111,7 @@ export function formatNumberToParts(number, options) {
|
|||
if (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) {
|
||||
formattedParts.push({ type: 'integer', value: numberPart });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,17 +3,18 @@ import { formatNumberToParts } from './formatNumberToParts.js';
|
|||
/**
|
||||
* Based on number, returns currency name like 'US dollar'
|
||||
*
|
||||
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
||||
* @param {string} currencyIso iso code like USD
|
||||
* @param {Object} options Intl options are available extended by roundMode
|
||||
* @returns {string} currency name like 'US dollar'
|
||||
*/
|
||||
export function getCurrencyName(currencyIso, options) {
|
||||
const parts = formatNumberToParts(1, {
|
||||
const parts = /** @type {FormatNumberPart[]} */ (formatNumberToParts(1, {
|
||||
...options,
|
||||
style: 'currency',
|
||||
currency: currencyIso,
|
||||
currencyDisplay: 'name',
|
||||
});
|
||||
}));
|
||||
const currencyName = parts
|
||||
.filter(p => p.type === 'currency')
|
||||
.map(o => o.value)
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import { getLocale } from './getLocale.js';
|
|||
* To get the decimal separator
|
||||
*
|
||||
* @param {string} locale To override the browser locale
|
||||
* @returns {Object} the separator
|
||||
* @returns {string} The separator
|
||||
*/
|
||||
export function getDecimalSeparator(locale) {
|
||||
const computedLocale = getLocale(locale);
|
||||
const formattedNumber = Intl.NumberFormat(computedLocale, {
|
||||
style: 'decimal',
|
||||
minimumFractionDigits: 1,
|
||||
}).format('1');
|
||||
}).format(1);
|
||||
return formattedNumber[1];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,15 @@ import { formatNumberToParts } from './formatNumberToParts.js';
|
|||
* @example
|
||||
* getFractionDigits('JOD'); // return 3
|
||||
*
|
||||
* @param {string} currency Currency code e.g. EUR
|
||||
* @return {number} fraction for the given currency
|
||||
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
||||
* @param {string} [currency="EUR"] Currency code e.g. EUR
|
||||
* @returns {number} fraction for the given currency
|
||||
*/
|
||||
export function getFractionDigits(currency = 'EUR') {
|
||||
const parts = formatNumberToParts(123, {
|
||||
const parts = /** @type {FormatNumberPart[]} */ (formatNumberToParts(123, {
|
||||
style: 'currency',
|
||||
currency,
|
||||
});
|
||||
}));
|
||||
const [fractionPart] = parts.filter(part => part.type === 'fraction');
|
||||
return fractionPart ? fractionPart.value.length : 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,16 +2,16 @@ import { getLocale } from './getLocale.js';
|
|||
import { normalSpaces } from './normalSpaces.js';
|
||||
|
||||
/**
|
||||
* To get the group separator
|
||||
* Gets the group separator
|
||||
*
|
||||
* @param {string} locale To override the browser locale
|
||||
* @returns {Object} the separator
|
||||
* @param {string} [locale] To override the browser locale
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getGroupSeparator(locale) {
|
||||
const computedLocale = getLocale(locale);
|
||||
const formattedNumber = Intl.NumberFormat(computedLocale, {
|
||||
style: 'decimal',
|
||||
minimumFractionDigits: 0,
|
||||
}).format('10000');
|
||||
}).format(10000);
|
||||
return normalSpaces(formattedNumber[2]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { localize } from '../localize.js';
|
|||
/**
|
||||
* Gets the locale to use
|
||||
*
|
||||
* @param {string} locale Locale to override browser locale
|
||||
* @param {string} [locale] Locale to override browser locale
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getLocale(locale) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* @param {Array} value
|
||||
* @return {Array} value with forced "normal" space
|
||||
* @param {string} value
|
||||
* @returns {string} value with forced "normal" space
|
||||
*/
|
||||
export function normalSpaces(value) {
|
||||
// If non-breaking space (160) or narrow non-breaking space (8239) then return ' '
|
||||
|
|
|
|||
|
|
@ -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
|
||||
* @returns {string} currency
|
||||
* @returns {string}
|
||||
*/
|
||||
export function normalizeCurrencyLabel(currency, locale) {
|
||||
if (currency === 'TRY' && locale === 'tr-TR') {
|
||||
return 'TL';
|
||||
}
|
||||
return currency;
|
||||
return currency === 'TRY' && locale === 'tr-TR' ? 'TL' : currency;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,15 +11,19 @@ import { forceENAUSymbols } from './forceENAUSymbols.js';
|
|||
/**
|
||||
* Function with all fixes on localize
|
||||
*
|
||||
* @param {Array} formattedParts
|
||||
* @param {Object} options
|
||||
* @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart
|
||||
* @param {FormatNumberPart[]} formattedParts
|
||||
* @param {Object} [options]
|
||||
* @param {string} [options.style]
|
||||
* @param {string} [options.currency]
|
||||
* @param {string} [options.currencyDisplay]
|
||||
* @param {string} _locale
|
||||
* @returns {*}
|
||||
* @returns {FormatNumberPart[]}
|
||||
*/
|
||||
export function normalizeIntl(formattedParts, options, _locale) {
|
||||
let normalize = forceNormalSpaces(formattedParts, options);
|
||||
export function normalizeIntl(formattedParts, options = {}, _locale) {
|
||||
let normalize = forceNormalSpaces(formattedParts);
|
||||
// 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') {
|
||||
normalize = forceCurrencyToEnd(normalize);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
*
|
||||
* @param {number} number
|
||||
* @param {string} roundMode
|
||||
* @returns {*}
|
||||
* @throws {Error} roundMode can only be round|floor|ceiling
|
||||
* @returns {number}
|
||||
*/
|
||||
export function roundNumber(number, roundMode) {
|
||||
switch (roundMode) {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,22 @@
|
|||
/**
|
||||
* @type {Object.<String, Object>}
|
||||
*/
|
||||
let fakeImports = {};
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {Object} data
|
||||
*/
|
||||
export function setupFakeImport(path, data) {
|
||||
const fakeExports = { ...data };
|
||||
Object.defineProperty(fakeExports, '__esModule', { value: true });
|
||||
fakeImports[path] = fakeExports;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} namespaces
|
||||
* @param {string[]} locales
|
||||
*/
|
||||
export function setupEmptyFakeImportsFor(namespaces, locales) {
|
||||
namespaces.forEach(namespace => {
|
||||
locales.forEach(locale => {
|
||||
|
|
@ -20,6 +31,11 @@ export function resetFakeImport() {
|
|||
fakeImports = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} result
|
||||
* @param {Function} resolve
|
||||
* @param {Function} reject
|
||||
*/
|
||||
function resolveOrReject(result, resolve, reject) {
|
||||
if (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) {
|
||||
const result = fakeImports[path];
|
||||
if (ms > 0) {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
import { expect, oneEvent, aTimeout } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { fetchMock } from '@bundled-es-modules/fetch-mock';
|
||||
import { setupFakeImport, resetFakeImport, fakeImport } from '../test-helpers.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) {
|
||||
return str.replace(/(\u200E|\u200E)/g, '');
|
||||
}
|
||||
|
||||
describe('LocalizeManager', () => {
|
||||
/** @type {LocalizeManager} */
|
||||
let manager;
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -43,7 +46,10 @@ describe('LocalizeManager', () => {
|
|||
|
||||
it('has teardown() method removing all side effects', () => {
|
||||
manager = new LocalizeManager();
|
||||
const disconnectObserverSpy = sinon.spy(manager._htmlLangAttributeObserver, 'disconnect');
|
||||
const disconnectObserverSpy = sinon.spy(
|
||||
manager._htmlLangAttributeObserver,
|
||||
/** @type {never} */ ('disconnect'),
|
||||
);
|
||||
manager.teardown();
|
||||
expect(disconnectObserverSpy.callCount).to.equal(1);
|
||||
});
|
||||
|
|
@ -54,7 +60,11 @@ describe('LocalizeManager', () => {
|
|||
setTimeout(() => {
|
||||
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.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', () => {
|
||||
manager = new LocalizeManager();
|
||||
const eventSpy = sinon.spy();
|
||||
|
||||
manager.addEventListener('localeChanged', eventSpy);
|
||||
manager.locale = 'en-US';
|
||||
manager.locale = 'en-US';
|
||||
|
|
@ -127,6 +138,7 @@ describe('LocalizeManager', () => {
|
|||
manager = new LocalizeManager();
|
||||
|
||||
await manager.loadNamespace({
|
||||
/** @param {string} locale */
|
||||
'my-component': locale => fakeImport(`./my-component/${locale}.js`),
|
||||
});
|
||||
|
||||
|
|
@ -145,6 +157,7 @@ describe('LocalizeManager', () => {
|
|||
|
||||
await manager.loadNamespace(
|
||||
{
|
||||
/** @param {string} locale */
|
||||
'my-component': locale => fakeImport(`./my-component/${locale}.js`),
|
||||
},
|
||||
{ locale: 'nl-NL' },
|
||||
|
|
@ -164,8 +177,14 @@ describe('LocalizeManager', () => {
|
|||
manager = new LocalizeManager();
|
||||
|
||||
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({
|
||||
|
|
@ -185,8 +204,14 @@ describe('LocalizeManager', () => {
|
|||
|
||||
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' },
|
||||
);
|
||||
|
|
@ -205,6 +230,7 @@ describe('LocalizeManager', () => {
|
|||
manager = new LocalizeManager();
|
||||
|
||||
await manager.loadNamespace({
|
||||
/** @param {string} locale */
|
||||
'my-component': locale => fakeImport(`./my-component/${locale}.js`),
|
||||
});
|
||||
|
||||
|
|
@ -220,6 +246,7 @@ describe('LocalizeManager', () => {
|
|||
|
||||
try {
|
||||
await manager.loadNamespace({
|
||||
/** @param {string} locale */
|
||||
'my-component': locale => fakeImport(`./my-component/${locale}.js`),
|
||||
});
|
||||
} catch (e) {
|
||||
|
|
@ -242,6 +269,7 @@ describe('LocalizeManager', () => {
|
|||
setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Hello!' } });
|
||||
|
||||
await manager.loadNamespace({
|
||||
/** @param {string} locale */
|
||||
'my-component': locale => fakeImport(`./my-component/${locale}.js`),
|
||||
});
|
||||
|
||||
|
|
@ -259,6 +287,7 @@ describe('LocalizeManager', () => {
|
|||
setupFakeImport('./my-component/en.js', { default: { greeting: 'Hello!' } });
|
||||
|
||||
await manager.loadNamespace({
|
||||
/** @param {string} locale */
|
||||
'my-component': locale => fakeImport(`./my-component/${locale}.js`),
|
||||
});
|
||||
|
||||
|
|
@ -275,6 +304,7 @@ describe('LocalizeManager', () => {
|
|||
|
||||
try {
|
||||
await manager.loadNamespace({
|
||||
/** @param {string} locale */
|
||||
'my-component': locale => fakeImport(`./my-component/${locale}.js`),
|
||||
});
|
||||
} catch (e) {
|
||||
|
|
@ -315,10 +345,14 @@ describe('LocalizeManager', () => {
|
|||
|
||||
manager = new LocalizeManager();
|
||||
|
||||
manager.setupNamespaceLoader('my-component', async locale => {
|
||||
const response = await fetch(`./my-component/${locale}.json`);
|
||||
return response.json();
|
||||
});
|
||||
manager.setupNamespaceLoader(
|
||||
'my-component',
|
||||
/** @param {string} locale */
|
||||
async locale => {
|
||||
const response = await fetch(`./my-component/${locale}.json`);
|
||||
return response.json();
|
||||
},
|
||||
);
|
||||
|
||||
await manager.loadNamespace('my-component');
|
||||
|
||||
|
|
@ -330,26 +364,43 @@ describe('LocalizeManager', () => {
|
|||
});
|
||||
|
||||
it('loads multiple namespaces via loadNamespaces() using string routes', async () => {
|
||||
fetchMock.get('./my-defaults/en-GB.json', { submit: 'Submit' });
|
||||
fetchMock.get('./my-send-button/en-GB.json', { submit: 'Send' });
|
||||
fetchMock.get('./my-defaults/en-GB.json', {
|
||||
submit: 'Submit',
|
||||
});
|
||||
fetchMock.get('./my-send-button/en-GB.json', {
|
||||
submit: 'Send',
|
||||
});
|
||||
|
||||
manager = new LocalizeManager();
|
||||
|
||||
manager.setupNamespaceLoader('my-defaults', async locale => {
|
||||
const response = await fetch(`./my-defaults/${locale}.json`);
|
||||
return response.json();
|
||||
});
|
||||
manager.setupNamespaceLoader('my-send-button', async locale => {
|
||||
const response = await fetch(`./my-send-button/${locale}.json`);
|
||||
return response.json();
|
||||
});
|
||||
manager.setupNamespaceLoader(
|
||||
'my-defaults',
|
||||
/** @param {string} locale */
|
||||
async locale => {
|
||||
const response = await fetch(`./my-defaults/${locale}.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']);
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
'en-GB': {
|
||||
'my-send-button': { submit: 'Send' },
|
||||
'my-defaults': { submit: 'Submit' },
|
||||
'my-send-button': {
|
||||
submit: 'Send',
|
||||
},
|
||||
'my-defaults': {
|
||||
submit: 'Submit',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -359,10 +410,17 @@ describe('LocalizeManager', () => {
|
|||
|
||||
manager = new LocalizeManager();
|
||||
|
||||
manager.setupNamespaceLoader(/my-.+/, async (locale, namespace) => {
|
||||
const response = await fetch(`./${namespace}/${locale}.json`);
|
||||
return response.json();
|
||||
});
|
||||
manager.setupNamespaceLoader(
|
||||
/my-.+/,
|
||||
/**
|
||||
* @param {string} locale
|
||||
* @param {string} namespace
|
||||
*/
|
||||
async (locale, namespace) => {
|
||||
const response = await fetch(`./${namespace}/${locale}.json`);
|
||||
return response.json();
|
||||
},
|
||||
);
|
||||
|
||||
await manager.loadNamespace('my-component');
|
||||
|
||||
|
|
@ -379,10 +437,17 @@ describe('LocalizeManager', () => {
|
|||
|
||||
manager = new LocalizeManager();
|
||||
|
||||
manager.setupNamespaceLoader(/my-.+/, async (locale, namespace) => {
|
||||
const response = await fetch(`./${namespace}/${locale}.json`);
|
||||
return response.json();
|
||||
});
|
||||
manager.setupNamespaceLoader(
|
||||
/my-.+/,
|
||||
/**
|
||||
* @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']);
|
||||
|
||||
|
|
@ -403,6 +468,7 @@ describe('LocalizeManager', () => {
|
|||
manager = new LocalizeManager({ autoLoadOnLocaleChange: true });
|
||||
|
||||
await manager.loadNamespace({
|
||||
/** @param {string} locale */
|
||||
'my-component': locale => fakeImport(`./my-component/${locale}.js`, 25),
|
||||
});
|
||||
|
||||
|
|
@ -426,6 +492,7 @@ describe('LocalizeManager', () => {
|
|||
manager = new LocalizeManager();
|
||||
|
||||
manager.loadNamespace({
|
||||
/** @param {string} locale */
|
||||
'my-component': locale => fakeImport(`./my-component/${locale}.js`, 25),
|
||||
});
|
||||
expect(manager.__storage).to.deep.equal({});
|
||||
|
|
@ -468,6 +535,7 @@ describe('LocalizeManager', () => {
|
|||
|
||||
let called = 0;
|
||||
await manager.loadNamespace({
|
||||
/** @param {string} locale */
|
||||
'my-component': locale => {
|
||||
called += 1;
|
||||
return fakeImport(`./my-component/${locale}.js`);
|
||||
|
|
@ -527,7 +595,7 @@ describe('LocalizeManager', () => {
|
|||
manager = new LocalizeManager();
|
||||
manager.addData('en-GB', 'my-ns', { greeting: 'Hi!' });
|
||||
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.addData('en-GB', 'my-ns', { greeting: 'Hi {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;
|
||||
const originalLang = document.documentElement.lang;
|
||||
|
||||
/** @param {string} lang */
|
||||
async function simulateGoogleTranslateOn(lang) {
|
||||
document.documentElement.lang = lang;
|
||||
}
|
||||
|
|
@ -569,6 +638,10 @@ describe('When supporting external translation tools like Google Translate', ()
|
|||
document.documentElement.lang = 'auto';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {...*} [cfg]
|
||||
* @returns {LocalizeManager}
|
||||
*/
|
||||
function getInstance(cfg) {
|
||||
LocalizeManager.resetInstance();
|
||||
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', () => {
|
||||
/** @type {LocalizeManager} */
|
||||
let manager;
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -677,16 +751,20 @@ describe('[deprecated] When not supporting external translation tools like Googl
|
|||
});
|
||||
|
||||
it('initializes locale from <html> by default', () => {
|
||||
manager = new LocalizeManager({ supportExternalTranslationTools: false });
|
||||
manager = new LocalizeManager({});
|
||||
expect(manager.locale).to.equal('en-GB');
|
||||
});
|
||||
|
||||
it('fires "localeChanged" event if locale was changed via <html lang> attribute', async () => {
|
||||
manager = new LocalizeManager({ supportExternalTranslationTools: false });
|
||||
manager = new LocalizeManager({});
|
||||
setTimeout(() => {
|
||||
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.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!' } });
|
||||
|
||||
manager = new LocalizeManager({
|
||||
supportExternalTranslationTools: false,
|
||||
autoLoadOnLocaleChange: true,
|
||||
});
|
||||
|
||||
await manager.loadNamespace({
|
||||
/** @param {string} locale */
|
||||
'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';
|
||||
await aTimeout(); // wait for mutation observer to be called
|
||||
await aTimeout(0); // wait for mutation observer to be called
|
||||
await manager.loadingComplete;
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
fixtureSync,
|
||||
html,
|
||||
nextFrame,
|
||||
unsafeStatic,
|
||||
} from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import { localize } from '../src/localize.js';
|
||||
|
|
@ -19,6 +20,10 @@ import {
|
|||
setupFakeImport,
|
||||
} from '../test-helpers.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('../types/LocalizeMixinTypes').LocalizeMixin} LocalizeMixinHost
|
||||
*/
|
||||
|
||||
describe('LocalizeMixin', () => {
|
||||
afterEach(() => {
|
||||
resetFakeImport();
|
||||
|
|
@ -26,21 +31,26 @@ describe('LocalizeMixin', () => {
|
|||
});
|
||||
|
||||
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 {}) {
|
||||
static get localizeNamespaces() {
|
||||
return [myElementNs, ...super.localizeNamespaces];
|
||||
}
|
||||
|
||||
requestUpdate() {}
|
||||
}
|
||||
const tagString = defineCE(
|
||||
// @ts-ignore
|
||||
class MyElement extends LocalizeMixin(LitElement) {
|
||||
static get localizeNamespaces() {
|
||||
return [myElementNs, ...super.localizeNamespaces];
|
||||
}
|
||||
},
|
||||
);
|
||||
const tag = unsafeStatic(tagString);
|
||||
|
||||
setupEmptyFakeImportsFor(['my-element'], ['en-GB']);
|
||||
|
||||
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.calledWith(myElementNs)).to.be.true;
|
||||
|
||||
|
|
@ -48,27 +58,41 @@ describe('LocalizeMixin', () => {
|
|||
});
|
||||
|
||||
it('ignores duplicates in "get localizeNamespaces()" chain', async () => {
|
||||
const defaultNs = { default: loc => fakeImport(`./default/${loc}.js`) };
|
||||
const parentElementNs = { 'parent-element': loc => fakeImport(`./parent-element/${loc}.js`) };
|
||||
const childElementNs = { 'child-element': loc => fakeImport(`./child-element/${loc}.js`) };
|
||||
const defaultNs = {
|
||||
/** @param {string} loc */
|
||||
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() {
|
||||
return [parentElementNs, defaultNs, ...super.localizeNamespaces];
|
||||
}
|
||||
}
|
||||
|
||||
class ChildElement extends LocalizeMixin(ParentElement) {
|
||||
static get localizeNamespaces() {
|
||||
return [childElementNs, defaultNs, ...super.localizeNamespaces];
|
||||
}
|
||||
}
|
||||
const tagString = defineCE(
|
||||
// @ts-ignore
|
||||
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']);
|
||||
|
||||
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.calledWith(childElementNs)).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 () => {
|
||||
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() {
|
||||
return [myElementNs, ...super.localizeNamespaces];
|
||||
}
|
||||
|
||||
requestUpdate() {}
|
||||
|
||||
onLocaleReady() {}
|
||||
}
|
||||
const tagString = defineCE(MyElement);
|
||||
|
||||
setupEmptyFakeImportsFor(['my-element'], ['en-GB']);
|
||||
|
||||
const element = new MyElement();
|
||||
const onLocaleReadySpy = sinon.spy(element, 'onLocaleReady');
|
||||
const el = /** @type {MyElement} */ (document.createElement(tagString));
|
||||
const wrapper = await fixture('<div></div>');
|
||||
const onLocaleReadySpy = sinon.spy(el, 'onLocaleReady');
|
||||
|
||||
await localize.loadingComplete;
|
||||
expect(onLocaleReadySpy.callCount).to.equal(0);
|
||||
|
||||
element.connectedCallback();
|
||||
wrapper.appendChild(el);
|
||||
|
||||
await localize.loadingComplete;
|
||||
expect(onLocaleReadySpy.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
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() {
|
||||
return [myElementNs, ...super.localizeNamespaces];
|
||||
}
|
||||
|
||||
requestUpdate() {}
|
||||
|
||||
onLocaleChanged() {}
|
||||
}
|
||||
|
||||
const tagString = defineCE(MyOtherElement);
|
||||
|
||||
setupEmptyFakeImportsFor(['my-element'], ['en-GB', 'nl-NL', 'ru-RU']);
|
||||
|
||||
const element = new MyElement();
|
||||
const onLocaleChangedSpy = sinon.spy(element, 'onLocaleChanged');
|
||||
const el = /** @type {MyOtherElement} */ (document.createElement(tagString));
|
||||
const wrapper = await fixture('<div></div>');
|
||||
const onLocaleChangedSpy = sinon.spy(el, 'onLocaleChanged');
|
||||
|
||||
await localize.loadingComplete;
|
||||
|
||||
|
|
@ -128,33 +159,40 @@ describe('LocalizeMixin', () => {
|
|||
await localize.loadingComplete;
|
||||
expect(onLocaleChangedSpy.callCount).to.equal(0);
|
||||
|
||||
element.connectedCallback();
|
||||
wrapper.appendChild(el);
|
||||
|
||||
localize.locale = 'ru-RU';
|
||||
await localize.loadingComplete;
|
||||
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 () => {
|
||||
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() {
|
||||
return [myElementNs, ...super.localizeNamespaces];
|
||||
}
|
||||
|
||||
requestUpdate() {}
|
||||
|
||||
onLocaleUpdated() {}
|
||||
}
|
||||
|
||||
const tagString = defineCE(MyElement);
|
||||
|
||||
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');
|
||||
|
||||
el.connectedCallback();
|
||||
wrapper.appendChild(el);
|
||||
await el.localizeNamespacesLoaded;
|
||||
expect(onLocaleUpdatedSpy.callCount).to.equal(1);
|
||||
|
||||
|
|
@ -162,8 +200,11 @@ describe('LocalizeMixin', () => {
|
|||
expect(onLocaleUpdatedSpy.callCount).to.equal(2);
|
||||
});
|
||||
|
||||
it('should have the localizeNamespacesLoaded avaliable within "onLocaleUpdated()"', async () => {
|
||||
const myElementNs = { 'my-element': locale => fakeImport(`./my-element/${locale}.js`) };
|
||||
it('should have the localizeNamespacesLoaded available within "onLocaleUpdated()"', async () => {
|
||||
const myElementNs = {
|
||||
/** @param {string} locale */
|
||||
'my-element': locale => fakeImport(`./my-element/${locale}.js`),
|
||||
};
|
||||
setupFakeImport('./my-element/en-GB.js', {
|
||||
default: {
|
||||
label: 'one',
|
||||
|
|
@ -174,13 +215,13 @@ describe('LocalizeMixin', () => {
|
|||
label: 'two',
|
||||
},
|
||||
});
|
||||
class MyElement extends LocalizeMixin(class {}) {
|
||||
|
||||
// @ts-ignore
|
||||
class MyElement extends LocalizeMixin(LitElement) {
|
||||
static get localizeNamespaces() {
|
||||
return [myElementNs, ...super.localizeNamespaces];
|
||||
}
|
||||
|
||||
requestUpdate() {}
|
||||
|
||||
async onLocaleUpdated() {
|
||||
super.onLocaleUpdated();
|
||||
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();
|
||||
|
||||
await el.localizeNamespacesLoaded;
|
||||
|
|
@ -201,19 +243,22 @@ describe('LocalizeMixin', () => {
|
|||
});
|
||||
|
||||
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() {
|
||||
return [myElementNs, ...super.localizeNamespaces];
|
||||
}
|
||||
|
||||
requestUpdate() {}
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
el.connectedCallback();
|
||||
|
|
@ -224,23 +269,29 @@ describe('LocalizeMixin', () => {
|
|||
});
|
||||
|
||||
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', {
|
||||
default: {
|
||||
greeting: 'Hi!',
|
||||
},
|
||||
});
|
||||
|
||||
class MyElement extends LocalizeMixin(class {}) {
|
||||
// @ts-ignore
|
||||
class MyElement extends LocalizeMixin(LitElement) {
|
||||
static get 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 messageDirective = element.msgLit('my-element:greeting');
|
||||
const messageDirective = el.msgLit('my-element:greeting');
|
||||
expect(lionLocalizeMessageSpy.callCount).to.equal(0);
|
||||
|
||||
expect(isDirective(messageDirective)).to.be.true;
|
||||
|
|
@ -250,7 +301,7 @@ describe('LocalizeMixin', () => {
|
|||
expect(lionLocalizeMessageSpy.callCount).to.equal(1);
|
||||
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(typeof message).to.equal('string');
|
||||
expect(lionLocalizeMessageSpy.callCount).to.equal(2);
|
||||
|
|
@ -259,22 +310,25 @@ describe('LocalizeMixin', () => {
|
|||
lionLocalizeMessageSpy.restore();
|
||||
});
|
||||
|
||||
it('has a Promise "localizeNamespacesLoaded" which resolves once tranlations are available', async () => {
|
||||
const myElementNs = { 'my-element': locale => fakeImport(`./my-element/${locale}.js`, 25) };
|
||||
it('has a Promise "localizeNamespacesLoaded" which resolves once translations are available', async () => {
|
||||
const myElementNs = {
|
||||
/** @param {string} locale */
|
||||
'my-element': locale => fakeImport(`./my-element/${locale}.js`, 25),
|
||||
};
|
||||
setupFakeImport('./my-element/en-GB.js', {
|
||||
default: {
|
||||
greeting: 'Hi!',
|
||||
},
|
||||
});
|
||||
|
||||
class MyElement extends LocalizeMixin(class {}) {
|
||||
// @ts-ignore
|
||||
class MyElement extends LocalizeMixin(LitElement) {
|
||||
static get 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');
|
||||
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 () => {
|
||||
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', {
|
||||
default: {
|
||||
greeting: 'Hi!',
|
||||
},
|
||||
});
|
||||
|
||||
const tag = defineCE(
|
||||
class extends LocalizeMixin(LitElement) {
|
||||
static get localizeNamespaces() {
|
||||
return [myElementNs, ...super.localizeNamespaces];
|
||||
}
|
||||
// @ts-ignore
|
||||
class MyLocalizedClass extends LocalizeMixin(LitElement) {
|
||||
static get localizeNamespaces() {
|
||||
return [myElementNs, ...super.localizeNamespaces];
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<p>${this.msgLit('my-element:greeting')}</p>`;
|
||||
}
|
||||
},
|
||||
);
|
||||
render() {
|
||||
return html`<p>${this.msgLit('my-element:greeting')}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
const el = await fixtureSync(`<${tag}></${tag}>`);
|
||||
expect(el.shadowRoot.children.length).to.equal(0);
|
||||
await el.updateComplete;
|
||||
expect(el.shadowRoot.querySelector('p').innerText).to.equal('Hi!');
|
||||
const tag = defineCE(MyLocalizedClass);
|
||||
const el = /** @type {MyLocalizedClass} */ (await fixtureSync(`<${tag}></${tag}>`));
|
||||
expect(el.shadowRoot).to.exist;
|
||||
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 () => {
|
||||
const myElementNs = { 'my-element': locale => fakeImport(`./my-element/${locale}.js`, 25) };
|
||||
it('re-render on locale change once all translations are loaded (if BaseElement supports it)', async () => {
|
||||
const myElementNs = {
|
||||
/** @param {string} locale */
|
||||
'my-element': locale => fakeImport(`./my-element/${locale}.js`, 25),
|
||||
};
|
||||
setupFakeImport('./my-element/en-GB.js', {
|
||||
default: {
|
||||
greeting: 'Hi!',
|
||||
|
|
@ -322,57 +389,69 @@ describe('LocalizeMixin', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const tag = defineCE(
|
||||
class TestPromise extends LocalizeMixin(LitElement) {
|
||||
static get localizeNamespaces() {
|
||||
return [myElementNs, ...super.localizeNamespaces];
|
||||
}
|
||||
// @ts-ignore
|
||||
class MyLocalizedClass extends LocalizeMixin(LitElement) {
|
||||
static get localizeNamespaces() {
|
||||
return [myElementNs, ...super.localizeNamespaces];
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<p>${this.msgLit('my-element:greeting')}</p>`;
|
||||
}
|
||||
},
|
||||
);
|
||||
render() {
|
||||
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;
|
||||
expect(el.shadowRoot.querySelector('p').innerText).to.equal('Hi!');
|
||||
|
||||
localize.locale = 'en-US';
|
||||
expect(el.shadowRoot.querySelector('p').innerText).to.equal('Hi!');
|
||||
await el.updateComplete;
|
||||
expect(el.shadowRoot.querySelector('p').innerText).to.equal('Howdy!');
|
||||
expect(el.shadowRoot).to.exist;
|
||||
if (el.shadowRoot) {
|
||||
const p = /** @type {HTMLParagraphElement} */ (el.shadowRoot.querySelector('p'));
|
||||
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 () => {
|
||||
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', {
|
||||
default: {
|
||||
greeting: 'Hi!',
|
||||
},
|
||||
});
|
||||
|
||||
const tag = defineCE(
|
||||
class extends LocalizeMixin(LitElement) {
|
||||
static get waitForLocalizeNamespaces() {
|
||||
return false;
|
||||
}
|
||||
// @ts-ignore
|
||||
class MyLocalizedClass extends LocalizeMixin(LitElement) {
|
||||
static get waitForLocalizeNamespaces() {
|
||||
return false;
|
||||
}
|
||||
|
||||
static get localizeNamespaces() {
|
||||
return [myElementNs, ...super.localizeNamespaces];
|
||||
}
|
||||
static get localizeNamespaces() {
|
||||
return [myElementNs, ...super.localizeNamespaces];
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<p>${this.msgLit('my-element:greeting')}</p>`;
|
||||
}
|
||||
},
|
||||
);
|
||||
render() {
|
||||
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;
|
||||
expect(el.shadowRoot.querySelector('p').innerText).to.equal('');
|
||||
await el.localizeNamespacesLoaded;
|
||||
await el.updateComplete;
|
||||
expect(el.shadowRoot.querySelector('p').innerText).to.equal('Hi!');
|
||||
expect(el.shadowRoot).to.exist;
|
||||
if (el.shadowRoot) {
|
||||
const p = /** @type {HTMLParagraphElement} */ (el.shadowRoot.querySelector('p'));
|
||||
expect(p.innerText).to.equal('');
|
||||
await el.localizeNamespacesLoaded;
|
||||
await el.updateComplete;
|
||||
expect(p.innerText).to.equal('Hi!');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -57,10 +57,10 @@ describe('formatDate', () => {
|
|||
locale: 'en-US',
|
||||
};
|
||||
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.');
|
||||
|
||||
date = parseDate('1970-11-3');
|
||||
date = /** @type {Date} */ (parseDate('1970-11-3'));
|
||||
expect(formatDate(date, options)).to.equal('Tuesday, November 03, 1970');
|
||||
});
|
||||
|
||||
|
|
@ -73,13 +73,13 @@ describe('formatDate', () => {
|
|||
locale: 'en-US',
|
||||
};
|
||||
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 г.');
|
||||
|
||||
date = parseDate('13-1-1940');
|
||||
date = /** @type {Date} */ (parseDate('13-1-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');
|
||||
});
|
||||
|
||||
|
|
@ -92,13 +92,13 @@ describe('formatDate', () => {
|
|||
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');
|
||||
|
||||
date = parseDate('1-13-1940');
|
||||
date = /** @type {Date} */ (parseDate('1-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');
|
||||
});
|
||||
|
||||
|
|
@ -110,10 +110,10 @@ describe('formatDate', () => {
|
|||
day: '2-digit',
|
||||
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');
|
||||
|
||||
parsedDate = parseDate('01-01-1940');
|
||||
parsedDate = /** @type {Date} */ (parseDate('01-01-1940'));
|
||||
options = {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
|
|
@ -130,7 +130,7 @@ describe('formatDate', () => {
|
|||
month: 'long',
|
||||
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');
|
||||
});
|
||||
|
||||
|
|
@ -140,7 +140,7 @@ describe('formatDate', () => {
|
|||
year: 'numeric',
|
||||
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');
|
||||
});
|
||||
|
||||
|
|
@ -150,12 +150,13 @@ describe('formatDate', () => {
|
|||
year: 'numeric',
|
||||
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');
|
||||
});
|
||||
|
||||
it('returns empty string when input is not a Date object', async () => {
|
||||
const date = '1-1-2016';
|
||||
// @ts-ignore tests what happens if you use a wrong type
|
||||
expect(formatDate(date)).to.equal('');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ import { expect } from '@open-wc/testing';
|
|||
|
||||
import { getMonthNames } from '../../src/date/getMonthNames.js';
|
||||
|
||||
/**
|
||||
* @param {TemplateStringsArray} strings
|
||||
*/
|
||||
function s(strings) {
|
||||
return strings[0].split(' ');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ import { expect } from '@open-wc/testing';
|
|||
|
||||
import { getWeekdayNames } from '../../src/date/getWeekdayNames.js';
|
||||
|
||||
/**
|
||||
* @param {TemplateStringsArray} strings
|
||||
*/
|
||||
function s(strings) {
|
||||
return strings[0].split(' ');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,15 @@ import { localizeTearDown } from '../../test-helpers.js';
|
|||
|
||||
import { parseDate } from '../../src/date/parseDate.js';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Date | undefined} value
|
||||
* @param {Date} date
|
||||
*/
|
||||
function equalsDate(value, date) {
|
||||
return (
|
||||
Object.prototype.toString.call(value) === '[object Date]' && // is Date Object
|
||||
value &&
|
||||
value.getDate() === date.getDate() && // day
|
||||
value.getMonth() === date.getMonth() && // month
|
||||
value.getFullYear() === date.getFullYear() // year
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ describe('isLocalizeESModule', () => {
|
|||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { LocalizeManager } from '../src/LocalizeManager.js';
|
|||
import { localize, setLocalize } from '../src/localize.js';
|
||||
|
||||
describe('localize', () => {
|
||||
// this is an importan mindset:
|
||||
// this is an important mindset:
|
||||
// we don't test the singleton
|
||||
// we check that it is an instance of the right class
|
||||
// we test newly created instances of this class separately
|
||||
|
|
@ -26,9 +26,11 @@ describe('localize', () => {
|
|||
const oldLocalizeTeardown = localize.teardown;
|
||||
localize.teardown = sinon.spy();
|
||||
|
||||
const newLocalize = { teardown: () => {} };
|
||||
const newLocalize = /** @type {LocalizeManager} */ ({ teardown: () => {} });
|
||||
setLocalize(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);
|
||||
|
||||
setLocalize(oldLocalize);
|
||||
|
|
|
|||
|
|
@ -3,14 +3,22 @@ import { localize } from '../../src/localize.js';
|
|||
import { formatNumber } from '../../src/number/formatNumber.js';
|
||||
import { localizeTearDown } from '../../test-helpers.js';
|
||||
|
||||
const currencyCode = currency => ({ style: 'currency', currencyDisplay: 'code', currency });
|
||||
const currencySymbol = currency => ({ style: 'currency', currencyDisplay: 'symbol', currency });
|
||||
const currencyCode = /** @param {string} currency */ currency => ({
|
||||
style: 'currency',
|
||||
currencyDisplay: 'code',
|
||||
currency,
|
||||
});
|
||||
const currencySymbol = /** @param {string} currency */ currency => ({
|
||||
style: 'currency',
|
||||
currencyDisplay: 'symbol',
|
||||
currency,
|
||||
});
|
||||
|
||||
describe('formatNumber', () => {
|
||||
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', () => {
|
||||
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('EUR')))).to.equal('123,456.79');
|
||||
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');
|
||||
});
|
||||
|
||||
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('');
|
||||
});
|
||||
|
||||
it('returns empty string when number is undefined', () => {
|
||||
// @ts-ignore tests what happens if you pass wrong type
|
||||
expect(formatNumber(undefined)).to.equal('');
|
||||
});
|
||||
|
||||
|
|
@ -66,12 +73,14 @@ describe('formatNumber', () => {
|
|||
const savedReturnIfNaN = localize.formatNumberOptions.returnIfNaN;
|
||||
|
||||
localize.formatNumberOptions.returnIfNaN = '-';
|
||||
// @ts-ignore
|
||||
expect(formatNumber('foo')).to.equal('-');
|
||||
|
||||
localize.formatNumberOptions.returnIfNaN = savedReturnIfNaN;
|
||||
});
|
||||
|
||||
it("can set what to returns when NaN via `returnIfNaN: 'foo'`", () => {
|
||||
// @ts-ignore
|
||||
expect(formatNumber('foo', { returnIfNaN: '-' })).to.equal('-');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,26 @@
|
|||
import { expect } from '@open-wc/testing';
|
||||
import { localize } from '../../src/localize.js';
|
||||
import { localizeTearDown } from '../../test-helpers.js';
|
||||
|
||||
import { formatNumberToParts } from '../../src/number/formatNumberToParts.js';
|
||||
|
||||
const c = v => ({ type: 'currency', value: v });
|
||||
const d = v => ({ type: 'decimal', value: v });
|
||||
const i = v => ({ type: 'integer', value: v });
|
||||
const f = v => ({ type: 'fraction', value: v });
|
||||
const g = v => ({ type: 'group', value: v });
|
||||
const l = v => ({ type: 'literal', value: v });
|
||||
const c = /** @param {string} v */ v => ({
|
||||
type: 'currency',
|
||||
value: v,
|
||||
});
|
||||
const d = /** @param {string} v */ v => ({ type: 'decimal', 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 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', () => {
|
||||
afterEach(localizeTearDown);
|
||||
|
|
@ -32,12 +40,14 @@ describe('formatNumberToParts', () => {
|
|||
];
|
||||
|
||||
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(
|
||||
formatNumberToParts(amount, {
|
||||
locale,
|
||||
formatNumberToParts(Number(amount), {
|
||||
style: 'currency',
|
||||
currency,
|
||||
locale: String(locale),
|
||||
currency: String(currency),
|
||||
}),
|
||||
).to.deep.equal(expectedResult);
|
||||
});
|
||||
|
|
@ -59,13 +69,15 @@ describe('formatNumberToParts', () => {
|
|||
];
|
||||
|
||||
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(
|
||||
formatNumberToParts(amount, {
|
||||
locale,
|
||||
formatNumberToParts(Number(amount), {
|
||||
style: 'currency',
|
||||
currencyDisplay: 'code',
|
||||
currency,
|
||||
locale: String(locale),
|
||||
currency: String(currency),
|
||||
}),
|
||||
).to.deep.equal(expectedResult);
|
||||
});
|
||||
|
|
@ -74,6 +86,7 @@ describe('formatNumberToParts', () => {
|
|||
|
||||
describe("style: 'decimal'", () => {
|
||||
describe('no minimumFractionDigits', () => {
|
||||
/** @type {Array.<Array.<?>>} */
|
||||
const specs = [
|
||||
['en-GB', 3500, [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]) => {
|
||||
it(`formats ${locale} ${amount} as "${stringifyParts(expectedResult)}"`, () => {
|
||||
it(`formats ${locale} ${amount} as "${stringifyParts(
|
||||
/** @type {FormatNumberPart[]} */ (expectedResult),
|
||||
)}"`, () => {
|
||||
localize.locale = locale;
|
||||
expect(
|
||||
formatNumberToParts(amount, {
|
||||
formatNumberToParts(Number(amount), {
|
||||
style: 'decimal',
|
||||
}),
|
||||
).to.deep.equal(expectedResult);
|
||||
|
|
@ -100,6 +115,7 @@ describe('formatNumberToParts', () => {
|
|||
});
|
||||
|
||||
describe('minimumFractionDigits: 2', () => {
|
||||
/** @type {Array.<Array.<?>>} */
|
||||
const specs = [
|
||||
['en-GB', 3500, [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]) => {
|
||||
it(`formats ${locale} ${amount} as "${stringifyParts(expectedResult)}"`, () => {
|
||||
it(`formats ${locale} ${amount} as "${stringifyParts(
|
||||
/** @type {FormatNumberPart[]} */ (expectedResult),
|
||||
)}"`, () => {
|
||||
localize.locale = locale;
|
||||
expect(
|
||||
formatNumberToParts(amount, {
|
||||
formatNumberToParts(Number(amount), {
|
||||
style: 'decimal',
|
||||
minimumFractionDigits: 2,
|
||||
}),
|
||||
|
|
@ -142,12 +160,14 @@ describe('formatNumberToParts', () => {
|
|||
];
|
||||
|
||||
specs.forEach(([locale, amount, expectedResult]) => {
|
||||
it(`formats ${locale} ${amount} as "${stringifyParts(expectedResult)}"`, () => {
|
||||
it(`formats ${locale} ${amount} as "${stringifyParts(
|
||||
/** @type {FormatNumberPart[]} */ (expectedResult),
|
||||
)}"`, () => {
|
||||
expect(
|
||||
formatNumberToParts(amount / 100, {
|
||||
locale,
|
||||
formatNumberToParts(Number(amount) / 100, {
|
||||
style: 'percent',
|
||||
minimumFractionDigits: 2,
|
||||
locale: String(locale),
|
||||
}),
|
||||
).to.deep.equal(expectedResult);
|
||||
});
|
||||
|
|
|
|||
52
packages/localize/types/LocalizeMixinTypes.d.ts
vendored
Normal file
52
packages/localize/types/LocalizeMixinTypes.d.ts
vendored
Normal 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
8
packages/localize/types/index.d.ts
vendored
Normal 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;
|
||||
}
|
||||
|
|
@ -3,6 +3,11 @@ export class SingletonManagerClass {
|
|||
this._map = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {any} value
|
||||
* @throws {Error} Will throw if the key is already defined
|
||||
*/
|
||||
set(key, value) {
|
||||
if (this.has(key)) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @returns
|
||||
*/
|
||||
get(key) {
|
||||
return this._map.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
has(key) {
|
||||
return this._map.has(key);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,15 +10,24 @@
|
|||
"strict": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
"types": ["node", "mocha"],
|
||||
"types": ["node", "mocha", "sinon"],
|
||||
"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": [
|
||||
"node_modules",
|
||||
"**/node_modules/*",
|
||||
"**/coverage/*",
|
||||
"**/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/"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,11 +20,13 @@ module.exports = {
|
|||
sessionStartTimeout: 60000,
|
||||
concurrency: 1,
|
||||
coverageConfig: {
|
||||
report: true,
|
||||
reportDir: 'coverage',
|
||||
threshold: {
|
||||
statements: 80,
|
||||
statements: 90,
|
||||
branches: 70,
|
||||
functions: 70,
|
||||
lines: 80,
|
||||
functions: 80,
|
||||
lines: 90,
|
||||
},
|
||||
},
|
||||
browsers: [
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ module.exports = {
|
|||
sessionStartTimeout: 30000,
|
||||
concurrency: 5,
|
||||
coverageConfig: {
|
||||
report: true,
|
||||
reportDir: 'coverage',
|
||||
threshold: {
|
||||
statements: 80,
|
||||
branches: 70,
|
||||
functions: 70,
|
||||
lines: 80,
|
||||
statements: 90,
|
||||
branches: 65,
|
||||
functions: 80,
|
||||
lines: 90,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue