Etsy Icon>

Code as Craft

How Etsy Formats Currency main image

How Etsy Formats Currency

  image

Imagine how you would feel if you went into a grocery store, and the prices were gibberish (“1,00.21 $” or “$100.A”). Would you feel confident buying from this store?

Etsy does business in more than 200 regions and 9 languages. It's important that our member experience is consistent and credible in all regions, which means we have to format prices correctly for all members.

In this post, I’ll cover:

  • Examples of bad currency formatting
  • How you can format currency correctly
  • Practical implementation decisions we made along the way

In order to follow along, you need to know one important thing: Currency formatting depends on three attributes: the currency, the member's location, and the member's language.

Examples of bad currency formatting

Here are some examples of bad currency formatting:

  • A member browsing in German goes to your site and sees an item for sale for "1,000.21 €".
  • A Japanese member sees an item selling for "¥ 847,809.34"
  • A Canadian member sees "$1.00".

If you don't know why the examples above are confusing, read on.

What's wrong with: A member browsing in German goes to your site and sees an item for sale for "1,000.21 €"?

The first example is the easiest. If a member is browsing in German, the commas and decimals in a price should be flipped. So "1,000.21 €" should really be formatted as "1.000,21 €". This isn't very confusing (as a German member, you can figure out what the price is supposed to be), but it is a bad experience.

By the way, if you are in Germany, using Euros, but browsing in English, what would you expect to see? Answer: "€1,000.21". The separators and symbol position are based on language here, not region.

What's wrong with: A Japanese member sees an item selling for "¥ 847,809.34"?

Japanese Yen doesn't have a fractional part. There's no such thing as half a Yen. So "¥ 847,809.34" could mean "¥ 847,809", or "¥ 84,780,934" or something else entirely.

What's wrong with: A Canadian member sees "$1.00"?

If your site is US-based, this can be confusing. Does "$" mean Canadian dollar or US dollar here? A simple fix is to add the currency code at the end: "$1.00 USD".

How to format currency correctly

Etsy's locale settings picker Etsy's locale settings picker

Formatting currency for international members is hard. Etsy supports browsing in 9 languages, 23 currencies, and hundreds of regions. Luckily, we don't have to figure out the right way to format in all of these combinations, because the nice folks at CLDR have done it for us. CLDR is a massive database of formatting styles that gets updated twice a year. The data gets packaged up into a portable library called libicu. Libicu is available everywhere, including mobile phones. If you want to format currency, you can use CLDR data to do it.

For each language + region + currency combination, CLDR gives you:

  • The currency symbol
  • The currency code
  • The decimal and grouping separators
  • The format pattern.

A typical pattern looks like this:

A cldr pattern (,0.00) A cldr pattern

This is the pattern for German + Germany + Euros. It tells you:

  • The currency symbol goes at the end
  • There's a space between the value and the currency symbol
  • Euros are grouped in sets of three, like $1,000,000 (vs something like rupees, that are grouped in sets of 2: $1,00,00,000. The pattern for Hindi + India + Rupees is “¤ #,##,##0.###”)
  • this is a fractional currency, formatted up to a precision of 2 (vs Japanese Yen, which is not a fractional currency, and uses the format “¤ ,0”).

NOTE: the pattern does not tell you what the decimal and grouping separators are. CLDR gives you those separately, they are not a part of the pattern. Now you can use this information to format a value:

,0.translates to 1.000,21

If you want to format prices using CLDR, your language might have libraries to do it for you already. PHP has NumberFormatter, for example. JavaScript has Intl.NumberFormat.

Practical implementation decisions

CLDR is great, but it is not the ultimate authority. It is a collaborative project, which means that anyone can add currency data to CLDR, and then everyone votes on whether the data looks correct or not. People can also vote to change existing currency data. CLDR data is not a precise thing, it is fluid and changing. Sometimes you need to customize CLDR for your use case. Here are the customizations we made.

The problem with currencies that use a dollar sign ($)

We use CLDR to format currency at Etsy, but we've made some changes to it. One issue in particular has really bugged us. Dollar currencies are really hard to work with. The symbol for CAD (Canadian dollars) is "$" in Canada, but it is "CA$" in the US and everywhere else to avoid confusion with US Dollars. So if we followed CLDR, Canadian members would see "$1.00". But our Canadian members might know that Etsy is a US-based company, in which case “$” would be ambiguous to them -- it could mean either Canadian dollars or US dollars. Here is how we choose a currency symbol to avoid confusion while still meeting member expectations:

What symbol does Etsy use for dollar-based currencies? What symbol does Etsy use for dollar-based currencies?

Here is the value “1000.21” formatted in different currency + region combinations:

05_table

You might be wondering, why not just add the currency code to the end of the price? For example, it could be “$1,000.21 USD” for US dollars, and “$1,000.21 CAD” for Canadian dollars. This is also explicit but we don’t need to have complicated logic to change the currency symbol. But this approach has another issue: redundancy.

Suppose we did add the currency code at the end everywhere to address the CAD problem. Euros would get formatted as "1.000,21 € EUR", but the "€ EUR" is redundant. Even worse, Swiss Francs doesn't have a currency symbol, so CLDR recommends using the currency code as the currency symbol. Which means they would see "1.000,21 CHF CHF", which is definitely redundant:

Adding the currency code at the end is explicit, but doesn’t meet member expectations. Our German members said they didn’t like how "1.000,21 € EUR" looked. In the end Etsy decided not to show the currency code. Instead, we change the currency symbol as needed to avoid confusion.  

Listing price with settings English / Canada / Canadian dollars Listing price with settings English / Canada / Canadian dollars

Overriding CLDR data

Here’s a simple case where we overrode CLDR formatting. We are a website, so of course we want our prices to be wrapped in html tags so that they can be styled appropriately. For example, on our listings manager, we want to format price input boxes correctly based on locale:

It’s hard to wrap a price in html tags after you have done the formatting: sometimes the symbol is at the end, sometimes there’s a space between the symbol and value, and sometimes there isn’t, etc etc. To make this work, the html tags need to be a part of the pattern, so we need to be able to override the CLDR patterns directly.

Ultimately we ended up overriding a lot of the default CLDR data:

  • symbols
  • patterns
  • pattern for negative formatting
  • adding html tags

Different libraries offered different levels of support for this. PHP’s NumberFormatter lets you override the pattern and symbol. JavaScript’s Intl.NumberFormat lets you override neither. None of the libraries had support for wrapping html tags around the output. In the end, we wrote our own JavaScript library and added wrappers for the rest.

Consistent formatting across platforms

We had to format currency in PHP, JavaScript, and in our iOS and Android apps. PHP, JavaScript, iOS and Android all had different versions of libicu, and so they had different CLDR data. How do we format consistently across these platforms? We went with a dual plan of attack: write tests that are the same across platforms, and make sure all CLDR overrides get shared between platforms.

We wrote a script that would export all our CLDR overrides as JSON / XML / plist. Every time the overrides change, we run the script to generate new data for all platforms. Here’s what our JSON file looks like right now (excerpt):

{
    "de_AU": {
        "symbol": {
            "AUD": "AU$",
            "BRL": "R$",
            "CAD": "CA$"
        },
        "decimal_separator": ",",
        "grouping_separator": ".",
        "pattern": {
            "AUD": "#,##0.00 \u00a4",
            "BRL": "#,##0.00 \u00a4",
            "CAD": "#,##0.00 \u00a4"
...

We wrote another script to generate test fixtures, which look like this (excerpt):

"test_symbol&&!code&&!html": {
    "de": {
        "DE": {
            "EUR": {
                "100000": "1.000,00 \u20ac",
                "100021": "1.000,21 \u20ac"
            }
        },
        "US": {
            "EUR": {
                "100000": "1.000,00 \u20ac",
                "100021": "1.000,21 \u20ac"
            },
            "USD": {
                "100000": "1.000,00 $",
                "100021": "1.000,21 $"
            }
        }
    }
}

This test says that given these settings:

  • Show the currency symbol
  • Hide the currency code
  • Format as plain text, not html
  • For the settings de/US/USD
  • The value 100021 should be formatted as “1.000,21 $”

We have hundreds of tests in total to check every combination of language/region/currency code with symbol shown vs. hidden, formatted as text vs. html, etc. These expected values get checked against the output of the currency formatters on all platforms, so we know that they all format currency correctly and consistently. Any time an override changes (for example, changing the symbol for CAD to be “CA$” in all regions), we update the CLDR data file so that the new override gets spread to all platforms. Then we update the test fixtures and re-run the tests to make sure the override worked on all platforms.

Conclusion

No more "¥ 847,809.34”! Formatting currency is hard. If you want to do it correctly, use the CLDR data, but make sure that you override it when necessary based on your unique circumstances. I hope our changes lead to a better experience for international members. Thanks for reading!