BeMyWords with Rails i18n
A complete integration guide for Rails applications using the standard I18n backend and YAML locale files.
Shape of the integration:
- At deploy time (or periodically), fetch translations from BeMyWords and write them as
config/locales/<lang>.yml. - Rails's built-in
I18n.treads those YAML files as normal. - Optionally, push your master English YAML to BeMyWords so translators work from a canonical source.
BeMyWords stores translations as flat key/value JSON. Rails expects nested YAML keyed by locale. This guide handles the conversion in both directions.
Prerequisites
- A Rails app with
I18nalready wired up (the Rails default). - A BeMyWords workspace, project, and namespace.
- A BeMyWords API key scoped to the project.
1. Environment variables
Add to .env (or your Rails.application.credentials if you prefer):
BEMYWORDS_BASE_URL=https://www.bemywords.no
BEMYWORDS_PROJECT_ID=<your-project-uuid>
BEMYWORDS_API_TOKEN=<your-api-token>
BEMYWORDS_NAMESPACE=app
2. Fetch translations via a Rake task
Create lib/tasks/bemywords.rake:
require "net/http"
require "json"
require "yaml"
namespace :bemywords do
desc "Fetch translations from BeMyWords and write config/locales/<lang>.yml"
task fetch: :environment do
base_url = ENV.fetch("BEMYWORDS_BASE_URL", "https://www.bemywords.no")
project_id = ENV.fetch("BEMYWORDS_PROJECT_ID")
token = ENV.fetch("BEMYWORDS_API_TOKEN")
namespace = ENV.fetch("BEMYWORDS_NAMESPACE", "app")
languages = I18n.available_locales.map(&:to_s)
languages.each do |lang|
uri = URI("#{base_url}/api/#{project_id}/latest/#{lang}/#{namespace}")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Token token=#{token}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
http.request(req)
end
unless res.is_a?(Net::HTTPSuccess)
warn " [#{lang}] HTTP #{res.code} — skipping"
next
end
flat = JSON.parse(res.body)
# BeMyWords uses dot-separated flat keys ("user.login.title").
# Rails YAML expects nested hashes under the locale code.
nested = flat.each_with_object({}) do |(key, value), hash|
parts = key.split(".")
cursor = hash
parts[0...-1].each do |part|
cursor[part] ||= {}
cursor = cursor[part]
end
cursor[parts.last] = value
end
out_path = Rails.root.join("config", "locales", "#{lang}.yml")
File.write(out_path, { lang => nested }.to_yaml)
puts " [#{lang}] wrote #{flat.size} keys to #{out_path.relative_path_from(Rails.root)}"
end
end
end
Add the generated files to .gitignore if you want them rebuilt on every deploy:
/config/locales/en.yml
/config/locales/nb.yml
Or keep them committed and update via CI — whichever matches your workflow.
3. Run it
bundle exec rails bemywords:fetch
After that, use I18n.t as you normally would:
# In a controller, view, or anywhere
I18n.t("user.login.title")
4. Wire it into your build / deploy
Heroku (release phase)
In Procfile:
release: bundle exec rails db:migrate bemywords:fetch
Any CI platform
Run bundle exec rails bemywords:fetch as a build step before assets:precompile.
Runtime fetch (not recommended)
If you want runtime fetching (translations change without a deploy), you'd typically use an on-disk cache plus a background refresh job. Out of scope for this guide — the build-time pattern is simpler, faster, and cheaper.
5. (Optional) Push English master to BeMyWords
To sync your English YAML to BeMyWords so translators have a canonical source:
# Add to lib/tasks/bemywords.rake
desc "Push English YAML to BeMyWords as master source"
task push_english: :environment do
base_url = ENV.fetch("BEMYWORDS_BASE_URL", "https://www.bemywords.no")
project_id = ENV.fetch("BEMYWORDS_PROJECT_ID")
token = ENV.fetch("BEMYWORDS_API_TOKEN")
namespace = ENV.fetch("BEMYWORDS_NAMESPACE", "app")
en_yaml = YAML.load_file(Rails.root.join("config", "locales", "en.yml"))
nested = en_yaml["en"]
# Flatten Rails-style nested YAML to BeMyWords-style dot-separated keys.
flat = {}
flatten = lambda do |h, prefix|
h.each do |k, v|
key = prefix.empty? ? k.to_s : "#{prefix}.#{k}"
if v.is_a?(Hash)
flatten.call(v, key)
else
flat[key] = v.to_s
end
end
end
flatten.call(nested, "")
uri = URI("#{base_url}/api/overwrite/#{project_id}/latest/en/#{namespace}")
req = Net::HTTP::Put.new(uri, "Content-Type" => "application/json")
req["Authorization"] = "Token token=#{token}"
req.body = { translations: flat }.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
http.request(req)
end
if res.is_a?(Net::HTTPSuccess)
puts "Pushed #{flat.size} English keys to BeMyWords."
else
warn "Push failed: HTTP #{res.code} — #{res.body}"
exit 1
end
end
Run when you've changed English master strings:
bundle exec rails bemywords:push_english
6. Handling Rails-specific features
Pluralization
Rails i18n handles plurals with CLDR plural categories:
en:
apples:
one: "1 apple"
other: "%{count} apples"
In BeMyWords, store each plural form as a separate dot-key:
{
"apples.one": "1 apple",
"apples.other": "%{count} apples"
}
The fetch task nests these back under apples on output. No changes to how you call I18n.t("apples", count: 3).
Variable interpolation
Rails uses %{name} placeholders. BeMyWords stores strings verbatim — placeholders pass through unchanged.
en:
greeting: "Hello, %{name}!"
Use placeholder validation in your BeMyWords project settings to catch translators who drop or mangle %{name}.
Namespaces: per-engine or per-feature
If your app has engines or large feature silos, use separate BeMyWords namespaces per engine. Set BEMYWORDS_NAMESPACE dynamically in the rake task, or use multiple rake tasks (bemywords:fetch_main, bemywords:fetch_admin, etc.).
API reference
See the Astro integration guide — the API surface is identical, only the client differs.
Troubleshooting
KeyError: key not found: "BEMYWORDS_PROJECT_ID": env var not set. On Heroku, heroku config:set BEMYWORDS_PROJECT_ID=....
I18n::MissingTranslation in views after fetch: check the generated YAML structure. The flat-to-nested conversion in the rake task assumes dot-separated keys in BeMyWords correspond directly to the nested Rails structure. If you have dots in actual key names (rare), the conversion needs adjustment.
Pluralization shows translation missing: …: ensure all plural forms (one, other, plus few, many etc. for locales that need them) exist in BeMyWords for the target language.
Rails won't start because a YAML file is malformed: usually means the fetch ran with garbage data. Check the raw response from the API manually with curl.