├── tools
├── .gitignore
├── psalm
│ └── composer.json
└── infection
│ └── composer.json
├── rector.php
├── src
├── LocaleProvider.php
└── Locale.php
├── CHANGELOG.md
├── LICENSE.md
├── composer.json
└── README.md
/tools/.gitignore:
--------------------------------------------------------------------------------
1 | /*/vendor
2 | /*/composer.lock
3 |
--------------------------------------------------------------------------------
/tools/psalm/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "require-dev": {
3 | "vimeo/psalm": "^5.26.1 || ^6.8.9"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/tools/infection/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "require-dev": {
3 | "infection/infection": "^0.26 || ^0.31.9"
4 | },
5 | "config": {
6 | "allow-plugins": {
7 | "infection/extension-installer": true
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/rector.php:
--------------------------------------------------------------------------------
1 | withPaths([
11 | __DIR__ . '/src',
12 | __DIR__ . '/tests',
13 | ])
14 | ->withPhpSets(php80: true)
15 | ->withRules([
16 | InlineConstructorDefaultToPropertyRector::class,
17 | ])
18 | ->withSkip([
19 | ClosureToArrowFunctionRector::class,
20 | ]);
21 |
--------------------------------------------------------------------------------
/src/LocaleProvider.php:
--------------------------------------------------------------------------------
1 | locale ?? $this->defaultLocale;
23 | }
24 |
25 | public function set(Locale $locale): void
26 | {
27 | $this->locale = $locale;
28 | }
29 |
30 | public function isDefaultLocale(): bool
31 | {
32 | return $this->locale === null || $this->locale->asString() === $this->defaultLocale->asString();
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Yii Internationalization Library Change Log
2 |
3 | ## 1.2.3 under development
4 |
5 | - no changes in this release.
6 |
7 | ## 1.2.2 November 29, 2025
8 |
9 | - Chg #75, #76: Change PHP constraint in `composer.json` to `8.0 - 8.5` (@vjik)
10 |
11 | ## 1.2.1 June 10, 2023
12 |
13 | - Bug #56: Fix `LocaleProvider::isDefaultLocale()` giving a wrong result if locale is set explicitly to the one matching default (@samdark)
14 |
15 | ## 1.2.0 June 04, 2023
16 |
17 | - New #55: Add `LocaleProvider` (@samdark)
18 | - Chg #55: Raise major PHP version to 8 (@samdark)
19 |
20 | ## 1.1.0 November 05, 2021
21 |
22 | - New #33: Add support for keywords `hours`, `colnumeric`, and `colcasefirst`. These
23 | keywords are part of the [ECMAScript 2022 Internationalization API Specification
24 | (ECMA-402 9th Edition)](https://tc39.es/ecma402/), and supporting them allows
25 | for better cross-communication between PHP and JavaScript layers.
26 | - `hours` defines an hour cycle for the locale (i.e. `h11`, `h12`, `h23`, `h24`).
27 | For more information see the [key/type definition for the Unicode Hour Cycle
28 | Identifier](https://www.unicode.org/reports/tr35/tr35-61/tr35.html#UnicodeHourCycleIdentifier).
29 | - `colnumeric` and `colcasefirst` are both collation settings defined as part
30 | of the [Unicode Locale Data Markup Language](https://www.unicode.org/reports/tr35/tr35-61/tr35-collation.html#Collation_Settings) (@ramsey)
31 |
32 | ## 1.0.0 December 25, 2020
33 |
34 | - Initial release.
35 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © 2008 by Yii Software ()
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions
6 | are met:
7 |
8 | * Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 | * Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in
12 | the documentation and/or other materials provided with the
13 | distribution.
14 | * Neither the name of Yii Software nor the names of its
15 | contributors may be used to endorse or promote products derived
16 | from this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 | POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yiisoft/i18n",
3 | "type": "library",
4 | "description": "Yii Internationalization Library",
5 | "keywords": [
6 | "i18n",
7 | "locale"
8 | ],
9 | "homepage": "https://www.yiiframework.com/",
10 | "license": "BSD-3-Clause",
11 | "support": {
12 | "issues": "https://github.com/yiisoft/i18n/issues?state=open",
13 | "source": "https://github.com/yiisoft/i18n",
14 | "forum": "https://www.yiiframework.com/forum/",
15 | "wiki": "https://www.yiiframework.com/wiki/",
16 | "irc": "ircs://irc.libera.chat:6697/yii",
17 | "chat": "https://t.me/yii3en"
18 | },
19 | "funding": [
20 | {
21 | "type": "opencollective",
22 | "url": "https://opencollective.com/yiisoft"
23 | },
24 | {
25 | "type": "github",
26 | "url": "https://github.com/sponsors/yiisoft"
27 | }
28 | ],
29 | "require": {
30 | "php": "8.0 - 8.5"
31 | },
32 | "require-dev": {
33 | "bamarni/composer-bin-plugin": "^1.8.3",
34 | "maglnet/composer-require-checker": "^4.4",
35 | "phpunit/phpunit": "^9.6.22",
36 | "rector/rector": "^2.0.10",
37 | "spatie/phpunit-watcher": "^1.23.6"
38 | },
39 | "autoload": {
40 | "psr-4": {
41 | "Yiisoft\\I18n\\": "src"
42 | }
43 | },
44 | "autoload-dev": {
45 | "psr-4": {
46 | "Yiisoft\\I18n\\Tests\\": "tests"
47 | }
48 | },
49 | "extra": {
50 | "bamarni-bin": {
51 | "bin-links": true,
52 | "target-directory": "tools",
53 | "forward-command": true
54 | }
55 | },
56 | "config": {
57 | "sort-packages": true,
58 | "allow-plugins": {
59 | "bamarni/composer-bin-plugin": true,
60 | "composer/package-versions-deprecated": true
61 | }
62 | },
63 | "scripts": {
64 | "test": "phpunit --testdox --no-interaction",
65 | "test-watch": "phpunit-watcher watch"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Yii Internationalization Library
6 |
7 |
8 |
9 | [](https://packagist.org/packages/yiisoft/i18n)
10 | [](https://packagist.org/packages/yiisoft/i18n)
11 | [](https://github.com/yiisoft/i18n/actions?query=workflow%3Abuild)
12 | [](https://codecov.io/gh/yiisoft/i18n)
13 | [](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/i18n/master)
14 | [](https://github.com/yiisoft/i18n/actions/workflows/static.yml?query=branch%3Amaster)
15 | [](https://shepherd.dev/github/yiisoft/i18n)
16 |
17 | The package provides common internationalization utilities:
18 |
19 | - `Locale` stores locale information created from [BCP 47](https://www.rfc-editor.org/info/bcp47) formatted string. It
20 | can parse locale string, modify locale parts, form locale string from parts, and derive fallback locale.
21 | - `LocaleProvider` is a stateful service that stores current locale.
22 |
23 | ## Requirements
24 |
25 | - PHP 8.0 - 8.5.
26 |
27 | ## Installation
28 |
29 | The package could be installed with [Composer](https://getcomposer.org):
30 |
31 | ```shell
32 | composer install yiisoft/i18n
33 | ```
34 |
35 | ## General usage
36 |
37 | Use `Locale` as follows:
38 |
39 | ```php
40 | $locale = new \Yiisoft\I18n\Locale('es-CL');
41 | echo $locale->language(); // es
42 | echo $locale->region(); // CL
43 |
44 | $locale = $locale->withLanguage('en');
45 | echo $locale->asString(); // en-CL
46 |
47 | echo $locale
48 | ->fallbackLocale()
49 | ->asString(); // en
50 | ```
51 |
52 | Use `LocaleProvider` as follows:
53 |
54 | ```php
55 | use \Yiisoft\I18n\LocaleProvider;
56 |
57 | final class MyService
58 | {
59 | public function __construct(
60 | private LocaleProvider $localeProvider
61 | ) {
62 | }
63 |
64 | public function doIt(): void
65 | {
66 | $locale = $this->localeProvider->get();
67 | if ($this->localeProvider->isDefaultLocale()) {
68 | // ...
69 | }
70 |
71 | // ...
72 | }
73 |
74 | }
75 | ```
76 |
77 | ## Documentation
78 |
79 | - [Internals](docs/internals.md)
80 |
81 | If you need help or have a question, the [Yii Forum](https://forum.yiiframework.com/c/yii-3-0/63) is a good place for that.
82 | You may also check out other [Yii Community Resources](https://www.yiiframework.com/community).
83 |
84 | ## License
85 |
86 | The Yii Internationalization Library is free software. It is released under the terms of the BSD License.
87 | Please see [`LICENSE`](./LICENSE.md) for more information.
88 |
89 | Maintained by [Yii Software](https://www.yiiframework.com/).
90 |
91 | ## Support the project
92 |
93 | [](https://opencollective.com/yiisoft)
94 |
95 | ## Follow updates
96 |
97 | [](https://www.yiiframework.com/)
98 | [](https://twitter.com/yiiframework)
99 | [](https://t.me/yii3en)
100 | [](https://www.facebook.com/groups/yiitalk)
101 | [](https://yiiframework.com/go/slack)
102 |
--------------------------------------------------------------------------------
/src/Locale.php:
--------------------------------------------------------------------------------
1 | language = strtolower($matches['language']);
122 | }
123 |
124 | if (!empty($matches['region'])) {
125 | $this->region = strtoupper($matches['region']);
126 | }
127 |
128 | if (!empty($matches['variant'])) {
129 | $this->variant = $matches['variant'];
130 | }
131 |
132 | if (!empty($matches['extendedLanguage'])) {
133 | $this->extendedLanguage = $matches['extendedLanguage'];
134 | }
135 |
136 | if (!empty($matches['extension'])) {
137 | $this->extension = $matches['extension'];
138 | }
139 |
140 | if (!empty($matches['script'])) {
141 | $this->script = ucfirst(strtolower($matches['script']));
142 | }
143 |
144 | if (!empty($matches['grandfathered'])) {
145 | $this->grandfathered = $matches['grandfathered'];
146 | }
147 |
148 | if (!empty($matches['private'])) {
149 | $this->private = preg_replace('~^x-~', '', $matches['private']);
150 | }
151 |
152 | if (!empty($matches['keywords'])) {
153 | foreach (explode(';', $matches['keywords']) as $pair) {
154 | [$key, $value] = explode('=', $pair);
155 |
156 | if ($key === 'calendar') {
157 | $this->calendar = $value;
158 | }
159 |
160 | if ($key === 'colcasefirst') {
161 | $this->colcasefirst = $value;
162 | }
163 |
164 | if ($key === 'collation') {
165 | $this->collation = $value;
166 | }
167 |
168 | if ($key === 'colnumeric') {
169 | $this->colnumeric = $value;
170 | }
171 |
172 | if ($key === 'currency') {
173 | $this->currency = $value;
174 | }
175 |
176 | if ($key === 'numbers') {
177 | $this->numbers = $value;
178 | }
179 |
180 | if ($key === 'hours') {
181 | $this->hours = $value;
182 | }
183 | }
184 | }
185 | }
186 |
187 | /**
188 | * @return string Four-letter ISO 15924 script code.
189 | *
190 | * @link https://www.unicode.org/iso15924/iso15924-codes.html
191 | */
192 | public function script(): ?string
193 | {
194 | return $this->script;
195 | }
196 |
197 | /**
198 | * @param string|null $script Four-letter ISO 15924 script code.
199 | *
200 | * @link https://www.unicode.org/iso15924/iso15924-codes.html
201 | */
202 | public function withScript(?string $script): self
203 | {
204 | $new = clone $this;
205 | $new->script = $script;
206 | return $new;
207 | }
208 |
209 | /**
210 | * @return string Variant of language conventions to use.
211 | */
212 | public function variant(): ?string
213 | {
214 | return $this->variant;
215 | }
216 |
217 | /**
218 | * @param string|null $variant Variant of language conventions to use.
219 | */
220 | public function withVariant(?string $variant): self
221 | {
222 | $new = clone $this;
223 | $new->variant = $variant;
224 | return $new;
225 | }
226 |
227 | /**
228 | * @return string|null Two-letter ISO-639-2 language code.
229 | *
230 | * @link https://www.loc.gov/standards/iso639-2/
231 | */
232 | public function language(): ?string
233 | {
234 | return $this->language;
235 | }
236 |
237 | /**
238 | * @param string|null $language Two-letter ISO-639-2 language code.
239 | *
240 | * @link https://www.loc.gov/standards/iso639-2/
241 | */
242 | public function withLanguage(?string $language): self
243 | {
244 | $new = clone $this;
245 | $new->language = $language;
246 | return $new;
247 | }
248 |
249 | /**
250 | * @return string|null ICU calendar.
251 | */
252 | public function calendar(): ?string
253 | {
254 | return $this->calendar;
255 | }
256 |
257 | /**
258 | * @param string|null $calendar ICU calendar.
259 | */
260 | public function withCalendar(?string $calendar): self
261 | {
262 | $new = clone $this;
263 | $new->calendar = $calendar;
264 | return $new;
265 | }
266 |
267 | /**
268 | * @return string|null ICU case-first collation.
269 | *
270 | * @link https://unicode-org.github.io/icu/userguide/collation/customization/#casefirst
271 | * @link https://www.unicode.org/reports/tr35/tr35-61/tr35-collation.html#Collation_Settings
272 | */
273 | public function colcasefirst(): ?string
274 | {
275 | return $this->colcasefirst;
276 | }
277 |
278 | /**
279 | * @param string|null $colcasefirst ICU case-first collation.
280 | *
281 | * @link https://unicode-org.github.io/icu/userguide/collation/customization/#casefirst
282 | * @link https://www.unicode.org/reports/tr35/tr35-61/tr35-collation.html#Collation_Settings
283 | */
284 | public function withColcasefirst(?string $colcasefirst): self
285 | {
286 | $new = clone $this;
287 | $new->colcasefirst = $colcasefirst;
288 | return $new;
289 | }
290 |
291 | /**
292 | * @return string|null ICU collation.
293 | */
294 | public function collation(): ?string
295 | {
296 | return $this->collation;
297 | }
298 |
299 | /**
300 | * @param string|null $collation ICU collation.
301 | */
302 | public function withCollation(?string $collation): self
303 | {
304 | $new = clone $this;
305 | $new->collation = $collation;
306 | return $new;
307 | }
308 |
309 | /**
310 | * @return string|null ICU numeric collation.
311 | *
312 | * @link https://unicode-org.github.io/icu/userguide/collation/customization/#numericordering
313 | * @link https://www.unicode.org/reports/tr35/tr35-61/tr35-collation.html#Collation_Settings
314 | */
315 | public function colnumeric(): ?string
316 | {
317 | return $this->colnumeric;
318 | }
319 |
320 | /**
321 | * @param string|null $colnumeric ICU numeric collation.
322 | *
323 | * @link https://unicode-org.github.io/icu/userguide/collation/customization/#numericordering
324 | * @link https://www.unicode.org/reports/tr35/tr35-61/tr35-collation.html#Collation_Settings
325 | */
326 | public function withColnumeric(?string $colnumeric): self
327 | {
328 | $new = clone $this;
329 | $new->colnumeric = $colnumeric;
330 | return $new;
331 | }
332 |
333 | /**
334 | * @return string|null ICU numbers.
335 | */
336 | public function numbers(): ?string
337 | {
338 | return $this->numbers;
339 | }
340 |
341 | /**
342 | * @param string|null $numbers ICU numbers.
343 | */
344 | public function withNumbers(?string $numbers): self
345 | {
346 | $new = clone $this;
347 | $new->numbers = $numbers;
348 | return $new;
349 | }
350 |
351 | /**
352 | * @return string|null Unicode hour cycle identifier.
353 | *
354 | * @link https://www.unicode.org/reports/tr35/#UnicodeHourCycleIdentifier
355 | */
356 | public function hours(): ?string
357 | {
358 | return $this->hours;
359 | }
360 |
361 | /**
362 | * @param string|null $hours Unicode hour cycle identifier.
363 | *
364 | * @link https://www.unicode.org/reports/tr35/#UnicodeHourCycleIdentifier
365 | */
366 | public function withHours(?string $hours): self
367 | {
368 | $new = clone $this;
369 | $new->hours = $hours;
370 | return $new;
371 | }
372 |
373 | /**
374 | * @return string Two-letter ISO 3166-1 country code.
375 | *
376 | * @link https://www.iso.org/iso-3166-country-codes.html
377 | */
378 | public function region(): ?string
379 | {
380 | return $this->region;
381 | }
382 |
383 | /**
384 | * @param string|null $region Two-letter ISO 3166-1 country code.
385 | *
386 | * @link https://www.iso.org/iso-3166-country-codes.html
387 | */
388 | public function withRegion(?string $region): self
389 | {
390 | $new = clone $this;
391 | $new->region = $region;
392 | return $new;
393 | }
394 |
395 | /**
396 | * @return string ICU currency.
397 | */
398 | public function currency(): ?string
399 | {
400 | return $this->currency;
401 | }
402 |
403 | /**
404 | * @param string|null $currency ICU currency.
405 | */
406 | public function withCurrency(?string $currency): self
407 | {
408 | $new = clone $this;
409 | $new->currency = $currency;
410 |
411 | return $new;
412 | }
413 |
414 | /**
415 | * @return string|null Extended language subtags.
416 | */
417 | public function extendedLanguage(): ?string
418 | {
419 | return $this->extendedLanguage;
420 | }
421 |
422 | /**
423 | * @param string|null $extendedLanguage Extended language subtags.
424 | */
425 | public function withExtendedLanguage(?string $extendedLanguage): self
426 | {
427 | $new = clone $this;
428 | $new->extendedLanguage = $extendedLanguage;
429 |
430 | return $new;
431 | }
432 |
433 | public function private(): ?string
434 | {
435 | return $this->private;
436 | }
437 |
438 | public function withPrivate(?string $private): self
439 | {
440 | $new = clone $this;
441 | $new->private = $private;
442 |
443 | return $new;
444 | }
445 |
446 | /**
447 | * @link https://www.rfc-editor.org/info/bcp47
448 | *
449 | * @return string Regular expression for parsing BCP 47.
450 | * @psalm-return non-empty-string
451 | */
452 | private static function getBCP47Regex(): string
453 | {
454 | $regular = '(?:art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)';
455 | $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)';
456 | $grandfathered = '(?' . $irregular . '|' . $regular . ')';
457 | $private = '(?x(?:-[A-Za-z0-9]{1,8})+)';
458 | $singleton = '[0-9A-WY-Za-wy-z]';
459 | $extension = '(?' . $singleton . '(?:-[A-Za-z0-9]{2,8})+)';
460 | $variant = '(?[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3})';
461 | $region = '(?[A-Za-z]{2}|[0-9]{3})';
462 | $script = '(?