diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 1f3c8cc654..cbba762fe8 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -31,6 +31,7 @@ M(InOperatorWithObject, "'in' operator must be used on an object") \ M(InstanceOfOperatorBadPrototype, "'prototype' property of {} is not an object") \ M(IntlInvalidLanguageTag, "{} is not a structurally valid language tag") \ + M(IntlInvalidCode, "'{}' is not a valid value for option type {}") \ M(InvalidAssignToConst, "Invalid assignment to const variable") \ M(InvalidCodePoint, "Invalid code point {}, must be an integer no less than 0 and no greater than 0x10FFFF") \ M(InvalidFormat, "Invalid {} format") \ diff --git a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp index 5c26aa6027..338c0919e2 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp @@ -377,4 +377,70 @@ Value get_option(GlobalObject& global_object, Value options, PropertyName const& return value; } +// 12.1.1 CanonicalCodeForDisplayNames ( type, code ), https://tc39.es/ecma402/#sec-canonicalcodefordisplaynames +Value canonical_code_for_display_names(GlobalObject& global_object, DisplayNames::Type type, StringView code) +{ + auto& vm = global_object.vm(); + + // 1. If type is "language", then + if (type == DisplayNames::Type::Language) { + // a. If code does not match the unicode_language_id production, throw a RangeError exception. + if (!Unicode::parse_unicode_language_id(code).has_value()) { + vm.throw_exception(global_object, ErrorType::IntlInvalidCode, code, "language"sv); + return {}; + } + + // b. If IsStructurallyValidLanguageTag(code) is false, throw a RangeError exception. + auto locale_id = is_structurally_valid_language_tag(code); + if (!locale_id.has_value()) { + vm.throw_exception(global_object, ErrorType::IntlInvalidLanguageTag, code); + return {}; + } + + // c. Set code to CanonicalizeUnicodeLocaleId(code). + // d. Return code. + auto canonicalized_tag = JS::Intl::canonicalize_unicode_locale_id(*locale_id); + return js_string(vm, move(canonicalized_tag)); + } + + // 2. If type is "region", then + if (type == DisplayNames::Type::Region) { + // a. If code does not match the unicode_region_subtag production, throw a RangeError exception. + if (!Unicode::is_unicode_region_subtag(code)) { + vm.throw_exception(global_object, ErrorType::IntlInvalidCode, code, "region"sv); + return {}; + } + + // b. Let code be the result of mapping code to upper case as described in 6.1. + // c. Return code. + return js_string(vm, code.to_uppercase_string()); + } + + // 3. If type is "script", then + if (type == DisplayNames::Type::Script) { + // a. If code does not match the unicode_script_subtag production, throw a RangeError exception. + if (!Unicode::is_unicode_script_subtag(code)) { + vm.throw_exception(global_object, ErrorType::IntlInvalidCode, code, "script"sv); + return {}; + } + + // b. Let code be the result of mapping the first character in code to upper case, and mapping the second, third, and fourth character in code to lower case, as described in 6.1. + // c. Return code. + return js_string(vm, code.to_titlecase_string()); + } + + // 4. Assert: type is "currency". + VERIFY(type == DisplayNames::Type::Currency); + + // 5. If ! IsWellFormedCurrencyCode(code) is false, throw a RangeError exception. + if (!is_well_formed_currency_code(code)) { + vm.throw_exception(global_object, ErrorType::IntlInvalidCode, code, "currency"sv); + return {}; + } + + // 6. Let code be the result of mapping code to upper case as described in 6.1. + // 7. Return code. + return js_string(vm, code.to_uppercase_string()); +} + } diff --git a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h index be2f1847af..182a79f705 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h @@ -28,5 +28,6 @@ struct LocaleResult { Vector canonicalize_locale_list(GlobalObject&, Value locales); Value get_option(GlobalObject& global_object, Value options, PropertyName const& property, Value::Type type, Vector const& values, Fallback fallback); LocaleResult resolve_locale(Vector const& requested_locales, LocaleOptions const& options, Vector relevant_extension_keys); +Value canonical_code_for_display_names(GlobalObject&, DisplayNames::Type type, StringView code); } diff --git a/Userland/Libraries/LibJS/Runtime/Intl/DisplayNamesPrototype.cpp b/Userland/Libraries/LibJS/Runtime/Intl/DisplayNamesPrototype.cpp index d002c6c1e2..43ba4404a1 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/DisplayNamesPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/DisplayNamesPrototype.cpp @@ -6,11 +6,29 @@ #include #include +#include #include #include +#include namespace JS::Intl { +static DisplayNames* typed_this(GlobalObject& global_object) +{ + auto& vm = global_object.vm(); + + auto* this_object = vm.this_value(global_object).to_object(global_object); + if (!this_object) + return nullptr; + + if (!is(this_object)) { + vm.throw_exception(global_object, ErrorType::NotA, "Intl.DisplayNames"); + return nullptr; + } + + return static_cast(this_object); +} + // 12.4 Properties of the Intl.DisplayNames Prototype Object, https://tc39.es/ecma402/#sec-properties-of-intl-displaynames-prototype-object DisplayNamesPrototype::DisplayNamesPrototype(GlobalObject& global_object) : Object(*global_object.object_prototype()) @@ -25,6 +43,60 @@ void DisplayNamesPrototype::initialize(GlobalObject& global_object) // 12.4.2 Intl.DisplayNames.prototype[ @@toStringTag ], https://tc39.es/ecma402/#sec-Intl.DisplayNames.prototype-@@tostringtag define_direct_property(*vm.well_known_symbol_to_string_tag(), js_string(vm, "Intl.DisplayNames"), Attribute::Configurable); + + u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function(vm.names.of, of, 1, attr); +} + +// 12.4.3 Intl.DisplayNames.prototype.of ( code ), https://tc39.es/ecma402/#sec-Intl.DisplayNames.prototype.of +JS_DEFINE_NATIVE_FUNCTION(DisplayNamesPrototype::of) +{ + auto code = vm.argument(0); + + // 1. Let displayNames be this value. + // 2. Perform ? RequireInternalSlot(displayNames, [[InitializedDisplayNames]]). + auto* display_names = typed_this(global_object); + if (!display_names) + return {}; + + // 3. Let code be ? ToString(code). + auto code_string = code.to_string(global_object); + if (vm.exception()) + return {}; + code = js_string(vm, move(code_string)); + + // 4. Let code be ? CanonicalCodeForDisplayNames(displayNames.[[Type]], code). + code = canonical_code_for_display_names(global_object, display_names->type(), code.as_string().string()); + if (vm.exception()) + return {}; + + // 5. Let fields be displayNames.[[Fields]]. + // 6. If fields has a field [[]], return fields.[[]]. + Optional result; + + switch (display_names->type()) { + case DisplayNames::Type::Language: + break; + case DisplayNames::Type::Region: + result = Unicode::get_locale_territory_mapping(display_names->locale(), code.as_string().string()); + break; + case DisplayNames::Type::Script: + break; + case DisplayNames::Type::Currency: + break; + default: + VERIFY_NOT_REACHED(); + } + + if (result.has_value()) + return js_string(vm, result.release_value()); + + // 7. If displayNames.[[Fallback]] is "code", return code. + if (display_names->fallback() == DisplayNames::Fallback::Code) + return code; + + // 8. Return undefined. + return js_undefined(); } } diff --git a/Userland/Libraries/LibJS/Runtime/Intl/DisplayNamesPrototype.h b/Userland/Libraries/LibJS/Runtime/Intl/DisplayNamesPrototype.h index dc829cda90..76ae2f86b1 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/DisplayNamesPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/DisplayNamesPrototype.h @@ -17,6 +17,9 @@ public: explicit DisplayNamesPrototype(GlobalObject&); virtual void initialize(GlobalObject&) override; virtual ~DisplayNamesPrototype() override = default; + +private: + JS_DECLARE_NATIVE_FUNCTION(of); }; } diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/DisplayNames/DisplayNames.prototype.of.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/DisplayNames/DisplayNames.prototype.of.js new file mode 100644 index 0000000000..de19b5a725 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/DisplayNames/DisplayNames.prototype.of.js @@ -0,0 +1,46 @@ +describe("errors", () => { + test("invalid language", () => { + expect(() => { + new Intl.DisplayNames("en", { type: "language" }).of("hello!"); + }).toThrowWithMessage(RangeError, "'hello!' is not a valid value for option type language"); + }); + + test("invalid region", () => { + expect(() => { + new Intl.DisplayNames("en", { type: "region" }).of("hello!"); + }).toThrowWithMessage(RangeError, "'hello!' is not a valid value for option type region"); + }); + + test("invalid script", () => { + expect(() => { + new Intl.DisplayNames("en", { type: "script" }).of("hello!"); + }).toThrowWithMessage(RangeError, "'hello!' is not a valid value for option type script"); + }); + + test("invalid currency", () => { + expect(() => { + new Intl.DisplayNames("en", { type: "currency" }).of("hello!"); + }).toThrowWithMessage(RangeError, "'hello!' is not a valid value for option type currency"); + }); +}); + +describe("correct behavior", () => { + test("length is 1", () => { + expect(Intl.DisplayNames.prototype.of).toHaveLength(1); + }); + + test("option type region", () => { + const en = new Intl.DisplayNames("en", { type: "region" }); + expect(en.of("US")).toBe("United States"); + + const es419 = new Intl.DisplayNames("es-419", { type: "region" }); + expect(es419.of("US")).toBe("Estados Unidos"); + + const zhHant = new Intl.DisplayNames(["zh-Hant"], { type: "region" }); + expect(zhHant.of("US")).toBe("美國"); + + expect(en.of("AA")).toBe("AA"); + expect(es419.of("AA")).toBe("AA"); + expect(zhHant.of("AA")).toBe("AA"); + }); +});