<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Diogo Heleno</title>
    <description>The latest articles on DEV Community by Diogo Heleno (@diogoheleno).</description>
    <link>https://dev.to/diogoheleno</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3856271%2F9c35a5a2-9068-4e47-9795-fca17fffbd0c.jpg</url>
      <title>DEV Community: Diogo Heleno</title>
      <link>https://dev.to/diogoheleno</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/diogoheleno"/>
    <language>en</language>
    <item>
      <title>Building a Terminology Pipeline for Multilingual SOPs (So Translators Don't Reinvent Your Vocabulary Every Cycle)</title>
      <dc:creator>Diogo Heleno</dc:creator>
      <pubDate>Fri, 13 Nov 2026 09:35:44 +0000</pubDate>
      <link>https://dev.to/diogoheleno/building-a-terminology-pipeline-for-multilingual-sops-so-translators-dont-reinvent-your-1069</link>
      <guid>https://dev.to/diogoheleno/building-a-terminology-pipeline-for-multilingual-sops-so-translators-dont-reinvent-your-1069</guid>
      <description>&lt;p&gt;A badly translated SOP isn't a copywriting problem, it's a data consistency problem. If your company runs branches in multiple countries, the SOP that gets translated for São Paulo and the one that gets translated for Luanda should use the exact same term for "batch," the exact same modal verb for "must," and the exact same reference to the regulatory clause. In practice, that almost never happens, because most teams treat translation as a one-off task instead of a pipeline with state.&lt;/p&gt;

&lt;p&gt;This article from M21Global lays out why &lt;a href="https://www.m21global.com/en/blog/translating-sop-overseas-branches/" rel="noopener noreferrer"&gt;SOP translation deserves special handling&lt;/a&gt; compared to other technical documents, focusing on linguistic registers, review tiers and formatting. Worth reading if you're the one requesting the translation. This post is for the people who have to manage the documents and versions once the translated files come back: engineers, technical writers, and whoever owns the docs repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat SOP translation like you treat code localization
&lt;/h2&gt;

&lt;p&gt;If you've built an i18n pipeline for a product UI, you already know the pattern: extract strings, keep a translation memory, never let two translators independently decide what "Cancel" means in Portuguese. SOPs need the same discipline, except the "strings" are full paragraphs with legal weight, and the "build" is a PDF that an auditor will read line by line.&lt;/p&gt;

&lt;p&gt;The failure mode is identical to what happens when you don't have a single source of truth for UI strings: duplicate keys, inconsistent wording, and a maintenance nightmare the moment you need to update something globally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set up a terminology database before you translate anything
&lt;/h2&gt;

&lt;p&gt;The fix isn't a better translator. It's infrastructure. Before any SOP goes out for translation, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A controlled glossary&lt;/strong&gt;: source term, target term per locale, definition, regulatory body reference (ANVISA, Infarmed, etc.), and approval status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A translation memory (TM)&lt;/strong&gt;: segment-level storage of previously approved translations, so repeated or near-identical sentences get reused instead of retranslated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A single owner for both&lt;/strong&gt;, usually the quality team at head office, not whichever branch happens to be expanding fastest that quarter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building this in-house rather than relying entirely on your translation vendor, the tooling looks a lot like a standard localization stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Example: structure for a terminology repo&lt;/span&gt;
/sop-l10n
  /glossary
    en-source.tbx       &lt;span class="c"&gt;# TBX is the standard interchange format for term bases&lt;/span&gt;
    pt-BR.tbx
    pt-AO.tbx
  /tm
    project.tmx         &lt;span class="c"&gt;# TMX stores aligned source/target segments&lt;/span&gt;
  /sop-source
    cleaning-procedure-v3.md
  /sop-translated
    cleaning-procedure-v3.pt-BR.md
    cleaning-procedure-v3.pt-AO.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TBX (TermBase eXchange) and TMX (Translation Memory eXchange) are both open XML standards, supported by every serious CAT tool (SDL Trados, memoQ, Phrase, Smartcat). If your vendor isn't working with these formats, you can't audit or reuse their terminology decisions, and you're locked into asking them to manually track consistency across every branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating consistency checks
&lt;/h2&gt;

&lt;p&gt;Once you have a glossary and TM, you can script QA checks instead of relying on a reviewer catching inconsistencies by eye. A few patterns worth automating:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="c1"&gt;# Flag modal verb downgrades that change obligation into permission&lt;/span&gt;
&lt;span class="no"&gt;RISKY_PATTERNS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;"deve"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"mandatory (deve)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# Portuguese "must"&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;"pode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"optional (pode)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;# Portuguese "may"&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_modal_consistency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="s2"&gt;"""
    Flags cases where a source 'must' clause might have been
    softened to 'may' in translation. Needs a source-target
    sentence alignment first (TM tools usually provide this).
    """&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s2"&gt;"must"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;source_text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="s2"&gt;"pode"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;target_text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"WARNING: possible obligation downgrade"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"OK"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is intentionally simplistic, real implementations should work on aligned segments from your TM rather than raw text diffing, but the point is: the same regex/linting mindset you apply to catching bugs in code can catch regulatory risk in translated SOPs. Nobody should be manually re-reading 40-page documents to catch a single "must" that became a "may."&lt;/p&gt;

&lt;h2&gt;
  
  
  Version control for translated documents
&lt;/h2&gt;

&lt;p&gt;Treat translated SOPs the way you treat compiled artifacts: never hand-edit them independently from the source. A sane setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source SOP lives in your main docs repo (Markdown, DITA, whatever you use).&lt;/li&gt;
&lt;li&gt;Each translation is a build output tied to a specific source commit/version.&lt;/li&gt;
&lt;li&gt;When the source changes, diff against the last translated commit and only send the delta to translators, not the whole document.&lt;/li&gt;
&lt;li&gt;Store the mapping (source version → translated version → translator → review status) somewhere queryable, not in a spreadsheet nobody updates.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# sop-manifest.yaml
&lt;/span&gt;&lt;span class="na"&gt;document&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cleaning-procedure&lt;/span&gt;
&lt;span class="na"&gt;source_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v3.2&lt;/span&gt;
&lt;span class="na"&gt;source_commit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;a1b2c3d&lt;/span&gt;
&lt;span class="na"&gt;translations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pt-BR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v3.2&lt;/span&gt;
    &lt;span class="na"&gt;translator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vendor-x&lt;/span&gt;
    &lt;span class="na"&gt;reviewed_by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;qa-team-brazil&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;approved&lt;/span&gt;
  &lt;span class="na"&gt;pt-AO&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v3.1&lt;/span&gt;   &lt;span class="c1"&gt;# lagging behind source, needs retranslation
&lt;/span&gt;    &lt;span class="na"&gt;translator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vendor-y&lt;/span&gt;
    &lt;span class="na"&gt;reviewed_by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pending&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This manifest alone will tell you, at a glance, which branches are running on outdated SOP translations, which is exactly the kind of gap that turns into a non-conformance during an audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this intersects with review tiers
&lt;/h2&gt;

&lt;p&gt;The source article breaks down review tiers by audit exposure: full dual-linguist review for anything an external auditor will see, lighter review for internal-only documents. That maps cleanly onto a CI/CD-style promotion model if you want to formalize it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Draft&lt;/strong&gt; → machine translation + glossary pass, internal use only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staging&lt;/strong&gt; → single linguist + glossary/TM enforcement, for internal procedure manuals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production&lt;/strong&gt; → dual linguist review + QA, for anything audit-facing or regulator-facing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Documents shouldn't move to "production" status until they pass the same kind of checklist you'd apply to a release: terminology check, formatting diff against source, control header fields intact, modal verb consistency pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real cost isn't translation, it's drift
&lt;/h2&gt;

&lt;p&gt;None of this is about mistrusting translators. It's about recognizing that the moment you have more than one branch and more than one language, SOP translation stops being a single task and becomes a system with state that can drift out of sync. Drift is what turns into a flagged non-conformance two years later, when nobody remembers why the Angola version says something subtly different from the Brazil version.&lt;/p&gt;

&lt;p&gt;If your organization is translating SOPs for multiple international branches, it's worth asking your vendor directly whether they maintain a shared glossary and TM across all your projects, or whether each branch effectively starts from zero. That answer tells you more about long-term translation quality than any single document review.&lt;/p&gt;

</description>
      <category>i18n</category>
      <category>productivity</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building a Terminology Validation Pipeline for CPNP Cosmetic Dossiers</title>
      <dc:creator>Diogo Heleno</dc:creator>
      <pubDate>Thu, 12 Nov 2026 09:04:40 +0000</pubDate>
      <link>https://dev.to/diogoheleno/building-a-terminology-validation-pipeline-for-cpnp-cosmetic-dossiers-4nk5</link>
      <guid>https://dev.to/diogoheleno/building-a-terminology-validation-pipeline-for-cpnp-cosmetic-dossiers-4nk5</guid>
      <description>&lt;p&gt;Regulatory translation for cosmetics has a nasty failure mode: the document reads perfectly well in the target language, passes a normal QA check, and is still non-compliant. INCI ingredient names get localized when they shouldn't be, standardized safety warnings get reworded for "flow", and claims like &lt;em&gt;hypoallergenic&lt;/em&gt; get translated literally into markets where that word carries different legal weight.&lt;/p&gt;

&lt;p&gt;The source article on &lt;a href="https://www.m21global.com/en/blog/cosmetic-documentation-translation-cpnp/" rel="noopener noreferrer"&gt;CPNP documentation translation&lt;/a&gt; covers the regulatory side of this well: what Regulation (EC) No 1223/2009 requires, which documents need translation, and where human review needs to happen. This article is about the complementary problem: if you're building or maintaining the software systems that generate, manage, or submit cosmetic product documentation, how do you catch these errors &lt;em&gt;before&lt;/em&gt; they reach a translator or a regulator?&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem is structural, not linguistic
&lt;/h2&gt;

&lt;p&gt;Most translation QA tooling checks for things like terminology consistency, tone, and glossary adherence. None of that catches a &lt;em&gt;correctly translated sentence that is still non-compliant&lt;/em&gt;. You need a layer that understands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which strings in your content model must never be translated (INCI names)&lt;/li&gt;
&lt;li&gt;Which strings must match an approved, standardized wording exactly (safety warnings)&lt;/li&gt;
&lt;li&gt;Which strings are free text but trigger regulatory review if certain terms appear (claims)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a classification problem you can solve with tooling, independent of whatever translation vendor or workflow you use downstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tagging content at the source
&lt;/h2&gt;

&lt;p&gt;If your product content lives in a CMS, PIM, or structured XML/JSON dossier format, the fix starts at the schema level. Don't let ingredient lists live as plain strings next to marketing copy. Tag them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"product_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"SKU-4471"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"ingredients"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"inci_list"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"locked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Aqua"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Glycerin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Retinol"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Parfum"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"warnings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"standardized_regulatory_text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"ref_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"EU-RETINOL-WARNING-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"locked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"claims"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"marketing_claim"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"locked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"flagged_terms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"hypoallergenic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"dermatologically tested"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;locked&lt;/code&gt; flag is the important part. It's a signal to every downstream system, translation memory tool, CAT tool, CMS export script, that this field should not be touched by free translation. It should either pass through unchanged (INCI) or be swapped for a pre-approved equivalent (standardized warnings).&lt;/p&gt;

&lt;h2&gt;
  
  
  A validation script, not a full pipeline
&lt;/h2&gt;

&lt;p&gt;You don't need a complex system to catch most of these errors. A pre-submission linter that runs against your exported dossier before it goes to CPNP or to a translation vendor catches a large share of mistakes for very little effort.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="no"&gt;INCI_PATTERN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;'^[A-Z][a-zA-Z\s\-]+$'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;FLAGGED_CLAIMS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"hypoallergenic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"dermatologically tested"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"paraben-free"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"clinically proven"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_ingredients&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ingredient_list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;translated_list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;original&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;translated&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ingredient_list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;translated_list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;original&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;translated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"INCI mismatch: '{original}' became '{translated}'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="no"&gt;INCI_PATTERN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;translated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Suspicious INCI format: '{translated}'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;flag_claims&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;term&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;term&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="no"&gt;FLAGGED_CLAIMS&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_warning_block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warning_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;approved_text_map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ref_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;approved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;approved_text_map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ref_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;approved&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;warning_text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;approved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Warning text for {ref_id} ({locale}) does not match approved wording"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn't production-grade, but it illustrates the shape of the check: diff against a source of truth, don't just spellcheck. The INCI check alone, a straight string comparison between source and translated ingredient fields, would have caught the most common error type the source article describes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maintaining an approved-text database
&lt;/h2&gt;

&lt;p&gt;Standardized warnings (retinol, UV filters, acids) exist in finite, enumerable sets per locale. If you're dealing with more than a handful of SKUs, it's worth maintaining these as a lookup table rather than trusting that each translator or CAT tool has the current approved wording.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;approved_warnings&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ref_id&lt;/span&gt; &lt;span class="kt"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;locale&lt;/span&gt; &lt;span class="kt"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;text&lt;/span&gt; &lt;span class="kt"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;regulation_ref&lt;/span&gt; &lt;span class="kt"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_updated&lt;/span&gt; &lt;span class="kt"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ref_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When regulatory wording changes (and it does, EU cosmetics legislation gets amended), you update one table instead of hunting through every product's labelling file. Your CI pipeline, if you have one for content, can run a job that flags any product referencing a &lt;code&gt;ref_id&lt;/code&gt; whose &lt;code&gt;last_updated&lt;/code&gt; predates the latest known regulation version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits with translation vendors
&lt;/h2&gt;

&lt;p&gt;None of this replaces a qualified translator or a second-linguist review for the documents that carry legal weight, the CPSR, safety data sheets, and labelling with claims. What it does is reduce the number of errors that reach that review stage, and it gives your translation vendor a clean signal about which fields are locked versus which are open for localization. Sending a vendor a dossier where ingredient fields are explicitly tagged &lt;code&gt;locked: true&lt;/code&gt; is a lot less error-prone than trusting that every translator in the chain knows INCI nomenclature by heart.&lt;/p&gt;

&lt;p&gt;If you're integrating with CPNP or managing a PIF (Product Information File) across multiple markets, the pre-submission validation pattern scales reasonably well: tag your content model, diff locked fields automatically, maintain approved text as data instead of as tribal knowledge in a style guide nobody reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Regulatory compliance for cosmetics translation is ultimately a data integrity problem wearing a legal costume. Treat INCI names, standardized warnings, and flagged claims as structured, validated fields in your content pipeline, not as free text that happens to go through a translation step, and you'll catch most of the expensive mistakes before they reach a regulator or a market surveillance inspector.&lt;/p&gt;

</description>
      <category>i18n</category>
      <category>webdev</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building a Document Requirements Checker for International Civil Registration Workflows</title>
      <dc:creator>Diogo Heleno</dc:creator>
      <pubDate>Thu, 12 Nov 2026 07:08:06 +0000</pubDate>
      <link>https://dev.to/diogoheleno/building-a-document-requirements-checker-for-international-civil-registration-workflows-1j8l</link>
      <guid>https://dev.to/diogoheleno/building-a-document-requirements-checker-for-international-civil-registration-workflows-1j8l</guid>
      <description>&lt;p&gt;Certified document translation sits at an odd intersection: it looks like a pure compliance problem, but in practice it's a workflow and data validation problem. If you've ever built internal tools for an immigration law firm, an HR team handling relocations, or a product that touches cross-border civil processes (marriage registration, visa sponsorship, dual citizenship), you've probably run into the same mess the source article describes from a non-technical angle: &lt;a href="https://www.m21global.com/en/blog/certified-translation-marriage-certificates-foreign-registration/" rel="noopener noreferrer"&gt;certified translation requirements for marriage certificates&lt;/a&gt; vary wildly by destination country, and getting the sequence wrong costs weeks.&lt;/p&gt;

&lt;p&gt;That article is written for people navigating the process manually. This one is for people who want to model it as a system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is a graph problem, not a form problem
&lt;/h2&gt;

&lt;p&gt;Most teams start by building a form: upload document, pick destination country, get a checklist. That works until you realize the requirements aren't independent fields, they're a dependency graph.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether you need an apostille or consular legalisation depends on whether the destination country is a Hague Convention signatory.&lt;/li&gt;
&lt;li&gt;Whether the translation needs to be apostilled &lt;em&gt;before&lt;/em&gt; or &lt;em&gt;after&lt;/em&gt; translation depends on the destination registry, not just the country.&lt;/li&gt;
&lt;li&gt;Document validity windows (e.g., "certificate must be issued within 6 months") are destination-specific and time-relative to submission date, not issuance date.&lt;/li&gt;
&lt;li&gt;Some destinations require translating supporting documents (birth certificates, prior divorce decrees) only &lt;em&gt;conditionally&lt;/em&gt;, based on facts about the applicant (was either party previously married?).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Modeling this as a flat checklist breaks the moment you have conditional requirements. A better fit is a rules engine over a small graph: country → convention status → document chain → conditional sub-documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal data model
&lt;/h2&gt;

&lt;p&gt;Here's a simplified schema that captures the real structure instead of hardcoding per-country logic in application code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="nf"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Enum&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LegalizationType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="no"&gt;APOSTILLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"apostille"&lt;/span&gt;
    &lt;span class="no"&gt;CONSULAR_LEGALIZATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"consular_legalization"&lt;/span&gt;
    &lt;span class="no"&gt;NONE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"none"&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CountryRequirement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;country_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;hague_member&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;legalization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;LegalizationType&lt;/span&gt;
    &lt;span class="n"&gt;translation_order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;  &lt;span class="c1"&gt;# "translate_then_legalize" | "legalize_then_translate"&lt;/span&gt;
    &lt;span class="n"&gt;certificate_validity_days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;requires_sworn_translator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicantFacts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;previously_married_either_party&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;destination_requires_individual_civil_records&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_document_checklist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CountryRequirement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;facts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ApplicantFacts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;issue_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;checklist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"original_marriage_certificate"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;issue_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;certificate_validity_days&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Certificate exceeds validity window — request a new one before proceeding"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;legalization&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;LegalizationType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;APOSTILLE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;checklist&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"apostille"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;legalization&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;LegalizationType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;CONSULAR_LEGALIZATION&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;checklist&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"foreign_ministry_authentication"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;checklist&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"consular_legalization"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;checklist&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"certified_translation"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;facts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;destination_requires_individual_civil_records&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;checklist&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"birth_certificates_both_spouses"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;facts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;previously_married_either_party&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;checklist&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"divorce_decree_or_death_certificate"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;checklist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is deliberately simple, but the point stands: encode country rules as data, not as &lt;code&gt;if country == "Angola"&lt;/code&gt; scattered across your codebase. Store &lt;code&gt;CountryRequirement&lt;/code&gt; records in a database table or config file, version them, and you get an audit trail when rules change (and they do change — consular requirements update without much notice).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hague Convention lookup isn't optional config, it's a maintained dataset
&lt;/h2&gt;

&lt;p&gt;Don't hardcode the list of Hague Apostille Convention members. It changes. As of the article's example, Angola and Mozambique are not members, so documents bound there always need consular legalisation regardless of where the source document originated. Build this as a small, separately versioned dataset you can update independently of your application release cycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"PT"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"hague_member"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"DE"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"hague_member"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"BR"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"hague_member"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"AO"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"hague_member"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"MZ"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"hague_member"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pull this from a maintained source (HCCH publishes the official member list) rather than trusting a static array someone wrote two years ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validity windows are a scheduling problem, not a validation problem
&lt;/h2&gt;

&lt;p&gt;The "certificate must be issued within X months" rule is where most manual processes actually fail, per the source article. This is solvable with basic date math, but the more useful engineering move is proactive: if you're building a case management tool, flag certificates approaching their expiry window &lt;em&gt;before&lt;/em&gt; submission, not after rejection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;days_until_expiry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;issue_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validity_days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;validity_days&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;today&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;issue_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;

&lt;span class="c1"&gt;# Surface a warning if fewer than 30 days remain before the certificate&lt;/span&gt;
&lt;span class="c1"&gt;# becomes stale relative to the destination's validity window&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your tool integrates with a translation vendor's API or intake form, trigger a reorder of the source certificate automatically when this threshold is crossed, instead of letting the applicant find out at submission time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Translator credentialing as a verifiable attribute, not a free-text field
&lt;/h2&gt;

&lt;p&gt;If you're building intake forms for translation vendors or law firms, don't store "translator qualification" as a free-text string. Destination countries check for specific credential types: court-sworn status, chamber registration, ISO 17100 workflow compliance (independent second-linguist review). Model it as an enum or tag set so you can filter and match translator pools against destination requirements programmatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TranslatorCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="no"&gt;COURT_SWORN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"court_sworn"&lt;/span&gt;
    &lt;span class="no"&gt;CHAMBER_REGISTERED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"chamber_registered"&lt;/span&gt;
    &lt;span class="no"&gt;ISO_17100_CERTIFIED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"iso_17100"&lt;/span&gt;
    &lt;span class="no"&gt;SELF_DECLARED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"self_declared"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters in practice: a translation produced under ISO 17100 (independent review by a second linguist) is a different compliance artifact than a self-declared accuracy statement, even if the output text is identical. If you're automating vendor selection, this is the field to filter on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The underlying process the source article walks through manually — check destination rules, confirm Hague status, sequence apostille and translation correctly, validate certificate age, assemble conditional sub-documents — maps cleanly onto a rules engine with versioned country data and a small dependency graph. If you're building tooling for immigration, relocation, or civil registration use cases, resist the urge to hardcode country logic into application code. Treat it as config, version it, and build expiry/validity checks as proactive alerts rather than post-submission failures.&lt;/p&gt;

&lt;p&gt;For the actual translation work, you still need a human translator with the right legal credential for the destination — that part doesn't automate. But everything around it (sequencing, validity checks, conditional document sets) is a solvable engineering problem most teams are still doing by hand in spreadsheets.&lt;/p&gt;

</description>
      <category>i18n</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Building a Localization Pipeline for EU Digital Product Passport Data</title>
      <dc:creator>Diogo Heleno</dc:creator>
      <pubDate>Wed, 11 Nov 2026 16:34:46 +0000</pubDate>
      <link>https://dev.to/diogoheleno/building-a-localization-pipeline-for-eu-digital-product-passport-data-45an</link>
      <guid>https://dev.to/diogoheleno/building-a-localization-pipeline-for-eu-digital-product-passport-data-45an</guid>
      <description>&lt;p&gt;The EU's Digital Product Passport (DPP) is one of those regulations that looks like a legal problem until you actually start implementing it, and then it turns into a very concrete data engineering problem. You end up with structured product data, multiple target languages per market, a QR code pointing to a live endpoint, and a compliance deadline. That's a systems design challenge, not just a translation task.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.m21global.com/en/blog/technical-translation-digital-product-passport/" rel="noopener noreferrer"&gt;M21Global article on DPP translation requirements&lt;/a&gt; does a good job covering the regulatory side: which documents need what level of review, and why a mistranslated substances-of-concern field can trigger a non-conformity rather than just a bad review. This post covers the other half: how to actually build the pipeline that keeps multilingual DPP content in sync with your product data as it changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this isn't a normal i18n problem
&lt;/h2&gt;

&lt;p&gt;If you've built multilingual product pages before, your instinct is to treat this like standard i18n: extract strings, send to a translation service, get them back, render per locale. DPP content breaks that model in a few ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's not UI copy, it's regulatory data with legal weight. A translated field isn't just displayed, it's audited.&lt;/li&gt;
&lt;li&gt;It needs to stay tied to structured formats (XML/JSON) that feed ERPs, catalogue systems, and the actual QR code endpoint.&lt;/li&gt;
&lt;li&gt;Different fields have different risk levels, so they can't all go through the same translation/review tier.&lt;/li&gt;
&lt;li&gt;Source content changes continuously as products get updated, recalled, or re-certified, so this is a living pipeline, not a one-time export.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So instead of a string-extraction tool, think of this as a content sync problem between your PLM/ERP system, a translation layer, and whatever renders the public-facing passport.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structuring the data so translation doesn't break it
&lt;/h2&gt;

&lt;p&gt;First problem to solve: keep translation changes scoped to translatable fields only, and never touch structure, units, or identifiers.&lt;/p&gt;

&lt;p&gt;A reasonable schema separates regulatory fields from descriptive fields explicitly, so you can route them differently later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"product_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"SKU-48213"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"regulatory"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"substances_of_concern"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"source_lang"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"pt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Contém 0,08% de ftalato de dibutilo (DBP)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"translatable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"risk_level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"high"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"product_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"SKU-48213"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"descriptive"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"marketing_description"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"source_lang"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"pt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Cadeira ergonómica fabricada com materiais reciclados"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"translatable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"risk_level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"low"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;risk_level&lt;/code&gt; field is the key addition. It lets you route content programmatically to the correct review tier instead of deciding manually per document, which doesn't scale past a few hundred SKUs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routing content by risk level
&lt;/h2&gt;

&lt;p&gt;Once fields are tagged, you can build a simple router that decides the translation workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;route_for_translation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"risk_level"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"TEP"&lt;/span&gt;       &lt;span class="c1"&gt;# translate + edit + proofread, second linguist review&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"risk_level"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"medium"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"MTPE"&lt;/span&gt;      &lt;span class="c1"&gt;# machine translation + full post-edit&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"MT_light"&lt;/span&gt;  &lt;span class="c1"&gt;# light post-edit or no review&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;product_fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"translatable"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;workflow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;route_for_translation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;queue_for_translation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This maps directly onto the tiered approach described in the source article (TEP for regulatory content, lighter MTPE-based workflows for high-volume descriptive content). The point of automating the routing is that nobody has to manually decide, SKU by SKU, whether a field is sensitive enough for a second linguist. You decide that once, at the schema level, and the pipeline enforces it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping terminology consistent at scale
&lt;/h2&gt;

&lt;p&gt;Terminology drift is the real failure mode here, not bad grammar. If "recyclable packaging" gets translated three different ways across your catalogue, that's a compliance inconsistency waiting to be flagged.&lt;/p&gt;

&lt;p&gt;If you're working with CAT tools or an LSP, make sure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A termbase (TBX format works well) is maintained per product line, not per project&lt;/li&gt;
&lt;li&gt;Translation memory (TMX) is reused across updates, not regenerated from scratch each release&lt;/li&gt;
&lt;li&gt;Any machine translation step is constrained by the termbase, not run freely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want a lightweight internal check before content even reaches translation, a simple glossary validation script catches obvious drift:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_terminology&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;glossary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;violations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;approved&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;glossary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;approved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;approved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;violations&lt;/span&gt;

&lt;span class="n"&gt;glossary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"product_line_glossary.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;violations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validate_terminology&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;glossary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This won't replace a human reviewer on regulatory content, but it catches obvious inconsistency before it ever reaches a translator, which saves review cycles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the QR code endpoint in sync
&lt;/h2&gt;

&lt;p&gt;The DPP has to be live and queryable, which means your translated content needs a versioning strategy, not just a one-time push. A few practical points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Version every field, not just the document. If a certificate gets re-issued, you need to know which language versions are stale.&lt;/li&gt;
&lt;li&gt;Treat the QR endpoint as an API, with locale as a query parameter or path segment (&lt;code&gt;/dpp/SKU-48213/en&lt;/code&gt;, &lt;code&gt;/dpp/SKU-48213/de&lt;/code&gt;), not a static generated file per language.&lt;/li&gt;
&lt;li&gt;Add a &lt;code&gt;last_verified&lt;/code&gt; timestamp per language version, separate from &lt;code&gt;last_updated&lt;/code&gt; on the source, so you can flag when a translation is lagging behind a source change.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"product_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"SKU-48213"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"lang"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"de"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"source_updated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"2025-03-10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"translation_updated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"2025-03-09"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"stale"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;status&lt;/code&gt; field is cheap to compute and extremely useful operationally: it's your early warning system for compliance drift across markets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;None of this replaces having a qualified linguist review your declarations of conformity, that part is non-negotiable and well covered in the source article. But the engineering work around it, tagging risk level, routing content, versioning translations against source changes, catching terminology drift before it reaches a human reviewer, is what determines whether this scales to a few thousand SKUs across a dozen EU markets or collapses into a spreadsheet nobody trusts.&lt;/p&gt;

&lt;p&gt;If you're building this now, start with the schema. Everything else (routing, versioning, glossary checks) falls out naturally once risk level and translation status are first-class fields in your product data, not an afterthought bolted on at export time.&lt;/p&gt;

</description>
      <category>i18n</category>
      <category>devops</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Tracking Multi-Language Document Pipelines: What Immigration Paperwork Can Teach Us About Data Integrity</title>
      <dc:creator>Diogo Heleno</dc:creator>
      <pubDate>Mon, 09 Nov 2026 08:28:36 +0000</pubDate>
      <link>https://dev.to/diogoheleno/tracking-multi-language-document-pipelines-what-immigration-paperwork-can-teach-us-about-data-23pj</link>
      <guid>https://dev.to/diogoheleno/tracking-multi-language-document-pipelines-what-immigration-paperwork-can-teach-us-about-data-23pj</guid>
      <description>&lt;p&gt;Immigration paperwork isn't something most developers think about, until they're the ones applying for a visa and realize their diploma, contract, and criminal record certificate all need certified translation before a government agency will even look at them.&lt;/p&gt;

&lt;p&gt;A recent article from M21Global (&lt;a href="https://www.m21global.com/en/blog/certified-translation-eu-blue-card-aima/" rel="noopener noreferrer"&gt;Certified Translation for EU Blue Card Applications&lt;/a&gt;) breaks down the document requirements for EU Blue Card applications filed with AIMA in Portugal. It's a solid read if you're navigating that specific process. But reading it through a technical lens surfaced something more generally interesting: this is fundamentally a &lt;strong&gt;data consistency problem across untrusted, human-generated inputs&lt;/strong&gt;, and it's one we deal with constantly in software, just with different stakes.&lt;/p&gt;

&lt;p&gt;If you've ever built an onboarding pipeline for KYC documents, multi-language form validation, or any system that ingests user-submitted PDFs and needs to reconcile names, dates, and identifiers across sources, this will feel familiar.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core problem: cross-document consistency
&lt;/h2&gt;

&lt;p&gt;The source article flags this as one of the most common failure points:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Inconsistent names across documents: the spelling of names must match exactly across all translated documents, including accents and surname order.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a string-matching and normalization problem, and it's exactly the kind of thing that should never be handled by eyeballing a stack of PDFs.&lt;/p&gt;

&lt;p&gt;If you're building (or advising on) a document intake system that handles multilingual identity documents, here's a practical approach:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Normalize before you compare
&lt;/h3&gt;

&lt;p&gt;Names with diacritics, transliterated scripts, and surname-order differences (given-name-first vs. family-name-first) break naive string equality checks immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Strip accents, lowercase, collapse whitespace
&lt;/span&gt;    &lt;span class="n"&gt;nfkd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;NFKD&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ascii_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nfkd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ASCII&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ignore&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ascii_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="nf"&gt;normalize_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;José María García-López&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# 'jose maria garcia-lopez'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This won't solve surname-order ambiguity on its own, but it eliminates the most common false mismatches before you even get to semantic comparison.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use fuzzy matching with a human review gate
&lt;/h3&gt;

&lt;p&gt;Don't auto-reject or auto-approve on fuzzy match scores. Use something like &lt;code&gt;rapidfuzz&lt;/code&gt; to flag documents for review rather than making binary pass/fail decisions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rapidfuzz&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fuzz&lt;/span&gt;

&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fuzz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;token_sort_ratio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;normalize_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ana Paula Silva&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;normalize_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Silva, Ana Paula&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# token_sort_ratio handles word-order differences
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set a threshold (say, 90+) for auto-pass, and route anything below that to a human reviewer instead of a hard rejection. The cost of a false rejection in a visa pipeline is an angry user and a support ticket; the cost of a false pass is a compliance problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apostilles and certification chains as a dependency graph
&lt;/h2&gt;

&lt;p&gt;The article also describes something that maps cleanly onto a concept developers already understand: dependency resolution.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For applicants bringing documents from countries that are party to the Hague Apostille Convention, the original document may also need an apostille in the country of issue before translation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a prerequisite step that has to complete before the next step (translation) can even start, and the required chain differs by country of origin. If you're designing a checklist tool or intake form for this kind of process (and plenty of relocation/immigration SaaS products exist to do exactly this), model it as a directed graph rather than a flat checklist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;documentRequirements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;diploma&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;requiresApostille&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;hagueConventionCountries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;requiresCertifiedTranslation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;apostille&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// must exist before translation step&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;criminalRecord&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;requiresApostille&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;hagueConventionCountries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;requiresCertifiedTranslation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;apostille&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A flat checklist UI ("have you translated your documents? yes/no") hides ordering requirements that cause real rework. A graph-based model, even a simple one, lets you surface the correct sequence to the user and block premature steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a lightweight i18n document tracker
&lt;/h2&gt;

&lt;p&gt;If you're a freelance developer or building internal tooling for an immigration law firm, relocation agency, or HR department handling visa sponsorships, a minimal tracker doesn't need to be complex. The core entities are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Document&lt;/strong&gt; (type, source language, issuing country, issue date)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CertificationStatus&lt;/strong&gt; (none, apostilled, translated, certified)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ValidationFlag&lt;/strong&gt; (name mismatch, missing annex, terminology concern)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;applicant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;doc_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- 'diploma', 'contract', 'criminal_record'&lt;/span&gt;
    &lt;span class="n"&gt;source_lang&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;issuing_country&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;needs_apostille&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;apostille_obtained_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;certified_translation_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt; &lt;span class="c1"&gt;-- pending, flagged, approved&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;validation_flags&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;document_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;flag_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- 'name_mismatch', 'missing_annex', 'terminology'&lt;/span&gt;
    &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;resolved&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you queryable state instead of a shared spreadsheet, which is still how a surprising number of relocation cases get tracked today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terminology mapping is a translation memory problem
&lt;/h2&gt;

&lt;p&gt;One detail in the source article is worth calling out for anyone working on localization tooling:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Academic terminology poorly rendered: degrees, institutions and fields of study need to correspond to the Portuguese education system or to a formulation AIMA can recognise.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This isn't a "translate word for word" problem, it's an equivalence-mapping problem, similar to currency or unit conversion but for institutional terms. If you're building tooling for certified translators (or integrating with services like M21Global's &lt;a href="https://m21global.com/en/services/legal-translation" rel="noopener noreferrer"&gt;legal translation workflow&lt;/a&gt;), a termbase or translation memory system (TMX format, or a simple key-value store per jurisdiction) pays for itself quickly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source_term"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bachelor of Science"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source_lang"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target_lang"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pt-PT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target_term"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Licenciatura"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jurisdiction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"notes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Not 'Bacharelato' - that term is used differently in PT-PT vs PT-BR"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This kind of mapping, maintained centrally and reused across cases, is exactly how certified translation agencies avoid the inconsistency that gets applications bounced back by agencies like AIMA.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The original article is aimed at people filing EU Blue Card applications and explains, correctly, why Portugal's certification process (lawyer or notary sign-off, not a sworn translator) differs from what applicants from Brazil or Spain might expect. That's valuable domain knowledge if you're the one filing.&lt;/p&gt;

&lt;p&gt;But if you're building tools adjacent to this space, immigration tech, relocation platforms, HR systems for international hires, the underlying patterns (fuzzy identity matching, dependency-ordered document workflows, and terminology equivalence mapping) are the same problems you'd solve in any system that has to reconcile human-submitted, multilingual, multi-jurisdiction data. Worth building for, even outside the immigration context.&lt;/p&gt;

</description>
      <category>i18n</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Building a Terminology Pipeline for Multilingual Pitch Decks (Before Your Series A Blows Up)</title>
      <dc:creator>Diogo Heleno</dc:creator>
      <pubDate>Mon, 09 Nov 2026 07:05:02 +0000</pubDate>
      <link>https://dev.to/diogoheleno/building-a-terminology-pipeline-for-multilingual-pitch-decks-before-your-series-a-blows-up-416n</link>
      <guid>https://dev.to/diogoheleno/building-a-terminology-pipeline-for-multilingual-pitch-decks-before-your-series-a-blows-up-416n</guid>
      <description>&lt;p&gt;A startup I worked with raised a seed round in Lisbon, then went to raise a Series A from a UK fund. Same company, same numbers, same traction. Three different people translated three different documents (the deck, the financial model notes, and the term sheet summary) over three weeks, with no shared glossary. The result: "receita" became "revenue" in one file and "turnover" in another, ARR was calculated two different ways in the deck versus the model, and the investor's associate flagged it in the first due diligence call.&lt;/p&gt;

&lt;p&gt;Nobody lost the round over this, but it cost a week of clarifying emails during a period when speed matters more than almost anything else. The source article on &lt;a href="https://www.m21global.com/en/blog/investor-pitch-deck-translation-funding-rounds/" rel="noopener noreferrer"&gt;pitch deck translation for funding rounds&lt;/a&gt; covers why this happens from a linguistic and financial-register perspective. This post covers it from the other side: how to build a pipeline so it doesn't happen to your documents, using tools you probably already have access to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual problem is version control, not translation quality
&lt;/h2&gt;

&lt;p&gt;Treat a pitch deck, a financial model, a term sheet and a data room as what they actually are: a distributed system with no single source of truth for terminology. Every document is edited independently, often by different people, often under deadline pressure, and nothing enforces consistency between them.&lt;/p&gt;

&lt;p&gt;This is a solved problem in software. We just don't usually apply the solution to fundraising docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: build a glossary as a structured data file, not a spreadsheet tab
&lt;/h2&gt;

&lt;p&gt;Skip the shared Google Sheet that three people forget to check. Define your glossary as a JSON or YAML file that lives in version control alongside your docs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# glossary.yml&lt;/span&gt;
&lt;span class="na"&gt;terms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;receita&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;recorrente&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;mensal"&lt;/span&gt;
    &lt;span class="na"&gt;target_en&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MRR"&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;financial_metric"&lt;/span&gt;
    &lt;span class="na"&gt;do_not_translate_as&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;monthly&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;recurring&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;income"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;monthly&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;revenue"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;taxa&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;de&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;queima"&lt;/span&gt;
    &lt;span class="na"&gt;target_en&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;burn&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;rate"&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;financial_metric"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pré-money"&lt;/span&gt;
    &lt;span class="na"&gt;target_en&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pre-money&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;valuation"&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cap_table"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quadro&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;de&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;capital"&lt;/span&gt;
    &lt;span class="na"&gt;target_en&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cap&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;table"&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cap_table"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you something machines and humans can both check against. Translators (human or AI-assisted) get an authoritative reference. You get something you can lint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: lint your documents against the glossary
&lt;/h2&gt;

&lt;p&gt;If your deck and financial model exist as Markdown, plain text exports, or even extracted PDF text, you can write a basic consistency checker in a few lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_glossary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;terms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_consistency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;glossary&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;issues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;term&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;glossary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;forbidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;do_not_translate_as&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;bad_phrase&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;forbidden&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bad_phrase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Found &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bad_phrase&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, expected &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;target_en&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;issues&lt;/span&gt;

&lt;span class="n"&gt;glossary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_glossary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;glossary.yml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;financial_model_notes.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;issue&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;check_consistency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;glossary&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is intentionally simple. You can extend it with fuzzy matching, number format validation (comma vs. period decimal separators are a real recurring bug, not a nitpick), or plug it into a CI step that runs every time someone updates a document in your data room repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: catch number formatting bugs explicitly
&lt;/h2&gt;

&lt;p&gt;The article mentions this and it deserves its own validation step, because it's a silent, high-severity bug. Portuguese uses comma as decimal separator and period as thousands separator. English does the reverse. A number like &lt;code&gt;1.500.000,00&lt;/code&gt; becomes &lt;code&gt;1,500,000.00&lt;/code&gt;, and if that conversion gets fumbled, your ask or valuation can silently change by a factor of a thousand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;convert_pt_to_en_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value_str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Portuguese: 1.500.000,50 -&amp;gt; English: 1500000.50
&lt;/span&gt;    &lt;span class="n"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value_str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;convert_pt_to_en_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.500.000,50&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;1500000.50&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run every number that appears in your deck, model, and term sheet through a function like this, then diff the output against what a human translator actually typed into the English version. This catches the single costliest class of error in the entire process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: treat your data room as a monorepo with a translation memory
&lt;/h2&gt;

&lt;p&gt;Tools like &lt;a href="https://weblate.org/" rel="noopener noreferrer"&gt;Weblate&lt;/a&gt;, &lt;a href="https://poeditor.com/" rel="noopener noreferrer"&gt;POEditor&lt;/a&gt;, or even a translation memory file in the &lt;a href="https://en.wikipedia.org/wiki/Translation_Memory_eXchange" rel="noopener noreferrer"&gt;TMX format&lt;/a&gt; let you store approved translations of recurring phrases and reuse them automatically across documents. If you're already using an LLM to draft or review translations, feed it the glossary as context rather than letting it improvise terminology per document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;system_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
You are translating investor-facing financial documents from Portuguese to English.
Use only the following approved terminology. Do not substitute synonyms:
&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;glossary_as_text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
Maintain investor-English register: direct, quantified, no unsupported adjectives.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This won't replace a certified human translator for legal documents like articles of association or board resolutions, where certification and notarisation requirements apply (the source article covers this well, including the Angola-specific case). But for the deck, one-pager, and financial model notes, a glossary-constrained review pass catches most of the drift before it reaches a human reviewer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: automate the diff check before every send
&lt;/h2&gt;

&lt;p&gt;Before sending an updated deck to an investor, run a quick script that extracts every number and every glossary term from the new version and diffs it against the previous approved version and the financial model. If MRR changed in the deck but not in the model, or a number's decimal placement looks suspicious, you want that flagged before the investor sees it, not after.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# simple pre-send check&lt;/span&gt;
python extract_terms.py deck_v3.md &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; deck_terms.txt
python extract_terms.py financial_model.md &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; model_terms.txt
diff deck_terms.txt model_terms.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where this fits with professional translation
&lt;/h2&gt;

&lt;p&gt;None of this replaces a qualified translator for the documents that require certification, or a native-speaker review pass for tone and register, which is where the real skill of adjusting "innovative and disruptive" into something an investor actually trusts lives. What it does is remove the class of error that has nothing to do with translation quality and everything to do with process: inconsistent terms across documents, silent number formatting bugs, and last-minute edits that don't propagate everywhere they should.&lt;/p&gt;

&lt;p&gt;If you're raising across borders, build the glossary and the lint step before the round opens, the same way you'd set up CI before a product launch. It's cheap insurance against the kind of mismatch that makes an investor's associate start asking questions you'd rather answer on your own terms.&lt;/p&gt;

</description>
      <category>i18n</category>
      <category>productivity</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building an i18n Pipeline for API Docs That Doesn't Break Every Release</title>
      <dc:creator>Diogo Heleno</dc:creator>
      <pubDate>Sun, 08 Nov 2026 07:06:12 +0000</pubDate>
      <link>https://dev.to/diogoheleno/building-an-i18n-pipeline-for-api-docs-that-doesnt-break-every-release-4e03</link>
      <guid>https://dev.to/diogoheleno/building-an-i18n-pipeline-for-api-docs-that-doesnt-break-every-release-4e03</guid>
      <description>&lt;h2&gt;
  
  
  The problem nobody budgets for
&lt;/h2&gt;

&lt;p&gt;If you've ever maintained a developer portal, you know the docs change faster than anyone wants to admit. New endpoint, new param, deprecation notice, a fixed typo in a code sample — it never stops. Now add translation into that mix and most teams panic and either give up on localisation entirely or bolt on a manual process that breaks within two releases.&lt;/p&gt;

&lt;p&gt;There's a good write-up on the content and terminology side of this problem in &lt;a href="https://www.m21global.com/en/blog/api-documentation-developer-portal-localisation/" rel="noopener noreferrer"&gt;this article on API documentation localisation&lt;/a&gt;. It covers why API docs need different handling than a user manual: code should never be translated, terminology has to stay locked across every page, and the whole thing is a moving target instead of a static document.&lt;/p&gt;

&lt;p&gt;That's the editorial side. This post is about the plumbing: how to actually wire translation into a docs pipeline that's built on Git, Markdown, and CI, without it turning into a manual copy-paste nightmare every sprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't translate the whole file, translate the diff
&lt;/h2&gt;

&lt;p&gt;The single biggest mistake teams make is sending the entire docs repo to translation every time something changes. If you have 300 pages of OpenAPI-generated reference and you change one endpoint, you don't need 299 pages retranslated — you need a diff-based workflow.&lt;/p&gt;

&lt;p&gt;A reasonable setup looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate a diff of changed docs since last translated commit&lt;/span&gt;
git diff last-translated-tag HEAD &lt;span class="nt"&gt;--&lt;/span&gt; docs/ &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; changes.diff

&lt;span class="c"&gt;# Extract only the files with actual content changes (not formatting)&lt;/span&gt;
git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; last-translated-tag HEAD &lt;span class="nt"&gt;--&lt;/span&gt; docs/&lt;span class="k"&gt;*&lt;/span&gt;.md docs/&lt;span class="k"&gt;*&lt;/span&gt;.mdx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Feed only the changed files into your translation step, then re-tag once the translated version comes back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git tag last-translated-tag HEAD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same mental model as incremental builds. Translating everything every time is like rebuilding your entire project instead of just the changed modules — it works, but it doesn't scale and it reintroduces risk into sections that were already reviewed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep translatable strings out of your code blocks
&lt;/h2&gt;

&lt;p&gt;If your docs are Markdown/MDX with embedded code fences, you need a convention that strictly separates translatable prose from non-translatable code. Most localisation tools (and humans) will happily try to "helpfully" translate a JSON key if you don't fence it off clearly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- GOOD: comment is translatable, code stays untouched --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
json&lt;br&gt;
{&lt;br&gt;
  "access_token": "ya29.a0AfH6...",  // short-lived token, expires in 1h&lt;br&gt;
  "token_type": "Bearer"&lt;br&gt;
}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
markdown&lt;/p&gt;

&lt;p&gt;A few rules worth enforcing with a linter or pre-commit hook:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code fences (

&lt;code&gt;```&lt;/code&gt;

) are never sent to translation as a whole block. Only inline comments inside them are candidates, and only if your team has explicitly approved translating comments.&lt;/li&gt;
&lt;li&gt;JSON field names, HTTP headers, and status codes are never translated, full stop. This is a technical convention most API consumers expect, even in localised docs.&lt;/li&gt;
&lt;li&gt;Markdown front matter (YAML metadata) needs its own extraction rule, since it often contains both translatable text (&lt;code&gt;title:&lt;/code&gt;) and non-translatable config (&lt;code&gt;slug:&lt;/code&gt;, &lt;code&gt;api_version:&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tools like &lt;code&gt;gettext&lt;/code&gt;, or frameworks with first-class i18n support (Docusaurus i18n, Next.js with &lt;code&gt;next-intl&lt;/code&gt;), handle this separation well if you structure content correctly from the start. Retrofitting it onto a repo that mixes everything into prose paragraphs is painful.&lt;/p&gt;
&lt;h2&gt;
  
  
  Structuring resource files vs. prose content
&lt;/h2&gt;

&lt;p&gt;API portals typically have two very different kinds of translatable content, and they shouldn't go through the same pipeline:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UI strings&lt;/strong&gt; (buttons, tooltips, error banners) belong in structured resource files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;en/portal.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"button.tryItOut"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Try it out"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error.rateLimited"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"You've hit the rate limit. Try again in {seconds}s."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;fr/portal.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"button.tryItOut"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Essayer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error.rateLimited"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Limite de requêtes atteinte. Réessayez dans {seconds}s."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are small, stable, and rarely change — a good candidate for full human review since a bad translation here shows up directly in the product UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Long-form docs&lt;/strong&gt; (guides, quickstarts, endpoint descriptions) live in Markdown and change constantly. These need the diff-based workflow above, plus a translation memory (TM) so repeated phrases ("Returns a 401 if the token is invalid") don't get retranslated — and potentially re-worded — every single time they appear on a new page.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal glossary enforcement script
&lt;/h2&gt;

&lt;p&gt;Terminology drift is the real failure mode, not bad grammar. If "rate limit" gets translated three different ways across your docs, that's worse than leaving it in English everywhere. A cheap way to catch this in CI before it ships:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;GLOSSARY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limite de débit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limite de requêtes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# banned -&amp;gt; approved
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jeton d&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;accès&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token d&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;accès&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;banned&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;approved&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;GLOSSARY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;banned&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: found &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;banned&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, use &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;approved&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; instead&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;all_errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]:&lt;/span&gt;
        &lt;span class="n"&gt;all_errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;check_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;all_errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_errors&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this as a pre-merge check on any translated Markdown file. It won't catch everything, but it stops the most common regressions: a translator or MT engine reintroducing a term that was already standardized months ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated vs. human review, mapped to risk
&lt;/h2&gt;

&lt;p&gt;Not every page deserves the same review budget, and this maps surprisingly well to standard software risk triage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auto-generated reference pages&lt;/strong&gt; (hundreds of near-identical endpoint descriptions from your OpenAPI spec): machine translation with spot checks is usually fine. The repetitive structure means errors are easy to catch with the glossary script above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guides that directly affect integration success&lt;/strong&gt; (auth flows, webhooks setup, error handling): these need a human reviewing full text, because a misunderstood paragraph here generates support tickets, not just an awkward sentence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything customer-facing in a sales context&lt;/strong&gt; (docs an enterprise prospect reads before signing): treat it like marketing copy — full review pass, consistent tone, the works.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Testing localised docs like you'd test a UI
&lt;/h2&gt;

&lt;p&gt;Once translated content is merged, don't assume it's done. German and Finnish strings run 20-35% longer than English on average, and that breaks fixed-width code sample containers, nav sidebars, and button layouts. Run your localised build through the same visual regression tests you'd use for any UI change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Example with Percy or a similar visual diff tool&lt;/span&gt;
npx percy &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; npm run build:docs &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--locale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;de
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you don't have visual regression testing set up for your docs site, this alone is worth adding before you scale to a third or fourth language.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Localising API docs isn't a translation problem bolted onto a docs problem — it's a pipeline problem. The teams that get it right treat translated content the same way they treat code: versioned, diffed, linted, and tested before it ships. The source article does a good job covering the terminology and tiering side of this; the engineering side is just as much work, and it's the part that determines whether your localised docs stay correct past the first release.&lt;/p&gt;

</description>
      <category>i18n</category>
      <category>webdev</category>
      <category>documentation</category>
      <category>api</category>
    </item>
    <item>
      <title>Building a Multilingual Policy Pipeline: A Technical Approach to GDPR/LGPD Documentation</title>
      <dc:creator>Diogo Heleno</dc:creator>
      <pubDate>Sat, 07 Nov 2026 07:03:39 +0000</pubDate>
      <link>https://dev.to/diogoheleno/building-a-multilingual-policy-pipeline-a-technical-approach-to-gdprlgpd-documentation-i7d</link>
      <guid>https://dev.to/diogoheleno/building-a-multilingual-policy-pipeline-a-technical-approach-to-gdprlgpd-documentation-i7d</guid>
      <description>&lt;p&gt;If you work at a company with employees across multiple countries, you've probably seen this pattern: legal/compliance drops a new data protection policy in a shared drive, HR sends a link, and someone in the Luanda or São Paulo office opens a PDF written in dense European Portuguese or English legalese. Nobody reads it. If there's ever an audit or a dispute, that's a problem.&lt;/p&gt;

&lt;p&gt;There's a good write-up on the legal side of this from &lt;a href="https://www.m21global.com/en/blog/translating-data-protection-policies-staff-jurisdictions/" rel="noopener noreferrer"&gt;M21Global&lt;/a&gt;, covering what GDPR, Angola's Law No. 22/11, and Brazil's LGPD actually require in terms of linguistic clarity. That article is aimed at compliance teams deciding whether to hire a translation service. This one is for the people who end up building and maintaining the actual document pipeline, because "just translate the policy" turns into a surprisingly gnarly content-ops problem once you have more than two jurisdictions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this isn't a one-off translation task
&lt;/h2&gt;

&lt;p&gt;A data protection policy isn't static. It changes when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Legislation changes (new guidance from a supervisory authority, amendments to local law)&lt;/li&gt;
&lt;li&gt;The org changes (new data processors, new retention periods, new subsidiaries)&lt;/li&gt;
&lt;li&gt;Internal tooling changes (new HR system, new vendor handling payroll data)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're maintaining four jurisdiction-specific versions of the same policy by hand, in four separate Word docs, you will eventually ship a version where the GDPR complaints procedure in the Portuguese file doesn't match the one in the French file. This is a content synchronization problem, and it should be treated like one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat policy documents like source code
&lt;/h2&gt;

&lt;p&gt;The cleanest approach I've seen (and used) is to version-control policy content the same way you'd version-control i18n strings in an app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Structure the policy as modular content, not a monolithic doc.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Break it into sections that map to legal concepts rather than paragraphs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# policy/sections/data-controller.yml&lt;/span&gt;
&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;data_controller_definition&lt;/span&gt;
&lt;span class="na"&gt;source_lang&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;en&lt;/span&gt;
&lt;span class="na"&gt;last_legal_review&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2024-11-03&lt;/span&gt;
&lt;span class="na"&gt;requires_jurisdiction_adaptation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;controller&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;entity&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;that&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;determines..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets you flag, at the content level, which sections are pure translation versus which require legal adaptation (complaints mechanisms, supervisory authority names, specific statutory rights). That distinction matters a lot operationally — the source article makes the legal case for why "responsável pelo tratamento" isn't just a stylistic choice over "gestor de dados"; from a pipeline perspective, that means certain sections need a legal reviewer in the loop, not just a linguist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Use a translation management system (TMS) with a review workflow, not raw files in a repo.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tools like Crowdin, Phrase, or Lokalise support multi-step review pipelines (translator → reviewer → QA) and, critically, translation memory. For legal documents this matters because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Translation memory ensures "data subject" is translated identically everywhere it appears in a given language, instead of drifting across documents translated at different times.&lt;/li&gt;
&lt;li&gt;You can lock terminology with a glossary feature so "titular dos dados" (Brazil) vs "titular de dados" (Portugal) doesn't get mixed up between your BR and PT locales, which look similar but are legally distinct markets.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;glossary.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;enforced&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;terms&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;per&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;locale&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data_controller"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pt-PT"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"responsável pelo tratamento"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pt-BR"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"controlador"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fr-FR"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"responsable du traitement"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Automate the diff-and-flag step when the source changes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When the master policy changes, you want an automatic flag on every derived version showing exactly what changed, not a full re-translation. A simple approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# pseudo-pipeline, runs on PR to policy/source/en.md&lt;/span&gt;
git diff &lt;span class="nt"&gt;--unified&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 policy/source/en.md &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; changes.diff
./scripts/extract_changed_sections.py changes.diff &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; flagged_sections.json
&lt;span class="c"&gt;# flagged_sections.json feeds into your TMS as a new translation job,&lt;/span&gt;
&lt;span class="c"&gt;# scoped only to the changed sections&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps turnaround fast and cheap for minor updates (a new vendor added to a processing list) versus a full-document legal re-translation (a new retention policy across the board).&lt;/p&gt;

&lt;h2&gt;
  
  
  Where human legal review has to stay in the loop
&lt;/h2&gt;

&lt;p&gt;Automation gets you consistency and speed, not legal accuracy. The source article's point about terminology (controller vs. processor, jurisdiction-specific complaints procedures, correct name of the supervisory authority) is the part no pipeline can fully automate. What the pipeline &lt;em&gt;can&lt;/em&gt; do is make sure those sections are reliably routed to a qualified reviewer every time, instead of relying on someone remembering to flag them.&lt;/p&gt;

&lt;p&gt;Practical rule of thumb for a CI-style workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sections tagged &lt;code&gt;requires_jurisdiction_adaptation: true&lt;/code&gt; always route to a legal/linguistic reviewer, no exceptions, even for a one-word change.&lt;/li&gt;
&lt;li&gt;Sections tagged &lt;code&gt;direct_translation: true&lt;/code&gt; can go through MT + human post-edit for speed.&lt;/li&gt;
&lt;li&gt;Every merged version gets a changelog entry and a &lt;code&gt;last_legal_review&lt;/code&gt; date, so you can prove, in an audit, when each jurisdiction's version was last checked against local law.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Don't skip the distribution problem
&lt;/h2&gt;

&lt;p&gt;Translating the policy correctly doesn't help if employees in Angola are still getting the English PDF because HR's distribution list wasn't updated. If you're pushing policies through an internal portal or onboarding flow, tie locale to employee jurisdiction at the data model level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;employee_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jurisdiction_locale&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;policy_acknowledgements&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;jurisdiction_locale&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;employee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jurisdiction_locale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that query periodically. If it returns rows, someone received a policy in the wrong language, which is exactly the failure mode the legal frameworks are trying to prevent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Legal teams decide &lt;em&gt;what&lt;/em&gt; the policy needs to say in each jurisdiction. Engineering and content-ops decide &lt;em&gt;how reliably&lt;/em&gt; that gets maintained, versioned, and delivered to the right person in the right language. If your org is scaling into new markets, it's worth building the pipeline before you have six jurisdictions and a spreadsheet nobody trusts. For the legal and compliance side of why this matters jurisdiction by jurisdiction, the original breakdown is worth reading: &lt;a href="https://www.m21global.com/en/blog/translating-data-protection-policies-staff-jurisdictions/" rel="noopener noreferrer"&gt;https://www.m21global.com/en/blog/translating-data-protection-policies-staff-jurisdictions/&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>i18n</category>
      <category>devops</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a Terminology Pipeline for EU Pay Transparency Compliance (Directive 2023/970)</title>
      <dc:creator>Diogo Heleno</dc:creator>
      <pubDate>Fri, 06 Nov 2026 10:44:43 +0000</pubDate>
      <link>https://dev.to/diogoheleno/building-a-terminology-pipeline-for-eu-pay-transparency-compliance-directive-2023970-m82</link>
      <guid>https://dev.to/diogoheleno/building-a-terminology-pipeline-for-eu-pay-transparency-compliance-directive-2023970-m82</guid>
      <description>&lt;h2&gt;
  
  
  The problem isn't translation, it's version drift
&lt;/h2&gt;

&lt;p&gt;If you work in internal tools, HR tech, or compliance engineering at a company with offices in more than one EU country, Directive 2023/970 is about to land on your backlog. Member states have until June 2026 to transpose it, and companies are already updating pay policies, progression criteria, and the transparency reports the directive mandates for employers with 100+ employees.&lt;/p&gt;

&lt;p&gt;The legal and HR side of this is covered well in &lt;a href="https://www.m21global.com/en/blog/pay-transparency-directive-translation/" rel="noopener noreferrer"&gt;this piece on translation requirements under the directive&lt;/a&gt;. I want to focus on something adjacent: the actual technical problem of keeping multiple language versions of a legally binding document in sync over time, without relying on someone manually re-reading five PDFs every time a clause changes.&lt;/p&gt;

&lt;p&gt;This is a version control problem wearing a compliance costume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this breaks normal translation workflows
&lt;/h2&gt;

&lt;p&gt;Most teams handle multilingual content with one of two approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ad hoc: someone exports a Word doc, sends it to a translator or a vendor, gets it back, pastes it into a wiki.&lt;/li&gt;
&lt;li&gt;CMS-based: content lives in a headless CMS with locale fields, and translations sync through a translation management system (TMS).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both break down for legal/compliance docs because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Terminology must be identical across every instance&lt;/strong&gt;, not just "close enough." A pay policy, an internal FAQ, and a transparency report might all reference "objective and gender-neutral criteria," and if one translator renders it differently in each document, you now have three legally inconsistent definitions of the same concept in the same language.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changes need audit trails.&lt;/strong&gt; If a labour inspectorate asks why the Portuguese version of a progression clause differs from the German one, "we don't know, a vendor updated it at some point" is not an acceptable answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There's no single source of truth document.&lt;/strong&gt; The policy, the report, and internal comms are separate files that reference the same underlying definitions, but nothing enforces that programmatically.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A practical fix: treat terminology as a schema, not prose
&lt;/h2&gt;

&lt;p&gt;The fix that scales is borrowed straight from software engineering: stop treating legal terms as free text and start treating them as controlled vocabulary with IDs.&lt;/p&gt;

&lt;p&gt;Here's a minimal structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"term_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpg-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gender pay gap"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"definition_en"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Difference in average pay between genders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"translations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"disparidade salarial de género"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"écart de rémunération entre les sexes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"de"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"geschlechtsspezifisches Lohngefälle"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"es"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"brecha salarial de género"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"legal_reference"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Directive (EU) 2023/970, Art. 3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"last_reviewed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-01-15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reviewed_by"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"legal-team"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every document that references "gender pay gap" pulls from &lt;code&gt;gpg-001&lt;/code&gt;, not from a translator's independent judgment each time. You build this once as a glossary database (a spreadsheet works for small orgs, a proper TMS glossary feature works better) and every subsequent document, policy update, or report references term IDs instead of re-translating from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits in a document pipeline
&lt;/h2&gt;

&lt;p&gt;If your org already uses a TMS (Phrase, Lokalise, Smartling, etc.), most support glossary enforcement during translation jobs. The workflow looks like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Source document authored&lt;/strong&gt; in the HQ language, with key terms tagged (either manually or via a simple regex pass against your glossary).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Glossary check runs before the job goes to translators.&lt;/strong&gt; Flag any defined term in the source that isn't in the glossary yet, force a legal review before translation starts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Translation job dispatched&lt;/strong&gt; with glossary attached as a constraint, not a suggestion. Most TMS platforms will highlight glossary mismatches in the translator's editor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QA pass&lt;/strong&gt; compares translated term usage against the glossary programmatically. A basic version of this is just a script:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_glossary_consistency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;translated_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;glossary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;issues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;term&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;glossary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;translations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="c1"&gt;# crude check: does the expected term appear at all
&lt;/span&gt;        &lt;span class="c1"&gt;# when the source term context is present
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;translated_text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Source term &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; leaked untranslated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;issues&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This won't catch nuanced legal errors (you still need a second linguist for that, which is exactly the review layer the source article describes for high-risk documents), but it catches the dumb, expensive mistakes: untranslated terms, inconsistent capitalization, or a term translated three different ways across three documents.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Legal sign-off recorded against the specific glossary version&lt;/strong&gt;, not just the document version. This is the audit trail piece. If &lt;code&gt;gpg-001&lt;/code&gt; changes on 2025-06-01, you want to know which documents were generated against the old definition and need re-review.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Handling multi-jurisdiction deadlines
&lt;/h2&gt;

&lt;p&gt;Since national transposition deadlines won't align perfectly across member states, you'll likely need to version documents by jurisdiction and date, not just by language. A simple convention:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/policies/pay-transparency/
  es/2026-03-01_v2.docx
  fr/2026-01-15_v1.docx
  de/2026-06-01_v1.docx
  pt/2026-02-01_v3.docx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pair this with a changelog that references the glossary version used, and you have something an auditor or legal team can actually trace.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Translation quality for compliance documents isn't really a linguistics problem, it's a data consistency problem. If you're building or maintaining the internal tooling for HR, legal, or compliance content at a multi-country company, the highest-leverage thing you can do before June 2026 is set up a controlled glossary with IDs, version history, and automated consistency checks, before the volume of required documents (policies, reports, FAQs, internal comms) makes manual tracking impossible.&lt;/p&gt;

&lt;p&gt;The legal review still needs to happen, and for documents with direct litigation exposure you still want a qualified second linguist checking the final text, as outlined in the &lt;a href="https://www.m21global.com/en/blog/pay-transparency-directive-translation/" rel="noopener noreferrer"&gt;source article on translation requirements for Directive 2023/970&lt;/a&gt;. But the infrastructure around that review is squarely an engineering problem, and it's one most compliance teams haven't built yet.&lt;/p&gt;

</description>
      <category>i18n</category>
      <category>compliance</category>
      <category>productivity</category>
      <category>devops</category>
    </item>
    <item>
      <title>Building a Multilingual Regulatory Reporting Pipeline for Solvency II (SFCR) Documents</title>
      <dc:creator>Diogo Heleno</dc:creator>
      <pubDate>Wed, 04 Nov 2026 10:09:36 +0000</pubDate>
      <link>https://dev.to/diogoheleno/building-a-multilingual-regulatory-reporting-pipeline-for-solvency-ii-sfcr-documents-5557</link>
      <guid>https://dev.to/diogoheleno/building-a-multilingual-regulatory-reporting-pipeline-for-solvency-ii-sfcr-documents-5557</guid>
      <description>&lt;h2&gt;
  
  
  The problem isn't just translation, it's data consistency across formats
&lt;/h2&gt;

&lt;p&gt;If you work in insurtech or build internal tooling for a multinational insurer, you've probably run into some version of this problem: the same regulatory figures need to exist, consistently, in a narrative document (the SFCR), a set of structured quantitative templates (QRTs), and multiple language versions, all of which have to agree with each other down to the decimal point.&lt;/p&gt;

&lt;p&gt;A related article from M21Global covers the linguistic and regulatory side of this well: &lt;a href="https://www.m21global.com/en/blog/sfcr-report-translation-insurers/" rel="noopener noreferrer"&gt;why SFCR translation requires Solvency II expertise, not just language fluency&lt;/a&gt;. That's the compliance angle. This post is about the engineering angle: how do you actually build a pipeline that keeps terminology, figures and structure consistent across languages and report cycles, without relying on manual cross-checking every year?&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is a data problem before it's a translation problem
&lt;/h2&gt;

&lt;p&gt;The SFCR mixes two very different content types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structured data&lt;/strong&gt;: SCR/MCR ratios, eligible own funds, QRT templates with EIOPA-standardized row/column labels&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unstructured narrative&lt;/strong&gt;: governance descriptions, risk profile commentary, valuation methodology text&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most teams treat the whole document as unstructured text and hand it to a translation vendor as a Word or InDesign file. That's how you get the exact bugs described in the source article: a term like "technical provisions" translated inconsistently in section 3 versus section 5, or a QRT label that doesn't match the officially published EIOPA translation for that language.&lt;/p&gt;

&lt;p&gt;The fix isn't better proofreading. It's separating the structured and unstructured parts of the pipeline and treating terminology as a controlled vocabulary, not free text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Pull QRT labels from EIOPA's own taxonomy
&lt;/h2&gt;

&lt;p&gt;EIOPA publishes the Solvency II reporting taxonomy (XBRL-based) with standardized labels in every official EU language. If your reporting system generates QRTs programmatically, you should be pulling labels directly from this taxonomy rather than letting a translator retype them.&lt;/p&gt;

&lt;p&gt;A minimal approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Pseudocode: resolve QRT labels from the EIOPA taxonomy
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;TAXONOMY_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://eiopa.europa.eu/taxonomy/2.x.x/labels.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;concept_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TAXONOMY_URL&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;concept_id&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;concept_id&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Example
&lt;/span&gt;&lt;span class="nf"&gt;get_label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s2md_met:mi362&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# -&amp;gt; "Provisions techniques"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This guarantees your QRT labels match what the French or Spanish supervisor already expects to see, because it's the same source EIOPA uses. No translator discretion involved for this part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Treat narrative terminology as a glossary, not prose
&lt;/h2&gt;

&lt;p&gt;For the narrative sections, build a termbase before translation starts, not after. Tools like SDL MultiTerm, Phrase TMS, or even a well-maintained spreadsheet synced to a CAT tool work fine. The key fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"term_en"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"loss-absorbing capacity of deferred taxes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"term_fr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"capacité d'absorption des pertes par les impôts différés"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"term_es"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"capacidad de absorción de pérdidas de los impuestos diferidos"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Solvency II - Pillar 1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EIOPA glossary v2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"do_not_translate_as"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"capacité d'absorption fiscale"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're integrating with a translation API (DeepL API, Google Cloud Translation, Phrase), most support glossary injection. For example, with the DeepL API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;deepl&lt;/span&gt;

&lt;span class="n"&gt;translator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deepl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Translator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;glossary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;translator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_glossary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;solvency-ii-fr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;source_lang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;target_lang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;technical provisions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;provisions techniques&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;own funds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fonds propres&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SCR coverage ratio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ratio de couverture du SCR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;translator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;translate_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The SCR coverage ratio remained above 150%.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;source_lang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;target_lang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;glossary&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;glossary&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This won't replace a qualified actuarial translator for the final narrative, but it catches the machine-translation baseline errors before a human reviewer even sees the draft, and it enforces consistency for terms that recur hundreds of times across a 100+ page group report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Version-control the translation memory, not just the document
&lt;/h2&gt;

&lt;p&gt;SFCRs repeat heavily year over year: boilerplate governance descriptions, standard risk disclosures, methodology notes. If your translation memory (TM) lives inside a vendor's proprietary tool with no export discipline, you lose that leverage every renewal cycle.&lt;/p&gt;

&lt;p&gt;Practical setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Export your TM as TMX after every cycle and store it in your document repo (or a dedicated bucket), tagged by reporting year and entity&lt;/li&gt;
&lt;li&gt;Diff this year's source text against last year's before sending anything out for translation, so you're only paying for and reviewing genuinely new content&lt;/li&gt;
&lt;li&gt;Run a terminology consistency check automatically using a tool like &lt;code&gt;checkmate&lt;/code&gt; or a custom script that flags any term translated two different ways within the same document set
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# crude but effective: flag inconsistent translations of a key term&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"technical provisions"&lt;/span&gt; report_en.txt | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"provisions techniques"&lt;/span&gt; report_fr.txt | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"claims provisions"&lt;/span&gt; report_fr.txt  &lt;span class="c"&gt;# should return nothing if mistranslated&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Build the cross-check between narrative and QRTs into CI
&lt;/h2&gt;

&lt;p&gt;The most expensive failure mode is a narrative section that states one SCR ratio while the QRT annex states another, something that happens more often than you'd expect when narrative and quantitative teams work in parallel and translation touches both independently.&lt;/p&gt;

&lt;p&gt;If your reporting pipeline already generates both outputs from a common data source (which it should), add a validation step that extracts key figures from both the translated narrative and the translated QRT and asserts they match:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_consistency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;narrative_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qrt_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;extracted_scr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_number_near&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;narrative_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword_map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scr_ratio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;qrt_scr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qrt_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scr_coverage_ratio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extracted_scr&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;qrt_scr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mismatch in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;extracted_scr&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; vs &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;qrt_scr&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a simple regex/NLP extraction problem, not magic, but almost nobody builds it because translation is treated as a separate workstream from the data pipeline. It shouldn't be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits with external translation vendors
&lt;/h2&gt;

&lt;p&gt;None of this replaces a qualified financial/insurance translator for the final narrative text. Regulatory language still needs a human who understands Solvency II, and a second reviewer for quality assurance, which is exactly the workflow described in the source article for sensitive compliance documents.&lt;/p&gt;

&lt;p&gt;What this pipeline does is reduce the surface area of what needs human judgment, by making sure QRT labels, recurring terminology and cross-document figures are consistent and machine-verifiable before anyone's billing hours to proofread them. For a report that runs past a hundred pages per market across three or more jurisdictions, that's the difference between a predictable annual process and a scramble every filing deadline.&lt;/p&gt;

&lt;p&gt;If you're responsible for the reporting infrastructure side of this rather than the compliance side, it's worth treating your SFCR cycle the same way you'd treat any other data pipeline with multiple consumers: single source of truth, automated validation, and version-controlled artifacts, language included.&lt;/p&gt;

</description>
      <category>i18n</category>
      <category>api</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Building a Multilingual Documentation Pipeline for EU Machinery Regulation 2023/1230</title>
      <dc:creator>Diogo Heleno</dc:creator>
      <pubDate>Wed, 04 Nov 2026 07:03:39 +0000</pubDate>
      <link>https://dev.to/diogoheleno/building-a-multilingual-documentation-pipeline-for-eu-machinery-regulation-20231230-4ma7</link>
      <guid>https://dev.to/diogoheleno/building-a-multilingual-documentation-pipeline-for-eu-machinery-regulation-20231230-4ma7</guid>
      <description>&lt;p&gt;Regulation (EU) 2023/1230 kicks in on 20 January 2027 and replaces the old Machinery Directive. If your company builds hardware that ships into the EU, this isn't just a legal department problem. Someone has to engineer the actual pipeline that takes a source manual and produces N compliant, synchronized, versioned translations, including documentation for AI-based safety components that didn't exist as a category under the old rules.&lt;/p&gt;

&lt;p&gt;The legal background is covered well in &lt;a href="https://www.m21global.com/en/blog/machinery-manual-translation-regulation-2023-1230/" rel="noopener noreferrer"&gt;this breakdown of the regulation&lt;/a&gt;. This post is about the other half of the problem: how do you actually build and maintain a system that keeps dozens of translated documents in sync with a source manual that keeps changing?&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is a content engineering problem, not just a translation problem
&lt;/h2&gt;

&lt;p&gt;A single machine's documentation set usually includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User manual&lt;/li&gt;
&lt;li&gt;Safety warnings and labels&lt;/li&gt;
&lt;li&gt;EC declaration of conformity&lt;/li&gt;
&lt;li&gt;Assembly / maintenance instructions&lt;/li&gt;
&lt;li&gt;AI system behavior documentation (new requirement)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Multiply that by every target market language, and you get a matrix where a single engineering change (say, a new emergency stop procedure) has to propagate correctly into every cell. Do this manually with email attachments and shared drives, and you will eventually ship a French manual with an outdated safety procedure. That's not a hypothetical, it's the default failure mode of unmanaged documentation.&lt;/p&gt;

&lt;p&gt;The fix is to treat your manuals the way you treat code: version-controlled source content, a defined build pipeline, and automated checks before anything gets published.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structure source content so it's translatable by design
&lt;/h2&gt;

&lt;p&gt;Before any translation tooling, your source documentation needs to be structured, not a monolithic Word doc or PDF. Options that work well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DITA XML&lt;/strong&gt; — purpose-built for technical documentation with reuse and conditional content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Markdown + a static site generator&lt;/strong&gt; (e.g., Docusaurus, which has built-in i18n support)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured FrameMaker or Paligo&lt;/strong&gt; if you're already in a DTP-heavy workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is separating content from layout, and splitting documents into translatable segments that can be tracked individually. If a safety warning is a discrete, identifiable unit, you can diff it, flag it for retranslation, and verify it didn't silently change meaning across 8 language versions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Example: segment metadata for a safety warning block&lt;/span&gt;
&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;safety-warning-estop-001&lt;/span&gt;
&lt;span class="na"&gt;source_lang&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;en&lt;/span&gt;
&lt;span class="na"&gt;criticality&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;high&lt;/span&gt;
&lt;span class="na"&gt;last_updated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2025-03-12&lt;/span&gt;
&lt;span class="na"&gt;translations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;es&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;approved&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;translator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;jsmith&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;reviewed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;pt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;pending_review&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;translator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;mcosta&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;reviewed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;false&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;pl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;outdated&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;reason&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;source_changed&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This kind of metadata is what lets you build automated gates later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Translation memory and terminology as infrastructure, not an afterthought
&lt;/h2&gt;

&lt;p&gt;The source article points out that inconsistent terminology across translated machinery manuals ("residual risk" vs "remaining risk", "emergency stop" vs "panic button") is a recurring compliance risk. From an implementation standpoint, this is solved with two artifacts that should live in your repo alongside the content:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A termbase&lt;/strong&gt; (TBX format is the standard) with approved terms per language, locked for safety-critical vocabulary&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A translation memory (TM)&lt;/strong&gt; in TMX format, shared across every translator and vendor touching the project&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're using CAT tools (memoQ, Trados, or open-source options like OmegaT), both plug into these formats directly. The key engineering decision is: don't let terminology live in a vendor's proprietary silo. Export it, version it, and treat changes to the termbase as pull requests that need review, because a wrong approval here propagates into every future translation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Example: validating a translated segment against the termbase&lt;/span&gt;
python validate_terms.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source&lt;/span&gt; manual_en.xml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target&lt;/span&gt; manual_es.xml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--termbase&lt;/span&gt; machinery_terms.tbx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--lang&lt;/span&gt; es
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simple script like this, run in CI, can flag when a translator used "protección genérica" instead of the approved "dispositivo de protección" for "safeguarding." It won't replace human QA, but it catches the cheap, high-frequency errors before they reach a reviewer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating change detection and re-translation triggers
&lt;/h2&gt;

&lt;p&gt;Under 2023/1230, any substantial modification to the machine requires updated documentation, in every translated version. This is a classic cache invalidation problem. You need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A way to detect that source content changed (git diff on structured XML/Markdown is enough)&lt;/li&gt;
&lt;li&gt;A way to map which translated segments are now stale&lt;/li&gt;
&lt;li&gt;A workflow that routes stale segments to translators/reviewers automatically, instead of relying on someone remembering to notify the translation vendor
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Pseudocode: flag translations as outdated when source changes
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;source_segments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;last_known_hash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;translation&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;translations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;translation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outdated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="nf"&gt;notify_translation_queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same pattern as invalidating a CDN cache after a deploy. The difference is the stakes: a stale safety instruction in Polish isn't a UX bug, it's a liability and compliance exposure.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI checks before publishing a manual
&lt;/h2&gt;

&lt;p&gt;Borrowing from software release practices, a documentation pipeline for regulated manuals should run automated checks before anything is marked ready for a notified body or market release:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Completeness check&lt;/strong&gt;: every required language has a translation for every segment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terminology check&lt;/strong&gt;: approved terms used consistently (script above)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Placeholder/variable check&lt;/strong&gt;: if the manual has dynamic fields (model number, voltage rating), verify they weren't mistranslated or dropped&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layout check&lt;/strong&gt;: safety icons and warning placement preserved (this is why desktop publishing matters as much as the text itself)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sign-off check&lt;/strong&gt;: high-criticality segments have a recorded second-linguist review, not just machine translation post-editing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A minimal CI config might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/docs-compliance.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docs-compliance&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python check_completeness.py --langs es,pt,pl,de,fr&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python validate_terms.py --termbase machinery_terms.tbx&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python check_critical_signoff.py --criticality high&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Failing this pipeline should block publication the same way a failing test suite blocks a merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to draw the line between automation and human review
&lt;/h2&gt;

&lt;p&gt;Machine translation plus post-editing is fine for descriptive content, parts catalogues, FAQs, general operating notes. It is not fine, on its own, for safety warnings, the declaration of conformity, or AI system behavior documentation, where a second human reviewer is the only thing standing between an ambiguous sentence and a market surveillance finding. Build your pipeline so criticality is a first-class metadata field, not something decided ad hoc per document.&lt;/p&gt;

&lt;p&gt;The regulatory deadline is 2027, but the pipeline work, structuring content, building termbases, wiring up CI checks, takes longer to get right than most teams expect. Start building the infrastructure now, even if your first few manuals still go through a mostly manual process.&lt;/p&gt;

</description>
      <category>i18n</category>
      <category>devops</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building a Document Validation Pipeline for Cross-Border Bureaucracy (Vehicle Import Case Study)</title>
      <dc:creator>Diogo Heleno</dc:creator>
      <pubDate>Tue, 03 Nov 2026 07:06:12 +0000</pubDate>
      <link>https://dev.to/diogoheleno/building-a-document-validation-pipeline-for-cross-border-bureaucracy-vehicle-import-case-study-jkb</link>
      <guid>https://dev.to/diogoheleno/building-a-document-validation-pipeline-for-cross-border-bureaucracy-vehicle-import-case-study-jkb</guid>
      <description>&lt;p&gt;Most of us never think about bureaucratic document workflows until we're stuck in one. Importing a car into Portugal is a good case study because it exposes a pattern that shows up everywhere in compliance-heavy software: data that is technically correct but administratively invalid.&lt;/p&gt;

&lt;p&gt;The source article from M21Global (&lt;a href="https://www.m21global.com/en/blog/certified-translation-imt-vehicle-registration/" rel="noopener noreferrer"&gt;certified translation for IMT vehicle registration&lt;/a&gt;) breaks down the legal side well: which documents need certified translation, why a sworn translation from Germany or Spain doesn't automatically count in Portugal, and when an apostille is actually required. I want to look at the same problem from a different angle: if you were building a system (internal tool, SaaS product, or just a personal script) to track and validate this kind of document chain, what would the data model and validation rules actually look like?&lt;/p&gt;

&lt;p&gt;This applies beyond cars. Any workflow involving foreign-issued legal documents, government agencies, and translation requirements hits the same structural problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core problem: validity isn't binary
&lt;/h2&gt;

&lt;p&gt;A document in this context has at least three independent states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linguistic validity&lt;/strong&gt;: is it in a language the receiving authority accepts?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Translation validity&lt;/strong&gt;: if translated, was it certified by an authority the receiving country recognizes?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authenticity validity&lt;/strong&gt;: does the receiving country need extra proof the original document is real (apostille/legalisation)?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mistake most naive systems (and most people) make is treating "translated" as a single boolean flag. It isn't. A translation can exist and still be worthless for a given jurisdiction, because the certifying authority doesn't match what the destination requires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modeling this properly
&lt;/h2&gt;

&lt;p&gt;Here's a rough schema for tracking documents through a cross-border bureaucratic process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CertificationAuthority&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;SWORN_TRANSLATOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sworn_translator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;      &lt;span class="c1"&gt;# e.g. Germany, Spain
&lt;/span&gt;    &lt;span class="n"&gt;LAWYER_NOTARY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lawyer_notary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;             &lt;span class="c1"&gt;# e.g. Portugal
&lt;/span&gt;    &lt;span class="n"&gt;NONE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuthenticationType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;APOSTILLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apostille&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;CONSULAR_LEGALISATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;consular_legalisation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;NOT_REQUIRED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not_required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;doc_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;                 &lt;span class="c1"&gt;# "registration_certificate", "invoice", "coc", etc.
&lt;/span&gt;    &lt;span class="n"&gt;origin_country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;origin_language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;issued_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TranslationRecord&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Document&lt;/span&gt;
    &lt;span class="n"&gt;target_language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;certifying_authority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CertificationAuthority&lt;/span&gt;
    &lt;span class="n"&gt;certifying_country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;translator_credential_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DestinationRequirement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;destination_country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;doc_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;accepted_certification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CertificationAuthority&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;authentication_required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AuthenticationType&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key relationship to validate is: &lt;strong&gt;does &lt;code&gt;TranslationRecord.certifying_authority&lt;/code&gt; intersect with &lt;code&gt;DestinationRequirement.accepted_certification&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the Portugal/IMT case, the answer is almost always "only &lt;code&gt;LAWYER_NOTARY&lt;/code&gt; counts," regardless of where the translation happened. A &lt;code&gt;SWORN_TRANSLATOR&lt;/code&gt; certification from Germany fails this check even though it's a perfectly valid certification in its own country. This is exactly the kind of cross-jurisdiction mismatch that trips up both manual processes and automated ones if you hardcode assumptions like "translated = valid."&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple validation function
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_translation_accepted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TranslationRecord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DestinationRequirement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc_type&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mismatched document type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;certifying_authority&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accepted_certification&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;needs_authentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DestinationRequirement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authentication_required&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;AuthenticationType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NOT_REQUIRED&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks trivial, but the value isn't the code, it's forcing the requirements into an explicit, queryable structure instead of leaving them as institutional knowledge scattered across PDFs and agency phone calls. If you're building an internal tool for a customs agency, dealership network, or translation company that processes many of these files, this structure scales a lot better than a checklist in a spreadsheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where automation actually helps (and where it doesn't)
&lt;/h2&gt;

&lt;p&gt;This is a good moment to be honest about limits. You can automate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flagging which documents require translation based on detected language (basic language detection via &lt;code&gt;langdetect&lt;/code&gt; or &lt;code&gt;fasttext&lt;/code&gt; is enough for a first pass)&lt;/li&gt;
&lt;li&gt;Cross-checking VIN numbers between original and translated documents with regex and a checksum validator (VINs have a known check-digit algorithm, ISO 3779)&lt;/li&gt;
&lt;li&gt;Tracking certification metadata and flagging mismatches like the Germany/Portugal example above&lt;/li&gt;
&lt;li&gt;Generating a checklist per vehicle origin country, pulled from a rules table your team maintains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You cannot automate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Producing the actual certified translation (this requires a licensed lawyer or notary under Portuguese rules, not a model or API)&lt;/li&gt;
&lt;li&gt;Guaranteeing a translation agency's certificate format will be accepted, since this is ultimately a judgment call by the receiving institution&lt;/li&gt;
&lt;li&gt;Replacing the final human review that catches terminology mismatches (the article mentions misclassified fuel types and vehicle categories as a common rejection cause, and that's a domain-glossary problem, not a translation-engine problem)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  VIN mismatch detection
&lt;/h2&gt;

&lt;p&gt;Since mismatched VINs between original and translated documents are called out as a top rejection reason, here's a minimal check-digit validator worth having in your toolkit if you're processing these documents in bulk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;TRANSLITERATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ABCDEFGHJKLMNPRSTUVWXYZ&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;])}}&lt;/span&gt;
&lt;span class="n"&gt;WEIGHTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;vin_check_digit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;char&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WEIGHTS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;char&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;char&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;TRANSLITERATION&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;char&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;
    &lt;span class="n"&gt;remainder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;remainder&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remainder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;vins_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;original&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;translated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;original&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;translated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this as a pre-submission check before a file goes to the IMT or any similar authority, and you catch transcription errors before they cost you a rejected application and another translation request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The legal and procedural details (which authority certifies what, when an apostille applies) are jurisdiction-specific and genuinely require expert review, which is the ground the original M21Global article covers well. But the underlying data problem, matching document states against destination requirements across incompatible certification systems, is a general software problem you can model, validate, and partially automate. If you're building tooling for an agency that handles a volume of these, treat "certification authority" and "destination requirement" as first-class entities instead of a free-text field, and you'll save yourself a lot of rejected filings.&lt;/p&gt;

</description>
      <category>i18n</category>
      <category>python</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
