Internationalization

Internationalize your Fumapress app.

Setup

To configure internationalization, define an i18n config:

press.config.tsx
import { defineConfig } from "fumapress";
import { defineDocs } from "fumadocs-mdx/macro";
import { defineI18n } from "fumadocs-core/i18n";
import { uiTranslations } from "fumadocs-ui/i18n";
import { fumapressTranslations } from "fumapress/i18n";

const docs = defineDocs({
  dir: "content/docs",
  docs: { async: true },
});

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

const translations = i18n
  .translations()
  .extend(uiTranslations())
  .extend(fumapressTranslations());

export default defineConfig({
  content: docs.toFumadocsSource(),
  translations,
});

You can add translations to UI like:

const translations = i18n
  .translations()
  .extend(uiTranslations())
  .extend(fumapressTranslations())
  .add({
    en: { displayName: "English" },
    cn: {
      displayName: "Chinese",
      "Search(search dialog)": "搜尋文檔",
      "Blog(blog)": "博客",
      "All Tags(blog tags page)": "全部标签",
    },
  });

If you have other integrations and want to add translations for them, include them via extend() like:

import { openapiTranslations } from "fumadocs-openapi/i18n";
import { aiTranslations } from "@fumapress/ai/i18n";
import { feedbackTranslations } from "@fumapress/feedback/i18n";

const translations = i18n
  .translations()
  .extend(uiTranslations())
  .extend(fumapressTranslations())
  .extend(openapiTranslations())
  .extend(feedbackTranslations())
  .extend(aiTranslations());

Using Language Packs

The official language pack extends @fumadocs/language and adds translations for Fumapress.

npm i @fumapress/language
press.config.tsx
import { zhCN } from "@fumapress/language/zh-cn";

const translations = i18n
  .translations()
  // add Simplified Chinese translations to `cn` locale
  .preset("cn", zhCN());

Available presets:

  • zhCN
  • zhTW

When using a language pack, you do not need to include integrations with extend() unless you want to override the default translations.

Learn More

See Fumadocs Translations API for adding translations.

Writing Content

Add Markdown/JSON files for different languages by appending .{locale} to your file name, like:

meta.json
meta.cn.json
get-started.mdx
get-started.cn.mdx

Fallback Pages

Locales without a translation of a page inherit the file of the fallback language, so /cn/get-started exists even when only get-started.mdx does. Inherited pages are served but not advertised: they get noindex with a canonical link to the source page, and the sitemap, RSS feed, llms-full.txt and Takumi images skip them.

Routing

Every language is served under its own URL prefix, and page.url includes it. To drop the prefix of the default language, set hideLocale in your i18n config:

press.config.tsx
const i18n = defineI18n({
  languages: ["cn", "en"],
  defaultLanguage: "en",
  hideLocale: "default-locale",
});
SourcehideLocale: "never" (default)hideLocale: "default-locale"
content/docs/index.mdx/en, /cn/, /cn
content/docs/setup.mdx/en/setup, /cn/setup/setup, /cn/setup
src/pages/about.tsx/en/about, /cn/about/about, /cn/about
Blog index (plugin routes)/en/blog, /cn/blog/blog, /cn/blog
Not found page/404, /en/404, /cn/404/404, /cn/404
/redirects to /enthe index page of en

File-based Pages

Pages and layouts in src/pages are registered once per language, under its prefix and with a lang prop:

src/pages/about.tsx
export default function Page({ lang }: { lang: string }) {
  return <h1>{lang === "cn" ? "关于" : "About"}</h1>;
}

With autoI18n: false, a page is registered once at its own path (e.g. /privacy) and rendered as the default language, root layout included. See Routing for route config.

In Plugins and Layouts

Build language-aware links with localizePath() from the app context, it adds the prefix unless hideLocale hides it:

const ctx = getPressContext();

ctx.localizePath("cn", "/blog"); // "/cn/blog"
ctx.localizePath("en", "/blog"); // "/blog" with `hideLocale: "default-locale"`, otherwise "/en/blog"

A plugin that creates its own pages registers one per language with createPage(): path is prefixed for lang, the page gets it as a prop, and the pages sharing a path link each other as translations:

press.config.tsx
import type { PressPlugin } from "fumapress";

const plugin: PressPlugin = {
  name: "changelog",
  createPages({ createPage }) {
    for (const lang of this.i18nConfig?.languages ?? [undefined]) {
      createPage({ path: "/changelog", lang, component: ChangelogPage });
    }
  },
};

Without lang, the page is served without prefix as the default language.

Last updated on

On this page