Passed
Push — main ( 410c99...ca1606 )
by MusikAnimal
03:04
created

I18nHelper::getLangForTranslatingNumerals()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 1
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace App\Helper;
6
7
use DateTime;
8
use IntlDateFormatter;
9
use Intuition;
10
use NumberFormatter;
11
use Symfony\Component\Config\Definition\Exception\Exception;
12
use Symfony\Component\DependencyInjection\ContainerInterface;
13
use Symfony\Component\HttpFoundation\Request;
14
use Symfony\Component\HttpFoundation\RequestStack;
15
16
/**
17
 * The I18nHelper centralizes all methods for i18n and l10n,
18
 * and interactions with the Intuition library.
19
 */
20
class I18nHelper
21
{
22
    private string $projectDir;
23
    protected ContainerInterface $container;
24
    protected Intuition $intuition;
25
    protected IntlDateFormatter $dateFormatter;
26
    protected NumberFormatter $numFormatter;
27
    protected NumberFormatter $percentFormatter;
28
    protected RequestStack $requestStack;
29
30
    /**
31
     * Constructor for the I18nHelper.
32
     * @param RequestStack $requestStack
33
     * @param string $projectDir
34
     */
35
    public function __construct(
36
        RequestStack $requestStack,
37
        string $projectDir
38
    ) {
39
        $this->requestStack = $requestStack;
40
        $this->projectDir = $projectDir;
41
    }
42
43
    /**
44
     * Get an Intuition object, set to the current language based on the query string or session
45
     * of the current request.
46
     * @return Intuition
47
     * @throws Exception If the 'i18n/en.json' file doesn't exist (as it's the default).
48
     */
49
    public function getIntuition(): Intuition
50
    {
51
        // Don't recreate the object.
52
        if (isset($this->intuition)) {
53
            return $this->intuition;
54
        }
55
56
        // Find the path, and complain if English doesn't exist.
57
        $path = $this->projectDir . '/i18n';
58
        if (!file_exists("$path/en.json")) {
59
            throw new Exception("Language directory doesn't exist: $path");
60
        }
61
62
        $useLang = $this->getIntuitionLang();
63
64
        // Save the language to the session.
65
        $session = $this->requestStack->getSession();
66
        if ($session->get('lang') !== $useLang) {
67
            $session->set('lang', $useLang);
68
        }
69
70
        // Set up Intuition, using the selected language.
71
        $intuition = new Intuition('xtools');
72
        $intuition->registerDomain('xtools', $path);
73
        $intuition->setLang(strtolower($useLang));
74
75
        $this->intuition = $intuition;
76
        return $intuition;
77
    }
78
79
    /**
80
     * Get the current language code.
81
     * @return string
82
     */
83
    public function getLang(): string
84
    {
85
        return $this->getIntuition()->getLang();
86
    }
87
88
    /**
89
     * Get the current language name (defaults to 'English').
90
     * @return string
91
     */
92
    public function getLangName(): string
93
    {
94
        return in_array(ucfirst($this->getIntuition()->getLangName()), $this->getAllLangs())
95
            ? $this->getIntuition()->getLangName()
96
            : 'English';
97
    }
98
99
    /**
100
     * Get all available languages in the i18n directory
101
     * @return string[] Associative array of langKey => langName
102
     */
103
    public function getAllLangs(): array
104
    {
105
        $messageFiles = glob($this->projectDir.'/i18n/*.json');
106
107
        $languages = array_values(array_unique(array_map(
108
            function ($filename) {
109
                return basename($filename, '.json');
110
            },
111
            $messageFiles
112
        )));
113
114
        $availableLanguages = [];
115
116
        foreach ($languages as $lang) {
117
            $availableLanguages[$lang] = ucfirst($this->getIntuition()->getLangName($lang));
118
        }
119
        asort($availableLanguages);
120
121
        return $availableLanguages;
122
    }
123
124
    /**
125
     * Whether the current language is right-to-left.
126
     * @param string|null $lang Optionally provide a specific language code.
127
     * @return bool
128
     */
129
    public function isRTL(?string $lang = null): bool
130
    {
131
        return $this->getIntuition()->isRTL(
132
            $lang ?? $this->getLang()
133
        );
134
    }
135
136
    /**
137
     * Get the fallback languages for the current or given language, so we know what to
138
     * load with jQuery.i18n. Languages for which no file exists are not returned.
139
     * @param string|null $useLang
140
     * @return string[]
141
     */
142
    public function getFallbacks(?string $useLang = null): array
143
    {
144
        $i18nPath = $this->projectDir.'/i18n/';
145
        $useLang = $useLang ?? $this->getLang();
146
147
        $fallbacks = array_merge(
148
            [$useLang],
149
            $this->getIntuition()->getLangFallbacks($useLang)
150
        );
151
152
        return array_filter($fallbacks, function ($lang) use ($i18nPath) {
153
            return is_file($i18nPath.$lang.'.json');
154
        });
155
    }
156
157
    /******************** MESSAGE HELPERS ********************/
158
159
    /**
160
     * Get an i18n message.
161
     * @param string|null $message
162
     * @param string[] $vars
163
     * @return string|null
164
     */
165
    public function msg(?string $message, array $vars = []): ?string
166
    {
167
        return $this->getIntuition()->msg($message, ['domain' => 'xtools', 'variables' => $vars]);
168
    }
169
170
    /**
171
     * See if a given i18n message exists.
172
     * @param string|null $message The message.
173
     * @param string[] $vars
174
     * @return bool
175
     */
176
    public function msgExists(?string $message, array $vars = []): bool
177
    {
178
        return $this->getIntuition()->msgExists($message, array_merge(
179
            ['domain' => 'xtools'],
180
            ['variables' => $vars]
181
        ));
182
    }
183
184
    /**
185
     * Get an i18n message if it exists, otherwise just get the message key.
186
     * @param string|null $message
187
     * @param string[] $vars
188
     * @return string
189
     */
190
    public function msgIfExists(?string $message, array $vars = []): string
191
    {
192
        if ($this->msgExists($message, $vars)) {
193
            return $this->msg($message, $vars);
194
        } else {
195
            return $message ?? '';
196
        }
197
    }
198
199
    /************************ NUMBERS ************************/
200
201
    /**
202
     * Format a number based on language settings.
203
     * @param int|float $number
204
     * @param int $decimals Number of decimals to format to.
205
     * @return string
206
     */
207
    public function numberFormat($number, int $decimals = 0): string
208
    {
209
        $lang = $this->getLangForTranslatingNumerals();
210
        if (!isset($this->numFormatter)) {
211
            $this->numFormatter = new NumberFormatter($lang, NumberFormatter::DECIMAL);
212
        }
213
214
        $this->numFormatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimals);
215
216
        return $this->numFormatter->format((float)$number ?? 0);
217
    }
218
219
    /**
220
     * Format a given number or fraction as a percentage.
221
     * @param int|float $numerator Numerator or single fraction if denominator is omitted.
222
     * @param int|null $denominator Denominator.
223
     * @param integer $precision Number of decimal places to show.
224
     * @return string Formatted percentage.
225
     */
226
    public function percentFormat($numerator, ?int $denominator = null, int $precision = 1): string
227
    {
228
        $lang = $this->getLangForTranslatingNumerals();
229
        if (!isset($this->percentFormatter)) {
230
            $this->percentFormatter = new NumberFormatter($lang, NumberFormatter::PERCENT);
231
        }
232
233
        $this->percentFormatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $precision);
234
235
        if (null === $denominator) {
236
            $quotient = $numerator / 100;
237
        } elseif (0 === $denominator) {
238
            $quotient = 0;
239
        } else {
240
            $quotient = $numerator / $denominator;
241
        }
242
243
        return $this->percentFormatter->format($quotient);
244
    }
245
246
    /************************ DATES ************************/
247
248
    /**
249
     * Localize the given date based on language settings.
250
     * @param string|int|DateTime $datetime
251
     * @param string $pattern Format according to this ICU date format.
252
     * @see http://userguide.icu-project.org/formatparse/datetime
253
     * @return string
254
     */
255
    public function dateFormat($datetime, string $pattern = 'yyyy-MM-dd HH:mm'): string
256
    {
257
        $lang = $this->getLangForTranslatingNumerals();
258
        if (!isset($this->dateFormatter)) {
259
            $this->dateFormatter = new IntlDateFormatter(
260
                $lang,
261
                IntlDateFormatter::SHORT,
262
                IntlDateFormatter::SHORT
263
            );
264
        }
265
266
        if (is_string($datetime)) {
267
            $datetime = new DateTime($datetime);
268
        } elseif (is_int($datetime)) {
269
            $datetime = DateTime::createFromFormat('U', (string)$datetime);
270
        } elseif (!is_a($datetime, 'DateTime')) {
271
            return ''; // Unknown format.
272
        }
273
274
        $this->dateFormatter->setPattern($pattern);
275
276
        return $this->dateFormatter->format($datetime);
277
    }
278
279
    /********************* PRIVATE METHODS *********************/
280
281
    /**
282
     * Return the language to be used when translating numberals.
283
     * Currently this just disables numeral translation for Arabic.
284
     * @see https://mediawiki.org/wiki/Topic:Y4ufad47v5o4ebpe
285
     * @todo This should go by $wgTranslateNumerals.
286
     * @return string
287
     */
288
    private function getLangForTranslatingNumerals(): string
289
    {
290
        return 'ar' === $this->getIntuition()->getLang() ? 'en': $this->getIntuition()->getLang();
291
    }
292
293
    /**
294
     * Determine the interface language, either from the current request or session.
295
     * @return string
296
     */
297
    private function getIntuitionLang(): string
298
    {
299
        $queryLang = $this->getRequest()->query->get('uselang');
300
        $sessionLang = $this->requestStack->getSession()->get('lang');
301
302
        if ('' !== $queryLang && null !== $queryLang) {
303
            return $queryLang;
304
        } elseif ('' !== $sessionLang && null !== $sessionLang) {
305
            return $sessionLang;
306
        }
307
308
        // English as default.
309
        return 'en';
310
    }
311
312
    /**
313
     * Shorthand to get the current request from the request stack.
314
     * @return Request|null Null in test suite.
315
     * There is no request stack in the tests.
316
     * @codeCoverageIgnore
317
     */
318
    private function getRequest(): ?Request
319
    {
320
        return $this->requestStack->getCurrentRequest();
321
    }
322
}
323