Ruby on Railsでコンテンツを複数言語で表示する


コンテンツを複数言語で表示できるウェブアプリケーションを作ってみましょう。この記事では、次の手順で翻訳機能を実装してみます。

1. データベースで翻訳を扱う方法を検討する
1. アプリケーションの言語に合わせて、適切な翻訳を表示する機能を作る

## データベースで翻訳を扱う方法

はじめに、データベースで翻訳を扱う方法を検討するために、翻訳がどのようなデータか確認しましょう。

翻訳は、1つのコンテンツをある言語で表したデータです。扱う言語の数だけ翻訳を用意する必要があります。

| 言語 | 挨拶 |
|--------|--------|
| 英語       | Hello |
| 日本語       | こんにちは |
| 中国語       | 你好  |

例えば、「挨拶」というコンテンツを英語と日本語、中国語に翻訳するためには、上記の表で表されるデータを用意します。

### データベースで翻訳を扱う方法

データベースで翻訳を扱う方法は、いくつか考えられます。簡単に思いついた方法を、3つ挙げてみました。それぞれに長所や短所があります。翻訳のデータ型や利用頻度などに応じて適切な方法を選択すると良いと思います。

| 方法 | カラム | 長所 | 短所 |
| ---- | ---- | ---- | ---- |
| 翻訳を別テーブルに保存する | greeting_id,locale, greeting | 言語が増えてもデータベース構造を変更する必要がない | 翻訳が必要なコンテンツごとに翻訳用テーブルが必要である |
| 翻訳を言語ごとのカラムに保存する | id, greeting_en, greeting_ja, greeting_zh| 一つのテーブルで翻訳を扱える | 言語が増えると翻訳用のカラムを追加する必要がある |
| 翻訳をJSONでシリアライズして保存する | id, serialized_greeting | 一つのカラムで複数の翻訳を扱える | 言語を指定して翻訳をselectできない |


## 翻訳機能を実装する

翻訳を別テーブルに保存する方法を採用した上で、翻訳機能を実装してみました。投稿とその翻訳を表すモデルクラスを、次のソースコードで示します。


```ruby
class Post
  validates :permalink, uniqueness: true

  # Has current locale translation
  has_one :translation, -> { locale: I18n.locale },
    class_name: 'PostTranslation'

  # Has many translations
  has_many :translations, class_name: 'PostTranslation'

  # Define reader methods that returns the translation of current locale
  # If the translation is not exists, it returns nil
  %i(title excerpt body).each do |translation_attribute|
    define_method(translation_attribute) do
      translation.try(translation_attribute)
    end
  end
end

class PostTranslation
  belongs_to :post, validate: true

  validates :locale, inclusion: { in: I18n.available_locales.map(&:to_s) }
end

```

投稿は、パーマリンクとタイトル、抜粋、本文の4つの属性を持ちます。

そのうち、タイトル、抜粋、本文はアプリケーションの言語に合わせた内容を返します。もし、翻訳が存在しない場合は、`nil`を返します。

```ruby
post = Post.create(permalink: "greeting-post")
post.translations.create(locale: "en", title: "Hello")
post.translations.create(locale: "ja", title: "こんにちは")
post.translations.create(locale: "zh", title: "你好")

I18n.locale = :en

post.title
=> Hello

I18n.locale = :ja

post.title
=> こんにちは

I18n.locale = :sw

post.title
=> nil

```

### ActiveSupport::Concernを使って、機能を分離する

翻訳機能は、様々なモデルで利用します。そこで、ActiveSupport::Concernを使い、翻訳機能を分離します。


```app/models/concerns/translatable.rb
module Translatable
  extend ActiveSupport::Concern

  included do
    # has current locale translation
    has_one :translation, -> { locale: I18n.locale },
      class_name: translation_class_name

    # has many translations
    has_many :translations, class_name: translation_class_name

    # define reader methods that returns the translation of current locale
    translation_attribute_names.each do |translation_attribute|
      define_method(translation_attribute) do
        translation.try(translation_attribute)
      end
    end
  end

  module ClassMethods
    def translation_class_name
      "#{self.name}Translation"
    end

    def translation_class
      Object.const_get(translation_class_name)
    end

    def translation_attribute_names
      self.attribute_names - translation_class.attribute_names
    end
  end
end
```

翻訳機能を利用するには、`Translatable`モジュールをモデルクラスでインクルードします。翻訳用クラスは、インクルードしたクラス名を元に決めました。

```app/models/post.rb
class Post < ActiveRecord::Base
  include Translatable
end

Post.translation_class
=> PostTranslation
```

## まとめ

別テーブルに保存した翻訳を言語に合わせて表示することができました。また、ActiveSupport::Concernを使うことで、翻訳機能を汎用的にすることができました。

ウェブアプリケーションに翻訳機能を実装することで、気軽にコンテンツを多言語で表示することができます。

ではでは。