🌍

【Vue】 vue-i18n カスタムブロック、使ってみたら便利だった

2024/12/18に公開

本記事は Vue Advent Calendar 2024 の18日目の記事です。

はじめに

こんにちは、がんがんです。

Vue の単一ファイルコンポーネント(以下、SFC 構文)といえば以下の 3 つを想像されるかと思います。

  • <template>
  • <script>
  • <style>

<docs>など一部のカスタムブロックを Vue2 時代に使ったことありますが、私も主にその 3 つしかないものだと思ってました。しかし、Vue のドキュメント(日本語)を徘徊すると以下のような記述を発見しました。

カスタムブロック ​

プロジェクト固有のニーズに応じて、\*.vue ファイルに <docs> ブロックのような追加のカスタムブロックを含めることができます。カスタムブロックの実際の例としては、以下のようなものがあります:

え、vue-i18n ってカスタムブロックいけるんですか?これは便利そう...
本記事では README.md を眺めながら vue-i18n カスタムブロックを試してみたいと思います。

README を読んでみる

まずは README.md を読んでみます(以下、README からの引用です)。

<template>
  <form>
    <label>{{ t('language') }}</label>
    <select v-model="locale">
      <option value="en">en</option>
      <option value="ja">ja</option>
    </select>
  </form>
  <p>{{ t('hello') }}</p>
</template>

<script>
import { useI18n } from 'vue-i18n'

export default {
  name: 'App',
  setup() {
    const { locale, t } = useI18n({
      inheritLocale: true
    })

    return { locale, t }
  }
}
</script>

<i18n>
{
  "en": {
    "language": "Language",
    "hello": "hello, world!"
  },
  "ja": {
    "language": "言語",
    "hello": "こんにちは、世界!"
  }
}
</i18n>

<i18n>カスタムブロック内で文言の設定を行えています。非常に使い勝手良さそうですね。
<i18n>ブロックも<script>ブロック同様にlangオプションがありました。デフォルトの json 以外にも yaml なども選べるようです。いいですね。

  • json (default)
  • yaml
  • yml
  • json5

言語ハイライトを当てたい場合はlang="json"があった方が良さそうです。

実際のユースケースを考えてみる

この<i18n>カスタムブロック、使い方によってはかなり便利に使えそうです。では、

  • locales/ja.jsonlocales/en.jsonなどlocales/にまとめるケース
  • vue-i18nカスタムブロックを利用するケース

はどのように使い分ければいいのでしょうか。考えてみます。


個人的には以下のようなユースケースの場合に適していると考えています。

  • 複数の箇所で利用しない文言を UIコンポーネントと合わせて SFC に閉じたい
  • feature/ でのみ利用する文言をコンポーネントとセットで実装したい

locales/ja.jsonで文言を管理する場合、チーム開発においては以下のような課題も散見されます。

  1. いつ誰が追加したのか分からない。またどのコンポーネントで利用されているのか分からない
  2. 文言の追加ルールが明確に定まっていないため、人によって定義方法(ネストのさせ方)が異なる


チーム開発を実施する場合、チーム内で明確なルールを定めていないと無秩序にファイルが肥大化する傾向にあります。
そのため、各コンポーネントやページファイルに紐付けて管理することで変更の容易性が向上します。

また、templateブロック、scriptsブロックと照らし合わせて文言のチェックを実施することが出来るためファイルを飛び回る必要がなくなります。
責務が近いものをまとめることはfeatureディレクトリ構造の採用やscoped css推奨と近しい感覚かと思います。


今回はvue-i18nを採用する前提でユースケースを考えてみました。
しかし、多言語化の要件レベルが高くない場合や日本でしか利用しないWebアプリの場合は無理に採用する必要はないと思います。

そのプロダクトにあった形を採用してもらうのが一番です。

Nuxt環境で導入してみる

実際にNuxt環境で導入実験を行っていきます。
私はNuxt I18nを採用しているのでこちらをベースに実験してみます。

環境情報とnuxt.config.tsの設定

UIライブラリはNuxt UIを使っていますが本題ではないため何を使っても問題ありません。

package.json(抜粋)
{
  "devDependencies": {
    "@nuxt/ui": "^2.18.7",
    "@nuxtjs/i18n": "9.1.0",
    "nuxt": "^3.14.159",
  }
}
nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@nuxt/ui',
    '@nuxtjs/i18n'
  ],
  i18n: {
    defaultLocale: 'ja'
  }
});

実際に使ってみる

実際に実験を行ってみます。今回はボタンを押下することでモーダルが開くようなシンプルなUIです。

app.config.ts
export default defineAppConfig({
  ui: {
    primary: 'sky',
    icons: {
      'open-modal': 'i-heroicons-lock-open-solid',
      'close-modal': 'i-heroicons-lock-closed-solid'
    }
  }
});

今回のコードで変わっているのは<i18n></i18n>ブロックがあることです。

pages/test.vue の一部
<script setup lang="ts">
const { t } = useI18n();

// t() を用いることで文言を取得している
const columns: Column[] = [
  { key: 'name', label: t('table.name') },
  { key: 'email', label: t('table.email') },
  { key: 'phone', label: t('table.phone') }
];

// 引数を利用したケースも利用可能
const rows: Rows[] = [...Array(10)].map((v, i) => {
  return {
    id: i + 1,
    name: t('dummy.name', { target: i + 1 }),
    email: t('dummy.email'),
    phone: t('dummy.phone', { target: String(i + 1).padStart(2, '0') })
  };
});
</script>

<tempalte>
  <!-- 中略 -->
  <!-- template内でも取得できています -->
  <UButton
    :label="t('button')"
    :icon="isOpen ? appConfig.ui.icons['open-modal'] : appConfig.ui.icons['close-modal']"
    @click="isOpen = true"
  />
  <!-- 中略 -->
</template>

<i18n lang="yaml">
ja:
  button: ダイアログを開く
  modal:
    title: テスト用モーダル😄
  table:
    name: 名前
    email: メールアドレス
    phone: 電話番号
  dummy:
    name: テストユーザー{target}
    email: example{'@'}example.com
    phone: 090-1234-56{target}
</i18n>
コード全体
pages/test.vue
<script setup lang="ts">
import type { TableRow } from '#ui/types';

interface Rows {
  id: number;
  name: string;
  email: string;
  phone: string;
}
interface Column extends TableRow {
  key: keyof Rows;
  label: string;
}

const { t } = useI18n();
const appConfig = useAppConfig();

const columns: Column[] = [
  { key: 'name', label: t('table.name') },
  { key: 'email', label: t('table.email') },
  { key: 'phone', label: t('table.phone') }
];

const rows: Rows[] = [...Array(10)].map((v, i) => {
  return {
    id: i + 1,
    name: t('dummy.name', { target: i + 1 }),
    email: t('dummy.email'),
    phone: t('dummy.phone', { target: String(i + 1).padStart(2, '0') })
  };
});

const isOpen = ref(false);
</script>

<template>
  <UContainer class="pt-16">
    <UButton
      :label="t('button')"
      :icon="isOpen ? appConfig.ui.icons['open-modal'] : appConfig.ui.icons['close-modal']"
      @click="isOpen = true"
    />
    <UModal v-model="isOpen">
      <UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
        <template #header>
          <div class="flex items-center justify-between">
            <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
              {{ t('modal.title') }}
            </h3>
            <UButton
              color="gray"
              variant="ghost"
              icon="i-heroicons-x-mark-20-solid"
              class="-my-1"
              @click="isOpen = false"
            />
          </div>
        </template>
        <UTable
          :columns
          :rows
        />
      </UCard>
    </UModal>
  </UContainer>
</template>

<i18n lang="yaml">
ja:
  button: ダイアログを開く
  modal:
    title: テスト用モーダル😄
  table:
    name: 名前
    email: メールアドレス
    phone: 電話番号
  dummy:
    name: テストユーザー{target}
    email: example{'@'}example.com
    phone: 090-1234-56{target}
</i18n>

ダイアログを開くボタン

ダイアログを開いた様子

Q. i18n カスタムブロック内のチェックはどうすれば良いのか?

i18n カスタムブロックは非常に便利だからこそESLintでチェックできると嬉しいです。
特に当ても無く eslint-plugin-vue-i18nのドキュメントを見ていると以下のような記述を見つけました。

More lint on JSON and YAML in <i18n> block
You can install eslint-plugin-jsonc and eslint-plugin-yml. These 2 plugins support Vue custom blocks.

You can also use jsonc/vue-custom-block/no-parsing-error and yml/vue-custom-block/no-parsing-error rules to find JSON and YAML parsing errors.

https://eslint-plugin-vue-i18n.intlify.dev/started.html#more-lint-on-json-and-yaml-in-i18n-block

公式ドキュメントを見る限りでは eslint-plugin-yml プラグインでチェックできるようです。
eslint-plugin-vue、eslint-plugin-vue-scoped-cssも同様ですが「このプラグインはあるのかな?と思ったものは大体存在しています。開発して頂き本当にありがとうございます🙏

おわりに

今回はvue-i18nのカスタムブロックを試してみました。
チームの運用ルールと相性が良ければ非常に使いやすいなという所感です。個人開発であればi18nカスタムブロックで事足りそうです。

本記事内ではESLintの設定までは実施していません。本件は別記事で試せたらと思います。

Vue・Nuxt 情報が集まる広場 / Plaza for Vue・Nuxt.

Discussion