Completed
Push — master ( 3f9378...6e7ea1 )
by Alexander
10:30
created

src/Locale.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
namespace Yiisoft\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 Yiisoft\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);
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