国际化 (i18n)

本页内容

EmDash 与 Astro 内置的 i18n 路由集成,提供多语言内容管理。Astro 处理 URL 路由和语言环境检测;EmDash 处理翻译内容的存储和检索。

每个翻译都是一个完整、独立的内容条目,拥有自己的 slug、状态和修订历史。文章的法语版本可以是草稿,而英语版本已发布。

配置

在 Astro 配置中添加 i18n 块来启用 i18n。EmDash 读取相同的配置来获取其语言环境列表、默认语言环境和回退链。

import { defineConfig } from "astro/config";
import emdash, { local } from "emdash/astro";
import { sqlite } from "emdash/db";

export default defineConfig({
	i18n: {
		defaultLocale: "en",
		locales: ["en", "fr", "es"],
		fallback: { fr: "en", es: "en" },
	},
	integrations: [
		emdash({
			database: sqlite({ url: "file:./data.db" }),
			storage: local({
				directory: "./uploads",
				baseUrl: "/_emdash/api/media/file",
			}),
		}),
	],
});

当 Astro 配置中没有 i18n 时,所有 i18n 功能将被禁用,EmDash 将作为单语言 CMS 运行。

翻译如何工作

EmDash 使用每语言环境一行的模型。每个翻译在数据库中都是自己的一行,有自己的 ID、slug 和状态,通过共享的 translation_group 标识符链接到其他翻译。一个包含三个翻译的 posts 表如下所示:

ec_posts:
id       | slug        | locale | translation_group | status
---------|-------------|--------|-------------------|----------
01ABC... | my-post     | en     | 01ABC...          | published
01DEF... | mon-article | fr     | 01ABC...          | draft
01GHI... | mi-entrada  | es     | 01ABC...          | published

这种设计意味着:

  • 每语言环境的 slug/blog/my-post/fr/blog/mon-article 自然运作
  • 每语言环境的发布 — 在法语保持草稿状态时发布英语版本
  • 每语言环境的修订 — 每个翻译都有自己的修订历史
  • 单语言环境查询 — 列表查询只返回一个语言环境的条目

查询翻译内容

单个条目

locale 传递给 getEmDashEntry 以检索特定翻译。省略时,默认为请求的当前语言环境(由 Astro 的 i18n 中间件设置)。

---
import { getEmDashEntry } from "emdash";

const { slug } = Astro.params;
const { entry: post, error } = await getEmDashEntry("posts", slug, {
  locale: Astro.currentLocale,
});

if (!post) return Astro.redirect("/404");
---

<article>
  <h1>{post.data.title}</h1>
</article>

回退链

当请求的语言环境不存在内容时,EmDash 遵循你在 Astro 配置中定义的回退链。给定 fallback: { fr: "en" }

  1. 尝试请求的语言环境(fr
  2. 尝试回退语言环境(en
  3. 尝试默认语言环境

回退仅适用于单条目查询。列表查询仅返回请求语言环境的条目。

菜单

菜单是按语言环境的——相同的 name(例如 "primary")可以存在于多个语言环境中,都通过共享的 translation_group 链接。菜单项根据活动语言环境的引用内容版本解析其内容引用。

以下组件获取活动语言环境的主菜单:

---
import { getMenu } from "emdash";

const menu = await getMenu("primary", { locale: Astro.currentLocale });
---

<nav aria-label="主要">
  <ul>
    {menu?.items.map((item) => (
      <li><a href={item.url}>{item.label}</a></li>
    ))}
  </ul>
</nav>

从管理界面的菜单列表创建现有菜单的翻译——项目会带着完整的 reference_id 克隆(它存储引用内容的 translation_group),因此新菜单的链接会自动指向正确的按语言环境的内容。

分类法(分类、标签)

术语是按语言环境的。定义(_emdash_taxonomy_defs)也是按语言环境的,因此 label / labelSingular 也可以翻译。枢纽 content_taxonomies.taxonomy_id 存储术语的 translation_group,因此单个分配跨越内容的所有语言环境。

以下示例获取活动语言环境的分类和文章术语:

---
import { getTaxonomyTerms, getEntryTerms } from "emdash";

const categories = await getTaxonomyTerms("category", {
  locale: Astro.currentLocale,
});
const terms = await getEntryTerms("posts", post.id, undefined, {
  locale: Astro.currentLocale,
});
---

翻译内容会自动继承源的术语分配——你只需要翻译术语本身一次,使用它们的每篇文章在读取时都会解析到正确的语言环境。

集合列表

按语言环境过滤集合:

---
import { getEmDashCollection } from "emdash";

const { entries: posts } = await getEmDashCollection("posts", {
  locale: Astro.currentLocale,
  status: "published",
});
---

<ul>
  {posts.map((post) => (
    <li><a href={`/${post.data.slug}`}>{post.data.title}</a></li>
  ))}
</ul>

语言切换器

使用 getTranslations 构建一个链接到当前条目现有翻译的语言切换器:

---
import { getTranslations } from "emdash";
import { getRelativeLocaleUrl } from "astro:i18n";

interface Props {
  collection: string;
  entryId: string;
}

const { collection, entryId } = Astro.props;
const { translations } = await getTranslations(collection, entryId);
---

<nav aria-label="语言">
  <ul>
    {translations.map((t) => (
      <li>
        <a
          href={getRelativeLocaleUrl(t.locale, `/blog/${t.slug}`)}
          aria-current={t.locale === Astro.currentLocale ? "page" : undefined}
        >
          {t.locale.toUpperCase()}
        </a>
      </li>
    ))}
  </ul>
</nav>

getTranslations 函数返回同一翻译组中的所有语言环境变体:

const { translationGroup, translations } = await getTranslations("posts", post.entry.id);
// translations: [
//   { locale: "en", id: "01ABC...", slug: "my-post", status: "published" },
//   { locale: "fr", id: "01DEF...", slug: "mon-article", status: "draft" },
// ]

在管理界面中管理翻译

内容列表

当 i18n 启用时,内容列表显示:

  • 一个显示每个条目语言环境的语言环境列
  • 工具栏中用于在语言环境之间切换的语言环境过滤器

创建翻译

在编辑器中打开任何内容条目。侧边栏显示一个翻译面板,列出所有配置的语言环境。对于每个语言环境:

  • “翻译” 出现在没有翻译的语言环境——点击创建
  • “编辑” 出现在有现有翻译的语言环境——点击导航到它
  • 当前语言环境用勾号标记

创建翻译时,新条目会用源语言环境的数据预填充,并分配一个默认 slug {源-slug}-{语言环境}。根据需要调整 slug 和内容,然后保存。

按语言环境发布

每个翻译都有自己的状态。独立地发布、取消发布或安排翻译。法语版本可以是草稿,而英语版本是在线的。

内容 API

语言环境参数

所有内容 API 路由接受可选的 locale 查询参数:

GET /_emdash/api/content/posts?locale=fr
GET /_emdash/api/content/posts/my-post?locale=fr

省略时,默认为配置的默认语言环境。

通过 API 创建翻译

通过将 localetranslationOf 传递给内容创建端点来创建翻译:

POST /_emdash/api/content/posts
Content-Type: application/json

{
  "locale": "fr",
  "translationOf": "01ABC...",
  "data": {
    "title": "Mon Article",
    "slug": "mon-article"
  }
}

新条目共享源条目的 translation_group 并以草稿状态开始。

列出翻译

检索给定条目的所有翻译:

GET /_emdash/api/content/posts/01ABC.../translations

返回翻译组 ID 和包含 ID、slug 和状态的语言环境变体数组。

CLI

CLI 在内容命令中支持 --locale 标志:

# 列出法语文章
emdash content list posts --locale fr

# 获取法语版本的特定条目
emdash content get posts my-post --locale fr

# 创建现有条目的法语翻译
emdash content create posts --locale fr --translation-of 01ABC...

种子多语言内容

种子文件使用 localetranslationOf 来表示翻译:

{
  "content": {
    "posts": [
      {
        "id": "welcome",
        "slug": "welcome",
        "locale": "en",
        "status": "published",
        "data": { "title": "Welcome" }
      },
      {
        "id": "welcome-fr",
        "slug": "bienvenue",
        "locale": "fr",
        "translationOf": "welcome",
        "status": "draft",
        "data": { "title": "Bienvenue" }
      }
    ]
  }
}

源语言环境条目必须在种子文件中出现在其翻译之前,以便 translationOf 引用能正确解析。

字段可翻译性

每个字段都有一个 translatable 设置(默认:true)。创建翻译时:

  • 可翻译字段 从源语言环境预填充以供编辑
  • 不可翻译字段 被复制并在组中的所有翻译之间保持同步

statuspublished_atauthor_id 等系统字段始终是按语言环境的,永远不会同步。

URL 策略

EmDash 不管理语言环境 URL——Astro 处理路由。常见模式:

# prefix-other-locales(Astro 默认)
/blog/my-post          → en(默认语言环境,无前缀)
/fr/blog/mon-article   → fr

# prefix-always
/en/blog/my-post       → en
/fr/blog/mon-article   → fr

使用 astro:i18ngetRelativeLocaleUrl 来构建正确的 URL,无论路由模式如何。

站点地图

/sitemap-{collection}.xml 的按集合站点地图是语言环境感知的。当 i18n 启用时,每个翻译作为自己的 <url> 条目输出,语言环境前缀通过 Astro 的 getRelativeLocaleUrl 解析。你的 prefixDefaultLocale 设置和任何自定义语言环境 path 映射会自动生效。

翻译兄弟通过 xhtml:link alternate 交叉链接,以便搜索引擎可以向每个用户提供正确的语言:

<url>
  <loc>https://example.com/blog/hello</loc>
  <lastmod>2026-05-28T16:33:15.461Z</lastmod>
  <xhtml:link rel="alternate" hreflang="en" href="https://example.com/blog/hello" />
  <xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr/blog/bonjour" />
  <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/blog/hello" />
</url>

兄弟按 translation_group 分组,因此稍后添加的行(现有文章的新语言环境变体)会自动作为 alternate 出现在其他每个变体上。单语言环境站点生成没有 xhtml 命名空间的普通站点地图。

页面 head 中的 hreflang alternate

相同的 alternate 属于每个内容页面的 <head>。如果你的布局使用 <EmDashHead>,这是自动的:当 i18n 启用且页面上下文包含 content 时,它会为每个已发布的翻译兄弟输出一个 <link rel="alternate">——包括 Google 推荐的自引用链接——外加 x-default

<link rel="alternate" hreflang="en" href="https://example.com/blog/hello" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr/blog/bonjour" />
<link rel="alternate" hreflang="x-default" href="https://example.com/blog/hello" />

对于手工编写的 head,使用 getHreflangAlternates 解析 alternate:

---
import { getEmDashEntry, getHreflangAlternates } from "emdash";

const { entry } = await getEmDashEntry("posts", Astro.params.slug);
const alternates = await getHreflangAlternates("posts", entry.data.id, {
	siteUrl: Astro.url.origin,
});
---

<head>
	{alternates.map((a) => <link rel="alternate" hreflang={a.hreflang} href={a.href} />)}
</head>

行为与站点地图完全匹配:

  • x-default 指向默认语言环境的变体。当默认语言环境没有已发布的翻译时,它回退到第一个可路由的变体,因此集合永远不会缺少 x-default
  • 未发布的兄弟被排除 — 草稿翻译永远不会泄漏到 alternate 中。
  • 不可路由的语言环境被丢弃。 其语言环境不在你配置的 i18n.locales 中的行无法被提供服务,将搜索引擎链接到 404 比没有链接更糟。
  • 未翻译的条目 在 i18n 启用时仍会获得自引用 alternate 和 x-default,与站点地图镜像。
  • i18n 禁用时,结果为空且不执行任何查询。

URL 从集合的 urlPattern 构建,通过你的 Astro i18n 路由配置(prefixDefaultLocale、自定义语言环境 path 映射)进行本地化,因此 head 和站点地图始终一致。

导入多语言内容

通过管理界面的迁移工具导入 WordPress 内容——参见内容导入从 WordPress 迁移。WXR 导出不包含 WPML 或 Polylang 添加的语言环境和翻译组结构,因此导入的内容会落在你的默认语言环境中。

要从导入的内容构建翻译,创建翻译条目并将其链接到原始条目:

emdash content create posts --locale fr --translation-of 01ABC...

这与上面种子多语言内容中展示的 --locale / --translation-of 工作流相同,在导入完成后应用。

下一步