codezero/laravel-localized-routes

在Laravel应用中设置、管理和使用本地化路由的便捷方式。

4.0.1 2024-03-17 22:41 UTC

README

GitHub release Laravel License Build Status Code Coverage Code Quality Total Downloads

ko-fi

在Laravel应用中设置和使用本地化路由的便捷方式。

📖 目录

✅ 要求

⬆ 升级

升级到新主要版本?请查看我们的升级指南以获取说明。

📦 安装

使用Composer安装此包

composer require codezero/laravel-localized-routes

Laravel将自动注册ServiceProvider。

⚙ 配置

☑ 发布配置文件

php artisan vendor:publish --provider="CodeZero\LocalizedRoutes\LocalizedRoutesServiceProvider" --tag="config"

现在您可以在config文件夹中找到一个localized-routes.php文件。

☑ 配置支持的本地化

简单本地化

将您希望支持的任何本地化添加到已发布的config/localized-routes.php文件中

'supported_locales' => ['en', 'nl'];

这些本地化将被用作别名,附加到您的本地化路由的URL前面。

自定义别名

您还可以为本地化使用自定义别名

'supported_locales' => [
    'en' => 'english-slug',
    'nl' => 'dutch-slug',
];

自定义域名

或者您可以为本地化使用自定义域名

'supported_locales' => [
    'en' => 'english-domain.test',
    'nl' => 'dutch-domain.test',
];

☑ 使用回退本地化

当使用route()辅助函数为不支持的语言生成URL时,Laravel会抛出Symfony\Component\Routing\Exception\RouteNotFoundException异常。但是,您可以配置回退本地化,尝试解析回退URL。如果这也失败,则抛出异常。

'fallback_locale' => 'en',

☑ 省略主本地化的别名

如果您想省略主本地化的别名,请指定您的默认本地化

'omitted_locale' => 'en',

如果使用域名而不是别名,此选项不起作用。

☑ 作用域选项

要仅为一组本地化路由设置选项,可以将它指定为本地化路由宏的第二个参数。这将覆盖配置文件设置。目前,只能覆盖2个选项。

Route::localized(function () {
    Route::get('about', [AboutController::class, 'index']);
}, [
    'supported_locales' => ['en', 'nl', 'fr'],
    'omitted_locale' => 'en',
]);

🧩 添加中间件以更新应用程序本地化

默认情况下,应用程序的本地化始终是您在config/app.php中配置的。要自动更新应用程序的本地化,您需要在web中间件组中注册中间件。确保在StartSession之后和SubstituteBindings之前添加它。

如果您使用本地化路由键(翻译后的别名),则中间件顺序非常重要!在设置区域设置时,会话必须处于活动状态,并且替换路由绑定时必须设置区域设置。

Laravel 11 及更高版本

将中间件添加到 bootstrap/app.php 中的 web 中间件组。

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->web(remove: [
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ]);
    $middleware->web(append: [
        \CodeZero\LocalizedRoutes\Middleware\SetLocale::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ]);
})

Laravel 10

将中间件添加到 app/Http/Kernel.php 中的 web 中间件组。

// app/Http/Kernel.php
protected $middlewareGroups = [
    'web' => [
        //...
        \Illuminate\Session\Middleware\StartSession::class, // <= after this
        //...
        \CodeZero\LocalizedRoutes\Middleware\SetLocale::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class, // <= before this
    ],
];

检测器

中间件将按顺序运行以下检测器,直到其中一个返回支持的区域设置

更新配置文件中的 detectors 数组,以选择要运行的检测器及其顺序。

您可以通过实现 CodeZero\LocalizedRoutes\Middleware\Detectors\Detector 接口来创建自己的检测器,并在配置文件中添加对其的引用。检测器由 Laravel 的 IOC 容器解析,因此您可以在构造函数中添加任何依赖项。

存储

如果检测到支持的区域设置,它将自动存储在

更新配置中的 stores 数组以选择要使用的存储。

您可以通过实现 CodeZero\LocalizedRoutes\Middleware\Stores\Store 接口来创建自己的存储,并在配置文件中添加对其的引用。存储由 Laravel 的 IOC 容器解析,因此您可以在构造函数中添加任何依赖项。

尽管不需要进一步配置,但您可以在配置文件中更改高级设置。

🚘 注册路由

Route::localized() 闭包内定义您的路由,以自动为每个区域设置注册它们。

这将在路由的 URI 和名称前添加区域设置。如果您配置了自定义域名,则将使用那些域名而不是 URI 别名。您也可以在闭包中使用路由组。

Route::localized(function () {
    Route::get('about', [AboutController::class, 'index'])->name('about');
});

对于支持的区域设置 ['en', 'nl'],上述代码将注册

如果省略的区域设置设置为 en,则结果将是

在大多数实际场景中,您会注册一个区域设置路由或非区域设置路由,但不会同时注册两者。如果您这样做,您将始终需要指定区域设置来使用 route() 辅助函数生成 URL,因为现有的路由名称始终具有优先级。特别是当省略主区域设置从 URL 中时,这可能会出现问题,因为您不能在这种情况下同时拥有区域化的 /about 路由和非区域化的 /about 路由。相同的概念也适用于 /(根)路由!请注意,即使省略了别名,路由名称仍然具有区域设置前缀。

☑ 使用路由模型绑定翻译参数

当解析来自请求的路由参数时,您可能依赖于 Laravel 的路由模型绑定。您在控制器中为模型添加类型提示,它将根据 ID 或特定的属性(如 {model:slug})查找 {model}。如果它找到与 URL 中参数值匹配的项,则将其注入到控制器中。

// Example: use the post slug as the route parameter
Route::get('posts/{post:slug}', [PostsController::class, 'index']);

// PostsController.php
public function index(Post $post)
{
    return $post;
}

但是,为了解析区域化参数,您需要向您的模型添加 resolveRouteBinding() 方法。在此方法中,您需要编写逻辑以找到匹配项,使用 URL 中的参数值。

例如,您可能在数据库中有一个包含翻译后别名的 JSON 列

public function resolveRouteBinding($value, $field = null)
{
    // Default field to query if no parameter field is specified
    $field = $field ?: $this->getRouteKeyName();
    
    // If the parameter field is 'slug',
    // lets query a JSON field with translations
    if ($field === 'slug') {
        $field .= '->' . App::getLocale(); 
    }
    
    // Perform the query to find the parameter value in the database
    return $this->where($field, $value)->firstOrFail();
}

如果您正在寻找在模型上实现翻译属性的好解决方案,请务必查看 spatie/laravel-translatable

☑ 翻译硬编码的 URI 别名

此包包括 codezero/laravel-uri-translator。它注册了一个 Lang::uri() 宏,该宏允许您翻译单个硬编码的 URI 别名。此宏不会翻译路由参数。

需要翻译URI的路由必须有一个名称,才能使用route()辅助函数或Route::localizedUrl()宏来生成其本地化版本。因为这些路由的slug根据语言环境而不同,所以路由名称是唯一将它们联系在一起的东西。

首先,为每个语言环境在你的应用lang文件夹中创建一个routes.php翻译文件,例如

lang/nl/routes.php
lang/fr/routes.php

然后,向每个文件添加适当的翻译

// lang/nl/routes.php
return [
    'about' => 'over',
    'us' => 'ons',
];

最后,在注册路由时使用宏

Route::localized(function () {
    Route::get(Lang::uri('about/us'), [AboutController::class, 'index'])->name('about');
});

URI宏接受两个额外参数

  1. 一个语言环境,如果你需要翻译成除了当前应用语言环境以外的语言环境。
  2. 一个命名空间,如果你的翻译文件位于一个包中。
Lang::uri('hello/world', 'fr', 'my-package');

你也可以使用trans()->uri('hello/world')代替Lang::uri('hello/world')

示例

使用这些示例翻译

// lang/nl/routes.php
return [
    'hello' => 'hallo',
    'world' => 'wereld',
    'override/hello/world' => 'something/very/different',
    'hello/world/{parameter}' => 'uri/with/{parameter}',
];

这些是可能的翻译结果

// Translate every slug individually
// Translates to: 'hallo/wereld'
Lang::uri('hello/world');

// Keep original slug when missing translation
// Translates to: 'hallo/big/wereld'
Lang::uri('hello/big/world');

// Translate slugs, but not parameter placeholders
// Translates to: 'hallo/{world}'
Lang::uri('hello/{world}');

// Translate full URIs if an exact translation exists
// Translates to: 'something/very/different'
Lang::uri('override/hello/world');

// Translate full URIs if an exact translation exists (with placeholder)
// Translates to: 'uri/with/{parameter}'
Lang::uri('hello/world/{parameter}');

🔦 本地化404页面

标准的404响应没有实际的Route,并且不通过中间件。这意味着我们的中间件将无法更新语言环境,请求无法本地化。

为了修复这个问题,你可以在你的routes/web.php文件的末尾注册这个回退路由

Route::fallback(\CodeZero\LocalizedRoutes\Controllers\FallbackController::class);

因为回退路由是一个实际的Route,中间件将运行并更新语言环境。

回退路由是Laravel提供的一个“捕获所有”路由。如果你输入一个不存在的URL,将触发这个路由而不是典型的404异常。

FallbackController将尝试返回一个位于resources/views/errors/404.blade.php的404错误视图。如果这个视图不存在,将抛出正常的Symfony\Component\HttpKernel\Exception\NotFoundHttpException异常。你可以通过更改配置文件中的404_view条目来配置使用哪个视图。

以下情况下,回退路由不会应用

  • 你的现有路由抛出404异常(如abort(404)
  • 你的现有路由抛出ModelNotFoundException(如使用路由模型绑定)
  • 你的现有路由抛出任何其他异常

🗄 缓存路由

在生产环境中,你可以安全地按常规缓存你的路由。

php artisan route:cache

⚓ 生成路由URL

☑ 为活动语言环境生成URL

你可以像平常一样使用route()辅助函数来获取命名路由的URL。

$url = route('about'); 

如果你注册了一个未本地化的about路由,那么about是一个现有的路由名称,其URL将返回。否则,这将尝试为活动语言环境生成about URL,例如en.about

☑ 为特定语言环境生成URL

在某些情况下,你可能需要为特定语言环境生成一个URL。为此,Laravel的route()辅助函数中添加了一个额外的语言环境参数。

$url = route('about', [], true, 'nl'); // this will load 'nl.about'

☑ 生成带有本地化参数的URL

有几种方法可以生成带有本地化参数的路由URL。

手动传递本地化参数

假设我们有一个具有getSlug()方法的Post模型

public function getSlug($locale = null)
{
    $locale = $locale ?: App::getLocale();
    
    $slugs = [
        'en' => 'en-slug',
        'nl' => 'nl-slug',
    ];

    return $slugs[$locale] ?? '';
}

当然,在实际项目中,slugs不会是硬编码的。如果你在寻找一个实现模型上翻译属性的好方案,务必查看spatie/laravel-translatable

现在你可以将本地化slug传递给route()函数

route('posts.show', [$post->getSlug()]);
route('posts.show', [$post->getSlug('nl')], true, 'nl');

使用自定义的本地化路由键

你可以通过向你的模型添加getRouteKey()方法来让Laravel自动解析本地化参数

public function getRouteKey()
{
    $locale = App::getLocale();
    
    $slugs = [
        'en' => 'en-slug',
        'nl' => 'nl-slug',
    ];

    return $slugs[$locale] ?? '';
}

现在你只需要传递模型

route('posts.show', [$post]);
route('posts.show', [$post], true, 'nl');

☑ 回退URL

可以在配置文件中提供回退语言环境。如果route()辅助函数的语言环境参数不是受支持的语言环境,则将使用回退语言环境。

// When the fallback locale is set to 'en'
// and the supported locales are 'en' and 'nl'

$url = route('about', [], true, 'nl'); // this will load 'nl.about'
$url = route('about', [], true, 'wk'); // this will load 'en.about'

如果既不能解析常规路由也不能解析本地化路由,将抛出Symfony\Component\Routing\Exception\RouteNotFoundException异常。

☑ 生成当前URL的本地化版本

要为任何地区生成当前路由的URL,您可以使用Route::localizedUrl()宏。

手动传递参数

就像使用route()辅助函数一样,您可以将参数作为第二个参数传递。

假设我们有一个具有getSlug()方法的Post模型

public function getSlug($locale = null)
{
    $locale = $locale ?: App::getLocale();
    
    $slugs = [
        'en' => 'en-slug',
        'nl' => 'nl-slug',
    ];

    return $slugs[$locale] ?? '';
}

现在您可以将本地化别名传递给宏

$current = Route::localizedUrl(null, [$post->getSlug()]);
$en = Route::localizedUrl('en', [$post->getSlug('en')]);
$nl = Route::localizedUrl('nl', [$post->getSlug('nl')]);

使用自定义路由键

如果您添加了模型的getRouteKey()方法,您根本不需要传递参数。

public function getRouteKey()
{
    $locale = App::getLocale();
    
    $slugs = [
        'en' => 'en-slug',
        'nl' => 'nl-slug',
    ];

    return $slugs[$locale] ?? '';
}

宏现在将自动确定当前路由具有哪些参数并获取这些值。

$current = Route::localizedUrl();
$en = Route::localizedUrl('en');
$nl = Route::localizedUrl('nl');

多个路由键

如果您有一个具有多个键的路由,例如/en/posts/{id}/{slug},那么您可以在模型中实现ProvidesRouteParameters接口。然后,从getRouteParameters()方法中返回所需的参数值。

use CodeZero\LocalizedRoutes\ProvidesRouteParameters;
use Illuminate\Database\Eloquent\Model;

class Post extends Model implements ProvidesRouteParameters
{
    public function getRouteParameters($locale = null)
    {
        return [
            $this->id,
            $this->getSlug($locale) // Add this method yourself of course :)
        ];
    }
}

现在,参数仍然会自动解析

$current = Route::localizedUrl();
$en = Route::localizedUrl('en');
$nl = Route::localizedUrl('nl');

保留或删除查询字符串

默认情况下,查询字符串将包含在生成的URL中。如果您不想这样,可以向宏传递一个额外的参数

$keepQuery = false;
$current = Route::localizedUrl(null, [], true, $keepQuery);

☑ 示例地区切换器

以下Blade代码片段将在每个替代地区添加当前页面的链接。

它只会在当前路由是本地化或回退路由时运行。

@if (Route::isLocalized() || Route::isFallback())
    <ul>
        @foreach(LocaleConfig::getLocales() as $locale)
            @if ( ! App::isLocale($locale))
                <li>
                    <a href="{{ Route::localizedUrl($locale) }}">
                        {{ strtoupper($locale) }}
                    </a>
                </li>
            @endif
        @endforeach
    </ul>
@endif

🖋 生成签名路由URL

生成本地化签名路由和临时签名路由URL与生成正常路由URL一样简单。传递路由名称和必要的参数,您将获得当前地区的URL。

$signedUrl = URL::signedRoute('reset.password', ['user' => $id]);
$signedUrl = URL::temporarySignedRoute('reset.password', now()->addMinutes(30), ['user' => $id]);

您还可以为特定地区生成签名路由URL

$signedUrl = URL::signedRoute('reset.password', ['user' => $id], null, true, 'nl');
$signedUrl = URL::temporarySignedRoute('reset.password', now()->addMinutes(30), ['user' => $id], true, 'nl');

有关签名路由的更多信息,请参阅Laravel文档

🚌 重定向到路由

您可以使用redirect()辅助函数或Redirect外观重定向到路由,就像在正常的Laravel应用程序中一样。

如果您注册了一个未本地化的about路由,那么about是一个现有的路由名称,其URL将被重定向。否则,这将尝试重定向到活动地区的about路由,例如en.about

return redirect()->route('about');

您还可以重定向到特定地区的URL

// Redirects to 'nl.about'
return redirect()->route('about', [], 302, [], 'nl');

还包括本地化的signedRoutetemporarySignedRoute重定向

// Redirects to the active locale
return redirect()->signedRoute('signed.route', ['user' => $id]);
return redirect()->temporarySignedRoute('signed.route', now()->addMinutes(30), ['user' => $id]);

// Redirects to 'nl.signed.route'
return redirect()->signedRoute('signed.route', ['user' => $id], null, 302, [], 'nl');
return redirect()->temporarySignedRoute('signed.route', now()->addMinutes(30), ['user' => $id], 302, [], 'nl');

🪧 自动重定向到本地化URL

要将任何非本地化URL重定向到其本地化版本,可以将配置选项redirect_to_localized_urls设置为true,并在您的routes/web.php文件末尾注册以下回退路由,使用FallbackController

Route::fallback(\CodeZero\LocalizedRoutes\Controllers\FallbackController::class);

回退路由是Laravel提供的一个“捕获所有”路由。如果你输入一个不存在的URL,将触发这个路由而不是典型的404异常。

FallbackController将尝试重定向到URL的本地化版本,或者如果它不存在,则返回一个本地化404响应

例如

如果省略的地区设置为en

如果路由不存在,将返回404响应。

🪜 辅助函数

Route::hasLocalized()

// Check if a named route exists in the active locale:
$exists = Route::hasLocalized('about');
// Check if a named route exists in a specific locale:
$exists = Route::hasLocalized('about', 'nl');

Route::isLocalized()

// Check if the current route is localized:
$isLocalized = Route::isLocalized();
// Check if the current route is localized and has a specific name:
$isLocalized = Route::isLocalized('about');
// Check if the current route has a specific locale and has a specific name:
$isLocalized = Route::isLocalized('about', 'nl');
// Check if the current route is localized and its name matches a pattern:
$isLocalized = Route::isLocalized(['admin.*', 'dashboard.*']);
// Check if the current route has one of the specified locales and has a specific name:
$isLocalized = Route::isLocalized('about', ['en', 'nl']);

Route::isFallback()

// Check if the current route is a fallback route:
$isFallback = Route::isFallback();

🚧 测试

composer test

☕ 致谢

🔒 安全性

如果您发现任何与安全相关的问题,请通过电子邮件与我联系,而不是使用问题跟踪器。

📑 更新日志

有关此包所有显著更改的完整列表,请参阅发行页面

📜 许可证

MIT许可证(MIT)。有关更多信息,请参阅许可证文件