Where the Currency Symbol Goes in Different Locales
8 min read · updated August 11, 2026
“Symbol before the number in English, after it in Europe” is right often enough to feel like a rule and wrong often enough to produce a bug report. The actual answer is a per-locale pattern with four independent parts, and it is published data.
Four decisions, not one
Formatting a monetary amount for a locale settles four questions, and they do not covary the way people assume:
- Position. Does the symbol precede the digits or follow them?
- Spacing. Is there a space between symbol and digits, and if so which space character? CLDR distinguishes a normal space from a no-break space here, and it matters, because a line break between a symbol and its amount is a rendering defect.
- Separators. Which characters group the thousands and mark the decimal — covered in detail in getting the decimal comma or point right.
- Negative form. Minus sign, and where; or parentheses; or a trailing sign. This is a separate pattern from the positive one, not a transformation of it.
Because they are independent, a locale can put the symbol first and use a decimal comma, or last and use a decimal point. There is no two-camp division of the world into “American” and “European” formatting.
Where the data lives
The authoritative machine-readable source is the Unicode Common Locale Data Repository, published by the Unicode Consortium. The number formatting patterns, including the currency patterns, are specified in UTS #35, Part 3: Numbers, and the per-locale values are in the CLDR data files themselves, released at cldr.unicode.org. The project ships on a published cadence of roughly two releases a year.
CLDR expresses currency formatting as a pattern string in which ¤ stands for the currency symbol — for example ¤#,##0.00 for symbol-first and #,##0.00 ¤ for symbol-last. Every ICU implementation, and therefore Intl.NumberFormat in JavaScript, babel in Python, NSNumberFormatter on Apple platforms and the JDK’s formatters, resolve to this same data. That is the important practical fact: you do not need to agree with the table, you need to call something that reads it.
A labelled set of locales
The following is what the CLDR currency patterns produce for one thousand two hundred and thirty-four units and fifty-six subunits, in each locale’s own currency. The spacing is described in words because the characters involved are not all visible.
en-US $1,234.56 symbol first, no space en-GB £1,234.56 symbol first, no space en-IN ₹1,234.56 symbol first, no space; grouping is Indian above 1,00,000 ja-JP ¥1,235 symbol first, no space; JPY has no minor unit de-DE 1.234,56 € symbol last, no-break space before it fr-FR 1 234,56 € symbol last, no-break space; group separator is also a space es-ES 1.234,56 € symbol last, no-break space it-IT 1.234,56 € symbol last, no-break space nl-NL € 1.234,56 symbol FIRST, with a space — same currency, different locale pt-BR R$ 1.234,56 symbol first, with a space pl-PL 1 234,56 zł symbol last, space sv-SE 1 234,56 kr symbol last, space de-CH CHF 1'234.56 symbol first; apostrophe grouping, decimal point fr-CA 1 234,56 $ symbol last en-CA $1,234.56 symbol first — same country, two conventions
Two rows in that list do the work. Dutch and German share the euro and disagree about where its symbol goes, which kills the idea that symbol placement is a property of the currency. English and French Canada share a country and a currency and disagree, which kills the idea that it is a property of the region. It is a property of the locale — the language-and-region pair — and that is why the lookup key has both parts in it.
The Japanese row is worth a second look for a different reason. The yen has no minor unit, so the correct number of fraction digits is zero, and a formatter that hardcodes two decimal places will produce ¥1,234.56, which is not a quantity of yen. Fraction digits are currency data rather than locale data, and are published by CLDR separately in its supplemental currency data.
The negative form is separate data
A currency pattern in CLDR can carry two subpatterns separated by a semicolon: positive, then negative. Where the negative subpattern is absent, the implementation prefixes a minus sign, which is the common case across locales. Where it is present it can do something else entirely.
The case worth knowing is accounting notation. In US financial documents a negative amount is conventionally written ($1,234.56) rather than -$1,234.56, and this is exposed as a distinct currency sign display option rather than as a different locale — in ECMA-402 it is currencySign: "accounting". Choosing between them is an editorial decision about the document you are producing, not a locale lookup, and it is a decision a model asked to “format this as currency” will make silently and inconsistently.
There is a third decision hiding behind “the symbol”, and it is which symbol. The dollar sign is used by more than twenty currencies, so $50 in a document read internationally does not identify an amount. ECMA-402 exposes four display choices — currencyDisplay set to "symbol", "narrowSymbol", "code" or "name" — and they resolve differently by locale. In en-US, Canadian dollars render as CA$50.00 under symbol and as $50.00 under narrowSymbol; in en-CA the local currency takes the bare sign and the US dollar gets the disambiguating prefix instead. The rule is that a locale disambiguates the currencies that are not its own, which is sensible and means the same amount renders differently to two readers. For anything cross-border — an invoice, a price list, a contract — the code display, giving USD 50.00, removes the ambiguity at the cost of looking bureaucratic, and that is usually the right trade.
The placement of the minus sign relative to the symbol is also locale data: some locales produce -1 234,56 € and others put the sign between symbol and digits. This is enough of a subject on its own that negative number formatting by locale takes it separately.
Looking it up rather than remembering it
All of the above is a single call in any ICU-backed environment, and the point of the table above is to show you what the call is doing, not to be copied:
const fmt = (locale, currency) =>
new Intl.NumberFormat(locale, { style: "currency", currency }).format(1234.56);
fmt("de-DE", "EUR") // "1.234,56 €"
fmt("nl-NL", "EUR") // "€ 1.234,56"
fmt("ja-JP", "JPY") // "¥1,235"
new Intl.NumberFormat("en-US", {
style: "currency", currency: "USD", currencySign: "accounting"
}).format(-1234.56) // "($1,234.56)"The one thing to check when you adopt this is that your runtime has full ICU data. Some minimal builds — Node compiled with small-icu, some container base images, some embedded runtimes — ship English-only data and silently fall back, producing US formatting for every locale you ask for. It fails without an error, which makes it look exactly like the model bug you were trying to eliminate. Test by formatting a known German amount and checking that the separators actually swapped.