├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── i18n.sh ├── locale └── .gitignore ├── src ├── Affinity.php ├── Codes.php ├── Http.php ├── I18n.php ├── Leniency.php ├── Locale.php └── Throwable │ ├── EmptyLocaleListError.php │ ├── Error.php │ ├── Exception.php │ ├── FormattingError.php │ ├── LocaleNotInstalledError.php │ ├── LocaleNotSupportedException.php │ └── PathNotFoundError.php └── tests └── index.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = tab 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | indent_style = space 13 | indent_size = 4 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ 2 | .idea/ 3 | 4 | # Composer 5 | vendor/ 6 | composer.phar 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) delight.im (https://www.delight.im/) 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 | # PHP-I18N 2 | 3 | **Internationalization and localization for PHP** 4 | 5 | Provide your application in multiple languages, to users in various countries, with different formats and conventions. 6 | 7 | ## Requirements 8 | 9 | * PHP 5.6.0+ 10 | * GNU gettext extension (`gettext`) 11 | * Internationalization extension (`intl`) 12 | 13 | **Note:** On Windows, you may have to use the non-thread-safe (NTS) version of PHP. 14 | 15 | ## Installation 16 | 17 | 1. Include the library via Composer [[?]](https://github.com/delight-im/Knowledge/blob/master/Composer%20(PHP).md): 18 | 19 | ```bash 20 | $ composer require delight-im/i18n 21 | ``` 22 | 23 | 1. Include the Composer autoloader: 24 | 25 | ```php 26 | require __DIR__ . '/vendor/autoload.php'; 27 | ``` 28 | 29 | ## Usage 30 | 31 | * [What is a locale?](#what-is-a-locale) 32 | * [Decide on your initial set of supported locales](#decide-on-your-initial-set-of-supported-locales) 33 | * [Creating a new instance](#creating-a-new-instance) 34 | * [Directory and file names for translation files](#directory-and-file-names-for-translation-files) 35 | * [Activating the correct locale for the user](#activating-the-correct-locale-for-the-user) 36 | * [Automatically](#automatically) 37 | * [Manually](#manually) 38 | * [Enabling aliases for translation](#enabling-aliases-for-translation) 39 | * [Identifying, marking and formatting translatable strings](#identifying-marking-and-formatting-translatable-strings) 40 | * [Basic strings](#basic-strings) 41 | * [Strings with formatting](#strings-with-formatting) 42 | * [Strings with extended formatting](#strings-with-extended-formatting) 43 | * [Singular and plural forms](#singular-and-plural-forms) 44 | * [Singular and plural forms with formatting](#singular-and-plural-forms-with-formatting) 45 | * [Singular and plural forms with extended formatting](#singular-and-plural-forms-with-extended-formatting) 46 | * [Strings with context](#strings-with-context) 47 | * [Strings marked for later translation](#strings-marked-for-later-translation) 48 | * [Extracting and updating translatable strings](#extracting-and-updating-translatable-strings) 49 | * [Translating the extracted strings](#translating-the-extracted-strings) 50 | * [Exporting translations to binary format](#exporting-translations-to-binary-format) 51 | * [Retrieving the active locale](#retrieving-the-active-locale) 52 | * [Information about locales](#information-about-locales) 53 | * [Names of locales in the current language](#names-of-locales-in-the-current-language) 54 | * [Native names of locales](#native-names-of-locales) 55 | * [English names of locales](#english-names-of-locales) 56 | * [Names of languages in the current language](#names-of-languages-in-the-current-language) 57 | * [Native names of languages](#native-names-of-languages) 58 | * [English names of languages](#english-names-of-languages) 59 | * [Names of scripts in the current language](#names-of-scripts-in-the-current-language) 60 | * [Native names of scripts](#native-names-of-scripts) 61 | * [English names of scripts](#english-names-of-scripts) 62 | * [Names of regions in the current language](#names-of-regions-in-the-current-language) 63 | * [Native names of regions](#native-names-of-regions) 64 | * [English names of regions](#english-names-of-regions) 65 | * [Language codes](#language-codes) 66 | * [Script codes](#script-codes) 67 | * [Region codes](#region-codes) 68 | * [Directionality of text](#directionality-of-text) 69 | * [Controlling the leniency for lookups and comparisons of locales](#controlling-the-leniency-for-lookups-and-comparisons-of-locales) 70 | 71 | ### What is a locale? 72 | 73 | Put simply, a locale is a set of user preferences and expectations, shared across larger communities in the world, and varying by geographic region. Notably, this includes a user’s language and their expectation of how numbers, dates and times are to be formatted. 74 | 75 | ### Decide on your initial set of supported locales 76 | 77 | Whatever set of languages, scripts and regions you decide to support at the beginning, you will be able to add or remove locales at any later time. So perhaps you might like to start with just 1–3 locales to get started faster. 78 | 79 | You can find a list of various locale codes in the [`Codes`](src/Codes.php) class and use the corresponding constants to refer to the locales, which is the recommended solution. Alternatively, you may copy their string values, which use a subset of IETF BCP 47 (RFC 5646) or Unicode CLDR identifiers. 80 | 81 | Prior to using your initial set of languages, you should ensure they’re installed on any machine you’d like to develop or deploy your application on, making sure they are known to the operating system: 82 | 83 | ```bash 84 | $ locale -a 85 | ``` 86 | 87 | Make sure to pay attention to the exact syntax of the locale names used by your operating system, especially with hyphens, underscores and suffixes, e.g. `en-US` vs `en_US`. 88 | 89 | If a certain locale is not installed yet, you can add it like the `es-AR` locale in the following example: 90 | 91 | ```bash 92 | $ sudo locale-gen es_AR 93 | $ sudo locale-gen es_AR.UTF-8 94 | $ sudo update-locale 95 | $ sudo service apache2 restart 96 | ``` 97 | 98 | **Note:** On Unix-like operating systems, the locale codes used during installation must use underscores. 99 | 100 | ### Creating a new instance 101 | 102 | In order to create an instance of the `I18n` class, just provide your set of supported locales. The only special entry is the very first locale, which also serves as the default locale if no better match can be found for the user. 103 | 104 | ```php 105 | $i18n = new \Delight\I18n\I18n([ 106 | \Delight\I18n\Codes::EN_US, 107 | \Delight\I18n\Codes::DA_DK, 108 | \Delight\I18n\Codes::ES, 109 | \Delight\I18n\Codes::ES_AR, 110 | \Delight\I18n\Codes::KO, 111 | \Delight\I18n\Codes::KO_KR, 112 | \Delight\I18n\Codes::RU_RU, 113 | \Delight\I18n\Codes::SW 114 | ]); 115 | ``` 116 | 117 | ### Directory and file names for translation files 118 | 119 | Your translation files will later have to be stored in the following location: 120 | 121 | ``` 122 | locale//LC_MESSAGES/messages.po 123 | ``` 124 | 125 | That may be, for example, using the `es-ES` locale: 126 | 127 | ``` 128 | locale/es_ES/LC_MESSAGES/messages.po 129 | ``` 130 | 131 | If you need to change the path to the `locale` directory or want to use a different name for that directory, just specify its path explicitly: 132 | 133 | ```php 134 | $i18n->setDirectory(__DIR__ . '/../translations'); 135 | ``` 136 | 137 | The filename in the `LC_MESSAGES` directory, i.e. `messages.po`, is the name of the application module with the extension for PO (Portable Object) files. There’s usually no need to change that, but if you still want to do that, simply call the following method: 138 | 139 | ```php 140 | $i18n->setModule('messages'); 141 | ``` 142 | 143 | **Note:** On Unix-like operating systems, the locale codes used in the directory names have to use underscores, whereas on Windows, the codes have to use hyphens. 144 | 145 | ### Activating the correct locale for the user 146 | 147 | #### Automatically 148 | 149 | The easiest way to pick the most suitable locale for the user is to let this library decide based on various signals and options automatically: 150 | 151 | ```php 152 | $i18n->setLocaleAutomatically(); 153 | ``` 154 | 155 | This will check and decide based on the following factors (in that order): 156 | 157 | 1. **Subdomain** with locale code (e.g. `da-DK.example.com`) 158 | 159 | (**Note:** Locale codes in the (leftmost) subdomain are case-insensitive, i.e. `da-dk` works as well, and you can leave out region or script names, i.e. merely `da` would be sufficient here.) 160 | 161 | 1. **Path prefix** with locale code (e.g. `http://www.example.com/pt-BR/welcome.html`) 162 | 163 | (**Note:** Locale codes in the path prefix are case-insensitive, i.e. `pt-br` works as well, and you can leave out region or script names, i.e. merely `pt` would be sufficient here.) 164 | 165 | 1. **Query string** with locale code 166 | 1. the `locale` parameter 167 | 1. the `language` parameter 168 | 1. the `lang` parameter 169 | 1. the `lc` parameter 170 | 1. **Session field** defined via `I18n#setSessionField` (e.g. `$i18n->setSessionField('locale');`) 171 | 1. **Cookie** defined via `I18n#setCookieName` (e.g. `$i18n->setCookieName('lc');`), with an optional lifetime defined via `I18n#setCookieLifetime` (e.g. `$i18n->setCookieLifetime(60 * 60 * 24);`), where a value of `null` means that the cookie is to expire at the end of the current browser session 172 | 1. **HTTP request header** `Accept-Language` (e.g. `en-US,en;q=0.5`) 173 | 174 | You will usually choose a single one of these options to store and transport your locale codes, with other factors (specifically the last one) as fallback options. The first three options (and the last one) may provide advantages in terms of search engine optimization (SEO) and caching. 175 | 176 | #### Manually 177 | 178 | Of course, you can also specify the locale for your users manually: 179 | 180 | ```php 181 | try { 182 | $i18n->setLocaleManually('es-AR'); 183 | } 184 | catch (\Delight\I18n\Throwable\LocaleNotSupportedException $e) { 185 | die('The locale requested by the user is not supported'); 186 | } 187 | ``` 188 | 189 | ### Enabling aliases for translation 190 | 191 | Set up the following aliases in your application code to simplify your work with this library, to make your code more readable, and to enable support for the included tooling and other GNU gettext utilities: 192 | 193 | ```php 194 | function _f($text, ...$replacements) { global $i18n; return $i18n->translateFormatted($text, ...$replacements); } 195 | 196 | function _fe($text, ...$replacements) { global $i18n; return $i18n->translateFormattedExtended($text, ...$replacements); } 197 | 198 | function _p($text, $alternative, $count) { global $i18n; return $i18n->translatePlural($text, $alternative, $count); } 199 | 200 | function _pf($text, $alternative, $count, ...$replacements) { global $i18n; return $i18n->translatePluralFormatted($text, $alternative, $count, ...$replacements); } 201 | 202 | function _pfe($text, $alternative, $count, ...$replacements) { global $i18n; return $i18n->translatePluralFormattedExtended($text, $alternative, $count, ...$replacements); } 203 | 204 | function _c($text, $context) { global $i18n; return $i18n->translateWithContext($text, $context); } 205 | 206 | function _m($text) { global $i18n; return $i18n->markForTranslation($text); } 207 | ``` 208 | 209 | If the variable holding your global `I18n` instance is not named `$i18n`, you have to adjust each occurrence of `$i18n` in the snippet above accordingly, of course. 210 | 211 | ### Identifying, marking and formatting translatable strings 212 | 213 | In order to internationalize your code base, you have to identify and mark strings that can be translated, and use formatting with more complex strings. Afterwards, these marked strings can be extracted automatically, to be translated outside of the actual code, and will be inserted again during runtime by this library. 214 | 215 | In general, you should follow these simple rules when marking strings for translations: 216 | 217 | * Use units of text as large as possible. This could be a single word (e.g. “Save” on a button), several words (e.g. “Create a new account” in a headline), or full sentences (e.g. “Your account has been created.”). 218 | * Strive to treat entire sentences as atomic units whenever possible, and don’t compose sentences from multiple translated words or parts unless absolutely necessary. 219 | * Use string formatting via one of the dedicated functions and methods instead of resorting to string concatenation or string interpolation. 220 | * Handle singular and plural forms using the dedicated functions and methods, which work even for languages with complex plural rules, which are not always as simple as the binary English rule. 221 | 222 | #### Basic strings 223 | 224 | Wrap the sentences, phrases and labels of your user interface inside of the `_` function: 225 | 226 | ```php 227 | _('Welcome to our online store!'); 228 | // Welcome to our online store! 229 | ``` 230 | 231 | ```php 232 | _('Create account'); 233 | // Create account 234 | ``` 235 | 236 | ```php 237 | _('You have been successfully logged out.'); 238 | // You have been successfully logged out. 239 | ``` 240 | 241 | #### Strings with formatting 242 | 243 | Wrap the sentences, phrases and labels of your user interface inside of the `_f` function: 244 | 245 | ```php 246 | _f('This is %1$s.', 'Bob'); 247 | // This is Bob. 248 | ``` 249 | 250 | ```php 251 | _f('This is %1$d.', 3); 252 | // This is 3. 253 | ``` 254 | 255 | ```php 256 | _f('This is %1$05d.', 3); 257 | // This is 00003. 258 | ``` 259 | 260 | ```php 261 | _f('This is %1$ 5d.', 3); 262 | // This is 3. 263 | // This is ␣␣␣␣3. 264 | ``` 265 | 266 | ```php 267 | _f('This is %1$+d.', 3); 268 | // This is +3. 269 | ``` 270 | 271 | ```php 272 | _f('This is %1$+06d.', 3); 273 | // This is +00003. 274 | ``` 275 | 276 | ```php 277 | _f('This is %1$+ 6d.', 3); 278 | // This is +3. 279 | // This is ␣␣␣␣+3. 280 | ``` 281 | 282 | ```php 283 | _f('This is %1$f.', 3.14); 284 | // This is 3.140000. 285 | ``` 286 | 287 | ```php 288 | _f('This is %1$012f.', 3.14); 289 | // This is 00003.140000. 290 | ``` 291 | 292 | ```php 293 | _f('This is %1$010.4f.', 3.14); 294 | // This is 00003.1400. 295 | ``` 296 | 297 | ```php 298 | _f('This is %1$ 12f.', 3.14); 299 | // This is 3.140000. 300 | // This is ␣␣␣␣3.140000. 301 | ``` 302 | 303 | ```php 304 | _f('This is %1$ 10.4f.', 3.14); 305 | // This is 3.1400. 306 | // This is ␣␣␣␣3.1400. 307 | ``` 308 | 309 | ```php 310 | _f('This is %1$+f.', 3.14); 311 | // This is +3.140000. 312 | ``` 313 | 314 | ```php 315 | _f('This is %1$+013f.', 3.14); 316 | // This is +00003.140000. 317 | ``` 318 | 319 | ```php 320 | _f('This is %1$+011.4f.', 3.14); 321 | // This is +00003.1400. 322 | ``` 323 | 324 | ```php 325 | _f('This is %1$+ 13f.', 3.14); 326 | // This is +3.140000. 327 | // This is ␣␣␣␣+3.140000. 328 | ``` 329 | 330 | ```php 331 | _f('This is %1$+ 11.4f.', 3.14); 332 | // This is +3.1400. 333 | // This is ␣␣␣␣+3.1400. 334 | ``` 335 | 336 | ```php 337 | _f('Hello %s!', 'Jane'); 338 | // Hello Jane! 339 | ``` 340 | 341 | ```php 342 | _f('%1$s is %2$d years old.', 'John', 30); 343 | // John is 30 years old. 344 | ``` 345 | 346 | **Note:** This uses the “printf” format string syntax, known from the C language (and also from PHP). In order to escape the percent sign (to use it literally), simply double it, as in `50 %%`. 347 | 348 | **Note:** When your format strings have more than one placeholder and replacement, always number the placeholders to avoid ambiguity and to allow for flexibility during translation. For example, instead of `%s is from %s`, use `%1$s is from %2$s`. 349 | 350 | #### Strings with extended formatting 351 | 352 | Wrap the sentences, phrases and labels of your user interface inside of the `_fe` function: 353 | 354 | ```php 355 | _fe('This is {0}.', 'Bob'); 356 | // This is Bob. 357 | ``` 358 | 359 | ```php 360 | _fe('This is {0, number}.', 1003.14); 361 | // This is 1,003.14. 362 | ``` 363 | 364 | ```php 365 | _fe('This is {0, number, percent}.', 0.42); 366 | // This is 42%. 367 | ``` 368 | 369 | ```php 370 | _fe('This is {0, date}.', -14182916); 371 | // This is Jul 20, 1969. 372 | ``` 373 | 374 | ```php 375 | _fe('This is {0, date, short}.', -14182916); 376 | // This is 7/20/69. 377 | ``` 378 | 379 | ```php 380 | _fe('This is {0, date, medium}.', -14182916); 381 | // This is Jul 20, 1969. 382 | ``` 383 | 384 | ```php 385 | _fe('This is {0, date, long}.', -14182916); 386 | // This is July 20, 1969. 387 | ``` 388 | 389 | ```php 390 | _fe('This is {0, date, full}.', -14182916); 391 | // This is Sunday, July 20, 1969. 392 | ``` 393 | 394 | ```php 395 | _fe('This is {0, time}.', -14182916); 396 | // This is 1:18:04 PM. 397 | ``` 398 | 399 | ```php 400 | _fe('This is {0, time, short}.', -14182916); 401 | // This is 1:18 PM. 402 | ``` 403 | 404 | ```php 405 | _fe('This is {0, time, medium}.', -14182916); 406 | // This is 1:18:04 PM. 407 | ``` 408 | 409 | ```php 410 | _fe('This is {0, time, long}.', -14182916); 411 | // This is 1:18:04 PM GMT-7. 412 | ``` 413 | 414 | ```php 415 | _fe('This is {0, time, full}.', -14182916); 416 | // This is 1:18:04 PM GMT-07:00. 417 | ``` 418 | 419 | ```php 420 | _fe('This is {0, spellout}.', 314159); 421 | // This is three hundred fourteen thousand one hundred fifty-nine. 422 | ``` 423 | 424 | ```php 425 | _fe('This is {0, ordinal}.', 314159); 426 | // This is 314,159th. 427 | ``` 428 | 429 | ```php 430 | _fe('Hello {0}!', 'Jane'); 431 | // Hello Jane! 432 | ``` 433 | 434 | ```php 435 | _fe('{0} is {1, number} years old.', 'John', 30); 436 | // John is 30 years old. 437 | ``` 438 | 439 | **Note:** This uses the ICU “MessageFormat” syntax. In order to escape curly brackets (to use them literally), wrap them in single quotes, as in `'{'` or `'}'`. In order to escape single quotes (to use them literally), simply double them, as in `it''s`. If you use single quotes for your string literals in PHP, you also have to escape the inserted single quotes with backslashes, as in `\'{\'`, `\'}\'` or `it\'\'s`. 440 | 441 | #### Singular and plural forms 442 | 443 | Wrap the sentences, phrases and labels of your user interface inside of the `_p` function: 444 | 445 | ```php 446 | _p('cat', 'cats', 1); 447 | // cat 448 | ``` 449 | 450 | ```php 451 | _p('cat', 'cats', 2); 452 | // cats 453 | ``` 454 | 455 | ```php 456 | _p('cat', 'cats', 3); 457 | // cats 458 | ``` 459 | 460 | ```php 461 | _p('The file has been saved.', 'The files have been saved.', 1); 462 | // The file has been saved. 463 | ``` 464 | 465 | ```php 466 | _p('The file has been saved.', 'The files have been saved.', 2); 467 | // The files have been saved. 468 | ``` 469 | 470 | ```php 471 | _p('The file has been saved.', 'The files have been saved.', 3); 472 | // The files have been saved. 473 | ``` 474 | 475 | #### Singular and plural forms with formatting 476 | 477 | Wrap the sentences, phrases and labels of your user interface inside of the `_pf` function: 478 | 479 | ```php 480 | _pf('There is %d monkey.', 'There are %d monkeys.', 0); 481 | // There are 0 monkeys. 482 | ``` 483 | 484 | ```php 485 | _pf('There is %d monkey.', 'There are %d monkeys.', 1); 486 | // There is 1 monkey. 487 | ``` 488 | 489 | ```php 490 | _pf('There is %d monkey.', 'There are %d monkeys.', 2); 491 | // There are 2 monkeys. 492 | ``` 493 | 494 | ```php 495 | _pf('There is %1$d monkey in %2$s.', 'There are %1$d monkeys in %2$s.', 3, 'Anytown'); 496 | // There are 3 monkeys in Anytown. 497 | ``` 498 | 499 | ```php 500 | _pf('You have %d new message', 'You have %d new messages', 0); 501 | // You have 0 new messages 502 | ``` 503 | 504 | ```php 505 | _pf('You have %d new message', 'You have %d new messages', 1); 506 | // You have 1 new message 507 | ``` 508 | 509 | ```php 510 | _pf('You have %d new message', 'You have %d new messages', 32); 511 | // You have 32 new messages 512 | ``` 513 | 514 | **Note:** This uses the “printf” format string syntax, known from the C language (and also from PHP). In order to escape the percent sign (to use it literally), simply double it, as in `50 %%`. 515 | 516 | #### Singular and plural forms with extended formatting 517 | 518 | Wrap the sentences, phrases and labels of your user interface inside of the `_pfe` function: 519 | 520 | ```php 521 | _pfe('There is {0, number} monkey.', 'There are {0, number} monkeys.', 0); 522 | // There are 0 monkeys. 523 | ``` 524 | 525 | ```php 526 | _pfe('There is {0, number} monkey.', 'There are {0, number} monkeys.', 1); 527 | // There is 1 monkey. 528 | ``` 529 | 530 | ```php 531 | _pfe('There is {0, number} monkey.', 'There are {0, number} monkeys.', 2); 532 | // There are 2 monkeys. 533 | ``` 534 | 535 | ```php 536 | _pfe('There is {0, number} monkey in {1}.', 'There are {0, number} monkeys in {1}.', 3, 'Anytown'); 537 | // There are 3 monkeys in Anytown. 538 | ``` 539 | 540 | ```php 541 | _pfe('You have {0, number} new message', 'You have {0, number} new messages', 0); 542 | // You have 0 new messages 543 | ``` 544 | 545 | ```php 546 | _pfe('You have {0, number} new message', 'You have {0, number} new messages', 1); 547 | // You have 1 new message 548 | ``` 549 | 550 | ```php 551 | _pfe('You have {0, number} new message', 'You have {0, number} new messages', 32); 552 | // You have 32 new messages 553 | ``` 554 | 555 | **Note:** This uses the ICU “MessageFormat” syntax. In order to escape curly brackets (to use them literally), wrap them in single quotes, as in `'{'` or `'}'`. In order to escape single quotes (to use them literally), simply double them, as in `it''s`. If you use single quotes for your string literals in PHP, you also have to escape the inserted single quotes with backslashes, as in `\'{\'`, `\'}\'` or `it\'\'s`. 556 | 557 | #### Strings with context 558 | 559 | Wrap the sentences, phrases and labels of your user interface inside of the `_c` function: 560 | 561 | ```php 562 | _c('Order', 'sorting'); 563 | // or 564 | _c('Order', 'purchase'); 565 | // or 566 | _c('Order', 'mathematics'); 567 | // or 568 | _c('Order', 'classification'); 569 | ``` 570 | 571 | ```php 572 | _c('Address:', 'location'); 573 | // or 574 | _c('Address:', 'www'); 575 | // or 576 | _c('Address:', 'email'); 577 | // or 578 | _c('Address:', 'letter'); 579 | // or 580 | _c('Address:', 'speech'); 581 | ``` 582 | 583 | #### Strings marked for later translation 584 | 585 | Wrap the sentences, phrases and labels of your user interface inside of the `_m` function. This is a no-op instruction, i.e. (at first glance), it does nothing. But it marks the wrapped text for later translation. This is useful if the text should not be translated *immediately* but will *later* be translated from a variable, usually at the latest point in time possible: 586 | 587 | ```php 588 | _m('User'); 589 | // User 590 | ``` 591 | 592 | This return value could be inserted into your database, for example, and will always use the original string from the source code. Later, you could then use the following call to translate that string from a variable: 593 | 594 | ```php 595 | $text = 'User'; 596 | _($text); 597 | // User 598 | ``` 599 | 600 | ### Extracting and updating translatable strings 601 | 602 | In order to extract all translatable strings from your PHP files, you can use the built-in tool for this task. Again, make sure to pay attention to the exact syntax of the locale names used by your *operating system*, especially with hyphens, underscores and suffixes, e.g. `en-US` vs `en_US`. If you are not sure, check the output of the `locale -a` command on the CLI again. 603 | 604 | ```bash 605 | # For the `mr-IN` locale, with the default directory, with the default domain, and with fuzzy matching 606 | $ bash ./i18n.sh mr-IN 607 | ``` 608 | 609 | ```bash 610 | # For the `sq-MK` locale, with the directory 'translations', with the default domain, and with fuzzy matching 611 | $ bash ./i18n.sh sq-MK translations 612 | ``` 613 | 614 | ```bash 615 | # For the `yo-NG` locale, with the default directory, with the domain 'plugin', and with fuzzy matching 616 | $ bash ./i18n.sh yo-NG "" plugin 617 | ``` 618 | 619 | ```bash 620 | # For the `fr-FR` locale, with the default directory, with the default domain, and without fuzzy matching 621 | $ bash ./i18n.sh fr-FR "" "" nofuzzy 622 | ``` 623 | 624 | This creates or updates a PO (Portable Object) file for the specified language, which you can then translate, share with your translation team, or send to external translators. 625 | 626 | If you only need a generic POT (Portable Object Template) file with all extracted strings, which is not specific to a certain language, just leave out the argument with the locale code (or set it to an empty string): 627 | 628 | ```bash 629 | # With the default directory, with the default domain, and with fuzzy matching 630 | $ bash ./i18n.sh 631 | ``` 632 | 633 | ```bash 634 | # With the directory 'translations', with the default domain, and with fuzzy matching 635 | $ bash ./i18n.sh "" translations 636 | ``` 637 | 638 | ```bash 639 | # With the default directory, with the domain 'plugin', and with fuzzy matching 640 | $ bash ./i18n.sh "" "" plugin 641 | ``` 642 | 643 | ```bash 644 | # With the default directory, with the default domain, and without fuzzy matching 645 | $ bash ./i18n.sh "" "" "" nofuzzy 646 | ``` 647 | 648 | ### Translating the extracted strings 649 | 650 | Whoever handles the actual task of translating the extracted strings, whether it’s you, your translation team, or external translators, the people in charge will need the PO (Portable Object) file for their language, or, in some cases, the generic POT (Portable Object Template) file. 651 | 652 | Just open the file in question and search for strings with `msgstr ""` below them. These are the strings with empty translations that you still need to work on. In addition to that, any string with `#, fuzzy` above it has had a translation before but the original string in the source code changed, so the translation must be reviewed (and the “fuzzy” flag or comment removed). 653 | 654 | ### Exporting translations to binary format 655 | 656 | After you have worked on your translations and saved the PO (Portable Object) file for a language, you need to run the command from [“Extracting and updating translatable strings”](#extracting-and-updating-translatable-strings) again in order to export these translations to a binary format. 657 | 658 | They will then be stored in a MO (Machine Object) file alongside your PO (Portable Object) file, ready to be automatically picked up and inserted in place of the original strings. 659 | 660 | ### Retrieving the active locale 661 | 662 | ```php 663 | $i18n->getLocale(); 664 | // en-US 665 | ``` 666 | 667 | ```php 668 | $i18n->getSystemLocale(); 669 | // en_US.utf8 670 | ``` 671 | 672 | ### Information about locales 673 | 674 | #### Names of locales in the current language 675 | 676 | ```php 677 | $i18n->getLocaleName(); 678 | // English (United States) 679 | ``` 680 | 681 | ```php 682 | $i18n->getLocaleName('fr-BE'); 683 | // French (Belgium) 684 | ``` 685 | 686 | ```php 687 | \Delight\I18n\Locale::toName('nb-NO'); 688 | // Norwegian Bokmål (Norway) 689 | ``` 690 | 691 | #### Native names of locales 692 | 693 | ```php 694 | $i18n->getNativeLocaleName(); 695 | // English (United States) 696 | ``` 697 | 698 | ```php 699 | $i18n->getNativeLocaleName('fr-BE'); 700 | // français (Belgique) 701 | ``` 702 | 703 | ```php 704 | \Delight\I18n\Locale::toNativeName('nb-NO'); 705 | // norsk bokmål (Norge) 706 | ``` 707 | 708 | #### English names of locales 709 | 710 | ```php 711 | \Delight\I18n\Locale::toEnglishName('nb-NO'); 712 | // Norwegian Bokmål (Norway) 713 | ``` 714 | 715 | #### Names of languages in the current language 716 | 717 | ```php 718 | $i18n->getLanguageName(); 719 | // English 720 | ``` 721 | 722 | ```php 723 | $i18n->getLanguageName('fr-BE'); 724 | // French 725 | ``` 726 | 727 | ```php 728 | \Delight\I18n\Locale::toLanguageName('nb-NO'); 729 | // Norwegian Bokmål 730 | ``` 731 | 732 | #### Native names of languages 733 | 734 | ```php 735 | $i18n->getNativeLanguageName(); 736 | // English 737 | ``` 738 | 739 | ```php 740 | $i18n->getNativeLanguageName('fr-BE'); 741 | // français 742 | ``` 743 | 744 | ```php 745 | \Delight\I18n\Locale::toNativeLanguageName('nb-NO'); 746 | // norsk bokmål 747 | ``` 748 | 749 | #### English names of languages 750 | 751 | ```php 752 | \Delight\I18n\Locale::toEnglishLanguageName('nb-NO'); 753 | // Norwegian Bokmål 754 | ``` 755 | 756 | #### Names of scripts in the current language 757 | 758 | ```php 759 | \Delight\I18n\Locale::toScriptName('nb-Latn-NO'); 760 | // Latin 761 | ``` 762 | 763 | #### Native names of scripts 764 | 765 | ```php 766 | \Delight\I18n\Locale::toNativeScriptName('nb-Latn-NO'); 767 | // latinsk 768 | ``` 769 | 770 | #### English names of scripts 771 | 772 | ```php 773 | \Delight\I18n\Locale::toEnglishScriptName('nb-Latn-NO'); 774 | // Latin 775 | ``` 776 | 777 | #### Names of regions in the current language 778 | 779 | ```php 780 | \Delight\I18n\Locale::toRegionName('nb-NO'); 781 | // Norway 782 | ``` 783 | 784 | #### Native names of regions 785 | 786 | ```php 787 | \Delight\I18n\Locale::toNativeRegionName('nb-NO'); 788 | // Norge 789 | ``` 790 | 791 | #### English names of regions 792 | 793 | ```php 794 | \Delight\I18n\Locale::toEnglishRegionName('nb-NO'); 795 | // Norway 796 | ``` 797 | 798 | #### Language codes 799 | 800 | ```php 801 | \Delight\I18n\Locale::toLanguageCode('nb-Latn-NO'); 802 | // nb 803 | ``` 804 | 805 | #### Script codes 806 | 807 | ```php 808 | \Delight\I18n\Locale::toScriptCode('nb-Latn-NO'); 809 | // Latn 810 | ``` 811 | 812 | #### Region codes 813 | 814 | ```php 815 | \Delight\I18n\Locale::toRegionCode('nb-Latn-NO'); 816 | // NO 817 | ``` 818 | 819 | #### Directionality of text 820 | 821 | ```php 822 | \Delight\I18n\Locale::isRtl('ur-PK'); 823 | // true 824 | ``` 825 | 826 | ```php 827 | \Delight\I18n\Locale::isLtr('ln-CD'); 828 | // true 829 | ``` 830 | 831 | ### Controlling the leniency for lookups and comparisons of locales 832 | 833 | When using `I18n#setLocaleAutomatically` to determine and activate the correct locale for the user automatically, you can control which locales to consider similar or related. Thus you can control the way lookups and comparisons of locales work. 834 | 835 | If the default behavior doesn’t work for you, simply provide the optional first argument, called `$leniency`, to `I18n#setLocaleAutomatically`. The following table lists the minimum leniency value that is required to match the two locale codes in question: 836 | 837 | | | `sr` | `sr-RS` | `sr-BA` | `sr-Cyrl` | `sr-Latn` | `sr-Cyrl-RS` | `sr-Cyrl-BA` | `sr-Latn-RS` | `sr-Latn-BA` | 838 | | :------------------------: | :------------------------: | :------------------------: | :------------------------: | :------------------------: | :------------------------: | :------------------------: | :------------------------: | :------------------------: | :------------------------: | 839 | | `sr` | `Leniency::NONE` | `Leniency::EXTREMELY_LOW` | `Leniency::EXTREMELY_LOW` | `Leniency::LOW` | `Leniency::LOW` | `Leniency::MODERATE` | `Leniency::MODERATE` | `Leniency::MODERATE` | `Leniency::MODERATE` | 840 | | `sr_RS` | `Leniency::EXTREMELY_LOW` | `Leniency::NONE` | `Leniency::VERY_LOW` | `Leniency::MODERATE` | `Leniency::MODERATE` | `Leniency::LOW` | `Leniency::HIGH` | `Leniency::LOW` | `Leniency::HIGH` | 841 | | `sr_BA` | `Leniency::EXTREMELY_LOW` | `Leniency::VERY_LOW` | `Leniency::NONE` | `Leniency::MODERATE` | `Leniency::MODERATE` | `Leniency::HIGH` | `Leniency::LOW` | `Leniency::HIGH` | `Leniency::LOW` | 842 | | `sr_Cyrl` | `Leniency::LOW` | `Leniency::MODERATE` | `Leniency::MODERATE` | `Leniency::NONE` | `Leniency::VERY_HIGH` | `Leniency::EXTREMELY_LOW` | `Leniency::EXTREMELY_LOW` | `Leniency::EXTREMELY_HIGH` | `Leniency::EXTREMELY_HIGH` | 843 | | `sr_Latn` | `Leniency::LOW` | `Leniency::MODERATE` | `Leniency::MODERATE` | `Leniency::VERY_HIGH` | `Leniency::NONE` | `Leniency::EXTREMELY_HIGH` | `Leniency::EXTREMELY_HIGH` | `Leniency::EXTREMELY_LOW` | `Leniency::EXTREMELY_LOW` | 844 | | `sr_Cyrl_RS` | `Leniency::MODERATE` | `Leniency::LOW` | `Leniency::HIGH` | `Leniency::EXTREMELY_LOW` | `Leniency::EXTREMELY_HIGH` | `Leniency::NONE` | `Leniency::VERY_LOW` | `Leniency::VERY_HIGH` | `Leniency::FULL` | 845 | | `sr_Cyrl_BA` | `Leniency::MODERATE` | `Leniency::HIGH` | `Leniency::LOW` | `Leniency::EXTREMELY_LOW` | `Leniency::EXTREMELY_HIGH` | `Leniency::VERY_LOW` | `Leniency::NONE` | `Leniency::FULL` | `Leniency::VERY_HIGH` | 846 | | `sr_Latn_RS` | `Leniency::MODERATE` | `Leniency::LOW` | `Leniency::HIGH` | `Leniency::EXTREMELY_HIGH` | `Leniency::EXTREMELY_LOW` | `Leniency::VERY_HIGH` | `Leniency::FULL` | `Leniency::NONE` | `Leniency::VERY_LOW` | 847 | | `sr_Latn_BA` | `Leniency::MODERATE` | `Leniency::HIGH` | `Leniency::LOW` | `Leniency::EXTREMELY_HIGH` | `Leniency::EXTREMELY_LOW` | `Leniency::FULL` | `Leniency::VERY_HIGH` | `Leniency::VERY_LOW` | `Leniency::NONE` | 848 | 849 | ## Troubleshooting 850 | 851 | * Translations are usually cached, so it may be necessary to restart the web server for any changes to take effect. 852 | 853 | ## Contributing 854 | 855 | All contributions are welcome! If you wish to contribute, please create an issue first so that your feature, problem or question can be discussed. 856 | 857 | ## License 858 | 859 | This project is licensed under the terms of the [MIT License](https://opensource.org/licenses/MIT). 860 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "delight-im/i18n", 3 | "description": "Internationalization and localization for PHP", 4 | "require": { 5 | "php": ">=5.6.0", 6 | "ext-gettext": "*", 7 | "ext-intl": "*" 8 | }, 9 | "type": "library", 10 | "keywords": [ "internationalization", "localization", "translation", "language", "locale", "gettext", "intl", "i18n", "l10n" ], 11 | "homepage": "https://github.com/delight-im/PHP-I18N", 12 | "license": "MIT", 13 | "autoload": { 14 | "psr-4": { 15 | "Delight\\I18n\\": "src/" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "25b1eaee2d27bea9e29d5fc91e489afd", 8 | "packages": [], 9 | "packages-dev": [], 10 | "aliases": [], 11 | "minimum-stability": "stable", 12 | "stability-flags": [], 13 | "prefer-stable": false, 14 | "prefer-lowest": false, 15 | "platform": { 16 | "php": ">=5.6.0", 17 | "ext-gettext": "*", 18 | "ext-intl": "*" 19 | }, 20 | "platform-dev": [] 21 | } 22 | -------------------------------------------------------------------------------- /i18n.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### PHP-I18N (https://github.com/delight-im/PHP-I18N) 4 | ### Copyright (c) delight.im (https://www.delight.im/) 5 | ### Licensed under the MIT License (https://opensource.org/licenses/MIT) 6 | 7 | set -eu 8 | 9 | # Switch to the directory where the current script is located 10 | cd "${BASH_SOURCE%/*}" || exit 1 11 | 12 | echo "Extracting and updating translations" 13 | 14 | LOCALE_CODE="${1:-}" 15 | LOCALE_PARENT_DIR="${2:-locale}" 16 | LOCALE_DOMAIN="${3:-messages}" 17 | GENERATE_FUZZY="${4:-fuzzy}" 18 | 19 | if [ ! -d "${LOCALE_PARENT_DIR}" ]; then 20 | echo " * Error: Target directory “${LOCALE_PARENT_DIR}” not found" 21 | exit 2 22 | fi 23 | 24 | if [ ! -w "${LOCALE_PARENT_DIR}" ]; then 25 | echo " * Error: Target directory “${LOCALE_PARENT_DIR}” not writable" 26 | exit 3 27 | fi 28 | 29 | if [ -z "${LOCALE_CODE}" ]; then 30 | echo " * Creating generic POT (Portable Object Template) file" 31 | fi 32 | 33 | find . -iname "*.php" -not -path "./vendor/*" | xargs xgettext --output="${LOCALE_DOMAIN}.pot" --output-dir="${LOCALE_PARENT_DIR}" --language=PHP --from-code=UTF-8 --force-po --no-location --no-wrap --sort-output --copyright-holder="" --keyword --keyword="_:1,1t" --keyword="_f:1" --keyword="_fe:1" --keyword="_p:1,2,3t" --keyword="_pf:1,2" --keyword="_pfe:1,2" --keyword="_c:1,2c,2t" --keyword="_m:1,1t" --flag="_f:1:php-format" --flag="_fe:1:no-php-format" --flag="_pf:1:php-format" --flag="_pfe:1:no-php-format" 34 | sed -i '/# SOME DESCRIPTIVE TITLE./d' "${LOCALE_PARENT_DIR}/${LOCALE_DOMAIN}.pot" 35 | sed -i '/# This file is put in the public domain./d' "${LOCALE_PARENT_DIR}/${LOCALE_DOMAIN}.pot" 36 | sed -i '/# FIRST AUTHOR , YEAR./d' "${LOCALE_PARENT_DIR}/${LOCALE_DOMAIN}.pot" 37 | sed -i '0,/#, fuzzy/{s/#, fuzzy//}' "${LOCALE_PARENT_DIR}/${LOCALE_DOMAIN}.pot" 38 | sed -i '/\"Project-Id-Version: PACKAGE VERSION\\n\"/d' "${LOCALE_PARENT_DIR}/${LOCALE_DOMAIN}.pot" 39 | sed -i '/\"Report-Msgid-Bugs-To: \\n\"/d' "${LOCALE_PARENT_DIR}/${LOCALE_DOMAIN}.pot" 40 | sed -i '/\"POT-Creation-Date: /d' "${LOCALE_PARENT_DIR}/${LOCALE_DOMAIN}.pot" 41 | sed -i '/\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"/d' "${LOCALE_PARENT_DIR}/${LOCALE_DOMAIN}.pot" 42 | sed -i '/\"Last-Translator: FULL NAME \\n\"/d' "${LOCALE_PARENT_DIR}/${LOCALE_DOMAIN}.pot" 43 | sed -i '/\"Language-Team: LANGUAGE \\n\"/d' "${LOCALE_PARENT_DIR}/${LOCALE_DOMAIN}.pot" 44 | sed -i '0,/\"Language: \\n\"/{s/\"Language: \\n\"/\"Language: xx\\n\"/}' "${LOCALE_PARENT_DIR}/${LOCALE_DOMAIN}.pot" 45 | sed -i '0,/\"Content-Type: text\/plain; charset=CHARSET\\n\"/{s/\"Content-Type: text\/plain; charset=CHARSET\\n\"/\"Content-Type: text\/plain; charset=UTF-8\\n\"/}' "${LOCALE_PARENT_DIR}/${LOCALE_DOMAIN}.pot" 46 | 47 | if [ ! -z "${LOCALE_CODE}" ]; then 48 | LOCALE_CONTENTS_DIR="${LOCALE_PARENT_DIR}/${LOCALE_CODE}/LC_MESSAGES" 49 | 50 | mkdir --parents "${LOCALE_CONTENTS_DIR}" 51 | 52 | echo " * Creating PO (Portable Object) file for “${LOCALE_CODE}”" 53 | 54 | if [ -f "${LOCALE_CONTENTS_DIR}/${LOCALE_DOMAIN}.po" ]; then 55 | msgmerge --update --backup=none --suffix=".bak" --previous --force-po --no-location $( [[ "$GENERATE_FUZZY" == "nofuzzy" ]] && printf %s '--no-fuzzy-matching' ) --no-wrap --sort-output "${LOCALE_CONTENTS_DIR}/${LOCALE_DOMAIN}.po" "${LOCALE_PARENT_DIR}/${LOCALE_DOMAIN}.pot" 56 | else 57 | msginit --input="${LOCALE_PARENT_DIR}/${LOCALE_DOMAIN}.pot" --output-file="${LOCALE_CONTENTS_DIR}/${LOCALE_DOMAIN}.po" --locale="${LOCALE_CODE}" --no-translator --no-wrap 58 | sed -i '/\"Project-Id-Version: /d' "${LOCALE_CONTENTS_DIR}/${LOCALE_DOMAIN}.po" 59 | sed -i '/\"Last-Translator: Automatically generated\\n\"/d' "${LOCALE_CONTENTS_DIR}/${LOCALE_DOMAIN}.po" 60 | sed -i '/\"Language-Team: none\\n\"/d' "${LOCALE_CONTENTS_DIR}/${LOCALE_DOMAIN}.po" 61 | fi 62 | 63 | echo " * Creating MO (Machine Object) file for “${LOCALE_CODE}”" 64 | 65 | msgfmt --output-file="${LOCALE_CONTENTS_DIR}/${LOCALE_DOMAIN}.mo" --check-format --check-domain "${LOCALE_CONTENTS_DIR}/${LOCALE_DOMAIN}.po" 66 | 67 | rm "${LOCALE_PARENT_DIR}/${LOCALE_DOMAIN}.pot" 68 | fi 69 | 70 | echo "Done" 71 | -------------------------------------------------------------------------------- /locale/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything except this file itself 2 | * 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /src/Affinity.php: -------------------------------------------------------------------------------- 1 | $bestMatchAffinity) { 60 | $bestMatchCode = $supported; 61 | $bestMatchAffinity = $affinity; 62 | } 63 | } 64 | 65 | if ($bestMatchAffinity >= $leniency) { 66 | return $bestMatchCode; 67 | } 68 | } 69 | 70 | return null; 71 | } 72 | 73 | /** 74 | * Returns the list of languages preferred by the HTTP client in the order of descending priority 75 | * 76 | * @param string|null $httpAcceptLanguage (optional) the value of the HTTP `Accept-Language` request header 77 | * @return string[] 78 | */ 79 | public static function determineClientLanguages($httpAcceptLanguage = null) { 80 | $httpAcceptLanguage = !empty($httpAcceptLanguage) ? (string) $httpAcceptLanguage : (!empty($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : null); 81 | 82 | if (!empty($httpAcceptLanguage)) { 83 | $httpAcceptLanguage = preg_replace('/\s+/', '', $httpAcceptLanguage); 84 | $entries = \explode(',', $httpAcceptLanguage); 85 | 86 | $entries = \array_map(function ($each) { 87 | $each = \explode(';q=', $each, 2); 88 | 89 | if (isset($each[1])) { 90 | $each[1] = (float) $each[1]; 91 | 92 | if ($each[1] > 1) { 93 | $each[1] = 1; 94 | } 95 | elseif ($each[1] < 0) { 96 | $each[1] = 0; 97 | } 98 | } 99 | else { 100 | $each[1] = 1; 101 | } 102 | 103 | return $each; 104 | }, $entries); 105 | 106 | \usort($entries, function ($a, $b) { 107 | return ($b[1] * 1000) - ($a[1] * 1000); 108 | }); 109 | 110 | $entries = \array_map(function ($each) { 111 | return $each[0]; 112 | }, $entries); 113 | 114 | return $entries; 115 | } 116 | else { 117 | return []; 118 | } 119 | } 120 | 121 | /** 122 | * Parses a language provided by the HTTP client and returns its components 123 | * 124 | * @param string $language a “language range” or “language tag” 125 | * @return string[] the language, script and region “subtags” 126 | */ 127 | public static function parseClientLanguage($language) { 128 | if (\preg_match(self::LANGUAGE_RANGE_OR_TAG_REGEX, $language, $matches)) { 129 | \array_shift($matches); 130 | } 131 | else { 132 | $matches = []; 133 | } 134 | 135 | if (empty($matches[0])) { 136 | $matches[0] = null; 137 | } 138 | 139 | if (empty($matches[1])) { 140 | $matches[1] = null; 141 | } 142 | 143 | if (empty($matches[2])) { 144 | $matches[2] = null; 145 | } 146 | 147 | return $matches; 148 | } 149 | 150 | private function __construct() {} 151 | 152 | } 153 | -------------------------------------------------------------------------------- /src/I18n.php: -------------------------------------------------------------------------------- 1 | supportedLocales, $leniency); 53 | 54 | if (!empty($subdomainMatches)) { 55 | try { 56 | $this->setLocaleManually($subdomainMatches); 57 | 58 | return; 59 | } 60 | catch (LocaleNotSupportedException $ignored) {} 61 | } 62 | } 63 | } 64 | 65 | $path = !empty($_SERVER['REQUEST_URI']) ? \parse_url($_SERVER['REQUEST_URI'], \PHP_URL_PATH) : null; 66 | 67 | if (!empty($path)) { 68 | $pathPrefix = \explode('/', \trim($path, '/'), 2)[0]; 69 | 70 | if (!empty($pathPrefix)) { 71 | $pathPrefixMatches = Http::matchPreferredLocales([ $pathPrefix ], $this->supportedLocales, $leniency); 72 | 73 | if (!empty($pathPrefixMatches)) { 74 | try { 75 | $this->setLocaleManually($pathPrefixMatches); 76 | 77 | return; 78 | } 79 | catch (LocaleNotSupportedException $ignored) {} 80 | } 81 | } 82 | } 83 | 84 | $queryString = !empty($_GET['locale']) ? $_GET['locale'] : (!empty($_GET['language']) ? $_GET['language'] : (!empty($_GET['lang']) ? $_GET['lang'] : (!empty($_GET['lc']) ? $_GET['lc'] : null))); 85 | 86 | if (!empty($queryString)) { 87 | try { 88 | $this->setLocaleManually($queryString); 89 | 90 | return; 91 | } 92 | catch (LocaleNotSupportedException $ignored) {} 93 | } 94 | 95 | if (!empty($this->sessionField)) { 96 | if (!empty($_SESSION[$this->sessionField])) { 97 | try { 98 | $this->setLocaleManually($_SESSION[$this->sessionField]); 99 | 100 | return; 101 | } 102 | catch (LocaleNotSupportedException $ignored) {} 103 | } 104 | } 105 | 106 | if (!empty($this->cookieName)) { 107 | if (!\headers_sent()) { 108 | \header('Vary: Cookie'); 109 | } 110 | 111 | if (!empty($_COOKIE[$this->cookieName])) { 112 | try { 113 | $this->setLocaleManually($_COOKIE[$this->cookieName]); 114 | 115 | return; 116 | } 117 | catch (LocaleNotSupportedException $ignored) {} 118 | } 119 | } 120 | 121 | if (!\headers_sent()) { 122 | \header('Vary: Accept-Language'); 123 | } 124 | 125 | $httpClientLanguage = Http::matchClientLanguages($this->supportedLocales, $leniency); 126 | 127 | if ($httpClientLanguage !== null) { 128 | $this->setLocale($httpClientLanguage); 129 | 130 | return; 131 | } 132 | 133 | $this->setLocale( 134 | \reset($this->supportedLocales) 135 | ); 136 | } 137 | 138 | /** 139 | * Attempts to set the locale manually 140 | * 141 | * @param string $code the locale code as one of the constants from {@see Codes} 142 | * @throws LocaleNotSupportedException 143 | */ 144 | public function setLocaleManually($code) { 145 | $code = \str_replace('_', '-', $code); 146 | 147 | foreach ($this->supportedLocales as $supportedLocale) { 148 | if (\strcasecmp($code, $supportedLocale) === 0) { 149 | $this->setLocale($supportedLocale); 150 | 151 | if (!empty($this->sessionField)) { 152 | $_SESSION[$this->sessionField] = $supportedLocale; 153 | } 154 | 155 | if (!empty($this->cookieName)) { 156 | if (!\headers_sent()) { 157 | \setcookie( 158 | $this->cookieName, 159 | $supportedLocale, 160 | !empty($this->cookieLifetime) ? \time() + (int) $this->cookieLifetime : 0, 161 | '/', 162 | '', 163 | false, 164 | false 165 | ); 166 | } 167 | } 168 | 169 | return; 170 | } 171 | } 172 | 173 | throw new LocaleNotSupportedException(); 174 | } 175 | 176 | /** 177 | * Returns the name of the locale actually being used 178 | * 179 | * @return string|null 180 | */ 181 | public function getLocale() { 182 | return $this->locale; 183 | } 184 | 185 | /** 186 | * Returns the name of the locale actually being used as it is known to the operating system 187 | * 188 | * @return string|null 189 | */ 190 | public function getSystemLocale() { 191 | return $this->systemLocale; 192 | } 193 | 194 | /** 195 | * Translates the specified text 196 | * 197 | * @param string $text 198 | * @return string 199 | */ 200 | public function translate($text) { 201 | return \gettext($text); 202 | } 203 | 204 | /** 205 | * Translates the specified text and inserts the given replacements at the designated positions 206 | * 207 | * This uses the “printf” format string syntax, known from the C language (and also from PHP) 208 | * 209 | * In order to escape the percent sign (to use it literally), simply double it 210 | * 211 | * @param string $text 212 | * @param array ...$replacements 213 | * @return string 214 | */ 215 | public function translateFormatted($text, ...$replacements) { 216 | $translated = \gettext($text); 217 | 218 | return self::format($translated, ...$replacements); 219 | } 220 | 221 | /** 222 | * Translates the specified text and inserts the given replacements at the designated positions 223 | * 224 | * This uses the ICU “MessageFormat” syntax 225 | * 226 | * In order to escape curly brackets (to use them literally), wrap them in single quotes 227 | * 228 | * In order to escape single quotes (to use them literally), simply double them 229 | * 230 | * If you use single quotes for your string literals in PHP, you also have to escape the inserted single quotes with backslashes 231 | * 232 | * @param string $text 233 | * @param array ...$replacements 234 | * @return string 235 | */ 236 | public function translateFormattedExtended($text, ...$replacements) { 237 | $translated = \gettext($text); 238 | 239 | return $this->formatExtended($translated, ...$replacements); 240 | } 241 | 242 | /** 243 | * Translates the specified text with a singular or plural form based on the given number 244 | * 245 | * @param string $text the text with the singular form (or one generic form) 246 | * @param string $alternative the text with the plural form (or an empty string) 247 | * @param int $count the number to use for the decision between singular and plural forms 248 | * @return string 249 | */ 250 | public function translatePlural($text, $alternative, $count) { 251 | return \ngettext($text, $alternative, $count); 252 | } 253 | 254 | /** 255 | * Translates the specified text with a singular or plural form based on the given number and inserts the given replacements at the designated positions 256 | * 257 | * This uses the “printf” format string syntax, known from the C language (and also from PHP) 258 | * 259 | * In order to escape the percent sign (to use it literally), simply double it 260 | * 261 | * @param string $text the text with the singular form (or one generic form) 262 | * @param string $alternative the text with the plural form (or an empty string) 263 | * @param int $count the number to use for the decision between singular and plural forms 264 | * @param array ...$replacements 265 | * @return string 266 | */ 267 | public function translatePluralFormatted($text, $alternative, $count, ...$replacements) { 268 | \array_unshift($replacements, $count); 269 | $translated = $this->translatePlural($text, $alternative, $count); 270 | 271 | return self::format($translated, ...$replacements); 272 | } 273 | 274 | /** 275 | * Translates the specified text with a singular or plural form based on the given number and inserts the given replacements at the designated positions 276 | * 277 | * This uses the ICU “MessageFormat” syntax 278 | * 279 | * In order to escape curly brackets (to use them literally), wrap them in single quotes 280 | * 281 | * In order to escape single quotes (to use them literally), simply double them 282 | * 283 | * If you use single quotes for your string literals in PHP, you also have to escape the inserted single quotes with backslashes 284 | * 285 | * @param string $text the text with the singular form (or one generic form) 286 | * @param string $alternative the text with the plural form (or an empty string) 287 | * @param int $count the number to use for the decision between singular and plural forms 288 | * @param array ...$replacements 289 | * @return string 290 | */ 291 | public function translatePluralFormattedExtended($text, $alternative, $count, ...$replacements) { 292 | \array_unshift($replacements, $count); 293 | $translated = $this->translatePlural($text, $alternative, $count); 294 | 295 | return $this->formatExtended($translated, ...$replacements); 296 | } 297 | 298 | /** 299 | * Translates the specified text based on the given context 300 | * 301 | * @param string $text 302 | * @param string $context the context for this occurrence of the text, e.g. “purchase” or “sorting” 303 | * @return string 304 | */ 305 | public function translateWithContext($text, $context) { 306 | $separator = "\x04"; 307 | $input = $context . $separator . $text; 308 | $output = \gettext($input); 309 | 310 | if ($output !== $input) { 311 | return $output; 312 | } 313 | else { 314 | return $text; 315 | } 316 | } 317 | 318 | /** 319 | * Marks the specified text for later translation (no-op) 320 | * 321 | * This is useful if the text should not be translated immediately but will later be translated from a variable (at the last possible point in time) 322 | * 323 | * For example, you may want to insert a piece of text into a database and later translate the text from a variable after retrieving it again 324 | * 325 | * @param string $text 326 | * @return string 327 | */ 328 | public function markForTranslation($text) { 329 | return $text; 330 | } 331 | 332 | public function _($text) { 333 | return $this->translate($text); 334 | } 335 | 336 | public function _f($text, ...$replacements) { 337 | return $this->translateFormatted($text, ...$replacements); 338 | } 339 | 340 | public function _fe($text, ...$replacements) { 341 | return $this->translateFormattedExtended($text, ...$replacements); 342 | } 343 | 344 | public function _p($text, $alternative, $count) { 345 | return $this->translatePlural($text, $alternative, $count); 346 | } 347 | 348 | public function _pf($text, $alternative, $count, ...$replacements) { 349 | return $this->translatePluralFormatted($text, $alternative, $count, ...$replacements); 350 | } 351 | 352 | public function _pfe($text, $alternative, $count, ...$replacements) { 353 | return $this->translatePluralFormattedExtended($text, $alternative, $count, ...$replacements); 354 | } 355 | 356 | public function _c($text, $context) { 357 | return $this->translateWithContext($text, $context); 358 | } 359 | 360 | public function _m($text) { 361 | return $this->markForTranslation($text); 362 | } 363 | 364 | /** 365 | * Returns a human-readable name for the specified locale (formatted in the current locale) 366 | * 367 | * @param string|null $code (optional) the locale code as one of the constants from {@see Codes}, or `null` for the current locale 368 | * @return string|null 369 | */ 370 | public function getLocaleName($code = null) { 371 | return Locale::toName(!empty($code) ? $code : $this->getLocale()); 372 | } 373 | 374 | /** 375 | * Returns a human-readable name for the specified locale (formatted in that same locale) 376 | * 377 | * @param string|null $code (optional) the locale code as one of the constants from {@see Codes}, or `null` for the current locale 378 | * @return string|null 379 | */ 380 | public function getNativeLocaleName($code = null) { 381 | return Locale::toNativeName(!empty($code) ? $code : $this->getLocale()); 382 | } 383 | 384 | /** 385 | * Returns a human-readable name for the language of the specified locale (formatted in the current locale) 386 | * 387 | * @param string|null $code (optional) the locale code as one of the constants from {@see Codes}, or `null` for the current locale 388 | * @return string|null 389 | */ 390 | public function getLanguageName($code = null) { 391 | return Locale::toLanguageName(!empty($code) ? $code : $this->getLocale()); 392 | } 393 | 394 | /** 395 | * Returns a human-readable name for the language of the specified locale (formatted in that same locale) 396 | * 397 | * @param string|null $code (optional) the locale code as one of the constants from {@see Codes}, or `null` for the current locale 398 | * @return string|null 399 | */ 400 | public function getNativeLanguageName($code = null) { 401 | return Locale::toNativeLanguageName(!empty($code) ? $code : $this->getLocale()); 402 | } 403 | 404 | /** 405 | * Sets the directory to load translations from 406 | * 407 | * @param string|null $directory 408 | */ 409 | public function setDirectory($directory) { 410 | if (!empty($directory)) { 411 | $path = \realpath((string) $directory); 412 | 413 | if (!empty($path)) { 414 | $this->directory = $path; 415 | } 416 | else { 417 | throw new PathNotFoundError(); 418 | } 419 | } 420 | else { 421 | $this->directory = null; 422 | } 423 | } 424 | 425 | /** 426 | * Returns the directory to load translations from 427 | * 428 | * @return string|null 429 | */ 430 | public function getDirectory() { 431 | return $this->directory; 432 | } 433 | 434 | /** 435 | * Sets the module of the application to load translations for 436 | * 437 | * @param string|null $module 438 | */ 439 | public function setModule($module) { 440 | $this->module = !empty($module) ? (string) $module : null; 441 | } 442 | 443 | /** 444 | * Returns the module of the application to load translations for 445 | * 446 | * @return string|null 447 | */ 448 | public function getModule() { 449 | return $this->module; 450 | } 451 | 452 | /** 453 | * @param string[] $supportedLocales the codes of the supported locales as constants from {@see Codes} 454 | */ 455 | public function __construct(array $supportedLocales) { 456 | if (!empty($supportedLocales)) { 457 | $this->supportedLocales = $supportedLocales; 458 | } 459 | else { 460 | throw new EmptyLocaleListError(); 461 | } 462 | } 463 | 464 | /** 465 | * Returns the list of all supported locales 466 | * 467 | * @return string[] 468 | */ 469 | public function getSupportedLocales() { 470 | return $this->supportedLocales; 471 | } 472 | 473 | /** 474 | * Sets the field in the session to use for retrieving and storing the preferred locale 475 | * 476 | * @param string|null $sessionField 477 | */ 478 | public function setSessionField($sessionField) { 479 | $this->sessionField = !empty($sessionField) ? (string) $sessionField : null; 480 | } 481 | 482 | /** 483 | * Returns the field in the session to use for retrieving and storing the preferred locale 484 | * 485 | * @return string|null 486 | */ 487 | public function getSessionField() { 488 | return $this->sessionField; 489 | } 490 | 491 | /** 492 | * Sets the name of the cookie to use for retrieving and storing the preferred locale 493 | * 494 | * @param string|null $cookieName 495 | */ 496 | public function setCookieName($cookieName) { 497 | $this->cookieName = !empty($cookieName) ? (string) $cookieName : null; 498 | } 499 | 500 | /** 501 | * Returns the name of the cookie to use for retrieving and storing the preferred locale 502 | * 503 | * @return string|null 504 | */ 505 | public function getCookieName() { 506 | return $this->cookieName; 507 | } 508 | 509 | /** 510 | * Sets the lifetime (in seconds) of the cookie to use for retrieving and storing the preferred locale 511 | * 512 | * A value of `null` means that the cookie is to expire at the end of the current browser session 513 | * 514 | * @param int|null $cookieLifetime 515 | */ 516 | public function setCookieLifetime($cookieLifetime) { 517 | $this->cookieLifetime = !empty($cookieLifetime) ? (int) $cookieLifetime : null; 518 | } 519 | 520 | /** 521 | * Returns the lifetime (in seconds) of the cookie to use for retrieving and storing the preferred locale 522 | * 523 | * A value of `null` means that the cookie is to expire at the end of the current browser session 524 | * 525 | * @return int|null 526 | */ 527 | public function getCookieLifetime() { 528 | return $this->cookieLifetime; 529 | } 530 | 531 | /** 532 | * Sets the locale 533 | * 534 | * @param string $code the locale code as one of the constants from {@see Codes} 535 | */ 536 | private function setLocale($code) { 537 | $systemLocale = \setlocale( 538 | \defined('LC_MESSAGES') ? \LC_MESSAGES : 5, 539 | self::createSystemLocaleVariants($code) 540 | ); 541 | 542 | if (!empty($systemLocale)) { 543 | \setlocale(\LC_NUMERIC, $systemLocale); 544 | \setlocale(\LC_TIME, $systemLocale); 545 | \setlocale(\LC_MONETARY, $systemLocale); 546 | 547 | \putenv('LC_MESSAGES=' . $systemLocale); 548 | \putenv('LC_NUMERIC=' . $systemLocale); 549 | \putenv('LC_TIME=' . $systemLocale); 550 | \putenv('LC_MONETARY=' . $systemLocale); 551 | 552 | // \putenv('LANG=' . $systemLocale); 553 | 554 | \bindtextdomain( 555 | !empty($this->module) ? $this->module : self::MODULE_DEFAULT, 556 | !empty($this->directory) ? $this->directory : self::makeDefaultDirectory() 557 | ); 558 | \bind_textdomain_codeset( 559 | !empty($this->module) ? $this->module : self::MODULE_DEFAULT, 560 | 'UTF-8' 561 | ); 562 | \textdomain(!empty($this->module) ? $this->module : self::MODULE_DEFAULT); 563 | 564 | $this->locale = $code; 565 | $this->systemLocale = $systemLocale; 566 | } 567 | else { 568 | throw new LocaleNotInstalledError(); 569 | } 570 | } 571 | 572 | /** 573 | * Creates possible variants of the locale code as it may be known to the operating system 574 | * 575 | * @param string $code the locale code as one of the constants from {@see Codes} 576 | * @return string[] 577 | */ 578 | private static function createSystemLocaleVariants($code) { 579 | $hyphen = \str_replace('_', '-', $code); 580 | $underscore = \str_replace('-', '_', $hyphen); 581 | 582 | return [ 583 | $hyphen . '.utf8', 584 | $underscore . '.utf8', 585 | $hyphen . '.UTF-8', 586 | $underscore . '.UTF-8', 587 | $hyphen, 588 | $underscore 589 | ]; 590 | } 591 | 592 | /** 593 | * Inserts the given replacements at the designated positions in the text 594 | * 595 | * This uses the “printf” format string syntax, known from the C language (and also from PHP) 596 | * 597 | * In order to escape the percent sign (to use it literally), simply double it 598 | * 599 | * @param string $text 600 | * @param array ...$replacements 601 | * @return string 602 | */ 603 | private static function format($text, ...$replacements) { 604 | $formatted = @\sprintf($text, ...$replacements); 605 | 606 | if ($formatted !== false) { 607 | return $formatted; 608 | } 609 | else { 610 | throw new FormattingError(); 611 | } 612 | } 613 | 614 | /** 615 | * Inserts the given replacements at the designated positions in the text 616 | * 617 | * This uses the ICU “MessageFormat” syntax 618 | * 619 | * In order to escape curly brackets (to use them literally), wrap them in single quotes 620 | * 621 | * In order to escape single quotes (to use them literally), simply double them 622 | * 623 | * If you use single quotes for your string literals in PHP, you also have to escape the inserted single quotes with backslashes 624 | * 625 | * @param string $text 626 | * @param array ...$replacements 627 | * @return string 628 | */ 629 | private function formatExtended($text, ...$replacements) { 630 | $formatted = \MessageFormatter::formatMessage($this->locale, $text, $replacements); 631 | 632 | if ($formatted !== false) { 633 | return $formatted; 634 | } 635 | else { 636 | throw new FormattingError(); 637 | } 638 | } 639 | 640 | /** 641 | * Determines the default directory to load translations from 642 | * 643 | * @return string 644 | */ 645 | private static function makeDefaultDirectory() { 646 | return __DIR__ . '/../../../../locale'; 647 | } 648 | 649 | } 650 | -------------------------------------------------------------------------------- /src/Leniency.php: -------------------------------------------------------------------------------- 1 | translateFormatted($text, ...$replacements); } 28 | 29 | function _fe($text, ...$replacements) { global $i18n; return $i18n->translateFormattedExtended($text, ...$replacements); } 30 | 31 | function _p($text, $alternative, $count) { global $i18n; return $i18n->translatePlural($text, $alternative, $count); } 32 | 33 | function _pf($text, $alternative, $count, ...$replacements) { global $i18n; return $i18n->translatePluralFormatted($text, $alternative, $count, ...$replacements); } 34 | 35 | function _pfe($text, $alternative, $count, ...$replacements) { global $i18n; return $i18n->translatePluralFormattedExtended($text, $alternative, $count, ...$replacements); } 36 | 37 | function _c($text, $context) { global $i18n; return $i18n->translateWithContext($text, $context); } 38 | 39 | function _m($text) { global $i18n; return $i18n->markForTranslation($text); } 40 | 41 | // END ALIASES 42 | 43 | // BEGIN TESTS 44 | 45 | try { 46 | $i18n = new \Delight\I18n\I18n([]); 47 | } 48 | catch (\Delight\I18n\Throwable\EmptyLocaleListError $e) { 49 | $i18n = null; 50 | } 51 | ($i18n === null) or \fail(__LINE__); 52 | 53 | 54 | $i18n = new \Delight\I18n\I18n([ 55 | \Delight\I18n\Codes::EN_US, 56 | \Delight\I18n\Codes::DA_DK, 57 | \Delight\I18n\Codes::ES_AR, 58 | \Delight\I18n\Codes::ES, 59 | \Delight\I18n\Codes::KO_KR, 60 | \Delight\I18n\Codes::KO, 61 | \Delight\I18n\Codes::SW, 62 | \Delight\I18n\Codes::RU_RU 63 | ]); 64 | ($i18n instanceof \Delight\I18n\I18n) or \fail(__LINE__); 65 | 66 | 67 | try { 68 | $i18n->setDirectory(__DIR__ . '/../language'); 69 | $invalidPath = false; 70 | } 71 | catch (\Delight\I18n\Throwable\PathNotFoundError $e) { 72 | $invalidPath = true; 73 | } 74 | ($invalidPath === true) or \fail(__LINE__); 75 | 76 | 77 | $i18n->setDirectory(__DIR__ . '/../locale'); 78 | ($i18n->getDirectory() === \realpath(__DIR__ . '/../locale')) or \fail(__LINE__); 79 | 80 | 81 | ($i18n->getModule() === null) or \fail(__LINE__); 82 | $i18n->setModule('messages'); 83 | ($i18n->getModule() === 'messages') or \fail(__LINE__); 84 | $i18n->setModule(null); 85 | ($i18n->getModule() === null) or \fail(__LINE__); 86 | 87 | 88 | ($i18n->getSessionField() === null) or \fail(__LINE__); 89 | $i18n->setSessionField('locale'); 90 | ($i18n->getSessionField() === 'locale') or \fail(__LINE__); 91 | $i18n->setSessionField(null); 92 | ($i18n->getSessionField() === null) or \fail(__LINE__); 93 | 94 | 95 | ($i18n->getCookieName() === null) or \fail(__LINE__); 96 | $i18n->setCookieName('lc'); 97 | ($i18n->getCookieName() === 'lc') or \fail(__LINE__); 98 | $i18n->setCookieName(null); 99 | ($i18n->getCookieName() === null) or \fail(__LINE__); 100 | 101 | 102 | ($i18n->getCookieLifetime() === null) or \fail(__LINE__); 103 | $i18n->setCookieLifetime(60 * 60 * 24); 104 | ($i18n->getCookieLifetime() === (60 * 60 * 24)) or \fail(__LINE__); 105 | $i18n->setCookieLifetime(null); 106 | ($i18n->getCookieLifetime() === null) or \fail(__LINE__); 107 | 108 | 109 | try { 110 | $i18n->setLocaleManually('eS_Mx'); 111 | $invalidLocale = false; 112 | } 113 | catch (\Delight\I18n\Throwable\LocaleNotSupportedException $e) { 114 | $invalidLocale = true; 115 | } 116 | ($invalidLocale === true) or \fail(__LINE__); 117 | ($i18n->getLocale() === null) or \fail(__LINE__); 118 | 119 | 120 | try { 121 | $i18n->setLocaleManually('eS_Ar'); 122 | $invalidLocale = false; 123 | } 124 | catch (\Delight\I18n\Throwable\LocaleNotSupportedException $e) { 125 | $invalidLocale = true; 126 | } 127 | ($invalidLocale === false) or \fail(__LINE__); 128 | ($i18n->getLocale() === \Delight\I18n\Codes::ES_AR) or \fail(__LINE__); 129 | 130 | 131 | $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'pt-BR, pt;q=0.8, en;q=0.4, jp;q=0.6, *;q=0.2'; 132 | $i18n->setLocaleAutomatically(); 133 | ($i18n->getLocale() === \Delight\I18n\Codes::EN_US) or \fail(__LINE__); 134 | 135 | 136 | echo 'Locale:' . "\t\t\t" . $i18n->getLocale() . "\n"; 137 | echo 'System locale:' . "\t\t" . $i18n->getSystemLocale() . "\n"; 138 | 139 | 140 | (\strftime("%A %e %B %Y", -14182916) === 'Sunday 20 July 1969') or \fail(__LINE__); 141 | 142 | 143 | (_('Welcome to our online store!') === 'Welcome to our online store!') or \fail(__LINE__); 144 | (_('You have been successfully logged out.') === 'You have been successfully logged out.') or \fail(__LINE__); 145 | 146 | 147 | (_f('Hello %s!', 'Jane') === 'Hello Jane!') or \fail(__LINE__); 148 | (_f('%1$s is %2$d years old.', 'John', 30) === 'John is 30 years old.') or \fail(__LINE__); 149 | (_f('%1$d %% of the market is controlled by %2$s.', 65, 'Acme Corporation') === '65 % of the market is controlled by Acme Corporation.') or \fail(__LINE__); 150 | (_f('Languages like %s use the curly brackets { and } to denote blocks.', 'C') === 'Languages like C use the curly brackets { and } to denote blocks.') or \fail(__LINE__); 151 | 152 | 153 | $filename1 = 'example.csv'; 154 | $filename2 = 'example.json'; 155 | 156 | 157 | $rw = true; 158 | (_f('File %1$s has %2$s protection', $filename1, ($rw ? _('write') : _('read'))) === 'File example.csv has write protection') or \fail(__LINE__); 159 | (($rw ? _f('File %1$s has write protection', $filename1) : _f('File %1$s has read protection', $filename1)) === 'File example.csv has write protection') or \fail(__LINE__); 160 | $rw = false; 161 | (_f('File %1$s has %2$s protection', $filename1, ($rw ? _('write') : _('read'))) === 'File example.csv has read protection') or \fail(__LINE__); 162 | (($rw ? _f('File %1$s has write protection', $filename1) : _f('File %1$s has read protection', $filename1)) === 'File example.csv has read protection') or \fail(__LINE__); 163 | 164 | 165 | ((_('Replace ') . $filename1 . _(' with ') . $filename2) === 'Replace example.csv with example.json') or \fail(__LINE__); 166 | (_f('Replace %1$s with %2$s', $filename1, $filename2) === 'Replace example.csv with example.json') or \fail(__LINE__); 167 | 168 | 169 | (_fe('Hello {0}!', 'Jane') === 'Hello Jane!') or \fail(__LINE__); 170 | (_fe('{0} is {1, number} years old.', 'John', 30) === 'John is 30 years old.') or \fail(__LINE__); 171 | (_fe('{0, number} % of the market is controlled by {1}.', 65, 'Acme Corporation') === '65 % of the market is controlled by Acme Corporation.') or \fail(__LINE__); 172 | (_fe('Languages like {0} use the curly brackets \'{\' and \'}\' to denote blocks.', 'C') === 'Languages like C use the curly brackets { and } to denote blocks.') or \fail(__LINE__); 173 | 174 | 175 | $rw = true; 176 | (_fe('File {0} has {1} protection', $filename1, ($rw ? _('write') : _('read'))) === 'File example.csv has write protection') or \fail(__LINE__); 177 | (($rw ? _fe('File {0} has write protection', $filename1) : _fe('File {0} has read protection', $filename1)) === 'File example.csv has write protection') or \fail(__LINE__); 178 | $rw = false; 179 | (_fe('File {0} has {1} protection', $filename1, ($rw ? _('write') : _('read'))) === 'File example.csv has read protection') or \fail(__LINE__); 180 | (($rw ? _fe('File {0} has write protection', $filename1) : _fe('File {0} has read protection', $filename1)) === 'File example.csv has read protection') or \fail(__LINE__); 181 | 182 | 183 | ((_('Replace ') . $filename1 . _(' with ') . $filename2) === 'Replace example.csv with example.json') or \fail(__LINE__); 184 | (_fe('Replace {0} with {1}', $filename1, $filename2) === 'Replace example.csv with example.json') or \fail(__LINE__); 185 | 186 | 187 | try { 188 | _f('Hello %s!'); 189 | $formattingError = false; 190 | } 191 | catch (\Delight\I18n\Throwable\FormattingError $e) { 192 | $formattingError = true; 193 | } 194 | ($formattingError === true) or \fail(__LINE__); 195 | 196 | 197 | try { 198 | _f('Hello %s!', 'Jane'); 199 | $formattingError = false; 200 | } 201 | catch (\Delight\I18n\Throwable\FormattingError $e) { 202 | $formattingError = true; 203 | } 204 | ($formattingError === false) or \fail(__LINE__); 205 | 206 | 207 | try { 208 | _f('Hello %s!', 'Jane', 'John'); 209 | $formattingError = false; 210 | } 211 | catch (\Delight\I18n\Throwable\FormattingError $e) { 212 | $formattingError = true; 213 | } 214 | ($formattingError === false) or \fail(__LINE__); 215 | 216 | 217 | try { 218 | _fe('Hello {0}!'); 219 | $formattingError = false; 220 | } 221 | catch (\Delight\I18n\Throwable\FormattingError $e) { 222 | $formattingError = true; 223 | } 224 | ($formattingError === false) or \fail(__LINE__); 225 | 226 | 227 | try { 228 | _fe('Hello {0}!', 'Jane'); 229 | $formattingError = false; 230 | } 231 | catch (\Delight\I18n\Throwable\FormattingError $e) { 232 | $formattingError = true; 233 | } 234 | ($formattingError === false) or \fail(__LINE__); 235 | 236 | 237 | try { 238 | _fe('Hello {0}!', 'Jane', 'John'); 239 | $formattingError = false; 240 | } 241 | catch (\Delight\I18n\Throwable\FormattingError $e) { 242 | $formattingError = true; 243 | } 244 | ($formattingError === false) or \fail(__LINE__); 245 | 246 | 247 | (_p('The file has been saved.', 'The files have been saved.', 1) === 'The file has been saved.') or \fail(__LINE__); 248 | (_p('The file has been saved.', 'The files have been saved.', 2) === 'The files have been saved.') or \fail(__LINE__); 249 | (_p('The file has been saved.', 'The files have been saved.', 3) === 'The files have been saved.') or \fail(__LINE__); 250 | 251 | 252 | (_pf('There is %d monkey.', 'There are %d monkeys.', 0) === 'There are 0 monkeys.') or \fail(__LINE__); 253 | (_pf('There is %d monkey.', 'There are %d monkeys.', 1) === 'There is 1 monkey.') or \fail(__LINE__); 254 | (_pf('There is %d monkey.', 'There are %d monkeys.', 2) === 'There are 2 monkeys.') or \fail(__LINE__); 255 | (_pf('There is %1$d monkey in %2$s.', 'There are %1$d monkeys in %2$s.', 3, 'Anytown') === 'There are 3 monkeys in Anytown.') or \fail(__LINE__); 256 | 257 | 258 | (_pf('You have %d new message', 'You have %d new messages', 1) === 'You have 1 new message') or \fail(__LINE__); 259 | (_pf('You have %d new message', 'You have %d new messages', 32) === 'You have 32 new messages') or \fail(__LINE__); 260 | 261 | 262 | (_pfe('There is {0, number} monkey.', 'There are {0, number} monkeys.', 0) === 'There are 0 monkeys.') or \fail(__LINE__); 263 | (_pfe('There is {0, number} monkey.', 'There are {0, number} monkeys.', 1) === 'There is 1 monkey.') or \fail(__LINE__); 264 | (_pfe('There is {0, number} monkey.', 'There are {0, number} monkeys.', 2) === 'There are 2 monkeys.') or \fail(__LINE__); 265 | (_pfe('There is {0, number} monkey in {1}.', 'There are {0, number} monkeys in {1}.', 3, 'Anytown') === 'There are 3 monkeys in Anytown.') or \fail(__LINE__); 266 | 267 | 268 | (_pfe('You have {0, number} new message', 'You have {0, number} new messages', 1) === 'You have 1 new message') or \fail(__LINE__); 269 | (_pfe('You have {0, number} new message', 'You have {0, number} new messages', 32) === 'You have 32 new messages') or \fail(__LINE__); 270 | 271 | 272 | (_c('Order', 'sorting') === 'Order') or \fail(__LINE__); 273 | (_c('Order', 'purchase') === 'Order') or \fail(__LINE__); 274 | (_c('Order', 'mathematics') === 'Order') or \fail(__LINE__); 275 | (_c('Order', 'classification') === 'Order') or \fail(__LINE__); 276 | 277 | 278 | (_c('Address:', 'location') === 'Address:') or \fail(__LINE__); 279 | (_c('Address:', 'www') === 'Address:') or \fail(__LINE__); 280 | (_c('Address:', 'email') === 'Address:') or \fail(__LINE__); 281 | (_c('Address:', 'letter') === 'Address:') or \fail(__LINE__); 282 | (_c('Address:', 'speech') === 'Address:') or \fail(__LINE__); 283 | 284 | 285 | (_m('User') === 'User') or \fail(__LINE__); 286 | $text = 'User'; 287 | (_($text) === 'User') or \fail(__LINE__); 288 | (_m('Moderator') === 'Moderator') or \fail(__LINE__); 289 | $text = 'Moderator'; 290 | (_($text) === 'Moderator') or \fail(__LINE__); 291 | (_m('Administrator') === 'Administrator') or \fail(__LINE__); 292 | $text = 'Administrator'; 293 | (_($text) === 'Administrator') or \fail(__LINE__); 294 | 295 | 296 | ($i18n->getLocaleName() === 'English (United States)') or \fail(__LINE__); 297 | ($i18n->getLocaleName('fr-BE') === 'French (Belgium)') or \fail(__LINE__); 298 | ($i18n->getNativeLocaleName() === 'English (United States)') or \fail(__LINE__); 299 | ($i18n->getNativeLocaleName('fr-BE') === 'français (Belgique)') or \fail(__LINE__); 300 | ($i18n->getLanguageName() === 'English') or \fail(__LINE__); 301 | ($i18n->getLanguageName('fr-BE') === 'French') or \fail(__LINE__); 302 | ($i18n->getNativeLanguageName() === 'English') or \fail(__LINE__); 303 | ($i18n->getNativeLanguageName('fr-BE') === 'français') or \fail(__LINE__); 304 | 305 | 306 | (\Delight\I18n\Locale::toName('nb-NO') === 'Norwegian Bokmål (Norway)') or \fail(__LINE__); 307 | (\Delight\I18n\Locale::toName('ru-UA') === 'Russian (Ukraine)') or \fail(__LINE__); 308 | (\Delight\I18n\Locale::toNativeName('nb-NO') === 'norsk bokmål (Norge)') or \fail(__LINE__); 309 | (\Delight\I18n\Locale::toNativeName('ru-UA') === 'русский (Украина)') or \fail(__LINE__); 310 | (\Delight\I18n\Locale::toEnglishName('nb-NO') === 'Norwegian Bokmål (Norway)') or \fail(__LINE__); 311 | (\Delight\I18n\Locale::toEnglishName('ru-UA') === 'Russian (Ukraine)') or \fail(__LINE__); 312 | (\Delight\I18n\Locale::toLanguageName('nb-NO') === 'Norwegian Bokmål') or \fail(__LINE__); 313 | (\Delight\I18n\Locale::toLanguageName('ru-UA') === 'Russian') or \fail(__LINE__); 314 | (\Delight\I18n\Locale::toNativeLanguageName('nb-NO') === 'norsk bokmål') or \fail(__LINE__); 315 | (\Delight\I18n\Locale::toNativeLanguageName('ru-UA') === 'русский') or \fail(__LINE__); 316 | (\Delight\I18n\Locale::toEnglishLanguageName('nb-NO') === 'Norwegian Bokmål') or \fail(__LINE__); 317 | (\Delight\I18n\Locale::toEnglishLanguageName('ru-UA') === 'Russian') or \fail(__LINE__); 318 | (\Delight\I18n\Locale::toScriptName('nb-Latn-NO') === 'Latin') or \fail(__LINE__); 319 | (\Delight\I18n\Locale::toScriptName('ru-Cyrl-UA') === 'Cyrillic') or \fail(__LINE__); 320 | (\Delight\I18n\Locale::toNativeScriptName('nb-Latn-NO') === 'latinsk') or \fail(__LINE__); 321 | (\Delight\I18n\Locale::toNativeScriptName('ru-Cyrl-UA') === 'кириллица') or \fail(__LINE__); 322 | (\Delight\I18n\Locale::toEnglishScriptName('nb-Latn-NO') === 'Latin') or \fail(__LINE__); 323 | (\Delight\I18n\Locale::toEnglishScriptName('ru-Cyrl-UA') === 'Cyrillic') or \fail(__LINE__); 324 | (\Delight\I18n\Locale::toRegionName('nb-NO') === 'Norway') or \fail(__LINE__); 325 | (\Delight\I18n\Locale::toRegionName('ru-UA') === 'Ukraine') or \fail(__LINE__); 326 | (\Delight\I18n\Locale::toNativeRegionName('nb-NO') === 'Norge') or \fail(__LINE__); 327 | (\Delight\I18n\Locale::toNativeRegionName('ru-UA') === 'Украина') or \fail(__LINE__); 328 | (\Delight\I18n\Locale::toEnglishRegionName('nb-NO') === 'Norway') or \fail(__LINE__); 329 | (\Delight\I18n\Locale::toEnglishRegionName('ru-UA') === 'Ukraine') or \fail(__LINE__); 330 | (\Delight\I18n\Locale::toLanguageCode('nb-Latn-NO') === 'nb') or \fail(__LINE__); 331 | (\Delight\I18n\Locale::toLanguageCode('ru-Cyrl-UA') === 'ru') or \fail(__LINE__); 332 | (\Delight\I18n\Locale::toScriptCode('nb-Latn-NO') === 'Latn') or \fail(__LINE__); 333 | (\Delight\I18n\Locale::toScriptCode('ru-Cyrl-UA') === 'Cyrl') or \fail(__LINE__); 334 | (\Delight\I18n\Locale::toRegionCode('nb-Latn-NO') === 'NO') or \fail(__LINE__); 335 | (\Delight\I18n\Locale::toRegionCode('ru-Cyrl-UA') === 'UA') or \fail(__LINE__); 336 | (\Delight\I18n\Locale::isRtl('ln-CD') === false) or \fail(__LINE__); 337 | (\Delight\I18n\Locale::isRtl('ur-PK') === true) or \fail(__LINE__); 338 | (\Delight\I18n\Locale::isLtr('ln-CD') === true) or \fail(__LINE__); 339 | (\Delight\I18n\Locale::isLtr('ur-PK') === false) or \fail(__LINE__); 340 | 341 | 342 | (\Delight\I18n\Http::parseClientLanguage('hello') == [ null, null, null ]) or \fail(__LINE__); 343 | (\Delight\I18n\Http::parseClientLanguage('am') === [ 'am', null, null ]) or \fail(__LINE__); 344 | (\Delight\I18n\Http::parseClientLanguage('amha') === [ null, null, null ]) or \fail(__LINE__); 345 | (\Delight\I18n\Http::parseClientLanguage('am-ET') === [ 'am', null, 'ET' ]) or \fail(__LINE__); 346 | (\Delight\I18n\Http::parseClientLanguage('am-ETH') === [ null, null, null ]) or \fail(__LINE__); 347 | (\Delight\I18n\Http::parseClientLanguage('am-Ethi') === [ 'am', 'Ethi', null ]) or \fail(__LINE__); 348 | (\Delight\I18n\Http::parseClientLanguage('am-Ethio') === [ null, null, null ]) or \fail(__LINE__); 349 | (\Delight\I18n\Http::parseClientLanguage('am-Ethi-ET') === [ 'am', 'Ethi', 'ET' ]) or \fail(__LINE__); 350 | (\Delight\I18n\Http::parseClientLanguage('am-Ethio-ET') === [ null, null, null ]) or \fail(__LINE__); 351 | 352 | 353 | (\Delight\I18n\Http::matchClientLanguages( 354 | [ 'de-Latn-CH', 'de-CH', 'de-Latn-DE', 'de-DE', 'de' ], 355 | \Delight\I18n\Leniency::NONE, 356 | 'de-DE' 357 | ) === 'de-DE') or \fail(__LINE__); 358 | (\Delight\I18n\Http::matchClientLanguages( 359 | [ 'de-Latn-CH', 'de-CH', 'de-Latn-DE', 'de-DE', 'de' ], 360 | \Delight\I18n\Leniency::VERY_LOW, 361 | 'de-DE' 362 | ) === 'de-DE') or \fail(__LINE__); 363 | (\Delight\I18n\Http::matchClientLanguages( 364 | [ 'de-Latn-CH', 'de-CH', 'de-Latn-DE', 'de-DE', 'de' ], 365 | \Delight\I18n\Leniency::MODERATE, 366 | 'de-DE' 367 | ) === 'de-DE') or \fail(__LINE__); 368 | (\Delight\I18n\Http::matchClientLanguages( 369 | [ 'de-Latn-CH', 'de-CH', 'de-Latn-DE', 'de-DE', 'de' ], 370 | null, 371 | 'de-DE' 372 | ) === 'de-DE') or \fail(__LINE__); 373 | (\Delight\I18n\Http::matchClientLanguages( 374 | [ 'de-Latn-CH', 'de-CH', 'de-Latn-DE', 'de-DE', 'de' ], 375 | \Delight\I18n\Leniency::VERY_HIGH, 376 | 'de-DE' 377 | ) === 'de-DE') or \fail(__LINE__); 378 | (\Delight\I18n\Http::matchClientLanguages( 379 | [ 'de-Latn-CH', 'de-CH', 'de-Latn-DE', 'de-DE', 'de' ], 380 | \Delight\I18n\Leniency::FULL, 381 | 'de-DE' 382 | ) === 'de-DE') or \fail(__LINE__); 383 | 384 | 385 | (\Delight\I18n\Http::matchClientLanguages( 386 | [ 'de-Latn-CH', 'de-CH', 'de-Latn-DE', 'de' ], 387 | \Delight\I18n\Leniency::NONE, 388 | 'de-DE' 389 | ) === null) or \fail(__LINE__); 390 | (\Delight\I18n\Http::matchClientLanguages( 391 | [ 'de-Latn-CH', 'de-CH', 'de-Latn-DE', 'de' ], 392 | \Delight\I18n\Leniency::VERY_LOW, 393 | 'de-DE' 394 | ) === 'de') or \fail(__LINE__); 395 | (\Delight\I18n\Http::matchClientLanguages( 396 | [ 'de-Latn-CH', 'de-CH', 'de-Latn-DE', 'de' ], 397 | \Delight\I18n\Leniency::MODERATE, 398 | 'de-DE' 399 | ) === 'de') or \fail(__LINE__); 400 | (\Delight\I18n\Http::matchClientLanguages( 401 | [ 'de-Latn-CH', 'de-CH', 'de-Latn-DE', 'de' ], 402 | null, 403 | 'de-DE' 404 | ) === 'de') or \fail(__LINE__); 405 | (\Delight\I18n\Http::matchClientLanguages( 406 | [ 'de-Latn-CH', 'de-CH', 'de-Latn-DE', 'de' ], 407 | \Delight\I18n\Leniency::VERY_HIGH, 408 | 'de-DE' 409 | ) === 'de') or \fail(__LINE__); 410 | (\Delight\I18n\Http::matchClientLanguages( 411 | [ 'de-Latn-CH', 'de-CH', 'de-Latn-DE', 'de' ], 412 | \Delight\I18n\Leniency::FULL, 413 | 'de-DE' 414 | ) === 'de') or \fail(__LINE__); 415 | 416 | 417 | (\Delight\I18n\Http::matchClientLanguages( 418 | [ 'de-DE' ], 419 | \Delight\I18n\Leniency::NONE, 420 | 'de-Latn-CH, de-CH;q=0.8, de-Latn-DE;q=0.6, de-DE;q=0.4, de;q=0.2' 421 | ) === 'de-DE') or \fail(__LINE__); 422 | (\Delight\I18n\Http::matchClientLanguages( 423 | [ 'de-DE' ], 424 | \Delight\I18n\Leniency::VERY_LOW, 425 | 'de-Latn-CH, de-CH;q=0.8, de-Latn-DE;q=0.6, de-DE;q=0.4, de;q=0.2' 426 | ) === 'de-DE') or \fail(__LINE__); 427 | (\Delight\I18n\Http::matchClientLanguages( 428 | [ 'de-DE' ], 429 | \Delight\I18n\Leniency::MODERATE, 430 | 'de-Latn-CH, de-CH;q=0.8, de-Latn-DE;q=0.6, de-DE;q=0.4, de;q=0.2' 431 | ) === 'de-DE') or \fail(__LINE__); 432 | (\Delight\I18n\Http::matchClientLanguages( 433 | [ 'de-DE' ], 434 | null, 435 | 'de-Latn-CH, de-CH;q=0.8, de-Latn-DE;q=0.6, de-DE;q=0.4, de;q=0.2' 436 | ) === 'de-DE') or \fail(__LINE__); 437 | (\Delight\I18n\Http::matchClientLanguages( 438 | [ 'de-DE' ], 439 | \Delight\I18n\Leniency::VERY_HIGH, 440 | 'de-Latn-CH, de-CH;q=0.8, de-Latn-DE;q=0.6, de-DE;q=0.4, de;q=0.2' 441 | ) === 'de-DE') or \fail(__LINE__); 442 | (\Delight\I18n\Http::matchClientLanguages( 443 | [ 'de-DE' ], 444 | \Delight\I18n\Leniency::FULL, 445 | 'de-Latn-CH, de-CH;q=0.8, de-Latn-DE;q=0.6, de-DE;q=0.4, de;q=0.2' 446 | ) === 'de-DE') or \fail(__LINE__); 447 | 448 | 449 | (\Delight\I18n\Http::matchClientLanguages( 450 | [ 'de-DE' ], 451 | \Delight\I18n\Leniency::NONE, 452 | 'de-Latn-CH, de-Latn-DE;q=0.5' 453 | ) === null) or \fail(__LINE__); 454 | (\Delight\I18n\Http::matchClientLanguages( 455 | [ 'de-DE' ], 456 | \Delight\I18n\Leniency::VERY_LOW, 457 | 'de-Latn-CH, de-Latn-DE;q=0.5' 458 | ) === null) or \fail(__LINE__); 459 | (\Delight\I18n\Http::matchClientLanguages( 460 | [ 'de-DE' ], 461 | \Delight\I18n\Leniency::MODERATE, 462 | 'de-Latn-CH, de-Latn-DE;q=0.5' 463 | ) === 'de-DE') or \fail(__LINE__); 464 | (\Delight\I18n\Http::matchClientLanguages( 465 | [ 'de-DE' ], 466 | null, 467 | 'de-Latn-CH, de-Latn-DE;q=0.5' 468 | ) === 'de-DE') or \fail(__LINE__); 469 | (\Delight\I18n\Http::matchClientLanguages( 470 | [ 'de-DE' ], 471 | \Delight\I18n\Leniency::VERY_HIGH, 472 | 'de-Latn-CH, de-Latn-DE;q=0.5' 473 | ) === 'de-DE') or \fail(__LINE__); 474 | (\Delight\I18n\Http::matchClientLanguages( 475 | [ 'de-DE' ], 476 | \Delight\I18n\Leniency::FULL, 477 | 'de-Latn-CH, de-Latn-DE;q=0.5' 478 | ) === 'de-DE') or \fail(__LINE__); 479 | 480 | 481 | (\Delight\I18n\Http::matchClientLanguages( 482 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 483 | null, 484 | 'KO-KR, ES-AR;q=0.5, SW;q=0.5, EN;q=0.5, ES;q=0.5, KO;q=0.5, RU-RU;q=0.5, DA-DK;q=0.5' 485 | ) === 'ko-KR') or \fail(__LINE__); 486 | (\Delight\I18n\Http::matchClientLanguages( 487 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 488 | null, 489 | 'ko-KR, eS-Ar;q=0.5, sw;q=0.5, eN;q=0.5, Es;q=0.5, KO;q=0.5, RU-ru;q=0.5, Da-dK;q=0.5' 490 | ) === 'ko-KR') or \fail(__LINE__); 491 | (\Delight\I18n\Http::matchClientLanguages( 492 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 493 | null, 494 | 'ko-kr, es-ar;q=0.5, sw;q=0.5, en;q=0.5, es;q=0.5, ko;q=0.5, ru-ru;q=0.5, da-dk;q=0.5' 495 | ) === 'ko-KR') or \fail(__LINE__); 496 | (\Delight\I18n\Http::matchClientLanguages( 497 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 498 | null, 499 | 'KO-KR,ES-AR;q=0.5,SW;q=0.5,EN;q=0.5,ES;q=0.5,KO;q=0.5,RU-RU;q=0.5,DA-DK;q=0.5' 500 | ) === 'ko-KR') or \fail(__LINE__); 501 | (\Delight\I18n\Http::matchClientLanguages( 502 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 503 | null, 504 | 'ko-KR,eS-Ar;q=0.5,sw;q=0.5,eN;q=0.5,Es;q=0.5,KO;q=0.5,RU-ru;q=0.5,Da-dK;q=0.5' 505 | ) === 'ko-KR') or \fail(__LINE__); 506 | (\Delight\I18n\Http::matchClientLanguages( 507 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 508 | null, 509 | 'ko-kr,es-ar;q=0.5,sw;q=0.5,en;q=0.5,es;q=0.5,ko;q=0.5,ru-ru;q=0.5,da-dk;q=0.5' 510 | ) === 'ko-KR') or \fail(__LINE__); 511 | 512 | 513 | (\Delight\I18n\Http::matchClientLanguages( 514 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 515 | null, 516 | 'KO-KR;q=0.5, ES-AR;q=0.5, SW, EN;q=0.5, ES;q=0.5, KO;q=0.5, RU-RU;q=0.5, DA-DK;q=0.5' 517 | ) === 'sw') or \fail(__LINE__); 518 | (\Delight\I18n\Http::matchClientLanguages( 519 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 520 | null, 521 | 'ko-KR;q=0.5, eS-Ar;q=0.5, sw, eN;q=0.5, Es;q=0.5, KO;q=0.5, RU-ru;q=0.5, Da-dK;q=0.5' 522 | ) === 'sw') or \fail(__LINE__); 523 | (\Delight\I18n\Http::matchClientLanguages( 524 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 525 | null, 526 | 'ko-kr;q=0.5, es-ar;q=0.5, sw, en;q=0.5, es;q=0.5, ko;q=0.5, ru-ru;q=0.5, da-dk;q=0.5' 527 | ) === 'sw') or \fail(__LINE__); 528 | (\Delight\I18n\Http::matchClientLanguages( 529 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 530 | null, 531 | 'KO-KR;q=0.5,ES-AR;q=0.5,SW,EN;q=0.5,ES;q=0.5,KO;q=0.5,RU-RU;q=0.5,DA-DK;q=0.5' 532 | ) === 'sw') or \fail(__LINE__); 533 | (\Delight\I18n\Http::matchClientLanguages( 534 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 535 | null, 536 | 'ko-KR;q=0.5,eS-Ar;q=0.5,sw,eN;q=0.5,Es;q=0.5,KO;q=0.5,RU-ru;q=0.5,Da-dK;q=0.5' 537 | ) === 'sw') or \fail(__LINE__); 538 | (\Delight\I18n\Http::matchClientLanguages( 539 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 540 | null, 541 | 'ko-kr;q=0.5,es-ar;q=0.5,sw,en;q=0.5,es;q=0.5,ko;q=0.5,ru-ru;q=0.5,da-dk;q=0.5' 542 | ) === 'sw') or \fail(__LINE__); 543 | 544 | 545 | (\Delight\I18n\Http::matchClientLanguages( 546 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 547 | null, 548 | 'KO-KR;q=0.5, ES-AR;q=0.5, SW;q=0.5, EN;q=0.5, ES;q=0.5, KO;q=0.5, RU-RU, DA-DK;q=0.5' 549 | ) === 'RU-ru') or \fail(__LINE__); 550 | (\Delight\I18n\Http::matchClientLanguages( 551 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 552 | null, 553 | 'ko-KR;q=0.5, eS-Ar;q=0.5, sw;q=0.5, eN;q=0.5, Es;q=0.5, KO;q=0.5, RU-ru, Da-dK;q=0.5' 554 | ) === 'RU-ru') or \fail(__LINE__); 555 | (\Delight\I18n\Http::matchClientLanguages( 556 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 557 | null, 558 | 'ko-kr;q=0.5, es-ar;q=0.5, sw;q=0.5, en;q=0.5, es;q=0.5, ko;q=0.5, ru-ru, da-dk;q=0.5' 559 | ) === 'RU-ru') or \fail(__LINE__); 560 | (\Delight\I18n\Http::matchClientLanguages( 561 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 562 | null, 563 | 'KO-KR;q=0.5,ES-AR;q=0.5,SW;q=0.5,EN;q=0.5,ES;q=0.5,KO;q=0.5,RU-RU,DA-DK;q=0.5' 564 | ) === 'RU-ru') or \fail(__LINE__); 565 | (\Delight\I18n\Http::matchClientLanguages( 566 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 567 | null, 568 | 'ko-KR;q=0.5,eS-Ar;q=0.5,sw;q=0.5,eN;q=0.5,Es;q=0.5,KO;q=0.5,RU-ru,Da-dK;q=0.5' 569 | ) === 'RU-ru') or \fail(__LINE__); 570 | (\Delight\I18n\Http::matchClientLanguages( 571 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 572 | null, 573 | 'ko-kr;q=0.5,es-ar;q=0.5,sw;q=0.5,en;q=0.5,es;q=0.5,ko;q=0.5,ru-ru,da-dk;q=0.5' 574 | ) === 'RU-ru') or \fail(__LINE__); 575 | 576 | 577 | (\Delight\I18n\Http::matchClientLanguages( 578 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 579 | null, 580 | 'KO-KR;q=0.5, ES-AR;q=0.75, SW;q=0.5, EN;q=0.5, ES;q=0.5, KO;q=0.5, RU-RU;q=0.5, DA-DK;q=0.5' 581 | ) === 'eS-Ar') or \fail(__LINE__); 582 | (\Delight\I18n\Http::matchClientLanguages( 583 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 584 | null, 585 | 'ko-KR;q=0.5, eS-Ar;q=0.75, sw;q=0.5, eN;q=0.5, Es;q=0.5, KO;q=0.5, RU-ru;q=0.5, Da-dK;q=0.5' 586 | ) === 'eS-Ar') or \fail(__LINE__); 587 | (\Delight\I18n\Http::matchClientLanguages( 588 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 589 | null, 590 | 'ko-kr;q=0.5, es-ar;q=0.75, sw;q=0.5, en;q=0.5, es;q=0.5, ko;q=0.5, ru-ru;q=0.5, da-dk;q=0.5' 591 | ) === 'eS-Ar') or \fail(__LINE__); 592 | (\Delight\I18n\Http::matchClientLanguages( 593 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 594 | null, 595 | 'KO-KR;q=0.5,ES-AR;q=0.75,SW;q=0.5,EN;q=0.5,ES;q=0.5,KO;q=0.5,RU-RU;q=0.5,DA-DK;q=0.5' 596 | ) === 'eS-Ar') or \fail(__LINE__); 597 | (\Delight\I18n\Http::matchClientLanguages( 598 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 599 | null, 600 | 'ko-KR;q=0.5,eS-Ar;q=0.75,sw;q=0.5,eN;q=0.5,Es;q=0.5,KO;q=0.5,RU-ru;q=0.5,Da-dK;q=0.5' 601 | ) === 'eS-Ar') or \fail(__LINE__); 602 | (\Delight\I18n\Http::matchClientLanguages( 603 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 604 | null, 605 | 'ko-kr;q=0.5,es-ar;q=0.75,sw;q=0.5,en;q=0.5,es;q=0.5,ko;q=0.5,ru-ru;q=0.5,da-dk;q=0.5' 606 | ) === 'eS-Ar') or \fail(__LINE__); 607 | 608 | 609 | (\Delight\I18n\Http::matchClientLanguages( 610 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 611 | null, 612 | 'ko;q=0.4' 613 | ) === 'KO') or \fail(__LINE__); 614 | (\Delight\I18n\Http::matchClientLanguages( 615 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 616 | null, 617 | 'ko-KR;q=0.4' 618 | ) === 'ko-KR') or \fail(__LINE__); 619 | (\Delight\I18n\Http::matchClientLanguages( 620 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 621 | null, 622 | 'ko-Hang-KR;q=0.4' 623 | ) === 'ko-KR') or \fail(__LINE__); 624 | 625 | 626 | (\Delight\I18n\Http::matchClientLanguages( 627 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 628 | null, 629 | 'es;q=0.4' 630 | ) === 'Es') or \fail(__LINE__); 631 | (\Delight\I18n\Http::matchClientLanguages( 632 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 633 | null, 634 | 'es-AR;q=0.4' 635 | ) === 'eS-Ar') or \fail(__LINE__); 636 | (\Delight\I18n\Http::matchClientLanguages( 637 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 638 | null, 639 | 'es-Latn-AR;q=0.4' 640 | ) === 'eS-Ar') or \fail(__LINE__); 641 | 642 | 643 | (\Delight\I18n\Http::matchClientLanguages( 644 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 645 | null, 646 | 'sw;q=0.4' 647 | ) === 'sw') or \fail(__LINE__); 648 | (\Delight\I18n\Http::matchClientLanguages( 649 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 650 | null, 651 | 'sw-KE;q=0.4' 652 | ) === 'sw') or \fail(__LINE__); 653 | (\Delight\I18n\Http::matchClientLanguages( 654 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 655 | null, 656 | 'sw-Latn-KE;q=0.4' 657 | ) === 'sw') or \fail(__LINE__); 658 | 659 | 660 | (\Delight\I18n\Http::matchClientLanguages( 661 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 662 | null, 663 | 'en;q=0.4' 664 | ) === 'eN') or \fail(__LINE__); 665 | (\Delight\I18n\Http::matchClientLanguages( 666 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 667 | null, 668 | 'en-GB;q=0.4' 669 | ) === 'eN') or \fail(__LINE__); 670 | (\Delight\I18n\Http::matchClientLanguages( 671 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 672 | null, 673 | 'en-Latn-GB;q=0.4' 674 | ) === 'eN') or \fail(__LINE__); 675 | 676 | 677 | (\Delight\I18n\Http::matchClientLanguages( 678 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 679 | null, 680 | 'es;q=0.4' 681 | ) === 'Es') or \fail(__LINE__); 682 | (\Delight\I18n\Http::matchClientLanguages( 683 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 684 | null, 685 | 'es-419;q=0.4' 686 | ) === 'Es') or \fail(__LINE__); 687 | (\Delight\I18n\Http::matchClientLanguages( 688 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 689 | null, 690 | 'es-Latn-419;q=0.4' 691 | ) === 'Es') or \fail(__LINE__); 692 | 693 | 694 | (\Delight\I18n\Http::matchClientLanguages( 695 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 696 | null, 697 | 'ru;q=0.4' 698 | ) === 'RU-ru') or \fail(__LINE__); 699 | (\Delight\I18n\Http::matchClientLanguages( 700 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 701 | null, 702 | 'ru-RU;q=0.4' 703 | ) === 'RU-ru') or \fail(__LINE__); 704 | (\Delight\I18n\Http::matchClientLanguages( 705 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 706 | null, 707 | 'ru-Cyrl-RU;q=0.4' 708 | ) === 'RU-ru') or \fail(__LINE__); 709 | 710 | 711 | (\Delight\I18n\Http::matchClientLanguages( 712 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 713 | null, 714 | 'da;q=0.4' 715 | ) === 'Da-dK') or \fail(__LINE__); 716 | (\Delight\I18n\Http::matchClientLanguages( 717 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 718 | null, 719 | 'da-DK;q=0.4' 720 | ) === 'Da-dK') or \fail(__LINE__); 721 | (\Delight\I18n\Http::matchClientLanguages( 722 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 723 | null, 724 | 'da-Latn-DK;q=0.4' 725 | ) === 'Da-dK') or \fail(__LINE__); 726 | 727 | 728 | (\Delight\I18n\Http::matchClientLanguages( 729 | [ 'eN', 'Da-dK', 'eS-Ar', 'Es', 'ko-KR', 'KO', 'sw', 'RU-ru' ], 730 | null, 731 | 'fr-CA' 732 | ) === null) or \fail(__LINE__); 733 | 734 | 735 | (\Delight\I18n\Http::matchClientLanguages( 736 | [ 'ar', 'en' ], 737 | null, 738 | 'ar' 739 | ) === 'ar') or \fail(__LINE__); 740 | (\Delight\I18n\Http::matchClientLanguages( 741 | [ 'ar', 'en' ], 742 | null, 743 | 'ar-Arab, en-AU' 744 | ) === 'ar') or \fail(__LINE__); 745 | (\Delight\I18n\Http::matchClientLanguages( 746 | [ 'ar', 'en' ], 747 | null, 748 | 'ar-Latn, en-AU' 749 | ) === 'ar') or \fail(__LINE__); 750 | (\Delight\I18n\Http::matchClientLanguages( 751 | [ 'ar', 'en' ], 752 | null, 753 | 'ar-Arab-EG, en-AU' 754 | ) === 'ar') or \fail(__LINE__); 755 | (\Delight\I18n\Http::matchClientLanguages( 756 | [ 'ar', 'en' ], 757 | null, 758 | 'ar-Latn-EG, en-AU' 759 | ) === 'ar') or \fail(__LINE__); 760 | (\Delight\I18n\Http::matchClientLanguages( 761 | [ 'ar-Arab', 'en' ], 762 | null, 763 | 'ar' 764 | ) === 'ar-Arab') or \fail(__LINE__); 765 | (\Delight\I18n\Http::matchClientLanguages( 766 | [ 'ar-Arab', 'en' ], 767 | null, 768 | 'ar-Arab, en-AU' 769 | ) === 'ar-Arab') or \fail(__LINE__); 770 | (\Delight\I18n\Http::matchClientLanguages( 771 | [ 'ar-Arab', 'en' ], 772 | null, 773 | 'ar-Latn, en-AU' 774 | ) === 'en') or \fail(__LINE__); 775 | (\Delight\I18n\Http::matchClientLanguages( 776 | [ 'ar-Arab', 'en' ], 777 | null, 778 | 'ar-Arab-EG, en-AU' 779 | ) === 'ar-Arab') or \fail(__LINE__); 780 | (\Delight\I18n\Http::matchClientLanguages( 781 | [ 'ar-Arab', 'en' ], 782 | null, 783 | 'ar-Latn-EG, en-AU' 784 | ) === 'en') or \fail(__LINE__); 785 | (\Delight\I18n\Http::matchClientLanguages( 786 | [ 'ar-Arab-EG', 'en' ], 787 | null, 788 | 'ar' 789 | ) === 'ar-Arab-EG') or \fail(__LINE__); 790 | (\Delight\I18n\Http::matchClientLanguages( 791 | [ 'ar-Arab-EG', 'en' ], 792 | null, 793 | 'ar-Arab, en-AU' 794 | ) === 'ar-Arab-EG') or \fail(__LINE__); 795 | (\Delight\I18n\Http::matchClientLanguages( 796 | [ 'ar-Arab-EG', 'en' ], 797 | null, 798 | 'ar-Latn, en-AU' 799 | ) === 'en') or \fail(__LINE__); 800 | (\Delight\I18n\Http::matchClientLanguages( 801 | [ 'ar-Arab-EG', 'en' ], 802 | null, 803 | 'ar-Arab-EG, en-AU' 804 | ) === 'ar-Arab-EG') or \fail(__LINE__); 805 | (\Delight\I18n\Http::matchClientLanguages( 806 | [ 'ar-Arab-EG', 'en' ], 807 | null, 808 | 'ar-Latn-EG, en-AU' 809 | ) === 'en') or \fail(__LINE__); 810 | 811 | 812 | echo 'ALL TESTS PASSED' . "\n"; 813 | 814 | // END TESTS 815 | 816 | function fail($lineNumber) { 817 | exit('Error in line ' . $lineNumber); 818 | } 819 | --------------------------------------------------------------------------------