From d5adeffe5e488c9a56dab68ffae41544db21d7c5 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Tue, 31 Dec 2024 19:27:48 +0300 Subject: [PATCH 01/13] feat: Add parameter for locale comparison --- app/Config/Feature.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/Config/Feature.php b/app/Config/Feature.php index 35024d357919..6585f716d54b 100644 --- a/app/Config/Feature.php +++ b/app/Config/Feature.php @@ -26,4 +26,10 @@ class Feature extends BaseConfig * If false, `limit(0)` returns no records. (the behavior of 3.1.9 or later in version 3.x.) */ public bool $limitZeroAsAll = true; + + /** + * Use an strict localization comparison (with territory en-*) instead of an abbreviated value + * Previously, the territory was cut off (en-* as en) before localization comparing + */ + public bool $simpleNegotiateLocale = true; } From eac043a955278b3600511573acc2fb70554c1e01 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Tue, 31 Dec 2024 19:29:00 +0300 Subject: [PATCH 02/13] feat: Update language negotiator --- system/HTTP/Negotiate.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/HTTP/Negotiate.php b/system/HTTP/Negotiate.php index 33938c8f0741..e241d2b473e6 100644 --- a/system/HTTP/Negotiate.php +++ b/system/HTTP/Negotiate.php @@ -14,6 +14,7 @@ namespace CodeIgniter\HTTP; use CodeIgniter\HTTP\Exceptions\HTTPException; +use Config\Feature; /** * Class Negotiate @@ -127,7 +128,7 @@ public function encoding(array $supported = []): string */ public function language(array $supported): string { - return $this->getBestMatch($supported, $this->request->getHeaderLine('accept-language'), false, false, true); + return $this->getBestMatch($supported, $this->request->getHeaderLine('accept-language'), false, false, config(Feature::class)->simpleNegotiateLocale); } // -------------------------------------------------------------------- From 63d7c9416c88adeb94d5ed73ad1cece02530d7f5 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Tue, 31 Dec 2024 19:30:17 +0300 Subject: [PATCH 03/13] tests: Update `NegotiateTest` --- tests/system/HTTP/NegotiateTest.php | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/system/HTTP/NegotiateTest.php b/tests/system/HTTP/NegotiateTest.php index 18d546c0a9c6..5417a1ec758d 100644 --- a/tests/system/HTTP/NegotiateTest.php +++ b/tests/system/HTTP/NegotiateTest.php @@ -16,6 +16,7 @@ use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\Test\CIUnitTestCase; use Config\App; +use Config\Feature; use PHPUnit\Framework\Attributes\Group; /** @@ -111,11 +112,23 @@ public function testNegotiatesEncodingBasics(): void public function testAcceptLanguageBasics(): void { - $this->request->setHeader('Accept-Language', 'da, en-gb;q=0.8, en;q=0.7'); + $this->request->setHeader('Accept-Language', 'da, en-gb, en-us;q=0.8, en;q=0.7'); $this->assertSame('da', $this->negotiate->language(['da', 'en'])); $this->assertSame('en-gb', $this->negotiate->language(['en-gb', 'en'])); $this->assertSame('en', $this->negotiate->language(['en'])); + + // Will find the first locale instead of "en-gb" + $this->assertSame('en-us', $this->negotiate->language(['en-us', 'en-gb', 'en'])); + $this->assertSame('en', $this->negotiate->language(['en', 'en-us', 'en-gb'])); + + config(Feature::class)->simpleNegotiateLocale = false; + + $this->assertSame('da', $this->negotiate->language(['da', 'en'])); + $this->assertSame('en-gb', $this->negotiate->language(['en-gb', 'en'])); + $this->assertSame('en', $this->negotiate->language(['en'])); + $this->assertSame('en-gb', $this->negotiate->language(['en-us', 'en-gb', 'en'])); + $this->assertSame('en-gb', $this->negotiate->language(['en', 'en-us', 'en-gb'])); } /** @@ -125,7 +138,17 @@ public function testAcceptLanguageMatchesBroadly(): void { $this->request->setHeader('Accept-Language', 'fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7'); - $this->assertSame('fr', $this->negotiate->language(['fr', 'en'])); + $this->assertSame('fr', $this->negotiate->language(['fr', 'fr-FR', 'en'])); + $this->assertSame('fr-FR', $this->negotiate->language(['fr-FR', 'fr', 'en'])); + $this->assertSame('fr-BE', $this->negotiate->language(['fr-BE', 'fr', 'en'])); + $this->assertSame('en', $this->negotiate->language(['en', 'en-US'])); + + config(Feature::class)->simpleNegotiateLocale = false; + + $this->assertSame('fr-FR', $this->negotiate->language(['fr', 'fr-FR', 'en'])); + $this->assertSame('fr-FR', $this->negotiate->language(['fr-FR', 'fr', 'en'])); + $this->assertSame('fr', $this->negotiate->language(['fr-BE', 'fr', 'en'])); + $this->assertSame('en-US', $this->negotiate->language(['en', 'en-US'])); } public function testBestMatchEmpty(): void From 596e550ef7011b57d7a98f1c1efb0cb8a91c1c5d Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Tue, 31 Dec 2024 19:30:47 +0300 Subject: [PATCH 04/13] docs: Update userguide --- user_guide_src/source/changelogs/v4.5.8.rst | 11 +++++++++++ user_guide_src/source/installation/upgrade_458.rst | 7 +++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.5.8.rst b/user_guide_src/source/changelogs/v4.5.8.rst index cad7fec30248..17f8dd0d2d7d 100644 --- a/user_guide_src/source/changelogs/v4.5.8.rst +++ b/user_guide_src/source/changelogs/v4.5.8.rst @@ -18,6 +18,17 @@ BREAKING Message Changes *************** +************ +Enhancements +************ + +Negotiator +========== + +- Added a feature flag ``Feature::$simpleNegotiateLocale`` fix simple locale comparison. + Previously, response with language headers ``Accept-language: en-US,en-GB;q=0.9`` returned the first allowed language ``en`` could instead of the exact language ``en-US`` or ``en-GB``. + Set the value to ``false`` to be able to get ``en-*`` + ******* Changes ******* diff --git a/user_guide_src/source/installation/upgrade_458.rst b/user_guide_src/source/installation/upgrade_458.rst index e8e8e681abe4..69a6fe92da9d 100644 --- a/user_guide_src/source/installation/upgrade_458.rst +++ b/user_guide_src/source/installation/upgrade_458.rst @@ -44,7 +44,10 @@ and it is recommended that you merge the updated versions with your application: Config ------ -- @TODO +app/Config/Feature.php +^^^^^^^^^^^^^^^^^^^^^^ + +- ``Config\Feature::$simpleNegotiateLocale`` has been added. All Changes =========== @@ -52,4 +55,4 @@ All Changes This is a list of all files in the **project space** that received changes; many will be simple comments or formatting that have no effect on the runtime: -- @TODO +- app/Config/Feature.php From 75ab3378a71b33b966230c0160491a72dae6a67e Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Fri, 3 Jan 2025 14:49:56 +0300 Subject: [PATCH 05/13] fix: Rename feature option --- app/Config/Feature.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Config/Feature.php b/app/Config/Feature.php index 6585f716d54b..220ff658cf94 100644 --- a/app/Config/Feature.php +++ b/app/Config/Feature.php @@ -28,8 +28,8 @@ class Feature extends BaseConfig public bool $limitZeroAsAll = true; /** - * Use an strict localization comparison (with territory en-*) instead of an abbreviated value - * Previously, the territory was cut off (en-* as en) before localization comparing + * Set `false` to use strict localization comparison (with territory en-*) instead of an abbreviated value. + * Set `true`, so territory was cut off (en-* as en) before localization comparing. */ - public bool $simpleNegotiateLocale = true; + public bool $looseLocaleNegotiation = true; } From 3c815e2fe014f23e29e755f220f0cb3fccc7f074 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Fri, 3 Jan 2025 14:51:08 +0300 Subject: [PATCH 06/13] feat: Add `getBestLocaleMatch()` --- system/HTTP/Negotiate.php | 64 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/system/HTTP/Negotiate.php b/system/HTTP/Negotiate.php index e241d2b473e6..9c0eee00d5e3 100644 --- a/system/HTTP/Negotiate.php +++ b/system/HTTP/Negotiate.php @@ -123,12 +123,16 @@ public function encoding(array $supported = []): string * types the application says it supports, and the types requested * by the client. * - * If no match is found, the first, highest-ranking client requested + * If loose locale negotiation is enabled and no match is found, the first, highest-ranking client requested * type is returned. */ public function language(array $supported): string { - return $this->getBestMatch($supported, $this->request->getHeaderLine('accept-language'), false, false, config(Feature::class)->simpleNegotiateLocale); + if (config(Feature::class)->looseLocaleNegotiation) { + return $this->getBestMatch($supported, $this->request->getHeaderLine('accept-language'), false, false, true); + } + + return $this->getBestLocaleMatch($supported, $this->request->getHeaderLine('accept-language')); } // -------------------------------------------------------------------- @@ -190,6 +194,62 @@ protected function getBestMatch( return $strictMatch ? '' : $supported[0]; } + /** + * Strict locale search, including territories (en-*) + * + * @param list $supported App-supported values + * @param ?string $header Compatible 'Accept-Language' header string + */ + protected function getBestLocaleMatch(array $supported, ?string $header): string + { + if ($supported === []) { + throw HTTPException::forEmptySupportedNegotiations(); + } + + if ($header === null || $header === '') { + return $supported[0]; + } + + $acceptable = $this->parseHeader($header); + $fallbackLocales = []; + + foreach ($acceptable as $accept) { + // if acceptable quality is zero, skip it. + if ($accept['q'] === 0.0) { + continue; + } + + // if acceptable value is "anything", return the first available + if ($accept['value'] === '*' || $accept['value'] === '*/*') { + return $supported[0]; + } + + // look for exact match + if (in_array($accept['value'], $supported, true)) { + return $accept['value']; + } + + // set a fallback locale + $fallbackLocales[] = strtok($accept['value'], '-'); + } + + foreach ($fallbackLocales as $fallbackLocale) { + // look for exact match + if (in_array($fallbackLocale, $supported, true)) { + return $fallbackLocale; + } + + // look for locale variants match + foreach ($supported as $locale) { + if (str_starts_with($locale, $fallbackLocale . '-')) { + return $locale; + } + } + } + + return $supported[0]; + } + /** * Parses an Accept* header into it's multiple values. * From a6515c7e64224caf79a51f36600cf468d6c9b400 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Fri, 3 Jan 2025 14:53:10 +0300 Subject: [PATCH 07/13] tests: Update `Negotiate` tests --- tests/system/HTTP/NegotiateTest.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/system/HTTP/NegotiateTest.php b/tests/system/HTTP/NegotiateTest.php index 5417a1ec758d..54fbfe25ffc2 100644 --- a/tests/system/HTTP/NegotiateTest.php +++ b/tests/system/HTTP/NegotiateTest.php @@ -122,7 +122,7 @@ public function testAcceptLanguageBasics(): void $this->assertSame('en-us', $this->negotiate->language(['en-us', 'en-gb', 'en'])); $this->assertSame('en', $this->negotiate->language(['en', 'en-us', 'en-gb'])); - config(Feature::class)->simpleNegotiateLocale = false; + config(Feature::class)->looseLocaleNegotiation = false; $this->assertSame('da', $this->negotiate->language(['da', 'en'])); $this->assertSame('en-gb', $this->negotiate->language(['en-gb', 'en'])); @@ -142,13 +142,15 @@ public function testAcceptLanguageMatchesBroadly(): void $this->assertSame('fr-FR', $this->negotiate->language(['fr-FR', 'fr', 'en'])); $this->assertSame('fr-BE', $this->negotiate->language(['fr-BE', 'fr', 'en'])); $this->assertSame('en', $this->negotiate->language(['en', 'en-US'])); + $this->assertSame('fr-BE', $this->negotiate->language(['ru', 'en-GB', 'fr-BE'])); - config(Feature::class)->simpleNegotiateLocale = false; + config(Feature::class)->looseLocaleNegotiation = false; $this->assertSame('fr-FR', $this->negotiate->language(['fr', 'fr-FR', 'en'])); $this->assertSame('fr-FR', $this->negotiate->language(['fr-FR', 'fr', 'en'])); $this->assertSame('fr', $this->negotiate->language(['fr-BE', 'fr', 'en'])); $this->assertSame('en-US', $this->negotiate->language(['en', 'en-US'])); + $this->assertSame('fr-BE', $this->negotiate->language(['ru', 'en-GB', 'fr-BE'])); } public function testBestMatchEmpty(): void From 29472c600aaab10916b46d59fc5ea64ce8a9fe2a Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Fri, 3 Jan 2025 14:54:27 +0300 Subject: [PATCH 08/13] docs: Update docs --- user_guide_src/source/changelogs/v4.5.8.rst | 11 ----------- user_guide_src/source/changelogs/v4.6.0.rst | 7 +++++++ .../source/incoming/content_negotiation.rst | 5 +++++ user_guide_src/source/installation/upgrade_458.rst | 7 ++----- user_guide_src/source/installation/upgrade_460.rst | 3 ++- 5 files changed, 16 insertions(+), 17 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.5.8.rst b/user_guide_src/source/changelogs/v4.5.8.rst index 17f8dd0d2d7d..cad7fec30248 100644 --- a/user_guide_src/source/changelogs/v4.5.8.rst +++ b/user_guide_src/source/changelogs/v4.5.8.rst @@ -18,17 +18,6 @@ BREAKING Message Changes *************** -************ -Enhancements -************ - -Negotiator -========== - -- Added a feature flag ``Feature::$simpleNegotiateLocale`` fix simple locale comparison. - Previously, response with language headers ``Accept-language: en-US,en-GB;q=0.9`` returned the first allowed language ``en`` could instead of the exact language ``en-US`` or ``en-GB``. - Set the value to ``false`` to be able to get ``en-*`` - ******* Changes ******* diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 2c75473b5220..d687fc5c55b9 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -239,6 +239,13 @@ Routing - Now you can specify multiple hostnames when restricting routes. +Negotiator +========== + +- Added a feature flag ``Feature::$looseLocaleNegotiation`` fix simple locale comparison. + Previously, response with language headers ``Accept-language: en-US,en-GB;q=0.9`` returned the first allowed language ``en`` could instead of the exact language ``en-US`` or ``en-GB``. + Set the value to ``false`` to be able to get ``en-*`` + Testing ======= diff --git a/user_guide_src/source/incoming/content_negotiation.rst b/user_guide_src/source/incoming/content_negotiation.rst index 702bb40afe64..342108d50fa6 100644 --- a/user_guide_src/source/incoming/content_negotiation.rst +++ b/user_guide_src/source/incoming/content_negotiation.rst @@ -102,6 +102,11 @@ and German you would do something like: In this example, 'en' would be returned as the current language. If no match is found, it will return the first element in the ``$supported`` array, so that should always be the preferred language. +.. versionadded:: 4.6.0 + +Disabling the ``Config\Feature::$looseLocaleNegotiation`` value allows you to strictly search for the requested language from the specified territory (``en-*``). +In the case of a non-strict search, the language may be limited only by the country ``en``. Don't forget to create files for the ``en-*`` locale if you need a translation. + Encoding ======== diff --git a/user_guide_src/source/installation/upgrade_458.rst b/user_guide_src/source/installation/upgrade_458.rst index 69a6fe92da9d..3a0a77f13fb4 100644 --- a/user_guide_src/source/installation/upgrade_458.rst +++ b/user_guide_src/source/installation/upgrade_458.rst @@ -44,10 +44,7 @@ and it is recommended that you merge the updated versions with your application: Config ------ -app/Config/Feature.php -^^^^^^^^^^^^^^^^^^^^^^ - -- ``Config\Feature::$simpleNegotiateLocale`` has been added. +- @TODO All Changes =========== @@ -55,4 +52,4 @@ All Changes This is a list of all files in the **project space** that received changes; many will be simple comments or formatting that have no effect on the runtime: -- app/Config/Feature.php +- @TODO \ No newline at end of file diff --git a/user_guide_src/source/installation/upgrade_460.rst b/user_guide_src/source/installation/upgrade_460.rst index a06729a30f29..9fd9dbad8fcc 100644 --- a/user_guide_src/source/installation/upgrade_460.rst +++ b/user_guide_src/source/installation/upgrade_460.rst @@ -211,13 +211,14 @@ Config - app/Config/Feature.php - ``Config\Feature::$autoRoutesImproved`` has been changed to ``true``. + - ``Config\Feature::$looseLocaleNegotiation`` has been added. - app/Config/Routing.php - ``Config\Routing::$translateUriToCamelCase`` has been changed to ``true``. - All Changes =========== This is a list of all files in the **project space** that received changes; many will be simple comments or formatting that have no effect on the runtime: +- app/Config/Feature.php - @TODO From e36b9a9f3c946e7e72ef3649b53d53f49b1281c9 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Fri, 3 Jan 2025 19:10:49 +0300 Subject: [PATCH 09/13] fix: Rename feature option to back --- app/Config/Feature.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/Config/Feature.php b/app/Config/Feature.php index 220ff658cf94..ec1435af413f 100644 --- a/app/Config/Feature.php +++ b/app/Config/Feature.php @@ -28,8 +28,10 @@ class Feature extends BaseConfig public bool $limitZeroAsAll = true; /** - * Set `false` to use strict localization comparison (with territory en-*) instead of an abbreviated value. - * Set `true`, so territory was cut off (en-* as en) before localization comparing. + * Use strict location negotiation. + * + * By default, the locale is selected based on a loose comparison of the language code (ISO 639-1) + * Enabling strict comparison will also consider the region code (ISO 3166-1 alpha-2). */ - public bool $looseLocaleNegotiation = true; + public bool $strictLocaleNegotiation = false; } From a172f81493793117f5f08519e93ba6f68fa1af3b Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Fri, 3 Jan 2025 19:14:39 +0300 Subject: [PATCH 10/13] docs: Update the description for Negotiation --- user_guide_src/source/changelogs/v4.6.0.rst | 4 +- .../source/incoming/content_negotiation.rst | 51 ++++++++++++++++++- .../source/installation/upgrade_460.rst | 6 +-- 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index d687fc5c55b9..db4e8abaa07b 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -242,9 +242,9 @@ Routing Negotiator ========== -- Added a feature flag ``Feature::$looseLocaleNegotiation`` fix simple locale comparison. +- Added a feature flag ``Feature::$strictLocaleNegotiation`` to enable strict locale comparision. Previously, response with language headers ``Accept-language: en-US,en-GB;q=0.9`` returned the first allowed language ``en`` could instead of the exact language ``en-US`` or ``en-GB``. - Set the value to ``false`` to be able to get ``en-*`` + Set the value to ``true`` to enable comparison not only by language code ('en' - ISO 639-1) but also by regional code ('en-US' - ISO 639-1 plus ISO 3166-1 alpha). Testing ======= diff --git a/user_guide_src/source/incoming/content_negotiation.rst b/user_guide_src/source/incoming/content_negotiation.rst index 342108d50fa6..596e364914a5 100644 --- a/user_guide_src/source/incoming/content_negotiation.rst +++ b/user_guide_src/source/incoming/content_negotiation.rst @@ -102,10 +102,57 @@ and German you would do something like: In this example, 'en' would be returned as the current language. If no match is found, it will return the first element in the ``$supported`` array, so that should always be the preferred language. +Strict Locale Negotiation +------------------------- + .. versionadded:: 4.6.0 -Disabling the ``Config\Feature::$looseLocaleNegotiation`` value allows you to strictly search for the requested language from the specified territory (``en-*``). -In the case of a non-strict search, the language may be limited only by the country ``en``. Don't forget to create files for the ``en-*`` locale if you need a translation. +By default, locale is determined on a lossy comparison basis. So only the first part of the locale string is taken +into account (language). This is usually sufficient. But sometimes we want to be able to distinguish between regional versions such as +``en-US`` and ``en-GB`` to serve different content. + +For such cases, we have introduced a new setting that can be enabled via ``Config\Feature::$strictLocaleNegotiation``. This will ensure +that the strict comparison will be made in the first place. + +.. note:: + + CodeIgniter comes with translations only for primary language tags ('en', 'fr', etc.). So if you enable this feature and your + settings in ``Config\App::$supportedLocales`` include regional language tags ('en-US', 'fr-FR', etc.), then keep in mind that + if you have your own translation files, you **must also change** the folder names for CodeIgniter's translation files to match + what you put in the ``$supportedLocales`` array. + + Now let's consider the below example. The browser's preferred language will be set as this:: + + GET /foo HTTP/1.1 + Accept-Language: fr; q=1.0, en-GB; q=0.5 + +In this example, the browser would prefer French, with a second choice of English (United Kingdom). Your website on another hand will +supports German and English (United States): + +.. code-block:: php + + $supported = [ + 'de', + 'en-US', + ]; + + $lang = $request->negotiate('language', $supported); + // or + $lang = $negotiate->language($supported); + +In this example, 'en-US' would be returned as the current language. If no match is found, it will return the first element +in the ``$supported`` array. Here is how exactly the locale selection process works. + +Even though the 'fr' is preferred by the browser it is not in our ``$supported`` array. The same problem occurs with 'en-GB', but here +we will be able to search for variants. First, we will fallback to the most general locale (in this case 'en') which again is not in our +array. Then we will search for the regional locale 'en-'. And that's when our value from the ``$supported`` array will be matched. +We will return 'en-US'. + +So the process of selecting a locale is as follows: + +#. strict match ('en-GB') - ISO 639-1 plus ISO 3166-1 alpha-2 +#. general locale match ('en') - ISO 639-1 +#. regional locale match ('en-') - ISO 639-1 plus "wildcard" for ISO 3166-1 alpha-2 Encoding ======== diff --git a/user_guide_src/source/installation/upgrade_460.rst b/user_guide_src/source/installation/upgrade_460.rst index 9fd9dbad8fcc..57554c63b7bc 100644 --- a/user_guide_src/source/installation/upgrade_460.rst +++ b/user_guide_src/source/installation/upgrade_460.rst @@ -211,14 +211,14 @@ Config - app/Config/Feature.php - ``Config\Feature::$autoRoutesImproved`` has been changed to ``true``. - - ``Config\Feature::$looseLocaleNegotiation`` has been added. + - ``Config\Feature::$strictLocaleNegotiation`` has been added. - app/Config/Routing.php - ``Config\Routing::$translateUriToCamelCase`` has been changed to ``true``. + All Changes =========== This is a list of all files in the **project space** that received changes; many will be simple comments or formatting that have no effect on the runtime: -- app/Config/Feature.php -- @TODO +- app/Config/Feature.php \ No newline at end of file From 159e44ddc4c22c75d154358fc5c0f17f406bdf7b Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Sat, 4 Jan 2025 12:56:36 +0300 Subject: [PATCH 11/13] fix: Update with the renamed option --- system/HTTP/Negotiate.php | 39 +++++++++++++++++------------ tests/system/HTTP/NegotiateTest.php | 4 +-- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/system/HTTP/Negotiate.php b/system/HTTP/Negotiate.php index 9c0eee00d5e3..4291256a2818 100644 --- a/system/HTTP/Negotiate.php +++ b/system/HTTP/Negotiate.php @@ -123,16 +123,16 @@ public function encoding(array $supported = []): string * types the application says it supports, and the types requested * by the client. * - * If loose locale negotiation is enabled and no match is found, the first, highest-ranking client requested + * If strict locale negotiation is disabled and no match is found, the first, highest-ranking client requested * type is returned. */ public function language(array $supported): string { - if (config(Feature::class)->looseLocaleNegotiation) { - return $this->getBestMatch($supported, $this->request->getHeaderLine('accept-language'), false, false, true); + if (config(Feature::class)->strictLocaleNegotiation) { + return $this->getBestLocaleMatch($supported, $this->request->getHeaderLine('accept-language')); } - return $this->getBestLocaleMatch($supported, $this->request->getHeaderLine('accept-language')); + return $this->getBestMatch($supported, $this->request->getHeaderLine('accept-language'), false, false, true); } // -------------------------------------------------------------------- @@ -195,19 +195,26 @@ protected function getBestMatch( } /** - * Strict locale search, including territories (en-*) + * Try to find the best matching locale. It supports strict locale comparison. * - * @param list $supported App-supported values - * @param ?string $header Compatible 'Accept-Language' header string + * If Config\App::$supportedLocales have "en-US" and "en-GB" locales, they can be recognized + * as two different locales. This method checks first for the strict match, then fallback + * to the most general locale (in this case "en") ISO 639-1 and finally to the locale variant + * "en-*" (ISO 639-1 plus "wildcard" for ISO 3166-1 alpha-2). + * + * If nothing from above is matched, then it returns the first option from the $supportedLocales array. + * + * @param list $supportedLocales App-supported values + * @param ?string $header Compatible 'Accept-Language' header string */ - protected function getBestLocaleMatch(array $supported, ?string $header): string + protected function getBestLocaleMatch(array $supportedLocales, ?string $header): string { - if ($supported === []) { + if ($supportedLocales === []) { throw HTTPException::forEmptySupportedNegotiations(); } if ($header === null || $header === '') { - return $supported[0]; + return $supportedLocales[0]; } $acceptable = $this->parseHeader($header); @@ -221,11 +228,11 @@ protected function getBestLocaleMatch(array $supported, ?string $header): string // if acceptable value is "anything", return the first available if ($accept['value'] === '*' || $accept['value'] === '*/*') { - return $supported[0]; + return $supportedLocales[0]; } // look for exact match - if (in_array($accept['value'], $supported, true)) { + if (in_array($accept['value'], $supportedLocales, true)) { return $accept['value']; } @@ -235,19 +242,19 @@ protected function getBestLocaleMatch(array $supported, ?string $header): string foreach ($fallbackLocales as $fallbackLocale) { // look for exact match - if (in_array($fallbackLocale, $supported, true)) { + if (in_array($fallbackLocale, $supportedLocales, true)) { return $fallbackLocale; } - // look for locale variants match - foreach ($supported as $locale) { + // look for regional locale match + foreach ($supportedLocales as $locale) { if (str_starts_with($locale, $fallbackLocale . '-')) { return $locale; } } } - return $supported[0]; + return $supportedLocales[0]; } /** diff --git a/tests/system/HTTP/NegotiateTest.php b/tests/system/HTTP/NegotiateTest.php index 54fbfe25ffc2..a2731ec5ddbf 100644 --- a/tests/system/HTTP/NegotiateTest.php +++ b/tests/system/HTTP/NegotiateTest.php @@ -122,7 +122,7 @@ public function testAcceptLanguageBasics(): void $this->assertSame('en-us', $this->negotiate->language(['en-us', 'en-gb', 'en'])); $this->assertSame('en', $this->negotiate->language(['en', 'en-us', 'en-gb'])); - config(Feature::class)->looseLocaleNegotiation = false; + config(Feature::class)->strictLocaleNegotiation = true; $this->assertSame('da', $this->negotiate->language(['da', 'en'])); $this->assertSame('en-gb', $this->negotiate->language(['en-gb', 'en'])); @@ -144,7 +144,7 @@ public function testAcceptLanguageMatchesBroadly(): void $this->assertSame('en', $this->negotiate->language(['en', 'en-US'])); $this->assertSame('fr-BE', $this->negotiate->language(['ru', 'en-GB', 'fr-BE'])); - config(Feature::class)->looseLocaleNegotiation = false; + config(Feature::class)->strictLocaleNegotiation = true; $this->assertSame('fr-FR', $this->negotiate->language(['fr', 'fr-FR', 'en'])); $this->assertSame('fr-FR', $this->negotiate->language(['fr-FR', 'fr', 'en'])); From 76cca3f8d599e92c1db102e970118caefd645dfe Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Sat, 4 Jan 2025 13:35:52 +0300 Subject: [PATCH 12/13] fix: Delete checking *.* --- system/HTTP/Negotiate.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/HTTP/Negotiate.php b/system/HTTP/Negotiate.php index 4291256a2818..026cafc75f39 100644 --- a/system/HTTP/Negotiate.php +++ b/system/HTTP/Negotiate.php @@ -227,7 +227,7 @@ protected function getBestLocaleMatch(array $supportedLocales, ?string $header): } // if acceptable value is "anything", return the first available - if ($accept['value'] === '*' || $accept['value'] === '*/*') { + if ($accept['value'] === '*') { return $supportedLocales[0]; } From bddbb95555c881cc58fe831cf6684dd1727ef166 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Sat, 4 Jan 2025 19:20:15 +0300 Subject: [PATCH 13/13] fix: Move code to file --- .../source/incoming/content_negotiation.rst | 15 +++------------ .../source/incoming/content_negotiation/008.php | 10 ++++++++++ 2 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 user_guide_src/source/incoming/content_negotiation/008.php diff --git a/user_guide_src/source/incoming/content_negotiation.rst b/user_guide_src/source/incoming/content_negotiation.rst index 596e364914a5..ba176feaebda 100644 --- a/user_guide_src/source/incoming/content_negotiation.rst +++ b/user_guide_src/source/incoming/content_negotiation.rst @@ -121,24 +121,15 @@ that the strict comparison will be made in the first place. if you have your own translation files, you **must also change** the folder names for CodeIgniter's translation files to match what you put in the ``$supportedLocales`` array. - Now let's consider the below example. The browser's preferred language will be set as this:: +Now let's consider the below example. The browser's preferred language will be set as this:: GET /foo HTTP/1.1 Accept-Language: fr; q=1.0, en-GB; q=0.5 -In this example, the browser would prefer French, with a second choice of English (United Kingdom). Your website on another hand will +In this example, the browser would prefer French, with a second choice of English (United Kingdom). Your website on another hand supports German and English (United States): -.. code-block:: php - - $supported = [ - 'de', - 'en-US', - ]; - - $lang = $request->negotiate('language', $supported); - // or - $lang = $negotiate->language($supported); +.. literalinclude:: content_negotiation/008.php In this example, 'en-US' would be returned as the current language. If no match is found, it will return the first element in the ``$supported`` array. Here is how exactly the locale selection process works. diff --git a/user_guide_src/source/incoming/content_negotiation/008.php b/user_guide_src/source/incoming/content_negotiation/008.php new file mode 100644 index 000000000000..2b4a67f1a6d0 --- /dev/null +++ b/user_guide_src/source/incoming/content_negotiation/008.php @@ -0,0 +1,10 @@ +negotiate('language', $supported); +// or +$lang = $negotiate->language($supported);