【i18n-js】Railsアプリのi18n(tメソッド)をVue.jsでも使用する

Railsアプリ開発の際、フロントをVue.jsで書いているのですが、
「i18nをフル活用し、ソースコードに日本語のベタ書きは限りなくゼロにしていこう！」
とした場合、
Rails側で使える`t('create')`のような**tメソッド**を
**「Vue.jsでも使いたい場合どうすればいいの」**
となったので実装の記録を書き残します。


#前提
- macOS Mojave 10.14.6
- Ruby 2.5.5
- Rails 5.2.3
- Vue 2.6.10
- webpacker 4.0.7

※ ディレクトリ構成はフロントエンドとバックエンドが分離されているものではなく、Railsアプリケーション上にWebpackerを用いてVue.jsを使用可能な環境としています。予めご了承ください。

#gem「i18n-js」を導入
```:Gemfile
gem 'i18n-js'
```

```:terminal
$ bundle install
```

JavaScriptでI18nを利用する場合、`config/locales/ja.yml`を直接使える訳ではなく、`.js`に変換してそれを読み込んで利用します。
i18n-jsはその変換出力のためのgemです。

#i18n-js.ymlを作成
どのように変換出力するかのルールを設定するファイルである`i18n-js.yml`を作成します。
以下のコマンドで`configディレクトリ`に新規作成されます。

```:terminal
rails generate i18n:js:config
```

```config/i18n-js.yml
translations:
- file: "app/javascript/packs/i18n/%{locale}.js"
  prefix: "import I18n from './i18n';\n"
```

**fileオプション**で変換出力する場所を指定するパスと、言語別に出力されるよう設定します。
（公式などを見ると一般的に使われる設定ではtranslation.jsという１つのファイルに全ての言語の設定を詰め込むみたいですが、ファイルが重くなってしまうため今回は言語別にしています。）

webpackerを導入しているので、**app/javascript/packs**配下に**i18nディレクトリ**を作成してその中で管理することとします。

さらに**prefixオプション**をつけると生成されるjsファイルの先頭に好きな文字列を追加できます。
付けた意味は後述します。　


#許可するlocaleを限定
後述する`rake i18n:js:export`をこのまま実行してしまうと、許可されている全ての言語のjsファイルが生成されてしまいます。（私の場合90個ぐらいでした:imp:）
日本で開発されている方であれば、日本語と英語だけで十分かと思いますので今回はその2言語のみ生成されるようにします。

```config/application.rb
# デフォルトのlocaleを日本語(:ja)にする
config.i18n.default_locale = :ja

# 許可するlocaleを日本語(:ja)と英語(:en)に限定する
config.i18n.available_locales = %i[ja en]
```

これで`ja.yml`は`ja.js`に、`en.yml`は`en.js`に変換出力されるようになります。

#rake i18n:js:exportで出力
gem「i18n-js」が用意するjs用localeファイルに変換するためのコマンドを実行します。

```:terminal
$ rake i18n:js:export
```

先ほどi18n-js.ymlファイルにfileオプションで指定したパスに対して出力されます

```config/i18n-js.yml
translations:
- file: "app/javascript/packs/i18n/%{locale}.js" # ←これ
  prefix: "import I18n from './i18n';\n"
```

<img width="250" alt="スクリーンショット 2020-04-03 11.44.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358307/4a60e180-bebb-eec7-a137-21dcca3b7d02.png">

`ja.js`と`en.js`は確認できましたか？

```app/javascript/packs/i18n/ja.js
import I18n from './i18n';
I18n.translations || (I18n.translations = {});
I18n.translations["ja"] = I18n.extend((I18n.translations["ja"] || {}), {"403_forbidden":"ページを表示できません", ......（中略）..... ,"zip":"郵便番号"});
```

中を見てみると
1行目でi18n-js.ymlファイルの**prefixオプション**で付け加えたものがあります。
3行目でRailsのi18nで使っている**ja.ymlファイル**の中身が1行に凝縮されています。
最終的に`tメソッド`はこの中からマッチするものを探してきます。

#i18n.jsを編集

同時に`public/javascript/`に`i18n.js`も出来ました。
機能の根幹となるメソッドがたくさん書いてあるファイルです。

この中から`defaultLocale`と`locale`を必要に応じて変更します。
railsのgem`i18n`とは別物なので、別途定義が必要です。
<img width="651" alt="スクリーンショット 2019-11-01 10.45.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358307/bebf323e-ad6b-0182-e4d1-20d1809400da.png">
日本語であれば`"ja"`ですね。

#i18n.jsを移動

今編集した`public/javascript/i18n.js`を`app/javascript/i18n/i18n.js`に移動してください。
<img width="195" alt="スクリーンショット 2020-04-03 11.45.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358307/ee1d012b-9161-a75c-c9a6-fd38e71cf7dc.png">
~~`rake i18n:js:export`コマンドを実行すると、毎回初期化されたi18n.jsが`public/packs`に作成されてしまうため、中身を変更したi18n.jsは別の場所に移動しなければいけないのです。（本来そのような必要はないかもしれませんが解決策がわからずこのように運用しています。）~~

**〜（2021/10/13 追記）〜**
コメント欄にてi18n-jsが新たに作成されない方法を教えていただきました！

```config/i18n-js.yml
translations:
- file: "app/javascript/packs/i18n/%{locale}.js"
  prefix: "import I18n from './i18n';\n"

export_i18n_js: false　# ←これを追記
```

**〜（追記終わり）〜**

今回はja.jsたちと同じ場所に移動することで、管理しやすくします。

そうすることで**prefixオプション**でつけたja.jsの1行目の記述が活きてきます。

```app/javascript/packs/i18n/ja.js
import I18n from './i18n';
I18n.translations || (I18n.translations = {});
I18n.translations["ja"] = I18n.extend((I18n.translations["ja"] || {}), {"403_forbidden":"ページを表示できません", ......（中略）..... ,"zip":"郵便番号"});
```

2行目からI18nというモジュールを呼び出そうとしていますが、importしていないものは呼び出すことができないので、先にimportさせているのです。
同じディレクトリに置いたのでパスも相対パスでOK。

# webpackに%{locale}.jsを読み込ませる

方法は2通りあります。

## 1.エントリーポイントで%{locale}.jsファイルをimport

```application.js
// 〜（中略）〜
import './i18n/ja'
```

webpackerのエントリーポイントで各localeごとのjsファイルをimportします。（enが必要な方はenもimportしてください）
webpackはimportしたファイルの中でさらにimportされているものは芋づる式にバンドル（まとめる）対象になるので、ここではメソッド本体である`i18n.js`はimportしなくて大丈夫です。

## 2.各Vueコンポーネント内でimport

```sample.vue
<template>
</template>

<script>
import I18n from "../i18n/i18n";
import "../i18n/ja";

export default {
}
</script>
```

この方法はVueコンポーネントがを作る度に書き加えないといけないので面倒なのですが、
そもそもI18nを使いたいVueコンポーネントが全Vueファイルの中で一部しかない場合などはこの方法でいいでしょう。

先ほど1番では本体である`import I18n from "../i18n/i18n";`は呼び出す必要はないと言いましたが、
後述する**[おまけ2:script内でもI18nを使いたい場合は？](https://qiita.com/yosaprog/items/3b2c509cf5de94f89cdb#%E3%81%8A%E3%81%BE%E3%81%912script%E5%86%85%E3%81%A7%E3%82%82i18n%E3%82%92%E4%BD%BF%E3%81%84%E3%81%9F%E3%81%84%E5%A0%B4%E5%90%88%E3%81%AF)**の中で
`'I18n' is not defined`
とLinterに怒られてしまう可能性があるため、便宜的に本体も呼び出しています。

（※2通りの方法を紹介しましたが、以下こちらの2番の方法で記述を進めていきます）

#consoleでI18nコマンドが使えるか確認

現時点でI18nが使えるかどうか、
rails serverを立ち上げてローカルでRailsアプリにアクセスします。
（2番の方法の場合、I18nをimportしてある.vueファイルが使われているページである必要があります。）
chromeのデベロッパーツールで画像のように打ち込んで確認してみましょう。
<img width="428" alt="スクリーンショット 2019-11-01 10.35.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358307/dc892a9a-25f4-b2ed-a9e0-301f5ffe0ab2.png">
2つ目はご自身のアプリで定義してあるものを適当に呼び出してみてくださいね。

#vueファイルでI18nコマンドを省略するためのmixinを作成

今のままでは`I18n.t('hoge')`のように毎回先頭に`I18n`を付けないと呼び出せないので面倒です。
そこで`mixin`を作成し、`t('hoge')`と省略形で呼び出せるようにします。

```app/javascript/packs/mixins/i18n.js
export const i18n = {
  methods: {
    t: (...args) => I18n.t(...args),
    currentLocale: () => I18n.currentLocale()
  }
};
```


#コンポーネントでmixinをimport

```vue:sample.vue
<template>
</template>

<script>
import I18n from "../i18n/i18n";
import "../i18n/ja";
import { i18n } from "../mixins/i18n";

export default {
  mixins: [i18n]
}
</script>
```

これでvueファイル内で呼び出す`t('hoge')`は`I18n.t('hoge')`であると理解してくれます。

#vueファイルでtメソッドが使えるか確認

```vue:sample.vue
<template>
  <p>{{ t('create') }}</p>
</template>

<script>
import I18n from "../i18n/i18n";
import "../i18n/ja";
import { i18n } from "../mixins/i18n";


export default {
  mixins: [i18n]
}
</script>
```

上記vueファイルが使われているページにアクセスし、
I18nで定義された日本語（今回の場合は「登録」）が表示されます。

以上です。お疲れ様でした！

#おまけ1：Rails側のi18nのyamlファイルを修正した場合は？

```terminal:terminal
$ rake i18n:js:export
```
修正を加える都度変換してください。

#おまけ2:script内でもI18nを使いたい場合は？
mixinは`template`内で`t('hoge')`を使うことを可能にするのですが、
`script`の中では省略ができません。

```sample.vue
<template>
  <p>{{ title }}</p>
</template>

<script>
import I18n from "../i18n/i18n";  // ←敢えて読み込んでいます
import "../i18n/ja";
import { i18n } from "../../mixins/i18n";

export default {
  mixins: [i18n],
  data() {
    return {
      title: I18n.t("title")  // ←ここで使うために
    };
  }
}
</script>
```

#おまけ3:引数の渡し方は？
`{}`(中括弧)で囲みます

```yaml:config/locale/ja.yml
ja:
  mypage: "マイページ"
  msg:
    update_successful: "%{v}を更新しました。"
```
jsファイルなら
<img width="428" alt="スクリーンショット 2019-11-13 13.24.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358307/2bf941ca-8f74-3667-2633-352b109f1171.png">

vueファイルなら

```vue:sample.vue
<template>
  <p>{{ t('msg.update_successful', { v: t('mypage') }) }}</p>
  <p>{{ message }}</p>
  // どちらも出力結果は同じ
</template>

<script>
import { i18n } from "../mixins/i18n";

export default {
  mixins: [i18n],
  data() {
    return {
      message: I18n.t('msg.update_successful', { v: I18n.t('mypage') })
    };
  }
}
</script>
```

#参考記事
・[i18n-js 公式ドキュメント](https://github.com/fnando/i18n-js)
・[[stackoverflow]単一のリポジトリでロケールを共有するRails + Vue.jsインスタンスにI18nがありますか？](https://stackoverflow.com/questions/48950685/is-there-a-i18n-on-a-railsvue-js-instance-which-share-locales-in-a-single-repos)
・[[Qiita]i18n-jsで必要な言語のみロードしてパフォーマンスUP](https://qiita.com/konifar/items/9886c7da97f20d3206cc)
・[[Qiita]react_railsとi18n-jsをwebpackerで動かす](https://qiita.com/Atsuyoshi-N/items/48f8b5d5802b9ac47732#i18n%E3%81%AE%E8%A8%AD%E5%AE%9Arails%E5%81%B4)