Phoenix/Ectoでモデルの多言語化に対応する方法

PhoenixにおけるViewの多言語化はGettextを使う方法がスタンダードですが、Railsの[globalize](https://github.com/globalize/globalize)のような、モデルの多言語化に対応するためのパッケージがElixir/Phoenixにはまだ存在していません。

ということで、作ってみました。

[translator](https://github.com/kenta-aktsk/translator)

(ちなみにEcto2.0以上限定になります)

# 使用方法

下記のようなUserモデルの、`name`と`profile`列を多言語化する手順について説明します。

```ex
defmodule MyApp.User do
  use MyApp.Web, :model

  schema "users" do
    field :email, :string
    field :name, :string
    field :profile, :string

    timestamps
  end

  @required_fields ~w(email name)a
  @optional_fields ~w(profile)a

  def changeset(user, params \\ %{}) do
    user
    |> cast(params, @required_fields ++ @optional_fields)
    |> validate_required(@required_fields)
  end
end
```

usersテーブルには下記のようなレコードが存在するとします。

```ex
%{id: 1, email: "user01@example.com", name: "Kenta Katsumata", profile: "Engineer, Writer"}
```

このモデル用の多言語化レコードを保持するtranslationテーブル用のmigrationファイルを作成します:

```ex
defmodule MyApp.Repo.Migrations.CreateUserTranslation do
  use Ecto.Migration

  def change do
    create table(:user_translations) do
      add :user_id, references(:users, on_delete: :delete_all), null: false
      add :locale, :string, null: false
      add :name, :string, null: false
      add :profile, :text

      timestamps
    end

    create index(:user_translations, [:user_id, :locale], unique: true)
  end
end
```

マイグレーションを実行します。

```
mix ecto.migrate
```

このtranslationテーブル用のモデルを下記のように定義します。

```ex
defmodule MyApp.UserTranslation do
  use MyApp.Web, :model
  use Translator.TranslationModel,
    schema: "user_translations", belongs_to: MyApp.User, required_fields: [:name], optional_fields: [:profile]
end
```

`User`モデルに、上記で作成した`UserTranslation`モデルへのリレーションを定義します。

```ex
defmodule MyApp.User do
  alias MyApp.UserTranslation
  ...
  schema "users" do
    ...
    has_one :translation, UserTranslation
    ...
  end
  def preload_all(query, locale) do
    from query, preload: [translation: ^UserTranslation.translation_query(locale)]
  end
end
```

下記のようなレコードをデータベースにINSERTします。

```
INSERT INTO user_translations (user_id, locale, name, profile) VALUES (1, "en", "Kenta Katsumata", "living in Tokyo");
INSERT INTO user_translations (user_id, locale, name, profile) VALUES (1, "ja", "勝又健太", "東京在住");
```

ここまでの設定で、下記のようなコードで多言語化されたレコードが取得できます。

```ex
alias MediaSample.{Repo, User}
user = User |> User.preload_all("ja") |> Repo.get!(1)
user.translation
# => %{user_id: 1, name: "勝又健太", profile: "東京在住"}

```

# Insert/Update

`insert_or_update/4`関数を使用して、translationレコードの新規作成と更新が可能です。下記のように使用します。(`Ecto.Multi`を使ったトランザクション内でも使用可能です)

```ex
# create action in user controller 
def create(conn, %{"user" => user_params}) do
  changeset = User.changeset(%User{}, user_params)

  Repo.transaction fn ->
    user = Repo.insert!(changeset)
    UserTranslation.insert_or_update(Repo, user, user_params, "ja")
  end
end

# update action in user controller 
def update(conn, %{"id" => id, "user" => user_params}) do
  user = Repo.get!(User, id)
  changeset = User.changeset(user, user_params)

  Repo.transaction fn ->
    user = Repo.update!(changeset)
    UserTranslation.insert_or_update(Repo, user, user_params, "ja")
  end
end
```

4番目の引数に渡すロケールは自分で設定する必要があります。(後述するサンプルで、URLにロケールを含めてそれをコントローラのアクションの引数に渡す手法を使っていますのでご参照ください)

## ヘルパー

ビュー/テンプレート側では下記のようなヘルパーを使用して、多言語化されたフィールドデータを取得出来ます。(アソシエーションで指定したtranslationテーブルをpreloadしてあることが前提となります。preloadされていない場合は親レコードのフィールドのデータを取得します)

```ex
# web.ex
def view do
  quote do
    ...
    import Translator.TranslationHelpers
  end
end

# templates/user/show.html.eex
<li>
  <%= translate(@user, :name) %>
</li>
```

## サンプル

[こちら](https://github.com/kenta-aktsk/media_sample)に、上記のパッケージを使用しているサンプルコードをアップしてあります。