No release in over 3 years
Zero-config internationalization for ActiveRecord models using JSON columns. No JOINs, no N+1 queries, just fast inline translations. Supports SQLite, PostgreSQL, and MySQL.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Runtime

 Project Readme

Internationalize

Gem Version Build

Lightweight, performant internationalization for Rails with JSON column storage.

Why Internationalize?

Internationalize is a focused, lightweight gem that does one thing well: JSON column translations. No backend abstraction layers, no plugin systems, no extra memory overhead.

Unlike Globalize (separate translation tables) or Mobility (JSON backend is PostgreSQL-only), Internationalize stores translations inline using JSON columns with full SQLite, PostgreSQL, and MySQL support:

  • No JOINs - translations live in the same table
  • No N+1 queries - data is always loaded with the record
  • No backend overhead - direct JSON column access, no abstraction layers
  • ~50% less memory - no per-instance backend objects or plugin chains
  • Direct method dispatch - no method_missing overhead
  • True multi-database JSON support - SQLite 3.38+, PostgreSQL 9.4+, MySQL 8.0+
  • ActionText support - internationalized rich text with attachments
  • Visible in schema.rb - translated fields appear directly in your model's schema

Note: Mobility's JSON/JSONB backends only work with PostgreSQL. For SQLite or MySQL, Mobility requires separate translation tables with JOINs. Internationalize provides JSON column querying across all three databases.

Supported Databases

Database JSON Column Query Syntax
SQLite 3.38+ json json_extract()
PostgreSQL 9.4+ json / jsonb ->> operator
MySQL 8.0+ json ->> operator

Installation

Add to your Gemfile:

gem "internationalize"

Usage

1. Generate a migration

rails generate internationalize:translation Article title description

This creates a migration adding title_translations and description_translations JSON columns with default: {}.

Important: JSON columns must have default: {} set. The generator handles this automatically, but if writing migrations manually:

add_column :articles, :title_translations, :json, default: {}

2. Include the model mixin

class Article < ApplicationRecord
  include Internationalize::Model
  international :title, :description
end

3. Use translations

# Set via current locale (I18n.locale)
article.title = "Hello World"

# Set for specific locale
article.title_en = "Hello World"
article.title_de = "Hallo Welt"

# Read via current locale
article.title  # => "Hello World" (when I18n.locale == :en)

# Read specific locale
article.title_de  # => "Hallo Welt"

# Access raw translations
article.title_translations  # => {"en" => "Hello World", "de" => "Hallo Welt"}

Creating Records

Use the helper methods for a cleaner syntax when creating records with translations:

# Create with multiple locales using a hash
Article.international_create!(
  title: { en: "Hello World", de: "Hallo Welt" },
  description: { en: "A greeting" },
  status: "published"  # non-translated attributes work normally
)

# Or use direct assignment for current locale (I18n.locale)
I18n.locale = :de
Article.international_create!(title: "Achtung!")  # Sets title_de

# Mix both styles
Article.international_create!(
  title: "Hello",  # Current locale only
  description: { en: "English", de: "German" }  # Multiple locales
)

# Build without saving
article = Article.international_new(title: "Hello")
article.save!

# Non-bang version returns unsaved record on validation failure
article = Article.international_create(title: { en: "Hello" })

Querying

All query methods default to the current I18n.locale and return ActiveRecord relations that can be chained with standard AR methods.

# Exact match on translation (uses current locale by default)
Article.i18n_where(title: "Hello World")
Article.i18n_where(title: "Hallo Welt", locale: :de)

# Partial match / search (case-insensitive LIKE)
Article.i18n_where(title: "hello", match: :partial)
Article.i18n_where(title: "Hello", match: :partial, case_sensitive: true)

# Exclude matches
Article.international_not(title: "Draft")
Article.international_not(title: "Entwurf", locale: :de)

# Order by translation
Article.international_order(:title)
Article.international_order(:title, :desc)
Article.international_order(:title, :asc, locale: :de)

# Find translated/untranslated records
Article.translated(:title)
Article.translated(:title, locale: :de)
Article.untranslated(:title, locale: :de)

# Chain with ActiveRecord methods
Article.i18n_where(title: "Hello World")
       .where(published: true)
       .includes(:author)
       .limit(10)

# Combine queries
Article.i18n_where(title: "hello", match: :partial)
       .where(status: "published")
       .merge(Article.international_order(:title, :desc))

Helper Methods

# Check if translation exists
article.translated?(:title, :de)  # => true/false

# Get all translated locales for an attribute
article.translated_locales(:title)  # => [:en, :de]

Fallbacks

By default, Internationalize falls back to the default locale when a translation is missing:

article.title_en = "Hello"
article.title_de  # => nil

I18n.locale = :de
article.title  # => "Hello" (falls back to :en)

Validations

For most validations, use standard Rails validators—they work with the virtual accessor for the current locale:

class Article < ApplicationRecord
  include Internationalize::Model
  international :title

  # Standard Rails validations (recommended)
  validates :title, presence: true
  validates :title, length: { minimum: 3, maximum: 100 }
  validates :title, format: { with: /\A[a-z0-9-]+\z/ }
end

Use validates_international only when you need:

class Article < ApplicationRecord
  include Internationalize::Model
  international :title

  # Uniqueness per-locale (requires JSON column querying)
  validates_international :title, uniqueness: true

  # Multi-locale presence (for admin interfaces editing all translations at once)
  validates_international :title, presence: { locales: [:en, :de] }
end

Errors from validates_international are added to locale-specific keys:

article.errors[:title_en]  # => ["has already been taken"]

ActionText Support

For rich text with attachments (requires ActionText):

class Article < ApplicationRecord
  include Internationalize::Model
  include Internationalize::RichText

  international_rich_text :content
end

This generates has_rich_text :content_en, has_rich_text :content_de, etc. for each locale, with a unified accessor:

article.content = "<p>Hello</p>"     # Sets for current locale
article.content                       # Gets for current locale (with fallback)
article.content_en                    # Direct access to English
article.content.body                  # ActionText::Content object
article.content.embeds                # Attachments work per-locale

Fixtures

Use the *_translations column name with nested locale keys:

# test/fixtures/articles.yml
hello_world:
  title_translations:
    en: "Hello World"
    de: "Hallo Welt"
  status: published

Configuration

No configuration required. Internationalize uses your existing Rails I18n settings:

  • Locales: I18n.available_locales
  • Fallback: I18n.default_locale

To override locales (rarely needed):

# config/initializers/internationalize.rb
Internationalize.configure do |config|
  config.available_locales = [:en, :de, :fr]
end

Performance Comparison

Benchmark with 1000 records, 2 translated attributes (title + body), 3 locales:

Metric Internationalize Mobility (Table) Improvement
Storage 172 KB 332 KB 48% smaller
Create 0.27s 2.1s 7.8x faster
Read all 0.005s 0.37s 74x faster
Query (match) 0.001s 0.01s 10x faster

Trade-offs

Pros

  • Faster reads - No JOINs needed, translations are inline
  • Less storage - No separate translation tables with foreign keys and indices
  • Simpler schema - Everything in one table

Cons

  • Schema changes required - Each translated attribute needs a JSON column added to the table
  • Migration complexity - Adding translations to existing tables requires data migration
  • JSON column support - Requires SQLite 3.38+, PostgreSQL 9.4+, or MySQL 8.0+

When to use Internationalize

  • New projects where you can design the schema upfront
  • Applications with heavy read workloads
  • When you need maximum query performance

When to consider Mobility

Consider Mobility if:

  • You need to add translations without modifying existing table schemas
  • You're on PostgreSQL and want their JSON backend (note: PostgreSQL-only)
  • You need the flexibility of multiple backend strategies

Mobility's table backend stores translations in separate tables with JOINs, which trades query performance for schema flexibility. Their JSON backend is PostgreSQL-only.

License

MIT License. See LICENSE.txt.