国際化(i18n)

このページ

EmDash は Astro 組み込みの i18n ルーティング と連携し、多言語コンテンツ管理を提供します。URL ルーティングとロケール検出は Astro が行い、翻訳コンテンツの保存と取得は EmDash が行います。

各翻訳は完全に独立したコンテンツエントリで、独自の slug・ステータス・改訂履歴を持ちます。例として、英語版が公開中でもフランス語版は下書きのままにできます。

設定

Astro の設定に i18n ブロックを追加すると 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 は ロケールごとに 1 行 のモデルを採用しています。各翻訳はデータベース上で独自の 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 が自然に共存
  • ロケール別公開 — 英語を公開しつつフランス語を下書きにできる
  • ロケール別改訂 — 翻訳ごとに改訂履歴を保持
  • 一覧は言語をまたがない — リストクエリは 1 ロケール分のみ返す

翻訳済みコンテンツの取得

単一エントリ

getEmDashEntrylocale を渡すと特定の翻訳を取得できます。省略時はリクエストの現在ロケール(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 が有効な場合、一覧には次が表示されます。

  • 各エントリのロケールを示す ロケール列
  • ツールバーの ロケールフィルター

翻訳の作成

エディターで任意のエントリを開くと、サイドバーに 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

コンテンツ系コマンドでは --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" }
      }
    ]
  }
}

ソースロケールのエントリは、シードファイル内でその翻訳より前に置く必要があります。

フィールドの翻訳可否

各フィールドには 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 を自動検出します。検出されると、インポートデータにロケールと翻訳グループのメタデータが含まれます。

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

次のステップ