diff --git a/.changeset/new-oranges-knock.md b/.changeset/new-oranges-knock.md new file mode 100644 index 000000000..3b5091d84 --- /dev/null +++ b/.changeset/new-oranges-knock.md @@ -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. diff --git a/.changeset/pretty-mice-wonder.md b/.changeset/pretty-mice-wonder.md new file mode 100644 index 000000000..33971dbc4 --- /dev/null +++ b/.changeset/pretty-mice-wonder.md @@ -0,0 +1,5 @@ +--- +'singleton-manager': patch +--- + +Added basic JSDocs types to SingletonManager, in order for localize to be able to be typed correctly. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 22dfc3e20..63c12aa1c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 }} diff --git a/packages/localize/src/LocalizeManager.js b/packages/localize/src/LocalizeManager.js index 86610800c..faa9840a6 100644 --- a/packages/localize/src/LocalizeManager.js +++ b/packages/localize/src/LocalizeManager.js @@ -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.>} */ this.__storage = {}; + + /** @type {Map.} */ this.__namespacePatternsMap = new Map(); + + /** @type {Object.} */ this.__namespaceLoadersCache = {}; + + /** @type {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.} + */ 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.} + */ 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.} + */ 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.} [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.} + */ _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.} */ (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.} + * @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.} 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.} + */ _loadAllMissing(newLocale, oldLocale) { const oldLocaleNamespaces = this.__storage[oldLocale] || {}; const newLocaleNamespaces = this.__storage[newLocale] || {}; + /** @type {Promise[]} */ 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} message + * @param {string} name + * @returns {string} + */ + (message, name) => (typeof message === 'object' ? message[name] : message), + messages, + ); + + return String(result || ''); } } diff --git a/packages/localize/src/LocalizeMixin.js b/packages/localize/src/LocalizeMixin.js index 3abd1cbec..aa74f8be7 100644 --- a/packages/localize/src/LocalizeMixin.js +++ b/packages/localize/src/LocalizeMixin.js @@ -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.[]} + */ + 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.} + */ + 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.} 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); diff --git a/packages/localize/src/date/addLeadingZero.js b/packages/localize/src/date/addLeadingZero.js index 67aecbd7a..dc05496ee 100644 --- a/packages/localize/src/date/addLeadingZero.js +++ b/packages/localize/src/date/addLeadingZero.js @@ -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); diff --git a/packages/localize/src/date/clean.js b/packages/localize/src/date/clean.js index fe817867a..a5e65ac7c 100644 --- a/packages/localize/src/date/clean.js +++ b/packages/localize/src/date/clean.js @@ -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 diff --git a/packages/localize/src/date/formatDate.js b/packages/localize/src/date/formatDate.js index a0239eeb8..ed191435c 100644 --- a/packages/localize/src/date/formatDate.js +++ b/packages/localize/src/date/formatDate.js @@ -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: diff --git a/packages/localize/src/date/getDateFormatBasedOnLocale.js b/packages/localize/src/date/getDateFormatBasedOnLocale.js index a4e44cdd1..6e216b53d 100644 --- a/packages/localize/src/date/getDateFormatBasedOnLocale.js +++ b/packages/localize/src/date/getDateFormatBasedOnLocale.js @@ -3,13 +3,28 @@ import { splitDate } from './splitDate.js'; /** * To compute the localized date format - * * @returns {string} */ export function getDateFormatBasedOnLocale() { + /** + * + * @param {ArrayLike.} 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.} */ + 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]}`; } diff --git a/packages/localize/src/date/getMonthNames.js b/packages/localize/src/date/getMonthNames.js index 66841bb24..716db16c7 100644 --- a/packages/localize/src/date/getMonthNames.js +++ b/packages/localize/src/date/getMonthNames.js @@ -1,12 +1,14 @@ import { normalizeIntlDate } from './normalizeIntlDate.js'; +/** @type {Object.>} */ 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]; diff --git a/packages/localize/src/date/getWeekdayNames.js b/packages/localize/src/date/getWeekdayNames.js index a48d5bcaf..d96f8dd14 100644 --- a/packages/localize/src/date/getWeekdayNames.js +++ b/packages/localize/src/date/getWeekdayNames.js @@ -1,24 +1,32 @@ import { normalizeIntlDate } from './normalizeIntlDate.js'; +/** @type {Object.>} */ 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.} - 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]; diff --git a/packages/localize/src/date/normalizeDateTime.js b/packages/localize/src/date/normalizeDateTime.js index 290b79df1..d5420dbb6 100644 --- a/packages/localize/src/date/normalizeDateTime.js +++ b/packages/localize/src/date/normalizeDateTime.js @@ -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()); } diff --git a/packages/localize/src/date/normalizeIntlDate.js b/packages/localize/src/date/normalizeIntlDate.js index c606f44f9..e3573724f 100644 --- a/packages/localize/src/date/normalizeIntlDate.js +++ b/packages/localize/src/date/normalizeIntlDate.js @@ -1,7 +1,7 @@ /** * To filter out some added characters in IE * - * @param str + * @param {string} str * @returns {string} */ export function normalizeIntlDate(str) { diff --git a/packages/localize/src/date/pad.js b/packages/localize/src/date/pad.js index f618bedeb..54d1b5911 100644 --- a/packages/localize/src/date/pad.js +++ b/packages/localize/src/date/pad.js @@ -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); } diff --git a/packages/localize/src/date/parseDate.js b/packages/localize/src/date/parseDate.js index 0ea60cda7..5aa6eefef 100644 --- a/packages/localize/src/date/parseDate.js +++ b/packages/localize/src/date/parseDate.js @@ -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.} */ 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; diff --git a/packages/localize/src/date/sanitizedDateTimeFormat.js b/packages/localize/src/date/sanitizedDateTimeFormat.js index 4c734e10a..c31a79b63 100644 --- a/packages/localize/src/date/sanitizedDateTimeFormat.js +++ b/packages/localize/src/date/sanitizedDateTimeFormat.js @@ -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); diff --git a/packages/localize/src/date/splitDate.js b/packages/localize/src/date/splitDate.js index 6c09d9429..94ce81692 100644 --- a/packages/localize/src/date/splitDate.js +++ b/packages/localize/src/date/splitDate.js @@ -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. | 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})/); } diff --git a/packages/localize/src/date/trim.js b/packages/localize/src/date/trim.js index b6add67b2..e20304267 100644 --- a/packages/localize/src/date/trim.js +++ b/packages/localize/src/date/trim.js @@ -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, ''); diff --git a/packages/localize/src/isLocalizeESModule.js b/packages/localize/src/isLocalizeESModule.js index e894f5aed..bbe631ea1 100644 --- a/packages/localize/src/isLocalizeESModule.js +++ b/packages/localize/src/isLocalizeESModule.js @@ -1,3 +1,7 @@ +/** + * @param {Object.} obj + * @returns {boolean} + */ export default function isLocalizeESModule(obj) { return !!(obj && obj.default && typeof obj.default === 'object' && Object.keys(obj).length === 1); } diff --git a/packages/localize/src/localize.js b/packages/localize/src/localize.js index 6f0d4ab62..ead5dac76 100644 --- a/packages/localize/src/localize.js +++ b/packages/localize/src/localize.js @@ -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; diff --git a/packages/localize/src/number/emptyStringWhenNumberNan.js b/packages/localize/src/number/emptyStringWhenNumberNan.js index 427a72ef1..c077e89f4 100644 --- a/packages/localize/src/number/emptyStringWhenNumberNan.js +++ b/packages/localize/src/number/emptyStringWhenNumberNan.js @@ -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; } diff --git a/packages/localize/src/number/forceAddGroupSeparators.js b/packages/localize/src/number/forceAddGroupSeparators.js index ae198871c..b3528d150 100644 --- a/packages/localize/src/number/forceAddGroupSeparators.js +++ b/packages/localize/src/number/forceAddGroupSeparators.js @@ -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; } diff --git a/packages/localize/src/number/forceCurrencyToEnd.js b/packages/localize/src/number/forceCurrencyToEnd.js index 101cf9dcd..d4f02f5b4 100644 --- a/packages/localize/src/number/forceCurrencyToEnd.js +++ b/packages/localize/src/number/forceCurrencyToEnd.js @@ -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') { diff --git a/packages/localize/src/number/forceENAUSymbols.js b/packages/localize/src/number/forceENAUSymbols.js index 14a583cef..4c9822492 100644 --- a/packages/localize/src/number/forceENAUSymbols.js +++ b/packages/localize/src/number/forceENAUSymbols.js @@ -1,19 +1,25 @@ -export function forceENAUSymbols(formattedParts, options) { +/** @type {Object.} */ +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 = ''; } diff --git a/packages/localize/src/number/forceNormalSpaces.js b/packages/localize/src/number/forceNormalSpaces.js index 444d824f7..b8d083f47 100644 --- a/packages/localize/src/number/forceNormalSpaces.js +++ b/packages/localize/src/number/forceNormalSpaces.js @@ -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({ diff --git a/packages/localize/src/number/forceSpaceBetweenCurrencyCodeAndNumber.js b/packages/localize/src/number/forceSpaceBetweenCurrencyCodeAndNumber.js index cbe9fbde6..0fa2a1271 100644 --- a/packages/localize/src/number/forceSpaceBetweenCurrencyCodeAndNumber.js +++ b/packages/localize/src/number/forceSpaceBetweenCurrencyCodeAndNumber.js @@ -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); diff --git a/packages/localize/src/number/forceSpaceInsteadOfZeroForGroup.js b/packages/localize/src/number/forceSpaceInsteadOfZeroForGroup.js index e968d8958..18c2acc74 100644 --- a/packages/localize/src/number/forceSpaceInsteadOfZeroForGroup.js +++ b/packages/localize/src/number/forceSpaceInsteadOfZeroForGroup.js @@ -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 => { diff --git a/packages/localize/src/number/forceTryCurrencyCode.js b/packages/localize/src/number/forceTryCurrencyCode.js index a9aba4465..54a24875f 100644 --- a/packages/localize/src/number/forceTryCurrencyCode.js +++ b/packages/localize/src/number/forceTryCurrencyCode.js @@ -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'; } diff --git a/packages/localize/src/number/forceYenSymbol.js b/packages/localize/src/number/forceYenSymbol.js index ac01af76f..eee6d0eef 100644 --- a/packages/localize/src/number/forceYenSymbol.js +++ b/packages/localize/src/number/forceYenSymbol.js @@ -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; diff --git a/packages/localize/src/number/formatNumber.js b/packages/localize/src/number/formatNumber.js index 60f227155..1da70f2cf 100644 --- a/packages/localize/src/number/formatNumber.js +++ b/packages/localize/src/number/formatNumber.js @@ -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; } diff --git a/packages/localize/src/number/formatNumberToParts.js b/packages/localize/src/number/formatNumberToParts.js index d109694b9..704e110e0 100644 --- a/packages/localize/src/number/formatNumberToParts.js +++ b/packages/localize/src/number/formatNumberToParts.js @@ -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 }); } diff --git a/packages/localize/src/number/getCurrencyName.js b/packages/localize/src/number/getCurrencyName.js index b2cc8e3dc..45bc00df0 100644 --- a/packages/localize/src/number/getCurrencyName.js +++ b/packages/localize/src/number/getCurrencyName.js @@ -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) diff --git a/packages/localize/src/number/getDecimalSeparator.js b/packages/localize/src/number/getDecimalSeparator.js index 8f2e730a6..8cd0a3400 100644 --- a/packages/localize/src/number/getDecimalSeparator.js +++ b/packages/localize/src/number/getDecimalSeparator.js @@ -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]; } diff --git a/packages/localize/src/number/getFractionDigits.js b/packages/localize/src/number/getFractionDigits.js index c2331b0c9..cd14c5ee0 100644 --- a/packages/localize/src/number/getFractionDigits.js +++ b/packages/localize/src/number/getFractionDigits.js @@ -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; } diff --git a/packages/localize/src/number/getGroupSeparator.js b/packages/localize/src/number/getGroupSeparator.js index 987bea89e..5f97eca6d 100644 --- a/packages/localize/src/number/getGroupSeparator.js +++ b/packages/localize/src/number/getGroupSeparator.js @@ -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]); } diff --git a/packages/localize/src/number/getLocale.js b/packages/localize/src/number/getLocale.js index a8e96b988..0725d2f8d 100644 --- a/packages/localize/src/number/getLocale.js +++ b/packages/localize/src/number/getLocale.js @@ -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) { diff --git a/packages/localize/src/number/normalSpaces.js b/packages/localize/src/number/normalSpaces.js index 80e8eeef6..8a370f4de 100644 --- a/packages/localize/src/number/normalSpaces.js +++ b/packages/localize/src/number/normalSpaces.js @@ -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 ' ' diff --git a/packages/localize/src/number/normalizeCurrencyLabel.js b/packages/localize/src/number/normalizeCurrencyLabel.js index 5054830fc..e8a2e8136 100644 --- a/packages/localize/src/number/normalizeCurrencyLabel.js +++ b/packages/localize/src/number/normalizeCurrencyLabel.js @@ -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; } diff --git a/packages/localize/src/number/normalizeIntl.js b/packages/localize/src/number/normalizeIntl.js index b1859905b..0492be9bd 100644 --- a/packages/localize/src/number/normalizeIntl.js +++ b/packages/localize/src/number/normalizeIntl.js @@ -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); } diff --git a/packages/localize/src/number/roundNumber.js b/packages/localize/src/number/roundNumber.js index 154787a65..c7438e94a 100644 --- a/packages/localize/src/number/roundNumber.js +++ b/packages/localize/src/number/roundNumber.js @@ -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) { diff --git a/packages/localize/test-helpers/fake-imports.js b/packages/localize/test-helpers/fake-imports.js index ca1490464..18deac662 100644 --- a/packages/localize/test-helpers/fake-imports.js +++ b/packages/localize/test-helpers/fake-imports.js @@ -1,11 +1,22 @@ +/** + * @type {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.} + */ export async function fakeImport(path, ms = 0) { const result = fakeImports[path]; if (ms > 0) { diff --git a/packages/localize/test/LocalizeManager.test.js b/packages/localize/test/LocalizeManager.test.js index bb9c978b5..3b9b6543f 100644 --- a/packages/localize/test/LocalizeManager.test.js +++ b/packages/localize/test/LocalizeManager.test.js @@ -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 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 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({ diff --git a/packages/localize/test/LocalizeMixin.test.js b/packages/localize/test/LocalizeMixin.test.js index 72317a4be..cf4a788e0 100644 --- a/packages/localize/test/LocalizeMixin.test.js +++ b/packages/localize/test/LocalizeMixin.test.js @@ -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}>`); - 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}>`); 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('
'); + 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('
'); + 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('
'); 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`

${this.msgLit('my-element:greeting')}

`; - } - }, - ); + render() { + return html`

${this.msgLit('my-element:greeting')}

`; + } + } - const el = await fixtureSync(`<${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}>`)); + 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`

${this.msgLit('my-element:greeting')}

`; - } - }, - ); + render() { + return html`

${this.msgLit('my-element:greeting')}

`; + } + } - const el = await fixture(`<${tag}>`); + const tag = defineCE(MyLocalizedClass); + const el = /** @type {MyLocalizedClass} */ (await fixture(`<${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`

${this.msgLit('my-element:greeting')}

`; - } - }, - ); + render() { + return html`

${this.msgLit('my-element:greeting')}

`; + } + } - const el = await fixture(`<${tag}>`); + const tag = defineCE(MyLocalizedClass); + + const el = /** @type {MyLocalizedClass} */ (await fixture(`<${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!'); + } }); }); diff --git a/packages/localize/test/date/formatDate.test.js b/packages/localize/test/date/formatDate.test.js index 1eb330f63..8ea6368b7 100644 --- a/packages/localize/test/date/formatDate.test.js +++ b/packages/localize/test/date/formatDate.test.js @@ -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(''); }); }); diff --git a/packages/localize/test/date/getMonthNames.test.js b/packages/localize/test/date/getMonthNames.test.js index 538717013..09063f066 100644 --- a/packages/localize/test/date/getMonthNames.test.js +++ b/packages/localize/test/date/getMonthNames.test.js @@ -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(' '); } diff --git a/packages/localize/test/date/getWeekdayNames.test.js b/packages/localize/test/date/getWeekdayNames.test.js index ded662192..3d8ffeecb 100644 --- a/packages/localize/test/date/getWeekdayNames.test.js +++ b/packages/localize/test/date/getWeekdayNames.test.js @@ -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(' '); } diff --git a/packages/localize/test/date/parseDate.test.js b/packages/localize/test/date/parseDate.test.js index 154aaae02..82fec7b5d 100644 --- a/packages/localize/test/date/parseDate.test.js +++ b/packages/localize/test/date/parseDate.test.js @@ -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 diff --git a/packages/localize/test/isLocalizeESModule.test.js b/packages/localize/test/isLocalizeESModule.test.js index 3894a61be..63990ec11 100644 --- a/packages/localize/test/isLocalizeESModule.test.js +++ b/packages/localize/test/isLocalizeESModule.test.js @@ -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); }); }); diff --git a/packages/localize/test/localize.test.js b/packages/localize/test/localize.test.js index b87b77dd8..f7ea31c6c 100644 --- a/packages/localize/test/localize.test.js +++ b/packages/localize/test/localize.test.js @@ -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); diff --git a/packages/localize/test/number/formatNumber.test.js b/packages/localize/test/number/formatNumber.test.js index 17e817e9f..35d6aedcd 100644 --- a/packages/localize/test/number/formatNumber.test.js +++ b/packages/localize/test/number/formatNumber.test.js @@ -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('-'); }); diff --git a/packages/localize/test/number/formatNumberToParts.test.js b/packages/localize/test/number/formatNumberToParts.test.js index 9e34d63e8..e88649194 100644 --- a/packages/localize/test/number/formatNumberToParts.test.js +++ b/packages/localize/test/number/formatNumberToParts.test.js @@ -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.>} */ 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.>} */ 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); }); diff --git a/packages/localize/types/LocalizeMixinTypes.d.ts b/packages/localize/types/LocalizeMixinTypes.d.ts new file mode 100644 index 000000000..a25e65d24 --- /dev/null +++ b/packages/localize/types/LocalizeMixinTypes.d.ts @@ -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; + + /** + * Hook into LitElement to only render once all translations are loaded + */ + public performUpdate(): Promise; + + 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>( + superclass: T, +): T & Constructor & typeof LocalizeMixinHost; + +export type LocalizeMixin = typeof LocalizeMixinImplementation; diff --git a/packages/localize/types/index.d.ts b/packages/localize/types/index.d.ts new file mode 100644 index 000000000..6ae7b1561 --- /dev/null +++ b/packages/localize/types/index.d.ts @@ -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; +} diff --git a/packages/singleton-manager/src/SingletonManagerClass.js b/packages/singleton-manager/src/SingletonManagerClass.js index c35c6c225..49e9b7659 100644 --- a/packages/singleton-manager/src/SingletonManagerClass.js +++ b/packages/singleton-manager/src/SingletonManagerClass.js @@ -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); } diff --git a/tsconfig.json b/tsconfig.json index 02514af21..34468d335 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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/" ] } diff --git a/web-test-runner-browserstack.config.js b/web-test-runner-browserstack.config.js index 104921967..dfb502dc6 100644 --- a/web-test-runner-browserstack.config.js +++ b/web-test-runner-browserstack.config.js @@ -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: [ diff --git a/web-test-runner.config.js b/web-test-runner.config.js index 366c9fa76..04bdee22f 100644 --- a/web-test-runner.config.js +++ b/web-test-runner.config.js @@ -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, }, }, };