1a25f0a04SGreg Roach<?php 2*3976b470SGreg Roach 3a25f0a04SGreg Roach/** 4a25f0a04SGreg Roach * webtrees: online genealogy 58fcd0d32SGreg Roach * Copyright (C) 2019 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 15a25f0a04SGreg Roach * along with this program. If not, see <http://www.gnu.org/licenses/>. 16a25f0a04SGreg Roach */ 17e7f56f2aSGreg Roachdeclare(strict_types=1); 18e7f56f2aSGreg Roach 1976692c8bSGreg Roachnamespace Fisharebest\Webtrees; 20a25f0a04SGreg Roach 21991b93ddSGreg Roachuse Collator; 22f1af7e1cSGreg Roachuse Exception; 23c999a340SGreg Roachuse Fisharebest\Localization\Locale; 241e71bdc0SGreg Roachuse Fisharebest\Localization\Locale\LocaleEnUs; 2515834aaeSGreg Roachuse Fisharebest\Localization\Locale\LocaleInterface; 263bdc890bSGreg Roachuse Fisharebest\Localization\Translation; 273bdc890bSGreg Roachuse Fisharebest\Localization\Translator; 28d37db671SGreg Roachuse Fisharebest\Webtrees\Module\ModuleCustomInterface; 2902086832SGreg Roachuse Fisharebest\Webtrees\Module\ModuleLanguageInterface; 30d37db671SGreg Roachuse Fisharebest\Webtrees\Services\ModuleService; 316cd97bf6SGreg Roachuse Illuminate\Support\Collection; 32*3976b470SGreg Roach 334f194b97SGreg Roachuse function array_merge; 34d68ee7a8SGreg Roachuse function class_exists; 35d68ee7a8SGreg Roachuse function html_entity_decode; 36d68ee7a8SGreg Roachuse function in_array; 37d68ee7a8SGreg Roachuse function mb_strtolower; 38d68ee7a8SGreg Roachuse function mb_strtoupper; 39d68ee7a8SGreg Roachuse function mb_substr; 40d68ee7a8SGreg Roachuse function ord; 41d68ee7a8SGreg Roachuse function sprintf; 42d68ee7a8SGreg Roachuse function str_replace; 43d68ee7a8SGreg Roachuse function strcmp; 44d68ee7a8SGreg Roachuse function strip_tags; 45d68ee7a8SGreg Roachuse function strlen; 46d68ee7a8SGreg Roachuse function strpos; 47d68ee7a8SGreg Roachuse function strtr; 48a25f0a04SGreg Roach 49a25f0a04SGreg Roach/** 5076692c8bSGreg Roach * Internationalization (i18n) and localization (l10n). 51a25f0a04SGreg Roach */ 52c1010edaSGreg Roachclass I18N 53c1010edaSGreg Roach{ 54d37db671SGreg Roach // MO files use special characters for plurals and context. 554f194b97SGreg Roach public const PLURAL = "\x00"; 564f194b97SGreg Roach public const CONTEXT = "\x04"; 5716d6367aSGreg Roach private const DIGITS = '0123456789٠١٢٣٤٥٦٧٨٩۰۱۲۳۴۵۶۷۸۹'; 5816d6367aSGreg Roach private const DOTLESS_I_LOCALES = [ 59c1010edaSGreg Roach 'az', 60c1010edaSGreg Roach 'tr', 61c1010edaSGreg Roach ]; 6216d6367aSGreg Roach private const DOTLESS_I_TOLOWER = [ 63c1010edaSGreg Roach 'I' => 'ı', 64c1010edaSGreg Roach 'İ' => 'i', 65c1010edaSGreg Roach ]; 66006094b9SGreg Roach 67006094b9SGreg Roach // Digits are always rendered LTR, even in RTL text. 6816d6367aSGreg Roach private const DOTLESS_I_TOUPPER = [ 69c1010edaSGreg Roach 'ı' => 'I', 70c1010edaSGreg Roach 'i' => 'İ', 71c1010edaSGreg Roach ]; 72a25f0a04SGreg Roach 73006094b9SGreg Roach // These locales need special handling for the dotless letter I. 7416d6367aSGreg Roach private const SCRIPT_CHARACTER_RANGES = [ 75c1010edaSGreg Roach [ 76c1010edaSGreg Roach 'Latn', 77c1010edaSGreg Roach 0x0041, 78c1010edaSGreg Roach 0x005A, 79c1010edaSGreg Roach ], 80c1010edaSGreg Roach [ 81c1010edaSGreg Roach 'Latn', 82c1010edaSGreg Roach 0x0061, 83c1010edaSGreg Roach 0x007A, 84c1010edaSGreg Roach ], 85c1010edaSGreg Roach [ 86c1010edaSGreg Roach 'Latn', 87c1010edaSGreg Roach 0x0100, 88c1010edaSGreg Roach 0x02AF, 89c1010edaSGreg Roach ], 90c1010edaSGreg Roach [ 91c1010edaSGreg Roach 'Grek', 92c1010edaSGreg Roach 0x0370, 93c1010edaSGreg Roach 0x03FF, 94c1010edaSGreg Roach ], 95c1010edaSGreg Roach [ 96c1010edaSGreg Roach 'Cyrl', 97c1010edaSGreg Roach 0x0400, 98c1010edaSGreg Roach 0x052F, 99c1010edaSGreg Roach ], 100c1010edaSGreg Roach [ 101c1010edaSGreg Roach 'Hebr', 102c1010edaSGreg Roach 0x0590, 103c1010edaSGreg Roach 0x05FF, 104c1010edaSGreg Roach ], 105c1010edaSGreg Roach [ 106c1010edaSGreg Roach 'Arab', 107c1010edaSGreg Roach 0x0600, 108c1010edaSGreg Roach 0x06FF, 109c1010edaSGreg Roach ], 110c1010edaSGreg Roach [ 111c1010edaSGreg Roach 'Arab', 112c1010edaSGreg Roach 0x0750, 113c1010edaSGreg Roach 0x077F, 114c1010edaSGreg Roach ], 115c1010edaSGreg Roach [ 116c1010edaSGreg Roach 'Arab', 117c1010edaSGreg Roach 0x08A0, 118c1010edaSGreg Roach 0x08FF, 119c1010edaSGreg Roach ], 120c1010edaSGreg Roach [ 121c1010edaSGreg Roach 'Deva', 122c1010edaSGreg Roach 0x0900, 123c1010edaSGreg Roach 0x097F, 124c1010edaSGreg Roach ], 125c1010edaSGreg Roach [ 126c1010edaSGreg Roach 'Taml', 127c1010edaSGreg Roach 0x0B80, 128c1010edaSGreg Roach 0x0BFF, 129c1010edaSGreg Roach ], 130c1010edaSGreg Roach [ 131c1010edaSGreg Roach 'Sinh', 132c1010edaSGreg Roach 0x0D80, 133c1010edaSGreg Roach 0x0DFF, 134c1010edaSGreg Roach ], 135c1010edaSGreg Roach [ 136c1010edaSGreg Roach 'Thai', 137c1010edaSGreg Roach 0x0E00, 138c1010edaSGreg Roach 0x0E7F, 139c1010edaSGreg Roach ], 140c1010edaSGreg Roach [ 141c1010edaSGreg Roach 'Geor', 142c1010edaSGreg Roach 0x10A0, 143c1010edaSGreg Roach 0x10FF, 144c1010edaSGreg Roach ], 145c1010edaSGreg Roach [ 146c1010edaSGreg Roach 'Grek', 147c1010edaSGreg Roach 0x1F00, 148c1010edaSGreg Roach 0x1FFF, 149c1010edaSGreg Roach ], 150c1010edaSGreg Roach [ 151c1010edaSGreg Roach 'Deva', 152c1010edaSGreg Roach 0xA8E0, 153c1010edaSGreg Roach 0xA8FF, 154c1010edaSGreg Roach ], 155c1010edaSGreg Roach [ 156c1010edaSGreg Roach 'Hans', 157c1010edaSGreg Roach 0x3000, 158c1010edaSGreg Roach 0x303F, 159c1010edaSGreg Roach ], 160c1010edaSGreg Roach // Mixed CJK, not just Hans 161c1010edaSGreg Roach [ 162c1010edaSGreg Roach 'Hans', 163c1010edaSGreg Roach 0x3400, 164c1010edaSGreg Roach 0xFAFF, 165c1010edaSGreg Roach ], 166c1010edaSGreg Roach // Mixed CJK, not just Hans 167c1010edaSGreg Roach [ 168c1010edaSGreg Roach 'Hans', 169c1010edaSGreg Roach 0x20000, 170c1010edaSGreg Roach 0x2FA1F, 171c1010edaSGreg Roach ], 172c1010edaSGreg Roach // Mixed CJK, not just Hans 17313abd6f3SGreg Roach ]; 17416d6367aSGreg Roach private const MIRROR_CHARACTERS = [ 175a25f0a04SGreg Roach '(' => ')', 176a25f0a04SGreg Roach ')' => '(', 177a25f0a04SGreg Roach '[' => ']', 178a25f0a04SGreg Roach ']' => '[', 179a25f0a04SGreg Roach '{' => '}', 180a25f0a04SGreg Roach '}' => '{', 181a25f0a04SGreg Roach '<' => '>', 182a25f0a04SGreg Roach '>' => '<', 183a25f0a04SGreg Roach '‹ ' => '›', 184a25f0a04SGreg Roach '› ' => '‹', 185a25f0a04SGreg Roach '«' => '»', 186a25f0a04SGreg Roach '»' => '«', 187a25f0a04SGreg Roach '﴾ ' => '﴿', 188a25f0a04SGreg Roach '﴿ ' => '﴾', 189a25f0a04SGreg Roach '“ ' => '”', 190a25f0a04SGreg Roach '” ' => '“', 191a25f0a04SGreg Roach '‘ ' => '’', 192a25f0a04SGreg Roach '’ ' => '‘', 19313abd6f3SGreg Roach ]; 194a25f0a04SGreg Roach /** @var string Punctuation used to separate list items, typically a comma */ 195a25f0a04SGreg Roach public static $list_separator; 196a25f0a04SGreg Roach 197006094b9SGreg Roach // The ranges of characters used by each script. 198006094b9SGreg Roach /** @var LocaleInterface The current locale (e.g. LocaleEnGb) */ 199006094b9SGreg Roach private static $locale; 200006094b9SGreg Roach 201006094b9SGreg Roach // Characters that are displayed in mirror form in RTL text. 202006094b9SGreg Roach /** @var Translator An object that performs translation */ 203006094b9SGreg Roach private static $translator; 204006094b9SGreg Roach /** @var Collator|null From the php-intl library */ 205006094b9SGreg Roach private static $collator; 206006094b9SGreg Roach 207a25f0a04SGreg Roach /** 20802086832SGreg Roach * The preferred locales for this site, or a default list if no preference. 209dfeee0a8SGreg Roach * 210dfeee0a8SGreg Roach * @return LocaleInterface[] 211dfeee0a8SGreg Roach */ 2128f53f488SRico Sonntag public static function activeLocales(): array 213c1010edaSGreg Roach { 214006094b9SGreg Roach /** @var Collection $locales */ 21502086832SGreg Roach $locales = app(ModuleService::class) 216d6137952SGreg Roach ->findByInterface(ModuleLanguageInterface::class, false, true) 2170b5fd0a6SGreg Roach ->map(static function (ModuleLanguageInterface $module): LocaleInterface { 21802086832SGreg Roach return $module->locale(); 21902086832SGreg Roach }); 220dfeee0a8SGreg Roach 22102086832SGreg Roach if ($locales->isEmpty()) { 22202086832SGreg Roach return [new LocaleEnUs()]; 223dfeee0a8SGreg Roach } 224dfeee0a8SGreg Roach 22502086832SGreg Roach return $locales->all(); 226dfeee0a8SGreg Roach } 227dfeee0a8SGreg Roach 228dfeee0a8SGreg Roach /** 229dfeee0a8SGreg Roach * Which MySQL collation should be used for this locale? 230dfeee0a8SGreg Roach * 231dfeee0a8SGreg Roach * @return string 232dfeee0a8SGreg Roach */ 233e364afe4SGreg Roach public static function collation(): string 234c1010edaSGreg Roach { 235dfeee0a8SGreg Roach $collation = self::$locale->collation(); 236dfeee0a8SGreg Roach switch ($collation) { 237dfeee0a8SGreg Roach case 'croatian_ci': 238dfeee0a8SGreg Roach case 'german2_ci': 239dfeee0a8SGreg Roach case 'vietnamese_ci': 240dfeee0a8SGreg Roach // Only available in MySQL 5.6 241dfeee0a8SGreg Roach return 'utf8_unicode_ci'; 242dfeee0a8SGreg Roach default: 243dfeee0a8SGreg Roach return 'utf8_' . $collation; 244dfeee0a8SGreg Roach } 245dfeee0a8SGreg Roach } 246dfeee0a8SGreg Roach 247dfeee0a8SGreg Roach /** 248dfeee0a8SGreg Roach * What format is used to display dates in the current locale? 249dfeee0a8SGreg Roach * 250dfeee0a8SGreg Roach * @return string 251dfeee0a8SGreg Roach */ 2528f53f488SRico Sonntag public static function dateFormat(): string 253c1010edaSGreg Roach { 254bbb76c12SGreg Roach /* I18N: This is the format string for full dates. See http://php.net/date for codes */ 255bbb76c12SGreg Roach return self::$translator->translate('%j %F %Y'); 256dfeee0a8SGreg Roach } 257dfeee0a8SGreg Roach 258dfeee0a8SGreg Roach /** 259dfeee0a8SGreg Roach * Convert the digits 0-9 into the local script 260dfeee0a8SGreg Roach * Used for years, etc., where we do not want thousands-separators, decimals, etc. 261dfeee0a8SGreg Roach * 26255664801SGreg Roach * @param string|int $n 263dfeee0a8SGreg Roach * 264dfeee0a8SGreg Roach * @return string 265dfeee0a8SGreg Roach */ 2668f53f488SRico Sonntag public static function digits($n): string 267c1010edaSGreg Roach { 26855664801SGreg Roach return self::$locale->digits((string) $n); 269dfeee0a8SGreg Roach } 270dfeee0a8SGreg Roach 271dfeee0a8SGreg Roach /** 272dfeee0a8SGreg Roach * What is the direction of the current locale 273dfeee0a8SGreg Roach * 274dfeee0a8SGreg Roach * @return string "ltr" or "rtl" 275dfeee0a8SGreg Roach */ 2768f53f488SRico Sonntag public static function direction(): string 277c1010edaSGreg Roach { 278dfeee0a8SGreg Roach return self::$locale->direction(); 279dfeee0a8SGreg Roach } 280dfeee0a8SGreg Roach 281dfeee0a8SGreg Roach /** 2827231a557SGreg Roach * What is the first day of the week. 2837231a557SGreg Roach * 284cbc1590aSGreg Roach * @return int Sunday=0, Monday=1, etc. 2857231a557SGreg Roach */ 2868f53f488SRico Sonntag public static function firstDay(): int 287c1010edaSGreg Roach { 2887231a557SGreg Roach return self::$locale->territory()->firstDay(); 2897231a557SGreg Roach } 2907231a557SGreg Roach 2917231a557SGreg Roach /** 292dfeee0a8SGreg Roach * Generate i18n markup for the <html> tag, e.g. lang="ar" dir="rtl" 293dfeee0a8SGreg Roach * 294dfeee0a8SGreg Roach * @return string 295dfeee0a8SGreg Roach */ 2968f53f488SRico Sonntag public static function htmlAttributes(): string 297c1010edaSGreg Roach { 298dfeee0a8SGreg Roach return self::$locale->htmlAttributes(); 299dfeee0a8SGreg Roach } 300dfeee0a8SGreg Roach 301dfeee0a8SGreg Roach /** 302a25f0a04SGreg Roach * Initialise the translation adapter with a locale setting. 303a25f0a04SGreg Roach * 30415d603e7SGreg Roach * @param string $code Use this locale/language code, or choose one automatically 305e58a20ffSGreg Roach * @param Tree|null $tree 306c116a5ccSGreg Roach * @param bool $setup During setup, we cannot access the database. 307a25f0a04SGreg Roach * 308a25f0a04SGreg Roach * @return string $string 309a25f0a04SGreg Roach */ 310081ddc56SGreg Roach public static function init(string $code = '', Tree $tree = null, $setup = false): string 311c1010edaSGreg Roach { 31215d603e7SGreg Roach if ($code !== '') { 3133bdc890bSGreg Roach // Create the specified locale 3143bdc890bSGreg Roach self::$locale = Locale::create($code); 315006094b9SGreg Roach } elseif (Session::has('language')) { 316e58a20ffSGreg Roach // Select a previously used locale 317a0801ffbSGreg Roach self::$locale = Locale::create(Session::get('language')); 3183bdc890bSGreg Roach } else { 319e58a20ffSGreg Roach if ($tree instanceof Tree) { 320e58a20ffSGreg Roach $default_locale = Locale::create($tree->getPreference('LANGUAGE', 'en-US')); 321e58a20ffSGreg Roach } else { 32259f2f229SGreg Roach $default_locale = new LocaleEnUs(); 3233bdc890bSGreg Roach } 324e58a20ffSGreg Roach 325e58a20ffSGreg Roach // Negotiate with the browser. 326e58a20ffSGreg Roach // Search engines don't negotiate. They get the default locale of the tree. 327c116a5ccSGreg Roach if ($setup) { 328c116a5ccSGreg Roach $installed_locales = app(ModuleService::class)->setupLanguages() 3290b5fd0a6SGreg Roach ->map(static function (ModuleLanguageInterface $module): LocaleInterface { 330c116a5ccSGreg Roach return $module->locale(); 331c116a5ccSGreg Roach }); 332c116a5ccSGreg Roach } else { 333c116a5ccSGreg Roach $installed_locales = self::installedLocales(); 334c116a5ccSGreg Roach } 335c116a5ccSGreg Roach 336c116a5ccSGreg Roach self::$locale = Locale::httpAcceptLanguage($_SERVER, $installed_locales->all(), $default_locale); 3373bdc890bSGreg Roach } 3383bdc890bSGreg Roach 3394f194b97SGreg Roach // Load the translation file 340006094b9SGreg Roach $translation_file = Webtrees::ROOT_DIR . 'resources/lang/' . self::$locale->languageTag() . '/messages.php'; 3414f194b97SGreg Roach 342f1af7e1cSGreg Roach try { 343006094b9SGreg Roach $translation = new Translation($translation_file); 344006094b9SGreg Roach $translations = $translation->asArray(); 345f1af7e1cSGreg Roach } catch (Exception $ex) { 346006094b9SGreg Roach // The translations files are created during the build process, and are 347006094b9SGreg Roach // not included in the source code. 348006094b9SGreg Roach // Assuming we are using dev code, and build (or rebuild) the files. 349006094b9SGreg Roach $po_file = Webtrees::ROOT_DIR . 'resources/lang/' . self::$locale->languageTag() . '/messages.po'; 350006094b9SGreg Roach $translation = new Translation($po_file); 351006094b9SGreg Roach $translations = $translation->asArray(); 352006094b9SGreg Roach file_put_contents($translation_file, '<?php return ' . var_export($translations, true) . ';'); 353a25f0a04SGreg Roach } 354a25f0a04SGreg Roach 3554f194b97SGreg Roach // Add translations from custom modules (but not during setup, as we have no database/modules) 356c116a5ccSGreg Roach if (!$setup) { 3574f194b97SGreg Roach $translations = app(ModuleService::class) 3584f194b97SGreg Roach ->findByInterface(ModuleCustomInterface::class) 35969253da9SGreg Roach ->reduce(static function (array $carry, ModuleCustomInterface $item): array { 3604f194b97SGreg Roach return array_merge($carry, $item->customTranslations(self::$locale->languageTag())); 3614f194b97SGreg Roach }, $translations); 362d37db671SGreg Roach } 363d37db671SGreg Roach 3643bdc890bSGreg Roach // Create a translator 3653bdc890bSGreg Roach self::$translator = new Translator($translations, self::$locale->pluralRule()); 366a25f0a04SGreg Roach 367bbb76c12SGreg Roach /* I18N: This punctuation is used to separate lists of items */ 368bbb76c12SGreg Roach self::$list_separator = self::translate(', '); 369a25f0a04SGreg Roach 370991b93ddSGreg Roach // Create a collator 371991b93ddSGreg Roach try { 372444a65ecSGreg Roach if (class_exists('Collator')) { 373c9ec599fSGreg Roach // Symfony provides a very incomplete polyfill - which cannot be used. 374991b93ddSGreg Roach self::$collator = new Collator(self::$locale->code()); 375991b93ddSGreg Roach // Ignore upper/lower case differences 376991b93ddSGreg Roach self::$collator->setStrength(Collator::SECONDARY); 377444a65ecSGreg Roach } 378991b93ddSGreg Roach } catch (Exception $ex) { 379991b93ddSGreg Roach // PHP-INTL is not installed? We'll use a fallback later. 380c9ec599fSGreg Roach self::$collator = null; 381991b93ddSGreg Roach } 382991b93ddSGreg Roach 3835331c5eaSGreg Roach return self::$locale->languageTag(); 384a25f0a04SGreg Roach } 385a25f0a04SGreg Roach 386a25f0a04SGreg Roach /** 387c999a340SGreg Roach * All locales for which a translation file exists. 388c999a340SGreg Roach * 389c116a5ccSGreg Roach * @return Collection 390c999a340SGreg Roach */ 391c116a5ccSGreg Roach public static function installedLocales(): Collection 392c1010edaSGreg Roach { 39302086832SGreg Roach return app(ModuleService::class) 39402086832SGreg Roach ->findByInterface(ModuleLanguageInterface::class, true) 3950b5fd0a6SGreg Roach ->map(static function (ModuleLanguageInterface $module): LocaleInterface { 39602086832SGreg Roach return $module->locale(); 397c116a5ccSGreg Roach }); 398a25f0a04SGreg Roach } 399a25f0a04SGreg Roach 400a25f0a04SGreg Roach /** 401006094b9SGreg Roach * Translate a string, and then substitute placeholders 402006094b9SGreg Roach * echo I18N::translate('Hello World!'); 403006094b9SGreg Roach * echo I18N::translate('The %s sat on the mat', 'cat'); 404006094b9SGreg Roach * 405006094b9SGreg Roach * @param string $message 406006094b9SGreg Roach * @param string ...$args 407006094b9SGreg Roach * 408006094b9SGreg Roach * @return string 409006094b9SGreg Roach */ 410006094b9SGreg Roach public static function translate(string $message, ...$args): string 411006094b9SGreg Roach { 412006094b9SGreg Roach $message = self::$translator->translate($message); 413006094b9SGreg Roach 414006094b9SGreg Roach return sprintf($message, ...$args); 415006094b9SGreg Roach } 416006094b9SGreg Roach 417006094b9SGreg Roach /** 418a25f0a04SGreg Roach * Return the endonym for a given language - as per http://cldr.unicode.org/ 419a25f0a04SGreg Roach * 420a25f0a04SGreg Roach * @param string $locale 421a25f0a04SGreg Roach * 422a25f0a04SGreg Roach * @return string 423a25f0a04SGreg Roach */ 42455664801SGreg Roach public static function languageName(string $locale): string 425c1010edaSGreg Roach { 426c999a340SGreg Roach return Locale::create($locale)->endonym(); 427a25f0a04SGreg Roach } 428a25f0a04SGreg Roach 429a25f0a04SGreg Roach /** 430a25f0a04SGreg Roach * Return the script used by a given language 431a25f0a04SGreg Roach * 432a25f0a04SGreg Roach * @param string $locale 433a25f0a04SGreg Roach * 434a25f0a04SGreg Roach * @return string 435a25f0a04SGreg Roach */ 43655664801SGreg Roach public static function languageScript(string $locale): string 437c1010edaSGreg Roach { 438c999a340SGreg Roach return Locale::create($locale)->script()->code(); 439a25f0a04SGreg Roach } 440a25f0a04SGreg Roach 441a25f0a04SGreg Roach /** 442dfeee0a8SGreg Roach * Translate a number into the local representation. 443dfeee0a8SGreg Roach * e.g. 12345.67 becomes 444dfeee0a8SGreg Roach * en: 12,345.67 445dfeee0a8SGreg Roach * fr: 12 345,67 446dfeee0a8SGreg Roach * de: 12.345,67 447dfeee0a8SGreg Roach * 448dfeee0a8SGreg Roach * @param float $n 449cbc1590aSGreg Roach * @param int $precision 450a25f0a04SGreg Roach * 451a25f0a04SGreg Roach * @return string 452a25f0a04SGreg Roach */ 45355664801SGreg Roach public static function number(float $n, int $precision = 0): string 454c1010edaSGreg Roach { 455dfeee0a8SGreg Roach return self::$locale->number(round($n, $precision)); 456dfeee0a8SGreg Roach } 457dfeee0a8SGreg Roach 458dfeee0a8SGreg Roach /** 459dfeee0a8SGreg Roach * Translate a fraction into a percentage. 460dfeee0a8SGreg Roach * e.g. 0.123 becomes 461dfeee0a8SGreg Roach * en: 12.3% 462dfeee0a8SGreg Roach * fr: 12,3 % 463dfeee0a8SGreg Roach * de: 12,3% 464dfeee0a8SGreg Roach * 465dfeee0a8SGreg Roach * @param float $n 466cbc1590aSGreg Roach * @param int $precision 467dfeee0a8SGreg Roach * 468dfeee0a8SGreg Roach * @return string 469dfeee0a8SGreg Roach */ 47055664801SGreg Roach public static function percentage(float $n, int $precision = 0): string 471c1010edaSGreg Roach { 472dfeee0a8SGreg Roach return self::$locale->percent(round($n, $precision + 2)); 473dfeee0a8SGreg Roach } 474dfeee0a8SGreg Roach 475dfeee0a8SGreg Roach /** 476dfeee0a8SGreg Roach * Translate a plural string 477dfeee0a8SGreg Roach * echo self::plural('There is an error', 'There are errors', $num_errors); 478dfeee0a8SGreg Roach * echo self::plural('There is one error', 'There are %s errors', $num_errors); 479dfeee0a8SGreg Roach * echo self::plural('There is %1$s %2$s cat', 'There are %1$s %2$s cats', $num, $num, $colour); 480dfeee0a8SGreg Roach * 481924d091bSGreg Roach * @param string $singular 482924d091bSGreg Roach * @param string $plural 483924d091bSGreg Roach * @param int $count 484a515be7cSGreg Roach * @param string ...$args 485e93111adSRico Sonntag * 486dfeee0a8SGreg Roach * @return string 487dfeee0a8SGreg Roach */ 488924d091bSGreg Roach public static function plural(string $singular, string $plural, int $count, ...$args): string 489c1010edaSGreg Roach { 490924d091bSGreg Roach $message = self::$translator->translatePlural($singular, $plural, $count); 491dfeee0a8SGreg Roach 492924d091bSGreg Roach return sprintf($message, ...$args); 493dfeee0a8SGreg Roach } 494dfeee0a8SGreg Roach 495dfeee0a8SGreg Roach /** 496dfeee0a8SGreg Roach * UTF8 version of PHP::strrev() 497dfeee0a8SGreg Roach * Reverse RTL text for third-party libraries such as GD2 and googlechart. 498dfeee0a8SGreg Roach * These do not support UTF8 text direction, so we must mimic it for them. 499dfeee0a8SGreg Roach * Numbers are always rendered LTR, even in RTL text. 500dfeee0a8SGreg Roach * The visual direction of characters such as parentheses should be reversed. 501dfeee0a8SGreg Roach * 502dfeee0a8SGreg Roach * @param string $text Text to be reversed 503dfeee0a8SGreg Roach * 504dfeee0a8SGreg Roach * @return string 505dfeee0a8SGreg Roach */ 5068f53f488SRico Sonntag public static function reverseText($text): string 507c1010edaSGreg Roach { 508dfeee0a8SGreg Roach // Remove HTML markup - we can't display it and it is LTR. 5099524b7b5SGreg Roach $text = strip_tags($text); 5109524b7b5SGreg Roach // Remove HTML entities. 5119524b7b5SGreg Roach $text = html_entity_decode($text, ENT_QUOTES, 'UTF-8'); 512dfeee0a8SGreg Roach 513dfeee0a8SGreg Roach // LTR text doesn't need reversing 514dfeee0a8SGreg Roach if (self::scriptDirection(self::textScript($text)) === 'ltr') { 515dfeee0a8SGreg Roach return $text; 516dfeee0a8SGreg Roach } 517dfeee0a8SGreg Roach 518dfeee0a8SGreg Roach // Mirrored characters 519991b93ddSGreg Roach $text = strtr($text, self::MIRROR_CHARACTERS); 520dfeee0a8SGreg Roach 521dfeee0a8SGreg Roach $reversed = ''; 522dfeee0a8SGreg Roach $digits = ''; 523e364afe4SGreg Roach while ($text !== '') { 524dfeee0a8SGreg Roach $letter = mb_substr($text, 0, 1); 525dfeee0a8SGreg Roach $text = mb_substr($text, 1); 526dfeee0a8SGreg Roach if (strpos(self::DIGITS, $letter) !== false) { 527dfeee0a8SGreg Roach $digits .= $letter; 528a25f0a04SGreg Roach } else { 529dfeee0a8SGreg Roach $reversed = $letter . $digits . $reversed; 530dfeee0a8SGreg Roach $digits = ''; 531dfeee0a8SGreg Roach } 532a25f0a04SGreg Roach } 533a25f0a04SGreg Roach 534dfeee0a8SGreg Roach return $digits . $reversed; 535a25f0a04SGreg Roach } 536a25f0a04SGreg Roach 537a25f0a04SGreg Roach /** 538a25f0a04SGreg Roach * Return the direction (ltr or rtl) for a given script 539a25f0a04SGreg Roach * The PHP/intl library does not provde this information, so we need 540a25f0a04SGreg Roach * our own lookup table. 541a25f0a04SGreg Roach * 542a25f0a04SGreg Roach * @param string $script 543a25f0a04SGreg Roach * 544a25f0a04SGreg Roach * @return string 545a25f0a04SGreg Roach */ 546e364afe4SGreg Roach public static function scriptDirection($script): string 547c1010edaSGreg Roach { 548a25f0a04SGreg Roach switch ($script) { 549a25f0a04SGreg Roach case 'Arab': 550a25f0a04SGreg Roach case 'Hebr': 551a25f0a04SGreg Roach case 'Mong': 552a25f0a04SGreg Roach case 'Thaa': 553a25f0a04SGreg Roach return 'rtl'; 554a25f0a04SGreg Roach default: 555a25f0a04SGreg Roach return 'ltr'; 556a25f0a04SGreg Roach } 557a25f0a04SGreg Roach } 558a25f0a04SGreg Roach 559a25f0a04SGreg Roach /** 560dfeee0a8SGreg Roach * Identify the script used for a piece of text 561dfeee0a8SGreg Roach * 562d0bfc631SGreg Roach * @param string $string 563dfeee0a8SGreg Roach * 564dfeee0a8SGreg Roach * @return string 565dfeee0a8SGreg Roach */ 5668f53f488SRico Sonntag public static function textScript($string): string 567c1010edaSGreg Roach { 568dfeee0a8SGreg Roach $string = strip_tags($string); // otherwise HTML tags show up as latin 569dfeee0a8SGreg Roach $string = html_entity_decode($string, ENT_QUOTES, 'UTF-8'); // otherwise HTML entities show up as latin 570c1010edaSGreg Roach $string = str_replace([ 571c1010edaSGreg Roach '@N.N.', 572c1010edaSGreg Roach '@P.N.', 573c1010edaSGreg Roach ], '', $string); // otherwise unknown names show up as latin 574dfeee0a8SGreg Roach $pos = 0; 575dfeee0a8SGreg Roach $strlen = strlen($string); 576dfeee0a8SGreg Roach while ($pos < $strlen) { 577dfeee0a8SGreg Roach // get the Unicode Code Point for the character at position $pos 578dfeee0a8SGreg Roach $byte1 = ord($string[$pos]); 579dfeee0a8SGreg Roach if ($byte1 < 0x80) { 580dfeee0a8SGreg Roach $code_point = $byte1; 581dfeee0a8SGreg Roach $chrlen = 1; 582dfeee0a8SGreg Roach } elseif ($byte1 < 0xC0) { 583dfeee0a8SGreg Roach // Invalid continuation character 584dfeee0a8SGreg Roach return 'Latn'; 585dfeee0a8SGreg Roach } elseif ($byte1 < 0xE0) { 586dfeee0a8SGreg Roach $code_point = (($byte1 & 0x1F) << 6) + (ord($string[$pos + 1]) & 0x3F); 587dfeee0a8SGreg Roach $chrlen = 2; 588dfeee0a8SGreg Roach } elseif ($byte1 < 0xF0) { 589dfeee0a8SGreg Roach $code_point = (($byte1 & 0x0F) << 12) + ((ord($string[$pos + 1]) & 0x3F) << 6) + (ord($string[$pos + 2]) & 0x3F); 590dfeee0a8SGreg Roach $chrlen = 3; 591dfeee0a8SGreg Roach } elseif ($byte1 < 0xF8) { 592dfeee0a8SGreg Roach $code_point = (($byte1 & 0x07) << 24) + ((ord($string[$pos + 1]) & 0x3F) << 12) + ((ord($string[$pos + 2]) & 0x3F) << 6) + (ord($string[$pos + 3]) & 0x3F); 593dfeee0a8SGreg Roach $chrlen = 3; 594dfeee0a8SGreg Roach } else { 595dfeee0a8SGreg Roach // Invalid UTF 596dfeee0a8SGreg Roach return 'Latn'; 597dfeee0a8SGreg Roach } 598dfeee0a8SGreg Roach 599991b93ddSGreg Roach foreach (self::SCRIPT_CHARACTER_RANGES as $range) { 600dfeee0a8SGreg Roach if ($code_point >= $range[1] && $code_point <= $range[2]) { 601dfeee0a8SGreg Roach return $range[0]; 602dfeee0a8SGreg Roach } 603dfeee0a8SGreg Roach } 604dfeee0a8SGreg Roach // Not a recognised script. Maybe punctuation, spacing, etc. Keep looking. 605dfeee0a8SGreg Roach $pos += $chrlen; 606dfeee0a8SGreg Roach } 607dfeee0a8SGreg Roach 608dfeee0a8SGreg Roach return 'Latn'; 609dfeee0a8SGreg Roach } 610dfeee0a8SGreg Roach 611dfeee0a8SGreg Roach /** 612006094b9SGreg Roach * Perform a case-insensitive comparison of two strings. 613006094b9SGreg Roach * 614006094b9SGreg Roach * @param string $string1 615006094b9SGreg Roach * @param string $string2 616006094b9SGreg Roach * 617006094b9SGreg Roach * @return int 618006094b9SGreg Roach */ 619006094b9SGreg Roach public static function strcasecmp($string1, $string2): int 620006094b9SGreg Roach { 621006094b9SGreg Roach if (self::$collator instanceof Collator) { 622006094b9SGreg Roach return self::$collator->compare($string1, $string2); 623006094b9SGreg Roach } 624006094b9SGreg Roach 625006094b9SGreg Roach return strcmp(self::strtolower($string1), self::strtolower($string2)); 626006094b9SGreg Roach } 627006094b9SGreg Roach 628006094b9SGreg Roach /** 629006094b9SGreg Roach * Convert a string to lower case. 630006094b9SGreg Roach * 631006094b9SGreg Roach * @param string $string 632006094b9SGreg Roach * 633006094b9SGreg Roach * @return string 634006094b9SGreg Roach */ 635006094b9SGreg Roach public static function strtolower($string): string 636006094b9SGreg Roach { 637006094b9SGreg Roach if (in_array(self::$locale->language()->code(), self::DOTLESS_I_LOCALES, true)) { 638006094b9SGreg Roach $string = strtr($string, self::DOTLESS_I_TOLOWER); 639006094b9SGreg Roach } 640006094b9SGreg Roach 641006094b9SGreg Roach return mb_strtolower($string); 642006094b9SGreg Roach } 643006094b9SGreg Roach 644006094b9SGreg Roach /** 645006094b9SGreg Roach * Convert a string to upper case. 646006094b9SGreg Roach * 647006094b9SGreg Roach * @param string $string 648006094b9SGreg Roach * 649006094b9SGreg Roach * @return string 650006094b9SGreg Roach */ 651006094b9SGreg Roach public static function strtoupper($string): string 652006094b9SGreg Roach { 653006094b9SGreg Roach if (in_array(self::$locale->language()->code(), self::DOTLESS_I_LOCALES, true)) { 654006094b9SGreg Roach $string = strtr($string, self::DOTLESS_I_TOUPPER); 655006094b9SGreg Roach } 656006094b9SGreg Roach 657006094b9SGreg Roach return mb_strtoupper($string); 658006094b9SGreg Roach } 659006094b9SGreg Roach 660006094b9SGreg Roach /** 661dfeee0a8SGreg Roach * What format is used to display dates in the current locale? 662dfeee0a8SGreg Roach * 663dfeee0a8SGreg Roach * @return string 664dfeee0a8SGreg Roach */ 6658f53f488SRico Sonntag public static function timeFormat(): string 666c1010edaSGreg Roach { 667bbb76c12SGreg Roach /* I18N: This is the format string for the time-of-day. See http://php.net/date for codes */ 668bbb76c12SGreg Roach return self::$translator->translate('%H:%i:%s'); 669dfeee0a8SGreg Roach } 670dfeee0a8SGreg Roach 671dfeee0a8SGreg Roach /** 672dfeee0a8SGreg Roach * Context sensitive version of translate. 673a4956c0eSGreg Roach * echo I18N::translateContext('NOMINATIVE', 'January'); 674a4956c0eSGreg Roach * echo I18N::translateContext('GENITIVE', 'January'); 675dfeee0a8SGreg Roach * 676924d091bSGreg Roach * @param string $context 677924d091bSGreg Roach * @param string $message 678a515be7cSGreg Roach * @param string ...$args 679c3283ed7SGreg Roach * 680dfeee0a8SGreg Roach * @return string 681dfeee0a8SGreg Roach */ 682924d091bSGreg Roach public static function translateContext(string $context, string $message, ...$args): string 683c1010edaSGreg Roach { 684924d091bSGreg Roach $message = self::$translator->translateContext($context, $message); 685dfeee0a8SGreg Roach 686924d091bSGreg Roach return sprintf($message, ...$args); 687a25f0a04SGreg Roach } 688a25f0a04SGreg Roach} 689