EmDashはAstroの組み込みi18nルーティングと統合して、多言語コンテンツ管理を提供します。AstroがURLルーティングとロケール検出を処理し、EmDashが翻訳されたコンテンツの保存と取得を処理します。
各翻訳は、独自のスラッグ、ステータス、リビジョン履歴を持つ完全で独立したコンテンツエントリです。投稿のフランス語版は下書きの状態で、英語版は公開されている可能性があります。
設定
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、スラッグ、ステータスを持つデータベース内の独自の行であり、共有のtranslation_group識別子を介して他の翻訳とリンクされています。3つの翻訳を持つ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
この設計は次のことを意味します:
- ロケールごとのスラッグ —
/blog/my-postと/fr/blog/mon-articleが自然に機能します - ロケールごとの公開 — フランス語を下書きのままにして英語版を公開します
- ロケールごとのリビジョン — 各翻訳には独自のリビジョン履歴があります
- 単一ロケールクエリ — リストクエリは1つのロケールのエントリのみを返します
翻訳されたコンテンツのクエリ
単一エントリ
getEmDashEntryにlocaleを渡して、特定の翻訳を取得します。省略した場合、リクエストの現在のロケール(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="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,
});
---
コンテンツを翻訳すると、ソースのターム割り当てが自動的に継承されます — ターム自体を一度翻訳するだけで、それらを使用するすべての投稿が読み取り時に正しいロケールに解決されます。
コレクションリスト
ロケールでコレクションをフィルタリングします:
---
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が有効になっている場合、コンテンツリストには次のものが表示されます:
- 各エントリのロケールを表示するロケール列
- ロケール間を切り替えるツールバーのロケールフィルター
翻訳の作成
エディタで任意のコンテンツエントリを開きます。サイドバーには、設定されたすべてのロケールをリストした翻訳パネルが表示されます。各ロケールについて:
- 「翻訳」 — 翻訳のないロケールに表示されます — クリックして作成します
- 「編集」 — 既存の翻訳があるロケールに表示されます — クリックして移動します
- 現在のロケールはチェックマークでマークされています
翻訳を作成すると、新しいエントリはソースロケールのデータで事前入力され、{ソーススラッグ}-{ロケール}のデフォルトスラッグが割り当てられます。必要に応じてスラッグとコンテンツを調整して保存します。
ロケールごとの公開
各翻訳には独自のステータスがあります。翻訳を独立して公開、非公開、またはスケジュールします。フランス語版は下書きで、英語版はライブにできます。
コンテンツAPI
localeパラメータ
すべてのコンテンツ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、スラッグ、ステータスを持つロケールバリアントの配列を返します。
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
ルーティングモードに関係なく正しいURLを構築するには、astro:i18nのgetRelativeLocaleUrlを使用します。
多言語コンテンツのインポート
管理移行ツールを介してWordPressコンテンツをインポートします — コンテンツインポートとWordPressからの移行を参照してください。WXRエクスポートには、WPMLまたはPolylangが追加するロケールと翻訳グループ構造が含まれていないため、インポートされたコンテンツはデフォルトロケールに到達します。
インポートされたコンテンツから翻訳を構築するには、翻訳されたエントリを作成し、元のエントリにリンクします:
emdash content create posts --locale fr --translation-of 01ABC...
これは、上記の多言語コンテンツのシードで示されている--locale / --translation-ofワークフローと同じで、インポートが完了した後に適用されます。
次のステップ
- コンテンツのクエリ — 完全なクエリAPIリファレンス
- コンテンツの操作 — 管理コンテンツ管理
- Astro i18nルーティング — Astroのルーティング設定