# Fumadocs (Framework Mode, Astro): 國際化
URL: /zh-TW/docs/astro/internationalization
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/[framework]/internationalization.zh-TW.mdx

讓您的 Astro 文件支援多種語言。

<Callout title="Fumadocs CLI">
執行 `npx @fumadocs/cli init --i18n` 即可建立支援 i18n 的新文件網站，您也可以依照下方說明手動設定。
</Callout>

## 概覽 [#overview]

請在 Astro 與 Fumadocs 兩端設定 i18n 路由。

Fumadocs 會負責文件內容與側邊欄的 i18n。應用程式的其他部分，請透過您自己的 i18n 設定來進行國際化。

## 設定 [#setup]

請在一個檔案中定義 i18n 設定。本指南會以 `@/lib/i18n` 匯入該設定。

```ts title="src/lib/i18n.ts"
import { defineI18n } from 'fumadocs-core/i18n';

export const i18n = defineI18n({
  defaultLanguage: 'en',
  languages: ['cn', 'en'],
});
```

> i18n 設定的可用選項請參閱[這裡](/docs/headless/internationalization/config)。

### 路由 [#routing]

為您的頁面加上 `[...lang]` 前綴。透過 rest 參數，預設語言可以不帶前綴 (例如 `/docs`) 。

<Files>
<Folder name="src/pages" defaultOpen>
<Folder name="[...lang]" defaultOpen>
<File name="index.astro" />

<Folder name="docs" defaultOpen>
<File name="[...slug].astro" />
</Folder>
</Folder>

<Folder name="api" defaultOpen>
<File name="search.ts" />
</Folder>
</Folder>
</Files>

啟用 `hideLocale`，讓 Fumadocs 產生的 URL 與這些路由一致：

```ts title="src/lib/i18n.ts"
export const i18n = defineI18n({
  defaultLanguage: 'en',
  languages: ['cn', 'en'],
  hideLocale: 'default-locale', // [!code ++]
});
```

### 翻譯 [#translations]

請在 `lib/layout.shared.tsx` 中定義您的翻譯。詳細資訊 (包括官方語言包) 請參閱[翻譯](/docs/ui/translations)。

您也可以為 `baseOptions()` 加上 `locale` 參數，以提供依語系調整的版面配置選項：

```tsx title="src/lib/layout.shared.tsx"
// [!code ++:2]
import { i18n } from '@/lib/i18n';
import { uiTranslations } from 'fumadocs-ui/i18n';
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';

// [!code ++:3]
export const translations = i18n
  .translations()
  .extend(uiTranslations());

export function baseOptions(): BaseLayoutProps { // [!code --]
export function baseOptions(locale: string): BaseLayoutProps { // [!code ++]
  return {
    nav: {
      title: 'My App',
      url: locale === i18n.defaultLanguage ? '/' : `/${locale}`, // [!code ++]
    },
  };
}
```

將翻譯傳入 `<Provider />`。由於 MDX 內容自成一個獨立的 island，因此也需要提供 provider：

```tsx tab="src/components/provider.tsx"
import { navigate } from 'astro:transitions/client';
import type { ReactNode } from 'react';
import { RootProvider } from 'fumadocs-ui/provider/astro';
// [!code ++:2]
import { i18nProvider } from 'fumadocs-ui/i18n';
import { translations } from '@/lib/layout.shared';
import SearchDialog from '@/components/search';

export interface ProviderProps {
  lang: string; // [!code ++]
  pathname: string;
  params: Record<string, string | undefined>;
  children?: ReactNode;
}

export function Provider({ lang, pathname, params, children }: ProviderProps) {
  return (
    <RootProvider
      pathname={pathname}
      params={params}
      navigate={navigate}
      // [!code ++]
      i18n={i18nProvider(translations, lang)}
      search={{ SearchDialog }}
    >
      {children}
    </RootProvider>
  );
}

// [!code ++:16]
/**
 * 供 MDX 內容使用，因為它自成一個獨立的 island。
 */
export function ContentProvider({ lang, pathname, params, children }: ProviderProps) {
  return (
    <RootProvider
      pathname={pathname}
      params={params}
      navigate={navigate}
      i18n={i18nProvider(translations, lang)}
      theme={{ enabled: false }}
      search={{ enabled: false }}
    >
      {children}
    </RootProvider>
  );
}
```

```tsx tab="src/components/mdx.tsx"
import defaultMdxComponents from 'fumadocs-ui/mdx';
import { DocsBody } from 'fumadocs-ui/layouts/docs/page';
import type { MDXComponents } from 'mdx/types';
import { ContentProvider, type ProviderProps } from '@/components/provider'; // [!code ++]

export function getMDXComponents(components?: MDXComponents) {
  return {
    ...defaultMdxComponents,
    ...components,
  } satisfies MDXComponents;
}

// 在 Fumadocs 整合中，`wrapper` 會接收 `<Content />` 的 props
export default getMDXComponents({
  wrapper: DocsBody, // [!code --]
  // [!code ++:5]
  wrapper: ({ children, ...props }: ProviderProps) => (
    <ContentProvider {...props}>
      <DocsBody>{children}</DocsBody>
    </ContentProvider>
  ),
});
```

### 傳入語系 [#pass-locale]

請在來源、版面配置與頁面中將語系傳給 Fumadocs。

```ts tab="src/lib/source.ts"
import { i18n } from '@/lib/i18n'; // [!code ++]

export const source = loader({
  source: toFumadocsSource(await getCollection('docs'), await getCollection('meta')),
  baseUrl: '/docs',
  i18n, // [!code ++]
});
```

```tsx tab="src/components/docs.tsx"
export function Docs({ tree, title, description, toc, children, ...props }: DocsProps) {
  const data = useFumadocsLoader({ tree });

  return (
    <Provider {...props}>
      {/* [!code highlight] */}
      <DocsLayout {...baseOptions(props.lang)} tree={data.tree}>
        {/* ... */}
      </DocsLayout>
    </Provider>
  );
}
```

```astro tab="src/layouts/Layout.astro"
---
interface Props {
  lang: string; // [!code ++]
  title: string;
  description?: string;
}

const { lang, title, description } = Astro.props;
---

<html lang={lang} transition:animate="none">
  {/* ... */}
</html>
```

```astro tab="文件頁面" title="src/pages/[...lang]/docs/[...slug].astro"
---
import type { GetStaticPaths, InferGetStaticPropsType } from 'astro';
import Layout from '@/layouts/Layout.astro';
import { Docs } from '@/components/docs';
import { i18n } from '@/lib/i18n';
import { source } from '@/lib/source';

export const prerender = true;

export const getStaticPaths = (() => {
  const paths = [];
  for (const { language, pages } of source.getLanguages()) {
    for (const page of pages) {
      paths.push({
        params: {
          lang: language === i18n.defaultLanguage ? undefined : language,
          slug: decodeURI(page.slugs.join('/')) || undefined,
        },
        props: { lang: language, page },
      });
    }
  }
  return paths;
}) satisfies GetStaticPaths;

type Props = InferGetStaticPropsType<typeof getStaticPaths>;

const { lang, page } = Astro.props;
const { body: Content, toc } = await page.data.load();
const pageTree = await source.serializePageTree(source.getPageTree(lang));
const provider = { lang, pathname: Astro.url.pathname, params: Astro.params };
---

<Layout lang={lang} title={page.data.title} description={page.data.description}>
  <Docs
    client:load
    transition:persist="docs"
    {...provider}
    tree={pageTree}
    title={page.data.title}
    description={page.data.description}
    toc={toc}
  >
    <Content {...provider} />
  </Docs>
</Layout>
```

```astro tab="首頁" title="src/pages/[...lang]/index.astro"
---
import type { GetStaticPaths, InferGetStaticPropsType } from 'astro';
import Layout from '@/layouts/Layout.astro';
import { i18n } from '@/lib/i18n';

export const prerender = true;

export const getStaticPaths = (() => {
  return i18n.languages.map((lang) => ({
    params: { lang: lang === i18n.defaultLanguage ? undefined : lang },
    props: { lang },
  }));
}) satisfies GetStaticPaths;

type Props = InferGetStaticPropsType<typeof getStaticPaths>;

const { lang } = Astro.props;
---

<Layout lang={lang} title="My App">
  <a href={lang === i18n.defaultLanguage ? '/docs' : `/${lang}/docs`}>
    Open Docs
  </a>
</Layout>
```

### 搜尋 [#search]

請在您使用的搜尋方案中設定 i18n。

* **內建搜尋：** 請參閱[國際化](/docs/headless/search/orama#internationalization)。
* **雲端方案 (例如 Algolia) ：** 這類方案通常有官方的多語系支援。

## 撰寫文件 [#writing-documents]

請參閱 [i18n 路由](/docs/astro/page-conventions#i18n-routing)，了解如何為特定語系建立頁面。

### 導覽 [#navigation]

Fumadocs 只會處理其自身版面配置 (例如側邊欄) 中的導覽。

至於其他地方，請從 `Astro.params` 或路徑中擷取語系，並將其加在連結的開頭。

```astro
---
const { lang } = Astro.params;
---

<a href={lang ? `/${lang}/about` : '/about'}>About Us</a>
```

[`fumadocs-core/dynamic-link`](/docs/headless/components/link#dynamic-hrefs) 元件支援動態 href，因此您可以在 Markdown/MDX 內容中用它加上語系前綴。

由於 MDX 內容會在其自身的 island 中算繪這些元件，請將它加入您的 MDX 元件：

```tsx title="src/components/mdx.tsx"
import { DynamicLink } from 'fumadocs-core/dynamic-link'; // [!code ++]

export function getMDXComponents(components?: MDXComponents) {
  return {
    ...defaultMdxComponents,
    DynamicLink, // [!code ++]
    ...components,
  } satisfies MDXComponents;
}
```

```mdx title="content.mdx"
<DynamicLink href="/[lang]/another-page">This is a link</DynamicLink>
```