UNPKG

@formatjs/intl-pluralrules

Version:
224 lines (223 loc) 8.55 kB
import { CanonicalizeLocaleList, SupportedLocales, ToIntlMathematicalValue } from "@formatjs/ecma402-abstract"; import "./abstract/GetOperands.js"; import { InitializePluralRules } from "./abstract/InitializePluralRules.js"; import { ResolvePlural } from "./abstract/ResolvePlural.js"; import { ResolvePluralRange } from "./abstract/ResolvePluralRange.js"; import getInternalSlots from "./get_internal_slots.js"; function validateInstance(instance, method) { if (!(instance instanceof PluralRules)) { throw new TypeError(`Method Intl.PluralRules.prototype.${method} called on incompatible receiver ${String(instance)}`); } } /** * http://ecma-international.org/ecma-402/7.0/index.html#sec-pluralruleselect * @param locale * @param type * @param _n * @param param3 */ function PluralRuleSelect(locale, type, _n, { IntegerDigits, NumberOfFractionDigits, FractionDigits, CompactExponent }) { // Always pass a string to the compiled function to preserve precision for huge numbers return PluralRules.localeData[locale].fn(NumberOfFractionDigits ? `${IntegerDigits}.${FractionDigits}` : String(IntegerDigits), type === "ordinal", CompactExponent); } /** * PluralRuleSelectRange ( locale, type, notation, compactDisplay, start, end ) * * Implementation-defined abstract operation that determines the plural category for a range * by consulting CLDR plural range data. Each locale defines how different combinations of * start and end plural categories map to a range plural category. * * Examples from CLDR: * - English: "one" + "other" → "other" (e.g., "1-2 items") * - French: "one" + "one" → "one" (e.g., "0-1 vue") * - Arabic: "few" + "many" → "many" (e.g., complex range rules) * * The spec allows this to be implementation-defined, and we use CLDR supplemental data * from pluralRanges.json which provides explicit mappings for each locale. * * @param locale - BCP 47 locale identifier * @param type - "cardinal" or "ordinal" * @param xp - Start plural category * @param yp - End plural category * @returns The plural category for the range */ function PluralRuleSelectRange(locale, type, xp, yp) { const localeData = PluralRules.localeData[locale]; if (!localeData || !localeData.pluralRanges) { // Fallback: If no range data is available, return the end category. // This is a reasonable default as the end value often determines the plural form. return yp; } // Construct lookup key: "start_end" (e.g., "one_other", "few_many") const key = `${xp}_${yp}`; // Select the appropriate range data based on type (cardinal vs ordinal) const rangeData = type === "ordinal" ? localeData.pluralRanges.ordinal : localeData.pluralRanges.cardinal; // Look up the result, falling back to end category if not found return rangeData?.[key] ?? yp; } export class PluralRules { constructor(locales, options) { // test262/test/intl402/RelativeTimeFormat/constructor/constructor/newtarget-undefined.js // Cannot use `new.target` bc of IE11 & TS transpiles it to something else const newTarget = this && this instanceof PluralRules ? this.constructor : void 0; if (!newTarget) { throw new TypeError("Intl.PluralRules must be called with 'new'"); } return InitializePluralRules(this, locales, options, { availableLocales: PluralRules.availableLocales, relevantExtensionKeys: PluralRules.relevantExtensionKeys, localeData: PluralRules.localeData, getDefaultLocale: PluralRules.getDefaultLocale, getInternalSlots }); } resolvedOptions() { validateInstance(this, "resolvedOptions"); const opts = Object.create(null); const internalSlots = getInternalSlots(this); opts.locale = internalSlots.locale; opts.type = internalSlots.type; [ "minimumIntegerDigits", "minimumFractionDigits", "maximumFractionDigits", "minimumSignificantDigits", "maximumSignificantDigits" ].forEach((field) => { const val = internalSlots[field]; if (val !== undefined) { opts[field] = val; } }); opts.pluralCategories = [...PluralRules.localeData[opts.locale].categories[opts.type]]; return opts; } select(val) { validateInstance(this, "select"); // Use ToIntlMathematicalValue which handles bigint per ECMA-402 // https://tc39.es/ecma402/#sec-intl.pluralrules.prototype.select const n = ToIntlMathematicalValue(val); return ResolvePlural(this, n, { getInternalSlots, PluralRuleSelect }); } /** * Intl.PluralRules.prototype.selectRange ( start, end ) * * Returns a string indicating which plural rule applies to a range of numbers. * This is useful for formatting ranges like "1-2 items" vs "2-3 items" where * different languages have different plural rules for ranges. * * Specification: https://tc39.es/ecma402/#sec-intl.pluralrules.prototype.selectrange * * @param start - The start value of the range (number or bigint) * @param end - The end value of the range (number or bigint) * @returns The plural category for the range (zero, one, two, few, many, or other) * * @example * const pr = new Intl.PluralRules('en'); * pr.selectRange(1, 2); // "other" (English: "1-2 items") * pr.selectRange(1, 1); // "one" (same value: "1 item") * * @example * const prFr = new Intl.PluralRules('fr'); * prFr.selectRange(0, 1); // "one" (French: "0-1 vue") * prFr.selectRange(1, 2); // "other" (French: "1-2 vues") * * @example * // BigInt support (spec-compliant, but Chrome has a bug as of early 2025) * pr.selectRange(BigInt(1), BigInt(2)); // "other" * * @throws {TypeError} If start or end is undefined * @throws {RangeError} If start or end is not a finite number (Infinity, NaN) * * @note Chrome's native implementation (as of early 2025) has a bug where it throws * "Cannot convert a BigInt value to a number" when using BigInt arguments. This is * a browser bug - the spec requires BigInt support. This polyfill handles BigInt correctly. */ selectRange(start, end) { validateInstance(this, "selectRange"); // Spec: https://tc39.es/ecma402/#sec-intl.pluralrules.prototype.selectrange // 1. Let pr be the this value. // 2. Perform ? RequireInternalSlot(pr, [[InitializedPluralRules]]). // (Validation is done by validateInstance above) // 3. If start is undefined or end is undefined, throw a TypeError exception. if (start === undefined || end === undefined) { throw new TypeError("selectRange requires both start and end arguments"); } // 4. Let x be ? ToIntlMathematicalValue(start). const x = ToIntlMathematicalValue(start); // 5. Let y be ? ToIntlMathematicalValue(end). const y = ToIntlMathematicalValue(end); // 6. Return ? ResolvePluralRange(pr, x, y). return ResolvePluralRange(this, x, y, { getInternalSlots, PluralRuleSelect, PluralRuleSelectRange }); } toString() { return "[object Intl.PluralRules]"; } static supportedLocalesOf(locales, options) { return SupportedLocales(PluralRules.availableLocales, CanonicalizeLocaleList(locales), options); } static __addLocaleData(...data) { for (const { data: d, locale } of data) { PluralRules.localeData[locale] = d; PluralRules.availableLocales.add(locale); if (!PluralRules.__defaultLocale) { PluralRules.__defaultLocale = locale; } } } static localeData = {}; static availableLocales = new Set(); static __defaultLocale = ""; static getDefaultLocale() { return PluralRules.__defaultLocale; } static relevantExtensionKeys = []; static polyfilled = true; } try { // IE11 does not have Symbol if (typeof Symbol !== "undefined") { Object.defineProperty(PluralRules.prototype, Symbol.toStringTag, { value: "Intl.PluralRules", writable: false, enumerable: false, configurable: true }); } try { // https://github.com/tc39/test262/blob/master/test/intl402/PluralRules/length.js Object.defineProperty(PluralRules, "length", { value: 0, writable: false, enumerable: false, configurable: true }); } catch {} // https://github.com/tc39/test262/blob/master/test/intl402/RelativeTimeFormat/constructor/length.js Object.defineProperty(PluralRules.prototype.constructor, "length", { value: 0, writable: false, enumerable: false, configurable: true }); // https://github.com/tc39/test262/blob/master/test/intl402/RelativeTimeFormat/constructor/supportedLocalesOf/length.js Object.defineProperty(PluralRules.supportedLocalesOf, "length", { value: 1, writable: false, enumerable: false, configurable: true }); Object.defineProperty(PluralRules, "name", { value: "PluralRules", writable: false, enumerable: false, configurable: true }); } catch {}