diff --git a/bun.lock b/bun.lock index a9cabb31114..d8ee468c50b 100644 --- a/bun.lock +++ b/bun.lock @@ -475,6 +475,7 @@ "shiki": "catalog:", "solid-js": "catalog:", "toolbeam-docs-theme": "0.4.8", + "unist-util-visit": "5.0.0", }, "devDependencies": { "@types/node": "catalog:", diff --git a/packages/console/app/script/generate-sitemap.ts b/packages/console/app/script/generate-sitemap.ts index 6cbffcb8516..9edcf36c8e7 100755 --- a/packages/console/app/script/generate-sitemap.ts +++ b/packages/console/app/script/generate-sitemap.ts @@ -42,19 +42,47 @@ async function getDocsRoutes(): Promise { const routes: SitemapEntry[] = [] try { - const files = await readdir(DOCS_DIR) - - for (const file of files) { - if (!file.endsWith(".mdx")) continue - - const slug = file.replace(".mdx", "") - const path = slug === "index" ? "/docs/" : `/docs/${slug}` + async function walkMdxFiles(rootDir: string, currentDir = rootDir): Promise { + const entries = await readdir(currentDir, { withFileTypes: true }) + const results: string[] = [] + + for (const entry of entries) { + const absolute = join(currentDir, entry.name) + if (entry.isDirectory()) { + results.push(...(await walkMdxFiles(rootDir, absolute))) + continue + } + if (!entry.isFile()) continue + if (!entry.name.endsWith(".mdx")) continue + results.push(absolute) + } + + return results + } - routes.push({ - url: `${BASE_URL}${path}`, - priority: slug === "index" ? 0.9 : 0.7, - changefreq: "weekly", - }) + const localeDirs = (await readdir(DOCS_DIR, { withFileTypes: true })) + .filter((d) => d.isDirectory()) + .map((d) => d.name) + + for (const docsDirName of localeDirs) { + const routeLocale = docsDirName === "en" ? "root" : docsDirName + const localeDir = join(DOCS_DIR, docsDirName) + const files = await walkMdxFiles(localeDir) + + for (const absolutePath of files) { + const relative = absolutePath.slice(localeDir.length + 1) + const slug = relative.replace(/\.mdx$/, "").replace(/\\/g, "/") + const normalizedSlug = + slug === "index" ? "" : slug.endsWith("/index") ? slug.slice(0, -6) : slug + const basePath = routeLocale === "root" ? "/docs/" : `/docs/${routeLocale}/` + const path = normalizedSlug ? `${basePath}${normalizedSlug}/` : basePath + + routes.push({ + url: `${BASE_URL}${path}`, + priority: slug === "index" ? 0.9 : 0.7, + changefreq: "weekly", + }) + } } } catch (error) { console.error("Error reading docs directory:", error) diff --git a/packages/web/astro.config.mjs b/packages/web/astro.config.mjs index 99a1c3bd80c..1d3467befdc 100644 --- a/packages/web/astro.config.mjs +++ b/packages/web/astro.config.mjs @@ -8,6 +8,7 @@ import config from "./config.mjs" import { rehypeHeadingIds } from "@astrojs/markdown-remark" import rehypeAutolinkHeadings from "rehype-autolink-headings" import { spawnSync } from "child_process" +import { visit } from "unist-util-visit" // https://astro.build/config export default defineConfig({ @@ -24,7 +25,33 @@ export default defineConfig({ host: "0.0.0.0", }, markdown: { - rehypePlugins: [rehypeHeadingIds, [rehypeAutolinkHeadings, { behavior: "wrap" }]], + rehypePlugins: [ + rehypeHeadingIds, + [rehypeAutolinkHeadings, { behavior: "wrap" }], + () => (tree, file) => { + const filePath = typeof file?.path === "string" ? file.path : "" + const normalizedPath = filePath.replace(/\\/g, "/") + const docsMarker = "/src/content/docs/" + const docsIndex = normalizedPath.lastIndexOf(docsMarker) + const localeDir = + docsIndex === -1 + ? "en" + : normalizedPath.slice(docsIndex + docsMarker.length).split("/")[0] || "en" + const locale = localeDir === "en" ? "root" : localeDir + const base = locale === "root" ? "/docs" : `/docs/${locale}` + + visit(tree, "element", (node) => { + if (node.tagName !== "a") return + const href = node.properties?.href + if (typeof href !== "string") return + if (!href.startsWith("/docs")) return + const isRoot = href === "/docs" + const isRootHash = href.startsWith("/docs#") + const normalized = isRoot ? base : isRootHash ? `${base}${href.slice(5)}` : href.replace("/docs/", `${base}/`) + node.properties.href = normalized + }) + }, + ], }, build: {}, integrations: [ @@ -32,6 +59,11 @@ export default defineConfig({ solidJs(), starlight({ title: "OpenCode", + locales: { + root: { label: "English", lang: "en" }, + "zh-cn": { label: "中文", lang: "zh-CN" }, + }, + defaultLocale: "root", lastUpdated: true, expressiveCode: { themes: ["github-light", "github-dark"] }, social: [ @@ -50,44 +82,88 @@ export default defineConfig({ dark: "./src/assets/logo-dark.svg", replacesTitle: true, }, - sidebar: [ - "", - "config", - "providers", - "network", - "enterprise", - "troubleshooting", - "1-0", + // @ts-ignore + sidebar: withTranslations([ + { + label: "Intro", + translations: { "zh-cn": "介绍" }, + link: "", + }, + { + label: "Config", + translations: { "zh-cn": "配置" }, + link: "config", + }, + { + label: "Providers", + translations: { "zh-cn": "提供商" }, + link: "providers", + }, + { + label: "Network", + translations: { "zh-cn": "网络" }, + link: "network", + }, + { + label: "Enterprise", + translations: { "zh-cn": "企业" }, + link: "enterprise", + }, + { + label: "Troubleshooting", + translations: { "zh-cn": "故障排查" }, + link: "troubleshooting", + }, + { + label: "1-0", + translations: { "zh-cn": "1-0 版本" }, + link: "1-0", + }, { label: "Usage", - items: ["tui", "cli", "web", "ide", "zen", "share", "github", "gitlab"], + translations: { "zh-cn": "使用" }, + items: [ + { label: "TUI", translations: { "zh-cn": "TUI" }, link: "tui" }, + { label: "CLI", translations: { "zh-cn": "CLI" }, link: "cli" }, + { label: "Web", translations: { "zh-cn": "Web" }, link: "web" }, + { label: "IDE", translations: { "zh-cn": "IDE" }, link: "ide" }, + { label: "Zen", translations: { "zh-cn": "Zen" }, link: "zen" }, + { label: "Share", translations: { "zh-cn": "分享" }, link: "share" }, + { label: "GitHub", translations: { "zh-cn": "GitHub" }, link: "github" }, + { label: "GitLab", translations: { "zh-cn": "GitLab" }, link: "gitlab" }, + ], }, - { label: "Configure", + translations: { "zh-cn": "配置" }, items: [ - "tools", - "rules", - "agents", - "models", - "themes", - "keybinds", - "commands", - "formatters", - "permissions", - "lsp", - "mcp-servers", - "acp", - "skills", - "custom-tools", + { label: "tools", translations: { "zh-cn": "tools" }, link: "tools" }, + { label: "rules", translations: { "zh-cn": "规则" }, link: "rules" }, + { label: "agents", translations: { "zh-cn": "agents" }, link: "agents" }, + { label: "models", translations: { "zh-cn": "模型" }, link: "models" }, + { label: "themes", translations: { "zh-cn": "主题" }, link: "themes" }, + { label: "keybinds", translations: { "zh-cn": "快捷键" }, link: "keybinds" }, + { label: "commands", translations: { "zh-cn": "命令" }, link: "commands" }, + { label: "formatters", translations: { "zh-cn": "格式化" }, link: "formatters" }, + { label: "permissions", translations: { "zh-cn": "权限" }, link: "permissions" }, + { label: "lsp", translations: { "zh-cn": "LSP" }, link: "lsp" }, + { label: "mcp-servers", translations: { "zh-cn": "mcp servers" }, link: "mcp-servers" }, + { label: "acp", translations: { "zh-cn": "ACP" }, link: "acp" }, + { label: "skills", translations: { "zh-cn": "skills" }, link: "skills" }, + { label: "custom tools", translations: { "zh-cn": "custom tools" }, link: "custom-tools" }, ], }, - { label: "Develop", - items: ["sdk", "server", "plugins", "ecosystem"], + translations: { "zh-cn": "开发" }, + items: [ + { label: "SDK", translations: { "zh-cn": "SDK" }, link: "sdk" }, + { label: "Server", translations: { "zh-cn": "服务器" }, link: "server" }, + { label: "Plugins", translations: { "zh-cn": "插件" }, link: "plugins" }, + { label: "Ecosystem", translations: { "zh-cn": "生态" }, link: "ecosystem" }, + ], }, - ], + ]), components: { Hero: "./src/components/Hero.astro", Head: "./src/components/Head.astro", @@ -114,3 +190,18 @@ function configSchema() { }, } } + +/** + * @param {any[]} sidebar + */ +function withTranslations(sidebar) { + return sidebar.map((item) => { + if (item.translations && item.translations["zh-cn"]) { + item.translations["zh-CN"] = item.translations["zh-cn"] + } + if (item.items) { + item.items = withTranslations(item.items) + } + return item + }) +} diff --git a/packages/web/package.json b/packages/web/package.json index aef7c0c706d..9415b59209a 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -31,7 +31,8 @@ "remeda": "catalog:", "shiki": "catalog:", "solid-js": "catalog:", - "toolbeam-docs-theme": "0.4.8" + "toolbeam-docs-theme": "0.4.8", + "unist-util-visit": "5.0.0" }, "devDependencies": { "opencode": "workspace:*", diff --git a/packages/web/src/components/Header.astro b/packages/web/src/components/Header.astro index 396200a3eb1..6164165c521 100644 --- a/packages/web/src/components/Header.astro +++ b/packages/web/src/components/Header.astro @@ -3,15 +3,50 @@ import config from '../../config.mjs'; import astroConfig from 'virtual:starlight/user-config'; import { Icon } from '@astrojs/starlight/components'; import { HeaderLinks } from 'toolbeam-docs-theme/components'; -import Default from 'toolbeam-docs-theme/overrides/Header.astro'; import SocialIcons from 'virtual:starlight/components/SocialIcons'; import SiteTitle from '@astrojs/starlight/components/SiteTitle.astro'; +import Search from "virtual:starlight/components/Search"; const path = Astro.url.pathname; const links = astroConfig.social || []; const headerLinks = config.headerLinks; +let relativePath = path.replace(/\/$/, ""); +if (relativePath.startsWith("/docs")) relativePath = relativePath.slice(5); +if (relativePath.startsWith("/")) relativePath = relativePath.slice(1); + +const locales = astroConfig.locales || {}; +const localeKeys = Object.keys(locales); + +const segments = relativePath.split("/").filter(Boolean); +const firstLower = (segments[0] || "").toLowerCase(); + +let locale = "root"; +if (firstLower === "en") { + segments.shift(); +} else { + const key = localeKeys.find((k) => k.toLowerCase() === firstLower); + if (key && key !== "root") { + locale = key; + segments.shift(); + } +} + +const slug = segments.join("/"); +const localeLabel = locales[locale]?.label ?? (locale === "root" ? "English" : locale); +const localeLinks = localeKeys.map((key) => { + const isRoot = key === "root"; + const href = isRoot ? (slug ? `/docs/${slug}` : "/docs") : (slug ? `/docs/${key}/${slug}` : `/docs/${key}`); + return { key, href, label: locales[key]?.label ?? key }; +}); + +const isDocs = path.startsWith("/docs"); + +const shouldRenderSearch = + astroConfig.pagefind || + astroConfig.components.Search !== "@astrojs/starlight/components/Search.astro"; + --- { path.startsWith("/s") @@ -27,6 +62,21 @@ const headerLinks = config.headerLinks; }
+ {isDocs && ( +
+ +
+ {localeLinks.map((l) => ( + {l.label} + ))} +
+
+ )} { links.length > 0 && (
- : + :
+
+ +
+
+ +
+
+
+ +
+ {localeLinks.map((l) => ( + {l.label} + ))} +
+
+ + { + links.length > 0 && ( + + ) + } + {shouldRenderSearch && } +
+
}