├── .phpstorm.meta.php ├── LICENSE ├── README.md ├── composer.json └── src ├── Debug ├── LanguageCollection.php ├── LanguageCollector.php └── icons │ └── language.svg ├── FallbackLevel.php └── Language.php /.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace PHPSTORM_META; 11 | 12 | registerArgumentsSet( 13 | 'date_styles', 14 | 'full', 15 | 'long', 16 | 'medium', 17 | 'short', 18 | ); 19 | registerArgumentsSet( 20 | 'currencies', 21 | 'AED', 22 | 'AFN', 23 | 'ALL', 24 | 'AMD', 25 | 'ANG', 26 | 'AOA', 27 | 'ARS', 28 | 'AUD', 29 | 'AWG', 30 | 'AZN', 31 | 'BAM', 32 | 'BBD', 33 | 'BDT', 34 | 'BGN', 35 | 'BHD', 36 | 'BIF', 37 | 'BMD', 38 | 'BND', 39 | 'BOB', 40 | 'BOV', 41 | 'BRL', 42 | 'BSD', 43 | 'BTN', 44 | 'BWP', 45 | 'BYN', 46 | 'BZD', 47 | 'CAD', 48 | 'CDF', 49 | 'CHE', 50 | 'CHF', 51 | 'CHW', 52 | 'CLF', 53 | 'CLP', 54 | 'CNY', 55 | 'COU', 56 | 'CRC', 57 | 'CUC', 58 | 'CUP', 59 | 'CVE', 60 | 'CZK', 61 | 'DJF', 62 | 'DKK', 63 | 'DOP', 64 | 'DZD', 65 | 'EGP', 66 | 'ERN', 67 | 'ETB', 68 | 'EUR', 69 | 'FJD', 70 | 'FKP', 71 | 'GBP', 72 | 'GEL', 73 | 'GHS', 74 | 'GIP', 75 | 'GMD', 76 | 'GNF', 77 | 'GTQ', 78 | 'GYD', 79 | 'HKD', 80 | 'HNL', 81 | 'HRK', 82 | 'HTG', 83 | 'HUF', 84 | 'IDR', 85 | 'ILS', 86 | 'INR', 87 | 'IQD', 88 | 'IRR', 89 | 'ISK', 90 | 'JMD', 91 | 'JOD', 92 | 'JPY', 93 | 'KES', 94 | 'KGS', 95 | 'KHR', 96 | 'KMF', 97 | 'KPW', 98 | 'KRW', 99 | 'KWD', 100 | 'KYD', 101 | 'KZT', 102 | 'LAK', 103 | 'LBP', 104 | 'LKR', 105 | 'LRD', 106 | 'LSL', 107 | 'LYD', 108 | 'MAD', 109 | 'MDL', 110 | 'MGA', 111 | 'MKD', 112 | 'MMK', 113 | 'MNT', 114 | 'MOP', 115 | 'MRU', 116 | 'MUR', 117 | 'MVR', 118 | 'MWK', 119 | 'MXN', 120 | 'MXV', 121 | 'MYR', 122 | 'MZN', 123 | 'NAD', 124 | 'NGN', 125 | 'NIO', 126 | 'NOK', 127 | 'NPR', 128 | 'NZD', 129 | 'OMR', 130 | 'PAB', 131 | 'PEN', 132 | 'PGK', 133 | 'PHP', 134 | 'PKR', 135 | 'PLN', 136 | 'PYG', 137 | 'QAR', 138 | 'RON', 139 | 'RSD', 140 | 'RUB', 141 | 'RWF', 142 | 'SAR', 143 | 'SBD', 144 | 'SCR', 145 | 'SDG', 146 | 'SEK', 147 | 'SGD', 148 | 'SHP', 149 | 'SLL', 150 | 'SOS', 151 | 'SRD', 152 | 'SSP', 153 | 'STN', 154 | 'SVC', 155 | 'SYP', 156 | 'SZL', 157 | 'THB', 158 | 'TJS', 159 | 'TMT', 160 | 'TND', 161 | 'TOP', 162 | 'TRY', 163 | 'TTD', 164 | 'TWD', 165 | 'TZS', 166 | 'UAH', 167 | 'UGX', 168 | 'USD', 169 | 'USN', 170 | 'UYI', 171 | 'UYU', 172 | 'UYW', 173 | 'UZS', 174 | 'VED', 175 | 'VES', 176 | 'VND', 177 | 'VUV', 178 | 'WST', 179 | 'XAF', 180 | 'XAG', 181 | 'XAU', 182 | 'XBA', 183 | 'XBB', 184 | 'XBC', 185 | 'XBD', 186 | 'XCD', 187 | 'XDR', 188 | 'XOF', 189 | 'XPD', 190 | 'XPF', 191 | 'XPT', 192 | 'XSU', 193 | 'XTS', 194 | 'XUA', 195 | 'XXX', 196 | 'YER', 197 | 'ZAR', 198 | 'ZMW', 199 | 'ZWL', 200 | ); 201 | registerArgumentsSet( 202 | 'fallback_levels', 203 | \Framework\Language\Language::FALLBACK_DEFAULT, 204 | \Framework\Language\Language::FALLBACK_NONE, 205 | \Framework\Language\Language::FALLBACK_PARENT, 206 | ); 207 | registerArgumentsSet( 208 | 'locales', 209 | 'aa', 210 | 'ab', 211 | 'af', 212 | 'ak', 213 | 'als', 214 | 'am', 215 | 'an', 216 | 'ang', 217 | 'ang', 218 | 'ar', 219 | 'arc', 220 | 'as', 221 | 'ast', 222 | 'av', 223 | 'awa', 224 | 'ay', 225 | 'az', 226 | 'ba', 227 | 'bar', 228 | 'bat-smg', 229 | 'bcl', 230 | 'be', 231 | 'be-x-old', 232 | 'bg', 233 | 'bh', 234 | 'bi', 235 | 'bm', 236 | 'bn', 237 | 'bo', 238 | 'bpy', 239 | 'br', 240 | 'brx', 241 | 'bs', 242 | 'bug', 243 | 'bxr', 244 | 'ca', 245 | 'cdo', 246 | 'ce', 247 | 'ceb', 248 | 'ch', 249 | 'cho', 250 | 'chr', 251 | 'chy', 252 | 'ckb', 253 | 'co', 254 | 'cr', 255 | 'cs', 256 | 'csb', 257 | 'cu', 258 | 'cv', 259 | 'cy', 260 | 'da', 261 | 'de', 262 | 'diq', 263 | 'dsb', 264 | 'dv', 265 | 'dz', 266 | 'ee', 267 | 'el', 268 | 'en', 269 | 'eo', 270 | 'es', 271 | 'et', 272 | 'eu', 273 | 'ext', 274 | 'fa', 275 | 'ff', 276 | 'fi', 277 | 'fiu-vro', 278 | 'fj', 279 | 'fo', 280 | 'fr', 281 | 'frp', 282 | 'fur', 283 | 'fy', 284 | 'ga', 285 | 'gan', 286 | 'gbm', 287 | 'gd', 288 | 'gil', 289 | 'gl', 290 | 'gn', 291 | 'got', 292 | 'gu', 293 | 'gv', 294 | 'ha', 295 | 'hak', 296 | 'haw', 297 | 'he', 298 | 'hi', 299 | 'ho', 300 | 'hr', 301 | 'ht', 302 | 'hu', 303 | 'hy', 304 | 'hz', 305 | 'ia', 306 | 'id', 307 | 'ie', 308 | 'ig', 309 | 'ii', 310 | 'ik', 311 | 'ilo', 312 | 'inh', 313 | 'io', 314 | 'is', 315 | 'it', 316 | 'iu', 317 | 'ja', 318 | 'jbo', 319 | 'jv', 320 | 'ka', 321 | 'kg', 322 | 'khw', 323 | 'ki', 324 | 'kj', 325 | 'kk', 326 | 'kl', 327 | 'km', 328 | 'kn', 329 | 'ko', 330 | 'kr', 331 | 'ks', 332 | 'ksh', 333 | 'ku', 334 | 'kv', 335 | 'kw', 336 | 'ky', 337 | 'la', 338 | 'lad', 339 | 'lan', 340 | 'lb', 341 | 'lg', 342 | 'li', 343 | 'lij', 344 | 'lmo', 345 | 'ln', 346 | 'lo', 347 | 'lt', 348 | 'lv', 349 | 'lzz', 350 | 'man', 351 | 'map-bms', 352 | 'mg', 353 | 'mh', 354 | 'mi', 355 | 'min', 356 | 'mk', 357 | 'ml', 358 | 'mn', 359 | 'mo', 360 | 'mr', 361 | 'mrh', 362 | 'ms', 363 | 'mt', 364 | 'mus', 365 | 'mwl', 366 | 'my', 367 | 'na', 368 | 'nah', 369 | 'nap', 370 | 'nd', 371 | 'nds', 372 | 'nds-nl', 373 | 'ne', 374 | 'new', 375 | 'ng', 376 | 'nl', 377 | 'nn', 378 | 'no', 379 | 'nr', 380 | 'nrm', 381 | 'nso', 382 | 'nv', 383 | 'ny', 384 | 'oc', 385 | 'oj', 386 | 'om', 387 | 'or', 388 | 'os', 389 | 'pa', 390 | 'pag', 391 | 'pam', 392 | 'pap', 393 | 'pdc', 394 | 'pi', 395 | 'pih', 396 | 'pl', 397 | 'pms', 398 | 'ps', 399 | 'pt', 400 | 'pt-br', 401 | 'qu', 402 | 'rm', 403 | 'rmy', 404 | 'rn', 405 | 'ro', 406 | 'roa-rup', 407 | 'ru', 408 | 'rw', 409 | 'sa', 410 | 'sc', 411 | 'scn', 412 | 'sco', 413 | 'sd', 414 | 'se', 415 | 'sg', 416 | 'sh', 417 | 'si', 418 | 'simple', 419 | 'sk', 420 | 'sl', 421 | 'sm', 422 | 'sn', 423 | 'so', 424 | 'sq', 425 | 'sr', 426 | 'ss', 427 | 'st', 428 | 'su', 429 | 'sv', 430 | 'sw', 431 | 'ta', 432 | 'te', 433 | 'tet', 434 | 'tg', 435 | 'th', 436 | 'ti', 437 | 'tk', 438 | 'tl', 439 | 'tlh', 440 | 'tn', 441 | 'to', 442 | 'tpi', 443 | 'tr', 444 | 'ts', 445 | 'tt', 446 | 'tum', 447 | 'tw', 448 | 'ty', 449 | 'udm', 450 | 'ug', 451 | 'uk', 452 | 'ur', 453 | 'uz', 454 | 'uz-af', 455 | 've', 456 | 'vec', 457 | 'vi', 458 | 'vls', 459 | 'vo', 460 | 'wa', 461 | 'war', 462 | 'wo', 463 | 'xal', 464 | 'xh', 465 | 'xmf', 466 | 'yi', 467 | 'yo', 468 | 'za', 469 | 'zh', 470 | 'zh-classical', 471 | 'zh-min-nan', 472 | 'zh-yue', 473 | 'zu', 474 | ); 475 | registerArgumentsSet( 476 | 'locale_directions', 477 | 'ltr', 478 | 'rtl', 479 | ); 480 | expectedArguments( 481 | \Framework\Language\Language::currency(), 482 | 1, 483 | argumentsSet('currencies') 484 | ); 485 | expectedArguments( 486 | \Framework\Language\Language::date(), 487 | 1, 488 | argumentsSet('date_styles') 489 | ); 490 | expectedArguments( 491 | \Framework\Language\Language::setFallbackLevel(), 492 | 0, 493 | argumentsSet('fallback_levels') 494 | ); 495 | expectedArguments( 496 | \Framework\Language\Language::__construct(), 497 | 0, 498 | argumentsSet('locales') 499 | ); 500 | expectedArguments( 501 | \Framework\Language\Language::addLines(), 502 | 0, 503 | argumentsSet('locales') 504 | ); 505 | expectedArguments( 506 | \Framework\Language\Language::currency(), 507 | 2, 508 | argumentsSet('locales') 509 | ); 510 | expectedArguments( 511 | \Framework\Language\Language::date(), 512 | 2, 513 | argumentsSet('locales') 514 | ); 515 | expectedArguments( 516 | \Framework\Language\Language::formatMessage(), 517 | 2, 518 | argumentsSet('locales') 519 | ); 520 | expectedArguments( 521 | \Framework\Language\Language::getLocaleDirection(), 522 | 0, 523 | argumentsSet('locales') 524 | ); 525 | expectedArguments( 526 | \Framework\Language\Language::lang(), 527 | 2, 528 | argumentsSet('locales') 529 | ); 530 | expectedArguments( 531 | \Framework\Language\Language::ordinal(), 532 | 1, 533 | argumentsSet('locales') 534 | ); 535 | expectedArguments( 536 | \Framework\Language\Language::render(), 537 | 3, 538 | argumentsSet('locales') 539 | ); 540 | expectedArguments( 541 | \Framework\Language\Language::setCurrentLocale(), 542 | 0, 543 | argumentsSet('locales') 544 | ); 545 | expectedArguments( 546 | \Framework\Language\Language::setDefaultLocale(), 547 | 0, 548 | argumentsSet('locales') 549 | ); 550 | expectedReturnValues( 551 | \Framework\Language\Language::getCurrentLocale(), 552 | argumentsSet('locales') 553 | ); 554 | expectedReturnValues( 555 | \Framework\Language\Language::getCurrentLocaleDirection(), 556 | argumentsSet('locale_directions') 557 | ); 558 | expectedReturnValues( 559 | \Framework\Language\Language::getDefaultLocale(), 560 | argumentsSet('locales') 561 | ); 562 | expectedReturnValues( 563 | \Framework\Language\Language::getLocaleDirection(), 564 | argumentsSet('locale_directions') 565 | ); 566 | expectedReturnValues( 567 | \Framework\Language\Language::getFallbackLevel(), 568 | argumentsSet('fallback_levels') 569 | ); 570 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Natan Felles 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Aplus Framework Language Library 2 | 3 | # Aplus Framework Language Library 4 | 5 | - [Home](https://aplus-framework.com/packages/language) 6 | - [User Guide](https://docs.aplus-framework.com/guides/libraries/language/index.html) 7 | - [API Documentation](https://docs.aplus-framework.com/packages/language.html) 8 | 9 | [![tests](https://github.com/aplus-framework/language/actions/workflows/tests.yml/badge.svg)](https://github.com/aplus-framework/language/actions/workflows/tests.yml) 10 | [![coverage](https://coveralls.io/repos/github/aplus-framework/language/badge.svg?branch=master)](https://coveralls.io/github/aplus-framework/language?branch=master) 11 | [![packagist](https://img.shields.io/packagist/v/aplus/language)](https://packagist.org/packages/aplus/language) 12 | [![open-source](https://img.shields.io/badge/open--source-sponsor-magenta)](https://aplus-framework.com/sponsor) 13 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aplus/language", 3 | "description": "Aplus Framework Language Library", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "language", 8 | "lang", 9 | "locale", 10 | "localization", 11 | "internationalization", 12 | "i18n", 13 | "l10n", 14 | "intl" 15 | ], 16 | "authors": [ 17 | { 18 | "name": "Natan Felles", 19 | "email": "natanfelles@gmail.com", 20 | "homepage": "https://natanfelles.github.io" 21 | } 22 | ], 23 | "homepage": "https://aplus-framework.com/packages/language", 24 | "support": { 25 | "email": "support@aplus-framework.com", 26 | "issues": "https://github.com/aplus-framework/language/issues", 27 | "forum": "https://aplus-framework.com/forum", 28 | "source": "https://github.com/aplus-framework/language", 29 | "docs": "https://docs.aplus-framework.com/guides/libraries/language/" 30 | }, 31 | "funding": [ 32 | { 33 | "type": "Aplus Sponsor", 34 | "url": "https://aplus-framework.com/sponsor" 35 | } 36 | ], 37 | "require": { 38 | "php": ">=8.3", 39 | "ext-intl": "*", 40 | "aplus/debug": "^4.3", 41 | "aplus/helpers": "^4.0" 42 | }, 43 | "require-dev": { 44 | "ext-xdebug": "*", 45 | "aplus/coding-standard": "^2.8", 46 | "ergebnis/composer-normalize": "^2.25", 47 | "jetbrains/phpstorm-attributes": "^1.0", 48 | "phpmd/phpmd": "^2.13", 49 | "phpstan/phpstan": "^1.9", 50 | "phpunit/phpunit": "^10.5" 51 | }, 52 | "minimum-stability": "dev", 53 | "prefer-stable": true, 54 | "autoload": { 55 | "psr-4": { 56 | "Framework\\Language\\": "src/" 57 | } 58 | }, 59 | "autoload-dev": { 60 | "psr-4": { 61 | "Tests\\Language\\": "tests/" 62 | } 63 | }, 64 | "config": { 65 | "allow-plugins": { 66 | "ergebnis/composer-normalize": true 67 | }, 68 | "optimize-autoloader": true, 69 | "preferred-install": "dist", 70 | "sort-packages": true 71 | }, 72 | "extra": { 73 | "branch-alias": { 74 | "dev-master": "4.x-dev" 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Debug/LanguageCollection.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Framework\Language\Debug; 11 | 12 | use Framework\Debug\Collection; 13 | 14 | /** 15 | * Class LanguageCollection. 16 | * 17 | * @package language 18 | */ 19 | class LanguageCollection extends Collection 20 | { 21 | protected string $iconPath = __DIR__ . '/icons/language.svg'; 22 | } 23 | -------------------------------------------------------------------------------- /src/Debug/LanguageCollector.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Framework\Language\Debug; 11 | 12 | use Framework\Debug\Collector; 13 | use Framework\Debug\Debugger; 14 | use Framework\Language\Language; 15 | 16 | /** 17 | * Class LanguageCollector. 18 | * 19 | * @package language 20 | */ 21 | class LanguageCollector extends Collector 22 | { 23 | protected Language $language; 24 | 25 | public function setLanguage(Language $language) : static 26 | { 27 | $this->language = $language; 28 | return $this; 29 | } 30 | 31 | public function getActivities() : array 32 | { 33 | $activities = []; 34 | foreach ($this->getData() as $index => $data) { 35 | $activities[] = [ 36 | 'collector' => $this->getName(), 37 | 'class' => static::class, 38 | 'description' => 'Render message ' . ($index + 1), 39 | 'start' => $data['start'], 40 | 'end' => $data['end'], 41 | ]; 42 | } 43 | return $activities; 44 | } 45 | 46 | public function getContents() : string 47 | { 48 | if (!isset($this->language)) { 49 | return '

A Language instance has not been set on this collector.

'; 50 | } 51 | \ob_start(); ?> 52 |

Default Locale: language->getDefaultLocale()) 54 | ?>

55 |

Current Locale: language->getCurrentLocale()) 57 | ?>

58 |

Supported Locales: language->getSupportedLocales())) 60 | ?>

61 |

Fallback Level: language->getFallbackLevel(); 63 | echo "{$level->value} ({$level->name})"; ?>

64 |

Rendered Messages

65 | renderRenderedMessages() ?> 66 |

Directories

67 | renderDirectories() ?> 68 |

Available Messages

69 | renderLines(); 71 | return \ob_get_clean(); // @phpstan-ignore-line 72 | } 73 | 74 | protected function renderRenderedMessages() : string 75 | { 76 | if (!$this->hasData()) { 77 | return '

No message has been rendered.

'; 78 | } 79 | $count = \count($this->getData()); 80 | \ob_start(); ?> 81 |

message has been rendered.

82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | getData() as $index => $data): ?> 95 | 96 | 97 | 98 | 99 | 102 | 103 | 104 | 105 | 106 | 107 |
#FileLineMessageLocaleTime
100 |
101 |
108 | language->getDirectories(); 115 | if (empty($directories)) { 116 | return '

No directory set for this Language instance.

'; 117 | } 118 | $count = \count($directories); 119 | \ob_start(); ?> 120 |

There set.

121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | language->getDirectories() as $index => $directory): ?> 130 | 131 | 132 | 133 | 134 | 135 | 136 |
#Directory
137 | getLines(); 144 | if (empty($lines)) { 145 | return '

No file lines available for this Language instance.

'; 146 | } 147 | $count = \count($lines); 148 | \ob_start(); ?> 149 |

There available to the current locale (language->getCurrentLocale() ?>). 151 |

152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | $line): ?> 165 | 166 | 167 | 168 | 169 | 172 | 173 | 174 | 175 | 176 | 177 |
#FileLineMessage PatternLocaleFallback
170 |
171 |
178 | > 184 | */ 185 | protected function getLines() : array 186 | { 187 | $allLines = $this->language->getLines(); 188 | $this->language->resetLines(); 189 | $files = []; 190 | foreach ($this->language->getDirectories() as $directory) { 191 | foreach ((array) \glob($directory . '*/*.php') as $file) { 192 | $file = (string) $file; 193 | $pos = \strrpos($file, \DIRECTORY_SEPARATOR); 194 | $file = \substr($file, $pos + 1, -4); 195 | $files[$file] = true; 196 | } 197 | } 198 | $files = \array_keys($files); 199 | foreach ($files as $file) { 200 | $this->language->render($file, '.·*·.'); 201 | } 202 | $result = []; 203 | foreach ($this->language->getLines() as $locale => $lines) { 204 | \ksort($lines); 205 | foreach ($lines as $file => $messages) { 206 | \ksort($messages); 207 | foreach ($messages as $line => $message) { 208 | foreach ($result as $data) { 209 | if ($data['file'] === $file && $data['line'] === $line) { 210 | continue 2; 211 | } 212 | } 213 | $result[] = [ 214 | 'file' => $file, 215 | 'line' => $line, 216 | 'message' => $message, 217 | 'locale' => $locale, 218 | 'fallback' => $this->getFallbackName($locale), 219 | ]; 220 | } 221 | } 222 | } 223 | \usort($result, static function ($str1, $str2) { 224 | $cmp = \strcmp($str1['file'], $str2['file']); 225 | if ($cmp === 0) { 226 | $cmp = \strcmp($str1['line'], $str2['line']); 227 | } 228 | return $cmp; 229 | }); 230 | foreach ($allLines as $locale => $lines) { 231 | foreach ($lines as $file => $messages) { 232 | $this->language->addLines($locale, $file, $messages); 233 | } 234 | } 235 | return $result; 236 | } 237 | 238 | protected function getFallbackName(string $locale) : string 239 | { 240 | $currentLocale = $this->language->getCurrentLocale(); 241 | if ($locale === $currentLocale) { 242 | return 'none'; 243 | } 244 | $parentLocale = \explode('-', $currentLocale)[0]; 245 | if ($locale === $parentLocale) { 246 | return 'parent'; 247 | } 248 | if ($locale === $this->language->getDefaultLocale()) { 249 | return 'default'; 250 | } 251 | return ''; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/Debug/icons/language.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/FallbackLevel.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Framework\Language; 11 | 12 | /** 13 | * Enum FallbackLevel. 14 | * 15 | * @package language 16 | */ 17 | enum FallbackLevel : int 18 | { 19 | /** 20 | * Disable fallback. 21 | * 22 | * Use language lines only from the given language. 23 | */ 24 | case none = 0; 25 | /** 26 | * Fallback to parent language. 27 | * 28 | * If the given language is pt-BR and a line is not found, try to use the line of pt. 29 | * 30 | * NOTE: The parent locale must be set in the Supported Locales to this fallback work. 31 | */ 32 | case parent = 1; 33 | /** 34 | * Fallback to default language. 35 | * 36 | * If the parent language is not found, try to use the default language. 37 | */ 38 | case default = 2; 39 | } 40 | -------------------------------------------------------------------------------- /src/Language.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Framework\Language; 11 | 12 | use Framework\Helpers\Isolation; 13 | use Framework\Language\Debug\LanguageCollector; 14 | use InvalidArgumentException; 15 | use JetBrains\PhpStorm\ArrayShape; 16 | use JetBrains\PhpStorm\Pure; 17 | 18 | /** 19 | * Class Language. 20 | * 21 | * @see https://www.sitepoint.com/localization-demystified-understanding-php-intl/ 22 | * @see https://unicode-org.github.io/icu-docs/#/icu4c/classMessageFormat.html 23 | * 24 | * @package language 25 | */ 26 | class Language 27 | { 28 | /** 29 | * The current locale. 30 | */ 31 | protected string $currentLocale; 32 | /** 33 | * The default locale. 34 | */ 35 | protected string $defaultLocale; 36 | /** 37 | * List of directories to find for files. 38 | * 39 | * @var array 40 | */ 41 | protected array $directories = []; 42 | /** 43 | * The locale fallback level. 44 | */ 45 | protected FallbackLevel $fallbackLevel = FallbackLevel::default; 46 | /** 47 | * List with locales of already scanned directories. 48 | * 49 | * @var array 50 | */ 51 | protected array $findedLocales = []; 52 | /** 53 | * Language lines. 54 | * 55 | * List of "locale" => "file" => "line" => "text" 56 | * 57 | * @var array>> 58 | */ 59 | protected array $languages = []; 60 | /** 61 | * Supported locales. Any other will be ignored. 62 | * 63 | * The default locale is always supported. 64 | * 65 | * @var array 66 | */ 67 | protected array $supportedLocales = []; 68 | protected LanguageCollector $debugCollector; 69 | 70 | /** 71 | * Language constructor. 72 | * 73 | * @param string $locale The default (and current) locale code 74 | * @param array $directories List of directory paths to find for language files 75 | */ 76 | public function __construct(string $locale = 'en', array $directories = []) 77 | { 78 | $this->setDefaultLocale($locale); 79 | $this->setCurrentLocale($locale); 80 | if ($directories) { 81 | $this->setDirectories($directories); 82 | } 83 | } 84 | 85 | /** 86 | * Adds a locale to the list of already scanned directories. 87 | * 88 | * @param string $locale 89 | * 90 | * @return static 91 | */ 92 | protected function addFindedLocale(string $locale) : static 93 | { 94 | $this->findedLocales[] = $locale; 95 | return $this; 96 | } 97 | 98 | /** 99 | * Adds custom lines for a specific locale. 100 | * 101 | * Useful to set lines from a database or any parsed file. 102 | * 103 | * NOTE: This function will always replace the old lines, as given from files. 104 | * 105 | * @param string $locale The locale code 106 | * @param string $file The file name 107 | * @param array $lines An array of "line" => "text" 108 | * 109 | * @return static 110 | */ 111 | public function addLines(string $locale, string $file, array $lines) : static 112 | { 113 | if (!$this->isFindedLocale($locale)) { 114 | // Certify that all directories are scanned first 115 | // So, this method always have priority on replacements 116 | $this->getLine($locale, $file, ''); 117 | } 118 | $this->languages[$locale][$file] = isset($this->languages[$locale][$file]) 119 | ? \array_replace($this->languages[$locale][$file], $lines) 120 | : $lines; 121 | return $this; 122 | } 123 | 124 | /** 125 | * Gets a currency value formatted in a given locale. 126 | * 127 | * @param float $value The money value 128 | * @param string $currency The Currency code. i.e. USD, BRL, JPY 129 | * @param string|null $locale A custom locale or null to use the current 130 | * 131 | * @see https://en.wikipedia.org/wiki/ISO_4217#Active_codes 132 | * 133 | * @return string 134 | */ 135 | public function currency(float $value, string $currency, ?string $locale = null) : string 136 | { 137 | // @phpstan-ignore-next-line 138 | return \NumberFormatter::create( 139 | $locale ?? $this->getCurrentLocale(), 140 | \NumberFormatter::CURRENCY 141 | )->formatCurrency($value, $currency); 142 | } 143 | 144 | /** 145 | * Gets a formatted date in a given locale. 146 | * 147 | * @param int $time An Unix timestamp 148 | * @param string|null $style One of: short, medium, long or full. Leave null to use short 149 | * @param string|null $locale A custom locale or null to use the current 150 | * 151 | * @throws InvalidArgumentException for invalid style format 152 | * 153 | * @return string 154 | */ 155 | public function date(int $time, ?string $style = null, ?string $locale = null) : string 156 | { 157 | if ($style && !\in_array($style, ['short', 'medium', 'long', 'full'], true)) { 158 | throw new InvalidArgumentException('Invalid date style format: ' . $style); 159 | } 160 | $style = $style ?: 'short'; 161 | // @phpstan-ignore-next-line 162 | return \MessageFormatter::formatMessage( 163 | $locale ?? $this->getCurrentLocale(), 164 | "{time, date, {$style}}", 165 | ['time' => $time] 166 | ); 167 | } 168 | 169 | /** 170 | * Find for absolute file paths from where language lines can be loaded. 171 | * 172 | * @param string $locale The required locale 173 | * @param string $file The required file 174 | * 175 | * @return array a list of valid filenames 176 | */ 177 | #[Pure] 178 | protected function findFilenames(string $locale, string $file) : array 179 | { 180 | $filenames = []; 181 | foreach ($this->getDirectories() as $directory) { 182 | $path = $directory . $locale . \DIRECTORY_SEPARATOR . $file . '.php'; 183 | if (\is_file($path)) { 184 | $filenames[] = $path; 185 | } 186 | } 187 | return $filenames; 188 | } 189 | 190 | /** 191 | * Gets the current locale. 192 | * 193 | * @return string 194 | */ 195 | #[Pure] 196 | public function getCurrentLocale() : string 197 | { 198 | return $this->currentLocale; 199 | } 200 | 201 | /** 202 | * Gets the current locale directionality. 203 | * 204 | * @return string 'ltr' for Left-To-Right ot 'rtl' for Right-To-Left 205 | */ 206 | #[Pure] 207 | public function getCurrentLocaleDirection() : string 208 | { 209 | return static::getLocaleDirection($this->getCurrentLocale()); 210 | } 211 | 212 | /** 213 | * Gets the default locale. 214 | * 215 | * @return string 216 | */ 217 | #[Pure] 218 | public function getDefaultLocale() : string 219 | { 220 | return $this->defaultLocale; 221 | } 222 | 223 | /** 224 | * Gets the list of directories where language files can be finded. 225 | * 226 | * @return array 227 | */ 228 | #[Pure] 229 | public function getDirectories() : array 230 | { 231 | return $this->directories; 232 | } 233 | 234 | /** 235 | * Gets the Fallback Level. 236 | * 237 | * @return FallbackLevel 238 | */ 239 | #[Pure] 240 | public function getFallbackLevel() : FallbackLevel 241 | { 242 | return $this->fallbackLevel; 243 | } 244 | 245 | /** 246 | * Gets a text line and locale according the Fallback Level. 247 | * 248 | * @param string $locale The locale to get his fallback line 249 | * @param string $file The file 250 | * @param string $line The line 251 | * 252 | * @return array Two numeric keys containg the used locale and text 253 | */ 254 | #[ArrayShape(['string', 'string|null'])] 255 | protected function getFallbackLine(string $locale, string $file, string $line) : array 256 | { 257 | $text = null; 258 | $level = $this->getFallbackLevel()->value; 259 | // Fallback to parent 260 | if ($level > FallbackLevel::none->value && \strpos($locale, '-') > 1) { 261 | [$locale] = \explode('-', $locale, 2); 262 | $text = $this->getLine($locale, $file, $line); 263 | } 264 | // Fallback to default 265 | if ($text === null 266 | && $level > FallbackLevel::parent->value 267 | && $locale !== $this->getDefaultLocale() 268 | ) { 269 | $locale = $this->getDefaultLocale(); 270 | $text = $this->getLine($locale, $file, $line); 271 | } 272 | return [ 273 | $locale, 274 | $text, 275 | ]; 276 | } 277 | 278 | /** 279 | * @param string $filename 280 | * 281 | * @return array 282 | */ 283 | protected function getFileLines(string $filename) : array 284 | { 285 | return Isolation::require($filename); 286 | } 287 | 288 | /** 289 | * Gets a language line text. 290 | * 291 | * @param string $locale The required locale 292 | * @param string $file The required file 293 | * @param string $line The required line 294 | * 295 | * @return string|null The text of the line or null if the line is not found 296 | */ 297 | protected function getLine(string $locale, string $file, string $line) : ?string 298 | { 299 | if (isset($this->languages[$locale][$file][$line])) { 300 | return $this->languages[$locale][$file][$line]; 301 | } 302 | if (!\in_array($locale, $this->getSupportedLocales(), true)) { 303 | return null; 304 | } 305 | $this->addFindedLocale($locale); 306 | $this->findLines($locale, $file); 307 | return $this->languages[$locale][$file][$line] ?? null; 308 | } 309 | 310 | /** 311 | * Find and add lines. 312 | * 313 | * This method can be overridden to find lines in custom storage, such as 314 | * in a database table. 315 | * 316 | * @param string $locale 317 | * @param string $file 318 | * 319 | * @return static 320 | */ 321 | protected function findLines(string $locale, string $file) : static 322 | { 323 | foreach ($this->findFilenames($locale, $file) as $filename) { 324 | $this->addLines($locale, $file, $this->getFileLines($filename)); 325 | } 326 | return $this; 327 | } 328 | 329 | /** 330 | * Gets the list of available locales, lines and texts. 331 | * 332 | * @return array>> 333 | */ 334 | #[Pure] 335 | public function getLines() : array 336 | { 337 | return $this->languages; 338 | } 339 | 340 | public function resetLines() : static 341 | { 342 | $this->languages = []; 343 | return $this; 344 | } 345 | 346 | /** 347 | * Gets the list of Supported Locales. 348 | * 349 | * @return array 350 | */ 351 | #[Pure] 352 | public function getSupportedLocales() : array 353 | { 354 | return $this->supportedLocales; 355 | } 356 | 357 | /** 358 | * Tells if a locale already was found in the directories. 359 | * 360 | * @param string $locale The locale 361 | * 362 | * @see \Framework\Language\Language::getLine() 363 | * 364 | * @return bool 365 | */ 366 | #[Pure] 367 | protected function isFindedLocale(string $locale) : bool 368 | { 369 | return \in_array($locale, $this->findedLocales, true); 370 | } 371 | 372 | /** 373 | * Renders a language file line with dot notation format. 374 | * 375 | * E.g. home.hello matches home for file and hello for line. 376 | * 377 | * @param string $line The dot notation file line 378 | * @param array $args The arguments to be used in the formatted text 379 | * @param string|null $locale A custom locale or null to use the current 380 | * 381 | * @return string|null The rendered text or null if not found 382 | */ 383 | public function lang(string $line, array $args = [], ?string $locale = null) : ?string 384 | { 385 | [$file, $line] = \explode('.', $line, 2); 386 | return $this->render($file, $line, $args, $locale); 387 | } 388 | 389 | /** 390 | * Gets an ordinal number in a given locale. 391 | * 392 | * @param int $number The number to be converted to ordinal 393 | * @param string|null $locale A custom locale or null to use the current 394 | * 395 | * @return string 396 | */ 397 | public function ordinal(int $number, ?string $locale = null) : string 398 | { 399 | // @phpstan-ignore-next-line 400 | return \MessageFormatter::formatMessage( 401 | $locale ?? $this->getCurrentLocale(), 402 | '{number, ordinal}', 403 | ['number' => $number] 404 | ); 405 | } 406 | 407 | /** 408 | * Renders a language file line. 409 | * 410 | * @param string $file The file 411 | * @param string $line The file line 412 | * @param array $args The arguments to be used in the formatted text 413 | * @param string|null $locale A custom locale or null to use the current 414 | * 415 | * @return string The rendered text or file.line expression 416 | */ 417 | public function render( 418 | string $file, 419 | string $line, 420 | array $args = [], 421 | ?string $locale = null 422 | ) : string { 423 | if (isset($this->debugCollector)) { 424 | $start = \microtime(true); 425 | $rendered = $this->getRenderedLine($file, $line, $args, $locale); 426 | $end = \microtime(true); 427 | $this->debugCollector->adddata([ 428 | 'start' => $start, 429 | 'end' => $end, 430 | 'file' => $file, 431 | 'line' => $line, 432 | 'locale' => $rendered['locale'], 433 | 'message' => $rendered['message'], 434 | ]); 435 | return $rendered['message']; 436 | } 437 | return $this->getRenderedLine($file, $line, $args, $locale)['message']; 438 | } 439 | 440 | /** 441 | * @param string $file 442 | * @param string $line 443 | * @param array $args 444 | * @param string|null $locale 445 | * 446 | * @return array 447 | */ 448 | #[ArrayShape(['locale' => 'string', 'message' => 'string'])] 449 | protected function getRenderedLine( 450 | string $file, 451 | string $line, 452 | array $args = [], 453 | ?string $locale = null 454 | ) : array { 455 | $locale ??= $this->getCurrentLocale(); 456 | $text = $this->getLine($locale, $file, $line); 457 | if ($text === null) { 458 | [$locale, $text] = $this->getFallbackLine($locale, $file, $line); 459 | } 460 | if ($text !== null) { 461 | $text = $this->formatMessage($text, $args, $locale); 462 | } 463 | return [ 464 | 'locale' => $locale, 465 | 'message' => $text ?? ($file . '.' . $line), 466 | ]; 467 | } 468 | 469 | /** 470 | * Checks if Language has a line. 471 | * 472 | * @param string $file The file 473 | * @param string $line The file line 474 | * @param string|null $locale A custom locale or null to use the current 475 | * 476 | * @return bool True if the line is found, otherwise false 477 | */ 478 | public function hasLine(string $file, string $line, ?string $locale = null) : bool 479 | { 480 | $locale ??= $this->getCurrentLocale(); 481 | $text = $this->getLine($locale, $file, $line); 482 | if ($text === null) { 483 | $text = $this->getFallbackLine($locale, $file, $line)[1]; 484 | } 485 | return $text !== null; 486 | } 487 | 488 | /** 489 | * @param string $text 490 | * @param array $args 491 | * @param string|null $locale 492 | * 493 | * @return string 494 | */ 495 | public function formatMessage(string $text, array $args = [], ?string $locale = null) : string 496 | { 497 | $args = \array_map(static function ($arg) : string { 498 | return (string) $arg; 499 | }, $args); 500 | $locale ??= $this->getCurrentLocale(); 501 | return \MessageFormatter::formatMessage($locale, $text, $args) ?: $text; 502 | } 503 | 504 | /** 505 | * Sets the current locale. 506 | * 507 | * @param string $locale The current locale. This automatically is set as 508 | * one of Supported Locales. 509 | * 510 | * @return static 511 | */ 512 | public function setCurrentLocale(string $locale) : static 513 | { 514 | $this->currentLocale = $locale; 515 | $locales = $this->getSupportedLocales(); 516 | $locales[] = $locale; 517 | $this->setSupportedLocales($locales); 518 | return $this; 519 | } 520 | 521 | /** 522 | * Sets the default locale. 523 | * 524 | * @param string $locale The default locale. This automatically is set as 525 | * one of Supported Locales. 526 | * 527 | * @return static 528 | */ 529 | public function setDefaultLocale(string $locale) : static 530 | { 531 | $this->defaultLocale = $locale; 532 | $locales = $this->getSupportedLocales(); 533 | $locales[] = $locale; 534 | $this->setSupportedLocales($locales); 535 | return $this; 536 | } 537 | 538 | /** 539 | * Sets a list of directories where language files can be found. 540 | * 541 | * @param array $directories a list of valid directory paths 542 | * 543 | * @throws InvalidArgumentException if a directory path is inaccessible 544 | * 545 | * @return static 546 | */ 547 | public function setDirectories(array $directories) : static 548 | { 549 | $dirs = []; 550 | foreach ($directories as $directory) { 551 | $path = \realpath($directory); 552 | if (!$path || !\is_dir($path)) { 553 | throw new InvalidArgumentException('Directory path inaccessible: ' . $directory); 554 | } 555 | $dirs[] = $path . \DIRECTORY_SEPARATOR; 556 | } 557 | $this->directories = $dirs ? \array_unique($dirs) : []; 558 | $this->reindex(); 559 | return $this; 560 | } 561 | 562 | /** 563 | * @param string $directory 564 | * 565 | * @return static 566 | */ 567 | public function addDirectory(string $directory) : static 568 | { 569 | $this->setDirectories(\array_merge([ 570 | $directory, 571 | ], $this->getDirectories())); 572 | return $this; 573 | } 574 | 575 | protected function reindex() : void 576 | { 577 | $this->findedLocales = []; 578 | foreach ($this->languages as $locale => $files) { 579 | foreach (\array_keys($files) as $file) { 580 | $this->findLines($locale, $file); 581 | } 582 | $this->addFindedLocale($locale); 583 | } 584 | } 585 | 586 | /** 587 | * Sets the Fallback Level. 588 | * 589 | * @param FallbackLevel|int $level 590 | * 591 | * @return static 592 | */ 593 | public function setFallbackLevel(FallbackLevel | int $level) : static 594 | { 595 | if (\is_int($level)) { 596 | $level = FallbackLevel::from($level); 597 | } 598 | $this->fallbackLevel = $level; 599 | return $this; 600 | } 601 | 602 | /** 603 | * Sets a list of Supported Locales. 604 | * 605 | * NOTE: the default locale always is supported. But the current can be exclude 606 | * if this function is called after {@see Language::setCurrentLocale()}. 607 | * 608 | * @param array $locales the supported locales 609 | * 610 | * @return static 611 | */ 612 | public function setSupportedLocales(array $locales) : static 613 | { 614 | $locales[] = $this->getDefaultLocale(); 615 | $locales = \array_unique($locales); 616 | \sort($locales); 617 | $this->supportedLocales = $locales; 618 | $this->reindex(); 619 | return $this; 620 | } 621 | 622 | public function setDebugCollector(LanguageCollector $debugCollector) : static 623 | { 624 | $this->debugCollector = $debugCollector; 625 | $this->debugCollector->setLanguage($this); 626 | return $this; 627 | } 628 | 629 | /** 630 | * Gets text directionality based on locale. 631 | * 632 | * @param string $locale The locale code 633 | * 634 | * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/dir 635 | * @see https://meta.wikimedia.org/wiki/Template:List_of_language_names_ordered_by_code 636 | * 637 | * @return string 'ltr' for Left-To-Right ot 'rtl' for Right-To-Left 638 | */ 639 | #[Pure] 640 | public static function getLocaleDirection(string $locale) : string 641 | { 642 | $locale = \strtolower($locale); 643 | $locale = \strtr($locale, ['_' => '-']); 644 | if (\in_array($locale, [ 645 | 'ar', 646 | 'arc', 647 | 'ckb', 648 | 'dv', 649 | 'fa', 650 | 'ha', 651 | 'he', 652 | 'khw', 653 | 'ks', 654 | 'ps', 655 | 'ur', 656 | 'uz-af', 657 | 'yi', 658 | ], true)) { 659 | return 'rtl'; 660 | } 661 | return 'ltr'; 662 | } 663 | } 664 | --------------------------------------------------------------------------------