Back to all docs

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.


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.