xref: /webtrees/app/I18N.php (revision e58a20ff6a8776678adb8f067c0874f8e2a916a1)
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
40991b93ddSGreg Roach    /** @var  Collator 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     * Convert a GEDCOM age string into translated_text
366dfeee0a8SGreg Roach     *
367dfeee0a8SGreg Roach     * NB: The import function will have normalised this, so we don't need
368dfeee0a8SGreg Roach     * to worry about badly formatted strings
3693d7a8a4cSGreg Roach     * NOTE: this function is not yet complete - eventually it will replace FunctionsDate::get_age_at_event()
370dfeee0a8SGreg Roach     *
371dfeee0a8SGreg Roach     * @param $string
372dfeee0a8SGreg Roach     *
373dfeee0a8SGreg Roach     * @return string
374dfeee0a8SGreg Roach     */
37555664801SGreg Roach    public static function gedcomAge(string $string): string
376c1010edaSGreg Roach    {
377dfeee0a8SGreg Roach        switch ($string) {
378dfeee0a8SGreg Roach            case 'STILLBORN':
379dfeee0a8SGreg Roach                // I18N: Description of an individual’s age at an event. For example, Died 14 Jan 1900 (stillborn)
380dfeee0a8SGreg Roach                return self::translate('(stillborn)');
381dfeee0a8SGreg Roach            case 'INFANT':
382dfeee0a8SGreg Roach                // I18N: Description of an individual’s age at an event. For example, Died 14 Jan 1900 (in infancy)
383dfeee0a8SGreg Roach                return self::translate('(in infancy)');
384dfeee0a8SGreg Roach            case 'CHILD':
385dfeee0a8SGreg Roach                // I18N: Description of an individual’s age at an event. For example, Died 14 Jan 1900 (in childhood)
386dfeee0a8SGreg Roach                return self::translate('(in childhood)');
387dfeee0a8SGreg Roach        }
38813abd6f3SGreg Roach        $age = [];
389dfeee0a8SGreg Roach        if (preg_match('/(\d+)y/', $string, $match)) {
39055664801SGreg Roach            $years = (int) $match[1];
391dfeee0a8SGreg Roach            // I18N: Part of an age string. e.g. 5 years, 4 months and 3 days
392dfeee0a8SGreg Roach            $age[] = self::plural('%s year', '%s years', $years, self::number($years));
393dfeee0a8SGreg Roach        } else {
394dfeee0a8SGreg Roach            $years = -1;
395dfeee0a8SGreg Roach        }
396dfeee0a8SGreg Roach        if (preg_match('/(\d+)m/', $string, $match)) {
39755664801SGreg Roach            $months = (int) $match[1];
398dfeee0a8SGreg Roach            // I18N: Part of an age string. e.g. 5 years, 4 months and 3 days
39955664801SGreg Roach            $age[] = self::plural('%s month', '%s months', $months, self::number($months));
400dfeee0a8SGreg Roach        }
401dfeee0a8SGreg Roach        if (preg_match('/(\d+)w/', $string, $match)) {
40255664801SGreg Roach            $weeks = (int) $match[1];
403dfeee0a8SGreg Roach            // I18N: Part of an age string. e.g. 7 weeks and 3 days
40455664801SGreg Roach            $age[] = self::plural('%s week', '%s weeks', $weeks, self::number($weeks));
405dfeee0a8SGreg Roach        }
406dfeee0a8SGreg Roach        if (preg_match('/(\d+)d/', $string, $match)) {
40755664801SGreg Roach            $days = (int) $match[1];
408dfeee0a8SGreg Roach            // I18N: Part of an age string. e.g. 5 years, 4 months and 3 days
40955664801SGreg Roach            $age[] = self::plural('%s day', '%s days', $days, self::number($days));
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);
419b2ce94c6SRico Sonntag            }
420b2ce94c6SRico Sonntag
421b2ce94c6SRico Sonntag            if (!substr_compare($string, '>', 0, 1)) {
422dfeee0a8SGreg Roach                // I18N: Description of an individual’s age at an event. For example, Died 14 Jan 1900 (aged more than 21 years)
423dfeee0a8SGreg Roach                return self::translate('(aged more than %s)', $age);
424b2ce94c6SRico Sonntag            }
425b2ce94c6SRico Sonntag
426dfeee0a8SGreg Roach            // I18N: Description of an individual’s age at an event. For example, Died 14 Jan 1900 (aged 43 years)
427dfeee0a8SGreg Roach            return self::translate('(aged %s)', $age);
428dfeee0a8SGreg Roach        }
429b2ce94c6SRico Sonntag
430dfeee0a8SGreg Roach        // Not a valid string?
431dfeee0a8SGreg Roach        return self::translate('(aged %s)', $string);
432dfeee0a8SGreg Roach    }
433dfeee0a8SGreg Roach
434dfeee0a8SGreg Roach    /**
435dfeee0a8SGreg Roach     * Generate i18n markup for the <html> tag, e.g. lang="ar" dir="rtl"
436dfeee0a8SGreg Roach     *
437dfeee0a8SGreg Roach     * @return string
438dfeee0a8SGreg Roach     */
4398f53f488SRico Sonntag    public static function htmlAttributes(): string
440c1010edaSGreg Roach    {
441dfeee0a8SGreg Roach        return self::$locale->htmlAttributes();
442dfeee0a8SGreg Roach    }
443dfeee0a8SGreg Roach
444dfeee0a8SGreg Roach    /**
445a25f0a04SGreg Roach     * Initialise the translation adapter with a locale setting.
446a25f0a04SGreg Roach     *
44715d603e7SGreg Roach     * @param string    $code Use this locale/language code, or choose one automatically
448*e58a20ffSGreg Roach     * @param Tree|null $tree
449a25f0a04SGreg Roach     *
450a25f0a04SGreg Roach     * @return string $string
451a25f0a04SGreg Roach     */
452*e58a20ffSGreg Roach    public static function init(string $code = '', Tree $tree = null): string
453c1010edaSGreg Roach    {
454c314ecc9SGreg Roach        mb_internal_encoding('UTF-8');
455c314ecc9SGreg Roach
45615d603e7SGreg Roach        if ($code !== '') {
4573bdc890bSGreg Roach            // Create the specified locale
4583bdc890bSGreg Roach            self::$locale = Locale::create($code);
459*e58a20ffSGreg Roach        } elseif (Session::has('locale') && file_exists(WT_ROOT . 'language/' . Session::get('locale') . '.mo')) {
460*e58a20ffSGreg Roach            // Select a previously used locale
46131bc7874SGreg Roach            self::$locale = Locale::create(Session::get('locale'));
4623bdc890bSGreg Roach        } else {
463*e58a20ffSGreg Roach            if ($tree instanceof Tree) {
464*e58a20ffSGreg Roach                $default_locale = Locale::create($tree->getPreference('LANGUAGE', 'en-US'));
465*e58a20ffSGreg Roach            } else {
46659f2f229SGreg Roach                $default_locale = new LocaleEnUs();
4673bdc890bSGreg Roach            }
468*e58a20ffSGreg Roach
469*e58a20ffSGreg Roach            // Negotiate with the browser.
470*e58a20ffSGreg Roach            // Search engines don't negotiate.  They get the default locale of the tree.
471149573a1SGreg Roach            self::$locale = Locale::httpAcceptLanguage($_SERVER, self::installedLocales(), $default_locale);
4723bdc890bSGreg Roach        }
4733bdc890bSGreg Roach
474f1af7e1cSGreg Roach        $cache_dir  = WT_DATA_DIR . 'cache/';
475f1af7e1cSGreg Roach        $cache_file = $cache_dir . 'language-' . self::$locale->languageTag() . '-cache.php';
4763bdc890bSGreg Roach        if (file_exists($cache_file)) {
4773bdc890bSGreg Roach            $filemtime = filemtime($cache_file);
4783bdc890bSGreg Roach        } else {
4793bdc890bSGreg Roach            $filemtime = 0;
4803bdc890bSGreg Roach        }
4813bdc890bSGreg Roach
4823bdc890bSGreg Roach        // Load the translation file(s)
4837d6e38dfSGreg Roach        // Note that glob() returns false instead of an empty array when open_basedir_restriction
4847d6e38dfSGreg Roach        // is in force and no files are found. See PHP bug #47358.
4857a7f87d7SGreg Roach        if (defined('GLOB_BRACE')) {
4863bdc890bSGreg Roach            $translation_files = array_merge(
48713abd6f3SGreg Roach                [WT_ROOT . 'language/' . self::$locale->languageTag() . '.mo'],
48813abd6f3SGreg Roach                glob(WT_MODULES_DIR . '*/language/' . self::$locale->languageTag() . '.{csv,php,mo}', GLOB_BRACE) ?: [],
48913abd6f3SGreg Roach                glob(WT_DATA_DIR . 'language/' . self::$locale->languageTag() . '.{csv,php,mo}', GLOB_BRACE) ?: []
490a25f0a04SGreg Roach            );
4917a7f87d7SGreg Roach        } else {
4927a7f87d7SGreg Roach            // Some servers do not have GLOB_BRACE - see http://php.net/manual/en/function.glob.php
4937a7f87d7SGreg Roach            $translation_files = array_merge(
49413abd6f3SGreg Roach                [WT_ROOT . 'language/' . self::$locale->languageTag() . '.mo'],
49513abd6f3SGreg Roach                glob(WT_MODULES_DIR . '*/language/' . self::$locale->languageTag() . '.csv') ?: [],
49613abd6f3SGreg Roach                glob(WT_MODULES_DIR . '*/language/' . self::$locale->languageTag() . '.php') ?: [],
49713abd6f3SGreg Roach                glob(WT_MODULES_DIR . '*/language/' . self::$locale->languageTag() . '.mo') ?: [],
49813abd6f3SGreg Roach                glob(WT_DATA_DIR . 'language/' . self::$locale->languageTag() . '.csv') ?: [],
49913abd6f3SGreg Roach                glob(WT_DATA_DIR . 'language/' . self::$locale->languageTag() . '.php') ?: [],
50013abd6f3SGreg Roach                glob(WT_DATA_DIR . 'language/' . self::$locale->languageTag() . '.mo') ?: []
5017a7f87d7SGreg Roach            );
5027a7f87d7SGreg Roach        }
5037a7f87d7SGreg Roach        // Rebuild files after one hour
5047a7f87d7SGreg Roach        $rebuild_cache = time() > $filemtime + 3600;
5051e71bdc0SGreg Roach        // Rebuild files if any translation file has been updated
5063bdc890bSGreg Roach        foreach ($translation_files as $translation_file) {
5073bdc890bSGreg Roach            if (filemtime($translation_file) > $filemtime) {
5083bdc890bSGreg Roach                $rebuild_cache = true;
509a25f0a04SGreg Roach                break;
510a25f0a04SGreg Roach            }
511a25f0a04SGreg Roach        }
5123bdc890bSGreg Roach
5133bdc890bSGreg Roach        if ($rebuild_cache) {
51413abd6f3SGreg Roach            $translations = [];
5153bdc890bSGreg Roach            foreach ($translation_files as $translation_file) {
5163bdc890bSGreg Roach                $translation  = new Translation($translation_file);
5173bdc890bSGreg Roach                $translations = array_merge($translations, $translation->asArray());
518a25f0a04SGreg Roach            }
519f1af7e1cSGreg Roach            try {
520f1af7e1cSGreg Roach                File::mkdir($cache_dir);
521f1af7e1cSGreg Roach                file_put_contents($cache_file, '<?php return ' . var_export($translations, true) . ';');
522f1af7e1cSGreg Roach            } catch (Exception $ex) {
523bd52fa32SGreg Roach                DebugBar::addThrowable($ex);
524bd52fa32SGreg Roach
5257c2999b4SGreg Roach                // During setup, we may not have been able to create it.
526c85fb0c4SGreg Roach            }
5273bdc890bSGreg Roach        } else {
5283bdc890bSGreg Roach            $translations = include $cache_file;
529a25f0a04SGreg Roach        }
530a25f0a04SGreg Roach
5313bdc890bSGreg Roach        // Create a translator
5323bdc890bSGreg Roach        self::$translator = new Translator($translations, self::$locale->pluralRule());
533a25f0a04SGreg Roach
534bbb76c12SGreg Roach        /* I18N: This punctuation is used to separate lists of items */
535bbb76c12SGreg Roach        self::$list_separator = self::translate(', ');
536a25f0a04SGreg Roach
537991b93ddSGreg Roach        // Create a collator
538991b93ddSGreg Roach        try {
539444a65ecSGreg Roach            // PHP 5.6 cannot catch errors, so test first
540444a65ecSGreg Roach            if (class_exists('Collator')) {
541991b93ddSGreg Roach                self::$collator = new Collator(self::$locale->code());
542991b93ddSGreg Roach                // Ignore upper/lower case differences
543991b93ddSGreg Roach                self::$collator->setStrength(Collator::SECONDARY);
544444a65ecSGreg Roach            }
545991b93ddSGreg Roach        } catch (Exception $ex) {
546bd52fa32SGreg Roach            DebugBar::addThrowable($ex);
547bd52fa32SGreg Roach
548991b93ddSGreg Roach            // PHP-INTL is not installed?  We'll use a fallback later.
549991b93ddSGreg Roach        }
550991b93ddSGreg Roach
5515331c5eaSGreg Roach        return self::$locale->languageTag();
552a25f0a04SGreg Roach    }
553a25f0a04SGreg Roach
554a25f0a04SGreg Roach    /**
555c999a340SGreg Roach     * All locales for which a translation file exists.
556c999a340SGreg Roach     *
55715834aaeSGreg Roach     * @return LocaleInterface[]
558c999a340SGreg Roach     */
5598f53f488SRico Sonntag    public static function installedLocales(): array
560c1010edaSGreg Roach    {
56113abd6f3SGreg Roach        $locales = [];
562c999a340SGreg Roach        foreach (glob(WT_ROOT . 'language/*.mo') as $file) {
563c999a340SGreg Roach            try {
564c999a340SGreg Roach                $locales[] = Locale::create(basename($file, '.mo'));
565c999a340SGreg Roach            } catch (\Exception $ex) {
566bd52fa32SGreg Roach                DebugBar::addThrowable($ex);
567bd52fa32SGreg Roach
5683bdc890bSGreg Roach                // Not a recognised locale
569a25f0a04SGreg Roach            }
570a25f0a04SGreg Roach        }
571c999a340SGreg Roach        usort($locales, '\Fisharebest\Localization\Locale::compare');
572c999a340SGreg Roach
573c999a340SGreg Roach        return $locales;
574a25f0a04SGreg Roach    }
575a25f0a04SGreg Roach
576a25f0a04SGreg Roach    /**
577a25f0a04SGreg Roach     * Return the endonym for a given language - as per http://cldr.unicode.org/
578a25f0a04SGreg Roach     *
579a25f0a04SGreg Roach     * @param string $locale
580a25f0a04SGreg Roach     *
581a25f0a04SGreg Roach     * @return string
582a25f0a04SGreg Roach     */
58355664801SGreg Roach    public static function languageName(string $locale): string
584c1010edaSGreg Roach    {
585c999a340SGreg Roach        return Locale::create($locale)->endonym();
586a25f0a04SGreg Roach    }
587a25f0a04SGreg Roach
588a25f0a04SGreg Roach    /**
589a25f0a04SGreg Roach     * Return the script used by a given language
590a25f0a04SGreg Roach     *
591a25f0a04SGreg Roach     * @param string $locale
592a25f0a04SGreg Roach     *
593a25f0a04SGreg Roach     * @return string
594a25f0a04SGreg Roach     */
59555664801SGreg Roach    public static function languageScript(string $locale): string
596c1010edaSGreg Roach    {
597c999a340SGreg Roach        return Locale::create($locale)->script()->code();
598a25f0a04SGreg Roach    }
599a25f0a04SGreg Roach
600a25f0a04SGreg Roach    /**
601dfeee0a8SGreg Roach     * Translate a number into the local representation.
602a25f0a04SGreg Roach     *
603dfeee0a8SGreg Roach     * e.g. 12345.67 becomes
604dfeee0a8SGreg Roach     * en: 12,345.67
605dfeee0a8SGreg Roach     * fr: 12 345,67
606dfeee0a8SGreg Roach     * de: 12.345,67
607dfeee0a8SGreg Roach     *
608dfeee0a8SGreg Roach     * @param float $n
609cbc1590aSGreg Roach     * @param int   $precision
610a25f0a04SGreg Roach     *
611a25f0a04SGreg Roach     * @return string
612a25f0a04SGreg Roach     */
61355664801SGreg Roach    public static function number(float $n, int $precision = 0): string
614c1010edaSGreg Roach    {
615dfeee0a8SGreg Roach        return self::$locale->number(round($n, $precision));
616dfeee0a8SGreg Roach    }
617dfeee0a8SGreg Roach
618dfeee0a8SGreg Roach    /**
619dfeee0a8SGreg Roach     * Translate a fraction into a percentage.
620dfeee0a8SGreg Roach     *
621dfeee0a8SGreg Roach     * e.g. 0.123 becomes
622dfeee0a8SGreg Roach     * en: 12.3%
623dfeee0a8SGreg Roach     * fr: 12,3 %
624dfeee0a8SGreg Roach     * de: 12,3%
625dfeee0a8SGreg Roach     *
626dfeee0a8SGreg Roach     * @param float $n
627cbc1590aSGreg Roach     * @param int   $precision
628dfeee0a8SGreg Roach     *
629dfeee0a8SGreg Roach     * @return string
630dfeee0a8SGreg Roach     */
63155664801SGreg Roach    public static function percentage(float $n, int $precision = 0): string
632c1010edaSGreg Roach    {
633dfeee0a8SGreg Roach        return self::$locale->percent(round($n, $precision + 2));
634dfeee0a8SGreg Roach    }
635dfeee0a8SGreg Roach
636dfeee0a8SGreg Roach    /**
637dfeee0a8SGreg Roach     * Translate a plural string
638dfeee0a8SGreg Roach     * echo self::plural('There is an error', 'There are errors', $num_errors);
639dfeee0a8SGreg Roach     * echo self::plural('There is one error', 'There are %s errors', $num_errors);
640dfeee0a8SGreg Roach     * echo self::plural('There is %1$s %2$s cat', 'There are %1$s %2$s cats', $num, $num, $colour);
641dfeee0a8SGreg Roach     *
642924d091bSGreg Roach     * @param string $singular
643924d091bSGreg Roach     * @param string $plural
644924d091bSGreg Roach     * @param int    $count
645a515be7cSGreg Roach     * @param string ...$args
646e93111adSRico Sonntag     *
647dfeee0a8SGreg Roach     * @return string
648dfeee0a8SGreg Roach     */
649924d091bSGreg Roach    public static function plural(string $singular, string $plural, int $count, ...$args): string
650c1010edaSGreg Roach    {
651924d091bSGreg Roach        $message = self::$translator->translatePlural($singular, $plural, $count);
652dfeee0a8SGreg Roach
653924d091bSGreg Roach        return sprintf($message, ...$args);
654dfeee0a8SGreg Roach    }
655dfeee0a8SGreg Roach
656dfeee0a8SGreg Roach    /**
657dfeee0a8SGreg Roach     * UTF8 version of PHP::strrev()
658dfeee0a8SGreg Roach     *
659dfeee0a8SGreg Roach     * Reverse RTL text for third-party libraries such as GD2 and googlechart.
660dfeee0a8SGreg Roach     *
661dfeee0a8SGreg Roach     * These do not support UTF8 text direction, so we must mimic it for them.
662dfeee0a8SGreg Roach     *
663dfeee0a8SGreg Roach     * Numbers are always rendered LTR, even in RTL text.
664dfeee0a8SGreg Roach     * The visual direction of characters such as parentheses should be reversed.
665dfeee0a8SGreg Roach     *
666dfeee0a8SGreg Roach     * @param string $text Text to be reversed
667dfeee0a8SGreg Roach     *
668dfeee0a8SGreg Roach     * @return string
669dfeee0a8SGreg Roach     */
6708f53f488SRico Sonntag    public static function reverseText($text): string
671c1010edaSGreg Roach    {
672dfeee0a8SGreg Roach        // Remove HTML markup - we can't display it and it is LTR.
6739524b7b5SGreg Roach        $text = strip_tags($text);
6749524b7b5SGreg Roach        // Remove HTML entities.
6759524b7b5SGreg Roach        $text = html_entity_decode($text, ENT_QUOTES, 'UTF-8');
676dfeee0a8SGreg Roach
677dfeee0a8SGreg Roach        // LTR text doesn't need reversing
678dfeee0a8SGreg Roach        if (self::scriptDirection(self::textScript($text)) === 'ltr') {
679dfeee0a8SGreg Roach            return $text;
680dfeee0a8SGreg Roach        }
681dfeee0a8SGreg Roach
682dfeee0a8SGreg Roach        // Mirrored characters
683991b93ddSGreg Roach        $text = strtr($text, self::MIRROR_CHARACTERS);
684dfeee0a8SGreg Roach
685dfeee0a8SGreg Roach        $reversed = '';
686dfeee0a8SGreg Roach        $digits   = '';
687dfeee0a8SGreg Roach        while ($text != '') {
688dfeee0a8SGreg Roach            $letter = mb_substr($text, 0, 1);
689dfeee0a8SGreg Roach            $text   = mb_substr($text, 1);
690dfeee0a8SGreg Roach            if (strpos(self::DIGITS, $letter) !== false) {
691dfeee0a8SGreg Roach                $digits .= $letter;
692a25f0a04SGreg Roach            } else {
693dfeee0a8SGreg Roach                $reversed = $letter . $digits . $reversed;
694dfeee0a8SGreg Roach                $digits   = '';
695dfeee0a8SGreg Roach            }
696a25f0a04SGreg Roach        }
697a25f0a04SGreg Roach
698dfeee0a8SGreg Roach        return $digits . $reversed;
699a25f0a04SGreg Roach    }
700a25f0a04SGreg Roach
701a25f0a04SGreg Roach    /**
702a25f0a04SGreg Roach     * Return the direction (ltr or rtl) for a given script
703a25f0a04SGreg Roach     *
704a25f0a04SGreg Roach     * The PHP/intl library does not provde this information, so we need
705a25f0a04SGreg Roach     * our own lookup table.
706a25f0a04SGreg Roach     *
707a25f0a04SGreg Roach     * @param string $script
708a25f0a04SGreg Roach     *
709a25f0a04SGreg Roach     * @return string
710a25f0a04SGreg Roach     */
711c1010edaSGreg Roach    public static function scriptDirection($script)
712c1010edaSGreg Roach    {
713a25f0a04SGreg Roach        switch ($script) {
714a25f0a04SGreg Roach            case 'Arab':
715a25f0a04SGreg Roach            case 'Hebr':
716a25f0a04SGreg Roach            case 'Mong':
717a25f0a04SGreg Roach            case 'Thaa':
718a25f0a04SGreg Roach                return 'rtl';
719a25f0a04SGreg Roach            default:
720a25f0a04SGreg Roach                return 'ltr';
721a25f0a04SGreg Roach        }
722a25f0a04SGreg Roach    }
723a25f0a04SGreg Roach
724a25f0a04SGreg Roach    /**
725991b93ddSGreg Roach     * Perform a case-insensitive comparison of two strings.
726a25f0a04SGreg Roach     *
727a25f0a04SGreg Roach     * @param string $string1
728a25f0a04SGreg Roach     * @param string $string2
729a25f0a04SGreg Roach     *
730cbc1590aSGreg Roach     * @return int
731a25f0a04SGreg Roach     */
732c1010edaSGreg Roach    public static function strcasecmp($string1, $string2)
733c1010edaSGreg Roach    {
734991b93ddSGreg Roach        if (self::$collator instanceof Collator) {
735991b93ddSGreg Roach            return self::$collator->compare($string1, $string2);
736a25f0a04SGreg Roach        }
737b2ce94c6SRico Sonntag
738b2ce94c6SRico Sonntag        return strcmp(self::strtolower($string1), self::strtolower($string2));
739a25f0a04SGreg Roach    }
740a25f0a04SGreg Roach
741a25f0a04SGreg Roach    /**
742991b93ddSGreg Roach     * Convert a string to lower case.
743a25f0a04SGreg Roach     *
744dfeee0a8SGreg Roach     * @param string $string
745a25f0a04SGreg Roach     *
746a25f0a04SGreg Roach     * @return string
747a25f0a04SGreg Roach     */
7488f53f488SRico Sonntag    public static function strtolower($string): string
749c1010edaSGreg Roach    {
750991b93ddSGreg Roach        if (in_array(self::$locale->language()->code(), self::DOTLESS_I_LOCALES)) {
751991b93ddSGreg Roach            $string = strtr($string, self::DOTLESS_I_TOLOWER);
752a25f0a04SGreg Roach        }
7535ddad20bSGreg Roach
7545ddad20bSGreg Roach        return mb_strtolower($string);
755a25f0a04SGreg Roach    }
756a25f0a04SGreg Roach
757a25f0a04SGreg Roach    /**
758991b93ddSGreg Roach     * Convert a string to upper case.
759dfeee0a8SGreg Roach     *
760dfeee0a8SGreg Roach     * @param string $string
761a25f0a04SGreg Roach     *
762a25f0a04SGreg Roach     * @return string
763a25f0a04SGreg Roach     */
7648f53f488SRico Sonntag    public static function strtoupper($string): string
765c1010edaSGreg Roach    {
766991b93ddSGreg Roach        if (in_array(self::$locale->language()->code(), self::DOTLESS_I_LOCALES)) {
767991b93ddSGreg Roach            $string = strtr($string, self::DOTLESS_I_TOUPPER);
768a25f0a04SGreg Roach        }
7695ddad20bSGreg Roach
7705ddad20bSGreg Roach        return mb_strtoupper($string);
771a25f0a04SGreg Roach    }
772a25f0a04SGreg Roach
773dfeee0a8SGreg Roach    /**
774dfeee0a8SGreg Roach     * Identify the script used for a piece of text
775dfeee0a8SGreg Roach     *
776dfeee0a8SGreg Roach     * @param $string
777dfeee0a8SGreg Roach     *
778dfeee0a8SGreg Roach     * @return string
779dfeee0a8SGreg Roach     */
7808f53f488SRico Sonntag    public static function textScript($string): string
781c1010edaSGreg Roach    {
782dfeee0a8SGreg Roach        $string = strip_tags($string); // otherwise HTML tags show up as latin
783dfeee0a8SGreg Roach        $string = html_entity_decode($string, ENT_QUOTES, 'UTF-8'); // otherwise HTML entities show up as latin
784c1010edaSGreg Roach        $string = str_replace([
785c1010edaSGreg Roach            '@N.N.',
786c1010edaSGreg Roach            '@P.N.',
787c1010edaSGreg Roach        ], '', $string); // otherwise unknown names show up as latin
788dfeee0a8SGreg Roach        $pos    = 0;
789dfeee0a8SGreg Roach        $strlen = strlen($string);
790dfeee0a8SGreg Roach        while ($pos < $strlen) {
791dfeee0a8SGreg Roach            // get the Unicode Code Point for the character at position $pos
792dfeee0a8SGreg Roach            $byte1 = ord($string[$pos]);
793dfeee0a8SGreg Roach            if ($byte1 < 0x80) {
794dfeee0a8SGreg Roach                $code_point = $byte1;
795dfeee0a8SGreg Roach                $chrlen     = 1;
796dfeee0a8SGreg Roach            } elseif ($byte1 < 0xC0) {
797dfeee0a8SGreg Roach                // Invalid continuation character
798dfeee0a8SGreg Roach                return 'Latn';
799dfeee0a8SGreg Roach            } elseif ($byte1 < 0xE0) {
800dfeee0a8SGreg Roach                $code_point = (($byte1 & 0x1F) << 6) + (ord($string[$pos + 1]) & 0x3F);
801dfeee0a8SGreg Roach                $chrlen     = 2;
802dfeee0a8SGreg Roach            } elseif ($byte1 < 0xF0) {
803dfeee0a8SGreg Roach                $code_point = (($byte1 & 0x0F) << 12) + ((ord($string[$pos + 1]) & 0x3F) << 6) + (ord($string[$pos + 2]) & 0x3F);
804dfeee0a8SGreg Roach                $chrlen     = 3;
805dfeee0a8SGreg Roach            } elseif ($byte1 < 0xF8) {
806dfeee0a8SGreg Roach                $code_point = (($byte1 & 0x07) << 24) + ((ord($string[$pos + 1]) & 0x3F) << 12) + ((ord($string[$pos + 2]) & 0x3F) << 6) + (ord($string[$pos + 3]) & 0x3F);
807dfeee0a8SGreg Roach                $chrlen     = 3;
808dfeee0a8SGreg Roach            } else {
809dfeee0a8SGreg Roach                // Invalid UTF
810dfeee0a8SGreg Roach                return 'Latn';
811dfeee0a8SGreg Roach            }
812dfeee0a8SGreg Roach
813991b93ddSGreg Roach            foreach (self::SCRIPT_CHARACTER_RANGES as $range) {
814dfeee0a8SGreg Roach                if ($code_point >= $range[1] && $code_point <= $range[2]) {
815dfeee0a8SGreg Roach                    return $range[0];
816dfeee0a8SGreg Roach                }
817dfeee0a8SGreg Roach            }
818dfeee0a8SGreg Roach            // Not a recognised script. Maybe punctuation, spacing, etc. Keep looking.
819dfeee0a8SGreg Roach            $pos += $chrlen;
820dfeee0a8SGreg Roach        }
821dfeee0a8SGreg Roach
822dfeee0a8SGreg Roach        return 'Latn';
823dfeee0a8SGreg Roach    }
824dfeee0a8SGreg Roach
825dfeee0a8SGreg Roach    /**
826dfeee0a8SGreg Roach     * Convert a number of seconds into a relative time. For example, 630 => "10 hours, 30 minutes ago"
827dfeee0a8SGreg Roach     *
828cbc1590aSGreg Roach     * @param int $seconds
829dfeee0a8SGreg Roach     *
830dfeee0a8SGreg Roach     * @return string
831dfeee0a8SGreg Roach     */
832c1010edaSGreg Roach    public static function timeAgo($seconds)
833c1010edaSGreg Roach    {
834dfeee0a8SGreg Roach        $minute = 60;
835dfeee0a8SGreg Roach        $hour   = 60 * $minute;
836dfeee0a8SGreg Roach        $day    = 24 * $hour;
837dfeee0a8SGreg Roach        $month  = 30 * $day;
838dfeee0a8SGreg Roach        $year   = 365 * $day;
839dfeee0a8SGreg Roach
840dfeee0a8SGreg Roach        if ($seconds > $year) {
841cdaafeeeSGreg Roach            $years = intdiv($seconds, $year);
842cbc1590aSGreg Roach
843dfeee0a8SGreg Roach            return self::plural('%s year ago', '%s years ago', $years, self::number($years));
844b2ce94c6SRico Sonntag        }
845b2ce94c6SRico Sonntag
846b2ce94c6SRico Sonntag        if ($seconds > $month) {
847cdaafeeeSGreg Roach            $months = intdiv($seconds, $month);
848cbc1590aSGreg Roach
849dfeee0a8SGreg Roach            return self::plural('%s month ago', '%s months ago', $months, self::number($months));
850b2ce94c6SRico Sonntag        }
851b2ce94c6SRico Sonntag
852b2ce94c6SRico Sonntag        if ($seconds > $day) {
853cdaafeeeSGreg Roach            $days = intdiv($seconds, $day);
854cbc1590aSGreg Roach
855dfeee0a8SGreg Roach            return self::plural('%s day ago', '%s days ago', $days, self::number($days));
856b2ce94c6SRico Sonntag        }
857b2ce94c6SRico Sonntag
858b2ce94c6SRico Sonntag        if ($seconds > $hour) {
859cdaafeeeSGreg Roach            $hours = intdiv($seconds, $hour);
860cbc1590aSGreg Roach
861dfeee0a8SGreg Roach            return self::plural('%s hour ago', '%s hours ago', $hours, self::number($hours));
862b2ce94c6SRico Sonntag        }
863b2ce94c6SRico Sonntag
864b2ce94c6SRico Sonntag        if ($seconds > $minute) {
865cdaafeeeSGreg Roach            $minutes = intdiv($seconds, $minute);
866cbc1590aSGreg Roach
867dfeee0a8SGreg Roach            return self::plural('%s minute ago', '%s minutes ago', $minutes, self::number($minutes));
868dfeee0a8SGreg Roach        }
869b2ce94c6SRico Sonntag
870b2ce94c6SRico Sonntag        return self::plural('%s second ago', '%s seconds ago', $seconds, self::number($seconds));
871dfeee0a8SGreg Roach    }
872dfeee0a8SGreg Roach
873dfeee0a8SGreg Roach    /**
874dfeee0a8SGreg Roach     * What format is used to display dates in the current locale?
875dfeee0a8SGreg Roach     *
876dfeee0a8SGreg Roach     * @return string
877dfeee0a8SGreg Roach     */
8788f53f488SRico Sonntag    public static function timeFormat(): string
879c1010edaSGreg Roach    {
880bbb76c12SGreg Roach        /* I18N: This is the format string for the time-of-day. See http://php.net/date for codes */
881bbb76c12SGreg Roach        return self::$translator->translate('%H:%i:%s');
882dfeee0a8SGreg Roach    }
883dfeee0a8SGreg Roach
884dfeee0a8SGreg Roach    /**
885dfeee0a8SGreg Roach     * Translate a string, and then substitute placeholders
886dfeee0a8SGreg Roach     *
887dfeee0a8SGreg Roach     * echo I18N::translate('Hello World!');
888dfeee0a8SGreg Roach     * echo I18N::translate('The %s sat on the mat', 'cat');
889dfeee0a8SGreg Roach     *
890924d091bSGreg Roach     * @param string $message
891a515be7cSGreg Roach     * @param string ...$args
892c3283ed7SGreg Roach     *
893dfeee0a8SGreg Roach     * @return string
894dfeee0a8SGreg Roach     */
895924d091bSGreg Roach    public static function translate(string $message, ...$args): string
896c1010edaSGreg Roach    {
897924d091bSGreg Roach        $message = self::$translator->translate($message);
898dfeee0a8SGreg Roach
899924d091bSGreg Roach        return sprintf($message, ...$args);
900dfeee0a8SGreg Roach    }
901dfeee0a8SGreg Roach
902dfeee0a8SGreg Roach    /**
903dfeee0a8SGreg Roach     * Context sensitive version of translate.
904a4956c0eSGreg Roach     * echo I18N::translateContext('NOMINATIVE', 'January');
905a4956c0eSGreg Roach     * echo I18N::translateContext('GENITIVE', 'January');
906dfeee0a8SGreg Roach     *
907924d091bSGreg Roach     * @param string $context
908924d091bSGreg Roach     * @param string $message
909a515be7cSGreg Roach     * @param string ...$args
910c3283ed7SGreg Roach     *
911dfeee0a8SGreg Roach     * @return string
912dfeee0a8SGreg Roach     */
913924d091bSGreg Roach    public static function translateContext(string $context, string $message, ...$args): string
914c1010edaSGreg Roach    {
915924d091bSGreg Roach        $message = self::$translator->translateContext($context, $message);
916dfeee0a8SGreg Roach
917924d091bSGreg Roach        return sprintf($message, ...$args);
918a25f0a04SGreg Roach    }
919a25f0a04SGreg Roach}
920