From 5f318453a6fff696bd0c0d47afe31f0fc5e9e323 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 8 Aug 2024 10:04:52 +0900 Subject: [PATCH 01/13] docs: improve description --- user_guide_src/source/outgoing/localization.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/outgoing/localization.rst b/user_guide_src/source/outgoing/localization.rst index f9f7e096b6e9..8e548585e717 100644 --- a/user_guide_src/source/outgoing/localization.rst +++ b/user_guide_src/source/outgoing/localization.rst @@ -15,7 +15,7 @@ localization of an application is a complex subject, it's simple to swap out str with different supported languages. Language strings are stored in the **app/Language** directory, with a sub-directory for each -supported language:: +supported language (locale):: app/ Language/ From ace19636d2009d5b679cea3c72dbf1877c567306 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 8 Aug 2024 10:15:24 +0900 Subject: [PATCH 02/13] docs: improve description for "Locale Detection" --- user_guide_src/source/outgoing/localization.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/user_guide_src/source/outgoing/localization.rst b/user_guide_src/source/outgoing/localization.rst index 8e548585e717..5b4290348744 100644 --- a/user_guide_src/source/outgoing/localization.rst +++ b/user_guide_src/source/outgoing/localization.rst @@ -24,9 +24,6 @@ supported language (locale):: fr/ App.php -.. important:: Locale detection only works for web-based requests that use the IncomingRequest class. - Command-line requests will not have these features. - Configuring the Locale ====================== @@ -52,10 +49,13 @@ directory existed at the **app/Language/en-US** directory then that would be use Locale Detection ================ -There are two methods supported to detect the correct locale during the request. The first is a "set and forget" -method that will automatically perform :doc:`content negotiation ` for you to -determine the correct locale to use. The second method allows you to specify a segment in your routes that -will be used to set the locale. +.. important:: Locale detection only works for web-based requests that use the IncomingRequest class. + Command-line requests will not have these features. + +There are two methods supported to detect the correct locale during the request. + +1. `Content Negotiation`_: The first is a "set and forget" method that will automatically perform :doc:`content negotiation ` for you to determine the correct locale to use. +2. `In Routes`_: The second method allows you to specify a segment in your routes that will be used to set the locale. Should you ever need to set the locale directly, see `Setting the Current Locale`_. From d87be95aec083b0c6d71a3fac62126e58276ea52 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 8 Aug 2024 10:20:29 +0900 Subject: [PATCH 03/13] docs: move paragraph to better place --- .../source/outgoing/localization.rst | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/user_guide_src/source/outgoing/localization.rst b/user_guide_src/source/outgoing/localization.rst index 5b4290348744..ab78658dbd27 100644 --- a/user_guide_src/source/outgoing/localization.rst +++ b/user_guide_src/source/outgoing/localization.rst @@ -14,16 +14,6 @@ CodeIgniter provides several tools to help you localize your application for dif localization of an application is a complex subject, it's simple to swap out strings in your application with different supported languages. -Language strings are stored in the **app/Language** directory, with a sub-directory for each -supported language (locale):: - - app/ - Language/ - en/ - App.php - fr/ - App.php - Configuring the Locale ====================== @@ -139,6 +129,16 @@ Language Localization Creating Language Files ======================= +Language strings are stored in the **app/Language** directory, with a sub-directory for each +supported language (locale):: + + app/ + Language/ + en/ + App.php + fr/ + App.php + .. note:: The Language Files do not have namespaces. Languages do not have any specific naming convention that are required. The file should be named logically to From b1d56ec30be4ed726123d752349c7b50b6ebba98 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 8 Aug 2024 10:22:43 +0900 Subject: [PATCH 04/13] docs: fix typo in lang filename --- user_guide_src/source/outgoing/localization.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/outgoing/localization.rst b/user_guide_src/source/outgoing/localization.rst index ab78658dbd27..b6939863e691 100644 --- a/user_guide_src/source/outgoing/localization.rst +++ b/user_guide_src/source/outgoing/localization.rst @@ -231,8 +231,8 @@ Language Fallback ================= If you have a set of messages for a given locale, for instance -**Language/en/app.php**, you can add language variants for that locale, -each in its own folder, for instance **Language/en-US/app.php**. +**Language/en/App.php**, you can add language variants for that locale, +each in its own folder, for instance **Language/en-US/App.php**. You only need to provide values for those messages that would be localized differently for that locale variant. Any missing message From 2d786f828a41b716eb486b8173a5478a82faec85 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 8 Aug 2024 10:26:34 +0900 Subject: [PATCH 05/13] docs: improve sub section title --- user_guide_src/source/outgoing/localization.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/user_guide_src/source/outgoing/localization.rst b/user_guide_src/source/outgoing/localization.rst index b6939863e691..b0bd761b9a36 100644 --- a/user_guide_src/source/outgoing/localization.rst +++ b/user_guide_src/source/outgoing/localization.rst @@ -246,10 +246,10 @@ So, if you are using the locale ``fr-CA``, then a localized message will first be sought in the **Language/fr-CA** directory, then in the **Language/fr** directory, and finally in the **Language/en** directory. -Message Translations -==================== +System Message Translations +=========================== -We have an "official" set of translations in their +We have an "official" set of the system message translations in their `own repository `_. You could download that repository, and copy its **Language** folder From a6e862e5c5acdeb9a9ed46fbaaf03b0d2c768b44 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 8 Aug 2024 10:39:20 +0900 Subject: [PATCH 06/13] docs: add "Overriding System Message Translations" --- user_guide_src/source/outgoing/localization.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/user_guide_src/source/outgoing/localization.rst b/user_guide_src/source/outgoing/localization.rst index b0bd761b9a36..0eba30e426eb 100644 --- a/user_guide_src/source/outgoing/localization.rst +++ b/user_guide_src/source/outgoing/localization.rst @@ -266,6 +266,16 @@ project: The translated messages will be automatically picked up because the translations folders get mapped appropriately. +Overriding System Message Translations +====================================== + +The framework provide `System Message Translations`_, and packages that you +installed may also provide the message translations. + +If you want to override some language messages, create language files in the +**app/Language** directory. Then, return only the array you want to override +in the file. + .. _generating-translation-files-via-command: Generating Translation Files via Command From 7e2ea399600e10183a48f8f0b17ac4eb86ab348c Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 9 Aug 2024 10:22:24 +0900 Subject: [PATCH 07/13] docs: add note for dots in lang keys --- user_guide_src/source/outgoing/localization.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/user_guide_src/source/outgoing/localization.rst b/user_guide_src/source/outgoing/localization.rst index 0eba30e426eb..e3e879cb8808 100644 --- a/user_guide_src/source/outgoing/localization.rst +++ b/user_guide_src/source/outgoing/localization.rst @@ -149,6 +149,8 @@ Within the file, you would return an array, where each element in the array has .. literalinclude:: localization/007.php +.. note:: You cannot use dots (``.``) at the beginning and end of language keys. + It also support nested definition: .. literalinclude:: localization/008.php From 8319a3c008e8299f19e61cbd4baa287d8aeacce2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 9 Aug 2024 10:22:48 +0900 Subject: [PATCH 08/13] test: add tests for lang keys with dots --- tests/system/Language/LanguageTest.php | 51 ++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/system/Language/LanguageTest.php b/tests/system/Language/LanguageTest.php index 2651413060c8..98f975bc3c98 100644 --- a/tests/system/Language/LanguageTest.php +++ b/tests/system/Language/LanguageTest.php @@ -62,6 +62,57 @@ public function testGetLineReturnsLine(): void $this->assertSame('We saved some more', $this->lang->getLine('books.booksSaved')); } + public function testGetLineReturnsLineWithKeyWithDots(): void + { + $this->lang->setData('books', [ + 'bookSaved.foo' => 'We kept the book free from the boogeyman', + 'booksSaved.bar.baz' => 'We saved some more', + ]); + + $this->assertSame( + 'We kept the book free from the boogeyman', + $this->lang->getLine('books.bookSaved.foo') + ); + $this->assertSame( + 'We saved some more', + $this->lang->getLine('books.booksSaved.bar.baz') + ); + } + + public function testGetLineCannotUseKeysWithLeadingDot(): void + { + $this->lang->setData('books', [ + '.bookSaved.foo.' => 'We kept the book free from the boogeyman', + '.booksSaved.bar.baz.' => 'We saved some more', + ]); + + $this->assertSame( + 'books.bookSaved.foo', // Can't get the message. + $this->lang->getLine('books.bookSaved.foo') + ); + $this->assertSame( + 'books.booksSaved.bar.baz', // Can't get the message. + $this->lang->getLine('books.booksSaved.bar.baz') + ); + } + + public function testGetLineCannotUseKeysWithTrailingDot(): void + { + $this->lang->setData('books', [ + 'bookSaved.foo.' => 'We kept the book free from the boogeyman', + 'booksSaved.bar.baz.' => 'We saved some more', + ]); + + $this->assertSame( + 'books.bookSaved.foo', // Can't get the message. + $this->lang->getLine('books.bookSaved.foo') + ); + $this->assertSame( + 'books.booksSaved.bar.baz', // Can't get the message. + $this->lang->getLine('books.booksSaved.bar.baz') + ); + } + public function testGetLineReturnsFallbackLine(): void { $this->lang From f08c4aede742080b265a77786fdc416e8a7173a8 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 9 Aug 2024 10:23:25 +0900 Subject: [PATCH 09/13] docs: make description more detailed --- user_guide_src/source/outgoing/localization.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/outgoing/localization.rst b/user_guide_src/source/outgoing/localization.rst index e3e879cb8808..19a9466b3f84 100644 --- a/user_guide_src/source/outgoing/localization.rst +++ b/user_guide_src/source/outgoing/localization.rst @@ -172,7 +172,7 @@ For nested definition, you would do the following: .. literalinclude:: localization/011.php -If the requested language key doesn't exist in the file for the current locale, the string will be passed +If the requested language key doesn't exist in the file for the current locale (after `Language Fallback`_), the string will be passed back, unchanged. In this example, it would return ``Errors.errorEmailMissing`` or ``Errors.nested.error.message`` if it didn't exist. Replacing Parameters @@ -240,7 +240,7 @@ You only need to provide values for those messages that would be localized differently for that locale variant. Any missing message definitions will be automatically pulled from the main locale settings. -It gets better - the localization can fall all the way back to English, +It gets better - the localization can fall all the way back to English (**en**), in case new messages are added to the framework and you haven't had a chance to translate them yet for your locale. From 6e9158f17da7bf8a23a627e2c7f21c8f0e2dadd8 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 9 Aug 2024 10:24:26 +0900 Subject: [PATCH 10/13] docs: add @var for better DX --- system/Common.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/system/Common.php b/system/Common.php index 49b3d2896753..dcc9487c588f 100644 --- a/system/Common.php +++ b/system/Common.php @@ -28,6 +28,7 @@ use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; +use CodeIgniter\Language\Language; use CodeIgniter\Model; use CodeIgniter\Session\Session; use CodeIgniter\Test\TestLogger; @@ -732,6 +733,7 @@ function is_windows(?bool $mock = null): bool */ function lang(string $line, array $args = [], ?string $locale = null) { + /** @var Language $language */ $language = service('language'); // Get active locale From 4f2de95ac7bc9e71f1cf972da047d659a42e0e25 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 9 Aug 2024 10:50:21 +0900 Subject: [PATCH 11/13] test: refactor to fix PHPStan errors --- phpstan-baseline.php | 24 ------------ tests/system/Language/LanguageTest.php | 54 ++++++++++++++++++-------- 2 files changed, 38 insertions(+), 40 deletions(-) diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 2fd4d79af642..b600bd8ec5b4 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -15925,30 +15925,6 @@ 'count' => 2, 'path' => __DIR__ . '/tests/system/Images/ImageMagickHandlerTest.php', ]; -$ignoreErrors[] = [ - // identifier: method.notFound - 'message' => '#^Call to an undefined method CodeIgniter\\\\Language\\\\Language\\:\\:disableIntlSupport\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Language/LanguageTest.php', -]; -$ignoreErrors[] = [ - // identifier: method.notFound - 'message' => '#^Call to an undefined method CodeIgniter\\\\Language\\\\Language\\:\\:loaded\\(\\)\\.$#', - 'count' => 3, - 'path' => __DIR__ . '/tests/system/Language/LanguageTest.php', -]; -$ignoreErrors[] = [ - // identifier: method.notFound - 'message' => '#^Call to an undefined method CodeIgniter\\\\Language\\\\Language\\:\\:loadem\\(\\)\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/tests/system/Language/LanguageTest.php', -]; -$ignoreErrors[] = [ - // identifier: method.notFound - 'message' => '#^Call to an undefined method CodeIgniter\\\\Language\\\\Language\\:\\:setData\\(\\)\\.$#', - 'count' => 9, - 'path' => __DIR__ . '/tests/system/Language/LanguageTest.php', -]; $ignoreErrors[] = [ // identifier: missingType.iterableValue 'message' => '#^Method CodeIgniter\\\\Language\\\\LanguageTest\\:\\:provideBundleUniqueKeys\\(\\) return type has no value type specified in iterable type iterable\\.$#', diff --git a/tests/system/Language/LanguageTest.php b/tests/system/Language/LanguageTest.php index 98f975bc3c98..8dd3a290b286 100644 --- a/tests/system/Language/LanguageTest.php +++ b/tests/system/Language/LanguageTest.php @@ -31,7 +31,7 @@ final class LanguageTest extends CIUnitTestCase protected function setUp(): void { - $this->lang = new MockLanguage('en'); + $this->lang = new Language('en'); } public function testReturnsStringWithNoFileInMessage(): void @@ -54,6 +54,8 @@ public function testReturnParsedStringWithNoFileInMessage(): void public function testGetLineReturnsLine(): void { + $this->lang = new MockLanguage('en'); + $this->lang->setData('books', [ 'bookSaved' => 'We kept the book free from the boogeyman', 'booksSaved' => 'We saved some more', @@ -64,6 +66,8 @@ public function testGetLineReturnsLine(): void public function testGetLineReturnsLineWithKeyWithDots(): void { + $this->lang = new MockLanguage('en'); + $this->lang->setData('books', [ 'bookSaved.foo' => 'We kept the book free from the boogeyman', 'booksSaved.bar.baz' => 'We saved some more', @@ -81,6 +85,8 @@ public function testGetLineReturnsLineWithKeyWithDots(): void public function testGetLineCannotUseKeysWithLeadingDot(): void { + $this->lang = new MockLanguage('en'); + $this->lang->setData('books', [ '.bookSaved.foo.' => 'We kept the book free from the boogeyman', '.booksSaved.bar.baz.' => 'We saved some more', @@ -98,6 +104,8 @@ public function testGetLineCannotUseKeysWithLeadingDot(): void public function testGetLineCannotUseKeysWithTrailingDot(): void { + $this->lang = new MockLanguage('en'); + $this->lang->setData('books', [ 'bookSaved.foo.' => 'We kept the book free from the boogeyman', 'booksSaved.bar.baz.' => 'We saved some more', @@ -115,6 +123,8 @@ public function testGetLineCannotUseKeysWithTrailingDot(): void public function testGetLineReturnsFallbackLine(): void { + $this->lang = new MockLanguage('en'); + $this->lang ->setLocale('en-US') ->setData('equivalent', [ @@ -137,6 +147,8 @@ public function testGetLineReturnsFallbackLine(): void public function testGetLineArrayReturnsLineArray(): void { + $this->lang = new MockLanguage('en'); + $this->lang->setData('books', [ 'booksList' => [ 'The Boogeyman', @@ -157,6 +169,8 @@ public function testGetLineFormatsMessage(): void $this->markTestSkipped('No intl support.'); } + $this->lang = new MockLanguage('en'); + $this->lang->setData('books', [ 'bookCount' => '{0, number, integer} books have been saved.', ]); @@ -171,6 +185,8 @@ public function testGetLineArrayFormatsMessages(): void $this->markTestSkipped('No intl support.'); } + $this->lang = new MockLanguage('en'); + $this->lang->setData('books', [ 'bookList' => [ '{0, number, integer} related books.', @@ -190,6 +206,8 @@ public function testGetLineInvalidFormatMessage(): void $this->markTestSkipped('No intl support.'); } + $this->lang = new MockLanguage('en'); + $this->lang->setLocale('ar'); $line = 'تم الكشف عن كلمة المرور {0} بسبب اختراق البيانات وشوهدت {1 ، عدد} مرة في {2} في كلمات المرور المخترقة.'; @@ -214,6 +232,8 @@ public function testLangAllowsOtherLocales(): void public function testLangDoesntFormat(): void { + $this->lang = new MockLanguage('en'); + $this->lang->disableIntlSupport(); $this->lang->setData('books', [ @@ -236,40 +256,42 @@ public function testLanguageDuplicateKey(): void public function testLanguageFileLoading(): void { - $this->lang = new SecondMockLanguage('en'); + $lang = new SecondMockLanguage('en'); - $this->lang->loadem('More', 'en'); - $this->assertContains('More', $this->lang->loaded()); + $lang->loadem('More', 'en'); + $this->assertContains('More', $lang->loaded()); - $this->lang->loadem('More', 'en'); - $this->assertCount(1, $this->lang->loaded()); // should only be there once + $lang->loadem('More', 'en'); + $this->assertCount(1, $lang->loaded()); // should only be there once } public function testLanguageFileLoadingReturns(): void { - $this->lang = new SecondMockLanguage('en'); + $lang = new SecondMockLanguage('en'); - $result = $this->lang->loadem('More', 'en', true); - $this->assertNotContains('More', $this->lang->loaded()); + $result = $lang->loadem('More', 'en', true); + $this->assertNotContains('More', $lang->loaded()); $this->assertCount(3, $result); - $this->lang->loadem('More', 'en'); - $this->assertContains('More', $this->lang->loaded()); - $this->assertCount(1, $this->lang->loaded()); + $lang->loadem('More', 'en'); + $this->assertContains('More', $lang->loaded()); + $this->assertCount(1, $lang->loaded()); } public function testLanguageSameKeyAndFileName(): void { + $lang = new MockLanguage('en'); + // first file data | example.message - $this->lang->setData('example', ['message' => 'This is an example message']); + $lang->setData('example', ['message' => 'This is an example message']); // force loading data into file Example - $this->assertSame('This is an example message', $this->lang->getLine('example.message')); + $this->assertSame('This is an example message', $lang->getLine('example.message')); // second file data | another.example - $this->lang->setData('another', ['example' => 'Another example']); + $lang->setData('another', ['example' => 'Another example']); - $this->assertSame('Another example', $this->lang->getLine('another.example')); + $this->assertSame('Another example', $lang->getLine('another.example')); } public function testGetLocale(): void From 3cb3839a538e87d8a3e61ca09fdb36eef4365098 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 10 Aug 2024 09:49:46 +0900 Subject: [PATCH 12/13] docs: add about missing Language locale --- .../source/outgoing/localization.rst | 26 ++++++++++++++++--- .../source/outgoing/localization/020.php | 4 +++ .../source/outgoing/localization/021.php | 5 ++++ 3 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 user_guide_src/source/outgoing/localization/020.php create mode 100644 user_guide_src/source/outgoing/localization/021.php diff --git a/user_guide_src/source/outgoing/localization.rst b/user_guide_src/source/outgoing/localization.rst index 19a9466b3f84..59c6454fdc2c 100644 --- a/user_guide_src/source/outgoing/localization.rst +++ b/user_guide_src/source/outgoing/localization.rst @@ -92,8 +92,14 @@ file: Setting the Current Locale ========================== -If you want to set the locale directly, you may use -``IncomingRequest::setLocale(string $locale)``. +IncomingRequest Locale +---------------------- + +If you want to set the locale directly, you may use the ``setLocale()`` method in +the :doc:`../incoming/incomingrequest`: + +.. literalinclude:: localization/020.php + :lines: 2- Before setting the locale, you must set valid locales. Because any attempt to set a locale that are not valid will result in @@ -108,6 +114,18 @@ in **app/Config/App.php**: set (and reset) valid locales. Use it if you want to change the valid locales dynamically. +Language Locale +--------------- + +The ``Language`` class used in the :php:func:`lang()` function also has the current +locale. This is set to the ``IncommingRequest`` locale during instantiating. + +If you want to change the locale after instantiating the language class, use the +``Language::setLocale()`` method. + +.. literalinclude:: localization/021.php + :lines: 2- + Retrieving the Current Locale ============================= @@ -218,10 +236,12 @@ Specifying Locale ----------------- To specify a different locale to be used when replacing parameters, you can pass the locale in as the -third parameter to the ``lang()`` function. +third parameter to the :php:func:`lang()` function. .. literalinclude:: localization/016.php +If you want to change the current locale, see `Language Locale`_. + Nested Arrays ------------- diff --git a/user_guide_src/source/outgoing/localization/020.php b/user_guide_src/source/outgoing/localization/020.php new file mode 100644 index 000000000000..3116bd0dedb7 --- /dev/null +++ b/user_guide_src/source/outgoing/localization/020.php @@ -0,0 +1,4 @@ +setLocale('ja'); diff --git a/user_guide_src/source/outgoing/localization/021.php b/user_guide_src/source/outgoing/localization/021.php new file mode 100644 index 000000000000..9dbc8d1f0fe4 --- /dev/null +++ b/user_guide_src/source/outgoing/localization/021.php @@ -0,0 +1,5 @@ +setLocale('ja'); From 2835cc425d68bc155431b875afcffd2be6f8cd40 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 10 Aug 2024 10:09:17 +0900 Subject: [PATCH 13/13] docs: remove unneeded ` to retrieve the current request: .. literalinclude:: localization/006.php + :lines: 2- .. _language-localization: @@ -185,10 +188,12 @@ For example, to load the ``errorEmailMissing`` string from the **Errors.php** language file, you would do the following: .. literalinclude:: localization/010.php + :lines: 2- For nested definition, you would do the following: .. literalinclude:: localization/011.php + :lines: 2- If the requested language key doesn't exist in the file for the current locale (after `Language Fallback`_), the string will be passed back, unchanged. In this example, it would return ``Errors.errorEmailMissing`` or ``Errors.nested.error.message`` if it didn't exist. @@ -208,10 +213,12 @@ You can pass an array of values to replace placeholders in the language string a The first item in the placeholder corresponds to the index of the item in the array, if it's numerical: .. literalinclude:: localization/013.php + :lines: 2- You can also use named keys to make it easier to keep things straight, if you'd like: .. literalinclude:: localization/014.php + :lines: 2- Obviously, you can do more than just number replacement. According to the `official ICU docs `_ for the underlying