lingorelay / guides / stop paying twice

Guide

Stop paying for the same translation twice.

Most machine-translation bills aren't for translation. They're for re-translation — the same 200 dish names, bought again every time the menu renders. The fix is one line of thinking: make the translation addressable by its content.

Disclosure: we make lingorelay, a translation API with this cache built in. But the idea in this guide is provider-neutral — it's a hash and a database table, and you can absolutely build it yourself in front of any translation API. lingorelay launched in 2026; the pattern is much older than we are.

The re-purchase problem

Per-character translation pricing is fair for new text. The trouble is that most production pipelines don't send new text — they send the same text again. A menu page that calls translate-on-render, a catalog that rebuilds nightly, a CMS that re-publishes on every edit: each run pays full price for strings that were translated identically last time. The output is byte-for-byte the same; only the invoice grows.

The cache key that fixes it

A translation is fully determined by three things: the source text, the target language, and whatever rules shaped it (in lingorelay, the project glossary). So address it by exactly those three:

the whole ideaone line
cacheKey = sha256( text + "|" + targetLang + "|" + glossaryVersion )

# before calling the provider:
#   hit  → return the stored translation. Cost: one DB read. Metered: 0 chars.
#   miss → translate once, store under cacheKey, return it.

Each part of the key earns its place:

  • text — hashing the full source string means only an exact repeat hits the cache. Change one character and it's honestly a new translation.
  • targetLang — "fr" and "ja" are different products of the same input; they cache separately.
  • glossaryVersion — the quiet hero. When you edit your glossary ("never translate Dinify", "render prawns as crevettes"), the version bumps, every affected key changes, and stale entries simply stop being found. Invalidation happens by construction — no cache-clearing cron job, no "why is it still showing the old wording" tickets.

Two implementation details worth copying: dedupe inside a batch too (the same string twice in one request should be translated — and paid for — once), and make cache reads free and unlimited, so a quota cap never takes already-translated content offline.

A worked example: one menu, three languages, rendered daily

Assumptions, stated plainly: a 200-item menu; each item's name + short description averages ~60 characters; you serve French, Japanese and Spanish; the menu page re-renders (and re-requests its translations) once a day for a month. Characters, not dollars — multiply by whatever your provider charges per character.

Characters actually sent to the model, over 30 days

 Translate-on-render (no cache)Content-hash cache
Day 1 (first pass)200 × 60 × 3 = 36,000200 × 60 × 3 = 36,000
Days 2–30 (nothing changed)29 × 36,000 = 1,044,0000 — every request is a cache hit
Mid-month: you reword 10 dishesincluded above10 × 60 × 3 = 1,800
Month total1,080,000 chars37,800 chars (~29× less)

In lingorelay plan terms: the cached column fits inside Starter's 200,000 monthly characters ($29 CAD/mo) with room for many more edits; the uncached column overruns Starter ~5× and even Growth's 1,000,000 ($79 CAD/mo) — for byte-identical output. The arithmetic is the argument.

Where this honestly doesn't help

  • First-pass costs are the same as anyone's. A string the model has never seen must hit the model once, and we meter it exactly like any provider would charge for it. The cache buys you nothing on day one — it pays off on every day after.
  • Low-repetition content caches badly. Live chat, user-generated reviews, one-off emails — mostly unique strings, mostly cache misses. If that's your whole workload, a cache layer is overhead, ours included.
  • You can build this yourself. A hash column, a unique index, an upsert — a competent developer has v1 in an afternoon in front of any translation API. What you'd still have to build is the unglamorous rest: glossary versioning, per-language purging, batch dedupe, hit-count visibility, and quota rules that keep cached content serving. That finished version is the product we sell.
14-day free trial · no card

See your own hit rate, not our example's.

Create a project, send your real menu or catalog through POST /v1/translate twice, and read the stats line: the second call reports providerCalls: 0.