On this page

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:

  1. The page may not exist in the target language
  2. Even if a page with the same slug exists, it may cover different content
  3. 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 de refuses if docs/de/_styleguide.md is 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:

  1. Moves docs/ content into docs/en/
  2. Creates docs/de/index.md stub for the new language
  3. Updates wire.yml with languages: config
  4. 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:

  1. Rename the directory:

    mv docs/de/guides/getting-started/ docs/de/guides/erste-schritte/
    
  2. Add a redirect for each rename to .wire/redirects.yml:

    - from: /de/guides/getting-started/
      to: /de/guides/erste-schritte/
      status: 301
    
  3. After all renames, run sanitize once to fix all internal links:

    python -m wire.chief sanitize --lang de
    
  4. Build 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.