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 식별자를 통해 다른 번역과 연결됩니다. 세 가지 번역이 있는 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이 자연스럽게 작동 - 로케일별 게시 — 프랑스어를 초안으로 유지하면서 영어 버전 게시
- 로케일별 개정 — 각 번역에 고유한 개정 기록
- 단일 로케일 쿼리 — 목록 쿼리는 하나의 로케일에 대한 항목만 반환
번역된 콘텐츠 쿼리
단일 항목
특정 번역을 검색하려면 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>
관리자의 Menus 목록에서 기존 메뉴의 번역을 생성합니다 — 항목은 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이 활성화되면 콘텐츠 목록에 다음이 표시됩니다:
- 각 항목의 로케일을 표시하는 로케일 열
- 로케일 간 전환을 위한 도구 모음의 로케일 필터
번역 생성
편집기에서 콘텐츠 항목을 엽니다. 사이드바에 구성된 모든 로케일을 나열하는 Translations 패널이 표시됩니다. 각 로케일에 대해:
- 번역이 없는 로케일에 “Translate” 표시 — 클릭하여 생성
- 기존 번역이 있는 로케일에 “Edit” 표시 — 클릭하여 이동
- 현재 로케일은 체크 표시로 표시
번역을 생성하면 새 항목은 소스 로케일의 데이터로 미리 채워지고 기본 슬러그 {source-slug}-{locale}이 할당됩니다. 필요에 따라 슬러그와 콘텐츠를 조정한 후 저장합니다.
로케일별 게시
각 번역에는 고유한 상태가 있습니다. 번역을 독립적으로 게시, 게시 취소 또는 예약합니다. 프랑스어 버전은 초안 상태에서 영어 버전이 라이브일 수 있습니다.
콘텐츠 API
로케일 매개변수
모든 콘텐츠 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을 사용합니다.
사이트맵
/sitemap-{collection}.xml의 컬렉션별 사이트맵은 로케일을 인식합니다. i18n이 활성화되면 각 번역은 Astro의 getRelativeLocaleUrl을 통해 해결된 로케일 접두사와 함께 자체 <url> 항목으로 출력됩니다. 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 네임스페이스 없는 일반 사이트맵을 생성합니다.
다국어 콘텐츠 가져오기
관리자 마이그레이션 도구를 통해 WordPress 콘텐츠 가져오기 — Content Import 및 Migrate from WordPress 참조. WXR 내보내기에는 WPML 또는 Polylang이 추가하는 로케일 및 번역 그룹 구조가 포함되지 않으므로 가져온 콘텐츠는 기본 로케일에 배치됩니다.
가져온 콘텐츠에서 번역을 구축하려면 번역된 항목을 생성하고 원본에 연결합니다:
emdash content create posts --locale fr --translation-of 01ABC...
이는 위의 다국어 콘텐츠 시드에 표시된 동일한 --locale / --translation-of 워크플로우로, 가져오기 완료 후 적용됩니다.
다음 단계
- Querying Content — 전체 쿼리 API 참조
- Working with Content — 관리자 콘텐츠 관리
- Astro i18n routing — Astro 라우팅 구성