Translate non-English post title/description to English via the operator's self-hosted Interpreter LAN service (LibreTranslate-compatible), surfaced translation-forward with a per-card "show original" toggle. Platform-agnostic (auto-detects language). Off + unconfigured by default.
Data (migration 0083): Post gains post_title_translated / description_translated / translated_source_lang / translation_engine_version / translated_at. ImportSettings gains translation_enabled (off), interpreter_base_url (empty — no default host; operator sets their own, behind a reverse proxy), translation_target_lang (en).
Client (interpreter_client.py, sync/requests — no new dep): batch translate mapping translatedText[]↔texts, passthrough unchanged, InterpreterUnavailable (503/conn) vs InterpreterBadRequest (400), health() via /v1/health.
Translate non-English post title/description to English via the operator's self-hosted **Interpreter** LAN service (LibreTranslate-compatible), surfaced translation-forward with a per-card "show original" toggle. Platform-agnostic (auto-detects language). Off + unconfigured by default.
- **Data** (migration **0083**): Post gains post_title_translated / description_translated / translated_source_lang / translation_engine_version / translated_at. ImportSettings gains translation_enabled (off), interpreter_base_url (**empty — no default host; operator sets their own, behind a reverse proxy**), translation_target_lang (en).
- **Client** (`interpreter_client.py`, sync/`requests` — no new dep): batch translate mapping translatedText[]↔texts, passthrough unchanged, InterpreterUnavailable (503/conn) vs InterpreterBadRequest (400), `health()` via `/v1/health`.
- **Sweep** (`tasks/translation.py`, maintenance_long lane + daily beat): translates untranslated non-English posts per-post, stores engine_version (cache-fast re-runs); idempotent (= rule-89 recovery), wall-clock bounded, no-op unless enabled + URL set + healthy.
- **API**: `/settings/import` exposes the 3 knobs; `/settings/translation/status`, `/run`, and `/test` (on-demand health check of a given URL).
- **UI**: PostCard + modal ProvenancePanel translation-forward with "Show original (⟨lang⟩)"; TranslationCard in Settings (enable, base URL w/ generic placeholder, target lang, reachability dot + **Test connection** + untranslated count + Translate now).
- Tests across client (mocked HTTP), sweep (stubbed client), settings, endpoints, serialization.
One migration (0083). CI green on dev head `6a25548` across every step.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Post gains post_title_translated / description_translated / translated_source_lang
/ translation_engine_version / translated_at — filled by the translate sweep so
viewing is instant. ImportSettings gains translation_enabled (OFF by default),
interpreter_base_url (EMPTY — no default host; the operator points it at their own
Interpreter proxy behind a reverse proxy) and translation_target_lang (en),
exposed + validated via /settings/import. Migration 0083. Settings defaults +
patch + validation test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
services/interpreter_client.py: sync (requests) client for the LibreTranslate-
compatible /v1/translate — no new dep, mirrors the platform clients. translate()
maps translatedText[]↔texts (order + length), returns detected_lang +
engine_version (aggregate = first item, fine for a per-post [title, description]
batch); passthrough items come back unchanged in their slot. InterpreterUnavailable
on 503 / connection error (retry later), InterpreterBadRequest on 400. health()
checks /v1/health engines.llm. 10 unit tests with mocked HTTP.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
tasks/translation.py — translate_posts: picks untranslated posts (title OR
description non-empty), per-post [title, description] batch via the Interpreter
client, stores translations + detected lang + engine_version; passthrough /
already-target posts are marked handled with no stored translation. 503 or a
connection error interrupts (retry next cycle), 400 stops (fix config), per-post
commit keeps progress; wall-clock bounded. Wired into celery (maintenance_long
lane) + a daily beat. No-op unless enabled + base URL set + healthy. GET
/settings/translation/status + POST .../run for the Settings card. Task tests
(stubbed client, monkeypatched session).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
post_feed_service (card + detail) and provenance_service._post_dict now include
post_title_translated, description_translated (card-truncated / detail-uncapped)
and translated_source_lang, keeping the originals for the toggle. Feed
serialization test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
PostCard + modal ProvenancePanel show the English title/description by default when
a translation exists, with a per-card "show original (<lang>)" toggle — translated
bodies render as plain text, originals keep their sanitized HTML. New
TranslationCard in Settings → Ingestion & filters: enable switch, Interpreter base
URL (generic placeholder, no default host), target language, a reachability
indicator + untranslated-posts count + "Translate now".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
/settings/translation/status defaults (off → no health call) + /run 400-when-
unconfigured + 202-when-configured (monkeypatched .delay). requests is already a
backend dep, so no requirements/ci-requirements change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
New POST /api/settings/translation/test pings /v1/health for a GIVEN base URL (not
the saved one), so the operator can verify a URL before enabling it. TranslationCard
gains a Test-connection button that reports reachable/unreachable inline and
updates the status dot. Endpoint test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Translate non-English post title/description to English via the operator's self-hosted Interpreter LAN service (LibreTranslate-compatible), surfaced translation-forward with a per-card "show original" toggle. Platform-agnostic (auto-detects language). Off + unconfigured by default.
interpreter_client.py, sync/requests— no new dep): batch translate mapping translatedText[]↔texts, passthrough unchanged, InterpreterUnavailable (503/conn) vs InterpreterBadRequest (400),health()via/v1/health.tasks/translation.py, maintenance_long lane + daily beat): translates untranslated non-English posts per-post, stores engine_version (cache-fast re-runs); idempotent (= rule-89 recovery), wall-clock bounded, no-op unless enabled + URL set + healthy./settings/importexposes the 3 knobs;/settings/translation/status,/run, and/test(on-demand health check of a given URL).One migration (0083). CI green on dev head
6a25548across every step.🤖 Generated with Claude Code