Skip to content

Translated output for reflow conversions

Offer a Spanish (and later any-language) download of a converted document. Reflow route only β€” the preserve-design route is pixel-identity-gated and cannot carry translated text by construction.

Status: proposed, not started. No code written.

Why this is tractable

The reflow pipeline’s canonical artifact is HTML; the PDF is a render of it:

PDF β†’ chunks β†’ Claude β†’ assembled accessible HTML (files.outputR2Key)
β†’ SQS 'export-accessible-pdf' β†’ WeasyPrint β†’ tagged PDF
β†’ pdf-accessibility-postprocessor (/Lang, XMP dc:language) β†’ veraPDF gate

So a Spanish PDF is translate the HTML, then re-run the export that already exists. No new rendering, tagging, or conformance machinery.

Engine decision: Claude via Bedrock, not Amazon Translate

Amazon Translate is sentence-level neural MT β€” fast, deterministic, HTML-tag aware, glossary-capable. It cannot take instructions, and every requirement here is an instruction:

  • keep the tag skeleton byte-identical (drift breaks the PDF/UA gate downstream)
  • translate alt text and keep it descriptive, not literal
  • wrap untranslated proper nouns in lang="en" spans (WCAG 3.1.2)
  • keep table headers semantically aligned with the cells they scope
  • leave code blocks, URLs, and email addresses alone

Cost does not argue for Translate either. A 10-page document β‰ˆ 25K characters β‰ˆ 6,250 input tokens; Spanish runs ~20% longer than English, so ~7,800 output tokens. Bedrock bills Claude at Anthropic list rates (confirmed by Larry 2026-09-13):

EnginePer 10-page doc
Claude Sonnet 5 ($2/$10 per MTok)~$0.09
Claude Sonnet 5 via Batch API (50% off)~$0.045
Claude Opus 5 ($5/$25 per MTok)~$0.23
Claude Opus 5 via Batch API~$0.12
Amazon Translate (~$15/M chars β€” unverified)~$0.38

Default to claude-opus-5; Sonnet 5 is the obvious A/B, but that call belongs to an eval, not to this brief. Translation is a queue job with nobody waiting on the response, so check Batch API availability on our Bedrock path β€” the discount is free money given the architecture is already async.

Prompt caching buys little (every document is a unique prefix); only the system prompt and per-tenant glossary are cacheable.

Using Bedrock Claude is also zero new infrastructure: ZDR posture, cost_ledger, resolveModelId, and budget caps are already wired. Amazon Translate would mean a new service, new IAM, new cost-tracking, and a separate data-residency review β€” for a worse fit.

Work breakdown

1. Translation service (the only genuinely new code)

New workers/api/src/services/document-translator.ts. Chunked Bedrock call over the assembled HTML. Reuse the existing chunking infra; book a cost_ledger row (new operation_type='document-translate') per the cost-tracking standard.

Hard gate β€” tag-skeleton diff. Extract the element sequence (tag names + nesting depth, attributes excluded) before and after translation; a mismatch fails the job rather than shipping. This converts the LLM’s one real disadvantage versus Translate into a caught error, and mirrors the flyer-tagger verify-gate philosophy: absence of a passing check is a failure, never a pass.

Per-tenant glossary in the prompt from day one β€” institutions have terms that must render consistently (degree names, office names, program titles). Stolen from Translate’s Active Custom Translation, which exists for exactly this reason.

2. lang plumbing β€” already 90% there

  • workers/api/src/services/pdf-accessibility-postprocessor.ts already accepts an arbitrary lang and writes /Lang + XMP dc:language. No change.
  • workers/batch/src/accessible-pdf-export-executor.ts:195 hardcodes lang: 'en' β€” take it from the message.
  • workers/api/src/services/chunk-assembler.ts:3104 hardcodes language: 'en' in runPostProcessing β€” same.
  • AccessiblePdfQueueMessage in workers/api/src/services/accessible-pdf-export.ts β€” add lang.

3. Storage model β€” the real design decision

Today the artifact slot is singular: accessiblePdfR2Key, accessiblePdfStatus, accessiblePdfScore, accessiblePdfError, accessiblePdfGeneratedAt (packages/shared/src/types.ts:164+). A Spanish PDF has nowhere to live.

Recommendation: a new document_translations table (fileId, lang, htmlR2Key, pdfR2Key, status, score, error, costUsd, createdAt; unique on (fileId, lang)).

Prior art worth reading first: migration 294_output_variants uses exactly this shape for the Enhance before/after. It does not stretch to cover this β€” it is keyed to the three form-conversion job tables (not files), its kind CHECK allows only 'enhanced', and it stores a PDF key with no status/score/error lifecycle. Extending it would add several nullable columns used by one kind only. Next migration number is 302.

4. Export path reuse

Enqueue the existing export-accessible-pdf message against the translated HTML key with lang: 'es', writing results to the translations row instead of the files columns. POST /files/:fileId/refresh-pdf (workers/api/src/routes/files.ts:1456) is the closest existing precedent for a forced regen and worth reading before writing the new route.

5. UI + billing

Language option on the Downloads menu (apps/web/src/components/file-detail/ActionMenu.tsx, apps/web/src/components/dashboard/control-center/FileActions.tsx), status polling, credit charge. Per the standards: optimistic UI, progress indicator (this will exceed 2s), all strings through t(), WCAG 2.2 AA on the new controls.

What will bite you

  1. Images keep their English text. Charts, screenshots, logos with words stay English while the prose is Spanish. Say so in the UI β€” do not let users discover it.
  2. Language of parts (WCAG 3.1.2). Untranslated proper nouns need lang="en" spans, or the Spanish PDF carries an AA failure the English one didn’t.
  3. Machine-translation liability. A Title II entity publishing a Spanish version of an official notice is making a legal representation. Label it machine-translated. This is a natural paid human-review upsell, not a silent auto-feature.
  4. Reports stay English. The WCAG/conformance report templates hardcode <html lang="en"> β€” the Spanish deliverable ships an English report unless those are templated too.
  5. Route parity. New routes must be registered on BOTH index-aws.ts (Lambda) and server.ts (Node .4), per the standing gotcha.

Estimate

  • MVP β€” Spanish only, reflow only, one extra download button: ~3–5 focused days, most of it in the translation service and the variants table.
  • Productized β€” multi-language, per-language scores/reports, review flow, pricing: ~2 weeks.

Open questions

  • Batch API availability on our Bedrock path (changes cost by 50%).
  • Current Amazon Translate rate β€” unverified, and academic unless the engine decision is revisited.
  • Which languages after Spanish? Drives whether the glossary is per-tenant per-language or per-tenant only.