국제화(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는 로케일당 한 행 모델을 씁니다. 각 번역은 DB에서 별도 행으로, 고유 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이 자연스럽게 공존
  • 언어별 게시 — 영어만 게시하고 프랑스어는 초안으로 둘 수 있음
  • 언어별 수정 이력 — 번역마다 별도 이력
  • 목록은 언어를 섞지 않음 — 목록 쿼리는 한 로케일의 항목만 반환

번역된 콘텐츠 조회

단일 항목

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로 현재 항목의 기존 번역으로 연결되는 언어 전환 UI를 만듭니다.

---
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

콘텐츠 관련 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) 번역을 만들 때:

  • 번역 가능 필드는 소스에서 미리 채워져 편집할 수 있음
  • 번역 불가 필드는 복사되어 그룹의 모든 번역에서 동기화됨

status, published_at, author_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

다음 단계