Skip to content

feat(translate): translation instructions the model follows - #140

Merged
TranBaDat2607 merged 1 commit into
mainfrom
feat/translation-instructions
Oct 26, 2026
Merged

TranBaDat2607 merged 1 commit into
mainfrom
feat/translation-instructions

Conversation

@TranBaDat2607

Copy link
Copy Markdown
Owner

Summary

Settings → Translation gets a box for free-text translation instructions: tone, terms to keep, how to render names. They are sent with every paragraph to the LLM that translates. Chat never receives them.

  • They go into our prompt, not BabelDOC's. TranslationConfig.custom_system_prompt would have no effect: BabelDOC reads it only when a translator implements do_llm_translate, and none of ours does. BaseTranslator._user_instructions_block appends them after the fixed rules, inside a <user-instructions> fence, and says the fixed rules (target language, reply with the translation alone, copy placeholders) win on conflict. With none saved, the prompt is byte-identical to before; a test pins the pre-change hashes.
  • Both caches key on them. |instructions=<text> is appended only when they are set, like pages=. No existing key changes and no migration is needed. Without this, a changed instruction was served the old instruction's translations.
  • Only LLM translation gets them. capabilities.translation_instructions(settings, service) is asked once, of the effective service, so a keyless LLM that runs as Argos gets none. The processor hands that one value to the translator and to both ends of the PDF cache. Chat, verify and pre-warm build translators without them, and a factory test guards that.
  • Config: translation.instructions, at most 2,000 characters, trimmed, blank stored as unset. PUT /config takes translation_instructions: omitted leaves it unchanged, null or blank clears it. Over-long text in a hand-edited config.toml drops just that field.
  • Cost: /translate/estimate counts the instructions on every paragraph. The constant was measured against the real prompt and is held within 10% by a test. The toolbar's estimate refetches when the instructions change.
  • UI:
    • a Translation tab in Settings with a counter, Save and Clear
    • the draft survives switching tabs (kept in Zustand)
    • the box is locked while a save is in flight
    • a note when the run will be Argos
    • an "Instructions on" badge in the toolbar when they will actually be sent

Docs: a new "Translation instructions" section in docs/architecture/translators.md, plus an invariant and the PUT /config contract in CLAUDE.md.

Tests

Written test-first. A separate agent wrote the red tests from the spec before any production code existed, and an opus reviewer checked the finished cycle. That review's findings are fixed here:

  • an OpenAPI regen made under an older FastAPI, which would have failed CI's diff
  • the stale token estimate
  • the draft being lost on a tab switch
  • the length being checked before trimming

Several guard tests were also mutation-checked: each was broken on purpose and confirmed to fail.

  • pytest tests: all pass, and ruff is clean.
  • pnpm test (654 tests) and pnpm build pass.
  • openapi.json and api-types.d.ts were regenerated under FastAPI 0.156, matching CI.

Checked by hand

  • A real EN→VI run on gemini-2.5-flash-lite, through the sidecar, with a 2-page programming PDF:

    Run Terms kept in English (index / array / loop / function / pointer) Cache
    No instruction 0 / 0 / 0 / 0 / 0 —
    "Keep programming terms such as index, array… in English" 10 / 7 / 3 / 2 / 3 —
    Same instruction again same hit
    Instruction changed same miss
  • pnpm tauri dev on Linux:

    • saving instructions from the Translation tab
    • the badge appearing once an LLM is picked
    • the Argos case, where instructions are not sent
  • Not covered by unit tests:

    • the processor's wiring (one value to the translator, the PDF-cache lookup and the store); the real run above checks it
    • the React components, apart from the pure helper lib/translation-instructions.ts

Notes for reviewers

  • Small models follow concrete instructions much better than vague ones. On flash-lite:

    • "keep programming terms such as index, array…" works
    • "keep technical terms" is mostly ignored
    • a separate 54-run experiment found the same pattern for other kinds of instruction

    Translating several paragraphs per request (BabelDOC's do_llm_translate path) did not change that, so this PR keeps one paragraph per request.

  • Typing Vietnamese with Lotus on Linux: the text box only takes it with Lotus in Preedit mode. Its default "Surrounding Text" mode doesn't work in WebKitGTK. This is environmental and not changed here.

🤖 Generated with Claude Code

Settings → Translation saves free text telling the model how to translate.
It is appended to every LLM translator's prompt after the fixed rules, in a
<user-instructions> fence, with the fixed rules winning on conflict; with
none saved the prompt is byte-identical to before. BabelDOC's
custom_system_prompt was not used: it is read only on the do_llm_translate
path, which none of our translators implements.

- Both caches key on the instructions, appended only when set, so no
  existing key moved.
- Only an LLM that will really run gets them
  (capabilities.translation_instructions, asked of the effective service);
  chat, verify and pre-warm never do.
- /translate/estimate counts them per paragraph, and the toolbar's estimate
  refetches when they change.
- The toolbar shows "Instructions on" when they will be sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@TranBaDat2607
TranBaDat2607 merged commit a9a5d0f into main Oct 26, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant