No release in over a year
Separate database records for each language, grouped together with an ID
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Runtime

 Project Readme

active_record-translated

RuboCop RSpec MIT License

This gem tackles localization of ActiveRecord models in Ruby on Rails by keeping records in different languages within the same database table. Translations are separated by a locale-column and linked together with an indexed record_id-column.

Since all database rows now are language-specific, this approach works best for models where all or most of the attributes should be translatable. Or else you may end up with a lot of duplicated data. For a visual example, check out examples further down this readme.

Guidelines

These are the guidlines we should follow when developing this gem:

  • Should work without any modifications to current application code.
  • Should not decrease performance with multiple SQL queries.
  • Should act like a normal record if no locale is set.
  • Should accept an unlimited number of languages without decreased performance caused by this gem.
  • Should not brake current specs/tests.
  • Should not brake model validations
  • All translations will have it's own record.
  • Translations of the same object should have the same record_id
  • A translation should know all other available translations with the same record_id
  • Should work independently on what type (int/uuid..) of primary key the database table is using.

Installation

Add the following line to Gemfile:

gem "active_record-translated", github: 'kjellberg/active_record-translated'

and run bundle install from your terminal to install it.

Getting started

To generate the initializer and default configuration, run:

rails generate translated:install

This will create an initializer at config/initializers/active_record-translated.rb

Make a model translatable

To enable translation for a model, run:

rails generate translated MODEL

This will generate a database migration for the locale and record_id attributes and setup your model for translation.

Examples

The posts table below represents a translated Post model with the attributes title and slug. Note that resource_id and locale was created by this gem when you generated the migrations.

id resource_id locale title slug
1 42fb060d-2000-4a73-bb74-cfb5ca799e0d en Good morning! a-blog-post
2 42fb060d-2000-4a73-bb74-cfb5ca799e0d es Buenos días! una-publicacion-de-blog
3 160f6aee-ba2d-4c7e-a273-96b99468c8f9 en Good night! another-blog-post
4 160f6aee-ba2d-4c7e-a273-96b99468c8f9 es Buenas noches! otra-entrada

Your model may look something like this:

# app/models/post.rb

class Post < ApplicationRecord
  # Enable translations for this model
  include ActiveRecord::Translated::Model
  
  # You can keep using validations the same way you did before installing this gem. The only
  # difference is that validations will be run on all translations separately.
  #
  # Examples:
  #  - Title must exist on each translation.
  #  - Every translation should have it's unique slug.
  validates :title, presence: true
  validates :slug, presence: true, uniqueness: true
end

Create a new record

The creation of a new record will autmatically generate a unique record_id and set the locale attribute to the current locale. The current locale is determined globally by I18n.locale and/or within the gem via ActiveRecord::Translated.locale, the latter with greater priority. The default locale if nothing is set is :en.

before_action do
  I18n.locale = :en_US # fallback if "ActiveRecord::Translated.locale" is not set
  ActiveRecord::Translated.locale = :en_UK # has priority over "I18n.locale"
end

# POST /posts/create
def create
  @post = Post.create(title: "A post!", ...)

  @post.id # => 5
  @post.record_id # => 448ecc54-cc82-4c24-aed4-89fae5d38ec4 (auto-generated)
  @post.locale # => :en_UK
  ...
end

Create a new record in a specific language

To create a post in a language other than the current locale, just pass a language code to the locale attribute.

post = Post.create(title: "Una entrada de blog", ..., locale: :es)
post.locale # => :es

Using #with_locale

You can also wrap your code inside ActiveRecord::Translated#with_locale. This will temporary override the current locale within the block.

post = ActiveRecord::Translated.with_locale(:es) do
  Post.create(title: "Una entrada de blog", ...)
end

post.locale # => :es

Create a new translation of an existing record

To translate an already existing post, create a second post with a different locale and add it to the first post via #translations. This will make sure that our two translations are linked together with a shared record_id.

# Create an english post
post = Post.create(title: "A post!", ...)

# Create a spanish translation
post_es = Post.create(title: "Una entrada de blog", ..., locale: :es)

# Associate the Spanish translation with the english post
post.translations << post_es

# Record ID should match:
post.record_id # => 448ecc54-cc82-4c24-aed4-89fae5d38ec4
post_es.record_id # => 448ecc54-cc82-4c24-aed4-89fae5d38ec4

# Locale should differ:
post.locale # => :en
post_es.locale # => :es

Fetch a translated record

Your translated model will automatically scope query results by the current locale. For example, with I18n.locale set to :sv, your model will only return Swedish results. There's some different ways to fetch translations from the database using ActiveRecord's #find:

Find by primary key

This gem wont break the default #find method. So if you already now the ID, just fetch it with a normal #find:

I18n.locale = :en # Ignored by #find when fetching a record by its primary key.

post = Post.find(5) # Spanish translation
post.locale #=> :es

Find by record_id

You can also find a translated record by using a record_id as the argument. This will look for a row that matches both the record_id and the current locale:

I18n.locale = :es

post = Post.find("0e048f11-0ad9-48f1-b493-36e1f01d7994") 
post.locale #=> :es

Fetching collection of records

All query results are scoped by the current locale. Use #unscoped_locale to disable the locale scope.

Post.create(title: "Foo bar", ..., locale: :es) # es
Post.create(title: "Foo bar", ..., locale: :es) # es
Post.create(title: "Foo bar", ..., locale: :pt) # pt

I18n.locale = :es
Post.count #=> :2

I18n.locale = :pt
Post.count #=> :1

# Returns all posts
Post.unscoped_locale.count #=> :3

License

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

Contributing

New contributors are very welcome and needed. This gem is an open-source, community project that anyone can contribute to. Reviewing and testing is highly valued and the most effective way you can contribute as a new contributor. It also will teach you much more about the code and process than opening pull requests.

Except for testing, there are several ways you can contribute to the betterment of the project:

  • Report an issue? - If the issue isn’t reported, we can’t fix it. Please report any bugs, feature, and/or improvement requests on the GitHub Issues tracker.
  • Submit patches - Do you have a new feature or a fix you'd like to share? Submit a pull request!
  • Write blog articles - Are you using this gem? We'd love to hear how you're using it in your projects. Write a tutorial and post it on your blog!

Development process

The main branch is regularly built and tested, but it is not guaranteed to be completely stable. Tags are created regularly from release branches to indicate new official, stable release versions of the libraries.

Commit message guidelines

A good commit message should describe what changed and why. This project uses semantic commit messages to streamline the release process. Before a pull request can be merged, it must have a pull request title with a semantic prefix.

Versioning

This application aims to adhere to Semantic Versioning. Violations of this scheme should be reported as bugs. Specifically, if a minor or patch version is released that breaks backward compatibility, that version should be immediately yanked and/or a new version should be immediately released that restores compatibility. Breaking changes to the public API will only be introduced with new major versions.