关于 laravel-internationalization-and-translation
面向 Laravel 应用国际化的技能,涵盖从翻译文件到本地化路由和消息的设计。内容包括 PHP、Blade 与 JavaScript 翻译方式,以及测试和维护实践。
主要功能
- 01配置语言区域与翻译文件
- 02在 PHP 和 Blade 中翻译字符串
- 03本地化路由、验证和邮件
- 04处理模型属性与日期格式
- 05将翻译导出到 JavaScript
- 06测试并缓存翻译
适用场景
- 01为 Laravel 应用增加语言支持
- 02本地化路由与验证反馈
- 03在 JavaScript 中提供翻译内容
- 04在自动化测试中检查翻译
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ noartem/ skills --skill laravel-internationalization-and-translation安装后在 Claude Code 中输入/laravel-internationalization-and-translation确认已加载。
详细介绍
国际化与翻译(i18n)
从一开始就考虑国际化,构建 Laravel 应用。即使最初只支持一种语言,将字符串封装在翻译函数中,也能让今后的本地化工作轻松许多。
为什么要从一开始就进行翻译?
// BAD: Hardcoded strings are difficult to change later
return view('welcome', [
'message' => 'Welcome to our application!'
]);
// GOOD: Translatable from day one
return view('welcome', [
'message' => __('Welcome to our application!')
]);
基本翻译设置
1. 配置可用语言区域
// config/app.php
return [
'locale' => 'en',
'fallback_locale' => 'en',
'available_locales' => ['en', 'es', 'fr', 'de', 'ja'],
'faker_locale' => 'en_US',
];
// app/Http/Middleware/SetLocale.php
class SetLocale
{
public function handle(Request $request, Closure $next): Response
{
$locale = $request->user()?->locale
?? session('locale')
?? $request->getPreferredLanguage(config('app.available_locales'))
?? config('app.locale');
app()->setLocale($locale);
return $next($request);
}
}
2. 翻译文件结构
// lang/en/messages.php
return [
'welcome' => 'Welcome to :app_name',
'greeting' => 'Hello, :name!',
'item_count' => '{0} No items|{1} One item|[2,*] :count items',
'account' => [
'profile' => 'Profile',
'settings' => 'Settings',
'logout' => 'Sign out',
],
];
// lang/es/messages.php
return [
'welcome' => 'Bienvenido a :app_name',
'greeting' => '¡Hola, :name!',
'item_count' => '{0} Sin artículos|{1} Un artículo|[2,*] :count artículos',
'account' => [
'profile' => 'Perfil',
'settings' => 'Configuración',
'logout' => 'Cerrar sesión',
],
];
3. JSON 翻译文件(用于单行键)
// lang/en.json
{
"Welcome back!": "Welcome back!",
"Your order has been confirmed": "Your order has been confirmed",
"Please verify your email address": "Please verify your email address"
}
// lang/es.json
{
"Welcome back!": "¡Bienvenido de nuevo!",
"Your order has been confirmed": "Su pedido ha sido confirmado",
"Please verify your email address": "Por favor verifica tu dirección de correo"
}
翻译辅助函数
基本翻译
// Using __() helper
echo __('messages.welcome', ['app_name' => config('app.name')]);
// Using trans() helper
echo trans('messages.greeting', ['name' => $user->name]);
// Direct strings (uses JSON files)
echo __('Welcome back!');
// Pluralization
echo trans_choice('messages.item_count', $count, ['count' => $count]);
在 Blade 模板中
{{-- Basic translation --}}
<h1>{{ __('messages.welcome', ['app_name' => config('app.name')]) }}</h1>
{{-- Direct string translation --}}
<p>{{ __('Please verify your email address') }}</p>
{{-- With @lang directive --}}
<p>@lang('messages.greeting', ['name' => $user->name])</p>
{{-- Pluralization --}}
<p>{{ trans_choice('messages.item_count', $cart->count(), ['count' => $cart->count()]) }}</p>
{{-- Inside attributes --}}
<button title="{{ __('Click to continue') }}">
{{ __('Next') }}
</button>
高级功能
1. 可翻译的模型属性
// app/Models/Product.php
use Spatie\Translatable\HasTranslations;
class Product extends Model
{
use HasTranslations;
public $translatable = ['name', 'description'];
protected $casts = [
'name' => 'array',
'description' => 'array',
];
}
// Usage
$product = Product::create([
'name' => [
'en' => 'Laptop',
'es' => 'Portátil',
'fr' => 'Ordinateur portable',
],
'description' => [
'en' => 'High-performance laptop',
'es' => 'Portátil de alto rendimiento',
'fr' => 'Ordinateur portable haute performance',
],
]);
// Automatic locale detection
echo $product->name; // Uses app()->getLocale()
// Specific locale
echo $product->getTranslation('name', 'es'); // Portátil
2. 路由本地化
// routes/web.php
Route::middleware(['web', 'setlocale'])->group(function () {
Route::get('/', [HomeController::class, 'index'])->name('home');
// Localized routes
Route::prefix('{locale}')
->where(['locale' => '[a-z]{2}'])
->middleware('locale')
->group(function () {
Route::get('/', [HomeController::class, 'index'])->name('localized.home');
Route::get('/about', [PageController::class, 'about'])->name('localized.about');
});
});
// app/Http/Middleware/LocaleMiddleware.php
class LocaleMiddleware
{
public function handle($request, Closure $next)
{
if ($locale = $request->route('locale')) {
if (in_array($locale, config('app.available_locales'))) {
app()->setLocale($locale);
session(['locale' => $locale]);
}
}
return $next($request);
}
}
3. 表单验证消息
// lang/en/validation.php
return [
'required' => 'The :attribute field is required.',
'email' => 'The :attribute must be a valid email address.',
'custom' => [
'email' => [
'required' => 'We need your email address!',
'unique' => 'This email is already registered.',
],
],
'attributes' => [
'email' => 'email address',
'name' => 'full name',
],
];
// In FormRequest
class RegisterRequest extends FormRequest
{
public function messages()
{
return [
'email.required' => __('validation.custom.email.required'),
'email.unique' => __('validation.custom.email.unique'),
];
}
public function attributes()
{
return [
'email' => __('validation.attributes.email'),
'name' => __('validation.attributes.name'),
];
}
}
4. 日期和数字格式化
// app/Helpers/LocaleHelper.php
class LocaleHelper
{
public static function formatDate(Carbon $date): string
{
return match(app()->getLocale()) {
'en' => $date->format('m/d/Y'),
'es' => $date->format('d/m/Y'),
'de' => $date->format('d.m.Y'),
'ja' => $date->format('Y年m月d日'),
default => $date->toDateString(),
};
}
public static function formatCurrency(float $amount, string $currency = 'USD'): string
{
$formatter = new NumberFormatter(app()->getLocale(), NumberFormatter::CURRENCY);
return $formatter->formatCurrency($amount, $currency);
}
public static function formatNumber(float $number, int $decimals = 2): string
{
$formatter = new NumberFormatter(app()->getLocale(), NumberFormatter::DECIMAL);
$formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimals);
return $formatter->format($number);
}
}
// Usage in Blade
<p>{{ LocaleHelper::formatDate($order->created_at) }}</p>
<p>{{ LocaleHelper::formatCurrency($order->total, 'EUR') }}</p>
<p>{{ LocaleHelper::formatNumber($product->weight, 2) }} kg</p>
5. 语言切换器组件
{{-- resources/views/components/language-switcher.blade.php --}}
<div class="language-switcher">
<select onchange="window.location.href=this.value"
aria-label="{{ __('Select Language') }}">
@foreach(config('app.available_locales') as $locale)
<option value="{{ route('locale.switch', $locale) }}"
@selected(app()->getLocale() === $locale)>
{{ strtoupper($locale) }}
</option>
@endforeach
</select>
</div>
{{-- Or as links --}}
<ul class="language-menu">
@foreach(config('app.available_locales') as $locale)
<li>
<a href="{{ route('locale.switch', $locale) }}"
@class(['active' => app()->getLocale() === $locale])>
{{ __("languages.$locale") }}
</a>
</li>
@endforeach
</ul>
6. 电子邮件本地化
// app/Mail/OrderConfirmation.php
class OrderConfirmation extends Mailable
{
use Queueable, SerializesModels;
public function __construct(
public Order $order,
public string $locale
) {}
public function build()
{
return $this->locale($this->locale)
->subject(__('mail.order_confirmation.subject'))
->view('emails.order-confirmation');
}
}
// Send email in user's language
Mail::to($user)->send(new OrderConfirmation($order, $user->locale));
JavaScript 翻译
1. 将翻译导出到 JavaScript
// app/Http/Controllers/TranslationController.php
class TranslationController extends Controller
{
public function index(Request $request)
{
$locale = $request->get('locale', app()->getLocale());
$translations = Cache::remember("translations.{$locale}", 3600, function () use ($locale) {
return collect(File::allFiles(lang_path($locale)))
->flatMap(function ($file) use ($locale) {
return [
$file->getBasename('.php') => include $file->getPathname()
];
})->toJson();
});
return response($translations)->header('Content-Type', 'application/json');
}
}
// In JavaScript
const translations = await fetch(`/api/translations?locale=${locale}`).then(r => r.json());
function __(key, replace = {}) {
let translation = key.split('.').reduce((t, i) => t?.[i], translations) || key;
Object.keys(replace).forEach(key => {
translation = translation.replace(`:${key}`, replace[key]);
});
return translation;
}
// Usage
console.log(__('messages.welcome', { app_name: 'MyApp' }));
2. 使用 Laravel Mix/Vite
// resources/js/i18n.js
import { createI18n } from 'vue-i18n';
const messages = {
en: require('./locales/en.json'),
es: require('./locales/es.json'),
fr: require('./locales/fr.json'),
};
export default createI18n({
locale: document.documentElement.lang || 'en',
fallbackLocale: 'en',
messages,
});
// In Vue component
<template>
<h1>{{ $t('messages.welcome', { app_name: appName }) }}</h1>
</template>
测试翻译
test('displays content in selected language', function () {
// Test English
$response = $this->withSession(['locale' => 'en'])
->get('/');
$response->assertSee('Welcome to our application');
// Test Spanish
$response = $this->withSession(['locale' => 'es'])
->get('/');
$response->assertSee('Bienvenido a nuestra aplicación');
});
test('falls back to default locale for missing translations', function () {
app()->setLocale('fr');
// If French translation missing, falls back to English
expect(__('some.missing.key'))->toBe('some.missing.key');
expect(__('messages.welcome', ['app_name' => 'Test']))->not->toBeEmpty();
});
test('pluralization works correctly', function () {
expect(trans_choice('messages.item_count', 0))->toBe('No items');
expect(trans_choice('messages.item_count', 1))->toBe('One item');
expect(trans_choice('messages.item_count', 5, ['count' => 5]))->toBe('5 items');
});
最佳实践
-
始终封装面向用户的字符串
// Even in single-language apps flash(__('Your changes have been saved.')); -
使用有意义的翻译键
// BAD __('msg1') // GOOD __('auth.login_successful') -
将相关翻译分组
// lang/en/auth.php return [ 'login' => 'Sign in', 'logout' => 'Sign out', 'register' => 'Register', 'forgot_password' => 'Forgot your password?', ]; -
在键中提供上下文
// Different contexts may need different translations __('button.save') // "Save" __('message.save') // "Your changes will be saved" __('title.save') // "Save Document" -
妥善处理缺失的翻译
class AppServiceProvider extends ServiceProvider { public function boot() { // Log missing translations in production if (app()->environment('production')) { Event::listen(TranslationNotFound::class, function ($event) { Log::warning('Missing translation', [ 'key' => $event->key, 'locale' => $event->locale, ]); }); } } } -
在生产环境中缓存翻译
# Cache for better performance sail artisan lang:cache # Clear when updating sail artisan lang:clear
请记住:从第一天起就开始翻译。随着应用的构建同步维护翻译,要比之后再补做容易得多!
评论
支持 Markdown