DEV Community

Noah Chen
Noah Chen

Posted on

Designing a Reliable PDF Translation Job Pipeline in TypeScript

Uploading a PDF and calling a translation model looks like a two-step feature. In production, it is a job pipeline with untrusted input, two different extraction paths, several expensive stages, and an output that can be fluent while still being wrong.

That distinction matters for a small SaaS team. The translation request may come from support, sales, or an internal operations task. Nobody wants to operate a document platform, but the workflow still needs to answer basic questions:

  • Was the upload actually a PDF?
  • Does the file contain selectable text or scanned page images?
  • Can a retry create a second charge or a conflicting result?
  • What happens when page 37 fails after the first 36 pages succeed?
  • How do we know the translated PDF is not blank or visually broken?
  • When are the source and result deleted?

The translation model is one component. Reliability comes from the system around it.

Define the Job Contract First

I would not let a file reach an extractor until the API has established a narrow contract.

For example, a translation request might include:

type TranslationStyle = "general" | "technical" | "academic";

interface CreateTranslationJob {
  uploadId: string;
  sourceLanguage: string | "auto";
  targetLanguage: string;
  style: TranslationStyle;
  idempotencyKey: string;
  containsRestrictedData: boolean;
}
Enter fullscreen mode Exit fullscreen mode

The request should be rejected when the source and target languages are identical, the upload is missing, the target language is unsupported, or policy says the document cannot leave an approved environment.

File validation should also be explicit. Do not trust the filename or browser-supplied MIME type. Check at least:

  1. the actual byte size;
  2. the file signature;
  3. whether the parser can open the document;
  4. whether the PDF is encrypted;
  5. the page count;
  6. whether the job fits the account or product limit.

A 20 MB limit is simple to explain in a user interface, but size alone is not a good predictor of work. A compressed 200-page text PDF can be smaller than a six-page scan. Page count, image area, and extracted character count are better inputs for estimating processing time.

Treat Preflight as Its Own Stage

The first useful result is not a translation. It is a document profile.

interface DocumentProfile {
  pageCount: number;
  encrypted: boolean;
  extractedCharacters: number;
  pagesWithText: number;
  pagesWithLargeImages: number;
  likelyScanned: boolean;
  estimatedOcrPages: number;
}
Enter fullscreen mode Exit fullscreen mode

This profile controls routing. A PDF where nearly every page has substantial selectable text can go directly to extraction. A document made of page-sized images needs OCR. Mixed documents need a page-level decision rather than a single flag for the entire file.

A crude scan heuristic could look like this:

function needsOcr(profile: DocumentProfile): boolean {
  if (profile.pageCount === 0) return false;

  const textCoverage = profile.pagesWithText / profile.pageCount;
  const imageCoverage = profile.pagesWithLargeImages / profile.pageCount;

  return textCoverage < 0.25 && imageCoverage > 0.6;
}
Enter fullscreen mode Exit fullscreen mode

The thresholds are product decisions, not universal constants. A form can contain a small amount of real text over a scanned background. A research paper may include image-heavy appendix pages. Store the measurements that led to the route so a failed job can be explained later.

Model the Pipeline as States, Not Progress Percentages

A single processing: true field hides too much.

I would rather expose a finite set of states:

type JobState =
  | "queued"
  | "validating"
  | "extracting"
  | "ocr"
  | "translating"
  | "rendering"
  | "verifying"
  | "ready"
  | "failed"
  | "expired";
Enter fullscreen mode Exit fullscreen mode

Each state should have a clear owner and output:

State Responsibility Durable output
validating inspect the upload and policy document profile
extracting recover text and geometry page blocks
ocr recognize image-only pages OCR blocks and confidence
translating translate normalized blocks translated segments
rendering rebuild the destination PDF candidate output
verifying run structural checks verification report
ready expose a time-limited download signed result reference

The state machine makes several bugs harder to create. A rendering worker cannot start before translation output exists. An expired job cannot silently return to ready. A retry can resume from the last durable stage instead of repeating the entire workflow.

Transitions should be enforced rather than implied:

const allowed: Record<JobState, JobState[]> = {
  queued: ["validating", "failed"],
  validating: ["extracting", "ocr", "failed"],
  extracting: ["ocr", "translating", "failed"],
  ocr: ["translating", "failed"],
  translating: ["rendering", "failed"],
  rendering: ["verifying", "failed"],
  verifying: ["ready", "failed"],
  ready: ["expired"],
  failed: [],
  expired: [],
};

function canTransition(from: JobState, to: JobState): boolean {
  return allowed[from].includes(to);
}
Enter fullscreen mode Exit fullscreen mode

In a real system I would also record attempt, updatedAt, failureCode, and the worker version that produced each stage. That is enough to distinguish a bad document from a deployment regression.

Put Retry Boundaries Around Durable Work

Retries are necessary, but “retry the job” is too broad.

Uploading, OCR, translation, and rendering have different failure modes. A network timeout while writing the final PDF should not trigger OCR again. A transient translation-provider error should not require another upload. A user pressing the submit button twice should not create two independent jobs.

The idempotency key belongs at job creation. Stage-specific keys can be derived from the job and input version:

function stageKey(
  jobId: string,
  stage: JobState,
  inputVersion: number,
): string {
  return `${jobId}:${stage}:${inputVersion}`;
}
Enter fullscreen mode Exit fullscreen mode

Workers should write their result before advancing the state. If the process stops between those operations, the next worker can see the existing stage output and continue safely.

Retries also need limits. OCR on a malformed image is unlikely to improve on attempt 12. Use stable failure codes such as PDF_ENCRYPTED, OCR_LOW_CONFIDENCE, TRANSLATION_TIMEOUT, and RENDER_OVERFLOW. They are more useful to support than a stack trace alone.

Translation Units Need Stable Identity

Sending one entire document as a string loses layout relationships. Sending every line independently loses context.

A practical middle ground is a collection of blocks with stable IDs:

interface TextBlock {
  id: string;
  page: number;
  role: "heading" | "paragraph" | "caption" | "table-cell" | "footer";
  sourceText: string;
  translatedText?: string;
  bounds: { x: number; y: number; width: number; height: number };
  ocrConfidence?: number;
}
Enter fullscreen mode Exit fullscreen mode

Stable IDs make it possible to retry one segment, preserve repeated headers, compare source and translation, and point a human reviewer to a specific page and region.

Context can be supplied by grouping adjacent blocks or attaching a document glossary. The glossary should protect product names, UI labels, units, and phrases that must remain unchanged. It is also the right place to keep reviewer-approved terminology between versions.

Verification Is More Than “The File Opens”

A translated PDF can be syntactically valid and unusable.

Automated verification should look for structural signals:

  • output page count differs unexpectedly from the source;
  • pages contain no text or images;
  • translated blocks are missing;
  • text extends outside its assigned bounds;
  • a table has a different number of rows or cells;
  • a required font lacks glyphs for the target language;
  • hyperlinks disappeared;
  • OCR confidence is below a review threshold;
  • numbers present in the source are absent from the translated block.

These checks do not prove semantic accuracy. They decide whether the result is safe to show as an ordinary completion or should be flagged for review.

For customer-facing, legal, medical, financial, or safety-related documents, a human review is not an optional fallback. The workflow should make that requirement visible instead of letting a fluent result imply certification.

Retention Is Part of the Data Model

Document pipelines tend to become accidental archives.

The source upload, extracted text, OCR images, model inputs, intermediate render, and final file may all exist in different systems. Deleting only the public download does not remove the job data.

Every artifact should carry a retention class and expiry:

interface StoredArtifact {
  key: string;
  kind: "source" | "extracted" | "ocr" | "translated" | "result";
  deleteAfter: string;
  encrypted: boolean;
}
Enter fullscreen mode Exit fullscreen mode

Use short-lived signed URLs for downloads. Run deletion as an observable job. Record completion without keeping the deleted content. If business users need a permanent copy, make them move it into the approved system of record rather than silently extending temporary storage.

Make Operations Visible

A progress bar is useful to the person waiting, but it is not enough for the team operating the workflow.

I would track metrics by stage and route:

  • validation failures by reason;
  • percentage of pages sent to OCR;
  • median and high-percentile duration for extraction, OCR, translation, and rendering;
  • retries and terminal failures by worker version;
  • output files flagged for human review;
  • deletion jobs completed after their deadline;
  • jobs abandoned before download.

These measurements answer different questions. A sudden increase in OCR pages may indicate a change in customer inputs. Longer rendering time after a deployment points toward layout code, not the translation provider. A high completion rate with a low download rate may mean results expire too quickly or the notification step is unreliable.

Logs should carry the job ID, stage, attempt, and block or page range, but not extracted document text by default. Document contents make debugging tempting and data handling much harder. Store diagnostic metadata first; collect a redacted sample only through an explicit support path.

Build, Run Locally, or Use a Hosted Workflow?

Not every team should build this pipeline.

Building makes sense when translation is part of the product, jobs repeat at scale, restricted documents are involved, or the team needs precise control over models, audit logs, and retention. Local tools are attractive when a technical operator can process sensitive files without uploading them.

For an occasional public or synthetic document, a hosted interface can remove a lot of setup. PDFTranslator is one example: it supports OCR, more than 100 languages, and translation styles, with a 20 MB upload limit and a stated free allowance of 1,000 pages per calendar month. It is still a cloud workflow. Its homepage says task files are deleted within 24 hours, which is useful operational information but not a reason to upload restricted material without an approved policy.

That tradeoff is the important part. Convenience changes who operates the pipeline; it does not remove the need to classify the document or review the result.

A Production-Readiness Checklist

Before calling a PDF translation workflow reliable, I would want clear answers to these questions:

  1. Do we validate file contents rather than extensions?
  2. Can we route text, scanned, and mixed PDFs differently?
  3. Are job states and legal transitions explicit?
  4. Are retries idempotent and limited to the failed stage?
  5. Can we trace translated blocks back to source regions?
  6. Do we verify page structure, overflow, fonts, tables, links, and numbers?
  7. Can the system require human review instead of merely suggesting it?
  8. Does every stored artifact have a deletion rule?
  9. Can support explain a failure without reading worker logs?
  10. Have we decided which documents are allowed to leave the device?

The model call may be the most visible part of translation, but it is not the system. The system is the set of boundaries that keeps an untrusted document, a long-running process, and a persuasive-looking output from creating a quiet operational failure.

Top comments (0)