国际化(i18n)

本页内容

EmDash 与 Astro 内置的 i18n 路由 集成,用于多语言内容管理。Astro 负责 URL 路由与语言检测;EmDash 负责翻译内容的存储与读取。

每条翻译都是完整、独立的内容条目,拥有各自的 slug、状态与修订历史。例如法文版可为草稿,英文版已发布。

配置

在 Astro 配置中加入 i18n 块即可启用国际化。EmDash 会自动读取该配置——无需在 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 与其他翻译关联。

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 可自然共存
  • 按语言发布 — 可单独发布英文,法文保持草稿
  • 按语言修订 — 每种翻译有各自的修订历史
  • 列表查询不混语言 — 列表只返回某一语言的条目

查询已翻译内容

单条内容

getEmDashEntry 传入 locale 可获取指定翻译。若省略,则使用当前请求的语言(由 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. 尝试默认语言

回退仅适用于单条查询。列表查询只返回请求语言的条目,不会跨语言混合。

集合列表

按语言筛选集合:

---
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="Language">
  <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 后,列表会显示:

  • 语言列,展示每条条目的 locale
  • 工具栏中的 语言筛选器

创建翻译

在编辑器中打开任意内容条目。侧栏会显示 Translations 面板。对每个已配置的语言:

  • 尚无翻译时显示 「Translate」 — 点击即可创建
  • 已有翻译时显示 「Edit」 — 点击跳转至该翻译
  • 当前语言以勾选标记

创建翻译时,新条目会用源语言数据预填,默认 slug 为 {源 slug}-{locale}。按需调整 slug 与正文后保存。

按语言发布

每种翻译有独立状态。可分别发布、下线或定时。法文可为草稿,英文可已上线。

内容 API

locale 参数

所有内容 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

# List French posts
emdash content list posts --locale fr

# Get a specific entry in French
emdash content get posts my-post --locale fr

# Create a French translation of an existing entry
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 default)
/blog/my-post          → en (default locale, no prefix)
/fr/blog/mon-article   → fr

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

使用 astro:i18ngetRelativeLocaleUrl 可在任意路由模式下生成正确 URL。

导入多语言内容

使用 WPML 或 Polylang 的 WordPress

WordPress 导入源会自动检测 WPML 与 Polylang。检测到时,导入内容会包含 locale 与翻译组元数据。

WXR 文件

WXR 导出不含 WPML/Polylang 元数据。可先按单一语言导入再手动补翻译,或使用 --locale 为所有导入项指定语言:

# Import a French WXR export
emdash import wordpress export-fr.xml --execute --locale fr

# Match against existing English content by slug
emdash import wordpress export-fr.xml --execute --locale fr --translation-of-locale en

下一步