EmDash se integra con el enrutado i18n integrado de Astro para gestionar contenido multilingüe. Astro gestiona las URLs y la detección de idioma; EmDash almacena y recupera las traducciones.
Cada traducción es una entrada de contenido completa e independiente, con su propio slug, estado e historial de revisiones. La versión en francés de una entrada puede estar en borrador mientras la versión en inglés está publicada.
Configuración
Activa i18n añadiendo un bloque i18n en la configuración de Astro. EmDash lee esta configuración automáticamente; no hay configuración de idiomas separada en 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",
}),
}),
],
});
Si no hay i18n en la configuración de Astro, todas las funciones i18n están desactivadas y EmDash se comporta como un CMS monolingüe.
Cómo funcionan las traducciones
EmDash usa un modelo de una fila por idioma (locale). Cada traducción es su propia fila en la base de datos con su propio ID, slug y estado, enlazada a otras traducciones mediante un translation_group compartido.
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
Esto implica:
- Slugs por idioma —
/blog/my-posty/fr/blog/mon-articleencajan de forma natural - Publicación por idioma — publicar en inglés y dejar el francés en borrador
- Revisiones por idioma — cada traducción tiene su propio historial
- Sin mezclar listas entre idiomas — las consultas de lista devuelven entradas solo para un locale
Consultar contenido traducido
Entrada única
Pasa locale a getEmDashEntry para obtener una traducción concreta. Si se omite, se usa el locale actual de la petición (definido por el middleware i18n de Astro).
---
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>
Cadena de reserva (fallback)
Si no hay contenido para el locale solicitado, EmDash sigue la cadena definida en la configuración de Astro. Con fallback: { fr: "en" }:
- Intenta el locale solicitado (
fr) - Intenta el locale de reserva (
en) - Intenta el locale por defecto
El fallback solo aplica a consultas de entrada única. Las listas devuelven entradas solo para el locale solicitado, sin mezclar.
Listado de colección
Filtra una colección por locale:
---
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>
Selector de idioma
Usa getTranslations para construir un selector que enlace a las traducciones existentes de la entrada actual:
---
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 devuelve todas las variantes de idioma del mismo grupo de traducción:
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" },
// ]
Gestionar traducciones en el admin
Lista de contenido
Con i18n activado, la lista muestra:
- Una columna de idioma con el locale de cada entrada
- Un filtro de idioma en la barra de herramientas
Crear traducciones
Abre cualquier entrada en el editor. En la barra lateral aparece el panel Translations. Para cada locale configurado:
- «Translate» si aún no hay traducción: al hacer clic se crea
- «Edit» si ya existe: al hacer clic navegas a esa traducción
- El locale actual aparece marcado con una marca de verificación
Al crear una traducción, la nueva entrada se rellena con datos del locale de origen y recibe un slug por defecto {slug-origen}-{locale}. Ajusta slug y contenido y guarda.
Publicación por idioma
Cada traducción tiene su propio estado. Publica, retira o programa de forma independiente. El francés puede estar en borrador mientras el inglés está en producción.
API de contenido
Parámetro locale
Todas las rutas de la API de contenido aceptan el parámetro de consulta opcional locale:
GET /_emdash/api/content/posts?locale=fr
GET /_emdash/api/content/posts/my-post?locale=fr
Si se omite, se usa el locale por defecto configurado.
Crear traducciones por API
Crea una traducción pasando locale y translationOf al endpoint de creación:
POST /_emdash/api/content/posts
Content-Type: application/json
{
"locale": "fr",
"translationOf": "01ABC...",
"data": {
"title": "Mon Article",
"slug": "mon-article"
}
}
La nueva entrada comparte el translation_group de la fuente y comienza como borrador.
Listar traducciones
Recupera todas las traducciones de una entrada:
GET /_emdash/api/content/posts/01ABC.../translations
Devuelve el ID del grupo de traducción y un array de variantes con sus IDs, slugs y estados.
CLI
La CLI admite la opción --locale en los comandos de contenido:
# 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...
Sembrar contenido multilingüe
Los archivos de seed expresan traducciones con locale y 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" }
}
]
}
}
La entrada del locale de origen debe aparecer antes que sus traducciones en el seed para que translationOf se resuelva correctamente.
Campos traducibles
Cada campo tiene la opción translatable (por defecto true). Al crear una traducción:
- Los campos traducibles se rellenan desde el locale de origen para editar
- Los no traducibles se copian y se mantienen sincronizados en todas las traducciones del grupo
Los campos del sistema como status, published_at y author_id son siempre por idioma y no se sincronizan.
Estrategia de URL
EmDash no gestiona las URLs por idioma; Astro gestiona el enrutado. Patrones habituales:
# 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
Usa getRelativeLocaleUrl de astro:i18n para construir URLs correctas en cualquier modo de enrutado.
Importar contenido multilingüe
WordPress con WPML o Polylang
El origen de importación de WordPress detecta WPML y Polylang automáticamente. Si se detectan, el contenido importado incluye metadatos de locale y grupo de traducción.
Archivos WXR
Las exportaciones WXR no incluyen metadatos de WPML/Polylang. Importa como un solo locale y crea traducciones a mano, o usa --locale para asignar un locale a todos los elementos importados:
# 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
Próximos pasos
- Querying Content — Referencia completa de consultas
- Working with Content — Gestión de contenido en el admin
- Astro i18n routing — Configuración de enrutado de Astro