FromClauseTranslate

This gem extends ActiveRecord models for easy manipulations with multilanguage data.

The name of gem stands for way that it is doing its magic - SQL's FROM statement.

The main idea is that you can have database columns name_en, name_ru, name_fr and you will still able to filter data with WHERE name LIKE '%some name%', all SQL statements will be working with just name column.

It allows making of complex SQL queries without need of interpolation of I18n.locale and without joining separate translation tables.

Requirements

Activerecord.

Postgresql, other databases were not tested.

Usage

Lets have translatable model Post:

class CreatePosts < ActiveRecord::Migration[5.2]
  def change
    create_table :posts do |t|
      %i[en ru uk de fr it es].each do |locale|
        t.string "slug_#{locale}"
        t.string "name_#{locale}"
        t.string "title_#{locale}"
        t.string "text_#{locale}"
        t.boolean "translated_#{locale}", null: false, default: false
      end
    end
  end
end

In migration array of locales should be hardcoded, because when new language will be added in future, this migration will still be reversable.

Then this FromClauseTranslate should be included in AplicationRecord or in the required model:

class ApplicationRecord < ActiveRecord::Base
  include FromClauseTranslate

  self.abstract_class = true
end

Make columns in the model translatable:

class Post < ApplicationRecord
  translates :name, :title, :text, :slug, :translated, plurals: %i[slugs translateds]
end

About plurals will be down below, now this model supports getting and setting values:

post = Post.new(name: 'Name for current locale')
post.name == 'Name for current locale' # true
post.name = 'New name'
post.name_fr = 'French name of post' # real columns are still accessible

Such ActiveRecord column methods are supported (star for column name): save_change_to_*, *_changed?, *_before_last_save, *_change_to_be_saved, *_in_database, saved_change_to_*?, will_save_change_to_*?

Querying

Following code will get posts just like they are in DB, with all of name_en, title_de columns:

post = Post.take
posts = Post.all

For receiving columns for current locale use .translated method with the list of required columns:

post = Post.translated(:title, :name, :text)

It accepts symbols and SQL strings that will go to SELECT statement.

After invoking .translated passed columns becomes available in querying methods:

Post.translated(:title, :name)
  .where(title: 'smth')
  .where("name ILIKE '%name%'")
  .order('title DESC, name ASC')

Fallbacks

Set up I18n.fallbacks in application.rb:

I18n.fallbacks = {uk: [:ru, :en]}

Now ukrainian will fallback to russian and then to english.

Lets take post record:

Post.translated(:name).take

Fallback rule will be present in produced SQL statement:

COALESCE(COALESCE("posts"."name_uk", name_ru), name_en) AS name

Fallbacks could be set through option of .translated method in model:

class Post
  translates :name, fallback: {_: %i[name_en title_en]}
end

Here _ key means any locale. If current locale is ru and name_ru is NULL then name_en will be used. If it is NULL too then title_en will be received. If current locale is en then name_en fallback rule will be ignored and title_en will be received.

Plurals

Described Post model has slug columns for human readable urls. Also it has translated boolean columns for hiding not translated posts from users.

class Post < ApplicationRecord
  translates :name, ..., plurals: %i[slugs translateds]
end

Query for show page loads slug and translated columns for all locales:

post = Post.translated(:slug, :slugs, :translateds).find_by(slug: params[:slug])

Now at the show page you can provide links to same post in different languages filtering out not translated:

<% I18n.available_locales.each do |locale| %>
  <% if @post.send("translated_#{locale}") %>
    <% link_to "Read in #{locale}", post_url(@post.send("slug_#{locale}")) %>
  <% end %>
<% end %>

Installation

Add this line to your application's Gemfile:

gem 'from_clause_translate'

And then execute:

$ bundle

Or install it yourself as:

$ gem install from_clause_translate

License

The gem is available as open source under the terms of the MIT License.