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自然可用 - 按语言区域发布 — 发布英语版同时保持法语版为草稿
- 按语言区域修订 — 每个翻译拥有独立的修订历史
- 单语言区域查询 — 列表查询仅返回一种语言区域的条目
查询翻译内容
单个条目
向 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" }:
- 尝试请求的语言区域(
fr) - 尝试回退语言区域(
en) - 尝试默认语言区域
回退仅适用于单条目查询。列表查询仅返回请求语言区域的条目。
菜单
菜单按语言区域划分 — 相同的 name(如 "primary")可在多个语言区域存在,均通过共享的 translation_group 关联。菜单项根据当前语言区域中引用内容的版本解析内容引用。
以下组件获取当前语言区域的主菜单:
---
import { getMenu } from "emdash";
const menu = await getMenu("primary", { locale: Astro.currentLocale });
---
<nav aria-label="Primary">
<ul>
{menu?.items.map((item) => (
<li><a href={item.url}>{item.label}</a></li>
))}
</ul>
</nav>
从管理后台的 Menus 列表创建现有菜单的翻译 — 项目会被克隆且 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="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 后,内容列表显示:
- 语言区域列,显示每个条目的语言区域
- 工具栏中的语言区域筛选器,用于切换语言区域
创建翻译
在编辑器中打开任意内容条目。侧边栏显示 Translations 面板,列出所有配置的语言区域。对于每种语言区域:
- 没有翻译的语言区域显示 “Translate” — 点击创建
- 已有翻译的语言区域显示 “Edit” — 点击导航
- 当前语言区域以勾选标记
创建翻译时,新条目会预填源语言区域的数据,并分配默认 slug {source-slug}-{locale}。根据需要调整 slug 和内容,然后保存。
按语言区域发布
每个翻译拥有独立状态。独立发布、取消发布或计划翻译。法语版可以是草稿,而英语版已上线。
内容 API
locale 参数
所有内容 API 路由接受可选的 locale 查询参数:
GET /_emdash/api/content/posts?locale=fr
GET /_emdash/api/content/posts/my-post?locale=fr
省略时,默认为配置的默认语言区域。
通过 API 创建翻译
向内容创建端点传递 locale 和 translationOf 以创建翻译:
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...
播种多语言内容
种子文件使用 locale 和 translationOf 表达翻译:
{
"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)。创建翻译时:
- 可翻译字段从源语言区域预填以供编辑
- 不可翻译字段被复制并在组内所有翻译中保持同步
status、published_at 和 author_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:i18n 的 getRelativeLocaleUrl 构建正确的 URL,无论路由模式如何。
站点地图
/sitemap-{collection}.xml 的按集合站点地图支持语言区域感知。启用 i18n 时,每个翻译作为独立的 <url> 条目输出,语言区域前缀通过 Astro 的 getRelativeLocaleUrl 解析。您的 prefixDefaultLocale 设置和任何自定义语言区域 path 映射会自动生效。
翻译兄弟条目通过 xhtml:link 替代链接相互关联,以便搜索引擎为每位用户提供正确的语言:
<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 分组,因此后添加的行(现有文章的新语言区域变体)会自动出现在每个其他变体的替代链接中。单语言区域站点生成不含 xhtml 命名空间的普通站点地图。
导入多语言内容
通过管理后台迁移工具导入 WordPress 内容 — 参见内容导入和从 WordPress 迁移。WXR 导出不包含 WPML 或 Polylang 添加的语言区域和翻译组结构,因此导入的内容会进入您的默认语言区域。
要从导入的内容构建翻译,创建翻译条目并将其链接到原始条目:
emdash content create posts --locale fr --translation-of 01ABC...
这与上文播种多语言内容中展示的相同 --locale / --translation-of 工作流相同,在导入完成后应用。
下一步
- 查询内容 — 完整查询 API 参考
- 使用内容 — 管理后台内容管理
- Astro i18n 路由 — Astro 路由配置