@formatjs/intl-listformat
Version:
Formats JS list in a i18n-safe way
218 lines (217 loc) • 7.22 kB
JavaScript
import { CanonicalizeLocaleList, getInternalSlot, GetOption, GetOptionsObject, invariant, isLiteralPart, PartitionPattern, setInternalSlot, SupportedLocales } from "@formatjs/ecma402-abstract";
import { ResolveLocale } from "@formatjs/intl-localematcher";
function validateInstance(instance, method) {
if (!(instance instanceof ListFormat)) {
throw new TypeError(`Method Intl.ListFormat.prototype.${method} called on incompatible receiver ${String(instance)}`);
}
}
/**
* https://tc39.es/proposal-intl-list-format/#sec-createstringlistfromiterable
* @param iterable list
*/
function stringListFromIterable(iterable) {
if (typeof iterable !== "object") return [];
const elements = [];
const iterator = iterable[Symbol.iterator]();
let result;
while (true) {
result = iterator.next();
if (result.done) break;
if (typeof result.value !== "string") {
const nextValue = result.value;
throw new TypeError(`Iterable yielded ${nextValue} which is not a string`);
}
elements.push(result.value);
}
return elements;
}
function createPartsFromList(internalSlotMap, lf, list) {
const size = list.length;
if (size === 0) {
return [];
}
if (size === 2) {
const pattern = getInternalSlot(internalSlotMap, lf, "templatePair");
const first = {
type: "element",
value: list[0]
};
const second = {
type: "element",
value: list[1]
};
return deconstructPattern(pattern, {
"0": first,
"1": second
});
}
const last = {
type: "element",
value: list[size - 1]
};
let parts = last;
let i = size - 2;
while (i >= 0) {
let pattern;
if (i === 0) {
pattern = getInternalSlot(internalSlotMap, lf, "templateStart");
} else if (i < size - 2) {
pattern = getInternalSlot(internalSlotMap, lf, "templateMiddle");
} else {
pattern = getInternalSlot(internalSlotMap, lf, "templateEnd");
}
const head = {
type: "element",
value: list[i]
};
parts = deconstructPattern(pattern, {
"0": head,
"1": parts
});
i--;
}
return parts;
}
function deconstructPattern(pattern, placeables) {
const patternParts = PartitionPattern(pattern);
const result = [];
for (const patternPart of patternParts) {
const { type: part } = patternPart;
if (isLiteralPart(patternPart)) {
result.push({
type: "literal",
value: patternPart.value
});
} else {
invariant(part in placeables, `${part} is missing from placables`);
const subst = placeables[part];
if (Array.isArray(subst)) {
result.push(...subst);
} else {
result.push(subst);
}
}
}
return result;
}
export default class ListFormat {
constructor(locales, options) {
// test262/test/intl402/ListFormat/constructor/constructor/newtarget-undefined.js
// Cannot use `new.target` bc of IE11 & TS transpiles it to something else
const newTarget = this && this instanceof ListFormat ? this.constructor : void 0;
if (!newTarget) {
throw new TypeError("Intl.ListFormat must be called with 'new'");
}
setInternalSlot(ListFormat.__INTERNAL_SLOT_MAP__, this, "initializedListFormat", true);
const requestedLocales = CanonicalizeLocaleList(locales);
const opt = Object.create(null);
const opts = GetOptionsObject(options);
const matcher = GetOption(opts, "localeMatcher", "string", ["best fit", "lookup"], "best fit");
opt.localeMatcher = matcher;
const { localeData } = ListFormat;
const r = ResolveLocale(ListFormat.availableLocales, requestedLocales, opt, ListFormat.relevantExtensionKeys, localeData, ListFormat.getDefaultLocale);
setInternalSlot(ListFormat.__INTERNAL_SLOT_MAP__, this, "locale", r.locale);
const type = GetOption(opts, "type", "string", [
"conjunction",
"disjunction",
"unit"
], "conjunction");
setInternalSlot(ListFormat.__INTERNAL_SLOT_MAP__, this, "type", type);
const style = GetOption(opts, "style", "string", [
"long",
"short",
"narrow"
], "long");
setInternalSlot(ListFormat.__INTERNAL_SLOT_MAP__, this, "style", style);
const { dataLocale } = r;
const dataLocaleData = localeData[dataLocale];
invariant(!!dataLocaleData, `Missing locale data for ${dataLocale}`);
const dataLocaleTypes = dataLocaleData[type];
const templates = dataLocaleTypes[style];
setInternalSlot(ListFormat.__INTERNAL_SLOT_MAP__, this, "templatePair", templates.pair);
setInternalSlot(ListFormat.__INTERNAL_SLOT_MAP__, this, "templateStart", templates.start);
setInternalSlot(ListFormat.__INTERNAL_SLOT_MAP__, this, "templateMiddle", templates.middle);
setInternalSlot(ListFormat.__INTERNAL_SLOT_MAP__, this, "templateEnd", templates.end);
}
format(elements) {
validateInstance(this, "format");
let result = "";
const parts = createPartsFromList(ListFormat.__INTERNAL_SLOT_MAP__, this, stringListFromIterable(elements));
if (!Array.isArray(parts)) {
return parts.value;
}
for (const p of parts) {
result += p.value;
}
return result;
}
formatToParts(elements) {
validateInstance(this, "format");
const parts = createPartsFromList(ListFormat.__INTERNAL_SLOT_MAP__, this, stringListFromIterable(elements));
if (!Array.isArray(parts)) {
return [parts];
}
const result = [];
for (const part of parts) {
result.push({ ...part });
}
return result;
}
resolvedOptions() {
validateInstance(this, "resolvedOptions");
return {
locale: getInternalSlot(ListFormat.__INTERNAL_SLOT_MAP__, this, "locale"),
type: getInternalSlot(ListFormat.__INTERNAL_SLOT_MAP__, this, "type"),
style: getInternalSlot(ListFormat.__INTERNAL_SLOT_MAP__, this, "style")
};
}
static supportedLocalesOf(locales, options) {
// test262/test/intl402/ListFormat/constructor/supportedLocalesOf/result-type.js
return SupportedLocales(ListFormat.availableLocales, CanonicalizeLocaleList(locales), options);
}
static __addLocaleData(...data) {
for (const { data: d, locale } of data) {
const minimizedLocale = new Intl.Locale(locale).minimize().toString();
ListFormat.localeData[locale] = ListFormat.localeData[minimizedLocale] = d;
ListFormat.availableLocales.add(minimizedLocale);
ListFormat.availableLocales.add(locale);
if (!ListFormat.__defaultLocale) {
ListFormat.__defaultLocale = minimizedLocale;
}
}
}
static localeData = {};
static availableLocales = new Set();
static __defaultLocale = "";
static getDefaultLocale() {
return ListFormat.__defaultLocale;
}
static relevantExtensionKeys = [];
static polyfilled = true;
static __INTERNAL_SLOT_MAP__ = new WeakMap();
}
try {
// IE11 does not have Symbol
if (typeof Symbol !== "undefined") {
Object.defineProperty(ListFormat.prototype, Symbol.toStringTag, {
value: "Intl.ListFormat",
writable: false,
enumerable: false,
configurable: true
});
}
// https://github.com/tc39/test262/blob/master/test/intl402/ListFormat/constructor/length.js
Object.defineProperty(ListFormat.prototype.constructor, "length", {
value: 0,
writable: false,
enumerable: false,
configurable: true
});
// https://github.com/tc39/test262/blob/master/test/intl402/ListFormat/constructor/supportedLocalesOf/length.js
Object.defineProperty(ListFormat.supportedLocalesOf, "length", {
value: 1,
writable: false,
enumerable: false,
configurable: true
});
} catch {}