Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Translator] Why does the currency depend on the locale? #32652

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
sylfabre opened this issue Jul 22, 2019 · 20 comments
Closed

[Translator] Why does the currency depend on the locale? #32652

sylfabre opened this issue Jul 22, 2019 · 20 comments

Comments

@sylfabre
Copy link
Contributor

sylfabre commented Jul 22, 2019

Symfony version(s) affected: 4.3

Description
According to https://symfony.com/doc/current/translation/message_format.html#numbers the translator uses the locale to select the currency:

# translations/messages+intl-icu.en.yaml
value_of_object: 'This artifact is worth {value, number, currency}'
// prints "This artifact is worth $9,988,776.65"
// if we would translate this to i.e. French, the value would be shown as
// "9 988 776,65 €"
echo $translator->trans('value_of_object', ['value' => 9988776.65]);

IMO, the currency does not depend on the locale: I may want to display euros in English and US dollars in French.

What is the expected way to display this sentence using the translator?

This artifact is worth €9,988,776.65

A possible workaround would be to define custom formats like with this JS lib:
https://github.com/formatjs/formatjs/tree/master/packages/intl-messageformat#user-defined-formats

@ro0NL
Copy link
Contributor

ro0NL commented Jul 22, 2019

The message depends on the locale, in this case the currency format is inherited as such.

AFAIK we don't support custom formats (yet).

For now you could format the currency first using NumberFormatter::formatCurrency(), with a different locale.

@stof
Copy link
Member

stof commented Jul 22, 2019

This behavior comes from the ICU behavior. Symfony is not the one implementing that formatting.

Pre-formatting the value (and then treating it as a text placeholder in the message) is indeed an option.

@sylfabre
Copy link
Contributor Author

@ro0NL @stof thank you for the explanation. Would you accept a PR on Symfony docs for https://github.com/formatjs/formatjs/tree/master/packages/intl-messageformat#user-defined-formats to explain this and how to handle my use case?

@ro0NL
Copy link
Contributor

ro0NL commented Jul 22, 2019

i think ulitmately we're blocked by PHP which is parsing the messages (https://php.net/messageformatter), and doenst accept custom formats. Not sure it's easy to implement such a feature on top.

@carsonbot
Copy link

Thank you for this issue.
There has not been a lot of activity here for a while. Has this been resolved?

@carsonbot
Copy link

Could I get an answer? If I do not hear anything I will assume this issue is resolved or abandoned. Please get back to me <3

@sylfabre sylfabre closed this as completed Jan 1, 2021
@sylfabre
Copy link
Contributor Author

sylfabre commented May 7, 2021

@ro0NL @stof I'm working again with intl and came across this issue again.

I'm thinking about decorating/modifying IntlFormatter to run a first parsing of the string to properly "translate" all the placeholders with date, time, or currency with the right timezone and currency using https://www.php.net/manual/en/class.intldateformatter.php and https://www.php.net/manual/en/class.numberformatter.php

The tight timezone and currency would come from new methods Translator::setTimezone() and Translator::setCurrency().

Placeholders seem to be easily identified with regex.

I'm also thinking about supporting https://moneyphp.org/en/stable/ as values for placeholders.

  1. Do you think such an improvement can make its way into Symfony?
  2. Do you know if a bundle or something already exist for this?

Thank you for your help, have a nice weekend

@sylfabre sylfabre reopened this May 7, 2021
@carsonbot carsonbot removed the Stalled label May 7, 2021
@ro0NL
Copy link
Contributor

ro0NL commented May 7, 2021

actually the spec recommends to format params separately: https://unicode-org.github.io/icu/userguide/format_parse/messages/#format-the-parameters-separately-recommended

im not sure we shoud start exending it :) From a i18n POV having different category locales globally feels odd, if not invalid sort of.

Note there should be many general purpose formats available upstream: https://unicode-org.github.io/icu/userguide/format_parse/numbers/skeletons.html#examples

@sylfabre
Copy link
Contributor Author

sylfabre commented May 8, 2021

@ro0NL This point is indeed to format params separately.

I've made a (very) quick POC: #41136 as some lines of code may be clearer that a lot of words!

@ro0NL
Copy link
Contributor

ro0NL commented May 8, 2021

I think why ICU reccomends separate formatting at the user level, is so we dont have to couple an implementation to a global currency locale nor specific parameter types. You can provide these values from the outside to any MessageFormat implementation (let this one be in PHP core).

Im generally curious about the use case for having a different currency locale globally, i tend to believe it breaks i18n given this context derives from a single current interface locale.

If anything, what we're trying to solve here should be solved upstream IMHO.

@sylfabre
Copy link
Contributor Author

sylfabre commented May 8, 2021

@ro0NL First of all, thank you for your time on this matter 👍

I believe the Translation component within the Symfony framework does an amazing job at:

This PHP class is a basic way to translate messages and use default settings for currency, timezone, ... which works fine for a lot of simple use-cases. If one has advanced needs, then the spec recommends to:

  1. Use classes like https://www.php.net/manual/en/class.numberformatter.php or https://www.php.net/manual/en/class.intldateformatter.php for complex parameters
  2. Then use the MessageFormatter to translate the message

As the Translation component is basically an easy-to-use wrapper around https://www.php.net/manual/en/class.messageformatter.php, then this component only works for the same simple use-cases. This is also amazing as it makes it very easy to translate messages.

Skeleton like {xx, number, :: currency/USD} makes it possible to configure a message in a locale-agnostic way. But it cannot be used to configure a message based on user data in parameters (eg. display a message for a bank account's balance where the bank account's currency varies from one bank account to another).

I think we both agree on this ☝️

These simple use-cases work with simple scalars as parameters. These scalars don't store any localization data. So PHP uses default settings:

  • numbers are formatted using locale rules (like space in French or comma in English for thousands separator)
  • currency come from the locale's country as I believe all countries only use one currency
  • global server timezone to format \DateTime instances as some countries span over multiple timezones (like the US)
  • ...

However, advanced objects exist that store localized data like:

My goal is to make the Translation component even more powerful with support for these objects as parameters and leveraging the localization data they store. (DateTime instances are actually supported by the component but the stored timezone is not used.).

# Translation file app+intl-icu.en_US.yml
payment: "{date, date, short} at {date, time, short} - {value, number, currency}"
payment2: "{date} at {time} - {value}"

# Translation file app+intl-icu.fr_FR.yml
payment: "{date, date, short} at {date, time, short} - {value, number, currency}"
payment2: "{date} at {time} - {value}"

Usage as per today
Please find below the code that must be used today:

// In order not to give the locale at every call of the translator
$this->translator->setLocale($userLocale); 

// BASIC USAGE

// en_US: 1/25/19 at 7:30 PM - $100.00
// fr_FR: 25/01/2019 à 19:30 - 100,00 €
$this->translator->trans('payment', [
    'date' => new \DateTime('2019-01-25 11:30:00', 'America/Los_Angeles'),
    'value' => 100
]);
// en_US: 1/25/19 at 10:30 AM - $100.00
// fr_FR: 25/01/2019 à 10:30 - 100,00 €
$this->translator->trans('payment', [
    'date' => new \DateTime('2019-01-25 11:30:00', 'Europe/Paris'),
    'value' => 100
]);

// ADVANCED USAGE

// en_US: 1/25/19 at 11:30 AM - $100.00
// fr_FR: 25/01/2019 à 11:30 - 100,00 $
$userCurrency = 'USD';
$datetime = new \DateTime('2019-01-25 11:30:00', 'Europe/Paris');
$dateFormatter = new IntlDateFormatter($this->translator->getLocale(), IntlDateFormatter::SHORT, IntlDateFormatter::NONE, $datetime->getTimezone());
$timeFormatter = new IntlDateFormatter($this->translator->getLocale(), IntlDateFormatter::NONE, IntlDateFormatter::SHORT, $datetime->getTimezone());
$numberFormatter = new NumberFormatter($this->translator->getLocale(), NumberFormatter::CURRENCY);
$this->translator->trans('payment2', [
    'date' => $dateFormatter->format($datetime),
    'time' => $timeFormatter->format($datetime),
    'value' => $numberFormatter->formatCurrency(100, $userCurrency)
]);

What I find wrong with ☝️

  • Way too verbose to use on a regular basis
  • Way too complicated to use for developers not experts with localization
  • I could use a service or something with DI to handle it but it requires work on my own
  • Some part of the localization is done in my code while another part happens within the Symfony component

=> I believe the Translation component should handle these advanced use-cases to make dev life even easier!

MUST HAVE

  1. Support Money objects as parameters for {xx, number, currency} and use the currency from the object instead of the locale's currency
  2. Make it possible to set globally the timezone for the Translation component without modifying server's ini settings
  3. Use the timezone from DateTime objects if different from UTC

NICE TO HAVE
4. Make it possible to set globally the currency for the Translation component

Why these 4 changes?

  1. Because Money is wildly used (13M downloads)
  2. Using UTC for the server and the database, while using user's timezone for display is a best practice. So Doctrine will return DateTime using UTC for entities' datetime properties. Change 2 make it possible to use the user's timezone everywhere very easily
  3. As the server works with UTC (cf best practice of the previous point), then change 3 is a convenient way to specify the timezone to use for a given message
  4. Use-case: European English-speaking finance software => no locale gives English and EUR as currency. So change 4 is like change 2 to make it possible to use the user's currency everywhere very easily.

Proposed usages

$this->translator->setLocale($userLocale); 

// en_US: 1/25/19 at 11:30 AM - $100.00
// fr_FR: 25/01/2019 à 11:30 - 100,00 €
$this->translator->trans('payment', [
    'date' => new \DateTime('2019-01-25 11:30:00'),
    'value' => 100
]);

// en_US: 1/25/19 at 11:30 AM - $100.00
// fr_FR: 25/01/2019 à 11:30 - 100,00 €
$this->translator->trans('payment', [
    'date' => new \DateTime('2019-01-25 11:30:00', 'Europe/Paris'),
    'value' => 100
]);

// en_US: 1/25/19 at 11:30 AM - €100.00
// fr_FR: 25/01/2019 à 11:30 - 100,00 €
$this->translator->trans('payment', [
    'date' => new \DateTime('2019-01-25 11:30:00', 'Europe/Paris'),
    'value' => Money::EUR(100)
]);

$this->translator->setTimezone('America/Los_Angeles');
$this->translator->setCurrency('USD');

// en_US: 1/25/19 at 3:30 AM - $100.00
// fr_FR: 25/01/2019 à 03:30 - 100,00 $
$this->translator->trans('payment', [
    'date' => new \DateTime('2019-01-25 11:30:00'),
    'value' => 100
]);

// en_US: 1/25/19 at 11:30 AM - €100.00
// fr_FR: 25/01/2019 à 11:30 - 100,00 €
$this->translator->trans('payment', [
    'date' => new \DateTime('2019-01-25 11:30:00', 'Europe/Paris'),
    'value' => Money::EUR(100)
]);

@ro0NL
Copy link
Contributor

ro0NL commented May 9, 2021

we are still coupling an implementation to specific parameter types. It breaks compatibility.

If we reuse the same message eg. client-side we may need a translator supporting the same feature set, which is far from ideal IMHO.

Other translators cannot opt-in, and our translator also cannot opt-out from handling such parameter types. To me it sounds like a dead-end, if not a rabbit hole.

I think we should focus on providing the parameter values from outside more easy, eg. twig-like

{{ 'payment2'|trans({value: 100|format_currency('EUR')})

In PHP perhaps: ['value' => TranslatableMessage::formatCurrency(100, 'EUR'[, $translator|'xx_XX'|null])]

NICE TO HAVE
4. Make it possible to set globally the currency for the Translation component

i really dont think this makes sense from i18n POV. We cannot know all messages need this context.

@ro0NL
Copy link
Contributor

ro0NL commented May 9, 2021

i tend to agree PHP translators can support eg. ['value' => new TranslatableCurrency(100, 'EUR'[, 'xx_XX'])], meaning we can translate it more lazy based on the translator's locale by default.

TranslatableCurrency::fromMoney() Because Money is wildly used (13M downloads) ;)

@sylfabre
Copy link
Contributor Author

@ro0NL Thank you for all your inputs.

I'll work on it with my team and come back with a PR or something!

@sylfabre
Copy link
Contributor Author

Hello @ro0NL
I've updated the PR #41136
Let me know if I going in the right direction, I'll then work on the tests 🙂

I have several questions about it:

  • I'm not sure if TranslatableMessage should be moved to a dedicated Translatable folder with its 2 new friends
  • I'm not sure if I should update the master composer.json, the component composer.json, or both to have moneyphp/money for require-dev

Have a nice evening

@carsonbot
Copy link

Thank you for this issue.
There has not been a lot of activity here for a while. Has this been resolved?

@sylfabre
Copy link
Contributor Author

#41136 should fix it

@carsonbot carsonbot removed the Stalled label Nov 22, 2021
@carsonbot
Copy link

Thank you for this issue.
There has not been a lot of activity here for a while. Has this been resolved?

@carsonbot
Copy link

Could I get a reply or should I close this?

@carsonbot
Copy link

Hey,

I didn't hear anything so I'm going to close it. Feel free to comment if this is still relevant, I can always reopen!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants