From 0aae3da67f5c3567f17e1c255a2df96c0adba120 Mon Sep 17 00:00:00 2001 From: Adam Nielsen Date: Wed, 18 Dec 2024 21:40:42 +0100 Subject: [PATCH 01/20] Change visibility of constructor properties --- .../LaravelLocalization/LaravelLocalization.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Mcamara/LaravelLocalization/LaravelLocalization.php b/src/Mcamara/LaravelLocalization/LaravelLocalization.php index 8cd2215..f4a2688 100644 --- a/src/Mcamara/LaravelLocalization/LaravelLocalization.php +++ b/src/Mcamara/LaravelLocalization/LaravelLocalization.php @@ -78,12 +78,12 @@ class LaravelLocalization * @throws UnsupportedLocaleException */ public function __construct( - private readonly Application $app, - private readonly ConfigRepository $configRepository, - private readonly Translator $translator, - private readonly Router $router, - private readonly Request $request, - private readonly UrlGenerator $url + protected readonly Application $app, + protected readonly ConfigRepository $configRepository, + protected readonly Translator $translator, + protected readonly Router $router, + protected readonly Request $request, + protected readonly UrlGenerator $url ) { // set default locale $this->defaultLocale = $this->configRepository->get('app.locale'); From 74b284b089b9b84b80c0392264e1e95ca8feac2d Mon Sep 17 00:00:00 2001 From: Adam Nielsen Date: Tue, 18 Mar 2025 23:31:12 +0100 Subject: [PATCH 02/20] Cleanup --- .../Middleware/LocaleMappingMiddleware.php | 20 +++++++++---------- .../Middleware/SetLocale.php | 19 +++++++++--------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/Mcamara/LaravelLocalization/Middleware/LocaleMappingMiddleware.php b/src/Mcamara/LaravelLocalization/Middleware/LocaleMappingMiddleware.php index 669968a..49a3324 100644 --- a/src/Mcamara/LaravelLocalization/Middleware/LocaleMappingMiddleware.php +++ b/src/Mcamara/LaravelLocalization/Middleware/LocaleMappingMiddleware.php @@ -4,13 +4,13 @@ use Closure; use Illuminate\Contracts\Config\Repository as ConfigRepository; -use Illuminate\Contracts\Foundation\Application; -use Illuminate\Contracts\Translation\Translator; +use Mcamara\LaravelLocalization\LaravelLocalization; class LocaleMappingMiddleware extends LaravelLocalizationMiddlewareBase { public function __construct( private readonly ConfigRepository $configRepository, + private readonly LaravelLocalization $laravelLocalization, ){ } @@ -20,22 +20,20 @@ public function handle($request, Closure $next) return $next($request); } - // Get the 'locale' parameter from the route $locale = $request->route('locale'); - - // Check if this locale has a mapping $localesMapping = $this->configRepository->get('laravellocalization.localesMapping'); if (array_key_exists($locale, $localesMapping)) { - // @toDO if locale maps to default locale, and hidedefault locale is on, simply redirect to locale = null - // @toDo needs to be tested, not sure if this works $mappedLocale = $localesMapping[$locale]; - $url = $request->fullUrl(); // Get the full URL + $url = $request->fullUrl(); - // Replace only the first occurrence of the locale in the URL - $newUrl = preg_replace("#/$locale#", "/$mappedLocale", $url, 1); + if($this->laravelLocalization->isHiddenDefault($mappedLocale)){ + $newUrl = preg_replace("#/$locale#", "/", $url, 1); + }else{ + $newUrl = preg_replace("#/$locale#", "/$mappedLocale", $url, 1); + } - return redirect($newUrl, 301); // Permanent redirect + return redirect($newUrl, 301); } return $next($request); diff --git a/src/Mcamara/LaravelLocalization/Middleware/SetLocale.php b/src/Mcamara/LaravelLocalization/Middleware/SetLocale.php index c30751c..beea15e 100644 --- a/src/Mcamara/LaravelLocalization/Middleware/SetLocale.php +++ b/src/Mcamara/LaravelLocalization/Middleware/SetLocale.php @@ -6,11 +6,8 @@ use Illuminate\Contracts\Config\Repository as ConfigRepository; use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Translation\Translator; -use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\URL; -use Mcamara\LaravelLocalization\Exceptions\SupportedLocalesNotDefined; -use Mcamara\LaravelLocalization\Exceptions\UnsupportedLocaleException; use Mcamara\LaravelLocalization\LanguageNegotiator; use Mcamara\LaravelLocalization\LaravelLocalization; @@ -33,8 +30,10 @@ public function handle(Request $request, Closure $next) $locale = $request->route('locale'); + // The locale here cannot be an "inverse" mapping, as such cases are handled + // earlier by the locale mapping middleware. if($locale == null || empty($this->laravelLocalization->getSupportedLocales()[$locale])) { - $locale = $this->fallbackLocale($request); + $locale = $this->computeLocale($request); } $this->app->setLocale($locale); @@ -42,7 +41,7 @@ public function handle(Request $request, Closure $next) $this->laravelLocalization->setCurrentLocale($locale); URL::defaults(['locale' => $locale]); - // Regional locale such as de_DE, so formatLocalized works in Carbon + // Configure regional locale settings (e.g., de_DE) for proper formatting in Carbon. $regional = $this->laravelLocalization->getCurrentLocaleRegional(); $suffix = $this->configRepository->get('laravellocalization.utf8suffix'); if ($regional) { @@ -53,18 +52,18 @@ public function handle(Request $request, Closure $next) return $next($request); } - // if the first segment/locale passed is not valid the system would either take default locale, - // (if hideDefaultLocaleInURL is set, or retrieve it from the browser - protected function fallbackLocale(Request $request): string + protected function computeLocale(Request $request): string { $defaultLocale = $this->configRepository->get('app.locale'); - // if we reached this point and hideDefaultLocaleInURL is true, take default + // If we reached this point and `hideDefaultLocaleInURL` is enabled, enforce the default locale. + // In this case, `useAcceptLanguageHeader` is only considered by the `LaravelSessionRedirect` middleware + // when no locale has been set in the session. if ($this->laravelLocalization->hideDefaultLocaleInURL()) { return $defaultLocale; } - // but if hideDefaultLocaleInURL is false, we may have to retrieve it from the browser... + // If browser language negotiation is enabled, attempt to detect the best match. if ($this->laravelLocalization->useAcceptLanguageHeader()) { $negotiator = new LanguageNegotiator($defaultLocale, $this->laravelLocalization->getSupportedLocales(), $request); From 80c816c33e5820b46e6c01ca63fc9316ead495df Mon Sep 17 00:00:00 2001 From: Adam Nielsen Date: Tue, 18 Mar 2025 23:35:09 +0100 Subject: [PATCH 03/20] Add support for routes without locale parameter in macro --- .../LaravelLocalizationServiceProvider.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php b/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php index 0e45a7b..b8f879b 100644 --- a/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php +++ b/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php @@ -71,10 +71,18 @@ protected function registerMacros(): void $supportedLocales = array_keys(config('laravellocalization.supportedLocales', [])); $localesMapping = array_keys(config('laravellocalization.localesMapping', [])); + $hideDefaultLocaleInURL = config('laravellocalization.hideDefaultLocaleInURL', false); + $allowedLocales = implode('|', array_unique(array_merge($supportedLocales, $localesMapping))); Route::prefix('/{locale}') ->where(['locale' => $allowedLocales]) ->group($routes); + + //@toDo translatedRoutes need to be defined inhere aswell + + if($hideDefaultLocaleInURL){ + Route::name('default_locale.')->group($routes); + } }); }); } From be359d4da1677a1cfaea5183c84a45865654d7cd Mon Sep 17 00:00:00 2001 From: Adam Nielsen Date: Tue, 18 Mar 2025 23:36:47 +0100 Subject: [PATCH 04/20] Remove alias `localizeURL` and baseUrl paramter --- .../LaravelLocalization.php | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/src/Mcamara/LaravelLocalization/LaravelLocalization.php b/src/Mcamara/LaravelLocalization/LaravelLocalization.php index f4a2688..4b860f3 100644 --- a/src/Mcamara/LaravelLocalization/LaravelLocalization.php +++ b/src/Mcamara/LaravelLocalization/LaravelLocalization.php @@ -16,13 +16,6 @@ class LaravelLocalization { - /** - * Illuminate request class. - * - * @var string - */ - protected $baseUrl; - /** * Default locale. * @@ -118,21 +111,6 @@ public function setSupportedLocales($locales) $this->supportedLocales = $locales; } - /** - * Returns an URL adapted to $locale or current locale. - * - * @param string $url URL to adapt. If not passed, the current url would be taken. - * @param string|bool $locale Locale to adapt, false to remove locale - * - * @throws UnsupportedLocaleException - * - * @return string URL translated - */ - public function localizeURL($url = null, $locale = null) - { - return $this->getLocalizedURL($locale, $url); - } - /** * Returns an URL adapted to $locale. * From 9b3089a1b8ed5a8d51a12445b315063d27996b25 Mon Sep 17 00:00:00 2001 From: Adam Nielsen Date: Tue, 18 Mar 2025 23:42:55 +0100 Subject: [PATCH 05/20] Refactor `getLocalizedURL` --- .../LaravelLocalization.php | 115 ++++++++---------- 1 file changed, 49 insertions(+), 66 deletions(-) diff --git a/src/Mcamara/LaravelLocalization/LaravelLocalization.php b/src/Mcamara/LaravelLocalization/LaravelLocalization.php index 4b860f3..dd69aee 100644 --- a/src/Mcamara/LaravelLocalization/LaravelLocalization.php +++ b/src/Mcamara/LaravelLocalization/LaravelLocalization.php @@ -115,8 +115,8 @@ public function setSupportedLocales($locales) * Returns an URL adapted to $locale. * * - * @param string|bool $locale Locale to adapt, false to remove locale - * @param string|false $url URL to adapt in the current language. If not passed, the current url would be taken. + * @param string|null $locale Locale to adapt, false to remove locale + * @param string|null $url URL to adapt in the current language. If not passed, the current url would be taken. * @param array $attributes Attributes to add to the route, if empty, the system would try to extract them from the url. * @param bool $forceDefaultLocation Force to show default location even hideDefaultLocaleInURL set as TRUE * @@ -125,95 +125,78 @@ public function setSupportedLocales($locales) * * @return string|false URL translated, False if url does not exist */ - public function getLocalizedURL($locale = null, $url = null, $attributes = [], $forceDefaultLocation = false) + public function getLocalizedURL(string|null $locale = null, string|null $url = null, array $attributes = [], bool $forceDefaultLocation = false): string|false { - if ($locale === null) { - $locale = $this->getCurrentLocale(); - } + $locale = $locale ?: $this->getCurrentLocale(); + $locale = $this->getLocaleFromMapping($locale); if (!$this->checkLocaleInSupportedLocales($locale)) { - throw new UnsupportedLocaleException('Locale \''.$locale.'\' is not in the list of supported locales.'); + throw new UnsupportedLocaleException("Locale '{$locale}' is not supported."); } - if (empty($attributes)) { - $attributes = $this->extractAttributes($url, $locale); + if($url === null){ + // Including protocol, domain and query , e.g. `https://example.com/posts?page=2&sort=asc` + $url = $this->request->fullUrl(); } - if (empty($url)) { - $url = $this->request->fullUrl(); - $urlQuery = parse_url($url, PHP_URL_QUERY); - $urlQuery = $urlQuery ? '?'.$urlQuery : ''; + $route = $this->matchRouteForAnyRoute($url); - if (!empty($this->routeName)) { - return $this->getURLFromRouteNameTranslated($locale, $this->routeName, $attributes, $forceDefaultLocation) . $urlQuery; - } - } else { - $urlQuery = parse_url($url, PHP_URL_QUERY); - $urlQuery = $urlQuery ? '?'.$urlQuery : ''; - - $url = $this->url->to($url); + if ($route === null) { + return false; } - $url = preg_replace('/'. preg_quote($urlQuery, '/') . '$/', '', $url); - - if ($locale && $translatedRoute = $this->findTranslatedRouteByUrl($url, $attributes, $this->currentLocale)) { - return $this->getURLFromRouteNameTranslated($locale, $translatedRoute, $attributes, $forceDefaultLocation).$urlQuery; + if(empty($attributes)){ + $attributes = $route->parameters(); } - $base_path = $this->request->getBaseUrl(); - $parsed_url = parse_url($url); - $url_locale = $this->getDefaultLocale(); + $uri = $route->uri(); + $urlQuery = parse_url($url, PHP_URL_QUERY); + // urlQuery , e.g. `?page=2&sort=asc` + $urlQuery = $urlQuery ? '?'.$urlQuery : ''; - if (!$parsed_url || empty($parsed_url['path'])) { - $path = $parsed_url['path'] = ''; - } else { - $parsed_url['path'] = str_replace($base_path, '', '/'.ltrim($parsed_url['path'], '/')); - $path = $parsed_url['path']; - foreach ($this->getSupportedLocales() as $localeCode => $lang) { - $localeCode = $this->getLocaleFromMapping($localeCode); - - $parsed_url['path'] = preg_replace('%^/?'.$localeCode.'/%', '$1', $parsed_url['path']); - if ($parsed_url['path'] !== $path) { - $url_locale = $localeCode; - break; - } - - $parsed_url['path'] = preg_replace('%^/?'.$localeCode.'$%', '$1', $parsed_url['path']); - if ($parsed_url['path'] !== $path) { - $url_locale = $localeCode; - break; - } + if (!isset($attributes['locale'])){ + if($locale === $this->getDefaultLocale()){ + return $url; } - } - $parsed_url['path'] = ltrim($parsed_url['path'], '/'); - - if ($translatedRoute = $this->findTranslatedRouteByPath($parsed_url['path'], $url_locale)) { - return $this->getURLFromRouteNameTranslated($locale, $translatedRoute, $attributes, $forceDefaultLocation).$urlQuery; + // Locale must be different from default, therefore it should not be hidden + return $this->url->to($locale . '/' . $uri, $attributes) . $urlQuery; } - $locale = $this->getLocaleFromMapping($locale); + $localeOfUrl = $attributes['locale']; - if (!empty($locale)) { - if ($forceDefaultLocation || $locale != $this->getDefaultLocale() || !$this->hideDefaultLocaleInURL()) { - $parsed_url['path'] = $locale.'/'.ltrim($parsed_url['path'], '/'); - } + if($locale === $localeOfUrl){ + return $url; } - $parsed_url['path'] = ltrim(ltrim($base_path, '/').'/'.$parsed_url['path'], '/'); - //Make sure that the pass path is returned with a leading slash only if it come in with one. - if (Str::startsWith($path, '/') === true) { - $parsed_url['path'] = '/'.$parsed_url['path']; + // if the locale is default and hidden by default, we need to workaround + if ($this->isHiddenDefault($locale)){ + unset($attributes['locale']); + $cleanedUri = preg_replace('%^/?{locale}(/|$)%', '', $uri); + return $this->url->to($cleanedUri, $attributes) . $urlQuery; } - $parsed_url['path'] = rtrim($parsed_url['path'], '/'); - $url = $this->unparseUrl($parsed_url); + // Update locale and move on + $attributes['locale'] = $locale; + return $this->url->to($uri, $attributes) . $urlQuery; + } + + protected function matchRouteForAnyRoute(string $url): Route|null + { + $methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']; - if ($this->checkUrl($url)) { - return $url.$urlQuery; + foreach ($methods as $method) { + try { + $request = Request::create($url, $method); + $route = $this->router->getRoutes()->match($request); + + return $route; + } catch (\Exception $e) { + continue; + } } - return $this->createUrlFromUri($url).$urlQuery; + return null; } /** From fc4e56bbbef99c2bde338a3f5229cfff3b0edb2f Mon Sep 17 00:00:00 2001 From: Adam Nielsen Date: Tue, 18 Mar 2025 23:45:42 +0100 Subject: [PATCH 06/20] Cleanup unused code after `getLocalizedURL` refactor Refactoring getLocalizedURL made a lot of code obsolete, especially parts related to translatedRoutes. --- .../LaravelLocalization.php | 285 +----------------- 1 file changed, 7 insertions(+), 278 deletions(-) diff --git a/src/Mcamara/LaravelLocalization/LaravelLocalization.php b/src/Mcamara/LaravelLocalization/LaravelLocalization.php index dd69aee..8596b0c 100644 --- a/src/Mcamara/LaravelLocalization/LaravelLocalization.php +++ b/src/Mcamara/LaravelLocalization/LaravelLocalization.php @@ -8,9 +8,8 @@ use Illuminate\Contracts\Routing\UrlRoutable; use Illuminate\Contracts\Translation\Translator; use Illuminate\Http\Request; +use Illuminate\Routing\Route; use Illuminate\Routing\Router; -use Illuminate\Support\Str; -use Illuminate\Support\Env; use Mcamara\LaravelLocalization\Exceptions\SupportedLocalesNotDefined; use Mcamara\LaravelLocalization\Exceptions\UnsupportedLocaleException; @@ -44,12 +43,6 @@ class LaravelLocalization */ protected $currentLocale = false; - /** - * An array that contains all routes that should be translated. - * - * @var array - */ - protected $translatedRoutes = []; /** * Name of the translation key of the current route, it is used for url translations. @@ -59,15 +52,6 @@ class LaravelLocalization protected $routeName; /** - * An array that contains all translated routes by url - * - * @var array - */ - protected $cachedTranslatedRoutesByUrl = []; - - /** - * Creates new instance. - * * @throws UnsupportedLocaleException */ public function __construct( @@ -78,7 +62,6 @@ public function __construct( protected readonly Request $request, protected readonly UrlGenerator $url ) { - // set default locale $this->defaultLocale = $this->configRepository->get('app.locale'); $supportedLocales = $this->getSupportedLocales(); @@ -87,19 +70,10 @@ public function __construct( } } - - /** - * Check if $locale is default locale and supposed to be hidden in url - * - * @param string $locale Locale to be checked - * - * @return boolean Returns true if above requirement are met, otherwise false - */ - - public function isHiddenDefault($locale) - { - return ($this->getDefaultLocale() === $locale && $this->hideDefaultLocaleInURL()); - } + public function isHiddenDefault($locale): bool + { + return ($this->getDefaultLocale() === $locale && $this->hideDefaultLocaleInURL()); + } /** * Set and return supported locales. @@ -199,62 +173,6 @@ protected function matchRouteForAnyRoute(string $url): Route|null return null; } - /** - * Returns an URL adapted to the route name and the locale given. - * - * - * @param string|bool $locale Locale to adapt - * @param string $transKeyName Translation key name of the url to adapt - * @param array $attributes Attributes for the route (only needed if transKeyName needs them) - * @param bool $forceDefaultLocation Force to show default location even hideDefaultLocaleInURL set as TRUE - * - * @throws SupportedLocalesNotDefined - * @throws UnsupportedLocaleException - * - * @return string|false URL translated - */ - public function getURLFromRouteNameTranslated($locale, $transKeyName, $attributes = [], $forceDefaultLocation = false) - { - if (!$this->checkLocaleInSupportedLocales($locale)) { - throw new UnsupportedLocaleException('Locale \''.$locale.'\' is not in the list of supported locales.'); - } - - if (!\is_string($locale)) { - $locale = $this->getDefaultLocale(); - } - - $route = ''; - - if ($forceDefaultLocation || !($locale === $this->defaultLocale && $this->hideDefaultLocaleInURL())) { - $route = '/'.$locale; - } - if (\is_string($locale) && $this->translator->has($transKeyName, $locale)) { - $translation = $this->translator->get($transKeyName, [], $locale); - $route .= '/'.$translation; - - $route = $this->substituteAttributesInRoute($attributes, $route, $locale); - } - - if (empty($route)) { - // This locale does not have any key for this route name - return false; - } - - return rtrim($this->createUrlFromUri($route), '/'); - } - - /** - * It returns an URL without locale (if it has it) - * Convenience function wrapping getLocalizedURL(false). - * - * @param string|false $url URL to clean, if false, current url would be taken - * - * @return string URL with no locale in path - */ - public function getNonLocalizedURL($url = null) - { - return $this->getLocalizedURL(false, $url); - } /** * Returns default locale. @@ -484,42 +402,6 @@ public function checkLocaleInSupportedLocales($locale) return true; } - /** - * Change route attributes for the ones in the $attributes array. - * - * @param $attributes array Array of attributes - * @param string $route string route to substitute - * - * @return string route with attributes changed - */ - protected function substituteAttributesInRoute($attributes, $route, $locale = null) - { - foreach ($attributes as $key => $value) { - if ($value instanceOf Interfaces\LocalizedUrlRoutable) { - $value = $value->getLocalizedRouteKey($locale); - } - elseif ($value instanceOf UrlRoutable) { - $value = $value->getRouteKey(); - } - $route = str_replace(array('{'.$key.'}', '{'.$key.'?}'), $value, $route); - } - - // delete empty optional arguments that are not in the $attributes array - $route = preg_replace('/\/{[^)]+\?}/', '', $route); - - return $route; - } - - /** - * Returns translated routes. - * - * @return array translated routes - */ - protected function getTranslatedRoutes() - { - return $this->translatedRoutes; - } - /** * Set current route name. * @@ -530,115 +412,6 @@ public function setRouteName($routeName) $this->routeName = $routeName; } - /** - * Translate routes and save them to the translated routes array (used in the localize route filter). - * - * @param string $routeName Key of the translated string - * - * @return string Translated string - */ - public function transRoute($routeName) - { - if (!\in_array($routeName, $this->translatedRoutes)) { - $this->translatedRoutes[] = $routeName; - } - - return $this->translator->get($routeName); - } - - /** - * Returns the translation key for a given path. - * - * @param string $path Path to get the key translated - * - * @return string|false Key for translation, false if not exist - */ - public function getRouteNameFromAPath($path) - { - $attributes = $this->extractAttributes($path); - - $path = parse_url($path)['path']; - $path = trim(str_replace('/'.$this->currentLocale.'/', '', $path), "/"); - - foreach ($this->translatedRoutes as $route) { - if (trim($this->substituteAttributesInRoute($attributes, $this->translator->get($route), $this->currentLocale), '/') === $path) { - return $route; - } - } - - return false; - } - - /** - * Returns the translated route for the path and the url given. - * - * @param string $path Path to check if it is a translated route - * @param string $url_locale Language to check if the path exists - * - * @return string|false Key for translation, false if not exist - */ - protected function findTranslatedRouteByPath($path, $url_locale) - { - // check if this url is a translated url - foreach ($this->translatedRoutes as $translatedRoute) { - if ($this->translator->get($translatedRoute, [], $url_locale) == rawurldecode($path)) { - return $translatedRoute; - } - } - - return false; - } - - /** - * Returns the translated route for an url and the attributes given and a locale. - * - * - * @param string|false|null $url Url to check if it is a translated route - * @param array $attributes Attributes to check if the url exists in the translated routes array - * @param string $locale Language to check if the url exists - * - * @throws SupportedLocalesNotDefined - * @throws UnsupportedLocaleException - * - * @return string|false Key for translation, false if not exist - */ - protected function findTranslatedRouteByUrl($url, $attributes, $locale) - { - if (empty($url)) { - return false; - } - - if (isset($this->cachedTranslatedRoutesByUrl[$locale][$url])) { - return $this->cachedTranslatedRoutesByUrl[$locale][$url]; - } - - // check if this url is a translated url - foreach ($this->translatedRoutes as $translatedRoute) { - $routeName = $this->getURLFromRouteNameTranslated($locale, $translatedRoute, $attributes); - - // We can ignore extra url parts and compare only their url_path (ignore arguments that are not attributes) - if (parse_url($this->getNonLocalizedURL($routeName), PHP_URL_PATH) == parse_url($this->getNonLocalizedURL(urldecode($url)), PHP_URL_PATH)) { - $this->cachedTranslatedRoutesByUrl[$locale][$url] = $translatedRoute; - - return $translatedRoute; - } - } - - return false; - } - - /** - * Returns true if the string given is a valid url. - * - * @param string $url String to check if it is a valid url - * - * @return bool Is the string given a valid url? - */ - protected function checkUrl($url) - { - return filter_var($url, FILTER_VALIDATE_URL); - } - /** * Returns the config repository for this instance. * @@ -681,54 +454,10 @@ public function hideDefaultLocaleInURL() * * @return string Url for the given uri */ - public function createUrlFromUri($uri) + public function createUrlFromUri(string $uri): string { $uri = ltrim($uri, '/'); - - if (empty($this->baseUrl)) { - return app('url')->to($uri); - } - - return $this->baseUrl.$uri; - } - - /** - * Sets the base url for the site. - * - * @param string $url Base url for the site - */ - public function setBaseUrl($url) - { - if (substr($url, -1) != '/') { - $url .= '/'; - } - - $this->baseUrl = $url; - } - - /** - * Returns serialized translated routes for caching purposes. - * - * @return string - */ - public function getSerializedTranslatedRoutes() - { - return base64_encode(serialize($this->translatedRoutes)); - } - - /** - * Sets the translated routes list. - * Only useful from a cached routes context. - * - * @param string $serializedRoutes - */ - public function setSerializedTranslatedRoutes($serializedRoutes) - { - if ( ! $serializedRoutes) { - return; - } - - $this->translatedRoutes = unserialize(base64_decode($serializedRoutes)); + return app('url')->to($uri); } /** From 79eb2fce6e35c22e191d1bd352f610b00e821ebd Mon Sep 17 00:00:00 2001 From: Adam Nielsen Date: Tue, 18 Mar 2025 23:50:06 +0100 Subject: [PATCH 07/20] Remove routeName property and LaraveLLocalizationRoute middleware No longer needed after refactor of `getLocalizedURL` --- .../LaravelLocalization.php | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/Mcamara/LaravelLocalization/LaravelLocalization.php b/src/Mcamara/LaravelLocalization/LaravelLocalization.php index 8596b0c..825c9ef 100644 --- a/src/Mcamara/LaravelLocalization/LaravelLocalization.php +++ b/src/Mcamara/LaravelLocalization/LaravelLocalization.php @@ -43,14 +43,6 @@ class LaravelLocalization */ protected $currentLocale = false; - - /** - * Name of the translation key of the current route, it is used for url translations. - * - * @var string - */ - protected $routeName; - /** * @throws UnsupportedLocaleException */ @@ -402,16 +394,6 @@ public function checkLocaleInSupportedLocales($locale) return true; } - /** - * Set current route name. - * - * @param string $routeName current route name - */ - public function setRouteName($routeName) - { - $this->routeName = $routeName; - } - /** * Returns the config repository for this instance. * From 88318ea7fa4e99cbd5e73761c0cb7a7ef12f081f Mon Sep 17 00:00:00 2001 From: Adam Nielsen Date: Tue, 18 Mar 2025 23:53:00 +0100 Subject: [PATCH 08/20] Add some notes of change --- v3_changes.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 v3_changes.md diff --git a/v3_changes.md b/v3_changes.md new file mode 100644 index 0000000..aabd4ac --- /dev/null +++ b/v3_changes.md @@ -0,0 +1,28 @@ +# Changes + +The major architectural change, discussed in [#921](https://github.com/mcamara/laravel-localization/issues/921), shifts locale handling from the route file to middleware. + +This allows us to remove the custom caching solution and rely on Laravel's default caching. However, it introduces a new workaround for translated routes—although they never worked completely bug-free in the first place. + +Below is a list of functions and features removed in v3. Some code was removed due to a lack of documentation, tests, or a clear explanation in the pull request. Additionally, a lot of code appeared to be unused, and even after careful reverse engineering, its purpose remained unclear—so it was removed. + +If you notice something critical missing, please **open an issue**. + +## Removals & Changes + +- **Removed custom caching command** – Now fully compatible with Laravel’s built-in caching. +- **Locale is now a route parameter** instead of being set directly in route definitions. +- **Removed `baseUrl` property and related methods**. +- **`getBaseUrl()` is no longer used** – If you have a valid use case, please open an issue. +- **Removed `route.translation` event** – Documentation was unclear, there were open issues, and it was inconsistently triggered (only when the URL was empty during localization). +- **The `data` attribute is no longer removed from routes attributes** – This should not be the responsibility of the package. +- **`getLocalizedURL(locale: false)` no longer removes the locale from the URL**. +- **Dropped alias `localizeURL`** – If needed, you can define a custom helper. +- **`translatedRoutes` is no longer stored inside `LaravelLocalization`**. +- **`transRoute()` method is no longer supported** – Use `__('routes.*')` instead. +- **Removed `getNonLocalizedURL()`**. +- **All `translatableRoutes` related methods have been removed** from `LaravelLocalization`. +- **Removed `LaravelLocalizationRoutes` middleware** and its associated `$routeName` attribute. + +If something crucial was removed by mistake or if you encounter missing functionality, feel free to **create an issue**. + From 113174a7cfa3a6e22195f1102f722114557a2fdd Mon Sep 17 00:00:00 2001 From: Adam Nielsen Date: Tue, 18 Mar 2025 23:56:10 +0100 Subject: [PATCH 09/20] Further cleanup --- .../LaravelLocalization.php | 168 ------------------ v3_changes.md | 2 + 2 files changed, 2 insertions(+), 168 deletions(-) diff --git a/src/Mcamara/LaravelLocalization/LaravelLocalization.php b/src/Mcamara/LaravelLocalization/LaravelLocalization.php index 825c9ef..81e3f8d 100644 --- a/src/Mcamara/LaravelLocalization/LaravelLocalization.php +++ b/src/Mcamara/LaravelLocalization/LaravelLocalization.php @@ -428,172 +428,4 @@ public function hideDefaultLocaleInURL() { return $this->configRepository->get('laravellocalization.hideDefaultLocaleInURL'); } - - /** - * Create an url from the uri. - * - * @param string $uri Uri - * - * @return string Url for the given uri - */ - public function createUrlFromUri(string $uri): string - { - $uri = ltrim($uri, '/'); - return app('url')->to($uri); - } - - /** - * Extract attributes for current url. - * - * @param bool|false|null|string $url to extract attributes, if not present, the system will look for attributes in the current call - * @param string $locale - * - * @return array Array with attributes - */ - protected function extractAttributes($url = false, $locale = '') - { - if (!empty($url)) { - $attributes = []; - $parse = parse_url($url); - if (isset($parse['path'])) { - $parse['path'] = trim(str_replace('/'.$this->currentLocale.'/', '', $parse['path']), "/"); - $url = explode('/', trim($parse['path'], '/')); - } else { - $url = []; - } - - foreach ($this->router->getRoutes() as $route) { - $attributes = []; - $path = method_exists($route, 'uri') ? $route->uri() : $route->getUri(); - - if (!preg_match("/{[\w]+\??}/", $path)) { - continue; - } - - $path = explode('/', $path); - $i = 0; - - // The system's route can't be smaller - // only the $url can be missing segments (optional parameters) - // We can assume it's the wrong route - if (count($path) < count($url)) { - continue; - } - - $match = true; - foreach ($path as $j => $segment) { - if (isset($url[$i])) { - if ($segment === $url[$i]) { - $i++; - continue; - } elseif (preg_match("/{[\w]+}/", $segment)) { - // must-have parameters - $attribute_name = preg_replace(['/}/', '/{/', "/\?/"], '', $segment); - $attributes[$attribute_name] = $url[$i]; - $i++; - continue; - } elseif (preg_match("/{[\w]+\?}/", $segment)) { - // optional parameters - if (!isset($path[$j + 1]) || $path[$j + 1] !== $url[$i]) { - // optional parameter taken - $attribute_name = preg_replace(['/}/', '/{/', "/\?/"], '', $segment); - $attributes[$attribute_name] = $url[$i]; - $i++; - continue; - } else { - $match = false; - break; - } - } else { - // As soon as one segment doesn't match, then we have the wrong route - $match = false; - break; - } - } elseif (preg_match("/{[\w]+\?}/", $segment)) { - $attribute_name = preg_replace(['/}/', '/{/', "/\?/"], '', $segment); - $attributes[$attribute_name] = null; - $i++; - } else { - // no optional parameters but no more $url given - // this route does not match the url - $match = false; - break; - } - } - - if (isset($url[$i + 1])) { - $match = false; - } - - if ($match) { - return $attributes; - } - } - } else { - if (!$this->router->current()) { - return []; - } - - $attributes = $this->normalizeAttributes($this->router->current()->parameters()); - $response = event('routes.translation', [$locale, $attributes]); - - if (!empty($response)) { - $response = array_shift($response); - } - - if (\is_array($response)) { - $attributes = array_merge($attributes, $response); - } - } - - return $attributes; - } - - /** - * Build URL using array data from parse_url. - * - * @param array|false $parsed_url Array of data from parse_url function - * - * @return string Returns URL as string. - */ - protected function unparseUrl($parsed_url) - { - if (empty($parsed_url)) { - return ''; - } - - $url = ''; - $url .= isset($parsed_url['scheme']) ? $parsed_url['scheme'].'://' : ''; - $url .= $parsed_url['host'] ?? ''; - $url .= isset($parsed_url['port']) ? ':'.$parsed_url['port'] : ''; - $user = $parsed_url['user'] ?? ''; - $pass = isset($parsed_url['pass']) ? ':'.$parsed_url['pass'] : ''; - $url .= $user.(($user || $pass) ? "$pass@" : ''); - - if (!empty($url)) { - $url .= isset($parsed_url['path']) ? '/'.ltrim($parsed_url['path'], '/') : ''; - } else { - $url .= $parsed_url['path'] ?? ''; - } - - $url .= isset($parsed_url['query']) ? '?'.$parsed_url['query'] : ''; - $url .= isset($parsed_url['fragment']) ? '#'.$parsed_url['fragment'] : ''; - - return $url; - } - - /** - * Normalize attributes gotten from request parameters. - * - * @param array $attributes The attributes - * @return array The normalized attributes - */ - protected function normalizeAttributes($attributes) - { - if (array_key_exists('data', $attributes) && \is_array($attributes['data']) && ! \count($attributes['data'])) { - $attributes['data'] = null; - return $attributes; - } - return $attributes; - } } diff --git a/v3_changes.md b/v3_changes.md index aabd4ac..43d77de 100644 --- a/v3_changes.md +++ b/v3_changes.md @@ -23,6 +23,8 @@ If you notice something critical missing, please **open an issue**. - **Removed `getNonLocalizedURL()`**. - **All `translatableRoutes` related methods have been removed** from `LaravelLocalization`. - **Removed `LaravelLocalizationRoutes` middleware** and its associated `$routeName` attribute. +- Removed `createUrlFromUri` method +- Removed huge `extractAttributes` method, no longer needed If something crucial was removed by mistake or if you encounter missing functionality, feel free to **create an issue**. From 3a85405981473a97fe1cbce7dca9ca559857111e Mon Sep 17 00:00:00 2001 From: Adam Nielsen Date: Tue, 18 Mar 2025 23:57:33 +0100 Subject: [PATCH 10/20] use typed class attributes --- .../LaravelLocalization.php | 31 +++---------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/src/Mcamara/LaravelLocalization/LaravelLocalization.php b/src/Mcamara/LaravelLocalization/LaravelLocalization.php index 81e3f8d..23aa49f 100644 --- a/src/Mcamara/LaravelLocalization/LaravelLocalization.php +++ b/src/Mcamara/LaravelLocalization/LaravelLocalization.php @@ -15,33 +15,10 @@ class LaravelLocalization { - /** - * Default locale. - * - * @var string - */ - protected $defaultLocale; - - /** - * Supported Locales. - * - * @var array - */ - protected $supportedLocales; - - /** - * Locales mapping. - * - * @var array - */ - protected $localesMapping; - - /** - * Current locale. - * - * @var string - */ - protected $currentLocale = false; + protected string $defaultLocale; + protected array $supportedLocales; + protected array $localesMapping; + protected string|bool $currentLocale = false; /** * @throws UnsupportedLocaleException From ab53bcbf854a51eef58642a80eef5a6d9a67b10e Mon Sep 17 00:00:00 2001 From: Adam Nielsen Date: Wed, 19 Mar 2025 22:06:26 +0100 Subject: [PATCH 11/20] Extract LocalizeUrlGenerator functionality into service class, add transRoutes and handle in service --- .../LaravelLocalization.php | 71 ++---------- .../LaravelLocalizationServiceProvider.php | 64 ++++++++++- .../LocalizedUrlGenerator.php | 108 ++++++++++++++++++ 3 files changed, 179 insertions(+), 64 deletions(-) create mode 100644 src/Mcamara/LaravelLocalization/LocalizedUrlGenerator.php diff --git a/src/Mcamara/LaravelLocalization/LaravelLocalization.php b/src/Mcamara/LaravelLocalization/LaravelLocalization.php index 23aa49f..320f833 100644 --- a/src/Mcamara/LaravelLocalization/LaravelLocalization.php +++ b/src/Mcamara/LaravelLocalization/LaravelLocalization.php @@ -26,10 +26,8 @@ class LaravelLocalization public function __construct( protected readonly Application $app, protected readonly ConfigRepository $configRepository, - protected readonly Translator $translator, - protected readonly Router $router, protected readonly Request $request, - protected readonly UrlGenerator $url + protected readonly LocalizedUrlGenerator $localizationUrlGenerator, ) { $this->defaultLocale = $this->configRepository->get('app.locale'); $supportedLocales = $this->getSupportedLocales(); @@ -82,67 +80,16 @@ public function getLocalizedURL(string|null $locale = null, string|null $url = n $url = $this->request->fullUrl(); } - $route = $this->matchRouteForAnyRoute($url); - - if ($route === null) { - return false; - } - - if(empty($attributes)){ - $attributes = $route->parameters(); - } - - $uri = $route->uri(); - $urlQuery = parse_url($url, PHP_URL_QUERY); - // urlQuery , e.g. `?page=2&sort=asc` - $urlQuery = $urlQuery ? '?'.$urlQuery : ''; - - if (!isset($attributes['locale'])){ - if($locale === $this->getDefaultLocale()){ - return $url; - } - - // Locale must be different from default, therefore it should not be hidden - return $this->url->to($locale . '/' . $uri, $attributes) . $urlQuery; - } - - $localeOfUrl = $attributes['locale']; - - if($locale === $localeOfUrl){ - return $url; - } - - // if the locale is default and hidden by default, we need to workaround - if ($this->isHiddenDefault($locale)){ - unset($attributes['locale']); - $cleanedUri = preg_replace('%^/?{locale}(/|$)%', '', $uri); - return $this->url->to($cleanedUri, $attributes) . $urlQuery; - } - - // Update locale and move on - $attributes['locale'] = $locale; - return $this->url->to($uri, $attributes) . $urlQuery; + return $this->localizationUrlGenerator->getLocalizedURL( + locale: $locale, + url: $url, + attributes: $attributes, + forceDefaultLocation: $forceDefaultLocation, + defaultLocale: $this->defaultLocale, + hiddenDefault: $this->hideDefaultLocaleInURL() + ); } - protected function matchRouteForAnyRoute(string $url): Route|null - { - $methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']; - - foreach ($methods as $method) { - try { - $request = Request::create($url, $method); - $route = $this->router->getRoutes()->match($request); - - return $route; - } catch (\Exception $e) { - continue; - } - } - - return null; - } - - /** * Returns default locale. * diff --git a/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php b/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php index b8f879b..87c9f98 100644 --- a/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php +++ b/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php @@ -2,6 +2,8 @@ namespace Mcamara\LaravelLocalization; +use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Route; use Illuminate\Support\ServiceProvider; @@ -66,6 +68,7 @@ protected function registerMacros(): void } Route::macro($localizationMacroName, function (callable $routes, array $middleware = []) { + $this->isInsideLocalizedGroup = true; Route::middleware($middleware)->group(function () use ($routes) { Route::name('default_lang.')->group($routes); @@ -78,12 +81,69 @@ protected function registerMacros(): void ->where(['locale' => $allowedLocales]) ->group($routes); - //@toDo translatedRoutes need to be defined inhere aswell - if($hideDefaultLocaleInURL){ Route::name('default_locale.')->group($routes); } }); + $this->isInsideLocalizedGroup = false; + }); + + Route::macro('transGet', function (string $routeKey, array $controller) { + $this->ensureNotInsideLocalizedGroup(); + $this->registerTransRoute($routeKey, $controller, 'get'); + }); + + Route::macro('transPost', function (string $routeKey, array $controller) { + $this->ensureNotInsideLocalizedGroup(); + $this->registerTransRoute($routeKey, $controller, 'post'); + }); + + Route::macro('transPut', function (string $routeKey, array $controller) { + $this->ensureNotInsideLocalizedGroup(); + $this->registerTransRoute($routeKey, $controller, 'put'); }); + + Route::macro('transDelete', function (string $routeKey, array $controller) { + $this->ensureNotInsideLocalizedGroup(); + $this->registerTransRoute($routeKey, $controller, 'delete'); + }); + } + + private function ensureNotInsideLocalizedGroup(): void + { + if (!empty($this->isInsideLocalizedGroup)) { + throw new \RuntimeException("You cannot use transRoute* inside a Route::localized() group."); + } + } + + private function registerTransRoute(string $routeKey, array $controller, string $methodType): void + { + $supportedLocales = array_keys(config('laravellocalization.supportedLocales', [])); + $localesMapping = array_keys(config('laravellocalization.localesMapping', [])); + $allowedLocales = array_unique(array_merge($supportedLocales, $localesMapping)); + + $hideDefaultLocaleInURL = config('laravellocalization.hideDefaultLocaleInURL', false); + + foreach ($allowedLocales as $locale) { + $routeFile = lang_path("$locale/routes.php"); + + if (File::exists($routeFile)) { + $routes = require $routeFile; + + if (isset($routes[$routeKey])) { + $route = ltrim($routes[$routeKey], '/'); + + if($hideDefaultLocaleInURL && $locale = App::getLocale()){ + Route::$methodType($route, $controller) + ->name("translated_route_{$locale}_{$routeKey}"); + }else{ + Route::$methodType($locale . '/' . $route, $controller) + ->name("translated_route_{$locale}_{$routeKey}"); + } + + + } + } + } } } diff --git a/src/Mcamara/LaravelLocalization/LocalizedUrlGenerator.php b/src/Mcamara/LaravelLocalization/LocalizedUrlGenerator.php new file mode 100644 index 0000000..a112003 --- /dev/null +++ b/src/Mcamara/LaravelLocalization/LocalizedUrlGenerator.php @@ -0,0 +1,108 @@ +matchRouteForAnyRoute($url); + + if ($route === null) { + return false; + } + + if(empty($attributes)){ + $attributes = $route->parameters(); + } + + $uri = $route->uri(); + $urlQuery = parse_url($url, PHP_URL_QUERY); + // e.g. `?page=2&sort=asc` + $urlQuery = $urlQuery ? '?'.$urlQuery : ''; + + // If the route is a translated route, get the corresponding localized route by name. + // Translated routes can have identical paths across languages, so we can't rely on the {locale} parameter. + // Even using `whereIn('locale', ['de'])` wouldn't work because routes with identical URLs overwrite each other, + // regardless of differing `whereIn` conditions. + if ($route->getName()) { + $routeName = $route->getName(); + + if (preg_match('/^translated_route_(.*?)_(.*)$/', $routeName, $matches)) { + $newRouteName = "translated_route_{$locale}_{$matches[2]}"; + return route($newRouteName, $attributes) . $urlQuery; + } + } + + $hideLocaleInUrl = ($locale === $defaultLocale && !$forceDefaultLocation && $hiddenDefault); + + if (!isset($attributes['locale'])){ + if($hideLocaleInUrl){ + // locale already hidden in url + return $url; + } + + return $this->urlGenerator->to($locale . '/' . $uri, $attributes) . $urlQuery; + } + + $localeOfUrl = $attributes['locale']; + if($locale === $localeOfUrl){ + // no need to change locale of url + return $url; + } + + if ($hideLocaleInUrl) { + unset($attributes['locale']); + $cleanedUri = preg_replace('%^/?{locale}(/|$)%', '', $uri); + return $this->urlGenerator->to($cleanedUri, $attributes) . $urlQuery; + } + + $attributes['locale'] = $locale; + return $this->urlGenerator->to($uri, $attributes) . $urlQuery; + } + + protected function matchRouteForAnyRoute(string $url): Route|null + { + $methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']; + + foreach ($methods as $method) { + try { + $request = Request::create($url, $method); + $route = $this->router->getRoutes()->match($request); + + return $route; + } catch (\Exception $e) { + continue; + } + } + + return null; + } +} From 7a36597d4875b16cd019400ce03893af630bad9a Mon Sep 17 00:00:00 2001 From: Adam Nielsen Date: Wed, 19 Mar 2025 22:37:50 +0100 Subject: [PATCH 12/20] Remove Routes Middleware, routeName no longer necessary in `getLocalizedUrl` --- .../Middleware/LaravelLocalizationRoutes.php | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 src/Mcamara/LaravelLocalization/Middleware/LaravelLocalizationRoutes.php diff --git a/src/Mcamara/LaravelLocalization/Middleware/LaravelLocalizationRoutes.php b/src/Mcamara/LaravelLocalization/Middleware/LaravelLocalizationRoutes.php deleted file mode 100644 index 5e62667..0000000 --- a/src/Mcamara/LaravelLocalization/Middleware/LaravelLocalizationRoutes.php +++ /dev/null @@ -1,25 +0,0 @@ -shouldIgnore($request)) { - return $next($request); - } - - $app = app(); - - $routeName = $app['laravellocalization']->getRouteNameFromAPath($request->getUri()); - - $app['laravellocalization']->setRouteName($routeName); - - return $next($request); - } -} From e0c86963dd6b647593470bf9b662b4824d7e385f Mon Sep 17 00:00:00 2001 From: Adam Nielsen Date: Wed, 19 Mar 2025 22:43:01 +0100 Subject: [PATCH 13/20] CustomTranslator no longer needed, since `setLocale` has been removed from class --- tests/CustomTranslatorTest.php | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 tests/CustomTranslatorTest.php diff --git a/tests/CustomTranslatorTest.php b/tests/CustomTranslatorTest.php deleted file mode 100644 index 8bb7f1e..0000000 --- a/tests/CustomTranslatorTest.php +++ /dev/null @@ -1,33 +0,0 @@ -app, - $this->app['config'], - $translator, - $this->app['router'], - $this->app['request'], - $this->app['url'] - ); - - $localization->setLocale('es'); - - $this->assertEquals('es', $translator->getLocale()); - } -} From 1a21ada65566b238573b17922c9512d6963f27bc Mon Sep 17 00:00:00 2001 From: Adam Nielsen Date: Fri, 21 Mar 2025 23:50:43 +0100 Subject: [PATCH 14/20] Fix testGetLocalizedURLTest() --- .../LaravelLocalization.php | 47 +++----- .../LaravelLocalizationServiceProvider.php | 64 ++-------- .../Middleware/LocaleCookieRedirect.php | 2 +- .../Middleware/LocaleSessionRedirect.php | 2 +- .../Middleware/SetLocale.php | 4 +- .../{ => Services}/LanguageNegotiator.php | 2 +- .../{ => Services}/LocalizedUrlGenerator.php | 50 ++++++-- .../Services/TransRouter.php | 60 ++++++++++ tests/LaravelLocalizationTest.php | 111 +++++++++++------- v3_changes.md | 1 + 10 files changed, 202 insertions(+), 141 deletions(-) rename src/Mcamara/LaravelLocalization/{ => Services}/LanguageNegotiator.php (99%) rename src/Mcamara/LaravelLocalization/{ => Services}/LocalizedUrlGenerator.php (66%) create mode 100644 src/Mcamara/LaravelLocalization/Services/TransRouter.php diff --git a/src/Mcamara/LaravelLocalization/LaravelLocalization.php b/src/Mcamara/LaravelLocalization/LaravelLocalization.php index e7b74ae..13d6dd8 100644 --- a/src/Mcamara/LaravelLocalization/LaravelLocalization.php +++ b/src/Mcamara/LaravelLocalization/LaravelLocalization.php @@ -4,21 +4,16 @@ use Illuminate\Contracts\Config\Repository as ConfigRepository; use Illuminate\Contracts\Foundation\Application; -use Illuminate\Contracts\Routing\UrlGenerator; -use Illuminate\Contracts\Routing\UrlRoutable; -use Illuminate\Contracts\Translation\Translator; -use Illuminate\Http\Request; -use Illuminate\Routing\Route; -use Illuminate\Routing\Router; use Mcamara\LaravelLocalization\Exceptions\SupportedLocalesNotDefined; use Mcamara\LaravelLocalization\Exceptions\UnsupportedLocaleException; +use Mcamara\LaravelLocalization\Services\LocalizedUrlGenerator; class LaravelLocalization { protected string $defaultLocale; protected array $supportedLocales; protected array $localesMapping; - protected string|bool $currentLocale = false; + protected string $currentLocale; /** * @throws UnsupportedLocaleException @@ -26,15 +21,17 @@ class LaravelLocalization public function __construct( protected readonly Application $app, protected readonly ConfigRepository $configRepository, - protected readonly Request $request, protected readonly LocalizedUrlGenerator $localizationUrlGenerator, ) { - $this->defaultLocale = $this->configRepository->get('app.locale'); + $locale = $this->configRepository->get('app.locale'); $supportedLocales = $this->getSupportedLocales(); - if (empty($supportedLocales[$this->defaultLocale])) { + if (empty($supportedLocales[$locale])) { throw new UnsupportedLocaleException('Laravel default locale is not in the supportedLocales array.'); } + + $this->defaultLocale = $locale; + $this->currentLocale = $locale; } public function isHiddenDefault(string $locale): bool @@ -54,7 +51,7 @@ public function setSupportedLocales(array $locales): void * Returns an URL adapted to $locale. * * - * @param string|null $locale Locale to adapt, false to remove locale + * @param string|null $locale Locale to adapt * @param string|null $url URL to adapt in the current language. If not passed, the current url would be taken. * @param array $attributes Attributes to add to the route, if empty, the system would try to extract them from the url. * @param bool $forceDefaultLocation Force to show default location even hideDefaultLocaleInURL set as TRUE @@ -62,9 +59,9 @@ public function setSupportedLocales(array $locales): void * @throws SupportedLocalesNotDefined * @throws UnsupportedLocaleException * - * @return string|false URL translated, False if url does not exist + * @return string URL translated, returns same url if no route is found */ - public function getLocalizedURL(string|null $locale = null, string|null $url = null, array $attributes = [], bool $forceDefaultLocation = false): string|false + public function getLocalizedURL(string|null $locale = null, string|null $url = null, array $attributes = [], bool $forceDefaultLocation = false): string { $locale = $locale ?: $this->getCurrentLocale(); $locale = $this->getLocaleFromMapping($locale); @@ -74,14 +71,19 @@ public function getLocalizedURL(string|null $locale = null, string|null $url = n } if($url === null){ - // Including protocol, domain and query , e.g. `https://example.com/posts?page=2&sort=asc` - $url = $this->request->fullUrl(); + // fullUrl() is including protocol, domain and query , e.g. `https://example.com/posts?page=2&sort=asc` + + // Use the request() helper instead of $this->request, + // because the injected request may be stale if this class + // was constructed before the current request was bound. + $url = request()->fullUrl(); } return $this->localizationUrlGenerator->getLocalizedURL( locale: $locale, url: $url, attributes: $attributes, + supportedLocales: $this->getSupportedLocales(), forceDefaultLocation: $forceDefaultLocation, defaultLocale: $this->defaultLocale, hiddenDefault: $this->hideDefaultLocaleInURL() @@ -224,22 +226,11 @@ public function setCurrentLocale(string $locale): void { } /** - * Returns current language. + * Returns current language of url request */ public function getCurrentLocale(): string { - if ($this->currentLocale) { - return $this->currentLocale; - } - - if ($this->useAcceptLanguageHeader() && !$this->app->runningInConsole()) { - $negotiator = new LanguageNegotiator($this->defaultLocale, $this->getSupportedLocales(), $this->request); - - return $negotiator->negotiateLanguage(); - } - - // or get application default language - return $this->configRepository->get('app.locale'); + return $this->currentLocale; } /** diff --git a/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php b/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php index b093466..1a87611 100644 --- a/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php +++ b/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php @@ -50,82 +50,32 @@ protected function registerMacros(): void } Route::macro($localizationMacroName, function (callable $routes, array $middleware = []) { - $this->isInsideLocalizedGroup = true; Route::middleware($middleware)->group(function () use ($routes) { Route::name('default_lang.')->group($routes); $supportedLocales = array_keys(config('laravellocalization.supportedLocales', [])); $localesMapping = array_keys(config('laravellocalization.localesMapping', [])); $hideDefaultLocaleInURL = config('laravellocalization.hideDefaultLocaleInURL', false); + $useAcceptLanguageHeader = config('laravellocalization.useAcceptLanguageHeader', false); $allowedLocales = implode('|', array_unique(array_merge($supportedLocales, $localesMapping))); Route::prefix('/{locale}') ->where(['locale' => $allowedLocales]) ->group($routes); - if($hideDefaultLocaleInURL){ + if($hideDefaultLocaleInURL || $useAcceptLanguageHeader){ Route::name('default_locale.')->group($routes); } }); - $this->isInsideLocalizedGroup = false; }); - Route::macro('transGet', function (string $routeKey, array $controller) { - $this->ensureNotInsideLocalizedGroup(); - $this->registerTransRoute($routeKey, $controller, 'get'); - }); - - Route::macro('transPost', function (string $routeKey, array $controller) { - $this->ensureNotInsideLocalizedGroup(); - $this->registerTransRoute($routeKey, $controller, 'post'); - }); - Route::macro('transPut', function (string $routeKey, array $controller) { - $this->ensureNotInsideLocalizedGroup(); - $this->registerTransRoute($routeKey, $controller, 'put'); - }); - - Route::macro('transDelete', function (string $routeKey, array $controller) { - $this->ensureNotInsideLocalizedGroup(); - $this->registerTransRoute($routeKey, $controller, 'delete'); - }); - } - - private function ensureNotInsideLocalizedGroup(): void - { - if (!empty($this->isInsideLocalizedGroup)) { - throw new \RuntimeException("You cannot use transRoute* inside a Route::localized() group."); - } - } + $transRouter = app(\Mcamara\LaravelLocalization\Services\TransRouter::class); - private function registerTransRoute(string $routeKey, array $controller, string $methodType): void - { - $supportedLocales = array_keys(config('laravellocalization.supportedLocales', [])); - $localesMapping = array_keys(config('laravellocalization.localesMapping', [])); - $allowedLocales = array_unique(array_merge($supportedLocales, $localesMapping)); - - $hideDefaultLocaleInURL = config('laravellocalization.hideDefaultLocaleInURL', false); - - foreach ($allowedLocales as $locale) { - $routeFile = lang_path("$locale/routes.php"); - - if (File::exists($routeFile)) { - $routes = require $routeFile; - - if (isset($routes[$routeKey])) { - $route = ltrim($routes[$routeKey], '/'); - - if($hideDefaultLocaleInURL && $locale = App::getLocale()){ - Route::$methodType($route, $controller) - ->name("translated_route_{$locale}_{$routeKey}"); - }else{ - Route::$methodType($locale . '/' . $route, $controller) - ->name("translated_route_{$locale}_{$routeKey}"); - } - - - } - } + foreach (['get', 'post', 'put', 'delete'] as $method) { + Route::macro("trans" . ucfirst($method), function (string $routeKey, array $controller) use ($transRouter, $method) { + $transRouter->registerTransRoute($routeKey, $controller, $method); + }); } } } diff --git a/src/Mcamara/LaravelLocalization/Middleware/LocaleCookieRedirect.php b/src/Mcamara/LaravelLocalization/Middleware/LocaleCookieRedirect.php index fe11fb5..10712c9 100644 --- a/src/Mcamara/LaravelLocalization/Middleware/LocaleCookieRedirect.php +++ b/src/Mcamara/LaravelLocalization/Middleware/LocaleCookieRedirect.php @@ -4,7 +4,7 @@ use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cookie; -use Mcamara\LaravelLocalization\LanguageNegotiator; +use Mcamara\LaravelLocalization\Services\LanguageNegotiator; class LocaleCookieRedirect extends LaravelLocalizationMiddlewareBase { diff --git a/src/Mcamara/LaravelLocalization/Middleware/LocaleSessionRedirect.php b/src/Mcamara/LaravelLocalization/Middleware/LocaleSessionRedirect.php index ac1d4a1..7618b3b 100644 --- a/src/Mcamara/LaravelLocalization/Middleware/LocaleSessionRedirect.php +++ b/src/Mcamara/LaravelLocalization/Middleware/LocaleSessionRedirect.php @@ -5,7 +5,7 @@ use Closure; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; -use Mcamara\LaravelLocalization\LanguageNegotiator; +use Mcamara\LaravelLocalization\Services\LanguageNegotiator; class LocaleSessionRedirect extends LaravelLocalizationMiddlewareBase { diff --git a/src/Mcamara/LaravelLocalization/Middleware/SetLocale.php b/src/Mcamara/LaravelLocalization/Middleware/SetLocale.php index 1541deb..b6a63d8 100644 --- a/src/Mcamara/LaravelLocalization/Middleware/SetLocale.php +++ b/src/Mcamara/LaravelLocalization/Middleware/SetLocale.php @@ -8,8 +8,8 @@ use Illuminate\Contracts\Translation\Translator; use Illuminate\Http\Request; use Illuminate\Support\Facades\URL; -use Mcamara\LaravelLocalization\LanguageNegotiator; use Mcamara\LaravelLocalization\LaravelLocalization; +use Mcamara\LaravelLocalization\Services\LanguageNegotiator; class SetLocale extends LaravelLocalizationMiddlewareBase { @@ -30,6 +30,8 @@ public function handle(Request $request, Closure $next): mixed $locale = $request->route('locale'); + // @toDo translated routes need to be here, or I need to use {locale} for them aswell.. + // The locale here cannot be an "inverse" mapping, as such cases are handled // earlier by the locale mapping middleware. if($locale == null || empty($this->laravelLocalization->getSupportedLocales()[$locale])) { diff --git a/src/Mcamara/LaravelLocalization/LanguageNegotiator.php b/src/Mcamara/LaravelLocalization/Services/LanguageNegotiator.php similarity index 99% rename from src/Mcamara/LaravelLocalization/LanguageNegotiator.php rename to src/Mcamara/LaravelLocalization/Services/LanguageNegotiator.php index 7ee9794..01105ff 100644 --- a/src/Mcamara/LaravelLocalization/LanguageNegotiator.php +++ b/src/Mcamara/LaravelLocalization/Services/LanguageNegotiator.php @@ -1,6 +1,6 @@ matchRouteForAnyRoute($url); - if ($route === null) { - return false; + if ($route === null){ + // If hideDefaultLocale is disabled and negotiator is disabled, then only routes with locale can be matched + $route = $this->attemptRouteMatchingWithDefaultLocale($url, $defaultLocale, $supportedLocales); + } + + if ($route === null){ + // no route found, gracefully return $url as fallback + return $url; } if(empty($attributes)){ @@ -47,21 +53,21 @@ public function getLocalizedURL(string|null $locale = null, string $url, string $urlQuery = parse_url($url, PHP_URL_QUERY); // e.g. `?page=2&sort=asc` $urlQuery = $urlQuery ? '?'.$urlQuery : ''; + $hideLocaleInUrl = ($locale === $defaultLocale && !$forceDefaultLocation && $hiddenDefault); + - // If the route is a translated route, get the corresponding localized route by name. - // Translated routes can have identical paths across languages, so we can't rely on the {locale} parameter. - // Even using `whereIn('locale', ['de'])` wouldn't work because routes with identical URLs overwrite each other, - // regardless of differing `whereIn` conditions. + // Handle transRoutes if ($route->getName()) { $routeName = $route->getName(); - if (preg_match('/^translated_route_(.*?)_(.*)$/', $routeName, $matches)) { - $newRouteName = "translated_route_{$locale}_{$matches[2]}"; + if (preg_match('/^trans_route_(with|no)_locale_(.*?)_(.*)$/', $routeName, $matches)) { + $type = ($hideLocaleInUrl) ? 'no' : 'with'; + $newRouteName = "trans_route_{$type}_locale_{$locale}_{$matches[3]}"; return route($newRouteName, $attributes) . $urlQuery; } } - $hideLocaleInUrl = ($locale === $defaultLocale && !$forceDefaultLocation && $hiddenDefault); + // Since we deal now with normal routes, we only need to modify, add or remove the locale from uri if (!isset($attributes['locale'])){ if($hideLocaleInUrl){ @@ -105,4 +111,26 @@ protected function matchRouteForAnyRoute(string $url): Route|null return null; } + + + protected function attemptRouteMatchingWithDefaultLocale(string $url, string $defaultLocale, array $supportedLocales): ?Route + { + $uri = parse_url($url, PHP_URL_PATH); + + // Extract the first segment of the URI + $segments = explode('/', trim($uri, '/')); + $firstSegment = $segments[0] ?? null; + + if(!empty($supportedLocales[$firstSegment])){ + array_unshift($segments, $defaultLocale); + $newUri = '/' . implode('/', $segments); + $url = preg_replace('/' . preg_quote($uri, '/') . '/', $newUri, $url, 1); + return $this->matchRouteForAnyRoute($url); + } + + return null; + } + + + } diff --git a/src/Mcamara/LaravelLocalization/Services/TransRouter.php b/src/Mcamara/LaravelLocalization/Services/TransRouter.php new file mode 100644 index 0000000..4b423b6 --- /dev/null +++ b/src/Mcamara/LaravelLocalization/Services/TransRouter.php @@ -0,0 +1,60 @@ +allowedLocales = array_unique(array_merge($supportedLocales, $localesMapping)); + $this->hideDefaultLocaleInURL = config('laravellocalization.hideDefaultLocaleInURL', false); + $this->useAcceptLanguageHeader = config('laravellocalization.useAcceptLanguageHeader', false); + } + + public function registerTransRoute(string $routeKey, array|callable $controller, string $methodType): void + { + foreach ($this->allowedLocales as $locale) { + $key = "routes.$routeKey"; + + $route = trans($key, [], $locale); + if ($route === $key) { + continue; + } + + $route = ltrim($route, '/'); + $name = "trans_route_with_locale_{$locale}_{$routeKey}"; + + $middleware = [SetLocale::class, LocaleSessionRedirect::class]; + + if ($this->hideDefaultLocaleInURL && $locale === App::getLocale()) { + Route::$methodType($route, $controller) + ->middleware($middleware) + ->name($name); + } else { + Route::$methodType($locale . '/' . $route, $controller) + ->middleware($middleware) + ->name($name); + + if ($this->useAcceptLanguageHeader) { + Route::$methodType($route, $controller) + ->middleware($middleware) + ->name("trans_route_no_locale_{$locale}_{$routeKey}"); + } + } + } + + + } +} diff --git a/tests/LaravelLocalizationTest.php b/tests/LaravelLocalizationTest.php index b9b814a..d928510 100644 --- a/tests/LaravelLocalizationTest.php +++ b/tests/LaravelLocalizationTest.php @@ -2,13 +2,13 @@ namespace Mcamara\LaravelLocalization\Tests; -use PHPUnit\Framework\Attributes\DataProvider; use Illuminate\Support\Facades\Request; use Illuminate\Support\Facades\Route; use Mcamara\LaravelLocalization\LaravelLocalization; use Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRedirectFilter; use Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRoutes; use Mcamara\LaravelLocalization\Middleware\SetLocale; +use PHPUnit\Framework\Attributes\DataProvider; final class LaravelLocalizationTest extends TestCase { @@ -28,22 +28,23 @@ protected function setUp(): void protected function setUpRoutes(): void { + Route::localized(function () { Route::get('/', ['as' => 'index', function () { - return app('translator')->get('LaravelLocalization::routes.hello'); + return __('routes.hello'); }]); Route::get('test', ['as' => 'test', function () { - return app('translator')->get('LaravelLocalization::routes.test_text'); + return __('routes.test_text'); }]); - Route::get(app('laravellocalization')->transRoute('LaravelLocalization::routes.about'), ['as' => 'about', function () { - return app('laravellocalization')->getLocalizedURL('es') ?: 'Not url available'; - }]); + // Route::get(app('laravellocalization')->transRoute('LaravelLocalization::routes.about'), ['as' => 'about', function () { + // return app('laravellocalization')->getLocalizedURL('es') ?: 'Not url available'; + // }]); Route::get(app('laravellocalization')->transRoute('LaravelLocalization::routes.view'), ['as' => 'view', function () { - return app('laravellocalization')->getLocalizedURL('es') ?: 'Not url available'; - }]); + return app('laravellocalization')->getLocalizedURL('es') ?: 'Not url available'; + }]); Route::get(app('laravellocalization')->transRoute('LaravelLocalization::routes.view_project'), ['as' => 'view_project', function () { return app('laravellocalization')->getLocalizedURL('es') ?: 'Not url available'; @@ -54,13 +55,19 @@ protected function setUpRoutes(): void }]); }, [ SetLocale::class, - LaravelLocalizationRoutes::class, LaravelLocalizationRedirectFilter::class, ]); + + Route::transGet('about', [function () { + return app('laravellocalization')->getLocalizedURL('es') ?: 'Not url available'; + }]); + + Route::get('/skipped', ['as' => 'skipped', function () { return Request::url(); }]); + } /** @@ -118,12 +125,7 @@ protected function getEnvironmentSetUp($app) $this->supportedLocales = app('config')->get('laravellocalization.supportedLocales'); - app('translator')->getLoader()->addNamespace('LaravelLocalization', realpath(dirname(__FILE__)).'/lang'); - - app('translator')->load('LaravelLocalization', 'routes', 'es'); - app('translator')->load('LaravelLocalization', 'routes', 'en'); - - app('laravellocalization')->setBaseUrl(self::$testUrl); + app()->useLangPath(realpath(dirname(__FILE__)).'/lang'); } public function testTranslatedRoutes(): void @@ -203,44 +205,71 @@ public function testLocalizeURL(): void ); } + private function getUrl(string $uri = '') + { + return self::$testUrl . $uri; + } + + public function testGetLocalizedURL(): void { + $localization = app('laravellocalization'); + $this->assertEquals( - self::$testUrl.app('laravellocalization')->getCurrentLocale(), - app('laravellocalization')->getLocalizedURL() + $this->getUrl('es/acerca'), + $localization->getLocalizedURL('es', $this->getUrl('about')) + ); + + $this->assertEquals( + $this->getUrl('en/about'), + $localization->getLocalizedURL('en', $this->getUrl('about')) + ); + + $this->assertEquals( + $this->getUrl('es/acerca'), + $localization->getLocalizedURL('es', $this->getUrl('acerca')) + ); + + $this->assertEquals( + $this->getUrl('en/about'), + $localization->getLocalizedURL('en', $this->getUrl('acerca')) + ); + + $this->assertEquals( + $this->getUrl('en'), + $localization->getLocalizedURL() ); app('config')->set('laravellocalization.hideDefaultLocaleInURL', true); - // testing default language hidden $this->assertNotEquals( - self::$testUrl.app('laravellocalization')->getDefaultLocale(), - app('laravellocalization')->getLocalizedURL() + $this->getUrl('en'), + $localization->getLocalizedURL() ); - app()->setLocale('es'); + $localization->setCurrentLocale('es'); $this->assertNotEquals( - self::$testUrl, - app('laravellocalization')->getLocalizedURL() + $this->getUrl(), + $localization->getLocalizedURL() ); $this->assertNotEquals( - self::$testUrl.app('laravellocalization')->getDefaultLocale(), - app('laravellocalization')->getLocalizedURL() + $this->getUrl('en'), + $localization->getLocalizedURL() ); $this->assertEquals( - self::$testUrl.app('laravellocalization')->getCurrentLocale(), - app('laravellocalization')->getLocalizedURL() + $this->getUrl('es'), + $localization->getLocalizedURL() ); $this->assertEquals( - self::$testUrl.'es/acerca', - app('laravellocalization')->getLocalizedURL('es', self::$testUrl.'about') + $this->getUrl('es/acerca'), + $localization->getLocalizedURL('es', $this->getUrl('acerca')) ); - app()->setLocale('en'); + app('laravellocalization')->setCurrentLocale('es'); $response = $this->get(self::$testUrl.'about', ['Accept-Language' => 'en,es']); @@ -253,17 +282,17 @@ public function testGetLocalizedURL(): void app('config')->set('laravellocalization.hideDefaultLocaleInURL', true); $this->assertEquals( - self::$testUrl.'test', - app('laravellocalization')->getLocalizedURL('en', self::$testUrl.'test') + $this->getUrl('test'), + $localization->getLocalizedURL('en', $this->getUrl('test')) ); $this->assertEquals( - self::$testUrl.'test?a=1', - app('laravellocalization')->getLocalizedURL('en', self::$testUrl.'test?a=1') + $this->getUrl('test?a=1'), + $localization->getLocalizedURL('en', $this->getUrl('test?a=1')) ); $response = $this->get( - app('laravellocalization')->getLocalizedURL('en', self::$testUrl.'test'), + $this->getUrl('test'), ['Accept-Language' => 'en,es'] ); @@ -274,13 +303,13 @@ public function testGetLocalizedURL(): void ); $this->assertEquals( - self::$testUrl.'es/test', - app('laravellocalization')->getLocalizedURL('es', self::$testUrl.'test') + $this->getUrl('es/test'), + $localization->getLocalizedURL('es', self::$testUrl.'test') ); $this->assertEquals( - self::$testUrl.'es/test?a=1', - app('laravellocalization')->getLocalizedURL('es', self::$testUrl.'test?a=1') + $this->getUrl('es/test?a=1'), + $localization->getLocalizedURL('es', $this->getUrl('test?a=1')) ); } @@ -737,7 +766,7 @@ public function testLanguageNegotiation($accept_string, $must_resolve_to, $asd = $request = $this->createMock(\Illuminate\Http\Request::class); $request->expects($this->any())->method('header')->with('Accept-Language')->willReturn($accept_string); - $negotiator = app(\Mcamara\LaravelLocalization\LanguageNegotiator::class, + $negotiator = app(\Mcamara\LaravelLocalization\Services\LanguageNegotiator::class, [ 'defaultLocale' => 'wrong', 'supportedLanguages' => $full_config['supportedLocales'], @@ -792,7 +821,7 @@ public function testLanguageNegotiationWithMapping(): void { $request = $this->createMock(\Illuminate\Http\Request::class); $request->expects($this->any())->method('header')->with('Accept-Language')->willReturn($accept_string); - $negotiator = app(\Mcamara\LaravelLocalization\LanguageNegotiator::class, + $negotiator = app(\Mcamara\LaravelLocalization\Services\LanguageNegotiator::class, [ 'defaultLocale' => 'wrong', 'supportedLanguages' => $full_config['supportedLocales'], diff --git a/v3_changes.md b/v3_changes.md index 43d77de..2e70182 100644 --- a/v3_changes.md +++ b/v3_changes.md @@ -17,6 +17,7 @@ If you notice something critical missing, please **open an issue**. - **Removed `route.translation` event** – Documentation was unclear, there were open issues, and it was inconsistently triggered (only when the URL was empty during localization). - **The `data` attribute is no longer removed from routes attributes** – This should not be the responsibility of the package. - **`getLocalizedURL(locale: false)` no longer removes the locale from the URL**. +- - **`getLocalizedURL()` no longer returns false if url is not found, instead, the same url is returned**. - **Dropped alias `localizeURL`** – If needed, you can define a custom helper. - **`translatedRoutes` is no longer stored inside `LaravelLocalization`**. - **`transRoute()` method is no longer supported** – Use `__('routes.*')` instead. From 75918a98f7be8c4bee67446fe83ce1a551d4f51c Mon Sep 17 00:00:00 2001 From: Adam Nielsen Date: Sat, 22 Mar 2025 00:15:24 +0100 Subject: [PATCH 15/20] Fix localized test routes --- tests/LaravelLocalizationTest.php | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/tests/LaravelLocalizationTest.php b/tests/LaravelLocalizationTest.php index d928510..8792a83 100644 --- a/tests/LaravelLocalizationTest.php +++ b/tests/LaravelLocalizationTest.php @@ -38,21 +38,6 @@ protected function setUpRoutes(): void return __('routes.test_text'); }]); - // Route::get(app('laravellocalization')->transRoute('LaravelLocalization::routes.about'), ['as' => 'about', function () { - // return app('laravellocalization')->getLocalizedURL('es') ?: 'Not url available'; - // }]); - - Route::get(app('laravellocalization')->transRoute('LaravelLocalization::routes.view'), ['as' => 'view', function () { - return app('laravellocalization')->getLocalizedURL('es') ?: 'Not url available'; - }]); - - Route::get(app('laravellocalization')->transRoute('LaravelLocalization::routes.view_project'), ['as' => 'view_project', function () { - return app('laravellocalization')->getLocalizedURL('es') ?: 'Not url available'; - }]); - - Route::get(app('laravellocalization')->transRoute('LaravelLocalization::routes.manage'), ['as' => 'manage', function () { - return app('laravellocalization')->getLocalizedURL('es') ?: 'Not url available'; - }]); }, [ SetLocale::class, LaravelLocalizationRedirectFilter::class, @@ -63,6 +48,18 @@ protected function setUpRoutes(): void return app('laravellocalization')->getLocalizedURL('es') ?: 'Not url available'; }]); + Route::transGet('view', [function () { + return app('laravellocalization')->getLocalizedURL('es') ?: 'Not url available'; + }]); + + Route::transGet('view', [function () { + return app('laravellocalization')->getLocalizedURL('es') ?: 'Not url available'; + }]); + + Route::transGet('manage', [function () { + return app('laravellocalization')->getLocalizedURL('es') ?: 'Not url available'; + }]); + Route::get('/skipped', ['as' => 'skipped', function () { return Request::url(); From 62390f455234d3391718e7e4195433eb387408c3 Mon Sep 17 00:00:00 2001 From: Adam Nielsen Date: Sat, 22 Mar 2025 00:42:43 +0100 Subject: [PATCH 16/20] Add `route` helper for localized routes. --- composer.json | 5 +++- src/Mcamara/LaravelLocalization/helper.php | 33 ++++++++++++++++++++++ tests/LaravelLocalizationTest.php | 2 +- 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 src/Mcamara/LaravelLocalization/helper.php diff --git a/composer.json b/composer.json index ff8018b..4de5f24 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,10 @@ "classmap": [], "psr-0": { "Mcamara\\LaravelLocalization": "src/" - } + }, + "files": [ + "src/Mcamara/LaravelLocalization/helper.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/src/Mcamara/LaravelLocalization/helper.php b/src/Mcamara/LaravelLocalization/helper.php new file mode 100644 index 0000000..5cf418d --- /dev/null +++ b/src/Mcamara/LaravelLocalization/helper.php @@ -0,0 +1,33 @@ +getLocale(); + + $withLocale = "trans_route_with_locale_{$computedLocale}_{$name}"; + $noLocale = "trans_route_no_locale_{$computedLocale}_{$name}"; + + // In tests, routes defined in setUp() are correctly registered but sometimes not recognized by + // Route::has(...), likely due to Laravel not populating the internal route name index (routesByName). + // This workaround manually checks route names to ensure they exist. + + if ($locale === null && $noLocale && collect(Route::getRoutes())->pluck('action.as')->filter()->contains($noLocale)) { + return route($noLocale, $parameters); + } + + if (collect(Route::getRoutes())->pluck('action.as')->filter()->contains($withLocale)) { + return route($withLocale, $parameters); + } + + if ($noLocale && collect(Route::getRoutes())->pluck('action.as')->filter()->contains($noLocale)) { + return route($noLocale, $parameters); + } + + + + return route($name, $parameters); + } +} diff --git a/tests/LaravelLocalizationTest.php b/tests/LaravelLocalizationTest.php index 8792a83..ed62d0d 100644 --- a/tests/LaravelLocalizationTest.php +++ b/tests/LaravelLocalizationTest.php @@ -127,7 +127,7 @@ protected function getEnvironmentSetUp($app) public function testTranslatedRoutes(): void { - $this->assertEquals(route('about'), 'http://localhost/about'); + $this->assertEquals(localized_route('about'), 'http://localhost/about'); $this->get(route('about', ['locale' => 'es'])) ->assertStatus(200); From def879708d7d221da2adead8ee2673d4537f0e7a Mon Sep 17 00:00:00 2001 From: Adam Nielsen Date: Sat, 22 Mar 2025 09:13:52 +0100 Subject: [PATCH 17/20] Fix testTranslatedRoutes tests --- .../Facades/LaravelLocalization.php | 5 +- .../LaravelLocalization.php | 34 ++++++++++++++ .../LaravelLocalizationServiceProvider.php | 2 +- .../Services/LocalizedUrlGenerator.php | 4 +- .../Services/TransRouter.php | 6 +-- src/Mcamara/LaravelLocalization/helper.php | 34 ++++---------- tests/LaravelLocalizationTest.php | 12 ++--- v3_changes.md | 46 ++++++++++++++++++- 8 files changed, 103 insertions(+), 40 deletions(-) diff --git a/src/Mcamara/LaravelLocalization/Facades/LaravelLocalization.php b/src/Mcamara/LaravelLocalization/Facades/LaravelLocalization.php index 153bdff..1002e10 100644 --- a/src/Mcamara/LaravelLocalization/Facades/LaravelLocalization.php +++ b/src/Mcamara/LaravelLocalization/Facades/LaravelLocalization.php @@ -8,7 +8,7 @@ * @method static bool isHiddenDefault(string $locale) * @method static void setSupportedLocales(array $locales) * @method static string localizeURL(string|null $url = null, string|bool|null $locale = null) - * @method static string|false getLocalizedURL(string|false|null $locale = null, string|bool|null $url = null, array $attributes = [], bool $forceDefaultLocation = false) + * @method static string getLocalizedURL(string|null $locale = null, string|null $url = null, array $attributes = [], bool $forceDefaultLocation = false) * @method static string|false getURLFromRouteNameTranslated(string|bool $locale, string $transKeyName, array $attributes = [], bool $forceDefaultLocation = false) * @method static string getNonLocalizedURL(string|false|null $url = null) * @method static string getDefaultLocale() @@ -28,7 +28,8 @@ * @method static array getSupportedLanguagesKeys() * @method static bool checkLocaleInSupportedLocales(string|bool $locale) * @method static void setRouteName(string $routeName) - * @method static string transRoute(string $routeName) + * @method static string transRoute(string $routeName, array $parameters = [], string|null $locale = null) + * @method static string route(string $key, array $parameters = [], string|null $locale = null) * @method static string|false getRouteNameFromAPath(string $path) * @method static \Illuminate\Contracts\Config\Repository getConfigRepository() * @method static bool useAcceptLanguageHeader() diff --git a/src/Mcamara/LaravelLocalization/LaravelLocalization.php b/src/Mcamara/LaravelLocalization/LaravelLocalization.php index 13d6dd8..6362d69 100644 --- a/src/Mcamara/LaravelLocalization/LaravelLocalization.php +++ b/src/Mcamara/LaravelLocalization/LaravelLocalization.php @@ -4,6 +4,7 @@ use Illuminate\Contracts\Config\Repository as ConfigRepository; use Illuminate\Contracts\Foundation\Application; +use Illuminate\Support\Facades\Route; use Mcamara\LaravelLocalization\Exceptions\SupportedLocalesNotDefined; use Mcamara\LaravelLocalization\Exceptions\UnsupportedLocaleException; use Mcamara\LaravelLocalization\Services\LocalizedUrlGenerator; @@ -47,6 +48,39 @@ public function setSupportedLocales(array $locales): void $this->supportedLocales = $locales; } + public function route(string $key, array $parameters = [], string|null $locale = null): string + { + $computedLocale = $locale ?? $this->getCurrentLocale(); + + if($this->isHiddenDefault($computedLocale)){ + return route('without_locale.' . $key, $parameters); + } + + return route($key, $parameters); + } + + public function transRoute(string $key, array $parameters = [], string|null $locale = null): string + { + $computedLocale = $locale ?? $this->getCurrentLocale(); + + $routeName = "trans_route_for_locale_{$computedLocale}_{$key}"; + + // In tests, routes defined in setUp() are correctly registered but sometimes not recognized by + // Route::has(...), likely due to Laravel not populating the internal route name index (routesByName). + // This workaround manually checks route names to ensure they exist. + $routes = collect(Route::getRoutes())->pluck('action.as')->filter(); + + if (!$routes->contains($routeName)) { + return $key; + } + + if(!isset($parameters['locale'])) { + $parameters['locale'] = $computedLocale; + } + + return route($routeName, $parameters); + } + /** * Returns an URL adapted to $locale. * diff --git a/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php b/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php index 1a87611..2119bea 100644 --- a/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php +++ b/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php @@ -64,7 +64,7 @@ protected function registerMacros(): void ->group($routes); if($hideDefaultLocaleInURL || $useAcceptLanguageHeader){ - Route::name('default_locale.')->group($routes); + Route::name('without_locale.')->group($routes); } }); }); diff --git a/src/Mcamara/LaravelLocalization/Services/LocalizedUrlGenerator.php b/src/Mcamara/LaravelLocalization/Services/LocalizedUrlGenerator.php index 7236b83..f675e7d 100644 --- a/src/Mcamara/LaravelLocalization/Services/LocalizedUrlGenerator.php +++ b/src/Mcamara/LaravelLocalization/Services/LocalizedUrlGenerator.php @@ -60,8 +60,8 @@ public function getLocalizedURL(string|null $locale = null, string $url, string if ($route->getName()) { $routeName = $route->getName(); - if (preg_match('/^trans_route_(with|no)_locale_(.*?)_(.*)$/', $routeName, $matches)) { - $type = ($hideLocaleInUrl) ? 'no' : 'with'; + if (preg_match('/^trans_route_(for|no)_locale_(.*?)_(.*)$/', $routeName, $matches)) { + $type = ($hideLocaleInUrl) ? 'no' : 'for'; $newRouteName = "trans_route_{$type}_locale_{$locale}_{$matches[3]}"; return route($newRouteName, $attributes) . $urlQuery; } diff --git a/src/Mcamara/LaravelLocalization/Services/TransRouter.php b/src/Mcamara/LaravelLocalization/Services/TransRouter.php index 4b423b6..c650870 100644 --- a/src/Mcamara/LaravelLocalization/Services/TransRouter.php +++ b/src/Mcamara/LaravelLocalization/Services/TransRouter.php @@ -34,16 +34,16 @@ public function registerTransRoute(string $routeKey, array|callable $controller, } $route = ltrim($route, '/'); - $name = "trans_route_with_locale_{$locale}_{$routeKey}"; + $name = "trans_route_for_locale_{$locale}_{$routeKey}"; - $middleware = [SetLocale::class, LocaleSessionRedirect::class]; + $middleware = [SetLocale::class]; if ($this->hideDefaultLocaleInURL && $locale === App::getLocale()) { Route::$methodType($route, $controller) ->middleware($middleware) ->name($name); } else { - Route::$methodType($locale . '/' . $route, $controller) + Route::prefix('/{locale}')->$methodType($route, $controller) ->middleware($middleware) ->name($name); diff --git a/src/Mcamara/LaravelLocalization/helper.php b/src/Mcamara/LaravelLocalization/helper.php index 5cf418d..4533d8b 100644 --- a/src/Mcamara/LaravelLocalization/helper.php +++ b/src/Mcamara/LaravelLocalization/helper.php @@ -2,32 +2,16 @@ use Illuminate\Support\Facades\Route; -if (!function_exists('localized_route')) { - function localized_route(string $name, array $parameters = [], string|null $locale = null, bool $noLocale = true): string +if (!function_exists('localized_trans_route')) { + function localized_trans_route(string $name, array $parameters = [], string|null $locale = null): string { - $computedLocale ??= app()->getLocale(); - - $withLocale = "trans_route_with_locale_{$computedLocale}_{$name}"; - $noLocale = "trans_route_no_locale_{$computedLocale}_{$name}"; - - // In tests, routes defined in setUp() are correctly registered but sometimes not recognized by - // Route::has(...), likely due to Laravel not populating the internal route name index (routesByName). - // This workaround manually checks route names to ensure they exist. - - if ($locale === null && $noLocale && collect(Route::getRoutes())->pluck('action.as')->filter()->contains($noLocale)) { - return route($noLocale, $parameters); - } - - if (collect(Route::getRoutes())->pluck('action.as')->filter()->contains($withLocale)) { - return route($withLocale, $parameters); - } - - if ($noLocale && collect(Route::getRoutes())->pluck('action.as')->filter()->contains($noLocale)) { - return route($noLocale, $parameters); - } - - + return \Mcamara\LaravelLocalization\Facades\LaravelLocalization::transRoute($name, $parameters, $locale); + } +} - return route($name, $parameters); +if (!function_exists('localized_route')) { + function localized_route(string $name, array $parameters = [], string|null $locale = null): string + { + return \Mcamara\LaravelLocalization\Facades\LaravelLocalization::route($name, $parameters, $locale); } } diff --git a/tests/LaravelLocalizationTest.php b/tests/LaravelLocalizationTest.php index ed62d0d..72056d8 100644 --- a/tests/LaravelLocalizationTest.php +++ b/tests/LaravelLocalizationTest.php @@ -127,20 +127,20 @@ protected function getEnvironmentSetUp($app) public function testTranslatedRoutes(): void { - $this->assertEquals(localized_route('about'), 'http://localhost/about'); + $this->assertEquals('http://localhost/en/about', localized_trans_route('about')); - $this->get(route('about', ['locale' => 'es'])) + $this->get(localized_trans_route('about', ['locale' => 'es'])) ->assertStatus(200); $this->assertEquals('es', app('laravellocalization')->getCurrentLocale()); - $this->assertEquals(route('about'), 'http://localhost/acerca'); + $this->assertEquals(localized_trans_route('about'), 'http://localhost/es/acerca'); - $this->get(route('about', ['locale' => 'en'])) + $this->get(localized_trans_route('about', ['locale' => 'en'])) ->assertStatus(200); - $this->assertEquals(route('about'), 'http://localhost/about'); + $this->assertEquals( 'http://localhost/en/about', localized_trans_route('about')); - $this->get(route('about', ['locale' => 'de'])) + $this->get(localized_trans_route('about', ['locale' => 'de'])) ->assertStatus(200); $this->assertEquals('en', app('laravellocalization')->getCurrentLocale()); diff --git a/v3_changes.md b/v3_changes.md index 2e70182..079c213 100644 --- a/v3_changes.md +++ b/v3_changes.md @@ -8,6 +8,21 @@ Below is a list of functions and features removed in v3. Some code was removed d If you notice something critical missing, please **open an issue**. +The main improvement of the code is: + +- We can use native caching +- Much faster +- Most of the 35 current open issues are fixed, should now be compatible with other packages +- Code base much simpler + +However, this comes with a cost: + +- Using `route(..)` helper does not work for translated routes (those defined in `/lang/routes.php` ). + You can use `localized_trans_route` helper instead. +- If you enable `hiddenDefaultLocales` and want to avoid an additional redirect for every route going to the default locale, + you should use `localized_route` helper instead. + + ## Removals & Changes - **Removed custom caching command** – Now fully compatible with Laravel’s built-in caching. @@ -20,12 +35,41 @@ If you notice something critical missing, please **open an issue**. - - **`getLocalizedURL()` no longer returns false if url is not found, instead, the same url is returned**. - **Dropped alias `localizeURL`** – If needed, you can define a custom helper. - **`translatedRoutes` is no longer stored inside `LaravelLocalization`**. -- **`transRoute()` method is no longer supported** – Use `__('routes.*')` instead. - **Removed `getNonLocalizedURL()`**. - **All `translatableRoutes` related methods have been removed** from `LaravelLocalization`. - **Removed `LaravelLocalizationRoutes` middleware** and its associated `$routeName` attribute. - Removed `createUrlFromUri` method - Removed huge `extractAttributes` method, no longer needed +- Translated routes (defined in `/lang/routes.php`) can no longer have a manual route name. Instead, each route gets a localized name per allowed locale. + Routes outside of `/lang/routes.php` can still use manual names. To generate URLs for translated routes, you may use `LaralvelLocalisation::transRoute($key)` or tha alias helper `localized_trans_route($key)` with the corresponding key `$key` from `/lang/routes.php.` +- In case you use `hiddenDefaultLocale` there might be an addition redirect when using `redirect(route(...))`. This is caused, because the name of your route is always mapped to the route with the `{localize}` parameter. + To avoid +- Removed side effect of `transRoute`, this no longer changes the current locale. +- The `currentLocale` variable of LaravelLocallization is now always identical to `App::getLocale()`. It may be removed. If something crucial was removed by mistake or if you encounter missing functionality, feel free to **create an issue**. +## Test changes + +### Changes in `testTranslatedRoutes` +General: Instead of `route` helper we need to use `localized_trans_route` helper. + +Old: + +```php +$this->assertEquals(route('about'), 'http://localhost/about'); +$this->assertEquals(route('about'), 'http://localhost/about'); +``` + +New: + +```php +$this->assertEquals('http://localhost/en/about', localized_trans_route('about')); +$this->assertEquals( 'http://localhost/en/about', localized_trans_route('about')); +``` + +Explanation: + +1. Translated routes can no longer have manual names. Use the `localized_trans_route` helper instead. +2. The expected value should come first in assertEquals. +3. `hiddenDefaultLocale` is disabled, and the locale is set to `en`, so the URL must include the locale. From b38095d1065105bb5cca4b5bbda3aef4ee369a00 Mon Sep 17 00:00:00 2001 From: Adam Nielsen Date: Sat, 22 Mar 2025 16:05:20 +0100 Subject: [PATCH 18/20] Remove invalid route naming. --- .../LaravelLocalization/LaravelLocalizationServiceProvider.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php b/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php index 2119bea..c854ab1 100644 --- a/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php +++ b/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php @@ -51,8 +51,6 @@ protected function registerMacros(): void Route::macro($localizationMacroName, function (callable $routes, array $middleware = []) { Route::middleware($middleware)->group(function () use ($routes) { - Route::name('default_lang.')->group($routes); - $supportedLocales = array_keys(config('laravellocalization.supportedLocales', [])); $localesMapping = array_keys(config('laravellocalization.localesMapping', [])); $hideDefaultLocaleInURL = config('laravellocalization.hideDefaultLocaleInURL', false); From e53ff3b978fb9be420ac0fc45aad6a1e63221801 Mon Sep 17 00:00:00 2001 From: Adam Nielsen Date: Sat, 22 Mar 2025 16:05:40 +0100 Subject: [PATCH 19/20] Add default url locale value, even if no middleware was triggered --- .../LaravelLocalization/LaravelLocalizationServiceProvider.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php b/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php index c854ab1..1e3f27a 100644 --- a/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php +++ b/src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php @@ -5,6 +5,7 @@ use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Route; +use Illuminate\Support\Facades\URL; use Illuminate\Support\ServiceProvider; class LaravelLocalizationServiceProvider extends ServiceProvider @@ -15,6 +16,8 @@ public function boot(): void __DIR__.'/../../config/config.php' => config_path('laravellocalization.php'), ], 'config'); + URL::defaults(['locale' => App::getLocale()]); + $this->registerMacros(); } From eefc9714dde09b20fb46b7ccee4529bdafc63290 Mon Sep 17 00:00:00 2001 From: Adam Nielsen Date: Sat, 22 Mar 2025 16:08:10 +0100 Subject: [PATCH 20/20] Fix tests Routes need to be reindexed after loading in test --- .../LaravelLocalization/LaravelLocalization.php | 7 +------ .../Services/LocalizedUrlGenerator.php | 4 ++++ tests/LaravelLocalizationTest.php | 13 +++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Mcamara/LaravelLocalization/LaravelLocalization.php b/src/Mcamara/LaravelLocalization/LaravelLocalization.php index 6362d69..6a41080 100644 --- a/src/Mcamara/LaravelLocalization/LaravelLocalization.php +++ b/src/Mcamara/LaravelLocalization/LaravelLocalization.php @@ -65,12 +65,7 @@ public function transRoute(string $key, array $parameters = [], string|null $loc $routeName = "trans_route_for_locale_{$computedLocale}_{$key}"; - // In tests, routes defined in setUp() are correctly registered but sometimes not recognized by - // Route::has(...), likely due to Laravel not populating the internal route name index (routesByName). - // This workaround manually checks route names to ensure they exist. - $routes = collect(Route::getRoutes())->pluck('action.as')->filter(); - - if (!$routes->contains($routeName)) { + if (!Route::has($routeName)) { return $key; } diff --git a/src/Mcamara/LaravelLocalization/Services/LocalizedUrlGenerator.php b/src/Mcamara/LaravelLocalization/Services/LocalizedUrlGenerator.php index f675e7d..9f9b825 100644 --- a/src/Mcamara/LaravelLocalization/Services/LocalizedUrlGenerator.php +++ b/src/Mcamara/LaravelLocalization/Services/LocalizedUrlGenerator.php @@ -63,6 +63,10 @@ public function getLocalizedURL(string|null $locale = null, string $url, string if (preg_match('/^trans_route_(for|no)_locale_(.*?)_(.*)$/', $routeName, $matches)) { $type = ($hideLocaleInUrl) ? 'no' : 'for'; $newRouteName = "trans_route_{$type}_locale_{$locale}_{$matches[3]}"; + if(!isset($attributes['locale'])){ + $attributes['locale'] = $locale; + } + return route($newRouteName, $attributes) . $urlQuery; } } diff --git a/tests/LaravelLocalizationTest.php b/tests/LaravelLocalizationTest.php index 72056d8..83716a2 100644 --- a/tests/LaravelLocalizationTest.php +++ b/tests/LaravelLocalizationTest.php @@ -24,11 +24,14 @@ protected function setUp(): void parent::setUp(); $this->setUpRoutes(); + + // Manually refresh named route lookups because routes are defined in setUp() + // and no request has been made yet to trigger Laravel's automatic boot logic + app('router')->getRoutes()->refreshNameLookups(); } protected function setUpRoutes(): void { - Route::localized(function () { Route::get('/', ['as' => 'index', function () { return __('routes.hello'); @@ -43,9 +46,8 @@ protected function setUpRoutes(): void LaravelLocalizationRedirectFilter::class, ]); - Route::transGet('about', [function () { - return app('laravellocalization')->getLocalizedURL('es') ?: 'Not url available'; + return app('laravellocalization')->getLocalizedURL('es') ?: 'Not url available'; }]); Route::transGet('view', [function () { @@ -64,7 +66,6 @@ protected function setUpRoutes(): void Route::get('/skipped', ['as' => 'skipped', function () { return Request::url(); }]); - } /** @@ -207,9 +208,9 @@ private function getUrl(string $uri = '') return self::$testUrl . $uri; } - - public function testGetLocalizedURL(): void + public function testGetLocalizedURLLong(): void { + /** @var LaravelLocalization $localization */ $localization = app('laravellocalization'); $this->assertEquals(