Passed
Push — master ( 26210f...4d7d71 )
by Alexander
02:12
created

Locale::withScript()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\I18n;
6
7
use InvalidArgumentException;
8
9
/**
10
 * Locale stores locale information created from BCP 47 formatted string.
11
 *
12
 * @see https://tools.ietf.org/html/bcp47
13
 */
14
final class Locale
15
{
16
    /**
17
     * @var string|null Two-letter ISO-639-2 language code.
18
     *
19
     * @see http://www.loc.gov/standards/iso639-2/
20
     */
21
    private ?string $language = null;
22
23
    /**
24
     * @var string|null Extended language subtags.
25
     */
26
    private ?string $extendedLanguage = null;
27
28
    /**
29
     * @var string|null
30
     */
31
    private ?string $extension = null;
32
33
    /**
34
     * @var string|null Four-letter ISO 15924 script code.
35
     *
36
     * @see http://www.unicode.org/iso15924/iso15924-codes.html
37
     */
38
    private ?string $script = null;
39
40
    /**
41
     * @var string|null Two-letter ISO 3166-1 country code.
42
     *
43
     * @see https://www.iso.org/iso-3166-country-codes.html
44
     */
45
    private ?string $region = null;
46
47
    /**
48
     * @var string|null variant of language conventions to use.
49
     */
50
    private ?string $variant = null;
51
52
    /**
53
     * @var string|null ICU currency.
54
     */
55
    private ?string $currency = null;
56
57
    /**
58
     * @var string|null ICU calendar.
59
     */
60
    private ?string $calendar = null;
61
62
    /**
63
     * @var string|null ICU collation.
64
     */
65
    private ?string $collation = null;
66
67
    /**
68
     * @var string|null ICU numbers.
69
     */
70
    private ?string $numbers = null;
71
72
    /**
73
     * @var string|null
74
     */
75
    private ?string $grandfathered = null;
76
77
    /**
78
     * @var string|null
79
     */
80
    private ?string $private = null;
81
82
    /**
83
     * Locale constructor.
84
     *
85
     * @param string $localeString BCP 47 formatted locale string.
86
     *
87
     * @see https://tools.ietf.org/html/bcp47
88
     *
89
     * @throws InvalidArgumentException
90
     */
91 28
    public function __construct(string $localeString)
92
    {
93 28
        if (!preg_match(self::getBCP47Regex(), $localeString, $matches)) {
94 1
            throw new InvalidArgumentException($localeString . ' is not valid BCP 47 formatted locale string.');
95
        }
96
97 27
        if (!empty($matches['language'])) {
98 26
            $this->language = strtolower($matches['language']);
99
        }
100
101 27
        if (!empty($matches['region'])) {
102 16
            $this->region = strtoupper($matches['region']);
103
        }
104
105 27
        if (!empty($matches['variant'])) {
106 5
            $this->variant = $matches['variant'];
107
        }
108
109 27
        if (!empty($matches['extendedLanguage'])) {
110 2
            $this->extendedLanguage = $matches['extendedLanguage'];
111
        }
112
113 27
        if (!empty($matches['extension'])) {
114 1
            $this->extension = $matches['extension'];
115
        }
116
117 27
        if (!empty($matches['script'])) {
118 5
            $this->script = ucfirst(strtolower($matches['script']));
119
        }
120
121 27
        if (!empty($matches['grandfathered'])) {
122 1
            $this->grandfathered = $matches['grandfathered'];
123
        }
124
125 27
        if (!empty($matches['private'])) {
126 5
            $this->private = preg_replace('~^x-~', '', $matches['private']);
127
        }
128
129 27
        if (!empty($matches['keywords'])) {
130 5
            foreach (explode(';', $matches['keywords']) as $pair) {
131 5
                [$key, $value] = explode('=', $pair);
132
133 5
                if ($key === 'calendar') {
134 2
                    $this->calendar = $value;
135
                }
136
137 5
                if ($key === 'collation') {
138 2
                    $this->collation = $value;
139
                }
140
141 5
                if ($key === 'currency') {
142 2
                    $this->currency = $value;
143
                }
144
145 5
                if ($key === 'numbers') {
146 2
                    $this->numbers = $value;
147
                }
148
            }
149
        }
150 27
    }
151
152
    /**
153
     * @return string Four-letter ISO 15924 script code.
154
     *
155
     * @see http://www.unicode.org/iso15924/iso15924-codes.html
156
     */
157 5
    public function script(): ?string
158
    {
159 5
        return $this->script;
160
    }
161
162
    /**
163
     * @param string|null $script Four-letter ISO 15924 script code.
164
     *
165
     * @see http://www.unicode.org/iso15924/iso15924-codes.html
166
     *
167
     * @return self
168
     */
169 2
    public function withScript(?string $script): self
170
    {
171 2
        $new = clone $this;
172 2
        $new->script = $script;
173 2
        return $new;
174
    }
175
176
    /**
177
     * @return string Variant of language conventions to use.
178
     */
179 4
    public function variant(): ?string
180
    {
181 4
        return $this->variant;
182
    }
183
184
    /**
185
     * @param string|null $variant Variant of language conventions to use.
186
     *
187
     * @return self
188
     */
189 2
    public function withVariant(?string $variant): self
190
    {
191 2
        $new = clone $this;
192 2
        $new->variant = $variant;
193 2
        return $new;
194
    }
195
196
    /**
197
     * @return string|null Two-letter ISO-639-2 language code.
198
     *
199
     * @see http://www.loc.gov/standards/iso639-2/
200
     */
201 3
    public function language(): ?string
202
    {
203 3
        return $this->language;
204
    }
205
206
    /**
207
     * @param string|null $language Two-letter ISO-639-2 language code.
208
     *
209
     * @see http://www.loc.gov/standards/iso639-2/
210
     *
211
     * @return self
212
     */
213 1
    public function withLanguage(?string $language): self
214
    {
215 1
        $new = clone $this;
216 1
        $new->language = $language;
217 1
        return $new;
218
    }
219
220
    /**
221
     * @return string|null ICU calendar.
222
     */
223 2
    public function calendar(): ?string
224
    {
225 2
        return $this->calendar;
226
    }
227
228
    /**
229
     * @param string|null $calendar ICU calendar.
230
     *
231
     * @return self
232
     */
233 3
    public function withCalendar(?string $calendar): self
234
    {
235 3
        $new = clone $this;
236 3
        $new->calendar = $calendar;
237 3
        return $new;
238
    }
239
240
    /**
241
     * @return string|null ICU collation.
242
     */
243 2
    public function collation(): ?string
244
    {
245 2
        return $this->collation;
246
    }
247
248
    /**
249
     * @param string|null $collation ICU collation.
250
     *
251
     * @return self
252
     */
253 3
    public function withCollation(?string $collation): self
254
    {
255 3
        $new = clone $this;
256 3
        $new->collation = $collation;
257 3
        return $new;
258
    }
259
260
    /**
261
     * @return string|null ICU numbers.
262
     */
263 2
    public function numbers(): ?string
264
    {
265 2
        return $this->numbers;
266
    }
267
268
    /**
269
     * @param string|null $numbers ICU numbers.
270
     *
271
     * @return self
272
     */
273 3
    public function withNumbers(?string $numbers): self
274
    {
275 3
        $new = clone $this;
276 3
        $new->numbers = $numbers;
277 3
        return $new;
278
    }
279
280
    /**
281
     * @return string Two-letter ISO 3166-1 country code.
282
     *
283
     * @see https://www.iso.org/iso-3166-country-codes.html
284
     */
285 5
    public function region(): ?string
286
    {
287 5
        return $this->region;
288
    }
289
290
    /**
291
     * @param string|null $region Two-letter ISO 3166-1 country code.
292
     *
293
     * @see https://www.iso.org/iso-3166-country-codes.html
294
     *
295
     * @return self
296
     */
297 2
    public function withRegion(?string $region): self
298
    {
299 2
        $new = clone $this;
300 2
        $new->region = $region;
301 2
        return $new;
302
    }
303
304
    /**
305
     * @return string ICU currency.
306
     */
307 2
    public function currency(): ?string
308
    {
309 2
        return $this->currency;
310
    }
311
312
    /**
313
     * @param string|null $currency ICU currency.
314
     *
315
     * @return self
316
     */
317 3
    public function withCurrency(?string $currency): self
318
    {
319 3
        $new = clone $this;
320 3
        $new->currency = $currency;
321
322 3
        return $new;
323
    }
324
325
    /**
326
     * @return string|null Extended language subtags.
327
     */
328 2
    public function extendedLanguage(): ?string
329
    {
330 2
        return $this->extendedLanguage;
331
    }
332
333
    /**
334
     * @param string|null $extendedLanguage Extended language subtags.
335
     *
336
     * @return self
337
     */
338 3
    public function withExtendedLanguage(?string $extendedLanguage): self
339
    {
340 3
        $new = clone $this;
341 3
        $new->extendedLanguage = $extendedLanguage;
342
343 3
        return $new;
344
    }
345
346
    /**
347
     * @return string|null
348
     */
349 3
    public function private(): ?string
350
    {
351 3
        return $this->private;
352
    }
353
354
    /**
355
     * @param string|null $private
356
     *
357
     * @return self
358
     */
359 3
    public function withPrivate(?string $private): self
360
    {
361 3
        $new = clone $this;
362 3
        $new->private = $private;
363
364 3
        return $new;
365
    }
366
367
    /**
368
     * @return string Regular expression for parsing BCP 47.
369
     *
370
     * @see https://tools.ietf.org/html/bcp47
371
     */
372 28
    private static function getBCP47Regex(): string
373
    {
374 28
        $regular = '(?:art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)';
375 28
        $irregular = '(?:en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)';
376 28
        $grandfathered = '(?<grandfathered>' . $irregular . '|' . $regular . ')';
377 28
        $private = '(?<private>x(?:-[A-Za-z0-9]{1,8})+)';
378 28
        $singleton = '[0-9A-WY-Za-wy-z]';
379 28
        $extension = '(?<extension>' . $singleton . '(?:-[A-Za-z0-9]{2,8})+)';
380 28
        $variant = '(?<variant>[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3})';
381 28
        $region = '(?<region>[A-Za-z]{2}|[0-9]{3})';
382 28
        $script = '(?<script>[A-Za-z]{4})';
383 28
        $extendedLanguage = '(?<extendedLanguage>[A-Za-z]{3}(?:-[A-Za-z]{3}){0,2})';
384 28
        $language = '(?<language>[A-Za-z]{4,8})|(?<language>[A-Za-z]{2,3})(?:-' . $extendedLanguage . ')?';
385 28
        $icuKeywords = '(?:@(?<keywords>.*?))?';
386 28
        $languageTag = '(?:' . $language . '(?:-' . $script . ')?' . '(?:-' . $region . ')?' . '(?:-' . $variant . ')*' . '(?:-' . $extension . ')*' . '(?:-' . $private . ')?' . ')';
387 28
        return '/^(?J:' . $grandfathered . '|' . $languageTag . '|' . $private . ')' . $icuKeywords . '$/';
388
    }
389
390 1
    public function __toString(): string
391
    {
392 1
        return $this->asString();
393
    }
394
395
    /**
396
     * @return string Locale string.
397
     */
398 3
    public function asString(): string
399
    {
400 3
        if ($this->grandfathered !== null) {
401 1
            return $this->grandfathered;
402
        }
403
404 3
        $result = [];
405 3
        if ($this->language !== null) {
406 3
            $result[] = $this->language;
407
408 3
            if ($this->extendedLanguage !== null) {
409 1
                $result[] = $this->extendedLanguage;
410
            }
411
412 3
            if ($this->script !== null) {
413 1
                $result[] = $this->script;
414
            }
415
416 3
            if ($this->region !== null) {
417 2
                $result[] = $this->region;
418
            }
419
420 3
            if ($this->variant !== null) {
421 1
                $result[] = $this->variant;
422
            }
423
424 3
            if ($this->extension !== null) {
425 1
                $result[] = $this->extension;
426
            }
427
        }
428
429 3
        if ($this->private !== null) {
430 1
            $result[] = 'x-' . $this->private;
431
        }
432
433 3
        $keywords = [];
434 3
        if ($this->currency !== null) {
435 1
            $keywords[] = 'currency=' . $this->currency;
436
        }
437 3
        if ($this->collation !== null) {
438 1
            $keywords[] = 'collation=' . $this->collation;
439
        }
440 3
        if ($this->calendar !== null) {
441 1
            $keywords[] = 'calendar=' . $this->calendar;
442
        }
443 3
        if ($this->numbers !== null) {
444 1
            $keywords[] = 'numbers=' . $this->numbers;
445
        }
446
447 3
        $string = implode('-', $result);
448
449 3
        if ($keywords !== []) {
450 1
            $string .= '@' . implode(';', $keywords);
451
        }
452
453 3
        return $string;
454
    }
455
456
    /**
457
     * Returns fallback locale.
458
     *
459
     * @return self Fallback locale.
460
     */
461 2
    public function fallbackLocale(): self
462
    {
463
        $fallback = $this
464 2
            ->withCalendar(null)
465 2
            ->withCollation(null)
466 2
            ->withCurrency(null)
467 2
            ->withExtendedLanguage(null)
468 2
            ->withNumbers(null)
469 2
            ->withPrivate(null);
470
471 2
        if ($fallback->variant() !== null) {
472 1
            return $fallback->withVariant(null);
473
        }
474
475 2
        if ($fallback->region() !== null) {
476 1
            return $fallback->withRegion(null);
477
        }
478
479 2
        if ($fallback->script() !== null) {
480 1
            return $fallback->withScript(null);
481
        }
482
483 1
        return $fallback;
484
    }
485
}
486