From 0baced3631241ec673a92a76f902436177b3309f Mon Sep 17 00:00:00 2001 From: Sam Feyaerts Date: Thu, 27 Aug 2026 11:29:30 +0200 Subject: [PATCH 1/2] feat: add `redirect_unprefixed` config option --- config/config.php | 14 +++ src/Controllers/RedirectController.php | 40 +++++++++ src/MultilingualRegistrar.php | 40 +++++++++ tests/UnprefixedRedirectTest.php | 119 +++++++++++++++++++++++++ 4 files changed, 213 insertions(+) create mode 100644 src/Controllers/RedirectController.php create mode 100644 tests/UnprefixedRedirectTest.php diff --git a/config/config.php b/config/config.php index d4b4951..d34f9d4 100644 --- a/config/config.php +++ b/config/config.php @@ -47,4 +47,18 @@ */ 'name_prefix_before_locale' => env('MULTILINGUAL_ROUTES_NAME_PREFIX_BEFORE_LOCALE', false), + + /* + |-------------------------------------------------------------------------- + | Redirect Unprefixed Configuration + |-------------------------------------------------------------------------- + | + | The configuration option that defines if routes without a locale prefix + | should be redirected to the default locale. + | + | Applies only if the default locale is prefixed. + | + */ + + 'redirect_unprefixed' => env('MULTILINGUAL_ROUTES_REDIRECT_UNPREFIXED', false), ]; diff --git a/src/Controllers/RedirectController.php b/src/Controllers/RedirectController.php new file mode 100644 index 0000000..6591f09 --- /dev/null +++ b/src/Controllers/RedirectController.php @@ -0,0 +1,40 @@ +route()->getAction(self::DESTINATION); + $route = (new Route('GET', $destination, [ + 'as' => 'multilingual_route_redirect_destination', + ]))->bind($request); + + $parameters = collect($request->route()->parameters())->only( + $route->getCompiled()->getPathVariables() + )->all(); + + $destination = $url->toRoute($route, $parameters, false); + + if ($query = $request->getQueryString()) { + $destination .= "?{$query}"; + } + + return new RedirectResponse($destination, 302); + } +} diff --git a/src/MultilingualRegistrar.php b/src/MultilingualRegistrar.php index 2fa142c..081f27d 100644 --- a/src/MultilingualRegistrar.php +++ b/src/MultilingualRegistrar.php @@ -2,6 +2,7 @@ namespace ChinLeung\MultilingualRoutes; +use ChinLeung\MultilingualRoutes\Controllers\RedirectController; use Illuminate\Routing\Route; use Illuminate\Routing\RouteCollection; use Illuminate\Routing\Router; @@ -46,6 +47,8 @@ public function register(string $key, $handle, array $locales, array $options): $route->defaults($paramKey, $paramValue); } } + + $this->registerUnprefixedRedirect($route, $locale); } return tap($this->router->getRoutes())->refreshNameLookups(); @@ -71,6 +74,8 @@ public function redirect(string $key, string $destination, int $status, array $l $route->defaults($paramKey, $paramValue); } } + + $this->registerUnprefixedRedirect($route, $locale); } return tap($this->router->getRoutes())->refreshNameLookups(); @@ -383,6 +388,41 @@ protected function shouldNotPrefixDefaultHome(string $locale): bool && ! config('laravel-multilingual-routes.prefix_default_home'); } + /** + * Register a redirect to a prefixed default locale route. + * + * @param \Illuminate\Routing\Route $route + * @param string $locale + * @return void + */ + protected function registerUnprefixedRedirect(Route $route, string $locale): void + { + if ( + ! config('laravel-multilingual-routes.redirect_unprefixed') + || $locale !== config('laravel-multilingual-routes.default') + || ! config('laravel-multilingual-routes.prefix_default') + || ! in_array('GET', $route->methods, true) + || ($route->uri === $locale && ! config('laravel-multilingual-routes.prefix_default_home')) + || ($route->uri !== $locale && ! str_starts_with($route->uri, "{$locale}/")) + ) { + return; + } + + $redirect = new Route( + ['GET', 'HEAD'], + $route->uri === $locale ? '/' : substr($route->uri, strlen($locale) + 1), + array_merge(Arr::except($route->action, ['as', 'controller', 'prefix']), [ + 'uses' => RedirectController::class, + RedirectController::DESTINATION => "/{$route->uri}", + ]) + ); + + $redirect->wheres = $route->wheres; + $redirect->defaults = $route->defaults; + + $this->router->getRoutes()->add($redirect); + } + /** * Apply the constraints of a route. * diff --git a/tests/UnprefixedRedirectTest.php b/tests/UnprefixedRedirectTest.php new file mode 100644 index 0000000..37b53ba --- /dev/null +++ b/tests/UnprefixedRedirectTest.php @@ -0,0 +1,119 @@ + ['en', 'fr'], + 'laravel-multilingual-routes.default' => 'en', + 'laravel-multilingual-routes.prefix_default' => true, + 'laravel-multilingual-routes.redirect_unprefixed' => true, + ]); + } + + /** @test */ + public function an_unprefixed_localized_route_redirects_to_the_default_locale(): void + { + Route::multilingual('posts/{post}', static function () { + // + })->where('post', '[0-9]+'); + + $this->get('/posts/123')->assertRedirect('/en/posts/123'); + $this->get('/posts/not-a-number')->assertNotFound(); + } + + /** @test */ + public function get_parameters_are_maintained_on_redirect(): void + { + Route::multilingual('search'); + + $response = $this->get('/search?query=multilingual%20routes&page=2'); + $location = $response->headers->get('Location'); + parse_str(parse_url($location, PHP_URL_QUERY), $query); + + $this->assertSame('/en/search', parse_url($location, PHP_URL_PATH)); + $this->assertSame('multilingual routes', $query['query']); + $this->assertSame('2', $query['page']); + } + + /** @test */ + public function a_url_without_an_applicable_default_locale_route_is_not_redirected(): void + { + Route::multilingual('french-only')->only('fr'); + + $this->get('/french-only')->assertNotFound(); + $this->get('/not-localized')->assertNotFound(); + } + + /** @test */ + public function an_unprefixed_default_locale_does_not_create_a_redirect(): void + { + config(['laravel-multilingual-routes.prefix_default' => false]); + + Route::multilingual('posts', static fn () => 'posts'); + + $this->get('/posts')->assertOk()->assertSee('posts'); + } + + /** @test */ + public function a_non_get_route_does_not_create_an_unprefixed_redirect(): void + { + Route::multilingual('posts', static function () { + // + })->method('post'); + + $this->post('/posts')->assertNotFound(); + $this->post('/en/posts')->assertOk(); + } + + /** @test */ + public function the_home_route_redirects_when_the_default_home_is_prefixed(): void + { + config(['laravel-multilingual-routes.prefix_default_home' => true]); + + Route::multilingual('/'); + + $this->get('/')->assertRedirect('/en'); + } + + /** @test */ + public function an_unprefixed_default_home_route_is_left_untouched(): void + { + config(['laravel-multilingual-routes.prefix_default_home' => false]); + + Route::multilingual('/', static fn () => 'home'); + + $this->get('/')->assertOk()->assertSee('home'); + } + + /** @test */ + public function an_unprefixed_redirect_respects_route_groups(): void + { + Route::prefix('admin')->group(static function () { + Route::multilingual('posts'); + }); + + $response = $this->get('/admin/posts'); + + $response->assertRedirect('/en/admin/posts'); + $this->assertSame('/en/admin/posts', $response->headers->get('Location')); + } + + protected function getPackageProviders($app) + { + return [ + LaravelLocalesServiceProvider::class, + MultilingualRoutesServiceProvider::class, + ]; + } +} From 4c35d6a4ed6711c64d74b5c405d1931dd0ca452f Mon Sep 17 00:00:00 2001 From: Sam Feyaerts Date: Thu, 27 Aug 2026 14:50:08 +0200 Subject: [PATCH 2/2] test: improve test compatibility --- tests/RouteTest.php | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/RouteTest.php b/tests/RouteTest.php index 3475e43..16e4e33 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -164,10 +164,10 @@ public function it_can_register_a_post_route(): void ->method('post') ->register(); - foreach ($routes as $route) { + foreach (locales() as $locale) { $this->assertContains( 'POST', - $route->methods + $routes->getByName("{$locale}.test")->methods ); } } @@ -532,10 +532,16 @@ public function the_default_home_page_can_be_registered_without_prefix(): void /** @test **/ public function a_route_can_be_registered_with_a_middleware(): void { - Route::multilingual('/')->middleware('web'); + $routes = Route::multilingual('/') + ->middleware('web') + ->name('home') + ->register(); - foreach (Route::getRoutes() as $route) { - $this->assertContains('web', data_get($route, 'action.middleware')); + foreach (locales() as $locale) { + $this->assertContains( + 'web', + data_get($routes->getByName("{$locale}.home"), 'action.middleware') + ); } }