ADR-0014: Bilingual docs site (EN baseline, DE alongside) without a custom Pages build
- Status: Accepted
- Date: 2026-06-13
Context
The docs/ Jekyll site published to GitHub Pages (the portfolixir.app
domain, see docs/CNAME) is English-only. We want it bilingual: the existing
English pages stay the source baseline, German lives alongside, and a reader
can switch languages with the choice remembered.
Two constraints shape the choice:
- GitHub Pages’ default build does not allow arbitrary Jekyll plugins.
Jekyll-Polyglot is the obvious
multilingual plugin, but it is not on the Pages safelist, so using it
requires replacing the default Pages build with a custom GitHub Actions
Pages workflow that runs
jekyll buildwith the plugin and uploads the artifact. That is a new piece of CI infrastructure to own, secure, and keep green for a site that is otherwise built automatically. - The Elixir
DocsTest(test/portfolixir/docs_test.exs) reads specific doc paths withFile.read!—docs/product-documentation.md,docs/integration/api-and-mcp.md,docs/index.md, and others — and asserts their content. Any mechanism that moves the English files (e.g. into an/enfolder) breaks that test and forces a parallel rewrite of the test’s path list. CI is the source of truth here (the Jekyll build is not run in CI;mix testis), so keepingDocsTestgreen is non-negotiable.
The realistic options were:
- A. Jekyll-Polyglot + a custom GitHub Actions Pages build. One source file
per page with inline translations; the plugin generates
/and/de/outputs. Cost: a bespoke Pages workflow (plugin install, Ruby/Jekyll pinning, artifact upload) that CI does not exercise, plus a moving target whenever Pages or the plugin changes. - B. A manual
/en/desplit. Move every English page under/enand add/de. Clean URLs, but it relocates every fileDocsTestreads, so the test’s path list and the navigation must change in lockstep — more churn and more risk for the same outcome. - C. A front-matter + parallel-folder approach on the default Pages build.
Keep the English files exactly where they are (so
DocsTest’sFile.read!paths are untouched) and add German counterparts underdocs/de/mirroring the English tree. Each page declares its language and the URLs of its counterparts in front matter (lang,lang_en,lang_de); the shared layout renders a language switcher from those fields, with dependency-free inline JS for the browser-language default andlocalStoragepersistence.
Decision
Adopt option C: a manual front-matter + parallel docs/de/ folder
structure served by the default GitHub Pages build. No Jekyll plugins, no
custom Actions Pages workflow.
- English stays the baseline, in place.
docs/product-documentation.md,docs/integration/api-and-mcp.md,docs/index.md, and the rest keep their current paths and content, so the existingDocsTestassertions and the navigation URLs do not move. - German lives under
docs/de/mirroring the English tree (docs/de/product-documentation.md,docs/de/integration/api-and-mcp.md), with the same Jekylllayout: docsfront matter pluslang: deand thelang_en/lang_decounterpart URLs. - The language switcher lives in the shared layout (
docs/_layouts/docs.html): EN/DE links built from each page’slang_en/lang_defront matter (falling back to the current URL on pages with no counterpart, so untranslated pages still render). A small inline script defaults a first-time visitor to the browser language (German only; English is the baseline), persists an explicit click inlocalStorage(portfolixir-docs-lang), and redirects once to the stored/preferred counterpart when it differs from the current page. - English remains the source of record. Per
AGENTS.md, every repository artifact is written in English; translated end-user documentation is the explicit exception, and German tracks the English baseline rather than diverging from it. Code, identifiers, endpoints, and tool names stay untranslated because they are API surface. - Translation coverage is pragmatic. The two core pages — the product handbook and the API/MCP reference — are translated in full. Deeper/secondary pages (architecture, ADRs, development guides, home deployment) stay English for now with a translation backlog noted; the switcher degrades gracefully on them.
Consequences
- The default GitHub Pages build keeps working unchanged — no plugin safelist fight, no custom Actions Pages workflow to own or secure, no Pages pipeline that CI cannot verify.
DocsTeststays green because no English file moves; the test is extended to also assert the German core pages exist and carry the key documented surface (the income endpoint/tool, the cash/allocation flags, the switcher front matter) in both languages.- Trade-off: translations are maintained by hand. Adding a documented route
or tool means updating the English page and its German counterpart (or
consciously deferring the German one to the backlog). There is no plugin
enforcing structural parity; the extended
DocsTestis the guard that the German core pages exist and cover the key surface. - Trade-off: some duplication and a flat URL convention. German pages live
under
/de/...rather than a plugin-managed locale routing; counterpart URLs are wired explicitly in front matter. This is more verbose than Polyglot but fully transparent and plugin-free. - Switching to Polyglot later remains possible: it would mean adding a custom Pages Actions build and collapsing each EN/DE pair into one source file. This ADR would then be superseded. Until the page count or translation burden justifies that infrastructure, the manual approach is the lower-risk choice.