xref: /webtrees/app/I18N.php (revision b0fcccb03b84c12d54b4ae85014c3af52863d08c)
1a25f0a04SGreg Roach<?php
23976b470SGreg 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 */
17fcfa147eSGreg Roach
18e7f56f2aSGreg Roachdeclare(strict_types=1);
19e7f56f2aSGreg Roach
2076692c8bSGreg Roachnamespace Fisharebest\Webtrees;
21a25f0a04SGreg Roach
22991b93ddSGreg Roachuse Collator;
23f1af7e1cSGreg Roachuse Exception;
24c999a340SGreg Roachuse Fisharebest\Localization\Locale;
251e71bdc0SGreg Roachuse Fisharebest\Localization\Locale\LocaleEnUs;
2615834aaeSGreg Roachuse Fisharebest\Localization\Locale\LocaleInterface;
273bdc890bSGreg Roachuse Fisharebest\Localization\Translation;
283bdc890bSGreg Roachuse Fisharebest\Localization\Translator;
29d37db671SGreg Roachuse Fisharebest\Webtrees\Module\ModuleCustomInterface;
3002086832SGreg Roachuse Fisharebest\Webtrees\Module\ModuleLanguageInterface;
31d37db671SGreg Roachuse Fisharebest\Webtrees\Services\ModuleService;
326cd97bf6SGreg Roachuse Illuminate\Support\Collection;
333976b470SGreg Roach
344f194b97SGreg Roachuse function array_merge;
35d68ee7a8SGreg Roachuse function class_exists;
36d68ee7a8SGreg Roachuse function html_entity_decode;
37d68ee7a8SGreg Roachuse function in_array;
38d68ee7a8SGreg Roachuse function mb_strtolower;
39d68ee7a8SGreg Roachuse function mb_strtoupper;
40d68ee7a8SGreg Roachuse function mb_substr;
41d68ee7a8SGreg Roachuse function ord;
42d68ee7a8SGreg Roachuse function sprintf;
43d68ee7a8SGreg Roachuse function str_replace;
44d68ee7a8SGreg Roachuse function strcmp;
45d68ee7a8SGreg Roachuse function strip_tags;
46d68ee7a8SGreg Roachuse function strlen;
47d68ee7a8SGreg Roachuse function strpos;
48d68ee7a8SGreg Roachuse function strtr;
49*b0fcccb0SGreg Roachuse function var_export;
50a25f0a04SGreg Roach
51a25f0a04SGreg Roach/**
5276692c8bSGreg Roach * Internationalization (i18n) and localization (l10n).
53a25f0a04SGreg Roach */
54c1010edaSGreg Roachclass I18N
55c1010edaSGreg Roach{
56d37db671SGreg Roach    // MO files use special characters for plurals and context.
574f194b97SGreg Roach    public const PLURAL  = "\x00";
584f194b97SGreg Roach    public const CONTEXT = "\x04";
5916d6367aSGreg Roach    private const DIGITS = '0123456789٠١٢٣٤٥٦٧٨٩۰۱۲۳۴۵۶۷۸۹';
6016d6367aSGreg Roach    private const DOTLESS_I_LOCALES = [
61c1010edaSGreg Roach        'az',
62c1010edaSGreg Roach        'tr',
63c1010edaSGreg Roach    ];
6416d6367aSGreg Roach    private const DOTLESS_I_TOLOWER = [
65c1010edaSGreg Roach        'I' => 'ı',
66c1010edaSGreg Roach        'İ' => 'i',
67c1010edaSGreg Roach    ];
68006094b9SGreg Roach
69006094b9SGreg Roach    // Digits are always rendered LTR, even in RTL text.
7016d6367aSGreg Roach    private const DOTLESS_I_TOUPPER = [
71c1010edaSGreg Roach        'ı' => 'I',
72c1010edaSGreg Roach        'i' => 'İ',
73c1010edaSGreg Roach    ];
74a25f0a04SGreg Roach
75006094b9SGreg Roach    // These locales need special handling for the dotless letter I.
7616d6367aSGreg Roach    private const SCRIPT_CHARACTER_RANGES = [
77c1010edaSGreg Roach        [
78c1010edaSGreg Roach            'Latn',
79c1010edaSGreg Roach            0x0041,
80c1010edaSGreg Roach            0x005A,
81c1010edaSGreg Roach        ],
82c1010edaSGreg Roach        [
83c1010edaSGreg Roach            'Latn',
84c1010edaSGreg Roach            0x0061,
85c1010edaSGreg Roach            0x007A,
86c1010edaSGreg Roach        ],
87c1010edaSGreg Roach        [
88c1010edaSGreg Roach            'Latn',
89c1010edaSGreg Roach            0x0100,
90c1010edaSGreg Roach            0x02AF,
91c1010edaSGreg Roach        ],
92c1010edaSGreg Roach        [
93c1010edaSGreg Roach            'Grek',
94c1010edaSGreg Roach            0x0370,
95c1010edaSGreg Roach            0x03FF,
96c1010edaSGreg Roach        ],
97c1010edaSGreg Roach        [
98c1010edaSGreg Roach            'Cyrl',
99c1010edaSGreg Roach            0x0400,
100c1010edaSGreg Roach            0x052F,
101c1010edaSGreg Roach        ],
102c1010edaSGreg Roach        [
103c1010edaSGreg Roach            'Hebr',
104c1010edaSGreg Roach            0x0590,
105c1010edaSGreg Roach            0x05FF,
106c1010edaSGreg Roach        ],
107c1010edaSGreg Roach        [
108c1010edaSGreg Roach            'Arab',
109c1010edaSGreg Roach            0x0600,
110c1010edaSGreg Roach            0x06FF,
111c1010edaSGreg Roach        ],
112c1010edaSGreg Roach        [
113c1010edaSGreg Roach            'Arab',
114c1010edaSGreg Roach            0x0750,
115c1010edaSGreg Roach            0x077F,
116c1010edaSGreg Roach        ],
117c1010edaSGreg Roach        [
118c1010edaSGreg Roach            'Arab',
119c1010edaSGreg Roach            0x08A0,
120c1010edaSGreg Roach            0x08FF,
121c1010edaSGreg Roach        ],
122c1010edaSGreg Roach        [
123c1010edaSGreg Roach            'Deva',
124c1010edaSGreg Roach            0x0900,
125c1010edaSGreg Roach            0x097F,
126c1010edaSGreg Roach        ],
127c1010edaSGreg Roach        [
128c1010edaSGreg Roach            'Taml',
129c1010edaSGreg Roach            0x0B80,
130c1010edaSGreg Roach            0x0BFF,
131c1010edaSGreg Roach        ],
132c1010edaSGreg Roach        [
133c1010edaSGreg Roach            'Sinh',
134c1010edaSGreg Roach            0x0D80,
135c1010edaSGreg Roach            0x0DFF,
136c1010edaSGreg Roach        ],
137c1010edaSGreg Roach        [
138c1010edaSGreg Roach            'Thai',
139c1010edaSGreg Roach            0x0E00,
140c1010edaSGreg Roach            0x0E7F,
141c1010edaSGreg Roach        ],
142c1010edaSGreg Roach        [
143c1010edaSGreg Roach            'Geor',
144c1010edaSGreg Roach            0x10A0,
145c1010edaSGreg Roach            0x10FF,
146c1010edaSGreg Roach        ],
147c1010edaSGreg Roach        [
148c1010edaSGreg Roach            'Grek',
149c1010edaSGreg Roach            0x1F00,
150c1010edaSGreg Roach            0x1FFF,
151c1010edaSGreg Roach        ],
152c1010edaSGreg Roach        [
153c1010edaSGreg Roach            'Deva',
154c1010edaSGreg Roach            0xA8E0,
155c1010edaSGreg Roach            0xA8FF,
156c1010edaSGreg Roach        ],
157c1010edaSGreg Roach        [
158c1010edaSGreg Roach            'Hans',
159c1010edaSGreg Roach            0x3000,
160c1010edaSGreg Roach            0x303F,
161c1010edaSGreg Roach        ],
162c1010edaSGreg Roach        // Mixed CJK, not just Hans
163c1010edaSGreg Roach        [
164c1010edaSGreg Roach            'Hans',
165c1010edaSGreg Roach            0x3400,
166c1010edaSGreg Roach            0xFAFF,
167c1010edaSGreg Roach        ],
168c1010edaSGreg Roach        // Mixed CJK, not just Hans
169c1010edaSGreg Roach        [
170c1010edaSGreg Roach            'Hans',
171c1010edaSGreg Roach            0x20000,
172c1010edaSGreg Roach            0x2FA1F,
173c1010edaSGreg Roach        ],
174c1010edaSGreg Roach        // Mixed CJK, not just Hans
17513abd6f3SGreg Roach    ];
17616d6367aSGreg Roach    private const MIRROR_CHARACTERS = [
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        '” ' => '“',
193a25f0a04SGreg Roach        '‘ ' => '’',
194a25f0a04SGreg Roach        '’ ' => '‘',
19513abd6f3SGreg Roach    ];
196a25f0a04SGreg Roach    /** @var string Punctuation used to separate list items, typically a comma */
197a25f0a04SGreg Roach    public static $list_separator;
198a25f0a04SGreg Roach
199006094b9SGreg Roach    // The ranges of characters used by each script.
200006094b9SGreg Roach    /** @var LocaleInterface The current locale (e.g. LocaleEnGb) */
201006094b9SGreg Roach    private static $locale;
202006094b9SGreg Roach
203006094b9SGreg Roach    // Characters that are displayed in mirror form in RTL text.
204006094b9SGreg Roach    /** @var Translator An object that performs translation */
205006094b9SGreg Roach    private static $translator;
206006094b9SGreg Roach    /** @var  Collator|null From the php-intl library */
207006094b9SGreg Roach    private static $collator;
208006094b9SGreg Roach
209a25f0a04SGreg Roach    /**
21002086832SGreg Roach     * The preferred locales for this site, or a default list if no preference.
211dfeee0a8SGreg Roach     *
212dfeee0a8SGreg Roach     * @return LocaleInterface[]
213dfeee0a8SGreg Roach     */
2148f53f488SRico Sonntag    public static function activeLocales(): array
215c1010edaSGreg Roach    {
216006094b9SGreg Roach        /** @var Collection $locales */
21702086832SGreg Roach        $locales = app(ModuleService::class)
218d6137952SGreg Roach            ->findByInterface(ModuleLanguageInterface::class, false, true)
2190b5fd0a6SGreg Roach            ->map(static function (ModuleLanguageInterface $module): LocaleInterface {
22002086832SGreg Roach                return $module->locale();
22102086832SGreg Roach            });
222dfeee0a8SGreg Roach
22302086832SGreg Roach        if ($locales->isEmpty()) {
22402086832SGreg Roach            return [new LocaleEnUs()];
225dfeee0a8SGreg Roach        }
226dfeee0a8SGreg Roach
22702086832SGreg Roach        return $locales->all();
228dfeee0a8SGreg Roach    }
229dfeee0a8SGreg Roach
230dfeee0a8SGreg Roach    /**
231dfeee0a8SGreg Roach     * Which MySQL collation should be used for this locale?
232dfeee0a8SGreg Roach     *
233dfeee0a8SGreg Roach     * @return string
234dfeee0a8SGreg Roach     */
235e364afe4SGreg Roach    public static function collation(): string
236c1010edaSGreg Roach    {
237dfeee0a8SGreg Roach        $collation = self::$locale->collation();
238dfeee0a8SGreg Roach        switch ($collation) {
239dfeee0a8SGreg Roach            case 'croatian_ci':
240dfeee0a8SGreg Roach            case 'german2_ci':
241dfeee0a8SGreg Roach            case 'vietnamese_ci':
242dfeee0a8SGreg Roach                // Only available in MySQL 5.6
243dfeee0a8SGreg Roach                return 'utf8_unicode_ci';
244dfeee0a8SGreg Roach            default:
245dfeee0a8SGreg Roach                return 'utf8_' . $collation;
246dfeee0a8SGreg Roach        }
247dfeee0a8SGreg Roach    }
248dfeee0a8SGreg Roach
249dfeee0a8SGreg Roach    /**
250dfeee0a8SGreg Roach     * What format is used to display dates in the current locale?
251dfeee0a8SGreg Roach     *
252dfeee0a8SGreg Roach     * @return string
253dfeee0a8SGreg Roach     */
2548f53f488SRico Sonntag    public static function dateFormat(): string
255c1010edaSGreg Roach    {
256bbb76c12SGreg Roach        /* I18N: This is the format string for full dates. See http://php.net/date for codes */
257bbb76c12SGreg Roach        return self::$translator->translate('%j %F %Y');
258dfeee0a8SGreg Roach    }
259dfeee0a8SGreg Roach
260dfeee0a8SGreg Roach    /**
261dfeee0a8SGreg Roach     * Convert the digits 0-9 into the local script
262dfeee0a8SGreg Roach     * Used for years, etc., where we do not want thousands-separators, decimals, etc.
263dfeee0a8SGreg Roach     *
26455664801SGreg Roach     * @param string|int $n
265dfeee0a8SGreg Roach     *
266dfeee0a8SGreg Roach     * @return string
267dfeee0a8SGreg Roach     */
2688f53f488SRico Sonntag    public static function digits($n): string
269c1010edaSGreg Roach    {
27055664801SGreg Roach        return self::$locale->digits((string) $n);
271dfeee0a8SGreg Roach    }
272dfeee0a8SGreg Roach
273dfeee0a8SGreg Roach    /**
274dfeee0a8SGreg Roach     * What is the direction of the current locale
275dfeee0a8SGreg Roach     *
276dfeee0a8SGreg Roach     * @return string "ltr" or "rtl"
277dfeee0a8SGreg Roach     */
2788f53f488SRico Sonntag    public static function direction(): string
279c1010edaSGreg Roach    {
280dfeee0a8SGreg Roach        return self::$locale->direction();
281dfeee0a8SGreg Roach    }
282dfeee0a8SGreg Roach
283dfeee0a8SGreg Roach    /**
284a25f0a04SGreg Roach     * Initialise the translation adapter with a locale setting.
285a25f0a04SGreg Roach     *
286150f35adSGreg Roach     * @param string $code
287150f35adSGreg Roach     * @param bool   $setup
288a25f0a04SGreg Roach     *
289150f35adSGreg Roach     * @return void
290a25f0a04SGreg Roach     */
291150f35adSGreg Roach    public static function init(string $code, bool $setup = false): void
292c1010edaSGreg Roach    {
2933bdc890bSGreg Roach        self::$locale = Locale::create($code);
2943bdc890bSGreg Roach
2954f194b97SGreg Roach        // Load the translation file
296150f35adSGreg Roach        $translation_file = __DIR__ . '/../resources/lang/' . self::$locale->languageTag() . '/messages.php';
2974f194b97SGreg Roach
298f1af7e1cSGreg Roach        try {
299006094b9SGreg Roach            $translation  = new Translation($translation_file);
300006094b9SGreg Roach            $translations = $translation->asArray();
301f1af7e1cSGreg Roach        } catch (Exception $ex) {
302006094b9SGreg Roach            // The translations files are created during the build process, and are
303006094b9SGreg Roach            // not included in the source code.
304006094b9SGreg Roach            // Assuming we are using dev code, and build (or rebuild) the files.
305006094b9SGreg Roach            $po_file      = Webtrees::ROOT_DIR . 'resources/lang/' . self::$locale->languageTag() . '/messages.po';
306006094b9SGreg Roach            $translation  = new Translation($po_file);
307006094b9SGreg Roach            $translations = $translation->asArray();
308*b0fcccb0SGreg Roach            file_put_contents($translation_file, "<?php\n\nreturn " . var_export($translations, true) . ";\n");
309a25f0a04SGreg Roach        }
310a25f0a04SGreg Roach
3114f194b97SGreg Roach        // Add translations from custom modules (but not during setup, as we have no database/modules)
312c116a5ccSGreg Roach        if (!$setup) {
3134f194b97SGreg Roach            $translations = app(ModuleService::class)
3144f194b97SGreg Roach                ->findByInterface(ModuleCustomInterface::class)
31569253da9SGreg Roach                ->reduce(static function (array $carry, ModuleCustomInterface $item): array {
3164f194b97SGreg Roach                    return array_merge($carry, $item->customTranslations(self::$locale->languageTag()));
3174f194b97SGreg Roach                }, $translations);
318d37db671SGreg Roach        }
319d37db671SGreg Roach
3203bdc890bSGreg Roach        // Create a translator
3213bdc890bSGreg Roach        self::$translator = new Translator($translations, self::$locale->pluralRule());
322a25f0a04SGreg Roach
323bbb76c12SGreg Roach        /* I18N: This punctuation is used to separate lists of items */
324bbb76c12SGreg Roach        self::$list_separator = self::translate(', ');
325a25f0a04SGreg Roach
326991b93ddSGreg Roach        // Create a collator
327991b93ddSGreg Roach        try {
328444a65ecSGreg Roach            if (class_exists('Collator')) {
329c9ec599fSGreg Roach                // Symfony provides a very incomplete polyfill - which cannot be used.
330991b93ddSGreg Roach                self::$collator = new Collator(self::$locale->code());
331991b93ddSGreg Roach                // Ignore upper/lower case differences
332991b93ddSGreg Roach                self::$collator->setStrength(Collator::SECONDARY);
333444a65ecSGreg Roach            }
334991b93ddSGreg Roach        } catch (Exception $ex) {
335991b93ddSGreg Roach            // PHP-INTL is not installed?  We'll use a fallback later.
336c9ec599fSGreg Roach            self::$collator = null;
337991b93ddSGreg Roach        }
338a25f0a04SGreg Roach    }
339a25f0a04SGreg Roach
340a25f0a04SGreg Roach    /**
341006094b9SGreg Roach     * Translate a string, and then substitute placeholders
342006094b9SGreg Roach     * echo I18N::translate('Hello World!');
343006094b9SGreg Roach     * echo I18N::translate('The %s sat on the mat', 'cat');
344006094b9SGreg Roach     *
345006094b9SGreg Roach     * @param string $message
346006094b9SGreg Roach     * @param string ...$args
347006094b9SGreg Roach     *
348006094b9SGreg Roach     * @return string
349006094b9SGreg Roach     */
350006094b9SGreg Roach    public static function translate(string $message, ...$args): string
351006094b9SGreg Roach    {
352006094b9SGreg Roach        $message = self::$translator->translate($message);
353006094b9SGreg Roach
354006094b9SGreg Roach        return sprintf($message, ...$args);
355006094b9SGreg Roach    }
356006094b9SGreg Roach
357006094b9SGreg Roach    /**
35890a2f718SGreg Roach     * @return string
35990a2f718SGreg Roach     */
36090a2f718SGreg Roach    public static function languageTag(): string
36190a2f718SGreg Roach    {
36290a2f718SGreg Roach        return self::$locale->languageTag();
36390a2f718SGreg Roach    }
36490a2f718SGreg Roach
36590a2f718SGreg Roach    /**
36665cf5706SGreg Roach     * @return LocaleInterface
36765cf5706SGreg Roach     */
36865cf5706SGreg Roach    public static function locale(): LocaleInterface
36965cf5706SGreg Roach    {
37065cf5706SGreg Roach        return self::$locale;
37165cf5706SGreg Roach    }
37265cf5706SGreg Roach
37365cf5706SGreg Roach    /**
374dfeee0a8SGreg Roach     * Translate a number into the local representation.
375dfeee0a8SGreg Roach     * e.g. 12345.67 becomes
376dfeee0a8SGreg Roach     * en: 12,345.67
377dfeee0a8SGreg Roach     * fr: 12 345,67
378dfeee0a8SGreg Roach     * de: 12.345,67
379dfeee0a8SGreg Roach     *
380dfeee0a8SGreg Roach     * @param float $n
381cbc1590aSGreg Roach     * @param int   $precision
382a25f0a04SGreg Roach     *
383a25f0a04SGreg Roach     * @return string
384a25f0a04SGreg Roach     */
38555664801SGreg Roach    public static function number(float $n, int $precision = 0): string
386c1010edaSGreg Roach    {
387dfeee0a8SGreg Roach        return self::$locale->number(round($n, $precision));
388dfeee0a8SGreg Roach    }
389dfeee0a8SGreg Roach
390dfeee0a8SGreg Roach    /**
391dfeee0a8SGreg Roach     * Translate a fraction into a percentage.
392dfeee0a8SGreg Roach     * e.g. 0.123 becomes
393dfeee0a8SGreg Roach     * en: 12.3%
394dfeee0a8SGreg Roach     * fr: 12,3 %
395dfeee0a8SGreg Roach     * de: 12,3%
396dfeee0a8SGreg Roach     *
397dfeee0a8SGreg Roach     * @param float $n
398cbc1590aSGreg Roach     * @param int   $precision
399dfeee0a8SGreg Roach     *
400dfeee0a8SGreg Roach     * @return string
401dfeee0a8SGreg Roach     */
40255664801SGreg Roach    public static function percentage(float $n, int $precision = 0): string
403c1010edaSGreg Roach    {
404dfeee0a8SGreg Roach        return self::$locale->percent(round($n, $precision + 2));
405dfeee0a8SGreg Roach    }
406dfeee0a8SGreg Roach
407dfeee0a8SGreg Roach    /**
408dfeee0a8SGreg Roach     * Translate a plural string
409dfeee0a8SGreg Roach     * echo self::plural('There is an error', 'There are errors', $num_errors);
410dfeee0a8SGreg Roach     * echo self::plural('There is one error', 'There are %s errors', $num_errors);
411dfeee0a8SGreg Roach     * echo self::plural('There is %1$s %2$s cat', 'There are %1$s %2$s cats', $num, $num, $colour);
412dfeee0a8SGreg Roach     *
413924d091bSGreg Roach     * @param string $singular
414924d091bSGreg Roach     * @param string $plural
415924d091bSGreg Roach     * @param int    $count
416a515be7cSGreg Roach     * @param string ...$args
417e93111adSRico Sonntag     *
418dfeee0a8SGreg Roach     * @return string
419dfeee0a8SGreg Roach     */
420924d091bSGreg Roach    public static function plural(string $singular, string $plural, int $count, ...$args): string
421c1010edaSGreg Roach    {
422924d091bSGreg Roach        $message = self::$translator->translatePlural($singular, $plural, $count);
423dfeee0a8SGreg Roach
424924d091bSGreg Roach        return sprintf($message, ...$args);
425dfeee0a8SGreg Roach    }
426dfeee0a8SGreg Roach
427dfeee0a8SGreg Roach    /**
428dfeee0a8SGreg Roach     * UTF8 version of PHP::strrev()
429dfeee0a8SGreg Roach     * Reverse RTL text for third-party libraries such as GD2 and googlechart.
430dfeee0a8SGreg Roach     * These do not support UTF8 text direction, so we must mimic it for them.
431dfeee0a8SGreg Roach     * Numbers are always rendered LTR, even in RTL text.
432dfeee0a8SGreg Roach     * The visual direction of characters such as parentheses should be reversed.
433dfeee0a8SGreg Roach     *
434dfeee0a8SGreg Roach     * @param string $text Text to be reversed
435dfeee0a8SGreg Roach     *
436dfeee0a8SGreg Roach     * @return string
437dfeee0a8SGreg Roach     */
4388f53f488SRico Sonntag    public static function reverseText($text): string
439c1010edaSGreg Roach    {
440dfeee0a8SGreg Roach        // Remove HTML markup - we can't display it and it is LTR.
4419524b7b5SGreg Roach        $text = strip_tags($text);
4429524b7b5SGreg Roach        // Remove HTML entities.
4439524b7b5SGreg Roach        $text = html_entity_decode($text, ENT_QUOTES, 'UTF-8');
444dfeee0a8SGreg Roach
445dfeee0a8SGreg Roach        // LTR text doesn't need reversing
446dfeee0a8SGreg Roach        if (self::scriptDirection(self::textScript($text)) === 'ltr') {
447dfeee0a8SGreg Roach            return $text;
448dfeee0a8SGreg Roach        }
449dfeee0a8SGreg Roach
450dfeee0a8SGreg Roach        // Mirrored characters
451991b93ddSGreg Roach        $text = strtr($text, self::MIRROR_CHARACTERS);
452dfeee0a8SGreg Roach
453dfeee0a8SGreg Roach        $reversed = '';
454dfeee0a8SGreg Roach        $digits   = '';
455e364afe4SGreg Roach        while ($text !== '') {
456dfeee0a8SGreg Roach            $letter = mb_substr($text, 0, 1);
457dfeee0a8SGreg Roach            $text   = mb_substr($text, 1);
458dfeee0a8SGreg Roach            if (strpos(self::DIGITS, $letter) !== false) {
459dfeee0a8SGreg Roach                $digits .= $letter;
460a25f0a04SGreg Roach            } else {
461dfeee0a8SGreg Roach                $reversed = $letter . $digits . $reversed;
462dfeee0a8SGreg Roach                $digits   = '';
463dfeee0a8SGreg Roach            }
464a25f0a04SGreg Roach        }
465a25f0a04SGreg Roach
466dfeee0a8SGreg Roach        return $digits . $reversed;
467a25f0a04SGreg Roach    }
468a25f0a04SGreg Roach
469a25f0a04SGreg Roach    /**
470a25f0a04SGreg Roach     * Return the direction (ltr or rtl) for a given script
471a25f0a04SGreg Roach     * The PHP/intl library does not provde this information, so we need
472a25f0a04SGreg Roach     * our own lookup table.
473a25f0a04SGreg Roach     *
474a25f0a04SGreg Roach     * @param string $script
475a25f0a04SGreg Roach     *
476a25f0a04SGreg Roach     * @return string
477a25f0a04SGreg Roach     */
478e364afe4SGreg Roach    public static function scriptDirection($script): string
479c1010edaSGreg Roach    {
480a25f0a04SGreg Roach        switch ($script) {
481a25f0a04SGreg Roach            case 'Arab':
482a25f0a04SGreg Roach            case 'Hebr':
483a25f0a04SGreg Roach            case 'Mong':
484a25f0a04SGreg Roach            case 'Thaa':
485a25f0a04SGreg Roach                return 'rtl';
486a25f0a04SGreg Roach            default:
487a25f0a04SGreg Roach                return 'ltr';
488a25f0a04SGreg Roach        }
489a25f0a04SGreg Roach    }
490a25f0a04SGreg Roach
491a25f0a04SGreg Roach    /**
492dfeee0a8SGreg Roach     * Identify the script used for a piece of text
493dfeee0a8SGreg Roach     *
494d0bfc631SGreg Roach     * @param string $string
495dfeee0a8SGreg Roach     *
496dfeee0a8SGreg Roach     * @return string
497dfeee0a8SGreg Roach     */
4988f53f488SRico Sonntag    public static function textScript($string): string
499c1010edaSGreg Roach    {
500dfeee0a8SGreg Roach        $string = strip_tags($string); // otherwise HTML tags show up as latin
501dfeee0a8SGreg Roach        $string = html_entity_decode($string, ENT_QUOTES, 'UTF-8'); // otherwise HTML entities show up as latin
502c1010edaSGreg Roach        $string = str_replace([
503c1010edaSGreg Roach            '@N.N.',
504c1010edaSGreg Roach            '@P.N.',
505c1010edaSGreg Roach        ], '', $string); // otherwise unknown names show up as latin
506dfeee0a8SGreg Roach        $pos    = 0;
507dfeee0a8SGreg Roach        $strlen = strlen($string);
508dfeee0a8SGreg Roach        while ($pos < $strlen) {
509dfeee0a8SGreg Roach            // get the Unicode Code Point for the character at position $pos
510dfeee0a8SGreg Roach            $byte1 = ord($string[$pos]);
511dfeee0a8SGreg Roach            if ($byte1 < 0x80) {
512dfeee0a8SGreg Roach                $code_point = $byte1;
513dfeee0a8SGreg Roach                $chrlen     = 1;
514dfeee0a8SGreg Roach            } elseif ($byte1 < 0xC0) {
515dfeee0a8SGreg Roach                // Invalid continuation character
516dfeee0a8SGreg Roach                return 'Latn';
517dfeee0a8SGreg Roach            } elseif ($byte1 < 0xE0) {
518dfeee0a8SGreg Roach                $code_point = (($byte1 & 0x1F) << 6) + (ord($string[$pos + 1]) & 0x3F);
519dfeee0a8SGreg Roach                $chrlen     = 2;
520dfeee0a8SGreg Roach            } elseif ($byte1 < 0xF0) {
521dfeee0a8SGreg Roach                $code_point = (($byte1 & 0x0F) << 12) + ((ord($string[$pos + 1]) & 0x3F) << 6) + (ord($string[$pos + 2]) & 0x3F);
522dfeee0a8SGreg Roach                $chrlen     = 3;
523dfeee0a8SGreg Roach            } elseif ($byte1 < 0xF8) {
524dfeee0a8SGreg Roach                $code_point = (($byte1 & 0x07) << 24) + ((ord($string[$pos + 1]) & 0x3F) << 12) + ((ord($string[$pos + 2]) & 0x3F) << 6) + (ord($string[$pos + 3]) & 0x3F);
525dfeee0a8SGreg Roach                $chrlen     = 3;
526dfeee0a8SGreg Roach            } else {
527dfeee0a8SGreg Roach                // Invalid UTF
528dfeee0a8SGreg Roach                return 'Latn';
529dfeee0a8SGreg Roach            }
530dfeee0a8SGreg Roach
531991b93ddSGreg Roach            foreach (self::SCRIPT_CHARACTER_RANGES as $range) {
532dfeee0a8SGreg Roach                if ($code_point >= $range[1] && $code_point <= $range[2]) {
533dfeee0a8SGreg Roach                    return $range[0];
534dfeee0a8SGreg Roach                }
535dfeee0a8SGreg Roach            }
536dfeee0a8SGreg Roach            // Not a recognised script. Maybe punctuation, spacing, etc. Keep looking.
537dfeee0a8SGreg Roach            $pos += $chrlen;
538dfeee0a8SGreg Roach        }
539dfeee0a8SGreg Roach
540dfeee0a8SGreg Roach        return 'Latn';
541dfeee0a8SGreg Roach    }
542dfeee0a8SGreg Roach
543dfeee0a8SGreg Roach    /**
544006094b9SGreg Roach     * Perform a case-insensitive comparison of two strings.
545006094b9SGreg Roach     *
546006094b9SGreg Roach     * @param string $string1
547006094b9SGreg Roach     * @param string $string2
548006094b9SGreg Roach     *
549006094b9SGreg Roach     * @return int
550006094b9SGreg Roach     */
551006094b9SGreg Roach    public static function strcasecmp($string1, $string2): int
552006094b9SGreg Roach    {
553006094b9SGreg Roach        if (self::$collator instanceof Collator) {
554006094b9SGreg Roach            return self::$collator->compare($string1, $string2);
555006094b9SGreg Roach        }
556006094b9SGreg Roach
557006094b9SGreg Roach        return strcmp(self::strtolower($string1), self::strtolower($string2));
558006094b9SGreg Roach    }
559006094b9SGreg Roach
560006094b9SGreg Roach    /**
561006094b9SGreg Roach     * Convert a string to lower case.
562006094b9SGreg Roach     *
563006094b9SGreg Roach     * @param string $string
564006094b9SGreg Roach     *
565006094b9SGreg Roach     * @return string
566006094b9SGreg Roach     */
567006094b9SGreg Roach    public static function strtolower($string): string
568006094b9SGreg Roach    {
569006094b9SGreg Roach        if (in_array(self::$locale->language()->code(), self::DOTLESS_I_LOCALES, true)) {
570006094b9SGreg Roach            $string = strtr($string, self::DOTLESS_I_TOLOWER);
571006094b9SGreg Roach        }
572006094b9SGreg Roach
573006094b9SGreg Roach        return mb_strtolower($string);
574006094b9SGreg Roach    }
575006094b9SGreg Roach
576006094b9SGreg Roach    /**
577006094b9SGreg Roach     * Convert a string to upper case.
578006094b9SGreg Roach     *
579006094b9SGreg Roach     * @param string $string
580006094b9SGreg Roach     *
581006094b9SGreg Roach     * @return string
582006094b9SGreg Roach     */
583006094b9SGreg Roach    public static function strtoupper($string): string
584006094b9SGreg Roach    {
585006094b9SGreg Roach        if (in_array(self::$locale->language()->code(), self::DOTLESS_I_LOCALES, true)) {
586006094b9SGreg Roach            $string = strtr($string, self::DOTLESS_I_TOUPPER);
587006094b9SGreg Roach        }
588006094b9SGreg Roach
589006094b9SGreg Roach        return mb_strtoupper($string);
590006094b9SGreg Roach    }
591006094b9SGreg Roach
592006094b9SGreg Roach    /**
593dfeee0a8SGreg Roach     * What format is used to display dates in the current locale?
594dfeee0a8SGreg Roach     *
595dfeee0a8SGreg Roach     * @return string
596dfeee0a8SGreg Roach     */
5978f53f488SRico Sonntag    public static function timeFormat(): string
598c1010edaSGreg Roach    {
599bbb76c12SGreg Roach        /* I18N: This is the format string for the time-of-day. See http://php.net/date for codes */
600bbb76c12SGreg Roach        return self::$translator->translate('%H:%i:%s');
601dfeee0a8SGreg Roach    }
602dfeee0a8SGreg Roach
603dfeee0a8SGreg Roach    /**
604dfeee0a8SGreg Roach     * Context sensitive version of translate.
605a4956c0eSGreg Roach     * echo I18N::translateContext('NOMINATIVE', 'January');
606a4956c0eSGreg Roach     * echo I18N::translateContext('GENITIVE', 'January');
607dfeee0a8SGreg Roach     *
608924d091bSGreg Roach     * @param string $context
609924d091bSGreg Roach     * @param string $message
610a515be7cSGreg Roach     * @param string ...$args
611c3283ed7SGreg Roach     *
612dfeee0a8SGreg Roach     * @return string
613dfeee0a8SGreg Roach     */
614924d091bSGreg Roach    public static function translateContext(string $context, string $message, ...$args): string
615c1010edaSGreg Roach    {
616924d091bSGreg Roach        $message = self::$translator->translateContext($context, $message);
617dfeee0a8SGreg Roach
618924d091bSGreg Roach        return sprintf($message, ...$args);
619a25f0a04SGreg Roach    }
620a25f0a04SGreg Roach}
621