feat(docs): add Traditional Chinese translations with path-based locale routing#170
feat(docs): add Traditional Chinese translations with path-based locale routing#170
Conversation
…le routing - Add Traditional Chinese translations for all 8 integrator-facing docs pages - Introduce path-based locale URLs (/docs/<lang>/<slug>) with legacy redirects - Detect locale from URL path, cookie, or Accept-Language header - Add an editorial language switcher to the docs sidebar with canonical-case 301 redirects - Precompute sidebar entries and switcher options per locale to avoid per-request allocations - Persist the docs_lang cookie only when the locale actually changes - Cover locale parity, routing, and cookie behavior with handler tests
There was a problem hiding this comment.
Pull request overview
Adds Traditional Chinese (zh-TW) docs support and introduces canonical, crawlable locale-in-path documentation routes, plus a sidebar language switcher and comprehensive handler tests.
Changes:
- Introduces
/docs/<locale>/<slug>canonical routing with locale detection + redirects from legacy entry points. - Adds zh-TW translations for the integrator-facing docs pages.
- Adds a docs language switcher UI and precomputes locale-specific sidebar/switcher props at boot, with tests covering routing/detection.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/templates/static/css/pages/docs.css | Styles the new sidebar language switcher and adjusts sidebar header layout. |
| internal/templates/props.go | Extends docs template props to include locale, slug, localized strings, and switcher options. |
| internal/templates/docs_page.templ | Updates docs links to locale paths and adds the language switcher + localized chrome. |
| internal/templates/docs/zh-TW/*.md | Adds Traditional Chinese versions of the docs pages. |
| internal/handlers/docs.go | Implements locale-aware docs loading, routing/redirects, cookie persistence, and locale detection helpers. |
| internal/handlers/docs_test.go | Adds tests for locale matching, redirects, canonicalisation, and fallback behavior. |
| internal/bootstrap/router.go | Replaces legacy docs route with entry + canonical locale routes. |
| internal/bootstrap/handlers.go | Updates docs handler initialization to pass production secure-cookie setting. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // docsStrings holds UI strings shown on the docs chrome (not the Markdown body), per locale. | ||
| type docsStrings struct { | ||
| SidebarTitle string | ||
| LangSwitcherLabel string | ||
| LangEnglish string | ||
| LangZhTW string | ||
| } | ||
|
|
||
| var docsStringsByLocale = map[Locale]docsStrings{ | ||
| LocaleEN: { | ||
| SidebarTitle: "Documentation", | ||
| LangSwitcherLabel: "Language", | ||
| LangEnglish: "English", | ||
| LangZhTW: "繁體中文", | ||
| }, | ||
| LocaleZHTW: { | ||
| SidebarTitle: "技術文件", | ||
| LangSwitcherLabel: "語言", | ||
| LangEnglish: "English", | ||
| LangZhTW: "繁體中文", | ||
| }, |
There was a problem hiding this comment.
PR description mentions the language switcher copy as "EN · 繁中", but the actual labels wired into docsStringsByLocale / DocsLocaleOption.Label are "English" and "繁體中文". If the intended UI is short codes, update these strings (and possibly use locale codes directly); otherwise update the PR description to match what will render.
| loc, ok := exactLocaleTag(langParam) | ||
| if !ok { | ||
| // Unknown slug → redirect to first page | ||
| if len(h.pages) > 0 { | ||
| c.Redirect(http.StatusFound, "/docs/"+h.pages[0].Slug) | ||
| } else { | ||
| c.Redirect(http.StatusFound, "/") | ||
| } | ||
| // Unknown locale in the URL — bounce to the detection entry so the | ||
| // user ends up on a valid canonical URL. | ||
| c.Redirect(http.StatusFound, "/docs") | ||
| return |
There was a problem hiding this comment.
/docs/:lang/:slug currently rejects locale aliases like zh/zh-Hant (because ShowDocsPage uses exactLocaleTag), so a request like /docs/zh/tokens redirects to /docs and loses the slug. Since /docs/:lang does accept these aliases via matchLocaleTag, this behavior is inconsistent and breaks deep links people are likely to try/share. Consider: if exactLocaleTag fails but matchLocaleTag(langParam) succeeds, 301/302 redirect to the canonical /docs/<canonical-locale>/<slug> (preserving :slug) instead of bouncing to /docs.
|
|
||
| if loc, ok := matchLocaleTag(raw); ok { | ||
| if pages := h.pages[loc]; len(pages) > 0 { | ||
| c.Redirect(http.StatusFound, docsPageURL(loc, pages[0].Slug)) |
There was a problem hiding this comment.
When ShowDocsEntry is hit with a supported locale code but non-canonical casing (e.g. /docs/zh-tw), it redirects with 302 Found. If the goal is a single indexable URL per locale entry point (similar to the 301 canonicalisation in ShowDocsPage), consider emitting 301 Moved Permanently when the input differs only by case from the canonical locale code (i.e. exactLocaleTag(raw) matches but raw != string(loc)).
| c.Redirect(http.StatusFound, docsPageURL(loc, pages[0].Slug)) | |
| status := http.StatusFound | |
| if raw != string(loc) && strings.EqualFold(raw, string(loc)) { | |
| status = http.StatusMovedPermanently | |
| } | |
| c.Redirect(status, docsPageURL(loc, pages[0].Slug)) |
| <div class="docs-layout" lang={ props.Locale }> | ||
| <aside class="docs-sidebar" aria-label={ props.SidebarTitle }> | ||
| <div class="docs-sidebar-header"> | ||
| <span class="docs-sidebar-title">Documentation</span> | ||
| <span class="docs-sidebar-title">{ props.SidebarTitle }</span> | ||
| @docsLangSwitcher(props) |
There was a problem hiding this comment.
The sidebar <aside> aria-label was changed from a descriptive phrase ("Documentation navigation") to just the localized title (props.SidebarTitle). This makes the landmark label less clear to screen readers. Consider keeping a dedicated localized navigation label (e.g. "Documentation navigation" / "技術文件導覽") or applying the aria-label to the <nav> element instead.
Summary
internal/templates/docs/zh-TW//docs/<lang>/<slug>) for crawler-friendly, bookmarkable pages — legacy/docs/<slug>URLs auto-redirect to the detected localedocs_langcookie →Accept-Languageheader → default (en)EN · 繁中) to the docs sidebar; non-canonical casings (e.g./docs/zh-tw/…) 301-redirect to the canonical case to keep one indexable URL per pageSecureflag follows the project convention (isProduction)Test plan
/docswith a clean browser → lands on/docs/en/getting-started/docswithAccept-Language: zh-TW→ lands on/docs/zh-TW/getting-started/docs/zh-TW/<current-slug>; all sidebar titles, headings, and body are Chinese/docs/zh-tw/tokens→ 301 to/docs/zh-TW/tokens→ renders/docs/ja-JP/tokens→ 302 to/docs/docs/getting-started(legacy) → 302 to/docs/<detected-locale>/getting-starteddocs_langcookie is only set on first visit and on locale change, not on every rendermake generate && make fmt && make lint && make testall green🤖 Generated with Claude Code