laravelの認証機構を見てみよう(8) : inertiaの多言語化にはlaravel-react-i18nを使え
ちゅーわけで今回は割と見て見ぬフリをしてきた多言語化であるがreactを組みあわせるとまあまあ面倒くさそうだなーという印象だ。まあ、やってみよう。なお、大抵の場合において多言語化っちゅーのは割と必要無い要件だったりするだろう。そらもうUIをそのまんま日本語で書けば大体満足されるからであるが、まあここはnoteだし、趣味のプログラムなら遠回りしてもいいんじゃないかっつことでね。
現在の状況
現在、バックエンドでは
config/app.php
// 'locale' => 'en',
'locale' => 'ja',
/*
|--------------------------------------------------------------------------
| Application Fallback Locale
|--------------------------------------------------------------------------
|
| The fallback locale determines the locale to use when the current one
| is not available. You may change the value to correspond to any of
| the language folders that are provided through your application.
|
*/
'fallback_locale' => 'en',
このようになっており、jaを直接指定している(まあそれもどうかっちゅー話もあるが)。だからBreadcrumbなどのbackendから送信しているものは実はこの段階でも言語の切り替えが出来る状態だ
routes/breadcrumbs.php
<?php
use Diglactic\Breadcrumbs\Breadcrumbs;
use Diglactic\Breadcrumbs\Generator as BreadcrumbTrail;
use App\Models\User;
Breadcrumbs::for('users.index', function(BreadcrumbTrail $trail)
{
$trail->push(__('Users'), route('users.index')); // <- こことか
});
しかし実際にはたとえばLogin.jsxなどは
export default function Login({ status, canResetPassword }) {
const { data, setData, post, processing, errors, reset } = useForm({
email: '',
password: '',
remember: false,
});
useEffect(() => {
return () => {
reset('password');
};
}, []);
const submit = (e) => {
e.preventDefault();
post(route('login'));
};
return (
<GuestLayout>
<Head title="Log in" />
{status && <div className="mb-4 font-medium text-sm text-green-600">{status}</div>}
<form onSubmit={submit}>
<div>
<InputLabel htmlFor="email" value="Email" />
こんな感じになっており実際この「Log in」とか「Email」とかはbackendと連動していないので、ここ単品ではどうにもできない。
laravel-react-i18nを使う
てかまあ他にもやりようがあるのかもだけど、個人的にはこのパッケージに辿りついている。
installおよび設定
結構ディープだし、これはfrontendでほぼ簡潔するのでlaravelとは異なる設定なので丁寧にやる。まずはnpmである
npm install laravel-react-i18n
そうしたら次にvite.config.jsを変更する
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';
import i18n from 'laravel-react-i18n/vite'; // これを増やして
// 略
plugins: [
laravel({
input: 'resources/js/app.jsx',
refresh: true,
}),
react(),
i18n(), // これを追加する
],
そうしたらガワを変更する
resources/js/app.jsx
import './bootstrap';
import '../css/app.css';
import { createRoot } from 'react-dom/client';
import { createInertiaApp } from '@inertiajs/react';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { LaravelReactI18nProvider } from 'laravel-react-i18n'; // これと
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) => resolvePageComponent(`./Pages/${name}.jsx`, import.meta.glob('./Pages/**/*.jsx')),
setup({ el, App, props }) {
const root = createRoot(el);
root.render(
{/* ↓ このへん */}
<LaravelReactI18nProvider
locale={'ja'}
fallbackLocale={'en'}
files={import.meta.glob('/lang/*.json')}
>
<App {...props} />
</LaravelReactI18nProvider>
{/* ↑ このへん */}
);
},
progress: {
color: '#4B5563',
},
});
これで前準備はok
試しにLogin.jsxを書き換えてみる
resources/js/Pages/Auth/Login.jsx
import { useLaravelReactI18n } from 'laravel-react-i18n';
export default function Login({ status, canResetPassword }) {
const { t, currentLocale } = useLaravelReactI18n();
console.log(currentLocale());
まず冒頭でuseLaravelReactI18nを追加すると同時に
const { t, currentLocale } = useLaravelReactI18n();
これで読みこんできて console.log でcurrentLocale()をまず表示する。そして
「ja」と表示される事を確認する。
次にlang/ja.json を確認する。これは前段でlaravel-langを導入しているというのが前提であるから、入ってなければ入れる必要がある。
で、lang/ja.jsonには
"Log in": "ログイン",
"Log Out": "ログアウト",
"Login": "ログイン",
"Logout": "ログアウト",
"Password": "パスワード",
"Remember me": "ログイン状態を保持する",
"Forgot your password?": "パスワードを忘れた方はこちら",
など、要するにBreeezeのログイン画面の一式くらいのエントリがあるから、これを利用して書き換えてみよう。
<GuestLayout>
<Head title={t("Log in")} />
{status && <div className="mb-4 font-medium text-sm text-green-600">{status}</div>}
<form onSubmit={submit}>
<div>
<InputLabel htmlFor="email" value={t("Email")} />
全部は書かないけど、これくらい書いたら後は何とかなるやろ
ちなみに、こういうのは先述の通りサーバーから送信されてくるので基本的にはこの形式で翻訳している限りではreactはノータッチである。だからある程度英語が残る可能性はあるが、まあこういうのは時間をかけて成熟させていこう。
言語ファイルにないやつ
この辺の画面は流石にあるやつと無いやつがある。とりあえずテーブルヘッダを全部t()にしてみよう
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{t("ID")}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{t("Email")}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{t("Name")}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{t("Roles")}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{t("Last Login")}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{t("Action")}
</th>
ま、こうなる。Rolesとかは翻訳されていないし、Nameが氏名になっているのも、微妙っちゃ微妙かもしれない。
これはja.jsonとかを書き換えちゃえばいいっちゃいい気もするんだけど、その流儀でいいのかというとどうなんかなーという気もする。
でまあ
"Action": "操作",
"Name": "名前",
"Last Login": "最終ログイン",
"Roles": "ロール",
みたいな感じで付け足していく
まあ、あとは単純な奴はこれで増やしていけばいいんだろうと思うが
これも直すとして、たとえば
"Users": "ユーザー",
とか書いてやれば、routes/breadcrumbs.phpが
Breadcrumbs::for('users.index', function(BreadcrumbTrail $trail)
{
$trail->push(__('Users'), route('users.index'));
});
のように __() で書いてるなら、この部分はbackendから送信されてくるから何とかなるってことで何度か書いてるけどフロントでやってるのとバックエンドでやってるのではちょっと変わってくるのでそこが面倒っちゃ面倒かもしれない。
dayjsがtodoになっていた件
export default function UserIndex({ auth, users }) {
const { t, currentLocale } = useLaravelReactI18n();
dayjs.extend(relativeTime);
dayjs.locale('ja'); // TODO ja-fix
これなんかはcurrentLocaleで取得できるので、それを入れてあげればいいかも。
export default function UserIndex({ auth, users }) {
const { t, currentLocale } = useLaravelReactI18n();
dayjs.extend(relativeTime);
const locale = currentLocale();
if (locale) {
dayjs.locale(locale);
}
ただ、これをコンポーネントごとに書いていくのは微妙なのでカスタムフックにした
resources/js/hooks/useConfiguredDayjs.js
import { useLaravelReactI18n } from 'laravel-react-i18n';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import 'dayjs/locale/ja';
export const useConfiguredDayjs = () => {
const { currentLocale } = useLaravelReactI18n();
dayjs.extend(relativeTime);
const locale = currentLocale();
if (locale) dayjs.locale(locale);
return dayjs;
}
これを使う
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link } from '@inertiajs/react';
import { VscVerifiedFilled, VscUnverified, VscInfo } from "react-icons/vsc";
import { useLaravelReactI18n } from 'laravel-react-i18n';
import { useConfiguredDayjs } from '@/hooks/useConfiguredDayjs';
export default function UserIndex({ auth, users }) {
const { t } = useLaravelReactI18n();
const dayjs = useConfiguredDayjs();
resources/js/hooks/useConfiguredDayjs.js のパスはまあある程度ご自由に…
まとめ
t('Welcome, :name!', { name: 'Francisco' }); // Bem-vindo Francisco!
t('Welcome, :NAME!', { name: 'Francisco' }); // Bem-vindo FRANCISCO!
みたいなやつとかは最初に書いたオフィシャルサイトにのっているから、大抵そこで要件が満たせるはずだけど、たとえばユーザーごとに言語をswitchしたいとかいう濃ゆい話はちょっとキツいよね。まあでも工夫次第でできるんじゃねえかなあという気はする。その場合はusersにlocaleカラムがあってもいいのかもしれない。そこはまあおまかせしますってことで。
次回、、、なんかあるっけ?パスワード強度の話とかもあるけどまああとは
ユーザーの作成/更新/削除を管理IFからやる
2FAをやる
ログインしたときの通知とかやる
ユーザーの一括import/exportとかやる
ユーザーのアバターをつける
てかemailじゃなくてusername(Account)ログインにする
oauth認証とかやる
とか考えるだけで色々やれるんだけど、やりすぎてもなーって感じもありますナ。マ、考えます。ここまで記事一本が結構重かったので時間ないときは軽いのも出るかも。あとsurvey.jsの奴に多分統合していきますよ。