HTML5テクニカルノート
Vue.js + Vue I18n: アプリケーションを多言語に対応(国際化)させる
- ID: FN1910002
- Technique: HTML5 / ECMAScript 2015
- Library: Vue.js 2.6.10
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を依存関係に加える
「Vue.js + CLI入門」シリーズと同じようにローカルにプロジェクトをつくったときは、npm install
コマンドでインストールします。そのほかのインストールの仕方については、公式サイトの「Installation」をお読みください。
npm install vue-i18n
そして、JavaScriptコードの側も、モジュールsrc/main.js
でVue.use()
メソッドによりプラグインVueI18n
をインストールしなければなりません。これで、Vue I18nを使う準備が整いました。
src/main.jsimport VueI18n from 'vue-i18n'; Vue.use(VueI18n);
02 ボタンのテキストを日英対応にする
まずは、フッタのボタン[Clear completed]の表記を日本語に切り替えられるようにしましょう。翻訳テキストが収められるオブジェクト(messages
)には、つぎのようにロケールen
とja
をプロパティとして定めます。その値となるオブジェクト(message
)に、翻訳テキストをそれぞれ同じプロバティ(
archive
)で加えればよいのです。
src/main.jsconst messages = { en: { message: { archive: 'Clear completed' } }, ja: { message: { archive: '断捨離' } } };
VueI18n()
コンストラクタの引数オブジェクトには、オプションlocale
にロケール、messages
に前掲翻訳テキストのオブジェクトを与えます。そして、VueI18n
インスタンスは、Vue()
コンストラクタのi18n
オプションに定めてください。これで、アプリケーションの子コンポーネントからVueI18n
の機能が使えるようになるのです。
src/main.jsconst 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■ロケールに応じたテキストがボタンに示される
一旦、ここまでのモジュール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.jsconst 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■翻訳テキストが取り出せない
このような場合の構文について、Vue I18n公式サイトには説明が見当たりませんでした。調べたところ、placeholder
属性はv-bind
(省略記法:
)でバインドしなければならないということです。これで、プレースホルダーのテキストも翻訳されます(図004)。
src/components/TodoInput.vue<template> <input :placeholder="$t('message.placeholder')" > </template>
図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.vueconst 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つのフィルタボタン
05 3つのボタンの表記を多言語対応させる
モジュールsrc/main.js
で、ボタンの翻訳テキストは3つのキーをプロパティにして定めます。
src/main.jsconst 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つのフィルタボタン
アプリケーションのモジュール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.jsconst messages = { en: { message: { remaining: '{count} items left', } }, ja: { message: { remaining: '残り{count}項目', } } };
図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.jsconst messages = { en: { message: { <!-- remaining: '{count} items left', --> remaining: '{count} item left | {count} items left', } }, };
図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>
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.