UNPKG

intl-messageformat

Version:

Formats ICU Message strings with number, date, plural, and select placeholders to create localized messages.

327 lines (326 loc) 11.5 kB
import { memoize, strategies } from "@formatjs/fast-memoize"; import { isArgumentElement, isDateElement, isDateTimeSkeleton, isLiteralElement, isNumberElement, isNumberSkeleton, isPluralElement, isPoundElement, isSelectElement, isTagElement, isTimeElement, parse } from "@formatjs/icu-messageformat-parser"; //#region packages/intl-messageformat/error.ts let ErrorCode = /* @__PURE__ */ function(ErrorCode) { ErrorCode["MISSING_VALUE"] = "MISSING_VALUE"; ErrorCode["INVALID_VALUE"] = "INVALID_VALUE"; ErrorCode["MISSING_INTL_API"] = "MISSING_INTL_API"; return ErrorCode; }({}); var FormatError = class extends Error { constructor(msg, code, originalMessage) { super(msg); this.code = code; this.originalMessage = originalMessage; } toString() { return `[formatjs Error: ${this.code}] ${this.message}`; } }; var InvalidValueError = class extends FormatError { constructor(variableId, value, options, originalMessage) { super(`Invalid values for "${variableId}": "${value}". Options are "${Object.keys(options).join("\", \"")}"`, ErrorCode.INVALID_VALUE, originalMessage); } }; var InvalidValueTypeError = class extends FormatError { constructor(value, type, originalMessage) { super(`Value for "${value}" must be of type ${type}`, ErrorCode.INVALID_VALUE, originalMessage); } }; var MissingValueError = class extends FormatError { constructor(variableId, originalMessage) { super(`The intl string context variable "${variableId}" was not provided to the string "${originalMessage}"`, ErrorCode.MISSING_VALUE, originalMessage); } }; //#endregion //#region packages/intl-messageformat/formatters.ts let PART_TYPE = /* @__PURE__ */ function(PART_TYPE) { PART_TYPE[PART_TYPE["literal"] = 0] = "literal"; PART_TYPE[PART_TYPE["object"] = 1] = "object"; return PART_TYPE; }({}); function mergeLiteral(parts) { if (parts.length < 2) return parts; return parts.reduce((all, part) => { const lastPart = all[all.length - 1]; if (!lastPart || lastPart.type !== PART_TYPE.literal || part.type !== PART_TYPE.literal) all.push(part); else lastPart.value += part.value; return all; }, []); } function isFormatXMLElementFn(el) { return typeof el === "function"; } function formatToParts(els, locales, formatters, formats, values, currentPluralValue, originalMessage) { if (els.length === 1 && isLiteralElement(els[0])) return [{ type: PART_TYPE.literal, value: els[0].value }]; const result = []; for (const el of els) { if (isLiteralElement(el)) { result.push({ type: PART_TYPE.literal, value: el.value }); continue; } if (isPoundElement(el)) { if (typeof currentPluralValue === "number") result.push({ type: PART_TYPE.literal, value: formatters.getNumberFormat(locales).format(currentPluralValue) }); continue; } const { value: varName } = el; if (!(values && varName in values)) throw new MissingValueError(varName, originalMessage); let value = values[varName]; if (isArgumentElement(el)) { if (!value || typeof value === "string" || typeof value === "number" || typeof value === "bigint") value = typeof value === "string" || typeof value === "number" || typeof value === "bigint" ? String(value) : ""; result.push({ type: typeof value === "string" ? PART_TYPE.literal : PART_TYPE.object, value }); continue; } if (isDateElement(el)) { const style = typeof el.style === "string" ? formats.date[el.style] : isDateTimeSkeleton(el.style) ? el.style.parsedOptions : void 0; result.push({ type: PART_TYPE.literal, value: formatters.getDateTimeFormat(locales, style).format(value) }); continue; } if (isTimeElement(el)) { const style = typeof el.style === "string" ? formats.time[el.style] : isDateTimeSkeleton(el.style) ? el.style.parsedOptions : formats.time.medium; result.push({ type: PART_TYPE.literal, value: formatters.getDateTimeFormat(locales, style).format(value) }); continue; } if (isNumberElement(el)) { const style = typeof el.style === "string" ? formats.number[el.style] : isNumberSkeleton(el.style) ? el.style.parsedOptions : void 0; if (style && style.scale) { const scale = style.scale || 1; if (typeof value === "bigint") { if (!Number.isInteger(scale)) throw new TypeError(`Cannot apply fractional scale ${scale} to bigint value. Scale must be an integer when formatting bigint.`); value = value * BigInt(scale); } else value = value * scale; } result.push({ type: PART_TYPE.literal, value: formatters.getNumberFormat(locales, style).format(value) }); continue; } if (isTagElement(el)) { const { children, value } = el; const formatFn = values[value]; if (!isFormatXMLElementFn(formatFn)) throw new InvalidValueTypeError(value, "function", originalMessage); let chunks = formatFn(formatToParts(children, locales, formatters, formats, values, currentPluralValue).map((p) => p.value)); if (!Array.isArray(chunks)) chunks = [chunks]; result.push(...chunks.map((c) => { return { type: typeof c === "string" ? PART_TYPE.literal : PART_TYPE.object, value: c }; })); } if (isSelectElement(el)) { const key = value; const opt = (Object.prototype.hasOwnProperty.call(el.options, key) ? el.options[key] : void 0) || el.options.other; if (!opt) throw new InvalidValueError(el.value, value, Object.keys(el.options), originalMessage); result.push(...formatToParts(opt.value, locales, formatters, formats, values)); continue; } if (isPluralElement(el)) { const exactKey = `=${value}`; let opt = Object.prototype.hasOwnProperty.call(el.options, exactKey) ? el.options[exactKey] : void 0; if (!opt) { if (!Intl.PluralRules) throw new FormatError(`Intl.PluralRules is not available in this environment. Try polyfilling it using "@formatjs/intl-pluralrules" `, ErrorCode.MISSING_INTL_API, originalMessage); const numericValue = typeof value === "bigint" ? Number(value) : value; const rule = formatters.getPluralRules(locales, { type: el.pluralType }).select(numericValue - (el.offset || 0)); opt = (Object.prototype.hasOwnProperty.call(el.options, rule) ? el.options[rule] : void 0) || el.options.other; } if (!opt) throw new InvalidValueError(el.value, value, Object.keys(el.options), originalMessage); const numericValue = typeof value === "bigint" ? Number(value) : value; result.push(...formatToParts(opt.value, locales, formatters, formats, values, numericValue - (el.offset || 0))); continue; } } return mergeLiteral(result); } //#endregion //#region packages/intl-messageformat/core.ts function mergeConfig(c1, c2) { if (!c2) return c1; return { ...c1, ...c2, ...Object.keys(c1).reduce((all, k) => { all[k] = { ...c1[k], ...c2[k] }; return all; }, {}) }; } function mergeConfigs(defaultConfig, configs) { if (!configs) return defaultConfig; return Object.keys(defaultConfig).reduce((all, k) => { all[k] = mergeConfig(defaultConfig[k], configs[k]); return all; }, { ...defaultConfig }); } function createFastMemoizeCache(store) { return { create() { return { get(key) { return store[key]; }, set(key, value) { store[key] = value; } }; } }; } function createDefaultFormatters(cache = { number: {}, dateTime: {}, pluralRules: {} }) { return { getNumberFormat: memoize((...args) => new Intl.NumberFormat(...args), { cache: createFastMemoizeCache(cache.number), strategy: strategies.variadic }), getDateTimeFormat: memoize((...args) => new Intl.DateTimeFormat(...args), { cache: createFastMemoizeCache(cache.dateTime), strategy: strategies.variadic }), getPluralRules: memoize((...args) => new Intl.PluralRules(...args), { cache: createFastMemoizeCache(cache.pluralRules), strategy: strategies.variadic }) }; } var IntlMessageFormat = class IntlMessageFormat { constructor(message, locales = IntlMessageFormat.defaultLocale, overrideFormats, opts) { this.formatterCache = { number: {}, dateTime: {}, pluralRules: {} }; this.format = (values) => { const parts = this.formatToParts(values); if (parts.length === 1) return parts[0].value; const result = parts.reduce((all, part) => { if (!all.length || part.type !== PART_TYPE.literal || typeof all[all.length - 1] !== "string") all.push(part.value); else all[all.length - 1] += part.value; return all; }, []); if (result.length <= 1) return result[0] || ""; return result; }; this.formatToParts = (values) => formatToParts(this.ast, this.locales, this.formatters, this.formats, values, void 0, this.message); this.resolvedOptions = () => ({ locale: this.resolvedLocale?.toString() || Intl.NumberFormat.supportedLocalesOf(this.locales)[0] }); this.getAst = () => this.ast; this.locales = locales; this.resolvedLocale = IntlMessageFormat.resolveLocale(locales); if (typeof message === "string") { this.message = message; if (!IntlMessageFormat.__parse) throw new TypeError("IntlMessageFormat.__parse must be set to process `message` of type `string`"); const { ...parseOpts } = opts || {}; this.ast = IntlMessageFormat.__parse(message, { ...parseOpts, locale: this.resolvedLocale }); } else this.ast = message; if (!Array.isArray(this.ast)) throw new TypeError("A message must be provided as a String or AST."); this.formats = mergeConfigs(IntlMessageFormat.formats, overrideFormats); this.formatters = opts && opts.formatters || createDefaultFormatters(this.formatterCache); } static { this.memoizedDefaultLocale = null; } static get defaultLocale() { if (!IntlMessageFormat.memoizedDefaultLocale) IntlMessageFormat.memoizedDefaultLocale = new Intl.NumberFormat().resolvedOptions().locale; return IntlMessageFormat.memoizedDefaultLocale; } static { this.resolveLocale = (locales) => { if (typeof Intl.Locale === "undefined") return; const supportedLocales = Intl.NumberFormat.supportedLocalesOf(locales); if (supportedLocales.length > 0) return new Intl.Locale(supportedLocales[0]); return new Intl.Locale(typeof locales === "string" ? locales : locales[0]); }; } static { this.__parse = parse; } static { this.formats = { number: { integer: { maximumFractionDigits: 0 }, currency: { style: "currency" }, percent: { style: "percent" } }, date: { short: { month: "numeric", day: "numeric", year: "2-digit" }, medium: { month: "short", day: "numeric", year: "numeric" }, long: { month: "long", day: "numeric", year: "numeric" }, full: { weekday: "long", month: "long", day: "numeric", year: "numeric" } }, time: { short: { hour: "numeric", minute: "numeric" }, medium: { hour: "numeric", minute: "numeric", second: "numeric" }, long: { hour: "numeric", minute: "numeric", second: "numeric", timeZoneName: "short" }, full: { hour: "numeric", minute: "numeric", second: "numeric", timeZoneName: "short" } } }; } }; //#endregion //#region packages/intl-messageformat/index.ts var intl_messageformat_default = IntlMessageFormat; //#endregion export { ErrorCode, FormatError, IntlMessageFormat, InvalidValueError, InvalidValueTypeError, MissingValueError, PART_TYPE, intl_messageformat_default as default, formatToParts, isFormatXMLElementFn }; //# sourceMappingURL=index.js.map