サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

Vue.js + Vue I18n: アプリケーションを多言語に対応(国際化)させる


Vue I18nは、Vueアプリケーションを国際化(多言語対応)させるためのプラグインです。ページに表示するテキストを、ロケールに応じて切り替えられます。多言語(日英)対応させるのは、「Vue.js + CLI入門 08: 要素にアニメーションを加える」でつくったつぎのサンプル001にしましょう。Todoリストで、インタフェースを含めたテキストはすべて英語です。これを日本語に切り替えられるようにします。

サンプル001■vue-todo-mvc-08

01 Vue I18nのインストール

CodeSandboxのサンプル001を[Fork]して試す場合には、[Add Dependency]ボタンでvue-i18nを依存関係に加えてください(図001)。

図001■CodeSandboxの[Add Dependency]ボタンでvue-i18nを依存関係に加える

図001

Vue.js + CLI入門」シリーズと同じようにローカルにプロジェクトをつくったときは、npm installコマンドでインストールします。そのほかのインストールの仕方については、公式サイトの「Installation」をお読みください。


npm install vue-i18n

そして、JavaScriptコードの側も、モジュールsrc/main.jsVue.use()メソッドによりプラグインVueI18nをインストールしなければなりません。これで、Vue I18nを使う準備が整いました。

src/main.js

import VueI18n from 'vue-i18n';

Vue.use(VueI18n);

02 ボタンのテキストを日英対応にする

まずは、フッタのボタン[Clear completed]の表記を日本語に切り替えられるようにしましょう。翻訳テキストが収められるオブジェクト(messages)には、つぎのようにロケールenjaをプロパティとして定めます。その値となるオブジェクト(message)に、翻訳テキストをそれぞれ同じプロバティ(archive)で加えればよいのです。

src/main.js

const messages = {
	en: {
		message: {
			archive: 'Clear completed'
		}
	},
	ja: {
		message: {
			archive: '断捨離'
		}
	}
};

VueI18n()コンストラクタの引数オブジェクトには、オプションlocaleにロケール、messagesに前掲翻訳テキストのオブジェクトを与えます。そして、VueI18nインスタンスは、Vue()コンストラクタのi18nオプションに定めてください。これで、アプリケーションの子コンポーネントからVueI18nの機能が使えるようになるのです。

src/main.js

const i18n = new VueI18n({
	locale: 'ja',
	messages,
});
new Vue({
	i18n,

}).$mount('#app');

コンポーネントは、翻訳したテキストを取り出すメソッド$t()が呼び出せるようになりました。コンポーネントsrc/components/TodoController.vueのテンプレートで、ボタンテキストをつぎのように$t()メソッドの呼び出しに書き替えれば、VueI18nインスタンスに定めたロケールのテキストが示されます(図002)。なお、メソッドの引数は、翻訳テキストのロケールからあとのプロパティを示すパスの文字列("message.archive")です。

src/components/TodoController.vue

<template>
	<footer class="footer" v-show="todos.length" v-cloak>

		<button class="clear-completed"

			>
			<!-- Clear completed -->
			{{$t("message.archive")}}
		</button>
	</footer>
</template>

図002■ロケールに応じたテキストがボタンに示される

図002

一旦、ここまでのモジュールsrc/main.jsのJavaScriptコードを、つぎにまとめておきましょう(コード001)。

コード001■VueアプリケーションにVueI18nの機能を組み込む

src/main.js

import Vue from 'vue'
import VueI18n from 'vue-i18n'
import App from './App.vue'

Vue.config.productionTip = false
Vue.use(VueI18n)

const messages = {
	en: {
		message: {
			archive: 'Clear completed'
		}
	},
	ja: {
		message: {
			archive: '断捨離'
		}
	}
};
const i18n = new VueI18n({
	locale: 'ja',
	messages,
});
new Vue({
	i18n,
	render: h => h(App),
}).$mount('#app');

03 プレースホルダーに多言語テキストを表示する

つぎは、入力フィールドのプレースホルダーに多言語テキストを表示してみましょう。翻訳テキストのオブジェクトのロケールに、それぞれのプロパティ(placeholder)とテキストを加えます。

src/main.js

const messages = {
	en: {
		message: {
			placeholder: 'What needs to be done?',

		}
	},
	ja: {
		message: {
			placeholder: '予定の項目を入力',

		}
	}
};

属性(placeholder)の値には、二重波かっこ{{}}はつけません。ところが、コンポーネントsrc/components/TodoInput.vueのテンプレートをつぎのように書き替えると、式がそのまま表示されて、翻訳テキストが取り出せないようです(図003)。

src/components/TodoInput.vue

<template>
	<input

		placeholder="$t('message.placeholder')"

		>
</template>

図003■翻訳テキストが取り出せない

図003

このような場合の構文について、Vue I18n公式サイトには説明が見当たりませんでした。調べたところ、placeholder属性はv-bind(省略記法:)でバインドしなければならないということです。これで、プレースホルダーのテキストも翻訳されます(図004)。

src/components/TodoInput.vue

<template>
	<input

		:placeholder="$t('message.placeholder')"

		>
</template>

図004■プレースホルダーに翻訳テキストが表示された

図004

コンポーネントsrc/components/TodoInput.vueには、これ以上手は加えません。つぎのコード002にまとめましょう。

コード002■入力フィールドのプレースホルダーを多言語化する

src/components/TodoInput.vue

<template>
	<input
		class="new-todo" autofocus autocomplete="off"
		:placeholder="$t('message.placeholder')"
		v-model="newTodo"
		@keypress.enter="addTodo">
</template>

<script>
export default {
	name: 'TodoInput',
	data() {
		return {
			newTodo: ''
		};
	},
	methods: {
		addTodo() {
			this.$emit('add-todo', this.newTodo);
			this.newTodo = '';
		}
	}
}
</script>

04 3つのボタンをv-forディレクティブで加える

フッタにある3つのフィルタボタンも多言語化します。ただその前に、コンポーネントsrc/components/TodoController.vueのコードを少し整理しましょう。プロパティvisibilityがもつのは、選ばれたフィルタを示すキーとなる値です。これは、<a>要素のリンク(href属性)やボタンのテキストにも用いられています。

src/components/TodoController.vue

<template>
	<footer class="footer" v-show="todos.length" v-cloak>

		<ul class="filters">
			<li><a href="#/all"
			:class="{selected: visibility === 'all'}">
			All</a></li>
			<li><a href="#/active"
			:class="{selected: visibility === 'active'}">
			Active</a></li>
			<li><a href="#/completed"
			:class="{selected: visibility === 'completed'}">
			Completed</a></li>
		</ul>

	</footer>
</template>

さらにアプリケーションモジュールsrc/App.vueで、フィルタのメソッドを定めたオブジェクト(filters)のメソッド名とも一致します。これらの値や名前はすべて手入力です。それなら、フィルタのオブジェクトからメソッド名を取り出して、フッタのコンポーネントに用いるのが効率的でしょう。

src/App.vue

const filters = {
	all(todos) {

	},
	active(todos) {

	},
	completed(todos) {

	}
};

そこで、アプリケーションモジュールsrc/App.vueから子コンポーネント(todo-controller)に、フィルタのオブジェクト(filters)をバインドします。

src/App.vue

<template>
	<section id="app" class="todoapp">

		<todo-controller

			:filters="filters"

		</todo-controller>
	</section>
</template>

<script>
export default {

	data() {
		return {

			filters: filters
		}
	},

}
</script>

これで、v-forディレクティブによりフィルタオブジェクト(filters)からメソッド名(key)を取り出して、<a>が設定できるようになりました。ただし、ボタン表記の頭文字が小文字です(図005)。これは、このあと翻訳テキストにより変更します。

src/components/TodoController.vue

<template>
	<footer class="footer" v-show="todos.length" v-cloak>

		<ul class="filters">
			<!-- <li><a href="#/all"
			:class="{selected: visibility === 'all'}">
			All</a></li>
			<li><a href="#/active"
			:class="{selected: visibility === 'active'}">
			Active</a></li>
			<li><a href="#/completed"
			:class="{selected: visibility === 'completed'}">
			Completed</a></li> -->
			<li
				v-for="(value, key) in filters"
				:key="key"
			>
				<a
					:href="'#/' + key"
					:class="{selected: visibility === key}"
				>
					{{key}}
				</a>
			</li>
		</ul>

	</footer>
</template>

<script>
export default {

	props: {

		filters: Object
	},

}
</script>

図005■v-forディレクティブで加えられた3つのフィルタボタン

図005

05 3つのボタンの表記を多言語対応させる

モジュールsrc/main.jsで、ボタンの翻訳テキストは3つのキーをプロパティにして定めます。

src/main.js

const messages = {
	en: {
		message: {

			all: 'All',
			active: 'Active',
			completed: 'Completed',

		}
	},
	ja: {
		message: {

			all: 'すべて',
			active: '未処理',
			completed: '処理済み',

		}
	}
};

問題はコンポーネントsrc/components/TodoController.vueのテンプレートです。$t()メソッドの引数に、変数(key)をどのように加えたらよいでしょうか。Vue I18nのドキュメントには説明が見当たりませんでした。メソッドの引数は文字列とされており、つぎのように文字列でパスを構成すればよいようです。これで、3つのボタンに翻訳テキストが表示されます(図006)。

src/components/TodoController.vue

<template>
	<footer class="footer" v-show="todos.length" v-cloak>

		<ul class="filters">

			<li

			>
				<a

				>
					<-- {{key}} -->
					{{$t("message." + key)}}
				</a>
			</li>
		</ul>

	</footer>
</template>

図006■日本語で表示された3つのフィルタボタン

図006

アプリケーションのモジュールsrc/App.vueには、もう手を加えません。つぎのコード003にまとめましょう。

コード003■アプリケーションモジュール

src/App.vue

<template>
	<section id="app" class="todoapp">
		<header class="header">
			<transition appear name="todo-head">
				<h1>todos</h1>
			</transition>
			<todo-input
				@add-todo="addTodo">
			</todo-input>
		</header>
		<todo-list
			:todos="todos"
			:filtered-todos="filteredTodos"
			:allDone="allDone"
			@remove-todo="removeTodo"
			@done="done"
			@allDone="onAllDone">
		</todo-list>
		<todo-controller
			:todos="todos"
			:remaining="remaining"
			:visibility="visibility"
			:filters="filters"
			@removeCompleted="removeCompleted">
		</todo-controller>
	</section>
</template>

<script>
import TodoInput from './components/TodoInput.vue';
import TodoList from './components/TodoList.vue';
import TodoController from './components/TodoController.vue';

const STORAGE_KEY = 'todos-vuejs-2.6';
const todoStorage = {
	fetch() {
		const todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
		todos.forEach(function(todo, index) {
			todo.id = index;
		});
		todoStorage.uid = todos.length;
		return todos;
	},
	save(todos) {
		localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
	}
};
const filters = {
	all(todos) {
		return todos;
	},
	active(todos) {
		return todos.filter((todo) =>
			!todo.completed
		);
	},
	completed(todos) {
		return todos.filter((todo) =>
			 todo.completed
		);
	}
};
export default {
	name: 'app',
	components: {
		TodoInput,
		TodoList,
		TodoController
	},
	data() {
		return {
			todos: todoStorage.fetch(),
			visibility: 'all',
			filters: filters
		}
	},
	computed: {
		filteredTodos() {
			return filters[this.visibility](this.todos);
		},
		remaining() {
			const todos = filters.active(this.todos);
			return todos.length;
		},
		allDone: {
			get() {
				return this.remaining === 0;
			},
			set(value) {
				this.todos.forEach((todo) =>
					todo.completed = value
				);
			}
		}
	},
	watch: {
		todos: {
			handler(todos) {
				todoStorage.save(todos);
			},
			deep: true
		}
	},
	mounted() {
		window.addEventListener('hashchange', this.onHashChange);
	},
	methods: {
		addTodo(todoTitle) {
			const newTodo = todoTitle && todoTitle.trim();
			if (!newTodo) {
				return;
			}
			this.todos.push({
				id: todoStorage.uid++,
				title: newTodo,
				completed: false
			});
		},
		removeTodo(todo) {
			this.todos = this.todos.filter((item) => item !== todo);
		},
		done(todo, completed) {
			todo.completed = completed;
		},
		onHashChange() {
			const visibility = window.location.hash.replace(/#\/?/, '');
			this.visibility = visibility;
		},
		onAllDone(done) {
			this.allDone = done;
		},
		removeCompleted() {
			this.todos = filters.active(this.todos);
		}
	}
}
</script>

<style>
@import url("https://unpkg.com/todomvc-app-css@2.2.0/index.css");
</style>

<style scoped>
.todo-head-enter-active {
	transition: 1s;
}
.todo-head-enter {
	opacity: 0;
	transform: translateY(-40px);
}
</style>

06 翻訳テキストにテンプレートから変数値を差し込む

フッタで残るは、処理済み項目数を示すテキストです。日本語のとき「残り○項目」と表示するにはどうするか考えましょう。テキストをふたつに分けて、算出プロパティの数値をはさむことはできます[*01]。けれど、$t()メソッドは、翻訳テキストに変数を送ることもできるのです。メソッドの第2引数に、つぎのようにオブジェクトでプロパティと値を渡してください。

src/components/TodoController.vue

<template>
	<footer class="footer" v-show="todos.length" v-cloak>
		<span class="todo-count">
			<!-- <strong>{{remaining}}</strong> {{remaining | pluralize}} left -->
			{{$t("message.remaining", {count: remaining})}}
		</span>

	</footer>
</template>

<script>
export default {

	/* filters: {
		pluralize(n) {
			return n === 1 ? 'item' : 'items';
		}
	}, */

}
</script>

翻訳テキストの側は、文字列に波かっこ{}でプロバティ名を加えることにより値が受け取れるのです。これで、変数値の差し込まれた翻訳テキストが表示されます(図007)。

src/main.js

const messages = {
	en: {
		message: {

			remaining: '{count} items left',

		}
	},
	ja: {
		message: {

			remaining: '残り{count}項目',

		}
	}
};

図007■変数値の差し込まれた翻訳テキストが表示される

図007

[*01] もとのサンプル001では、数値(算出プロパティ)の値は<strong>要素に加えられていました。この構成を保つなら、テキストをふたつに分けるのがよいでしょう。けれど、今回の作例では<strong>要素に表現上の違いが見られません。そのため、テキストはひとつとします。

07 翻訳テキストを単数と複数で切り替える

処理済み項目数について、もうひとつ英語テキストに加えなければならない機能が残っています。単数と複数の単語切り替えです。これには、$tc()メソッドを用います。送る変数のオブジェクトは第3引数に移し、第2引数に渡すのが単数・複数を決める値です。

src/components/TodoController.vue

<template>
	<footer class="footer" v-show="todos.length" v-cloak>
		<span class="todo-count">
			<!-- {{$t("message.remaining", {count: remaining})}} -->
			{{$tc("message.remaining", remaining, {count: remaining})}}
		</span>

	</footer>
</template>

翻訳テキストはパイプ|の左辺に単数、右辺に複数の文字列をそれぞれ定めるだけです。これで、英語テキストの単数と複数の表示が切り替わります(図008)。詳しい構文については「Pluralization」をお読みください。

src/main.js

const messages = {
	en: {
		message: {

			<!-- remaining: '{count} items left', -->
			remaining: '{count} item left | {count} items left',

		}
	},

};

図008■英語テキストの単数形と複数形が切り替わる

図008

コンポーネントsrc/components/TodoController.vueの多言語対応も済みました。モジュールsrc/main.jsと併せて、以下にコード全体をまとめます(コード004)。また、前掲サンプル001を多言語対応に書き替えて、以下のサンプル002に掲げました。

コード004■テキストへの変数値の差し込みと単数・複数の切り替え

src/components/TodoController.vue

<template>
	<footer class="footer" v-show="todos.length" v-cloak>
		<span class="todo-count">
			{{$tc("message.remaining", remaining, {count: remaining})}}
		</span>
		<ul class="filters">
			<li
				v-for="(value, key) in filters"
				:key="key"
			>
				<a
					:href="'#/' + key"
					:class="{selected: visibility === key}"
				>
					{{$t("message." + key)}}
				</a>
			</li>
		</ul>
		<button class="clear-completed"
			v-show="todos.length > remaining"
			@click="removeCompleted">
			{{$t("message.archive")}}
		</button>
	</footer>
</template>

<script>
export default {
	name: 'TodoController',
	filters: {
		pluralize(n) {
			return n === 1 ? 'item' : 'items';
		}
	},
	props: {
		todos: Array,
		remaining: Number,
		visibility: String,
		filters: Object
	},
	methods: {
		removeCompleted() {
			this.$emit('removeCompleted');
		}
	}
}
</script>

src/main.js

import Vue from 'vue'
import VueI18n from 'vue-i18n'
import App from './App.vue'

Vue.config.productionTip = false
Vue.use(VueI18n)

const messages = {
	en: {
		message: {
			placeholder: 'What needs to be done?',
			remaining: '{count} item left | {count} items left',
			all: 'All',
			active: 'Active',
			completed: 'Completed',
			archive: 'Clear completed'
		}
	},
	ja: {
		message: {
			placeholder: '予定の項目を入力',
			remaining: '残り{count}項目',
			all: 'すべて',
			active: '未処理',
			completed: '処理済み',
			archive: '断捨離'
		}
	}
};
const i18n = new VueI18n({
	locale: 'en',
	messages,
});
new Vue({
	i18n,
	render: h => h(App),
}).$mount('#app');

サンプル002■vue-i18n-todo-mvc


作成者: 野中文雄
作成日: 2019年10月08日


Copyright © 2001-2019 Fumio Nonaka.  All rights reserved.