mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-15 22:29:04 +02:00
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:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export const homeCopy = {
|
||||
},
|
||||
ko: {
|
||||
eyebrow: "Multica 문서",
|
||||
titleLead: "사람과 agent,",
|
||||
titleLead: "사람과 에이전트,",
|
||||
titleAccent: "한곳에서.",
|
||||
byline: ["시작하기", "2026년 4월 업데이트", "약 6분 읽기"],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user