From 89c5e94feed60b068b2edafe1e19fa7b95056278 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Fri, 12 Jul 2024 11:24:06 +0800 Subject: [PATCH] MDL-82445 core: Create locale helper utility --- admin/tool/langimport/classes/locale.php | 25 ++-- admin/tool/langimport/tests/locale_test.php | 149 +++++--------------- lib/classes/locale.php | 80 +++++++++++ lib/tests/locale_test.php | 126 +++++++++++++++++ 4 files changed, 254 insertions(+), 126 deletions(-) create mode 100644 lib/classes/locale.php create mode 100644 lib/tests/locale_test.php diff --git a/admin/tool/langimport/classes/locale.php b/admin/tool/langimport/classes/locale.php index 202bc5673f2..6709b42959b 100644 --- a/admin/tool/langimport/classes/locale.php +++ b/admin/tool/langimport/classes/locale.php @@ -57,7 +57,7 @@ class locale { } // Store current locale. - $currentlocale = $this->set_locale(LC_ALL, 0); + $currentlocale = $this->get_locale(); $locale = get_string_manager()->get_string($stringtofetch, 'langconfig', $a = null, $langpackcode); @@ -79,19 +79,16 @@ class locale { * @return string|false Returns the new current locale, or FALSE on error. */ protected function set_locale(int $category = LC_ALL, string $locale = '0') { - if (strlen($locale) <= 255 || PHP_OS_FAMILY === 'BSD' || PHP_OS_FAMILY === 'Darwin') { - // We can set the whole locale all together. - return setlocale($category, $locale); - } + return \core\locale::set_locale($category, $locale); + } - // Too long locale with linux or windows, let's split it into known and supported categories. - $split = explode(';', $locale); - foreach ($split as $element) { - [$category, $value] = explode('=', $element); - if (defined($category)) { // Only if the category exists, there are OS differences. - setlocale(constant($category), $value); - } - } - return setlocale(LC_ALL, 0); // Finally, return the complete configured locale. + /** + * Get the current locale. + * + * @param int $category + * @return string|false + */ + protected function get_locale(int $category = LC_ALL): string|false { + return \core\locale::get_locale($category); } } diff --git a/admin/tool/langimport/tests/locale_test.php b/admin/tool/langimport/tests/locale_test.php index 247e6fc4013..aab50f7edc3 100644 --- a/admin/tool/langimport/tests/locale_test.php +++ b/admin/tool/langimport/tests/locale_test.php @@ -21,39 +21,59 @@ namespace tool_langimport; * * @package tool_langimport * @category test - * @coversDefaultClass \tool_langimport\locale + * @covers \tool_langimport\locale * @copyright 2018 Université Rennes 2 {@link https://www.univ-rennes2.fr} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class locale_test extends \advanced_testcase { +final class locale_test extends \advanced_testcase { + /** @var string Locale */ + protected string $locale; + + #[\Override] + public function setUp(): void { + parent::setUp(); + $this->locale = \core\locale::get_locale(); + } + + #[\Override] + public function tearDown(): void { + parent::tearDown(); + \core\locale::set_locale(LC_ALL, $this->locale); + } + /** * Test that \tool_langimport\locale::check_locale_availability() works as expected. - * - * @covers ::check_locale_availability - * @return void */ public function test_check_locale_availability(): void { - // Create a mock of set_locale() method to simulate : - // - first setlocale() call which backup current locale - // - second setlocale() call which try to set new 'es' locale - // - third setlocale() call which restore locale. + // Create a mock of set_locale() method to simulate: + // - get_locale() call which backup current locale + // - first set_locale() call which try to set new 'es' locale + // - second set_locale() call which restore locale. $mock = $this->getMockBuilder(locale::class) - ->onlyMethods(['set_locale']) + ->onlyMethods([ + 'get_locale', + 'set_locale', + ]) ->getMock(); - $mock->method('set_locale')->will($this->onConsecutiveCalls('en', 'es', 'en')); + $mock->method('get_locale')->will($this->onConsecutiveCalls('en')); + $mock->method('set_locale')->will($this->onConsecutiveCalls('es', 'en')); // Test what happen when locale is available on system. $result = $mock->check_locale_availability('en'); $this->assertTrue($result); - // Create a mock of set_locale() method to simulate : - // - first setlocale() call which backup current locale - // - second setlocale() call which fail to set new locale - // - third setlocale() call which restore locale. + // Create a mock of set_locale() method to simulate: + // - get_locale() call which backup current locale + // - first set_locale() call which fail to set new locale + // - second set_locale() call which restore locale. $mock = $this->getMockBuilder(locale::class) - ->onlyMethods(['set_locale']) + ->onlyMethods([ + 'get_locale', + 'set_locale', + ]) ->getMock(); - $mock->method('set_locale')->will($this->onConsecutiveCalls('en', false, 'en')); + $mock->method('get_locale')->will($this->onConsecutiveCalls('en')); + $mock->method('set_locale')->will($this->onConsecutiveCalls(false, 'en')); // Test what happen when locale is not available on system. $result = $mock->check_locale_availability('en'); @@ -64,99 +84,4 @@ class locale_test extends \advanced_testcase { $this->expectException(\coding_exception::class); $locale->check_locale_availability(''); } - - /** - * Test \tool_langimport\locale::set_locale() own logic. - * - * We have to explicitly test set_locale() own logic and results, - * that effectively sets the current locale, so we need to restore - * the original locale after every test (ugly, from a purist unit test - * point of view, but needed). - * - * @dataProvider set_locale_provider - * @covers ::set_locale - * - * @param string $set locale string to be set. - * @param string $ret expected results returned after setting the locale. - */ - public function test_set_locale(string $set, string $ret): void { - // Make set_locale() public. - $loc = new locale(); - $rc = new \ReflectionClass(locale::class); - $rm = $rc->getMethod('set_locale'); - - // Capture current locale for later restore (funnily, using the set_locale() method itself. - $originallocale = $rm->invokeArgs($loc, [LC_ALL, 0]); - - // Assert we get the locale defined as expected. - $this->assertEquals($ret, $rm->invokeArgs($loc, [LC_ALL, $set])); - - // We have finished, restore the original locale, so this doesn't affect other tests at distance. - // (again, funnily, using the very same set_locale() method). - $rm->invokeArgs($loc, [LC_ALL, $originallocale]); - - } - - /** - * Data provider for test_set_locale(). - * - * Provides a locale to be set (as 'set') and a expected return value (as 'ret'). Note that - * some of the locales are OS dependent, so only the ones matching the OS will be provided. - * - * We make extensive use of the en_AU.UTF-8/English_Australia.1252 locale that is mandatory to - * be installed in any system running PHPUnit tests. - */ - public function set_locale_provider(): array { - // Let's list the allowed categories by OS. - $bsdallowed = ['LC_COLLATE', 'LC_CTYPE', 'LC_MESSAGES', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME']; - $winallowed = ['LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME']; - $linuxallowed = [ - 'LC_COLLATE', 'LC_CTYPE', 'LC_MESSAGES', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME', - 'LC_PAPER', 'LC_NAME', 'LC_ADDRESS', 'LC_TELEPHONE', 'LC_MEASUREMENT', 'LC_IDENTIFICATION' - ]; - - // The base locale name is also OS dependent. - $baselocale = get_string('locale', 'langconfig'); - if (PHP_OS_FAMILY === 'Windows') { - $baselocale = get_string('localewin', 'langconfig'); - } - - // Here we'll go accumulating cases to be provided. - $cases = []; - - // First, the simplest case, just pass a locale name, without categories. - $cases['rawlocale'] = [ - 'set' => $baselocale, - 'ret' => $baselocale, - ]; - - // Now, let's fill ALL LC categories, we should get back the locale name if all them are set with same value. - // Note that this case is the one that, under Linux only, covers the changes performed to the set_locale() method. - // Pick the correct categories depending on the OS. - $oscategories = $bsdallowed; // Default to BSD/Dawrwin ones because they are the standard 6 supported by PHP. - if (PHP_OS_FAMILY === 'Windows') { - $oscategories = $winallowed; - } else if (PHP_OS_FAMILY === 'Linux') { - $oscategories = $linuxallowed; - } - - $localestr = ''; - foreach ($oscategories as $category) { - // Format is different by OS too, so let build the string conditionally. - if (PHP_OS_FAMILY === 'BSD' || PHP_OS_FAMILY === 'Darwin') { - // BSD uses slashes (/) separated list of the 6 values in exact order. - $localestr .= '/' . $baselocale; - } else { - // Linux/Windows use semicolon (;) separated list of category=value pairs. - $localestr .= ';' . $category . '=' . $baselocale; - } - } - $cases['allcategories'] = [ - 'set' => trim($localestr, ';/'), - 'ret' => $baselocale, - ]; - - // Return all the built cases. - return $cases; - } } diff --git a/lib/classes/locale.php b/lib/classes/locale.php new file mode 100644 index 00000000000..d925f05797c --- /dev/null +++ b/lib/classes/locale.php @@ -0,0 +1,80 @@ +. + +namespace core; + +/** + * Helper utility to interact with Locales. + * + * @package core + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class locale { + /** + * Wrap for the native PHP function setlocale(). + * + * @param int $category Specifying the category of the functions affected by the locale setting. + * @param string $locale E.g.: en_AU.utf8, en_GB.utf8, es_ES.utf8, fr_FR.utf8, de_DE.utf8. + * @return string|false Returns the new current locale, or FALSE on error. + */ + public static function set_locale(int $category = LC_ALL, string $locale = '0'): string|false { + if (strlen($locale) <= 255 || PHP_OS_FAMILY === 'BSD' || PHP_OS_FAMILY === 'Darwin') { + // We can set the whole locale all together. + return setlocale($category, $locale); + } + + // Too long locale with linux or windows, let's split it into known and supported categories. + $split = explode(';', self::standardise_locale($locale)); + foreach ($split as $element) { + [$category, $value] = explode('=', $element); + if (defined($category)) { // Only if the category exists, there are OS differences. + setlocale(constant($category), $value); + } + } + + // Finally, return the complete configured locale. + return self::get_locale(); + } + + /** + * Get the current locale. + * + * @param int $category + * @return string|false + */ + public static function get_locale(int $category = LC_ALL): string|false { + return setlocale($category, "0"); + } + + /** + * Standardise a string-based locale, removing any deprecated locale categories and ordering it. + * + * @param string $locale + * @return string + */ + public static function standardise_locale(string $locale): string { + $locales = array_filter( + explode(';', $locale), + function ($locale): bool { + [$category, ] = explode('=', $locale); + return defined($category); + }, + ); + sort($locales); + return implode(';', $locales); + } +} diff --git a/lib/tests/locale_test.php b/lib/tests/locale_test.php new file mode 100644 index 00000000000..b55ae19ac7b --- /dev/null +++ b/lib/tests/locale_test.php @@ -0,0 +1,126 @@ +. + +namespace core; + +/** + * Tests for \core\locale class. + * + * @package core + * @category test + * @copyright 2018 Université Rennes 2 {@link https://www.univ-rennes2.fr} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \core\locale + */ +final class locale_test extends \advanced_testcase { + /** @var string Locale */ + protected string $locale; + + #[\Override] + public function setUp(): void { + parent::setUp(); + $this->locale = \core\locale::get_locale(); + } + + #[\Override] + public function tearDown(): void { + parent::tearDown(); + \core\locale::set_locale(LC_ALL, $this->locale); + } + + /** + * Test \tool_langimport\locale::set_locale() own logic. + * + * We have to explicitly test set_locale() own logic and results, + * that effectively sets the current locale, so we need to restore + * the original locale after every test (ugly, from a purist unit test + * point of view, but needed). + * + * @dataProvider set_locale_provider + * @param string $set locale string to be set. + * @param string $ret expected results returned after setting the locale. + */ + public function test_set_locale(string $set, string $ret): void { + // Capture current locale for later restore (funnily, using the set_locale() method itself. + $originallocale = locale::set_locale(LC_ALL, 0); + + // Assert we get the locale defined as expected. + $this->assertEquals($ret, locale::set_locale(LC_ALL, $set)); + } + + /** + * Data provider for test_set_locale(). + * + * Provides a locale to be set (as 'set') and a expected return value (as 'ret'). Note that + * some of the locales are OS dependent, so only the ones matching the OS will be provided. + * + * We make extensive use of the en_AU.UTF-8/English_Australia.1252 locale that is mandatory to + * be installed in any system running PHPUnit tests. + */ + public static function set_locale_provider(): array { + // Let's list the allowed categories by OS. + $bsdallowed = ['LC_COLLATE', 'LC_CTYPE', 'LC_MESSAGES', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME']; + $winallowed = ['LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME']; + $linuxallowed = [ + 'LC_COLLATE', 'LC_CTYPE', 'LC_MESSAGES', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME', + 'LC_PAPER', 'LC_NAME', 'LC_ADDRESS', 'LC_TELEPHONE', 'LC_MEASUREMENT', 'LC_IDENTIFICATION', + ]; + + // The base locale name is also OS dependent. + $baselocale = get_string('locale', 'langconfig'); + if (PHP_OS_FAMILY === 'Windows') { + $baselocale = get_string('localewin', 'langconfig'); + } + + // Here we'll go accumulating cases to be provided. + $cases = []; + + // First, the simplest case, just pass a locale name, without categories. + $cases['rawlocale'] = [ + 'set' => $baselocale, + 'ret' => $baselocale, + ]; + + // Now, let's fill ALL LC categories, we should get back the locale name if all them are set with same value. + // Note that this case is the one that, under Linux only, covers the changes performed to the set_locale() method. + // Pick the correct categories depending on the OS. + $oscategories = $bsdallowed; // Default to BSD/Dawrwin ones because they are the standard 6 supported by PHP. + if (PHP_OS_FAMILY === 'Windows') { + $oscategories = $winallowed; + } else if (PHP_OS_FAMILY === 'Linux') { + $oscategories = $linuxallowed; + } + + $localestr = ''; + foreach ($oscategories as $category) { + // Format is different by OS too, so let build the string conditionally. + if (PHP_OS_FAMILY === 'BSD' || PHP_OS_FAMILY === 'Darwin') { + // BSD uses slashes (/) separated list of the 6 values in exact order. + $localestr .= '/' . $baselocale; + } else { + // Linux/Windows use semicolon (;) separated list of category=value pairs. + $localestr .= ';' . $category . '=' . $baselocale; + } + } + $cases['allcategories'] = [ + 'set' => trim($localestr, ';/'), + 'ret' => $baselocale, + ]; + + // Return all the built cases. + return $cases; + } +}