Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
23 changes: 23 additions & 0 deletions scripts/build/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,29 @@ export const defines: Flag[] = [
// ═══════════════════════════════════════════════════════════════════════════

export const linkerFlags: Flag[] = [
// ─── Force-include weakly-referenced archive members ───
// IntlCanonicalize.o defines Bun__canonicalizeLocaleID, which only
// libJavaScriptCore.a references and only weakly. In CI's split link the
// bun C++ objects are archived, and a weak ref never pulls an archive
// member, so without this the symbol stays null and JSC falls back to
// uloc_canonicalize. Local full builds pass the .o directly and don't
// need it, but it's harmless there.
{
flag: "-Wl,-u,_Bun__canonicalizeLocaleID",
when: c => c.darwin,
desc: "Pull IntlCanonicalize.o from the C++ archive (Mach-O underscore prefix)",
},
{
flag: "-Wl,-u,Bun__canonicalizeLocaleID",
when: c => c.unix && !c.darwin,
desc: "Pull IntlCanonicalize.o from the C++ archive",
},
{
flag: "/include:Bun__canonicalizeLocaleID",
when: c => c.windows,
desc: "Pull IntlCanonicalize.o from the C++ archive",
},

// ─── Sanitizers ───
{
flag: "-fsanitize=address",
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
54 changes: 54 additions & 0 deletions src/jsc/bindings/IntlCanonicalize.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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)
{
// uloc_forLanguageTag("und") yields "", and on macOS 13/14 libicucore
// canonicalizes "" to the process default locale; keep the root locale as-is.
Comment thread
robobun marked this conversation as resolved.
if (localeID != nullptr && localeID[0] == '\0') {
if (nameCapacity > 0)
name[0] = '\0';
return 0;
}
Comment thread
robobun marked this conversation as resolved.
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
79 changes: 77 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,83 @@ 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");
// `und` round-trips through an empty ICU locale ID; on macOS 13/14
// libicucore canonicalizes "" to the process default locale, which would
// leak into the result as e.g. "sr-Latn-US-u-va-posix".
expect(new Intl.Locale("und").toString()).toBe("und");
expect(new Intl.Locale("und-u-ca-gregory").toString()).toBe("und-u-ca-gregory");
expect(new Intl.Locale("und-x-private").toString()).toBe("und-x-private");
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