Write translations
Use this skill whenever you edit any non-English file under services/*/messages/ (e.g. de.json, fr.json), packages/ui/src/i18n/messages/, or any page under docs/<locale>/, add a locale, or touch a glossary term — Tale ships as one narrator written natively per language, never a word-for-word render of the English. Load it before touching any non-English string; never translate by rendering the source words. Per-locale voice doctrine lives in locales/<locale>/AGENTS.md; the loanword buckets in BUCKETS.md, the conventions template in CONVENTIONS.md, the glossary workflow in GLOSSARY_GUIDE.md.From its SKILL.md
npx -y skills add tale-project/tale --skill write-translationsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 20 stars20 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
9.8 KB, ~2.3k tokens by cl100k_base, as published. Nobody here has run it
write-translations
Tale ships one calm, opinionated, second-person-informal narrator in three languages — the German page
is the same voice written natively in German, never a German rendering of the English. This file is the
cross-locale contract; the data that changes between languages (strike lists, drift patterns, gender
maps, formal-pronoun denylists) lives in the framework's per-locale test data under
packages/ui/src/i18n/tests/locales/ and the
per-locale voice files. Read this first, then the locale file for the locale you're in.
When this applies
Editing any non-English value under services/*/messages/ (e.g. de.json, fr.json),
packages/ui/src/i18n/messages/, or any page under docs/<locale>/. Then read
locales/<locale>/AGENTS.md for that language's voice doctrine and drift catalogue. The
two reliable failure modes are bureaucratic German (passive present, sentence-final erfolgreich,
third-person Sie) and marketed French (Découvrez, N'hésitez pas à, stacked nominal phrases) —
both translate the words and lose the voice.
Write a note first
Invoke write-notes and record your answers to this form before you translate:
- Locale & files: Describe the locale and files, and the source meaning you must convey (not the words).
- Voice risk: Describe this locale's drift mode to avoid (bureaucratic German / marketed French) and how you'll keep the native voice.
- Must-match: Describe the UI labels and compound terms that must stay exact, and how you confirmed them against the shipped strings.
- Risks & unknowns: Describe where the translation might read non-natively or drift from the source meaning.
The rules
These four fail review. The first is reviewer-caught (voice doesn't lint cleanly); the rest are enforced by the i18n test suite (see Patterns).
-
Same voice across locales. Translation is a rewrite of the same narrator in another language, not a render of the source words — translate meaning, not words. Sentence structure, idiom, and noun choice all differ: the German equivalent of an English three-clause sentence is often one sentence with a verb-final subordinate clause; the French equivalent of a stacked English noun phrase is often a relative clause. A page that reads calmly in English and bureaucratically in German has a tone bug; fix the wording. The drift modes are language-specific — your per-locale file names yours. (reviewer-caught)
-
Informal pronoun, always.
duin DE and de-CH,tuin FR. NeverSie, nevervous— formal pronouns put distance between Tale and the reader. The carve-out for sentence-initial DESie(third-person feminine) is built into the check. (enforced bypronouns-formal) -
The shipped UI string is the source of truth. Every button, menu, panel, or feature name in a translated page matches
services/platform/messages/<locale>.jsonexactly. When JSON and a glossary or doc disagree, the JSON wins — the contract bends to what ships. Half-translated walkthroughs (Öffne **Settings > Members**) are the most common bug. (enforced byterminology-ui-label) -
Compound terms are whole or kept whole.
Pull Requeststays English in DE/FR;Knowledge Basetranslates whole toWissensdatenbank/Base de connaissances. Half is always wrong —Pull Anfrage,Code Review-Prozess,Merge-Anfragefail. Whether a compound stays English or translates is a bucket decision (see BUCKETS.md); whether it's a half is the rule. (enforced byterminology-half-compound)
Patterns
A correct translation that correctly does not translate one thing:
EN: Open a pull request from your feature branch. The CI pipeline runs against the head of the branch; the merge into
mainis gated on green.DE: Öffne einen Pull Request aus deinem Feature-Branch. Die CI-Pipeline läuft gegen den Kopf des Branches; der Merge in
mainist erst möglich, wenn die Pipeline grün ist.
Pull Request,Feature-Branch,CI,Pipeline,Merge,Branchstay English (Git-domain loanwords; bucket 2).du, neverSie. Noerfolgreich, noWird X…. The English-kept terms are the words a German-speaking developer uses without thinking — not lazy translation.
The shipped UI read back to the reader:
Drift: Open Settings > Members und klicke auf Invite member.
Target: Öffne Einstellungen > Mitglieder und klicke auf Mitglied einladen.
The reader sees the German UI; the page must echo it. Specifics: code identifiers stay English
everywhere (CLI flags tale deploy --detach, env vars TALE_CONFIG_DIR, file paths, API paths POST /api/v1/documents); role names ship per locale (Owner / Inhaber / Propriétaire); parenthetical lists
translate ((Products, Customers, Vendors) → (Produkte, Kunden, Lieferanten)); navigation paths
translate segment by segment (Settings > Members → Einstellungen > Mitglieder, never
Einstellungen > Members).
Three buckets, summary (full lists + assignment in BUCKETS.md):
| Bucket | Examples | Behaviour |
|---|---|---|
| Always English | Tale, Convex, AI, LLM, MCP, env vars, CLI flags | Never translates — brand, acronym, code identifier. |
| Established loanwords | Workflow, Dashboard, Webhook, Pull Request, Branch, Merge | Stays English in DE/FR; hyphenated in DE compounds. |
| Translate-bucket | Header → Kopfzeile, Request → Anfrage, Email → E-Mail | Must translate in DE/FR/de-CH; caught by terminology-loanword. |
The bucket lives on each term's entry in
tests/glossary/glossary.json. Moving a
term between buckets is a glossary PR, not a skill PR.
What the suite catches — two layers run on every bun run check: parity + usage (sibling test
files in each consumer's lib/i18n/ — key parity, orphan detection), and the centralized
@tale/ui/i18n/tests — 26 checks (the registry's 28 entries in
tests/registry.ts minus parity + usage) over
terminology, voice, grammar, style, ICU parity, heuristics, and markdown. Most start in report mode
during rollout; flip to enforce after findings clear. Not caught — reviewer territory: subtler
calques past the small denylist, tone drift inside passing prose, sentence flow across clauses, ICU
plural correctness within branches, idiomatic word choice (Duden-correct ≠ native-sounding).
Adding a locale (e.g. Italian) — three concerns:
- Runtime registry — add
ittoSUPPORTED_LOCALESinpackages/ui/src/i18n/locales.ts. - Test framework data — create
packages/ui/src/i18n/tests/locales/it/withindex.ts,style.ts,voice.ts,terminology.ts,grammar.ts,patterns.ts, and aplanted/folder of positive/negative fixtures per applicable check; register it. The startup-drift assertion inlocales/index.tskeeps the runtime and test registries in sync. Optionally extendglossary.jsonwithitforms on translating terms. - Doctrine — create
locales/it/AGENTS.mdper the template in the existing locale files, and add its row to Companion files below.
Before you call the translation done
Tick every box, or N/A with a reason:
- Same voice as the source — rewritten natively, not word-rendered; no bureaucratic German, no marketed French.
- Informal pronoun throughout —
du(DE/de-CH),tu(FR); neverSie/vous. - Every UI label matches
services/platform/messages/<locale>.jsonexactly — no half-translated walkthroughs. - Compound terms whole or kept whole — no
Pull Anfrage/Merge-Anfrage; bucket decisions honoured. -
en.jsonparity — every key present, dead keys removed everywhere;de-CHholds only overrides. - The i18n suite is green —
bun run check.
Companion files
locales/<locale>/AGENTS.md— read when editing that locale's JSON or docs: the voice doctrine and language-specific drift catalogue (en,de,fr, and thede-CHSwiss overlay of differences-from-DE only).- BUCKETS.md — read when deciding whether an English term translates, stays English, or matches the UI verbatim; holds the full per-bucket lists, the assignment workflow, and the half-compound denylists.
- CONVENTIONS.md — read when handling quotes, apostrophes, dates, numbers, currency, percent, NBSP, dashes, or ß: the 14-row conventions template every locale fills.
- GLOSSARY_GUIDE.md — read when adding a glossary term, choosing its category, or
using
_lintExcludeto defer a UI-vs-bucket mismatch; also documents the role table and the audit script.
What ships with it: 16 files
58.3 KB alongside SKILL.md
locales/
- de/AGENTS.md6.9 KB
- de-CH/AGENTS.md3.1 KB
- de-CH/examples.md937 B
- de/drift-catalogue.md4.5 KB
- de/examples.md4.1 KB
- de/glossary-notes.md774 B
- en/AGENTS.md4.6 KB
- en/examples.md2.9 KB
- en/glossary-notes.md209 B
- fr/AGENTS.md6.1 KB
- fr/drift-catalogue.md3.2 KB
- fr/examples.md3.1 KB
- fr/glossary-notes.md953 B
- BUCKETS.md6.2 KB
- CONVENTIONS.md7.0 KB
- GLOSSARY_GUIDE.md3.7 KB