Skip to content

Multilingual Support & Language Switching

EpoCanvas Docs ships complete content-level multilingual support: the UI text (navigation bar, sidebar, search hints, and so on) and the body text of every document are each available in 10 independent languages. After selecting a language from the top-right corner of the page, the reader is taken to the same article in that language, with the body text and the UI switching together.


Supported Languages

The site currently offers 10 languages, with Simplified Chinese as the default:

LanguageLanguage nameURL prefixUI abbreviation
简体中文Simplified Chinese (default)noneZH
繁體中文Traditional Chinese/zh-tw/TW
EnglishEnglish/en/EN
日本語Japanese/ja/JA
한국어Korean/ko/KO
EspañolSpanish/es/ES
FrançaisFrench/fr/FR
DeutschGerman/de/DE
РусскийRussian/ru/RU
PortuguêsPortuguese/pt/PT

The language list is defined in SUPPORTED_LANGUAGES in src/utils/i18n.ts, and the language routes are registered in the locales configuration in astro.config.mjs. Keep the two in sync.


How to Switch Languages

  1. Find the language button on the right side of the top navigation bar (it shows the abbreviation of the current language, such as ZH);
  2. Click the button and a dropdown menu lists all 10 languages;
  3. Click the target language (for example English or 日本語);
  4. The browser navigates to the current article in that language; the navigation, sidebar, table of contents, and body text all switch to the target language together.

Annotated view of the language switcher dropdown when open: (1) language button, (2) language list, (3) current language

Figure: what the page looks like after clicking the language button on the “Multilingual Support & Language Switching” page. (1) The language switcher button on the right of the header; (2) the expanded dropdown listing all 10 languages (the list scrolls); (3) the currently active language (marked with a check).

Diagram of how language switching works

Figure: the flow of a language switch. At build time a full set of static pages is generated for each language; clicking the language button jumps to the same article in that language. When a page is missing a translation, it automatically falls back to Chinese.


URL Rules

Each language has its own URL prefix; the same article differs across languages only by that prefix:

/canvas/i18n/ → Simplified Chinese (default language, no prefix)
/en/canvas/i18n/ → English
/ja/canvas/i18n/ → Japanese
/zh-tw/canvas/i18n/ → Traditional Chinese
  • The language is encoded directly in the URL, so a switched page can be bookmarked and shared as-is, and search engines can index each language separately;
  • Internal links that appear on a page (navigation, sidebar, recommended links in the body) automatically carry the current language prefix, so readers who keep reading within one language are never bounced back to a Chinese page;
  • Every language version is an independent static page generated at build time, and the page’s <html lang> attribute matches the language, so screen readers and translation tools can identify it correctly.

What Happens When a Translation Is Missing

Missing translations are handled with a “default language fallback” strategy: if a document does not yet have a translation for some language, visiting that language’s page shows the default Simplified Chinese content, while the rest of the page (navigation, sidebar, UI text) stays in the target language. No 404 error and no blank page.

The benefit of this strategy: when adding a new document, you do not need to wait until translations in all languages are ready. Publish the Chinese version first, then rebuild once the translations are filled in.


How It Works

The multilingual capability is carried by the internationalization mechanisms of Astro and Starlight. The build process works as follows:

  1. Content directories are split by language: documents in the default language live in src/content/docs/ (Simplified Chinese), and other languages live under src/content/docs/<language>/ with a directory structure that mirrors the Chinese one exactly. For example, the English edition of “Multilingual Support & Language Switching” is src/content/docs/en/canvas/i18n.md;
  2. All language pages are generated at build time: astro build reads the locales configuration in astro.config.mjs and produces a complete set of static pages for each language;
  3. UI strings are rendered per language: header navigation, search hints, table-of-contents titles, and other UI text are output in the target language at build time by components calling getTranslation(key, lang); there is no runtime replacement script on the page;
  4. Internal body links are localized automatically: the rehypeLocalizeInternalLinks plugin in the build pipeline automatically adds the current language prefix to /canvas/xxx internal links inside translated body text;
  5. Full-text search is isolated per language: Pagefind builds a separate index for each language, so searching on an English page returns only English results.
  6. Body images fall back per language: the rehypeLocalizeDiagramImages plugin rewrites /images/canvas/<file> in translated text to /images/canvas/<lang>/<file>, with the localized files kept under public/images/canvas/<lang>/; when no file with the same name exists the original is left in place, so a new image does not have to be produced in all 10 languages at once;
  7. Asides and footnotes follow the page language: rehypeLocalizeAsides and rehypeLocalizeFootnotes rewrite aside titles and footnote labels from the language directory of the source file — Starlight infers the language from an absolute path during remark, while a local build passes a relative path, so without this every label would fall back to Chinese;
  8. hreflang and the 404 page: after the build, hreflangXDefault() adds one hreflang="x-default" link per page pointing at the Simplified Chinese version, and src/pages/404.astro reuses the same dictionary to render its message in the browser language, with the language menu linking to each language’s home page.

For Developers: How to Add a Language

Taking Italian (Italiano, code it) as an example:

  1. Register the language route: add it: { label: 'Italiano', lang: 'it' } to locales in astro.config.mjs;
  2. Add sidebar text: in the same file, add it translations for all groups and entries in SIDEBAR_I18N;
  3. Add UI strings: register the language (code, name, abbreviation, URL prefix) in SUPPORTED_LANGUAGES in src/utils/i18n.ts, and add the language’s UI strings in UI_TRANSLATIONS;
  4. Translate the document content: create the src/content/docs/it/ directory and put translations there following the Chinese directory structure. Untranslated pages fall back to Chinese for now and can be filled in gradually; To localize screenshots and diagrams, add a public/images/canvas/it/ directory with files of the same name — pages pick them up automatically.
  5. Rebuild: run pnpm run build, and the new language’s pages and search index are generated. After building, run node scripts/audit-i18n.mjs and node scripts/check-localized-images.mjs to re-check translation structure and image references; both are required checks in CI.

For Developers: How to Add or Modify UI Strings

When adding a new UI element, add its strings to UI_TRANSLATIONS in src/utils/i18n.ts:

src/utils/i18n.ts
// The dictionary is organized in two levels, "language → string key", matching the lookup order of getTranslation
export const UI_TRANSLATIONS: Record<string, Record<string, string>> = {
// Step 1: add the string under the default language zh-CN (if missing, the UI falls back to showing the key name)
'zh-CN': {
'nav.plugins': '插件生态',
// ...other existing strings
},
// Step 2: add translations under the same key for other languages; languages without one fall back to Chinese
en: {
'nav.plugins': 'Plugins',
},
ja: {
'nav.plugins': 'プラグイン',
},
};

Then call getTranslation in the component, passing in the current language:

---
import { getLangFromLocale, getTranslation } from '../../utils/i18n';
const lang = getLangFromLocale(Astro.currentLocale);
---
<a href={localizedHref('/plugins/', lang)}>{getTranslation('nav.plugins', lang)}</a>

Strings are rendered at build time as static text in the target language, with no runtime overhead.