1a25f0a04SGreg Roach<?php 2a25f0a04SGreg Roach/** 3a25f0a04SGreg Roach * webtrees: online genealogy 41062a142SGreg Roach * Copyright (C) 2018 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; 2715d603e7SGreg Roachuse Fisharebest\Webtrees\Functions\FunctionsEdit; 28a25f0a04SGreg Roach 29a25f0a04SGreg Roach/** 3076692c8bSGreg Roach * Internationalization (i18n) and localization (l10n). 31a25f0a04SGreg Roach */ 32c1010edaSGreg Roachclass I18N 33c1010edaSGreg Roach{ 3415834aaeSGreg Roach /** @var LocaleInterface The current locale (e.g. LocaleEnGb) */ 35c999a340SGreg Roach private static $locale; 36c999a340SGreg Roach 3776692c8bSGreg Roach /** @var Translator An object that performs translation */ 383bdc890bSGreg Roach private static $translator; 393bdc890bSGreg Roach 40c9ec599fSGreg Roach /** @var Collator|null From the php-intl library */ 41991b93ddSGreg Roach private static $collator; 42991b93ddSGreg Roach 43a25f0a04SGreg Roach // Digits are always rendered LTR, even in RTL text. 44a25f0a04SGreg Roach const DIGITS = '0123456789٠١٢٣٤٥٦٧٨٩۰۱۲۳۴۵۶۷۸۹'; 45a25f0a04SGreg Roach 46991b93ddSGreg Roach // These locales need special handling for the dotless letter I. 47c1010edaSGreg Roach const DOTLESS_I_LOCALES = [ 48c1010edaSGreg Roach 'az', 49c1010edaSGreg Roach 'tr', 50c1010edaSGreg Roach ]; 51c1010edaSGreg Roach const DOTLESS_I_TOLOWER = [ 52c1010edaSGreg Roach 'I' => 'ı', 53c1010edaSGreg Roach 'İ' => 'i', 54c1010edaSGreg Roach ]; 55c1010edaSGreg Roach const DOTLESS_I_TOUPPER = [ 56c1010edaSGreg Roach 'ı' => 'I', 57c1010edaSGreg Roach 'i' => 'İ', 58c1010edaSGreg Roach ]; 59a25f0a04SGreg Roach 60991b93ddSGreg Roach // The ranges of characters used by each script. 61991b93ddSGreg Roach const SCRIPT_CHARACTER_RANGES = [ 62c1010edaSGreg Roach [ 63c1010edaSGreg Roach 'Latn', 64c1010edaSGreg Roach 0x0041, 65c1010edaSGreg Roach 0x005A, 66c1010edaSGreg Roach ], 67c1010edaSGreg Roach [ 68c1010edaSGreg Roach 'Latn', 69c1010edaSGreg Roach 0x0061, 70c1010edaSGreg Roach 0x007A, 71c1010edaSGreg Roach ], 72c1010edaSGreg Roach [ 73c1010edaSGreg Roach 'Latn', 74c1010edaSGreg Roach 0x0100, 75c1010edaSGreg Roach 0x02AF, 76c1010edaSGreg Roach ], 77c1010edaSGreg Roach [ 78c1010edaSGreg Roach 'Grek', 79c1010edaSGreg Roach 0x0370, 80c1010edaSGreg Roach 0x03FF, 81c1010edaSGreg Roach ], 82c1010edaSGreg Roach [ 83c1010edaSGreg Roach 'Cyrl', 84c1010edaSGreg Roach 0x0400, 85c1010edaSGreg Roach 0x052F, 86c1010edaSGreg Roach ], 87c1010edaSGreg Roach [ 88c1010edaSGreg Roach 'Hebr', 89c1010edaSGreg Roach 0x0590, 90c1010edaSGreg Roach 0x05FF, 91c1010edaSGreg Roach ], 92c1010edaSGreg Roach [ 93c1010edaSGreg Roach 'Arab', 94c1010edaSGreg Roach 0x0600, 95c1010edaSGreg Roach 0x06FF, 96c1010edaSGreg Roach ], 97c1010edaSGreg Roach [ 98c1010edaSGreg Roach 'Arab', 99c1010edaSGreg Roach 0x0750, 100c1010edaSGreg Roach 0x077F, 101c1010edaSGreg Roach ], 102c1010edaSGreg Roach [ 103c1010edaSGreg Roach 'Arab', 104c1010edaSGreg Roach 0x08A0, 105c1010edaSGreg Roach 0x08FF, 106c1010edaSGreg Roach ], 107c1010edaSGreg Roach [ 108c1010edaSGreg Roach 'Deva', 109c1010edaSGreg Roach 0x0900, 110c1010edaSGreg Roach 0x097F, 111c1010edaSGreg Roach ], 112c1010edaSGreg Roach [ 113c1010edaSGreg Roach 'Taml', 114c1010edaSGreg Roach 0x0B80, 115c1010edaSGreg Roach 0x0BFF, 116c1010edaSGreg Roach ], 117c1010edaSGreg Roach [ 118c1010edaSGreg Roach 'Sinh', 119c1010edaSGreg Roach 0x0D80, 120c1010edaSGreg Roach 0x0DFF, 121c1010edaSGreg Roach ], 122c1010edaSGreg Roach [ 123c1010edaSGreg Roach 'Thai', 124c1010edaSGreg Roach 0x0E00, 125c1010edaSGreg Roach 0x0E7F, 126c1010edaSGreg Roach ], 127c1010edaSGreg Roach [ 128c1010edaSGreg Roach 'Geor', 129c1010edaSGreg Roach 0x10A0, 130c1010edaSGreg Roach 0x10FF, 131c1010edaSGreg Roach ], 132c1010edaSGreg Roach [ 133c1010edaSGreg Roach 'Grek', 134c1010edaSGreg Roach 0x1F00, 135c1010edaSGreg Roach 0x1FFF, 136c1010edaSGreg Roach ], 137c1010edaSGreg Roach [ 138c1010edaSGreg Roach 'Deva', 139c1010edaSGreg Roach 0xA8E0, 140c1010edaSGreg Roach 0xA8FF, 141c1010edaSGreg Roach ], 142c1010edaSGreg Roach [ 143c1010edaSGreg Roach 'Hans', 144c1010edaSGreg Roach 0x3000, 145c1010edaSGreg Roach 0x303F, 146c1010edaSGreg Roach ], 147c1010edaSGreg Roach // Mixed CJK, not just Hans 148c1010edaSGreg Roach [ 149c1010edaSGreg Roach 'Hans', 150c1010edaSGreg Roach 0x3400, 151c1010edaSGreg Roach 0xFAFF, 152c1010edaSGreg Roach ], 153c1010edaSGreg Roach // Mixed CJK, not just Hans 154c1010edaSGreg Roach [ 155c1010edaSGreg Roach 'Hans', 156c1010edaSGreg Roach 0x20000, 157c1010edaSGreg Roach 0x2FA1F, 158c1010edaSGreg Roach ], 159c1010edaSGreg Roach // Mixed CJK, not just Hans 16013abd6f3SGreg Roach ]; 161a25f0a04SGreg Roach 162991b93ddSGreg Roach // Characters that are displayed in mirror form in RTL text. 163991b93ddSGreg Roach const MIRROR_CHARACTERS = [ 164a25f0a04SGreg Roach '(' => ')', 165a25f0a04SGreg Roach ')' => '(', 166a25f0a04SGreg Roach '[' => ']', 167a25f0a04SGreg Roach ']' => '[', 168a25f0a04SGreg Roach '{' => '}', 169a25f0a04SGreg Roach '}' => '{', 170a25f0a04SGreg Roach '<' => '>', 171a25f0a04SGreg Roach '>' => '<', 172a25f0a04SGreg Roach '‹ ' => '›', 173a25f0a04SGreg Roach '› ' => '‹', 174a25f0a04SGreg Roach '«' => '»', 175a25f0a04SGreg Roach '»' => '«', 176a25f0a04SGreg Roach '﴾ ' => '﴿', 177a25f0a04SGreg Roach '﴿ ' => '﴾', 178a25f0a04SGreg Roach '“ ' => '”', 179a25f0a04SGreg Roach '” ' => '“', 180a25f0a04SGreg Roach '‘ ' => '’', 181a25f0a04SGreg Roach '’ ' => '‘', 18213abd6f3SGreg Roach ]; 183a25f0a04SGreg Roach 184991b93ddSGreg Roach // Default list of locales to show in the menu. 185991b93ddSGreg Roach const DEFAULT_LOCALES = [ 186c1010edaSGreg Roach 'ar', 187c1010edaSGreg Roach 'bg', 188c1010edaSGreg Roach 'bs', 189c1010edaSGreg Roach 'ca', 190c1010edaSGreg Roach 'cs', 191c1010edaSGreg Roach 'da', 192c1010edaSGreg Roach 'de', 193c1010edaSGreg Roach 'el', 194c1010edaSGreg Roach 'en-GB', 195c1010edaSGreg Roach 'en-US', 196c1010edaSGreg Roach 'es', 197c1010edaSGreg Roach 'et', 198c1010edaSGreg Roach 'fi', 199c1010edaSGreg Roach 'fr', 200c1010edaSGreg Roach 'he', 201c1010edaSGreg Roach 'hr', 202c1010edaSGreg Roach 'hu', 203c1010edaSGreg Roach 'is', 204c1010edaSGreg Roach 'it', 205c1010edaSGreg Roach 'ka', 206c1010edaSGreg Roach 'kk', 207c1010edaSGreg Roach 'lt', 208c1010edaSGreg Roach 'mr', 209c1010edaSGreg Roach 'nb', 210c1010edaSGreg Roach 'nl', 211c1010edaSGreg Roach 'nn', 212c1010edaSGreg Roach 'pl', 213c1010edaSGreg Roach 'pt', 214c1010edaSGreg Roach 'ru', 215c1010edaSGreg Roach 'sk', 216c1010edaSGreg Roach 'sv', 217c1010edaSGreg Roach 'tr', 218c1010edaSGreg Roach 'uk', 219c1010edaSGreg Roach 'vi', 220c1010edaSGreg Roach 'zh-Hans', 221991b93ddSGreg Roach ]; 222991b93ddSGreg Roach 223a25f0a04SGreg Roach /** @var string Punctuation used to separate list items, typically a comma */ 224a25f0a04SGreg Roach public static $list_separator; 225a25f0a04SGreg Roach 226a25f0a04SGreg Roach /** 227dfeee0a8SGreg Roach * The prefered locales for this site, or a default list if no preference. 228dfeee0a8SGreg Roach * 229dfeee0a8SGreg Roach * @return LocaleInterface[] 230dfeee0a8SGreg Roach */ 2318f53f488SRico Sonntag public static function activeLocales(): array 232c1010edaSGreg Roach { 233dfeee0a8SGreg Roach $code_list = Site::getPreference('LANGUAGES'); 234dfeee0a8SGreg Roach 23515d603e7SGreg Roach if ($code_list === '') { 236991b93ddSGreg Roach $codes = self::DEFAULT_LOCALES; 237dfeee0a8SGreg Roach } else { 238991b93ddSGreg Roach $codes = explode(',', $code_list); 239dfeee0a8SGreg Roach } 240dfeee0a8SGreg Roach 24113abd6f3SGreg Roach $locales = []; 242dfeee0a8SGreg Roach foreach ($codes as $code) { 243dfeee0a8SGreg Roach if (file_exists(WT_ROOT . 'language/' . $code . '.mo')) { 244dfeee0a8SGreg Roach try { 245dfeee0a8SGreg Roach $locales[] = Locale::create($code); 246dfeee0a8SGreg Roach } catch (\Exception $ex) { 247bd52fa32SGreg Roach DebugBar::addThrowable($ex); 248bd52fa32SGreg Roach 249dfeee0a8SGreg Roach // No such locale exists? 250dfeee0a8SGreg Roach } 251dfeee0a8SGreg Roach } 252dfeee0a8SGreg Roach } 253dfeee0a8SGreg Roach usort($locales, '\Fisharebest\Localization\Locale::compare'); 254dfeee0a8SGreg Roach 255dfeee0a8SGreg Roach return $locales; 256dfeee0a8SGreg Roach } 257dfeee0a8SGreg Roach 258dfeee0a8SGreg Roach /** 259dfeee0a8SGreg Roach * Which MySQL collation should be used for this locale? 260dfeee0a8SGreg Roach * 261dfeee0a8SGreg Roach * @return string 262dfeee0a8SGreg Roach */ 263c1010edaSGreg Roach public static function collation() 264c1010edaSGreg Roach { 265dfeee0a8SGreg Roach $collation = self::$locale->collation(); 266dfeee0a8SGreg Roach switch ($collation) { 267dfeee0a8SGreg Roach case 'croatian_ci': 268dfeee0a8SGreg Roach case 'german2_ci': 269dfeee0a8SGreg Roach case 'vietnamese_ci': 270dfeee0a8SGreg Roach // Only available in MySQL 5.6 271dfeee0a8SGreg Roach return 'utf8_unicode_ci'; 272dfeee0a8SGreg Roach default: 273dfeee0a8SGreg Roach return 'utf8_' . $collation; 274dfeee0a8SGreg Roach } 275dfeee0a8SGreg Roach } 276dfeee0a8SGreg Roach 277dfeee0a8SGreg Roach /** 278dfeee0a8SGreg Roach * What format is used to display dates in the current locale? 279dfeee0a8SGreg Roach * 280dfeee0a8SGreg Roach * @return string 281dfeee0a8SGreg Roach */ 2828f53f488SRico Sonntag public static function dateFormat(): string 283c1010edaSGreg Roach { 284bbb76c12SGreg Roach /* I18N: This is the format string for full dates. See http://php.net/date for codes */ 285bbb76c12SGreg Roach return self::$translator->translate('%j %F %Y'); 286dfeee0a8SGreg Roach } 287dfeee0a8SGreg Roach 288dfeee0a8SGreg Roach /** 289dfeee0a8SGreg Roach * Generate consistent I18N for datatables.js 290dfeee0a8SGreg Roach * 29155664801SGreg Roach * @param int[] $lengths An optional array of page lengths 292dfeee0a8SGreg Roach * 293dfeee0a8SGreg Roach * @return string 294dfeee0a8SGreg Roach */ 295c1010edaSGreg Roach public static function datatablesI18N(array $lengths = [ 296c1010edaSGreg Roach 10, 297c1010edaSGreg Roach 20, 298c1010edaSGreg Roach 30, 299c1010edaSGreg Roach 50, 300c1010edaSGreg Roach 100, 301c1010edaSGreg Roach -1, 30255664801SGreg Roach ]): string 30355664801SGreg Roach { 30455664801SGreg Roach $length_options = Bootstrap4::select(FunctionsEdit::numericOptions($lengths), '10'); 305dfeee0a8SGreg Roach 306dfeee0a8SGreg Roach return 3072d9b2ebaSGreg Roach '"formatNumber": function(n) { return String(n).replace(/[0-9]/g, function(w) { return ("' . self::$locale->digits('0123456789') . '")[+w]; }); },' . 308dfeee0a8SGreg Roach '"language": {' . 309dfeee0a8SGreg Roach ' "paginate": {' . 310bbb76c12SGreg Roach ' "first": "' . self::translate('first') . '",' . 311bbb76c12SGreg Roach ' "last": "' . self::translate('last') . '",' . 312bbb76c12SGreg Roach ' "next": "' . self::translate('next') . '",' . 313bbb76c12SGreg Roach ' "previous": "' . self::translate('previous') . '"' . 314dfeee0a8SGreg Roach ' },' . 315dfeee0a8SGreg Roach ' "emptyTable": "' . self::translate('No records to display') . '",' . 316c1010edaSGreg Roach ' "info": "' . /* I18N: %s are placeholders for numbers */ 317c1010edaSGreg Roach self::translate('Showing %1$s to %2$s of %3$s', '_START_', '_END_', '_TOTAL_') . '",' . 3180b6446e1SGreg Roach ' "infoEmpty": "' . self::translate('Showing %1$s to %2$s of %3$s', self::$locale->digits('0'), self::$locale->digits('0'), self::$locale->digits('0')) . '",' . 319c1010edaSGreg Roach ' "infoFiltered": "' . /* I18N: %s is a placeholder for a number */ 320c1010edaSGreg Roach self::translate('(filtered from %s total entries)', '_MAX_') . '",' . 321c1010edaSGreg Roach ' "lengthMenu": "' . /* I18N: %s is a number of records per page */ 322c1010edaSGreg Roach self::translate('Display %s', addslashes($length_options)) . '",' . 323dfeee0a8SGreg Roach ' "loadingRecords": "' . self::translate('Loading…') . '",' . 324dfeee0a8SGreg Roach ' "processing": "' . self::translate('Loading…') . '",' . 325dfeee0a8SGreg Roach ' "search": "' . self::translate('Filter') . '",' . 326dfeee0a8SGreg Roach ' "zeroRecords": "' . self::translate('No records to display') . '"' . 3272d9b2ebaSGreg Roach '}'; 328dfeee0a8SGreg Roach } 329dfeee0a8SGreg Roach 330dfeee0a8SGreg Roach /** 331dfeee0a8SGreg Roach * Convert the digits 0-9 into the local script 332dfeee0a8SGreg Roach * 333dfeee0a8SGreg Roach * Used for years, etc., where we do not want thousands-separators, decimals, etc. 334dfeee0a8SGreg Roach * 33555664801SGreg Roach * @param string|int $n 336dfeee0a8SGreg Roach * 337dfeee0a8SGreg Roach * @return string 338dfeee0a8SGreg Roach */ 3398f53f488SRico Sonntag public static function digits($n): string 340c1010edaSGreg Roach { 34155664801SGreg Roach return self::$locale->digits((string) $n); 342dfeee0a8SGreg Roach } 343dfeee0a8SGreg Roach 344dfeee0a8SGreg Roach /** 345dfeee0a8SGreg Roach * What is the direction of the current locale 346dfeee0a8SGreg Roach * 347dfeee0a8SGreg Roach * @return string "ltr" or "rtl" 348dfeee0a8SGreg Roach */ 3498f53f488SRico Sonntag public static function direction(): string 350c1010edaSGreg Roach { 351dfeee0a8SGreg Roach return self::$locale->direction(); 352dfeee0a8SGreg Roach } 353dfeee0a8SGreg Roach 354dfeee0a8SGreg Roach /** 3557231a557SGreg Roach * What is the first day of the week. 3567231a557SGreg Roach * 357cbc1590aSGreg Roach * @return int Sunday=0, Monday=1, etc. 3587231a557SGreg Roach */ 3598f53f488SRico Sonntag public static function firstDay(): int 360c1010edaSGreg Roach { 3617231a557SGreg Roach return self::$locale->territory()->firstDay(); 3627231a557SGreg Roach } 3637231a557SGreg Roach 3647231a557SGreg Roach /** 365dfeee0a8SGreg Roach * Generate i18n markup for the <html> tag, e.g. lang="ar" dir="rtl" 366dfeee0a8SGreg Roach * 367dfeee0a8SGreg Roach * @return string 368dfeee0a8SGreg Roach */ 3698f53f488SRico Sonntag public static function htmlAttributes(): string 370c1010edaSGreg Roach { 371dfeee0a8SGreg Roach return self::$locale->htmlAttributes(); 372dfeee0a8SGreg Roach } 373dfeee0a8SGreg Roach 374dfeee0a8SGreg Roach /** 375a25f0a04SGreg Roach * Initialise the translation adapter with a locale setting. 376a25f0a04SGreg Roach * 37715d603e7SGreg Roach * @param string $code Use this locale/language code, or choose one automatically 378e58a20ffSGreg Roach * @param Tree|null $tree 379a25f0a04SGreg Roach * 380a25f0a04SGreg Roach * @return string $string 381a25f0a04SGreg Roach */ 382e58a20ffSGreg Roach public static function init(string $code = '', Tree $tree = null): string 383c1010edaSGreg Roach { 384c314ecc9SGreg Roach mb_internal_encoding('UTF-8'); 385c314ecc9SGreg Roach 38615d603e7SGreg Roach if ($code !== '') { 3873bdc890bSGreg Roach // Create the specified locale 3883bdc890bSGreg Roach self::$locale = Locale::create($code); 389e58a20ffSGreg Roach } elseif (Session::has('locale') && file_exists(WT_ROOT . 'language/' . Session::get('locale') . '.mo')) { 390e58a20ffSGreg Roach // Select a previously used locale 39131bc7874SGreg Roach self::$locale = Locale::create(Session::get('locale')); 3923bdc890bSGreg Roach } else { 393e58a20ffSGreg Roach if ($tree instanceof Tree) { 394e58a20ffSGreg Roach $default_locale = Locale::create($tree->getPreference('LANGUAGE', 'en-US')); 395e58a20ffSGreg Roach } else { 39659f2f229SGreg Roach $default_locale = new LocaleEnUs(); 3973bdc890bSGreg Roach } 398e58a20ffSGreg Roach 399e58a20ffSGreg Roach // Negotiate with the browser. 400e58a20ffSGreg Roach // Search engines don't negotiate. They get the default locale of the tree. 401149573a1SGreg Roach self::$locale = Locale::httpAcceptLanguage($_SERVER, self::installedLocales(), $default_locale); 4023bdc890bSGreg Roach } 4033bdc890bSGreg Roach 404f1af7e1cSGreg Roach $cache_dir = WT_DATA_DIR . 'cache/'; 405f1af7e1cSGreg Roach $cache_file = $cache_dir . 'language-' . self::$locale->languageTag() . '-cache.php'; 4063bdc890bSGreg Roach if (file_exists($cache_file)) { 4073bdc890bSGreg Roach $filemtime = filemtime($cache_file); 4083bdc890bSGreg Roach } else { 4093bdc890bSGreg Roach $filemtime = 0; 4103bdc890bSGreg Roach } 4113bdc890bSGreg Roach 4123bdc890bSGreg Roach // Load the translation file(s) 4137d6e38dfSGreg Roach // Note that glob() returns false instead of an empty array when open_basedir_restriction 4147d6e38dfSGreg Roach // is in force and no files are found. See PHP bug #47358. 4157a7f87d7SGreg Roach if (defined('GLOB_BRACE')) { 4163bdc890bSGreg Roach $translation_files = array_merge( 41713abd6f3SGreg Roach [WT_ROOT . 'language/' . self::$locale->languageTag() . '.mo'], 418*8d0ebef0SGreg Roach glob(Webtrees::MODULES_PATH . '*/language/' . self::$locale->languageTag() . '.{csv,php,mo}', GLOB_BRACE) ?: [], 41913abd6f3SGreg Roach glob(WT_DATA_DIR . 'language/' . self::$locale->languageTag() . '.{csv,php,mo}', GLOB_BRACE) ?: [] 420a25f0a04SGreg Roach ); 4217a7f87d7SGreg Roach } else { 4227a7f87d7SGreg Roach // Some servers do not have GLOB_BRACE - see http://php.net/manual/en/function.glob.php 4237a7f87d7SGreg Roach $translation_files = array_merge( 42413abd6f3SGreg Roach [WT_ROOT . 'language/' . self::$locale->languageTag() . '.mo'], 425*8d0ebef0SGreg Roach glob(Webtrees::MODULES_PATH . '*/language/' . self::$locale->languageTag() . '.csv') ?: [], 426*8d0ebef0SGreg Roach glob(Webtrees::MODULES_PATH . '*/language/' . self::$locale->languageTag() . '.php') ?: [], 427*8d0ebef0SGreg Roach glob(Webtrees::MODULES_PATH . '*/language/' . self::$locale->languageTag() . '.mo') ?: [], 42813abd6f3SGreg Roach glob(WT_DATA_DIR . 'language/' . self::$locale->languageTag() . '.csv') ?: [], 42913abd6f3SGreg Roach glob(WT_DATA_DIR . 'language/' . self::$locale->languageTag() . '.php') ?: [], 43013abd6f3SGreg Roach glob(WT_DATA_DIR . 'language/' . self::$locale->languageTag() . '.mo') ?: [] 4317a7f87d7SGreg Roach ); 4327a7f87d7SGreg Roach } 4337a7f87d7SGreg Roach // Rebuild files after one hour 4347a7f87d7SGreg Roach $rebuild_cache = time() > $filemtime + 3600; 4351e71bdc0SGreg Roach // Rebuild files if any translation file has been updated 4363bdc890bSGreg Roach foreach ($translation_files as $translation_file) { 4373bdc890bSGreg Roach if (filemtime($translation_file) > $filemtime) { 4383bdc890bSGreg Roach $rebuild_cache = true; 439a25f0a04SGreg Roach break; 440a25f0a04SGreg Roach } 441a25f0a04SGreg Roach } 4423bdc890bSGreg Roach 4433bdc890bSGreg Roach if ($rebuild_cache) { 44413abd6f3SGreg Roach $translations = []; 4453bdc890bSGreg Roach foreach ($translation_files as $translation_file) { 4463bdc890bSGreg Roach $translation = new Translation($translation_file); 4473bdc890bSGreg Roach $translations = array_merge($translations, $translation->asArray()); 448a25f0a04SGreg Roach } 449f1af7e1cSGreg Roach try { 450f1af7e1cSGreg Roach File::mkdir($cache_dir); 451f1af7e1cSGreg Roach file_put_contents($cache_file, '<?php return ' . var_export($translations, true) . ';'); 452f1af7e1cSGreg Roach } catch (Exception $ex) { 453bd52fa32SGreg Roach DebugBar::addThrowable($ex); 454bd52fa32SGreg Roach 4557c2999b4SGreg Roach // During setup, we may not have been able to create it. 456c85fb0c4SGreg Roach } 4573bdc890bSGreg Roach } else { 4583bdc890bSGreg Roach $translations = include $cache_file; 459a25f0a04SGreg Roach } 460a25f0a04SGreg Roach 4613bdc890bSGreg Roach // Create a translator 4623bdc890bSGreg Roach self::$translator = new Translator($translations, self::$locale->pluralRule()); 463a25f0a04SGreg Roach 464bbb76c12SGreg Roach /* I18N: This punctuation is used to separate lists of items */ 465bbb76c12SGreg Roach self::$list_separator = self::translate(', '); 466a25f0a04SGreg Roach 467991b93ddSGreg Roach // Create a collator 468991b93ddSGreg Roach try { 469444a65ecSGreg Roach if (class_exists('Collator')) { 470c9ec599fSGreg Roach // Symfony provides a very incomplete polyfill - which cannot be used. 471991b93ddSGreg Roach self::$collator = new Collator(self::$locale->code()); 472991b93ddSGreg Roach // Ignore upper/lower case differences 473991b93ddSGreg Roach self::$collator->setStrength(Collator::SECONDARY); 474444a65ecSGreg Roach } 475991b93ddSGreg Roach } catch (Exception $ex) { 476991b93ddSGreg Roach // PHP-INTL is not installed? We'll use a fallback later. 477c9ec599fSGreg Roach self::$collator = null; 478991b93ddSGreg Roach } 479991b93ddSGreg Roach 4805331c5eaSGreg Roach return self::$locale->languageTag(); 481a25f0a04SGreg Roach } 482a25f0a04SGreg Roach 483a25f0a04SGreg Roach /** 484c999a340SGreg Roach * All locales for which a translation file exists. 485c999a340SGreg Roach * 48615834aaeSGreg Roach * @return LocaleInterface[] 487c999a340SGreg Roach */ 4888f53f488SRico Sonntag public static function installedLocales(): array 489c1010edaSGreg Roach { 49013abd6f3SGreg Roach $locales = []; 491c999a340SGreg Roach foreach (glob(WT_ROOT . 'language/*.mo') as $file) { 492c999a340SGreg Roach try { 493c999a340SGreg Roach $locales[] = Locale::create(basename($file, '.mo')); 494c999a340SGreg Roach } catch (\Exception $ex) { 495bd52fa32SGreg Roach DebugBar::addThrowable($ex); 496bd52fa32SGreg Roach 4973bdc890bSGreg Roach // Not a recognised locale 498a25f0a04SGreg Roach } 499a25f0a04SGreg Roach } 500c999a340SGreg Roach usort($locales, '\Fisharebest\Localization\Locale::compare'); 501c999a340SGreg Roach 502c999a340SGreg Roach return $locales; 503a25f0a04SGreg Roach } 504a25f0a04SGreg Roach 505a25f0a04SGreg Roach /** 506a25f0a04SGreg Roach * Return the endonym for a given language - as per http://cldr.unicode.org/ 507a25f0a04SGreg Roach * 508a25f0a04SGreg Roach * @param string $locale 509a25f0a04SGreg Roach * 510a25f0a04SGreg Roach * @return string 511a25f0a04SGreg Roach */ 51255664801SGreg Roach public static function languageName(string $locale): string 513c1010edaSGreg Roach { 514c999a340SGreg Roach return Locale::create($locale)->endonym(); 515a25f0a04SGreg Roach } 516a25f0a04SGreg Roach 517a25f0a04SGreg Roach /** 518a25f0a04SGreg Roach * Return the script used by a given language 519a25f0a04SGreg Roach * 520a25f0a04SGreg Roach * @param string $locale 521a25f0a04SGreg Roach * 522a25f0a04SGreg Roach * @return string 523a25f0a04SGreg Roach */ 52455664801SGreg Roach public static function languageScript(string $locale): string 525c1010edaSGreg Roach { 526c999a340SGreg Roach return Locale::create($locale)->script()->code(); 527a25f0a04SGreg Roach } 528a25f0a04SGreg Roach 529a25f0a04SGreg Roach /** 530dfeee0a8SGreg Roach * Translate a number into the local representation. 531a25f0a04SGreg Roach * 532dfeee0a8SGreg Roach * e.g. 12345.67 becomes 533dfeee0a8SGreg Roach * en: 12,345.67 534dfeee0a8SGreg Roach * fr: 12 345,67 535dfeee0a8SGreg Roach * de: 12.345,67 536dfeee0a8SGreg Roach * 537dfeee0a8SGreg Roach * @param float $n 538cbc1590aSGreg Roach * @param int $precision 539a25f0a04SGreg Roach * 540a25f0a04SGreg Roach * @return string 541a25f0a04SGreg Roach */ 54255664801SGreg Roach public static function number(float $n, int $precision = 0): string 543c1010edaSGreg Roach { 544dfeee0a8SGreg Roach return self::$locale->number(round($n, $precision)); 545dfeee0a8SGreg Roach } 546dfeee0a8SGreg Roach 547dfeee0a8SGreg Roach /** 548dfeee0a8SGreg Roach * Translate a fraction into a percentage. 549dfeee0a8SGreg Roach * 550dfeee0a8SGreg Roach * e.g. 0.123 becomes 551dfeee0a8SGreg Roach * en: 12.3% 552dfeee0a8SGreg Roach * fr: 12,3 % 553dfeee0a8SGreg Roach * de: 12,3% 554dfeee0a8SGreg Roach * 555dfeee0a8SGreg Roach * @param float $n 556cbc1590aSGreg Roach * @param int $precision 557dfeee0a8SGreg Roach * 558dfeee0a8SGreg Roach * @return string 559dfeee0a8SGreg Roach */ 56055664801SGreg Roach public static function percentage(float $n, int $precision = 0): string 561c1010edaSGreg Roach { 562dfeee0a8SGreg Roach return self::$locale->percent(round($n, $precision + 2)); 563dfeee0a8SGreg Roach } 564dfeee0a8SGreg Roach 565dfeee0a8SGreg Roach /** 566dfeee0a8SGreg Roach * Translate a plural string 567dfeee0a8SGreg Roach * echo self::plural('There is an error', 'There are errors', $num_errors); 568dfeee0a8SGreg Roach * echo self::plural('There is one error', 'There are %s errors', $num_errors); 569dfeee0a8SGreg Roach * echo self::plural('There is %1$s %2$s cat', 'There are %1$s %2$s cats', $num, $num, $colour); 570dfeee0a8SGreg Roach * 571924d091bSGreg Roach * @param string $singular 572924d091bSGreg Roach * @param string $plural 573924d091bSGreg Roach * @param int $count 574a515be7cSGreg Roach * @param string ...$args 575e93111adSRico Sonntag * 576dfeee0a8SGreg Roach * @return string 577dfeee0a8SGreg Roach */ 578924d091bSGreg Roach public static function plural(string $singular, string $plural, int $count, ...$args): string 579c1010edaSGreg Roach { 580924d091bSGreg Roach $message = self::$translator->translatePlural($singular, $plural, $count); 581dfeee0a8SGreg Roach 582924d091bSGreg Roach return sprintf($message, ...$args); 583dfeee0a8SGreg Roach } 584dfeee0a8SGreg Roach 585dfeee0a8SGreg Roach /** 586dfeee0a8SGreg Roach * UTF8 version of PHP::strrev() 587dfeee0a8SGreg Roach * 588dfeee0a8SGreg Roach * Reverse RTL text for third-party libraries such as GD2 and googlechart. 589dfeee0a8SGreg Roach * 590dfeee0a8SGreg Roach * These do not support UTF8 text direction, so we must mimic it for them. 591dfeee0a8SGreg Roach * 592dfeee0a8SGreg Roach * Numbers are always rendered LTR, even in RTL text. 593dfeee0a8SGreg Roach * The visual direction of characters such as parentheses should be reversed. 594dfeee0a8SGreg Roach * 595dfeee0a8SGreg Roach * @param string $text Text to be reversed 596dfeee0a8SGreg Roach * 597dfeee0a8SGreg Roach * @return string 598dfeee0a8SGreg Roach */ 5998f53f488SRico Sonntag public static function reverseText($text): string 600c1010edaSGreg Roach { 601dfeee0a8SGreg Roach // Remove HTML markup - we can't display it and it is LTR. 6029524b7b5SGreg Roach $text = strip_tags($text); 6039524b7b5SGreg Roach // Remove HTML entities. 6049524b7b5SGreg Roach $text = html_entity_decode($text, ENT_QUOTES, 'UTF-8'); 605dfeee0a8SGreg Roach 606dfeee0a8SGreg Roach // LTR text doesn't need reversing 607dfeee0a8SGreg Roach if (self::scriptDirection(self::textScript($text)) === 'ltr') { 608dfeee0a8SGreg Roach return $text; 609dfeee0a8SGreg Roach } 610dfeee0a8SGreg Roach 611dfeee0a8SGreg Roach // Mirrored characters 612991b93ddSGreg Roach $text = strtr($text, self::MIRROR_CHARACTERS); 613dfeee0a8SGreg Roach 614dfeee0a8SGreg Roach $reversed = ''; 615dfeee0a8SGreg Roach $digits = ''; 616dfeee0a8SGreg Roach while ($text != '') { 617dfeee0a8SGreg Roach $letter = mb_substr($text, 0, 1); 618dfeee0a8SGreg Roach $text = mb_substr($text, 1); 619dfeee0a8SGreg Roach if (strpos(self::DIGITS, $letter) !== false) { 620dfeee0a8SGreg Roach $digits .= $letter; 621a25f0a04SGreg Roach } else { 622dfeee0a8SGreg Roach $reversed = $letter . $digits . $reversed; 623dfeee0a8SGreg Roach $digits = ''; 624dfeee0a8SGreg Roach } 625a25f0a04SGreg Roach } 626a25f0a04SGreg Roach 627dfeee0a8SGreg Roach return $digits . $reversed; 628a25f0a04SGreg Roach } 629a25f0a04SGreg Roach 630a25f0a04SGreg Roach /** 631a25f0a04SGreg Roach * Return the direction (ltr or rtl) for a given script 632a25f0a04SGreg Roach * 633a25f0a04SGreg Roach * The PHP/intl library does not provde this information, so we need 634a25f0a04SGreg Roach * our own lookup table. 635a25f0a04SGreg Roach * 636a25f0a04SGreg Roach * @param string $script 637a25f0a04SGreg Roach * 638a25f0a04SGreg Roach * @return string 639a25f0a04SGreg Roach */ 640c1010edaSGreg Roach public static function scriptDirection($script) 641c1010edaSGreg Roach { 642a25f0a04SGreg Roach switch ($script) { 643a25f0a04SGreg Roach case 'Arab': 644a25f0a04SGreg Roach case 'Hebr': 645a25f0a04SGreg Roach case 'Mong': 646a25f0a04SGreg Roach case 'Thaa': 647a25f0a04SGreg Roach return 'rtl'; 648a25f0a04SGreg Roach default: 649a25f0a04SGreg Roach return 'ltr'; 650a25f0a04SGreg Roach } 651a25f0a04SGreg Roach } 652a25f0a04SGreg Roach 653a25f0a04SGreg Roach /** 654991b93ddSGreg Roach * Perform a case-insensitive comparison of two strings. 655a25f0a04SGreg Roach * 656a25f0a04SGreg Roach * @param string $string1 657a25f0a04SGreg Roach * @param string $string2 658a25f0a04SGreg Roach * 659cbc1590aSGreg Roach * @return int 660a25f0a04SGreg Roach */ 661c1010edaSGreg Roach public static function strcasecmp($string1, $string2) 662c1010edaSGreg Roach { 663991b93ddSGreg Roach if (self::$collator instanceof Collator) { 664991b93ddSGreg Roach return self::$collator->compare($string1, $string2); 665c9ec599fSGreg Roach } else { 666b2ce94c6SRico Sonntag return strcmp(self::strtolower($string1), self::strtolower($string2)); 667a25f0a04SGreg Roach } 668c9ec599fSGreg Roach } 669a25f0a04SGreg Roach 670a25f0a04SGreg Roach /** 671991b93ddSGreg Roach * Convert a string to lower case. 672a25f0a04SGreg Roach * 673dfeee0a8SGreg Roach * @param string $string 674a25f0a04SGreg Roach * 675a25f0a04SGreg Roach * @return string 676a25f0a04SGreg Roach */ 6778f53f488SRico Sonntag public static function strtolower($string): string 678c1010edaSGreg Roach { 679991b93ddSGreg Roach if (in_array(self::$locale->language()->code(), self::DOTLESS_I_LOCALES)) { 680991b93ddSGreg Roach $string = strtr($string, self::DOTLESS_I_TOLOWER); 681a25f0a04SGreg Roach } 6825ddad20bSGreg Roach 6835ddad20bSGreg Roach return mb_strtolower($string); 684a25f0a04SGreg Roach } 685a25f0a04SGreg Roach 686a25f0a04SGreg Roach /** 687991b93ddSGreg Roach * Convert a string to upper case. 688dfeee0a8SGreg Roach * 689dfeee0a8SGreg Roach * @param string $string 690a25f0a04SGreg Roach * 691a25f0a04SGreg Roach * @return string 692a25f0a04SGreg Roach */ 6938f53f488SRico Sonntag public static function strtoupper($string): string 694c1010edaSGreg Roach { 695991b93ddSGreg Roach if (in_array(self::$locale->language()->code(), self::DOTLESS_I_LOCALES)) { 696991b93ddSGreg Roach $string = strtr($string, self::DOTLESS_I_TOUPPER); 697a25f0a04SGreg Roach } 6985ddad20bSGreg Roach 6995ddad20bSGreg Roach return mb_strtoupper($string); 700a25f0a04SGreg Roach } 701a25f0a04SGreg Roach 702dfeee0a8SGreg Roach /** 703dfeee0a8SGreg Roach * Identify the script used for a piece of text 704dfeee0a8SGreg Roach * 705d0bfc631SGreg Roach * @param string $string 706dfeee0a8SGreg Roach * 707dfeee0a8SGreg Roach * @return string 708dfeee0a8SGreg Roach */ 7098f53f488SRico Sonntag public static function textScript($string): string 710c1010edaSGreg Roach { 711dfeee0a8SGreg Roach $string = strip_tags($string); // otherwise HTML tags show up as latin 712dfeee0a8SGreg Roach $string = html_entity_decode($string, ENT_QUOTES, 'UTF-8'); // otherwise HTML entities show up as latin 713c1010edaSGreg Roach $string = str_replace([ 714c1010edaSGreg Roach '@N.N.', 715c1010edaSGreg Roach '@P.N.', 716c1010edaSGreg Roach ], '', $string); // otherwise unknown names show up as latin 717dfeee0a8SGreg Roach $pos = 0; 718dfeee0a8SGreg Roach $strlen = strlen($string); 719dfeee0a8SGreg Roach while ($pos < $strlen) { 720dfeee0a8SGreg Roach // get the Unicode Code Point for the character at position $pos 721dfeee0a8SGreg Roach $byte1 = ord($string[$pos]); 722dfeee0a8SGreg Roach if ($byte1 < 0x80) { 723dfeee0a8SGreg Roach $code_point = $byte1; 724dfeee0a8SGreg Roach $chrlen = 1; 725dfeee0a8SGreg Roach } elseif ($byte1 < 0xC0) { 726dfeee0a8SGreg Roach // Invalid continuation character 727dfeee0a8SGreg Roach return 'Latn'; 728dfeee0a8SGreg Roach } elseif ($byte1 < 0xE0) { 729dfeee0a8SGreg Roach $code_point = (($byte1 & 0x1F) << 6) + (ord($string[$pos + 1]) & 0x3F); 730dfeee0a8SGreg Roach $chrlen = 2; 731dfeee0a8SGreg Roach } elseif ($byte1 < 0xF0) { 732dfeee0a8SGreg Roach $code_point = (($byte1 & 0x0F) << 12) + ((ord($string[$pos + 1]) & 0x3F) << 6) + (ord($string[$pos + 2]) & 0x3F); 733dfeee0a8SGreg Roach $chrlen = 3; 734dfeee0a8SGreg Roach } elseif ($byte1 < 0xF8) { 735dfeee0a8SGreg Roach $code_point = (($byte1 & 0x07) << 24) + ((ord($string[$pos + 1]) & 0x3F) << 12) + ((ord($string[$pos + 2]) & 0x3F) << 6) + (ord($string[$pos + 3]) & 0x3F); 736dfeee0a8SGreg Roach $chrlen = 3; 737dfeee0a8SGreg Roach } else { 738dfeee0a8SGreg Roach // Invalid UTF 739dfeee0a8SGreg Roach return 'Latn'; 740dfeee0a8SGreg Roach } 741dfeee0a8SGreg Roach 742991b93ddSGreg Roach foreach (self::SCRIPT_CHARACTER_RANGES as $range) { 743dfeee0a8SGreg Roach if ($code_point >= $range[1] && $code_point <= $range[2]) { 744dfeee0a8SGreg Roach return $range[0]; 745dfeee0a8SGreg Roach } 746dfeee0a8SGreg Roach } 747dfeee0a8SGreg Roach // Not a recognised script. Maybe punctuation, spacing, etc. Keep looking. 748dfeee0a8SGreg Roach $pos += $chrlen; 749dfeee0a8SGreg Roach } 750dfeee0a8SGreg Roach 751dfeee0a8SGreg Roach return 'Latn'; 752dfeee0a8SGreg Roach } 753dfeee0a8SGreg Roach 754dfeee0a8SGreg Roach /** 755dfeee0a8SGreg Roach * Convert a number of seconds into a relative time. For example, 630 => "10 hours, 30 minutes ago" 756dfeee0a8SGreg Roach * 757cbc1590aSGreg Roach * @param int $seconds 758dfeee0a8SGreg Roach * 759dfeee0a8SGreg Roach * @return string 760dfeee0a8SGreg Roach */ 761c1010edaSGreg Roach public static function timeAgo($seconds) 762c1010edaSGreg Roach { 763dfeee0a8SGreg Roach $minute = 60; 764dfeee0a8SGreg Roach $hour = 60 * $minute; 765dfeee0a8SGreg Roach $day = 24 * $hour; 766dfeee0a8SGreg Roach $month = 30 * $day; 767dfeee0a8SGreg Roach $year = 365 * $day; 768dfeee0a8SGreg Roach 769dfeee0a8SGreg Roach if ($seconds > $year) { 770cdaafeeeSGreg Roach $years = intdiv($seconds, $year); 771cbc1590aSGreg Roach 772dfeee0a8SGreg Roach return self::plural('%s year ago', '%s years ago', $years, self::number($years)); 773b2ce94c6SRico Sonntag } 774b2ce94c6SRico Sonntag 775b2ce94c6SRico Sonntag if ($seconds > $month) { 776cdaafeeeSGreg Roach $months = intdiv($seconds, $month); 777cbc1590aSGreg Roach 778dfeee0a8SGreg Roach return self::plural('%s month ago', '%s months ago', $months, self::number($months)); 779b2ce94c6SRico Sonntag } 780b2ce94c6SRico Sonntag 781b2ce94c6SRico Sonntag if ($seconds > $day) { 782cdaafeeeSGreg Roach $days = intdiv($seconds, $day); 783cbc1590aSGreg Roach 784dfeee0a8SGreg Roach return self::plural('%s day ago', '%s days ago', $days, self::number($days)); 785b2ce94c6SRico Sonntag } 786b2ce94c6SRico Sonntag 787b2ce94c6SRico Sonntag if ($seconds > $hour) { 788cdaafeeeSGreg Roach $hours = intdiv($seconds, $hour); 789cbc1590aSGreg Roach 790dfeee0a8SGreg Roach return self::plural('%s hour ago', '%s hours ago', $hours, self::number($hours)); 791b2ce94c6SRico Sonntag } 792b2ce94c6SRico Sonntag 793b2ce94c6SRico Sonntag if ($seconds > $minute) { 794cdaafeeeSGreg Roach $minutes = intdiv($seconds, $minute); 795cbc1590aSGreg Roach 796dfeee0a8SGreg Roach return self::plural('%s minute ago', '%s minutes ago', $minutes, self::number($minutes)); 797dfeee0a8SGreg Roach } 798b2ce94c6SRico Sonntag 799b2ce94c6SRico Sonntag return self::plural('%s second ago', '%s seconds ago', $seconds, self::number($seconds)); 800dfeee0a8SGreg Roach } 801dfeee0a8SGreg Roach 802dfeee0a8SGreg Roach /** 803dfeee0a8SGreg Roach * What format is used to display dates in the current locale? 804dfeee0a8SGreg Roach * 805dfeee0a8SGreg Roach * @return string 806dfeee0a8SGreg Roach */ 8078f53f488SRico Sonntag public static function timeFormat(): string 808c1010edaSGreg Roach { 809bbb76c12SGreg Roach /* I18N: This is the format string for the time-of-day. See http://php.net/date for codes */ 810bbb76c12SGreg Roach return self::$translator->translate('%H:%i:%s'); 811dfeee0a8SGreg Roach } 812dfeee0a8SGreg Roach 813dfeee0a8SGreg Roach /** 814dfeee0a8SGreg Roach * Translate a string, and then substitute placeholders 815dfeee0a8SGreg Roach * 816dfeee0a8SGreg Roach * echo I18N::translate('Hello World!'); 817dfeee0a8SGreg Roach * echo I18N::translate('The %s sat on the mat', 'cat'); 818dfeee0a8SGreg Roach * 819924d091bSGreg Roach * @param string $message 820a515be7cSGreg Roach * @param string ...$args 821c3283ed7SGreg Roach * 822dfeee0a8SGreg Roach * @return string 823dfeee0a8SGreg Roach */ 824924d091bSGreg Roach public static function translate(string $message, ...$args): string 825c1010edaSGreg Roach { 826924d091bSGreg Roach $message = self::$translator->translate($message); 827dfeee0a8SGreg Roach 828924d091bSGreg Roach return sprintf($message, ...$args); 829dfeee0a8SGreg Roach } 830dfeee0a8SGreg Roach 831dfeee0a8SGreg Roach /** 832dfeee0a8SGreg Roach * Context sensitive version of translate. 833a4956c0eSGreg Roach * echo I18N::translateContext('NOMINATIVE', 'January'); 834a4956c0eSGreg Roach * echo I18N::translateContext('GENITIVE', 'January'); 835dfeee0a8SGreg Roach * 836924d091bSGreg Roach * @param string $context 837924d091bSGreg Roach * @param string $message 838a515be7cSGreg Roach * @param string ...$args 839c3283ed7SGreg Roach * 840dfeee0a8SGreg Roach * @return string 841dfeee0a8SGreg Roach */ 842924d091bSGreg Roach public static function translateContext(string $context, string $message, ...$args): string 843c1010edaSGreg Roach { 844924d091bSGreg Roach $message = self::$translator->translateContext($context, $message); 845dfeee0a8SGreg Roach 846924d091bSGreg Roach return sprintf($message, ...$args); 847a25f0a04SGreg Roach } 848a25f0a04SGreg Roach} 849