LibJS: Implement Intl.DisplayNames.prototype.of

Note that only option type=region is really implemented. Other types
will resort to the fallback option. This prototype method will be able
to implement other type options once LibUnicode supports more.
This commit is contained in:
Timothy Flynn 2021-08-24 23:10:50 -04:00 committed by Linus Groh
parent 38d29a40dc
commit 17bb652775
Notes: sideshowbarker 2024-07-18 05:15:23 +09:00
6 changed files with 189 additions and 0 deletions

View file

@ -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") \

View file

@ -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<RangeError>(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<RangeError>(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<RangeError>(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<RangeError>(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<RangeError>(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());
}
}

View file

@ -28,5 +28,6 @@ struct LocaleResult {
Vector<String> canonicalize_locale_list(GlobalObject&, Value locales);
Value get_option(GlobalObject& global_object, Value options, PropertyName const& property, Value::Type type, Vector<StringView> const& values, Fallback fallback);
LocaleResult resolve_locale(Vector<String> const& requested_locales, LocaleOptions const& options, Vector<StringView> relevant_extension_keys);
Value canonical_code_for_display_names(GlobalObject&, DisplayNames::Type type, StringView code);
}

View file

@ -6,11 +6,29 @@
#include <AK/TypeCasts.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/Intl/AbstractOperations.h>
#include <LibJS/Runtime/Intl/DisplayNames.h>
#include <LibJS/Runtime/Intl/DisplayNamesPrototype.h>
#include <LibUnicode/Locale.h>
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<DisplayNames>(this_object)) {
vm.throw_exception<TypeError>(global_object, ErrorType::NotA, "Intl.DisplayNames");
return nullptr;
}
return static_cast<DisplayNames*>(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 [[<code>]], return fields.[[<code>]].
Optional<StringView> 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();
}
}

View file

@ -17,6 +17,9 @@ public:
explicit DisplayNamesPrototype(GlobalObject&);
virtual void initialize(GlobalObject&) override;
virtual ~DisplayNamesPrototype() override = default;
private:
JS_DECLARE_NATIVE_FUNCTION(of);
};
}

View file

@ -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");
});
});