xref: /webtrees/app/I18N.php (revision c58d56c4224fdf3dfe6876027d39dae1abe8ee1f)
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    {
287c1010edaSGreg Roach        return /* I18N: This is the format string for full dates. See http://php.net/date for codes */
288c1010edaSGreg Roach            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": {' .
313c1010edaSGreg Roach            '  "first":    "' . /* I18N: A button label, first page */
314c1010edaSGreg Roach            self::translate('first') . '",' .
315c1010edaSGreg Roach            '  "last":     "' . /* I18N: A button label, last page */
316c1010edaSGreg Roach            self::translate('last') . '",' .
317c1010edaSGreg Roach            '  "next":     "' . /* I18N: A button label, next page */
318c1010edaSGreg Roach            self::translate('next') . '",' .
319c1010edaSGreg Roach            '  "previous": "' . /* I18N: A button label, previous page */
320c1010edaSGreg Roach            self::translate('previous') . '"' .
321dfeee0a8SGreg Roach            ' },' .
322dfeee0a8SGreg Roach            ' "emptyTable":     "' . self::translate('No records to display') . '",' .
323c1010edaSGreg Roach            ' "info":           "' . /* I18N: %s are placeholders for numbers */
324c1010edaSGreg Roach            self::translate('Showing %1$s to %2$s of %3$s', '_START_', '_END_', '_TOTAL_') . '",' .
3250b6446e1SGreg 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')) . '",' .
326c1010edaSGreg Roach            ' "infoFiltered":   "' . /* I18N: %s is a placeholder for a number */
327c1010edaSGreg Roach            self::translate('(filtered from %s total entries)', '_MAX_') . '",' .
328c1010edaSGreg Roach            ' "lengthMenu":     "' . /* I18N: %s is a number of records per page */
329c1010edaSGreg Roach            self::translate('Display %s', addslashes($length_options)) . '",' .
330dfeee0a8SGreg Roach            ' "loadingRecords": "' . self::translate('Loading…') . '",' .
331dfeee0a8SGreg Roach            ' "processing":     "' . self::translate('Loading…') . '",' .
332dfeee0a8SGreg Roach            ' "search":         "' . self::translate('Filter') . '",' .
333dfeee0a8SGreg Roach            ' "zeroRecords":    "' . self::translate('No records to display') . '"' .
3342d9b2ebaSGreg Roach            '}';
335dfeee0a8SGreg Roach    }
336dfeee0a8SGreg Roach
337dfeee0a8SGreg Roach    /**
338dfeee0a8SGreg Roach     * Convert the digits 0-9 into the local script
339dfeee0a8SGreg Roach     *
340dfeee0a8SGreg Roach     * Used for years, etc., where we do not want thousands-separators, decimals, etc.
341dfeee0a8SGreg Roach     *
342cbc1590aSGreg Roach     * @param int $n
343dfeee0a8SGreg Roach     *
344dfeee0a8SGreg Roach     * @return string
345dfeee0a8SGreg Roach     */
346c1010edaSGreg Roach    public static function digits($n)
347c1010edaSGreg Roach    {
348dfeee0a8SGreg Roach        return self::$locale->digits($n);
349dfeee0a8SGreg Roach    }
350dfeee0a8SGreg Roach
351dfeee0a8SGreg Roach    /**
352dfeee0a8SGreg Roach     * What is the direction of the current locale
353dfeee0a8SGreg Roach     *
354dfeee0a8SGreg Roach     * @return string "ltr" or "rtl"
355dfeee0a8SGreg Roach     */
356c1010edaSGreg Roach    public static function direction()
357c1010edaSGreg Roach    {
358dfeee0a8SGreg Roach        return self::$locale->direction();
359dfeee0a8SGreg Roach    }
360dfeee0a8SGreg Roach
361dfeee0a8SGreg Roach    /**
3627231a557SGreg Roach     * What is the first day of the week.
3637231a557SGreg Roach     *
364cbc1590aSGreg Roach     * @return int Sunday=0, Monday=1, etc.
3657231a557SGreg Roach     */
366c1010edaSGreg Roach    public static function firstDay()
367c1010edaSGreg Roach    {
3687231a557SGreg Roach        return self::$locale->territory()->firstDay();
3697231a557SGreg Roach    }
3707231a557SGreg Roach
3717231a557SGreg Roach    /**
372dfeee0a8SGreg Roach     * Convert a GEDCOM age string into translated_text
373dfeee0a8SGreg Roach     *
374dfeee0a8SGreg Roach     * NB: The import function will have normalised this, so we don't need
375dfeee0a8SGreg Roach     * to worry about badly formatted strings
3763d7a8a4cSGreg Roach     * NOTE: this function is not yet complete - eventually it will replace FunctionsDate::get_age_at_event()
377dfeee0a8SGreg Roach     *
378dfeee0a8SGreg Roach     * @param $string
379dfeee0a8SGreg Roach     *
380dfeee0a8SGreg Roach     * @return string
381dfeee0a8SGreg Roach     */
382c1010edaSGreg Roach    public static function gedcomAge($string)
383c1010edaSGreg Roach    {
384dfeee0a8SGreg Roach        switch ($string) {
385dfeee0a8SGreg Roach            case 'STILLBORN':
386dfeee0a8SGreg Roach                // I18N: Description of an individual’s age at an event. For example, Died 14 Jan 1900 (stillborn)
387dfeee0a8SGreg Roach                return self::translate('(stillborn)');
388dfeee0a8SGreg Roach            case 'INFANT':
389dfeee0a8SGreg Roach                // I18N: Description of an individual’s age at an event. For example, Died 14 Jan 1900 (in infancy)
390dfeee0a8SGreg Roach                return self::translate('(in infancy)');
391dfeee0a8SGreg Roach            case 'CHILD':
392dfeee0a8SGreg Roach                // I18N: Description of an individual’s age at an event. For example, Died 14 Jan 1900 (in childhood)
393dfeee0a8SGreg Roach                return self::translate('(in childhood)');
394dfeee0a8SGreg Roach        }
39513abd6f3SGreg Roach        $age = [];
396dfeee0a8SGreg Roach        if (preg_match('/(\d+)y/', $string, $match)) {
397dfeee0a8SGreg Roach            // I18N: Part of an age string. e.g. 5 years, 4 months and 3 days
398dfeee0a8SGreg Roach            $years = $match[1];
399dfeee0a8SGreg Roach            $age[] = self::plural('%s year', '%s years', $years, self::number($years));
400dfeee0a8SGreg Roach        } else {
401dfeee0a8SGreg Roach            $years = -1;
402dfeee0a8SGreg Roach        }
403dfeee0a8SGreg Roach        if (preg_match('/(\d+)m/', $string, $match)) {
404dfeee0a8SGreg Roach            // I18N: Part of an age string. e.g. 5 years, 4 months and 3 days
405dfeee0a8SGreg Roach            $age[] = self::plural('%s month', '%s months', $match[1], self::number($match[1]));
406dfeee0a8SGreg Roach        }
407dfeee0a8SGreg Roach        if (preg_match('/(\d+)w/', $string, $match)) {
408dfeee0a8SGreg Roach            // I18N: Part of an age string. e.g. 7 weeks and 3 days
409dfeee0a8SGreg Roach            $age[] = self::plural('%s week', '%s weeks', $match[1], self::number($match[1]));
410dfeee0a8SGreg Roach        }
411dfeee0a8SGreg Roach        if (preg_match('/(\d+)d/', $string, $match)) {
412dfeee0a8SGreg Roach            // I18N: Part of an age string. e.g. 5 years, 4 months and 3 days
413dfeee0a8SGreg Roach            $age[] = self::plural('%s day', '%s days', $match[1], self::number($match[1]));
414dfeee0a8SGreg Roach        }
415dfeee0a8SGreg Roach        // If an age is just a number of years, only show the number
416dfeee0a8SGreg Roach        if (count($age) === 1 && $years >= 0) {
417dfeee0a8SGreg Roach            $age = $years;
418dfeee0a8SGreg Roach        }
419dfeee0a8SGreg Roach        if ($age) {
420dfeee0a8SGreg Roach            if (!substr_compare($string, '<', 0, 1)) {
421dfeee0a8SGreg Roach                // I18N: Description of an individual’s age at an event. For example, Died 14 Jan 1900 (aged less than 21 years)
422dfeee0a8SGreg Roach                return self::translate('(aged less than %s)', $age);
423dfeee0a8SGreg Roach            } elseif (!substr_compare($string, '>', 0, 1)) {
424dfeee0a8SGreg Roach                // I18N: Description of an individual’s age at an event. For example, Died 14 Jan 1900 (aged more than 21 years)
425dfeee0a8SGreg Roach                return self::translate('(aged more than %s)', $age);
426dfeee0a8SGreg Roach            } else {
427dfeee0a8SGreg Roach                // I18N: Description of an individual’s age at an event. For example, Died 14 Jan 1900 (aged 43 years)
428dfeee0a8SGreg Roach                return self::translate('(aged %s)', $age);
429dfeee0a8SGreg Roach            }
430dfeee0a8SGreg Roach        } else {
431dfeee0a8SGreg Roach            // Not a valid string?
432dfeee0a8SGreg Roach            return self::translate('(aged %s)', $string);
433dfeee0a8SGreg Roach        }
434dfeee0a8SGreg Roach    }
435dfeee0a8SGreg Roach
436dfeee0a8SGreg Roach    /**
437dfeee0a8SGreg Roach     * Generate i18n markup for the <html> tag, e.g. lang="ar" dir="rtl"
438dfeee0a8SGreg Roach     *
439dfeee0a8SGreg Roach     * @return string
440dfeee0a8SGreg Roach     */
441c1010edaSGreg Roach    public static function htmlAttributes()
442c1010edaSGreg Roach    {
443dfeee0a8SGreg Roach        return self::$locale->htmlAttributes();
444dfeee0a8SGreg Roach    }
445dfeee0a8SGreg Roach
446dfeee0a8SGreg Roach    /**
447a25f0a04SGreg Roach     * Initialise the translation adapter with a locale setting.
448a25f0a04SGreg Roach     *
44915d603e7SGreg Roach     * @param string $code Use this locale/language code, or choose one automatically
450a25f0a04SGreg Roach     *
451a25f0a04SGreg Roach     * @return string $string
452a25f0a04SGreg Roach     */
453c1010edaSGreg Roach    public static function init($code = '')
454c1010edaSGreg Roach    {
455c314ecc9SGreg Roach        mb_internal_encoding('UTF-8');
456c314ecc9SGreg Roach
45715d603e7SGreg Roach        if ($code !== '') {
4583bdc890bSGreg Roach            // Create the specified locale
4593bdc890bSGreg Roach            self::$locale = Locale::create($code);
4603bdc890bSGreg Roach        } else {
4613bdc890bSGreg Roach            // Negotiate a locale, but if we can't then use a failsafe
4623bdc890bSGreg Roach            self::$locale = new LocaleEnUs;
463cbf1be5dSGreg Roach            if (Session::has('locale') && file_exists(WT_ROOT . 'language/' . Session::get('locale') . '.mo')) {
4643bdc890bSGreg Roach                // Previously used
46531bc7874SGreg Roach                self::$locale = Locale::create(Session::get('locale'));
4663bdc890bSGreg Roach            } else {
4671e71bdc0SGreg Roach                // Browser negotiation
4681e71bdc0SGreg Roach                $default_locale = new LocaleEnUs;
4693bdc890bSGreg Roach                try {
470c7e7cb40SGreg Roach                    // @TODO, when no language is requested by the user (e.g. search engines), we should use
471c7e7cb40SGreg Roach                    // the tree's default language.  However, we currently initialise languages before trees,
472c7e7cb40SGreg Roach                    //  so there is no tree available for us to use.
4733bdc890bSGreg Roach                } catch (\Exception $ex) {
474bd52fa32SGreg Roach                    DebugBar::addThrowable($ex);
4753bdc890bSGreg Roach                }
476149573a1SGreg Roach                self::$locale = Locale::httpAcceptLanguage($_SERVER, self::installedLocales(), $default_locale);
4773bdc890bSGreg Roach            }
4783bdc890bSGreg Roach        }
4793bdc890bSGreg Roach
480f1af7e1cSGreg Roach        $cache_dir  = WT_DATA_DIR . 'cache/';
481f1af7e1cSGreg Roach        $cache_file = $cache_dir . 'language-' . self::$locale->languageTag() . '-cache.php';
4823bdc890bSGreg Roach        if (file_exists($cache_file)) {
4833bdc890bSGreg Roach            $filemtime = filemtime($cache_file);
4843bdc890bSGreg Roach        } else {
4853bdc890bSGreg Roach            $filemtime = 0;
4863bdc890bSGreg Roach        }
4873bdc890bSGreg Roach
4883bdc890bSGreg Roach        // Load the translation file(s)
4897d6e38dfSGreg Roach        // Note that glob() returns false instead of an empty array when open_basedir_restriction
4907d6e38dfSGreg Roach        // is in force and no files are found. See PHP bug #47358.
4917a7f87d7SGreg Roach        if (defined('GLOB_BRACE')) {
4923bdc890bSGreg Roach            $translation_files = array_merge(
49313abd6f3SGreg Roach                [WT_ROOT . 'language/' . self::$locale->languageTag() . '.mo'],
49413abd6f3SGreg Roach                glob(WT_MODULES_DIR . '*/language/' . self::$locale->languageTag() . '.{csv,php,mo}', GLOB_BRACE) ?: [],
49513abd6f3SGreg Roach                glob(WT_DATA_DIR . 'language/' . self::$locale->languageTag() . '.{csv,php,mo}', GLOB_BRACE) ?: []
496a25f0a04SGreg Roach            );
4977a7f87d7SGreg Roach        } else {
4987a7f87d7SGreg Roach            // Some servers do not have GLOB_BRACE - see http://php.net/manual/en/function.glob.php
4997a7f87d7SGreg Roach            $translation_files = array_merge(
50013abd6f3SGreg Roach                [WT_ROOT . 'language/' . self::$locale->languageTag() . '.mo'],
50113abd6f3SGreg Roach                glob(WT_MODULES_DIR . '*/language/' . self::$locale->languageTag() . '.csv') ?: [],
50213abd6f3SGreg Roach                glob(WT_MODULES_DIR . '*/language/' . self::$locale->languageTag() . '.php') ?: [],
50313abd6f3SGreg Roach                glob(WT_MODULES_DIR . '*/language/' . self::$locale->languageTag() . '.mo') ?: [],
50413abd6f3SGreg Roach                glob(WT_DATA_DIR . 'language/' . self::$locale->languageTag() . '.csv') ?: [],
50513abd6f3SGreg Roach                glob(WT_DATA_DIR . 'language/' . self::$locale->languageTag() . '.php') ?: [],
50613abd6f3SGreg Roach                glob(WT_DATA_DIR . 'language/' . self::$locale->languageTag() . '.mo') ?: []
5077a7f87d7SGreg Roach            );
5087a7f87d7SGreg Roach        }
5097a7f87d7SGreg Roach        // Rebuild files after one hour
5107a7f87d7SGreg Roach        $rebuild_cache = time() > $filemtime + 3600;
5111e71bdc0SGreg Roach        // Rebuild files if any translation file has been updated
5123bdc890bSGreg Roach        foreach ($translation_files as $translation_file) {
5133bdc890bSGreg Roach            if (filemtime($translation_file) > $filemtime) {
5143bdc890bSGreg Roach                $rebuild_cache = true;
515a25f0a04SGreg Roach                break;
516a25f0a04SGreg Roach            }
517a25f0a04SGreg Roach        }
5183bdc890bSGreg Roach
5193bdc890bSGreg Roach        if ($rebuild_cache) {
52013abd6f3SGreg Roach            $translations = [];
5213bdc890bSGreg Roach            foreach ($translation_files as $translation_file) {
5223bdc890bSGreg Roach                $translation  = new Translation($translation_file);
5233bdc890bSGreg Roach                $translations = array_merge($translations, $translation->asArray());
524a25f0a04SGreg Roach            }
525f1af7e1cSGreg Roach            try {
526f1af7e1cSGreg Roach                File::mkdir($cache_dir);
527f1af7e1cSGreg Roach                file_put_contents($cache_file, '<?php return ' . var_export($translations, true) . ';');
528f1af7e1cSGreg Roach            } catch (Exception $ex) {
529bd52fa32SGreg Roach                DebugBar::addThrowable($ex);
530bd52fa32SGreg Roach
5317c2999b4SGreg Roach                // During setup, we may not have been able to create it.
532c85fb0c4SGreg Roach            }
5333bdc890bSGreg Roach        } else {
5343bdc890bSGreg Roach            $translations = include $cache_file;
535a25f0a04SGreg Roach        }
536a25f0a04SGreg Roach
5373bdc890bSGreg Roach        // Create a translator
5383bdc890bSGreg Roach        self::$translator = new Translator($translations, self::$locale->pluralRule());
539a25f0a04SGreg Roach
540c1010edaSGreg Roach        self::$list_separator = /* I18N: This punctuation is used to separate lists of items */
541c1010edaSGreg Roach            self::translate(', ');
542a25f0a04SGreg Roach
543991b93ddSGreg Roach        // Create a collator
544991b93ddSGreg Roach        try {
545444a65ecSGreg Roach            // PHP 5.6 cannot catch errors, so test first
546444a65ecSGreg Roach            if (class_exists('Collator')) {
547991b93ddSGreg Roach                self::$collator = new Collator(self::$locale->code());
548991b93ddSGreg Roach                // Ignore upper/lower case differences
549991b93ddSGreg Roach                self::$collator->setStrength(Collator::SECONDARY);
550444a65ecSGreg Roach            }
551991b93ddSGreg Roach        } catch (Exception $ex) {
552bd52fa32SGreg Roach            DebugBar::addThrowable($ex);
553bd52fa32SGreg Roach
554991b93ddSGreg Roach            // PHP-INTL is not installed?  We'll use a fallback later.
555991b93ddSGreg Roach        }
556991b93ddSGreg Roach
5575331c5eaSGreg Roach        return self::$locale->languageTag();
558a25f0a04SGreg Roach    }
559a25f0a04SGreg Roach
560a25f0a04SGreg Roach    /**
561c999a340SGreg Roach     * All locales for which a translation file exists.
562c999a340SGreg Roach     *
56315834aaeSGreg Roach     * @return LocaleInterface[]
564c999a340SGreg Roach     */
565c1010edaSGreg Roach    public static function installedLocales()
566c1010edaSGreg Roach    {
56713abd6f3SGreg Roach        $locales = [];
568c999a340SGreg Roach        foreach (glob(WT_ROOT . 'language/*.mo') as $file) {
569c999a340SGreg Roach            try {
570c999a340SGreg Roach                $locales[] = Locale::create(basename($file, '.mo'));
571c999a340SGreg Roach            } catch (\Exception $ex) {
572bd52fa32SGreg Roach                DebugBar::addThrowable($ex);
573bd52fa32SGreg Roach
5743bdc890bSGreg Roach                // Not a recognised locale
575a25f0a04SGreg Roach            }
576a25f0a04SGreg Roach        }
577c999a340SGreg Roach        usort($locales, '\Fisharebest\Localization\Locale::compare');
578c999a340SGreg Roach
579c999a340SGreg Roach        return $locales;
580a25f0a04SGreg Roach    }
581a25f0a04SGreg Roach
582a25f0a04SGreg Roach    /**
583a25f0a04SGreg Roach     * Return the endonym for a given language - as per http://cldr.unicode.org/
584a25f0a04SGreg Roach     *
585a25f0a04SGreg Roach     * @param string $locale
586a25f0a04SGreg Roach     *
587a25f0a04SGreg Roach     * @return string
588a25f0a04SGreg Roach     */
589c1010edaSGreg Roach    public static function languageName($locale)
590c1010edaSGreg Roach    {
591c999a340SGreg Roach        return Locale::create($locale)->endonym();
592a25f0a04SGreg Roach    }
593a25f0a04SGreg Roach
594a25f0a04SGreg Roach    /**
595a25f0a04SGreg Roach     * Return the script used by a given language
596a25f0a04SGreg Roach     *
597a25f0a04SGreg Roach     * @param string $locale
598a25f0a04SGreg Roach     *
599a25f0a04SGreg Roach     * @return string
600a25f0a04SGreg Roach     */
601c1010edaSGreg Roach    public static function languageScript($locale)
602c1010edaSGreg Roach    {
603c999a340SGreg Roach        return Locale::create($locale)->script()->code();
604a25f0a04SGreg Roach    }
605a25f0a04SGreg Roach
606a25f0a04SGreg Roach    /**
607dfeee0a8SGreg Roach     * Translate a number into the local representation.
608a25f0a04SGreg Roach     *
609dfeee0a8SGreg Roach     * e.g. 12345.67 becomes
610dfeee0a8SGreg Roach     * en: 12,345.67
611dfeee0a8SGreg Roach     * fr: 12 345,67
612dfeee0a8SGreg Roach     * de: 12.345,67
613dfeee0a8SGreg Roach     *
614dfeee0a8SGreg Roach     * @param float $n
615cbc1590aSGreg Roach     * @param int   $precision
616a25f0a04SGreg Roach     *
617a25f0a04SGreg Roach     * @return string
618a25f0a04SGreg Roach     */
619c1010edaSGreg Roach    public static function number($n, $precision = 0)
620c1010edaSGreg Roach    {
621dfeee0a8SGreg Roach        return self::$locale->number(round($n, $precision));
622dfeee0a8SGreg Roach    }
623dfeee0a8SGreg Roach
624dfeee0a8SGreg Roach    /**
625dfeee0a8SGreg Roach     * Translate a fraction into a percentage.
626dfeee0a8SGreg Roach     *
627dfeee0a8SGreg Roach     * e.g. 0.123 becomes
628dfeee0a8SGreg Roach     * en: 12.3%
629dfeee0a8SGreg Roach     * fr: 12,3 %
630dfeee0a8SGreg Roach     * de: 12,3%
631dfeee0a8SGreg Roach     *
632dfeee0a8SGreg Roach     * @param float $n
633cbc1590aSGreg Roach     * @param int   $precision
634dfeee0a8SGreg Roach     *
635dfeee0a8SGreg Roach     * @return string
636dfeee0a8SGreg Roach     */
637c1010edaSGreg Roach    public static function percentage($n, $precision = 0)
638c1010edaSGreg Roach    {
639dfeee0a8SGreg Roach        return self::$locale->percent(round($n, $precision + 2));
640dfeee0a8SGreg Roach    }
641dfeee0a8SGreg Roach
642dfeee0a8SGreg Roach    /**
643dfeee0a8SGreg Roach     * Translate a plural string
644dfeee0a8SGreg Roach     *
645dfeee0a8SGreg Roach     * echo self::plural('There is an error', 'There are errors', $num_errors);
646dfeee0a8SGreg Roach     * echo self::plural('There is one error', 'There are %s errors', $num_errors);
647dfeee0a8SGreg Roach     * echo self::plural('There is %1$s %2$s cat', 'There are %1$s %2$s cats', $num, $num, $colour);
648dfeee0a8SGreg Roach     *
649dfeee0a8SGreg Roach     * @return string
650dfeee0a8SGreg Roach     */
651*c58d56c4SGreg Roach    public static function plural(...$args)
652c1010edaSGreg Roach    {
653beb72eacSGreg Roach        $args[0] = self::$translator->translatePlural($args[0], $args[1], (int) $args[2]);
654dfeee0a8SGreg Roach        unset($args[1], $args[2]);
655dfeee0a8SGreg Roach
656*c58d56c4SGreg Roach        return sprintf(...$args);
657dfeee0a8SGreg Roach    }
658dfeee0a8SGreg Roach
659dfeee0a8SGreg Roach    /**
660dfeee0a8SGreg Roach     * UTF8 version of PHP::strrev()
661dfeee0a8SGreg Roach     *
662dfeee0a8SGreg Roach     * Reverse RTL text for third-party libraries such as GD2 and googlechart.
663dfeee0a8SGreg Roach     *
664dfeee0a8SGreg Roach     * These do not support UTF8 text direction, so we must mimic it for them.
665dfeee0a8SGreg Roach     *
666dfeee0a8SGreg Roach     * Numbers are always rendered LTR, even in RTL text.
667dfeee0a8SGreg Roach     * The visual direction of characters such as parentheses should be reversed.
668dfeee0a8SGreg Roach     *
669dfeee0a8SGreg Roach     * @param string $text Text to be reversed
670dfeee0a8SGreg Roach     *
671dfeee0a8SGreg Roach     * @return string
672dfeee0a8SGreg Roach     */
673c1010edaSGreg Roach    public static function reverseText($text)
674c1010edaSGreg Roach    {
675dfeee0a8SGreg Roach        // Remove HTML markup - we can't display it and it is LTR.
6769524b7b5SGreg Roach        $text = strip_tags($text);
6779524b7b5SGreg Roach        // Remove HTML entities.
6789524b7b5SGreg Roach        $text = html_entity_decode($text, ENT_QUOTES, 'UTF-8');
679dfeee0a8SGreg Roach
680dfeee0a8SGreg Roach        // LTR text doesn't need reversing
681dfeee0a8SGreg Roach        if (self::scriptDirection(self::textScript($text)) === 'ltr') {
682dfeee0a8SGreg Roach            return $text;
683dfeee0a8SGreg Roach        }
684dfeee0a8SGreg Roach
685dfeee0a8SGreg Roach        // Mirrored characters
686991b93ddSGreg Roach        $text = strtr($text, self::MIRROR_CHARACTERS);
687dfeee0a8SGreg Roach
688dfeee0a8SGreg Roach        $reversed = '';
689dfeee0a8SGreg Roach        $digits   = '';
690dfeee0a8SGreg Roach        while ($text != '') {
691dfeee0a8SGreg Roach            $letter = mb_substr($text, 0, 1);
692dfeee0a8SGreg Roach            $text   = mb_substr($text, 1);
693dfeee0a8SGreg Roach            if (strpos(self::DIGITS, $letter) !== false) {
694dfeee0a8SGreg Roach                $digits .= $letter;
695a25f0a04SGreg Roach            } else {
696dfeee0a8SGreg Roach                $reversed = $letter . $digits . $reversed;
697dfeee0a8SGreg Roach                $digits   = '';
698dfeee0a8SGreg Roach            }
699a25f0a04SGreg Roach        }
700a25f0a04SGreg Roach
701dfeee0a8SGreg Roach        return $digits . $reversed;
702a25f0a04SGreg Roach    }
703a25f0a04SGreg Roach
704a25f0a04SGreg Roach    /**
705a25f0a04SGreg Roach     * Return the direction (ltr or rtl) for a given script
706a25f0a04SGreg Roach     *
707a25f0a04SGreg Roach     * The PHP/intl library does not provde this information, so we need
708a25f0a04SGreg Roach     * our own lookup table.
709a25f0a04SGreg Roach     *
710a25f0a04SGreg Roach     * @param string $script
711a25f0a04SGreg Roach     *
712a25f0a04SGreg Roach     * @return string
713a25f0a04SGreg Roach     */
714c1010edaSGreg Roach    public static function scriptDirection($script)
715c1010edaSGreg Roach    {
716a25f0a04SGreg Roach        switch ($script) {
717a25f0a04SGreg Roach            case 'Arab':
718a25f0a04SGreg Roach            case 'Hebr':
719a25f0a04SGreg Roach            case 'Mong':
720a25f0a04SGreg Roach            case 'Thaa':
721a25f0a04SGreg Roach                return 'rtl';
722a25f0a04SGreg Roach            default:
723a25f0a04SGreg Roach                return 'ltr';
724a25f0a04SGreg Roach        }
725a25f0a04SGreg Roach    }
726a25f0a04SGreg Roach
727a25f0a04SGreg Roach    /**
728991b93ddSGreg Roach     * Perform a case-insensitive comparison of two strings.
729a25f0a04SGreg Roach     *
730a25f0a04SGreg Roach     * @param string $string1
731a25f0a04SGreg Roach     * @param string $string2
732a25f0a04SGreg Roach     *
733cbc1590aSGreg Roach     * @return int
734a25f0a04SGreg Roach     */
735c1010edaSGreg Roach    public static function strcasecmp($string1, $string2)
736c1010edaSGreg Roach    {
737991b93ddSGreg Roach        if (self::$collator instanceof Collator) {
738991b93ddSGreg Roach            return self::$collator->compare($string1, $string2);
739a25f0a04SGreg Roach        } else {
740991b93ddSGreg Roach            return strcmp(self::strtolower($string1), self::strtolower($string2));
741a25f0a04SGreg Roach        }
742a25f0a04SGreg Roach    }
743a25f0a04SGreg Roach
744a25f0a04SGreg Roach    /**
745991b93ddSGreg Roach     * Convert a string to lower case.
746a25f0a04SGreg Roach     *
747dfeee0a8SGreg Roach     * @param string $string
748a25f0a04SGreg Roach     *
749a25f0a04SGreg Roach     * @return string
750a25f0a04SGreg Roach     */
751c1010edaSGreg Roach    public static function strtolower($string)
752c1010edaSGreg Roach    {
753991b93ddSGreg Roach        if (in_array(self::$locale->language()->code(), self::DOTLESS_I_LOCALES)) {
754991b93ddSGreg Roach            $string = strtr($string, self::DOTLESS_I_TOLOWER);
755a25f0a04SGreg Roach        }
7565ddad20bSGreg Roach
7575ddad20bSGreg Roach        return mb_strtolower($string);
758a25f0a04SGreg Roach    }
759a25f0a04SGreg Roach
760a25f0a04SGreg Roach    /**
761991b93ddSGreg Roach     * Convert a string to upper case.
762dfeee0a8SGreg Roach     *
763dfeee0a8SGreg Roach     * @param string $string
764a25f0a04SGreg Roach     *
765a25f0a04SGreg Roach     * @return string
766a25f0a04SGreg Roach     */
767c1010edaSGreg Roach    public static function strtoupper($string)
768c1010edaSGreg Roach    {
769991b93ddSGreg Roach        if (in_array(self::$locale->language()->code(), self::DOTLESS_I_LOCALES)) {
770991b93ddSGreg Roach            $string = strtr($string, self::DOTLESS_I_TOUPPER);
771a25f0a04SGreg Roach        }
7725ddad20bSGreg Roach
7735ddad20bSGreg Roach        return mb_strtoupper($string);
774a25f0a04SGreg Roach    }
775a25f0a04SGreg Roach
776dfeee0a8SGreg Roach    /**
777dfeee0a8SGreg Roach     * Identify the script used for a piece of text
778dfeee0a8SGreg Roach     *
779dfeee0a8SGreg Roach     * @param $string
780dfeee0a8SGreg Roach     *
781dfeee0a8SGreg Roach     * @return string
782dfeee0a8SGreg Roach     */
783c1010edaSGreg Roach    public static function textScript($string)
784c1010edaSGreg Roach    {
785dfeee0a8SGreg Roach        $string = strip_tags($string); // otherwise HTML tags show up as latin
786dfeee0a8SGreg Roach        $string = html_entity_decode($string, ENT_QUOTES, 'UTF-8'); // otherwise HTML entities show up as latin
787c1010edaSGreg Roach        $string = str_replace([
788c1010edaSGreg Roach            '@N.N.',
789c1010edaSGreg Roach            '@P.N.',
790c1010edaSGreg Roach        ], '', $string); // otherwise unknown names show up as latin
791dfeee0a8SGreg Roach        $pos    = 0;
792dfeee0a8SGreg Roach        $strlen = strlen($string);
793dfeee0a8SGreg Roach        while ($pos < $strlen) {
794dfeee0a8SGreg Roach            // get the Unicode Code Point for the character at position $pos
795dfeee0a8SGreg Roach            $byte1 = ord($string[$pos]);
796dfeee0a8SGreg Roach            if ($byte1 < 0x80) {
797dfeee0a8SGreg Roach                $code_point = $byte1;
798dfeee0a8SGreg Roach                $chrlen     = 1;
799dfeee0a8SGreg Roach            } elseif ($byte1 < 0xC0) {
800dfeee0a8SGreg Roach                // Invalid continuation character
801dfeee0a8SGreg Roach                return 'Latn';
802dfeee0a8SGreg Roach            } elseif ($byte1 < 0xE0) {
803dfeee0a8SGreg Roach                $code_point = (($byte1 & 0x1F) << 6) + (ord($string[$pos + 1]) & 0x3F);
804dfeee0a8SGreg Roach                $chrlen     = 2;
805dfeee0a8SGreg Roach            } elseif ($byte1 < 0xF0) {
806dfeee0a8SGreg Roach                $code_point = (($byte1 & 0x0F) << 12) + ((ord($string[$pos + 1]) & 0x3F) << 6) + (ord($string[$pos + 2]) & 0x3F);
807dfeee0a8SGreg Roach                $chrlen     = 3;
808dfeee0a8SGreg Roach            } elseif ($byte1 < 0xF8) {
809dfeee0a8SGreg Roach                $code_point = (($byte1 & 0x07) << 24) + ((ord($string[$pos + 1]) & 0x3F) << 12) + ((ord($string[$pos + 2]) & 0x3F) << 6) + (ord($string[$pos + 3]) & 0x3F);
810dfeee0a8SGreg Roach                $chrlen     = 3;
811dfeee0a8SGreg Roach            } else {
812dfeee0a8SGreg Roach                // Invalid UTF
813dfeee0a8SGreg Roach                return 'Latn';
814dfeee0a8SGreg Roach            }
815dfeee0a8SGreg Roach
816991b93ddSGreg Roach            foreach (self::SCRIPT_CHARACTER_RANGES as $range) {
817dfeee0a8SGreg Roach                if ($code_point >= $range[1] && $code_point <= $range[2]) {
818dfeee0a8SGreg Roach                    return $range[0];
819dfeee0a8SGreg Roach                }
820dfeee0a8SGreg Roach            }
821dfeee0a8SGreg Roach            // Not a recognised script. Maybe punctuation, spacing, etc. Keep looking.
822dfeee0a8SGreg Roach            $pos += $chrlen;
823dfeee0a8SGreg Roach        }
824dfeee0a8SGreg Roach
825dfeee0a8SGreg Roach        return 'Latn';
826dfeee0a8SGreg Roach    }
827dfeee0a8SGreg Roach
828dfeee0a8SGreg Roach    /**
829dfeee0a8SGreg Roach     * Convert a number of seconds into a relative time. For example, 630 => "10 hours, 30 minutes ago"
830dfeee0a8SGreg Roach     *
831cbc1590aSGreg Roach     * @param int $seconds
832dfeee0a8SGreg Roach     *
833dfeee0a8SGreg Roach     * @return string
834dfeee0a8SGreg Roach     */
835c1010edaSGreg Roach    public static function timeAgo($seconds)
836c1010edaSGreg Roach    {
837dfeee0a8SGreg Roach        $minute = 60;
838dfeee0a8SGreg Roach        $hour   = 60 * $minute;
839dfeee0a8SGreg Roach        $day    = 24 * $hour;
840dfeee0a8SGreg Roach        $month  = 30 * $day;
841dfeee0a8SGreg Roach        $year   = 365 * $day;
842dfeee0a8SGreg Roach
843dfeee0a8SGreg Roach        if ($seconds > $year) {
844dfeee0a8SGreg Roach            $years = (int)($seconds / $year);
845cbc1590aSGreg Roach
846dfeee0a8SGreg Roach            return self::plural('%s year ago', '%s years ago', $years, self::number($years));
847dfeee0a8SGreg Roach        } elseif ($seconds > $month) {
848dfeee0a8SGreg Roach            $months = (int)($seconds / $month);
849cbc1590aSGreg Roach
850dfeee0a8SGreg Roach            return self::plural('%s month ago', '%s months ago', $months, self::number($months));
851dfeee0a8SGreg Roach        } elseif ($seconds > $day) {
852dfeee0a8SGreg Roach            $days = (int)($seconds / $day);
853cbc1590aSGreg Roach
854dfeee0a8SGreg Roach            return self::plural('%s day ago', '%s days ago', $days, self::number($days));
855dfeee0a8SGreg Roach        } elseif ($seconds > $hour) {
856dfeee0a8SGreg Roach            $hours = (int)($seconds / $hour);
857cbc1590aSGreg Roach
858dfeee0a8SGreg Roach            return self::plural('%s hour ago', '%s hours ago', $hours, self::number($hours));
859dfeee0a8SGreg Roach        } elseif ($seconds > $minute) {
860dfeee0a8SGreg Roach            $minutes = (int)($seconds / $minute);
861cbc1590aSGreg Roach
862dfeee0a8SGreg Roach            return self::plural('%s minute ago', '%s minutes ago', $minutes, self::number($minutes));
863dfeee0a8SGreg Roach        } else {
864dfeee0a8SGreg Roach            return self::plural('%s second ago', '%s seconds ago', $seconds, self::number($seconds));
865dfeee0a8SGreg Roach        }
866dfeee0a8SGreg Roach    }
867dfeee0a8SGreg Roach
868dfeee0a8SGreg Roach    /**
869dfeee0a8SGreg Roach     * What format is used to display dates in the current locale?
870dfeee0a8SGreg Roach     *
871dfeee0a8SGreg Roach     * @return string
872dfeee0a8SGreg Roach     */
873c1010edaSGreg Roach    public static function timeFormat()
874c1010edaSGreg Roach    {
875c1010edaSGreg Roach        return /* I18N: This is the format string for the time-of-day. See http://php.net/date for codes */
876c1010edaSGreg Roach            self::$translator->translate('%H:%i:%s');
877dfeee0a8SGreg Roach    }
878dfeee0a8SGreg Roach
879dfeee0a8SGreg Roach    /**
880dfeee0a8SGreg Roach     * Translate a string, and then substitute placeholders
881dfeee0a8SGreg Roach     *
882dfeee0a8SGreg Roach     * echo I18N::translate('Hello World!');
883dfeee0a8SGreg Roach     * echo I18N::translate('The %s sat on the mat', 'cat');
884dfeee0a8SGreg Roach     *
885dfeee0a8SGreg Roach     * @return string
886dfeee0a8SGreg Roach     */
887*c58d56c4SGreg Roach    public static function translate(...$args)
888c1010edaSGreg Roach    {
889dfeee0a8SGreg Roach        $args[0] = self::$translator->translate($args[0]);
890dfeee0a8SGreg Roach
891*c58d56c4SGreg Roach        return sprintf(...$args);
892dfeee0a8SGreg Roach    }
893dfeee0a8SGreg Roach
894dfeee0a8SGreg Roach    /**
895dfeee0a8SGreg Roach     * Context sensitive version of translate.
896dfeee0a8SGreg Roach     *
897a4956c0eSGreg Roach     * echo I18N::translateContext('NOMINATIVE', 'January');
898a4956c0eSGreg Roach     * echo I18N::translateContext('GENITIVE', 'January');
899dfeee0a8SGreg Roach     *
900dfeee0a8SGreg Roach     * @return string
901dfeee0a8SGreg Roach     */
902*c58d56c4SGreg Roach    public static function translateContext(...$args)
903c1010edaSGreg Roach    {
904*c58d56c4SGreg Roach        $args[1] = self::$translator->translateContext($args[0], $args[1]);
905*c58d56c4SGreg Roach        unset($args[0]);
906dfeee0a8SGreg Roach
907*c58d56c4SGreg Roach        return sprintf(...$args);
908a25f0a04SGreg Roach    }
909a25f0a04SGreg Roach}
910