Back to all docs

This integration guide is available in English only.

BeMyWords with Rails i18n

A complete integration guide for Rails applications using the standard I18n backend and YAML locale files.

Shape of the integration:

  1. At deploy time (or periodically), fetch translations from BeMyWords and write them as config/locales/<lang>.yml.
  2. Rails's built-in I18n.t reads those YAML files as normal.
  3. 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.


Before and after

The point of this integration is that you stop maintaining non-English locale files by hand. Concretely:

Before — every language is a file in your repo that a developer edits:

config/locales/
├── en.yml      ← you add a key here
├── nb.yml      ← …then remember to add it here
├── sv.yml      ← …and here
└── de.yml      ← …and here, usually with a TODO or the English string
# A new string means a pull request touching every locale file, and a
# separate round-trip to whoever actually speaks the language.
t("billing.invoice.overdue_notice")
# => "translation missing: nb.billing.invoice.overdue_notice"

After — English is the only locale file you write. The rest are build artifacts:

config/locales/
├── en.yml      ← the only file you edit (or push from BeMyWords)
├── nb.yml      ← generated by rake bemywords:fetch, gitignored
├── sv.yml      ← generated
└── de.yml      ← generated
# Add the key once, in English. The next deploy fetches every other
# language — already translated, reviewed in BeMyWords, with the
# glossary and do-not-translate terms applied.
t("billing.invoice.overdue_notice")
# => "Faktura forfalt til betaling"

The diff in your deploy configuration is one line:

  release: |
    bundle exec rails db:migrate
+   bundle exec rails bemywords:fetch

Nothing changes in your application code: I18n.t still reads YAML from config/locales, and there is no runtime dependency on BeMyWords. If the fetch fails, the previous YAML is still on disk and the app keeps serving the last known-good translations.


Prerequisites

  • A Rails app with I18n already 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.

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.