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" }:
- 嘗試請求的語言環境(
fr) - 嘗試回退語言環境(
en) - 嘗試預設語言環境
回退僅適用於單條目查詢。列表查詢僅返回請求語言環境的條目。
選單
選單是按語言環境的——相同的 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 建立翻譯
透過將 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 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 工作流程相同,在匯入完成後套用。
下一步
- 查詢內容 — 完整的查詢 API 參考
- 使用內容 — 管理介面內容管理
- Astro i18n 路由 — Astro 的路由設定