@formatjs/intl-pluralrules 6.1.2 → 6.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/abstract/GetOperands.d.ts +37 -10
  2. package/abstract/GetOperands.js +11 -5
  3. package/abstract/InitializePluralRules.js +15 -0
  4. package/abstract/ResolvePlural.d.ts +31 -1
  5. package/abstract/ResolvePlural.js +64 -10
  6. package/abstract/ResolvePluralRange.d.ts +23 -0
  7. package/abstract/ResolvePluralRange.js +59 -0
  8. package/get_internal_slots.d.ts +0 -2
  9. package/index.d.ts +60 -2
  10. package/index.js +98 -4
  11. package/locale-data/af.js +14 -4
  12. package/locale-data/ak.js +14 -4
  13. package/locale-data/am.js +15 -4
  14. package/locale-data/an.js +14 -4
  15. package/locale-data/ar.js +22 -10
  16. package/locale-data/ars.js +21 -9
  17. package/locale-data/as.js +23 -8
  18. package/locale-data/asa.js +13 -3
  19. package/locale-data/ast.js +14 -4
  20. package/locale-data/az.js +21 -8
  21. package/locale-data/bal.js +15 -2
  22. package/locale-data/be.js +20 -8
  23. package/locale-data/bem.js +13 -3
  24. package/locale-data/bez.js +13 -3
  25. package/locale-data/bg.js +14 -4
  26. package/locale-data/bho.js +13 -3
  27. package/locale-data/bm.js +2 -2
  28. package/locale-data/bn.js +23 -8
  29. package/locale-data/bo.js +2 -2
  30. package/locale-data/br.js +19 -8
  31. package/locale-data/brx.js +13 -3
  32. package/locale-data/bs.js +18 -7
  33. package/locale-data/ca.js +25 -10
  34. package/locale-data/ce.js +13 -3
  35. package/locale-data/ceb.js +15 -4
  36. package/locale-data/cgg.js +13 -3
  37. package/locale-data/chr.js +13 -3
  38. package/locale-data/ckb.js +13 -3
  39. package/locale-data/cs.js +19 -8
  40. package/locale-data/cy.js +32 -14
  41. package/locale-data/da.js +17 -5
  42. package/locale-data/de.js +15 -5
  43. package/locale-data/doi.js +14 -3
  44. package/locale-data/dsb.js +19 -7
  45. package/locale-data/dv.js +13 -3
  46. package/locale-data/dz.js +2 -2
  47. package/locale-data/ee.js +13 -3
  48. package/locale-data/el.js +14 -4
  49. package/locale-data/en.js +22 -8
  50. package/locale-data/eo.js +13 -3
  51. package/locale-data/es.js +19 -7
  52. package/locale-data/et.js +15 -5
  53. package/locale-data/eu.js +14 -4
  54. package/locale-data/fa.js +15 -4
  55. package/locale-data/ff.js +13 -3
  56. package/locale-data/fi.js +15 -5
  57. package/locale-data/fil.js +19 -5
  58. package/locale-data/fo.js +13 -3
  59. package/locale-data/fr.js +21 -7
  60. package/locale-data/fur.js +13 -3
  61. package/locale-data/fy.js +14 -4
  62. package/locale-data/ga.js +22 -9
  63. package/locale-data/gd.js +23 -10
  64. package/locale-data/gl.js +15 -5
  65. package/locale-data/gsw.js +14 -4
  66. package/locale-data/gu.js +23 -8
  67. package/locale-data/guw.js +13 -3
  68. package/locale-data/gv.js +20 -8
  69. package/locale-data/ha.js +13 -3
  70. package/locale-data/haw.js +13 -3
  71. package/locale-data/he.js +17 -7
  72. package/locale-data/hi.js +23 -8
  73. package/locale-data/hnj.js +2 -2
  74. package/locale-data/hr.js +18 -7
  75. package/locale-data/hsb.js +19 -7
  76. package/locale-data/hu.js +16 -4
  77. package/locale-data/hy.js +17 -4
  78. package/locale-data/ia.js +15 -5
  79. package/locale-data/id.js +3 -3
  80. package/locale-data/ig.js +2 -2
  81. package/locale-data/ii.js +2 -2
  82. package/locale-data/io.js +15 -5
  83. package/locale-data/is.js +16 -5
  84. package/locale-data/it.js +21 -7
  85. package/locale-data/iu.js +15 -5
  86. package/locale-data/ja.js +3 -3
  87. package/locale-data/jbo.js +2 -2
  88. package/locale-data/jgo.js +13 -3
  89. package/locale-data/jmc.js +13 -3
  90. package/locale-data/jv.js +2 -2
  91. package/locale-data/jw.js +2 -2
  92. package/locale-data/ka.js +19 -7
  93. package/locale-data/kab.js +13 -3
  94. package/locale-data/kaj.js +13 -3
  95. package/locale-data/kcg.js +13 -3
  96. package/locale-data/kde.js +2 -2
  97. package/locale-data/kea.js +2 -2
  98. package/locale-data/kk.js +16 -5
  99. package/locale-data/kkj.js +13 -3
  100. package/locale-data/kl.js +13 -3
  101. package/locale-data/km.js +3 -3
  102. package/locale-data/kn.js +15 -4
  103. package/locale-data/ko.js +3 -3
  104. package/locale-data/ks.js +13 -3
  105. package/locale-data/ksb.js +13 -3
  106. package/locale-data/ksh.js +15 -5
  107. package/locale-data/ku.js +13 -3
  108. package/locale-data/kw.js +25 -11
  109. package/locale-data/ky.js +14 -4
  110. package/locale-data/lag.js +16 -6
  111. package/locale-data/lb.js +13 -3
  112. package/locale-data/lg.js +13 -3
  113. package/locale-data/lij.js +18 -5
  114. package/locale-data/lkt.js +2 -2
  115. package/locale-data/ln.js +13 -3
  116. package/locale-data/lo.js +14 -4
  117. package/locale-data/lt.js +20 -8
  118. package/locale-data/lv.js +18 -7
  119. package/locale-data/mas.js +13 -3
  120. package/locale-data/mg.js +13 -3
  121. package/locale-data/mgo.js +13 -3
  122. package/locale-data/mk.js +22 -8
  123. package/locale-data/ml.js +14 -4
  124. package/locale-data/mn.js +14 -4
  125. package/locale-data/mo.js +19 -6
  126. package/locale-data/mr.js +20 -7
  127. package/locale-data/ms.js +14 -4
  128. package/locale-data/mt.js +19 -8
  129. package/locale-data/my.js +3 -3
  130. package/locale-data/nah.js +13 -3
  131. package/locale-data/naq.js +15 -5
  132. package/locale-data/nb.js +14 -4
  133. package/locale-data/nd.js +13 -3
  134. package/locale-data/ne.js +16 -5
  135. package/locale-data/nl.js +15 -5
  136. package/locale-data/nn.js +13 -3
  137. package/locale-data/nnh.js +13 -3
  138. package/locale-data/no.js +14 -4
  139. package/locale-data/nqo.js +2 -2
  140. package/locale-data/nr.js +13 -3
  141. package/locale-data/nso.js +13 -3
  142. package/locale-data/ny.js +13 -3
  143. package/locale-data/nyn.js +13 -3
  144. package/locale-data/om.js +13 -3
  145. package/locale-data/or.js +22 -9
  146. package/locale-data/os.js +13 -3
  147. package/locale-data/osa.js +2 -2
  148. package/locale-data/pa.js +14 -4
  149. package/locale-data/pap.js +13 -3
  150. package/locale-data/pcm.js +15 -4
  151. package/locale-data/pl.js +19 -8
  152. package/locale-data/prg.js +17 -6
  153. package/locale-data/ps.js +14 -4
  154. package/locale-data/pt-PT.js +17 -6
  155. package/locale-data/pt.js +18 -7
  156. package/locale-data/rm.js +13 -3
  157. package/locale-data/ro.js +20 -7
  158. package/locale-data/rof.js +13 -3
  159. package/locale-data/ru.js +19 -8
  160. package/locale-data/rwk.js +13 -3
  161. package/locale-data/sah.js +2 -2
  162. package/locale-data/saq.js +13 -3
  163. package/locale-data/sat.js +15 -5
  164. package/locale-data/sc.js +18 -5
  165. package/locale-data/scn.js +21 -7
  166. package/locale-data/sd.js +14 -4
  167. package/locale-data/sdh.js +13 -3
  168. package/locale-data/se.js +15 -5
  169. package/locale-data/seh.js +13 -3
  170. package/locale-data/ses.js +2 -2
  171. package/locale-data/sg.js +2 -2
  172. package/locale-data/sh.js +17 -6
  173. package/locale-data/shi.js +16 -6
  174. package/locale-data/si.js +17 -5
  175. package/locale-data/sk.js +19 -8
  176. package/locale-data/sl.js +19 -8
  177. package/locale-data/sma.js +15 -5
  178. package/locale-data/smi.js +15 -5
  179. package/locale-data/smj.js +15 -5
  180. package/locale-data/smn.js +15 -5
  181. package/locale-data/sms.js +15 -5
  182. package/locale-data/sn.js +13 -3
  183. package/locale-data/so.js +13 -3
  184. package/locale-data/sq.js +18 -7
  185. package/locale-data/sr.js +18 -7
  186. package/locale-data/ss.js +13 -3
  187. package/locale-data/ssy.js +13 -3
  188. package/locale-data/st.js +13 -3
  189. package/locale-data/su.js +2 -2
  190. package/locale-data/sv.js +18 -5
  191. package/locale-data/sw.js +15 -5
  192. package/locale-data/syr.js +13 -3
  193. package/locale-data/ta.js +14 -4
  194. package/locale-data/te.js +14 -4
  195. package/locale-data/teo.js +13 -3
  196. package/locale-data/th.js +3 -3
  197. package/locale-data/ti.js +13 -3
  198. package/locale-data/tig.js +13 -3
  199. package/locale-data/tk.js +16 -5
  200. package/locale-data/tl.js +18 -4
  201. package/locale-data/tn.js +13 -3
  202. package/locale-data/to.js +2 -2
  203. package/locale-data/tpi.js +2 -2
  204. package/locale-data/tr.js +14 -4
  205. package/locale-data/ts.js +13 -3
  206. package/locale-data/tzm.js +13 -4
  207. package/locale-data/ug.js +14 -4
  208. package/locale-data/uk.js +22 -8
  209. package/locale-data/und.js +2 -2
  210. package/locale-data/ur.js +15 -5
  211. package/locale-data/uz.js +14 -4
  212. package/locale-data/ve.js +13 -3
  213. package/locale-data/vi.js +14 -4
  214. package/locale-data/vo.js +13 -3
  215. package/locale-data/vun.js +13 -3
  216. package/locale-data/wa.js +13 -3
  217. package/locale-data/wae.js +13 -3
  218. package/locale-data/wo.js +2 -2
  219. package/locale-data/xh.js +13 -3
  220. package/locale-data/xog.js +13 -3
  221. package/locale-data/yi.js +14 -4
  222. package/locale-data/yo.js +2 -2
  223. package/locale-data/yue.js +3 -3
  224. package/locale-data/zh.js +3 -3
  225. package/locale-data/zu.js +15 -4
  226. package/package.json +7 -7
  227. package/polyfill.iife.js +238 -16
@@ -1,32 +1,59 @@
1
1
  import type Decimal from "decimal.js";
2
+ /**
3
+ * CLDR Spec: Operands as defined in https://unicode.org/reports/tr35/tr35-numbers.html#Operands
4
+ * ECMA-402 Spec: GetOperands abstract operation (https://tc39.es/ecma402/#sec-getoperands)
5
+ *
6
+ * Maps CLDR operand symbols to JavaScript property names:
7
+ * - n → Number (absolute value)
8
+ * - i → IntegerDigits
9
+ * - v → NumberOfFractionDigits
10
+ * - w → NumberOfFractionDigitsWithoutTrailing
11
+ * - f → FractionDigits
12
+ * - t → FractionDigitsWithoutTrailing
13
+ * - c, e → CompactExponent (extension for compact notation)
14
+ */
2
15
  export interface OperandsRecord {
3
16
  /**
4
- * Absolute value of the source number (integer and decimals)
17
+ * CLDR operand: n (absolute value of the source number)
5
18
  */
6
19
  Number: Decimal;
7
20
  /**
8
- * Number of digits of `number`
21
+ * CLDR operand: i (integer digits of n)
22
+ * Implementation: String for very large numbers exceeding Number.MAX_SAFE_INTEGER
9
23
  */
10
- IntegerDigits: number;
24
+ IntegerDigits: number | string;
11
25
  /**
12
- * Number of visible fraction digits in [[Number]], with trailing zeroes.
26
+ * CLDR operand: v (number of visible fraction digits in n, with trailing zeros)
13
27
  */
14
28
  NumberOfFractionDigits: number;
15
29
  /**
16
- * Number of visible fraction digits in [[Number]], without trailing zeroes.
30
+ * CLDR operand: w (number of visible fraction digits in n, without trailing zeros)
17
31
  */
18
32
  NumberOfFractionDigitsWithoutTrailing: number;
19
33
  /**
20
- * Number of visible fractional digits in [[Number]], with trailing zeroes.
34
+ * CLDR operand: f (visible fractional digits in n, with trailing zeros)
21
35
  */
22
36
  FractionDigits: number;
23
37
  /**
24
- * Number of visible fractional digits in [[Number]], without trailing zeroes.
38
+ * CLDR operand: t (visible fractional digits in n, without trailing zeros)
25
39
  */
26
40
  FractionDigitsWithoutTrailing: number;
41
+ /**
42
+ * CLDR operands: c and e (synonyms for compact decimal exponent)
43
+ *
44
+ * Extension: Not in base ECMA-402 spec, but defined in CLDR for compact notation.
45
+ * Example: "1.2M" has exponent 6 (since M = 10^6)
46
+ * Used by 9 locales: ca, es, fr, it, lld, pt, pt-PT, scn, vec
47
+ */
48
+ CompactExponent: number;
27
49
  }
28
50
  /**
29
- * http://ecma-international.org/ecma-402/7.0/index.html#sec-getoperands
30
- * @param s
51
+ * ECMA-402 Spec: GetOperands abstract operation
52
+ * https://tc39.es/ecma402/#sec-getoperands
53
+ *
54
+ * Implementation: Extended to support compact exponent (c/e operands)
55
+ *
56
+ * @param s Formatted number string
57
+ * @param exponent Compact decimal exponent (c/e operand), defaults to 0
31
58
  */
32
- export declare function GetOperands(s: string): OperandsRecord;
59
+ export declare function GetOperands(s: string, exponent?: number): OperandsRecord;
@@ -1,9 +1,14 @@
1
1
  import { invariant, ToNumber, ZERO } from "@formatjs/ecma402-abstract";
2
2
  /**
3
- * http://ecma-international.org/ecma-402/7.0/index.html#sec-getoperands
4
- * @param s
3
+ * ECMA-402 Spec: GetOperands abstract operation
4
+ * https://tc39.es/ecma402/#sec-getoperands
5
+ *
6
+ * Implementation: Extended to support compact exponent (c/e operands)
7
+ *
8
+ * @param s Formatted number string
9
+ * @param exponent Compact decimal exponent (c/e operand), defaults to 0
5
10
  */
6
- export function GetOperands(s) {
11
+ export function GetOperands(s, exponent = 0) {
7
12
  invariant(typeof s === "string", `GetOperands should have been called with a string`);
8
13
  const n = ToNumber(s);
9
14
  invariant(n.isFinite(), "n should be finite");
@@ -35,10 +40,11 @@ export function GetOperands(s) {
35
40
  }
36
41
  return {
37
42
  Number: n,
38
- IntegerDigits: i.toNumber(),
43
+ IntegerDigits: i.lessThanOrEqualTo(Number.MAX_SAFE_INTEGER) && i.greaterThanOrEqualTo(-Number.MAX_SAFE_INTEGER) ? i.toNumber() : i.toString(),
39
44
  NumberOfFractionDigits: v,
40
45
  NumberOfFractionDigitsWithoutTrailing: w,
41
46
  FractionDigits: f.toNumber(),
42
- FractionDigitsWithoutTrailing: t.toNumber()
47
+ FractionDigitsWithoutTrailing: t.toNumber(),
48
+ CompactExponent: exponent
43
49
  };
44
50
  }
@@ -10,7 +10,22 @@ export function InitializePluralRules(pl, locales, options, { availableLocales,
10
10
  opt.localeMatcher = matcher;
11
11
  const r = ResolveLocale(availableLocales, requestedLocales, opt, relevantExtensionKeys, localeData, getDefaultLocale);
12
12
  internalSlots.locale = r.locale;
13
+ // ECMA-402 Spec: type option ('cardinal' or 'ordinal')
13
14
  internalSlots.type = GetOption(opts, "type", "string", ["cardinal", "ordinal"], "cardinal");
15
+ // Extension: notation options for compact notation support
16
+ // Not in ECMA-402 spec, but mirrors Intl.NumberFormat notation option
17
+ // Enables proper plural selection for compact numbers (e.g., "1.2M")
18
+ const notation = GetOption(opts, "notation", "string", ["standard", "compact"], "standard");
19
+ internalSlots.notation = notation;
20
+ if (notation === "compact") {
21
+ // Extension: compactDisplay option (mirrors Intl.NumberFormat)
22
+ internalSlots.compactDisplay = GetOption(opts, "compactDisplay", "string", ["short", "long"], "short");
23
+ // Implementation: Load NumberFormat locale data if available (soft dependency)
24
+ // This is needed to calculate compact exponents using ComputeExponentForMagnitude
25
+ if (typeof Intl !== "undefined" && Intl.NumberFormat && Intl.NumberFormat.localeData) {
26
+ internalSlots.dataLocaleData = Intl.NumberFormat.localeData[r.locale];
27
+ }
28
+ }
14
29
  SetNumberFormatDigitOptions(internalSlots, opts, 0, 3, "standard");
15
30
  return pl;
16
31
  }
@@ -1,7 +1,37 @@
1
1
  import { type LDMLPluralRule, type PluralRulesInternal } from "@formatjs/ecma402-abstract";
2
- import type Decimal from "decimal.js";
2
+ import Decimal from "decimal.js";
3
3
  import { type OperandsRecord } from "./GetOperands.js";
4
4
  /**
5
+ * Result of ResolvePluralInternal containing both the formatted string and plural category.
6
+ * This corresponds to a Record with [[FormattedString]] and [[PluralCategory]] fields
7
+ * as described in the ECMA-402 spec for ResolvePluralRange.
8
+ */
9
+ export interface ResolvePluralResult {
10
+ /** The formatted representation of the number */
11
+ formattedString: string;
12
+ /** The LDML plural category (zero, one, two, few, many, or other) */
13
+ pluralCategory: LDMLPluralRule;
14
+ }
15
+ /**
16
+ * ResolvePluralInternal ( pluralRules, n )
17
+ *
18
+ * Internal version of ResolvePlural that returns both the formatted string and plural category.
19
+ * This is needed for selectRange, which must compare formatted strings to determine if the
20
+ * start and end values are identical.
21
+ *
22
+ * The formatted string is obtained by applying the number formatting options (digit options)
23
+ * from the PluralRules object to the input number. This ensures that formatting-sensitive
24
+ * plural rules work correctly (e.g., rules that depend on visible fraction digits).
25
+ *
26
+ * @param pl - An initialized PluralRules object
27
+ * @param n - Mathematical value to resolve
28
+ * @returns Record containing the formatted string and plural category
29
+ */
30
+ export declare function ResolvePluralInternal(pl: Intl.PluralRules, n: Decimal, { getInternalSlots, PluralRuleSelect }: {
31
+ getInternalSlots(pl: Intl.PluralRules): PluralRulesInternal;
32
+ PluralRuleSelect: (locale: string, type: "cardinal" | "ordinal", n: Decimal, operands: OperandsRecord) => LDMLPluralRule;
33
+ }): ResolvePluralResult;
34
+ /**
5
35
  * http://ecma-international.org/ecma-402/7.0/index.html#sec-resolveplural
6
36
  * @param pl
7
37
  * @param n
@@ -1,21 +1,75 @@
1
- import { FormatNumericToString, invariant, Type } from "@formatjs/ecma402-abstract";
1
+ import { ComputeExponentForMagnitude, FormatNumericToString, invariant, Type } from "@formatjs/ecma402-abstract";
2
+ import Decimal from "decimal.js";
2
3
  import { GetOperands } from "./GetOperands.js";
3
4
  /**
4
- * http://ecma-international.org/ecma-402/7.0/index.html#sec-resolveplural
5
- * @param pl
6
- * @param n
7
- * @param PluralRuleSelect Has to pass in bc it's implementation-specific
5
+ * ResolvePluralInternal ( pluralRules, n )
6
+ *
7
+ * Internal version of ResolvePlural that returns both the formatted string and plural category.
8
+ * This is needed for selectRange, which must compare formatted strings to determine if the
9
+ * start and end values are identical.
10
+ *
11
+ * The formatted string is obtained by applying the number formatting options (digit options)
12
+ * from the PluralRules object to the input number. This ensures that formatting-sensitive
13
+ * plural rules work correctly (e.g., rules that depend on visible fraction digits).
14
+ *
15
+ * @param pl - An initialized PluralRules object
16
+ * @param n - Mathematical value to resolve
17
+ * @returns Record containing the formatted string and plural category
8
18
  */
9
- export function ResolvePlural(pl, n, { getInternalSlots, PluralRuleSelect }) {
19
+ export function ResolvePluralInternal(pl, n, { getInternalSlots, PluralRuleSelect }) {
10
20
  const internalSlots = getInternalSlots(pl);
11
21
  invariant(Type(internalSlots) === "Object", "pl has to be an object");
12
22
  invariant("initializedPluralRules" in internalSlots, "pluralrules must be initialized");
23
+ // Handle non-finite values (Infinity, -Infinity, NaN)
13
24
  if (!n.isFinite()) {
14
- return "other";
25
+ return {
26
+ formattedString: String(n),
27
+ pluralCategory: "other"
28
+ };
15
29
  }
16
- const { locale, type } = internalSlots;
30
+ const { locale, type, notation } = internalSlots;
31
+ // ECMA-402 Spec: Format the number according to digit options
17
32
  const res = FormatNumericToString(internalSlots, n);
18
33
  const s = res.formattedString;
19
- const operands = GetOperands(s);
20
- return PluralRuleSelect(locale, type, n, operands);
34
+ // Extension: Calculate compact exponent if using compact notation
35
+ // This enables CLDR c/e operands for proper plural selection with compact numbers
36
+ let exponent = 0;
37
+ if (notation === "compact" && !n.isZero()) {
38
+ // Implementation: Only calculate exponent if NumberFormat locale data is available (soft dependency)
39
+ if (internalSlots.dataLocaleData?.numbers) {
40
+ try {
41
+ // Calculate magnitude (floor of log10 of absolute value)
42
+ const magnitudeNum = Math.floor(Math.log10(Math.abs(n.toNumber())));
43
+ const magnitude = new Decimal(magnitudeNum);
44
+ // Use ComputeExponentForMagnitude from ecma402-abstract
45
+ // This determines which compact notation pattern to use (K, M, B, etc.)
46
+ // Cast to any since it expects NumberFormatInternal
47
+ exponent = ComputeExponentForMagnitude(internalSlots, magnitude);
48
+ } catch {
49
+ // Gracefully fall back to 0 if exponent calculation fails
50
+ exponent = 0;
51
+ }
52
+ }
53
+ }
54
+ // ECMA-402 Spec: Extract CLDR operands from the formatted string
55
+ // Extension: Pass exponent for c/e operands
56
+ const operands = GetOperands(s, exponent);
57
+ // ECMA-402 Spec: Select the appropriate plural category using the locale's plural rules
58
+ const pluralCategory = PluralRuleSelect(locale, type, n, operands);
59
+ return {
60
+ formattedString: s,
61
+ pluralCategory
62
+ };
63
+ }
64
+ /**
65
+ * http://ecma-international.org/ecma-402/7.0/index.html#sec-resolveplural
66
+ * @param pl
67
+ * @param n
68
+ * @param PluralRuleSelect Has to pass in bc it's implementation-specific
69
+ */
70
+ export function ResolvePlural(pl, n, { getInternalSlots, PluralRuleSelect }) {
71
+ return ResolvePluralInternal(pl, n, {
72
+ getInternalSlots,
73
+ PluralRuleSelect
74
+ }).pluralCategory;
21
75
  }
@@ -0,0 +1,23 @@
1
+ import { type LDMLPluralRule, type PluralRulesInternal } from "@formatjs/ecma402-abstract";
2
+ import type Decimal from "decimal.js";
3
+ import { type OperandsRecord } from "./GetOperands.js";
4
+ /**
5
+ * ResolvePluralRange ( pluralRules, x, y )
6
+ *
7
+ * The ResolvePluralRange abstract operation is called with arguments pluralRules (which must be
8
+ * an object initialized as a PluralRules), x (a mathematical value), and y (a mathematical value).
9
+ * It resolves the appropriate plural form for a range by determining the plural forms of both the
10
+ * start and end values, then consulting locale-specific range data.
11
+ *
12
+ * Specification: https://tc39.es/ecma402/#sec-resolvepluralrange
13
+ *
14
+ * @param pluralRules - An initialized PluralRules object
15
+ * @param x - Mathematical value for the range start
16
+ * @param y - Mathematical value for the range end
17
+ * @returns The plural category for the range (zero, one, two, few, many, or other)
18
+ */
19
+ export declare function ResolvePluralRange(pluralRules: Intl.PluralRules, x: Decimal, y: Decimal, { getInternalSlots, PluralRuleSelect, PluralRuleSelectRange }: {
20
+ getInternalSlots(pl: Intl.PluralRules): PluralRulesInternal;
21
+ PluralRuleSelect: (locale: string, type: "cardinal" | "ordinal", n: Decimal, operands: OperandsRecord) => LDMLPluralRule;
22
+ PluralRuleSelectRange: (locale: string, type: "cardinal" | "ordinal", xp: LDMLPluralRule, yp: LDMLPluralRule) => LDMLPluralRule;
23
+ }): LDMLPluralRule;
@@ -0,0 +1,59 @@
1
+ import { invariant, Type } from "@formatjs/ecma402-abstract";
2
+ import "./GetOperands.js";
3
+ import { ResolvePluralInternal } from "./ResolvePlural.js";
4
+ /**
5
+ * ResolvePluralRange ( pluralRules, x, y )
6
+ *
7
+ * The ResolvePluralRange abstract operation is called with arguments pluralRules (which must be
8
+ * an object initialized as a PluralRules), x (a mathematical value), and y (a mathematical value).
9
+ * It resolves the appropriate plural form for a range by determining the plural forms of both the
10
+ * start and end values, then consulting locale-specific range data.
11
+ *
12
+ * Specification: https://tc39.es/ecma402/#sec-resolvepluralrange
13
+ *
14
+ * @param pluralRules - An initialized PluralRules object
15
+ * @param x - Mathematical value for the range start
16
+ * @param y - Mathematical value for the range end
17
+ * @returns The plural category for the range (zero, one, two, few, many, or other)
18
+ */
19
+ export function ResolvePluralRange(pluralRules, x, y, { getInternalSlots, PluralRuleSelect, PluralRuleSelectRange }) {
20
+ // 1. If x is not-a-number or y is not-a-number, throw a RangeError exception.
21
+ if (!x.isFinite() || !y.isFinite()) {
22
+ throw new RangeError("selectRange requires start and end values to be finite numbers");
23
+ }
24
+ // Validation: Assert that pluralRules has been initialized
25
+ const internalSlots = getInternalSlots(pluralRules);
26
+ invariant(Type(internalSlots) === "Object", "pluralRules has to be an object");
27
+ invariant("initializedPluralRules" in internalSlots, "pluralrules must be initialized");
28
+ // 2. Let xp be ResolvePlural(pluralRules, x).
29
+ // Note: ResolvePlural returns a Record with [[FormattedString]] and [[PluralCategory]]
30
+ const xp = ResolvePluralInternal(pluralRules, x, {
31
+ getInternalSlots,
32
+ PluralRuleSelect
33
+ });
34
+ // 3. Let yp be ResolvePlural(pluralRules, y).
35
+ const yp = ResolvePluralInternal(pluralRules, y, {
36
+ getInternalSlots,
37
+ PluralRuleSelect
38
+ });
39
+ // 4. If xp.[[FormattedString]] is yp.[[FormattedString]], then
40
+ // a. Return xp.[[PluralCategory]].
41
+ // Note: When the formatted strings are identical (e.g., "1" and "1"), the values are
42
+ // effectively the same, so we return the plural category of the start value.
43
+ if (xp.formattedString === yp.formattedString) {
44
+ return xp.pluralCategory;
45
+ }
46
+ // 5. Let locale be pluralRules.[[Locale]].
47
+ // 6. Let type be pluralRules.[[Type]].
48
+ const { locale, type } = internalSlots;
49
+ // 7. Let notation be pluralRules.[[Notation]].
50
+ // 8. Let compactDisplay be pluralRules.[[CompactDisplay]].
51
+ // Note: notation and compactDisplay are not yet implemented for PluralRules polyfill.
52
+ // When implemented, these would affect how the range is formatted and thus which
53
+ // plural rules apply (see c/e operand support).
54
+ // 9. Return PluralRuleSelectRange(locale, type, notation, compactDisplay, xp.[[PluralCategory]], yp.[[PluralCategory]]).
55
+ // Note: PluralRuleSelectRange is implementation-defined and uses CLDR plural range data
56
+ // to determine the appropriate plural category for a range based on the start and end categories.
57
+ // Example: In English, "one" to "other" → "other" (e.g., "1-2 items")
58
+ return PluralRuleSelectRange(locale, type, xp.pluralCategory, yp.pluralCategory);
59
+ }
@@ -1,5 +1,3 @@
1
- // Type-only circular import
2
- // eslint-disable-next-line import/no-cycle
3
1
  import type { PluralRules } from "./index.js";
4
2
  import { type PluralRulesInternal } from "./index.js";
5
3
  export default function getInternalSlots(x: PluralRules): PluralRulesInternal;
package/index.d.ts CHANGED
@@ -1,13 +1,71 @@
1
1
  import { type LDMLPluralRule, type NumberFormatDigitInternalSlots, type PluralRulesData, type PluralRulesLocaleData } from "@formatjs/ecma402-abstract";
2
+ /**
3
+ * Type augmentation for Intl.PluralRules
4
+ *
5
+ * ECMA-402 Spec: selectRange method (Intl.PluralRules.prototype.selectRange)
6
+ * https://tc39.es/ecma402/#sec-intl.pluralrules.prototype.selectrange
7
+ *
8
+ * Extension: notation and compactDisplay options (not in ECMA-402 spec)
9
+ * Mirrors Intl.NumberFormat notation option for proper plural selection with compact numbers
10
+ */
11
+ declare global {
12
+ namespace Intl {
13
+ interface PluralRules {
14
+ selectRange(start: number | bigint, end: number | bigint): LDMLPluralRule;
15
+ }
16
+ interface PluralRulesOptions {
17
+ notation?: "standard" | "compact";
18
+ compactDisplay?: "short" | "long";
19
+ }
20
+ }
21
+ }
2
22
  export interface PluralRulesInternal extends NumberFormatDigitInternalSlots {
3
23
  initializedPluralRules: boolean;
4
24
  locale: string;
5
25
  type: "cardinal" | "ordinal";
26
+ notation: "standard" | "compact";
27
+ compactDisplay?: "short" | "long";
28
+ dataLocaleData?: any;
6
29
  }
7
- export declare class PluralRules implements Intl.PluralRules {
30
+ export declare class PluralRules {
8
31
  constructor(locales?: string | string[], options?: Intl.PluralRulesOptions);
9
32
  resolvedOptions(): Intl.ResolvedPluralRulesOptions;
10
- select(val: number): LDMLPluralRule;
33
+ select(val: number | bigint): LDMLPluralRule;
34
+ /**
35
+ * Intl.PluralRules.prototype.selectRange ( start, end )
36
+ *
37
+ * Returns a string indicating which plural rule applies to a range of numbers.
38
+ * This is useful for formatting ranges like "1-2 items" vs "2-3 items" where
39
+ * different languages have different plural rules for ranges.
40
+ *
41
+ * Specification: https://tc39.es/ecma402/#sec-intl.pluralrules.prototype.selectrange
42
+ *
43
+ * @param start - The start value of the range (number or bigint)
44
+ * @param end - The end value of the range (number or bigint)
45
+ * @returns The plural category for the range (zero, one, two, few, many, or other)
46
+ *
47
+ * @example
48
+ * const pr = new Intl.PluralRules('en');
49
+ * pr.selectRange(1, 2); // "other" (English: "1-2 items")
50
+ * pr.selectRange(1, 1); // "one" (same value: "1 item")
51
+ *
52
+ * @example
53
+ * const prFr = new Intl.PluralRules('fr');
54
+ * prFr.selectRange(0, 1); // "one" (French: "0-1 vue")
55
+ * prFr.selectRange(1, 2); // "other" (French: "1-2 vues")
56
+ *
57
+ * @example
58
+ * // BigInt support (spec-compliant, but Chrome has a bug as of early 2025)
59
+ * pr.selectRange(BigInt(1), BigInt(2)); // "other"
60
+ *
61
+ * @throws {TypeError} If start or end is undefined
62
+ * @throws {RangeError} If start or end is not a finite number (Infinity, NaN)
63
+ *
64
+ * @note Chrome's native implementation (as of early 2025) has a bug where it throws
65
+ * "Cannot convert a BigInt value to a number" when using BigInt arguments. This is
66
+ * a browser bug - the spec requires BigInt support. This polyfill handles BigInt correctly.
67
+ */
68
+ selectRange(start: number | bigint, end: number | bigint): LDMLPluralRule;
11
69
  toString(): string;
12
70
  static supportedLocalesOf(locales?: string | string[], options?: Pick<Intl.PluralRulesOptions, "localeMatcher">): string[];
13
71
  static __addLocaleData(...data: PluralRulesLocaleData[]): void;
package/index.js CHANGED
@@ -1,7 +1,8 @@
1
- import { CanonicalizeLocaleList, SupportedLocales, ToNumber } from "@formatjs/ecma402-abstract";
1
+ import { CanonicalizeLocaleList, SupportedLocales, ToIntlMathematicalValue } from "@formatjs/ecma402-abstract";
2
2
  import "./abstract/GetOperands.js";
3
3
  import { InitializePluralRules } from "./abstract/InitializePluralRules.js";
4
4
  import { ResolvePlural } from "./abstract/ResolvePlural.js";
5
+ import { ResolvePluralRange } from "./abstract/ResolvePluralRange.js";
5
6
  import getInternalSlots from "./get_internal_slots.js";
6
7
  function validateInstance(instance, method) {
7
8
  if (!(instance instanceof PluralRules)) {
@@ -15,8 +16,44 @@ function validateInstance(instance, method) {
15
16
  * @param _n
16
17
  * @param param3
17
18
  */
18
- function PluralRuleSelect(locale, type, _n, { IntegerDigits, NumberOfFractionDigits, FractionDigits }) {
19
- return PluralRules.localeData[locale].fn(NumberOfFractionDigits ? `${IntegerDigits}.${FractionDigits}` : IntegerDigits, type === "ordinal");
19
+ function PluralRuleSelect(locale, type, _n, { IntegerDigits, NumberOfFractionDigits, FractionDigits, CompactExponent }) {
20
+ // Always pass a string to the compiled function to preserve precision for huge numbers
21
+ return PluralRules.localeData[locale].fn(NumberOfFractionDigits ? `${IntegerDigits}.${FractionDigits}` : String(IntegerDigits), type === "ordinal", CompactExponent);
22
+ }
23
+ /**
24
+ * PluralRuleSelectRange ( locale, type, notation, compactDisplay, start, end )
25
+ *
26
+ * Implementation-defined abstract operation that determines the plural category for a range
27
+ * by consulting CLDR plural range data. Each locale defines how different combinations of
28
+ * start and end plural categories map to a range plural category.
29
+ *
30
+ * Examples from CLDR:
31
+ * - English: "one" + "other" → "other" (e.g., "1-2 items")
32
+ * - French: "one" + "one" → "one" (e.g., "0-1 vue")
33
+ * - Arabic: "few" + "many" → "many" (e.g., complex range rules)
34
+ *
35
+ * The spec allows this to be implementation-defined, and we use CLDR supplemental data
36
+ * from pluralRanges.json which provides explicit mappings for each locale.
37
+ *
38
+ * @param locale - BCP 47 locale identifier
39
+ * @param type - "cardinal" or "ordinal"
40
+ * @param xp - Start plural category
41
+ * @param yp - End plural category
42
+ * @returns The plural category for the range
43
+ */
44
+ function PluralRuleSelectRange(locale, type, xp, yp) {
45
+ const localeData = PluralRules.localeData[locale];
46
+ if (!localeData || !localeData.pluralRanges) {
47
+ // Fallback: If no range data is available, return the end category.
48
+ // This is a reasonable default as the end value often determines the plural form.
49
+ return yp;
50
+ }
51
+ // Construct lookup key: "start_end" (e.g., "one_other", "few_many")
52
+ const key = `${xp}_${yp}`;
53
+ // Select the appropriate range data based on type (cardinal vs ordinal)
54
+ const rangeData = type === "ordinal" ? localeData.pluralRanges.ordinal : localeData.pluralRanges.cardinal;
55
+ // Look up the result, falling back to end category if not found
56
+ return rangeData?.[key] ?? yp;
20
57
  }
21
58
  export class PluralRules {
22
59
  constructor(locales, options) {
@@ -57,12 +94,69 @@ export class PluralRules {
57
94
  }
58
95
  select(val) {
59
96
  validateInstance(this, "select");
60
- const n = ToNumber(val);
97
+ // Use ToIntlMathematicalValue which handles bigint per ECMA-402
98
+ // https://tc39.es/ecma402/#sec-intl.pluralrules.prototype.select
99
+ const n = ToIntlMathematicalValue(val);
61
100
  return ResolvePlural(this, n, {
62
101
  getInternalSlots,
63
102
  PluralRuleSelect
64
103
  });
65
104
  }
105
+ /**
106
+ * Intl.PluralRules.prototype.selectRange ( start, end )
107
+ *
108
+ * Returns a string indicating which plural rule applies to a range of numbers.
109
+ * This is useful for formatting ranges like "1-2 items" vs "2-3 items" where
110
+ * different languages have different plural rules for ranges.
111
+ *
112
+ * Specification: https://tc39.es/ecma402/#sec-intl.pluralrules.prototype.selectrange
113
+ *
114
+ * @param start - The start value of the range (number or bigint)
115
+ * @param end - The end value of the range (number or bigint)
116
+ * @returns The plural category for the range (zero, one, two, few, many, or other)
117
+ *
118
+ * @example
119
+ * const pr = new Intl.PluralRules('en');
120
+ * pr.selectRange(1, 2); // "other" (English: "1-2 items")
121
+ * pr.selectRange(1, 1); // "one" (same value: "1 item")
122
+ *
123
+ * @example
124
+ * const prFr = new Intl.PluralRules('fr');
125
+ * prFr.selectRange(0, 1); // "one" (French: "0-1 vue")
126
+ * prFr.selectRange(1, 2); // "other" (French: "1-2 vues")
127
+ *
128
+ * @example
129
+ * // BigInt support (spec-compliant, but Chrome has a bug as of early 2025)
130
+ * pr.selectRange(BigInt(1), BigInt(2)); // "other"
131
+ *
132
+ * @throws {TypeError} If start or end is undefined
133
+ * @throws {RangeError} If start or end is not a finite number (Infinity, NaN)
134
+ *
135
+ * @note Chrome's native implementation (as of early 2025) has a bug where it throws
136
+ * "Cannot convert a BigInt value to a number" when using BigInt arguments. This is
137
+ * a browser bug - the spec requires BigInt support. This polyfill handles BigInt correctly.
138
+ */
139
+ selectRange(start, end) {
140
+ validateInstance(this, "selectRange");
141
+ // Spec: https://tc39.es/ecma402/#sec-intl.pluralrules.prototype.selectrange
142
+ // 1. Let pr be the this value.
143
+ // 2. Perform ? RequireInternalSlot(pr, [[InitializedPluralRules]]).
144
+ // (Validation is done by validateInstance above)
145
+ // 3. If start is undefined or end is undefined, throw a TypeError exception.
146
+ if (start === undefined || end === undefined) {
147
+ throw new TypeError("selectRange requires both start and end arguments");
148
+ }
149
+ // 4. Let x be ? ToIntlMathematicalValue(start).
150
+ const x = ToIntlMathematicalValue(start);
151
+ // 5. Let y be ? ToIntlMathematicalValue(end).
152
+ const y = ToIntlMathematicalValue(end);
153
+ // 6. Return ? ResolvePluralRange(pr, x, y).
154
+ return ResolvePluralRange(this, x, y, {
155
+ getInternalSlots,
156
+ PluralRuleSelect,
157
+ PluralRuleSelectRange
158
+ });
159
+ }
66
160
  toString() {
67
161
  return "[object Intl.PluralRules]";
68
162
  }
package/locale-data/af.js CHANGED
@@ -1,8 +1,18 @@
1
1
  /* @generated */
2
2
  // prettier-ignore
3
3
  if (Intl.PluralRules && typeof Intl.PluralRules.__addLocaleData === 'function') {
4
- Intl.PluralRules.__addLocaleData({"data":{"categories":{"cardinal":["one","other"],"ordinal":["other"]},"fn":function(n, ord) {
5
- if (ord) return 'other';
6
- return n == 1 ? 'one' : 'other';
7
- }},"locale":"af"})
4
+ Intl.PluralRules.__addLocaleData({"data":{"categories":{"cardinal":["one","other"],"ordinal":["other"]},"fn":function(num, isOrdinal, exponent = 0) {
5
+ const numStr = String(num);
6
+ const parts = numStr.split(".");
7
+ const integerPart = parts[0];
8
+ const decimalPart = parts[1] || "";
9
+ const n = Math.abs(parseFloat(numStr));
10
+ if (isOrdinal) {
11
+ }
12
+ else {
13
+ if (n === 1)
14
+ return "one";
15
+ }
16
+ return "other";
17
+ },"pluralRanges":{"cardinal":{"one_other":"other","other_one":"other","other_other":"other"},"ordinal":{}}},"locale":"af"})
8
18
  }
package/locale-data/ak.js CHANGED
@@ -1,8 +1,18 @@
1
1
  /* @generated */
2
2
  // prettier-ignore
3
3
  if (Intl.PluralRules && typeof Intl.PluralRules.__addLocaleData === 'function') {
4
- Intl.PluralRules.__addLocaleData({"data":{"categories":{"cardinal":["one","other"],"ordinal":["other"]},"fn":function(n, ord) {
5
- if (ord) return 'other';
6
- return (n == 0 || n == 1) ? 'one' : 'other';
7
- }},"locale":"ak"})
4
+ Intl.PluralRules.__addLocaleData({"data":{"categories":{"cardinal":["one","other"],"ordinal":["other"]},"fn":function(num, isOrdinal, exponent = 0) {
5
+ const numStr = String(num);
6
+ const parts = numStr.split(".");
7
+ const integerPart = parts[0];
8
+ const decimalPart = parts[1] || "";
9
+ const n = Math.abs(parseFloat(numStr));
10
+ if (isOrdinal) {
11
+ }
12
+ else {
13
+ if ((n >= 0 && n <= 1))
14
+ return "one";
15
+ }
16
+ return "other";
17
+ },"pluralRanges":{"cardinal":{"one_one":"other","one_other":"other","other_one":"one","other_other":"other"},"ordinal":{}}},"locale":"ak"})
8
18
  }
package/locale-data/am.js CHANGED
@@ -1,8 +1,19 @@
1
1
  /* @generated */
2
2
  // prettier-ignore
3
3
  if (Intl.PluralRules && typeof Intl.PluralRules.__addLocaleData === 'function') {
4
- Intl.PluralRules.__addLocaleData({"data":{"categories":{"cardinal":["one","other"],"ordinal":["other"]},"fn":function(n, ord) {
5
- if (ord) return 'other';
6
- return n >= 0 && n <= 1 ? 'one' : 'other';
7
- }},"locale":"am"})
4
+ Intl.PluralRules.__addLocaleData({"data":{"categories":{"cardinal":["one","other"],"ordinal":["other"]},"fn":function(num, isOrdinal, exponent = 0) {
5
+ const numStr = String(num);
6
+ const parts = numStr.split(".");
7
+ const integerPart = parts[0];
8
+ const decimalPart = parts[1] || "";
9
+ const n = Math.abs(parseFloat(numStr));
10
+ const i = Math.floor(Math.abs(parseFloat(integerPart)));
11
+ if (isOrdinal) {
12
+ }
13
+ else {
14
+ if (i === 0 || n === 1)
15
+ return "one";
16
+ }
17
+ return "other";
18
+ },"pluralRanges":{"cardinal":{"one_one":"one","one_other":"other","other_other":"other"},"ordinal":{}}},"locale":"am"})
8
19
  }
package/locale-data/an.js CHANGED
@@ -1,8 +1,18 @@
1
1
  /* @generated */
2
2
  // prettier-ignore
3
3
  if (Intl.PluralRules && typeof Intl.PluralRules.__addLocaleData === 'function') {
4
- Intl.PluralRules.__addLocaleData({"data":{"categories":{"cardinal":["one","other"],"ordinal":["other"]},"fn":function(n, ord) {
5
- if (ord) return 'other';
6
- return n == 1 ? 'one' : 'other';
7
- }},"locale":"an"})
4
+ Intl.PluralRules.__addLocaleData({"data":{"categories":{"cardinal":["one","other"],"ordinal":["other"]},"fn":function(num, isOrdinal, exponent = 0) {
5
+ const numStr = String(num);
6
+ const parts = numStr.split(".");
7
+ const integerPart = parts[0];
8
+ const decimalPart = parts[1] || "";
9
+ const n = Math.abs(parseFloat(numStr));
10
+ if (isOrdinal) {
11
+ }
12
+ else {
13
+ if (n === 1)
14
+ return "one";
15
+ }
16
+ return "other";
17
+ },"pluralRanges":{"cardinal":{"one_other":"other","other_one":"other","other_other":"other"},"ordinal":{}}},"locale":"an"})
8
18
  }