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; + } }