Translate from history via DeepL → fresh Compose draft
Pick an archived post → "translate to fr" in the detail view → its master_text
is translated by DeepL server-side and lands in Compose as a fresh draft with
the Bluesky/Mastodon language fields set to the chosen target. Confirms first
if there's an in-progress draft, mirroring the Reply/Quote hand-off.
- new internal/translate package: DeepL client (auto-detects Free vs Pro from
the API-key ':fx' suffix per DeepL spec), an ISO 639-1 → DeepL target_lang
allowlist of the 29 supported codes (lb/Luxembourgish notably absent), and
Intersect() to filter USER_LANGUAGES. Form-encoded request, JSON response;
covered by httptest stubs (free-vs-pro routing, pt→PT-PT mapping, error
surfacing, unsupported-target rejection, empty-text rejection).
- config: new DEEPL_API_KEY env (default unset). When unset, /api/translate
returns 503 and /api/config emits translate_targets:[] so the UI hides the
button — the feature is opt-in.
- api: new Translator interface (mirrors Verifier/Resolver pattern); new
POST /api/translate proxies to DeepL with the server-side key (never sent
to the client); GET /api/config now also returns translate_targets, the
intersection of USER_LANGUAGES and DeepL-supported codes preserving order.
- web: state.translateTargets populated from /api/config on boot; new
loadDraft({text,lang}) in compose.js confirms-before-replace, exits any
interaction mode, sets master + both lang fields, and switches tab; new
translateSelect() in history.js renders a small inline dropdown next to the
"text" header in the detail view (hidden when translate_targets is empty).
On pick: POST /api/translate → loadDraft.
Translate from history via DeepL → fresh Compose draft
Pick an archived post → "translate to fr" in the detail view → its master_text
is translated by DeepL server-side and lands in Compose as a fresh draft with
the Bluesky/Mastodon language fields set to the chosen target. Confirms first
if there's an in-progress draft, mirroring the Reply/Quote hand-off.
- new internal/translate package: DeepL client (auto-detects Free vs Pro from
the API-key ':fx' suffix per DeepL spec), an ISO 639-1 → DeepL target_lang
allowlist of the 29 supported codes (lb/Luxembourgish notably absent), and
Intersect() to filter USER_LANGUAGES. Form-encoded request, JSON response;
covered by httptest stubs (free-vs-pro routing, pt→PT-PT mapping, error
surfacing, unsupported-target rejection, empty-text rejection).
- config: new DEEPL_API_KEY env (default unset). When unset, /api/translate
returns 503 and /api/config emits translate_targets:[] so the UI hides the
button — the feature is opt-in.
- api: new Translator interface (mirrors Verifier/Resolver pattern); new
POST /api/translate proxies to DeepL with the server-side key (never sent
to the client); GET /api/config now also returns translate_targets, the
intersection of USER_LANGUAGES and DeepL-supported codes preserving order.
- web: state.translateTargets populated from /api/config on boot; new
loadDraft({text,lang}) in compose.js confirms-before-replace, exits any
interaction mode, sets master + both lang fields, and switches tab; new
translateSelect() in history.js renders a small inline dropdown next to the
"text" header in the detail view (hidden when translate_targets is empty).
On pick: POST /api/translate → loadDraft.