From 9cfb1f57c1c71beabe5fb765cd3fadbba93395d0 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:01:45 +0000 Subject: [PATCH 1/2] ICU: backport ICU-23110 (percent formatRange scaled twice on approximately path) NumberRangeFormatterImpl::formatApproximately re-runs preProcess on data.quantity1 after the caller already preProcessed it, so any scale multiplier (scale/100 for style:"percent") is applied twice on the identity/approximately path. Intl.NumberFormat({style:"percent"}) .formatRange(0.5, 0.5) returns "~5,000%" instead of "~50%". Backports unicode-org/icu@757be359 (ICU-23110, fixed in ICU 78.1) as a build-time patch applied in every ICU-building Dockerfile. macOS is unaffected as it links Apple's system ICU, which already renders correctly. Verified the patch applies to both 75.1 (linux/musl/android/freebsd) and 73.2 (windows), and that a patched 75.1 build produces "~50%". --- Dockerfile | 1 + Dockerfile.android | 5 ++- Dockerfile.freebsd | 5 ++- Dockerfile.musl | 1 + Dockerfile.windows | 3 +- icu/icu-23110-numrange-percent.patch | 56 ++++++++++++++++++++++++++++ 6 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 icu/icu-23110-numrange-percent.patch diff --git a/Dockerfile b/Dockerfile index 483f1b557048e..346c1fa822ef5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -232,6 +232,7 @@ RUN --mount=type=tmpfs,target=/icu \ tar -xf /icu.tgz --strip-components=1 && \ rm /icu.tgz && \ patch -p1 < /icu-bun/udata-decompress-hook.patch && \ + patch -p1 < /icu-bun/icu-23110-numrange-percent.patch && \ cd source && \ ./configure --enable-static --disable-shared --disable-layoutex --disable-layout --with-data-packaging=static --disable-samples --disable-debug --disable-tests --disable-extras --disable-icuio && \ make -j$(nproc) && \ diff --git a/Dockerfile.android b/Dockerfile.android index a8e4969f21c68..4a750a07f85cc 100644 --- a/Dockerfile.android +++ b/Dockerfile.android @@ -90,6 +90,7 @@ ARG ICU_VERSION ARG ICU_RELEASE_TAG ARG ICU_SHA256 ADD --checksum=sha256:${ICU_SHA256} https://github.com/unicode-org/icu/releases/download/${ICU_RELEASE_TAG}/icu4c-${ICU_VERSION}-src.tgz /icu.tgz +COPY icu/icu-23110-numrange-percent.patch /icu-bun/ # Host build (tools only — no LTO, fast). RUN mkdir -p /icu-host && cd /icu-host \ @@ -116,7 +117,9 @@ RUN --mount=type=tmpfs,target=/icu \ export CFLAGS="${CROSS_FLAGS} ${DEFAULT_CFLAGS} ${MARCH_FLAG} -Os -std=c17 ${LTO_FLAG}" && \ export CXXFLAGS="${CROSS_FLAGS} ${DEFAULT_CFLAGS} ${MARCH_FLAG} -Os -std=c++20 -fno-exceptions ${LTO_FLAG} -fno-c++-static-destructors" && \ export LDFLAGS="${CROSS_FLAGS} --rtlib=compiler-rt -fuse-ld=lld" && \ - cd /icu && tar -xf /icu.tgz --strip-components=1 && rm /icu.tgz && cd source && \ + cd /icu && tar -xf /icu.tgz --strip-components=1 && rm /icu.tgz && \ + patch -p1 < /icu-bun/icu-23110-numrange-percent.patch && \ + cd source && \ /icu-host/source/bin/icupkg -l data/in/icudt75l.dat | grep -E '\.(cnv|spp|cfu)$|^cnvalias\.icu$|^translit/|^rbnf/|^unames\.icu$' | grep -vE '^rbnf/(root|res_index|ja|zh|zh_Hant)\.res$' > data/in/rm.lst && \ /icu-host/source/bin/icupkg --auto_toc_prefix -r data/in/rm.lst data/in/icudt75l.dat data/in/icudt75l_filtered.dat && \ mv -f data/in/icudt75l_filtered.dat data/in/icudt75l.dat && \ diff --git a/Dockerfile.freebsd b/Dockerfile.freebsd index 283e4ab9892b7..7bc11fff0d934 100644 --- a/Dockerfile.freebsd +++ b/Dockerfile.freebsd @@ -80,6 +80,7 @@ ARG ICU_VERSION ARG ICU_RELEASE_TAG ARG ICU_SHA256 ADD --checksum=sha256:${ICU_SHA256} https://github.com/unicode-org/icu/releases/download/${ICU_RELEASE_TAG}/icu4c-${ICU_VERSION}-src.tgz /icu.tgz +COPY icu/icu-23110-numrange-percent.patch /icu-bun/ # Host build (tools only — no LTO, fast). RUN mkdir -p /icu-host && cd /icu-host \ @@ -106,7 +107,9 @@ RUN --mount=type=tmpfs,target=/icu \ export CFLAGS="${CROSS_FLAGS} ${DEFAULT_CFLAGS} ${MARCH_FLAG} -Os -std=c17 ${LTO_FLAG}" && \ export CXXFLAGS="${CROSS_FLAGS} ${DEFAULT_CFLAGS} ${MARCH_FLAG} -Os -std=c++20 -fno-exceptions ${LTO_FLAG} -fno-c++-static-destructors" && \ export LDFLAGS="${CROSS_FLAGS} -fuse-ld=lld" && \ - cd /icu && tar -xf /icu.tgz --strip-components=1 && rm /icu.tgz && cd source && \ + cd /icu && tar -xf /icu.tgz --strip-components=1 && rm /icu.tgz && \ + patch -p1 < /icu-bun/icu-23110-numrange-percent.patch && \ + cd source && \ /icu-host/source/bin/icupkg -l data/in/icudt75l.dat | grep -E '\.(cnv|spp|cfu)$|^cnvalias\.icu$|^translit/|^rbnf/|^unames\.icu$' | grep -vE '^rbnf/(root|res_index|ja|zh|zh_Hant)\.res$' > data/in/rm.lst && \ /icu-host/source/bin/icupkg --auto_toc_prefix -r data/in/rm.lst data/in/icudt75l.dat data/in/icudt75l_filtered.dat && \ mv -f data/in/icudt75l_filtered.dat data/in/icudt75l.dat && \ diff --git a/Dockerfile.musl b/Dockerfile.musl index 3a1b52acc5d54..a7067360ca1c0 100644 --- a/Dockerfile.musl +++ b/Dockerfile.musl @@ -79,6 +79,7 @@ RUN --mount=type=tmpfs,target=/icu \ tar -xf /icu.tgz --strip-components=1 && \ rm /icu.tgz && \ patch -p1 < /icu-bun/udata-decompress-hook.patch && \ + patch -p1 < /icu-bun/icu-23110-numrange-percent.patch && \ cd source && \ ./configure --enable-static --disable-shared --with-data-packaging=static --disable-samples --disable-debug --disable-tests && \ make -j$(nproc) && \ diff --git a/Dockerfile.windows b/Dockerfile.windows index a86dec861827e..7d1936f66ed38 100644 --- a/Dockerfile.windows +++ b/Dockerfile.windows @@ -206,7 +206,8 @@ ADD --checksum=sha256:${ICU_SHA256} \ # The udata.cpp patch adds the weak per-item decompression hook (see the # stage comment above); it lands in the cross-built sicuuc.lib. RUN mkdir -p /icu-src && tar -xzf /icu-src.tgz -C /icu-src --strip-components=1 && rm /icu-src.tgz && \ - patch -p1 -d /icu-src < /icu-bun/udata-decompress-hook.patch + patch -p1 -d /icu-src < /icu-bun/udata-decompress-hook.patch && \ + patch -p1 -d /icu-src < /icu-bun/icu-23110-numrange-percent.patch # Stage 1: host build — only the tools matter. RUN mkdir -p /icu-host && cd /icu-host && \ diff --git a/icu/icu-23110-numrange-percent.patch b/icu/icu-23110-numrange-percent.patch new file mode 100644 index 0000000000000..7018d6ce488f5 --- /dev/null +++ b/icu/icu-23110-numrange-percent.patch @@ -0,0 +1,56 @@ +Backport of ICU-23110 (https://unicode-org.atlassian.net/browse/ICU-23110, +upstream commit 757be359). NumberRangeFormatterImpl::formatApproximately +re-ran preProcess on data.quantity1 after the caller had already preProcessed +it, so any multiplier (scale/100 for style:"percent") was applied twice on +the identity/approximately path. Fixed upstream in ICU 78.1; drop this patch +once the bundled ICU is >= 78. + +--- a/source/i18n/numrange_impl.cpp ++++ b/source/i18n/numrange_impl.cpp +@@ -160,6 +160,8 @@ void NumberRangeFormatterImpl::format(UFormattedNumberRangeData& data, bool equa + return; + } + ++ DecimalQuantity quantityBackup(data.quantity1); ++ + MicroProps micros1; + MicroProps micros2; + formatterImpl1.preProcess(data.quantity1, micros1, status); +@@ -216,7 +218,7 @@ void NumberRangeFormatterImpl::format(UFormattedNumberRangeData& data, bool equa + UNUM_IDENTITY_RESULT_EQUAL_BEFORE_ROUNDING): + case identity2d(UNUM_IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE, + UNUM_IDENTITY_RESULT_EQUAL_AFTER_ROUNDING): +- formatApproximately(data, micros1, micros2, status); ++ formatApproximately(data, quantityBackup, micros1, micros2, status); + break; + + case identity2d(UNUM_IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE, +@@ -248,15 +250,15 @@ void NumberRangeFormatterImpl::formatSingleValue(UFormattedNumberRangeData& data + + + void NumberRangeFormatterImpl::formatApproximately (UFormattedNumberRangeData& data, ++ DecimalQuantity quantity, + MicroProps& micros1, MicroProps& micros2, + UErrorCode& status) const { + if (U_FAILURE(status)) { return; } + if (fSameFormatters) { + // Re-format using the approximately formatter: + MicroProps microsAppx; +- data.quantity1.resetExponent(); +- fApproximatelyFormatter.preProcess(data.quantity1, microsAppx, status); +- int32_t length = NumberFormatterImpl::writeNumber(microsAppx.simple, data.quantity1, data.getStringRef(), 0, status); ++ fApproximatelyFormatter.preProcess(quantity, microsAppx, status); ++ int32_t length = NumberFormatterImpl::writeNumber(microsAppx.simple, quantity, data.getStringRef(), 0, status); + length += microsAppx.modInner->apply(data.getStringRef(), 0, length, status); + length += microsAppx.modMiddle->apply(data.getStringRef(), 0, length, status); + microsAppx.modOuter->apply(data.getStringRef(), 0, length, status); +--- a/source/i18n/numrange_impl.h ++++ b/source/i18n/numrange_impl.h +@@ -64,6 +64,7 @@ class NumberRangeFormatterImpl : public UMemory { + UErrorCode& status) const; + + void formatApproximately(UFormattedNumberRangeData& data, ++ DecimalQuantity quantity, + MicroProps& micros1, MicroProps& micros2, + UErrorCode& status) const; + From 46c558e61340ba83ee4a3b347019ad2b77584555 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:10:02 +0000 Subject: [PATCH 2/2] Dockerfile.{android,freebsd}: install patch ubuntu:24.04 does not ship patch and these images previously had no patch step; the new numrange patch needs it (same as Dockerfile.windows already does for the udata hook). --- Dockerfile.android | 2 +- Dockerfile.freebsd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile.android b/Dockerfile.android index 4a750a07f85cc..17bb43da1eb19 100644 --- a/Dockerfile.android +++ b/Dockerfile.android @@ -21,7 +21,7 @@ ARG NDK_VERSION RUN apt-get update && apt-get install -y --no-install-recommends \ wget unzip xz-utils ca-certificates \ - cmake ninja-build make git \ + cmake ninja-build make git patch \ ruby ruby-getoptlong perl python3 rsync file cpio \ lsb-release software-properties-common gnupg \ && rm -rf /var/lib/apt/lists/* diff --git a/Dockerfile.freebsd b/Dockerfile.freebsd index 7bc11fff0d934..2764f9b063d9a 100644 --- a/Dockerfile.freebsd +++ b/Dockerfile.freebsd @@ -24,7 +24,7 @@ ARG FREEBSD_ARCH RUN apt-get update && apt-get install -y --no-install-recommends \ wget unzip xz-utils ca-certificates \ - cmake ninja-build make git \ + cmake ninja-build make git patch \ ruby ruby-getoptlong perl python3 rsync file cpio \ lsb-release software-properties-common gnupg \ && rm -rf /var/lib/apt/lists/*