diff --git a/scripts/build/bun.ts b/scripts/build/bun.ts index be8aa5a23074..4323967dd637 100644 --- a/scripts/build/bun.ts +++ b/scripts/build/bun.ts @@ -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(); + // 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 diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 45502005f29a..5950d51bcdca 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` 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"; /** * WebKit (JavaScriptCore) — the JS engine. diff --git a/scripts/build/flags.ts b/scripts/build/flags.ts index 11077235f547..31ab9156e7da 100644 --- a/scripts/build/flags.ts +++ b/scripts/build/flags.ts @@ -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", diff --git a/scripts/build/unified.ts b/scripts/build/unified.ts index 38036a112387..4523c1334c39 100644 --- a/scripts/build/unified.ts +++ b/scripts/build/unified.ts @@ -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. diff --git a/src/jsc/bindings/IntlCanonicalize.cpp b/src/jsc/bindings/IntlCanonicalize.cpp new file mode 100644 index 000000000000..206767acda90 --- /dev/null +++ b/src/jsc/bindings/IntlCanonicalize.cpp @@ -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 . Signature matches +// uloc_canonicalize for callBufferProducingFunction. + +#if defined(__APPLE__) + +#include + +// 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. + if (localeID != nullptr && localeID[0] == '\0') { + if (nameCapacity > 0) + name[0] = '\0'; + return 0; + } + return ualoc_canonicalForm(localeID, name, nameCapacity, err); +} + +#else + +#include +#include + +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(std::strlen(canonical)); + if (length < nameCapacity) { + std::memcpy(name, canonical, static_cast(length)); + name[length] = 0; + } else { + if (nameCapacity > 0) + std::memcpy(name, canonical, static_cast(nameCapacity)); + *err = length == nameCapacity ? U_STRING_NOT_TERMINATED_WARNING : U_BUFFER_OVERFLOW_ERROR; + } + return length; +} + +#endif diff --git a/test/js/web/intl/intl.test.ts b/test/js/web/intl/intl.test.ts index 86dc3758a8e8..2ba7f216bc61 100644 --- a/test/js/web/intl/intl.test.ts +++ b/test/js/web/intl/intl.test.ts @@ -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", + ]); }); });