feat(theme): multilingual theme — i18n, language switch, hreflang, render-time links #196

Merged
dominik.polakovics merged 1 commit from afk/191 into main 2026-10-10 23:00:03 +02:00

Closes #191

What

The deckle theme and the news/categories bundles now render every configured language correctly from the translation files #190 introduced.

  • Page fields come from the default-language page. Three new partials (l10n/source.html, l10n/here.html, l10n/pages.html) drive backend layouts, menus (weight, hidden, hideChildren), the footer (footer), child teasers, page-teasers, the news list and element, and the category pages.
    • Drafts are per language.
    • Untranslated pages are left out of menus and lists (strict mode).
  • Page-type dispatch. Hugo picks a layout by the translation's own path-derived type, which is the wrong one. So page-type bodies move to partials/pagetypes/<type>.html and category entries to partials/category-entry/<type>.html; index.html and _default/{list,single}.html dispatch on the source page's type.
    • The news bundle goes to v5 and categories to v2.
  • Links resolve at render time. ce/href.html looks a stored value up by content path. It returns the current-language translation, else the default-language page, else a bundle file, else the value as stored. The fragment and query are kept. External, mailto:, tel: and /media/… values are emitted as stored.
    • _default/_markup/render-link.html applies the same rule to markdown links. A test checks it renders byte-for-byte like goldmark's own link rendering.
    • The stored new-tab raw <a> form is resolved by a post-pass in backendlayout/render.html, because render hooks never see raw HTML.
    • Every link param uses it: button, card, img, image.
  • i18n. Every UI string goes through l10n/t.html. This includes the skip link, nav labels, 404, pager, lightbox and consent labels, video and contact-form defaults, "All news →", RSS text and the date format.
    • Strings ship in themes/deckle/i18n/{en,de}.toml and features/news/i18n/news/{en,de}.toml.
    • l10n/t.html wraps T. When a key is missing it falls back to the English string, so a site whose default language has no i18n file (e.g. harness init --lang fr) still shows its labels.
    • The two scripts with text become per-language templates; the default language keeps the exact same file and path.
    • Dates use time.Format, so German shows "5. Januar 2026".
  • Language switch in the header: only the languages the page is published in, labelled with .Language.Label, the current one marked aria-current="page", no JS. It is not rendered on a single-language site or on a page without translations.
  • hreflang: one alternate per translation plus x-default, on multilingual sites only.
  • hugo.toml comments: per-language title and [languages.<code>.params] are documented, and the stale "old address kept as an alias" claim is fixed (#194).
  • Docs: the theme README (new "Page-type templates" and "Languages" sections, including the key table), AGENTS.md, README.md and the skills redo-theme (plus showcase translation), add-element, create-feature, add-page-type, edit-content, setup-site, migrate-site and add-backend-layout are updated.
    • verify-site and remove-element were checked and left unchanged.

Verification

Fixture sites. I built a fixture with the pinned Hugo 0.167.0 and --minify: a one-language site (news, categories, the showcase page, a links table) and the same site after harness lang add de with German translations, using slug: dienste on Services.

  • Single-language output, compared with main. Every HTML page and JS file is byte-identical except two expected changes:
    • css/theme.css gains the .site-lang rules.
    • A tel: button link that used to render as #ZgotmplZ (html/template blanked it) now renders as tel:+43.
  • English pages of the two-language site. They differ from before in three ways:
    • the switch and hreflang markup;
    • stored internal links now carry /en/, e.g. /services/ → /en/services/. The old values were dead links, since no aliases exist;
    • the same tel: fix.
  • German pages. They show:
    • German strings and dates;
    • Services in two-col, with hidden pages absent from the German menu;
    • news items using the news-item layout, with their source dates and newest first;
    • /services/web/ resolved to /de/dienste/web/, and an untranslated target resolved to /en/…;
    • the switch and three hreflang links.

New integration tests in internal/theme:

  • l10n_fields_test.go: the translation file carries no backendLayout, weight or hidden, yet the page renders with them; plus menus, footer, news, categories and drafts.
  • links_test.go: the resolution table, the goldmark byte-identity corpus, and single-language output staying unchanged.
  • i18n_test.go: German strings and dates, the switch and hreflang rules, a fr-default site without fr.toml, and a missing key falling back.

Checks:

  • go test ./..., including the skills and AGENTS.md drift tests, with hugo on PATH.
  • go vet ./... and gofmt -l are clean.
  • harness check reports 0 errors on the fixture site.

Known limits (documented)

  • A translated news section gets no RSS feed, because Hugo reads outputs from the translation file.
  • A translation whose default-language page is a draft is not reachable from menus or lists in a production build.
  • Relative markdown link values are not resolved. The engine never stores them.
  • Upgrading an existing site:
    • Re-running harness add news only fills gaps, so the old v4 templates stay. AGENTS.md describes deleting the unmodified vendored files and re-adding.
    • The new bundles need the current theme's l10n/ partials.

🤖 Generated with Claude Code

Closes #191 ## What The deckle theme and the news/categories bundles now render every configured language correctly from the translation files #190 introduced. - **Page fields come from the default-language page.** Three new partials (`l10n/source.html`, `l10n/here.html`, `l10n/pages.html`) drive backend layouts, menus (`weight`, `hidden`, `hideChildren`), the footer (`footer`), child teasers, `page-teasers`, the news list and element, and the category pages. - Drafts are per language. - Untranslated pages are left out of menus and lists (strict mode). - **Page-type dispatch.** Hugo picks a layout by the translation's own path-derived type, which is the wrong one. So page-type bodies move to `partials/pagetypes/<type>.html` and category entries to `partials/category-entry/<type>.html`; `index.html` and `_default/{list,single}.html` dispatch on the source page's type. - The news bundle goes to v5 and categories to v2. - **Links resolve at render time.** `ce/href.html` looks a stored value up by content path. It returns the current-language translation, else the default-language page, else a bundle file, else the value as stored. The fragment and query are kept. External, `mailto:`, `tel:` and `/media/…` values are emitted as stored. - `_default/_markup/render-link.html` applies the same rule to markdown links. A test checks it renders byte-for-byte like goldmark's own link rendering. - The stored new-tab raw `<a>` form is resolved by a post-pass in `backendlayout/render.html`, because render hooks never see raw HTML. - Every link param uses it: button, card, img, image. - **i18n.** Every UI string goes through `l10n/t.html`. This includes the skip link, nav labels, 404, pager, lightbox and consent labels, video and contact-form defaults, "All news →", RSS text and the date format. - Strings ship in `themes/deckle/i18n/{en,de}.toml` and `features/news/i18n/news/{en,de}.toml`. - `l10n/t.html` wraps `T`. When a key is missing it falls back to the English string, so a site whose default language has no i18n file (e.g. `harness init --lang fr`) still shows its labels. - The two scripts with text become per-language templates; the default language keeps the exact same file and path. - Dates use `time.Format`, so German shows "5. Januar 2026". - **Language switch** in the header: only the languages the page is published in, labelled with `.Language.Label`, the current one marked `aria-current="page"`, no JS. It is not rendered on a single-language site or on a page without translations. - **hreflang**: one alternate per translation plus `x-default`, on multilingual sites only. - **`hugo.toml` comments**: per-language `title` and `[languages.<code>.params]` are documented, and the stale "old address kept as an alias" claim is fixed (#194). - **Docs**: the theme README (new "Page-type templates" and "Languages" sections, including the key table), AGENTS.md, README.md and the skills redo-theme (plus showcase translation), add-element, create-feature, add-page-type, edit-content, setup-site, migrate-site and add-backend-layout are updated. - verify-site and remove-element were checked and left unchanged. ## Verification **Fixture sites.** I built a fixture with the pinned Hugo 0.167.0 and `--minify`: a one-language site (news, categories, the showcase page, a links table) and the same site after `harness lang add de` with German translations, using `slug: dienste` on Services. - **Single-language output, compared with `main`.** Every HTML page and JS file is byte-identical except two expected changes: - `css/theme.css` gains the `.site-lang` rules. - A `tel:` button link that used to render as `#ZgotmplZ` (html/template blanked it) now renders as `tel:+43`. - **English pages of the two-language site.** They differ from before in three ways: - the switch and hreflang markup; - stored internal links now carry `/en/`, e.g. `/services/` → `/en/services/`. The old values were dead links, since no aliases exist; - the same `tel:` fix. - **German pages.** They show: - German strings and dates; - Services in `two-col`, with hidden pages absent from the German menu; - news items using the news-item layout, with their source dates and newest first; - `/services/web/` resolved to `/de/dienste/web/`, and an untranslated target resolved to `/en/…`; - the switch and three hreflang links. **New integration tests** in `internal/theme`: - `l10n_fields_test.go`: the translation file carries no `backendLayout`, `weight` or `hidden`, yet the page renders with them; plus menus, footer, news, categories and drafts. - `links_test.go`: the resolution table, the goldmark byte-identity corpus, and single-language output staying unchanged. - `i18n_test.go`: German strings and dates, the switch and hreflang rules, a fr-default site without `fr.toml`, and a missing key falling back. **Checks:** - `go test ./...`, including the skills and AGENTS.md drift tests, with hugo on PATH. - `go vet ./...` and `gofmt -l` are clean. - `harness check` reports 0 errors on the fixture site. ## Known limits (documented) - A translated news section gets no RSS feed, because Hugo reads `outputs` from the translation file. - A translation whose default-language page is a draft is not reachable from menus or lists in a production build. - Relative markdown link values are not resolved. The engine never stores them. - Upgrading an existing site: - Re-running `harness add news` only fills gaps, so the old v4 templates stay. AGENTS.md describes deleting the unmodified vendored files and re-adding. - The new bundles need the current theme's `l10n/` partials. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(theme): multilingual theme — i18n strings, language switch, hreflang and render-time link resolution
All checks were successful
ci / build-and-test (pull_request) Successful in 13m30s
f6cc2870c9
- Page fields come from the default-language page: new l10n/source,
  l10n/here and l10n/pages partials drive layouts, menus, footer, child
  lists, page-teasers, news and categories; drafts stay per language and
  untranslated pages are left out (strict mode).
- Page-type bodies move to partials/pagetypes/<type>.html and category
  entries to partials/category-entry/<type>.html, dispatched on the source
  type so translations render with their source's layout (news v5,
  categories v2).
- ce/href.html and a markdown link render hook resolve language-neutral
  link values by content path at render time; a post-pass in
  backendlayout/render.html resolves the stored new-tab anchor form.
- Every UI string goes through i18n (theme en/de, news bundle en/de) via
  l10n/t.html, which never returns empty; the date format is translated
  and the scripts with text are rendered per language.
- Header language switch and hreflang/x-default alternates on
  multilingual sites; hugo.toml documents per-language title and params.
- Theme README, AGENTS.md, README and the scaffolded skills follow.

Closes #191

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Owner

[autoland] verdict: pass

PASS — CI aggregate success (ci / build-and-test: npm build+test, go vet, go test ./... with pinned Hugo 0.167.0) on head f6cc287. Conventional title, Closes #191, base contained (no conflict). Diff matches #191: ce/href.html + render-link hook + new-tab post-pass, l10n/{source,here,pages} for source-page fields and strict menus/lists, i18n via l10n/t.html (en/de, English fallback), header switch, hreflang/x-default, hugo.toml per-language params comment. AGENTS.md, theme README and the affected skills updated per CLAUDE.md; drift tests pass. Also re-ran locally: go vet + go test ./... with Hugo 0.167.0 (theme integration tests not skipped) — all green.

[autoland] verdict: pass PASS — CI aggregate success (ci / build-and-test: npm build+test, go vet, go test ./... with pinned Hugo 0.167.0) on head f6cc287. Conventional title, Closes #191, base contained (no conflict). Diff matches #191: ce/href.html + render-link hook + new-tab post-pass, l10n/{source,here,pages} for source-page fields and strict menus/lists, i18n via l10n/t.html (en/de, English fallback), header switch, hreflang/x-default, hugo.toml per-language params comment. AGENTS.md, theme README and the affected skills updated per CLAUDE.md; drift tests pass. Also re-ran locally: go vet + go test ./... with Hugo 0.167.0 (theme integration tests not skipped) — all green.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle!196
No description provided.