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 */ 1676692c8bSGreg Roachnamespace Fisharebest\Webtrees; 17a25f0a04SGreg Roach 18991b93ddSGreg Roachuse Collator; 19f1af7e1cSGreg Roachuse Exception; 2027a79457SGreg Roachuse Fisharebest\ExtCalendar\ArabicCalendar; 2127a79457SGreg Roachuse Fisharebest\ExtCalendar\CalendarInterface; 2227a79457SGreg Roachuse Fisharebest\ExtCalendar\GregorianCalendar; 2327a79457SGreg Roachuse Fisharebest\ExtCalendar\JewishCalendar; 2427a79457SGreg Roachuse Fisharebest\ExtCalendar\PersianCalendar; 25c999a340SGreg Roachuse Fisharebest\Localization\Locale; 261e71bdc0SGreg Roachuse Fisharebest\Localization\Locale\LocaleEnUs; 2715834aaeSGreg Roachuse Fisharebest\Localization\Locale\LocaleInterface; 283bdc890bSGreg Roachuse Fisharebest\Localization\Translation; 293bdc890bSGreg Roachuse Fisharebest\Localization\Translator; 3015d603e7SGreg Roachuse Fisharebest\Webtrees\Functions\FunctionsEdit; 31a25f0a04SGreg Roach 32a25f0a04SGreg Roach/** 3376692c8bSGreg Roach * Internationalization (i18n) and localization (l10n). 34a25f0a04SGreg Roach */ 35c1010edaSGreg Roachclass I18N 36c1010edaSGreg Roach{ 3715834aaeSGreg Roach /** @var LocaleInterface The current locale (e.g. LocaleEnGb) */ 38c999a340SGreg Roach private static $locale; 39c999a340SGreg Roach 4076692c8bSGreg Roach /** @var Translator An object that performs translation */ 413bdc890bSGreg Roach private static $translator; 423bdc890bSGreg Roach 43991b93ddSGreg Roach /** @var Collator From the php-intl library */ 44991b93ddSGreg Roach private static $collator; 45991b93ddSGreg Roach 46a25f0a04SGreg Roach // Digits are always rendered LTR, even in RTL text. 47a25f0a04SGreg Roach const DIGITS = '0123456789٠١٢٣٤٥٦٧٨٩۰۱۲۳۴۵۶۷۸۹'; 48a25f0a04SGreg Roach 49991b93ddSGreg Roach // These locales need special handling for the dotless letter I. 50c1010edaSGreg Roach const DOTLESS_I_LOCALES = [ 51c1010edaSGreg Roach 'az', 52c1010edaSGreg Roach 'tr', 53c1010edaSGreg Roach ]; 54c1010edaSGreg Roach const DOTLESS_I_TOLOWER = [ 55c1010edaSGreg Roach 'I' => 'ı', 56c1010edaSGreg Roach 'İ' => 'i', 57c1010edaSGreg Roach ]; 58c1010edaSGreg Roach const DOTLESS_I_TOUPPER = [ 59c1010edaSGreg Roach 'ı' => 'I', 60c1010edaSGreg Roach 'i' => 'İ', 61c1010edaSGreg Roach ]; 62a25f0a04SGreg Roach 63991b93ddSGreg Roach // The ranges of characters used by each script. 64991b93ddSGreg Roach const SCRIPT_CHARACTER_RANGES = [ 65c1010edaSGreg Roach [ 66c1010edaSGreg Roach 'Latn', 67c1010edaSGreg Roach 0x0041, 68c1010edaSGreg Roach 0x005A, 69c1010edaSGreg Roach ], 70c1010edaSGreg Roach [ 71c1010edaSGreg Roach 'Latn', 72c1010edaSGreg Roach 0x0061, 73c1010edaSGreg Roach 0x007A, 74c1010edaSGreg Roach ], 75c1010edaSGreg Roach [ 76c1010edaSGreg Roach 'Latn', 77c1010edaSGreg Roach 0x0100, 78c1010edaSGreg Roach 0x02AF, 79c1010edaSGreg Roach ], 80c1010edaSGreg Roach [ 81c1010edaSGreg Roach 'Grek', 82c1010edaSGreg Roach 0x0370, 83c1010edaSGreg Roach 0x03FF, 84c1010edaSGreg Roach ], 85c1010edaSGreg Roach [ 86c1010edaSGreg Roach 'Cyrl', 87c1010edaSGreg Roach 0x0400, 88c1010edaSGreg Roach 0x052F, 89c1010edaSGreg Roach ], 90c1010edaSGreg Roach [ 91c1010edaSGreg Roach 'Hebr', 92c1010edaSGreg Roach 0x0590, 93c1010edaSGreg Roach 0x05FF, 94c1010edaSGreg Roach ], 95c1010edaSGreg Roach [ 96c1010edaSGreg Roach 'Arab', 97c1010edaSGreg Roach 0x0600, 98c1010edaSGreg Roach 0x06FF, 99c1010edaSGreg Roach ], 100c1010edaSGreg Roach [ 101c1010edaSGreg Roach 'Arab', 102c1010edaSGreg Roach 0x0750, 103c1010edaSGreg Roach 0x077F, 104c1010edaSGreg Roach ], 105c1010edaSGreg Roach [ 106c1010edaSGreg Roach 'Arab', 107c1010edaSGreg Roach 0x08A0, 108c1010edaSGreg Roach 0x08FF, 109c1010edaSGreg Roach ], 110c1010edaSGreg Roach [ 111c1010edaSGreg Roach 'Deva', 112c1010edaSGreg Roach 0x0900, 113c1010edaSGreg Roach 0x097F, 114c1010edaSGreg Roach ], 115c1010edaSGreg Roach [ 116c1010edaSGreg Roach 'Taml', 117c1010edaSGreg Roach 0x0B80, 118c1010edaSGreg Roach 0x0BFF, 119c1010edaSGreg Roach ], 120c1010edaSGreg Roach [ 121c1010edaSGreg Roach 'Sinh', 122c1010edaSGreg Roach 0x0D80, 123c1010edaSGreg Roach 0x0DFF, 124c1010edaSGreg Roach ], 125c1010edaSGreg Roach [ 126c1010edaSGreg Roach 'Thai', 127c1010edaSGreg Roach 0x0E00, 128c1010edaSGreg Roach 0x0E7F, 129c1010edaSGreg Roach ], 130c1010edaSGreg Roach [ 131c1010edaSGreg Roach 'Geor', 132c1010edaSGreg Roach 0x10A0, 133c1010edaSGreg Roach 0x10FF, 134c1010edaSGreg Roach ], 135c1010edaSGreg Roach [ 136c1010edaSGreg Roach 'Grek', 137c1010edaSGreg Roach 0x1F00, 138c1010edaSGreg Roach 0x1FFF, 139c1010edaSGreg Roach ], 140c1010edaSGreg Roach [ 141c1010edaSGreg Roach 'Deva', 142c1010edaSGreg Roach 0xA8E0, 143c1010edaSGreg Roach 0xA8FF, 144c1010edaSGreg Roach ], 145c1010edaSGreg Roach [ 146c1010edaSGreg Roach 'Hans', 147c1010edaSGreg Roach 0x3000, 148c1010edaSGreg Roach 0x303F, 149c1010edaSGreg Roach ], 150c1010edaSGreg Roach // Mixed CJK, not just Hans 151c1010edaSGreg Roach [ 152c1010edaSGreg Roach 'Hans', 153c1010edaSGreg Roach 0x3400, 154c1010edaSGreg Roach 0xFAFF, 155c1010edaSGreg Roach ], 156c1010edaSGreg Roach // Mixed CJK, not just Hans 157c1010edaSGreg Roach [ 158c1010edaSGreg Roach 'Hans', 159c1010edaSGreg Roach 0x20000, 160c1010edaSGreg Roach 0x2FA1F, 161c1010edaSGreg Roach ], 162c1010edaSGreg Roach // Mixed CJK, not just Hans 16313abd6f3SGreg Roach ]; 164a25f0a04SGreg Roach 165991b93ddSGreg Roach // Characters that are displayed in mirror form in RTL text. 166991b93ddSGreg Roach const MIRROR_CHARACTERS = [ 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 '“ ' => '”', 182a25f0a04SGreg Roach '” ' => '“', 183a25f0a04SGreg Roach '‘ ' => '’', 184a25f0a04SGreg Roach '’ ' => '‘', 18513abd6f3SGreg Roach ]; 186a25f0a04SGreg Roach 187991b93ddSGreg Roach // Default list of locales to show in the menu. 188991b93ddSGreg Roach const DEFAULT_LOCALES = [ 189c1010edaSGreg Roach 'ar', 190c1010edaSGreg Roach 'bg', 191c1010edaSGreg Roach 'bs', 192c1010edaSGreg Roach 'ca', 193c1010edaSGreg Roach 'cs', 194c1010edaSGreg Roach 'da', 195c1010edaSGreg Roach 'de', 196c1010edaSGreg Roach 'el', 197c1010edaSGreg Roach 'en-GB', 198c1010edaSGreg Roach 'en-US', 199c1010edaSGreg Roach 'es', 200c1010edaSGreg Roach 'et', 201c1010edaSGreg Roach 'fi', 202c1010edaSGreg Roach 'fr', 203c1010edaSGreg Roach 'he', 204c1010edaSGreg Roach 'hr', 205c1010edaSGreg Roach 'hu', 206c1010edaSGreg Roach 'is', 207c1010edaSGreg Roach 'it', 208c1010edaSGreg Roach 'ka', 209c1010edaSGreg Roach 'kk', 210c1010edaSGreg Roach 'lt', 211c1010edaSGreg Roach 'mr', 212c1010edaSGreg Roach 'nb', 213c1010edaSGreg Roach 'nl', 214c1010edaSGreg Roach 'nn', 215c1010edaSGreg Roach 'pl', 216c1010edaSGreg Roach 'pt', 217c1010edaSGreg Roach 'ru', 218c1010edaSGreg Roach 'sk', 219c1010edaSGreg Roach 'sv', 220c1010edaSGreg Roach 'tr', 221c1010edaSGreg Roach 'uk', 222c1010edaSGreg Roach 'vi', 223c1010edaSGreg Roach 'zh-Hans', 224991b93ddSGreg Roach ]; 225991b93ddSGreg Roach 226a25f0a04SGreg Roach /** @var string Punctuation used to separate list items, typically a comma */ 227a25f0a04SGreg Roach public static $list_separator; 228a25f0a04SGreg Roach 229a25f0a04SGreg Roach /** 230dfeee0a8SGreg Roach * The prefered locales for this site, or a default list if no preference. 231dfeee0a8SGreg Roach * 232dfeee0a8SGreg Roach * @return LocaleInterface[] 233dfeee0a8SGreg Roach */ 234c1010edaSGreg Roach public static function activeLocales() 235c1010edaSGreg Roach { 236dfeee0a8SGreg Roach $code_list = Site::getPreference('LANGUAGES'); 237dfeee0a8SGreg Roach 23815d603e7SGreg Roach if ($code_list === '') { 239991b93ddSGreg Roach $codes = self::DEFAULT_LOCALES; 240dfeee0a8SGreg Roach } else { 241991b93ddSGreg Roach $codes = explode(',', $code_list); 242dfeee0a8SGreg Roach } 243dfeee0a8SGreg Roach 24413abd6f3SGreg Roach $locales = []; 245dfeee0a8SGreg Roach foreach ($codes as $code) { 246dfeee0a8SGreg Roach if (file_exists(WT_ROOT . 'language/' . $code . '.mo')) { 247dfeee0a8SGreg Roach try { 248dfeee0a8SGreg Roach $locales[] = Locale::create($code); 249dfeee0a8SGreg Roach } catch (\Exception $ex) { 250bd52fa32SGreg Roach DebugBar::addThrowable($ex); 251bd52fa32SGreg Roach 252dfeee0a8SGreg Roach // No such locale exists? 253dfeee0a8SGreg Roach } 254dfeee0a8SGreg Roach } 255dfeee0a8SGreg Roach } 256dfeee0a8SGreg Roach usort($locales, '\Fisharebest\Localization\Locale::compare'); 257dfeee0a8SGreg Roach 258dfeee0a8SGreg Roach return $locales; 259dfeee0a8SGreg Roach } 260dfeee0a8SGreg Roach 261dfeee0a8SGreg Roach /** 262dfeee0a8SGreg Roach * Which MySQL collation should be used for this locale? 263dfeee0a8SGreg Roach * 264dfeee0a8SGreg Roach * @return string 265dfeee0a8SGreg Roach */ 266c1010edaSGreg Roach public static function collation() 267c1010edaSGreg Roach { 268dfeee0a8SGreg Roach $collation = self::$locale->collation(); 269dfeee0a8SGreg Roach switch ($collation) { 270dfeee0a8SGreg Roach case 'croatian_ci': 271dfeee0a8SGreg Roach case 'german2_ci': 272dfeee0a8SGreg Roach case 'vietnamese_ci': 273dfeee0a8SGreg Roach // Only available in MySQL 5.6 274dfeee0a8SGreg Roach return 'utf8_unicode_ci'; 275dfeee0a8SGreg Roach default: 276dfeee0a8SGreg Roach return 'utf8_' . $collation; 277dfeee0a8SGreg Roach } 278dfeee0a8SGreg Roach } 279dfeee0a8SGreg Roach 280dfeee0a8SGreg Roach /** 281dfeee0a8SGreg Roach * What format is used to display dates in the current locale? 282dfeee0a8SGreg Roach * 283dfeee0a8SGreg Roach * @return string 284dfeee0a8SGreg Roach */ 285c1010edaSGreg Roach public static function dateFormat() 286c1010edaSGreg Roach { 287bbb76c12SGreg Roach /* I18N: This is the format string for full dates. See http://php.net/date for codes */ 288bbb76c12SGreg Roach return self::$translator->translate('%j %F %Y'); 289dfeee0a8SGreg Roach } 290dfeee0a8SGreg Roach 291dfeee0a8SGreg Roach /** 292dfeee0a8SGreg Roach * Generate consistent I18N for datatables.js 293dfeee0a8SGreg Roach * 294dfeee0a8SGreg Roach * @param array|null $lengths An optional array of page lengths 295dfeee0a8SGreg Roach * 296dfeee0a8SGreg Roach * @return string 297dfeee0a8SGreg Roach */ 298c1010edaSGreg Roach public static function datatablesI18N(array $lengths = [ 299c1010edaSGreg Roach 10, 300c1010edaSGreg Roach 20, 301c1010edaSGreg Roach 30, 302c1010edaSGreg Roach 50, 303c1010edaSGreg Roach 100, 304c1010edaSGreg Roach -1, 305c1010edaSGreg Roach ]) 306c1010edaSGreg Roach { 30715d603e7SGreg Roach $length_options = Bootstrap4::select(FunctionsEdit::numericOptions($lengths), 10); 308dfeee0a8SGreg Roach 309dfeee0a8SGreg Roach return 3102d9b2ebaSGreg Roach '"formatNumber": function(n) { return String(n).replace(/[0-9]/g, function(w) { return ("' . self::$locale->digits('0123456789') . '")[+w]; }); },' . 311dfeee0a8SGreg Roach '"language": {' . 312dfeee0a8SGreg Roach ' "paginate": {' . 313bbb76c12SGreg Roach ' "first": "' . self::translate('first') . '",' . 314bbb76c12SGreg Roach ' "last": "' . self::translate('last') . '",' . 315bbb76c12SGreg Roach ' "next": "' . self::translate('next') . '",' . 316bbb76c12SGreg Roach ' "previous": "' . self::translate('previous') . '"' . 317dfeee0a8SGreg Roach ' },' . 318dfeee0a8SGreg Roach ' "emptyTable": "' . self::translate('No records to display') . '",' . 319c1010edaSGreg Roach ' "info": "' . /* I18N: %s are placeholders for numbers */ 320c1010edaSGreg Roach self::translate('Showing %1$s to %2$s of %3$s', '_START_', '_END_', '_TOTAL_') . '",' . 3210b6446e1SGreg 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')) . '",' . 322c1010edaSGreg Roach ' "infoFiltered": "' . /* I18N: %s is a placeholder for a number */ 323c1010edaSGreg Roach self::translate('(filtered from %s total entries)', '_MAX_') . '",' . 324c1010edaSGreg Roach ' "lengthMenu": "' . /* I18N: %s is a number of records per page */ 325c1010edaSGreg Roach self::translate('Display %s', addslashes($length_options)) . '",' . 326dfeee0a8SGreg Roach ' "loadingRecords": "' . self::translate('Loading…') . '",' . 327dfeee0a8SGreg Roach ' "processing": "' . self::translate('Loading…') . '",' . 328dfeee0a8SGreg Roach ' "search": "' . self::translate('Filter') . '",' . 329dfeee0a8SGreg Roach ' "zeroRecords": "' . self::translate('No records to display') . '"' . 3302d9b2ebaSGreg Roach '}'; 331dfeee0a8SGreg Roach } 332dfeee0a8SGreg Roach 333dfeee0a8SGreg Roach /** 334dfeee0a8SGreg Roach * Convert the digits 0-9 into the local script 335dfeee0a8SGreg Roach * 336dfeee0a8SGreg Roach * Used for years, etc., where we do not want thousands-separators, decimals, etc. 337dfeee0a8SGreg Roach * 338cbc1590aSGreg Roach * @param int $n 339dfeee0a8SGreg Roach * 340dfeee0a8SGreg Roach * @return string 341dfeee0a8SGreg Roach */ 342c1010edaSGreg Roach public static function digits($n) 343c1010edaSGreg Roach { 344dfeee0a8SGreg Roach return self::$locale->digits($n); 345dfeee0a8SGreg Roach } 346dfeee0a8SGreg Roach 347dfeee0a8SGreg Roach /** 348dfeee0a8SGreg Roach * What is the direction of the current locale 349dfeee0a8SGreg Roach * 350dfeee0a8SGreg Roach * @return string "ltr" or "rtl" 351dfeee0a8SGreg Roach */ 352c1010edaSGreg Roach public static function direction() 353c1010edaSGreg Roach { 354dfeee0a8SGreg Roach return self::$locale->direction(); 355dfeee0a8SGreg Roach } 356dfeee0a8SGreg Roach 357dfeee0a8SGreg Roach /** 3587231a557SGreg Roach * What is the first day of the week. 3597231a557SGreg Roach * 360cbc1590aSGreg Roach * @return int Sunday=0, Monday=1, etc. 3617231a557SGreg Roach */ 362c1010edaSGreg Roach public static function firstDay() 363c1010edaSGreg Roach { 3647231a557SGreg Roach return self::$locale->territory()->firstDay(); 3657231a557SGreg Roach } 3667231a557SGreg Roach 3677231a557SGreg Roach /** 368dfeee0a8SGreg Roach * Convert a GEDCOM age string into translated_text 369dfeee0a8SGreg Roach * 370dfeee0a8SGreg Roach * NB: The import function will have normalised this, so we don't need 371dfeee0a8SGreg Roach * to worry about badly formatted strings 3723d7a8a4cSGreg Roach * NOTE: this function is not yet complete - eventually it will replace FunctionsDate::get_age_at_event() 373dfeee0a8SGreg Roach * 374dfeee0a8SGreg Roach * @param $string 375dfeee0a8SGreg Roach * 376dfeee0a8SGreg Roach * @return string 377dfeee0a8SGreg Roach */ 378c1010edaSGreg Roach public static function gedcomAge($string) 379c1010edaSGreg Roach { 380dfeee0a8SGreg Roach switch ($string) { 381dfeee0a8SGreg Roach case 'STILLBORN': 382dfeee0a8SGreg Roach // I18N: Description of an individual’s age at an event. For example, Died 14 Jan 1900 (stillborn) 383dfeee0a8SGreg Roach return self::translate('(stillborn)'); 384dfeee0a8SGreg Roach case 'INFANT': 385dfeee0a8SGreg Roach // I18N: Description of an individual’s age at an event. For example, Died 14 Jan 1900 (in infancy) 386dfeee0a8SGreg Roach return self::translate('(in infancy)'); 387dfeee0a8SGreg Roach case 'CHILD': 388dfeee0a8SGreg Roach // I18N: Description of an individual’s age at an event. For example, Died 14 Jan 1900 (in childhood) 389dfeee0a8SGreg Roach return self::translate('(in childhood)'); 390dfeee0a8SGreg Roach } 39113abd6f3SGreg Roach $age = []; 392dfeee0a8SGreg Roach if (preg_match('/(\d+)y/', $string, $match)) { 393dfeee0a8SGreg Roach // I18N: Part of an age string. e.g. 5 years, 4 months and 3 days 394dfeee0a8SGreg Roach $years = $match[1]; 395dfeee0a8SGreg Roach $age[] = self::plural('%s year', '%s years', $years, self::number($years)); 396dfeee0a8SGreg Roach } else { 397dfeee0a8SGreg Roach $years = -1; 398dfeee0a8SGreg Roach } 399dfeee0a8SGreg Roach if (preg_match('/(\d+)m/', $string, $match)) { 400dfeee0a8SGreg Roach // I18N: Part of an age string. e.g. 5 years, 4 months and 3 days 401dfeee0a8SGreg Roach $age[] = self::plural('%s month', '%s months', $match[1], self::number($match[1])); 402dfeee0a8SGreg Roach } 403dfeee0a8SGreg Roach if (preg_match('/(\d+)w/', $string, $match)) { 404dfeee0a8SGreg Roach // I18N: Part of an age string. e.g. 7 weeks and 3 days 405dfeee0a8SGreg Roach $age[] = self::plural('%s week', '%s weeks', $match[1], self::number($match[1])); 406dfeee0a8SGreg Roach } 407dfeee0a8SGreg Roach if (preg_match('/(\d+)d/', $string, $match)) { 408dfeee0a8SGreg Roach // I18N: Part of an age string. e.g. 5 years, 4 months and 3 days 409dfeee0a8SGreg Roach $age[] = self::plural('%s day', '%s days', $match[1], self::number($match[1])); 410dfeee0a8SGreg Roach } 411dfeee0a8SGreg Roach // If an age is just a number of years, only show the number 412dfeee0a8SGreg Roach if (count($age) === 1 && $years >= 0) { 413dfeee0a8SGreg Roach $age = $years; 414dfeee0a8SGreg Roach } 415dfeee0a8SGreg Roach if ($age) { 416dfeee0a8SGreg Roach if (!substr_compare($string, '<', 0, 1)) { 417dfeee0a8SGreg Roach // I18N: Description of an individual’s age at an event. For example, Died 14 Jan 1900 (aged less than 21 years) 418dfeee0a8SGreg Roach return self::translate('(aged less than %s)', $age); 419dfeee0a8SGreg Roach } elseif (!substr_compare($string, '>', 0, 1)) { 420dfeee0a8SGreg Roach // I18N: Description of an individual’s age at an event. For example, Died 14 Jan 1900 (aged more than 21 years) 421dfeee0a8SGreg Roach return self::translate('(aged more than %s)', $age); 422dfeee0a8SGreg Roach } else { 423dfeee0a8SGreg Roach // I18N: Description of an individual’s age at an event. For example, Died 14 Jan 1900 (aged 43 years) 424dfeee0a8SGreg Roach return self::translate('(aged %s)', $age); 425dfeee0a8SGreg Roach } 426dfeee0a8SGreg Roach } else { 427dfeee0a8SGreg Roach // Not a valid string? 428dfeee0a8SGreg Roach return self::translate('(aged %s)', $string); 429dfeee0a8SGreg Roach } 430dfeee0a8SGreg Roach } 431dfeee0a8SGreg Roach 432dfeee0a8SGreg Roach /** 433dfeee0a8SGreg Roach * Generate i18n markup for the <html> tag, e.g. lang="ar" dir="rtl" 434dfeee0a8SGreg Roach * 435dfeee0a8SGreg Roach * @return string 436dfeee0a8SGreg Roach */ 437c1010edaSGreg Roach public static function htmlAttributes() 438c1010edaSGreg Roach { 439dfeee0a8SGreg Roach return self::$locale->htmlAttributes(); 440dfeee0a8SGreg Roach } 441dfeee0a8SGreg Roach 442dfeee0a8SGreg Roach /** 443a25f0a04SGreg Roach * Initialise the translation adapter with a locale setting. 444a25f0a04SGreg Roach * 44515d603e7SGreg Roach * @param string $code Use this locale/language code, or choose one automatically 446a25f0a04SGreg Roach * 447a25f0a04SGreg Roach * @return string $string 448a25f0a04SGreg Roach */ 449c1010edaSGreg Roach public static function init($code = '') 450c1010edaSGreg Roach { 451c314ecc9SGreg Roach mb_internal_encoding('UTF-8'); 452c314ecc9SGreg Roach 45315d603e7SGreg Roach if ($code !== '') { 4543bdc890bSGreg Roach // Create the specified locale 4553bdc890bSGreg Roach self::$locale = Locale::create($code); 4563bdc890bSGreg Roach } else { 4573bdc890bSGreg Roach // Negotiate a locale, but if we can't then use a failsafe 458*59f2f229SGreg Roach self::$locale = new LocaleEnUs(); 459cbf1be5dSGreg Roach if (Session::has('locale') && file_exists(WT_ROOT . 'language/' . Session::get('locale') . '.mo')) { 4603bdc890bSGreg Roach // Previously used 46131bc7874SGreg Roach self::$locale = Locale::create(Session::get('locale')); 4623bdc890bSGreg Roach } else { 4631e71bdc0SGreg Roach // Browser negotiation 464*59f2f229SGreg Roach $default_locale = new LocaleEnUs(); 4653bdc890bSGreg Roach try { 466c7e7cb40SGreg Roach // @TODO, when no language is requested by the user (e.g. search engines), we should use 467c7e7cb40SGreg Roach // the tree's default language. However, we currently initialise languages before trees, 468c7e7cb40SGreg Roach // so there is no tree available for us to use. 4693bdc890bSGreg Roach } catch (\Exception $ex) { 470bd52fa32SGreg Roach DebugBar::addThrowable($ex); 4713bdc890bSGreg Roach } 472149573a1SGreg Roach self::$locale = Locale::httpAcceptLanguage($_SERVER, self::installedLocales(), $default_locale); 4733bdc890bSGreg Roach } 4743bdc890bSGreg Roach } 4753bdc890bSGreg Roach 476f1af7e1cSGreg Roach $cache_dir = WT_DATA_DIR . 'cache/'; 477f1af7e1cSGreg Roach $cache_file = $cache_dir . 'language-' . self::$locale->languageTag() . '-cache.php'; 4783bdc890bSGreg Roach if (file_exists($cache_file)) { 4793bdc890bSGreg Roach $filemtime = filemtime($cache_file); 4803bdc890bSGreg Roach } else { 4813bdc890bSGreg Roach $filemtime = 0; 4823bdc890bSGreg Roach } 4833bdc890bSGreg Roach 4843bdc890bSGreg Roach // Load the translation file(s) 4857d6e38dfSGreg Roach // Note that glob() returns false instead of an empty array when open_basedir_restriction 4867d6e38dfSGreg Roach // is in force and no files are found. See PHP bug #47358. 4877a7f87d7SGreg Roach if (defined('GLOB_BRACE')) { 4883bdc890bSGreg Roach $translation_files = array_merge( 48913abd6f3SGreg Roach [WT_ROOT . 'language/' . self::$locale->languageTag() . '.mo'], 49013abd6f3SGreg Roach glob(WT_MODULES_DIR . '*/language/' . self::$locale->languageTag() . '.{csv,php,mo}', GLOB_BRACE) ?: [], 49113abd6f3SGreg Roach glob(WT_DATA_DIR . 'language/' . self::$locale->languageTag() . '.{csv,php,mo}', GLOB_BRACE) ?: [] 492a25f0a04SGreg Roach ); 4937a7f87d7SGreg Roach } else { 4947a7f87d7SGreg Roach // Some servers do not have GLOB_BRACE - see http://php.net/manual/en/function.glob.php 4957a7f87d7SGreg Roach $translation_files = array_merge( 49613abd6f3SGreg Roach [WT_ROOT . 'language/' . self::$locale->languageTag() . '.mo'], 49713abd6f3SGreg Roach glob(WT_MODULES_DIR . '*/language/' . self::$locale->languageTag() . '.csv') ?: [], 49813abd6f3SGreg Roach glob(WT_MODULES_DIR . '*/language/' . self::$locale->languageTag() . '.php') ?: [], 49913abd6f3SGreg Roach glob(WT_MODULES_DIR . '*/language/' . self::$locale->languageTag() . '.mo') ?: [], 50013abd6f3SGreg Roach glob(WT_DATA_DIR . 'language/' . self::$locale->languageTag() . '.csv') ?: [], 50113abd6f3SGreg Roach glob(WT_DATA_DIR . 'language/' . self::$locale->languageTag() . '.php') ?: [], 50213abd6f3SGreg Roach glob(WT_DATA_DIR . 'language/' . self::$locale->languageTag() . '.mo') ?: [] 5037a7f87d7SGreg Roach ); 5047a7f87d7SGreg Roach } 5057a7f87d7SGreg Roach // Rebuild files after one hour 5067a7f87d7SGreg Roach $rebuild_cache = time() > $filemtime + 3600; 5071e71bdc0SGreg Roach // Rebuild files if any translation file has been updated 5083bdc890bSGreg Roach foreach ($translation_files as $translation_file) { 5093bdc890bSGreg Roach if (filemtime($translation_file) > $filemtime) { 5103bdc890bSGreg Roach $rebuild_cache = true; 511a25f0a04SGreg Roach break; 512a25f0a04SGreg Roach } 513a25f0a04SGreg Roach } 5143bdc890bSGreg Roach 5153bdc890bSGreg Roach if ($rebuild_cache) { 51613abd6f3SGreg Roach $translations = []; 5173bdc890bSGreg Roach foreach ($translation_files as $translation_file) { 5183bdc890bSGreg Roach $translation = new Translation($translation_file); 5193bdc890bSGreg Roach $translations = array_merge($translations, $translation->asArray()); 520a25f0a04SGreg Roach } 521f1af7e1cSGreg Roach try { 522f1af7e1cSGreg Roach File::mkdir($cache_dir); 523f1af7e1cSGreg Roach file_put_contents($cache_file, '<?php return ' . var_export($translations, true) . ';'); 524f1af7e1cSGreg Roach } catch (Exception $ex) { 525bd52fa32SGreg Roach DebugBar::addThrowable($ex); 526bd52fa32SGreg Roach 5277c2999b4SGreg Roach // During setup, we may not have been able to create it. 528c85fb0c4SGreg Roach } 5293bdc890bSGreg Roach } else { 5303bdc890bSGreg Roach $translations = include $cache_file; 531a25f0a04SGreg Roach } 532a25f0a04SGreg Roach 5333bdc890bSGreg Roach // Create a translator 5343bdc890bSGreg Roach self::$translator = new Translator($translations, self::$locale->pluralRule()); 535a25f0a04SGreg Roach 536bbb76c12SGreg Roach /* I18N: This punctuation is used to separate lists of items */ 537bbb76c12SGreg Roach self::$list_separator = self::translate(', '); 538a25f0a04SGreg Roach 539991b93ddSGreg Roach // Create a collator 540991b93ddSGreg Roach try { 541444a65ecSGreg Roach // PHP 5.6 cannot catch errors, so test first 542444a65ecSGreg Roach if (class_exists('Collator')) { 543991b93ddSGreg Roach self::$collator = new Collator(self::$locale->code()); 544991b93ddSGreg Roach // Ignore upper/lower case differences 545991b93ddSGreg Roach self::$collator->setStrength(Collator::SECONDARY); 546444a65ecSGreg Roach } 547991b93ddSGreg Roach } catch (Exception $ex) { 548bd52fa32SGreg Roach DebugBar::addThrowable($ex); 549bd52fa32SGreg Roach 550991b93ddSGreg Roach // PHP-INTL is not installed? We'll use a fallback later. 551991b93ddSGreg Roach } 552991b93ddSGreg Roach 5535331c5eaSGreg Roach return self::$locale->languageTag(); 554a25f0a04SGreg Roach } 555a25f0a04SGreg Roach 556a25f0a04SGreg Roach /** 557c999a340SGreg Roach * All locales for which a translation file exists. 558c999a340SGreg Roach * 55915834aaeSGreg Roach * @return LocaleInterface[] 560c999a340SGreg Roach */ 561c1010edaSGreg Roach public static function installedLocales() 562c1010edaSGreg Roach { 56313abd6f3SGreg Roach $locales = []; 564c999a340SGreg Roach foreach (glob(WT_ROOT . 'language/*.mo') as $file) { 565c999a340SGreg Roach try { 566c999a340SGreg Roach $locales[] = Locale::create(basename($file, '.mo')); 567c999a340SGreg Roach } catch (\Exception $ex) { 568bd52fa32SGreg Roach DebugBar::addThrowable($ex); 569bd52fa32SGreg Roach 5703bdc890bSGreg Roach // Not a recognised locale 571a25f0a04SGreg Roach } 572a25f0a04SGreg Roach } 573c999a340SGreg Roach usort($locales, '\Fisharebest\Localization\Locale::compare'); 574c999a340SGreg Roach 575c999a340SGreg Roach return $locales; 576a25f0a04SGreg Roach } 577a25f0a04SGreg Roach 578a25f0a04SGreg Roach /** 579a25f0a04SGreg Roach * Return the endonym for a given language - as per http://cldr.unicode.org/ 580a25f0a04SGreg Roach * 581a25f0a04SGreg Roach * @param string $locale 582a25f0a04SGreg Roach * 583a25f0a04SGreg Roach * @return string 584a25f0a04SGreg Roach */ 585c1010edaSGreg Roach public static function languageName($locale) 586c1010edaSGreg Roach { 587c999a340SGreg Roach return Locale::create($locale)->endonym(); 588a25f0a04SGreg Roach } 589a25f0a04SGreg Roach 590a25f0a04SGreg Roach /** 591a25f0a04SGreg Roach * Return the script used by a given language 592a25f0a04SGreg Roach * 593a25f0a04SGreg Roach * @param string $locale 594a25f0a04SGreg Roach * 595a25f0a04SGreg Roach * @return string 596a25f0a04SGreg Roach */ 597c1010edaSGreg Roach public static function languageScript($locale) 598c1010edaSGreg Roach { 599c999a340SGreg Roach return Locale::create($locale)->script()->code(); 600a25f0a04SGreg Roach } 601a25f0a04SGreg Roach 602a25f0a04SGreg Roach /** 603dfeee0a8SGreg Roach * Translate a number into the local representation. 604a25f0a04SGreg Roach * 605dfeee0a8SGreg Roach * e.g. 12345.67 becomes 606dfeee0a8SGreg Roach * en: 12,345.67 607dfeee0a8SGreg Roach * fr: 12 345,67 608dfeee0a8SGreg Roach * de: 12.345,67 609dfeee0a8SGreg Roach * 610dfeee0a8SGreg Roach * @param float $n 611cbc1590aSGreg Roach * @param int $precision 612a25f0a04SGreg Roach * 613a25f0a04SGreg Roach * @return string 614a25f0a04SGreg Roach */ 615c1010edaSGreg Roach public static function number($n, $precision = 0) 616c1010edaSGreg Roach { 617dfeee0a8SGreg Roach return self::$locale->number(round($n, $precision)); 618dfeee0a8SGreg Roach } 619dfeee0a8SGreg Roach 620dfeee0a8SGreg Roach /** 621dfeee0a8SGreg Roach * Translate a fraction into a percentage. 622dfeee0a8SGreg Roach * 623dfeee0a8SGreg Roach * e.g. 0.123 becomes 624dfeee0a8SGreg Roach * en: 12.3% 625dfeee0a8SGreg Roach * fr: 12,3 % 626dfeee0a8SGreg Roach * de: 12,3% 627dfeee0a8SGreg Roach * 628dfeee0a8SGreg Roach * @param float $n 629cbc1590aSGreg Roach * @param int $precision 630dfeee0a8SGreg Roach * 631dfeee0a8SGreg Roach * @return string 632dfeee0a8SGreg Roach */ 633c1010edaSGreg Roach public static function percentage($n, $precision = 0) 634c1010edaSGreg Roach { 635dfeee0a8SGreg Roach return self::$locale->percent(round($n, $precision + 2)); 636dfeee0a8SGreg Roach } 637dfeee0a8SGreg Roach 638dfeee0a8SGreg Roach /** 639dfeee0a8SGreg Roach * Translate a plural string 640dfeee0a8SGreg Roach * 641dfeee0a8SGreg Roach * echo self::plural('There is an error', 'There are errors', $num_errors); 642dfeee0a8SGreg Roach * echo self::plural('There is one error', 'There are %s errors', $num_errors); 643dfeee0a8SGreg Roach * echo self::plural('There is %1$s %2$s cat', 'There are %1$s %2$s cats', $num, $num, $colour); 644dfeee0a8SGreg Roach * 645dfeee0a8SGreg Roach * @return string 646dfeee0a8SGreg Roach */ 647c58d56c4SGreg Roach public static function plural(...$args) 648c1010edaSGreg Roach { 649beb72eacSGreg Roach $args[0] = self::$translator->translatePlural($args[0], $args[1], (int) $args[2]); 650dfeee0a8SGreg Roach unset($args[1], $args[2]); 651dfeee0a8SGreg Roach 652c58d56c4SGreg Roach return sprintf(...$args); 653dfeee0a8SGreg Roach } 654dfeee0a8SGreg Roach 655dfeee0a8SGreg Roach /** 656dfeee0a8SGreg Roach * UTF8 version of PHP::strrev() 657dfeee0a8SGreg Roach * 658dfeee0a8SGreg Roach * Reverse RTL text for third-party libraries such as GD2 and googlechart. 659dfeee0a8SGreg Roach * 660dfeee0a8SGreg Roach * These do not support UTF8 text direction, so we must mimic it for them. 661dfeee0a8SGreg Roach * 662dfeee0a8SGreg Roach * Numbers are always rendered LTR, even in RTL text. 663dfeee0a8SGreg Roach * The visual direction of characters such as parentheses should be reversed. 664dfeee0a8SGreg Roach * 665dfeee0a8SGreg Roach * @param string $text Text to be reversed 666dfeee0a8SGreg Roach * 667dfeee0a8SGreg Roach * @return string 668dfeee0a8SGreg Roach */ 669c1010edaSGreg Roach public static function reverseText($text) 670c1010edaSGreg Roach { 671dfeee0a8SGreg Roach // Remove HTML markup - we can't display it and it is LTR. 6729524b7b5SGreg Roach $text = strip_tags($text); 6739524b7b5SGreg Roach // Remove HTML entities. 6749524b7b5SGreg Roach $text = html_entity_decode($text, ENT_QUOTES, 'UTF-8'); 675dfeee0a8SGreg Roach 676dfeee0a8SGreg Roach // LTR text doesn't need reversing 677dfeee0a8SGreg Roach if (self::scriptDirection(self::textScript($text)) === 'ltr') { 678dfeee0a8SGreg Roach return $text; 679dfeee0a8SGreg Roach } 680dfeee0a8SGreg Roach 681dfeee0a8SGreg Roach // Mirrored characters 682991b93ddSGreg Roach $text = strtr($text, self::MIRROR_CHARACTERS); 683dfeee0a8SGreg Roach 684dfeee0a8SGreg Roach $reversed = ''; 685dfeee0a8SGreg Roach $digits = ''; 686dfeee0a8SGreg Roach while ($text != '') { 687dfeee0a8SGreg Roach $letter = mb_substr($text, 0, 1); 688dfeee0a8SGreg Roach $text = mb_substr($text, 1); 689dfeee0a8SGreg Roach if (strpos(self::DIGITS, $letter) !== false) { 690dfeee0a8SGreg Roach $digits .= $letter; 691a25f0a04SGreg Roach } else { 692dfeee0a8SGreg Roach $reversed = $letter . $digits . $reversed; 693dfeee0a8SGreg Roach $digits = ''; 694dfeee0a8SGreg Roach } 695a25f0a04SGreg Roach } 696a25f0a04SGreg Roach 697dfeee0a8SGreg Roach return $digits . $reversed; 698a25f0a04SGreg Roach } 699a25f0a04SGreg Roach 700a25f0a04SGreg Roach /** 701a25f0a04SGreg Roach * Return the direction (ltr or rtl) for a given script 702a25f0a04SGreg Roach * 703a25f0a04SGreg Roach * The PHP/intl library does not provde this information, so we need 704a25f0a04SGreg Roach * our own lookup table. 705a25f0a04SGreg Roach * 706a25f0a04SGreg Roach * @param string $script 707a25f0a04SGreg Roach * 708a25f0a04SGreg Roach * @return string 709a25f0a04SGreg Roach */ 710c1010edaSGreg Roach public static function scriptDirection($script) 711c1010edaSGreg Roach { 712a25f0a04SGreg Roach switch ($script) { 713a25f0a04SGreg Roach case 'Arab': 714a25f0a04SGreg Roach case 'Hebr': 715a25f0a04SGreg Roach case 'Mong': 716a25f0a04SGreg Roach case 'Thaa': 717a25f0a04SGreg Roach return 'rtl'; 718a25f0a04SGreg Roach default: 719a25f0a04SGreg Roach return 'ltr'; 720a25f0a04SGreg Roach } 721a25f0a04SGreg Roach } 722a25f0a04SGreg Roach 723a25f0a04SGreg Roach /** 724991b93ddSGreg Roach * Perform a case-insensitive comparison of two strings. 725a25f0a04SGreg Roach * 726a25f0a04SGreg Roach * @param string $string1 727a25f0a04SGreg Roach * @param string $string2 728a25f0a04SGreg Roach * 729cbc1590aSGreg Roach * @return int 730a25f0a04SGreg Roach */ 731c1010edaSGreg Roach public static function strcasecmp($string1, $string2) 732c1010edaSGreg Roach { 733991b93ddSGreg Roach if (self::$collator instanceof Collator) { 734991b93ddSGreg Roach return self::$collator->compare($string1, $string2); 735a25f0a04SGreg Roach } else { 736991b93ddSGreg Roach return strcmp(self::strtolower($string1), self::strtolower($string2)); 737a25f0a04SGreg Roach } 738a25f0a04SGreg Roach } 739a25f0a04SGreg Roach 740a25f0a04SGreg Roach /** 741991b93ddSGreg Roach * Convert a string to lower case. 742a25f0a04SGreg Roach * 743dfeee0a8SGreg Roach * @param string $string 744a25f0a04SGreg Roach * 745a25f0a04SGreg Roach * @return string 746a25f0a04SGreg Roach */ 747c1010edaSGreg Roach public static function strtolower($string) 748c1010edaSGreg Roach { 749991b93ddSGreg Roach if (in_array(self::$locale->language()->code(), self::DOTLESS_I_LOCALES)) { 750991b93ddSGreg Roach $string = strtr($string, self::DOTLESS_I_TOLOWER); 751a25f0a04SGreg Roach } 7525ddad20bSGreg Roach 7535ddad20bSGreg Roach return mb_strtolower($string); 754a25f0a04SGreg Roach } 755a25f0a04SGreg Roach 756a25f0a04SGreg Roach /** 757991b93ddSGreg Roach * Convert a string to upper case. 758dfeee0a8SGreg Roach * 759dfeee0a8SGreg Roach * @param string $string 760a25f0a04SGreg Roach * 761a25f0a04SGreg Roach * @return string 762a25f0a04SGreg Roach */ 763c1010edaSGreg Roach public static function strtoupper($string) 764c1010edaSGreg Roach { 765991b93ddSGreg Roach if (in_array(self::$locale->language()->code(), self::DOTLESS_I_LOCALES)) { 766991b93ddSGreg Roach $string = strtr($string, self::DOTLESS_I_TOUPPER); 767a25f0a04SGreg Roach } 7685ddad20bSGreg Roach 7695ddad20bSGreg Roach return mb_strtoupper($string); 770a25f0a04SGreg Roach } 771a25f0a04SGreg Roach 772dfeee0a8SGreg Roach /** 773dfeee0a8SGreg Roach * Identify the script used for a piece of text 774dfeee0a8SGreg Roach * 775dfeee0a8SGreg Roach * @param $string 776dfeee0a8SGreg Roach * 777dfeee0a8SGreg Roach * @return string 778dfeee0a8SGreg Roach */ 779c1010edaSGreg Roach public static function textScript($string) 780c1010edaSGreg Roach { 781dfeee0a8SGreg Roach $string = strip_tags($string); // otherwise HTML tags show up as latin 782dfeee0a8SGreg Roach $string = html_entity_decode($string, ENT_QUOTES, 'UTF-8'); // otherwise HTML entities show up as latin 783c1010edaSGreg Roach $string = str_replace([ 784c1010edaSGreg Roach '@N.N.', 785c1010edaSGreg Roach '@P.N.', 786c1010edaSGreg Roach ], '', $string); // otherwise unknown names show up as latin 787dfeee0a8SGreg Roach $pos = 0; 788dfeee0a8SGreg Roach $strlen = strlen($string); 789dfeee0a8SGreg Roach while ($pos < $strlen) { 790dfeee0a8SGreg Roach // get the Unicode Code Point for the character at position $pos 791dfeee0a8SGreg Roach $byte1 = ord($string[$pos]); 792dfeee0a8SGreg Roach if ($byte1 < 0x80) { 793dfeee0a8SGreg Roach $code_point = $byte1; 794dfeee0a8SGreg Roach $chrlen = 1; 795dfeee0a8SGreg Roach } elseif ($byte1 < 0xC0) { 796dfeee0a8SGreg Roach // Invalid continuation character 797dfeee0a8SGreg Roach return 'Latn'; 798dfeee0a8SGreg Roach } elseif ($byte1 < 0xE0) { 799dfeee0a8SGreg Roach $code_point = (($byte1 & 0x1F) << 6) + (ord($string[$pos + 1]) & 0x3F); 800dfeee0a8SGreg Roach $chrlen = 2; 801dfeee0a8SGreg Roach } elseif ($byte1 < 0xF0) { 802dfeee0a8SGreg Roach $code_point = (($byte1 & 0x0F) << 12) + ((ord($string[$pos + 1]) & 0x3F) << 6) + (ord($string[$pos + 2]) & 0x3F); 803dfeee0a8SGreg Roach $chrlen = 3; 804dfeee0a8SGreg Roach } elseif ($byte1 < 0xF8) { 805dfeee0a8SGreg Roach $code_point = (($byte1 & 0x07) << 24) + ((ord($string[$pos + 1]) & 0x3F) << 12) + ((ord($string[$pos + 2]) & 0x3F) << 6) + (ord($string[$pos + 3]) & 0x3F); 806dfeee0a8SGreg Roach $chrlen = 3; 807dfeee0a8SGreg Roach } else { 808dfeee0a8SGreg Roach // Invalid UTF 809dfeee0a8SGreg Roach return 'Latn'; 810dfeee0a8SGreg Roach } 811dfeee0a8SGreg Roach 812991b93ddSGreg Roach foreach (self::SCRIPT_CHARACTER_RANGES as $range) { 813dfeee0a8SGreg Roach if ($code_point >= $range[1] && $code_point <= $range[2]) { 814dfeee0a8SGreg Roach return $range[0]; 815dfeee0a8SGreg Roach } 816dfeee0a8SGreg Roach } 817dfeee0a8SGreg Roach // Not a recognised script. Maybe punctuation, spacing, etc. Keep looking. 818dfeee0a8SGreg Roach $pos += $chrlen; 819dfeee0a8SGreg Roach } 820dfeee0a8SGreg Roach 821dfeee0a8SGreg Roach return 'Latn'; 822dfeee0a8SGreg Roach } 823dfeee0a8SGreg Roach 824dfeee0a8SGreg Roach /** 825dfeee0a8SGreg Roach * Convert a number of seconds into a relative time. For example, 630 => "10 hours, 30 minutes ago" 826dfeee0a8SGreg Roach * 827cbc1590aSGreg Roach * @param int $seconds 828dfeee0a8SGreg Roach * 829dfeee0a8SGreg Roach * @return string 830dfeee0a8SGreg Roach */ 831c1010edaSGreg Roach public static function timeAgo($seconds) 832c1010edaSGreg Roach { 833dfeee0a8SGreg Roach $minute = 60; 834dfeee0a8SGreg Roach $hour = 60 * $minute; 835dfeee0a8SGreg Roach $day = 24 * $hour; 836dfeee0a8SGreg Roach $month = 30 * $day; 837dfeee0a8SGreg Roach $year = 365 * $day; 838dfeee0a8SGreg Roach 839dfeee0a8SGreg Roach if ($seconds > $year) { 840dfeee0a8SGreg Roach $years = (int)($seconds / $year); 841cbc1590aSGreg Roach 842dfeee0a8SGreg Roach return self::plural('%s year ago', '%s years ago', $years, self::number($years)); 843dfeee0a8SGreg Roach } elseif ($seconds > $month) { 844dfeee0a8SGreg Roach $months = (int)($seconds / $month); 845cbc1590aSGreg Roach 846dfeee0a8SGreg Roach return self::plural('%s month ago', '%s months ago', $months, self::number($months)); 847dfeee0a8SGreg Roach } elseif ($seconds > $day) { 848dfeee0a8SGreg Roach $days = (int)($seconds / $day); 849cbc1590aSGreg Roach 850dfeee0a8SGreg Roach return self::plural('%s day ago', '%s days ago', $days, self::number($days)); 851dfeee0a8SGreg Roach } elseif ($seconds > $hour) { 852dfeee0a8SGreg Roach $hours = (int)($seconds / $hour); 853cbc1590aSGreg Roach 854dfeee0a8SGreg Roach return self::plural('%s hour ago', '%s hours ago', $hours, self::number($hours)); 855dfeee0a8SGreg Roach } elseif ($seconds > $minute) { 856dfeee0a8SGreg Roach $minutes = (int)($seconds / $minute); 857cbc1590aSGreg Roach 858dfeee0a8SGreg Roach return self::plural('%s minute ago', '%s minutes ago', $minutes, self::number($minutes)); 859dfeee0a8SGreg Roach } else { 860dfeee0a8SGreg Roach return self::plural('%s second ago', '%s seconds ago', $seconds, self::number($seconds)); 861dfeee0a8SGreg Roach } 862dfeee0a8SGreg Roach } 863dfeee0a8SGreg Roach 864dfeee0a8SGreg Roach /** 865dfeee0a8SGreg Roach * What format is used to display dates in the current locale? 866dfeee0a8SGreg Roach * 867dfeee0a8SGreg Roach * @return string 868dfeee0a8SGreg Roach */ 869c1010edaSGreg Roach public static function timeFormat() 870c1010edaSGreg Roach { 871bbb76c12SGreg Roach /* I18N: This is the format string for the time-of-day. See http://php.net/date for codes */ 872bbb76c12SGreg Roach return self::$translator->translate('%H:%i:%s'); 873dfeee0a8SGreg Roach } 874dfeee0a8SGreg Roach 875dfeee0a8SGreg Roach /** 876dfeee0a8SGreg Roach * Translate a string, and then substitute placeholders 877dfeee0a8SGreg Roach * 878dfeee0a8SGreg Roach * echo I18N::translate('Hello World!'); 879dfeee0a8SGreg Roach * echo I18N::translate('The %s sat on the mat', 'cat'); 880dfeee0a8SGreg Roach * 881dfeee0a8SGreg Roach * @return string 882dfeee0a8SGreg Roach */ 883c58d56c4SGreg Roach public static function translate(...$args) 884c1010edaSGreg Roach { 885dfeee0a8SGreg Roach $args[0] = self::$translator->translate($args[0]); 886dfeee0a8SGreg Roach 887c58d56c4SGreg Roach return sprintf(...$args); 888dfeee0a8SGreg Roach } 889dfeee0a8SGreg Roach 890dfeee0a8SGreg Roach /** 891dfeee0a8SGreg Roach * Context sensitive version of translate. 892dfeee0a8SGreg Roach * 893a4956c0eSGreg Roach * echo I18N::translateContext('NOMINATIVE', 'January'); 894a4956c0eSGreg Roach * echo I18N::translateContext('GENITIVE', 'January'); 895dfeee0a8SGreg Roach * 896dfeee0a8SGreg Roach * @return string 897dfeee0a8SGreg Roach */ 898c58d56c4SGreg Roach public static function translateContext(...$args) 899c1010edaSGreg Roach { 900c58d56c4SGreg Roach $args[1] = self::$translator->translateContext($args[0], $args[1]); 901c58d56c4SGreg Roach unset($args[0]); 902dfeee0a8SGreg Roach 903c58d56c4SGreg Roach return sprintf(...$args); 904a25f0a04SGreg Roach } 905a25f0a04SGreg Roach} 906