Configuring Translations with the Mobility Gem in PropertyWebBuilder

PropertyWebBuilder stores multilingual CMS content using the Mobility gem's container backend, which serializes all language versions into a single JSONB column with automatic fallback support and GIN-indexed queries.

PropertyWebBuilder is an open-source Rails-based real estate CMS that manages multilingual pages, properties, and content through the Mobility gem. Configuring translations with the Mobility gem enables the application to maintain a single database schema across all supported languages while providing efficient JSONB storage and query capabilities. This implementation replaces legacy Globalize patterns with a modern container-based approach that simplifies both database migrations and runtime performance.

Global Mobility Configuration

The foundation of PropertyWebBuilder's translation system is established in the global initializer. The configuration selects the container backend and activates essential plugins for ActiveRecord integration.

In config/initializers/mobility.rb (lines 4-37), the setup configures:

  • ActiveRecord plugin as the base ORM integration
  • Container backend storing translations in a jsonb column named translations
  • Locale accessors generating methods like title_en and title_es for every locale in I18n.available_locales
  • Query plugin enabling translated attribute lookups in where clauses
  • Caching to reduce database hits on repeated translations
  • Presence and fallbacks treating blank strings as nil and defaulting missing translations to English
Mobility.configure do
  plugins do
    active_record
    backend :container
    reader
    writer
    backend_reader
    locale_accessors I18n.available_locales
    query
    cache
    presence
    fallbacks
    fallbacks({ :'en-US' => :en, :'en-GB' => :en, :'es-ES' => :es, :'es-MX' => :es, :'fr-FR' => :fr, :'fr-CA' => :fr })
  end
end

Database Schema Setup

During the migration from Globalize to Mobility, each translatable table receives a dedicated JSONB column. The migration file db/migrate/20251204205742_add_mobility_translations_columns.rb adds the column with proper indexing:

add_column :pwb_props,    :translations, :jsonb, default: {}, null: false
add_index  :pwb_props,    :translations, using: :gin

# Repeated for pwb_pages, pwb_contents, pwb_links

The GIN index on the translations column optimizes JSONB key lookups, particularly when combined with Mobility's query plugin for searching within specific language keys.

Declaring Translatable Attributes in Models

Any ActiveRecord model requiring multilingual support extends Mobility and declares translatable fields using the translates macro. This automatically generates locale-specific accessors based on the initializer's locale_accessors configuration.

In app/models/pwb/page.rb (lines 49-53), SEO-related fields are translated:

extend Mobility
translates :raw_html, :page_title, :link_title,
           :seo_title, :meta_description, :meta_keywords

For simpler content blocks, app/models/pwb/content.rb (lines 49-51) translates a single attribute:

extend Mobility
translates :raw

These declarations immediately provide methods like page_title_en, page_title_es, raw_fr, etc., allowing direct assignment and retrieval of specific language versions.

Admin and API Helpers for Translation Management

To expose translatable fields in administrative interfaces and JSON APIs, models implement dynamic attribute enumeration. The mobility_attribute_names method constructs a list of all possible translation keys:

def mobility_attribute_names
  attributes = []
  self.class.mobility_attributes.each do |attr|
    I18n.available_locales.each do |locale|
      attributes << "#{attr}_#{locale}".to_sym
    end
  end
  attributes
end

In app/models/pwb/page.rb (lines 120-128), this list feeds into admin_attribute_names to ensure admin payloads include every translation field. Similarly, app/models/pwb/content.rb (lines 63-71) combines these attributes with associated photos for complete content serialization.

Locale Normalization for URL Routing

When handling regional locale variants (e.g., en-US) in public URLs, controllers normalize these to base locales that Mobility recognizes. The normalize_locale_for_mobility method in app/controllers/api_public/v1/localized_pages_controller.rb (lines 51-59) strips regional suffixes:

def normalize_locale_for_mobility(locale)
  return nil if locale.blank?
  base_locale = locale.to_s.split('-').first.to_sym
  I18n.available_locales.include?(base_locale) ? base_locale : nil
end

This ensures routes like /en-US/p/home correctly resolve to the :en translation stored within the JSONB container.

Fallback Behavior and Querying

Mobility handles translation fallbacks transparently. When accessing page.seo_title with I18n.locale set to :fr, if no French translation exists, the system returns the English version per the fallback configuration in the initializer.

For database queries, the query plugin enables scoped searches:


# Search across all locales

pages = Pwb::Page.i18n { where('seo_title ILIKE ?', '%Beach%') }

# Target specific locale

pages_es = Pwb::Page.i18n(:es).where(seo_title: 'Playa')

The block syntax i18n { ... } generates proper JSONB queries against the container column.

Adding New Translatable Fields

To add a translatable attribute to an existing model:

  1. Update the model in app/models/pwb/page.rb:
extend Mobility
translates :raw_html, :page_title, :link_title,
           :seo_title, :meta_description, :meta_keywords,
           :subtitle  # New field
  1. Use the accessors:
page = Pwb::Page.find(1)
page.subtitle_fr = "Bienvenue"
page.save!

I18n.locale = :fr
puts page.subtitle  # => "Bienvenue" (falls back if blank)
  1. Admin integration happens automatically through mobility_attribute_names, which now includes subtitle_en, subtitle_fr, etc.

Summary

  • Global configuration in config/initializers/mobility.rb sets the container backend with JSONB storage, locale accessors, and fallback chains.
  • Database migrations add a translations JSONB column with GIN indexing to tables like pwb_pages and pwb_contents.
  • Model declarations use extend Mobility and translates to define multilingual attributes, automatically generating per-locale accessor methods.
  • Admin helpers such as mobility_attribute_names dynamically enumerate all translation fields for API serialization.
  • Locale normalization in controllers converts regional codes (e.g., en-US) to base locales before querying Mobility.
  • Query plugin enables efficient JSONB searches using Model.i18n { where(...) } syntax.

Frequently Asked Questions

How does PropertyWebBuilder store translations in the database?

PropertyWebBuilder uses Mobility's container backend, which stores all language versions of an attribute in a single jsonb column named translations. This approach replaces the traditional pattern of separate translation tables, reducing schema complexity and enabling GIN-indexed queries across all locales simultaneously.

What is the difference between page.title and page.title_en in Mobility?

page.title returns the translation for the current I18n.locale, applying any configured fallbacks if the translation is missing. page.title_en directly accesses the English version stored in the JSONB container, bypassing the fallback chain. The locale-specific accessors are generated automatically by the locale_accessors plugin configured in config/initializers/mobility.rb.

How do I query records by a translated attribute in a specific language?

Use Mobility's query plugin with the i18n scope method. For example, Pwb::Page.i18n(:es).where(seo_title: 'Playa') searches only the Spanish translation within the JSONB column. Alternatively, Pwb::Page.i18n { where('seo_title ILIKE ?', '%Beach%') } searches across all locales using the GIN index on the translations column.

Can I migrate existing Globalize data to Mobility in PropertyWebBuilder?

Yes. The repository includes lib/tasks/mobility_migration.rake, which provides tasks for migrating legacy Globalize translation tables into the Mobility container format. The migration preserves all existing translations by moving them into the JSONB translations column added via db/migrate/20251204205742_add_mobility_translations_columns.rb.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →