tldt

tldt translates UI micro-copy with Claude. Write your strings inline in English, run one command, and get back gender-aware, plural-correct translations for every language you configured — consumed at runtime through a single typed hook.

Why it exists

Micro-copy is the worst kind of translation work: hundreds of tiny strings, each needing the right plural form, the right grammatical gender, the right tone — and all of it drifting out of sync the moment a developer edits a button label. tldt makes the source string in your component the single source of truth:

  • Strings live in code. translate("Save {count:number} files") is the entry — no key indirection, no JSON to hand-edit, no string table to keep aligned.
  • Claude fills the matrix. Plurals (CLDR forms), audience (male/female), and language direction are generated for you, then validated against each language's rules before they're written.
  • Typed at the call site. Variables are inferred straight from the template — {name:string}, {total:currency}, {due:date} — so a wrong argument is a compile error, not a runtime undefined.
  • Build-time cost, runtime simplicity. Generation happens in the CLI. The app ships a generated TS file and one hook.
  • Cheap to run in bulk. Teaching Claude how to translate — the conventions, the markers, your context files — is the expensive part of every request. tldt marks that system prompt and tool schema as cacheable, so the first string pays to warm the cache and every string after it reads from cache at a fraction of the cost. Translating a batch is dramatically cheaper than the per-string price implies.

Core concepts

tldt is two halves that meet at a generated TypeScript file:

Three layers, three jobs. Each is owned by someone different, so you only ever touch the one that's yours:

  • System — owned by the library. Out of the box it ships every LTR and RTL language with tasteful, standards-compliant defaults for plural forms, text direction, and grammatical gender. A project developer never edits this.

  • Project — your defineTldtConfig. For the most part the only decision you make is which languages to support — pick them and you're done. Beyond that you can fine-tune if you want (tone of voice, per-language context), plus point at where output lands and what to scan.

  • Call site — the third argument to translate(...). Per-string context, a model override, strict checking — guidance that applies to this string only.

A variable carries a type hint in the template — {name:string}, {total:currency}, {due:date}. The hint tells Claude how the value will be used when it generates the translation, and it types the argument you pass at runtime:

A taste of the API

1. ConfiguredefineTldtConfig says which languages to generate, where the output lands, and which files to scan. Context files give Claude tone and glossary guidance per language.

2. Generate — the CLI scans for translate() calls, diffs against what's already translated, and asks Claude only for what's new. --dry (or diff) previews; reconcile re-checks and patches incomplete entries.

What lands in the file — a source string in, a fully-resolved entry out. The template uses markers Claude generates: {var} for variables, #p1[...] for a plural group, #a1[...] for an audience (gender) group. Each marker's data lives beside it, keyed by group:

Validated until it fits — no runtime surprises. Claude returns through a forced tool call (structured output), then the CLI runs the candidate through a battery of validators: variables preserved, plural forms complete for that language's CLDR set, plural and audience markers in the template synced with their data, source identity intact. If anything fails, the exact errors are fed back to Claude and it retries — looping until the entry is structurally valid or the retry budget is spent (an optional strictCheck adds a further quality-review pass). What reaches your generated file is guaranteed to satisfy the runtime's shape.

3. ConsumecreateUseTldt binds the generated translations to your config and returns one hook. The boundary owns the language/audience state; leaves inherit it.

The hook hands back everything a view needs: translate for copy, intl for the current lang / dir / isRtl / audience, and setIntl to switch language or audience — with the plural and gender variants Claude generated resolving automatically behind each string.

Direction is automatic. TldtProvider stamps the current dir (and lang) onto its child, so switching to an RTL language like he-IL flips the whole layout with no extra work. This assumes your styles use logical CSS properties (margin-inline-start, padding-inline-end, inset-inline) rather than physical ones (margin-left, padding-right) — logical properties resolve against dir, physical ones don't.