1a25f0a04SGreg Roach<?php 2a25f0a04SGreg Roach/** 3a25f0a04SGreg Roach * webtrees: online genealogy 48fcd0d32SGreg Roach * Copyright (C) 2019 webtrees development team 5a25f0a04SGreg Roach * This program is free software: you can redistribute it and/or modify 6a25f0a04SGreg Roach * it under the terms of the GNU General Public License as published by 7a25f0a04SGreg Roach * the Free Software Foundation, either version 3 of the License, or 8a25f0a04SGreg Roach * (at your option) any later version. 9a25f0a04SGreg Roach * This program is distributed in the hope that it will be useful, 10a25f0a04SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 11a25f0a04SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12a25f0a04SGreg Roach * GNU General Public License for more details. 13a25f0a04SGreg Roach * You should have received a copy of the GNU General Public License 14a25f0a04SGreg Roach * along with this program. If not, see <http://www.gnu.org/licenses/>. 15a25f0a04SGreg Roach */ 16e7f56f2aSGreg Roachdeclare(strict_types=1); 17e7f56f2aSGreg Roach 1876692c8bSGreg Roachnamespace Fisharebest\Webtrees; 19a25f0a04SGreg Roach 20991b93ddSGreg Roachuse Collator; 21f1af7e1cSGreg Roachuse Exception; 22c999a340SGreg Roachuse Fisharebest\Localization\Locale; 231e71bdc0SGreg Roachuse Fisharebest\Localization\Locale\LocaleEnUs; 2415834aaeSGreg Roachuse Fisharebest\Localization\Locale\LocaleInterface; 253bdc890bSGreg Roachuse Fisharebest\Localization\Translation; 263bdc890bSGreg Roachuse Fisharebest\Localization\Translator; 27d37db671SGreg Roachuse Fisharebest\Webtrees\Module\ModuleCustomInterface; 2802086832SGreg Roachuse Fisharebest\Webtrees\Module\ModuleLanguageInterface; 29d37db671SGreg Roachuse Fisharebest\Webtrees\Services\ModuleService; 306cd97bf6SGreg Roachuse Illuminate\Support\Collection; 31*4f194b97SGreg Roachuse function array_merge; 32*4f194b97SGreg Roachuse function filemtime; 33a25f0a04SGreg Roach 34a25f0a04SGreg Roach/** 3576692c8bSGreg Roach * Internationalization (i18n) and localization (l10n). 36a25f0a04SGreg Roach */ 37c1010edaSGreg Roachclass I18N 38c1010edaSGreg Roach{ 39d37db671SGreg Roach // MO files use special characters for plurals and context. 40*4f194b97SGreg Roach public const PLURAL = "\x00"; 41*4f194b97SGreg Roach public const CONTEXT = "\x04"; 42d37db671SGreg Roach 4315834aaeSGreg Roach /** @var LocaleInterface The current locale (e.g. LocaleEnGb) */ 44c999a340SGreg Roach private static $locale; 45c999a340SGreg Roach 4676692c8bSGreg Roach /** @var Translator An object that performs translation */ 473bdc890bSGreg Roach private static $translator; 483bdc890bSGreg Roach 49c9ec599fSGreg Roach /** @var Collator|null From the php-intl library */ 50991b93ddSGreg Roach private static $collator; 51991b93ddSGreg Roach 52a25f0a04SGreg Roach // Digits are always rendered LTR, even in RTL text. 5316d6367aSGreg Roach private const DIGITS = '0123456789٠١٢٣٤٥٦٧٨٩۰۱۲۳۴۵۶۷۸۹'; 54a25f0a04SGreg Roach 55991b93ddSGreg Roach // These locales need special handling for the dotless letter I. 5616d6367aSGreg Roach private const DOTLESS_I_LOCALES = [ 57c1010edaSGreg Roach 'az', 58c1010edaSGreg Roach 'tr', 59c1010edaSGreg Roach ]; 6016d6367aSGreg Roach private const DOTLESS_I_TOLOWER = [ 61c1010edaSGreg Roach 'I' => 'ı', 62c1010edaSGreg Roach 'İ' => 'i', 63c1010edaSGreg Roach ]; 6416d6367aSGreg Roach private const DOTLESS_I_TOUPPER = [ 65c1010edaSGreg Roach 'ı' => 'I', 66c1010edaSGreg Roach 'i' => 'İ', 67c1010edaSGreg Roach ]; 68a25f0a04SGreg Roach 69991b93ddSGreg Roach // The ranges of characters used by each script. 7016d6367aSGreg Roach private const SCRIPT_CHARACTER_RANGES = [ 71c1010edaSGreg Roach [ 72c1010edaSGreg Roach 'Latn', 73c1010edaSGreg Roach 0x0041, 74c1010edaSGreg Roach 0x005A, 75c1010edaSGreg Roach ], 76c1010edaSGreg Roach [ 77c1010edaSGreg Roach 'Latn', 78c1010edaSGreg Roach 0x0061, 79c1010edaSGreg Roach 0x007A, 80c1010edaSGreg Roach ], 81c1010edaSGreg Roach [ 82c1010edaSGreg Roach 'Latn', 83c1010edaSGreg Roach 0x0100, 84c1010edaSGreg Roach 0x02AF, 85c1010edaSGreg Roach ], 86c1010edaSGreg Roach [ 87c1010edaSGreg Roach 'Grek', 88c1010edaSGreg Roach 0x0370, 89c1010edaSGreg Roach 0x03FF, 90c1010edaSGreg Roach ], 91c1010edaSGreg Roach [ 92c1010edaSGreg Roach 'Cyrl', 93c1010edaSGreg Roach 0x0400, 94c1010edaSGreg Roach 0x052F, 95c1010edaSGreg Roach ], 96c1010edaSGreg Roach [ 97c1010edaSGreg Roach 'Hebr', 98c1010edaSGreg Roach 0x0590, 99c1010edaSGreg Roach 0x05FF, 100c1010edaSGreg Roach ], 101c1010edaSGreg Roach [ 102c1010edaSGreg Roach 'Arab', 103c1010edaSGreg Roach 0x0600, 104c1010edaSGreg Roach 0x06FF, 105c1010edaSGreg Roach ], 106c1010edaSGreg Roach [ 107c1010edaSGreg Roach 'Arab', 108c1010edaSGreg Roach 0x0750, 109c1010edaSGreg Roach 0x077F, 110c1010edaSGreg Roach ], 111c1010edaSGreg Roach [ 112c1010edaSGreg Roach 'Arab', 113c1010edaSGreg Roach 0x08A0, 114c1010edaSGreg Roach 0x08FF, 115c1010edaSGreg Roach ], 116c1010edaSGreg Roach [ 117c1010edaSGreg Roach 'Deva', 118c1010edaSGreg Roach 0x0900, 119c1010edaSGreg Roach 0x097F, 120c1010edaSGreg Roach ], 121c1010edaSGreg Roach [ 122c1010edaSGreg Roach 'Taml', 123c1010edaSGreg Roach 0x0B80, 124c1010edaSGreg Roach 0x0BFF, 125c1010edaSGreg Roach ], 126c1010edaSGreg Roach [ 127c1010edaSGreg Roach 'Sinh', 128c1010edaSGreg Roach 0x0D80, 129c1010edaSGreg Roach 0x0DFF, 130c1010edaSGreg Roach ], 131c1010edaSGreg Roach [ 132c1010edaSGreg Roach 'Thai', 133c1010edaSGreg Roach 0x0E00, 134c1010edaSGreg Roach 0x0E7F, 135c1010edaSGreg Roach ], 136c1010edaSGreg Roach [ 137c1010edaSGreg Roach 'Geor', 138c1010edaSGreg Roach 0x10A0, 139c1010edaSGreg Roach 0x10FF, 140c1010edaSGreg Roach ], 141c1010edaSGreg Roach [ 142c1010edaSGreg Roach 'Grek', 143c1010edaSGreg Roach 0x1F00, 144c1010edaSGreg Roach 0x1FFF, 145c1010edaSGreg Roach ], 146c1010edaSGreg Roach [ 147c1010edaSGreg Roach 'Deva', 148c1010edaSGreg Roach 0xA8E0, 149c1010edaSGreg Roach 0xA8FF, 150c1010edaSGreg Roach ], 151c1010edaSGreg Roach [ 152c1010edaSGreg Roach 'Hans', 153c1010edaSGreg Roach 0x3000, 154c1010edaSGreg Roach 0x303F, 155c1010edaSGreg Roach ], 156c1010edaSGreg Roach // Mixed CJK, not just Hans 157c1010edaSGreg Roach [ 158c1010edaSGreg Roach 'Hans', 159c1010edaSGreg Roach 0x3400, 160c1010edaSGreg Roach 0xFAFF, 161c1010edaSGreg Roach ], 162c1010edaSGreg Roach // Mixed CJK, not just Hans 163c1010edaSGreg Roach [ 164c1010edaSGreg Roach 'Hans', 165c1010edaSGreg Roach 0x20000, 166c1010edaSGreg Roach 0x2FA1F, 167c1010edaSGreg Roach ], 168c1010edaSGreg Roach // Mixed CJK, not just Hans 16913abd6f3SGreg Roach ]; 170a25f0a04SGreg Roach 171991b93ddSGreg Roach // Characters that are displayed in mirror form in RTL text. 17216d6367aSGreg Roach private const MIRROR_CHARACTERS = [ 173a25f0a04SGreg Roach '(' => ')', 174a25f0a04SGreg Roach ')' => '(', 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 '’ ' => '‘', 19113abd6f3SGreg Roach ]; 192a25f0a04SGreg Roach 193a25f0a04SGreg Roach /** @var string Punctuation used to separate list items, typically a comma */ 194a25f0a04SGreg Roach public static $list_separator; 195a25f0a04SGreg Roach 196a25f0a04SGreg Roach /** 19702086832SGreg Roach * The preferred locales for this site, or a default list if no preference. 198dfeee0a8SGreg Roach * 199dfeee0a8SGreg Roach * @return LocaleInterface[] 200dfeee0a8SGreg Roach */ 2018f53f488SRico Sonntag public static function activeLocales(): array 202c1010edaSGreg Roach { 20302086832SGreg Roach $locales = app(ModuleService::class) 204d6137952SGreg Roach ->findByInterface(ModuleLanguageInterface::class, false, true) 2050b5fd0a6SGreg Roach ->map(static function (ModuleLanguageInterface $module): LocaleInterface { 20602086832SGreg Roach return $module->locale(); 20702086832SGreg Roach }); 208dfeee0a8SGreg Roach 20902086832SGreg Roach if ($locales->isEmpty()) { 21002086832SGreg Roach return [new LocaleEnUs()]; 211dfeee0a8SGreg Roach } 212dfeee0a8SGreg Roach 21302086832SGreg Roach return $locales->all(); 214dfeee0a8SGreg Roach } 215dfeee0a8SGreg Roach 216dfeee0a8SGreg Roach /** 217dfeee0a8SGreg Roach * Which MySQL collation should be used for this locale? 218dfeee0a8SGreg Roach * 219dfeee0a8SGreg Roach * @return string 220dfeee0a8SGreg Roach */ 221e364afe4SGreg Roach public static function collation(): string 222c1010edaSGreg Roach { 223dfeee0a8SGreg Roach $collation = self::$locale->collation(); 224dfeee0a8SGreg Roach switch ($collation) { 225dfeee0a8SGreg Roach case 'croatian_ci': 226dfeee0a8SGreg Roach case 'german2_ci': 227dfeee0a8SGreg Roach case 'vietnamese_ci': 228dfeee0a8SGreg Roach // Only available in MySQL 5.6 229dfeee0a8SGreg Roach return 'utf8_unicode_ci'; 230dfeee0a8SGreg Roach default: 231dfeee0a8SGreg Roach return 'utf8_' . $collation; 232dfeee0a8SGreg Roach } 233dfeee0a8SGreg Roach } 234dfeee0a8SGreg Roach 235dfeee0a8SGreg Roach /** 236dfeee0a8SGreg Roach * What format is used to display dates in the current locale? 237dfeee0a8SGreg Roach * 238dfeee0a8SGreg Roach * @return string 239dfeee0a8SGreg Roach */ 2408f53f488SRico Sonntag public static function dateFormat(): string 241c1010edaSGreg Roach { 242bbb76c12SGreg Roach /* I18N: This is the format string for full dates. See http://php.net/date for codes */ 243bbb76c12SGreg Roach return self::$translator->translate('%j %F %Y'); 244dfeee0a8SGreg Roach } 245dfeee0a8SGreg Roach 246dfeee0a8SGreg Roach /** 247dfeee0a8SGreg Roach * Convert the digits 0-9 into the local script 248dfeee0a8SGreg Roach * Used for years, etc., where we do not want thousands-separators, decimals, etc. 249dfeee0a8SGreg Roach * 25055664801SGreg Roach * @param string|int $n 251dfeee0a8SGreg Roach * 252dfeee0a8SGreg Roach * @return string 253dfeee0a8SGreg Roach */ 2548f53f488SRico Sonntag public static function digits($n): string 255c1010edaSGreg Roach { 25655664801SGreg Roach return self::$locale->digits((string) $n); 257dfeee0a8SGreg Roach } 258dfeee0a8SGreg Roach 259dfeee0a8SGreg Roach /** 260dfeee0a8SGreg Roach * What is the direction of the current locale 261dfeee0a8SGreg Roach * 262dfeee0a8SGreg Roach * @return string "ltr" or "rtl" 263dfeee0a8SGreg Roach */ 2648f53f488SRico Sonntag public static function direction(): string 265c1010edaSGreg Roach { 266dfeee0a8SGreg Roach return self::$locale->direction(); 267dfeee0a8SGreg Roach } 268dfeee0a8SGreg Roach 269dfeee0a8SGreg Roach /** 2707231a557SGreg Roach * What is the first day of the week. 2717231a557SGreg Roach * 272cbc1590aSGreg Roach * @return int Sunday=0, Monday=1, etc. 2737231a557SGreg Roach */ 2748f53f488SRico Sonntag public static function firstDay(): int 275c1010edaSGreg Roach { 2767231a557SGreg Roach return self::$locale->territory()->firstDay(); 2777231a557SGreg Roach } 2787231a557SGreg Roach 2797231a557SGreg Roach /** 280dfeee0a8SGreg Roach * Generate i18n markup for the <html> tag, e.g. lang="ar" dir="rtl" 281dfeee0a8SGreg Roach * 282dfeee0a8SGreg Roach * @return string 283dfeee0a8SGreg Roach */ 2848f53f488SRico Sonntag public static function htmlAttributes(): string 285c1010edaSGreg Roach { 286dfeee0a8SGreg Roach return self::$locale->htmlAttributes(); 287dfeee0a8SGreg Roach } 288dfeee0a8SGreg Roach 289dfeee0a8SGreg Roach /** 290a25f0a04SGreg Roach * Initialise the translation adapter with a locale setting. 291a25f0a04SGreg Roach * 29215d603e7SGreg Roach * @param string $code Use this locale/language code, or choose one automatically 293e58a20ffSGreg Roach * @param Tree|null $tree 294c116a5ccSGreg Roach * @param bool $setup During setup, we cannot access the database. 295a25f0a04SGreg Roach * 296a25f0a04SGreg Roach * @return string $string 297a25f0a04SGreg Roach */ 298081ddc56SGreg Roach public static function init(string $code = '', Tree $tree = null, $setup = false): string 299c1010edaSGreg Roach { 30015d603e7SGreg Roach if ($code !== '') { 3013bdc890bSGreg Roach // Create the specified locale 3023bdc890bSGreg Roach self::$locale = Locale::create($code); 3034ee95e68SRico Sonntag } elseif (Session::has('locale') && file_exists(WT_ROOT . 'resources/lang/' . Session::get('locale') . '/messages.mo')) { 304e58a20ffSGreg Roach // Select a previously used locale 30531bc7874SGreg Roach self::$locale = Locale::create(Session::get('locale')); 3063bdc890bSGreg Roach } else { 307e58a20ffSGreg Roach if ($tree instanceof Tree) { 308e58a20ffSGreg Roach $default_locale = Locale::create($tree->getPreference('LANGUAGE', 'en-US')); 309e58a20ffSGreg Roach } else { 31059f2f229SGreg Roach $default_locale = new LocaleEnUs(); 3113bdc890bSGreg Roach } 312e58a20ffSGreg Roach 313e58a20ffSGreg Roach // Negotiate with the browser. 314e58a20ffSGreg Roach // Search engines don't negotiate. They get the default locale of the tree. 315c116a5ccSGreg Roach if ($setup) { 316c116a5ccSGreg Roach $installed_locales = app(ModuleService::class)->setupLanguages() 3170b5fd0a6SGreg Roach ->map(static function (ModuleLanguageInterface $module): LocaleInterface { 318c116a5ccSGreg Roach return $module->locale(); 319c116a5ccSGreg Roach }); 320c116a5ccSGreg Roach } else { 321c116a5ccSGreg Roach $installed_locales = self::installedLocales(); 322c116a5ccSGreg Roach } 323c116a5ccSGreg Roach 324c116a5ccSGreg Roach self::$locale = Locale::httpAcceptLanguage($_SERVER, $installed_locales->all(), $default_locale); 3253bdc890bSGreg Roach } 3263bdc890bSGreg Roach 327f1af7e1cSGreg Roach $cache_dir = WT_DATA_DIR . 'cache/'; 328f1af7e1cSGreg Roach $cache_file = $cache_dir . 'language-' . self::$locale->languageTag() . '-cache.php'; 3293bdc890bSGreg Roach if (file_exists($cache_file)) { 3303bdc890bSGreg Roach $filemtime = filemtime($cache_file); 3313bdc890bSGreg Roach } else { 3323bdc890bSGreg Roach $filemtime = 0; 3333bdc890bSGreg Roach } 3343bdc890bSGreg Roach 335*4f194b97SGreg Roach // Load the translation file 336*4f194b97SGreg Roach $translation_file = WT_ROOT . 'resources/lang/' . self::$locale->languageTag() . '/messages.mo'; 337362b8464SGreg Roach 338*4f194b97SGreg Roach // Rebuild files if the translation file has been updated 3393bdc890bSGreg Roach if (filemtime($translation_file) > $filemtime) { 3403bdc890bSGreg Roach $translation = new Translation($translation_file); 341*4f194b97SGreg Roach $translations = $translation->asArray(); 342*4f194b97SGreg Roach 343f1af7e1cSGreg Roach try { 344f1af7e1cSGreg Roach File::mkdir($cache_dir); 345f1af7e1cSGreg Roach file_put_contents($cache_file, '<?php return ' . var_export($translations, true) . ';'); 346f1af7e1cSGreg Roach } catch (Exception $ex) { 3477c2999b4SGreg Roach // During setup, we may not have been able to create it. 348c85fb0c4SGreg Roach } 3493bdc890bSGreg Roach } else { 3503bdc890bSGreg Roach $translations = include $cache_file; 351a25f0a04SGreg Roach } 352a25f0a04SGreg Roach 353*4f194b97SGreg Roach // Add translations from custom modules (but not during setup, as we have no database/modules) 354c116a5ccSGreg Roach if (!$setup) { 355*4f194b97SGreg Roach $translations = app(ModuleService::class) 356*4f194b97SGreg Roach ->findByInterface(ModuleCustomInterface::class) 357*4f194b97SGreg Roach ->reduce(function (array $carry, ModuleCustomInterface $item): array { 358*4f194b97SGreg Roach return array_merge($carry, $item->customTranslations(self::$locale->languageTag())); 359*4f194b97SGreg Roach }, $translations); 360d37db671SGreg Roach } 361d37db671SGreg Roach 3623bdc890bSGreg Roach // Create a translator 3633bdc890bSGreg Roach self::$translator = new Translator($translations, self::$locale->pluralRule()); 364a25f0a04SGreg Roach 365bbb76c12SGreg Roach /* I18N: This punctuation is used to separate lists of items */ 366bbb76c12SGreg Roach self::$list_separator = self::translate(', '); 367a25f0a04SGreg Roach 368991b93ddSGreg Roach // Create a collator 369991b93ddSGreg Roach try { 370444a65ecSGreg Roach if (class_exists('Collator')) { 371c9ec599fSGreg Roach // Symfony provides a very incomplete polyfill - which cannot be used. 372991b93ddSGreg Roach self::$collator = new Collator(self::$locale->code()); 373991b93ddSGreg Roach // Ignore upper/lower case differences 374991b93ddSGreg Roach self::$collator->setStrength(Collator::SECONDARY); 375444a65ecSGreg Roach } 376991b93ddSGreg Roach } catch (Exception $ex) { 377991b93ddSGreg Roach // PHP-INTL is not installed? We'll use a fallback later. 378c9ec599fSGreg Roach self::$collator = null; 379991b93ddSGreg Roach } 380991b93ddSGreg Roach 3815331c5eaSGreg Roach return self::$locale->languageTag(); 382a25f0a04SGreg Roach } 383a25f0a04SGreg Roach 384a25f0a04SGreg Roach /** 385c999a340SGreg Roach * All locales for which a translation file exists. 386c999a340SGreg Roach * 387c116a5ccSGreg Roach * @return Collection 38815834aaeSGreg Roach * @return LocaleInterface[] 389c999a340SGreg Roach */ 390c116a5ccSGreg Roach public static function installedLocales(): Collection 391c1010edaSGreg Roach { 39202086832SGreg Roach return app(ModuleService::class) 39302086832SGreg Roach ->findByInterface(ModuleLanguageInterface::class, true) 3940b5fd0a6SGreg Roach ->map(static function (ModuleLanguageInterface $module): LocaleInterface { 39502086832SGreg Roach return $module->locale(); 396c116a5ccSGreg Roach }); 397a25f0a04SGreg Roach } 398a25f0a04SGreg Roach 399a25f0a04SGreg Roach /** 400a25f0a04SGreg Roach * Return the endonym for a given language - as per http://cldr.unicode.org/ 401a25f0a04SGreg Roach * 402a25f0a04SGreg Roach * @param string $locale 403a25f0a04SGreg Roach * 404a25f0a04SGreg Roach * @return string 405a25f0a04SGreg Roach */ 40655664801SGreg Roach public static function languageName(string $locale): string 407c1010edaSGreg Roach { 408c999a340SGreg Roach return Locale::create($locale)->endonym(); 409a25f0a04SGreg Roach } 410a25f0a04SGreg Roach 411a25f0a04SGreg Roach /** 412a25f0a04SGreg Roach * Return the script used by a given language 413a25f0a04SGreg Roach * 414a25f0a04SGreg Roach * @param string $locale 415a25f0a04SGreg Roach * 416a25f0a04SGreg Roach * @return string 417a25f0a04SGreg Roach */ 41855664801SGreg Roach public static function languageScript(string $locale): string 419c1010edaSGreg Roach { 420c999a340SGreg Roach return Locale::create($locale)->script()->code(); 421a25f0a04SGreg Roach } 422a25f0a04SGreg Roach 423a25f0a04SGreg Roach /** 424dfeee0a8SGreg Roach * Translate a number into the local representation. 425dfeee0a8SGreg Roach * e.g. 12345.67 becomes 426dfeee0a8SGreg Roach * en: 12,345.67 427dfeee0a8SGreg Roach * fr: 12 345,67 428dfeee0a8SGreg Roach * de: 12.345,67 429dfeee0a8SGreg Roach * 430dfeee0a8SGreg Roach * @param float $n 431cbc1590aSGreg Roach * @param int $precision 432a25f0a04SGreg Roach * 433a25f0a04SGreg Roach * @return string 434a25f0a04SGreg Roach */ 43555664801SGreg Roach public static function number(float $n, int $precision = 0): string 436c1010edaSGreg Roach { 437dfeee0a8SGreg Roach return self::$locale->number(round($n, $precision)); 438dfeee0a8SGreg Roach } 439dfeee0a8SGreg Roach 440dfeee0a8SGreg Roach /** 441dfeee0a8SGreg Roach * Translate a fraction into a percentage. 442dfeee0a8SGreg Roach * e.g. 0.123 becomes 443dfeee0a8SGreg Roach * en: 12.3% 444dfeee0a8SGreg Roach * fr: 12,3 % 445dfeee0a8SGreg Roach * de: 12,3% 446dfeee0a8SGreg Roach * 447dfeee0a8SGreg Roach * @param float $n 448cbc1590aSGreg Roach * @param int $precision 449dfeee0a8SGreg Roach * 450dfeee0a8SGreg Roach * @return string 451dfeee0a8SGreg Roach */ 45255664801SGreg Roach public static function percentage(float $n, int $precision = 0): string 453c1010edaSGreg Roach { 454dfeee0a8SGreg Roach return self::$locale->percent(round($n, $precision + 2)); 455dfeee0a8SGreg Roach } 456dfeee0a8SGreg Roach 457dfeee0a8SGreg Roach /** 458dfeee0a8SGreg Roach * Translate a plural string 459dfeee0a8SGreg Roach * echo self::plural('There is an error', 'There are errors', $num_errors); 460dfeee0a8SGreg Roach * echo self::plural('There is one error', 'There are %s errors', $num_errors); 461dfeee0a8SGreg Roach * echo self::plural('There is %1$s %2$s cat', 'There are %1$s %2$s cats', $num, $num, $colour); 462dfeee0a8SGreg Roach * 463924d091bSGreg Roach * @param string $singular 464924d091bSGreg Roach * @param string $plural 465924d091bSGreg Roach * @param int $count 466a515be7cSGreg Roach * @param string ...$args 467e93111adSRico Sonntag * 468dfeee0a8SGreg Roach * @return string 469dfeee0a8SGreg Roach */ 470924d091bSGreg Roach public static function plural(string $singular, string $plural, int $count, ...$args): string 471c1010edaSGreg Roach { 472924d091bSGreg Roach $message = self::$translator->translatePlural($singular, $plural, $count); 473dfeee0a8SGreg Roach 474924d091bSGreg Roach return sprintf($message, ...$args); 475dfeee0a8SGreg Roach } 476dfeee0a8SGreg Roach 477dfeee0a8SGreg Roach /** 478dfeee0a8SGreg Roach * UTF8 version of PHP::strrev() 479dfeee0a8SGreg Roach * Reverse RTL text for third-party libraries such as GD2 and googlechart. 480dfeee0a8SGreg Roach * These do not support UTF8 text direction, so we must mimic it for them. 481dfeee0a8SGreg Roach * Numbers are always rendered LTR, even in RTL text. 482dfeee0a8SGreg Roach * The visual direction of characters such as parentheses should be reversed. 483dfeee0a8SGreg Roach * 484dfeee0a8SGreg Roach * @param string $text Text to be reversed 485dfeee0a8SGreg Roach * 486dfeee0a8SGreg Roach * @return string 487dfeee0a8SGreg Roach */ 4888f53f488SRico Sonntag public static function reverseText($text): string 489c1010edaSGreg Roach { 490dfeee0a8SGreg Roach // Remove HTML markup - we can't display it and it is LTR. 4919524b7b5SGreg Roach $text = strip_tags($text); 4929524b7b5SGreg Roach // Remove HTML entities. 4939524b7b5SGreg Roach $text = html_entity_decode($text, ENT_QUOTES, 'UTF-8'); 494dfeee0a8SGreg Roach 495dfeee0a8SGreg Roach // LTR text doesn't need reversing 496dfeee0a8SGreg Roach if (self::scriptDirection(self::textScript($text)) === 'ltr') { 497dfeee0a8SGreg Roach return $text; 498dfeee0a8SGreg Roach } 499dfeee0a8SGreg Roach 500dfeee0a8SGreg Roach // Mirrored characters 501991b93ddSGreg Roach $text = strtr($text, self::MIRROR_CHARACTERS); 502dfeee0a8SGreg Roach 503dfeee0a8SGreg Roach $reversed = ''; 504dfeee0a8SGreg Roach $digits = ''; 505e364afe4SGreg Roach while ($text !== '') { 506dfeee0a8SGreg Roach $letter = mb_substr($text, 0, 1); 507dfeee0a8SGreg Roach $text = mb_substr($text, 1); 508dfeee0a8SGreg Roach if (strpos(self::DIGITS, $letter) !== false) { 509dfeee0a8SGreg Roach $digits .= $letter; 510a25f0a04SGreg Roach } else { 511dfeee0a8SGreg Roach $reversed = $letter . $digits . $reversed; 512dfeee0a8SGreg Roach $digits = ''; 513dfeee0a8SGreg Roach } 514a25f0a04SGreg Roach } 515a25f0a04SGreg Roach 516dfeee0a8SGreg Roach return $digits . $reversed; 517a25f0a04SGreg Roach } 518a25f0a04SGreg Roach 519a25f0a04SGreg Roach /** 520a25f0a04SGreg Roach * Return the direction (ltr or rtl) for a given script 521a25f0a04SGreg Roach * The PHP/intl library does not provde this information, so we need 522a25f0a04SGreg Roach * our own lookup table. 523a25f0a04SGreg Roach * 524a25f0a04SGreg Roach * @param string $script 525a25f0a04SGreg Roach * 526a25f0a04SGreg Roach * @return string 527a25f0a04SGreg Roach */ 528e364afe4SGreg Roach public static function scriptDirection($script): string 529c1010edaSGreg Roach { 530a25f0a04SGreg Roach switch ($script) { 531a25f0a04SGreg Roach case 'Arab': 532a25f0a04SGreg Roach case 'Hebr': 533a25f0a04SGreg Roach case 'Mong': 534a25f0a04SGreg Roach case 'Thaa': 535a25f0a04SGreg Roach return 'rtl'; 536a25f0a04SGreg Roach default: 537a25f0a04SGreg Roach return 'ltr'; 538a25f0a04SGreg Roach } 539a25f0a04SGreg Roach } 540a25f0a04SGreg Roach 541a25f0a04SGreg Roach /** 542991b93ddSGreg Roach * Perform a case-insensitive comparison of two strings. 543a25f0a04SGreg Roach * 544a25f0a04SGreg Roach * @param string $string1 545a25f0a04SGreg Roach * @param string $string2 546a25f0a04SGreg Roach * 547cbc1590aSGreg Roach * @return int 548a25f0a04SGreg Roach */ 549e364afe4SGreg Roach public static function strcasecmp($string1, $string2): int 550c1010edaSGreg Roach { 551991b93ddSGreg Roach if (self::$collator instanceof Collator) { 552991b93ddSGreg Roach return self::$collator->compare($string1, $string2); 553a25f0a04SGreg Roach } 554e364afe4SGreg Roach 555e364afe4SGreg Roach return strcmp(self::strtolower($string1), self::strtolower($string2)); 556c9ec599fSGreg Roach } 557a25f0a04SGreg Roach 558a25f0a04SGreg Roach /** 559991b93ddSGreg Roach * Convert a string to lower case. 560a25f0a04SGreg Roach * 561dfeee0a8SGreg Roach * @param string $string 562a25f0a04SGreg Roach * 563a25f0a04SGreg Roach * @return string 564a25f0a04SGreg Roach */ 5658f53f488SRico Sonntag public static function strtolower($string): string 566c1010edaSGreg Roach { 56702086832SGreg Roach if (in_array(self::$locale->language()->code(), self::DOTLESS_I_LOCALES, true)) { 568991b93ddSGreg Roach $string = strtr($string, self::DOTLESS_I_TOLOWER); 569a25f0a04SGreg Roach } 5705ddad20bSGreg Roach 5715ddad20bSGreg Roach return mb_strtolower($string); 572a25f0a04SGreg Roach } 573a25f0a04SGreg Roach 574a25f0a04SGreg Roach /** 575991b93ddSGreg Roach * Convert a string to upper case. 576dfeee0a8SGreg Roach * 577dfeee0a8SGreg Roach * @param string $string 578a25f0a04SGreg Roach * 579a25f0a04SGreg Roach * @return string 580a25f0a04SGreg Roach */ 5818f53f488SRico Sonntag public static function strtoupper($string): string 582c1010edaSGreg Roach { 58302086832SGreg Roach if (in_array(self::$locale->language()->code(), self::DOTLESS_I_LOCALES, true)) { 584991b93ddSGreg Roach $string = strtr($string, self::DOTLESS_I_TOUPPER); 585a25f0a04SGreg Roach } 5865ddad20bSGreg Roach 5875ddad20bSGreg Roach return mb_strtoupper($string); 588a25f0a04SGreg Roach } 589a25f0a04SGreg Roach 590dfeee0a8SGreg Roach /** 591dfeee0a8SGreg Roach * Identify the script used for a piece of text 592dfeee0a8SGreg Roach * 593d0bfc631SGreg Roach * @param string $string 594dfeee0a8SGreg Roach * 595dfeee0a8SGreg Roach * @return string 596dfeee0a8SGreg Roach */ 5978f53f488SRico Sonntag public static function textScript($string): string 598c1010edaSGreg Roach { 599dfeee0a8SGreg Roach $string = strip_tags($string); // otherwise HTML tags show up as latin 600dfeee0a8SGreg Roach $string = html_entity_decode($string, ENT_QUOTES, 'UTF-8'); // otherwise HTML entities show up as latin 601c1010edaSGreg Roach $string = str_replace([ 602c1010edaSGreg Roach '@N.N.', 603c1010edaSGreg Roach '@P.N.', 604c1010edaSGreg Roach ], '', $string); // otherwise unknown names show up as latin 605dfeee0a8SGreg Roach $pos = 0; 606dfeee0a8SGreg Roach $strlen = strlen($string); 607dfeee0a8SGreg Roach while ($pos < $strlen) { 608dfeee0a8SGreg Roach // get the Unicode Code Point for the character at position $pos 609dfeee0a8SGreg Roach $byte1 = ord($string[$pos]); 610dfeee0a8SGreg Roach if ($byte1 < 0x80) { 611dfeee0a8SGreg Roach $code_point = $byte1; 612dfeee0a8SGreg Roach $chrlen = 1; 613dfeee0a8SGreg Roach } elseif ($byte1 < 0xC0) { 614dfeee0a8SGreg Roach // Invalid continuation character 615dfeee0a8SGreg Roach return 'Latn'; 616dfeee0a8SGreg Roach } elseif ($byte1 < 0xE0) { 617dfeee0a8SGreg Roach $code_point = (($byte1 & 0x1F) << 6) + (ord($string[$pos + 1]) & 0x3F); 618dfeee0a8SGreg Roach $chrlen = 2; 619dfeee0a8SGreg Roach } elseif ($byte1 < 0xF0) { 620dfeee0a8SGreg Roach $code_point = (($byte1 & 0x0F) << 12) + ((ord($string[$pos + 1]) & 0x3F) << 6) + (ord($string[$pos + 2]) & 0x3F); 621dfeee0a8SGreg Roach $chrlen = 3; 622dfeee0a8SGreg Roach } elseif ($byte1 < 0xF8) { 623dfeee0a8SGreg Roach $code_point = (($byte1 & 0x07) << 24) + ((ord($string[$pos + 1]) & 0x3F) << 12) + ((ord($string[$pos + 2]) & 0x3F) << 6) + (ord($string[$pos + 3]) & 0x3F); 624dfeee0a8SGreg Roach $chrlen = 3; 625dfeee0a8SGreg Roach } else { 626dfeee0a8SGreg Roach // Invalid UTF 627dfeee0a8SGreg Roach return 'Latn'; 628dfeee0a8SGreg Roach } 629dfeee0a8SGreg Roach 630991b93ddSGreg Roach foreach (self::SCRIPT_CHARACTER_RANGES as $range) { 631dfeee0a8SGreg Roach if ($code_point >= $range[1] && $code_point <= $range[2]) { 632dfeee0a8SGreg Roach return $range[0]; 633dfeee0a8SGreg Roach } 634dfeee0a8SGreg Roach } 635dfeee0a8SGreg Roach // Not a recognised script. Maybe punctuation, spacing, etc. Keep looking. 636dfeee0a8SGreg Roach $pos += $chrlen; 637dfeee0a8SGreg Roach } 638dfeee0a8SGreg Roach 639dfeee0a8SGreg Roach return 'Latn'; 640dfeee0a8SGreg Roach } 641dfeee0a8SGreg Roach 642dfeee0a8SGreg Roach /** 643dfeee0a8SGreg Roach * Convert a number of seconds into a relative time. For example, 630 => "10 hours, 30 minutes ago" 644dfeee0a8SGreg Roach * 645cbc1590aSGreg Roach * @param int $seconds 646dfeee0a8SGreg Roach * 647dfeee0a8SGreg Roach * @return string 648dfeee0a8SGreg Roach */ 649e364afe4SGreg Roach public static function timeAgo($seconds): string 650c1010edaSGreg Roach { 651dfeee0a8SGreg Roach $minute = 60; 652dfeee0a8SGreg Roach $hour = 60 * $minute; 653dfeee0a8SGreg Roach $day = 24 * $hour; 654dfeee0a8SGreg Roach $month = 30 * $day; 655dfeee0a8SGreg Roach $year = 365 * $day; 656dfeee0a8SGreg Roach 657dfeee0a8SGreg Roach if ($seconds > $year) { 658cdaafeeeSGreg Roach $years = intdiv($seconds, $year); 659cbc1590aSGreg Roach 660dfeee0a8SGreg Roach return self::plural('%s year ago', '%s years ago', $years, self::number($years)); 661b2ce94c6SRico Sonntag } 662b2ce94c6SRico Sonntag 663b2ce94c6SRico Sonntag if ($seconds > $month) { 664cdaafeeeSGreg Roach $months = intdiv($seconds, $month); 665cbc1590aSGreg Roach 666dfeee0a8SGreg Roach return self::plural('%s month ago', '%s months ago', $months, self::number($months)); 667b2ce94c6SRico Sonntag } 668b2ce94c6SRico Sonntag 669b2ce94c6SRico Sonntag if ($seconds > $day) { 670cdaafeeeSGreg Roach $days = intdiv($seconds, $day); 671cbc1590aSGreg Roach 672dfeee0a8SGreg Roach return self::plural('%s day ago', '%s days ago', $days, self::number($days)); 673b2ce94c6SRico Sonntag } 674b2ce94c6SRico Sonntag 675b2ce94c6SRico Sonntag if ($seconds > $hour) { 676cdaafeeeSGreg Roach $hours = intdiv($seconds, $hour); 677cbc1590aSGreg Roach 678dfeee0a8SGreg Roach return self::plural('%s hour ago', '%s hours ago', $hours, self::number($hours)); 679b2ce94c6SRico Sonntag } 680b2ce94c6SRico Sonntag 681b2ce94c6SRico Sonntag if ($seconds > $minute) { 682cdaafeeeSGreg Roach $minutes = intdiv($seconds, $minute); 683cbc1590aSGreg Roach 684dfeee0a8SGreg Roach return self::plural('%s minute ago', '%s minutes ago', $minutes, self::number($minutes)); 685dfeee0a8SGreg Roach } 686b2ce94c6SRico Sonntag 687b2ce94c6SRico Sonntag return self::plural('%s second ago', '%s seconds ago', $seconds, self::number($seconds)); 688dfeee0a8SGreg Roach } 689dfeee0a8SGreg Roach 690dfeee0a8SGreg Roach /** 691dfeee0a8SGreg Roach * What format is used to display dates in the current locale? 692dfeee0a8SGreg Roach * 693dfeee0a8SGreg Roach * @return string 694dfeee0a8SGreg Roach */ 6958f53f488SRico Sonntag public static function timeFormat(): string 696c1010edaSGreg Roach { 697bbb76c12SGreg Roach /* I18N: This is the format string for the time-of-day. See http://php.net/date for codes */ 698bbb76c12SGreg Roach return self::$translator->translate('%H:%i:%s'); 699dfeee0a8SGreg Roach } 700dfeee0a8SGreg Roach 701dfeee0a8SGreg Roach /** 702dfeee0a8SGreg Roach * Translate a string, and then substitute placeholders 703dfeee0a8SGreg Roach * echo I18N::translate('Hello World!'); 704dfeee0a8SGreg Roach * echo I18N::translate('The %s sat on the mat', 'cat'); 705dfeee0a8SGreg Roach * 706924d091bSGreg Roach * @param string $message 707a515be7cSGreg Roach * @param string ...$args 708c3283ed7SGreg Roach * 709dfeee0a8SGreg Roach * @return string 710dfeee0a8SGreg Roach */ 711924d091bSGreg Roach public static function translate(string $message, ...$args): string 712c1010edaSGreg Roach { 713924d091bSGreg Roach $message = self::$translator->translate($message); 714dfeee0a8SGreg Roach 715924d091bSGreg Roach return sprintf($message, ...$args); 716dfeee0a8SGreg Roach } 717dfeee0a8SGreg Roach 718dfeee0a8SGreg Roach /** 719dfeee0a8SGreg Roach * Context sensitive version of translate. 720a4956c0eSGreg Roach * echo I18N::translateContext('NOMINATIVE', 'January'); 721a4956c0eSGreg Roach * echo I18N::translateContext('GENITIVE', 'January'); 722dfeee0a8SGreg Roach * 723924d091bSGreg Roach * @param string $context 724924d091bSGreg Roach * @param string $message 725a515be7cSGreg Roach * @param string ...$args 726c3283ed7SGreg Roach * 727dfeee0a8SGreg Roach * @return string 728dfeee0a8SGreg Roach */ 729924d091bSGreg Roach public static function translateContext(string $context, string $message, ...$args): string 730c1010edaSGreg Roach { 731924d091bSGreg Roach $message = self::$translator->translateContext($context, $message); 732dfeee0a8SGreg Roach 733924d091bSGreg Roach return sprintf($message, ...$args); 734a25f0a04SGreg Roach } 735a25f0a04SGreg Roach} 736