🎉

Vite版 Laravel Jetstream + Vue の多言語対応(i18n)

2022/08/02に公開

Laravel Jetstream (Inertia + Vue 構成) を多言語対応にする

Laravel 本体が v9.2 から Vite 対応になったので、

https://zenn.dev/blancpanda/articles/jetstream-vue-i18n

↑ こちらの記事の Vite 対応版です。

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 を定義する。

resources/views/app.blade.php
    <!-- Scripts -->
    @routes
    <script>
        var __locale = '{{ app()->getLocale() }}';
        var __fallback_locale = '{{ app()->getFallbackLocale() }}';
    </script>
    @vite('resources/js/app.js')

app.js に多言語化(i18n)処理を追加する。
legacy: false、globalInjection: true を指定することでグローバルに、どの vue ファイルからでもメッセージ翻訳関数 $t() の使用が可能になる。

resources/js/app.js
import '../css/app.css'
import './bootstrap'

import { createInertiaApp } from '@inertiajs/inertia-vue3'
import { InertiaProgress } from '@inertiajs/progress'
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'
import { createApp, h } from 'vue'
import { createI18n } from 'vue-i18n'
import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/vue.m'

const appName =
  window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel'

async function loadLocaleMessages() {
  var messages = {}
  const resources = import.meta.glob('LANG_PATH*.json', { as: 'raw' })
  for (const path in resources) {
    const raw = await resources[path]()
    const lang = path.match(/([^/]+)\.json$/)[1]
    var langMsg = JSON.parse(raw)
    Object.entries(langMsg).forEach(([key, value]) => {
      langMsg[key] = String(value).replace(/:(\w+)/g, '{$1}')
    })
    messages[lang] = langMsg
  }
  return messages
}

createInertiaApp({
  title: (title) => `${title} - ${appName}`,
  resolve: (name) =>
    resolvePageComponent(
      `./Pages/${name}.vue`,
      import.meta.glob('./Pages/**/*.vue'),
    ),
  async setup({ el, app, props, plugin }) {
    const i18n = createI18n({
      legacy: false,
      globalInjection: true,
      locale: __locale,
      fallbackLocale: __fallback_locale,
      messages: await loadLocaleMessages(),
    })

    return createApp({ render: () => h(app, props) })
      .use(plugin)
      .use(ZiggyVue, Ziggy)
      .use(i18n)
      .mount(el)
  },
})

InertiaProgress.init({ color: '#4B5563' })

3. Vue ファイルを修正

多言語対応する文字列に翻訳関数 $t() を適用する。
ほとんどの文字列は resources/lang/*.json に定義されている。
resources/js 以下の Vue ファイルはほぼすべて修正が必要。

  • 文字列、タイトル、ボタン名、リンク名
  • ラベル
  • プレースホルダ

※ props に翻訳対象の文字列が入ってくる場合がある

例) resources/js/Jetstream/ConfirmsPassword.vue
<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 タグ)場合は以下のように記述することで翻訳できる。

resources/js/Pages/Auth/Register.vue
<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 引数にバリデーション属性のカスタマイズを指定する。
ここでは __ ヘルパ関数が使える。

例) app/Actions/Fortify/CreateNewUser.php
    // ...

    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();

    // ...

Discussion