If you've ever helped a friend, partner, or yourself apply for a job abroad, you know the paperwork problem isn't really about translation quality. It's about tracking a pile of documents (employer letters, work certificates, reference letters) through multiple states: original → scanned → translated → certified → apostilled → submitted.
This is a document pipeline problem, and it's one developers are well equipped to solve, even with simple tooling. The legal and procedural side of certified translation for employment documents is covered well in this article on certified translation of employment experience letters. This post covers the technical side: how to build a lightweight system to manage the process instead of tracking it in your email inbox.
Why this is harder than it looks
A single international application can involve:
- 3-5 source documents (employer letters, certificates, performance reviews)
- Each needing OCR-quality verification before translation
- A certification step (lawyer or notary signature, in Portugal's case)
- Possibly an apostille from a separate government body
- Cross-referencing names and dates against a CV
Miss a stamp during scanning, or let one document's dates drift from your CV, and the whole application can bounce. This is exactly the kind of multi-step, stateful process that benefits from being modeled explicitly rather than tracked ad hoc.
Step 1: Validate scans before they go anywhere
The source article notes that "faint stamps or signatures cut off during scanning slow down certification." This is a solvable problem with basic image processing before you ever send a document to a translator.
A simple pre-check script using OpenCV and Tesseract can flag likely problem scans:
import cv2
import pytesseract
def check_scan_quality(image_path, min_text_confidence=60):
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Flag low contrast (common cause of faint stamps)
contrast = gray.std()
if contrast < 40:
print(f"WARNING: low contrast ({contrast:.1f}) — stamps may be unreadable")
# Run OCR confidence check
data = pytesseract.image_to_data(gray, output_type=pytesseract.Output.DICT)
confidences = [int(c) for c in data['conf'] if c != '-1']
avg_conf = sum(confidences) / len(confidences) if confidences else 0
if avg_conf < min_text_confidence:
print(f"WARNING: OCR confidence {avg_conf:.1f}% — rescan recommended")
return {"contrast": contrast, "ocr_confidence": avg_conf}
This won't replace a human translator checking the document, but it catches the obvious failures (cut-off edges, washed-out stamps) before you waste a certification cycle on a bad scan.
Step 2: Build a document state machine
Each document in the application needs to move through a defined set of states. Modeling this explicitly, even in a spreadsheet or a small SQLite table, prevents the "did I already send this one" problem.
from enum import Enum
from dataclasses import dataclass, field
from datetime import date
class DocState(Enum):
COLLECTED = "collected"
SCANNED = "scanned"
SENT_FOR_TRANSLATION = "sent_for_translation"
TRANSLATED = "translated"
CERTIFIED = "certified"
APOSTILLED = "apostilled"
SUBMITTED = "submitted"
@dataclass
class Document:
name: str
doc_type: str # employer_letter, reference_letter, certificate
state: DocState = DocState.COLLECTED
requires_apostille: bool = False
dates_mentioned: list = field(default_factory=list)
names_mentioned: list = field(default_factory=list)
def can_submit(self):
if self.requires_apostille:
return self.state == DocState.APOSTILLED
return self.state in (DocState.CERTIFIED, DocState.APOSTILLED)
The can_submit() check matters because it encodes exactly the rule the source article flags as a common mistake: documents go out without the apostille they actually needed, because someone assumed certification alone was enough.
Step 3: Cross-reference dates and names automatically
One of the stated reasons applications get rejected is inconsistent names and dates across documents. This is a text-matching problem you can partially automate.
import re
from datetime import datetime
def extract_dates(text):
# crude pattern, adjust for your document formats
pattern = r'\b(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})\b'
return re.findall(pattern, text)
def cross_check_documents(documents_text: dict, cv_text: str):
cv_dates = set(extract_dates(cv_text))
mismatches = {}
for doc_name, text in documents_text.items():
doc_dates = set(extract_dates(text))
missing_in_cv = doc_dates - cv_dates
if missing_in_cv:
mismatches[doc_name] = missing_in_cv
return mismatches
This isn't a substitute for careful proofreading, but running every document and your CV through a script like this before sending anything for certification catches the kind of typo that otherwise surfaces three weeks later when a qualifications body sends a rejection email.
Step 4: Track the certification and apostille chain
The source article explains that Portugal's certification model routes through a lawyer or notary, and the apostille is a separate step through the PGR. These are two different authorities with two different turnaround times, and conflating them in your tracking is how deadlines get missed.
If you're coordinating this for multiple documents or multiple family members' applications, a simple status table beats a scattered email thread:
| Document | Certification status | Apostille needed | Apostille status | Submission deadline |
|---|---|---|---|---|
| Employer letter | Certified 03/14 | Yes | Pending | 04/01 |
| Reference letter | Pending | No | N/A | 04/01 |
Even a Google Sheet with conditional formatting on the deadline column solves 80% of the coordination problem. If you want something more durable, this maps cleanly onto a small Airtable base or a few tables in Postgres with a status enum matching the DocState above.
Where this pipeline actually ends
None of this replaces the legal step. A script can flag a blurry stamp, but it can't certify a translation, and it definitely can't decide whether your destination country needs an apostille or consular legalisation. That part is a legal and procedural question, covered in detail in the source article on certified translation for employment experience letters.
What this pipeline does is remove the operational noise: bad scans, missed apostille steps, and inconsistent dates, so that by the time a document reaches a certified translator or a notary, it's actually ready to be processed correctly the first time.
If you're building something similar for your own relocation or for a team handling multiple applicants, start with the state machine. Everything else (OCR checks, date matching, deadline tracking) can be added incrementally once you have a clear model of what state each document is actually in.
Top comments (1)
I'd bind the certification/apostille state to an exact document version, not just its name. A useful fixture: certify v1, then replace its scan or translation with v2 while leaving the status row untouched. can_submit() should not treat v2 as certified merely because v1 reached that state. A source hash, translated-file hash and evidence link for each completed step would make the chain reviewable. Also, a stamp cropped out of an otherwise crisp page can leave average OCR confidence high, so I'd keep the image checks as triage rather than proof that every stamp/signature is present.