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自然運作 - 每區域發布 — 發布英語版本同時保持法語為草稿
- 每區域修訂 — 每個翻譯都有自己的修訂歷史
- 單區域查詢 — 列表查詢僅回傳一個區域設定的項目
Slug、項目 ID 和資料庫 ID
一個項目有兩個不同用途的識別碼:
entry.id是項目的 slug。在建構公開 URL 時使用。entry.data.id是資料庫 ID。用於 API 操作和參考儲存內容行的輔助函式,包括getTranslations()和getEntryTerms()。
翻譯具有不同的資料庫 ID,因為每個區域設定是單獨的行。它們共享的 translation_group 記錄了這些行是同一內容的翻譯。EmDash 在你建立翻譯時管理該群組;範本通常只需要群組中任何行的資料庫 ID。
查詢翻譯內容
單一項目
在多語言路由上將 Astro.currentLocale 傳遞給 getEmDashEntry。Astro 知道其路由器選擇的區域設定,而 EmDash 需要明確值來消除可能存在於多個區域設定中的 slug 的歧義。對集合查詢也要這樣做。
---
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>
回退鏈
當請求的區域設定中不存在匹配的已發布項目時,getEmDashEntry 遵循 Astro 設定中的回退鏈。在預覽或視覺化編輯模式下,同一查找可以回傳草稿。給定 fallback: { fr: "en" }:
- 嘗試請求的區域設定(
fr) - 嘗試回退區域設定(
en) - 如果尚未在鏈中,嘗試預設區域設定
回退僅適用於單項目查詢。列表查詢僅回傳請求區域設定的項目。
每次回退查找使用相同的 id 參數。例如,對 slug about 的請求可以從法語回退到 slug 也是 about 的英語項目。對 a-propos 的請求無法發現 slug 為 about 的英語項目;這兩行使用不同的公開識別碼。使用 getTranslations() 來找到和連結具有不同 slug 的區域變體。
選單
選單是每區域的 — 相同的 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>
從管理後台的選單列表建立現有選單的翻譯 — 項目會保持 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.data.id, undefined, {
locale: Astro.currentLocale,
});
---
翻譯內容會自動繼承來源的術語指派 — 你只需翻譯術語本身一次,使用它們的每篇文章都會在讀取時解析到正確的區域設定。
修復分類法區域設定不匹配
當管理後台載入其網站清單時,如果分類法定義或術語使用了不在網站設定的 i18n.locales 中的區域設定,EmDash 會在伺服器日誌中發出警告。沒有 i18n 設定時,en 是有效區域設定。這些行不會被更改,因為 EmDash 無法推斷現有內容打算使用哪個設定的區域設定。
備份資料庫,然後檢查警告中提到的受影響行:
SELECT id, name, locale FROM _emdash_taxonomy_defs ORDER BY name, locale;
SELECT id, name, slug, locale FROM taxonomies ORDER BY name, slug, locale;
確認每行的預期區域設定後,按 id 更新:
UPDATE _emdash_taxonomy_defs SET locale = 'ja' WHERE id = '<definition-id>';
UPDATE taxonomies SET locale = 'ja' WHERE id = '<term-id>';
使用 i18n.locales 中的確切大小寫。更新前,檢查是否存在具有相同分類法名稱和目標區域設定,或相同術語名稱、slug 和目標區域設定的行。這些組合是唯一的;如果目標行已存在,請調解翻譯而不是套用批次區域設定更新。重新啟動 EmDash 並確認警告不再出現。
集合列表
按區域設定篩選集合:
---
import { getEmDashCollection } from "emdash";
const { entries: posts } = await getEmDashCollection("posts", {
locale: Astro.currentLocale,
status: "published",
});
---
<ul>
{posts.map((post) => (
<li><a href={`/${post.id}`}>{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);
const publishedTranslations = translations.filter(
(translation): translation is typeof translation & { slug: string } =>
translation.status === "published" && translation.slug !== null
);
---
<nav aria-label="Language">
<ul>
{publishedTranslations.map((translation) => (
<li>
<a
href={getRelativeLocaleUrl(translation.locale, `/blog/${translation.slug}`)}
aria-current={translation.locale === Astro.currentLocale ? "page" : undefined}
>
{translation.locale.toUpperCase()}
</a>
</li>
))}
</ul>
</nav>
getTranslations 函式回傳同一翻譯群組中的所有區域變體:
const { translationGroup, translations } = await getTranslations("posts", post.data.id);
// translations: [
// { locale: "en", id: "01ABC...", slug: "my-post", status: "published" },
// { locale: "fr", id: "01DEF...", slug: "mon-article", status: "draft" },
// ]
在管理後台管理翻譯
內容列表
當 i18n 啟用時,內容列表顯示:
- 顯示每個項目區域設定的區域設定欄
- 工具列中用於在區域設定之間切換的區域設定篩選器
建立翻譯
在編輯器中開啟任何內容項目。側邊欄顯示一個翻譯面板,列出所有設定的區域設定。對於每個區域設定:
- “Translate” 出現在沒有翻譯的區域設定 — 點擊建立一個
- “Edit” 出現在有現有翻譯的區域設定 — 點擊導覽到它
- 目前區域設定用勾號標記
建立翻譯時,新項目用來源區域設定的資料預先填入,並指派 {來源slug}-{區域設定} 的預設 slug。根據需要調整 slug 和內容,然後儲存。
每區域發布
每個翻譯都有自己的狀態。獨立地發布、取消發布或排程翻譯。法語版本可以是草稿,而英語版本是上線的。
使用內容 API
locale 參數
內容 API 路由需要認證工作階段或 bearer 權杖。列表路由接受選擇性的 locale 查詢參數。當路徑使用 slug 時,單項目路由也接受它;資料庫 ID 是全域唯一的,不需要區域設定消歧。
GET /_emdash/api/content/posts?locale=fr
GET /_emdash/api/content/posts/my-post?locale=fr
當列表請求省略 locale 時,使用設定的預設區域設定。
透過 API 建立翻譯
透過向內容建立端點傳遞 locale 和 translationOf 來建立翻譯:
POST /_emdash/api/content/posts
Content-Type: application/json
X-EmDash-Request: 1
{
"locale": "fr",
"translationOf": "01ABC...",
"slug": "mon-article",
"data": {
"title": "Mon Article"
}
}
translationOf 是來源行的資料庫 ID,如 entry.data.id。新項目共享來源項目的 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... \
--slug mon-article \
--data '{"title":"Mon article"}' \
--draft
content create 需要來自 --data、--file 或 --stdin 的輸入。除非傳遞 --draft,否則建立後發布。
播種多語言內容
種子檔案使用 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 儲存區域設定;Astro 處理公開路由。支援的 EmDash 設定讓預設區域設定不帶前綴:
# prefix-other-locales(Astro 預設)
/blog/my-post → en(預設區域設定,無前綴)
/fr/blog/mon-article → fr
使用 astro:i18n 中的 getRelativeLocaleUrl 新增正確的前綴和任何自訂區域設定路徑對應。不要啟用預設區域設定前綴;如設定區域設定中所述,該路由策略會阻止注入的管理頁面載入。
網站地圖
/sitemap-{collection}.xml 的每集合網站地圖是區域設定感知的。它包括來自可路由、啟用 SEO 的集合的已發布項目。已刪除的項目、沒有 slug 的項目和標記為 noindex 的項目被排除。每個包含的翻譯成為自己的 <url> 項目。EmDash 從集合的 urlPattern 建構路徑,然後套用 Astro 的區域設定前綴和任何自訂區域設定 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 分組,因此已發布的區域變體作為每個其他已發布、可索引變體的替代出現。i18n.locales 中缺少的區域設定被省略,因為 Astro 沒有為它們提供路由。單區域設定網站產生沒有 xhtml 命名空間的普通網站地圖。
向頁面標頭新增 hreflang 連結
相同的替代屬於每個內容頁面的 <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" />
對於手工編寫的標頭,使用 getHreflangAlternates 解析替代:
---
import { getEmDashEntry, getHreflangAlternates } from "emdash";
const { entry, error } = await getEmDashEntry("posts", Astro.params.slug, {
locale: Astro.currentLocale,
});
if (error) return new Response("Server error", { status: 500 });
if (!entry) return Astro.redirect("/404");
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。- 未發布的兄弟被排除 — 草稿翻譯永遠不會洩漏到替代中。
noindex兄弟被排除。 如果目前項目是noindex,不回傳替代。- 不可路由的區域設定被刪除。 區域設定不在設定的
i18n.locales中的行無法提供服務,將搜尋引擎連結到 404 比沒有連結更糟。 - 未翻譯的項目在 i18n 啟用時仍然取得自我參考替代和
x-default,反映網站地圖。 - 當 i18n 停用時,結果為空且不執行查詢。
URL 從集合的 urlPattern 建構,並透過 Astro i18n 設定進行本地化。getHreflangAlternates() 需要絕對網站 URL。它使用呼叫中的 siteUrl 或網站設定 URL;沒有任何一個時回傳空陣列,因為 hreflang 連結必須是絕對的。
匯入多語言內容
透過管理遷移工具匯入 WordPress 內容 — 參見內容匯入和從 WordPress 遷移。WXR 匯出不包含 WPML 或 Polylang 新增的區域設定和翻譯群組結構,因此匯入的內容會落在你的預設區域設定中。
要從匯入的內容建構翻譯,將翻譯後的項目建立為草稿並連結到原始資料庫 ID:
emdash content create posts \
--locale fr \
--translation-of 01ABC... \
--slug mon-article \
--data '{"title":"Mon article"}' \
--draft
這與種子檔案使用的 --locale 和 --translation-of 關係相同,在匯入完成後套用。
下一步
- 查詢內容 — 完整查詢 API 參考
- 使用內容 — 管理內容管理
- Astro i18n 路由 — Astro 的路由設定