From da460d9d00c283bf2e34680603521bb7e5e715ff Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Thu, 21 May 2020 00:22:46 -0700 Subject: [PATCH] CRM_Core_I18n::setLocale() - Fix bug with repeated usage Overview -------- Suppose you use `setLocale()` to change back and forth between languages, e.g. ```php /*1*/ $i18n->setLocale('en_US'); echo ts('Yes'); /*2*/ $i18n->setLocale('fr_FR'); echo ts('Yes'); /*3*/ $i18n->setLocale('de_DE'); echo ts('Yes'); /*4*/ $i18n->setLocale('en_US'); echo ts('Yes'); /*5*/ $i18n->setLocale('fr_FR'); echo ts('Yes'); /*6*/ $i18n->setLocale('de_DE'); echo ts('Yes'); ``` This fixes a bug with that use-case. For more detailed example/reproduction, see https://gist.github.com/totten/de0dc9add8dee3956ecad29ec0906cc3 - specifically variant 2. Before ------ There is a leak which causes mistranslations. After ----- You can freely switch between instances of `CRM_Core_I18n`. Technical Details ----------------- (1) I'm using a default configuration, wherein `CIVICRM_GETTEXT_NATIVE` is not set... so it uses phpgettext. (2) As you use multiple locales, `CRM_Core_I18n::singleton()` constructs multiple instances of `CRM_Core_I18n` (one for each locale; this despite the misnomer of `singleton`). `singleton()` always returns the instance indicated by the active locale (as directed by `global $tsLocale`). The problem: if you call `$enUS->setPhpGettextLocale('fr_FR')`, then future requests for American English will actually return French. Similarly, if you call `$frFR->setPhpGettextLocale('de_DE')`, then future requests for French will actually return German. --- CRM/Core/I18n.php | 7 ++++--- tests/phpunit/CRM/Core/I18n/LocaleTest.php | 13 +++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CRM/Core/I18n.php b/CRM/Core/I18n.php index 357dc2f936..a033519dbe 100644 --- a/CRM/Core/I18n.php +++ b/CRM/Core/I18n.php @@ -623,13 +623,14 @@ class CRM_Core_I18n { // Change the language of the CMS as well, for URLs. CRM_Utils_System::setUFLocale($locale); - // change the gettext ressources if ($this->_nativegettext) { + // Native gettext has its own global flag indicating the active locale. $this->setNativeGettextLocale($locale); } else { - // phpgettext - $this->setPhpGettextLocale($locale); + // With phpgettext, each locale is retained as a separate instance of CRM_Core_I18n. + // It is sufficient to update `$tsLocale` and switch to the designated instance. + // Changing the locale for a live instance will actually cause problems when alternating locales. } // For sql queries, if running in DB multi-lingual mode. diff --git a/tests/phpunit/CRM/Core/I18n/LocaleTest.php b/tests/phpunit/CRM/Core/I18n/LocaleTest.php index ab66bd4dff..95f8d5e0c8 100644 --- a/tests/phpunit/CRM/Core/I18n/LocaleTest.php +++ b/tests/phpunit/CRM/Core/I18n/LocaleTest.php @@ -45,6 +45,11 @@ class CRM_Core_I18n_LocaleTest extends CiviUnitTestCase { 'fr_CA' => 'French (Canada)', 'de_DE' => 'German', ]; + $examples = [ + 'en_US' => 'Yes', + 'fr_CA' => 'Oui', + 'de_DE' => 'Ja', + ]; $codes = array_keys($languages); Civi::settings()->set('uiLanguages', $codes); @@ -77,6 +82,14 @@ class CRM_Core_I18n_LocaleTest extends CiviUnitTestCase { 'fr_CA' => 'French (Canada)', ], $result); + // If you switch back and forth among these languages, `ts()` should follow suit. + for ($trial = 0; $trial < 3; $trial++) { + foreach ($examples as $exLocale => $exString) { + CRM_Core_I18n::singleton()->setLocale($exLocale); + $this->assertEquals($exString, ts('Yes'), "Translate"); + } + } + CRM_Core_I18n::singleton()->setLocale('en_US'); CRM_Core_I18n_Schema::makeSinglelingual('en_US'); Civi::$statics['CRM_Core_I18n']['singleton'] = []; -- 2.25.1