diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 3c2db54f7e4a..f0a882e68b01 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -15727,30 +15727,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/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 diff --git a/tests/system/Language/LanguageTest.php b/tests/system/Language/LanguageTest.php index 2651413060c8..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', @@ -62,8 +64,67 @@ public function testGetLineReturnsLine(): void $this->assertSame('We saved some more', $this->lang->getLine('books.booksSaved')); } + 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', + ]); + + $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 = new MockLanguage('en'); + + $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 = new MockLanguage('en'); + + $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 = new MockLanguage('en'); + $this->lang ->setLocale('en-US') ->setData('equivalent', [ @@ -86,6 +147,8 @@ public function testGetLineReturnsFallbackLine(): void public function testGetLineArrayReturnsLineArray(): void { + $this->lang = new MockLanguage('en'); + $this->lang->setData('books', [ 'booksList' => [ 'The Boogeyman', @@ -106,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.', ]); @@ -120,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.', @@ -139,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} في كلمات المرور المخترقة.'; @@ -163,6 +232,8 @@ public function testLangAllowsOtherLocales(): void public function testLangDoesntFormat(): void { + $this->lang = new MockLanguage('en'); + $this->lang->disableIntlSupport(); $this->lang->setData('books', [ @@ -185,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 diff --git a/user_guide_src/source/outgoing/localization.rst b/user_guide_src/source/outgoing/localization.rst index f9f7e096b6e9..c5fba00adbc5 100644 --- a/user_guide_src/source/outgoing/localization.rst +++ b/user_guide_src/source/outgoing/localization.rst @@ -14,19 +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:: - - app/ - Language/ - en/ - App.php - 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 +39,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`_. @@ -87,6 +77,7 @@ placeholder ``{locale}`` can be placed as a segment in your route. If present, t segment will be your locale: .. literalinclude:: localization/004.php + :lines: 2- In this example, if the user tried to visit **http://example.com/fr/books**, then the locale would be set to ``fr``, assuming it was configured as a valid locale. @@ -96,14 +87,21 @@ locale will be used in it's place, unless you set to use only the supported loca file: .. literalinclude:: localization/018.php + :lines: 2- .. note:: The ``useSupportedLocalesOnly()`` method can be used since v4.3.0. 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 @@ -118,6 +116,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 ============================= @@ -129,6 +139,7 @@ If your controller is extending ``CodeIgniter\Controller``, this will be availab Alternatively, you can use the :doc:`Services class ` to retrieve the current request: .. literalinclude:: localization/006.php + :lines: 2- .. _language-localization: @@ -139,6 +150,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 @@ -149,6 +170,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 @@ -165,12 +188,14 @@ 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, 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 @@ -188,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 @@ -216,10 +243,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 ------------- @@ -231,14 +260,14 @@ 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 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. @@ -246,10 +275,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 @@ -266,6 +295,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 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');