Completed
Push — master ( ab75e4...3f9378 )
by Alexander
12:59
created

Locale::extendedLanguage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
namespace Yii\I18n;
3
4
/**
5
 * Locale stores locale information created from BCP 47 formatted string
6
 * https://tools.ietf.org/html/bcp47
7
 */
8
final class Locale
9
{
10
    /**
11
     * @var string|null Two-letter ISO-639-2 language code
12
     * @see http://www.loc.gov/standards/iso639-2/
13
     */
14
    private $language;
15
16
    /**
17
     * @var string|null extended language subtags
18
     */
19
    private $extendedLanguage;
20
21
    /**
22
     * @var string|null
23
     */
24
    private $extension;
25
26
    /**
27
     * @var string|null Four-letter ISO 15924 script code
28
     * @see http://www.unicode.org/iso15924/iso15924-codes.html
29
     */
30
    private $script;
31
32
    /**
33
     * @var string|null Two-letter ISO 3166-1 country code
34
     * @see https://www.iso.org/iso-3166-country-codes.html
35
     */
36
    private $region;
37
38
    /**
39
     * @var string|null variant of language conventions to use
40
     */
41
    private $variant;
42
43
    /**
44
     * @var string|null ICU currency
45
     */
46
    private $currency;
47
48
    /**
49
     * @var string|null ICU calendar
50
     */
51
    private $calendar;
52
53
    /**
54
     * @var string ICU collation
55
     */
56
    private $collation;
57
58
    /**
59
     * @var string|null ICU numbers
60
     */
61
    private $numbers;
62
63
    /**
64
     * @var string|null
65
     */
66
    private $grandfathered;
67
68
    /**
69
     * @var string|null
70
     */
71
    private $private;
72
73
    /**
74
     * Locale constructor.
75
     * @param string $localeString BCP 47 formatted locale string
76
     * @see https://tools.ietf.org/html/bcp47
77
     * @throws \InvalidArgumentException
78
     */
79
    public function __construct(string $localeString)
80
    {
81
        if (!preg_match(static::getBCP47Regex(), $localeString, $matches)) {
0 ignored issues
show
Comprehensibility introduced by
Since Yii\I18n\Locale is declared final, using late-static binding will have no effect. You might want to replace static with self instead.

Late static binding only has effect in subclasses. A final class cannot be extended anymore so late static binding cannot occurr. Consider replacing static:: with self::.

To learn more about late static binding, please refer to the PHP core documentation.

Loading history...
82
            throw new \InvalidArgumentException($localeString . ' is not valid BCP 47 formatted locale string');
83
        }
84
85
        if (!empty($matches['language'])) {
86
            $this->language = strtolower($matches['language']);
87
        }
88
89
        if (!empty($matches['region'])) {
90
            $this->region = strtoupper($matches['region']);
91
        }
92
93
        if (!empty($matches['variant'])) {
94
            $this->variant = $matches['variant'];
95
        }
96
97
        if (!empty($matches['extendedLanguage'])) {
98
            $this->extendedLanguage = $matches['extendedLanguage'];
99
        }
100
101
        if (!empty($matches['extension'])) {
102
            $this->extension = $matches['extension'];
103
        }
104
105
        if (!empty($matches['script'])) {
106
            $this->script = ucfirst(strtolower($matches['script']));
107
        }
108
109
        if (!empty($matches['grandfathered'])) {
110
            $this->grandfathered = $matches['grandfathered'];
111
        }
112
113
        if (!empty($matches['private'])) {
114
            $this->private = preg_replace('~^x-~', '', $matches['private']);
115
        }
116
117
        if (!empty($matches['keywords'])) {
118
            foreach (explode(';', $matches['keywords']) as $pair) {
119
                [$key, $value] = explode('=', $pair);
0 ignored issues
show
Bug introduced by
The variable $key does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $value does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
120
121
                if ($key === 'calendar') {
122
                    $this->calendar = $value;
123
                }
124
125
                if ($key === 'collation') {
126
                    $this->collation = $value;
127
                }
128
129
                if ($key === 'currency') {
130
                    $this->currency = $value;
131
                }
132
133
                if ($key === 'numbers') {
134
                    $this->numbers = $value;
135
                }
136
            }
137
        }
138
    }
139
140
    /**
141
     * @return string Four-letter ISO 15924 script code
142
     * @see http://www.unicode.org/iso15924/iso15924-codes.html
143
     */
144
    public function script(): ?string
145
    {
146
        return $this->script;
147
    }
148
149
    /**
150
     * @param null|string $script Four-letter ISO 15924 script code
151
     * @see http://www.unicode.org/iso15924/iso15924-codes.html
152
     * @return self
153
     */
154
    public function withScript(?string $script): self
155
    {
156
        $new = clone $this;
157
        $new->script = $script;
158
        return $new;
159
    }
160
161
162
    /**
163
     * @return string variant of language conventions to use
164
     */
165
    public function variant(): ?string
166
    {
167
        return $this->variant;
168
    }
169
170
    /**
171
     * @param null|string $variant variant of language conventions to use
172
     * @return self
173
     */
174
    public function withVariant(?string $variant): self
175
    {
176
        $new = clone $this;
177
        $new->variant = $variant;
178
        return $new;
179
    }
180
181
    /**
182
     * @return string|null Two-letter ISO-639-2 language code
183
     * @see http://www.loc.gov/standards/iso639-2/
184
     */
185
    public function language(): string
186
    {
187
        return $this->language;
188
    }
189
190
    /**
191
     * @param null|string $language Two-letter ISO-639-2 language code
192
     * @see http://www.loc.gov/standards/iso639-2/
193
     * @return self
194
     */
195
    public function withLanguage(?string $language): self
196
    {
197
        $new = clone $this;
198
        $new->language = $language;
199
        return $new;
200
    }
201
202
    /**
203
     * @return null|string ICU calendar
204
     */
205
    public function calendar(): ?string
206
    {
207
        return $this->calendar;
208
    }
209
210
    /**
211
     * @param null|string $calendar ICU calendar
212
     * @return self
213
     */
214
    public function withCalendar(?string $calendar): self
215
    {
216
        $new = clone $this;
217
        $new->calendar = $calendar;
218
        return $new;
219
    }
220
221
222
    /**
223
     * @return null|string ICU collation
224
     */
225
    public function collation(): ?string
226
    {
227
        return $this->collation;
228
    }
229
230
    /**
231
     * @param null|string $collation ICU collation
232
     * @return self
233
     */
234
    public function withCollation(?string $collation): self
235
    {
236
        $new = clone $this;
237
        $new->collation = $collation;
238
        return $new;
239
    }
240
241
    /**
242
     * @return null|string ICU numbers
243
     */
244
    public function numbers(): ?string
245
    {
246
        return $this->numbers;
247
    }
248
249
    /**
250
     * @param null|string $numbers ICU numbers
251
     * @return self
252
     */
253
    public function withNumbers(?string $numbers): self
254
    {
255
        $new = clone $this;
256
        $new->numbers = $numbers;
257
        return $new;
258
    }
259
260
    /**
261
     * @return string Two-letter ISO 3166-1 country code
262
     * @see https://www.iso.org/iso-3166-country-codes.html
263
     */
264
    public function region(): ?string
265
    {
266
        return $this->region;
267
    }
268
269
    /**
270
     * @param null|string $region Two-letter ISO 3166-1 country code
271
     * @see https://www.iso.org/iso-3166-country-codes.html
272
     * @return self
273
     */
274
    public function withRegion(?string $region): self
275
    {
276
        $new = clone $this;
277
        $new->region = $region;
278
        return $new;
279
    }
280
281
    /**
282
     * @return string ICU currency
283
     */
284
    public function currency(): ?string
285
    {
286
        return $this->currency;
287
    }
288
289
    /**
290
     * @param null|string $currency ICU currency
291
     * @return self
292
     */
293
    public function withCurrency(?string $currency): self
294
    {
295
        $new = clone $this;
296
        $new->currency = $currency;
297
298
        return $new;
299
    }
300
301
    /**
302
     * @return null|string extended language subtags
303
     */
304
    public function extendedLanguage(): ?string
305
    {
306
        return $this->extendedLanguage;
307
    }
308
309
    /**
310
     * @param null|string $extendedLanguage extended language subtags
311
     * @return self
312
     */
313
    public function withExtendedLanguage(?string $extendedLanguage): self
314
    {
315
        $new = clone $this;
316
        $new->extendedLanguage = $extendedLanguage;
317
318
        return $new;
319
    }
320
321
322
    /**
323
     * @return null|string
324
     */
325
    public function private(): ?string
326
    {
327
        return $this->private;
328
    }
329
330
    /**
331
     * @param null|string $private
332
     * @return self
333
     */
334
    public function withPrivate(?string $private): self
335
    {
336
        $new = clone $this;
337
        $new->private = $private;
338
339
        return $new;
340
    }
341
342
    /**
343
     * @return string regular expression for parsing BCP 47
344
     * @see https://tools.ietf.org/html/bcp47
345
     */
346
    private static function getBCP47Regex(): string
347
    {
348
        $regular = '(?:art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)';
349
        $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)';
350
        $grandfathered = '(?<grandfathered>' . $irregular . '|' . $regular . ')';
351
        $private = '(?<private>x(?:-[A-Za-z0-9]{1,8})+)';
352
        $singleton = '[0-9A-WY-Za-wy-z]';
353
        $extension = '(?<extension>' . $singleton . '(?:-[A-Za-z0-9]{2,8})+)';
354
        $variant = '(?<variant>[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3})';
355
        $region = '(?<region>[A-Za-z]{2}|[0-9]{3})';
356
        $script = '(?<script>[A-Za-z]{4})';
357
        $extendedLanguage = '(?<extendedLanguage>[A-Za-z]{3}(?:-[A-Za-z]{3}){0,2})';
358
        $language = '(?<language>[A-Za-z]{4,8})|(?<language>[A-Za-z]{2,3})(?:-' . $extendedLanguage . ')?';
359
        $icuKeywords = '(?:@(?<keywords>.*?))?';
360
        $languageTag = '(?:' . $language . '(?:-' . $script . ')?' . '(?:-' . $region . ')?' . '(?:-' . $variant . ')*' . '(?:-' . $extension . ')*' . '(?:-' . $private . ')?' . ')';
361
        return '/^(?J:' . $grandfathered . '|' . $languageTag . '|' . $private . ')' . $icuKeywords . '$/';
362
    }
363
364
    public function __toString(): string
365
    {
366
        return $this->asString();
367
    }
368
369
    /**
370
     * @return string
371
     */
372
    public function asString(): string
373
    {
374
        if ($this->grandfathered !== null) {
375
            return $this->grandfathered;
376
        }
377
378
        $result = [];
379
        if ($this->language !== null) {
380
            $result[] = $this->language;
381
382
            if ($this->extendedLanguage !== null) {
383
                $result[] = $this->extendedLanguage;
384
            }
385
386
            if ($this->script !== null) {
387
                $result[] = $this->script;
388
            }
389
390
            if ($this->region !== null) {
391
                $result[] = $this->region;
392
            }
393
394
            if ($this->variant !== null) {
395
                $result[] = $this->variant;
396
            }
397
398
            if ($this->extension !== null) {
399
                $result[] = $this->extension;
400
            }
401
        }
402
403
        if ($this->private !== null) {
404
            $result[] = 'x-' . $this->private;
405
        }
406
407
        $keywords = [];
408
        if ($this->currency !== null) {
409
            $keywords[] = 'currency=' . $this->currency;
410
        }
411
        if ($this->collation !== null) {
412
            $keywords[] = 'collation=' . $this->collation;
413
        }
414
        if ($this->calendar !== null) {
415
            $keywords[] = 'calendar=' . $this->calendar;
416
        }
417
        if ($this->numbers !== null) {
418
            $keywords[] = 'numbers=' . $this->numbers;
419
        }
420
421
        $string = implode('-', $result);
422
423
        if ($keywords !== []) {
424
            $string .= '@' . implode(';', $keywords);
425
        }
426
427
        return $string;
428
    }
429
430
    /**
431
     * Returns fallback locale
432
     *
433
     * @return self fallback locale
434
     */
435
    public function fallbackLocale(): self
436
    {
437
        $fallback = $this
438
            ->withCalendar(null)
439
            ->withCollation(null)
440
            ->withCurrency(null)
441
            ->withExtendedLanguage(null)
442
            ->withNumbers(null)
443
            ->withPrivate(null);
444
445
        if ($fallback->variant() !== null) {
446
            return $fallback->withVariant(null);
447
        }
448
449
        if ($fallback->region() !== null) {
450
            return $fallback->withRegion(null);
451
        }
452
453
        if ($fallback->script() !== null) {
454
            return $fallback->withScript(null);
455
        }
456
457
        return $fallback;
458
    }
459
}
460