國際化 (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 識別碼與其他翻譯關聯。一個包含三個翻譯的文章表格如下所示:

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="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.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.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 後,內容列表顯示:

  • 顯示每個項目地區設定的地區設定欄
  • 工具列中用於在地區設定間切換的地區設定篩選器

建立翻譯

在編輯器中開啟任何內容項目。側邊欄顯示一個列出所有設定地區設定的翻譯面板。對於每個地區設定:

  • 沒有翻譯的地區設定顯示**「翻譯」**——點擊建立
  • 有現有翻譯的地區設定顯示**「編輯」**——點擊導覽到它
  • 目前地區設定標記有勾選標記

建立翻譯時,新項目預先填入了來源地區設定的資料,並指派了 {source-slug}-{locale} 的預設 slug。根據需要調整 slug 和內容,然後儲存。

按地區設定發布

每個翻譯有自己的狀態。獨立發布、取消發布或排程翻譯。法語版本可以是草稿而英語版本已上線。

Content 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 替代項交叉連結,以便搜尋引擎可以向每個使用者提供正確的語言:

<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 命名空間的普通網站地圖。

頁面標頭中的 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" />

對於手寫的 head,使用 getHreflangAlternates 解析替代項:

---
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
  • 未發布的兄弟被排除——草稿翻譯永遠不會洩漏到替代項中。
  • 不可路由的地區設定被捨棄。 地區設定不在設定的 i18n.locales 中的列無法被服務,將搜尋引擎連結到 404 比沒有連結更糟糕。
  • 未翻譯的項目在 i18n 啟用時仍然取得自我參考替代項和 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 工作流程相同,在匯入完成後套用。

下一步