Purizumu
Purizumu auto-translates Rails model attributes and stores the translated content in your database. Translations are generated lazily the first time an attribute is read for a locale that does not already exist in the translation table.
For example, if Gem#human_name is marked as translatable and a request reads that attribute while I18n.locale is :jp, Purizumu checks its translation table first. If no Japanese translation exists yet, it asks the configured translation engine to generate one, stores the result, and returns it.
Installation
gem install purizumu
Or add it to your Rails application's Gemfile:
gem "purizumu"
Then install dependencies:
bundle install
Generate the migration and migrate:
rails generate purizumu:install
rails db:migrate
Configuration
Create an initializer such as config/initializers/purizumu.rb:
Purizumu.configure do |config|
config.engine = :xai
config.key = ENV["XAI_TRANSLATE_KEY"]
config.model = "grok-4.20-reasoning"
end
Available configuration options:
config.engine: translation engine symbol. Purizumu currently ships with:xaiand:test(see Test Mode).config.key: API key for the configured engine.config.model: LLM model name.config.base_url: API base URL. Defaults tohttps://api.x.ai/v1.config.source_locale: source locale used for untranslated records. Defaults toI18n.default_locale.config.transport: optional callable used for HTTP requests. Mainly useful for testing or custom networking.
Usage
Mark model attributes as translatable:
class Gem < ApplicationRecord
attribute_translation :human_name
end
When the attribute is read, Purizumu checks the translations table before falling back to generation:
class GemsController < ApplicationController
def index
gem = Gem.find(48_127)
I18n.locale = :en
puts gem.human_name
I18n.locale = :jp
puts gem.human_name
I18n.locale = :ru
puts gem.human_name
end
end
Example output:
Prism
プリズム
Призма
How It Works
Purizumu stores translations in purizumu_translations. Each translation row is keyed by:
model_class_namerecord_idattribute_namelocale
The table also stores the translated content.
This means a single model record can have a different translation for each tracked attribute and locale.
Example rows:
| model_class_name | record_id | attribute_name | locale | content |
|---|---|---|---|---|
| Gem | 48127 | human_name | en | Prism |
| Gem | 48127 | human_name | es | Prisma |
| Gem | 48127 | human_name | jp | プリズム |
| Gem | 48127 | human_name | ru | Призма |
| Gem | 73491 | human_name | en | Different Gem |
| Gem | 73491 | human_name | es | Gema Diferente |
| Gem | 73491 | human_name | jp | 異なる宝石 |
| Gem | 73491 | human_name | ru | Другой самоцвет |
Purizumu intentionally uses Rails-safe key names in the translation table:
record_idis used instead of a plainidcolumn because the table needs its own primary key.model_class_nameis used instead ofmodel_namebecausemodel_nameis already defined by Active Record internals.
xAI / Grok Engine
The bundled xAI engine uses the chat completions API and enforces structured output with a function tool. It does not rely on "please return JSON" prompting alone. Purizumu sends a single required function definition and forces the model to answer through that tool call.
The expected tool payload is:
{
"translated_text": "..."
}
Test Mode
In a test suite you usually do not want translated attribute reads to hit the configured translation engine's API. Enable test mode and Purizumu will stub translations locally without any network requests:
Purizumu.test_mode!
While test mode is on, Purizumu.engine returns a stub engine regardless of the configured engine, and no API key or model is required. By default, the stub returns "[<locale>] <original content>", so "Prism" translated to :jp becomes "[jp] Prism" — deterministic and easy to assert against.
You can also supply a block to control what the stub returns. The block receives the full translation request as keyword arguments (model_name, attribute_name, content, locale, source_locale):
Purizumu.test_mode! do |content:, locale:, **|
"#{content} (#{locale})"
end
To turn it back off (and clear any custom block):
Purizumu.disable_test_mode!
You can check the current state with Purizumu.test_mode?.
RSpec
Enable it once for the entire run in spec/spec_helper.rb (or rails_helper.rb):
RSpec.configure do |config|
config.before(:suite) do
Purizumu.test_mode!
end
end
Or, if you only want it for specific examples, enable it per test:
before { Purizumu.test_mode! }
after { Purizumu.disable_test_mode! }
Test mode is independent of configuration, so it survives Purizumu.reset_configuration! — enabling it in before(:suite) keeps it on even if individual tests reset or rebuild the configuration.
Configuring the Test Engine Directly
If you prefer plain configuration — for example in a Rails config/environments/test.rb setup where nothing resets Purizumu's configuration — the stub engine is also available as a regular engine:
Purizumu.configure do |config|
config.engine = :test
end
This behaves like test mode with no block: no network requests, no API key or model required, and the same "[<locale>] <original content>" stub output. The difference is that it lives in the configuration, so Purizumu.reset_configuration! or a later Purizumu.configure will replace it. Purizumu.test_mode! still overrides whatever engine is configured.
Generator Output
rails generate purizumu:install creates a migration for the translation table with:
model_namemodel_class_namerecord_idattribute_namelocalecontent
It also adds a unique composite index across model_class_name, record_id, attribute_name, and locale.
Development
Run the test suite:
bundle exec rspec
Run Rubocop:
bundle exec rubocop