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