Passed
Push — master ( 714bef...604738 )
by Alexander
01:27
created

Locale::fallbackLocale()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4.1574

Importance

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