Laravel Jetstream + Vue の多言語対応(i18n)
Laravel Jetstream (Inertia + Vue 構成) を多言語対応にする
最近 Laravel を使うときには Jetstream (Inertia + Vue) の構成でインストールすることが多い。
言語ファイルは取得できても Vue ファイル側が対応していないので、日本語で表示するために毎回対応が必要になっている。
修正範囲が広いので大体抜けが出る(悲しい)。
2022/3/31 追記
↑ 抜けが出過ぎて悲しいので、自分用に Jetstream をフォークしました。
この記事の内容と同等の変更を加えています。
1. 言語ファイルの取得
resources/lang 以下のファイルは、必要に応じて Laravel Lang などから取得する
Laravel Lang Publisher で管理すると楽なのでこちらを利用する。
composer require laravel-lang/publisher laravel-lang/lang --dev
英語と日本語のリソースを追加
php artisan lang:add en ja
※ 更新するときは reset を使うとプロジェクトで追加した項目を保持したまま翻訳データが更新される
php artisan lang:reset en ja
php artisan lang:reset
2. vue-i18n を導入
vue-i18n インストール
npm install vue-i18n@9
※ バージョンはVue I18n のドキュメントで確認する。
app.blade.php, app.js 修正
ルートテンプレート (app.blade.php) に locale を定義する。
<!-- Scripts -->
@routes
<script>
var __locale = '{{ app()->getLocale() }}'
</script>
<script src="{{ mix('js/app.js') }}" defer></script>
app.js に多言語化(i18n)処理を追加する。
legacy: false、globalInjection: true を指定することでグローバルに、どの vue ファイルからでもメッセージ翻訳関数 $t() の使用が可能になる。
require('./bootstrap')
import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/inertia-vue3'
import { InertiaProgress } from '@inertiajs/progress'
import { createI18n } from 'vue-i18n'
const appName =
window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel'
async function loadAndConvertMessages(locale) {
const messages = await import(`../lang/${locale}.json`)
var msgObj = {}
Object.entries(messages).forEach(([key, value]) => {
// :parameter -> {parameter}
msgObj[key] = String(value).replace(/:([a-zA-Z_]+)/g, '{$1}')
})
return msgObj
}
async function loadLocaleMessages(i18n, locale) {
const messages = await loadAndConvertMessages(locale)
i18n.global.setLocaleMessage(locale, messages)
}
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) => require(`./Pages/${name}.vue`),
async setup({ el, app, props, plugin }) {
const i18n = createI18n({
legacy: false,
globalInjection: true,
locale: __locale,
})
await loadLocaleMessages(i18n, __locale)
return createApp({ render: () => h(app, props) })
.use(plugin)
.use(i18n)
.mixin({ methods: { route } })
.mount(el)
},
})
InertiaProgress.init({ color: '#4B5563' })
3. Vue ファイルを修正
多言語対応する文字列に翻訳関数 $t() を適用する。
ほとんどの文字列は resources/lang/*.json に定義されている。
resources/js 以下の Vue ファイルはほぼすべて修正が必要。
- 文字列、タイトル、ボタン名、リンク名
- ラベル
- プレースホルダ
※ props に翻訳対象の文字列が入ってくる場合がある
<template>
<span>
<span @click="startConfirmingPassword">
<slot />
</span>
<jet-dialog-modal :show="confirmingPassword" @close="closeModal">
<template #title>
{{ $t(title) }}
</template>
<template #content>
{{ $t(content) }}
<div class="mt-4">
<jet-input
type="password"
class="mt-1 block w-3/4"
:placeholder="$t('Password')"
ref="password"
v-model="form.password"
@keyup.enter="confirmPassword"
/>
<jet-input-error :message="form.error" class="mt-2" />
</div>
</template>
<template #footer>
<jet-secondary-button @click="closeModal">
{{ $t('Cancel') }}
</jet-secondary-button>
<jet-button
class="ml-3"
@click="confirmPassword"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
>
{{ $t(button) }}
</jet-button>
</template>
</jet-dialog-modal>
</span>
</template>
<script>
import { defineComponent } from 'vue'
import JetButton from './Button.vue'
import JetDialogModal from './DialogModal.vue'
import JetInput from './Input.vue'
import JetInputError from './InputError.vue'
import JetSecondaryButton from './SecondaryButton.vue'
export default defineComponent({
emits: ['confirmed'],
props: {
title: {
default: 'Confirm Password',
},
content: {
default: 'For your security, please confirm your password to continue.',
},
button: {
default: 'Confirm',
},
},
// ...
})
</script>
上記例では、title, content, button のデフォルト値が翻訳対象になる。
変数の値に翻訳をかけるため、クオテーションを入れないように注意する。
NG: {{ $t('title') }}
OK: {{ $t(title) }}
※ HTML タグを含むメッセージ
ユーザ登録画面の "サービス利用規約とプライバシーポリシーに同意する" の部分のように、パラメータを置き換えつつリンクを張る(a タグ)場合は以下のように記述することで翻訳できる。
<div class="ml-2">
<i18n-t keypath="I agree to the :terms_of_service and :privacy_policy">
<template #terms_of_service>
<a target="_blank" :href="route('terms.show')" class="underline text-sm text-gray-600 hover:text-gray-900">{{ $t('Terms of Service') }}</a>
</template>
<template #privacy_policy>
<a target="_blank" :href="route('policy.show')" class="underline text-sm text-gray-600 hover:text-gray-900">{{ $t('Privacy Policy') }}</a>
</template>
</i18n-t>
</div>
参考: https://vue-i18n.intlify.dev/ja/guide/advanced/component.html
4. バリデーションの属性も修正
※ 忘れがちだけどエラーメッセージの項目名だけが英語だと悲しい。
app/Actions 以下のファイルのバリデーション部分で、Validator::make() の第 3 引数にバリデーション属性のカスタマイズを指定する。
ここでは __ ヘルパ関数が使える。
// ...
public function create(array $input)
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => $this->passwordRules(),
'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '',
], [], [
'name' => __('Name'),
'email' => __('Email'),
'password' => __('Password'),
'terms' => __('Terms of Service'),
])->validate();
// ...
Special Thanks :
Discussion