Back to blog
Terminal window showing a Gemfile entry for the PolyLingo gem alongside a short Ruby translate call and its output hash.

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

MethodEndpointAuth required
client.healthGET /healthNo
client.languagesGET /languagesNo
client.translate(...)POST /translateYes
client.batch(...)POST /translate/batchYes
client.usageGET /usageYes
client.jobs.create(...)POST /jobsYes
client.jobs.get(job_id)GET /jobs/:idYes
client.jobs.translate(...)POST /jobs + pollingYes

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

Get your API key