Software localization

How Do I Convert a Decimal to a String with Thousands Separators?

When converting codebase numbers into locale-aware strings, we often need to take care of thousands separators. Here's how to get it done easily.
Software localization blog category featured image | Phrase

This is a common concern when working with i18n. We have a number as a double or float, and we want to present it to our users as a string with proper, locale-aware formatting. That often means we need to take care of thousands separators; so 1000000 is formatted to the string "1,000,000", in the US English (en-US) locale, for example. Let's take a look at how to convert a decimal to a string.

The Basic Algorithm

The basic function for converting a number to a thousands-separted string looks like this:

function formatWithThousandsSeparator(num) {

  let numAsString = num.toString();

  let characters = numAsString.split("").reverse();

  let parts = [];

  for (let i = 0; i < characters.length; i += 3) {

    let part = characters.slice(i, i + 3).reverse().join("");

    parts.unshift(part);

  }

  return parts.join(",");

}

This is in JavaScript, but the algorithm can be applied in any language:

  1. Split the number into separate characters or strings, one for each digit,
  2. iterate through the list of digit characters from back to front, taking a slice of three digit characters each time, and,
  3. join these slices back into a string, separating the slices with a comma character.

Of course, we have to be mindful when slicing our parts so that we don't go outside the bounds of our characters list or array. The JavaScript array slice() method is kind enough to stop at the bounds of an array when it takes a slice, regardless of how zealous we are with the range we've given it. If you're implementing this algorithm in a different programming language, you might have to check within your loop if you're going to step outside the bounds of an array when you slice, and guard against it.

We could call the function above like, formatWithThousandsSeparators(1000000), and get the output "1,000,000". Of course, that's not the whole story. Read on.

Accounting for a Locale's Numeral System and Separator Characters

We've been using the Western Arabic (Latin) numeral system, or set of digits (0, 1, 2, 3, ...), when formatting numbers. We've also been using the US English separation rules. However, different locales can use different numeral systems. Arabic locales can prefer the Eastern Arabic numeral system (٠، ١، ٢، ٣، ...) when formatting numbers, for example.

Also, different locales have different rules around where a number is separated. The Hindi locale, for instance, while prefering the Western Arabic numeral system, "groups the rightmost three digits together (until the hundreds place), and thereafter groups by sets of two digits." (Wikipedia). So, in Hindi, 1000000 should be formatted as "10,00,000".

As you can see, when localizing our software, we can't assume the numeral system (digit characters), or separation rules. In fact, we can't even assume the separation characters, since different locales will use different characters when separating their digits. As an example, 1000000.00 in French would be formatted as "1 000 000,00". Note that the space character is used for thousands sepration, and the comma for decimal separation.

🤿 Go deeper » We've written A Concise Guide to Number Localization which covers numeral systems and separator characters. Check it out.

Of course, we could alter the algorithm we wrote above to account for these things. Luckily, however, smart people in the world over have already tackled these problems, and locale-aware number formatting is baked into many popular programming languages and their i18n libraries. Here are a few popular examples.

✋🏽 Heads Up » Some of the following first-party solutions will only work with Western Arabic (Latin) numerals. Or at least they take a lot of digging to make them display other numeral systems. However, you can probably find third-party libraries that work with many numeral systems with little to no fuss.

C#

In C#, the System.Globalization.CultureInfo class is our friend, and we can pass an instance of it to many of the built-in C# string formatting methods. Just pass the CultureInfo constructor the locale code you want to format for.

using System;

using System.Globalization;

namespace myApp

{

    class Program

    {

        static void Main()

        {

            double value = 123456.78d;

            IFormatProvider usFormatProvider =

                new System.Globalization.CultureInfo("en-US");

            Console.WriteLine(value.ToString("#,##0.00", usFormatProvider));

            // => 123,456.78 (American format)

            IFormatProvider frenchFormatProvider =

                new System.Globalization.CultureInfo("fr-FR");

            Console.WriteLine(value.ToString("#,##0.00", frenchFormatProvider));

            // => 123 456,78 (French format)

        }

    }

}

JavaScript

The standard Intl.NumberFormat consturctor in JavaScript does exactly what we want here.

const number = 123456.78;

const usFormatter = new Intl.NumberFormat("en-US");

console.log(usFormatter.format(number)); // => 123,456.78 (American format)

const frenchFormatter = new Intl.NumberFormat("fr-FR");

console.log(frenchFormatter.format(number)); // => 123 456,78 (French format)

const arabicFormatter = new Intl.NumberFormat("ar-AR");

console.log(arabicFormatter.format(number)); // => ١٢٣٬٤٥٦٫٧٨ (Arabic format)

PHP

The built-in NumberFormatter class does the trick for us in PHP.

<?php

$number = 123456.78;

$americanFormatter = new NumberFormatter('en-US', NumberFormatter::DECIMAL);

echo $americanFormatter->format($number); // => 123,456.78 (American format)

$frenchFormatter = new NumberFormatter('fr-FR', NumberFormatter::DECIMAL);

echo $frenchFormatter->format($number); // => 123 456,78 (French format)

$arabicFormatter = new NumberFormatter('ar-AR', NumberFormatter::DECIMAL);

echo $arabicFormatter->format($number); // => ١٢٣٬٤٥٦٫٧٨ (Arabic format)

Java

The standard NumberFormat class composes with a Locale object to format numbers per-locale in Java.

import java.util.Locale;

import java.text.NumberFormat;

public class FormatNumbers {

  public static void main(String []args) {

    double number = 123456.78;

    NumberFormat usFormatter = NumberFormat.getInstance(new Locale("en", "US"));

    System.out.println(usFormatter.format(number));

    // => 123,456.78 (American format)

    NumberFormat frenchFormatter = NumberFormat.getInstance(new Locale("fr", "FR"));

    System.out.println(frenchFormatter.format(number));

    // => 123 456,78 (French format)

    NumberFormat thaiFormatter =

      NumberFormat.getInstance(new Locale("th", "TH", "TH"));

    System.out.println(thaiFormatter.format(number));

    // => ๑๒๓,๔๕๖.๗๘ (Thai format)

 }

}

Python

Python's built-in localized number formatting is such a pain to use, I strongly recommend that you employ a third-party library for the job. Babel will do the trick for many purposes (although it doesn't seem to cover non-Latin numeral systems). Follow the official documentation to install Babel. After that the following code will work.

from babel.numbers import format_decimal

value = 123456.78

print(format_decimal(value, locale='en_US')) # => 123,456.78 (American format)

print(format_decimal(value, locale='fr_FR')) # => 123 456,78 (French format)

In Conclusion

Formatting a number with thousands separation isn't rocket science. We do have to be aware of locale differences when formatting in our i18n work, however. And to make your i18n work buttery smooth, take a look at Phrase. An all-in-one i18n platform, built by developers for developers, Phrase features a comprenhesive CLI, API, GitHub and BitBucket sync, over-the-air (OTA) translations, and much more. These features, along with the translator-friendly web console for managing translation strings, and others, have resulted in a 50% reduction in development teams' deployment time. Check out all of Phrase's products, and sign up for a free 14-day trial.