GeezerVison is showing github.com — Ship the Android and iOS V8 runtimes with ICU so Intl is available · Issue #11563 · NativeScript/NativeScript
Ship the Android and iOS V8 runtimes with ICU so Intl is available #11563DescriptionIs your feature request related to a problem? Please describe.
The NativeScript 9 Android and iOS runtimes ship V8 without ICU, so Intl does not exist:
typeof Intl // 'undefined'
new Intl.NumberFormat() // ReferenceError: Intl is not defined
Verified at runtime with @nativescript/android 9.1.1 on a physical device (Android 16) and @nativescript/ios 9.1.0 on an iOS 27 simulator. The same code works in Chromium, Safari/JSC, Node, and Hermes.
The cause is a build flag, not a runtime bug. Both build scripts in NativeScript/v8-buildscripts set it:
The script comment says this "reproduces the shape of the 10.3 build these libraries replace". The flag was carried over from the old build rather than chosen fresh. The 14.9 rebuild seems like a good moment to revisit it.
The flag removes more than the Intl constructors. These engine features also fail or quietly misbehave on Android and iOS:
| Feature |
Behavior without ICU |
Intl.* (NumberFormat, DateTimeFormat, PluralRules, Collator, Segmenter, …) |
ReferenceError |
Number#toLocaleString, Date#toLocale*String |
Options and locales ignored (#8249, #8472) |
String#localeCompare |
Locale and options ignored (#7810, #10617) |
String#normalize |
No-op |
toLocaleUpperCase / toLocaleLowerCase |
Locale rules ignored |
RegExp \p{…} property escapes |
SyntaxError at parse time |
Real-world impact:
- Libraries crash at import. Any module that builds a formatter at module scope throws on load. One example is the common
const fmt = new Intl.NumberFormat() pattern.
- i18n libraries break.
@lingui/core calls Intl.PluralRules, so every pluralized message throws.
- Bundles need patching. Libraries that use
\p{L} regexes (linkifyjs, @lexical/link, and others) fail to parse. Apps have to patch them or transpile the regexes.
Describe the solution you'd like
Build the Android and iOS V8 libraries with v8_enable_i18n_support=true, and initialize ICU in both runtimes before V8 starts.
Two packaging options would work:
- On by default. This matches browsers and Node. React Native's Hermes exposes
Intl on Android and iOS too, but delegates to the platform's ICU instead of shipping its own data — a precedent for exposing Intl by default, not for bundling ICU data (and that delegation path isn't available to V8 anyway; see alternatives).
- An ICU runtime flavor. Ship it next to the existing AAR flavors and the iOS framework, selectable from app config. This also fits the "optional" direction in WinterTC Minimum Common Web Platform API Alignment #10297 (comment). The optional piece would be the engine build, not a JS polyfill.
Size data to help weigh the cost:
ICU data file (Chromium's ICU checkout, chromium/deps/icu) |
Size |
common/icudtl.dat (full CLDR) |
10.3 MiB (4.4 MiB gzip) |
android/icudtl.dat (Chromium's Android filter) |
8.9 MiB |
ios/icudtl.dat (Chromium's iOS filter) |
6.2 MiB |
flutter/icudtl.dat (Flutter's filter — text layout only, not an Intl implementation) |
0.8 MiB |
- Ship the data once. The scripts set
icu_use_data_file=false, which embeds the data in every ABI's .so. With icu_use_data_file=true, Android can ship one architecture-independent icudtl.dat in assets. That stores it once per APK instead of once per ABI.
- Filter the data. ICU's data filters can trim locales and features further, as the Chromium and Flutter variants show.
- Precedent:
react-native-v8 reports about 7 MiB per ABI for ICU, and the jsc-android intl variant about 6 MiB.
- Not measured yet: the ICU code-size delta under these exact GN args.
Describe alternatives you've considered
FormatJS polyfills. These are the current workaround, as suggested in #10588. They work: for the cases we tested, forced FormatJS gave byte-identical formatter output on Android, iOS, Chromium, and macOS JSC. They aren't a substitute for engine ICU:
- They can't fix engine features.
\p{…} regexes, normalize, locale case mapping, and localeCompare stay broken.
- Size, measured with esbuild, minified:
- About 957 KiB (250 KiB gzip) for eight constructors with English only.
- About +337 KiB per extra locale.
- About +789 KiB for time-zone data.
Intl.Collator with all locale data is 5.8 MiB.
- Startup: importing a full bundle (about 10 MiB) took 314 ms in a debug build on a physical Android device.
- Setup burden: every app must wire up load order, locale data, and defaults itself. Without that, the polyfills fall back to
en and UTC on device.
Platform formatter bridges (android.icu, NSNumberFormatter/NSDateFormatter, like the legacy nativescript-intl). These give only a partial API whose output varies by OS version. iOS has no public plural-rules API. Hermes's maintainers describe the platform APIs as "simply inadequate for a complete and correct implementation" (meta/hermes#1211). Hermes itself took this path for space efficiency and still hasn't reached parity — it lacks PluralRules, RelativeTimeFormat, ListFormat, Locale, and Segmenter, which a third-party ICU4X library (hermes-intl) now fills.
Anything else?
- Related: WinterTC Minimum Common Web Platform API Alignment #10297 (comment on
Intl), Improved support for localeCompare #10617, Add Device number format #10588, toLocaleString() doesn't localise the number #8249, JavaScript Date functions not fully working #8472, localCompare not working with 'Umlaut' #7810.
- Where this came from: we found this building a cross-platform framework on NativeScript, where the same app code runs on web, macOS, iOS, and Android.
- Open question for maintainers: which shape would you accept — on by default or a separate flavor, and embedded data or a shared data file?
Please accept these terms
Activityfarfromrefug commented on Oct 22, 2026 @aleclarson i think this is wrong React Native's default engine, Hermes do not package Intl by default. I think they use a shim to talk to native. And i think they are right not to ship it.
here is an hermes version shipping it https://github.com/magrinj/hermes-intl. and here is a repo explaining what they actually package https://github.com/i18next/i18next-icu
I think you are also wrong to compare to flutter. I think flutter does NOT package Intl and so those ICU numbers are wrong.
Flutter has a plugin for this https://pub.dev/packages/intl . i am not sure yet how they get ICU data. Looked at the code quickly but could not find it.
We should be careful with those AI reports! aleclarson commented on Oct 25, 2026 ContributorAuthor More actions Thanks for the correction, @farfromrefug — you're right about Hermes, and I've updated the post.
Verified in the source: Intl is on by default in Hermes, but on Android it's JNI shims (lib/Platform/Intl/java/) calling android.icu.*, and on iOS it calls into system frameworks. Their own doc says it plainly: "We decided to consume the Android and iOS platform provided facilities for space efficiency, but at the cost of some variance in behaviours."
That said, the correction doesn't change the recommendation here, for three reasons:
- Delegation isn't an option for V8. Meta could take that path because they implemented ECMA-402 themselves — the "shim" is hand-written Java/ObjC inside the engine. V8's
Intl is compiled statically against ICU; there's no build flag to route it to android.icu or Foundation. Even Chromium bundles icudtl.dat on Android rather than calling the platform ICU. For V8 the choice is bundled data or no Intl at all — which is why the proposal is about how to package the data (one filtered icudtl.dat per app, opt-in flavor), not whether to ship it.
- Hermes's delegation left
Intl permanently incomplete. Stock Hermes still lacks PluralRules, RelativeTimeFormat, ListFormat, Locale, and Segmenter. That's exactly why the hermes-intl repo you linked exists — it isn't a Hermes build with ICU, it's a Rust/ICU4X library patching in the missing APIs at ~1–1.7 MB per ABI. Users ended up shipping ICU anyway.
- Half the broken surface can't be delegated or polyfilled anyway.
\p{…} regexes, String#normalize, localeCompare, toLocale* live inside V8 — only v8_enable_i18n_support fixes them, and silently-wrong localeCompare is a worse outcome than a missing constructor.
On Flutter: you're right that it doesn't expose Intl — the intl pub package is pure Dart with CLDR-generated data. But flutter/icudtl.dat is real (864,880 B in chromium/deps/icu, matching the 0.8 MiB figure); it's the engine's text-layout filter, cited only as precedent that ICU data filters work. I've annotated the table so it isn't read as an Intl claim. CatchABus commented on Nov 8, 2026 As a user of the polyfills, I can relate to the problem. Just recently, I developed a plugin to save ~1MB from the formatjs timezone polyfill and use the native apis to retrieve the data instead.
From my perspective, it would be a shame to ditch the idea of adding support using native apis.
It might be a good opportunity to try poc plugins for each Intl constructor (or a few of them for start) and see how much is missing for a complete implementation.
The nativescript-intl plugin didn't even use android ICU back in the day so it wouldn't help much to use it as a point of reference.
Of course, I wouldn't want my apps to use ICU format for thousands of dates in a chart and consequently do tons of JNI calls from JS so the maintainers may probably have to decide if there's anything that fits into the runtimes. farfromrefug commented on Nov 9, 2026 I think we could have 2 plugins to bring intl support:
- intl based on native API as @CatchABus mentioned, actually love that idea. Even though sometimes timezone data is wrong natively (happened last few month with moroco), but the same issue can happen with ICU
- an ICU plugin bringing the full ICU data to the native napi runtimes and thus enabling Intl. @ammarahm-ed i think this would be possible but i would like your opinion on this.
Having those separated from runtimes bring easier maintenance, faster release process and less runtimes releases
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|