laravel:internationalization-and-translation について
Laravel アプリに初期段階から国際化を取り入れる開発者向けのスキルです。ロケール設定、翻訳ファイル、ルート、検証メッセージのほか、モデル属性、メール、JavaScript 翻訳、テスト、キャッシュを扱います。
主な機能
- 01利用可能なロケールとフォールバック設定
- 02PHP・JSON 翻訳ファイルの利用
- 03ルートと検証メッセージのローカライズ
- 04モデル属性・メール・日付形式への対応
- 05JavaScript 向け翻訳の出力
- 06翻訳のテストとキャッシュ
活用シーン
- 01画面テキストへの翻訳追加
- 02ローカライズしたルートとフォームエラーの設定
- 03メールと日付表示のローカライズ
- 04指定言語でのページ内容のテスト
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ jpcaparas/ superpowers-laravel --skill laravel:internationalization-and-translationインストール後、Claude Code で/laravel:internationalization-and-translationを入力して読み込まれたか確認します。
詳細
国際化と翻訳(i18n)
最初から国際化を考慮して Laravel アプリケーションを構築しましょう。最初は 1 つの言語のみをサポートする場合でも、文字列を翻訳関数で囲んでおくと、将来のローカライズがずっと簡単になります。
最初から翻訳する理由
// 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 対応