ZF-10038: Zend_Currency uses locale settings to determine currency

Description

There is no way using Zend_Currency to show a price in dollars to a German user (browser language set to 'de_DE') geo-located in the US, because 'de_US' is not a valid locale.

I'm not clear on why Zend_Currency determines the currency to use by doing a locale region-based lookup. It would be much better to be able to explicitly set the currency using the ISO 4217 code ('USD' etc), and only use a locale region-based lookup as a fallback/default if the ISO 4217 code is not provided.

Related, it's counter-intuitive that the 'currency' option to Zend_Currency specifies the currency abbreviation to display, rather than actually specifying which currency to use for the object.

Basically I expect to be able to do the following:


$currency = new Zend_Currency(array(
                'value' => 1234.56
                'set_currency' => 'USD',
                'format'   => 'de'));
print $currency; // returns $1.234,56

Currently Zend_Currency throws an exception if a user's browser has no region information in the locale (e.g. 'de' instead of 'de_DE'). This is because as currently built, Zend_Currency is dependent on the locale region to determine currency to display. With the changes above, Zend_Currency should only throw an exception if a) 'set_currency' did not contain a valid ISO 4217 code AND b) the locale region is not set.

Comments

This is not an issue for multiple reasons. Firstly, you can explicitly state a locale to use as the second param of the constructor. Secondly, the automatic lookup is because you have not initilalised Zend_Locale, so it is doing its best (according to common best practice) to work out the expected locale for the http client. lastly, the automatic lookup can be overridden in your config if you are using Zend_Application with resources.local = "de_DE"

Please direct support requests to either the mailing list, or #zftalk on IRC. the issue tracker is not an appropriate place.

Did you read my issue Ryan? You've given me three different ways of setting a de_DE locale. That's great, and I was aware of all three of them (they're all in the Zend documentation).

But a locale of de_DE will not allow me to show a German-formatted dollar price to a German based in the US. To do that, I would have to set a locale of 'de_US', and we both know that locale doesn't exist. (Zend throws a "Uncaught exception 'Zend_Currency_Exception' with message 'No region found within the locale 'de'" if you try it).

The problem is a simple one: using a locale region as the only way to determine which currency a Zend_Currency object can display is wrong. Not being able to manually configure the currency of a Zend Currency object using a valid ISO 4217 code is just odd.

My German in the US example is perhaps a little niche, but I can think of plenty of other examples where you would want to manually configure Zend_Currency objects using ISO 4217s (say, any forex application).

Yes, I did read it.

from your comment now, it seems more that you have an issue with how locale codes are universally used, not just with ZF.

If you want to do non-standard formatting, then you must use non-standard means to do so. ZF cannot possibly accommodate all non-standard possibilities, and for it to attempt to do so, would be just wrong.

Hey Ryan,

I don't have any issues with the locale system - I'm very happy for de_US not to exist as a locale code (there's no reason why it needs to exist). My problem is specifically with ZF using locale region as the only way of configuring a Zend Currency object.

As ZF currently stands, to create a Zend Currency object, it's not enough for me to know the ISO 4217 currency code for the currency I want to create. I also need to have up my sleeve a valid locale for a country which uses that currency. I can't write: "create a currency in EUR", I have to write: "create a currency for, let's say, de_DE".

I've just looked in the Zend codebase and the problem seems to be an architectural one: namely that currencies are merely attributes () of individual locales (en_US.xml etc). I would suggest that currencies should be entities in their own right, if the Zend_Currency object is going to be of general use to people doing work with currencies.

(As it stands, I'm now using my own lookup table and Zend_Locale_Format in place of Zend_Currency.)

Reopened issue:

Note: I expect that you did not read the manual completly. The locale itself is not needed to set the currency.

You can define ANY setting of a currency. Still at initiation the locale must be given to know the default number format. Zend_Currency MUST have any sort of locale as the needed formatting and currency informations are only available within the locale database.

Your parameters are: "format" can be used to set a own number formatting. "currency" can be used to set the wished currency (f.e. "USD"). "locale" must be set to know the proper grouping and decimal signs.

So the proper usecase for your example would be the following:


array('locale' => 'de_DE', 'currency' => 'USD');

When you know that you must display the currency with english notation then simply use "en_US" as locale.

Locale upgrading is not that easy but it actually in development for Zend_Locale. As long as it's not available Zend_Currency works as is which means that for default number formatting and number recognition a proper locale must be given.

Hi Thomas,

Thanks for your detailed and thoughtful response.

Unfortunately, array('locale' => 'de_DE', 'currency' => 'USD') throws an exception if the user has their browser locale set to 'de' rather than e.g. 'de_DE'.

Also: you say that that "currency" can be used to set the wished currency (e.g. "USD"), but the manual (http://zendframework.com/manual/en/…) clearly says:

"currency: Defines the abbreviation which can be displayed."

In other words, 'currency' merely customises the currency shortname - it does not actually override currency as inferred by the region locale. I've done some additional tests so that I can understand empirically the behaviour of Zend_Currency - I reproduce them below because I don't believe they are consistent with the behaviour of Zend_Currency as you describe, or even as the manual describes. All tests were with browser locale set to 'de', because that seems to be a better way of detecting issues with Zend_Currency:

$currency = new Zend_Currency(array('value' => 1234.56, 'locale' => 'de_DE', 'format' => 'de', 'currency' => 'USD')); // Throws exception. 'locale' does not override browser locale, which is region-less

$locale = new Zend_Locale('en_GB'); Zend_Registry::set('Zend_Locale', $locale); $currency = new Zend_Currency(array('value' => 1234.56, 'format' => 'de', 'display' => Zend_Currency::USE_SYMBOL)); print $currency; // Shows '1.234,56 £', using GBP as inferred from the locale region, and German formatting style

$locale = new Zend_Locale('en_GB'); Zend_Registry::set('Zend_Locale', $locale); $currency = new Zend_Currency(array('value' => 1234.56, 'format' => 'de', 'currency' => 'USD', 'display' => Zend_Currency::USE_SYMBOL)); print $currency; // Still shows '1.234,56 £' - i.e. 'currency' of 'USD' does not override 'locale'

$locale = new Zend_Locale('en_GB'); Zend_Registry::set('Zend_Locale', $locale); $currency = new Zend_Currency(array('value' => 1234.56, 'format' => 'de', 'currency' => 'USD', 'display' => Zend_Currency::USE_SHORTNAME)); print $currency; // Still shows '1.234,56 £', 'currency' does not successfully override 'locale' and 'display' is not working

My conclusions from these tests would be: - There seems to be a bug where 'locale' does not override a region-less browser locale, and an exception is thrown - There seems to be a bug whereby the 'display' settings e.g. Zend_Currency::USE_SHORTNAME do not work - As per my original issue title, Zend_Currency can only use locale settings to determine a currency object

My recommendations would be: - Rename the 'currency' option to 'shortname' to make it clear that this option is just about customising the currency abbreviation aka shortname - Add a new 'currency' option which takes a ISO 4217 code to define which currency a Zend_Currency object should represent - Do not throw an exception if locale region is not available through any of the three methods set out by Ryan, as long as both 'currency' and 'format' are supplied as options

Nope... my fault... ZF 2 code will not work with ZF 1 ;-)

It should be:


Zend_Currency(array('currency' => 'USD'), 'de_DE');

Otherwise the locale parameter is null and it will use automatic detection which means browser first hand.

Regarding USE_SHORTNAME and USE_SYMBOL: When there is no shortname or symbol defined for a currency within the defined locale, then it falls back to the next declaration for this currency. Symbol -> Shortname -> Name

Btw: You can also override any of the default settings done by locale by using the options as described within the manual:


     'position'  => Position for the currency sign
     'script'    => Script for the output
     'format'    => Locale for numeric output
     'display'   => Currency detail to show
     'precision' => Precision for the currency
     'name'      => Name for this currency
     'currency'  => 3 lettered international abbreviation
     'symbol'    => Currency symbol
     'locale'    => Locale for this currency 
     'value'     => Money value
     'service'   => Exchange service to use

So you can set your own number format, currency sign or whatever.

As conclusion to your tests: 1.) No bug with locale 2.) SHORTNAME works as expected (otherwise you would have an empty string.. you would not even know that it's an currency when there is no fallback) 3.) This is no bug as locale setting must be given. Otherwise there is no way to determinate which details should be returned. Using "de" you would have "Amerikanische Dollar" and using "en" you would have "US Dollar". Simply using "USD" would negotate the relation which Zend_Currency needs to know where to search for currency details.

Hi Thomas,

Thanks for bearing with me! A couple of things from your last comment:

  1. "Regarding USE_SHORTNAME and USE_SYMBOL: When there is no shortname or symbol defined for a currency within the defined locale, then it falls back to the next declaration for this currency. Symbol -> Shortname -> Name"

Just to confirm, do you mean that some locales are not aware of the ISO 4217 code or the name of their own currency? The following code seems to bear this out:

$currency = new Zend_Currency(array('value' => 1234.56, 'display' => Zend_Currency::USE_SHORTNAME), 'en_GB'); return $currency; // Prints £1,234.56 not GBP 1,234.56

Having currencies which don't know their own ISO 4217 seems very weird to me in a currency library...

  1. Also your suggested code doesn't fulfil my original use case:

$currency = new Zend_Currency(array( 'value' => 1234.56, 'currency' => 'USD'), 'de_DE'); return $currency; // Returns 1.234,56 € not 1.234,56 $

If I want to show a currency in USD to a German in a German format, I need to do this instead:

$currency = new Zend_Currency(array( 'value' => 1234.56, 'format' => 'de'), 'en_US'); return $currency; // Returns 1.234,56 $

I think I'm repeating myself now, but it's totally counter-intuitive to me that if I want to show a US Dollar amount to a German, I have to change locale to 'en_US' and then override format with 'de' rather than just specifying 'USD':

"This is no bug as locale setting must be given. Otherwise there is no way to determinate which details should be returned. Using "de" you would have "Amerikanische Dollar" and using "en" you would have "US Dollar". Simply using "USD" would negotate the relation which Zend_Currency needs to know where to search for currency details."

I hope you don't take this the wrong way Thomas, but you seem to be confusing currencies with currency formatting. The Euro is a currency - it exists independently of any 'locale', and it has an ISO 4217 code of 'EUR'. It has its own symbol, and it has a dynamic exchange rate with other currencies, which also have ISO 4217 codes.

The point of a Currency object is to make it possible to create an amount of EUR 100, just as the point of a Date object is to make it possible to define a date of 24th Jan 2010. Currency formatting, like date formatting, is just a human-readable, locale-sensitive expression of a currency object. In other words, it should be the default to create a currency object like this:

$currency = new Zend_Currency(array('iso_4217' => 'EUR', 'value' => 1234.56));

Locale/localisation is only important when formatting a currency for display - and then only around a) the full name of the currency (e.g. "Amerikanische Dollar") and b) the currency layout (e.g. digit grouping, symbol position). Symbols and ISO codes are constant. It makes total sense to use the current locale to determine name and layout, and I do acknowledge that e.g. different de_ countries format currencies in different ways, but throwing an exception if the locale is region-less is wrong - it's just an artefact of this strange dependency on using the locale to determine the currency.

I appreciate that the current architecture of ZF may make some of my points above difficult to implement, but I wanted to give you my view on what a currency library should be, rather than what a currency library can be currently, given its technical legacy. :-)

Sorry to comment again before your response Thomas, but a colleague pointed out to me that Cuba actually has two official currencies (ISO 4217s CUC and CUP), so the idea of a one-to-one mapping from locale to currency breaks down still further.

And of course you've got historic currencies (DEM, CYP etc)... As far as I can see there is no support for these either in the Zend_Currency object.

@1) You are using "en_GB" as locale. When no currency is given Zend_Currency tries to detect the currency by using the region. In your example the region GB is connected with "english pound". The locale is in this case used as fallback for a not given currency. Otherwise Zend_Currency would have to return an exeption.

Why do you think that Zend_Currency does not know its internals? You did not call it's info methods but only output a representation for the object which contains not all informations.

Please note that all details for a currency come from unicode's CLDR library. When CLDR does not define a symbol or a longname for a currency then non of these names will be available for representation.

@2) Your thoughts contain multiple problems.

You want to have USD => "US Dollar" and the proper number notation. But these notations are localized. Within an other locale it could be "Amerikanische Dollar" and also a different notation. Therefor a currency can not exist without a proper locale in case of Zend_Currency.

Also when only "EUR" would be accepted Zend_Currency would not know how to format this currency. French and english notations are completly different. This would not work.

A currency itself is already a localized representation... it is not possible to have a "normalized" currency value because this would be a plain float value which is completly useless for a currency representation.

Actually ALL currencies are supported... even historic as also CU or GW or IO. There are several countries which use multiple currencies.

Thanks Thomas, a few questions about your last comment:

  1. "When no currency is given Zend_Currency tries to detect the currency by using the region."

You say that region is a fallback for Zend_Currency when currency is not given - but I can't find a way of giving currency to the constructor for Zend_Currency. Yes there is a "currency" option, but the documentation (http://framework.zend.com/manual/en/…) clearly says that "currency: Defines the abbreviation which can be displayed" - i.e. it's a formatting option, it doesn't determine the currency of the object.

If you can provide a code excerpt which demonstrates creating a Zend_Currency using an ISO 4217 code, I would love to see it.

  1. "Please note that all details for a currency come from unicode's CLDR library."

Can you show me a working example of Zend_Currency::USE_SHORTNAME? I can't get it working for any locales I've tried.

  1. "a currency can not exist without a proper locale in case of Zend_Currency"

Sorry, but that's a design limitation of Zend_Currency rather than best practice for a money/currency object. Java and Python, two languages which see a lot of financial/monetary usage, totally disagree with this approach. See the following examples:

java.util.Currency: public static Currency getInstance(String currencyCode)

And python-money: USD100 = Money(100, "USD") EUR100 = Money(100, "EUR") UAH100 = Money(100, "UAH")

  1. "A currency itself is already a localized representation... it is not possible to have a "normalized" currency value because this would be a plain float value which is completly useless for a currency representation."

Sorry no, a currency is not a localized representation, that's why we have ISO standards. It's probably worth looking at the Wikipedia page for ISO 4217: "ISO 4217 is the international standard describing three-letter codes (also known as the currency code) to define the names of currencies established by the International Organization for Standardization (ISO)".

There is nothing localized about an ISO 4217 currency - it is internationalized. That's what the 'I' in ISO stands for. And there is nothing localized about [float=400.00 iso4217=EUR], which is a perfectly useful description of 400 units of the Euro currency. Plenty of software systems could happily work with a float+iso4217 definition of a currency without worrying about locale - to repeat my previous point, locale is only an issue when you are rendering currencies for a user. It's irrelevant for when you are defining currency objects, doing exchange rate conversions, adding currency amounts together etc.

  1. A final point on this - I've never encountered a system where you have a money amount of "10" and you show it as "10 EUR" to somebody in de_AT and show the same amount as "$10" to somebody in en_US. Pricing just doesn't happen that way. You store a price as "10 GBP for UK", and "12 EUR for Europe" (or whatever), you never just "localise" a fixed number to get the right money amount for the user's current locale.

Phew! I think that's everything. Look forward to your thoughts and example code.

I'm trying to use Zend_Currency for the first time. After reading the documentation several times, and playing a little with some code, I still found things puzzling, much along the lines that Alex described. I, too, came to the subject with the expectation that the primary attributes for the constructor would be the standard name for the currency and the amount, that the purpose of locale information would be primarily for formatting the value, and that having the currency default from the region of the locale in effect was just a convenience. Reading this issue and its replies has clarified things tremendously!

The biggest eye-opener for me was Thomas' point that a currency without a locale isn't really useful. While Alex argues to the contrary by naming ways that it could be used, I think Thomas' point wasn't so much that it has no use, but that it isn't complete, because it might not be able to be displayed. While the ISO 4217 codes provide a standard way to identify currencies, they don't provide a standard way of rendering to the user the values of those currencies. So I take Thomas' point to mean that renderings are always localized - and if you don't have a locale that knows how to render a currency, then you can't really say that you support that currency.

I think the points made about currencies not always having short names, and not properly recognizing the importance of ISO 4217 are a little misleading. At least using ZF 1.11.1, I have verified that every currency has a 3-letter short name, and I'm pretty sure that every short name is an ISO 4217 3-letter code (I only did the latter by eyeball, didn't write code to do it). So in order to create a currency object for an ISO 4217 code, you need a table of which locales have regions that use that currency, and you then can pass one of those locales to the constructor. You might choose the locale based on matching the language part with the browser's language, or if there's no match possibly by asking the user; or as a last resort choose one arbitrarily and use the SHORT_NAME display. It's easy enough to construct the table from the available ZF functions (though time-consuming, you'd want to do it once and serialize it to a file to read in for use). While that might seem a little inconvenient, it guarantees that when it comes time to display the value, it will be done using a locale that knows how to display it in a way that people who use the currency expect to see it. Of course the counter to that is that people who deal with multiple foreign currencies probably would much prefer to see the values displayed using their native locale's conventions for displaying numeric values, along with the 3-letter code placed in the usual place where they'd see it for their own currency, even though that representation might appear bizarre to native users of the currency. You can get that behavior, but you have to adjust each individual option manually. Probably most users would expect that setting the 'currency' option, which is the 3-letter ISO 4217 code, to a currency code that isn't one of the ones supported by the specified locale, would at least prevent the value from being displayed with the wrong currency symbol or long name; perhaps it could null out those values.

Up to this point, the issues are just a matter of preference and convenience. But one real puzzle that remains for me is the intended treatment of regions that have more than one currency. The getCurrencyList($region) function returns an array of currencies used in the specified region. As it happens in ZF 1.11.1, for every region of every locale that has a region, this function returns an array of one element, so it's hard to call this a bug. But if there were a region that returned more than one currency, I see no way to tell the constructor which currency to choose. Presumably, the 'currency' option would be used to specify the 3-letter code, but it appears that the implementation simply stores that value for display, and does not use it to make any other adjustments to the internals, such as resetting the symbol and long name...

There is no way, to initialize Zend_Currency objects from database values

[100, 'USD'] [200, 'EUR'] [300, 'RUB']

right?

All coding questions not related to a specific issue should be asked within the mailing list. Questions like "is there a way to..." or "this is my first time but..." are no bugs and will therefor not be fixed within ZF's svn.

Please ask in the mailing list when you need help or when you have coding problems.

Dmitry, amazingly, you are correct - in fact your comment is an excellent synopsis of this thread.

Due to an epic architectural flaw in Zend, there is no way to create a Zend_Currency without specifying a locale first. Workarounds are: 1. Create a mapping table to map an appropriate locale onto each currency you want to work with 2. Switch to a sane language and currency library, such as: Ruby + RubyMoney/Currency Python + python-money Java + java.util.Currency

All of these allow you to create a new currency object using data such as [100, 'USD'].

Hope this helps!

Alex

I have to say I'm rather dismayed that such a common PHP library (the Zend_Currency component) is so utterly unfit for purpose because of a fundamental misunderstanding about the nature of currencies. Currency amounts are independent of locale - how would the global financial system work if dollars suddenly became euros just because you were looking at the same thing on a different computer? As Alex points out, formatting is locale-specific, but the money amount itself is not. I only hope ZF2 has rewritten this and gone a different way, such as using the intl component of PHP 5.3 for currency rendering.

So there is no simple way for the following code:


$options = // Some options to tell we want to deal with euros
$currency = new Zend_Currency($options);
$currency->setValue(1500.42);

To display: {{1,500.42 €}} with browser (or forced) {{en}} locale {{1 500,42 €}} with browser (or forced) {{fr}} locale {{1500,66 €}} with browser (or forced) {{ar}} locale ?

I agree with the fact that a Zend currency object should be composed of a number (float), an associated currency (British pounds, U.S. dollars, European euros, French francs, etc.) and it's position regarding the number (before ? after ? separator character between number and currency). Localization of number when displaying should be done via {{Zend_Locale_Format}}.

Forgot to use full length locales in my examples: {{1,500.42 €}} with browser (or forced) {{en_US}} locale {{1 500,42 €}} with browser (or forced) {{fr_FR}} locale {{1500,42 €}} with browser (or forced) {{ar_SA}} locale

I might also add: {{1'500.42 €}} with browser (or forced) {{fr_CH}} locale