國際化(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 可自然並存
  • 依語系發佈 — 可單獨發佈英文,法文維持草稿
  • 依語系修訂 — 每種翻譯有各自的修訂歷史
  • 清單查詢不混語系 — 清單只回傳某一語系的條目

查詢已翻譯內容

單筆內容

傳入 localegetEmDashEntry 可取得指定翻譯。若省略,則使用目前請求的語系(由 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

下一步