Repository navigation
Ship the Android and iOS V8 runtimes with ICU so Intl is available #11563
Description
Is 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 definedVerified 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:
scripts/matrix/build-android.sh:v8_enable_i18n_support=falsescripts/matrix/build-ios.sh:v8_enable_i18n_support=false
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/corecallsIntl.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
Intlon Android and iOS too, but delegates to the platform's ICU instead of shipping its own data — a precedent for exposingIntlby 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. Withicu_use_data_file=true, Android can ship one architecture-independenticudtl.datin 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-v8reports about 7 MiB per ABI for ICU, and thejsc-androidintl 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, andlocaleComparestay 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.Collatorwith 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
enandUTCon 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" (facebook/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
- I have searched the existing issues as well as StackOverflow and this has not been posted before
- I agree to follow this project's Code of Conduct
Activity
farfromrefug commentedon Oct 22, 2026
@aleclarson i think this is wrong
React Native's default engine, Hermesdo 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 commentedon Oct 25, 2026
Thanks for the correction, @farfromrefug — you're right about Hermes, and I've updated the post.
Verified in the source:
Intlis on by default in Hermes, but on Android it's JNI shims (lib/Platform/Intl/java/) callingandroid.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:
Intlis compiled statically against ICU; there's no build flag to route it toandroid.icuor Foundation. Even Chromium bundlesicudtl.daton Android rather than calling the platform ICU. For V8 the choice is bundled data or noIntlat all — which is why the proposal is about how to package the data (one filteredicudtl.datper app, opt-in flavor), not whether to ship it.Intlpermanently incomplete. Stock Hermes still lacksPluralRules,RelativeTimeFormat,ListFormat,Locale, andSegmenter. That's exactly why thehermes-intlrepo 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.\p{…}regexes,String#normalize,localeCompare,toLocale*live inside V8 — onlyv8_enable_i18n_supportfixes them, and silently-wronglocaleCompareis a worse outcome than a missing constructor.On Flutter: you're right that it doesn't expose
Intl— theintlpub package is pure Dart with CLDR-generated data. Butflutter/icudtl.datis real (864,880 B inchromium/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 anIntlclaim.