1a25f0a04SGreg Roach<?php 23976b470SGreg Roach 3a25f0a04SGreg Roach/** 4a25f0a04SGreg Roach * webtrees: online genealogy 589f7189bSGreg Roach * Copyright (C) 2021 webtrees development team 6a25f0a04SGreg Roach * This program is free software: you can redistribute it and/or modify 7a25f0a04SGreg Roach * it under the terms of the GNU General Public License as published by 8a25f0a04SGreg Roach * the Free Software Foundation, either version 3 of the License, or 9a25f0a04SGreg Roach * (at your option) any later version. 10a25f0a04SGreg Roach * This program is distributed in the hope that it will be useful, 11a25f0a04SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 12a25f0a04SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13a25f0a04SGreg Roach * GNU General Public License for more details. 14a25f0a04SGreg Roach * You should have received a copy of the GNU General Public License 1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>. 16a25f0a04SGreg Roach */ 17fcfa147eSGreg Roach 18e7f56f2aSGreg Roachdeclare(strict_types=1); 19e7f56f2aSGreg Roach 2076692c8bSGreg Roachnamespace Fisharebest\Webtrees; 21a25f0a04SGreg Roach 2237646143SGreg Roachuse Closure; 23991b93ddSGreg Roachuse Collator; 24f1af7e1cSGreg Roachuse Exception; 25c999a340SGreg Roachuse Fisharebest\Localization\Locale; 261e71bdc0SGreg Roachuse Fisharebest\Localization\Locale\LocaleEnUs; 2715834aaeSGreg Roachuse Fisharebest\Localization\Locale\LocaleInterface; 283bdc890bSGreg Roachuse Fisharebest\Localization\Translation; 293bdc890bSGreg Roachuse Fisharebest\Localization\Translator; 30d37db671SGreg Roachuse Fisharebest\Webtrees\Module\ModuleCustomInterface; 3102086832SGreg Roachuse Fisharebest\Webtrees\Module\ModuleLanguageInterface; 32d37db671SGreg Roachuse Fisharebest\Webtrees\Services\ModuleService; 336cd97bf6SGreg Roachuse Illuminate\Support\Collection; 343976b470SGreg Roach 354f194b97SGreg Roachuse function array_merge; 36d68ee7a8SGreg Roachuse function class_exists; 37d68ee7a8SGreg Roachuse function html_entity_decode; 38d68ee7a8SGreg Roachuse function in_array; 39d68ee7a8SGreg Roachuse function mb_strtolower; 40d68ee7a8SGreg Roachuse function mb_strtoupper; 41d68ee7a8SGreg Roachuse function mb_substr; 42d68ee7a8SGreg Roachuse function ord; 43d68ee7a8SGreg Roachuse function sprintf; 44dec352c1SGreg Roachuse function str_contains; 45d68ee7a8SGreg Roachuse function str_replace; 46d68ee7a8SGreg Roachuse function strcmp; 47d68ee7a8SGreg Roachuse function strip_tags; 48d68ee7a8SGreg Roachuse function strlen; 49d68ee7a8SGreg Roachuse function strtr; 50b0fcccb0SGreg Roachuse function var_export; 51a25f0a04SGreg Roach 52a25f0a04SGreg Roach/** 5376692c8bSGreg Roach * Internationalization (i18n) and localization (l10n). 54a25f0a04SGreg Roach */ 55c1010edaSGreg Roachclass I18N 56c1010edaSGreg Roach{ 57d37db671SGreg Roach // MO files use special characters for plurals and context. 584f194b97SGreg Roach public const PLURAL = "\x00"; 594f194b97SGreg Roach public const CONTEXT = "\x04"; 606fcafd02SGreg Roach 616fcafd02SGreg Roach // Digits are always rendered LTR, even in RTL text. 6216d6367aSGreg Roach private const DIGITS = '0123456789٠١٢٣٤٥٦٧٨٩۰۱۲۳۴۵۶۷۸۹'; 636fcafd02SGreg Roach 646fcafd02SGreg Roach // These locales need special handling for the dotless letter I. 6516d6367aSGreg Roach private const DOTLESS_I_LOCALES = [ 66c1010edaSGreg Roach 'az', 67c1010edaSGreg Roach 'tr', 68c1010edaSGreg Roach ]; 696fcafd02SGreg Roach 7016d6367aSGreg Roach private const DOTLESS_I_TOLOWER = [ 71c1010edaSGreg Roach 'I' => 'ı', 72c1010edaSGreg Roach 'İ' => 'i', 73c1010edaSGreg Roach ]; 74006094b9SGreg Roach 7516d6367aSGreg Roach private const DOTLESS_I_TOUPPER = [ 76c1010edaSGreg Roach 'ı' => 'I', 77c1010edaSGreg Roach 'i' => 'İ', 78c1010edaSGreg Roach ]; 79a25f0a04SGreg Roach 806fcafd02SGreg Roach // The ranges of characters used by each script. 8116d6367aSGreg Roach private const SCRIPT_CHARACTER_RANGES = [ 82c1010edaSGreg Roach [ 83c1010edaSGreg Roach 'Latn', 84c1010edaSGreg Roach 0x0041, 85c1010edaSGreg Roach 0x005A, 86c1010edaSGreg Roach ], 87c1010edaSGreg Roach [ 88c1010edaSGreg Roach 'Latn', 89c1010edaSGreg Roach 0x0061, 90c1010edaSGreg Roach 0x007A, 91c1010edaSGreg Roach ], 92c1010edaSGreg Roach [ 93c1010edaSGreg Roach 'Latn', 94c1010edaSGreg Roach 0x0100, 95c1010edaSGreg Roach 0x02AF, 96c1010edaSGreg Roach ], 97c1010edaSGreg Roach [ 98c1010edaSGreg Roach 'Grek', 99c1010edaSGreg Roach 0x0370, 100c1010edaSGreg Roach 0x03FF, 101c1010edaSGreg Roach ], 102c1010edaSGreg Roach [ 103c1010edaSGreg Roach 'Cyrl', 104c1010edaSGreg Roach 0x0400, 105c1010edaSGreg Roach 0x052F, 106c1010edaSGreg Roach ], 107c1010edaSGreg Roach [ 108c1010edaSGreg Roach 'Hebr', 109c1010edaSGreg Roach 0x0590, 110c1010edaSGreg Roach 0x05FF, 111c1010edaSGreg Roach ], 112c1010edaSGreg Roach [ 113c1010edaSGreg Roach 'Arab', 114c1010edaSGreg Roach 0x0600, 115c1010edaSGreg Roach 0x06FF, 116c1010edaSGreg Roach ], 117c1010edaSGreg Roach [ 118c1010edaSGreg Roach 'Arab', 119c1010edaSGreg Roach 0x0750, 120c1010edaSGreg Roach 0x077F, 121c1010edaSGreg Roach ], 122c1010edaSGreg Roach [ 123c1010edaSGreg Roach 'Arab', 124c1010edaSGreg Roach 0x08A0, 125c1010edaSGreg Roach 0x08FF, 126c1010edaSGreg Roach ], 127c1010edaSGreg Roach [ 128c1010edaSGreg Roach 'Deva', 129c1010edaSGreg Roach 0x0900, 130c1010edaSGreg Roach 0x097F, 131c1010edaSGreg Roach ], 132c1010edaSGreg Roach [ 133c1010edaSGreg Roach 'Taml', 134c1010edaSGreg Roach 0x0B80, 135c1010edaSGreg Roach 0x0BFF, 136c1010edaSGreg Roach ], 137c1010edaSGreg Roach [ 138c1010edaSGreg Roach 'Sinh', 139c1010edaSGreg Roach 0x0D80, 140c1010edaSGreg Roach 0x0DFF, 141c1010edaSGreg Roach ], 142c1010edaSGreg Roach [ 143c1010edaSGreg Roach 'Thai', 144c1010edaSGreg Roach 0x0E00, 145c1010edaSGreg Roach 0x0E7F, 146c1010edaSGreg Roach ], 147c1010edaSGreg Roach [ 148c1010edaSGreg Roach 'Geor', 149c1010edaSGreg Roach 0x10A0, 150c1010edaSGreg Roach 0x10FF, 151c1010edaSGreg Roach ], 152c1010edaSGreg Roach [ 153c1010edaSGreg Roach 'Grek', 154c1010edaSGreg Roach 0x1F00, 155c1010edaSGreg Roach 0x1FFF, 156c1010edaSGreg Roach ], 157c1010edaSGreg Roach [ 158c1010edaSGreg Roach 'Deva', 159c1010edaSGreg Roach 0xA8E0, 160c1010edaSGreg Roach 0xA8FF, 161c1010edaSGreg Roach ], 162c1010edaSGreg Roach [ 163c1010edaSGreg Roach 'Hans', 164c1010edaSGreg Roach 0x3000, 165c1010edaSGreg Roach 0x303F, 166c1010edaSGreg Roach ], 167c1010edaSGreg Roach // Mixed CJK, not just Hans 168c1010edaSGreg Roach [ 169c1010edaSGreg Roach 'Hans', 170c1010edaSGreg Roach 0x3400, 171c1010edaSGreg Roach 0xFAFF, 172c1010edaSGreg Roach ], 173c1010edaSGreg Roach // Mixed CJK, not just Hans 174c1010edaSGreg Roach [ 175c1010edaSGreg Roach 'Hans', 176c1010edaSGreg Roach 0x20000, 177c1010edaSGreg Roach 0x2FA1F, 178c1010edaSGreg Roach ], 179c1010edaSGreg Roach // Mixed CJK, not just Hans 18013abd6f3SGreg Roach ]; 1816fcafd02SGreg Roach 1826fcafd02SGreg Roach // Characters that are displayed in mirror form in RTL text. 18316d6367aSGreg Roach private const MIRROR_CHARACTERS = [ 184a25f0a04SGreg Roach '(' => ')', 185a25f0a04SGreg Roach ')' => '(', 186a25f0a04SGreg Roach '[' => ']', 187a25f0a04SGreg Roach ']' => '[', 188a25f0a04SGreg Roach '{' => '}', 189a25f0a04SGreg Roach '}' => '{', 190a25f0a04SGreg Roach '<' => '>', 191a25f0a04SGreg Roach '>' => '<', 192a25f0a04SGreg Roach '‹ ' => '›', 193a25f0a04SGreg Roach '› ' => '‹', 194a25f0a04SGreg Roach '«' => '»', 195a25f0a04SGreg Roach '»' => '«', 196a25f0a04SGreg Roach '﴾ ' => '﴿', 197a25f0a04SGreg Roach '﴿ ' => '﴾', 198a25f0a04SGreg Roach '“ ' => '”', 199a25f0a04SGreg Roach '” ' => '“', 200a25f0a04SGreg Roach '‘ ' => '’', 201a25f0a04SGreg Roach '’ ' => '‘', 20213abd6f3SGreg Roach ]; 203a25f0a04SGreg Roach 2046fcafd02SGreg Roach // Punctuation used to separate list items, typically a comma 2056fcafd02SGreg Roach public static string $list_separator; 206006094b9SGreg Roach 2076fcafd02SGreg Roach private static ?ModuleLanguageInterface $language; 2086fcafd02SGreg Roach 2096fcafd02SGreg Roach private static LocaleInterface $locale; 2106fcafd02SGreg Roach 2116fcafd02SGreg Roach private static Translator $translator; 2126fcafd02SGreg Roach 2136fcafd02SGreg Roach private static ?Collator $collator; 214006094b9SGreg Roach 215a25f0a04SGreg Roach /** 21602086832SGreg Roach * The preferred locales for this site, or a default list if no preference. 217dfeee0a8SGreg Roach * 218dfeee0a8SGreg Roach * @return LocaleInterface[] 219dfeee0a8SGreg Roach */ 2208f53f488SRico Sonntag public static function activeLocales(): array 221c1010edaSGreg Roach { 222006094b9SGreg Roach /** @var Collection $locales */ 22302086832SGreg Roach $locales = app(ModuleService::class) 224d6137952SGreg Roach ->findByInterface(ModuleLanguageInterface::class, false, true) 2250b5fd0a6SGreg Roach ->map(static function (ModuleLanguageInterface $module): LocaleInterface { 22602086832SGreg Roach return $module->locale(); 22702086832SGreg Roach }); 228dfeee0a8SGreg Roach 22902086832SGreg Roach if ($locales->isEmpty()) { 23002086832SGreg Roach return [new LocaleEnUs()]; 231dfeee0a8SGreg Roach } 232dfeee0a8SGreg Roach 23302086832SGreg Roach return $locales->all(); 234dfeee0a8SGreg Roach } 235dfeee0a8SGreg Roach 236dfeee0a8SGreg Roach /** 237dfeee0a8SGreg Roach * Which MySQL collation should be used for this locale? 238dfeee0a8SGreg Roach * 239dfeee0a8SGreg Roach * @return string 240dfeee0a8SGreg Roach */ 241e364afe4SGreg Roach public static function collation(): string 242c1010edaSGreg Roach { 243dfeee0a8SGreg Roach $collation = self::$locale->collation(); 244dfeee0a8SGreg Roach switch ($collation) { 245dfeee0a8SGreg Roach case 'croatian_ci': 246dfeee0a8SGreg Roach case 'german2_ci': 247dfeee0a8SGreg Roach case 'vietnamese_ci': 248dfeee0a8SGreg Roach // Only available in MySQL 5.6 249dfeee0a8SGreg Roach return 'utf8_unicode_ci'; 250dfeee0a8SGreg Roach default: 251dfeee0a8SGreg Roach return 'utf8_' . $collation; 252dfeee0a8SGreg Roach } 253dfeee0a8SGreg Roach } 254dfeee0a8SGreg Roach 255dfeee0a8SGreg Roach /** 256dfeee0a8SGreg Roach * What format is used to display dates in the current locale? 257dfeee0a8SGreg Roach * 258dfeee0a8SGreg Roach * @return string 259dfeee0a8SGreg Roach */ 2608f53f488SRico Sonntag public static function dateFormat(): string 261c1010edaSGreg Roach { 262bbb76c12SGreg Roach /* I18N: This is the format string for full dates. See http://php.net/date for codes */ 263bbb76c12SGreg Roach return self::$translator->translate('%j %F %Y'); 264dfeee0a8SGreg Roach } 265dfeee0a8SGreg Roach 266dfeee0a8SGreg Roach /** 267dfeee0a8SGreg Roach * Convert the digits 0-9 into the local script 268dfeee0a8SGreg Roach * Used for years, etc., where we do not want thousands-separators, decimals, etc. 269dfeee0a8SGreg Roach * 27055664801SGreg Roach * @param string|int $n 271dfeee0a8SGreg Roach * 272dfeee0a8SGreg Roach * @return string 273dfeee0a8SGreg Roach */ 2748f53f488SRico Sonntag public static function digits($n): string 275c1010edaSGreg Roach { 27655664801SGreg Roach return self::$locale->digits((string) $n); 277dfeee0a8SGreg Roach } 278dfeee0a8SGreg Roach 279dfeee0a8SGreg Roach /** 280dfeee0a8SGreg Roach * What is the direction of the current locale 281dfeee0a8SGreg Roach * 282dfeee0a8SGreg Roach * @return string "ltr" or "rtl" 283dfeee0a8SGreg Roach */ 2848f53f488SRico Sonntag public static function direction(): string 285c1010edaSGreg Roach { 286dfeee0a8SGreg Roach return self::$locale->direction(); 287dfeee0a8SGreg Roach } 288dfeee0a8SGreg Roach 289dfeee0a8SGreg Roach /** 290a25f0a04SGreg Roach * Initialise the translation adapter with a locale setting. 291a25f0a04SGreg Roach * 292150f35adSGreg Roach * @param string $code 293150f35adSGreg Roach * @param bool $setup 294a25f0a04SGreg Roach * 295150f35adSGreg Roach * @return void 296a25f0a04SGreg Roach */ 297150f35adSGreg Roach public static function init(string $code, bool $setup = false): void 298c1010edaSGreg Roach { 2993bdc890bSGreg Roach self::$locale = Locale::create($code); 3003bdc890bSGreg Roach 3014f194b97SGreg Roach // Load the translation file 302150f35adSGreg Roach $translation_file = __DIR__ . '/../resources/lang/' . self::$locale->languageTag() . '/messages.php'; 3034f194b97SGreg Roach 304f1af7e1cSGreg Roach try { 305006094b9SGreg Roach $translation = new Translation($translation_file); 306006094b9SGreg Roach $translations = $translation->asArray(); 307f1af7e1cSGreg Roach } catch (Exception $ex) { 308006094b9SGreg Roach // The translations files are created during the build process, and are 309006094b9SGreg Roach // not included in the source code. 310006094b9SGreg Roach // Assuming we are using dev code, and build (or rebuild) the files. 311006094b9SGreg Roach $po_file = Webtrees::ROOT_DIR . 'resources/lang/' . self::$locale->languageTag() . '/messages.po'; 312006094b9SGreg Roach $translation = new Translation($po_file); 313006094b9SGreg Roach $translations = $translation->asArray(); 314b0fcccb0SGreg Roach file_put_contents($translation_file, "<?php\n\nreturn " . var_export($translations, true) . ";\n"); 315a25f0a04SGreg Roach } 316a25f0a04SGreg Roach 3174f194b97SGreg Roach // Add translations from custom modules (but not during setup, as we have no database/modules) 318c116a5ccSGreg Roach if (!$setup) { 3196fcafd02SGreg Roach $module_service = app(ModuleService::class); 3206fcafd02SGreg Roach 3216fcafd02SGreg Roach $translations = $module_service 3224f194b97SGreg Roach ->findByInterface(ModuleCustomInterface::class) 32369253da9SGreg Roach ->reduce(static function (array $carry, ModuleCustomInterface $item): array { 3244f194b97SGreg Roach return array_merge($carry, $item->customTranslations(self::$locale->languageTag())); 3254f194b97SGreg Roach }, $translations); 3266fcafd02SGreg Roach 3276fcafd02SGreg Roach self::$language = $module_service 328*32ed8ceeSGreg Roach ->findByInterface(ModuleLanguageInterface::class) 3296fcafd02SGreg Roach ->first(fn (ModuleLanguageInterface $module): bool => $module->locale()->languageTag() === $code); 330d37db671SGreg Roach } 331d37db671SGreg Roach 3323bdc890bSGreg Roach // Create a translator 3333bdc890bSGreg Roach self::$translator = new Translator($translations, self::$locale->pluralRule()); 334a25f0a04SGreg Roach 335bbb76c12SGreg Roach /* I18N: This punctuation is used to separate lists of items */ 336bbb76c12SGreg Roach self::$list_separator = self::translate(', '); 337a25f0a04SGreg Roach 338991b93ddSGreg Roach // Create a collator 339991b93ddSGreg Roach try { 340444a65ecSGreg Roach if (class_exists('Collator')) { 341c9ec599fSGreg Roach // Symfony provides a very incomplete polyfill - which cannot be used. 342991b93ddSGreg Roach self::$collator = new Collator(self::$locale->code()); 343991b93ddSGreg Roach // Ignore upper/lower case differences 344991b93ddSGreg Roach self::$collator->setStrength(Collator::SECONDARY); 345444a65ecSGreg Roach } 346991b93ddSGreg Roach } catch (Exception $ex) { 347991b93ddSGreg Roach // PHP-INTL is not installed? We'll use a fallback later. 348c9ec599fSGreg Roach self::$collator = null; 349991b93ddSGreg Roach } 350a25f0a04SGreg Roach } 351a25f0a04SGreg Roach 352a25f0a04SGreg Roach /** 353006094b9SGreg Roach * Translate a string, and then substitute placeholders 354006094b9SGreg Roach * echo I18N::translate('Hello World!'); 355006094b9SGreg Roach * echo I18N::translate('The %s sat on the mat', 'cat'); 356006094b9SGreg Roach * 357006094b9SGreg Roach * @param string $message 358006094b9SGreg Roach * @param string ...$args 359006094b9SGreg Roach * 360006094b9SGreg Roach * @return string 361006094b9SGreg Roach */ 362006094b9SGreg Roach public static function translate(string $message, ...$args): string 363006094b9SGreg Roach { 364006094b9SGreg Roach $message = self::$translator->translate($message); 365006094b9SGreg Roach 366006094b9SGreg Roach return sprintf($message, ...$args); 367006094b9SGreg Roach } 368006094b9SGreg Roach 369006094b9SGreg Roach /** 37090a2f718SGreg Roach * @return string 37190a2f718SGreg Roach */ 37290a2f718SGreg Roach public static function languageTag(): string 37390a2f718SGreg Roach { 37490a2f718SGreg Roach return self::$locale->languageTag(); 37590a2f718SGreg Roach } 37690a2f718SGreg Roach 37790a2f718SGreg Roach /** 37865cf5706SGreg Roach * @return LocaleInterface 37965cf5706SGreg Roach */ 38065cf5706SGreg Roach public static function locale(): LocaleInterface 38165cf5706SGreg Roach { 38265cf5706SGreg Roach return self::$locale; 38365cf5706SGreg Roach } 38465cf5706SGreg Roach 38565cf5706SGreg Roach /** 3866fcafd02SGreg Roach * @return ModuleLanguageInterface 3876fcafd02SGreg Roach */ 3886fcafd02SGreg Roach public static function language(): ModuleLanguageInterface 3896fcafd02SGreg Roach { 3906fcafd02SGreg Roach return self::$language; 3916fcafd02SGreg Roach } 3926fcafd02SGreg Roach 3936fcafd02SGreg Roach /** 394dfeee0a8SGreg Roach * Translate a number into the local representation. 395dfeee0a8SGreg Roach * e.g. 12345.67 becomes 396dfeee0a8SGreg Roach * en: 12,345.67 397dfeee0a8SGreg Roach * fr: 12 345,67 398dfeee0a8SGreg Roach * de: 12.345,67 399dfeee0a8SGreg Roach * 400dfeee0a8SGreg Roach * @param float $n 401cbc1590aSGreg Roach * @param int $precision 402a25f0a04SGreg Roach * 403a25f0a04SGreg Roach * @return string 404a25f0a04SGreg Roach */ 40555664801SGreg Roach public static function number(float $n, int $precision = 0): string 406c1010edaSGreg Roach { 407dfeee0a8SGreg Roach return self::$locale->number(round($n, $precision)); 408dfeee0a8SGreg Roach } 409dfeee0a8SGreg Roach 410dfeee0a8SGreg Roach /** 411dfeee0a8SGreg Roach * Translate a fraction into a percentage. 412dfeee0a8SGreg Roach * e.g. 0.123 becomes 413dfeee0a8SGreg Roach * en: 12.3% 414dfeee0a8SGreg Roach * fr: 12,3 % 415dfeee0a8SGreg Roach * de: 12,3% 416dfeee0a8SGreg Roach * 417dfeee0a8SGreg Roach * @param float $n 418cbc1590aSGreg Roach * @param int $precision 419dfeee0a8SGreg Roach * 420dfeee0a8SGreg Roach * @return string 421dfeee0a8SGreg Roach */ 42255664801SGreg Roach public static function percentage(float $n, int $precision = 0): string 423c1010edaSGreg Roach { 424dfeee0a8SGreg Roach return self::$locale->percent(round($n, $precision + 2)); 425dfeee0a8SGreg Roach } 426dfeee0a8SGreg Roach 427dfeee0a8SGreg Roach /** 428dfeee0a8SGreg Roach * Translate a plural string 429dfeee0a8SGreg Roach * echo self::plural('There is an error', 'There are errors', $num_errors); 430dfeee0a8SGreg Roach * echo self::plural('There is one error', 'There are %s errors', $num_errors); 431dfeee0a8SGreg Roach * echo self::plural('There is %1$s %2$s cat', 'There are %1$s %2$s cats', $num, $num, $colour); 432dfeee0a8SGreg Roach * 433924d091bSGreg Roach * @param string $singular 434924d091bSGreg Roach * @param string $plural 435924d091bSGreg Roach * @param int $count 436a515be7cSGreg Roach * @param string ...$args 437e93111adSRico Sonntag * 438dfeee0a8SGreg Roach * @return string 439dfeee0a8SGreg Roach */ 440924d091bSGreg Roach public static function plural(string $singular, string $plural, int $count, ...$args): string 441c1010edaSGreg Roach { 442924d091bSGreg Roach $message = self::$translator->translatePlural($singular, $plural, $count); 443dfeee0a8SGreg Roach 444924d091bSGreg Roach return sprintf($message, ...$args); 445dfeee0a8SGreg Roach } 446dfeee0a8SGreg Roach 447dfeee0a8SGreg Roach /** 448dfeee0a8SGreg Roach * UTF8 version of PHP::strrev() 449dfeee0a8SGreg Roach * Reverse RTL text for third-party libraries such as GD2 and googlechart. 450dfeee0a8SGreg Roach * These do not support UTF8 text direction, so we must mimic it for them. 451dfeee0a8SGreg Roach * Numbers are always rendered LTR, even in RTL text. 452dfeee0a8SGreg Roach * The visual direction of characters such as parentheses should be reversed. 453dfeee0a8SGreg Roach * 454dfeee0a8SGreg Roach * @param string $text Text to be reversed 455dfeee0a8SGreg Roach * 456dfeee0a8SGreg Roach * @return string 457dfeee0a8SGreg Roach */ 458e0c85f48SGreg Roach public static function reverseText(string $text): string 459c1010edaSGreg Roach { 460dfeee0a8SGreg Roach // Remove HTML markup - we can't display it and it is LTR. 4619524b7b5SGreg Roach $text = strip_tags($text); 4629524b7b5SGreg Roach // Remove HTML entities. 4639524b7b5SGreg Roach $text = html_entity_decode($text, ENT_QUOTES, 'UTF-8'); 464dfeee0a8SGreg Roach 465dfeee0a8SGreg Roach // LTR text doesn't need reversing 466dfeee0a8SGreg Roach if (self::scriptDirection(self::textScript($text)) === 'ltr') { 467dfeee0a8SGreg Roach return $text; 468dfeee0a8SGreg Roach } 469dfeee0a8SGreg Roach 470dfeee0a8SGreg Roach // Mirrored characters 471991b93ddSGreg Roach $text = strtr($text, self::MIRROR_CHARACTERS); 472dfeee0a8SGreg Roach 473dfeee0a8SGreg Roach $reversed = ''; 474dfeee0a8SGreg Roach $digits = ''; 475e364afe4SGreg Roach while ($text !== '') { 476dfeee0a8SGreg Roach $letter = mb_substr($text, 0, 1); 477dfeee0a8SGreg Roach $text = mb_substr($text, 1); 478dec352c1SGreg Roach if (str_contains(self::DIGITS, $letter)) { 479dfeee0a8SGreg Roach $digits .= $letter; 480a25f0a04SGreg Roach } else { 481dfeee0a8SGreg Roach $reversed = $letter . $digits . $reversed; 482dfeee0a8SGreg Roach $digits = ''; 483dfeee0a8SGreg Roach } 484a25f0a04SGreg Roach } 485a25f0a04SGreg Roach 486dfeee0a8SGreg Roach return $digits . $reversed; 487a25f0a04SGreg Roach } 488a25f0a04SGreg Roach 489a25f0a04SGreg Roach /** 490a25f0a04SGreg Roach * Return the direction (ltr or rtl) for a given script 491a25f0a04SGreg Roach * The PHP/intl library does not provde this information, so we need 492a25f0a04SGreg Roach * our own lookup table. 493a25f0a04SGreg Roach * 494a25f0a04SGreg Roach * @param string $script 495a25f0a04SGreg Roach * 496a25f0a04SGreg Roach * @return string 497a25f0a04SGreg Roach */ 498e0c85f48SGreg Roach public static function scriptDirection(string $script): string 499c1010edaSGreg Roach { 500a25f0a04SGreg Roach switch ($script) { 501a25f0a04SGreg Roach case 'Arab': 502a25f0a04SGreg Roach case 'Hebr': 503a25f0a04SGreg Roach case 'Mong': 504a25f0a04SGreg Roach case 'Thaa': 505a25f0a04SGreg Roach return 'rtl'; 506a25f0a04SGreg Roach default: 507a25f0a04SGreg Roach return 'ltr'; 508a25f0a04SGreg Roach } 509a25f0a04SGreg Roach } 510a25f0a04SGreg Roach 511a25f0a04SGreg Roach /** 512dfeee0a8SGreg Roach * Identify the script used for a piece of text 513dfeee0a8SGreg Roach * 514d0bfc631SGreg Roach * @param string $string 515dfeee0a8SGreg Roach * 516dfeee0a8SGreg Roach * @return string 517dfeee0a8SGreg Roach */ 518e0c85f48SGreg Roach public static function textScript(string $string): string 519c1010edaSGreg Roach { 520dfeee0a8SGreg Roach $string = strip_tags($string); // otherwise HTML tags show up as latin 521dfeee0a8SGreg Roach $string = html_entity_decode($string, ENT_QUOTES, 'UTF-8'); // otherwise HTML entities show up as latin 522c1010edaSGreg Roach $string = str_replace([ 5238fb4e87cSGreg Roach Individual::NOMEN_NESCIO, 5248fb4e87cSGreg Roach Individual::PRAENOMEN_NESCIO, 5258fb4e87cSGreg Roach ], '', $string); 526dfeee0a8SGreg Roach $pos = 0; 527dfeee0a8SGreg Roach $strlen = strlen($string); 528dfeee0a8SGreg Roach while ($pos < $strlen) { 529dfeee0a8SGreg Roach // get the Unicode Code Point for the character at position $pos 530dfeee0a8SGreg Roach $byte1 = ord($string[$pos]); 531dfeee0a8SGreg Roach if ($byte1 < 0x80) { 532dfeee0a8SGreg Roach $code_point = $byte1; 533dfeee0a8SGreg Roach $chrlen = 1; 534dfeee0a8SGreg Roach } elseif ($byte1 < 0xC0) { 535dfeee0a8SGreg Roach // Invalid continuation character 536dfeee0a8SGreg Roach return 'Latn'; 537dfeee0a8SGreg Roach } elseif ($byte1 < 0xE0) { 538dfeee0a8SGreg Roach $code_point = (($byte1 & 0x1F) << 6) + (ord($string[$pos + 1]) & 0x3F); 539dfeee0a8SGreg Roach $chrlen = 2; 540dfeee0a8SGreg Roach } elseif ($byte1 < 0xF0) { 541dfeee0a8SGreg Roach $code_point = (($byte1 & 0x0F) << 12) + ((ord($string[$pos + 1]) & 0x3F) << 6) + (ord($string[$pos + 2]) & 0x3F); 542dfeee0a8SGreg Roach $chrlen = 3; 543dfeee0a8SGreg Roach } elseif ($byte1 < 0xF8) { 544dfeee0a8SGreg Roach $code_point = (($byte1 & 0x07) << 24) + ((ord($string[$pos + 1]) & 0x3F) << 12) + ((ord($string[$pos + 2]) & 0x3F) << 6) + (ord($string[$pos + 3]) & 0x3F); 545dfeee0a8SGreg Roach $chrlen = 3; 546dfeee0a8SGreg Roach } else { 547dfeee0a8SGreg Roach // Invalid UTF 548dfeee0a8SGreg Roach return 'Latn'; 549dfeee0a8SGreg Roach } 550dfeee0a8SGreg Roach 551991b93ddSGreg Roach foreach (self::SCRIPT_CHARACTER_RANGES as $range) { 552dfeee0a8SGreg Roach if ($code_point >= $range[1] && $code_point <= $range[2]) { 553dfeee0a8SGreg Roach return $range[0]; 554dfeee0a8SGreg Roach } 555dfeee0a8SGreg Roach } 556dfeee0a8SGreg Roach // Not a recognised script. Maybe punctuation, spacing, etc. Keep looking. 557dfeee0a8SGreg Roach $pos += $chrlen; 558dfeee0a8SGreg Roach } 559dfeee0a8SGreg Roach 560dfeee0a8SGreg Roach return 'Latn'; 561dfeee0a8SGreg Roach } 562dfeee0a8SGreg Roach 563dfeee0a8SGreg Roach /** 56437646143SGreg Roach * A closure which will compare strings using local collation rules. 565006094b9SGreg Roach * 56637646143SGreg Roach * @return Closure 567006094b9SGreg Roach */ 56837646143SGreg Roach public static function comparator(): Closure 569006094b9SGreg Roach { 570006094b9SGreg Roach if (self::$collator instanceof Collator) { 57137646143SGreg Roach return static function (string $x, string $y): int { 57237646143SGreg Roach return (int) self::$collator->compare($x, $y); 57337646143SGreg Roach }; 574006094b9SGreg Roach } 575006094b9SGreg Roach 57637646143SGreg Roach return static function (string $x, string $y): int { 57737646143SGreg Roach return strcmp(self::strtolower($x), self::strtolower($y)); 57837646143SGreg Roach }; 579006094b9SGreg Roach } 580006094b9SGreg Roach 58137646143SGreg Roach 58237646143SGreg Roach 583006094b9SGreg Roach /** 584006094b9SGreg Roach * Convert a string to lower case. 585006094b9SGreg Roach * 586006094b9SGreg Roach * @param string $string 587006094b9SGreg Roach * 588006094b9SGreg Roach * @return string 589006094b9SGreg Roach */ 590e0c85f48SGreg Roach public static function strtolower(string $string): string 591006094b9SGreg Roach { 592006094b9SGreg Roach if (in_array(self::$locale->language()->code(), self::DOTLESS_I_LOCALES, true)) { 593006094b9SGreg Roach $string = strtr($string, self::DOTLESS_I_TOLOWER); 594006094b9SGreg Roach } 595006094b9SGreg Roach 596006094b9SGreg Roach return mb_strtolower($string); 597006094b9SGreg Roach } 598006094b9SGreg Roach 599006094b9SGreg Roach /** 600006094b9SGreg Roach * Convert a string to upper case. 601006094b9SGreg Roach * 602006094b9SGreg Roach * @param string $string 603006094b9SGreg Roach * 604006094b9SGreg Roach * @return string 605006094b9SGreg Roach */ 606e0c85f48SGreg Roach public static function strtoupper(string $string): string 607006094b9SGreg Roach { 608006094b9SGreg Roach if (in_array(self::$locale->language()->code(), self::DOTLESS_I_LOCALES, true)) { 609006094b9SGreg Roach $string = strtr($string, self::DOTLESS_I_TOUPPER); 610006094b9SGreg Roach } 611006094b9SGreg Roach 612006094b9SGreg Roach return mb_strtoupper($string); 613006094b9SGreg Roach } 614006094b9SGreg Roach 615006094b9SGreg Roach /** 616dfeee0a8SGreg Roach * What format is used to display dates in the current locale? 617dfeee0a8SGreg Roach * 618dfeee0a8SGreg Roach * @return string 619dfeee0a8SGreg Roach */ 6208f53f488SRico Sonntag public static function timeFormat(): string 621c1010edaSGreg Roach { 622bbb76c12SGreg Roach /* I18N: This is the format string for the time-of-day. See http://php.net/date for codes */ 623bbb76c12SGreg Roach return self::$translator->translate('%H:%i:%s'); 624dfeee0a8SGreg Roach } 625dfeee0a8SGreg Roach 626dfeee0a8SGreg Roach /** 627dfeee0a8SGreg Roach * Context sensitive version of translate. 628a4956c0eSGreg Roach * echo I18N::translateContext('NOMINATIVE', 'January'); 629a4956c0eSGreg Roach * echo I18N::translateContext('GENITIVE', 'January'); 630dfeee0a8SGreg Roach * 631924d091bSGreg Roach * @param string $context 632924d091bSGreg Roach * @param string $message 633a515be7cSGreg Roach * @param string ...$args 634c3283ed7SGreg Roach * 635dfeee0a8SGreg Roach * @return string 636dfeee0a8SGreg Roach */ 637924d091bSGreg Roach public static function translateContext(string $context, string $message, ...$args): string 638c1010edaSGreg Roach { 639924d091bSGreg Roach $message = self::$translator->translateContext($context, $message); 640dfeee0a8SGreg Roach 641924d091bSGreg Roach return sprintf($message, ...$args); 642a25f0a04SGreg Roach } 643a25f0a04SGreg Roach} 644