├── 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 | Yii 4 | 5 |

Yii Internationalization Library

6 |
7 |

8 | 9 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/i18n/v/stable.png)](https://packagist.org/packages/yiisoft/i18n) 10 | [![Total Downloads](https://poser.pugx.org/yiisoft/i18n/downloads.png)](https://packagist.org/packages/yiisoft/i18n) 11 | [![Build status](https://github.com/yiisoft/i18n/workflows/build/badge.svg)](https://github.com/yiisoft/i18n/actions?query=workflow%3Abuild) 12 | [![Code Coverage](https://codecov.io/gh/yiisoft/i18n/branch/master/graph/badge.svg)](https://codecov.io/gh/yiisoft/i18n) 13 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2Fi18n%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/i18n/master) 14 | [![Static analysis](https://github.com/yiisoft/i18n/actions/workflows/static.yml/badge.svg?branch=master)](https://github.com/yiisoft/i18n/actions/workflows/static.yml?query=branch%3Amaster) 15 | [![type-coverage](https://shepherd.dev/github/yiisoft/i18n/coverage.svg)](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 | [![Open Collective](https://img.shields.io/badge/Open%20Collective-sponsor-7eadf1?logo=open%20collective&logoColor=7eadf1&labelColor=555555)](https://opencollective.com/yiisoft) 94 | 95 | ## Follow updates 96 | 97 | [![Official website](https://img.shields.io/badge/Powered_by-Yii_Framework-green.svg?style=flat)](https://www.yiiframework.com/) 98 | [![Twitter](https://img.shields.io/badge/twitter-follow-1DA1F2?logo=twitter&logoColor=1DA1F2&labelColor=555555?style=flat)](https://twitter.com/yiiframework) 99 | [![Telegram](https://img.shields.io/badge/telegram-join-1DA1F2?style=flat&logo=telegram)](https://t.me/yii3en) 100 | [![Facebook](https://img.shields.io/badge/facebook-join-1DA1F2?style=flat&logo=facebook&logoColor=ffffff)](https://www.facebook.com/groups/yiitalk) 101 | [![Slack](https://img.shields.io/badge/slack-join-1DA1F2?style=flat&logo=slack)](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 = '(?