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
JavaScript
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