From 82bc99b181cf71231daaba74b3e1e8c71ca9b705 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Wed, 13 Apr 2022 16:26:15 +0900 Subject: [PATCH] Language\ namespace update Update the Core Language classes to have all method parameter type declaration. GetTextReader has gettext as alias to translate. GetTextReader public methods to get cache enable status and short circuit (no translation loaded) status Main language: Add new methods for plural and plural in context (__n, __p, __pn) Deprecate gettext, ngettext, _e Add new translation loader in gettext standard /LC_MESSAGES/ style Including locales checker, auto detect on enviroment variables, return self as class (for functions type) return translator class after loading, etc New LoadFunctions to run all like functions. Names like php but with two underscores prefixed. eg gettext -> __gettext --- 4dev/locale/en_US.admin.UTF-8.po | 27 +- 4dev/locale/en_US.frontend.UTF-8.po | 22 + 4dev/locale/ja_JP.admin.UTF-8.po | 16 +- 4dev/locale/ja_JP.frontend.UTF-8.po | 6 + 4dev/tests/CoreLibsLanguageL10nTest.php | 1026 ++++++++++++++++- 4dev/tests/includes/create_po.sh | 13 + .../includes/locale/en_US.admin.UTF-8.po | 32 + .../includes/locale/en_US.frontend.UTF-8.po | 32 + .../locale/en_US/LC_MESSAGES/admin.mo | Bin 88 -> 807 bytes .../locale/en_US/LC_MESSAGES/frontend.mo | Bin 91 -> 834 bytes .../includes/locale/ja_JP.admin.UTF-8.po | 33 + .../includes/locale/ja_JP.frontend.UTF-8.po | 33 + .../locale/ja_JP/LC_MESSAGES/admin.mo | Bin 88 -> 839 bytes .../locale/ja_JP/LC_MESSAGES/frontend.mo | Bin 91 -> 866 bytes www/admin/class_test.lang.php | 62 +- .../locale/en_US/LC_MESSAGES/admin.mo | Bin 710 -> 1075 bytes www/includes/locale/ja/LC_MESSAGES/admin.mo | Bin 1134 -> 1352 bytes www/lib/CoreLibs/Basic.php | 22 +- www/lib/CoreLibs/Language/Core/FileReader.php | 10 +- .../CoreLibs/Language/Core/GetTextReader.php | 105 +- .../CoreLibs/Language/Core/StreamReader.php | 8 +- .../CoreLibs/Language/Core/StringReader.php | 10 +- www/lib/CoreLibs/Language/L10n.php | 599 +++++++++- www/lib/CoreLibs/Language/l10n_functions.php | 208 ++++ 24 files changed, 2122 insertions(+), 142 deletions(-) create mode 100755 4dev/tests/includes/create_po.sh create mode 100644 www/lib/CoreLibs/Language/l10n_functions.php diff --git a/4dev/locale/en_US.admin.UTF-8.po b/4dev/locale/en_US.admin.UTF-8.po index e6241638..b6c191f3 100644 --- a/4dev/locale/en_US.admin.UTF-8.po +++ b/4dev/locale/en_US.admin.UTF-8.po @@ -3,21 +3,22 @@ # CREATED: 2005/08/09 # SHORT DESCRIPTION: # Backned English Messages file for gettext -# to craete: msgfmt -o ja.mo messages_en.po -# HISTORY: +# to craete: msgfmt -o # ********************************************************************/ msgid "" msgstr "" -"Project-Id-Version: Project Version\n" +"Project-Id-Version: en_US.UTF-8 LC_MESSAGES admin\n" "Report-Msgid-Bugs-To: clemens.schwaighofer@egplusww.com\n" "POT-Creation-Date: 2018-03-28 10:40+0900\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: \n" +"Last-Translator: clemens.schwaighofer@egplusww.co\n" "Language-Team: E-GRAPHICS COMMUNICATIONS Japan \n" +"Language: en_US\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" msgid "Year" msgstr "Year" @@ -28,6 +29,24 @@ msgstr "Month" msgid "INPUT TEST" msgstr "OUTPUT TEST ADMIN EN" +# testing multi +msgid "single" +msgid_plural "multi" +msgstr[0] "Multi Admin en_US 0" +msgstr[1] "Multi Admin en_US 1" +msgstr[2] "Multi Admin en_US 2" + +msgctxt "month name" +msgid "May" +msgstr "May Admin en_US" + +msgctxt "month name" +msgid "single" +msgid_plural "multi" +msgstr[0] "Multi Admin month en_US 0" +msgstr[1] "Multi Admin month en_US 1" +msgstr[2] "Multi Admin month en_US 2" + msgid "I should be translated" msgstr "I should be translated: I WAS TRANSLATED" diff --git a/4dev/locale/en_US.frontend.UTF-8.po b/4dev/locale/en_US.frontend.UTF-8.po index 6e9f76f2..765359d0 100644 --- a/4dev/locale/en_US.frontend.UTF-8.po +++ b/4dev/locale/en_US.frontend.UTF-8.po @@ -1,2 +1,24 @@ +# to craete: msgfmt -o + +msgid "" +msgstr "" +"Project-Id-Version: en_US.UTF-8 LC_MESSAGES frontend\n" +"Report-Msgid-Bugs-To: clemens.schwaighofer@egplusww.com\n" +"POT-Creation-Date: 2018-03-28 10:40+0900\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: clemens.schwaighofer@egplusww.co\n" +"Language-Team: E-GRAPHICS COMMUNICATIONS Japan \n" +"Language: en_US\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + msgid "INPUT TEST" msgstr "OUTPUT TEST FRONTEND EN" + +msgid "single" +msgid_plural "multi" +msgstr[0] "Multi frontend en_US 0" +msgstr[1] "Multi frontend en_US 1" +msgstr[2] "Multi frontend en_US 2" diff --git a/4dev/locale/ja_JP.admin.UTF-8.po b/4dev/locale/ja_JP.admin.UTF-8.po index 4d49c4a9..35e08f80 100644 --- a/4dev/locale/ja_JP.admin.UTF-8.po +++ b/4dev/locale/ja_JP.admin.UTF-8.po @@ -3,21 +3,22 @@ # CREATED: 2018/03/28 # SHORT DESCRIPTION: # Backend Japanese Messages file for gettext -# to craete: msgfmt -o ja.mo messages_ja.po -# HISTORY: +# to craete: msgfmt -o # ********************************************************************/ msgid "" msgstr "" -"Project-Id-Version: Project Version\n" +"Project-Id-Version: ja_JP.UTF-8 LC_MESSAGES admin\n" "Report-Msgid-Bugs-To: clemens.schwaighofer@egplusww.com\n" "POT-Creation-Date: 2018-03-28 10:40+0900\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: \n" +"Last-Translator: clemens.schwaighofer@egplusww.com\n" "Language-Team: E-GRAPHICS COMMUNICATIONS Japan \n" +"Language: ja\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" msgid "Yes" msgstr "はい" @@ -65,6 +66,13 @@ msgstr "日" msgid "INPUT TEST" msgstr "OUTPUT TEST ADMIN JA" +# testing multi +msgid "single" +msgid_plural "multi" +msgstr[0] "Multi Admin ja_JP 0" +msgstr[1] "Multi Admin ja_JP 1" +msgstr[2] "Multi Admin ja_JP 2" + # login string msgid "Hello %s" msgstr "こにちは %s" diff --git a/4dev/locale/ja_JP.frontend.UTF-8.po b/4dev/locale/ja_JP.frontend.UTF-8.po index 91c739b2..049707a1 100644 --- a/4dev/locale/ja_JP.frontend.UTF-8.po +++ b/4dev/locale/ja_JP.frontend.UTF-8.po @@ -1,2 +1,8 @@ +# to craete: msgfmt -o + +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + msgid "INPUT TEST" msgstr "OUTPUT TEST FRONTEND JA" diff --git a/4dev/tests/CoreLibsLanguageL10nTest.php b/4dev/tests/CoreLibsLanguageL10nTest.php index 57adf084..ac866c06 100644 --- a/4dev/tests/CoreLibsLanguageL10nTest.php +++ b/4dev/tests/CoreLibsLanguageL10nTest.php @@ -1,4 +1,4 @@ -assertTrue(true, 'Language L10n Tests not implemented'); - $this->markTestIncomplete( - 'Language\L10n Tests have not yet been implemented' + define('DEFAULT_LANG', 'en_US'); + // default web page encoding setting + define('DEFAULT_ENCODING', 'UTF-8'); + // default lang + encoding + define('DEFAULT_LOCALE', 'en_US.UTF-8'); + // site + define('SITE_LANG', DEFAULT_LANG); + // just set + define('BASE', str_replace('/configs', '', __DIR__) . DIRECTORY_SEPARATOR); + define('INCLUDES', 'includes' . DIRECTORY_SEPARATOR); + define('LANG', 'lang' . DIRECTORY_SEPARATOR); + define('LOCALE', 'locale' . DIRECTORY_SEPARATOR); + define('CONTENT_PATH', 'frontend' . DIRECTORY_SEPARATOR); + } + + /** + * get instance self type check + * + * @covers ::getInstance + * @testdox check that getInstance() returns valid instance + * + * @return void + */ + public function testGetInstance(): void + { + $l10n_obj = \CoreLibs\Language\L10n::getInstance(); + $this->assertIsObject( + $l10n_obj + ); + $this->assertInstanceOf( + '\CoreLibs\Language\L10n', + $l10n_obj + ); + } + + /** + * get current translator class type check + * + * @covers ::getTranslatorClass + * @testdox check that getTranslatorClass() returns valid instance + * + * @return void + */ + public function testGetTranslatorClass(): void + { + $l10n = new \CoreLibs\Language\L10n(); + $translator = $l10n->getTranslatorClass(); + $this->assertIsObject( + $translator + ); + $this->assertInstanceOf( + '\CoreLibs\Language\Core\GetTextReader', + $translator + ); + } + + /** + * provider for class load parameters + * + * @return array + */ + public function l10nObjectProvider(): array + { + return [ + // 0: locale + // 1: path + // 2: domain + // 3: legacy load (Default true) + // 4: locale expected + // 5: domain exepcted + // 6: context (null for none) + // 7: test string in + // 8: test translated + 'legacy load en' => [ + 'en_utf8', + null, + null, + null, + // + 'en_utf8', + '', + // + null, + 'Original', + 'Translated frontend en_US', + ], + 'legacy load ja' => [ + 'ja_utf8', + null, + null, + null, + 'ja_utf8', + '', + null, + 'Original', + 'Translated frontend ja_JP', + ], + // new style load + 'gettext load en' => [ + 'en_US.UTF-8', + __DIR__ . 'includes/locale/', + 'frontend', + false, + 'en_US.UTF-8', + 'frontend', + null, + 'Original', + 'Translated frontend en_US', + ], + 'gettext load en' => [ + 'en_US.UTF-8', + __DIR__ . 'includes/locale/', + 'frontend', + false, + 'en_US.UTF-8', + 'frontend', + 'context', + 'Original', + 'Original context frontend en_US', + ], + 'gettext load ja' => [ + 'ja_JP.UTF-8', + __DIR__ . 'includes/locale/', + 'admin', + false, + 'ja_JP.UTF-8', + 'admin', + null, + 'Original', + 'Translated admin ja_JP', + ], + // null set locale legacy + 'empty load legacy' => [ + null, + null, + null, + null, + '', + '', + null, + 'Original', + 'Original', + ], + 'empty load new ' => [ + '', + '', + '', + false, + '', + '', + null, + 'Original', + 'Original', + ] + ]; + } + + /** + * new class load test (basic test) + * + * @covers ::__construct + * @dataProvider l10nObjectProvider + * @testdox check l10n init with Locale $locale, Path $path, Domain $domain, Legacy: $legacy with $context [$_dataName] + * + * @param string|null $locale + * @param string|null $path + * @param string|null $domain + * @param bool|null $legacy + * @param string $locale_expected + * @param string $domain_expected + * @param ?string $context + * @param string $original + * @param string $translated + * @return void + */ + public function testL10nObject( + ?string $locale, + ?string $path, + ?string $domain, + ?bool $legacy, + string $locale_expected, + string $domain_expected, + ?string $context, + string $original, + string $translated, + ): void { + if ($locale === null) { + $l10n = new \CoreLibs\Language\L10n(); + } elseif ($path === null) { + $l10n = new \CoreLibs\Language\L10n($locale); + } elseif ($domain === null) { + $l10n = new \CoreLibs\Language\L10n($locale, $path); + } elseif ($legacy === null) { + $l10n = new \CoreLibs\Language\L10n($locale, $path, $domain); + } else { + $l10n = new \CoreLibs\Language\L10n($locale, $path, $domain, $legacy); + } + // print "LOC: " . $locale . ", " . $l10n->getLocale() . ", " . $locale_expected . "\n"; + // print "MO: " . $l10n->getMoFile() . "\n"; + $this->assertEquals( + $locale_expected, + $l10n->getLocale(), + 'Locale assert failed' + ); + $this->assertEquals( + $domain_expected, + $l10n->getDomain(), + 'Domain assert failed' + ); + if (empty($context)) { + $this->assertEquals( + $translated, + $l10n->__($original), + 'Translated string assert failed' + ); + } else { + $this->assertEquals( + $translated, + $l10n->__p($context, $original), + 'Translated string assert failed in context: ' . $context + ); + } + } + + // l10nReloadMOfile and getTranslator + // null init with loader + // loader with reload (change type) + + /** + * Undocumented function + * + * @return array + */ + public function getTranslatorProvider(): array + { + return [ + // 0: locale + // 1: path + // 2: domain + // 3: legacy flag + // 4: input string to translated + // 5: expected locale + // 6: expected domain + // 7: expected translation + // 8: change locale + // 9: change path + // 10: change domain + // 11: legacy flag + // 12: expected locale + // 13: expected domain + // 14: expected translation + 'legacy load and change (en->ja)' => [ + // set 0-3 + 'en_utf8', + null, + null, + null, + // status 4 + false, + // to translate 5 + 'Original', + // check setter 6-8 + 'en_utf8', + '', + 'Translated frontend en_US', + // set new 9-12 + 'ja_utf8', + null, + null, + null, + // status new 13 + false, + // check new setter 14-16 + 'ja_utf8', + '', + 'Translated frontend ja_JP', + ], + 'load and change (en->ja)' => [ + // set 0-3 + 'en_US.UTF-8', + __DIR__ . 'includes/locale/', + 'frontend', + false, + // status 4 + false, + // to translate 5 + 'Original', + // check setter 6-8 + 'en_US.UTF-8', + 'frontend', + 'Translated frontend en_US', + // set new 9-12 + 'ja_JP.UTF-8', + __DIR__ . 'includes/locale/', + 'frontend', + false, + // status new 13 + false, + // check new setter 14-16 + 'ja_JP.UTF-8', + 'frontend', + 'Translated frontend ja_JP', + ], + 'empty load and change to en' => [ + // set 0-3 + '', + '', + '', + false, + // status 4 + false, + // to translate 5 + 'Original', + // check setter 6-8 + '', + '', + 'Original', + // set new 9-12 + 'en_US.UTF-8', + __DIR__ . 'includes/locale/', + 'frontend', + false, + // status new 13 + false, + // check new setter 14-16 + 'en_US.UTF-8', + 'frontend', + 'Translated frontend en_US', + ] + ]; + } + + /** + * init check and connected change translation + * + * @covers ::getTranslator + * @covers ::l10nReloadMOfile + * @dataProvider getTranslatorProvider + * @testdox change locale from $locale and domain $domain to locale $locale_new and domain $domain_new [$_dataName] + * + * @param string|null $locale + * @param string|null $path + * @param string|null $domain + * @param bool|null $legacy + * @param bool $load_error + * @param string $original + * @param string $locale_expected_a + * @param string $domain_expected_a + * @param string $translated_a + * @param string|null $locale_new + * @param string|null $path_new + * @param string|null $domain_new + * @param bool|null $legacy_new + * @param bool $load_error_new + * @param string $locale_expected_b + * @param string $domain_expected_b + * @param string $translated_b + * @return void + */ + public function testGetTranslator( + // 0-3 + ?string $locale, + ?string $path, + ?string $domain, + ?bool $legacy, + // 4 + bool $load_error, + // 5 + string $original, + // 6-8 + string $locale_expected_a, + string $domain_expected_a, + string $translated_a, + // 9-12 + ?string $locale_new, + ?string $path_new, + ?string $domain_new, + ?bool $legacy_new, + // 13 + bool $load_error_new, + // 14-16 + string $locale_expected_b, + string $domain_expected_b, + string $translated_b, + ): void { + if ($locale === null) { + $l10n = new \CoreLibs\Language\L10n(); + } elseif ($path === null) { + $l10n = new \CoreLibs\Language\L10n($locale); + } elseif ($domain === null) { + $l10n = new \CoreLibs\Language\L10n($locale, $path); + } elseif ($legacy === null) { + $l10n = new \CoreLibs\Language\L10n($locale, $path, $domain); + } else { + $l10n = new \CoreLibs\Language\L10n($locale, $path, $domain, $legacy); + } + // print "LOC: " . $locale . ", " . $l10n->getLocale() . ", " . $locale_expected . "\n"; + // status check + $this->assertEquals( + $load_error, + $l10n->getLoadError(), + 'Legacy method load error init check' + ); + $this->assertEquals( + $locale_expected_a, + $l10n->getLocale(), + 'Locale init assert failed' + ); + $this->assertEquals( + $domain_expected_a, + $l10n->getDomain(), + 'Domain init assert failed' + ); + $this->assertEquals( + $translated_a, + $l10n->__($original), + 'Translated string init assert failed' + ); + + // do reload with legacy l10nReloadMOfile IF legacy is null or true + // use getTranslator if legacy is false + if ($legacy === null || $legacy === true) { + // there is no null/empty locale allowed directly + // if empty will set previous one + if ($path_new === null) { + $ret_status = $l10n->l10nReloadMOfile($locale_new); + } elseif ($domain_new === null) { + $ret_status = $l10n->l10nReloadMOfile($locale_new, $path_new); + } elseif ($legacy_new === null) { + $ret_status = $l10n->l10nReloadMOfile($locale_new, $path_new, $domain_new); + } else { + $ret_status = $l10n->l10nReloadMOfile($locale_new, $path_new, $domain_new, $legacy_new); + } + // status check + $this->assertEquals( + $load_error_new, + $l10n->getLoadError(), + 'Legacy method load error change check' + ); + // retun status check is inverted to load error check + $this->assertEquals( + $load_error_new ? false : true, + $ret_status, + 'Legacy return load error change check' + ); + } else { + if ($locale_new === null) { + $translator = $l10n->getTranslator(); + } elseif ($path_new === null) { + $translator = $l10n->getTranslator($locale_new); + } elseif ($domain_new === null) { + $translator = $l10n->getTranslator($locale_new, $path_new); + } elseif ($legacy_new === null) { + $translator = $l10n->getTranslator($locale_new, $path_new, $domain_new); + } else { + $translator = $l10n->getTranslator($locale_new, $path_new, $domain_new, $legacy_new); + } + // status check + $this->assertEquals( + $load_error_new, + $l10n->getLoadError(), + 'Translate method load error change check' + ); + // check that returned is class GetTextReader and object + $this->assertIsObject( + $translator, + 'translater class is object assert failed' + ); + $this->assertInstanceOf( + '\CoreLibs\Language\Core\GetTextReader', + $translator, + 'translator class is correct instance assert failed' + ); + // translator class + $this->assertEquals( + $translated_b, + $translator->gettext($original), + 'Translated string change assert failed from returned class' + ); + } + // new set check + $this->assertEquals( + $locale_expected_b, + $l10n->getLocale(), + 'Locale change assert failed' + ); + $this->assertEquals( + $domain_expected_b, + $l10n->getDomain(), + 'Domain change assert failed' + ); + $this->assertEquals( + $translated_b, + $l10n->__($original), + 'Translated string change assert failed' + ); + } + + // TODO: test all translation types + // __/gettext + // __n/ngettext + // ->dgettext + // ->dngettext + // ->dpgettext + // ->dpngettext + + /** + * Undocumented function + * + * @return array + */ + public function ngettextProvider(): array + { + return [ + // 0: locale + // 1: path + // 2: domain + // 3: context (null for none) + // 4: single string + // 5: plural string + // 6: array for each n value expected string + 'plural text en' => [ + 'en_US', + __DIR__ . 'includes/locale/', + 'admin', + // context + null, + // text single/multi in + 'single', + 'multi', + // confirm translation, pos in array equal n + [ + 0 => 'Multi admin en_US 1', + 1 => 'Multi admin en_US 0', + 2 => 'Multi admin en_US 1', + ] + ], + 'plural text context en' => [ + 'en_US', + __DIR__ . 'includes/locale/', + 'admin', + // context + 'context', + // text single/multi in + 'single', + 'multi', + // confirm translation, pos in array equal n + [ + 0 => 'Multi context admin en_US 1', + 1 => 'Multi context admin en_US 0', + 2 => 'Multi context admin en_US 1', + ] + ], + ]; + } + + /** + * Undocumented function + * + * @covers ::__n + * @dataProvider ngettextProvider + * @testdox plural string test for locale $locale and domain $domain with $context [$_dataName] + * + * @param string $locale + * @param string $path + * @param string $domain + * @param ?string $context + * @param string $original_single + * @param string $original_plural + * @param array $expected_strings + * @return void + */ + public function testNgettext( + // config 0-3 + string $locale, + string $path, + string $domain, + // context string + ?string $context, + // input strings + string $original_single, + string $original_plural, + // expected + array $expected_strings + ): void { + $l10n = new \CoreLibs\Language\L10n($locale, $path, $domain, false); + + foreach ($expected_strings as $n => $expected) { + if (empty($context)) { + $this->assertEquals( + $expected, + $l10n->__n($original_single, $original_plural, $n), + 'assert failed for plural: ' . $n + ); + } else { + $this->assertEquals( + $expected, + $l10n->__pn($context, $original_single, $original_plural, $n), + 'assert failed for plural: ' . $n . ' in context: ' . $context + ); + } + } + } + + /** + * locales list for testing locale folder lookup + * + * @return array + */ + public function localesProvider(): array + { + return [ + // 0: locale + // 1: return array + 'en' => [ + 'en', + [ + 'en', + ] + ], + 'en.UTF-8' => [ + 'en.UTF-8', + [ + 'en.UTF-8', + 'en', + ] + ], + 'en_US' => [ + 'en_US', + [ + 'en_US', + 'en', + ] + ], + 'en_US.UTF-8' => [ + 'en_US.UTF-8', + [ + 'en_US.UTF-8', + 'en_US', + 'en', + ] + ], + 'en_US@subtext' => [ + 'en_US@subtext', + [ + 'en_US@subtext', + 'en@subtext', + 'en_US', + 'en', + ] + ], + 'en_US.UTF-8@subtext' => [ + 'en_US.UTF-8@subtext', + [ + 'en_US.UTF-8@subtext', + 'en_US@subtext', + 'en@subtext', + 'en_US.UTF-8', + 'en_US', + 'en', + ] + ] + ]; + } + + /** + * test locales array return + * + * @covers ::listLocales + * @dataProvider localesProvider + * @testdox check $locale [$_dataName] + * + * @param string $locale + * @param array $expected + * @return void + */ + public function testListLocales(string $locale, array $expected): void + { + $locale_list = \CoreLibs\Language\L10n::listLocales($locale); + // print "LOCALES: " . print_r($locale_list, true) . "\n"; + $this->assertEquals( + $expected, + $locale_list + ); + } + + // @covers ::detectLocale + + /** + * Undocumented function + * + * @return array + */ + public function detectLocaleProvider(): array + { + return [ + // 0: type: global | env + // 1: global variable name or enviroment var + // 2: value to set + // 3: value to expect back + 'global locale' => [ + 'global', + 'LOCALE', + 'ja_JP.UTF-8', + 'ja_JP.UTF-8', + ], + 'env LC_ALL' => [ + 'env', + 'LC_ALL', + 'ja_JP.UTF-8', + 'ja_JP.UTF-8', + ], + 'env LANG' => [ + 'env', + 'LANG', + 'ja_JP.UTF-8', + 'ja_JP.UTF-8', + ], + 'default return' => [ + 'env', + 'LC_ALL', + '', + 'en', + ] + ]; + } + + /** + * Undocumented function + * @covers ::detectLocale + * @dataProvider detectLocaleProvider + * @testdox check detectLocale for $type with $var and $value is $expected [$_dataName] + * + * @return void + */ + public function testDetectLocale( + string $type, + string $var, + string $value, + string $expected, + ): void { + switch ($type) { + case 'global': + $GLOBALS[$var] = $value; + break; + case 'env': + $old_value = getenv("$var"); + putenv("$var=$value"); + // unset all other env vars + foreach (['LC_ALL', 'LC_MESSAGES', 'LANG'] as $env) { + if ($env != $var) { + putenv("$env="); + } + } + break; + } + $locale = \CoreLibs\Language\L10n::detectLocale(); + $this->assertEquals( + $expected, + $locale + ); + // reset post run + switch ($type) { + case 'global': + unset($GLOBALS[$var]); + break; + case 'env': + putenv("$var=$old_value"); + break; + } + } + + // set/get text domain, domain, locale + + /** + * Undocumented function + * + * @return array + */ + public function textDomainProvider(): array + { + return [ + // 0: set domain + // 1: set path + // 2: get domain + // 3: expected path + 'valid set and get' => [ + 'foo', + 'foo/bar', + 'foo', + 'foo/bar', + ], + 'invalid set and get' => [ + 'foo', + 'foo/bar', + 'iamnotset', + false + ] + ]; + } + + /** + * Undocumented function + * + * @covers ::setTextDomain + * @covers ::getTextDomain + * @dataProvider textDomainProvider + * @testdox set $domain with $path and get $get_domain and expect $expected [$_dataName] + * + * @param string $domain + * @param string $path + * @param string $get_domain + * @param string|bool $expected + * @return void + */ + public function testSetGetTextDomain(string $domain, string $path, string $get_domain, $expected): void + { + $l10n = new \CoreLibs\Language\L10n(); + $l10n->setTextDomain($domain, $path); + $this->assertEquals( + $expected, + $l10n->getTextDomain($get_domain) + ); + } + + /** + * Undocumented function + * + * @return array + */ + public function domainProvider(): array + { + return [ + // 0: set domain + // 1: expected domain from get + 'valid domain' => [ + 'foo', + 'foo', + ], + 'empty domain' => [ + '', + '', + ] + ]; + } + + /** + * Undocumented function + * + * @covers ::setDomain + * @covers ::getDomain + * @dataProvider domainProvider + * @testdox set $domain and expect $expected [$_dataName] + * + * @param string $domain + * @param string $expected + * @return void + */ + public function testSetGetDomain(string $domain, string $expected): void + { + $l10n = new \CoreLibs\Language\L10n(); + $l10n->setDomain($domain); + $this->assertEquals( + $expected, + $l10n->getDomain() + ); + } + + /** + * Undocumented function + * + * @return array + */ + public function localeProvider(): array + { + return [ + // 0: set locale + // 1: pre set if not null or not empty + // 2: expected return from set + // 3: expected from get + 'valid locale' => [ + 'foo', + null, + 'foo', + 'foo', + ], + 'empty locale' => [ + '', + null, + '', + '', + ], + 'empty locale, pre set' => [ + '', + 'foo', + 'foo', + 'foo', + ], + ]; + } + + /** + * Undocumented function + * + * @covers ::setLocale + * @covers ::getLocale + * @dataProvider localeProvider + * @testdox set $locale with $expected_return and expect $expected [$_dataName] + * + * @param string $locale + * @param string $pre_locale + * @param string $expected_return + * @param string $expected + * @return void + */ + public function testSetGetLocale( + string $locale, + ?string $pre_locale, + string $expected_return, + string $expected + ): void { + $l10n = new \CoreLibs\Language\L10n(); + if (!empty($pre_locale)) { + $l10n->setLocale($pre_locale); + } + $returned = $l10n->setLocale($locale); + $this->assertEquals( + $expected_return, + $returned, + 'Set locale return assert failed' + ); + $this->assertEquals( + $expected, + $l10n->getLocale(), + 'Get locale aszert failed' + ); + } + + // static load + + /** + * Undocumented function + * + * @return array + */ + public function functionsProvider(): array + { + return [ + // 0: lang/locale + // 1: path + // 2: domain + // 3: encoding + // 4: string + // 5: translated string + 'standard en' => [ + 'en_US.UTF-8', + __DIR__ . 'includes/locale/', + 'frontend', + 'UTF-8', + 'Original', + 'Translated frontend en_US', + ], + 'standard ja' => [ + 'ja_JP.UTF-8', + __DIR__ . 'includes/locale/', + 'admin', + 'UTF-8', + 'Original', + 'Translated admin ja_JP', + ] + ]; + } + + /** + * fuctions check + * TODO: others d/dn/dp/dpn gettext functions + * + * @covers __setlocale + * @covers __bindtextdomain + * @covers __bind_textdomain_codeset + * @covers __textdomain + * @covers __gettext + * @covers __ + * @dataProvider functionsProvider + * @testdox check functions with locale $locale and domain $domain [$_dataName] + * @param string $locale + * @param string $path + * @param string $domain + * @param string $encoding + * @param string $original + * @param string $translated + * @return void + */ + public function testFunctions( + string $locale, + string $path, + string $domain, + string $encoding, + string $original, + string $translated + ): void { + \CoreLibs\Language\L10n::loadFunctions(); + __setlocale(LC_MESSAGES, $locale); + __textdomain($domain); + __bindtextdomain($domain, $path); + __bind_textdomain_codeset($domain, $encoding); + $this->assertEquals( + $translated, + __($original), + 'function __ assert failed' + ); + $this->assertEquals( + $translated, + __gettext($original), + 'function gettext assert failed' ); } } diff --git a/4dev/tests/includes/create_po.sh b/4dev/tests/includes/create_po.sh new file mode 100755 index 00000000..d549d19a --- /dev/null +++ b/4dev/tests/includes/create_po.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# if we don't have one base file we are in the wrong folder +if [ ! -f "locale/en_US/LC_MESSAGES/admin.mo" ]; then + echo "Locale file is missing, wrong base folder?" + echo "Should be: 4dev/tests/includes/" + exit; +fi; + +msgfmt -o locale/en_US/LC_MESSAGES/admin.mo locale/en_US.admin.UTF-8.po +msgfmt -o locale/en_US/LC_MESSAGES/frontend.mo locale/en_US.frontend.UTF-8.po +msgfmt -o locale/ja_JP/LC_MESSAGES/admin.mo locale/ja_JP.admin.UTF-8.po +msgfmt -o locale/ja_JP/LC_MESSAGES/frontend.mo locale/ja_JP.frontend.UTF-8.po diff --git a/4dev/tests/includes/locale/en_US.admin.UTF-8.po b/4dev/tests/includes/locale/en_US.admin.UTF-8.po index af91eed8..c35caf23 100644 --- a/4dev/tests/includes/locale/en_US.admin.UTF-8.po +++ b/4dev/tests/includes/locale/en_US.admin.UTF-8.po @@ -1,2 +1,34 @@ + +msgid "" +msgstr "" +"Project-Id-Version: en_US.UTF-8 LC_MESSAGES admin\n" +"Report-Msgid-Bugs-To: clemens.schwaighofer@egplusww.com\n" +"POT-Creation-Date: 2018-03-28 10:40+0900\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: clemens.schwaighofer@egplusww.co\n" +"Language-Team: E-GRAPHICS COMMUNICATIONS Japan \n" +"Language: en_US\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + msgid "Original" msgstr "Translated admin en_US" + +msgid "single" +msgid_plural "multi" +msgstr[0] "Multi admin en_US 0" +msgstr[1] "Multi admin en_US 1" +msgstr[2] "Multi admin en_US 2" + +msgctxt "context" +msgid "Original" +msgstr "Original context admin en_US" + +msgctxt "context" +msgid "single" +msgid_plural "multi" +msgstr[0] "Multi context admin en_US 0" +msgstr[1] "Multi context admin en_US 1" +msgstr[2] "Multi context admin en_US 2" diff --git a/4dev/tests/includes/locale/en_US.frontend.UTF-8.po b/4dev/tests/includes/locale/en_US.frontend.UTF-8.po index f955356b..a2f58edb 100644 --- a/4dev/tests/includes/locale/en_US.frontend.UTF-8.po +++ b/4dev/tests/includes/locale/en_US.frontend.UTF-8.po @@ -1,2 +1,34 @@ + +msgid "" +msgstr "" +"Project-Id-Version: en_US.UTF-8 LC_MESSAGES frontend\n" +"Report-Msgid-Bugs-To: clemens.schwaighofer@egplusww.com\n" +"POT-Creation-Date: 2018-03-28 10:40+0900\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: clemens.schwaighofer@egplusww.co\n" +"Language-Team: E-GRAPHICS COMMUNICATIONS Japan \n" +"Language: en_US\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + msgid "Original" msgstr "Translated frontend en_US" + +msgid "single" +msgid_plural "multi" +msgstr[0] "Multi frontend en_US 0" +msgstr[1] "Multi frontend en_US 1" +msgstr[2] "Multi frontend en_US 2" + +msgctxt "context" +msgid "Original" +msgstr "Original context frontend en_US" + +msgctxt "context" +msgid "single" +msgid_plural "multi" +msgstr[0] "Multi context frontend en_US 0" +msgstr[1] "Multi context frontend en_US 1" +msgstr[2] "Multi context frontend en_US 2" diff --git a/4dev/tests/includes/locale/en_US/LC_MESSAGES/admin.mo b/4dev/tests/includes/locale/en_US/LC_MESSAGES/admin.mo index f52560b8e844270163ffed74e1428082b971809a..c681b0c4234124df6b37b71d66b5b1032368b096 100644 GIT binary patch literal 807 zcmaix&5qMB5XTJ^fg%nFAx<33dF8O}X_Zn$HqGt^ZJMe{k>Ij2i8uAiu_N2r0#5)} zB*cjmFTo4&8oU56!KCSSx8iH0pT^^{$K(I}xYzrHz-|H$fiZ9k5I}41f!n|b;4bhH zxDR{8YeBgj z;(S51VRp3A?DAau3DALrGFJtRz{7j5xpk(Z(NvHFr7PoM2^v9YMuQ>lmLrKDjc|YH ypbHsT(TU%UW;1ZS9{5%=ZioD*4Bkb5&-MS28?1Ay`FAaBYF*vCsBv{~fPMk|*6&FG literal 88 zcmca7#4?ou2pEA_28dOFm>Gz5fS3b_Er3`Ih#i3#qy`53i!#$Q^Ad9yLW&aeigOZ6 TQd1NXQ*tx&6jJlzLxULrhrYI=H zD)=t=Pxue`Z}^`GPWRVwE1rQ5lbJ~;)9>Whz0Owz>n89J7y`Ed0krEAa2xmx+y%Y> z_kr)g9pDG>?HWQq!9D#>;R{(dv}7b>wDNFBjwb&2I0{l6 zByl_)MS-71$tcAyX-y@5%H>SCXZ5A_o^7kFC2-;>4liWsy8BMBQ~;X$P($~v*iULf zx$NV7PPJinu$1lMT>BHC0|{lW3Ydb258iR>j76iVAcsm<#={ac0@nI@(*ztPoyZN}C_CJz%A6=R5UY+i()0-Eu>S$YZC%h@P6YiluPb~YF literal 91 zcmca7#4?ou2pEA_28dOFm>Gz5fS3b_Er3`Oh#i3#qy`53i!#$Q^Ad9yLW&aeigOZ6 WQd1Ptit_VHQu9(2QuE?NgBbw11rJsL diff --git a/4dev/tests/includes/locale/ja_JP.admin.UTF-8.po b/4dev/tests/includes/locale/ja_JP.admin.UTF-8.po index 3f4dccbe..e2a8795c 100644 --- a/4dev/tests/includes/locale/ja_JP.admin.UTF-8.po +++ b/4dev/tests/includes/locale/ja_JP.admin.UTF-8.po @@ -1,2 +1,35 @@ + +msgid "" +msgstr "" +"Project-Id-Version: ja_JP.UTF-8 LC_MESSAGES admin\n" +"Report-Msgid-Bugs-To: clemens.schwaighofer@egplusww.com\n" +"POT-Creation-Date: 2018-03-28 10:40+0900\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: clemens.schwaighofer@egplusww.co\n" +"Language-Team: E-GRAPHICS COMMUNICATIONS Japan \n" +"Language: ja\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2;\n" +# 0, 1, 2 plural + msgid "Original" msgstr "Translated admin ja_JP" + +msgid "single" +msgid_plural "multi" +msgstr[0] "Multi admin ja_JP 0" +msgstr[1] "Multi admin ja_JP 1" +msgstr[2] "Multi admin ja_JP 2" + +msgctxt "context" +msgid "Original" +msgstr "Original context admin ja_JP" + +msgctxt "context" +msgid "single" +msgid_plural "multi" +msgstr[0] "Multi context admin ja_JP 0" +msgstr[1] "Multi context admin ja_JP 1" +msgstr[2] "Multi context admin ja_JP 2" diff --git a/4dev/tests/includes/locale/ja_JP.frontend.UTF-8.po b/4dev/tests/includes/locale/ja_JP.frontend.UTF-8.po index 3d7318ff..ccdba7a6 100644 --- a/4dev/tests/includes/locale/ja_JP.frontend.UTF-8.po +++ b/4dev/tests/includes/locale/ja_JP.frontend.UTF-8.po @@ -1,2 +1,35 @@ + +msgid "" +msgstr "" +"Project-Id-Version: ja_JP.UTF-8 LC_MESSAGES frontend\n" +"Report-Msgid-Bugs-To: clemens.schwaighofer@egplusww.com\n" +"POT-Creation-Date: 2018-03-28 10:40+0900\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: clemens.schwaighofer@egplusww.co\n" +"Language-Team: E-GRAPHICS COMMUNICATIONS Japan \n" +"Language: ja\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2;\n" +# 0, 1, 2 plural + msgid "Original" msgstr "Translated frontend ja_JP" + +msgid "single" +msgid_plural "multi" +msgstr[0] "Multi frontend ja_JP 0" +msgstr[1] "Multi frontend ja_JP 1" +msgstr[2] "Multi frontend ja_JP 2" + +msgctxt "context" +msgid "Original" +msgstr "Original context frontend ja_JP" + +msgctxt "context" +msgid "single" +msgid_plural "multi" +msgstr[0] "Multi context frontend ja_JP 0" +msgstr[1] "Multi context frontend ja_JP 1" +msgstr[2] "Multi context frontend ja_JP 2" diff --git a/4dev/tests/includes/locale/ja_JP/LC_MESSAGES/admin.mo b/4dev/tests/includes/locale/ja_JP/LC_MESSAGES/admin.mo index f4985b59c4d45b62048505de9de0d639093b5bc8..df8d579c47826434472d5bcc25e26560b28d0014 100644 GIT binary patch literal 839 zcmaiy&2G~`5P%m50vQenAx<2o2YNvp>~N_ZOzS3Y>#B~UI7Na>wb)x{BYW5KIt0#K z;EKeBD-Qt|j=ToX!q{%pM*NNRd3I*jv$M1F^KR!8LAj1RK!(T-BtvR>kK9B)Aa{_D z$UWo>avS-Ee7{1-57hUOcURjypHW9>e?{F#ed`(_xUCU%HRqwLkxu;$OOXo6G7>9U z@i$fHH-{E7%{a*yStZDtF)r1d#})Mx`ka?qC~3o-og9w4$J0H!1xJIEz>6Yx-;01H zxsb+$7pkmipi`01-9@VDRM`+`Jm*q(bv#=#ku z&5w(W3E78u#!AgAr)k*5zWN5(1nS9HCD?x(wqA+K7-x%;Wpq!Kxwb)KgV5B@lRmUd zM?SPn$FX2@6Y8b8;aC=Ig9*4BHXK}RgM~(~Z;Gz5fS3b_Er3`Ih#i3#qy`53i!#$Q^Ad9yLW&aeigOZ6 TQd1NXQ*tx&6tWWIy#g2jhj|U= diff --git a/4dev/tests/includes/locale/ja_JP/LC_MESSAGES/frontend.mo b/4dev/tests/includes/locale/ja_JP/LC_MESSAGES/frontend.mo index 3e45df2d93eace207bd4535e7b5a46c26fe76ecd..c8b841444f5ddfbc29800d5bb35db52b9c5ab462 100644 GIT binary patch literal 866 zcmaiyQE$^Q5P%I7fg&Cd;spuB?SZ`@ho-zt>B1;!wq;tHs_l?KNRvt3)KzLnc1qz7 zAb3UM9f?1|BQN|7ehcSz?X)7sNng&sbI#|x`+cYOm7rWl?jvpF22vnaJ|Q=e&&X}$ z3vw6vj@&|iAV04V@(cAn==RRr||egv<+0rn90F?bVrJ%ZH&aumo6;VC zC+sb>%_{E0tEknZNlM!-I86N{e*8M^M9wiY6&+|MO~ERq{~sFvkok<|oDMjf`Vi5B zUaNZ;hkXdsBsuBCVQUblojx3~l1bPVa-`fzc_F=L>nOGaP7){4c}+aG?u3iyV`d*p zOx;%cNm(!m ehZE}LQnz=xyK&}TP0TW*bGz5fS3b_Er3`Oh#i3#qy`53i!#$Q^Ad9yLW&aeigOZ6 WQd1Ptit_VHQu9(2vJ&IH0vG_eyAMDB diff --git a/www/admin/class_test.lang.php b/www/admin/class_test.lang.php index c2b936d2..b672478c 100644 --- a/www/admin/class_test.lang.php +++ b/www/admin/class_test.lang.php @@ -40,15 +40,35 @@ use CoreLibs\Language\L10n; $string = 'INPUT TEST'; +echo "
LEGACY TEST
"; + +$lang = 'en_utf8'; $l = new CoreLibs\Language\L10n($lang); +echo "*
"; echo "LANGUAGE WANT/SET: " . $lang . '/' . $l->getLocale() . "
"; echo "LANGUAGE FILE: " . $l->getMoFile() . "
"; echo "LOAD ERROR: " . $l->getLoadError() . "
"; echo "INPUT TEST: " . $string . " => " . $l->__($string) . "
"; +$single_string = 'single'; +$multi_string = 'multi'; +for ($n = 0; $n <= 3; $n++) { + echo "MULTI TEST $n: " . $single_string . "/" . $multi_string . " => " + . $l->__n($single_string, $multi_string, $n) . "
"; +} +$context = "month name"; +$context_string = "May"; +echo "CONTEXT TRANSLATION: " . $context_string . " => " . $l->__p($context, $context_string) . "
"; +$single_string = 'single'; +$multi_string = 'multi'; +for ($n = 0; $n <= 3; $n++) { + echo "CONTEXT MULTI TEST $n: " . $single_string . "/" . $multi_string . " => " + . $l->__pn($context, $single_string, $multi_string, $n) . "
"; +} // switch to other language $lang = 'ja_utf8'; $l->l10nReloadMOfile($lang); +echo "*
"; echo "LANGUAGE WANT/SET: " . $lang . '/' . $l->getLocale() . "
"; echo "LANGUAGE FILE: " . $l->getMoFile() . "
"; echo "LOAD ERROR: " . $l->getLoadError() . "
"; @@ -56,6 +76,7 @@ echo "INPUT TEST: " . $string . " => " . $l->__($string) . "
"; // switch to non existing language $lang = 'tr_utf8'; $l->l10nReloadMOfile($lang); +echo "*
"; echo "LANGUAGE WANT/SET: " . $lang . '/' . $l->getLocale() . "
"; echo "LANGUAGE FILE: " . $l->getMoFile() . "
"; echo "LOAD ERROR: " . $l->getLoadError() . "
"; @@ -79,33 +100,64 @@ $encoding = 'UTF-8'; $path = BASE . INCLUDES . LOCALE; $l = new CoreLibs\Language\L10n($lang, $path, $domain, false); +echo "*
"; echo "LANGUAGE WANT/SET: " . $lang . '/' . $l->getLocale() . "
"; echo "DOMAIN WANT/SET: " . $domain . '/' . $l->getDomain() . "
"; echo "LANGUAGE FILE: " . $l->getMoFile() . "
"; +echo "CONTENT PATH: " . $l->getBaseContentPath() . "
"; +echo "DOMAIN PATH: " . $l->getTextDomain($domain) . "
"; +echo "BASE PATH: " . $l->getBaseLocalePath() . "
"; +echo "LOAD ERROR: " . $l->getLoadError() . "
"; +echo "INPUT TEST: " . $string . " => " . $l->__($string) . "
"; +echo "TROUGH LOAD: " . $l->getTranslatorClass()->gettext($string) . "
"; +$single_string = 'single'; +$multi_string = 'multi'; +for ($n = 0; $n <= 3; $n++) { + echo "MULTI TEST $n: " . $single_string . "/" . $multi_string . " => " + . $l->__n($single_string, $multi_string, $n) . "
"; +} + +$domain = 'frontend'; +$l->getTranslator('', $path, $domain); +echo "*
"; +echo "LANGUAGE WANT/SET: " . $lang . '/' . $l->getLocale() . "
"; +echo "DOMAIN WANT/SET: " . $domain . '/' . $l->getDomain() . "
"; +echo "LANGUAGE FILE: " . $l->getMoFile() . "
"; +echo "CONTENT PATH: " . $l->getBaseContentPath() . "
"; +echo "DOMAIN PATH: " . $l->getTextDomain($domain) . "
"; +echo "BASE PATH: " . $l->getBaseLocalePath() . "
"; echo "LOAD ERROR: " . $l->getLoadError() . "
"; echo "INPUT TEST: " . $string . " => " . $l->__($string) . "
"; echo "TROUGH LOAD: " . $l->getTranslatorClass()->gettext($string) . "
"; - -echo "
STATIC TYPE
"; +$domain = 'admin'; +echo "
STATIC TYPE TEST
"; // static tests from l10n_load L10n::getInstance()->setLocale($lang); echo "SET LOCALE: " . L10n::getInstance()->getLocale() . "
"; L10n::getInstance()->setDomain($domain); echo "SET DOMAIN: " . L10n::getInstance()->getDomain() . "
"; L10n::getInstance()->setTextDomain($domain, $path); -echo "SET DOMAIN: " . L10n::getInstance()->getTextDomain($domain) . "
"; +echo "SET TEXT DOMAIN: " . L10n::getInstance()->getTextDomain($domain) . "
"; // null call __bind_textdomain_codeset echo "INPUT TEST: " . $string . " => " . L10n::getInstance()->getTranslator()->gettext($string) . "
"; echo "
FUNCTIONS
"; // real statisc test L10n::loadFunctions(); -__setlocale(LC_MESSAGES, $lang); +$locale = 'ja'; +__setlocale(LC_MESSAGES, $locale); __textdomain($domain); __bindtextdomain($domain, $path); __bind_textdomain_codeset($domain, $encoding); -echo "INPUT TEST: " . $string . " => " . __($string) . "
"; +echo "INPUT TEST $locale: " . $string . " => " . __($string) . "
"; + +$locale = 'en_US.UTF-8'; +__setlocale(LC_MESSAGES, $locale); +__textdomain($domain); +__bindtextdomain($domain, $path); +__bind_textdomain_codeset($domain, $encoding); +echo "INPUT TEST $locale: " . $string . " => " . __($string) . "
"; print ""; diff --git a/www/includes/locale/en_US/LC_MESSAGES/admin.mo b/www/includes/locale/en_US/LC_MESSAGES/admin.mo index da820bb0fb59fb00c72177fd2b338eb80a635747..6407f2e08176a1345112b0198356d1fb78734158 100644 GIT binary patch delta 581 zcmZ{fO-lk%6o&8ESQeFJpP38LWP)tU~&;HBLQE}M zw2szoTl+Km2SKgc_TCv63VGn=IbZkX+!>pmjAa?~*#VLzGz6#&Z^1?I9#n>p zU=n-=P4EpYfxjTU!ccqaZ7eF;Bse|D)yEPL*4dQmRa`;Wg1Ro3p8e1_cAbvwUXbLD z&`o~BhDxt3Tee_M&+fL_a1ktM%NZe?4Qsn`Qp@d=ao9AgmQ|@&E#l`+JJ{2_z|bgo z)#^!u%T|xKM3KTL!IyTHJ+^x;w{x|Q>v%gHCjAXiPg}C;73H!pP)5ACXocrBRD$vB3TS9jbr2 delta 241 zcmdnYag4S8o)F7a1|VPuVi_O~0b*_-?g3&D*a5^sK)e%(#enz>5OV?XV<2V);&(tS z0mNT`m<5P=85tPNfLI8K<$-)pCI$u#Ae{-MMS%1qAPv&D97qcTl`^aaGC&#+0%@Qg zFaS9Q1lS-HSOYVZ4N}WA@olV9Kv8~HYI2FLXNqoEYEf}!ex8*AR7?RPFnJwg@nlCP O_sKJuq9#i-*8u=ldm@Se diff --git a/www/includes/locale/ja/LC_MESSAGES/admin.mo b/www/includes/locale/ja/LC_MESSAGES/admin.mo index 6d2ebed8540ff65970503500923db8af7fa23d7e..76cd4a38ada9cbfe2321200ad6f2c8fcf3cb0b06 100644 GIT binary patch delta 658 zcmZwDPfG$p7zXfJ(TdB||JV{~C$Vsv8| z3U0U_p$9&}2KYvPz$WA`=!7l@V;#^9-LMBX!a*qa1)&R$K>V1%t&_1Lo5VyGER!o_ zip-KalrJ(V+pqBFDjzpog=4aRK82pFddFO4f7i5s)&lGYC!%qM delta 407 zcmXZXu}i{16u|L!`YY5${2|exAUZh24~3vXXcZhnMN}ju$W(Gjuy+55AC3JH;_52s zAZUAm(BL3$Elqu|-f{0f_a1lexMk!RntZy>t1#k3f|wIYqHS0h8+37tLHttp7$QHQ zgWq_DKN!JSK;#q?Xzyk41hdE#kL6rsB6&9UKuNt(t7=_sp?#>M`8{6oSRb?0-)Lg{ zEYuZ7$=8~HV4i%3_OmX{g3{*l-$vv@&0&}Wml(rqt(VoB+Ej1VJGG1U=L5$dnxYYZ lY8L6rcr@&FU%b+T*XWI(A4gAxl-5%Jh1o0+H-5o+{{`PvEgS#< diff --git a/www/lib/CoreLibs/Basic.php b/www/lib/CoreLibs/Basic.php index 20fbe1b7..1ff06b44 100644 --- a/www/lib/CoreLibs/Basic.php +++ b/www/lib/CoreLibs/Basic.php @@ -715,20 +715,20 @@ class Basic // *** ARRAY HANDLING END // [!!! DEPRECATED !!!] - // Moved to \CoreLibs\Language\Encoding + // Moved to \CoreLibs\Convert\MimeEncode /** * wrapper function for mb mime convert, for correct conversion with long strings * @param string $string string to encode * @param string $encoding target encoding * @return string encoded string - * @deprecated Use \CoreLibs\Language\Encoding::__mbMimeEncode() instead + * @deprecated Use \CoreLibs\Convert\MimeEncode::__mbMimeEncode() instead */ public static function __mbMimeEncode(string $string, string $encoding): string { - trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Language\Encoding::__mbMimeEncode()', E_USER_DEPRECATED); - return \CoreLibs\Language\Encoding::__mbMimeEncode($string, $encoding); + trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Convert\MimeEncode::__mbMimeEncode()', E_USER_DEPRECATED); + return \CoreLibs\Convert\MimeEncode::__mbMimeEncode($string, $encoding); } // *** HUMAND BYTE READABLE CONVERT @@ -1009,7 +1009,7 @@ class Basic // *** ENCODING FUNCTIONS // [!!! DEPRECATED !!!] - // Moved to \CoreLibs\Language\Encoding + // Moved to \CoreLibs\Check\Encoding and \CoreLibs\Convert\Encoding /** * test if a string can be safely convert between encodings. mostly utf8 to shift jis @@ -1026,12 +1026,12 @@ class Basic * @param string $from_encoding encoding of string to test * @param string $to_encoding target encoding * @return bool|array false if no error or array with failed characters - * @deprecated use \CoreLibs\Language\Encoding::checkConvertEncoding() instead + * @deprecated use \CoreLibs\Check\Encoding::checkConvertEncoding() instead */ public function checkConvertEncoding(string $string, string $from_encoding, string $to_encoding) { - trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Language\Encoding::checkConvertEncoding()', E_USER_DEPRECATED); - return \CoreLibs\Language\Encoding::checkConvertEncoding($string, $from_encoding, $to_encoding); + trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Check\Encoding::checkConvertEncoding()', E_USER_DEPRECATED); + return \CoreLibs\Check\Encoding::checkConvertEncoding($string, $from_encoding, $to_encoding); } /** @@ -1047,12 +1047,12 @@ class Basic * check that the source is actually matching * to what we sav the source is * @return string encoding converted string - * @deprecated use \CoreLibs\Language\Encoding::convertEncoding() instead + * @deprecated use \CoreLibs\Convert\Encoding::convertEncoding() instead */ public static function convertEncoding(string $string, string $to_encoding, string $source_encoding = '', bool $auto_check = true): string { - trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Language\Encoding::convertEncoding()', E_USER_DEPRECATED); - return \CoreLibs\Language\Encoding::convertEncoding($string, $to_encoding, $source_encoding, $auto_check); + trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Convert\Encoding::convertEncoding()', E_USER_DEPRECATED); + return \CoreLibs\Convert\Encoding::convertEncoding($string, $to_encoding, $source_encoding, $auto_check); } // *** ENCODING FUNCTIONS END diff --git a/www/lib/CoreLibs/Language/Core/FileReader.php b/www/lib/CoreLibs/Language/Core/FileReader.php index 17bf0dce..cbee1b04 100644 --- a/www/lib/CoreLibs/Language/Core/FileReader.php +++ b/www/lib/CoreLibs/Language/Core/FileReader.php @@ -39,7 +39,7 @@ class FileReader * file read constructor * @param string $filename file name to load */ - public function __construct($filename) + public function __construct(string $filename) { if (file_exists($filename)) { $this->fr_length = filesize($filename) ?: 0; @@ -58,7 +58,7 @@ class FileReader * @param int $bytes how many bytes to read * @return string read data as string */ - public function read($bytes) + public function read(int $bytes): string { if (!$bytes || !is_resource($this->fr_fd)) { return ''; @@ -86,7 +86,7 @@ class FileReader * @param int $pos position where to go to * @return int file position after seek done */ - public function seekto($pos) + public function seekto(int $pos): int { if (!is_resource($this->fr_fd)) { return 0; @@ -100,7 +100,7 @@ class FileReader * get current position in file * @return int current position in bytes */ - public function currentpos() + public function currentpos(): int { return $this->fr_pos; } @@ -109,7 +109,7 @@ class FileReader * file length/size * @return int file size in bytes */ - public function length() + public function length(): int { return $this->fr_length; } diff --git a/www/lib/CoreLibs/Language/Core/GetTextReader.php b/www/lib/CoreLibs/Language/Core/GetTextReader.php index 550a1125..f15576e4 100644 --- a/www/lib/CoreLibs/Language/Core/GetTextReader.php +++ b/www/lib/CoreLibs/Language/Core/GetTextReader.php @@ -67,17 +67,15 @@ class GetTextReader /** @var array */ private $cache_translations = []; // original -> translation mapping - /* Methods */ - /** * Reads a 32bit Integer from the Stream * * @access private * @return int Integer from the Stream */ - private function readint() + private function readint(): int { if ($this->BYTEORDER == 0) { // low endian @@ -91,10 +89,11 @@ class GetTextReader /** * read bytes + * * @param int $bytes byte length to read * @return string return data, possible string */ - public function read($bytes) + public function read(int $bytes): string { return $this->STREAM->read($bytes); } @@ -105,7 +104,7 @@ class GetTextReader * @param int $count How many elements should be read * @return array Array of Integers */ - public function readintarray($count) + public function readintarray(int $count): array { if ($this->BYTEORDER == 0) { // low endian @@ -120,9 +119,10 @@ class GetTextReader * Constructor * * @param FileReader|bool $Reader the StreamReader object - * @param bool $enable_cache Enable or disable caching of strings (default on) + * @param bool $enable_cache Enable or disable caching + * of strings (default on) */ - public function __construct($Reader, $enable_cache = true) + public function __construct($Reader, bool $enable_cache = true) { // If there isn't a StreamReader, turn on short circuit mode. if ((!is_object($Reader) && !$Reader) || (is_object($Reader) && $Reader->error)) { @@ -159,6 +159,26 @@ class GetTextReader $this->translations = $this->readint(); } + /** + * Get current short circuit, equals to no translator running + * + * @return bool + */ + public function getShortCircuit(): bool + { + return $this->short_circuit; + } + + /** + * get the current cache enabled status + * + * @return bool + */ + public function getEnableCache(): bool + { + return $this->enable_cache; + } + /** * Loads the translation tables from the MO file into the cache * If caching is enabled, also loads all strings into a cache @@ -207,7 +227,7 @@ class GetTextReader * @param int $num Offset number of original string * @return string Requested string if found, otherwise '' */ - private function getOriginalString($num) + private function getOriginalString(int $num): string { $length = $this->table_originals[$num * 2 + 1] ?? 0; $offset = $this->table_originals[$num * 2 + 2] ?? 0; @@ -226,7 +246,7 @@ class GetTextReader * @param int $num Offset number of original string * @return string Requested string if found, otherwise '' */ - private function getTranslationString($num) + private function getTranslationString(int $num): string { $length = $this->table_translations[$num * 2 + 1] ?? 0; $offset = $this->table_translations[$num * 2 + 2] ?? 0; @@ -242,12 +262,12 @@ class GetTextReader * Binary search for string * * @access private - * @param string $string string to find - * @param int $start (internally used in recursive function) - * @param int $end (internally used in recursive function) - * @return int (offset in originals table) + * @param string $string string to find + * @param int $start (internally used in recursive function) + * @param int $end (internally used in recursive function) + * @return int (offset in originals table) */ - private function findString($string, $start = -1, $end = -1) + private function findString(string $string, int $start = -1, int $end = -1): int { if (($start == -1) or ($end == -1)) { // findString is called with only one parameter, set start end end @@ -289,7 +309,7 @@ class GetTextReader * @param string $string to be translated * @return string translated string (or original, if not found) */ - public function translate($string) + public function translate(string $string): string { if ($this->short_circuit) { return $string; @@ -298,7 +318,10 @@ class GetTextReader if ($this->enable_cache) { // Caching enabled, get translated string from cache - if (is_array($this->cache_translations) && array_key_exists($string, $this->cache_translations)) { + if ( + is_array($this->cache_translations) && + array_key_exists($string, $this->cache_translations) + ) { return $this->cache_translations[$string]; } else { return $string; @@ -321,7 +344,7 @@ class GetTextReader * @param string $expr an expression to match * @return string sanitized plural form expression */ - private function sanitizePluralExpression($expr) + private function sanitizePluralExpression(string $expr): string { // Get rid of disallowed characters. $expr = preg_replace('@[^a-zA-Z0-9_:;\(\)\?\|\&=!<>+*/\%-]@', '', $expr); @@ -358,7 +381,7 @@ class GetTextReader * @param string $header header search in plurals * @return string verbatim plural form header field */ - private function extractPluralFormsHeaderFromPoHeader($header) + private function extractPluralFormsHeaderFromPoHeader(string $header): string { if (preg_match("/(^|\n)plural-forms: ([^\n]*)\n/i", $header, $regs)) { $expr = $regs[2]; @@ -374,14 +397,14 @@ class GetTextReader * @access private * @return string plural form header */ - private function getPluralForms() + private function getPluralForms(): string { // lets assume message number 0 is header // this is true, right? $this->loadTables(); // cache header field for plural forms - if (! is_string($this->pluralheader)) { + if (empty($this->pluralheader) || !is_string($this->pluralheader)) { if ($this->enable_cache) { $header = $this->cache_translations['']; } else { @@ -397,25 +420,37 @@ class GetTextReader * Detects which plural form to take * * @access private - * @param string $n count - * @return int array index of the right plural form + * @param int $n count + * @return int array index of the right plural form */ - private function selectString($n) + private function selectString(int $n): int { $string = $this->getPluralForms(); $string = str_replace('nplurals', "\$total", $string); - $string = str_replace("n", $n, $string); + $string = str_replace("n", (string)$n, $string); $string = str_replace('plural', "\$plural", $string); $total = 0; $plural = 0; eval("$string"); - /** @phpstan-ignore-next-line */ + /** @phpstan-ignore-next-line 0 >= 0 is always true*/ if ($plural >= $total) { $plural = $total - 1; } - return $plural; + return (int)$plural; + } + + /** + * wrapper for translate() method + * + * @access public + * @param string $string + * @return string + */ + public function gettext(string $string): string + { + return $this->translate($string); } /** @@ -424,10 +459,10 @@ class GetTextReader * @access public * @param string $single * @param string $plural - * @param string $number + * @param int $number * @return string plural form */ - public function ngettext($single, $plural, $number) + public function ngettext(string $single, string $plural, int $number): string { if ($this->short_circuit) { if ($number != 1) { @@ -465,11 +500,12 @@ class GetTextReader /** * p get text + * * @param string $context [description] * @param string $msgid [description] * @return string [description] */ - public function pgettext($context, $msgid) + public function pgettext(string $context, string $msgid): string { $key = $context . chr(4) . $msgid; $ret = $this->translate($key); @@ -482,14 +518,19 @@ class GetTextReader /** * np get text + * * @param string $context [description] * @param string $singular [description] * @param string $plural [description] - * @param string $number [description] + * @param int $number [description] * @return string [description] */ - public function npgettext($context, $singular, $plural, $number) - { + public function npgettext( + string $context, + string $singular, + string $plural, + int $number + ): string { $key = $context . chr(4) . $singular; $ret = $this->ngettext($key, $plural, $number); if (strpos($ret, "\004") !== false) { diff --git a/www/lib/CoreLibs/Language/Core/StreamReader.php b/www/lib/CoreLibs/Language/Core/StreamReader.php index a02450e6..fdd87f0c 100644 --- a/www/lib/CoreLibs/Language/Core/StreamReader.php +++ b/www/lib/CoreLibs/Language/Core/StreamReader.php @@ -41,7 +41,7 @@ class StreamReader * @param int $bytes bytes to read * @return bool dummy false */ - public function read($bytes) + public function read(int $bytes): bool { return false; } @@ -51,7 +51,7 @@ class StreamReader * @param int $position seek to position * @return bool dummy false */ - public function seekto($position) + public function seekto(int $position): bool { return false; } @@ -60,7 +60,7 @@ class StreamReader * returns current position * @return bool dummy false */ - public function currentpos() + public function currentpos(): bool { return false; } @@ -69,7 +69,7 @@ class StreamReader * returns length of entire stream (limit for seekto()s) * @return bool dummy false */ - public function length() + public function length(): bool { return false; } diff --git a/www/lib/CoreLibs/Language/Core/StringReader.php b/www/lib/CoreLibs/Language/Core/StringReader.php index 4bf0a7d3..243795ea 100644 --- a/www/lib/CoreLibs/Language/Core/StringReader.php +++ b/www/lib/CoreLibs/Language/Core/StringReader.php @@ -35,7 +35,7 @@ class StringReader * constructor for string reader * @param string $str basic string */ - public function __construct($str = '') + public function __construct(string $str = '') { $this->sr_str = $str; $this->sr_pos = 0; @@ -46,7 +46,7 @@ class StringReader * @param int $bytes bytes to read in string * @return string data read in length of bytes as string */ - public function read($bytes) + public function read(int $bytes): string { $data = substr($this->sr_str, $this->sr_pos, $bytes); $this->sr_pos += $bytes; @@ -62,7 +62,7 @@ class StringReader * @param int $pos position in string * @return int new position in string after seek */ - public function seekto($pos) + public function seekto(int $pos): int { $this->sr_pos = $pos; if (strlen($this->sr_str) < $this->sr_pos) { @@ -75,7 +75,7 @@ class StringReader * get current position in string * @return int position in string */ - public function currentpos() + public function currentpos(): int { return $this->sr_pos; } @@ -84,7 +84,7 @@ class StringReader * get length of string * @return int return length of assigned string */ - public function length() + public function length(): int { return strlen($this->sr_str); } diff --git a/www/lib/CoreLibs/Language/L10n.php b/www/lib/CoreLibs/Language/L10n.php index 81667a27..6b37ab86 100644 --- a/www/lib/CoreLibs/Language/L10n.php +++ b/www/lib/CoreLibs/Language/L10n.php @@ -40,104 +40,530 @@ use CoreLibs\Language\Core\GetTextReader; class L10n { - /** @var string */ - private $lang = ''; - /** @var string */ + /** @var string the current locale */ + private $locale = ''; + /** @var string the default selected/active domain */ + private $domain = ''; + /** @var array> locale > domain = translator */ + private $domains = []; + /** @var array bound paths for domains */ + private $paths = ['' => './']; + /** @var string the full path to the mo file to loaded */ private $mofile = ''; - /** @var FileReader|bool */ - private $input; - /** @var GetTextReader */ + /** @var string base path to search level */ + private $base_locale_path = ''; + /** @var string dynamic set path to where the mo file is actually */ + private $base_content_path = ''; + /** @var bool if load of mo file was unsuccessful */ + private $load_failure = false; + + /** @var FileReader|bool reader class for file reading, false for short circuit */ + private $input = false; + /** @var GetTextReader reader class for MO data */ private $l10n; + /** + * @static + * @var L10n self class + */ + private static $instance; /** * class constructor call for language getstring - * @param string $lang language name (optional), fallback is en - * @param string $path path, if empty fallback on default internal path + * if locale is not empty will load translation + * else getTranslator needs to be called + * + * @param string $locale language name, default empty string + * will return self instance + * @param string $path path, if empty fallback on default internal path + * @param string $domain override CONTENT_PATH . $encoding name for mo file + * @param bool $legacy default true, if set to true, will look in the old + * folder format lang/ CONTENT_PATH / $lang . mo */ - public function __construct(string $lang = '', string $path = '') - { - if (!$lang) { - $this->lang = 'en'; - } else { - $this->lang = $lang; + public function __construct( + string $locale = '', + string $path = '', + string $domain = '', + bool $legacy = true + ) { + // load the mo file if locale is not empty + if (!empty($locale)) { + $this->getTranslator($locale, $path, $domain, $legacy); } - - // override path check - if (!is_dir($path)) { - $path = BASE . INCLUDES . LANG . CONTENT_PATH; - } - - $this->mofile = $path . $this->lang . ".mo"; - - // check if get a readable mofile - if (is_readable($this->mofile)) { - $this->input = new FileReader($this->mofile); - } else { - $this->input = false; - } - $this->l10n = new GetTextReader($this->input); } /** - * reloads the mofile, if the location of the lang file changes - * @param string $lang language to reload data - * @param string $path optional path, if not set fallback on internal - * @return bool successfull reload true/false + * Returns the singleton L10n object. + * For function wrapper use + * + * @return L10n object */ - public function l10nReloadMOfile(string $lang, string $path = ''): bool + public static function getInstance(): L10n { - $success = false; - $old_mofile = $this->mofile; - $old_lang = $this->lang; - - $this->lang = $lang; - - // override path check - if (!is_dir($path)) { - $path = BASE . INCLUDES . LANG . CONTENT_PATH; + /** @phpstan-ignore-next-line */ + if (empty(self::$instance)) { + self::$instance = new self(); } + return self::$instance; + } - $this->mofile = $path . $this->lang . ".mo"; + /** + * Loads global localization functions. + * prefixed with double underscore + * eg: gettext -> __gettext + */ + public static function loadFunctions(): void + { + require_once __DIR__ . '/l10n_functions.php'; + } + + /** + * legacy loader name for getTranslator + * instead of returning the GetTextReader object it returns + * true or false for successful load. + * NOTE: some time down the road this will be deprecated + * + * @param string $locale + * @param string $path + * @param string $domain + * @param bool $legacy + * @return bool Returns true for successfull load, false for error + */ + public function l10nReloadMOfile( + string $locale, + string $path = '', + string $domain = '', + bool $legacy = true + ): bool { + $this->getTranslator($locale, $path, $domain, $legacy); + return $this->load_failure ? false : true; + } + + /** + * loads the mo file base on path, locale and domain set + * + * @param string $locale language name (optional), fallback is en + * @param string $path path, if empty fallback on default internal path + * @param string $domain override CONTENT_PATH . $encoding name for mo file + * @param bool $legacy default true, if set to true, will look in the old + * folder format lang/ CONTENT_PATH / $lang . mo + * @return GetTextReader the main gettext reader object + */ + public function getTranslator( + string $locale = '', + string $path = '', + string $domain = '', + bool $legacy = false + ): GetTextReader { + // set local if not from parameter + if (empty($locale)) { + $locale = $this->locale; + } + // set domain if not given + if (empty($domain)) { + $domain = $this->domain; + } + // store old settings + $old_mofile = $this->mofile; + $old_lang = $this->locale; + $old_domain = $this->domain; + $old_base_locale_path = $this->base_locale_path; + $old_base_content_path = $this->base_content_path; + + // legacy or new type + // legacy will use the old lang/content/file.mo type as default + // if path is not set, also locale is the file name + // for new type it follows the gettext spec and path is just the + // base folder where the mo files will be searched + if ($legacy === true) { + if (!is_dir($path)) { + $this->base_locale_path = BASE . INCLUDES . LANG; + $this->base_content_path = CONTENT_PATH; + $path = $this->base_locale_path . $this->base_content_path; + } + $this->mofile = $path . $locale . ".mo"; + } else { + // if new path is a dir + // 1) from a previous set domain + // 2) from method option as is + // 3) fallback if BASE/INCLUDES/LOCALE set + // 4) current dir + if (!empty($this->paths[$domain]) && is_dir($this->paths[$domain])) { + $this->base_locale_path = $this->paths[$domain]; + } elseif (is_dir($path)) { + $this->base_locale_path = $path; + } elseif ( + defined('BASE') && defined('INCLUDES') && defined('LOCALE') + ) { + // set fallback base path if constant set + $this->base_locale_path = BASE . INCLUDES . LOCALE; + } else { + $this->base_locale_path = './'; + } + // now we loop over lang compositions to get the base path + // then we check + $locales = $this->listLocales($locale); + foreach ($locales as $_locale) { + $this->base_content_path = $_locale . DIRECTORY_SEPARATOR + . 'LC_MESSAGES' . DIRECTORY_SEPARATOR; + $this->mofile = $this->base_locale_path + . $this->base_content_path + . $domain . '.mo'; + if (file_exists($this->mofile)) { + break; + } + } + } // check if get a readable mofile if (is_readable($this->mofile)) { + // locale and domain current wanted + $this->locale = $locale; + $this->domain = $domain; + // set empty domains path with current locale + if (empty($this->domains[$locale])) { + $this->domains[$locale] = []; + } + // store current base path (without locale, etc) + if (empty($this->paths[$domain])) { + $this->paths[$domain] = $this->base_locale_path; + } + // file reader and mo reader $this->input = new FileReader($this->mofile); $this->l10n = new GetTextReader($this->input); - // we successfully loaded - $success = true; - } else { + // if short circuit is true, we failed to have a translator loaded + $this->load_failure = $this->l10n->getShortCircuit(); + // below is not used at the moment, but can be to avoid reloading + $this->domains[$this->locale][$domain] = $this->l10n; + } elseif (!empty($old_mofile)) { + // mo file not readable + $this->load_failure = true; // else fall back to the old ones $this->mofile = $old_mofile; - $this->lang = $old_lang; + $this->locale = $old_lang; + $this->domain = $old_domain; + $this->base_locale_path = $old_base_locale_path; + $this->base_content_path = $old_base_content_path; + } else { + // mo file not readable, no previous mo file set, set short circuit + $this->load_failure = true; + // dummy + $this->l10n = new GetTextReader($this->input); } - return $success; + return $this->l10n; } - /** + /** + * return current set GetTextReader or return the one for given + * domain name if set + * This can be used to access all the public methods from the + * GetTextReader + * + * @param string $domain optional domain name + * @return GetTextReader + */ + public function getTranslatorClass(string $domain = ''): GetTextReader + { + if (!empty($domain) && !empty($this->domains[$this->locale][$domain])) { + return $this->domains[$this->locale][$domain]; + } + // if null return short circuit version + if ($this->l10n === null) { + return new GetTextReader($this->input); + } + return $this->l10n; + } + + /** + * original: + * vendor/phpmyadmin/motranslator/src/Loader.php + * + * Returns array with all possible locale combinations based on the + * given locale name + * + * I.e. for sr_CS.UTF-8@latin, look through all of + * sr_CS.UTF-8@latin, sr_CS@latin, sr@latin, sr_CS.UTF-8, sr_CS, sr. + * + * @param string $locale Locale string + * @return array List of locale path parts that can be possible + */ + public static function listLocales(string $locale): array + { + $locale_list = []; + + if (empty($locale)) { + return $locale_list; + } + // is matching regex + if ( + !preg_match( + // language code + '/^(?P[a-z]{2,3})' + // country code + . '(?:_(?P[A-Z]{2}))?' + // charset + . '(?:\\.(?P[-A-Za-z0-9_]+))?' + // @ modifier + . '(?:@(?P[-A-Za-z0-9_]+))?$/', + $locale, + $matches + ) + ) { + // not matching, return as is + return [$locale]; + } + // do matching run + $lang = $matches['lang'] ?? null; + $country = $matches['country'] ?? null; + $charset = $matches['charset'] ?? null; + $modifier = $matches['modifier'] ?? null; + // we need to add all possible cominations from not null set + // entries to the list, from longest to shortest + // %s_%s.%s@%s (lang _ country . encoding @ suffix) + // %s_%s@%s (lang _ country @ suffix) + // %s@%s (lang @ suffix) + // %s_%s.%s (lang _ country . encoding) + // %s_%s (lang _ country) + // %s (lang) + + // if lang is set + if ($lang) { + // modifier group + if ($modifier) { + if ($country) { + if ($charset) { + array_push( + $locale_list, + sprintf('%s_%s.%s@%s', $lang, $country, $charset, $modifier) + ); + } + + array_push( + $locale_list, + sprintf('%s_%s@%s', $lang, $country, $modifier) + ); + } elseif ($charset) { + array_push( + $locale_list, + sprintf('%s.%s@%s', $lang, $charset, $modifier) + ); + } + + array_push( + $locale_list, + sprintf('%s@%s', $lang, $modifier) + ); + } + // country group + if ($country) { + if ($charset) { + array_push( + $locale_list, + sprintf('%s_%s.%s', $lang, $country, $charset) + ); + } + + array_push( + $locale_list, + sprintf('%s_%s', $lang, $country) + ); + } elseif ($charset) { + array_push( + $locale_list, + sprintf('%s.%s', $lang, $charset) + ); + } + // lang only + array_push($locale_list, $lang); + } + + // If the locale name doesn't match POSIX style, just include it as-is. + if (!in_array($locale, $locale_list)) { + array_push($locale_list, $locale); + } + + return $locale_list; + } + + /** + * tries to detect the locale set in the following order: + * - globals: LOCALE + * - globals: LANG + * - env: LC_ALL + * - env: LC_MESSAGES + * - env: LANG + * if nothing set, returns 'en' as default + * + * @return string + */ + public static function detectLocale(): string + { + // globals + foreach (['LOCALE', 'LANG'] as $global) { + if (!empty($GLOBALS[$global])) { + return $GLOBALS[$global]; + } + } + // enviroment + foreach (['LC_ALL', 'LC_MESSAGES', 'LANG'] as $env) { + $locale = getenv($env); + if ($locale !== false && !empty($locale)) { + return $locale; + } + } + return 'en'; + } + + /************ + * INTERNAL VAR SET/GET + */ + + /** + * Sets the path for a domain. + * must be set before running l10nReloadMOfile + * + * @param string $domain Domain name + * @param string $path Path where to find locales + */ + public function setTextDomain(string $domain, string $path): void + { + $this->paths[$domain] = $path; + } + + /** + * return set path for given domain + * if not found return false + * + * @param string $domain + * @return string|bool + */ + public function getTextDomain(string $domain) + { + return $this->paths[$domain] ?? false; + } + + /** + * sets the default domain. + * + * @param string $domain Domain name + */ + public function setDomain(string $domain): void + { + $this->domain = $domain; + } + + /** + * return current set domain name + * + * @return string + */ + public function getDomain(): string + { + return $this->domain; + } + + /** + * sets a requested locale. + * + * @param string $locale Locale name + * @return string Set or current locale + */ + public function setLocale(string $locale): string + { + if (!empty($locale)) { + $this->locale = $locale; + } + return $this->locale; + } + + /** + * get current set locale + * + * @return string + */ + public function getLocale(): string + { + return $this->locale; + } + + /** * get current set language + * * @return string current set language string + * @deprecated Use getLocale() */ public function __getLang(): string { - return $this->lang; + return $this->getLocale(); } /** * get current set mo file + * * @return string current set mo language file */ - public function __getMoFile(): string + public function getMoFile(): string { return $this->mofile; } + /** + * get current set mo file + * + * @return string current set mo language file + * @deprecated Use getMoFile() + */ + public function __getMoFile(): string + { + return $this->getMoFile(); + } + + /** + * get the current base path in which we search + * + * @return string + */ + public function getBaseLocalePath(): string + { + return $this->base_locale_path; + } + + /** + * the path below the base path to where the mo file is located + * + * @return string + */ + public function getBaseContentPath(): string + { + return $this->base_content_path; + } + + /** + * get the current load error status + * if true then the mo file failed to load + * + * @return bool + */ + public function getLoadError(): bool + { + return $this->load_failure; + } + + /************ + * TRANSLATION METHODS + */ + /** * translates a string and returns translated text + * * @param string $text text to translate * @return string translated text */ public function __(string $text): string { + // fallback passthrough + if ($this->l10n === null) { + return $text; + } return $this->l10n->translate($text); } @@ -145,32 +571,75 @@ class L10n * prints translated string out to the screen * @param string $text text to translate * @return void has no return + * @deprecated use echo __() instead */ public function __e(string $text): void { + // fallback passthrough + if ($this->l10n === null) { + echo $text; + } echo $this->l10n->translate($text); } - // Return the plural form. /** * Return the plural form. - * @param string $single string for single word - * @param string $plural string for plural word - * @param int|float $number number value - * @return string translated plural string + * + * @param string $single string for single word + * @param string $plural string for plural word + * @param int $number number value + * @return string translated plural string */ - public function __n(string $single, string $plural, $number): string + public function __n(string $single, string $plural, int $number): string { + // in case nothing got set yet, this is fallback + if ($this->l10n === null) { + return $number > 1 ? $plural : $single; + } return $this->l10n->ngettext($single, $plural, $number); } + /** + * context translation via msgctxt + * + * @param string $context context string + * @param string $text text to translate + * @return string + */ + public function __p(string $context, string $text): string + { + if ($this->l10n === null) { + return $text; + } + return $this->l10n->pgettext($context, $text); + } + + /** + * context translation via msgctxt + * + * @param string $context context string + * @param string $single string for single word + * @param string $plural string for plural word + * @param int $number number value + * @return string + */ + public function __pn(string $context, string $single, string $plural, int $number): string + { + if ($this->l10n === null) { + return $number > 1 ? $plural : $single; + } + return $this->l10n->npgettext($context, $single, $plural, $number); + } + // alias functions to mimic gettext calls /** - * Undocumented function + * alias for gettext, + * calls __ * * @param string $text * @return string + * @deprecated Use __() */ public function gettext(string $text): string { @@ -178,22 +647,24 @@ class L10n } /** - * Undocumented function + * alias for ngettext + * calls __n * * @param string $single * @param string $plural - * @param int|float $number + * @param int $number * @return string + * @deprecated Use __n() */ - public function ngettext(string $single, string $plural, $number): string + public function ngettext(string $single, string $plural, int $number): string { return $this->__n($single, $plural, $number); } // TODO: dgettext(string $domain, string $message): string // TODO: dngettext(string $domain, string $singular, string $plural, int $count): string - // TODO: dcgettext(string $domain, string $message, int $category): string - // TODO: dcngettext(string $domain, string $singular, string $plural, int $count, int $category): string + // TODO: dpgettext(string $domain, string $message, int $category): string + // TODO: dpngettext(string $domain, string $singular, string $plural, int $count, int $category): string } // __END__ diff --git a/www/lib/CoreLibs/Language/l10n_functions.php b/www/lib/CoreLibs/Language/l10n_functions.php new file mode 100644 index 00000000..93a3188c --- /dev/null +++ b/www/lib/CoreLibs/Language/l10n_functions.php @@ -0,0 +1,208 @@ +setLocale($locale); +} + +/** + * Sets the path for a domain. + * + * @param string $domain Domain name + * @param string $path Path where to find locales + */ +function __bindtextdomain(string $domain, string $path): void +{ + L10n::getInstance()->setTextDomain($domain, $path); +} + +/** + * Dummy compatibility function, MoTranslator assumes + * everything is using same character set on input and + * output. + * + * Generally it is wise to output in UTF-8 and have + * mo files in UTF-8. + * + * @param string $domain Domain where to set character set + * @param string $codeset Character set to set + */ +function __bind_textdomain_codeset(string $domain, string $codeset): void +{ +} + +/** + * Sets the default domain. + * + * @param string $domain Domain name + */ +function __textdomain(string $domain): void +{ + L10n::getInstance()->setDomain($domain); +} + +/** + * Translates a string. + * + * @param string $msgid String to be translated + * + * @return string translated string (or original, if not found) + */ +function __gettext(string $msgid): string +{ + return L10n::getInstance()->getTranslator()->gettext( + $msgid + ); +} + +/** + * Translates a string, alias for _gettext. + * + * @param string $msgid String to be translated + * + * @return string translated string (or original, if not found) + */ +function __(string $msgid): string +{ + return L10n::getInstance()->getTranslator()->gettext( + $msgid + ); +} + +/** + * Plural version of gettext. + * + * @param string $msgid Single form + * @param string $msgidPlural Plural form + * @param int $number Number of objects + * + * @return string translated plural form + */ +function __ngettext(string $msgid, string $msgidPlural, int $number): string +{ + return L10n::getInstance()->getTranslator()->ngettext( + $msgid, + $msgidPlural, + $number + ); +} + +/** + * Translate with context. + * + * @param string $msgctxt Context + * @param string $msgid String to be translated + * + * @return string translated plural form + */ +function __pgettext(string $msgctxt, string $msgid): string +{ + return L10n::getInstance()->getTranslator()->pgettext( + $msgctxt, + $msgid + ); +} + +/** + * Plural version of pgettext. + * + * @param string $msgctxt Context + * @param string $msgid Single form + * @param string $msgidPlural Plural form + * @param int $number Number of objects + * + * @return string translated plural form + */ +function __npgettext(string $msgctxt, string $msgid, string $msgidPlural, int $number): string +{ + return L10n::getInstance()->getTranslator()->npgettext( + $msgctxt, + $msgid, + $msgidPlural, + $number + ); +} + +/** + * Translates a string. + * + * @param string $domain Domain to use + * @param string $msgid String to be translated + * + * @return string translated string (or original, if not found) + */ +function __dgettext(string $domain, string $msgid): string +{ + return L10n::getInstance()->getTranslator('', '', $domain)->gettext( + $msgid + ); +} + +/** + * Plural version of gettext. + * + * @param string $domain Domain to use + * @param string $msgid Single form + * @param string $msgidPlural Plural form + * @param int $number Number of objects + * + * @return string translated plural form + */ +function __dngettext(string $domain, string $msgid, string $msgidPlural, int $number): string +{ + return L10n::getInstance()->getTranslator('', '', $domain)->ngettext( + $msgid, + $msgidPlural, + $number + ); +} + +/** + * Translate with context. + * + * @param string $domain Domain to use + * @param string $msgctxt Context + * @param string $msgid String to be translated + * + * @return string translated plural form + */ +function __dpgettext(string $domain, string $msgctxt, string $msgid): string +{ + return L10n::getInstance()->getTranslator('', '', $domain)->pgettext( + $msgctxt, + $msgid + ); +} + +/** + * Plural version of pgettext. + * + * @param string $domain Domain to use + * @param string $msgctxt Context + * @param string $msgid Single form + * @param string $msgidPlural Plural form + * @param int $number Number of objects + * + * @return string translated plural form + */ +function __dnpgettext(string $domain, string $msgctxt, string $msgid, string $msgidPlural, int $number): string +{ + return L10n::getInstance()->getTranslator('', '', $domain)->npgettext( + $msgctxt, + $msgid, + $msgidPlural, + $number + ); +}