diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..b0a92f1 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,3 @@ +# Security Policy + +If you discover any security related issues, please email ivan@codezero.be instead of using the issue tracker. diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..4554c07 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,53 @@ +name: Tests + +on: [ push, pull_request ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + php: [ 8.1, 8.2, 8.3 ] + laravel: [ 10.*, 11.* ] + dependency-version: [ prefer-stable ] + exclude: + - laravel: 11.* + php: 8.1 + include: + - laravel: 10.* + testbench: 8.* + - laravel: 11.* + testbench: 9.* + + name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ~/.composer/cache/files + key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick + coverage: xdebug + + - name: Install dependencies + run: composer update --with="illuminate/support:${{ matrix.laravel }}" --with="orchestra/testbench:${{ matrix.testbench }}" --prefer-dist --no-interaction --no-progress + + - name: Execute tests + run: vendor/bin/phpunit --coverage-clover=coverage.xml + + - if: github.event_name == 'push' + name: Run Codacy Coverage Reporter + uses: codacy/codacy-coverage-reporter-action@master + with: + project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} + coverage-reports: coverage.xml diff --git a/.gitignore b/.gitignore index fe1052a..667fe37 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /vendor /phpunit.xml composer.lock +/.phpunit.cache /.phpunit.result.cache diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index 58d1353..0000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,28 +0,0 @@ -filter: - excluded_paths: - - "config/" - - "tests/" -checks: - php: - code_rating: true - remove_extra_empty_lines: true - remove_php_closing_tag: true - remove_trailing_whitespace: true - fix_use_statements: - remove_unused: true - preserve_multiple: false - preserve_blanklines: true - order_alphabetically: true - fix_php_opening_tag: true - fix_linefeed: true - fix_line_ending: true - fix_identation_4spaces: true - fix_doc_comments: true -build: - tests: - override: - - - command: 'vendor/bin/phpunit --coverage-clover=coverage.clover' - coverage: - file: 'coverage.clover' - format: 'clover' diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 18c4d09..0000000 --- a/.travis.yml +++ /dev/null @@ -1,61 +0,0 @@ -language: php -dist: trusty - -matrix: - include: - - php: 7.1 - env: ILLUMINATE_VERSION=5.8.* TESTBENCH_VERSION=3.8.* - - php: 7.1 - env: ILLUMINATE_VERSION=5.7.* TESTBENCH_VERSION=3.7.* - - php: 7.1 - env: ILLUMINATE_VERSION=5.6.* TESTBENCH_VERSION=3.6.* - - php: 7.2 - env: ILLUMINATE_VERSION=6.* TESTBENCH_VERSION=4.* - - php: 7.2 - env: ILLUMINATE_VERSION=5.8.* TESTBENCH_VERSION=3.8.* - - php: 7.2 - env: ILLUMINATE_VERSION=5.7.* TESTBENCH_VERSION=3.7.* - - php: 7.2 - env: ILLUMINATE_VERSION=5.6.* TESTBENCH_VERSION=3.6.* - - php: 7.3 - env: ILLUMINATE_VERSION=7.* TESTBENCH_VERSION=5.* - - php: 7.3 - env: ILLUMINATE_VERSION=8.* TESTBENCH_VERSION=6.* - - php: 7.3 - env: ILLUMINATE_VERSION=6.* TESTBENCH_VERSION=4.* - - php: 7.3 - env: ILLUMINATE_VERSION=5.8.* TESTBENCH_VERSION=3.8.* - - php: 7.3 - env: ILLUMINATE_VERSION=5.7.* TESTBENCH_VERSION=3.7.* - - php: 7.3 - env: ILLUMINATE_VERSION=5.6.* TESTBENCH_VERSION=3.6.* - - php: 7.4 - env: ILLUMINATE_VERSION=7.* TESTBENCH_VERSION=5.* - - php: 7.4 - env: ILLUMINATE_VERSION=8.* TESTBENCH_VERSION=6.* - - php: 7.4 - env: ILLUMINATE_VERSION=6.* TESTBENCH_VERSION=4.* - - php: 7.4 - env: ILLUMINATE_VERSION=5.8.* TESTBENCH_VERSION=3.8.* - - php: 7.4 - env: ILLUMINATE_VERSION=5.7.* TESTBENCH_VERSION=3.7.* - - php: 7.4 - env: ILLUMINATE_VERSION=5.6.* TESTBENCH_VERSION=3.6.* - -before_script: - -before_install: - - travis_retry composer self-update - - composer require "orchestra/testbench:${TESTBENCH_VERSION}" --no-update --dev - - composer require "illuminate/support:${ILLUMINATE_VERSION}" --no-update - -install: composer update --prefer-source --no-interaction - -sudo: false - -cache: - directories: - - $HOME/.composer/cache - -script: - - vendor/bin/phpunit diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 6c13f6a..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,24 +0,0 @@ -# Changelog - -All Notable changes to **Laravel Localizer** will be documented in this file. - -## 1.3.0 (2020-09-07) - -- Add support for Laravel 8 - -## 1.2.0 (2020-03-03) - -- Add support for Laravel 7 -- Add test config for PHP 7.4 - -## 1.1.0 (2019-12-15) - -- Add support for Laravel 6 -- Refactor tests - -## 1.0.0 (2018-04-01) - -- Define your supported locales and match your visitor's preference -- Uses the most common locale detectors by default -- Uses the most common locale stores by default -- Easily create and add your own detectors and stores diff --git a/LICENSE.md b/LICENSE.md index 100fd1b..f77ab2f 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ # The MIT License (MIT) -Copyright (c) 2018 Ivan Vermeyen () +Copyright (c) Ivan Vermeyen (ivan@codezero.be) > Permission is hereby granted, free of charge, to any person obtaining a copy > of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 129531a..8ce4846 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,35 @@ # Laravel Localizer -[![GitHub release](https://img.shields.io/github/release/codezero-be/laravel-localizer.svg)]() -[![License](https://img.shields.io/packagist/l/codezero/laravel-localizer.svg)]() -[![Build Status](https://scrutinizer-ci.com/g/codezero-be/laravel-localizer/badges/build.png?b=master)](https://scrutinizer-ci.com/g/codezero-be/laravel-localizer/build-status/master) -[![Code Coverage](https://scrutinizer-ci.com/g/codezero-be/laravel-localizer/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/codezero-be/laravel-localizer/?branch=master) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/codezero-be/laravel-localizer/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/codezero-be/laravel-localizer/?branch=master) -[![Total Downloads](https://img.shields.io/packagist/dt/codezero/laravel-localizer.svg)](https://packagist.org/packages/codezero/laravel-localizer) +[![GitHub release](https://img.shields.io/github/release/codezero-be/laravel-localizer.svg?style=flat-square)](https://github.com/codezero-be/laravel-localizer/releases) +[![Laravel](https://img.shields.io/badge/laravel-11-red?style=flat-square&logo=laravel&logoColor=white)](https://laravel.com) +[![License](https://img.shields.io/packagist/l/codezero/laravel-localizer.svg?style=flat-square)](LICENSE.md) +[![Build Status](https://img.shields.io/github/actions/workflow/status/codezero-be/laravel-localizer/run-tests.yml?style=flat-square&logo=github&logoColor=white&label=tests)](https://github.com/codezero-be/laravel-localizer/actions) +[![Code Coverage](https://img.shields.io/codacy/coverage/ad6fcea152b449d380a187a375d0f7d7/master?style=flat-square)](https://app.codacy.com/gh/codezero-be/laravel-localizer) +[![Code Quality](https://img.shields.io/codacy/grade/ad6fcea152b449d380a187a375d0f7d7/master?style=flat-square)](https://app.codacy.com/gh/codezero-be/laravel-localizer) +[![Total Downloads](https://img.shields.io/packagist/dt/codezero/laravel-localizer.svg?style=flat-square)](https://packagist.org/packages/codezero/laravel-localizer) -#### Automatically detect and set an app locale that matches your visitor's preference. +[![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R3UQ8V) + +Automatically detect and set an app locale that matches your visitor's preference. - Define your supported locales and match your visitor's preference -- Uses the most common locale [detectors](#detectors) by default -- Uses the most common locale [stores](#stores) by default +- Uses the most common locale [detectors](#-detectors) by default +- Uses the most common locale [stores](#-stores) by default - Easily create and add your own detectors and stores -## Requirements +## ✅ Requirements + +- PHP >= 8.1 +- Laravel >= 10.0 + +## ⬆ Upgrade -- PHP >= 7.1 -- Laravel >= 5.6 +Upgrading to a new major version? +Check our [upgrade guide](UPGRADE.md) for instructions. -## Install +## 📦 Install + +Install this package with Composer: ```bash composer require codezero/laravel-localizer @@ -27,35 +37,52 @@ composer require codezero/laravel-localizer Laravel will automatically register the ServiceProvider. -#### Add Middleware +## 🧩 Add Middleware + +By default, the app locale will always be what you configured in `config/app.php`. +To automatically update the app locale, you need to register the middleware in the `web` middleware group. +Make sure to add it after `StartSession` and before `SubstituteBindings`. -Add the middleware to the `web` middleware group in `app/Http/Kernel.php`, after `StartSession` and before `SubstituteBindings`: +The order of the middleware is important if you are using localized route keys (translated slugs)! +The session needs to be active when setting the locale, and the locale needs to be set when substituting the route bindings. + +### Laravel 11 and newer: + +Add the middleware to the `web` middleware group in `bootstrap/app.php`. ```php +// bootstrap/app.php +->withMiddleware(function (Middleware $middleware) { + $middleware->web(remove: [ + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ]); + $middleware->web(append: [ + \CodeZero\Localizer\Middleware\SetLocale::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ]); +}) +``` + +### Laravel 10: + +Add the middleware to the `web` middleware group in `app/Http/Kernel.php`. + +```php +// app/Http/Kernel.php protected $middlewareGroups = [ 'web' => [ + //... \Illuminate\Session\Middleware\StartSession::class, // <= after this //... \CodeZero\Localizer\Middleware\SetLocale::class, - //... \Illuminate\Routing\Middleware\SubstituteBindings::class, // <= before this ], ]; ``` -In Laravel 6.x you also need to add the middleware to the `$middlewarePriority` array in `app/Http/Kernel.php` to trigger it in the correct order: - -```php -protected $middlewarePriority = [ - \Illuminate\Session\Middleware\StartSession::class, // <= after this - //... - \CodeZero\Localizer\Middleware\SetLocale::class, - //... - \Illuminate\Routing\Middleware\SubstituteBindings::class, // <= before this -]; -``` +## ⚙ Configure -#### Publish Configuration File +### Publish Configuration File ```bash php artisan vendor:publish --provider="CodeZero\Localizer\LocalizerServiceProvider" --tag="config" @@ -63,56 +90,165 @@ php artisan vendor:publish --provider="CodeZero\Localizer\LocalizerServiceProvid You will now find a `localizer.php` file in the `config` folder. -#### Configure Supported Locales +### Configure Supported Locales Add any locales you wish to support to your published `config/localizer.php` file: ```php -'supported-locales' => ['en', 'nl', 'fr']; +'supported_locales' => ['en', 'nl']; ``` -## Drivers +By default, the `UrlDetector` will look for these locales in the URL. -#### Detectors +You can also use one or more custom slugs for a locale: -By default the middleware will use the following detectors to check for a supported locale in: +```php +'supported_locales' => [ + 'en' => 'english-slug', + 'nl' => ['dutch-slug', 'nederlandse-slug'], +]; +``` -1. The URL slug -2. The session -3. A cookie -4. The browser -5. The app's default locale +Or you can use one or more custom domains for a locale: + +```php +'supported_locales' => [ + 'en' => 'english-domain.test', + 'nl' => ['dutch-domain.test', 'nederlands-domain.test'], +]; +``` -If you publish the configuration file, you can choose which detectors to run and in what order. +## 🔍 Detectors -You can also create your own detector by implementing the `\CodeZero\Localizer\Detectors\Detector` interface and add a reference to it in the config file. The detectors are resolved from Laravel's IOC container, so you can add any dependencies to your constructor. +By default, the middleware will use the following detectors to check for a supported locale in: -#### Stores +| # | Detector | Description | +|:---:|-------------------------|------------------------------------------------------------------------| +| 1. | `RouteActionDetector` | Checks for a locale in a custom route action. | +| 2. | `UrlDetector` | Tries to find a locale based on the URL slugs or domain. | +| 3. | `OmittedLocaleDetector` | Required if an omitted locale is configured. This will always be used. | +| 4. | `UserDetector` | Checks a configurable `locale` attribute on the authenticated user. | +| 5. | `SessionDetector` | Checks the session for a previously stored locale. | +| 6. | `CookieDetector` | Checks a cookie for a previously stored locale. | +| 7. | `BrowserDetector` | Checks the preferred language settings of the visitor's browser. | +| 8. | `AppDetector` | Checks the default app locale as a last resort. | -The first supported locale that is returned by a detector will then be stored in: +Update the `detectors` array in the config file to choose which detectors to run and in what order. -- The session -- A cookie -- The app locale +> You can create your own detector by implementing the `CodeZero\Localizer\Detectors\Detector` interface +> and add a reference to it in the config file. The detectors are resolved from Laravel's IOC container, +> so you can add any dependencies to your constructor. -If you publish the configuration file, you can choose which stores to use. +## 💾 Stores -You can also create your own store by implementing the `\CodeZero\Localizer\Stores\Store` interface and add a reference to it in the config file. The stores are resolved from Laravel's IOC container, so you can add any dependencies to your constructor. +The first supported locale that is returned by a detector will automatically be stored in: -## Testing +| # | Store | Description | +|:---:|----------------|-------------------------------------------| +| 1. | `SessionStore` | Stores the locale in the session. | +| 2. | `CookieStore` | Stores the locale in a cookie. | +| 3. | `AppStore` | Sets the locale as the active app locale. | +Update the `stores` array in the config file to choose which stores to use. + +> You can create your own store by implementing the `CodeZero\Localizer\Stores\Store` interface +> and add a reference to it in the config file. The stores are resolved from Laravel's IOC container, +> so you can add any dependencies to your constructor. + +## 🛠 More Configuration + +### ☑ `omitted_locale` + +If you don't want your main locale to have a slug, you can set it as the `omitted_locale` (not the custom slug). + +If you do this, no additional detectors will run after the `UrlDetector` and `OmittedLocaleDetector`. +This makes sense, because the locale will always be determined by those two in this scenario. + +Example: + +```php +'omitted_locale' => 'en', ``` + +Result: + +- /example-route (English without slug) +- /nl/example-route (Other locales with slug) + +Default: `null` + +### ☑ `trusted_detectors` + +Add any detector class name to this array to make it trusted. (do not remove it from the `detectors` array) +When a trusted detector returns a locale, it will be used as the app locale, regardless if it's a supported locale or not. + +Default: `[]` + +### ☑ `url_segment` + +The index of the URL segment that has the locale, when using the `UrlDetector`. + +Default: `1` + +### ☑ `route_action` + +The custom route action that holds the locale, when using the `RouteActionDetector`. + +Default: `locale` + +To use the custom route action `locale`, you register a route like this: + +```php +Route::group(['locale' => 'nl'], function () { + //Route::get(...); +}); +``` + +### ☑ `user_attribute` + +The attribute on the user model that holds the locale, when using the `UserDetector`. +If the user model does not have this attribute, this detector check will be skipped. + +Default: `locale` + +### ☑ `session_key` + +The session key that holds the locale, when using the `SessionDetector` and `SessionStore`. + +Default: `locale` + +### ☑ `cookie_name` + +The name of the cookie that holds the locale, when using the `CookieDetector` and `CookieStore`. + +Default: `locale` + +### ☑ `cookie_minutes` + +The lifetime of the cookie that holds the locale, when using the `CookieStore`. + +Default: `60 * 24 * 365` (1 year) + +## 🚧 Testing + +```bash composer test ``` -## Security +## ☕ Credits + +- [Ivan Vermeyen](https://github.com/ivanvermeyen) +- [All contributors](https://github.com/codezero-be/laravel-localizer/contributors) + +## 🔒 Security If you discover any security related issues, please [e-mail me](mailto:ivan@codezero.be) instead of using the issue tracker. -## Changelog +## 📑 Changelog -See a list of important changes in the [changelog](CHANGELOG.md). +A complete list of all notable changes to this package can be found on the +[releases page](https://github.com/codezero-be/laravel-localizer/releases). -## License +## 📜 License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000..87bf804 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,41 @@ +# Upgrade Guide + +## Upgrading To 3.0 From 2.x + +### ➡ Minimum Requirements Updated + +Due to PHP and PHPUnit version constraints with Laravel 11, we dropped support for Laravel 7.x, 8.x and 9.x. + +- The minimum PHP version required is now 8.1 +- The minimum Laravel version required is now 10.0 + +--- + +### ➡ Re-register Middleware + +Laravel 11 no longer has a `app/Http/Kernel.php` to register middleware. +This is now handled in `bootstrap/app.php`. + +🔸 **Actions Required** + +If you use Laravel 11, register the middleware in `bootstrap/app.php` as described in the README. + +## Upgrading To 2.0 From 1.x + +### ➡ Minimum Requirements Updated + +We dropped support for Laravel 5.6, 5.7, 5.8 and 6.x. + +- The minimum PHP version required is now 7.2.5 +- The minimum Laravel version required is now 7.0 + +--- + +### ➡ Names of Config Options Updated + +Every config option that contained a `-` (dash) in its name has been updated and the dash is replaced by an `_` (underscore). +This is done mainly for consistency across other packages. + +🔸 **Actions Required** + +- Review and update your published config file accordingly. diff --git a/composer.json b/composer.json index 73eaa39..6444ae8 100644 --- a/composer.json +++ b/composer.json @@ -21,14 +21,14 @@ } ], "require": { - "php": "^7.1", + "php": "^8.1", "codezero/browser-locale": "^3.0", - "illuminate/support": "^5.6|^6.0|^7.0|^8.0" + "illuminate/support": "^10.0|^11.0" }, "require-dev": { - "mockery/mockery": "^1.0", - "orchestra/testbench": "^3.6|^4.0|^5.0|^6.0", - "phpunit/phpunit": "^7.0|^8.0|^9.0" + "mockery/mockery": "^1.3.3", + "orchestra/testbench": "^8.0|^9.0", + "phpunit/phpunit": "^10.5" }, "scripts": { "test": "phpunit" diff --git a/config/localizer.php b/config/localizer.php index 3fab7fe..bf34d30 100644 --- a/config/localizer.php +++ b/config/localizer.php @@ -5,20 +5,40 @@ /** * The locales you wish to support. */ - 'supported-locales' => [], + 'supported_locales' => [], + + /** + * If your main locale is omitted from the URL, set it here. + * It will always be used if no supported locale is found in the URL. + * Note that no other detectors will run after the OmittedLocaleDetector! + * Setting this option to `null` will disable this detector. + */ + 'omitted_locale' => null, /** * The detectors to use to find a matching locale. * These will be executed in the order that they are added to the array! */ 'detectors' => [ + CodeZero\Localizer\Detectors\RouteActionDetector::class, CodeZero\Localizer\Detectors\UrlDetector::class, + CodeZero\Localizer\Detectors\OmittedLocaleDetector::class, + CodeZero\Localizer\Detectors\UserDetector::class, CodeZero\Localizer\Detectors\SessionDetector::class, CodeZero\Localizer\Detectors\CookieDetector::class, CodeZero\Localizer\Detectors\BrowserDetector::class, CodeZero\Localizer\Detectors\AppDetector::class, ], + /** + * Add any of the above detector class names here to make it trusted. + * When a trusted detector returns a locale, it will be used + * as the app locale, regardless if it's a supported locale or not. + */ + 'trusted_detectors' => [ + // + ], + /** * The stores to store the first matching locale in. */ @@ -32,24 +52,36 @@ * The index of the segment that has the locale, * when using the UrlDetector. */ - 'url-segment' => 1, + 'url_segment' => 1, + + /** + * The attribute or "action" on the route that holds the locale, + * when using the RouteActionDetector. + */ + 'route_action' => 'locale', + + /** + * The attribute on the user model that holds the locale, + * when using the UserDetector. + */ + 'user_attribute' => 'locale', /** * The session key that holds the locale, * when using the SessionDetector and SessionStore. */ - 'session-key' => 'locale', + 'session_key' => 'locale', /** * The name of the cookie that holds the locale, * when using the CookieDetector and CookieStore. */ - 'cookie-name' => 'locale', + 'cookie_name' => 'locale', /** * The lifetime of the cookie that holds the locale, * when using the CookieStore. */ - 'cookie-minutes' => 60 * 24 * 365, // 1 year + 'cookie_minutes' => 60 * 24 * 365, // 1 year ]; diff --git a/phpunit.xml.dist b/phpunit.xml.dist index e1988a6..dc4366a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,13 +1,13 @@ - + stopOnFailure="false" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" + cacheDirectory=".phpunit.cache" + backupStaticProperties="false"> ./tests/Unit @@ -16,15 +16,15 @@ ./tests/Feature - - - ./src - - + + + ./src + + diff --git a/src/Detectors/CookieDetector.php b/src/Detectors/CookieDetector.php index 8cd4ef7..b56fb32 100644 --- a/src/Detectors/CookieDetector.php +++ b/src/Detectors/CookieDetector.php @@ -14,7 +14,7 @@ class CookieDetector implements Detector */ public function detect() { - $key = Config::get('localizer.cookie-name'); + $key = Config::get('localizer.cookie_name'); return Cookie::get($key); } diff --git a/src/Detectors/OmittedLocaleDetector.php b/src/Detectors/OmittedLocaleDetector.php new file mode 100644 index 0000000..c29d2f3 --- /dev/null +++ b/src/Detectors/OmittedLocaleDetector.php @@ -0,0 +1,18 @@ +getAction($action); + } +} diff --git a/src/Detectors/SessionDetector.php b/src/Detectors/SessionDetector.php index 1eb7739..b420556 100644 --- a/src/Detectors/SessionDetector.php +++ b/src/Detectors/SessionDetector.php @@ -14,7 +14,7 @@ class SessionDetector implements Detector */ public function detect() { - $key = Config::get('localizer.session-key'); + $key = Config::get('localizer.session_key'); return Session::get($key); } diff --git a/src/Detectors/UrlDetector.php b/src/Detectors/UrlDetector.php index 9c46d20..ed96df1 100644 --- a/src/Detectors/UrlDetector.php +++ b/src/Detectors/UrlDetector.php @@ -14,8 +14,46 @@ class UrlDetector implements Detector */ public function detect() { - $position = Config::get('localizer.url-segment'); + $locales = Config::get('localizer.supported_locales'); + $position = Config::get('localizer.url_segment'); + $slug = Request::segment($position); - return Request::segment($position); + // If supported locales is a simple array like ['en', 'nl'] + // just return the slug and let Localizer check if it is supported. + if (count($locales) === 0 || is_numeric(key($locales))) { + return $slug; + } + + // Find the locale that belongs to the custom domain or slug. + // Return the original slug as fallback. + // The calling code should validate and handle it. + $domain = Request::getHttpHost(); + $locales = $this->flipLocalesArray($locales); + $locale = $locales[$domain] ?? $locales[$slug] ?? $slug; + + return $locale; + } + + /** + * Flip the locales array so the custom domain or slug + * become the key and the locale becomes te value. + * + * @param array $locales + * + * @return array + */ + protected function flipLocalesArray($locales) + { + $flipped = []; + + foreach ($locales as $locale => $values) { + $values = is_array($values) ? $values : [$values]; + + foreach ($values as $value) { + $flipped[$value] = $locale; + } + } + + return $flipped; } } diff --git a/src/Detectors/UserDetector.php b/src/Detectors/UserDetector.php new file mode 100644 index 0000000..a9921d2 --- /dev/null +++ b/src/Detectors/UserDetector.php @@ -0,0 +1,27 @@ +getAttributeValue($attribute); + } +} diff --git a/src/Localizer.php b/src/Localizer.php index e5f0702..58bd5ae 100644 --- a/src/Localizer.php +++ b/src/Localizer.php @@ -14,7 +14,7 @@ class Localizer protected $locales; /** - * \CoderZero\Localizer\Detectors\Detector instances. + * \CoderZero\Localizer\Detectors\Detector class names or instances. * * @var \Illuminate\Support\Collection|array */ @@ -27,18 +27,27 @@ class Localizer */ protected $stores; + /** + * \CoderZero\Localizer\Detectors\Detector class names. + * + * @var \Illuminate\Support\Collection|array + */ + protected $trustedDetectors; + /** * Create a new Localizer instance. * * @param \Illuminate\Support\Collection|array $locales * @param \Illuminate\Support\Collection|array $detectors * @param \Illuminate\Support\Collection|array $stores + * @param \Illuminate\Support\Collection|array $trustedDetectors */ - public function __construct($locales, $detectors, $stores = []) + public function __construct($locales, $detectors, $stores = [], $trustedDetectors = []) { - $this->locales = $locales; + $this->setSupportedLocales($locales); $this->detectors = $detectors; $this->stores = $stores; + $this->trustedDetectors = $trustedDetectors; } /** @@ -52,7 +61,7 @@ public function detect() $locales = (array) $this->getInstance($detector)->detect(); foreach ($locales as $locale) { - if ($this->isSupportedLocale($locale)) { + if ($locale && ($this->isSupportedLocale($locale) || $this->isTrustedDetector($detector))) { return $locale; } } @@ -84,6 +93,10 @@ public function store($locale) */ public function setSupportedLocales(array $locales) { + if ( ! array_key_exists(0, $locales)) { + $locales = array_keys($locales); + } + $this->locales = $locales; return $this; @@ -101,6 +114,28 @@ protected function isSupportedLocale($locale) return in_array($locale, $this->locales); } + /** + * Check if the given Detector class is trusted. + * + * @param \CodeZero\Localizer\Detectors\Detector|string $detector + * + * @return bool + */ + protected function isTrustedDetector($detector) + { + if (is_string($detector)) { + return in_array($detector, $this->trustedDetectors); + } + + foreach ($this->trustedDetectors as $trustedDetector) { + if ($detector instanceof $trustedDetector) { + return true; + } + } + + return false; + } + /** * Get the class from Laravel's IOC container if it is a string. * diff --git a/src/LocalizerServiceProvider.php b/src/LocalizerServiceProvider.php index 17b48f7..b0fa9fa 100644 --- a/src/LocalizerServiceProvider.php +++ b/src/LocalizerServiceProvider.php @@ -2,6 +2,7 @@ namespace CodeZero\Localizer; +use CodeZero\BrowserLocale\Laravel\BrowserLocaleServiceProvider; use Illuminate\Support\ServiceProvider; class LocalizerServiceProvider extends ServiceProvider @@ -32,6 +33,7 @@ public function register() { $this->mergeConfig(); $this->registerLocalizer(); + $this->registerProviders(); } /** @@ -65,11 +67,22 @@ protected function mergeConfig() protected function registerLocalizer() { $this->app->bind(Localizer::class, function ($app) { - $locales = $app['config']->get("{$this->name}.supported-locales"); + $locales = $app['config']->get("{$this->name}.supported_locales"); $detectors = $app['config']->get("{$this->name}.detectors"); $stores = $app['config']->get("{$this->name}.stores"); + $trustedDetectors = $app['config']->get("{$this->name}.trusted_detectors"); - return new Localizer($locales, $detectors, $stores); + return new Localizer($locales, $detectors, $stores, $trustedDetectors); }); } + + /** + * Registers the package dependencies + * + * @return void + */ + protected function registerProviders() + { + $this->app->register(BrowserLocaleServiceProvider::class); + } } diff --git a/src/Stores/CookieStore.php b/src/Stores/CookieStore.php index 402a4ef..cb2ea2e 100644 --- a/src/Stores/CookieStore.php +++ b/src/Stores/CookieStore.php @@ -16,8 +16,8 @@ class CookieStore implements Store */ public function store($locale) { - $name = Config::get('localizer.cookie-name'); - $minutes = Config::get('localizer.cookie-minutes'); + $name = Config::get('localizer.cookie_name'); + $minutes = Config::get('localizer.cookie_minutes'); Cookie::queue($name, $locale, $minutes); } diff --git a/src/Stores/SessionStore.php b/src/Stores/SessionStore.php index e56fd84..832896c 100644 --- a/src/Stores/SessionStore.php +++ b/src/Stores/SessionStore.php @@ -16,7 +16,7 @@ class SessionStore implements Store */ public function store($locale) { - $key = Config::get('localizer.session-key'); + $key = Config::get('localizer.session_key'); Session::put($key, $locale); } diff --git a/tests/Feature/SetLocaleTest.php b/tests/Feature/SetLocaleTest.php index f30266f..fcd3656 100644 --- a/tests/Feature/SetLocaleTest.php +++ b/tests/Feature/SetLocaleTest.php @@ -2,16 +2,18 @@ namespace CodeZero\Localizer\Tests\Feature; +use PHPUnit\Framework\Attributes\Test; use CodeZero\BrowserLocale\BrowserLocale; use CodeZero\Localizer\Middleware\SetLocale; use CodeZero\Localizer\Tests\TestCase; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Foundation\Auth\User; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Config; -use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Session; -class SetLocaleTest extends TestCase +final class SetLocaleTest extends TestCase { protected $sessionKey; protected $cookieName; @@ -25,147 +27,358 @@ protected function setUp(): void { parent::setUp(); - $this->sessionKey = Config::get('localizer.session-key'); - $this->cookieName = Config::get('localizer.cookie-name'); + // Remove any default browser locales + $this->setBrowserLocales(null); + + $this->sessionKey = Config::get('localizer.session_key'); + $this->cookieName = Config::get('localizer.cookie_name'); + } + + #[Test] + public function it_looks_for_a_locale_in_a_custom_route_action(): void + { + $this->setSupportedLocales(['en', 'nl']); + $this->setAppLocale('en'); + + $routeAction = ['locale' => 'nl']; + + Route::group($routeAction, function () { + Route::get('some/route', function () { + return App::getLocale(); + })->middleware(['web', \CodeZero\Localizer\Middleware\SetLocale::class]); + }); + + $response = $this->get('some/route'); + + $response->assertSessionHas($this->sessionKey, 'nl'); + $response->assertCookie($this->cookieName, 'nl'); + $this->assertEquals('nl', $response->original); } - /** @test */ - public function it_looks_for_a_locale_in_the_url_first() + #[Test] + public function it_looks_for_a_locale_in_the_url(): void { - $this->setSupportedLocales(['en', 'nl', 'fr', 'de', 'es', 'it']); - $this->setSessionLocale('fr'); - $this->setBrowserLocales('it'); + $this->setSupportedLocales(['en', 'nl']); $this->setAppLocale('en'); - $cookie = 'de'; Route::get('nl/some/route', function () { return App::getLocale(); - })->middleware(['web', SetLocale::class]); + })->middleware(['web', \CodeZero\Localizer\Middleware\SetLocale::class]); - $response = $this->getWithCookie('nl/some/route', $cookie); + $response = $this->get('nl/some/route'); $response->assertSessionHas($this->sessionKey, 'nl'); $response->assertCookie($this->cookieName, 'nl'); $this->assertEquals('nl', $response->original); } - /** @test */ - public function you_can_configure_which_segment_to_use_as_locale() + #[Test] + public function you_can_configure_which_segment_to_use_as_locale(): void { - $this->setSupportedLocales(['en', 'nl', 'fr', 'de', 'es', 'it']); - $this->setSessionLocale('fr'); - $this->setBrowserLocales('it'); + $this->setSupportedLocales(['en', 'nl']); $this->setAppLocale('en'); - $cookie = 'de'; - Config::set('localizer.url-segment', 2); + Config::set('localizer.url_segment', 2); Route::get('some/nl/route', function () { return App::getLocale(); - })->middleware(['web', SetLocale::class]); + })->middleware(['web', \CodeZero\Localizer\Middleware\SetLocale::class]); - $response = $this->getWithCookie('some/nl/route', $cookie); + $response = $this->get('some/nl/route'); $response->assertSessionHas($this->sessionKey, 'nl'); $response->assertCookie($this->cookieName, 'nl'); $this->assertEquals('nl', $response->original); } - /** @test */ - public function it_looks_for_a_locale_in_the_session_if_not_found_in_the_url() + #[Test] + public function it_looks_for_custom_slugs(): void { - $this->setSupportedLocales(['en', 'nl', 'fr', 'de', 'es', 'it']); - $this->setSessionLocale('fr'); - $this->setBrowserLocales('it'); + $this->setSupportedLocales([ + 'en' => 'english', + 'nl' => 'dutch', + ]); $this->setAppLocale('en'); - $cookie = 'de'; + + Route::get('dutch/some/route', function () { + return App::getLocale(); + })->middleware(['web', \CodeZero\Localizer\Middleware\SetLocale::class]); + + $response = $this->get('dutch/some/route'); + + $response->assertSessionHas($this->sessionKey, 'nl'); + $response->assertCookie($this->cookieName, 'nl'); + $this->assertEquals('nl', $response->original); + } + + #[Test] + public function you_can_use_multiple_slugs_for_a_locale(): void + { + $this->setSupportedLocales([ + 'en' => 'english', + 'nl' => ['dutch', 'nederlands'], + ]); + $this->setAppLocale('en'); + + Route::get('dutch/some/route', function () { + return App::getLocale(); + })->middleware(['web', \CodeZero\Localizer\Middleware\SetLocale::class]); + + Route::get('nederlands/some/route', function () { + return App::getLocale(); + })->middleware(['web', \CodeZero\Localizer\Middleware\SetLocale::class]); + + $response = $this->get('dutch/some/route'); + + $response->assertSessionHas($this->sessionKey, 'nl'); + $response->assertCookie($this->cookieName, 'nl'); + $this->assertEquals('nl', $response->original); + + $response = $this->get('nederlands/some/route'); + + $response->assertSessionHas($this->sessionKey, 'nl'); + $response->assertCookie($this->cookieName, 'nl'); + $this->assertEquals('nl', $response->original); + } + + #[Test] + public function it_looks_for_custom_domains(): void + { + $this->setSupportedLocales([ + 'en' => 'english.test', + 'nl' => 'dutch.test', + ]); + $this->setAppLocale('en'); + + Route::group(['domain' => 'dutch.test'], function () { + Route::get('some/route', function () { + return App::getLocale(); + })->middleware(['web', \CodeZero\Localizer\Middleware\SetLocale::class]); + }); + + $response = $this->get('http://dutch.test/some/route'); + + $response->assertSessionHas($this->sessionKey, 'nl'); + $response->assertCookie($this->cookieName, 'nl'); + $this->assertEquals('nl', $response->original); + } + + #[Test] + public function you_can_use_multiple_domains_for_a_locale(): void + { + $this->setSupportedLocales([ + 'en' => 'english.test', + 'nl' => ['dutch.test', 'nederlands.test'], + ]); + $this->setAppLocale('en'); + + Route::group(['domain' => 'dutch.test'], function () { + Route::get('some/route', function () { + return App::getLocale(); + })->middleware(['web', \CodeZero\Localizer\Middleware\SetLocale::class]); + }); + + Route::group(['domain' => 'nederlands.test'], function () { + Route::get('some/route', function () { + return App::getLocale(); + })->middleware(['web', \CodeZero\Localizer\Middleware\SetLocale::class]); + }); + + $response = $this->get('http://dutch.test/some/route'); + + $response->assertSessionHas($this->sessionKey, 'nl'); + $response->assertCookie($this->cookieName, 'nl'); + $this->assertEquals('nl', $response->original); + + $response = $this->get('http://nederlands.test/some/route'); + + $response->assertSessionHas($this->sessionKey, 'nl'); + $response->assertCookie($this->cookieName, 'nl'); + $this->assertEquals('nl', $response->original); + } + + #[Test] + public function it_checks_for_a_configured_omitted_locale(): void + { + $this->setSupportedLocales(['en', 'nl']); + $this->setAppLocale('en'); + + $this->setOmittedLocale('nl'); Route::get('some/route', function () { return App::getLocale(); - })->middleware(['web', SetLocale::class]); + })->middleware(['web', \CodeZero\Localizer\Middleware\SetLocale::class]); - $response = $this->getWithCookie('some/route', $cookie); + $response = $this->get('some/route'); - $response->assertSessionHas($this->sessionKey, 'fr'); - $response->assertCookie($this->cookieName, 'fr'); - $this->assertEquals('fr', $response->original); + $response->assertSessionHas($this->sessionKey, 'nl'); + $response->assertCookie($this->cookieName, 'nl'); + $this->assertEquals('nl', $response->original); } - /** @test */ - public function it_looks_for_a_locale_in_a_cookie_if_not_found_in_the_url_or_session() + #[Test] + public function it_looks_for_a_locale_on_the_authenticated_user(): void { - $this->setSupportedLocales(['en', 'nl', 'fr', 'de', 'es', 'it']); - $this->setSessionLocale(null); - $this->setBrowserLocales('it'); + $this->setSupportedLocales(['en', 'nl']); $this->setAppLocale('en'); - $cookie = 'de'; + + $attribute = Config::get('localizer.user_attribute'); + $user = new User(); + $user->$attribute = 'nl'; Route::get('some/route', function () { return App::getLocale(); - })->middleware(['web', SetLocale::class]); + })->middleware(['web', \CodeZero\Localizer\Middleware\SetLocale::class]); - $response = $this->getWithCookie('some/route', $cookie); + $response = $this->actingAs($user)->get('some/route'); - $response->assertSessionHas($this->sessionKey, 'de'); - $response->assertCookie($this->cookieName, 'de'); - $this->assertEquals('de', $response->original); + $response->assertSessionHas($this->sessionKey, 'nl'); + $response->assertCookie($this->cookieName, 'nl'); + $this->assertEquals('nl', $response->original); } - /** @test */ - public function it_looks_for_a_locale_in_the_browser_if_not_found_in_the_url_or_session_or_cookie() + #[Test] + public function it_will_bypass_missing_attribute_exception_if_the_locale_attribute_is_missing_on_the_user_model(): void { - $this->setSupportedLocales(['en', 'nl', 'fr', 'de', 'es', 'it']); - $this->setSessionLocale(null); - $this->setBrowserLocales('it'); + if (version_compare(App::version(), '9.35.0') === -1) { + $this->markTestSkipped('This test only applies to Laravel 9.35.0 and higher.'); + } + + $this->setSupportedLocales(['en', 'nl']); $this->setAppLocale('en'); + $user = new User(); + $user->exists = true; // exception is only thrown if user "exists" + Model::preventAccessingMissingAttributes(); + Route::get('some/route', function () { return App::getLocale(); - })->middleware(['web', SetLocale::class]); + })->middleware(['web', \CodeZero\Localizer\Middleware\SetLocale::class]); + + $response = $this->actingAs($user)->get('some/route'); + + $response->assertSessionHas($this->sessionKey, 'en'); + $response->assertCookie($this->cookieName, 'en'); + $this->assertEquals('en', $response->original); + } + + #[Test] + public function it_looks_for_a_locale_in_the_session(): void + { + $this->setSupportedLocales(['en', 'nl']); + $this->setAppLocale('en'); + + $this->setSessionLocale('nl'); + + Route::get('some/route', function () { + return App::getLocale(); + })->middleware(['web', \CodeZero\Localizer\Middleware\SetLocale::class]); $response = $this->get('some/route'); - $response->assertSessionHas($this->sessionKey, 'it'); - $response->assertCookie($this->cookieName, 'it'); - $this->assertEquals('it', $response->original); + $response->assertSessionHas($this->sessionKey, 'nl'); + $response->assertCookie($this->cookieName, 'nl'); + $this->assertEquals('nl', $response->original); + } + + #[Test] + public function it_looks_for_a_locale_in_a_cookie(): void + { + $this->setSupportedLocales(['en', 'nl']); + $this->setAppLocale('en'); + + $cookie = 'nl'; + + Route::get('some/route', function () { + return App::getLocale(); + })->middleware(['web', \CodeZero\Localizer\Middleware\SetLocale::class]); + + $response = $this->withCookie($this->cookieName, $cookie) + ->get('some/route'); + + $response->assertSessionHas($this->sessionKey, 'nl'); + $response->assertCookie($this->cookieName, 'nl'); + $this->assertEquals('nl', $response->original); } - /** @test */ - public function it_returns_the_best_match_when_a_browser_locale_is_used() + #[Test] + public function it_looks_for_a_locale_in_the_browser(): void { - $this->setSupportedLocales(['en', 'nl', 'fr', 'de', 'es', 'it']); - $this->setSessionLocale(null); - $this->setBrowserLocales('cs,it-IT;q=0.4,es;q=0.8'); + $this->setSupportedLocales(['en', 'nl']); $this->setAppLocale('en'); + $this->setBrowserLocales('nl'); + Route::get('some/route', function () { return App::getLocale(); - })->middleware(['web', SetLocale::class]); + })->middleware(['web', \CodeZero\Localizer\Middleware\SetLocale::class]); $response = $this->get('some/route'); - $response->assertSessionHas($this->sessionKey, 'es'); - $response->assertCookie($this->cookieName, 'es'); - $this->assertEquals('es', $response->original); + $response->assertSessionHas($this->sessionKey, 'nl'); + $response->assertCookie($this->cookieName, 'nl'); + $this->assertEquals('nl', $response->original); } - /** @test */ - public function it_defaults_to_the_current_app_locale() + #[Test] + public function it_returns_the_best_match_when_a_browser_locale_is_used(): void { - $this->setSupportedLocales(['en', 'nl', 'fr', 'de', 'es', 'it']); - $this->setSessionLocale(null); - $this->setBrowserLocales(null); + $this->setSupportedLocales(['en', 'nl', 'fr']); $this->setAppLocale('en'); + $this->setBrowserLocales('de,fr;q=0.4,nl-BE;q=0.8'); + Route::get('some/route', function () { return App::getLocale(); - })->middleware(['web', SetLocale::class]); + })->middleware(['web', \CodeZero\Localizer\Middleware\SetLocale::class]); $response = $this->get('some/route'); - $response->assertSessionHas($this->sessionKey, 'en'); - $response->assertCookie($this->cookieName, 'en'); - $this->assertEquals('en', $response->original); + $response->assertSessionHas($this->sessionKey, 'nl'); + $response->assertCookie($this->cookieName, 'nl'); + $this->assertEquals('nl', $response->original); + } + + #[Test] + public function it_looks_for_the_current_app_locale(): void + { + $this->setSupportedLocales(['en', 'nl']); + $this->setAppLocale('nl'); + + Route::get('some/route', function () { + return App::getLocale(); + })->middleware(['web', \CodeZero\Localizer\Middleware\SetLocale::class]); + + $response = $this->get('some/route'); + + $response->assertSessionHas($this->sessionKey, 'nl'); + $response->assertCookie($this->cookieName, 'nl'); + $this->assertEquals('nl', $response->original); + } + + #[Test] + public function trusted_detectors_ignore_supported_locales_and_may_set_any_locale(): void + { + $this->setSupportedLocales(['en']); + $this->setAppLocale('en'); + + $routeAction = ['locale' => 'nl']; + + Config::set('localizer.trusted_detectors', [ + \CodeZero\Localizer\Detectors\RouteActionDetector::class, + ]); + + Route::group($routeAction, function () { + Route::get('some/route', function () { + return App::getLocale(); + })->middleware(['web', \CodeZero\Localizer\Middleware\SetLocale::class]); + }); + + $response = $this->get('some/route'); + + $response->assertSessionHas($this->sessionKey, 'nl'); + $response->assertCookie($this->cookieName, 'nl'); + $this->assertEquals('nl', $response->original); } /** @@ -191,7 +404,21 @@ protected function setAppLocale($locale) */ protected function setSupportedLocales(array $locales) { - Config::set('localizer.supported-locales', $locales); + Config::set('localizer.supported_locales', $locales); + + return $this; + } + + /** + * Set the omitted locale. + * + * @param string $locale + * + * @return $this + */ + protected function setOmittedLocale($locale) + { + Config::set('localizer.omitted_locale', $locale); return $this; } @@ -225,19 +452,4 @@ protected function setBrowserLocales($locales) return $this; } - - /** - * Perform a GET request when the given cookie was previously set. - * - * @param string $url - * @param string $cookie - * - * @return \Illuminate\Testing\TestResponse - */ - protected function getWithCookie($url, $cookie) - { - return App::version() < 6 - ? $this->call('GET', $url, [], [$this->cookieName => Crypt::encrypt($cookie, false)]) - : $this->withCookie($this->cookieName, $cookie)->get($url); - } } diff --git a/tests/Unit/LocalizerTest.php b/tests/Unit/LocalizerTest.php index 6180860..ef45338 100644 --- a/tests/Unit/LocalizerTest.php +++ b/tests/Unit/LocalizerTest.php @@ -2,6 +2,7 @@ namespace CodeZero\Localizer\Tests\Unit; +use PHPUnit\Framework\Attributes\Test; use CodeZero\Localizer\Detectors\Detector; use CodeZero\Localizer\Localizer; use CodeZero\Localizer\Stores\Store; @@ -9,10 +10,10 @@ use Illuminate\Support\Facades\App; use Mockery; -class LocalizerTest extends TestCase +final class LocalizerTest extends TestCase { - /** @test */ - public function it_loops_through_the_detectors_and_returns_the_first_supported_locale() + #[Test] + public function it_loops_through_the_detectors_and_returns_the_first_supported_locale(): void { $supportedLocales = ['en', 'nl']; $detectors = [ @@ -26,8 +27,8 @@ public function it_loops_through_the_detectors_and_returns_the_first_supported_l $this->assertEquals('nl', $localizer->detect()); } - /** @test */ - public function it_returns_the_best_match_if_an_array_of_locales_is_detected() + #[Test] + public function it_returns_the_first_match_if_an_array_of_locales_is_detected(): void { $supportedLocales = ['en', 'nl']; $detectors = [ @@ -39,30 +40,34 @@ public function it_returns_the_best_match_if_an_array_of_locales_is_detected() $this->assertEquals('nl', $localizer->detect()); } - /** @test */ - public function it_returns_false_if_no_supported_locale_could_be_detected() + #[Test] + public function trusted_detectors_ignore_supported_locales_and_may_set_any_locale(): void { $supportedLocales = ['en']; $detectors = [ - Mockery::mock(Detector::class)->allows()->detect()->andReturns('de')->getMock(), Mockery::mock(Detector::class)->allows()->detect()->andReturns('nl')->getMock(), - Mockery::mock(Detector::class)->allows()->detect()->andReturns('fr')->getMock(), + ]; + $trustedDetectors = [ + Detector::class, ]; - $localizer = new Localizer($supportedLocales, $detectors); + $localizer = new Localizer($supportedLocales, $detectors, [], $trustedDetectors); - $this->assertFalse($localizer->detect()); + $this->assertEquals('nl', $localizer->detect()); } - /** @test */ - public function it_skips_null_and_false_and_empty_values() + #[Test] + public function it_skips_null_and_false_and_empty_values(): void { + App::instance(Detector::class, Mockery::mock(Detector::class)->allows()->detect()->andReturns('')->getMock()); + $supportedLocales = ['nl']; $detectors = [ - Mockery::mock(Detector::class)->allows()->detect()->andReturns(false)->getMock(), + Detector::class, Mockery::mock(Detector::class)->allows()->detect()->andReturns(null)->getMock(), - Mockery::mock(Detector::class)->allows()->detect()->andReturns([])->getMock(), + Mockery::mock(Detector::class)->allows()->detect()->andReturns(false)->getMock(), Mockery::mock(Detector::class)->allows()->detect()->andReturns('')->getMock(), + Mockery::mock(Detector::class)->allows()->detect()->andReturns([])->getMock(), Mockery::mock(Detector::class)->allows()->detect()->andReturns('nl')->getMock(), ]; @@ -71,8 +76,46 @@ public function it_skips_null_and_false_and_empty_values() $this->assertEquals('nl', $localizer->detect()); } - /** @test */ - public function it_loops_through_the_stores_and_calls_the_store_method_with_the_given_locale() + #[Test] + public function it_skips_null_and_false_and_empty_values_from_trusted_detectors(): void + { + App::instance(Detector::class, Mockery::mock(Detector::class)->allows()->detect()->andReturns('')->getMock()); + + $supportedLocales = ['en']; + $detectors = [ + Detector::class, + Mockery::mock(Detector::class)->allows()->detect()->andReturns(null)->getMock(), + Mockery::mock(Detector::class)->allows()->detect()->andReturns(false)->getMock(), + Mockery::mock(Detector::class)->allows()->detect()->andReturns('')->getMock(), + Mockery::mock(Detector::class)->allows()->detect()->andReturns([])->getMock(), + Mockery::mock(Detector::class)->allows()->detect()->andReturns('nl')->getMock(), + ]; + $trustedDetectors = [ + Detector::class, + ]; + + $localizer = new Localizer($supportedLocales, $detectors, [], $trustedDetectors); + + $this->assertEquals('nl', $localizer->detect()); + } + + #[Test] + public function it_returns_false_if_no_supported_locale_could_be_detected(): void + { + $supportedLocales = ['en']; + $detectors = [ + Mockery::mock(Detector::class)->allows()->detect()->andReturns('de')->getMock(), + Mockery::mock(Detector::class)->allows()->detect()->andReturns('nl')->getMock(), + Mockery::mock(Detector::class)->allows()->detect()->andReturns('fr')->getMock(), + ]; + + $localizer = new Localizer($supportedLocales, $detectors); + + $this->assertFalse($localizer->detect()); + } + + #[Test] + public function it_loops_through_the_stores_and_calls_the_store_method_with_the_given_locale(): void { $stores = [ Mockery::mock(Store::class)->expects()->store('nl')->once()->getMock(), @@ -85,8 +128,8 @@ public function it_loops_through_the_stores_and_calls_the_store_method_with_the_ $localizer->store('nl'); } - /** @test */ - public function it_accepts_class_names_instead_of_instances_in_the_constructor() + #[Test] + public function it_accepts_class_names_instead_of_instances_in_the_constructor(): void { App::instance(Store::class, Mockery::mock(Store::class)->expects()->store('nl')->once()->getMock()); App::instance(Detector::class, Mockery::mock(Detector::class)->expects()->detect()->once()->getMock()); @@ -100,8 +143,8 @@ public function it_accepts_class_names_instead_of_instances_in_the_constructor() $localizer->store('nl'); } - /** @test */ - public function you_can_set_the_supported_locales_at_runtime() + #[Test] + public function you_can_set_the_supported_locales_at_runtime(): void { $supportedLocales = ['en']; $detectors = [