laravel9.x + inertiajs(vue3) + typescript 多言語化(i18n)の設定備忘録

# 実行環境
・osバージョン
rocky linux 8.7

・composerパッケージのバージョン
laravel 9.48.0
inertiajs/inertia-laravel v0.5.4
laravel-lang/lang 12.11.5
laravel-lang/publisher v14.5.0

・node(npm)パッケージで関連しそうなバージョン
typescript@4.9.4
@inertiajs/inertia-vue3@0.6.0
@inertiajs/inertia@0.11.0
@inertiajs/progress@0.2.7
@vue/compiler-sfc@3.2.45
laravel-mix-vue3@0.7.0
vue-i18n@9.2.2
vue-loader@17.0.1
vue-template-compiler@2.7.14
vue@3.2.45

# 各ファイルの設定
## webpack.mix.js
```js:webpack.mix.js
const mix = require('laravel-mix');
require("laravel-mix-vue3");

/*
 |--------------------------------------------------------------------------
 | Mix Asset Management
 |--------------------------------------------------------------------------
 |
 | Mix provides a clean, fluent API for defining some Webpack build steps
 | for your Laravel applications. By default, we are compiling the CSS
 | file for the application as well as bundling up all the JS files.
 |
 */

mix.ts('resources/js/app.ts', 'public/js')
    .postCss('resources/css/tailwind.css', 'public/css', [require('tailwindcss'), require('autoprefixer')])
    .sass('resources/scss/bootstrap.scss', 'public/css')
    .alias({
        '@': 'resources/js',
    })
    .babelConfig({
        plugins: ['@babel/plugin-syntax-dynamic-import'],
    })
    .webpackConfig({
        stats: {
            children: true
        }
    })
    .sourceMaps()
    .vue();
if (mix.inProduction()) {
    mix.version();
}
```

## tsconfig.json
有効な設定の未抜粋。
```json:tsconfig.json
{
  "compilerOptions": {
    "target": "es2020",
    "module": "commonjs",
    "baseUrl": "./",
    "typeRoots": [
        "./resources/js/@types"
    ],
    "types": [
        "node"
    ],
    "resolveJsonModule": true,
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noImplicitAny": false,
    "skipLibCheck": true
  },
  "include": [
    "./resources/js/**/*"
  ]
}
```
## package.json
scriptsのみ抜粋
```json:package.json
{
    "scripts": {
        "dev": "npm run development",
        "development": "mix",
        "watch": "mix watch",
        "watch-poll": "mix watch -- --watch-options-poll=1000",
        "hot": "mix watch --hot",
        "prod": "npm run production",
        "production": "mix --production"
    },
}
```
# 多言語化(i18n)
## laravel側の設定
```bash:
$ composer require laravel-lang/lang laravel-lang/lang-publisher --dev
$ composer require laravel-lang/attributes laravel-lang/http-statuses --dev
$ php artisan lang:add en ja
```
laravel9から`lang`はプロジェクトの直下に置かれる。
```
./lang
+ en
 +- auth.php
 +- http-statuses.php
 +- pagination.php
 +- passwords.php
 +- validation.php
+ ja
 +- auth.php
 +- http-statuses.php
 +- pagination.php
 +- passwords.php
 +- validation.php
```
デフォルトでないものは各言語ファイルに追加する。

# vue3のフォルダ構成
`resources`配下に`js`,`css`,`scss`,`views`を用意。
## resources/scssとresources/css
前述の`webpack.mix.js`で指定したように
tailwindとbootstrapで別々のcssファイルに生成するようにする。
`scss`にはbootstrap.scssファイルを格納
`css`にはtailwind.cssファイルを格納、
さらにアプリ独自のcssファイルの設定があれば
app.cssなどを用意する。

### resources/scss/bootstrap.scss
```scss:./scss/bootstrap.scss
@import "~bootstrap/scss/bootstrap";
```
### resources/css/tailwind.css
```css:resources/css/tailwind.css
@tailwind base;
@tailwind components;
@tailwind utilities;
```
## resources/views
既存のテンプレートシステム(`*.blade.php`)が格納されているが
vue3ではjs配下に画面＋処理のファイル(`*.vue`)を格納するので
`app.blade.php`のみ使用する。

### resources/views/app.blade.php
```html:resources/views/app.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="csrf-token" content="{{ csrf_token() }}">

        <title inertia>{{ config('app.name', 'Laravel') }}</title>

        <!-- Fonts -->
        <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap">

        <!-- Styles -->
<!--        <link rel="stylesheet" href="{{ asset('css/app.css') }}"> -->
        <link rel="stylesheet" href="{{ asset('css/tailwind.css') }}">

        <!-- Scripts -->
        @routes
        <script src="{{ mix('js/app.js') }}" defer></script>
        @inertiaHead
    </head>
    <body class="font-sans antialiased">
        @inertia

        @env ('local')
<!--            <script src="http://localhost:8080/js/bundle.js"></script> -->
        @endenv
    </body>
</html>
```
`@routes`はvueファイルのscript内やtemplate内でlaravelの`route`関数を使えるようにする指定
`@env('local')`はローカル環境で動作させるとき
内部のサーバ経由にするらしいが余計なお世話なのでコメント化。

## resource/js

### resource/js/bootstrap.js
`larave-echo`と`pusher-js`はブロードキャスト機能の記述のため使わない場合はコメント化
```javascript:resource/js/bootstrap.js
window._ = require('lodash');

try {
    require('bootstrap');
} catch (e) {}

/**
 * We'll load the axios HTTP library which allows us to easily issue requests
 * to our Laravel back-end. This library automatically handles sending the
 * CSRF token as a header based on the value of the "XSRF" token cookie.
 */

window.axios = require('axios');
/**
 * Echo exposes an expressive API for subscribing to channels and listening
 * for events that are broadcast by Laravel. Echo and event broadcasting
 * allows your team to easily build robust real-time web applications.
 */

import Echo from 'laravel-echo';

window.Pusher = require('pusher-js');
// import Pusher from 'pusher-js';

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: process.env.MIX_PUSHER_APP_KEY,
    cluster: process.env.MIX_PUSHER_APP_CLUSTER,
    forceTLS: true,
    wsHost: 'websocketのホスト名',
    wssPort: 443,
});
```

### resource/js/app.ts

laravelの言語ファイル(php/json)をvue3で使うための設定も追記。
`route`に`ziggy-js`を使う。

```typescript:resource/js/app.ts
import './bootstrap';

import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/inertia-vue3';
import { InertiaProgress } from '@inertiajs/progress';
import { createI18n } from 'vue-i18n';
import route from 'ziggy-js';
import 'flowbite';
import * as jaJP from '../../lang/ja.json';
import * as enUS from '../../lang/en.json';

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

createInertiaApp({
    title: (title) => `${title} - ${appName}`,
    resolve: (name) => require(`./Pages/${name}`),
    setup({ el, app, props, plugin }) {
        const setupAsync = async ({ el, app, props, plugin }) => {
            const i18n = createI18n({
                legacy: false,
                globalInjection: true,
                locale: 'ja-JP',
                fallbackLocale: 'en-US',
                messages: {
                    'en-US': enUS,
                    'ja-JP': jaJP
                }
            });
            return createApp({ render: () => h(app, props) })
                .use(plugin)
                .use(i18n)
                .mixin({ methods: { route } })
                .mount(el);
        }
        setupAsync({ el, app, props, plugin });
    },
});

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

さらに以下のフォルダを配置する。
### `resource/js/@types`
typescript用の型定義ファイルを置く。コンパイルやvscodeでのエラー対策。
tsconfig.jsonにて指定する。
```typescript:resource/js/@types/shim-vue.d.ts
declare module '*.vue' {
    import type { DefineComponent } from "vue";
    const component: DefineComponent<{}, {}, any>;
    export default component;
}
```
```typescript:resource/js/@types/global.d.ts
interface Window {
  Echo: any;
  Pusher: any;
}

declare var window: Window;
```

### `resource/js/Components`
画面の各ボタンやロゴやInputなどの部品を格納初期である程度用意されている。
`ApplicationLogo.vue`,`Checkbox.vue`,`DropdownLink.vue`,`InputError.vue`,`NavLink.vue`,`ResponsiveNavLink.vue`,`Button.vue`,`Dropdown.vue`,`Input.vue`,`Label.vue`,`NavMenu.vue`,`ValidationErrors.vue`

### `resource/js/Layouts`
ログインしていない状)用のレイアウトファイル(Guest.vue)とログイン済みユーザー用のレイアウトファイル(Authenticated.vue)
ヘッダー要素と各ページへの指定がされているだけ。

### `resource/js/Pages`
各ページの内容を記載する。
初期はログイン前の画面(`Welcome.vue`)とログイン後の画面(`Dashboard.vue`)と
`Pages/Auth`に認証に必要な画面(ログイン、アカウント作成、パスワードリセット)のみ。
`{{ $t('Dashboard') }}`の個所は後述の多言語化対応

```vue:resource/js/Pages/Dashboard.vue
<script setup lang="ts">
import BreezeAuthenticatedLayout from '@/Layouts/Authenticated.vue';
import { Head, Link } from '@inertiajs/inertia-vue3';
</script>

<template>
    <Head>
        <title>{{ $t('Dashboard') }}</title>
        <meta name="description" content="Dahsboad page description">
        <link rel="shortcut icon" href="/favicon.svg">
    </Head>

    <BreezeAuthenticatedLayout>
        <template #header>
            <h2 class="font-semibold text-xl text-gray-800 leading-tight">
                {{ $t('Dashboard') }}
            </h2>
        </template>

        <div class="py-12">
            <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
                <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                    <div class="p-6 bg-white border-b border-gray-200">
                        {{ $t("You are logged in!") }}
                    </div>
                </div>
            </div>
        </div>
    </BreezeAuthenticatedLayout>
</template>
```
vueファイル内で多言語メッセージを使う場合は`{{ $t('メッセージ') }}`のように使用する。