Skip to content
Chris Salzberg edited this page Aug 18, 2017 · 23 revisions

The KeyValue backend stores translations on a set of two shared tables, one for string-valued translations and one for text-valued translations. These tables are generated when you run the default install generator (rails generate mobility:install) and then migrate your database (rake db:migrate).

A detailed description of the backend is described as "Strategy #3" in this blog post.

The tables generated by the default migration look like this:

create_table "mobility_string_translations", force: :cascade do |t|
  t.string   "locale"
  t.string   "key"
  t.string   "value"
  t.integer  "translatable_id"
  t.string   "translatable_type"
  t.datetime "created_at",        null: false
  t.datetime "updated_at",        null: false
  t.index ["translatable_id", "translatable_type", "key"], name: "index_mobility_string_translations_on_translatable_attribute"
  t.index ["translatable_id", "translatable_type", "locale", "key"], name: "index_mobility_string_translations_on_keys", unique: true
  t.index ["translatable_type", "key", "value", "locale"], name: "index_mobility_string_translations_on_query_keys"
end

create_table "mobility_text_translations", force: :cascade do |t|
  t.string   "locale"
  t.string   "key"
  t.text     "value"
  t.integer  "translatable_id"
  t.string   "translatable_type"
  t.datetime "created_at",        null: false
  t.datetime "updated_at",        null: false
  t.index ["translatable_id", "translatable_type", "key"], name: "index_mobility_text_translations_on_translatable_attribute"
  t.index ["translatable_id", "translatable_type", "locale", "key"], name: "index_mobility_text_translations_on_keys", unique: true
end

Both tables have a translatable_type string column, because these will be used in a polymorphic relationship with translated models.

To understand how this works, suppose we have a model Post:

class Post < ApplicationRecord
  extend Mobility
  translates :title,   backend: :key_value, type: :string
  translates :content, backend: :key_value, type: :text
end

The backend will create associations on the class roughly resulting in:

class Post < ApplicationRecord
  has_many :mobility_string_translations, ->{ where key: [:title] },
    as:         :translatable,
    class_name: Mobility::ActiveRecord::StringTranslation,
    dependent:  :destroy,
    inverse_of: :translatable,
    autosave:   true

  has_many :mobility_text_translations,   ->{ where key: [:content] },
    as:         :translatable,
    class_name: Mobility::ActiveRecord::TextTranslation,
    dependent:  :destroy,
    inverse_of: :translatable,
    autosave:   true
end

So the backend adds associations for each type, scoped to only include translations whose keys are :title and :content, respectively.

The classes Mobility::ActiveRecord::StringTranslation and Mobility::ActiveRecord::TextTranslation both inherit from Mobility::ActiveRecord::Translation, which is an abstract class:

module Mobility
  module ActiveRecord
    class Translation < ::ActiveRecord::Base
      self.abstract_class = true

      belongs_to :translatable, polymorphic: true

      validates :key, presence: true, uniqueness: { scope: [:translatable_id, :translatable_type, :locale] }
      validates :translatable, presence: true
      validates :locale, presence: true
    end
  end
end

Since the class itself is abstract, it has no table; the table is set in the descendants to mobility_string_translations and mobility_text_translations, respectively. The different tables is actually the only difference between the string and text translation classes, and the only difference between the tables is that their value columns are of different types (string/text) (and that the string table has one additional index on value).

When you get a value with post.title, the backend does roughly the following to get the value:

locale = Mobility.locale
translation = mobility_string_translations.find { |t| t.key == "title" && t.locale == locale.to_s }
translation ||= translations.build(locale: locale, key: "title")
translation.value

So it finds the translation from all translations associated with the model which has a key "title" and a locale equal to Mobility.locale. If such a translation does not exist, it builds one. It then returns the value. Setting is much the same, with the value of the translation updated to a new value.