xref: /webtrees/app/I18N.php (revision 3976b4703df669696105ed6b024b96d433c8fbdb)
1a25f0a04SGreg Roach<?php
2*3976b470SGreg Roach
3a25f0a04SGreg Roach/**
4a25f0a04SGreg Roach * webtrees: online genealogy
58fcd0d32SGreg Roach * Copyright (C) 2019 webtrees development team
6a25f0a04SGreg Roach * This program is free software: you can redistribute it and/or modify
7a25f0a04SGreg Roach * it under the terms of the GNU General Public License as published by
8a25f0a04SGreg Roach * the Free Software Foundation, either version 3 of the License, or
9a25f0a04SGreg Roach * (at your option) any later version.
10a25f0a04SGreg Roach * This program is distributed in the hope that it will be useful,
11a25f0a04SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
12a25f0a04SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13a25f0a04SGreg Roach * GNU General Public License for more details.
14a25f0a04SGreg Roach * You should have received a copy of the GNU General Public License
15a25f0a04SGreg Roach * along with this program. If not, see <http://www.gnu.org/licenses/>.
16a25f0a04SGreg Roach */
17e7f56f2aSGreg Roachdeclare(strict_types=1);
18e7f56f2aSGreg Roach
1976692c8bSGreg Roachnamespace Fisharebest\Webtrees;
20a25f0a04SGreg Roach
21991b93ddSGreg Roachuse Collator;
22f1af7e1cSGreg Roachuse Exception;
23c999a340SGreg Roachuse Fisharebest\Localization\Locale;
241e71bdc0SGreg Roachuse Fisharebest\Localization\Locale\LocaleEnUs;
2515834aaeSGreg Roachuse Fisharebest\Localization\Locale\LocaleInterface;
263bdc890bSGreg Roachuse Fisharebest\Localization\Translation;
273bdc890bSGreg Roachuse Fisharebest\Localization\Translator;
28d37db671SGreg Roachuse Fisharebest\Webtrees\Module\ModuleCustomInterface;
2902086832SGreg Roachuse Fisharebest\Webtrees\Module\ModuleLanguageInterface;
30d37db671SGreg Roachuse Fisharebest\Webtrees\Services\ModuleService;
316cd97bf6SGreg Roachuse Illuminate\Support\Collection;
32*3976b470SGreg Roach
334f194b97SGreg Roachuse function array_merge;
34d68ee7a8SGreg Roachuse function class_exists;
35d68ee7a8SGreg Roachuse function html_entity_decode;
36d68ee7a8SGreg Roachuse function in_array;
37d68ee7a8SGreg Roachuse function mb_strtolower;
38d68ee7a8SGreg Roachuse function mb_strtoupper;
39d68ee7a8SGreg Roachuse function mb_substr;
40d68ee7a8SGreg Roachuse function ord;
41d68ee7a8SGreg Roachuse function sprintf;
42d68ee7a8SGreg Roachuse function str_replace;
43d68ee7a8SGreg Roachuse function strcmp;
44d68ee7a8SGreg Roachuse function strip_tags;
45d68ee7a8SGreg Roachuse function strlen;
46d68ee7a8SGreg Roachuse function strpos;
47d68ee7a8SGreg Roachuse function strtr;
48a25f0a04SGreg Roach
49a25f0a04SGreg Roach/**
5076692c8bSGreg Roach * Internationalization (i18n) and localization (l10n).
51a25f0a04SGreg Roach */
52c1010edaSGreg Roachclass I18N
53c1010edaSGreg Roach{
54d37db671SGreg Roach    // MO files use special characters for plurals and context.
554f194b97SGreg Roach    public const PLURAL  = "\x00";
564f194b97SGreg Roach    public const CONTEXT = "\x04";
5716d6367aSGreg Roach    private const DIGITS = '0123456789٠١٢٣٤٥٦٧٨٩۰۱۲۳۴۵۶۷۸۹';
5816d6367aSGreg Roach    private const DOTLESS_I_LOCALES = [
59c1010edaSGreg Roach        'az',
60c1010edaSGreg Roach        'tr',
61c1010edaSGreg Roach    ];
6216d6367aSGreg Roach    private const DOTLESS_I_TOLOWER = [
63c1010edaSGreg Roach        'I' => 'ı',
64c1010edaSGreg Roach        'İ' => 'i',
65c1010edaSGreg Roach    ];
66006094b9SGreg Roach
67006094b9SGreg Roach    // Digits are always rendered LTR, even in RTL text.
6816d6367aSGreg Roach    private const DOTLESS_I_TOUPPER = [
69c1010edaSGreg Roach        'ı' => 'I',
70c1010edaSGreg Roach        'i' => 'İ',
71c1010edaSGreg Roach    ];
72a25f0a04SGreg Roach
73006094b9SGreg Roach    // These locales need special handling for the dotless letter I.
7416d6367aSGreg Roach    private const SCRIPT_CHARACTER_RANGES = [
75c1010edaSGreg Roach        [
76c1010edaSGreg Roach            'Latn',
77c1010edaSGreg Roach            0x0041,
78c1010edaSGreg Roach            0x005A,
79c1010edaSGreg Roach        ],
80c1010edaSGreg Roach        [
81c1010edaSGreg Roach            'Latn',
82c1010edaSGreg Roach            0x0061,
83c1010edaSGreg Roach            0x007A,
84c1010edaSGreg Roach        ],
85c1010edaSGreg Roach        [
86c1010edaSGreg Roach            'Latn',
87c1010edaSGreg Roach            0x0100,
88c1010edaSGreg Roach            0x02AF,
89c1010edaSGreg Roach        ],
90c1010edaSGreg Roach        [
91c1010edaSGreg Roach            'Grek',
92c1010edaSGreg Roach            0x0370,
93c1010edaSGreg Roach            0x03FF,
94c1010edaSGreg Roach        ],
95c1010edaSGreg Roach        [
96c1010edaSGreg Roach            'Cyrl',
97c1010edaSGreg Roach            0x0400,
98c1010edaSGreg Roach            0x052F,
99c1010edaSGreg Roach        ],
100c1010edaSGreg Roach        [
101c1010edaSGreg Roach            'Hebr',
102c1010edaSGreg Roach            0x0590,
103c1010edaSGreg Roach            0x05FF,
104c1010edaSGreg Roach        ],
105c1010edaSGreg Roach        [
106c1010edaSGreg Roach            'Arab',
107c1010edaSGreg Roach            0x0600,
108c1010edaSGreg Roach            0x06FF,
109c1010edaSGreg Roach        ],
110c1010edaSGreg Roach        [
111c1010edaSGreg Roach            'Arab',
112c1010edaSGreg Roach            0x0750,
113c1010edaSGreg Roach            0x077F,
114c1010edaSGreg Roach        ],
115c1010edaSGreg Roach        [
116c1010edaSGreg Roach            'Arab',
117c1010edaSGreg Roach            0x08A0,
118c1010edaSGreg Roach            0x08FF,
119c1010edaSGreg Roach        ],
120c1010edaSGreg Roach        [
121c1010edaSGreg Roach            'Deva',
122c1010edaSGreg Roach            0x0900,
123c1010edaSGreg Roach            0x097F,
124c1010edaSGreg Roach        ],
125c1010edaSGreg Roach        [
126c1010edaSGreg Roach            'Taml',
127c1010edaSGreg Roach            0x0B80,
128c1010edaSGreg Roach            0x0BFF,
129c1010edaSGreg Roach        ],
130c1010edaSGreg Roach        [
131c1010edaSGreg Roach            'Sinh',
132c1010edaSGreg Roach            0x0D80,
133c1010edaSGreg Roach            0x0DFF,
134c1010edaSGreg Roach        ],
135c1010edaSGreg Roach        [
136c1010edaSGreg Roach            'Thai',
137c1010edaSGreg Roach            0x0E00,
138c1010edaSGreg Roach            0x0E7F,
139c1010edaSGreg Roach        ],
140c1010edaSGreg Roach        [
141c1010edaSGreg Roach            'Geor',
142c1010edaSGreg Roach            0x10A0,
143c1010edaSGreg Roach            0x10FF,
144c1010edaSGreg Roach        ],
145c1010edaSGreg Roach        [
146c1010edaSGreg Roach            'Grek',
147c1010edaSGreg Roach            0x1F00,
148c1010edaSGreg Roach            0x1FFF,
149c1010edaSGreg Roach        ],
150c1010edaSGreg Roach        [
151c1010edaSGreg Roach            'Deva',
152c1010edaSGreg Roach            0xA8E0,
153c1010edaSGreg Roach            0xA8FF,
154c1010edaSGreg Roach        ],
155c1010edaSGreg Roach        [
156c1010edaSGreg Roach            'Hans',
157c1010edaSGreg Roach            0x3000,
158c1010edaSGreg Roach            0x303F,
159c1010edaSGreg Roach        ],
160c1010edaSGreg Roach        // Mixed CJK, not just Hans
161c1010edaSGreg Roach        [
162c1010edaSGreg Roach            'Hans',
163c1010edaSGreg Roach            0x3400,
164c1010edaSGreg Roach            0xFAFF,
165c1010edaSGreg Roach        ],
166c1010edaSGreg Roach        // Mixed CJK, not just Hans
167c1010edaSGreg Roach        [
168c1010edaSGreg Roach            'Hans',
169c1010edaSGreg Roach            0x20000,
170c1010edaSGreg Roach            0x2FA1F,
171c1010edaSGreg Roach        ],
172c1010edaSGreg Roach        // Mixed CJK, not just Hans
17313abd6f3SGreg Roach    ];
17416d6367aSGreg Roach    private const MIRROR_CHARACTERS = [
175a25f0a04SGreg Roach        '('  => ')',
176a25f0a04SGreg Roach        ')'  => '(',
177a25f0a04SGreg Roach        '['  => ']',
178a25f0a04SGreg Roach        ']'  => '[',
179a25f0a04SGreg Roach        '{'  => '}',
180a25f0a04SGreg Roach        '}'  => '{',
181a25f0a04SGreg Roach        '<'  => '>',
182a25f0a04SGreg Roach        '>'  => '<',
183a25f0a04SGreg Roach        '‹ ' => '›',
184a25f0a04SGreg Roach        '› ' => '‹',
185a25f0a04SGreg Roach        '«'  => '»',
186a25f0a04SGreg Roach        '»'  => '«',
187a25f0a04SGreg Roach        '﴾ ' => '﴿',
188a25f0a04SGreg Roach        '﴿ ' => '﴾',
189a25f0a04SGreg Roach        '“ ' => '”',
190a25f0a04SGreg Roach        '” ' => '“',
191a25f0a04SGreg Roach        '‘ ' => '’',
192a25f0a04SGreg Roach        '’ ' => '‘',
19313abd6f3SGreg Roach    ];
194a25f0a04SGreg Roach    /** @var string Punctuation used to separate list items, typically a comma */
195a25f0a04SGreg Roach    public static $list_separator;
196a25f0a04SGreg Roach
197006094b9SGreg Roach    // The ranges of characters used by each script.
198006094b9SGreg Roach    /** @var LocaleInterface The current locale (e.g. LocaleEnGb) */
199006094b9SGreg Roach    private static $locale;
200006094b9SGreg Roach
201006094b9SGreg Roach    // Characters that are displayed in mirror form in RTL text.
202006094b9SGreg Roach    /** @var Translator An object that performs translation */
203006094b9SGreg Roach    private static $translator;
204006094b9SGreg Roach    /** @var  Collator|null From the php-intl library */
205006094b9SGreg Roach    private static $collator;
206006094b9SGreg Roach
207a25f0a04SGreg Roach    /**
20802086832SGreg Roach     * The preferred locales for this site, or a default list if no preference.
209dfeee0a8SGreg Roach     *
210dfeee0a8SGreg Roach     * @return LocaleInterface[]
211dfeee0a8SGreg Roach     */
2128f53f488SRico Sonntag    public static function activeLocales(): array
213c1010edaSGreg Roach    {
214006094b9SGreg Roach        /** @var Collection $locales */
21502086832SGreg Roach        $locales = app(ModuleService::class)
216d6137952SGreg Roach            ->findByInterface(ModuleLanguageInterface::class, false, true)
2170b5fd0a6SGreg Roach            ->map(static function (ModuleLanguageInterface $module): LocaleInterface {
21802086832SGreg Roach                return $module->locale();
21902086832SGreg Roach            });
220dfeee0a8SGreg Roach
22102086832SGreg Roach        if ($locales->isEmpty()) {
22202086832SGreg Roach            return [new LocaleEnUs()];
223dfeee0a8SGreg Roach        }
224dfeee0a8SGreg Roach
22502086832SGreg Roach        return $locales->all();
226dfeee0a8SGreg Roach    }
227dfeee0a8SGreg Roach
228dfeee0a8SGreg Roach    /**
229dfeee0a8SGreg Roach     * Which MySQL collation should be used for this locale?
230dfeee0a8SGreg Roach     *
231dfeee0a8SGreg Roach     * @return string
232dfeee0a8SGreg Roach     */
233e364afe4SGreg Roach    public static function collation(): string
234c1010edaSGreg Roach    {
235dfeee0a8SGreg Roach        $collation = self::$locale->collation();
236dfeee0a8SGreg Roach        switch ($collation) {
237dfeee0a8SGreg Roach            case 'croatian_ci':
238dfeee0a8SGreg Roach            case 'german2_ci':
239dfeee0a8SGreg Roach            case 'vietnamese_ci':
240dfeee0a8SGreg Roach                // Only available in MySQL 5.6
241dfeee0a8SGreg Roach                return 'utf8_unicode_ci';
242dfeee0a8SGreg Roach            default:
243dfeee0a8SGreg Roach                return 'utf8_' . $collation;
244dfeee0a8SGreg Roach        }
245dfeee0a8SGreg Roach    }
246dfeee0a8SGreg Roach
247dfeee0a8SGreg Roach    /**
248dfeee0a8SGreg Roach     * What format is used to display dates in the current locale?
249dfeee0a8SGreg Roach     *
250dfeee0a8SGreg Roach     * @return string
251dfeee0a8SGreg Roach     */
2528f53f488SRico Sonntag    public static function dateFormat(): string
253c1010edaSGreg Roach    {
254bbb76c12SGreg Roach        /* I18N: This is the format string for full dates. See http://php.net/date for codes */
255bbb76c12SGreg Roach        return self::$translator->translate('%j %F %Y');
256dfeee0a8SGreg Roach    }
257dfeee0a8SGreg Roach
258dfeee0a8SGreg Roach    /**
259dfeee0a8SGreg Roach     * Convert the digits 0-9 into the local script
260dfeee0a8SGreg Roach     * Used for years, etc., where we do not want thousands-separators, decimals, etc.
261dfeee0a8SGreg Roach     *
26255664801SGreg Roach     * @param string|int $n
263dfeee0a8SGreg Roach     *
264dfeee0a8SGreg Roach     * @return string
265dfeee0a8SGreg Roach     */
2668f53f488SRico Sonntag    public static function digits($n): string
267c1010edaSGreg Roach    {
26855664801SGreg Roach        return self::$locale->digits((string) $n);
269dfeee0a8SGreg Roach    }
270dfeee0a8SGreg Roach
271dfeee0a8SGreg Roach    /**
272dfeee0a8SGreg Roach     * What is the direction of the current locale
273dfeee0a8SGreg Roach     *
274dfeee0a8SGreg Roach     * @return string "ltr" or "rtl"
275dfeee0a8SGreg Roach     */
2768f53f488SRico Sonntag    public static function direction(): string
277c1010edaSGreg Roach    {
278dfeee0a8SGreg Roach        return self::$locale->direction();
279dfeee0a8SGreg Roach    }
280dfeee0a8SGreg Roach
281dfeee0a8SGreg Roach    /**
2827231a557SGreg Roach     * What is the first day of the week.
2837231a557SGreg Roach     *
284cbc1590aSGreg Roach     * @return int Sunday=0, Monday=1, etc.
2857231a557SGreg Roach     */
2868f53f488SRico Sonntag    public static function firstDay(): int
287c1010edaSGreg Roach    {
2887231a557SGreg Roach        return self::$locale->territory()->firstDay();
2897231a557SGreg Roach    }
2907231a557SGreg Roach
2917231a557SGreg Roach    /**
292dfeee0a8SGreg Roach     * Generate i18n markup for the <html> tag, e.g. lang="ar" dir="rtl"
293dfeee0a8SGreg Roach     *
294dfeee0a8SGreg Roach     * @return string
295dfeee0a8SGreg Roach     */
2968f53f488SRico Sonntag    public static function htmlAttributes(): string
297c1010edaSGreg Roach    {
298dfeee0a8SGreg Roach        return self::$locale->htmlAttributes();
299dfeee0a8SGreg Roach    }
300dfeee0a8SGreg Roach
301dfeee0a8SGreg Roach    /**
302a25f0a04SGreg Roach     * Initialise the translation adapter with a locale setting.
303a25f0a04SGreg Roach     *
30415d603e7SGreg Roach     * @param string    $code  Use this locale/language code, or choose one automatically
305e58a20ffSGreg Roach     * @param Tree|null $tree
306c116a5ccSGreg Roach     * @param bool      $setup During setup, we cannot access the database.
307a25f0a04SGreg Roach     *
308a25f0a04SGreg Roach     * @return string $string
309a25f0a04SGreg Roach     */
310081ddc56SGreg Roach    public static function init(string $code = '', Tree $tree = null, $setup = false): string
311c1010edaSGreg Roach    {
31215d603e7SGreg Roach        if ($code !== '') {
3133bdc890bSGreg Roach            // Create the specified locale
3143bdc890bSGreg Roach            self::$locale = Locale::create($code);
315006094b9SGreg Roach        } elseif (Session::has('language')) {
316e58a20ffSGreg Roach            // Select a previously used locale
317a0801ffbSGreg Roach            self::$locale = Locale::create(Session::get('language'));
3183bdc890bSGreg Roach        } else {
319e58a20ffSGreg Roach            if ($tree instanceof Tree) {
320e58a20ffSGreg Roach                $default_locale = Locale::create($tree->getPreference('LANGUAGE', 'en-US'));
321e58a20ffSGreg Roach            } else {
32259f2f229SGreg Roach                $default_locale = new LocaleEnUs();
3233bdc890bSGreg Roach            }
324e58a20ffSGreg Roach
325e58a20ffSGreg Roach            // Negotiate with the browser.
326e58a20ffSGreg Roach            // Search engines don't negotiate.  They get the default locale of the tree.
327c116a5ccSGreg Roach            if ($setup) {
328c116a5ccSGreg Roach                $installed_locales = app(ModuleService::class)->setupLanguages()
3290b5fd0a6SGreg Roach                    ->map(static function (ModuleLanguageInterface $module): LocaleInterface {
330c116a5ccSGreg Roach                        return $module->locale();
331c116a5ccSGreg Roach                    });
332c116a5ccSGreg Roach            } else {
333c116a5ccSGreg Roach                $installed_locales = self::installedLocales();
334c116a5ccSGreg Roach            }
335c116a5ccSGreg Roach
336c116a5ccSGreg Roach            self::$locale = Locale::httpAcceptLanguage($_SERVER, $installed_locales->all(), $default_locale);
3373bdc890bSGreg Roach        }
3383bdc890bSGreg Roach
3394f194b97SGreg Roach        // Load the translation file
340006094b9SGreg Roach        $translation_file = Webtrees::ROOT_DIR . 'resources/lang/' . self::$locale->languageTag() . '/messages.php';
3414f194b97SGreg Roach
342f1af7e1cSGreg Roach        try {
343006094b9SGreg Roach            $translation  = new Translation($translation_file);
344006094b9SGreg Roach            $translations = $translation->asArray();
345f1af7e1cSGreg Roach        } catch (Exception $ex) {
346006094b9SGreg Roach            // The translations files are created during the build process, and are
347006094b9SGreg Roach            // not included in the source code.
348006094b9SGreg Roach            // Assuming we are using dev code, and build (or rebuild) the files.
349006094b9SGreg Roach            $po_file      = Webtrees::ROOT_DIR . 'resources/lang/' . self::$locale->languageTag() . '/messages.po';
350006094b9SGreg Roach            $translation  = new Translation($po_file);
351006094b9SGreg Roach            $translations = $translation->asArray();
352006094b9SGreg Roach            file_put_contents($translation_file, '<?php return ' . var_export($translations, true) . ';');
353a25f0a04SGreg Roach        }
354a25f0a04SGreg Roach
3554f194b97SGreg Roach        // Add translations from custom modules (but not during setup, as we have no database/modules)
356c116a5ccSGreg Roach        if (!$setup) {
3574f194b97SGreg Roach            $translations = app(ModuleService::class)
3584f194b97SGreg Roach                ->findByInterface(ModuleCustomInterface::class)
35969253da9SGreg Roach                ->reduce(static function (array $carry, ModuleCustomInterface $item): array {
3604f194b97SGreg Roach                    return array_merge($carry, $item->customTranslations(self::$locale->languageTag()));
3614f194b97SGreg Roach                }, $translations);
362d37db671SGreg Roach        }
363d37db671SGreg Roach
3643bdc890bSGreg Roach        // Create a translator
3653bdc890bSGreg Roach        self::$translator = new Translator($translations, self::$locale->pluralRule());
366a25f0a04SGreg Roach
367bbb76c12SGreg Roach        /* I18N: This punctuation is used to separate lists of items */
368bbb76c12SGreg Roach        self::$list_separator = self::translate(', ');
369a25f0a04SGreg Roach
370991b93ddSGreg Roach        // Create a collator
371991b93ddSGreg Roach        try {
372444a65ecSGreg Roach            if (class_exists('Collator')) {
373c9ec599fSGreg Roach                // Symfony provides a very incomplete polyfill - which cannot be used.
374991b93ddSGreg Roach                self::$collator = new Collator(self::$locale->code());
375991b93ddSGreg Roach                // Ignore upper/lower case differences
376991b93ddSGreg Roach                self::$collator->setStrength(Collator::SECONDARY);
377444a65ecSGreg Roach            }
378991b93ddSGreg Roach        } catch (Exception $ex) {
379991b93ddSGreg Roach            // PHP-INTL is not installed?  We'll use a fallback later.
380c9ec599fSGreg Roach            self::$collator = null;
381991b93ddSGreg Roach        }
382991b93ddSGreg Roach
3835331c5eaSGreg Roach        return self::$locale->languageTag();
384a25f0a04SGreg Roach    }
385a25f0a04SGreg Roach
386a25f0a04SGreg Roach    /**
387c999a340SGreg Roach     * All locales for which a translation file exists.
388c999a340SGreg Roach     *
389c116a5ccSGreg Roach     * @return Collection
390c999a340SGreg Roach     */
391c116a5ccSGreg Roach    public static function installedLocales(): Collection
392c1010edaSGreg Roach    {
39302086832SGreg Roach        return app(ModuleService::class)
39402086832SGreg Roach            ->findByInterface(ModuleLanguageInterface::class, true)
3950b5fd0a6SGreg Roach            ->map(static function (ModuleLanguageInterface $module): LocaleInterface {
39602086832SGreg Roach                return $module->locale();
397c116a5ccSGreg Roach            });
398a25f0a04SGreg Roach    }
399a25f0a04SGreg Roach
400a25f0a04SGreg Roach    /**
401006094b9SGreg Roach     * Translate a string, and then substitute placeholders
402006094b9SGreg Roach     * echo I18N::translate('Hello World!');
403006094b9SGreg Roach     * echo I18N::translate('The %s sat on the mat', 'cat');
404006094b9SGreg Roach     *
405006094b9SGreg Roach     * @param string $message
406006094b9SGreg Roach     * @param string ...$args
407006094b9SGreg Roach     *
408006094b9SGreg Roach     * @return string
409006094b9SGreg Roach     */
410006094b9SGreg Roach    public static function translate(string $message, ...$args): string
411006094b9SGreg Roach    {
412006094b9SGreg Roach        $message = self::$translator->translate($message);
413006094b9SGreg Roach
414006094b9SGreg Roach        return sprintf($message, ...$args);
415006094b9SGreg Roach    }
416006094b9SGreg Roach
417006094b9SGreg Roach    /**
418a25f0a04SGreg Roach     * Return the endonym for a given language - as per http://cldr.unicode.org/
419a25f0a04SGreg Roach     *
420a25f0a04SGreg Roach     * @param string $locale
421a25f0a04SGreg Roach     *
422a25f0a04SGreg Roach     * @return string
423a25f0a04SGreg Roach     */
42455664801SGreg Roach    public static function languageName(string $locale): string
425c1010edaSGreg Roach    {
426c999a340SGreg Roach        return Locale::create($locale)->endonym();
427a25f0a04SGreg Roach    }
428a25f0a04SGreg Roach
429a25f0a04SGreg Roach    /**
430a25f0a04SGreg Roach     * Return the script used by a given language
431a25f0a04SGreg Roach     *
432a25f0a04SGreg Roach     * @param string $locale
433a25f0a04SGreg Roach     *
434a25f0a04SGreg Roach     * @return string
435a25f0a04SGreg Roach     */
43655664801SGreg Roach    public static function languageScript(string $locale): string
437c1010edaSGreg Roach    {
438c999a340SGreg Roach        return Locale::create($locale)->script()->code();
439a25f0a04SGreg Roach    }
440a25f0a04SGreg Roach
441a25f0a04SGreg Roach    /**
442dfeee0a8SGreg Roach     * Translate a number into the local representation.
443dfeee0a8SGreg Roach     * e.g. 12345.67 becomes
444dfeee0a8SGreg Roach     * en: 12,345.67
445dfeee0a8SGreg Roach     * fr: 12 345,67
446dfeee0a8SGreg Roach     * de: 12.345,67
447dfeee0a8SGreg Roach     *
448dfeee0a8SGreg Roach     * @param float $n
449cbc1590aSGreg Roach     * @param int   $precision
450a25f0a04SGreg Roach     *
451a25f0a04SGreg Roach     * @return string
452a25f0a04SGreg Roach     */
45355664801SGreg Roach    public static function number(float $n, int $precision = 0): string
454c1010edaSGreg Roach    {
455dfeee0a8SGreg Roach        return self::$locale->number(round($n, $precision));
456dfeee0a8SGreg Roach    }
457dfeee0a8SGreg Roach
458dfeee0a8SGreg Roach    /**
459dfeee0a8SGreg Roach     * Translate a fraction into a percentage.
460dfeee0a8SGreg Roach     * e.g. 0.123 becomes
461dfeee0a8SGreg Roach     * en: 12.3%
462dfeee0a8SGreg Roach     * fr: 12,3 %
463dfeee0a8SGreg Roach     * de: 12,3%
464dfeee0a8SGreg Roach     *
465dfeee0a8SGreg Roach     * @param float $n
466cbc1590aSGreg Roach     * @param int   $precision
467dfeee0a8SGreg Roach     *
468dfeee0a8SGreg Roach     * @return string
469dfeee0a8SGreg Roach     */
47055664801SGreg Roach    public static function percentage(float $n, int $precision = 0): string
471c1010edaSGreg Roach    {
472dfeee0a8SGreg Roach        return self::$locale->percent(round($n, $precision + 2));
473dfeee0a8SGreg Roach    }
474dfeee0a8SGreg Roach
475dfeee0a8SGreg Roach    /**
476dfeee0a8SGreg Roach     * Translate a plural string
477dfeee0a8SGreg Roach     * echo self::plural('There is an error', 'There are errors', $num_errors);
478dfeee0a8SGreg Roach     * echo self::plural('There is one error', 'There are %s errors', $num_errors);
479dfeee0a8SGreg Roach     * echo self::plural('There is %1$s %2$s cat', 'There are %1$s %2$s cats', $num, $num, $colour);
480dfeee0a8SGreg Roach     *
481924d091bSGreg Roach     * @param string $singular
482924d091bSGreg Roach     * @param string $plural
483924d091bSGreg Roach     * @param int    $count
484a515be7cSGreg Roach     * @param string ...$args
485e93111adSRico Sonntag     *
486dfeee0a8SGreg Roach     * @return string
487dfeee0a8SGreg Roach     */
488924d091bSGreg Roach    public static function plural(string $singular, string $plural, int $count, ...$args): string
489c1010edaSGreg Roach    {
490924d091bSGreg Roach        $message = self::$translator->translatePlural($singular, $plural, $count);
491dfeee0a8SGreg Roach
492924d091bSGreg Roach        return sprintf($message, ...$args);
493dfeee0a8SGreg Roach    }
494dfeee0a8SGreg Roach
495dfeee0a8SGreg Roach    /**
496dfeee0a8SGreg Roach     * UTF8 version of PHP::strrev()
497dfeee0a8SGreg Roach     * Reverse RTL text for third-party libraries such as GD2 and googlechart.
498dfeee0a8SGreg Roach     * These do not support UTF8 text direction, so we must mimic it for them.
499dfeee0a8SGreg Roach     * Numbers are always rendered LTR, even in RTL text.
500dfeee0a8SGreg Roach     * The visual direction of characters such as parentheses should be reversed.
501dfeee0a8SGreg Roach     *
502dfeee0a8SGreg Roach     * @param string $text Text to be reversed
503dfeee0a8SGreg Roach     *
504dfeee0a8SGreg Roach     * @return string
505dfeee0a8SGreg Roach     */
5068f53f488SRico Sonntag    public static function reverseText($text): string
507c1010edaSGreg Roach    {
508dfeee0a8SGreg Roach        // Remove HTML markup - we can't display it and it is LTR.
5099524b7b5SGreg Roach        $text = strip_tags($text);
5109524b7b5SGreg Roach        // Remove HTML entities.
5119524b7b5SGreg Roach        $text = html_entity_decode($text, ENT_QUOTES, 'UTF-8');
512dfeee0a8SGreg Roach
513dfeee0a8SGreg Roach        // LTR text doesn't need reversing
514dfeee0a8SGreg Roach        if (self::scriptDirection(self::textScript($text)) === 'ltr') {
515dfeee0a8SGreg Roach            return $text;
516dfeee0a8SGreg Roach        }
517dfeee0a8SGreg Roach
518dfeee0a8SGreg Roach        // Mirrored characters
519991b93ddSGreg Roach        $text = strtr($text, self::MIRROR_CHARACTERS);
520dfeee0a8SGreg Roach
521dfeee0a8SGreg Roach        $reversed = '';
522dfeee0a8SGreg Roach        $digits   = '';
523e364afe4SGreg Roach        while ($text !== '') {
524dfeee0a8SGreg Roach            $letter = mb_substr($text, 0, 1);
525dfeee0a8SGreg Roach            $text   = mb_substr($text, 1);
526dfeee0a8SGreg Roach            if (strpos(self::DIGITS, $letter) !== false) {
527dfeee0a8SGreg Roach                $digits .= $letter;
528a25f0a04SGreg Roach            } else {
529dfeee0a8SGreg Roach                $reversed = $letter . $digits . $reversed;
530dfeee0a8SGreg Roach                $digits   = '';
531dfeee0a8SGreg Roach            }
532a25f0a04SGreg Roach        }
533a25f0a04SGreg Roach
534dfeee0a8SGreg Roach        return $digits . $reversed;
535a25f0a04SGreg Roach    }
536a25f0a04SGreg Roach
537a25f0a04SGreg Roach    /**
538a25f0a04SGreg Roach     * Return the direction (ltr or rtl) for a given script
539a25f0a04SGreg Roach     * The PHP/intl library does not provde this information, so we need
540a25f0a04SGreg Roach     * our own lookup table.
541a25f0a04SGreg Roach     *
542a25f0a04SGreg Roach     * @param string $script
543a25f0a04SGreg Roach     *
544a25f0a04SGreg Roach     * @return string
545a25f0a04SGreg Roach     */
546e364afe4SGreg Roach    public static function scriptDirection($script): string
547c1010edaSGreg Roach    {
548a25f0a04SGreg Roach        switch ($script) {
549a25f0a04SGreg Roach            case 'Arab':
550a25f0a04SGreg Roach            case 'Hebr':
551a25f0a04SGreg Roach            case 'Mong':
552a25f0a04SGreg Roach            case 'Thaa':
553a25f0a04SGreg Roach                return 'rtl';
554a25f0a04SGreg Roach            default:
555a25f0a04SGreg Roach                return 'ltr';
556a25f0a04SGreg Roach        }
557a25f0a04SGreg Roach    }
558a25f0a04SGreg Roach
559a25f0a04SGreg Roach    /**
560dfeee0a8SGreg Roach     * Identify the script used for a piece of text
561dfeee0a8SGreg Roach     *
562d0bfc631SGreg Roach     * @param string $string
563dfeee0a8SGreg Roach     *
564dfeee0a8SGreg Roach     * @return string
565dfeee0a8SGreg Roach     */
5668f53f488SRico Sonntag    public static function textScript($string): string
567c1010edaSGreg Roach    {
568dfeee0a8SGreg Roach        $string = strip_tags($string); // otherwise HTML tags show up as latin
569dfeee0a8SGreg Roach        $string = html_entity_decode($string, ENT_QUOTES, 'UTF-8'); // otherwise HTML entities show up as latin
570c1010edaSGreg Roach        $string = str_replace([
571c1010edaSGreg Roach            '@N.N.',
572c1010edaSGreg Roach            '@P.N.',
573c1010edaSGreg Roach        ], '', $string); // otherwise unknown names show up as latin
574dfeee0a8SGreg Roach        $pos    = 0;
575dfeee0a8SGreg Roach        $strlen = strlen($string);
576dfeee0a8SGreg Roach        while ($pos < $strlen) {
577dfeee0a8SGreg Roach            // get the Unicode Code Point for the character at position $pos
578dfeee0a8SGreg Roach            $byte1 = ord($string[$pos]);
579dfeee0a8SGreg Roach            if ($byte1 < 0x80) {
580dfeee0a8SGreg Roach                $code_point = $byte1;
581dfeee0a8SGreg Roach                $chrlen     = 1;
582dfeee0a8SGreg Roach            } elseif ($byte1 < 0xC0) {
583dfeee0a8SGreg Roach                // Invalid continuation character
584dfeee0a8SGreg Roach                return 'Latn';
585dfeee0a8SGreg Roach            } elseif ($byte1 < 0xE0) {
586dfeee0a8SGreg Roach                $code_point = (($byte1 & 0x1F) << 6) + (ord($string[$pos + 1]) & 0x3F);
587dfeee0a8SGreg Roach                $chrlen     = 2;
588dfeee0a8SGreg Roach            } elseif ($byte1 < 0xF0) {
589dfeee0a8SGreg Roach                $code_point = (($byte1 & 0x0F) << 12) + ((ord($string[$pos + 1]) & 0x3F) << 6) + (ord($string[$pos + 2]) & 0x3F);
590dfeee0a8SGreg Roach                $chrlen     = 3;
591dfeee0a8SGreg Roach            } elseif ($byte1 < 0xF8) {
592dfeee0a8SGreg Roach                $code_point = (($byte1 & 0x07) << 24) + ((ord($string[$pos + 1]) & 0x3F) << 12) + ((ord($string[$pos + 2]) & 0x3F) << 6) + (ord($string[$pos + 3]) & 0x3F);
593dfeee0a8SGreg Roach                $chrlen     = 3;
594dfeee0a8SGreg Roach            } else {
595dfeee0a8SGreg Roach                // Invalid UTF
596dfeee0a8SGreg Roach                return 'Latn';
597dfeee0a8SGreg Roach            }
598dfeee0a8SGreg Roach
599991b93ddSGreg Roach            foreach (self::SCRIPT_CHARACTER_RANGES as $range) {
600dfeee0a8SGreg Roach                if ($code_point >= $range[1] && $code_point <= $range[2]) {
601dfeee0a8SGreg Roach                    return $range[0];
602dfeee0a8SGreg Roach                }
603dfeee0a8SGreg Roach            }
604dfeee0a8SGreg Roach            // Not a recognised script. Maybe punctuation, spacing, etc. Keep looking.
605dfeee0a8SGreg Roach            $pos += $chrlen;
606dfeee0a8SGreg Roach        }
607dfeee0a8SGreg Roach
608dfeee0a8SGreg Roach        return 'Latn';
609dfeee0a8SGreg Roach    }
610dfeee0a8SGreg Roach
611dfeee0a8SGreg Roach    /**
612006094b9SGreg Roach     * Perform a case-insensitive comparison of two strings.
613006094b9SGreg Roach     *
614006094b9SGreg Roach     * @param string $string1
615006094b9SGreg Roach     * @param string $string2
616006094b9SGreg Roach     *
617006094b9SGreg Roach     * @return int
618006094b9SGreg Roach     */
619006094b9SGreg Roach    public static function strcasecmp($string1, $string2): int
620006094b9SGreg Roach    {
621006094b9SGreg Roach        if (self::$collator instanceof Collator) {
622006094b9SGreg Roach            return self::$collator->compare($string1, $string2);
623006094b9SGreg Roach        }
624006094b9SGreg Roach
625006094b9SGreg Roach        return strcmp(self::strtolower($string1), self::strtolower($string2));
626006094b9SGreg Roach    }
627006094b9SGreg Roach
628006094b9SGreg Roach    /**
629006094b9SGreg Roach     * Convert a string to lower case.
630006094b9SGreg Roach     *
631006094b9SGreg Roach     * @param string $string
632006094b9SGreg Roach     *
633006094b9SGreg Roach     * @return string
634006094b9SGreg Roach     */
635006094b9SGreg Roach    public static function strtolower($string): string
636006094b9SGreg Roach    {
637006094b9SGreg Roach        if (in_array(self::$locale->language()->code(), self::DOTLESS_I_LOCALES, true)) {
638006094b9SGreg Roach            $string = strtr($string, self::DOTLESS_I_TOLOWER);
639006094b9SGreg Roach        }
640006094b9SGreg Roach
641006094b9SGreg Roach        return mb_strtolower($string);
642006094b9SGreg Roach    }
643006094b9SGreg Roach
644006094b9SGreg Roach    /**
645006094b9SGreg Roach     * Convert a string to upper case.
646006094b9SGreg Roach     *
647006094b9SGreg Roach     * @param string $string
648006094b9SGreg Roach     *
649006094b9SGreg Roach     * @return string
650006094b9SGreg Roach     */
651006094b9SGreg Roach    public static function strtoupper($string): string
652006094b9SGreg Roach    {
653006094b9SGreg Roach        if (in_array(self::$locale->language()->code(), self::DOTLESS_I_LOCALES, true)) {
654006094b9SGreg Roach            $string = strtr($string, self::DOTLESS_I_TOUPPER);
655006094b9SGreg Roach        }
656006094b9SGreg Roach
657006094b9SGreg Roach        return mb_strtoupper($string);
658006094b9SGreg Roach    }
659006094b9SGreg Roach
660006094b9SGreg Roach    /**
661dfeee0a8SGreg Roach     * What format is used to display dates in the current locale?
662dfeee0a8SGreg Roach     *
663dfeee0a8SGreg Roach     * @return string
664dfeee0a8SGreg Roach     */
6658f53f488SRico Sonntag    public static function timeFormat(): string
666c1010edaSGreg Roach    {
667bbb76c12SGreg Roach        /* I18N: This is the format string for the time-of-day. See http://php.net/date for codes */
668bbb76c12SGreg Roach        return self::$translator->translate('%H:%i:%s');
669dfeee0a8SGreg Roach    }
670dfeee0a8SGreg Roach
671dfeee0a8SGreg Roach    /**
672dfeee0a8SGreg Roach     * Context sensitive version of translate.
673a4956c0eSGreg Roach     * echo I18N::translateContext('NOMINATIVE', 'January');
674a4956c0eSGreg Roach     * echo I18N::translateContext('GENITIVE', 'January');
675dfeee0a8SGreg Roach     *
676924d091bSGreg Roach     * @param string $context
677924d091bSGreg Roach     * @param string $message
678a515be7cSGreg Roach     * @param string ...$args
679c3283ed7SGreg Roach     *
680dfeee0a8SGreg Roach     * @return string
681dfeee0a8SGreg Roach     */
682924d091bSGreg Roach    public static function translateContext(string $context, string $message, ...$args): string
683c1010edaSGreg Roach    {
684924d091bSGreg Roach        $message = self::$translator->translateContext($context, $message);
685dfeee0a8SGreg Roach
686924d091bSGreg Roach        return sprintf($message, ...$args);
687a25f0a04SGreg Roach    }
688a25f0a04SGreg Roach}
689