How to coerce currency symbols with a character prefix? E.g. US$ not $ (even if "en" is the locale)

ex_cldr and ex_money are magnificent libraries and it is probable I am doing something very wrong.

Basically I cannot display the currency dollar symbols with a character prefix when used in the same locale as the currency. This is important as I will need to disambiguate the feedback to the user.

My Cldr config is as follows:

defmodule MyApp.Cldr do
  use Cldr,
    locales: ["en", "th", "fr", "de"],
    default_locale: "en",
    add_fallback_locales: false,
    data_dir: "./priv/cldr",
    otp_app: :my_app,
    precompile_number_formats: ["¤¤#,##0.##"],
    precompile_transliterations: [{:latn, :arab}, {:thai, :latn}],
    providers: [Cldr.Number, Money],
    generate_docs: true,
    force_locale_download: false
end

libraries:

      {:ex_cldr, "~> 2.25.0"},
      {:ex_cldr_numbers, "~> 2.24.0"},
      {:ex_cldr_dates_times, "~> 2.10.2"},
      {:ex_money, "~> 5.7.4"},

I am building my knowledge off this example:
https://hexdocs.pm/ex_cldr_currencies/Cldr.Currency.html#strings_for_currency/3

Where i realize there are many possible ways to display currencies but I am not sure how do I coerce the display of a specific format.

Commands run:
string = Money.to_string!(Money.new(currency, amount), locale: "en")

It will result in displaying plain “$” with the value for locale: “en” and yet display US$ for locale: “de” for example.

  1. How do I coerce the display of the prefix: “US$” regardless of locale (which may vary) and likewise for the £/€/A$ symbols? This is to prevent any ambiguity or confusion in my system where currency symbols must be clearly displayed at all times especially for the dollar symbol which is used by countless currencies.

  2. Some symbols are missing for certain locales for example… in Singapore and S$. How do I customize the library to add in the symbol manually?

Many thanks.

2 Likes

Thanks for the kind words, and thanks for the very complete understanding of what you are trying to do.

As you know, each locale specifies currency formatting meant to be appropriate to that language and culture. The underlying data doesn’t attempt to provide a global canonical format for anything. So while CLDR data is an amazing resource, it doesn’t satisfy all requirements.

TLDR;

The nearest approximation I think is possible directly is to use the :currency_sumbol option to Money.to_string/2. This option is actually passed to Cldr.Number.to_string/3 and is documented there. I will make sure it is also documented in ex_money. For example:

iex> Money.to_string Money.new(:USD, 100), currency_symbol: :iso, locale: "en"
{:ok, "USD 100.00"}

iex> Money.to_string Money.new(:USD, 100), currency_symbol: :iso, locale: "de"
{:ok, "100,00 USD"}

iex> Money.to_string Money.new(:USD, 100), currency_symbol: :iso, locale: "fr"
{:ok, "100,00 USD"}

This outputs a consistent currency indicator using the ISO 4217 code.

Longer version

In fact you can pass any string you like to the :currency_symbol option but this definitely requires care. Here’s an example using galleons:

iex> Money.to_string Money.new(:USD, 100), currency_symbol: "Galleons", locale: "it"
{:ok, "100,00 Galleons"}

You can identify what format data drives the money - and any other CDLR number - formatting by introspection:

iex> MyApp.Cldr.Number.Format.formats_for "en", :native
{:ok,
 %Cldr.Number.Format{
   accounting: "¤#,##0.00;(¤#,##0.00)",
   currency: "¤#,##0.00",
   currency_long: %{one: [0, " ", 1], other: [0, " ", 1]},
   currency_short: [
     [1000, %{one: ["¤0K", 1], other: ["¤0K", 1]}],
     [10000, %{one: ["¤00K", 2], other: ["¤00K", 2]}],
     [100000, %{one: ["¤000K", 3], other: ["¤000K", 3]}],
     [1000000, %{one: ["¤0M", 1], other: ["¤0M", 1]}],
     [10000000, %{one: ["¤00M", 2], other: ["¤00M", 2]}],
     [100000000, %{one: ["¤000M", 3], other: ["¤000M", 3]}],
     [1000000000, %{one: ["¤0B", 1], other: ["¤0B", 1]}],
     [10000000000, %{one: ["¤00B", 2], other: ["¤00B", 2]}],
     [100000000000, %{one: ["¤000B", 3], other: ["¤000B", 3]}],
     [1000000000000, %{one: ["¤0T", 1], other: ["¤0T", 1]}],
     [10000000000000, %{one: ["¤00T", 2], other: ["¤00T", 2]}],
     [100000000000000, %{one: ["¤000T", 3], other: ["¤000T", 3]}]
   ],
   currency_spacing: %{
     after_currency: %{
       currency_match: "[[:^S:]&[:^Z:]]",
       insert_between: " ",
       surrounding_match: "[[:digit:]]"
     },
     before_currency: %{
       currency_match: "[[:^S:]&[:^Z:]]",
       insert_between: " ",
       surrounding_match: "[[:digit:]]"
     }
   },
   decimal_long: [
     [1000, %{one: ["0 thousand", 1], other: ["0 thousand", 1]}],
     [10000, %{one: ["00 thousand", 2], other: ["00 thousand", 2]}],
     [100000, %{one: ["000 thousand", 3], other: ["000 thousand", 3]}],
     [1000000, %{one: ["0 million", 1], other: ["0 million", 1]}],
     [10000000, %{one: ["00 million", 2], other: ["00 million", 2]}],
     [100000000, %{one: ["000 million", 3], other: ["000 million", 3]}],
     [1000000000, %{one: ["0 billion", 1], other: ["0 billion", 1]}],
     [10000000000, %{one: ["00 billion", 2], other: ["00 billion", 2]}],
     [100000000000, %{one: ["000 billion", 3], other: ["000 billion", 3]}],
     [1000000000000, %{one: ["0 trillion", 1], other: ["0 trillion", 1]}],
     [10000000000000, %{one: ["00 trillion", 2], other: ["00 trillion", 2]}],
     [100000000000000, %{one: ["000 trillion", 3], other: ["000 trillion", 3]}]
   ],
   decimal_short: [
     [1000, %{one: ["0K", 1], other: ["0K", 1]}],
     [10000, %{one: ["00K", 2], other: ["00K", 2]}],
     [100000, %{one: ["000K", 3], other: ["000K", 3]}],
     [1000000, %{one: ["0M", 1], other: ["0M", 1]}],
     [10000000, %{one: ["00M", 2], other: ["00M", 2]}],
     [100000000, %{one: ["000M", 3], other: ["000M", 3]}],
     [1000000000, %{one: ["0B", 1], other: ["0B", 1]}],
     [10000000000, %{one: ["00B", 2], other: ["00B", 2]}],
     [100000000000, %{one: ["000B", 3], other: ["000B", 3]}],
     [1000000000000, %{one: ["0T", 1], other: ["0T", 1]}],
     [10000000000000, %{one: ["00T", 2], other: ["00T", 2]}],
     [100000000000000, %{one: ["000T", 3], other: ["000T", 3]}]
   ],
   other: %{
     approximately: ["~", 0],
     at_least: [0, "+"],
     at_most: ["≤", 0],
     range: [0, "–", 1]
   },
   percent: "#,##0%",
   scientific: "#E0",
   standard: "#,##0.###"
 }}

And use the returned data to make a decision about what format you might want use by introspecting the currency data:

iex> {:ok, currency} = MyApp.Cldr.Currency.currency_for_code :USD, locale: "fr"
{:ok, #Cldr.Currency<"USD">}
iex> currency.
__struct__       alt_code         cash_digits      cash_rounding    
code             count            digits           from             
iso_digits       name             narrow_symbol    rounding         
symbol           tender           to               
iex> currency.symbol
"$US"
iex> currency.narrow_symbol
"$"

Singapore dollar symbols

There is a bug in the CLDR data for the Singapore dollar that should be fixed for CLDR 41 that is currently in “pre alpha” which will probably be release in April. ex_cldr will follow suit on the same day of CLDR release.

Hope this helps, happy to hear any other thoughts, ideas or suggestions!

5 Likes

This is a great reply. I now understand Cldr better and i really appreciate that.

To check my understanding, you are saying that Cldr is unable to be authoritative - and at best can only display currencies (and other data) as they are displayed in the specified locale passed to the function.

So to disambiguate, it is recommended to use the ISO names instead.

Alternatively, I have decided on a ‘band-aid solution’ by displaying the currencies in the locale: ‘th’ as I understand in Asia, they do display the country prefix before the dollar symbol. And to add on to this understanding, this may need to be refactored if my users are European and may prefer to see currencies displayed in their preferred format (i.e. as a suffix)

The other alternative of currency introspection will be less desirable than ISO formats as it would incur some performance penalty on an unnecessary subroutine.

Regarding the SGD data, I look forward to your update and thank you for all the work you have done and generously shared with the Elixir community as well as your hospitality in this forum. It is much appreciated.

2 Likes

You might find the locale en-001 is closer to your needs. 001 is the UN M.49 code for “the world” so en-001 means “world English”. its not a great locale for typical use but it might suit in this case. Here are some examples:

iex> Money.to_string Money.new(:USD, 100), locale: "en-001"
{:ok, "US$100.00"}
iex> Money.to_string Money.new(:AUD, 100), locale: "en-001"
{:ok, "A$100.00"}
iex> Money.to_string Money.new(:THB, 100), locale: "en-001"
{:ok, "THB 100.00"}
iex> Money.to_string Money.new(:SGD, 100), locale: "en-001"
{:ok, "SGD 100.00"}

However if you are intending to localise your user experience then I would propose simply using Money.to_string money, currency_symbols: :iso since at least then the number formatting, symbol placement, digit grouping and so on will be appropriate for the users expectations.

1 Like

Hi kip, many thanks for the kind suggestions.

As the locale, en-001 isn’t quite consistent e.g. displaying AUD as A$ but THB as THB not ฿ - i suppose sticking to an Asian locale such as th might be better.

Agreed your suggestion of using iso currency symbols would be better whenever I need locale-sensitive formatting such as symbol placement, digit groupings etc.

Hope to hear your views of this ugly hack of using an Asian locale in circumstances where locale-sensitivity is not a priority and just for displaying the correct currency symbol. Thanks very much

Nothing intrinsically incorrect about using the th locale since it does appear to use currency symbols for quite a lot of currencies. I tried KRW, INR, USD, CAD, AUD, JPY and CNY and all seem to deliver what you are after.

The difference will be that a user requesting a different locale will expect numbers and monies to be formatted according to that locale and may be surprised that the developers choice is overriding their own. If they don’t have an ability to select their own locale then it is what it is.

Unless you are writing a forex or other trading app, I’m not sure a wide audience would recognise the Indian Rupee symbol or the Korean Won symbol but perhaps I’m being patronising. Many (most?) of the less traded currencies (Kyrgyzstan Som, Indonesian Rupiah, etc etc) are rendered with their ISO 4217 symbol in the th locale as it is.

Anyway, I think at this point you have a good grasp of how CLDR data is used and how you can apply it. More than happy to answer any other questions and feel free to open an issue if you see any problems.

2 Likes

I understand what you are saying - I am only commenting about audience recognising currency symbols part in context of localisation.

Users will always recognise their national currency symbols. Global e-commerce sites like Amazon , PayPal, social networks use local currency symbols. Most of the sites localise based on the IP address (so people will see complete different version of the same site based on their IP address). Social networks have so much content with these symbols - but not visible to others due to social media bubbles.

Agreed, very reasonable to assume that a user in a given locale would recognise their local currency symbol.

I think the OPs post is focused on presenting multiple currencies to users and I’m not sure how familiar anyone is with currency symbols of currencies outside their locale.

You also mentioned the use of IP address to detect locale, and I recognise that’s a common “feature”. I really wish web sites would stop making assumptions about me based upon my location (derived from IP address). This is not only an unreliable way to derive location, it ignores my express preference delivered in the Accept-Language header by browsers.

<rant>
Not the central theme of your post I know, just a pet peeve of mine. I’m travelling in the US right now. My home is in Singapore. But I have my browser set to Australian English (en-AU). I don’t know why sites think my IP address is a better determinate of my preferences than the setting I expressly made to communicate that preference.
</rant>

1 Like