#nullable enable using ICU4N.Globalization; using ICU4N.Impl; using ICU4N.Text; using ICU4N.Util; using Jint.Native.Intl; namespace Jint.Tests.Test262; /// /// CLDR provider implementation that combines ICU4N features with default provider fallback. /// Uses ICU4N's PluralRules for plural category selection and ICUResourceBundle for /// direct CLDR data access (unit patterns, list patterns, etc.). /// Falls back to DefaultCldrProvider when ICU4N data is not available. /// public sealed class IcuCldrProvider : ICldrProvider { private readonly ICldrProvider _fallback = DefaultCldrProvider.Instance; /// /// Singleton instance. /// public static readonly IcuCldrProvider Instance = new(); private IcuCldrProvider() { } // === CLDR Resource Bundle Access === private static UResourceBundle? GetBundle(string locale) { try { var culture = new UCultureInfo(locale); return UResourceBundle.GetBundleInstance(ICUData.IcuBaseName, culture); } catch { return null; } } private static UResourceBundle? GetBundleAt(UResourceBundle bundle, string path) { try { foreach (var segment in path.Split('/')) { bundle = bundle.Get(segment); } return bundle; } catch { return null; } } private static string? TryGetString(UResourceBundle bundle, string key) { try { return bundle.GetString(key); } catch { return null; } } // === List Patterns === // ICU4N doesn't have ListFormatter API, but we can access CLDR data directly public ListPatterns? GetListPatterns(string locale, string type, string style) { // Map Intl type/style to CLDR path var cldrType = (type, style) switch { ("conjunction", "long") => "standard", ("conjunction", "short") => "standard-short", ("conjunction", "narrow") => "standard-narrow", ("disjunction", "long") => "or", ("disjunction", "short") => "or-short", ("disjunction", "narrow") => "or-narrow", ("unit", "long") => "unit", ("unit", "short") => "unit-short", ("unit", "narrow") => "unit-narrow", _ => "standard" }; var path = $"listPattern/{cldrType}"; try { var bundle = GetBundle(locale); if (bundle == null) { return _fallback.GetListPatterns(locale, type, style); } var listBundle = GetBundleAt(bundle, path); if (listBundle == null) { return _fallback.GetListPatterns(locale, type, style); } var two = TryGetString(listBundle, "2"); var start = TryGetString(listBundle, "start"); var middle = TryGetString(listBundle, "middle"); var end = TryGetString(listBundle, "end"); // If we don't have the patterns, fall back if (two == null && start == null && end == null) { return _fallback.GetListPatterns(locale, type, style); } return new ListPatterns { Two = two ?? "{0}, {1}", Start = start ?? "{0}, {1}", Middle = middle ?? "{0}, {1}", End = end ?? "{0}, {1}" }; } catch { return _fallback.GetListPatterns(locale, type, style); } } // === Relative Time Patterns === // ICU4N doesn't have RelativeDateTimeFormatter, use fallback public RelativeTimePatterns? GetRelativeTimePatterns(string locale, string unit, string style) => _fallback.GetRelativeTimePatterns(locale, unit, style); public string? GetRelativeTimeSpecialPhrase(string locale, string unit, int value, bool past, string style) => _fallback.GetRelativeTimeSpecialPhrase(locale, unit, value, past, style); // === Number Formatting === public string? GetNumberingSystemDigits(string numberingSystem) { // Use ICU's numbering system data try { var ns = NumberingSystem.GetInstanceByName(numberingSystem); if (ns != null && !ns.IsAlgorithmic) { return ns.Description; // Contains the digit characters } return _fallback.GetNumberingSystemDigits(numberingSystem); } catch { return _fallback.GetNumberingSystemDigits(numberingSystem); } } public string? GetDefaultNumberingSystem(string locale) { try { var culture = new UCultureInfo(locale); var ns = NumberingSystem.GetInstance(culture); return ns?.Name; } catch { return _fallback.GetDefaultNumberingSystem(locale); } } public CompactPatterns? GetCompactPatterns(string locale, string style) => _fallback.GetCompactPatterns(locale, style); public Jint.Native.Intl.CurrencyData? GetCurrencyData(string locale, string currencyCode) => _fallback.GetCurrencyData(locale, currencyCode); public UnitPatterns? GetUnitPatterns(string locale, string unit, string style) { // Map Intl unit names to CLDR paths var cldrUnit = MapToCldrUnit(unit); var path = $"units/{style}/{cldrUnit}"; try { var bundle = GetBundle(locale); if (bundle == null) { return _fallback.GetUnitPatterns(locale, unit, style); } var unitBundle = GetBundleAt(bundle, path); if (unitBundle == null) { return _fallback.GetUnitPatterns(locale, unit, style); } var displayName = TryGetString(unitBundle, "displayName"); // Get patterns for each plural category var other = TryGetString(unitBundle, "unitPattern-count-other"); var one = TryGetString(unitBundle, "unitPattern-count-one"); var zero = TryGetString(unitBundle, "unitPattern-count-zero"); var two = TryGetString(unitBundle, "unitPattern-count-two"); var few = TryGetString(unitBundle, "unitPattern-count-few"); var many = TryGetString(unitBundle, "unitPattern-count-many"); // If we don't have any patterns, fall back if (other == null && one == null) { return _fallback.GetUnitPatterns(locale, unit, style); } return new UnitPatterns { DisplayName = displayName ?? unit, Other = other ?? $"{{0}} {unit}", One = one, Zero = zero, Two = two, Few = few, Many = many }; } catch { return _fallback.GetUnitPatterns(locale, unit, style); } } private static string MapToCldrUnit(string unit) { return unit switch { // Duration units "year" or "years" => "duration-year", "month" or "months" => "duration-month", "week" or "weeks" => "duration-week", "day" or "days" => "duration-day", "hour" or "hours" => "duration-hour", "minute" or "minutes" => "duration-minute", "second" or "seconds" => "duration-second", "millisecond" or "milliseconds" => "duration-millisecond", "microsecond" or "microseconds" => "duration-microsecond", "nanosecond" or "nanoseconds" => "duration-nanosecond", // Length units "meter" => "length-meter", "kilometer" => "length-kilometer", "centimeter" => "length-centimeter", "millimeter" => "length-millimeter", "inch" => "length-inch", "foot" => "length-foot", "yard" => "length-yard", "mile" => "length-mile", // Mass units "gram" => "mass-gram", "kilogram" => "mass-kilogram", "milligram" => "mass-milligram", "pound" => "mass-pound", "ounce" => "mass-ounce", // Other common units "liter" => "volume-liter", "milliliter" => "volume-milliliter", "gallon" => "volume-gallon", "celsius" => "temperature-celsius", "fahrenheit" => "temperature-fahrenheit", "percent" => "concentr-percent", "byte" => "digital-byte", "kilobyte" => "digital-kilobyte", "megabyte" => "digital-megabyte", "gigabyte" => "digital-gigabyte", "terabyte" => "digital-terabyte", _ => unit }; } // === Date/Time Formatting === // Note: ICU4N doesn't have DateFormatSymbols ported yet, so we use the fallback provider // which uses .NET's CultureInfo for basic date/time data. public DateTimePatterns? GetDateTimePatterns(string locale, string? dateStyle, string? timeStyle) => _fallback.GetDateTimePatterns(locale, dateStyle, timeStyle); public string[]? GetMonthNames(string locale, string style, string? calendar) => _fallback.GetMonthNames(locale, style, calendar); public string[]? GetWeekdayNames(string locale, string style) => _fallback.GetWeekdayNames(locale, style); public string[]? GetDayPeriods(string locale, string style, string? calendar) => _fallback.GetDayPeriods(locale, style, calendar); public string[]? GetEraNames(string locale, string style, string? calendar) => _fallback.GetEraNames(locale, style, calendar); // === Display Names === public string? GetCurrencyDisplayName(string locale, string code) => _fallback.GetCurrencyDisplayName(locale, code); // === Locale Data === public string? GetLikelySubtags(string locale) { try { var icuLocale = new UCultureInfo(locale); var maximized = UCultureInfo.AddLikelySubtags(icuLocale); return maximized?.Name ?? _fallback.GetLikelySubtags(locale); } catch { return _fallback.GetLikelySubtags(locale); } } public WeekInfo? GetWeekInfo(string locale) => _fallback.GetWeekInfo(locale); // === Supported Values === public IReadOnlyCollection GetSupportedCalendars() => _fallback.GetSupportedCalendars(); public IReadOnlyCollection GetSupportedCollations() => _fallback.GetSupportedCollations(); public IReadOnlyCollection GetSupportedCurrencies() => _fallback.GetSupportedCurrencies(); public IReadOnlyCollection GetSupportedNumberingSystems() { // Use fallback - ICU4N's list may not include all required numbering systems // Our embedded NumberingSystemData has the complete ECMA-402 spec list return _fallback.GetSupportedNumberingSystems(); } public IReadOnlyCollection GetSupportedTimeZones() => _fallback.GetSupportedTimeZones(); public IReadOnlyCollection GetSupportedUnits() => _fallback.GetSupportedUnits(); // === Plural Rules === public string SelectPluralCategory(string locale, double value, string type) { try { var culture = new UCultureInfo(locale); var pluralType = string.Equals(type, "ordinal", StringComparison.Ordinal) ? PluralType.Ordinal : PluralType.Cardinal; var rules = PluralRules.GetInstance(culture, pluralType); var category = rules.Select(value); // ICU4N returns the category name (e.g., "one", "other", "few", "many", "zero", "two") return category ?? "other"; } catch { // Fallback to default provider's English rules return _fallback.SelectPluralCategory(locale, value, type); } } }