at Degica • @shioyama on github, twitter, etc • I program in Ruby • I live in Tokyo, Japan • I am the author of Mobility • I blog at dejimata.com • I coined “Module Builder Pattern”
many translated models? • Growing application? Adding new models? • What’s your database? • What are your performance requirements? • How good are you at Arel?
you read and write translations? – What other features are supported? 2. Storage Strategies – How are translations actually stored? – Good and bad of each strategy 3. Mobility 4. Looking Ahead
attribute accessors • Language fetched or stored is the value of the global locale (I18n.locale or similar) • Models can be queried using translated attributes (like normal column-based attributes)
with Mobility") #=> (post with English title "Translating...") I18n.locale = :fr Post.find_by(title: "Traduction avec Mobilité") #=> (returns post with French title "Traduction...") I18n.locale = :en Post.find_by(title: "Translating with Mobility") #=> (post with English title "Translating...") I18n.locale = :fr Post.find_by(title: "Traduction avec Mobilité") #=> (returns post with French title "Traduction...") • Override query methods to return records by translated attribute • Hides details of storage strategy from query interface
to construct queries – Good performance (no joins) • Bad – Every new language requires a migration for every translated model – Columns created for every locale even if there is no translation for a locale
– locale column – foreign key pointing to model table (post_id) – one or more translated columns • has_many association between model and translations • Translation table records are created/updated when attribute is set and model is saved has_many :translations, class_name: PostTranslation has_many :translations, class_name: PostTranslation
of languages (no new migrations to add locale) – Space usage is more efficient (only create record if translation is stored) • Bad – Querying may be less performant (need to join translation table) – Code is more complex due to joining – Migration to create translation table/column for every model/attribute
– locale column – foreign key pointing to model id (translatable_id) – column holding the model class (translatable_type) – column holding the name of the attribute (key) – column holding the value of the attribute (value) • One translation table for each value type (string, text, ...) has_many :translations, as: :translatable, class_name: ::Mobility::ActiveRecord::StringTranslation has_many :translations, as: :translatable, class_name: ::Mobility::ActiveRecord::StringTranslation
all, so easy to scale to new locales + new models – Space usage is relatively efficient, scales with number of locales + models • Bad – Very complex querying – All translations in the same table, can be difficult to maintain a consistent database state
single database column class Post < ApplicationRecord serialize :title_translations, Hash end post = Post.new post.title_translations = { "en" => "Translating with Mobility" } post.save class Post < ApplicationRecord serialize :title_translations, Hash end post = Post.new post.title_translations = { "en" => "Translating with Mobility" } post.save
additional migrations) – Easy to add translations to model (simply add text column for each translated attribute) – Simple to access • Bad – Querying is not possible – Loading one translation requires loading all translations at once (serialized column)
is possible and can be fast • Database-dependent strategy, highly coupled to persistence layer • (Jsonb generally preferred over Hstore) Post.where(title: "foo").to_sql #=> SELECT "posts".* FROM "posts" WHERE ("posts"."title" @> '{"en":"foo"}') Post.where(title: "foo").to_sql #=> SELECT "posts".* FROM "posts" WHERE ("posts"."title" @> '{"en":"foo"}')
storage strategy – couple implementation to ORM (ActiveRecord) and framework (Rails) – have similar but not identical interfaces • What I wanted: – Support customizable, swappable storage solutions – No hard dependency on AR or Rails – Interface that is consistent and unified
– One translation interface, swappable storage backends – Supports all existing strategies, plus shared tables (“key value”) – Supports both ActiveRecord and Sequel ORM • Like the OmniAuth of Translation
g mobility:install class Post < ApplicationRecord include Mobility translates :title, :body end class Post < ApplicationRecord include Mobility translates :title, :body end Mobility.configure do |config| config.default_backend = :key_value config.accessor_method = :translates config.query_method = :i18n end Mobility.configure do |config| config.default_backend = :key_value config.accessor_method = :translates config.query_method = :i18n end :key_value :table :column :jsonb :hstore :serialized
single attribute (or set of attributes) • All backends automatically support locale accessors, fallbacks, dirty tracking, cache • Backends are tested against shared test suite to ensure consistency
:model def read(locale, **options) # Read attribute in locale. end def write(locale, value, **options) # Write value to attribute in locale. end def self.configure(options) # Normalize options (e.g. set any defaults) end setup do |attributes, options| # Do something with attributes and options in context of model class. end end class MyBackend include Mobility::Backend # attr_reader :attribute, :model def read(locale, **options) # Read attribute in locale. end def write(locale, value, **options) # Write value to attribute in locale. end def self.configure(options) # Normalize options (e.g. set any defaults) end setup do |attributes, options| # Do something with attributes and options in context of model class. end end
[:en, :ja] end class Post translates :title, backend: :key_value, locale_accessors: [:en, :ja] end class Post include Mobility::Attributes.new(:accessor, [:title], backend: :key_value, model_class: Post) end class Post include Mobility::Attributes.new(:accessor, [:title], backend: :key_value, model_class: Post) end It’s a Module Builder!
... attributes.each do |attribute| define_method attribute do # ... end # ... end end end class Post include Attributes.new(:accessor, [:title]) end class Attributes < Module def initialize(method, *attributes) # ... attributes.each do |attribute| define_method attribute do # ... end # ... end end end class Post include Attributes.new(:accessor, [:title]) end 1) Subclass Module 2) Define module methods in an initializer 3) Include instance of Module subclass in other classes
... def read(locale, **options) fallback = options.delete(:fallback) return super if fallback == false || (fallback.nil? && fallbacks.nil?) (fallback ? [locale, *fallback] : fallbacks[locale]).detect do |fallback_locale| value = super(fallback_locale, **options) break value if value.present? end end end end module Mobility module Fallbacks # ... def read(locale, **options) fallback = options.delete(:fallback) return super if fallback == false || (fallback.nil? && fallbacks.nil?) (fallback ? [locale, *fallback] : fallbacks[locale]).detect do |fallback_locale| value = super(fallback_locale, **options) break value if value.present? end end end end • Modules included into backend (fallbacks, cache, dirty) override read and/or write methods to add functionality Find first non-nil value of attribute
into a single class – supports every storage strategy – supports multiple strategies in a single model – supports all features (fallbacks, dirty tracking, caching, locale accessors, etc) at the attribute level – can be used to design new backends using flexible interface • Mobility keeps all your doors open
essential, but (almost) always an afterthought • Gems lock you into a specific storage strategy: – Globalize? Locked to model translation tables – Traco? Locked to translatable columns – json_translate? Locked to PostgreSQL jsonb
simplifies and standardizes content translation – does not lock you into a single storage strategy • For developers of other gems: – A single Mobility integration and will support all current and future storage strategies – Example: friendly_id-mobility