【Vue】 vue-i18n カスタムブロック、使ってみたら便利だった
本記事は 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.json
、locales/en.json
などlocales/
にまとめるケース -
vue-i18n
カスタムブロックを利用するケース
はどのように使い分ければいいのでしょうか。考えてみます。
個人的には以下のようなユースケースの場合に適していると考えています。
- 複数の箇所で利用しない文言を UIコンポーネントと合わせて SFC に閉じたい
- feature/ でのみ利用する文言をコンポーネントとセットで実装したい
locales/ja.json
で文言を管理する場合、チーム開発においては以下のような課題も散見されます。
- いつ誰が追加したのか分からない。またどのコンポーネントで利用されているのか分からない
- 文言の追加ルールが明確に定まっていないため、人によって定義方法(ネストのさせ方)が異なる
チーム開発を実施する場合、チーム内で明確なルールを定めていないと無秩序にファイルが肥大化する傾向にあります。
そのため、各コンポーネントやページファイルに紐付けて管理することで変更の容易性が向上します。
また、templateブロック、scriptsブロックと照らし合わせて文言のチェックを実施することが出来るためファイルを飛び回る必要がなくなります。
責務が近いものをまとめることはfeatureディレクトリ構造の採用やscoped css推奨と近しい感覚かと思います。
今回はvue-i18nを採用する前提でユースケースを考えてみました。
しかし、多言語化の要件レベルが高くない場合や日本でしか利用しないWebアプリの場合は無理に採用する必要はないと思います。
そのプロダクトにあった形を採用してもらうのが一番です。
Nuxt環境で導入してみる
実際にNuxt環境で導入実験を行っていきます。
私はNuxt I18nを採用しているのでこちらをベースに実験してみます。
環境情報とnuxt.config.tsの設定
UIライブラリはNuxt UIを使っていますが本題ではないため何を使っても問題ありません。
{
"devDependencies": {
"@nuxt/ui": "^2.18.7",
"@nuxtjs/i18n": "9.1.0",
"nuxt": "^3.14.159",
}
}
export default defineNuxtConfig({
modules: [
'@nuxt/ui',
'@nuxtjs/i18n'
],
i18n: {
defaultLocale: 'ja'
}
});
実際に使ってみる
実際に実験を行ってみます。今回はボタンを押下することでモーダルが開くようなシンプルなUIです。
export default defineAppConfig({
ui: {
primary: 'sky',
icons: {
'open-modal': 'i-heroicons-lock-open-solid',
'close-modal': 'i-heroicons-lock-closed-solid'
}
}
});
今回のコードで変わっているのは<i18n></i18n>
ブロックがあることです。
<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>
コード全体
<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.
公式ドキュメントを見る限りでは eslint-plugin-yml プラグインでチェックできるようです。
eslint-plugin-vue、eslint-plugin-vue-scoped-cssも同様ですが「このプラグインはあるのかな?と思ったものは大体存在しています。開発して頂き本当にありがとうございます🙏
おわりに
今回はvue-i18nのカスタムブロックを試してみました。
チームの運用ルールと相性が良ければ非常に使いやすいなという所感です。個人開発であればi18nカスタムブロックで事足りそうです。
本記事内ではESLintの設定までは実施していません。本件は別記事で試せたらと思います。
Discussion