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.
| Volume | Google Translate | DeepL | Langbly | You Save |
|---|---|---|---|---|
| 500K chars/mo | $0 | Check live plan | $0 | Both included/evaluation |
| 1M chars/mo | $10 | Check live plan | $2.50 | 75% vs Google |
| 5M chars/mo | $90 | Check live plan | $22.50 | 75% vs Google |
| 25M chars/mo | $490 | Check live plan | $122.50 | 75% vs Google |
| 100M chars/mo | $1,990 | Above Growth limit | $497.50 | 75% vs Google |
Feature comparison
| Feature | Google Translate | DeepL | Langbly |
|---|---|---|---|
| Ruby gem | google-cloud-translate | deepl-rb (community) | None needed, plain HTTP |
| Official Ruby SDK | |||
| Works with Net::HTTP / Faraday | REST v2 | ||
| Auth method | Service account JSON | DeepL-Auth-Key header | Bearer token |
| Steps to first call | 5+ steps | 2 steps | 2 steps |
| Fits Rails credentials | JSON file on disk | ||
| Google v2 request format | |||
| Batch translate (array of strings) | |||
| HTML-safe mode | tag_handling | format: "html" | |
| Source language detection | |||
| Languages | 100+ | 30+ | 100+ |
| Published usage rate | $20/1M after 500K | Check live Growth plan | $5/1M after 500K |
| Included/evaluation usage | 500K/mo | Developer: 1M total | 500K/mo |
| Translation quality | NMT | NMT + AI | Next-gen AI |
| Context-aware | Limited |
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.