VitePress Theme Terminal

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