UNPKG

ember-intl

Version:

Internationalization for Ember projects

432 lines (403 loc) 11.3 kB
import { cancel, next } from '@ember/runloop'; import Service from '@ember/service'; import { isHTMLSafe, htmlSafe } from '@ember/template'; import { tracked } from '@glimmer/tracking'; import { createIntlCache, createIntl } from '@formatjs/intl'; import { getOwner } from '@ember/owner'; import { g, i } from 'decorator-transforms/runtime-esm'; function formatDate(intlShape, ...[value, formatOptions]) { return intlShape.formatDate(value, formatOptions); } function formatDateRange(intlShape, ...[from, to, formatOptions]) { return intlShape.formatDateTimeRange(from, to, formatOptions); } function formatDisplayName(intlShape, ...[value, formatOptions]) { return intlShape.formatDisplayName(value, formatOptions) ?? ''; } function formatList(intlShape, ...[value, formatOptions]) { return intlShape.formatList(value, formatOptions); } function formatMessage(intlShape, ...[descriptor, parameters]) { return intlShape.formatMessage(descriptor, parameters, { ignoreTag: true }); } function formatNumber(intlShape, ...[value, formatOptions]) { return intlShape.formatNumber(value, formatOptions); } function formatRelativeTime(intlShape, ...[value, unit, formatOptions]) { return intlShape.formatRelativeTime(value, unit, formatOptions); } function formatTime(intlShape, ...[value, formatOptions]) { return intlShape.formatTime(value, formatOptions); } function convertToFormatjsFormats(formats) { const formatjsFormats = { dateTimeRange: formats.formatDateRange, date: formats.formatDate, number: formats.formatNumber, relative: formats.formatRelativeTime, time: formats.formatTime }; return formatjsFormats; } const escaped = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#x27;', '`': '&#x60;', '=': '&#x3D;' }; const needToEscape = /[&<>"'`=]/; const badCharacters = /[&<>"'`=]/g; // https://github.com/emberjs/ember.js/blob/v5.12.0/packages/%40ember/-internals/glimmer/lib/utils/string.ts#L103-L118 function escapeExpression(value) { if (!needToEscape.test(value)) { return value; } return value.replace(badCharacters, character => { return escaped[character]; }); } /** * @private */ function escapeFormatMessageOptions(options) { const escapedOptions = {}; for (const [key, value] of Object.entries(options)) { let newValue; if (isHTMLSafe(value)) { /* Cast `value`, an instance of `SafeString`, to a string using `.toHTML()`. Since `value` is assumed to be safe, we don't need to call `escapeExpression()`. */ newValue = value.toHTML(); } else if (typeof value === 'string') { newValue = escapeExpression(value); } else { newValue = value; } // @ts-expect-error: Type not specific enough escapedOptions[key] = newValue; } return escapedOptions; } /** * @private */ function getHtmlElement(context) { const owner = getOwner(context); if (owner === undefined) { return undefined; } const documentService = owner.lookup('service:-document'); return documentService?.documentElement; } /** * @private */ function convertToArray(locale) { if (Array.isArray(locale)) { return locale; } return [locale]; } /** * @private */ function convertToString(locale) { if (Array.isArray(locale)) { return locale[0]; } return locale; } /** * @private */ function hasLocaleChanged(locale1, locale2) { if (!Array.isArray(locale2)) { return true; } return locale1.toString() !== locale2.toString(); } /** * @private */ function normalizeLocale(locale) { return locale.replace(/_/g, '-').toLowerCase(); } /** * @private */ function flattenKeys(object) { const result = {}; for (const key in object) { if (!Object.prototype.hasOwnProperty.call(object, key)) { continue; } const value = object[key]; // If `value` is not `null` if (value && typeof value === 'object') { const hash = flattenKeys(value); for (const suffix in hash) { const translation = hash[suffix]; if (typeof translation !== 'undefined') { result[`${key}.${suffix}`] = translation; } } } else { if (typeof value !== 'undefined') { result[key] = value; } } } return result; } class IntlService extends Service { static { g(this.prototype, "_intls", [tracked], function () { return {}; }); } #_intls = (i(this, "_intls"), void 0); static { g(this.prototype, "_locale", [tracked]); } #_locale = (i(this, "_locale"), void 0); _cache = createIntlCache(); _formats = {}; _onFormatjsError = error => { switch (error.code) { // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison case 'MISSING_DATA': { console.warn(error.message); break; } // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison case 'MISSING_TRANSLATION': { // Do nothing break; } default: { throw error; } } }; _onMissingTranslation = (key, locales) => { const locale = locales.join(', '); return `Missing translation "${key}" for locale "${locale}"`; }; _timer; get locales() { return Object.keys(this._intls); } get primaryLocale() { if (!this._locale) { return; } return this._locale[0]; } addTranslations(locale, translations) { const messages = flattenKeys(translations); this.updateIntl(locale, messages); } createIntl(locale, messages = {}) { const resolvedLocale = convertToString(locale); const formats = convertToFormatjsFormats(this._formats); return createIntl({ defaultFormats: formats, defaultLocale: resolvedLocale, formats, locale: resolvedLocale, // @ts-expect-error: Type 'Record<string, unknown>' is not assignable messages, onError: this._onFormatjsError }, this._cache); } exists(key, locale) { const locales = locale ? convertToArray(locale) : this._locale; return locales.some(locale => { return this.getTranslation(key, locale) !== undefined; }); } formatDate(value, options) { if (value === undefined || value === null) { return ''; } const intlShape = this.getIntlShape(options?.locale); return formatDate(intlShape, value, options); } formatDateRange(from, to, options) { if (from === undefined || from === null) { return ''; } if (to === undefined || to === null) { return ''; } const intlShape = this.getIntlShape(options?.locale); return formatDateRange(intlShape, from, to, options); } formatDisplayName(value, options) { if (value === undefined || value === null) { return ''; } const intlShape = this.getIntlShape(options?.locale); return formatDisplayName(intlShape, value, options); } formatList(value, options) { if (value === undefined || value === null) { return ''; } const intlShape = this.getIntlShape(options?.locale); return formatList(intlShape, value, options); } formatMessage(value, options) { if (value === undefined || value === null) { return ''; } const intlShape = this.getIntlShape(options?.locale); const descriptor = typeof value === 'object' ? value : { defaultMessage: value, description: undefined, id: value }; if (options?.htmlSafe) { const output = formatMessage(intlShape, descriptor, escapeFormatMessageOptions(options)); return htmlSafe(output); } return formatMessage(intlShape, descriptor, options); } formatNumber(value, options) { if (value === undefined || value === null) { return ''; } const intlShape = this.getIntlShape(options?.locale); return formatNumber(intlShape, value, options); } formatRelativeTime(value, options) { if (value === undefined || value === null) { return ''; } const intlShape = this.getIntlShape(options?.locale); return formatRelativeTime(intlShape, value, options?.unit, options); } formatTime(value, options) { if (value === undefined || value === null) { return ''; } const intlShape = this.getIntlShape(options?.locale); return formatTime(intlShape, value, options); } getIntl(locale) { const resolvedLocale = normalizeLocale(convertToString(locale)); return this._intls[resolvedLocale]; } getIntlShape(locale) { if (locale) { return this.createIntl(locale); } return this.getIntl(this._locale); } getTranslation(key, locale) { const messages = this.getIntl(locale)?.messages; if (!messages) { return; } return messages[key]; } setFormats(formats) { this._formats = formats; // Call `updateIntl` to update `formats` for each locale this.locales.forEach(locale => { this.updateIntl(locale, {}); }); } setLocale(locale) { const proposedLocale = convertToArray(locale); if (hasLocaleChanged(proposedLocale, this._locale)) { this._locale = proposedLocale; // eslint-disable-next-line ember/no-runloop cancel(this._timer); // eslint-disable-next-line ember/no-runloop this._timer = next(() => { this.updateDocumentLanguage(); }); } this.updateIntl(proposedLocale); } setOnFormatjsError(onFormatjsError) { this._onFormatjsError = onFormatjsError; // Call `updateIntl` to update `onError` for each locale this.locales.forEach(locale => { this.updateIntl(locale, {}); }); } setOnMissingTranslation(onMissingTranslation) { this._onMissingTranslation = onMissingTranslation; } t(key, options) { const locales = options?.locale ? [options.locale] : this._locale; let translation; for (const locale of locales) { translation = this.getTranslation(key, locale); if (translation !== undefined) { break; } } if (translation === undefined) { return this._onMissingTranslation(key, locales, options); } // Bypass @formatjs/intl if (translation === '') { return ''; } return this.formatMessage({ defaultMessage: translation, id: key }, options); } updateDocumentLanguage() { const html = getHtmlElement(this); const { primaryLocale } = this; if (!html || !primaryLocale) { return; } html.setAttribute('lang', primaryLocale); } updateIntl(locale, messages) { const resolvedLocale = normalizeLocale(convertToString(locale)); const intl = this._intls[resolvedLocale]; let newIntl; if (!intl) { newIntl = this.createIntl(resolvedLocale, messages); } else if (messages) { newIntl = this.createIntl(resolvedLocale, { ...(intl.messages ?? {}), ...messages }); } if (!newIntl) { return; } this._intls = { ...this._intls, [resolvedLocale]: newIntl }; } willDestroy() { super.willDestroy(); // eslint-disable-next-line ember/no-runloop cancel(this._timer); } } // DO NOT DELETE: this is how TypeScript knows how to look up your services. export { IntlService as default }; //# sourceMappingURL=intl.js.map