feat(ai): Translate picks the target first, so it never burns a wasted call (LC-694) #661

Merged
longjacksonle merged 4 commits from feat/LC-694-translate-flow into main 2026-08-09 19:53:24 +02:00

What

Fixes the wasted-call Translate flow and polishes the block/dropdown. Fixes LC-694.

Before: clicking Translate auto-translated to the viewer's own language and ran Ollama immediately. On an already-English message that first call returned the text verbatim (a useless multi-second call), and getting the language you actually wanted meant opening the dropdown and running a second call.

How

  • Pick first, translate once. The Translate menu action now opens a picker-only block ("Translate to [language] [Translate]") and runs no LLM call. The AI runs only when the user confirms a target (the Translate button or a language change) - one call per target, cached per (message, target). routes/translate.rs branches on the presence of lang: absent -> picker only (translated_html: None); present -> translate. Source-language detection needs its own model call, so picker-first is the correct way to avoid translating a message into the language it is already in.
  • Remember the target. The viewer's last-chosen language is stored in localStorage and pre-selected in the picker (live.js) - applied only to the picker-only select, never auto-translating.
  • Polish. The translated text now sits in a quiet rounded accent-tinted surface with the accent left border (distinct from the original). The <select> and the Translate / Show original controls are styled as proper compact controls. The multi-second call reuses the shared data-lc-ai-pending spinner ("Translating..."); the menu trigger drops that spinner since opening the picker is instant.

Template stays comparison-free where it matters; the two states are driven by Option<String>.

Scope

routes/translate.rs, views/translate.rs, templates/room/translation_block.html, templates/room/message.html, assets/live.js, assets/main.css (authored CSS, served directly), locales/en+es/room.ftl. No schema change.

Deferred (noted in the ticket)

  • A fully custom popover dropdown with a "recent languages" section (kept the native select for accessibility / zero-JS, ordered common-first).
  • Token streaming (kept the single-shot call with the spinner).

Verification

  • cargo check / cargo clippy --lib: clean.
  • cargo fmt --check: clean.
  • node --check live.js: clean.
  • Full lib suite: 317 passed (incl. i18n en/es parity).

🤖 Generated with Claude Code

https://claude.ai/code/session_011M7CNRsGzgzc48KinZUAQ8

## What Fixes the wasted-call Translate flow and polishes the block/dropdown. Fixes LC-694. Before: clicking Translate auto-translated to the viewer's own language and ran Ollama immediately. On an already-English message that first call returned the text verbatim (a useless multi-second call), and getting the language you actually wanted meant opening the dropdown and running a second call. ## How - **Pick first, translate once.** The Translate menu action now opens a picker-only block ("Translate to [language] [Translate]") and runs no LLM call. The AI runs only when the user confirms a target (the Translate button or a language change) - one call per target, cached per (message, target). `routes/translate.rs` branches on the presence of `lang`: absent -> picker only (`translated_html: None`); present -> translate. Source-language detection needs its own model call, so picker-first is the correct way to avoid translating a message into the language it is already in. - **Remember the target.** The viewer's last-chosen language is stored in `localStorage` and pre-selected in the picker (`live.js`) - applied only to the picker-only select, never auto-translating. - **Polish.** The translated text now sits in a quiet rounded accent-tinted surface with the accent left border (distinct from the original). The `<select>` and the Translate / Show original controls are styled as proper compact controls. The multi-second call reuses the shared `data-lc-ai-pending` spinner ("Translating..."); the menu trigger drops that spinner since opening the picker is instant. Template stays comparison-free where it matters; the two states are driven by `Option<String>`. ## Scope `routes/translate.rs`, `views/translate.rs`, `templates/room/translation_block.html`, `templates/room/message.html`, `assets/live.js`, `assets/main.css` (authored CSS, served directly), `locales/en+es/room.ftl`. No schema change. ## Deferred (noted in the ticket) - A fully custom popover dropdown with a "recent languages" section (kept the native select for accessibility / zero-JS, ordered common-first). - Token streaming (kept the single-shot call with the spinner). ## Verification - `cargo check` / `cargo clippy --lib`: clean. - `cargo fmt --check`: clean. - `node --check live.js`: clean. - Full lib suite: 317 passed (incl. i18n en/es parity). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_011M7CNRsGzgzc48KinZUAQ8
feat(ai): Translate picks the target first, so it never burns a wasted call (LC-694)
Some checks failed
check-secrets / Nosey parker (push) Successful in 8s
check-secrets / TruffleHog (push) Successful in 8s
check-secrets / Kingfisher (push) Successful in 10s
check-secrets / TruffleHog (pull_request) Successful in 2m32s
check-secrets / Nosey parker (pull_request) Successful in 2m32s
check-secrets / Kingfisher (pull_request) Successful in 2m32s
Check / clippy + fmt + tests (pull_request) Failing after 6m47s
33453064f0
Clicking Translate used to immediately translate to the viewer's own language and run the Ollama request. When the message was already in that language the first call produced identical text - a useless multi-second call - and the user then had to open the dropdown and run a SECOND call to get the language they actually wanted. Two calls for one useful result.

Translate now opens a language picker instead of auto-translating. The first click renders a picker-only block ("Translate to [language] [Translate]") and runs no LLM call; the AI runs only once the user confirms a target (the Translate button or a language change), so a message is never translated into the language it is already in, and there is exactly one call per chosen target. Results stay cached per (message, target). Source-language detection needs its own model call, so picker-first is the right way to avoid the no-op translation.

The viewer's last-chosen target is remembered in localStorage and pre-selected in the picker (live.js), so frequent users do not re-pick every time - applied only to the picker-only select, never auto-translating.

Polish: the translation block is now a quiet rounded accent-tinted surface with the accent left border so the translated text reads as distinct from the original; the language select and the Translate / Show original controls are styled as proper compact controls; the multi-second call reuses the shared data-lc-ai-pending spinner ("Translating..."). The menu trigger no longer shows that spinner, since opening the picker is instant.

No schema change. i18n en/es both updated (room-msg-translate-to).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011M7CNRsGzgzc48KinZUAQ8
longjacksonle scheduled this pull request to auto merge when all checks succeed 2026-08-09 19:12:18 +02:00
test(lc77): refresh webhook/email render fixtures for the LC-694 Translate button
Some checks failed
check-secrets / TruffleHog (pull_request) Successful in 4s
check-secrets / TruffleHog (push) Successful in 6s
check-secrets / Kingfisher (pull_request) Successful in 6s
check-secrets / Nosey parker (push) Successful in 7s
check-secrets / Nosey parker (pull_request) Successful in 6s
check-secrets / Kingfisher (push) Successful in 7s
Check / clippy + fmt + tests (pull_request) Failing after 5m11s
53d6a703e6
The LC-694 change drops the data-lc-ai-pending spinner attributes from the message overflow menu's Translate button (opening the picker is instant, no LLM call). The lc77 render-fixture pins include the full message row, so their golden HTML needed regenerating to match. Only the three removed attributes differ.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011M7CNRsGzgzc48KinZUAQ8
test(translate): update the integration test for the LC-694 picker-first flow
Some checks failed
check-secrets / Nosey parker (pull_request) Successful in 5s
check-secrets / Nosey parker (push) Successful in 6s
check-secrets / Kingfisher (pull_request) Successful in 6s
check-secrets / Kingfisher (push) Successful in 7s
check-secrets / TruffleHog (pull_request) Successful in 7s
check-secrets / TruffleHog (push) Successful in 8s
Check / clippy + fmt + tests (pull_request) Failing after 20s
4df632b457
translate_returns_translation_and_caches asserted the old behavior: an empty POST (no lang) translated immediately to the viewer's locale and cached. LC-694 changed that - an empty POST now opens the language picker and runs no LLM call. Rewrite it as translate_opens_picker_then_translates_and_caches: the first (empty) POST renders the picker and caches nothing, and a POST with lang=es translates once and caches under es.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011M7CNRsGzgzc48KinZUAQ8
style(test): rustfmt the LC-694 translate test assert_eq
Some checks failed
check-secrets / Nosey parker (push) Failing after 1m11s
check-secrets / Kingfisher (push) Failing after 1m12s
check-secrets / TruffleHog (push) Failing after 1m12s
Check / clippy + fmt + tests (pull_request) Successful in 8m30s
Create release / Create release from merged PR (pull_request) Has been skipped
check-secrets / Nosey parker (pull_request) Successful in 2m6s
check-secrets / TruffleHog (pull_request) Successful in 2m10s
check-secrets / Kingfisher (pull_request) Successful in 2m11s
0e41d514f1
CI's `cargo fmt --check` reflows the three-arg assert_eq! onto multiple lines. (My earlier `cargo fmt -- --check` did not flag tests/ - the two invocations differ.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011M7CNRsGzgzc48KinZUAQ8
longjacksonle deleted branch feat/LC-694-translate-flow 2026-08-09 19:53:24 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!661
No description provided.