DEV Community

Cover image for Building a Document Translation API: Handling Legal Requirements Programmatically
Diogo Heleno
Diogo Heleno

Posted on • Originally published at m21global.com

Building a Document Translation API: Handling Legal Requirements Programmatically

Building a Document Translation API: Handling Legal Requirements Programmatically

When building applications that handle international documents, you'll quickly encounter the complexity of legal translation requirements. Different document types need different authentication levels, and getting it wrong means rejected submissions and frustrated users.

I recently built a document processing system for a client handling immigration paperwork, and learned the hard way that "translation" isn't just about converting text from one language to another. Legal documents require specific certification levels that vary by jurisdiction and use case.

Here's how to architect a system that handles these requirements correctly.

Understanding the Three Document States

Before writing any code, you need to model the different authentication levels your system might encounter:

const DocumentAuthLevels = {
  CERTIFIED: 'certified',
  SWORN: 'sworn', 
  APOSTILLED: 'apostilled'
};

const DocumentRequirement = {
  level: DocumentAuthLevels.CERTIFIED,
  requiresNotarization: false,
  acceptedBy: ['universities', 'hr_departments'],
  jurisdiction: 'US'
};
Enter fullscreen mode Exit fullscreen mode

Certified translations work for most administrative purposes. The translation company provides a signed declaration that the translation is accurate. This covers university applications, HR onboarding, and many business processes.

Sworn translations carry legal weight through official government authorization. In some countries, only government-appointed translators can produce these. Others require notarial authentication of the translator's signature.

Apostilles authenticate the original document itself, not the translation. They're issued by government authorities and remove the need for embassy legalization between Hague Convention countries.

Designing the Requirements Engine

The key insight is that translation requirements depend on three variables: document purpose, receiving institution, and destination jurisdiction. You need a rules engine to map these correctly:

class TranslationRequirements {
  constructor() {
    this.rules = new Map();
    this.loadRequirements();
  }

  loadRequirements() {
    // Court documents always need sworn translation
    this.addRule({
      purpose: 'judicial',
      requirement: DocumentAuthLevels.SWORN,
      notarization: true
    });

    // Immigration varies by country
    this.addRule({
      purpose: 'immigration',
      jurisdiction: 'PT',
      requirement: DocumentAuthLevels.SWORN,
      notarization: true
    });

    // Academic applications often accept certified
    this.addRule({
      purpose: 'academic',
      institution: 'university',
      requirement: DocumentAuthLevels.CERTIFIED
    });
  }

  getRequirements(purpose, jurisdiction, institution = null) {
    const key = `${purpose}-${jurisdiction}-${institution || 'default'}`;
    return this.rules.get(key) || this.getDefaultRequirement();
  }

  requiresApostille(sourceCountry, destCountry) {
    const hagueCountries = this.getHagueConventionCountries();
    return hagueCountries.includes(sourceCountry) && 
           hagueCountries.includes(destCountry);
  }
}
Enter fullscreen mode Exit fullscreen mode

Implementing Document Workflow Logic

Once you know what's required, you need to orchestrate the actual workflow. Documents often need multiple steps in sequence:

class DocumentProcessor {
  async processDocument(document, requirements) {
    const workflow = this.buildWorkflow(requirements);
    let result = document;

    for (const step of workflow.steps) {
      result = await this.executeStep(step, result);

      // Store intermediate results for audit trail
      await this.saveProgress(document.id, step.type, result);
    }

    return result;
  }

  buildWorkflow(requirements) {
    const steps = [];

    // Step 1: Apostille original if needed
    if (requirements.needsApostille) {
      steps.push({
        type: 'apostille',
        target: 'original',
        authority: requirements.apostilleAuthority
      });
    }

    // Step 2: Translate document
    steps.push({
      type: 'translate',
      level: requirements.authLevel,
      targetLanguage: requirements.language
    });

    // Step 3: Notarize translation if required
    if (requirements.notarization) {
      steps.push({
        type: 'notarize',
        target: 'translation'
      });
    }

    return { steps, estimatedDays: this.calculateTimeline(steps) };
  }
}
Enter fullscreen mode Exit fullscreen mode

Handling Provider Integration

Different translation providers have different capabilities. Some can handle sworn translations, others only certified. Build an adapter pattern to manage this:

class TranslationProviderAdapter {
  constructor() {
    this.providers = {
      certified: new CertifiedTranslationAPI(),
      sworn: new SwornTranslationAPI(),
      notary: new NotaryAPI()
    };
  }

  async requestTranslation(document, requirements) {
    const provider = this.selectProvider(requirements);

    if (!provider) {
      throw new Error(`No provider available for ${requirements.authLevel}`);
    }

    const request = {
      documentId: document.id,
      sourceLanguage: document.language,
      targetLanguage: requirements.targetLanguage,
      authLevel: requirements.authLevel,
      urgency: requirements.urgency || 'standard'
    };

    return await provider.submitTranslation(request);
  }

  selectProvider(requirements) {
    if (requirements.authLevel === DocumentAuthLevels.SWORN) {
      return this.providers.sworn;
    }
    return this.providers.certified;
  }
}
Enter fullscreen mode Exit fullscreen mode

Validation and Error Prevention

The biggest operational risk is submitting the wrong document type. Build validation that catches this early:

class DocumentValidator {
  validate(document, submissionContext) {
    const required = this.getRequirements(
      submissionContext.purpose,
      submissionContext.jurisdiction,
      submissionContext.institution
    );

    const errors = [];

    if (required.authLevel === DocumentAuthLevels.SWORN && 
        document.authLevel === DocumentAuthLevels.CERTIFIED) {
      errors.push({
        code: 'INSUFFICIENT_AUTH',
        message: 'Sworn translation required, certified provided',
        severity: 'blocking'
      });
    }

    if (required.needsApostille && !document.apostilled) {
      errors.push({
        code: 'MISSING_APOSTILLE',
        message: 'Document requires apostille authentication',
        severity: 'blocking'
      });
    }

    return { valid: errors.length === 0, errors };
  }
}
Enter fullscreen mode Exit fullscreen mode

Tracking Status and Costs

Legal translations involve multiple parties and can take weeks. Build comprehensive tracking:

const DocumentStatus = {
  RECEIVED: 'received',
  APOSTILLE_PENDING: 'apostille_pending',
  TRANSLATION_PENDING: 'translation_pending',
  NOTARIZATION_PENDING: 'notarization_pending',
  COMPLETE: 'complete',
  REJECTED: 'rejected'
};

class DocumentTracker {
  async updateStatus(documentId, status, metadata = {}) {
    await this.db.documents.update(documentId, {
      status,
      updatedAt: new Date(),
      metadata: {
        ...metadata,
        estimatedCompletion: this.calculateCompletion(status)
      }
    });

    // Notify interested parties
    await this.notifications.send(documentId, status);
  }

  calculateCosts(workflow) {
    let total = 0;

    workflow.steps.forEach(step => {
      switch(step.type) {
        case 'apostille':
          total += this.fees.apostille;
          break;
        case 'translate':
          total += step.level === DocumentAuthLevels.SWORN 
            ? this.fees.swornTranslation 
            : this.fees.certifiedTranslation;
          break;
        case 'notarize':
          total += this.fees.notarization;
          break;
      }
    });

    return total;
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

Building systems that handle legal document translation requires more than just language processing. You need to model complex regulatory requirements, orchestrate multi-step workflows, and provide clear validation to prevent costly mistakes.

The rules engine approach scales well as you add new jurisdictions and document types. The adapter pattern lets you work with different providers without coupling your core logic to their APIs.

Most importantly, always validate requirements upfront. A certified translation rejected for legal proceedings wastes weeks and damages user trust.

For more context on the different authentication levels and when each applies, this practical guide to certified, sworn, and apostilled translations breaks down the legal distinctions clearly.

The complexity is worth it. Get this right, and you'll have a system that handles international document workflows reliably at scale.

Top comments (0)