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 runtimeundefined. - 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. Configure — defineTldtConfig 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. Consume — createUseTldt 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.