画面遷移なく js で多言語化する: vite + i18next

サンプルの粒度が分からない

概要

簡単に触ったけど、たぶん案件そのものが流れるからもったいなくて残したかっただけのもの

要件

  • アクセス時は日本語表示
  • 英語/簡体語 のボタンを押下すると、押下したボタンに応じて言語が変更される
  • 日本語 のボタンを押下すると現在のぺージにリダイレクト

奇妙に思われるかもしれないね

scaffold

vite のグローバルインストール

$ sudo npm install -g vite

vite でプロジェクト作成。今回はバニラ(Vanilla)でプレーン js

$ npm create vite

必要なパッケージのインストール。
jquery, bootstrap のバージョンは adminlte 3.1 のサンプルで使用されているバージョンに揃えたもの

$ npm install i18next i18next-http-backend jquery-i18next admin-lte@~3.1 jquery@~3.7 bootstrap@~4.6

// package.json
  "devDependencies": {
    "vite": "^5.4.10"
  },
  "dependencies": {
    "admin-lte": "~3.1",
    "bootstrap": "~4.6",
    "i18next": "^23.16.6",
    "i18next-http-backend": "^2.6.2",
    "jquery": "~3.7",
    "jquery-i18next": "^1.2.1"
  }

開発時は基本的に vite のローカル web サーバを立てて行う。
ホットリロードなしで開発はちょっとね

$ npm run dev

// ビルド: 初期設定なら dist ディレクトリ配下に出力される
$ npm run build

ソース

いろいろファイル追加した後のディレクトリ構成

$ tree .
.
├── dist
├── index.html
├── js
│   ├── app.js
│   ├── core_i18n.js
│   └── index.js
├── locales
│   ├── en
│   │   └── index.json
│   └── zh-CN
│       └── index.json
├── node_modules
├── package-lock.json
└── package.json

index.html の要旨

<div class="btn-group btn-group-toggle" data-toggle="buttons">
    <label class="btn btn-secondary active">
        <input type="radio" name="lang" autocomplete="off" value="jp" checked>日本語
    </label>
    <label class="btn btn-secondary">
        <input type="radio" name="lang" autocomplete="off" value="en">English
    </label>
    <label class="btn btn-secondary">
        <input type="radio" name="lang" autocomplete="off" value="zh-CN">簡体語
    </label>
</div>
<div class="for_translating">
    <span data-i18n="text">日本語</span>
</div>
<script src="./js/app.js" type="module"></script>
<script src="./js/index.js" type="module"></script>

app.js: 全ページで読み込むのを意図したもの。

import jQuery from "jquery";
window.$ = window.jQuery = jQuery;
import "bootstrap";
import "admin-lte";

import "admin-lte/dist/css/adminlte.min.css";

index.js: index.html 用の画面個別 js。
想定では画面で名前空間を割るつもりだったので、名前空間と同義に扱うつもりだったけどなんだかんだあってやめた。
core_i18n からインポートした i18n() をコールすると翻訳が行われる。

import i18n from "./core_i18n.js";

i18n("jp", "index");

core_i18n.js: i18next の初期化処理等を行うコア部分

import i18next from "i18next";
import HttpApi from "i18next-http-backend";
import jqueryI18next from "jquery-i18next";

const defaultLang = "jp";

const i18n = (lang, name) => {
    i18next.use(HttpApi).init(
      {
        debug: true,
        lng: lang,
        fallbackLng: defaultLang,
        load: "currentOnly",
        backend: {
          loadPath: `../locales/{{ lng }}/${name}.json`,
        },
      },
      (err, t) => {
        console.log(err);

        jqueryI18next.init(i18next, $, {
          useOptionsAttr: true,
        });

        document.getElementsByName("lang").forEach((radio) => {
          let value = radio.value;

          if (value == "jp") {
            // 日本語なら現在のパスにリダイレクト
            radio.onchange = () => {
              location.href = location.pathname;
            };
          } else {
            // 指定された言語で翻訳
            radio.onchange = () => {
              i18next.changeLanguage(value).then((t) => {
                $(".for_translating").localize();
              });
            };
          }
        });
      }
    );
};

export default i18n;

技術詳細

i18next の初期化オプション

  • debug: コンソールにログ吐くかどうか
  • lng: 選択した言語
  • fallbackLng: 選択した言語に関する情報がないとかでエラーが起きた場合のフォールバック先言語
  • load: 分かりにくい名前。lng に設定した言語コードの辞書探索ロジックを切り替えるパラメータで、currentOnly なら指定した言語だけを探す
    • lng: "en-US" の場合、デフォルト(all)なら en-US, en, dev を探したりする
  • backend: プラグインオプションの 1 つ。今回の場合は backend に i18next-http-backend を設定しています
  • loadPath: 辞書のパス。i18next のパラメータを変数に指定できるみたいで {{ }} を用いればいいようです

言語を動的に切り替える

i18next.changeLanguage() をコールします。
当該関数は Promise を返却するので .then() でコールバックを記述できます。

今回の実装の場合、ボタンを押下した際に当該関数をコールして対応した辞書を読み込み、コールバックで翻訳してます。

jquery-i18next

単に jquery で翻訳関数である t() をコールするだけのパッケージ。
adminlte の依存先に既に jquery が含まれてるので使ってもいいかなって。別に使わなくても問題ない。
(今や彼も jquery に依存しなくなりましたね)

直せ

今回デフォルト言語として jp をセットしているものの、ディレクトリ構造で分かるように jp ディレクトリは存在しない。
よって i18next もコンソールで当然怒っている。

html に日本語テキストの代わりにキーだけを記述し、locales/jp/index.json にテキストを記述すればいい。
じゃあなぜやらないのか。
めんどくさかったんだよ