35

So I was writing a small helper method to convert numbers into a valid money format ($xx,xxx.xx) using .toLocaleString(). Everything works as expected when using it inside Chrome, however it seems completely broken when using inside Node.js.

Example:

var n = 6000
console.log( n.toLocaleString('USD', {
  style: 'currency',
  currency: "USD",
  minimumFractionDigits : 2,
  maximumFractionDigits : 2
}) );

If you run this in the browser, it prints $6,000.00. If you run this snippet inside of Node.js REPL or application, it returns 6000 as a String.

Guessing this is a bug with Node.js? Is there a work around you could do here?

AlbertEngelB
  • 16,016
  • 15
  • 66
  • 93
  • 1
    Have a look at http://stackoverflow.com/questions/17935594/can-i-get-node-to-output-commas-in-number-strings-without-bringing-in-i18n They reference a github issue that seems to suggest that it's a bug/feature since they don't want to include i18n support by default. You could however compile your own node version with support included if it's important for you. – Tim Apr 21 '14 at 15:07
  • @TheShellfishMeme Good catch! It's not 100%, but I can alter it to work for what I need. – AlbertEngelB Apr 21 '14 at 15:12

4 Answers4

37

Based on this issue it appears that it was decided that shipping node.js with internationalization would make it too large. You can npm install intl and require that, and it will replace toLocaleString with a version that works.

Aaron Dufour
  • 17,288
  • 1
  • 47
  • 69
  • 2
    Unfortunately `intl`'s `.toLocaleString()` appends 'US' to the beginning of the amounts returned. (`US$50,000.00`) – AlbertEngelB Apr 21 '14 at 15:20
  • 1
    @Dropped.on.Caprica You can `.toLocaleString(...).slice(2)` to get rid of those. Or use a custom function; it depends on how much other use you're going to get out of `intl`. – Aaron Dufour Apr 21 '14 at 15:24
  • 4
    Honestly I'm 90% sure I'm going to go with something custom. Adding a workaround to a workaround isn't optimal, plus I get no other real use out of `intl` other than a fixed `.toLocaleString()` method. Thanks for the tip on `intl` though. – AlbertEngelB Apr 21 '14 at 15:34
  • 1
    I'm going to set you as the accepted answer, as I don't suggest people use my method (as it's a bit hackish for my tastes). Cheers! – AlbertEngelB Apr 22 '14 at 14:02
  • 3
    As of today, running Node v0.12.4, `intl` doesn't seem to replace `toLocaleString`. However, I get the desired result when I do `var us_format = require('intl')('en-US', {style: 'currency', currency: 'USD'})` and `us_format(12324.23)` returns `$12,324.23` – godfrzero Aug 12 '15 at 10:18
24

The results depend on the ICU data used by Node. From site.icu-project.org:

ICU is a mature, widely used set of – – libraries providing Unicode and Globalization support for software applications. ICU is widely portable and gives applications the same results on all platforms – –.

Node 13+

Starting from version 13.0.0, Node comes with full ICU support by default. This means that formatting numbers should automatically work the same way in Node (from version 13 onwards) as it does in browsers.

From v13 changelog:

Node.js releases are now built with default full-icu support. This means that all locales supported by ICU are now included and Intl-related APIs may return different values than before (Richard Lau) #29887.

Issue #19214 has also relevant discussion about this.

Note: v13 is not an LTS (long-time support) version, but v14 is, so v14 is a better choice. See Node.js Releases.

Node 12 and earlier

You need to install and enable full ICU data manually. Here's how:

  1. Run npm install full-icu --save.

  2. Run also npm install cross-env --save to support Windows users (optional but recommended).

  3. Update the scripts section of package.json to set the environment variable NODE_ICU_DATA. For example:

    {
      "scripts": {
        // Before
        "start": "react-scripts start",
        "test": "react-scripts test",
    
        // After (omit "cross-env" if you didn't install the package in step two)
        "start": "react-scripts start",
        "test": "cross-env NODE_ICU_DATA=node_modules/full-icu react-scripts test"
      }
    }
    

This way, when you run npm test, Node will load the full ICU data from node_modules/full-icu, and you should get consistent results between browser and server environments.

You could also modify the start script, but it might be unnecessary; depends on what the script does. In the example above, the script opens a browser, so modifying it would be unnecessary.

Disclaimer: I haven't tested whether this affects performance. In my case, I did this to fix Jest tests of an app that runs in the browser, so a small performance hit when running the tests would have been acceptable.

For further details, see "Internationalization support" in Node (v12) docs.

Kudos to Rndmax's answer here: Date toLocaleDateString in node

Matias Kinnunen
  • 7,828
  • 3
  • 35
  • 46
  • Does the start script need to be changed too? I'd assume that changing only the test script would suffice, because ICU should mimic the native toLocaleString behavior? Thanks for your answer though, helped a lot! – Robin Wieruch Jan 20 '21 at 08:35
  • Good point. That depends on what the `start` script does. If it opens a browser, changing the script is likely unnececssary. I updated the answer. – Matias Kinnunen Jan 20 '21 at 11:40
  • Thanks, very good explanation! Workaround for 12 Node worked for me like a charm. – Valery Kovalev Apr 22 '22 at 11:31
5

Just in case someone else stumbles upon this, here's how I formatted a number into a valid US dollar string while in a Node.js environment.

Number.prototype.toMoney = function() {
  var integer = this.toString().split('.')[0];
  var decimal = this.getDecimal();

  integer = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ",");

  if( !decimal || !decimal.length ) {
    decimal = "00";
  } else if ( decimal.length === 1) {
    decimal += '0';
  } else if ( decimal.length > 2 ) {
    decimal = decimal.substr(0, 2);
  }

  return '$' + integer + '.' + decimal;
};

Number.prototype.getDecimal = function() {
  var n = Math.abs(this);
  var dec = n - Math.floor(n);
  dec = ( Math.round( dec * 100 ) / 100 ).toString();

  if( dec.split('.').length ) {
    return dec.split('.')[1];
  } else return "";
};

There are a few boo-boo's here, namely extending the native Number prototype. You will want to avoid this is 90% of the time; this is more specific to my particular implementation.

I blatantly stole the regex for formatting the commas from this question. and hacked together the decimal support of my own volition. Mileage may vary.

Community
  • 1
  • 1
AlbertEngelB
  • 16,016
  • 15
  • 66
  • 93
2

So to update this for anyone facing the same issue...

We had used intl for our localization solution when server side rendering, but we recently had a requirement to add {timeZoneName: 'short'} to our .toLocaleString() options and this field is not supported.

Source Code from intl.js:

case 'timeZoneName':
  fv = ''; // ###TODO
  break;

Additionally, there was a breaking change in the latest patch release which forced us to lock down our version to 1.2.4.

Ultimately, we dropped usage of intl in favor of full-icu. Simply adding it to our yarn.lock solved all our Node.js server side date localization issues. Haven't verified currency localization, but so far so good. I recommend giving it a try.

Tony Rossi
  • 61
  • 3