mcpField
L
laravel-internationalization-and-translation

laravel-internationalization-and-translation

github.com/noartem开发1375 次安装3 周前更新
代码仓库

关于 laravel-internationalization-and-translation

面向 Laravel 应用国际化的技能,涵盖从翻译文件到本地化路由和消息的设计。内容包括 PHP、Blade 与 JavaScript 翻译方式,以及测试和维护实践。

主要功能

  1. 01配置语言区域与翻译文件
  2. 02在 PHP 和 Blade 中翻译字符串
  3. 03本地化路由、验证和邮件
  4. 04处理模型属性与日期格式
  5. 05将翻译导出到 JavaScript
  6. 06测试并缓存翻译

适用场景

  1. 01为 Laravel 应用增加语言支持
  2. 02本地化路由与验证反馈
  3. 03在 JavaScript 中提供翻译内容
  4. 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');
});

最佳实践

  1. 始终封装面向用户的字符串

    // Even in single-language apps
    flash(__('Your changes have been saved.'));
    
  2. 使用有意义的翻译键

    // BAD
    __('msg1')
    
    // GOOD
    __('auth.login_successful')
    
  3. 将相关翻译分组

    // lang/en/auth.php
    return [
        'login' => 'Sign in',
        'logout' => 'Sign out',
        'register' => 'Register',
        'forgot_password' => 'Forgot your password?',
    ];
    
  4. 在键中提供上下文

    // Different contexts may need different translations
    __('button.save')  // "Save"
    __('message.save')  // "Your changes will be saved"
    __('title.save')    // "Save Document"
    
  5. 妥善处理缺失的翻译

    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,
                    ]);
                });
            }
        }
    }
    
  6. 在生产环境中缓存翻译

    # Cache for better performance
    sail artisan lang:cache
    
    # Clear when updating
    sail artisan lang:clear
    

请记住:从第一天起就开始翻译。随着应用的构建同步维护翻译,要比之后再补做容易得多!

评论

支持 Markdown