Skip to content

ICU behavior discrepancy in Current and LTS Node.js versions and latest browsers #58870

Closed

Description

@sidoruk-sv

Version

v22.17.0

Platform

Darwin ... 24.5.0 Darwin Kernel Version 24.5.0: Tue Apr 22 19:54:29 PDT 2025; root:xnu-11417.121.6~2/RELEASE_ARM64_T6030 arm64

Subsystem

No response

What steps will reproduce the bug?

run node-22 (where full ICU is enabled by default) with:

node -e "console.log(new Intl.NumberFormat('uk-UA', { style: 'currency', currency: 'UAH', currencyDisplay: 'symbol' }).format(1234.56))"

and compare result with Chromium Engine Version 138+ (which use latest ICU):

console.log(new Intl.NumberFormat('uk-UA', { style: 'currency', currency: 'UAH', currencyDisplay: 'symbol' }).format(1234.56))

How often does it reproduce? Is there a required condition?

100% cases

What is the expected behavior? Why is that the expected behavior?

Output:

1 234,56 грн

Newer CLDR rules will be used with ICU 77.1+

What do you see instead?

Output:

1 234,56 ₴

Additional information

In newer ICU versions (77.1+), which are used by modern browsers the behavior for Ukrainian currency formatting is:
For locale: uk and currency: UAH with currencyDisplay: symbol, the formatted result should use the textual currency symbol грн, e.g.:

1 000,00 грн

For currencyDisplay narrowSymbol, the formatted result should use the narrow symbol ₴, e.g.:

1 000,00 ₴

So to sumarize:

ICU Version Locale currencyDisplay Output Example
76.1 uk symbol 1 000,00 ₴
77.1+ uk symbol 1 000,00 грн
76.1/77.1+ uk narrowSymbol 1 000,00 ₴

Why?

The change in ICU 77.1+ aligns with the official CLDR (Unicode Common Locale Data Repository) data, which specifies "грн" as the correct symbol for Ukrainian Hryvnia in the Ukrainian locale when using currencyDisplay: symbol. "₴" is reserved for currencyDisplay: narrowSymbol.

The solution will be to update ICU to 77.1+ version.

Activity

  1. climba03003 commented on Jun 28, 2025

    @climba03003
    Contributor

    Node.js is actually using the latest version of ICU 77.1 with CIDR 47.
    You can check the source in https://github.com/nodejs/node/tree/main/deps/icu-small or using command node -e "console.log(process.versions)".

    Chromium is not using the latest version as you stated.
    You can find the latest commit in https://chromiumdash.appspot.com/branches
    Referencing the version 138.0.7204.140 https://chromium.googlesource.com/chromium/src.git/+/c3479d8b40c9b0340e8f3cbb8fb4838cfb835f6a
    Check the third-party CIDR version https://chromium.googlesource.com/chromium/src.git/+/c3479d8b40c9b0340e8f3cbb8fb4838cfb835f6a/third_party/cldr/
    Check the third-party ICU version https://chromium.googlesource.com/chromium/src.git/+/c3479d8b40c9b0340e8f3cbb8fb4838cfb835f6a/third_party/icu
    The ICU version is linked to https://chromium.googlesource.com/chromium/deps/icu/+/b929596baebf0ab4ac7ec07f38365db4c50a559d

    In conclude Chromium 138 is using ICU 74 with CIDR 45.

    Most of the time Node.js updates ICU more often than Chromium.
    If there is difference, Chromium is actually the outdated one.

    Note: Firefox shows the same result with Node.js.

  2. sidoruk-sv commented on Jun 28, 2025

    @sidoruk-sv
    Author

    Ooofff... this bug was deeper than I thought.

    Yes, you are right about updated ICU versions used in Node.js, so I will close the issue.

    And I, on the other hand, had an incorrect assumption that browsers have to be a better "updaters" than they actually are.

    Thank you for detailed answer and time ❤


    To leave some meta and explain myself:

    I got into confusions.

    First confusion:

    The real data of CLDR v45 and v47 for that area was not touched. So for me it looks like some other parts of CLDR/ICU handling was changed, or I don't have full picture on how this things handled and defined:
    CLDR v45

    <symbol>₴</symbol>
    <symbol alt="narrow">↑↑↑</symbol>
    <symbol alt="variant">грн</symbol>
    

    https://github.com/unicode-org/cldr/blob/release-45/common/main/uk.xml#L7834-L7836

    CLDR v47

    <symbol>₴</symbol>
    <symbol alt="narrow">↑↑↑</symbol>
    <symbol alt="variant">грн</symbol>
    

    https://github.com/unicode-org/cldr/blob/release-47/common/main/uk.xml#L7889-L7891

    Second confusion:

    currencyDisplay: "narrowSymbol" - first I thought that it means "short name", so hrn or грн (in uk locale) is really a short name for "Ukrainian hryvnas" and price tags usually use that.

    Also as National Bank of Ukraine stated:

    uk en
    https://bank.gov.ua/uk/uah https://bank.gov.ua/en/uah
    Код валюти літерний – UAH The letter code of the currency is UAH
    код валюти цифровий – 980 the numerical code is 980
    скорочена назва – грн the shortened name is hrn
    графічний символ – ₴ the graphical symbol is ₴

    Third confusion:

    Inside ECMA spec there is a part about currencyDisplay:

    [[CurrencyDisplay]] is one of the String values "code", "symbol", "narrowSymbol", or "name", specifying whether to display the currency as an ISO 4217 alphabetic currency code, a localized currency symbol, or a localized currency name if formatting with the "currency" style.
    https://402.ecma-international.org/#sec-properties-of-intl-numberformat-instances

    And looks like there is no description on what should be used for narrowSymbol:
    "code" - ISO 4217 alphabetic currency code (UAH)
    "symbol" - a localized currency symbol (₴)
    "narrowSymbol" - looks like there is no explanation on what should be used for narrowSymbol
    "name" - localized currency name (Ukrainian Hryvnia)

    P.S. if you know someone from ECMA standardization team, please ping them, so this bug will be addressed (on whatever level (ECMA, CLDR, ICU) that happening).

    For me, I'm a bit tired to dig into this problem, so for now I will continue using a hack to correctly show narrowSymbol in my code

  3. climba03003 commented on Jun 28, 2025

    @climba03003
    Contributor

    Here the data being use for the currency.

    // uk.xml
    <currency type="UAH">
    	<displayName>українська гривня</displayName>
    	<displayName count="one">українська гривня</displayName>
    	<displayName count="few">українські гривні</displayName>
    	<displayName count="many">українських гривень</displayName>
    	<displayName count="other">української гривні</displayName>
    	<symbol>₴</symbol>
    	<symbol alt="narrow">↑↑↑</symbol>
    	<symbol alt="variant">грн</symbol>
    </currency>
    
    // root.xml
    <currency type="UAH">
    	<symbol alt="narrow">₴</symbol>
    </currency>

    From my understanding, the currency display should map into the below table.

    currencyDisplay data
    code currency[type]
    symbol currency.symbol
    narrowSymbol currency.symbol[alt="narrow"]
    name currency.displayName

    In other to understand why it's show difference on symbol and narrowSymbol.
    Node.js is using icu-small and Chromium is using icu.

    For the data use by Chromium is actually generated from CLDR and transformed in another format.
    Name: https://chromium.googlesource.com/chromium/deps/icu/+/b929596baebf0ab4ac7ec07f38365db4c50a559d/source/data/curr/uk.txt#932
    Symbol: https://chromium.googlesource.com/chromium/deps/icu/+/b929596baebf0ab4ac7ec07f38365db4c50a559d/source/data/curr/uk.txt#931
    Narrow Symbol: https://chromium.googlesource.com/chromium/deps/icu/+/b929596baebf0ab4ac7ec07f38365db4c50a559d/source/data/curr/root.txt#189

    You can see the generated data match the data field I mention and there is difference between the CLDR data and the generated data.

    In conclude, Node.js is actually close to the expectation on using the CLDR data. Chromium may need to update the transformed data in order to display correctly.

  4. jay123anta commented on Aug 6, 2026

    @jay123anta

    Update a year later: still diverging. Chrome 150 shows грн for symbol; stock Node at CLDR 48 (and Firefox) still show ₴ - upstream CLDR still has symbol=₴ / variant=грн, so the Chromium transformed-data divergence found above remains unfixed.

    Check your own browser against a Node snapshot (~30s, client-side): https://jay123anta.github.io/intl-drift/demo/ - from https://github.com/jay123anta/intl-drift, which catches server-vs-browser drift in CI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions