Skip to content

Migrating from Globalize

Chris Salzberg edited this page May 27, 2018 · 58 revisions

Mobility's behaviour and interface is largely the same as Globalize, so migrating is straightforward. Note that there are some minor differences, so having a comprehensive test suite for your existing application is important.

Basic setup

Change your Gemfile to remove globalize and add mobility

-gem 'globalize'
+gem 'mobility'

Run rails generate mobility:install to create the initializer.

Edit config/initializers/mobility.rb and change the default backend to table and turn on dirty tracking. ("Table" is the name of the backend where translations are stored in another table, equivalent to what Globalize does. See the wiki page on the Table backend for details.)

Although not required, it is also recommended to enable locale accessors for use with the dirty plugin, and set I18n.available_locales.

 Mobility.configure do |config|
+  config.default_backend = :table
-  config.default_backend = :key_value
   config.accessor_method = :translates
   config.query_method    = :i18n
+  config.default_options[:dirty] = true
+  config.default_options[:locale_accessors] = true # recommended
end

Extend Mobility from every model which you plan to translate before calling translates, e.g.:

 class Post < ApplicationRecord
+  extend Mobility
   translates :title, :content
   # ...
 end

Alternatively, you can just extend Mobility from ApplicationRecord (but extending each model separately is preferred):

 class ApplicationRecord < ActiveRecord::Base
+  extend Mobility
   self.abstract_class = true
 end

Fallbacks

If you were using fallbacks, also turn them on in mobility:

 Mobility.configure do |config|
   config.default_backend = :table
   config.accessor_method = :translates
   config.query_method    = :i18n
   config.default_options[:dirty] = true
+  config.default_options[:fallbacks] = { en: :ja, ja: :en }
end

Note that fallbacks (like all options) can be customized for each model, by passing the fallbacks option to translates. The default_options above just sets the default in case none is specified in the model.

Dirty Tracking

Mobility supports tracking changed (translated) attributes like Globalize. To enable this, enable the dirty option in your mobility initializer (as mentioned above).

 Mobility.configure do |config|
   config.default_backend = :table
   config.accessor_method = :translates
   config.query_method    = :i18n
+  config.default_options[:dirty] = true
end

There is a subtle difference in how Mobility handles dirty attributes. In Globalize, a change to the title attribute in any locale will be tracked as a change to title. Mobility does things differently: changes to an attribute in a given locale will be tracked with a suffix containing the locale, so that you can see changes to multiple locales at once.

post.changed
["title_en", "title_ja"]

Again, like fallbacks, you can enable or disable dirty tracking on each model if you like by passing the dirty option to translates.

globalize-accessors

Mobility supports having locale specific accessors (e.g., title_en or title_ja) like the globalize-accessors gem does.

To use this, first remove globalize-accessors from your Gemfile

-gem 'globalize-accessors'

Then turn on the locale_accessors plugin in your mobility initializer:

 Mobility.configure do |config|
   config.default_backend = :table
   config.accessor_method = :translates
   config.query_method    = :i18n
   config.default_options[:dirty] = true
+  config.default_options[:locale_accessors] = true
end

This will define locale accessor methods for all locales in I18n.available_locales.

Also be sure to remove any globalize_accessors from your models. Locale accessors can be customized per model using the locale_accessors option to translates.

class Post < ApplicationRecord
  translates :title, locale_accessors: [:en, :fr, :de] # overrides default
end

Associations

The association from model to translations is the same as Globalize, a has_many relation (by default) named translations:

post = Post.first
post.translations #=> returns post translations

(You can actually change the name of the association to something else with the association_name option to translates.)

However, whereas the inverse association in Globalize is called globalized_model, it is called translated_model in Mobility:

translation = post.translations.first
translation.translated_model          #=> returns the post

This means, among other things, that if you are using fixtures, you'll need to change globalized_model everywhere to translated_model.

Migrations

Mobility does not have migration helper methods like Post.create_translation_table! to create new translation tables for translated models. Instead, there are rails generators for this.

If you have a model Post and want to add translations for attributes title (string) and content (text), you would do this with:

rails generate mobility:translations post title:string content:text

Querying

Mobility supports querying on translated attributes like Globalize. However, unlike Globalize, this is not enabled by default. To query on translated attributes, you need to use the i18n scope (the name of this scope can be customized using the query_method configuration option):

Post.i18n.where(title: "foo")
Post.i18n.find_by(title: "foo")
Post.i18n.find_by_title("foo")

Although it is not recommended, you can make this query scope enabled by default using a default_scope, like this, in your model class:

default_scope { i18n }

Then you can query on translated attributes like with Globalize, without any extra scope:

Post.where(title: "foo")
Post.find_by(title: "foo")
Post.find_by_title("foo")

To enable the i18n by default on all models, put the default scope above in ApplicationRecord.

Note that Mobility does not currently support querying with fallbacks like Globalize does, although this is in the roadmap. It also does not support ordering on translated attributes (there is an issue to add support for this). These are the only issues remaining before Mobility can claim full feature parity with Globalize.

See also the note below about INNER and OUTER JOINS.

Uniqueness Validation

Mobility has a uniqueness validator like Globalize, however unlike Globalize it does not use monkey-patching. This means that in order for it to work, you need to call validates after you extend Mobility, like this:

class Post < ApplicationRecord
  extend Mobility
  translates :title

  validates :title, uniqueness: true
end

Also, uniqueness validation in Mobility does not currently support case-sensitivity. If you try to validate with case sensitivity enabled Mobility will issue a warning, and case sensitive validation will be used.

Interpolation

Mobility does not support interpolation like Globalize does, but you can get the same effect of code like this (in Globalize):

greeter.greeting(name: 'Chris')

with the slightly more verbose:

I18n.interpolate(greeter.greeting, name: 'Chris')

See #162 for more discussion on this.

Translation Locale is a String, not a Symbol

If you have any code which currently compares the value of a translation locale, like this (from RefineryCMS):

def translated_to_default_locale?
  persisted? && translations.any? { |t| t.locale == Refinery::I18n.default_frontend_locale}
end

... you will need to change it to convert the translation locale to a symbol, like this:

def translated_to_default_locale?
  persisted? && translations.any? { |t| t.locale.to_sym == Refinery::I18n.default_frontend_locale}
end

This is because unlike Globalize, Mobility does not override the locale method on the translation class to convert the value to a symbol.

Blank Translations

A quirk in Globalize is that if you save a model, even with no translated attributes set, you will save a translation in the current locale, which can be a problem (see globalize/globalize#328). Mobility does not do this. If you save a translated model and no translated attributes are present, no translation will be created in any locale.

This actually also applies to updating. If a model has a translated attribute, and you set that attribute to a blank value (blank string or nil) and save, and no other translated attributes are present in that locale, the translation record will be destroyed. Mobility strives to keep your database in a state where models only have translation records if at least one attribute in the given locale is present.

This is a good thing, but to make it really work correctly Mobility has to do some very careful work when querying. For reference, when you query for Post.where(title: nil) (where title is translated), Globalize will use an INNER JOIN which will only return posts that have a matching joined translation in the current locale. If a post has no translation in this locale, we consider the title value to be nil, so we should return a post with a missing English translation, but an INNER join will miss this post and incorrectly return no results. Since Globalize always creates a blank translation in the current locale, this kind of works some of the time, but not reliably.

Mobility does this better. Instead of always usign an INNER join, Mobility uses either an INNER or OUTER join depending on what you are querying for, which is the correct approach. So if you call Post.i18n.where(title: "foo"), Mobility will use an INNER join like Globalize. But if you call Post.i18n.where(title: nil), Mobility will use an OUTER join to correctly return posts which have no translation in this locale. For more details see the API docs.

Gem Integrations

More info

See the Table Backend section of the wiki.