From 164c319ac187296d800b01ce1c25bd23b3252cf1 Mon Sep 17 00:00:00 2001 From: zihluwang Date: Fri, 25 Sep 2026 15:38:03 +0800 Subject: [PATCH] feat(i18n): default the cn build to Chinese MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The .cn site exists for Chinese readers, so the unprefixed routes should serve Chinese there. Rspress omits the prefix for the default locale, so this also moves the routes: .com .cn English / /en-gb/ Chinese /zh-hans/ / - Make the default locale a per-target value rather than a constant, and move the localePrefix helper inside createConfig so a prefix can no longer be hardcoded elsewhere. - Build the sitemap priority map from localePrefix. The keys are full route paths, so the previous hardcoded "/zh-hans/..." entries would have silently stopped matching on .cn. - Widen the nav Home activeMatch to cover either prefixed home, since the two targets put a different locale behind the prefix. Note: README.md also carries the licence wording changed in the next commit — a single file's hunks cannot be split non-interactively. --- CLAUDE.md | 11 +++-- README.md | 31 ++++++++++-- docs/en-gb/_nav.json | 2 +- docs/zh-hans/_nav.json | 2 +- rspress.config.ts | 109 +++++++++++++++++++++++++++-------------- 5 files changed, 110 insertions(+), 45 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7016816..c1373da 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,9 +19,14 @@ pnpm format # Prettier — format all files `rspress.config.ts` exports a `createConfig(target)` factory; `rspress.cn.config.ts` is a three-line wrapper calling it with `"cn"`, passed via `--config`. The two -targets differ in the canonical origin used for `head`, the sitemap and -`robots.txt`, and in which social links are rendered. Both write to `doc_build/`, -so build and deploy one target at a time. +targets differ in the default (unprefixed) language, the canonical origin used +for `head`, the sitemap and `robots.txt`, and in which social links are rendered. +Both write to `doc_build/`, so build and deploy one target at a time. + +`.com` defaults to `en-gb` and `.cn` to `zh-hans`, so the same content is served +at different paths per target — Rspress omits the prefix for the default locale. +Anything referencing a locale prefix must go through the `localePrefix` helper +inside `createConfig`, never a hardcoded `/zh-hans`. ## Architecture diff --git a/README.md b/README.md index 6bd081a..eb755c8 100644 --- a/README.md +++ b/README.md @@ -42,14 +42,28 @@ pnpm format # Prettier — format all files The site is published on two domains, and the two builds are not interchangeable. `pnpm build:cn` runs `rspress build --config -rspress.cn.config.ts`, which differs from the default in three ways: +rspress.cn.config.ts`, which differs from the default in four ways: | | `build:com` | `build:cn` | | ------------------------------- | ----------------- | -------------- | +| Default language (unprefixed) | `en-gb` | `zh-hans` | | Canonical, `hreflang`, `og:url` | `onixbyte.com` | `onixbyte.cn` | | `sitemap.xml`, `robots.txt` | `onixbyte.com` | `onixbyte.cn` | | Social links | GitHub, Douyin, X | GitHub, Douyin | +**The default language differs per target**, so the routes do too. Rspress +serves the default locale without a prefix: + +| | `.com` | `.cn` | +| --------------- | ------------------------------- | ----------------------------- | +| English home | `/` | `/en-gb/` | +| Chinese home | `/zh-hans/` | `/` | +| English article | `/blogs/git-cheatsheet` | `/en-gb/blogs/git-cheatsheet` | +| Chinese article | `/zh-hans/blogs/git-cheatsheet` | `/blogs/git-cheatsheet` | + +Content paths under `docs/` are unaffected — only the prefixes change, and +Rspress rewrites internal links per locale automatically. + X is omitted from the `.cn` build because it is not reachable from mainland China. Which links count as domestic is a single list in `rspress.config.ts` (`SOCIAL_LINKS`) — move an entry between the two arrays to change a build. @@ -73,7 +87,7 @@ second build overwrites the first. To preview the `.cn` variant in dev, run docs/ ├── public/ # Static assets served from the site root │ └── onixbyte-*.svg -├── en-gb/ # English content (default locale, no URL prefix) +├── en-gb/ # English content │ ├── _nav.json # Top navigation; labels are i18n keys │ ├── index.md # Marketing homepage │ ├── blogs/ # Technical articles ("Insights") @@ -89,6 +103,7 @@ theme/ # Custom Rspress theme ├── index.css # Brand colours and section styling ├── lib/ │ ├── site.ts # Shared site constants +│ ├── i18n.ts # Types i18n.json for useI18n() │ └── social-icons.ts # Inline SVG for social links with no preset icon └── components/ ├── Author.tsx # Byline, rendered from `author` frontmatter @@ -146,6 +161,16 @@ Redirects for the retired `/projects/` paths live in `vercel.json`. The `.cn` host needs the equivalent rule in its own web server config, which is not tracked in this repository. +## Contributing + +This repository is **not open for external contributions** — pull requests are +not accepted. Issues reporting broken links, factual errors, typos or +reproducible bugs are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md). + ## Licence -MIT +All rights reserved. Nothing here is licensed for reuse: no permission is +granted to copy, modify or distribute any part of this repository. Reading it +and linking to it are fine. See [LICENCE](./LICENCE) for the full notice, and +[THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md) for the dependencies, which +keep their own licences. diff --git a/docs/en-gb/_nav.json b/docs/en-gb/_nav.json index 2e6d0b0..478c718 100644 --- a/docs/en-gb/_nav.json +++ b/docs/en-gb/_nav.json @@ -2,7 +2,7 @@ { "text": "nav.home", "link": "/", - "activeMatch": "^/$|^/zh-hans/$" + "activeMatch": "^/(en-gb/|zh-hans/)?$" }, { "text": "nav.products", diff --git a/docs/zh-hans/_nav.json b/docs/zh-hans/_nav.json index 2e6d0b0..478c718 100644 --- a/docs/zh-hans/_nav.json +++ b/docs/zh-hans/_nav.json @@ -2,7 +2,7 @@ { "text": "nav.home", "link": "/", - "activeMatch": "^/$|^/zh-hans/$" + "activeMatch": "^/(en-gb/|zh-hans/)?$" }, { "text": "nav.products", diff --git a/rspress.config.ts b/rspress.config.ts index 54bb328..e41eb94 100644 --- a/rspress.config.ts +++ b/rspress.config.ts @@ -22,13 +22,44 @@ const ORIGINS: Record = { cn: "https://onixbyte.cn", } -/** Locale used for the unprefixed routes. */ -const DEFAULT_LANG = "en-gb" +/** Every locale the site is published in, default first for each target. */ +const LANGS = ["en-gb", "zh-hans"] as const + +/** + * Locale served from the unprefixed routes. + * + * `.com` leads with English; `.cn` leads with Chinese, so a visitor landing on + * the bare domain gets the language that site exists for. The other locale then + * moves to a prefix — on `.cn`, English lives at `/en-gb/`. + */ +const DEFAULT_LANG: Record = { + com: "en-gb", + cn: "zh-hans", +} /** hreflang and Open Graph locale codes, keyed by Rspress lang. */ const HREFLANG: Record = { "en-gb": "en-GB", "zh-hans": "zh-Hans" } const OG_LOCALE: Record = { "en-gb": "en_GB", "zh-hans": "zh_CN" } +/** Language-switcher labels, keyed by Rspress lang. */ +const LOCALE_LABELS: Record = { + "en-gb": "English (Great Britain)", + "zh-hans": "简体中文", +} + +/** Site title and description, keyed by Rspress lang. */ +const LOCALE_META: Record = { + "en-gb": { + title: "OnixByte", + description: + "OnixByte builds self-developed SaaS products and delivers custom software engineering services.", + }, + "zh-hans": { + title: "OnixByte", + description: "曜珀科技自主研发 SaaS 产品,并提供定制化软件开发服务。", + }, +} + /** * Social links, split by whether they are reliably reachable from mainland * China. The `.cn` build renders only the `domestic` set. @@ -73,19 +104,31 @@ const SOCIAL_LINKS: Record<"domestic" | "international", SocialLink[]> = { type CustomMapsOption = NonNullable[0]["customMaps"]> type SitemapOverrides = Record> -const SITEMAP_PRIORITIES = { - "/products/": { priority: "1.0" }, - "/zh-hans/products/": { priority: "1.0" }, - "/services/": { priority: "1.0" }, - "/zh-hans/services/": { priority: "1.0" }, - "/opensource-projects/": { priority: "0.8" }, - "/zh-hans/opensource-projects/": { priority: "0.8" }, -} satisfies SitemapOverrides +const PRIORITISED_ROUTES = [ + ["/products/", "1.0"], + ["/services/", "1.0"], + ["/opensource-projects/", "0.8"], +] as const -const localePrefix = (lang: string) => (lang === DEFAULT_LANG ? "" : `/${lang}`) +/** + * Builds the override map for a target. + * + * The keys are full route paths, so they depend on which locale is unprefixed — + * `/products/` means English on `.com` and Chinese on `.cn`. + */ +function sitemapPriorities(localePrefix: (lang: string) => string): SitemapOverrides { + return Object.fromEntries( + LANGS.flatMap((lang) => + PRIORITISED_ROUTES.map(([routePath, priority]) => [ + `${localePrefix(lang)}${routePath}`, + { priority }, + ]) + ) + ) satisfies SitemapOverrides +} /** Strips the language prefix, always returning a leading slash. */ -function bareRoutePath(routePath: string, lang: string): string { +function bareRoutePath(localePrefix: (lang: string) => string, routePath: string, lang: string) { const prefix = localePrefix(lang) const rest = prefix && routePath.startsWith(prefix) ? routePath.slice(prefix.length) : routePath return rest.startsWith("/") ? rest : `/${rest}` @@ -112,16 +155,19 @@ function pluginRobots(origin: string): RspressPlugin { export function createConfig(target: SiteTarget): UserConfig { const origin = ORIGINS[target] + const defaultLang = DEFAULT_LANG[target] const socialLinks = [ ...SOCIAL_LINKS.domestic, ...(target === "cn" ? [] : SOCIAL_LINKS.international), ] + /** Rspress drops the prefix from the default locale's routes. */ + const localePrefix = (lang: string) => (lang === defaultLang ? "" : `/${lang}`) + return defineConfig({ root: path.join(__dirname, "docs"), - title: "OnixByte", - description: - "OnixByte builds self-developed SaaS products and delivers custom software engineering services.", + title: LOCALE_META[defaultLang].title, + description: LOCALE_META[defaultLang].description, icon: "/onixbyte-icon.svg", logo: { light: "/onixbyte-light-logo.svg", @@ -139,8 +185,8 @@ export function createConfig(target: SiteTarget): UserConfig { head: [ (route) => { const canonical = `${origin}${route.routePath}` - const bare = bareRoutePath(route.routePath, route.lang) - const otherLang = route.lang === DEFAULT_LANG ? "zh-hans" : DEFAULT_LANG + const bare = bareRoutePath(localePrefix, route.routePath, route.lang) + const otherLang = LANGS.find((lang) => lang !== route.lang) ?? route.lang const alternate = `${origin}${localePrefix(otherLang)}${bare === "/" ? "/" : bare}` const defaultUrl = `${origin}${bare === "/" ? "/" : bare}` @@ -172,22 +218,13 @@ export function createConfig(target: SiteTarget): UserConfig { markdown: { showLineNumbers: true, }, - lang: DEFAULT_LANG, - locales: [ - { - lang: "en-gb", - label: "English (Great Britain)", - title: "OnixByte", - description: - "OnixByte builds self-developed SaaS products and delivers custom software engineering services.", - }, - { - lang: "zh-hans", - label: "简体中文", - title: "OnixByte", - description: "曜珀科技自主研发 SaaS 产品,并提供定制化软件开发服务。", - }, - ], + lang: defaultLang, + // The default locale is listed first so the language switcher leads with it. + locales: [defaultLang, ...LANGS.filter((lang) => lang !== defaultLang)].map((lang) => ({ + lang, + label: LOCALE_LABELS[lang], + ...LOCALE_META[lang], + })), // Local full-text search. This is already the Rspress v2 default; stated // explicitly so the intent is visible. The index is emitted per locale to // `static/search_index...json` at build time. @@ -200,12 +237,10 @@ export function createConfig(target: SiteTarget): UserConfig { siteUrl: origin, defaultChangeFreq: "weekly", defaultPriority: "0.5", - // Keys are full route paths, so the non-default locale needs its own - // entries — `/products/` does not match `/zh-hans/products/`. // Double assertion rather than a plain cast: the plugin's `Sitemap` type // requires `loc`, so the two types do not overlap enough for a single - // assertion. See `SITEMAP_PRIORITIES` above for why omitting it is right. - customMaps: SITEMAP_PRIORITIES as unknown as CustomMapsOption, + // assertion. See `sitemapPriorities` above for why omitting it is right. + customMaps: sitemapPriorities(localePrefix) as unknown as CustomMapsOption, }), pluginRobots(origin), ],