On this page
- Architecture: Parallel Sites, Not Translations
- Configuration
- Per-Language Labels and CTAs
- Language Switcher
- Alternate Page Mapping (hreflang)
- Reciprocal Mappings Required
- When to Use Alternates
- Chief and Multi-Language
- What Wire Handles Automatically
- What Wire Does Not Do
- Directory Structure
- GSC Setup
- Trade-offs
- Build-Time Validation
- Translation Workflow
- Step 1: Scaffold the new language
- Step 2: Translate the styleguide FIRST
- Step 3: Translate content pages
- Step 3b: Expect lint issues after translate
- Step 4: Build during translation
- Step 5: Full build after translation
- Common BUILD REFUSED errors during translation
- Step 6: Translate URL slugs (optional)
Wire supports multi-language sites where each language is an independent content site sharing a domain. This guide explains the architecture, what Wire does and does not do, and why. For more guides, see the Guides overview.
Architecture: Parallel Sites, Not Translations
Traditional CMS i18n assumes pages are 1:1 translations. Wire rejects this assumption.
When Chief (Wire's content orchestrator, invoked via python -m wire.chief) writes a German page about "Bundesanzeiger," that page has no English equivalent. The AI authored it for a German audience with German-specific regulatory context. The English site might cover "SEC Filings" instead. These are parallel content sites that happen to share a domain, not translations with drift.
This is a deliberate design choice:
- Pages are authored independently per language by Chief
- Each language has its own topics, slugs, and editorial direction
- A page in one language may have no counterpart in another
- Pages that started as translations may diverge completely over time
Configuration
Each language gets its own docs directory. Wire builds them as independent sub-sites under language prefixes.
# wire.yml
languages:
- code: en
name: English
docs_dir: docs/en
- code: de
name: Deutsch
docs_dir: docs/de
- code: es
name: "Espa\xF1ol"
docs_dir: docs/es
Mark one language as default: true to set the root redirect target. Without it, the first entry is used.
languages:
- code: de
name: Deutsch
docs_dir: docs/de
default: true
- code: en
name: English
docs_dir: docs/en
Output structure:
site/
index.html # Root redirect to /de/ (default language)
en/ # English sub-site
index.html
search/index.html
search_index.json
sitemap.xml
de/ # German sub-site
index.html
search/index.html
search_index.json
sitemap.xml
Each language gets its own search page, search index, sitemap, RSS feed, and llms.txt. Search only returns results from the current language. Sitemap URLs include the language prefix (e.g., https://example.com/de/guides/).
Per-Language Labels and CTAs
Multi-language sites need per-language UI text. Wire supports nested config where each language code maps to its own labels, sidebar CTA, and article CTA:
extra:
wire:
labels:
de:
on_this_page: "Auf dieser Seite"
related_articles: "Verwandte Artikel"
read_more: "Weiterlesen"
en:
on_this_page: "On This Page"
related_articles: "Related Articles"
read_more: "Read More"
sidebar_cta:
de:
title: "Jetzt testen"
description: "Sehen Sie in 30 Minuten, wie Konfuzio Ihre Dokumente verarbeitet."
link: /de/demo/
link_text: "Demo anfragen"
en:
title: "Try it now"
description: "See how Konfuzio processes your documents in 30 minutes."
link: /en/demo/
link_text: "Schedule Demo"
article_cta:
de:
title: "Bereit für den nächsten Schritt?"
description: "Jetzt Demo vereinbaren."
link: /de/kontakt/
link_text: "Kontakt"
en:
title: "Ready to get started?"
description: "Book a free consultation."
link: /en/contact/
link_text: "Get in touch"
Wire detects the nested format automatically: if any top-level key under labels, sidebar_cta, or article_cta matches a configured language code, the config is treated as per-language. Each configured language must have a complete entry; Wire refuses to build with missing language keys.
Single-language sites continue to use the flat format unchanged.
Language Switcher
The header renders a dropdown with all configured languages. Clicking a language redirects to that language's homepage (/de/, /en/).
Wire does not attempt to link to the "same page" in another language because:
- The page may not exist in the target language
- Even if a page with the same slug exists, it may cover different content
- False cross-links are worse than no cross-links
If you need explicit page-to-page links between languages, add them manually in your content.
Alternate Page Mapping (hreflang)
Wire does not guess which pages are translations of each other. False hreflang signals confuse Google's ranking more than missing signals. But when you know two pages are the same content in different languages, declare it explicitly with alternate: in frontmatter:
---
title: Getting Started
alternate:
de: /de/guides/erste-schritte/
---
Wire generates <link rel="alternate" hreflang="..."> tags for every page that has alternate: mappings. The value can be a dict (multiple languages) or a string (single target).
Reciprocal Mappings Required
Alternate mappings must be bidirectional. If the English page points to the German page, the German page must point back to the English page. Wire validates this at build time.
# docs/en/guides/getting-started/index.md
---
title: Getting Started
alternate:
de: /de/guides/erste-schritte/
---
# docs/de/guides/erste-schritte/index.md
---
title: Erste Schritte
alternate:
en: /en/guides/getting-started/
---
Three BUILD REFUSED errors enforce correctness:
| Error | Cause | Fix |
|---|---|---|
| Alternate target does not exist | Page at the target path is missing | Create the page or remove the mapping |
| Invalid language code | Target language not in wire.yml languages: | Fix the language code or add it to config |
| Non-reciprocal alternate | A maps to B but B does not map back to A | Add the reverse alternate: to the target page |
When to Use Alternates
Use alternate: when pages are genuine translations of each other and you want Google to show the right language version in search results. Do not add alternates for pages that merely cover similar topics in different languages. When pages drift apart over time, remove the mapping rather than leaving stale signals.
Pages without alternate: still work. Google detects page language from <html lang="">, content analysis, and URL structure (/de/, /en/). Alternates are an optimization, not a requirement.
Chief and Multi-Language
On multi-language sites, every content command requires --lang <code>. Wire refuses without it:
Multi-language site. Use --lang <code>. Available: en, de
The only exception is data, which runs all languages automatically when --lang is omitted.
# Single-language site (no --lang needed)
python -m wire.chief audit vendors
python -m wire.chief lint-fix
# Multi-language site (--lang required)
python -m wire.chief --lang en audit vendors
python -m wire.chief --lang en lint-fix
python -m wire.chief --lang de refine blog
# Exception: data runs all languages automatically
python -m wire.chief data
All languages share a single GSC database (.wire/gsc.db) with a lang column. Each language has its own:
- Progress tracking (
.wire/{lang}/progress-*.json) - Content analysis scoped to that language's pages only
- Link context generated from that language's site directory
When you run --lang de, Chief filters GSC data to German pages automatically. Each language competes in its own search market. The German audit does not mix with English results.
What Wire Handles Automatically
| Feature | Scope | Notes |
|---|---|---|
<html lang=""> | Per language | From languages[].code |
| Search index | Per language | Only indexes that language's pages |
| Sitemap | Per language | Submitted separately to GSC |
| RSS feed | Per language | Separate feed per language |
| Navigation | Per language | Built from that language's docs |
| Language dropdown | Shared | Links to each language's homepage |
| 404 page | Shared | One 404, language-neutral |
What Wire Does Not Do
- No automatic translation. Write content per language or use external translation tools before importing.
- No page mapping. Wire does not track which pages are "the same" across languages.
- No hreflang generation. Declare explicitly in frontmatter when pages truly correspond.
- No shared content. Each language is fully independent. No template inheritance across languages.
- No fallback language. If a page does not exist in German, Wire does not serve the English version.
Directory Structure
docs/
en/
index.md
guides/
getting-started/index.md
seo-reference/index.md
vendors/
acme/index.md
de/
index.md
guides/
erste-schritte/index.md
seo-referenz/index.md
anbieter/
bundesanzeiger/index.md # No English equivalent
Topics and slugs can differ between languages. The German site might have "anbieter" where English has "vendors." Chief treats each as a separate topic namespace.
GSC Setup
Each language sub-site needs its own GSC property or URL prefix:
https://example.com/en/(English)https://example.com/de/(German)
Run data to fetch GSC data. Without --lang, Wire fetches all languages automatically:
python -m wire.chief data # All languages
python -m wire.chief data --lang de # German only
Trade-offs
What you gain:
- Simple, honest architecture with no false signals to Google
- Each language evolves independently based on its own search data
- Chief makes optimal decisions per market, not compromises across markets
- No complexity from page mapping, sync status, or translation tracking
What you give up:
- No automatic "view in English" links on German pages
- No hreflang SEO signals (but false signals are worse)
- Content teams must manage each language independently
- Duplicate editorial effort for truly shared content
This trade-off is correct for content sites where quality and market fit matter more than translation coverage.
Build-Time Validation
Wire catches common multi-language mistakes at build time:
- Language asymmetry. If the secondary language has less than 20% of the default language's page count, the build is refused (ghost-town translation).
- Duplicate titles. Identical titles across languages trigger a warning (likely untranslated content).
- Cross-language internal links (RULE-69). A page in
/de/linking to/en/content is a lint failure. Customers copying markdown between languages often forget to update internal links. External URLs with language paths (e.g.google.com/en/) are fine. Language switcher links are excluded automatically. - Untranslated body content. Wire analyzes page text using stopword ratios (zero dependencies). If a page in
/de/is clearly written in English, the build is refused. Pages under 50 words are skipped to avoid false positives. Technical content with English terms in German text passes fine because German function words (der, die, und, ist) still dominate. Supported languages: de, en, es, fr. Unsupported languages refuse to build with a clear message. - Styleguide language mismatch.
wire translate --lang derefuses ifdocs/de/_styleguide.mdis written in English. The styleguide is the most important input to translation quality. Wrong language means all translations sound foreign instead of native.
Translation Workflow
Adding a new language to an existing site takes hours. Wire validates aggressively during translation, so you cannot build the full site until translation is complete. Here is the step-by-step workflow and what BUILD REFUSED errors to expect at each stage.
Step 1: Scaffold the new language
Use migrate-lang to convert a single-language site to multi-language:
python -m wire.chief migrate-lang --from en --add de
This does four things automatically:
- Moves
docs/content intodocs/en/ - Creates
docs/de/index.mdstub for the new language - Updates wire.yml with
languages:config - Generates 301 redirects from old paths (
/about/) to new paths (/en/about/)
You can add multiple languages at once:
python -m wire.chief migrate-lang --from en --add de --add fr
Constraints: refuses if the site already has languages: configured, and refuses if --from is also in --add.
After scaffolding, copy the source content as a starting point for translation:
cp -r docs/en/* docs/de/
Step 2: Translate the styleguide FIRST
The styleguide teaches Chief how to write. If the styleguide is in English, all German translations will read like translated English.
# Wire refuses to translate if the styleguide language doesn't match:
# BUILD REFUSED: styleguide at docs/de/_styleguide.md is in English but target is German.
Translate docs/de/_styleguide.md to German manually. Include German-specific rules: Sie-form, Hochdeutsch spelling, compound noun conventions.
Step 3: Translate content pages
python -m wire.chief --lang de translate
This takes hours for large sites (1-2 minutes per page). Wire detects which pages are still in English (stopword analysis) and translates them.
If translate exits non-zero, some pages failed (CLI timeout, empty response). Run with --resume to retry only the failed pages.
Step 3b: Expect lint issues after translate
A freshly translated language will have lint issues even if the source language was clean. This is normal. Common issues:
- RULE-52 (em dashes): The translator introduces em dashes despite the styleguide.
- RULE-33 (broken links): Translated pages may link to pages that do not exist yet in the target language.
- RULE-95/05 (title length): German titles are naturally longer than English.
- RULE-73 (duplicate descriptions): Frontmatter descriptions may still match the source language.
- RULE-08 (thin content): Pages that timed out during translate have placeholder bodies.
Translation is complete when translate + lint-fix + manual review all pass:
python -m wire.chief --lang de translate # Step 1: translate bodies
python -m wire.chief --lang de lint-fix # Step 2: fix mechanical issues
python -m wire.build --lang de # Step 3: check remaining issues
Expect 2-3 rounds of lint-fix after translate. Some issues (like German titles being longer) require manual shortening, not automation.
Step 4: Build during translation
While translation runs, you cannot do a full build because untranslated pages trigger BUILD REFUSED:
BUILD REFUSED: Title 'Getting Started' appears in both 'en' and 'de' - likely untranslated.
Use --lang to build one language at a time:
# Build only English (always works — source language is complete)
python -m wire.build --lang en
# Build only German (works after enough pages are translated)
python -m wire.build --lang de
The --lang flag skips cross-language gates (asymmetry, duplicate titles, untranslated bodies). This is not a "skip check" - it builds one language where cross-language checks do not apply.
Step 5: Full build after translation
Once all pages are translated:
python -m wire.build
This runs ALL gates: asymmetry check, duplicate title detection, untranslated body detection, and cross-language link validation. If any gate fires, the build is refused with a clear error and the previous site/ output is preserved.
Common BUILD REFUSED errors during translation
| Error | Cause | Fix |
|---|---|---|
| Language asymmetry (<20%) | DE has too few pages | Continue translating, or use --lang en |
| Duplicate titles | Untranslated page titles | Translate titles, or use --lang en |
| Orphan language directory | docs/de/ exists but not in languages: | Add to wire.yml languages: |
| Styleguide language mismatch | English styleguide in German dir | Translate _styleguide.md first |
| Untranslated body content | English text in /de/ directory | Run wire translate --lang de |
Step 6: Translate URL slugs (optional)
After translate, pages keep the source language's slugs (/de/guides/getting-started/ instead of /de/guides/erste-schritte/). URL slugs are SEO signals in the target language, so translating them improves ranking.
This is a manual process. For each page with a foreign-language slug:
Rename the directory:
mv docs/de/guides/getting-started/ docs/de/guides/erste-schritte/Add a redirect for each rename to
.wire/redirects.yml:- from: /de/guides/getting-started/ to: /de/guides/erste-schritte/ status: 301After all renames, run sanitize once to fix all internal links:
python -m wire.chief sanitize --lang deBuild to verify:
python -m wire.build
Rules:
- Keep brand names, product names, and established technical terms untranslated
- Use lowercase, hyphen-separated slugs (Wire standard)
- Do all renames first, then one sanitize run. Do not sanitize between individual renames.
- Do NOT manually edit links in markdown files. Sanitize handles this via the redirect map.
Related: configuration reference, workflow guide, URL management. Source: Google hreflang documentation. See the Guides overview for all Wire documentation.