# Internationalisation (i18n)

i18n is opt-in. Existing sites, including sites using the legacy compact
`languages` option, keep their current behaviour until `i18n.enabled` is set.

## Configuration

```yaml
languages:
  - code: pl
    locale: pl-PL
    name: Polski
    timezone: Europe/Warsaw
  - code: en
    locale: en-GB
    name: English
    timezone: Europe/London
default_language: pl

i18n:
  enabled: true
  prefix_default_language: false
  translations_dir: i18n
  dictionary_fallback: true
  missing_translation: warn # error, warn, fallback, empty
  invalid_language: fail    # fail, warn
  duplicate_translation: fail # fail, warn
  fallback_languages:
    pl: [en]
    en: []
```

The compact form remains valid:

```yaml
languages: [pl, en]
default_language: pl
language_timezones:
  pl: Europe/Warsaw
```

With `prefix_default_language: false`, Polish `/o-nas/` and English
`/en/about/` are generated. Setting it to `true` produces `/pl/o-nas/` and
`/en/about/`.

## Content translations

Connect translated files with a stable key; translated slugs may differ:

```yaml
title: About
slug: about
lang: en
translation_key: about
status: publish
```

When `lang` is absent it resolves to the language its section assigns
(`language_sections`, below) and then to `default_language`. If
`translation_key` is absent, SSG derives it deterministically from the source
filename. Duplicate `(translation_key, lang)` pairs, unknown languages,
fallback cycles, invalid timezones and output collisions are validated before
rendering.

Templates receive `.Page.Lang`, `.Page.Locale`, `.Page.TranslationKey`,
`.Page.Translations`, `.Site.Language`, `.Site.Languages`,
`.Site.DefaultLanguage`, `.Site.LanguagePages` and `.Site.LanguagePosts`.

```gotemplate
{{range .Page.Translations}}
  <a href="{{.URL}}" hreflang="{{.Lang}}" {{if .IsCurrent}}aria-current="page"{{end}}>{{.Lang}}</a>
{{end}}
```

Helpers: `hasTranslation`, `translationURL`, `languageURL`, `localizeDate` and
`t`. Existing helpers such as `formatDatePL` remain available.

## A language for a whole section

```yaml
language_sections:
  de: de
  fr/blog: fr
  home: en
```

Migrated sites are where this matters. A bilingual WordPress site keeps its
languages in `/de/` and `/fr/` and says so nowhere a page carries: the language
was a plugin's property of the *section*, not a field on the post. The export
produces a few hundred documents with no `lang` at all, and writing one into
every file is undone by the next export — a migration is run again whenever the
source changes.

Keys are content directories relative to the source, longest prefix wins, and
`home` names the site root — the same convention `output_encoding_sections` and
`schema_defaults` use. A page's own `lang:` still wins; a section that claims no
page changes nothing; and a section naming a language `languages:` does not
declare warns once for the section rather than once per file.

The language is assigned before translation grouping, prefixing and hreflang, so
everything downstream sees it. A page carrying an explicit `link:` keeps that
URL untouched, so `link: /de/impressum/` does not gain a second `/de/`.

## Translation dictionaries

Place `pl.yaml`, `en.yaml` or JSON equivalents in `translations_dir`:

```yaml
navigation:
  home: Strona główna
post:
  reading_time: "{{count}} minut"
```

Use `{{t "navigation.home"}}` or
`{{t "post.reading_time" (dict "count" .ReadingTime)}}`. Values are returned
as ordinary strings and remain subject to `html/template` escaping. Only named
placeholders are substituted; catalog values are never executed as templates.

### The generated 404

ssg writes a `404.html` when the site does not provide one, because a static
host with no 404 falls back to `index.html` and answers **200** — so every dead
URL becomes, to a crawler, a live page duplicating the home page. After a
migration, dead URLs are what old links produce.

That page reads three keys, and translating it is the only way to change its
language:

```yaml
not_found:
  title: "404 — nie znaleziono strony"
  body: "Ta strona nie istnieje w serwisie {{site}}."
  home: "Przejdź na stronę główną"
```

**All three or none.** A page with two translated lines, one English one and
`lang="pl"` is worse than a wholly English page: the mislabelled part is exactly
the part a screen reader gets wrong, and nobody proof-reads a 404. With any key
missing, the English copy is used whole and the page is labelled `en`.

`lang` follows the copy that was actually used, which is why the attribute alone
was never the fix — it describes the language of the content, and declaring `pl`
over English text makes a screen reader switch voice for words that did not
change.

A theme owns this entirely if it wants to: a page slugged `404` renders to
`/404.html`, and ssg then leaves it alone. `not_found_off` suppresses the page.

## Generated output

Pages, aliases, home pages, pagination, JSON records, Atom feeds and search
indexes follow the configured prefix rule. Sitemap page entries contain XHTML
language alternates and `x-default`; opt-in SEO output includes Open Graph
locale metadata and JSON-LD `inLanguage`.

## Internal Markdown links

`rewrite_md_links` is language-aware: a link like `installation.md` resolves to
the **active language's** translation of that document. An explicit
language-suffixed link (`installation.en.md`) keeps the author's choice. When
the active-language translation does not exist, the `content_fallback` chain
(`fallback_languages` → default language) applies **only** when
`i18n.content_fallback: true`; otherwise the link is left untouched and a
warning names the link and language (once per pair).

## Deferred features

Planned follow-ups, not yet implemented: language-scoped taxonomy pages
(category/tag/author/series listings are still cross-language), a language
selector and `t` labels inside the built-in themes (the output `<html lang>` is
already corrected at render time), localized month names in `localizeDate`,
and plural rules.

## Migration

Add the `i18n` section only when ready. Start by assigning `lang` and a shared
`translation_key` to variants, then add dictionaries and a language selector.
Run a clean build after enabling i18n so obsolete unprefixed artifacts do not
remain in the output directory.
