# Configuring Translations with the Mobility Gem in PropertyWebBuilder

> Learn to configure translations in PropertyWebBuilder using the Mobility gem. Master multilingual CMS content with JSONB, automatic fallbacks, and GIN-indexed queries.

- Repository: [Ed Tee/property_web_builder](https://github.com/etewiah/property_web_builder)
- Tags: how-to-guide
- Published: 2026-03-01

---

**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`](https://github.com/etewiah/property_web_builder/blob/main/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

```ruby
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`](https://github.com/etewiah/property_web_builder/blob/main/db/migrate/20251204205742_add_mobility_translations_columns.rb) adds the column with proper indexing:

```ruby
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`](https://github.com/etewiah/property_web_builder/blob/main/app/models/pwb/page.rb) (lines 49-53), SEO-related fields are translated:

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

```

For simpler content blocks, [`app/models/pwb/content.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/models/pwb/content.rb) (lines 49-51) translates a single attribute:

```ruby
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:

```ruby
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`](https://github.com/etewiah/property_web_builder/blob/main/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`](https://github.com/etewiah/property_web_builder/blob/main/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`](https://github.com/etewiah/property_web_builder/blob/main/app/controllers/api_public/v1/localized_pages_controller.rb) (lines 51-59) strips regional suffixes:

```ruby
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:

```ruby

# 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`](https://github.com/etewiah/property_web_builder/blob/main/app/models/pwb/page.rb):

```ruby
extend Mobility
translates :raw_html, :page_title, :link_title,
           :seo_title, :meta_description, :meta_keywords,
           :subtitle  # New field

```

2. **Use the accessors**:

```ruby
page = Pwb::Page.find(1)
page.subtitle_fr = "Bienvenue"
page.save!

I18n.locale = :fr
puts page.subtitle  # => "Bienvenue" (falls back if blank)

```

3. **Admin integration** happens automatically through `mobility_attribute_names`, which now includes `subtitle_en`, `subtitle_fr`, etc.

## Summary

- **Global configuration** in [`config/initializers/mobility.rb`](https://github.com/etewiah/property_web_builder/blob/main/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`](https://github.com/etewiah/property_web_builder/blob/main/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`](https://github.com/etewiah/property_web_builder/blob/main/db/migrate/20251204205742_add_mobility_translations_columns.rb).