Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions scripts/build/bun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,10 @@ export function emitBun(n: Ninja, cfg: Config, sources: Sources): BunOutput {
// force-include would lock those in before the source can speak.
const noPchSources = new Set<string>();

// Needs the ICU C++ API (icu::Locale::createCanonical) which the PCH hides
// via wtf/Platform.h's U_SHOW_CPLUSPLUS_API=0. Also in unified.ts noUnify.
noPchSources.add(resolve(cfg.cwd, "src/jsc/bindings/IntlCanonicalize.cpp"));

// highway_json.cpp is compiled -O2 even in debug profiles (see its
// fileOverrides entry in flags.ts); a TU at a different -O level than the
// PCH cannot use the PCH ("__OPTIMIZE__ ... was disabled in precompiled
Expand Down
2 changes: 1 addition & 1 deletion scripts/build/deps/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* for local mode. Override via `--webkit-version=<hash>` to test a branch.
* From https://github.com/oven-sh/WebKit releases.
*/
export const WEBKIT_VERSION = "34c01d13391e00c06862a3d2c5b7fff350ac87e0";
export const WEBKIT_VERSION = "autobuild-preview-pr-381-edc9ed33";
Comment thread
robobun marked this conversation as resolved.

/**
* WebKit (JavaScriptCore) — the JS engine.
Expand Down
4 changes: 4 additions & 0 deletions scripts/build/unified.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ const noUnify: readonly string[] = [
// Fifth highway TU (JSON structural indexer) — same foreach_target.h
// include-guard reason.
"src/jsc/bindings/highway_json.cpp",
// Needs the ICU C++ API (icu::Locale) which wtf/Platform.h hides via
// U_SHOW_CPLUSPLUS_API=0. Bundled siblings would include ICU C headers
// first and lock out locid.h's C++-only dependencies. Also noPchSources.
"src/jsc/bindings/IntlCanonicalize.cpp",
// Declares its own minimal CGRect/kCFStringEncodingUTF8/kCFNumberDoubleType
// so it doesn't pull a CoreGraphics load command; bundled with files that
// include the real CF headers those names become ambiguous.
Expand Down
47 changes: 47 additions & 0 deletions src/jsc/bindings/IntlCanonicalize.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// CLDR-aware locale canonicalization for JSC (IntlObject.cpp, ICU-21506).
// uloc_canonicalize skips alias mappings (fre->fr, en-UK->en-GB); this wraps
// the ICU paths that don't. Built no-PCH/no-unify so wtf/Platform.h's
// U_SHOW_CPLUSPLUS_API=0 never reaches <unicode/locid.h>. Signature matches
// uloc_canonicalize for callBufferProducingFunction.
Comment thread
robobun marked this conversation as resolved.

#if defined(__APPLE__)

#include <unicode/utypes.h>

// Apple libicucore SPI (rdar://74314220); present on macOS 13+ per SDK .tbd.
extern "C" int32_t ualoc_canonicalForm(const char*, char*, int32_t, UErrorCode*);

extern "C" int32_t Bun__canonicalizeLocaleID(const char* localeID, char* name, int32_t nameCapacity, UErrorCode* err)
{
return ualoc_canonicalForm(localeID, name, nameCapacity, err);
}

#else

#include <unicode/locid.h>
#include <cstring>

extern "C" int32_t Bun__canonicalizeLocaleID(const char* localeID, char* name, int32_t nameCapacity, UErrorCode* err)
{
if (err == nullptr || U_FAILURE(*err))
return 0;
// Runs ICU's CLDR AliasReplacer since ICU 68; static link, so C++ ABI is fixed.
icu::Locale locale = icu::Locale::createCanonical(localeID);
if (locale.isBogus()) {
*err = U_ILLEGAL_ARGUMENT_ERROR;
return 0;
}
const char* canonical = locale.getName();
int32_t length = static_cast<int32_t>(std::strlen(canonical));
if (length < nameCapacity) {
std::memcpy(name, canonical, static_cast<size_t>(length));
name[length] = 0;
} else {
if (nameCapacity > 0)
std::memcpy(name, canonical, static_cast<size_t>(nameCapacity));
*err = length == nameCapacity ? U_STRING_NOT_TERMINATED_WARNING : U_BUFFER_OVERFLOW_ERROR;
}
return length;
}

#endif
73 changes: 71 additions & 2 deletions test/js/web/intl/intl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,77 @@ describe("Intl.getCanonicalLocales", () => {
mo: Intl.getCanonicalLocales("mo")[0],
ji: Intl.getCanonicalLocales("ji")[0],
}).toEqual({ in: "id", iw: "he", mo: "ro", ji: "yi" });
// sh/tl/no are kept as-is (ICU ships bundles under both names)
expect(Intl.getCanonicalLocales(["sh", "tl", "no"])).toEqual(["sh", "tl", "no"]);
// `no` is the CLDR macrolanguage and stays as-is; `sh`/`tl` map to their
// CLDR replacements via the languageAlias table.
expect(Intl.getCanonicalLocales(["sh", "tl", "no"])).toEqual(["sr-Latn", "fil", "no"]);
});
});

// https://github.com/oven-sh/bun/issues/14713
// ICU's uloc_canonicalize does not apply CLDR alias mappings; JSC must route
// through icu::Locale::createCanonical / ualoc_canonicalForm to get the
// UTS #35 canonical form that ECMA-402 requires.
describe("Intl.Locale canonicalization", () => {
test("deprecated language subtags", () => {
expect({
fre: new Intl.Locale("fre").baseName,
cmn: new Intl.Locale("cmn").baseName,
sh: new Intl.Locale("sh").baseName,
tl: new Intl.Locale("tl").baseName,
aam: new Intl.Locale("aam").baseName,
}).toEqual({ fre: "fr", cmn: "zh", sh: "sr-Latn", tl: "fil", aam: "aas" });
});

test("deprecated region subtags", () => {
expect({
"en-uk": new Intl.Locale("en-uk").baseName,
"en-DD": new Intl.Locale("en-DD").baseName,
"en-BU": new Intl.Locale("en-BU").baseName,
"ru-SU": new Intl.Locale("ru-SU").baseName,
"hy-SU": new Intl.Locale("hy-SU").baseName,
"und-Armn-SU": new Intl.Locale("und-Armn-SU").baseName,
}).toEqual({
"en-uk": "en-GB",
"en-DD": "en-DE",
"en-BU": "en-MM",
"ru-SU": "ru-RU",
"hy-SU": "hy-AM",
"und-Armn-SU": "und-Armn-AM",
});
});

test("canonicalization runs before and after option overrides", () => {
// CanonicalizeUnicodeLocaleId is applied to the input tag before options
// are merged, so SU resolves against und-Armn (-> AM) rather than ru (-> RU).
expect(new Intl.Locale("und-Armn-SU", { language: "ru" }).toString()).toBe("ru-Armn-AM");
// And again after, so deprecated option values are also replaced.
expect(new Intl.Locale("en", { region: "uk" }).toString()).toBe("en-GB");
expect(new Intl.Locale("fre", { region: "uk" }).toString()).toBe("fr-GB");
expect(new Intl.Locale("und", { language: "sh" }).toString()).toBe("sr-Latn");
});

test("derived getters reflect the canonical form", () => {
const uk = new Intl.Locale("en-uk");
const sh = new Intl.Locale("sh");
expect({ region: uk.region, language: sh.language, script: sh.script }).toEqual({
region: "GB",
language: "sr",
script: "Latn",
});
});

test("unicode extension keywords survive alias replacement", () => {
expect(new Intl.Locale("fre-u-ca-gregory").toString()).toBe("fr-u-ca-gregory");
expect(new Intl.Locale("en-uk-u-ca-gregory").toString()).toBe("en-GB-u-ca-gregory");
});

test("Intl.getCanonicalLocales applies the same alias mappings", () => {
expect(Intl.getCanonicalLocales(["fre", "en-uk", "und-Armn-SU", "cmn"])).toEqual([
"fr",
"en-GB",
"und-Armn-AM",
"zh",
]);
});
});

Expand Down
Loading