feat(docs): serve Korean docs content, remove English-fallback stopgap

Now that the *.ko.mdx corpus exists, drop the temporary docsContentLang
ko→en shim and the static-params fallback-synthesis loop so /docs/ko/*
renders real Korean content. Korean is now a first-class locale whose
params come straight from source.generateParams(). Also align the docs
home hero copy (agent→에이전트) with the app and the translated body.

MUL-2817

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
J
2026-05-29 16:19:17 +08:00
parent fc2617089c
commit f77f75ed2e
7 changed files with 31 additions and 40 deletions

View File

@@ -9,7 +9,7 @@ import { notFound } from "next/navigation";
import defaultMdxComponents from "fumadocs-ui/mdx";
import type { Metadata } from "next";
import { docsAlternates } from "@/lib/site";
import { docsContentLang, i18n, type Lang } from "@/lib/i18n";
import { i18n, type Lang } from "@/lib/i18n";
import { DocsLocaleProvider, LocaleLink } from "@/components/locale-link";
import { docsSlugStaticParams } from "@/lib/static-params";
@@ -24,7 +24,7 @@ export default async function Page(props: {
}) {
const params = await props.params;
const lang = asLang(params.lang);
const page = source.getPage(params.slug, docsContentLang(lang));
const page = source.getPage(params.slug, lang);
if (!page) notFound();
const MDX = page.data.body;
@@ -51,7 +51,7 @@ export async function generateMetadata(props: {
}): Promise<Metadata> {
const params = await props.params;
const lang = asLang(params.lang);
const page = source.getPage(params.slug, docsContentLang(lang));
const page = source.getPage(params.slug, lang);
if (!page) notFound();
return {

View File

@@ -7,7 +7,7 @@ import type { Metadata } from "next";
import { cn } from "@multica/ui/lib/utils";
import { baseOptions } from "@/app/layout.config";
import { source } from "@/lib/source";
import { docsContentLang, i18n, type Lang } from "@/lib/i18n";
import { i18n, type Lang } from "@/lib/i18n";
import { uiTranslations, localeLabels } from "@/lib/translations";
import { DocsSettings } from "@/components/docs-settings";
@@ -76,7 +76,6 @@ export default async function Layout({
const lang = (i18n.languages as readonly string[]).includes(rawLang)
? (rawLang as Lang)
: (i18n.defaultLanguage as Lang);
const contentLang = docsContentLang(lang);
const locales = i18n.languages.map((l) => ({
locale: l,
name: localeLabels[l],
@@ -84,7 +83,7 @@ export default async function Layout({
return (
<html
lang={contentLang}
lang={lang}
suppressHydrationWarning
className={cn(
"antialiased",
@@ -103,7 +102,7 @@ export default async function Layout({
search={{ options: { api: "/docs/api/search" } }}
>
<DocsLayout
tree={source.getPageTree(contentLang)}
tree={source.getPageTree(lang)}
// Suppress Fumadocs's default sidebar-footer icons (theme +
// language + search). Our custom <DocsSettings> is mounted as
// the sidebar footer instead — two labelled buttons, not three

View File

@@ -5,7 +5,7 @@ import defaultMdxComponents from "fumadocs-ui/mdx";
import type { Metadata } from "next";
import { DocsHero } from "@/components/hero";
import { Byline, NumberedCards, NumberedCard, NumberedSteps, Step } from "@/components/editorial";
import { docsContentLang, i18n, type Lang } from "@/lib/i18n";
import { i18n, type Lang } from "@/lib/i18n";
import { homeCopy } from "@/lib/translations";
import { docsAlternates } from "@/lib/site";
import { DocsLocaleProvider, LocaleLink } from "@/components/locale-link";
@@ -31,7 +31,7 @@ export default async function Page({
}) {
const { lang: rawLang } = await params;
const lang = asLang(rawLang);
const page = source.getPage([], docsContentLang(lang));
const page = source.getPage([], lang);
if (!page) notFound();
const MDX = page.data.body;
@@ -77,7 +77,7 @@ export async function generateMetadata({
}): Promise<Metadata> {
const { lang: rawLang } = await params;
const lang = asLang(rawLang);
const page = source.getPage([], docsContentLang(lang));
const page = source.getPage([], lang);
if (!page) notFound();
return {

View File

@@ -1,9 +1,9 @@
import { defineI18n } from "fumadocs-core/i18n";
// English is the default; Chinese is available under /zh/.
// English is the default; Chinese (/zh/) and Korean (/ko/) are available.
// hideLocale: 'default-locale' keeps English URLs prefix-free
// (`/docs/`) while translated locales live under `/docs/<lang>/...`.
// parser: 'dot' picks up `page.zh.mdx` and `meta.zh.json`.
// parser: 'dot' picks up `page.zh.mdx` / `page.ko.mdx` and `meta.<lang>.json`.
export const i18n = defineI18n({
languages: ["en", "zh", "ko"],
defaultLanguage: "en",
@@ -12,10 +12,3 @@ export const i18n = defineI18n({
});
export type Lang = (typeof i18n.languages)[number];
// Korean docs routes are enabled before the full MDX corpus is translated.
// Until `*.ko.mdx` files exist, render the English source with Korean docs
// chrome so `/docs/ko/...` remains a stable locale URL instead of 404ing.
export function docsContentLang(lang: Lang): Lang {
return lang === "ko" ? "en" : lang;
}

View File

@@ -1,13 +1,24 @@
import { describe, expect, it } from "vitest";
import { docsSlugStaticParams, type DocsStaticParam } from "./static-params";
import { docsSlugStaticParams } from "./static-params";
// `source.generateParams()` hands back loosely-typed params (`lang: string`),
// so the inputs here mirror that shape — the `lang` strings are validated and
// narrowed by `docsSlugStaticParams` itself.
type RawParam = { lang: string; slug: string[] };
describe("docsSlugStaticParams", () => {
it("adds Korean fallback params for every English slug page", () => {
const params: DocsStaticParam[] = [
it("returns every localized slug page and drops the home param", () => {
// Each locale's pages come straight from `source.generateParams()` now
// that `*.ko.mdx` files exist — Korean is a first-class locale, not an
// English fallback. The only transform is dropping the empty-slug home
// param (rendered by `[lang]/page.tsx`, not the catch-all route).
const params: RawParam[] = [
{ lang: "en", slug: [] },
{ lang: "en", slug: ["agents"] },
{ lang: "en", slug: ["cli", "reference"] },
{ lang: "zh", slug: ["agents"] },
{ lang: "ko", slug: ["agents"] },
{ lang: "ko", slug: ["cli", "reference"] },
];
expect(docsSlugStaticParams(params)).toEqual([
@@ -19,17 +30,15 @@ describe("docsSlugStaticParams", () => {
]);
});
it("keeps existing localized params and does not duplicate Korean pages", () => {
const params: DocsStaticParam[] = [
{ lang: "en", slug: ["agents"] },
it("drops unknown languages and de-duplicates repeated params", () => {
const params: RawParam[] = [
{ lang: "ko", slug: ["agents"] },
{ lang: "zh", slug: ["guides", "quickstart"] },
{ lang: "ko", slug: ["agents"] },
{ lang: "fr", slug: ["agents"] },
];
expect(docsSlugStaticParams(params)).toEqual([
{ lang: "en", slug: ["agents"] },
{ lang: "ko", slug: ["agents"] },
{ lang: "zh", slug: ["guides", "quickstart"] },
]);
});
});

View File

@@ -1,4 +1,4 @@
import { docsContentLang, i18n, type Lang } from "@/lib/i18n";
import { i18n, type Lang } from "@/lib/i18n";
export type DocsStaticParam = {
lang: Lang;
@@ -37,15 +37,5 @@ export function docsSlugStaticParams(
for (const param of slugParams) addParam(param);
for (const lang of i18n.languages) {
const contentLang = docsContentLang(lang);
if (contentLang === lang) continue;
for (const param of slugParams) {
if (param.lang !== contentLang) continue;
addParam({ lang, slug: param.slug });
}
}
return output;
}

View File

@@ -54,7 +54,7 @@ export const homeCopy = {
},
ko: {
eyebrow: "Multica 문서",
titleLead: "사람과 agent,",
titleLead: "사람과 에이전트,",
titleAccent: "한곳에서.",
byline: ["시작하기", "2026년 4월 업데이트", "약 6분 읽기"],
},