Skip to content

ADR-0035: Unified Translation Policy — App MT+Review, Docs Manual+Pins ​

Status: accepted Date: 2026-08-09 Deciders: @barateza Tags: [i18n, translation, locales, docs/en, v1] Superseded in part by: ADR-0046 — the docs track (§2 pins/enforcement, §4 docs onboarding, §5 docs parity) is void. The app track (§1) and the app/docs boundary (§3) remain in force.

Note (2026-09-14): the docs/en mirror was removed and the portal now ships pt-BR only (ADR-0046). Read §2, §4 (docs track) and §5 as historical record, not as current policy. §1 (app strings) still applies.

Context ​

The project historically carried two translation policies that never explicitly reconciled (research "Gap 7", docs/research/i18n-docs-pre-mvp.md):

  • App (issue #203): machine translation (DeepL/Google Translate) + human review on critical paths, extracted to locales/*.json.
  • Docs (issue #588): hand-maintained translations + default_lang_commit freshness pins (OpenTelemetry model).

They did not collide operationally (app strings vs docs prose are different surfaces), but the boundary was never written down, so maintenance decisions were ad hoc: which strings require human review, which root pages legitimately stay byte-identical to their en mirror, who owns a term's translation, and how a new language gets onboarded.

The v1 release (milestone v1.0, spec .specs/features/i18n-docs-v1/spec.md) requires both tracks live at once — app pt-BR/en-US/es-ES in production (REQ-I18N-V1-001) and docs/en with full coverage, freshness pins, and CI enforcement (REQ-I18N-V1-003). This ADR is the single document that makes the policy coherent. It resolves REQ-I18N-V1-004.

Decision ​

1. App strings — MT + human review with explicit critical-path criteria ​

App user-facing strings are extracted from app/src/app/utils/i18n/domains/*.ts into locales/<lang>.json (extraction script, REQ-I18N-POST-001). pt-BR is the source of truth; every other locale is derived from it.

  • Method: machine translation (DeepL/Google Translate) of the full key set, then human review per the criteria below.
  • Critical-path criteria — human review is REQUIRED for any string that:
    1. appears in a security/auth flow (domain auth; password, session, MFA, account-recovery text);
    2. touches LGPD/privacy/consent — children's PII, image consent, data policy, parental authorization;
    3. is legal, pricing, or licensing (payments, plans, terms, refunds);
    4. appears in a child-safety flow (domain checkin — check-in/check-out; a mistranslated instruction can delay a pickup);
    5. contains money, health, or date-sensitive content (allergies, medication, event dates/fees).
  • MT-acceptable with spot-check only: generic UI chrome in domains common, classes, importExport, nuclei, reports, settings (labels, empty states, navigation).
  • Review gate: the human review is recorded in the PR that introduces or updates a locale — the reviewer must be a second person, not the machine-translation author. A locale file with pending unreviewed critical-path strings is not mergeable.

2. Docs — manual translation + freshness pins, hard-blocked at v1 ​

The docs portal follows the OpenTelemetry/Kubernetes model already built in #588:

  • Root docs/ is the canonical pt-BR source. The docs/en tree is a hand-maintained translation layer, never a co-author or byte copy.

  • Pin semantics: every docs/en/ page records default_lang_commit: <hash> — the commit of the root (pt-BR) page it was translated from. A page is drifted when its pin is older than the root file's current commit; the checker flags it with drifted_from_default: true (which also exempts it from the lychee dead-link check).

  • Enforcement (v1, REQ-I18N-V1-003): hard-block. scripts/check-i18n.sh exits 1 when any non-exempt, published en page is drifted, so the CI docs job fails on drift. Drift is no longer informational. Pages with status: draft frontmatter are treated as unpublished for drift purposes — like exempt pages, their en mirrors are best-effort and never hard-blocked on drift.

  • Escape hatches (so an approved source change cannot deadlock the pipeline):

    • # patched marker in the en frontmatter — acknowledges a mechanical fix (link retarget, build fix) where the en content genuinely did not need resync; waives the drift comparison.
    • pnpm docs:i18n:commit -- <paths> (check-i18n.sh commit HEAD) — refreshes the pin after the en translation was actually updated, declaring the page resynced. A fresh pin supersedes any stale # patched waiver.
  • Language-neutral exceptions — files where the root and en content are legitimately identical (or where en presence is deliberately skipped), matching docs/en/.i18n-exempt:

    ClassPages (glob, relative to docs/)Rationale
    Internal-only workspaces (never in the en portal)analysis/**, big-fat-refactor-01/**, research/**, roadmap/**Working notes and research, pt-BR-only by convention
    Taxonomy & reference catalogs (language-neutral content: codes, labels, tables)quality/testing-category, quality/code-quality-audit, quality/audit-exceptions, operations/network-troubleshooting, reference/api-guide, reference/error-codes, reference/mcp-auth-model, reference/mcp-catalogThe content is a taxonomy/reference whose terms are the same in both languages; byte-identical root/en is legitimate
    Design specs (SDDs)architecture/sdd-*, architecture/vite8-bump-specDesign artifacts, author's language; en mirror best-effort — only the index is required in the en portal
    Individual ADRsarchitecture/adr/ADR-*, architecture/adr/absence-model-researchDecision records, author's language; en mirror best-effort — only the ADR index is required
  • Legacy English-authored root pages — docs/index.md, docs/maintenance.md, docs/community/security-policy.md, docs/community/code-of-conduct.md are still English-bodied at v1. They are documented exceptions (not language-neutral): the "root = pt-BR" rule is waived until they are converted, tracked in docs/maintenance.md → Source Language Policy and in the post-v1 conversion backlog. Converting them is a content-translation pass, not a policy change.

3. Boundary — app strings vs docs prose ​

App stringsDocs prose
WhatUser-visible UI text (buttons, labels, errors, confirmations)Portal pages under docs/ (guides, specs, references)
Where it livesapp/src/app/utils/i18n/domains/*.ts → generated app/src/locales/*.jsondocs/ (pt-BR root) + docs/en/ (translation)
OwnerEngineering (ships with app releases)Docs maintainers (ships with the docs portal)
Translation policy§1 — MT + human review§2 — manual + pins
EnforcementSnapshot tests + review criteriacheck-i18n.sh (hard-block) in the CI docs job

Product terms: the canonical translation of a product concept (e.g. núcleo/nucleus, check-in, carteirinha/credential) lives in the app locales. Docs reuse the app's term for each language instead of inventing a second one — this is the only place the two tracks must agree, and the app locale file wins.

4. Procedure for adding a new language ​

App track:

  1. Extract: run the extraction script to produce locales/<lang>.json from the pt-BR domains.
  2. Translate: machine translation of the full key set, then human review per §1 critical-path criteria.
  3. Wire up: register <lang> in the runtime fallback chain (active → en-US → pt-BR) and in the Settings language selector; add navigator.language auto-detection mapping.
  4. Verify: run the i18n snapshot tests and a manual pass of the critical flows in the new locale.

Docs track:

  1. Decide: a new docs locale roughly doubles portal maintenance (every root page + every future change must be translated and pinned). v1 ships only en; any additional locale is an explicit cost decision.
  2. Create the mirror: docs/<lang>/ + VitePress locale config (sidebar, nav, label), following the docs/en structure.
  3. Translate with the no-partial policy: no byte-identical stubs, no orphans, no missing pages — the same check-i18n.sh checks apply per locale (extend the script's locale loop).
  4. Exemption decisions are per-locale: the .i18n-exempt globs are re-evaluated for each new locale.

Community platform: Crowdin (or an equivalent) remains a future enabler gated by the SaaS/open-core decision (#158). It is not required for the v1 manual tracks; when adopted, it replaces the human-review tooling but not the review criteria.

5. How the two policies interact at v1 ​

  • App ships pt-BR + en-US + es-ES (REQ-I18N-V1-001); docs ships pt-BR root + en mirror with hard-blocked freshness (REQ-I18N-V1-003). Both are release-critical: a drifted en page or an unreviewed critical-path string is a v1 blocker.
  • The two en-US surfaces (app locale, docs mirror) are independent deliverables with different owners — but they must agree on product terms (§3). No shared string pool at v1; the term list is the coordination point.
  • Root canonicalization (REQ-I18N-V1-005) makes default_lang_commit semantics well-defined for every page: the root file's commit is the source pin, the en counterpart is a translation of it. The language-neutral exceptions in §2 are the only pages where this does not hold by design.

Consequences ​

Positive ​

  • One document answers every translation question: what to translate, who reviews, what is exempt, how a language is added, and what fails CI.
  • Hard-block drift enforcement makes freshness a release gate, matching the no-partial philosophy of #588 and the OTel/K8s/MDN conventions it cites.
  • Explicit critical-path criteria make the app's human-review requirement auditable instead of aspirational.

Negative ​

  • Hard-block drift raises the bar on every root docs change: the en translation (or a # patched marker / pin refresh) must land in the same commit or the CI docs job fails. This is the intended discipline, but it slows docs-only PRs.
  • The legacy English-bodied root pages (index, maintenance, security-policy, code-of-conduct) remain documented exceptions at v1 — the "root = pt-BR" premise is not 100% true until they are converted (post-v1 backlog).

Neutral ​

  • The .i18n-exempt file remains the executable list; this ADR is its normative rationale. Changes to one must be mirrored in the other.
  • The app and docs tracks keep separate tooling; the only shared artifact is the product-term list.

References ​

  • .specs/features/i18n-docs-v1/spec.md — REQ-I18N-V1-003 (freshness enforcement), REQ-I18N-V1-004 (unified policy), REQ-I18N-V1-005 (root canonicalization)
  • .specs/features/i18n-app-post-mvp/spec.md — REQ-I18N-POST-001..007 (app locale pipeline)
  • docs/research/i18n-docs-pre-mvp.md — Gap 7 (policy divergence)
  • docs/architecture/sdd-203-internationalization.md — app i18n design
  • Issues: #203 (app multi-locale), #588 (docs/en freshness), #158 (SaaS/Crowdin)

Distribuído sob licença MIT.