Passed
Pull Request — master (#376)
by MusikAnimal
10:04 queued 21s
created

I18nHelper::msgExists()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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