diff --git a/Jint/Native/Intl/IntlInstance.cs b/Jint/Native/Intl/IntlInstance.cs
index e4303f3a66..0f1ea35b36 100644
--- a/Jint/Native/Intl/IntlInstance.cs
+++ b/Jint/Native/Intl/IntlInstance.cs
@@ -36,6 +36,7 @@ protected override void Initialize()
{
["Collator"] = new(_realm.Intrinsics.Collator, PropertyFlags),
["Locale"] = new(_realm.Intrinsics.Locale, PropertyFlags),
+ ["PluralRules"] = new(_realm.Intrinsics.PluralRules, PropertyFlags),
["getCanonicalLocales"] = new(new ClrFunction(Engine, "getCanonicalLocales", GetCanonicalLocales, 1, PropertyFlag.Configurable), PropertyFlags),
["supportedValuesOf"] = new(new ClrFunction(Engine, "supportedValuesOf", SupportedValuesOf, 1, PropertyFlag.Configurable), PropertyFlags),
};
diff --git a/Jint/Native/Intl/JsPluralRules.cs b/Jint/Native/Intl/JsPluralRules.cs
new file mode 100644
index 0000000000..8f81b45f93
--- /dev/null
+++ b/Jint/Native/Intl/JsPluralRules.cs
@@ -0,0 +1,385 @@
+using System.Globalization;
+using Jint.Native.Object;
+
+namespace Jint.Native.Intl;
+
+///
+/// https://tc39.es/ecma402/#sec-pluralrules-objects
+/// Represents an Intl.PluralRules instance for locale-aware plural form selection.
+///
+internal sealed class JsPluralRules : ObjectInstance
+{
+ internal JsPluralRules(
+ Engine engine,
+ ObjectInstance prototype,
+ string locale,
+ string pluralRuleType,
+ string notation,
+ int minimumIntegerDigits,
+ int? minimumFractionDigits,
+ int? maximumFractionDigits,
+ int? minimumSignificantDigits,
+ int? maximumSignificantDigits,
+ string roundingMode,
+ string roundingPriority,
+ int roundingIncrement,
+ string trailingZeroDisplay,
+ CultureInfo cultureInfo) : base(engine)
+ {
+ _prototype = prototype;
+ Locale = locale;
+ PluralRuleType = pluralRuleType;
+ Notation = notation;
+ MinimumIntegerDigits = minimumIntegerDigits;
+ MinimumFractionDigits = minimumFractionDigits;
+ MaximumFractionDigits = maximumFractionDigits;
+ MinimumSignificantDigits = minimumSignificantDigits;
+ MaximumSignificantDigits = maximumSignificantDigits;
+ RoundingMode = roundingMode;
+ RoundingPriority = roundingPriority;
+ RoundingIncrement = roundingIncrement;
+ TrailingZeroDisplay = trailingZeroDisplay;
+ CultureInfo = cultureInfo;
+ }
+
+ ///
+ /// The locale used for plural rules.
+ ///
+ internal string Locale { get; }
+
+ ///
+ /// The type of plural rules: "cardinal" or "ordinal".
+ ///
+ internal string PluralRuleType { get; }
+
+ ///
+ /// The notation style: "standard", "compact", "scientific", or "engineering".
+ ///
+ internal string Notation { get; }
+
+ ///
+ /// Minimum integer digits.
+ ///
+ internal int MinimumIntegerDigits { get; }
+
+ ///
+ /// Minimum fraction digits (null if significant digits are used).
+ ///
+ internal int? MinimumFractionDigits { get; }
+
+ ///
+ /// Maximum fraction digits (null if significant digits are used).
+ ///
+ internal int? MaximumFractionDigits { get; }
+
+ ///
+ /// Minimum significant digits (null if fraction digits are used).
+ ///
+ internal int? MinimumSignificantDigits { get; }
+
+ ///
+ /// Maximum significant digits (null if fraction digits are used).
+ ///
+ internal int? MaximumSignificantDigits { get; }
+
+ ///
+ /// The rounding mode.
+ ///
+ internal string RoundingMode { get; }
+
+ ///
+ /// The rounding priority.
+ ///
+ internal string RoundingPriority { get; }
+
+ ///
+ /// The rounding increment.
+ ///
+ internal int RoundingIncrement { get; }
+
+ ///
+ /// The trailing zero display mode.
+ ///
+ internal string TrailingZeroDisplay { get; }
+
+ ///
+ /// The .NET CultureInfo for the locale.
+ ///
+ internal CultureInfo CultureInfo { get; }
+
+ ///
+ /// Selects the plural category for a number.
+ /// Returns: "zero", "one", "two", "few", "many", or "other"
+ ///
+ internal string Select(double n)
+ {
+ if (string.Equals(PluralRuleType, "ordinal", StringComparison.Ordinal))
+ {
+ return SelectOrdinal(n);
+ }
+
+ return SelectCardinal(n);
+ }
+
+ ///
+ /// Selects the plural category for cardinal numbers (counting).
+ ///
+ private string SelectCardinal(double n)
+ {
+ // Handle special values
+ if (double.IsNaN(n) || double.IsInfinity(n))
+ {
+ return "other";
+ }
+
+ var absN = System.Math.Abs(n);
+ var i = (long) System.Math.Floor(absN); // integer part
+ var v = GetVisibleFractionDigitCount(n); // visible fraction digit count
+ var f = GetFractionDigits(n, v); // visible fraction digits
+
+ // Get language code from locale
+ var lang = GetLanguageCode();
+
+ // Simplified plural rules for common languages
+ // A full implementation would use CLDR plural rules data
+ return lang switch
+ {
+ // English, German, Dutch, etc. - "one" for 1, "other" for everything else
+ "en" or "de" or "nl" or "sv" or "da" or "no" or "nb" or "nn" =>
+ (i == 1 && v == 0) ? "one" : "other",
+
+ // French - "one" for 0 and 1, "many" for multiples of 1 million with no fraction
+ "fr" => SelectFrenchCardinal(i, v),
+
+ // Portuguese - "one" for 0 and 1
+ "pt" => (i == 0 || i == 1) ? "one" : "other",
+
+ // Spanish, Italian - "one" for 1
+ "es" or "it" =>
+ (i == 1 && v == 0) ? "one" : "other",
+
+ // Russian, Ukrainian, Polish - complex rules
+ "ru" or "uk" => SelectSlavicCardinal(i, v),
+
+ // Polish
+ "pl" => SelectPolishCardinal(i, v),
+
+ // Arabic - has six forms
+ "ar" => SelectArabicCardinal(i),
+
+ // Chinese, Japanese, Korean, Vietnamese - no plural forms
+ "zh" or "ja" or "ko" or "vi" => "other",
+
+ // Default to simple one/other distinction
+ _ => (i == 1 && v == 0) ? "one" : "other"
+ };
+ }
+
+ ///
+ /// Selects the plural category for ordinal numbers (ordering: 1st, 2nd, 3rd).
+ ///
+ private string SelectOrdinal(double n)
+ {
+ if (double.IsNaN(n) || double.IsInfinity(n))
+ {
+ return "other";
+ }
+
+ var absN = System.Math.Abs(n);
+ var i = (long) System.Math.Floor(absN);
+
+ var lang = GetLanguageCode();
+
+ return lang switch
+ {
+ // English ordinals: 1st, 2nd, 3rd, 4th, 11th, 12th, 13th, 21st, 22nd, 23rd, etc.
+ "en" => SelectEnglishOrdinal(i),
+
+ // Most other languages don't have distinct ordinal forms
+ _ => "other"
+ };
+ }
+
+ private static string SelectEnglishOrdinal(long n)
+ {
+ var mod10 = n % 10;
+ var mod100 = n % 100;
+
+ // 11th, 12th, 13th are exceptions
+ if (mod100 >= 11 && mod100 <= 13)
+ {
+ return "other";
+ }
+
+ return mod10 switch
+ {
+ 1 => "one", // 1st, 21st, 31st, etc.
+ 2 => "two", // 2nd, 22nd, 32nd, etc.
+ 3 => "few", // 3rd, 23rd, 33rd, etc.
+ _ => "other" // 4th, 5th, ..., 11th, 12th, 13th, etc.
+ };
+ }
+
+ private static string SelectFrenchCardinal(long i, int v)
+ {
+ // French cardinal rules (from CLDR):
+ // one: i = 0,1 (integer part is 0 or 1)
+ // many: e = 0 and i != 0 and i % 1000000 = 0 and v = 0 (integer is a non-zero multiple of 1 million with no fraction)
+ // other: everything else
+ if (i == 0 || i == 1)
+ {
+ return "one";
+ }
+
+ if (v == 0 && i != 0 && i % 1000000 == 0)
+ {
+ return "many";
+ }
+
+ return "other";
+ }
+
+ private static string SelectSlavicCardinal(long i, int v)
+ {
+ // Russian/Ukrainian cardinal rules
+ if (v != 0)
+ {
+ return "other";
+ }
+
+ var mod10 = i % 10;
+ var mod100 = i % 100;
+
+ if (mod10 == 1 && mod100 != 11)
+ {
+ return "one";
+ }
+
+ if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14))
+ {
+ return "few";
+ }
+
+ return "other";
+ }
+
+ private static string SelectPolishCardinal(long i, int v)
+ {
+ if (v != 0)
+ {
+ return "other";
+ }
+
+ if (i == 1)
+ {
+ return "one";
+ }
+
+ var mod10 = i % 10;
+ var mod100 = i % 100;
+
+ if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14))
+ {
+ return "few";
+ }
+
+ return "other";
+ }
+
+ private static string SelectArabicCardinal(long i)
+ {
+ if (i == 0)
+ {
+ return "zero";
+ }
+
+ if (i == 1)
+ {
+ return "one";
+ }
+
+ if (i == 2)
+ {
+ return "two";
+ }
+
+ var mod100 = i % 100;
+ if (mod100 >= 3 && mod100 <= 10)
+ {
+ return "few";
+ }
+
+ if (mod100 >= 11 && mod100 <= 99)
+ {
+ return "many";
+ }
+
+ return "other";
+ }
+
+ private string GetLanguageCode()
+ {
+ var locale = Locale;
+ var dashIndex = locale.IndexOf('-');
+ return dashIndex > 0 ? locale.Substring(0, dashIndex).ToLowerInvariant() : locale.ToLowerInvariant();
+ }
+
+ private static int GetVisibleFractionDigitCount(double n)
+ {
+ var str = n.ToString(System.Globalization.CultureInfo.InvariantCulture);
+ var dotIndex = str.IndexOf('.');
+ if (dotIndex < 0)
+ {
+ return 0;
+ }
+
+ return str.Length - dotIndex - 1;
+ }
+
+ private static long GetFractionDigits(double n, int v)
+ {
+ if (v == 0)
+ {
+ return 0;
+ }
+
+ var str = n.ToString(System.Globalization.CultureInfo.InvariantCulture);
+ var dotIndex = str.IndexOf('.');
+ if (dotIndex < 0)
+ {
+ return 0;
+ }
+
+ var fractionStr = str.Substring(dotIndex + 1);
+ return long.TryParse(fractionStr, System.Globalization.NumberStyles.None, System.Globalization.CultureInfo.InvariantCulture, out var result) ? result : 0;
+ }
+
+ ///
+ /// Returns the available plural categories for this locale.
+ ///
+ internal string[] GetPluralCategories()
+ {
+ var lang = GetLanguageCode();
+
+ if (string.Equals(PluralRuleType, "ordinal", StringComparison.Ordinal))
+ {
+ return lang switch
+ {
+ "en" => new[] { "one", "two", "few", "other" },
+ _ => new[] { "other" }
+ };
+ }
+
+ // Cardinal plural categories by language
+ // Based on CLDR plural rules
+ return lang switch
+ {
+ "ar" => new[] { "zero", "one", "two", "few", "many", "other" },
+ "ru" or "uk" or "pl" => new[] { "one", "few", "many", "other" },
+ // French and Portuguese have "many" for compact notation (large numbers)
+ "fr" or "pt" => new[] { "one", "many", "other" },
+ "zh" or "ja" or "ko" or "vi" => new[] { "other" },
+ _ => new[] { "one", "other" }
+ };
+ }
+}
diff --git a/Jint/Native/Intl/PluralRulesConstructor.cs b/Jint/Native/Intl/PluralRulesConstructor.cs
index 2910a7f509..4537b11271 100644
--- a/Jint/Native/Intl/PluralRulesConstructor.cs
+++ b/Jint/Native/Intl/PluralRulesConstructor.cs
@@ -1,7 +1,9 @@
+using System.Globalization;
using Jint.Native.Function;
using Jint.Native.Object;
using Jint.Runtime;
using Jint.Runtime.Descriptors;
+using Jint.Runtime.Interop;
namespace Jint.Native.Intl;
@@ -11,6 +13,12 @@ namespace Jint.Native.Intl;
internal sealed class PluralRulesConstructor : Constructor
{
private static readonly JsString _functionName = new("PluralRules");
+ private static readonly HashSet LocaleMatcherValues = ["lookup", "best fit"];
+ private static readonly HashSet TypeValues = ["cardinal", "ordinal"];
+ private static readonly HashSet NotationValues = ["standard", "scientific", "engineering", "compact"];
+ private static readonly HashSet RoundingModeValues = ["ceil", "floor", "expand", "trunc", "halfCeil", "halfFloor", "halfExpand", "halfTrunc", "halfEven"];
+ private static readonly HashSet RoundingPriorityValues = ["auto", "morePrecision", "lessPrecision"];
+ private static readonly HashSet TrailingZeroDisplayValues = ["auto", "stripIfInteger"];
public PluralRulesConstructor(
Engine engine,
@@ -24,32 +32,202 @@ public PluralRulesConstructor(
_prototypeDescriptor = new PropertyDescriptor(PrototypeObject, PropertyFlag.AllForbidden);
}
+ protected override void Initialize()
+ {
+ const PropertyFlag PropertyFlags = PropertyFlag.Configurable | PropertyFlag.Writable;
+ var properties = new PropertyDictionary(1, checkExistingKeys: false)
+ {
+ ["supportedLocalesOf"] = new(new ClrFunction(Engine, "supportedLocalesOf", SupportedLocalesOf, 1, PropertyFlag.Configurable), PropertyFlags)
+ };
+ SetProperties(properties);
+ }
+
public PluralRulesPrototype PrototypeObject { get; }
+ ///
+ /// Called when Intl.PluralRules is invoked without `new`.
+ /// PluralRules must throw TypeError when called without new.
+ ///
+ protected internal override JsValue Call(JsValue thisObject, JsCallArguments arguments)
+ {
+ Throw.TypeError(_realm, "Intl.PluralRules must be called with 'new'");
+ return JsValue.Undefined;
+ }
+
+ ///
+ /// https://tc39.es/ecma402/#sec-intl.pluralrules
+ ///
public override ObjectInstance Construct(JsCallArguments arguments, JsValue newTarget)
{
var locales = arguments.At(0);
var options = arguments.At(1);
- if (newTarget.IsUndefined())
+ // Get options object
+ var optionsObj = IntlUtilities.CoerceOptionsToObject(_engine, options);
+
+ // Note: localeMatcher is read by ResolveLocale, so we don't read it here
+
+ // Resolve locale
+ var requestedLocales = IntlUtilities.CanonicalizeLocaleList(_engine, locales);
+ var availableLocales = IntlUtilities.GetAvailableLocales();
+ var resolvedLocale = ResolvePluralRulesLocale(_engine, availableLocales, requestedLocales, optionsObj);
+
+ // Get type option
+ var type = GetStringOption(optionsObj, "type", TypeValues, "cardinal");
+
+ // Read notation option
+ var notation = GetStringOption(optionsObj, "notation", NotationValues, "standard");
+
+ // Digit options - must be read in spec order (SetNumberFormatDigitOptions)
+ var minimumIntegerDigits = GetNumberOption(optionsObj, "minimumIntegerDigits", 1, 21, 1);
+
+ // Read fraction digits first (per spec order)
+ var minFracValue = optionsObj.Get("minimumFractionDigits");
+ var maxFracValue = optionsObj.Get("maximumFractionDigits");
+ var hasMinFrac = !minFracValue.IsUndefined();
+ var hasMaxFrac = !maxFracValue.IsUndefined();
+
+ // Then read significant digits (per spec order)
+ var minSigValue = optionsObj.Get("minimumSignificantDigits");
+ var maxSigValue = optionsObj.Get("maximumSignificantDigits");
+ var hasMinSig = !minSigValue.IsUndefined();
+ var hasMaxSig = !maxSigValue.IsUndefined();
+
+ int? minimumFractionDigits = null;
+ int? maximumFractionDigits = null;
+ int? minimumSignificantDigits = null;
+ int? maximumSignificantDigits = null;
+
+ if (hasMinSig || hasMaxSig)
{
- newTarget = this;
+ // Use significant digits - validate them now
+ minimumSignificantDigits = hasMinSig ? GetNumberOptionFromValue(minSigValue, "minimumSignificantDigits", 1, 21, 1) : 1;
+ maximumSignificantDigits = hasMaxSig ? GetNumberOptionFromValue(maxSigValue, "maximumSignificantDigits", minimumSignificantDigits.Value, 21, 21) : 21;
}
+ else
+ {
+ // Use fraction digits - validate them now
+ var minFrac = hasMinFrac ? GetNumberOptionFromValue(minFracValue, "minimumFractionDigits", 0, 20, 0) : 0;
+ minimumFractionDigits = minFrac;
+ var maxFracDefault = System.Math.Max(minFrac, 3);
+ maximumFractionDigits = hasMaxFrac ? GetNumberOptionFromValue(maxFracValue, "maximumFractionDigits", minFrac, 20, maxFracDefault) : maxFracDefault;
+ }
+
+ // Rounding options (in spec order)
+ var roundingIncrement = GetNumberOption(optionsObj, "roundingIncrement", 1, 5000, 1);
+ var roundingMode = GetStringOption(optionsObj, "roundingMode", RoundingModeValues, "halfExpand");
+ var roundingPriority = GetStringOption(optionsObj, "roundingPriority", RoundingPriorityValues, "auto");
+ var trailingZeroDisplay = GetStringOption(optionsObj, "trailingZeroDisplay", TrailingZeroDisplayValues, "auto");
+
+ // Get CultureInfo for the locale
+ var culture = IntlUtilities.GetCultureInfo(resolvedLocale) ?? CultureInfo.InvariantCulture;
- var pluralRules = OrdinaryCreateFromConstructor(
- newTarget,
- static intrinsics => intrinsics.PluralRules.PrototypeObject,
- static (engine, _, _) => new JsObject(engine));
+ // Get prototype from newTarget (for cross-realm construction)
+ var proto = GetPrototypeFromConstructor(newTarget, static intrinsics => intrinsics.PluralRules.PrototypeObject);
- InitializePluralRules(pluralRules, locales, options);
- return pluralRules;
+ return new JsPluralRules(
+ _engine,
+ proto,
+ resolvedLocale,
+ type,
+ notation,
+ minimumIntegerDigits,
+ minimumFractionDigits,
+ maximumFractionDigits,
+ minimumSignificantDigits,
+ maximumSignificantDigits,
+ roundingMode,
+ roundingPriority,
+ roundingIncrement,
+ trailingZeroDisplay,
+ culture);
+ }
+
+ private string GetStringOption(ObjectInstance options, string property, HashSet values, string fallback)
+ {
+ var value = options.Get(property);
+ if (value.IsUndefined())
+ {
+ return fallback;
+ }
+
+ var stringValue = TypeConverter.ToString(value);
+
+ if (values != null && values.Count > 0)
+ {
+ if (!values.Contains(stringValue))
+ {
+ Throw.RangeError(_realm, $"Invalid value '{stringValue}' for option '{property}'");
+ }
+ }
+
+ return stringValue;
+ }
+
+ private int GetNumberOption(ObjectInstance options, string property, int minimum, int maximum, int fallback)
+ {
+ var value = options.Get(property);
+ if (value.IsUndefined())
+ {
+ return fallback;
+ }
+
+ return GetNumberOptionFromValue(value, property, minimum, maximum, fallback);
+ }
+
+ private int GetNumberOptionFromValue(JsValue value, string property, int minimum, int maximum, int fallback)
+ {
+ if (value.IsUndefined())
+ {
+ return fallback;
+ }
+
+ var number = TypeConverter.ToNumber(value);
+ if (double.IsNaN(number))
+ {
+ Throw.RangeError(_realm, $"Invalid number for option '{property}'");
+ }
+
+ var intValue = (int) System.Math.Floor(number);
+ if (intValue < minimum || intValue > maximum)
+ {
+ Throw.RangeError(_realm, $"Value {intValue} for option '{property}' is out of range [{minimum}, {maximum}]");
+ }
+
+ return intValue;
+ }
+
+ private static string ResolvePluralRulesLocale(Engine engine, HashSet availableLocales, List requestedLocales, ObjectInstance options)
+ {
+ var resolved = IntlUtilities.ResolveLocale(engine, availableLocales, requestedLocales, options, []);
+ return resolved.Locale;
}
///
- /// https://tc39.es/ecma402/#sec-initializepluralrules
+ /// https://tc39.es/ecma402/#sec-intl.pluralrules.supportedlocalesof
///
- private static void InitializePluralRules(JsObject pluralRules, JsValue locales, JsValue options)
+ private JsArray SupportedLocalesOf(JsValue thisObject, JsCallArguments arguments)
{
- // TODO
+ var locales = arguments.At(0);
+ var options = arguments.At(1);
+
+ var requestedLocales = IntlUtilities.CanonicalizeLocaleList(_engine, locales);
+ var availableLocales = IntlUtilities.GetAvailableLocales();
+
+ // Validate localeMatcher option
+ var optionsObj = IntlUtilities.CoerceOptionsToObject(_engine, options);
+ GetStringOption(optionsObj, "localeMatcher", LocaleMatcherValues, "best fit");
+
+ List supported = [];
+ foreach (var locale in requestedLocales)
+ {
+ var bestAvailable = IntlUtilities.BestAvailableLocale(availableLocales, locale);
+ if (bestAvailable != null)
+ {
+ supported.Add(locale);
+ }
+ }
+
+ return new JsArray(_engine, supported.ToArray());
}
}
diff --git a/Jint/Native/Intl/PluralRulesPrototype.cs b/Jint/Native/Intl/PluralRulesPrototype.cs
index d26b30177f..5728a97cfe 100644
--- a/Jint/Native/Intl/PluralRulesPrototype.cs
+++ b/Jint/Native/Intl/PluralRulesPrototype.cs
@@ -2,6 +2,7 @@
using Jint.Native.Symbol;
using Jint.Runtime;
using Jint.Runtime.Descriptors;
+using Jint.Runtime.Interop;
namespace Jint.Native.Intl;
@@ -24,9 +25,15 @@ public PluralRulesPrototype(
protected override void Initialize()
{
- var properties = new PropertyDictionary(2, checkExistingKeys: false)
+ const PropertyFlag LengthFlags = PropertyFlag.Configurable;
+ const PropertyFlag PropertyFlags = PropertyFlag.Writable | PropertyFlag.Configurable;
+
+ var properties = new PropertyDictionary(4, checkExistingKeys: false)
{
- ["constructor"] = new PropertyDescriptor(_constructor, true, false, true),
+ ["constructor"] = new PropertyDescriptor(_constructor, PropertyFlag.NonEnumerable),
+ ["select"] = new PropertyDescriptor(new ClrFunction(Engine, "select", Select, 1, LengthFlags), PropertyFlags),
+ ["selectRange"] = new PropertyDescriptor(new ClrFunction(Engine, "selectRange", SelectRange, 2, LengthFlags), PropertyFlags),
+ ["resolvedOptions"] = new PropertyDescriptor(new ClrFunction(Engine, "resolvedOptions", ResolvedOptions, 0, LengthFlags), PropertyFlags),
};
SetProperties(properties);
@@ -36,4 +43,101 @@ protected override void Initialize()
};
SetSymbols(symbols);
}
+
+ private JsPluralRules ValidatePluralRules(JsValue thisObject)
+ {
+ if (thisObject is JsPluralRules pluralRules)
+ {
+ return pluralRules;
+ }
+
+ Throw.TypeError(_realm, "Value is not an Intl.PluralRules");
+ return null!; // Never reached
+ }
+
+ ///
+ /// https://tc39.es/ecma402/#sec-intl.pluralrules.prototype.select
+ ///
+ private JsValue Select(JsValue thisObject, JsCallArguments arguments)
+ {
+ var pluralRules = ValidatePluralRules(thisObject);
+ var n = arguments.At(0);
+
+ var x = TypeConverter.ToNumber(n);
+ return pluralRules.Select(x);
+ }
+
+ ///
+ /// https://tc39.es/ecma402/#sec-intl.pluralrules.prototype.selectrange
+ ///
+ private JsValue SelectRange(JsValue thisObject, JsCallArguments arguments)
+ {
+ var pluralRules = ValidatePluralRules(thisObject);
+ var start = arguments.At(0);
+ var end = arguments.At(1);
+
+ if (start.IsUndefined())
+ {
+ Throw.TypeError(_realm, "start is undefined");
+ }
+
+ if (end.IsUndefined())
+ {
+ Throw.TypeError(_realm, "end is undefined");
+ }
+
+ var x = TypeConverter.ToNumber(start);
+ var y = TypeConverter.ToNumber(end);
+
+ if (double.IsNaN(x) || double.IsNaN(y))
+ {
+ Throw.RangeError(_realm, "Invalid number");
+ }
+
+ // For range, we typically use the end value's plural category
+ // This is a simplification - full CLDR data would have specific range rules
+ return pluralRules.Select(y);
+ }
+
+ ///
+ /// https://tc39.es/ecma402/#sec-intl.pluralrules.prototype.resolvedoptions
+ ///
+ private JsObject ResolvedOptions(JsValue thisObject, JsCallArguments arguments)
+ {
+ var pluralRules = ValidatePluralRules(thisObject);
+
+ var result = OrdinaryObjectCreate(Engine, Engine.Realm.Intrinsics.Object.PrototypeObject);
+
+ // Properties in spec-defined order:
+ // locale, type, notation, minimumIntegerDigits,
+ // then either (minimumFractionDigits, maximumFractionDigits) OR (minimumSignificantDigits, maximumSignificantDigits),
+ // then pluralCategories
+ result.CreateDataPropertyOrThrow("locale", pluralRules.Locale);
+ result.CreateDataPropertyOrThrow("type", pluralRules.PluralRuleType);
+ result.CreateDataPropertyOrThrow("notation", pluralRules.Notation);
+ result.CreateDataPropertyOrThrow("minimumIntegerDigits", pluralRules.MinimumIntegerDigits);
+
+ // Include either fraction digits or significant digits (not both)
+ if (pluralRules.MinimumSignificantDigits.HasValue)
+ {
+ result.CreateDataPropertyOrThrow("minimumSignificantDigits", pluralRules.MinimumSignificantDigits.Value);
+ result.CreateDataPropertyOrThrow("maximumSignificantDigits", pluralRules.MaximumSignificantDigits!.Value);
+ }
+ else
+ {
+ result.CreateDataPropertyOrThrow("minimumFractionDigits", pluralRules.MinimumFractionDigits!.Value);
+ result.CreateDataPropertyOrThrow("maximumFractionDigits", pluralRules.MaximumFractionDigits!.Value);
+ }
+
+ // Return plural categories
+ var categories = pluralRules.GetPluralCategories();
+ var categoriesArray = new JsArray(_engine, (uint) categories.Length);
+ for (var i = 0; i < categories.Length; i++)
+ {
+ categoriesArray.SetIndexValue((uint) i, categories[i], updateLength: true);
+ }
+ result.CreateDataPropertyOrThrow("pluralCategories", categoriesArray);
+
+ return result;
+ }
}