500K characters included every month. Add payment details to start.
Ruby & Rails Developer Guide

Best Translation API
for Ruby and Rails

Compare Google Translate, DeepL, and Langbly from a Ruby perspective. Real Net::HTTP and Faraday code, a Rails service object, I18n locale files, background jobs, and what each one costs.

Payment details required. No charge within 500K characters/month.

Pricing comparison

Real pricing based on monthly character volume. All prices in USD.

VolumeGoogle TranslateDeepLLangblyYou Save
500K chars/mo$0Check live plan$0
Both included/evaluation
1M chars/mo$10Check live plan$2.50
75% vs Google
5M chars/mo$90Check live plan$22.50
75% vs Google
25M chars/mo$490Check live plan$122.50
75% vs Google
100M chars/mo$1,990Above Growth limit$497.50
75% vs Google

Feature comparison

FeatureGoogle TranslateDeepLLangbly
Ruby gemgoogle-cloud-translatedeepl-rb (community)None needed, plain HTTP
Official Ruby SDK
Works with Net::HTTP / FaradayREST v2
Auth methodService account JSONDeepL-Auth-Key headerBearer token
Steps to first call5+ steps2 steps2 steps
Fits Rails credentialsJSON file on disk
Google v2 request format
Batch translate (array of strings)
HTML-safe modetag_handlingformat: "html"
Source language detection
Languages100+30+100+
Published usage rate$20/1M after 500KCheck live Growth plan$5/1M after 500K
Included/evaluation usage500K/moDeveloper: 1M total500K/mo
Translation qualityNMTNMT + AINext-gen AI
Context-awareLimited

Three ways to call a translation API from Ruby

Ruby developers rarely need a heavy client library for a single POST request. Here is the same translation, "Hello world" into Dutch, written three ways.

Google Translate (google-cloud-translate gem):

require "google/cloud/translate/v2"

translate = Google::Cloud::Translate::V2.new(
  project_id: "your-gcp-project",
  credentials: "config/service-account.json"
)

translation = translate.translate("Hello world", to: "nl")
puts translation.text
# => "Hallo wereld"

DeepL (deepl-rb, community gem):

require "deepl"

DeepL.configure { |config| config.auth_key = ENV.fetch("DEEPL_AUTH_KEY") }

translation = DeepL.translate("Hello world", nil, "NL")
puts translation.text

Langbly (standard library, no gem):

require "net/http"
require "json"
require "uri"

uri = URI("https://api.langbly.com/language/translate/v2")

request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('LANGBLY_API_KEY')}"
request["Content-Type"]  = "application/json"
request.body = JSON.generate(q: "Hello world", target: "nl")

response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
  http.request(request)
end

payload = JSON.parse(response.body)
puts payload.dig("data", "translations", 0, "translatedText")
# => "Hallo wereld"

The response body is the familiar Google Translate v2 envelope:

{"data":{"translations":[{"translatedText":"Hallo wereld","detectedSourceLanguage":"en"}]}}

Two things worth calling out. First, there is no official Langbly gem. Official SDKs exist for Python, JavaScript/TypeScript, and PHP, and Ruby is covered by plain HTTP because the API is small enough that a gem would only get in the way. Second, Langbly is a drop-in replacement for the Google Translate v2 API. Request shape, field names, and response shape are the same, so any Google v2 style code you already have keeps working once you point it at https://api.langbly.com/language/translate/v2 and swap the auth header for Authorization: Bearer YOUR_API_KEY.

Optional body fields: source to skip detection, and format, which is either "text" or "html". Use "html" when you are translating rich text and want tags left intact.

A Rails service object you can drop in

In a Rails app you want one place that knows about the API, reads the key from credentials, handles 429s, and returns a plain String. A small PORO does the job. Faraday is already in most Rails dependency trees through other gems, but Net::HTTP works just as well if you would rather not add one.

# app/services/translator.rb
class Translator
  ENDPOINT = "https://api.langbly.com/language/translate/v2".freeze

  class Error < StandardError; end
  class RateLimited < Error; end

  def initialize(api_key: nil)
    @api_key = api_key ||
      Rails.application.credentials.dig(:langbly, :api_key) ||
      ENV.fetch("LANGBLY_API_KEY")
  end

  # text can be a String or an Array of Strings.
  # Returns a String or an Array, matching what you passed in.
  def call(text, target:, source: nil, format: "text")
    body = { q: text, target: target, format: format }
    body[:source] = source if source

    response = connection.post(ENDPOINT, body)

    case response.status
    when 200
      extract(response.body, text)
    when 429
      raise RateLimited, "Rate limited, retry after #{response.headers['retry-after']}s"
    else
      raise Error, "Translation failed (#{response.status}): #{response.body}"
    end
  end

  private

  def connection
    @connection ||= Faraday.new do |f|
      f.request :json
      f.response :json
      f.options.timeout = 20
      f.options.open_timeout = 5
      f.headers["Authorization"] = "Bearer #{@api_key}"
    end
  end

  def extract(body, original)
    texts = Array(body.dig("data", "translations")).map { |t| t["translatedText"] }
    original.is_a?(Array) ? texts : texts.first
  end
end

Store the key with rails credentials:edit:

langbly:
  api_key: your_api_key_here

Then call it from anywhere:

Translator.new.call("Hello world", target: "nl")
# => "Hallo wereld"

Translator.new.call(["Hello", "Goodbye"], target: "de")
# => ["Hallo", "Auf Wiedersehen"]

User facing translation should never block a request. Push it into ActiveJob and let the queue absorb rate limits:

# app/jobs/translate_post_job.rb
class TranslatePostJob < ApplicationJob
  queue_as :translations

  retry_on Translator::RateLimited, wait: :polynomially_longer, attempts: 5
  discard_on ActiveRecord::RecordNotFound

  def perform(post_id, locale)
    post = Post.find(post_id)

    translated = Translator.new.call(post.body, target: locale, format: "html")

    translation = post.translations.find_or_initialize_by(locale: locale)
    translation.update!(body: translated, source_checksum: Digest::SHA256.hexdigest(post.body))
  end
end

Storing a checksum of the source text means you can skip re-translating rows that have not changed, which is the single biggest lever on your bill once a content catalogue gets large.

Compare that setup to Google. There you need a Google Cloud project, an enabled API, a service account, a downloaded JSON key, and a way to get that file onto every dyno or container without committing it. On Heroku or a container platform that alone is an afternoon. A bearer token in Rails credentials is one line.

Translating your I18n locale files with a rake task

Most Rails apps carry a config/locales/en.yml that has grown for years. Translating it by hand is miserable, and it goes stale the moment someone adds a key. A rake task that walks the YAML tree and writes sibling locale files solves it in one command.

# lib/tasks/i18n.rake
namespace :i18n do
  desc "Translate config/locales/en.yml into other locales: rake 'i18n:translate[nl,de,fr]'"
  task :translate, [:locales] => :environment do |_task, args|
    translator = Translator.new
    source = YAML.load_file(Rails.root.join("config/locales/en.yml")).fetch("en")

    walk = lambda do |node, locale|
      case node
      when Hash   then node.transform_values { |value| walk.call(value, locale) }
      when Array  then node.map { |value| walk.call(value, locale) }
      when String then translator.call(node, target: locale)
      else node
      end
    end

    locales = args[:locales].to_s.split(",").map(&:strip).reject(&:empty?)
    abort "Usage: rake 'i18n:translate[nl,de,fr]'" if locales.empty?

    locales.each do |locale|
      path = Rails.root.join("config/locales/#{locale}.yml")
      File.write(path, { locale => walk.call(source, locale) }.to_yaml(line_width: -1))
      puts "Wrote #{path}"
    end
  end
end

Run it with rake 'i18n:translate[nl,de,fr]' and commit the diff like any other change. Two practical notes. Keep line_width: -1 so Psych does not wrap long strings into a format that is painful to review. And do a spot check on keys that contain %{name} interpolation or pluralization subtrees before you ship, because those are where any translation service is most likely to surprise you.

If your locale file is big, batch it. Passing an array of strings in a single q field is far cheaper in wall clock time than one request per key, and the service object above already accepts an array.

Volume is where the price difference stops being academic. Google and Langbly both include the first 500,000 characters each month. At 5M total characters, their current standard text totals are $90 and $22.50; at 100M they are $1,990 and $497.50. DeepL requires its live Growth subscription and overage for a fair comparison.

Frequently asked questions

What is the best translation API for Ruby?

For most Ruby and Rails apps, Langbly gives you the shortest path: a bearer token, one POST request, and a response in the Google Translate v2 format that any Ruby HTTP client can parse. DeepL is a reasonable choice if you only work with European languages. Google Translate has broad coverage but the heaviest setup, since it wants a Google Cloud project and a service account JSON file.

Is there a Langbly Ruby gem?

No. There is no official Langbly gem, and you do not need one. The API is a single JSON endpoint, so Net::HTTP from the standard library or Faraday is enough, usually under ten lines. Official SDKs exist for Python, JavaScript/TypeScript, and PHP. Wrapping the endpoint in a small service object is the idiomatic Rails approach anyway.

How do I translate text in a Rails app?

Put the API key in Rails credentials, add a service object under app/services that POSTs to https://api.langbly.com/language/translate/v2 with an Authorization bearer header and a JSON body of {"q": "Hello world", "target": "nl"}, then read data.translations[0].translatedText from the response. Call that service from an ActiveJob so translation never blocks a web request.

Can I switch from the Google Translate API to Langbly without rewriting my Ruby code?

Mostly yes, if you are calling the REST API directly. Langbly is a drop-in replacement for the Google Translate v2 API, so the request body and the response JSON are identical. You change the base URL to https://api.langbly.com/language/translate/v2 and send an Authorization bearer header instead of a Google API key or service account token. If you use the google-cloud-translate gem, you replace the gem call with a plain HTTP call, which is the part that takes an hour rather than a day.

Can I translate Rails I18n locale files automatically?

Yes. A rake task can load config/locales/en.yml, walk the nested hash, translate every String leaf, and write config/locales/nl.yml and friends. Batch the strings into a single request where you can, and review keys with %{interpolation} placeholders or pluralization rules before committing. There is a working rake task on this page you can copy.

Ready to try Langbly?

500K characters included every month. Migrate in minutes.

Same API as Google Translate v2. Just change the URL.