Qubik's Site

Internationalization

The theme is multilingual without multiplying your URLs. The UI language is a client-side preference, not a path segment: there is no /en/, no /zh/, and switching language never navigates. One page has one URL in every language.

The design rationale is in design/design-language.md §9; this page is how to use it.

How a language is chosen

  1. The site's lang (in .vitepress/config.mts) is the default and the language everything is server-rendered in.
  2. A reader who switches language in the status bar or settings window has that choice stored (ct-lang) and applied on every later visit.
  3. Switching updates the theme strings, localized config text, localized frontmatter, and per-language page bodies in place, along with <html lang>.

Built-in string tables ship for English (en) and Simplified Chinese (zh-Hans).

Language tags

Use the minimal canonical BCP 47 tag: a language subtag, plus a script subtag only where the script disambiguates. So en, not en-US; zh-Hans, not zh-CN.

Region-tagged input still resolves: a site lang of en-US or a frontmatter key of zh-CN matches by primary subtag. Canonical tags are what you should write, not a constraint on what is accepted.

Localizable text

Every user-facing text value in the configuration surface — and the displayed frontmatter fields — accepts either form:

ts
ts
title: "VitePress Theme Terminal"                                    // all languages
title: { en: "VitePress Theme Terminal", "zh-Hans": "VitePress 终端主题" }

Maps resolve with one fallback chain everywhere:

exact tag → primary subtag → en → first entry.

So a { en, zh-Hans } map serves a zh-CN reader Chinese, a fr reader English, and a reader of any language something rather than nothing.

This applies to themeConfig.title, .description, author name, nav and action labels, explorer labels, taxonomy labels, friend-group labels, home page copy — and to the frontmatter title, explorerTitle, and description, plus explorer.json and series.yml titles.

yaml
yaml
---
title:
  en: Hello, Terminal
  zh-Hans: 你好,终端
description:
  en: A first look at the theme.
  zh-Hans: 主题初识。
---

Explorer labels, listing cards, the archives, the license card, and the browser tab all re-resolve these in place when the reader switches.

Per-language page bodies

For content, not just labels, wrap each language's body in a ::: lang block:

md
md
::: lang en
English body…
:::

::: lang zh-Hans
中文正文…
:::
  • Content outside any block always shows — share code samples, images, and headings instead of duplicating them.
  • Adjacent blocks form one group; exactly one is shown per group, chosen by the same fallback chain (exact → primary subtag → site default → first block).
  • The site-default block is server-rendered visible, so there is no flash and the page still reads correctly with JavaScript off.

Marker length matters. The closing marker is matched line by line — including inside fenced code blocks, which the container scanner does not skip. So if a language block contains a code sample whose lines start with colons (a callout, a swiper deck, another ::: lang example), the wrapper needs more colons than any of them; otherwise the first such sample line closes the block early and the rest of the body escapes it, showing in every language. These documentation pages wrap their bodies in ::::: lang en / ::::: lang zh-Hans for exactly that reason.

Taxonomy labels

Tags and categories are shared across posts, so their display labels live in the site config rather than in any one post's frontmatter. Posts keep declaring plain names:

ts
ts
taxonomy: {
  tags: { theme: { en: "theme", "zh-Hans": "主题" } },
  categories: { Guides: { en: "Guides", "zh-Hans": "指南" } },
},

Only the label switches. Slugs, /tags/… and /categories/… URLs, and grouping identity always come from the authored name, so a link shared in one language works in every other. Terms without an entry render verbatim.

Overriding theme strings

The theme's own UI strings (Copy, On this page, Search the site…, callout titles, …) resolve through the locale layer. Override any of them per language:

ts
ts
localeStrings: {
  "zh-Hans": { "mode.paper": "阅读" },
},

The full key list is the English table, .vitepress/theme/locales/en.ts — keys are dotted and grouped by surface (mode.*, nav.*, explorer.*, search.*, settings.*, status.*, footer.*, license.*, comments.*, post.*, series.*, friends.*, callout.*, toc.*, anchor.*, code.*, swiper.*, notFound.*).

Some strings carry {placeholders}footer.copyright has {year} and {author}, anchor.permalink has {title}, post.taggedWith has {term}. Keep them in your override.

Adding a language

Add a table under a new tag in the same option — no component edits, no new files:

ts
ts
localeStrings: {
  fr: {
    "lang.label": "Français",     // how it appears in the switcher
    "mode.dark": "Sombre",
    "toc.title": "Sur cette page",
    // …
  },
},

Any tag you add appears in the language switcher automatically. Two details:

  • lang.label is the switcher entry. Without it the raw tag is shown.
  • English backfills. Missing keys fall back to English rather than disappearing, so a partial table is usable from the first key — you can translate incrementally.

If you'd rather ship a complete built-in table, add a file beside locales/en.ts and register it in locales/index.ts; the registry is the only place that needs to know.

Server rendering

The server-rendered HTML is always in the site's lang. That covers meta tags, the initial paint, and search engines; the client then applies the reader's preference. Two consequences worth knowing:

  • A per-language frontmatter description is resolved to the build language for the <meta> tag, then re-resolved client-side.
  • Build-time Markdown output (callout default titles) is generated in the build language and re-localized after hydration.

Neither needs configuration — but it's why the site lang should be the language most of your readers arrive in.

READ~/docs/guide/internationalization
TOPdark