
Translate structured content from Ruby with the PolyLingo gem
By Robert M
Translate structured content from Ruby with the PolyLingo gem
The PolyLingo Ruby gem is now available on RubyGems. It covers the full PolyLingo API surface: synchronous translation, batch requests, async jobs with polling, and all utility endpoints. It has no runtime dependencies and requires Ruby 2.7 or later.
This post walks through installation, the client setup, and every method available in the gem.
Installation
Add to your Gemfile:
gem "polylingo"
Then run:
bundle install
Or install directly:
gem install polylingo
No runtime dependencies. The gem uses only Ruby's standard library for HTTP.
Setting up the client
require "polylingo"
client = PolyLingo.new(api_key: ENV.fetch("POLYLINGO_API_KEY"))
Two optional settings are available:
client = PolyLingo.new(
api_key: ENV.fetch("POLYLINGO_API_KEY"),
base_url: "https://api.usepolylingo.com/v1", # default, override for self-hosted instances
timeout: 120, # seconds (open + read), default 120
)
Keep your API key in an environment variable. Never hardcode it and never commit it to version control.
Translating content
Single request
Pass your content, an array of target language codes, and an optional format:
result = client.translate(
content: "# Hello\n\nThis is **structured** content.",
targets: %w[es fr de],
format: "markdown"
)
puts result["translations"]["es"]
puts result["usage"]["total_tokens"]
The format parameter accepts plain, markdown, json, or html. If omitted, the API auto-detects the format. You can also pass source as a language hint and model as either "standard" (default) or "advanced".
Format preservation works the same way as the rest of the PolyLingo API. For json, only string values are translated and keys, nesting, and non-string types are left untouched. For markdown, headings stay headings and code blocks are left verbatim. For html, tags and attributes are preserved and only text nodes are translated.
Translating a JSON locale file
require "json"
source = JSON.parse(File.read("config/locales/en.yml"))
result = client.translate(
content: source.to_json,
format: "json",
targets: %w[fr de ja]
)
%w[fr de ja].each do |locale|
translated = JSON.parse(result["translations"][locale])
File.write(
"config/locales/#{locale}.json",
JSON.pretty_generate(translated)
)
puts "Wrote config/locales/#{locale}.json"
end
One request handles all three locales. Keys are identical to the source in every output file.
Batch requests
Send up to 100 content items in a single request, each with its own id and optional format:
result = client.batch(
items: [
{ id: "hero_title", content: "Welcome back", format: "plain" },
{ id: "hero_subtitle", content: "Here is what changed today", format: "plain" },
{ id: "cta", content: "Get started", format: "plain" },
],
targets: %w[es fr de]
)
result["results"].each do |row|
puts "#{row["id"]}: #{row["translations"]["es"]}"
end
All items share the same targets array. The id you pass in is preserved in the response so you can map results back to your source data without relying on order.
Async jobs
For long-running translations, the jobs API accepts the request, returns a job_id immediately, and lets you poll for the result. The gem handles this two ways.
Enqueue and poll manually
accepted = client.jobs.create(
content: File.read("content/long-article.md"),
targets: %w[es fr de ja zh],
format: "markdown"
)
job_id = accepted["job_id"]
loop do
sleep 5
state = client.jobs.get(job_id)
break if %w[complete failed].include?(state["status"])
end
if state["status"] == "complete"
translations = state["translations"]
end
One call that polls until done
done = client.jobs.translate(
content: File.read("content/long-article.md"),
targets: %w[es fr de],
format: "markdown",
# Optional overrides (defaults shown):
poll_interval: 5, # seconds between polls, default 5
timeout: 1200, # total wait budget in seconds, default 1200 (20 minutes)
on_progress: ->(queue_position) { puts "Queue position: #{queue_position.inspect}" }
)
translations = done["translations"]
usage = done["usage"]
All time values in the Ruby gem use seconds, consistent with Ruby conventions. The on_progress lambda fires on each poll and receives the current queue position as an integer, or nil if the API does not return one.
Utility endpoints
health = client.health
# => { "status" => "ok", "timestamp" => "..." }
languages = client.languages
# => { "languages" => [{ "code" => "en", "name" => "English", "rtl" => false }, ...] }
usage = client.usage
# => { "usage" => { "tokens_used" => 12000, "tokens_remaining" => 38000, ... } }
health and languages require no API key. usage returns token consumption for the current calendar month for the authenticated account.
Error handling
All errors inherit from PolyLingo::PolyLingoError. Catch the specific subclasses you want to handle:
begin
result = client.translate(
content: "# Hello",
targets: %w[es],
format: "markdown"
)
rescue PolyLingo::AuthError => e
# HTTP 401 — invalid, missing, or revoked API key
puts "Auth failed: #{e.message}"
rescue PolyLingo::RateLimitError => e
# HTTP 429 — per-minute limit reached
retry_after = e.retry_after # integer seconds or nil
sleep retry_after if retry_after
retry
rescue PolyLingo::JobFailedError => e
# Async job reached a failed terminal status, or polling timed out
puts "Job failed: #{e.job_id}"
rescue PolyLingo::PolyLingoError => e
# All other API errors
puts "#{e.status}: #{e.error} — #{e.message}"
end
RateLimitError#retry_after returns an integer from either the JSON response body or the Retry-After header, whichever is present, or nil if neither is included. JobFailedError#job_id gives you the ID of the failed job when it is known.
Quick reference
| Method | Endpoint | Auth required |
|---|---|---|
client.health | GET /health | No |
client.languages | GET /languages | No |
client.translate(...) | POST /translate | Yes |
client.batch(...) | POST /translate/batch | Yes |
client.usage | GET /usage | Yes |
client.jobs.create(...) | POST /jobs | Yes |
client.jobs.get(job_id) | GET /jobs/:id | Yes |
client.jobs.translate(...) | POST /jobs + polling | Yes |
Get started
The gem is on RubyGems at rubygems.org/gems/polylingo. Source code is at github.com/UsePolyLingo/polylingo-ruby. Full API documentation is at usepolylingo.com/docs.
The free tier includes 50,000 tokens per month. No credit card required.
gem install polylingo