O EmDash integra-se com o encaminhamento i18n integrado do Astro para gestão de conteúdo multilingue. O Astro gere URLs e deteção de idioma; o EmDash armazena e obtém traduções.
Cada tradução é uma entrada de conteúdo completa e independente, com o seu próprio slug, estado e histórico de revisões. A versão em francês de um artigo pode estar em rascunho enquanto a versão em inglês está publicada.
Configuração
Ative o i18n adicionando um bloco i18n à configuração do Astro. O EmDash lê esta configuração automaticamente; não há definição de idioma separada no 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",
}),
}),
],
});
Se não existir i18n na configuração do Astro, todas as funções i18n ficam desativadas e o EmDash comporta-se como um CMS monolingue.
Como funcionam as traduções
O EmDash usa um modelo de uma linha por idioma (locale). Cada tradução é a sua própria linha na base de dados com o seu ID, slug e estado, ligada às outras através de um translation_group partilhado.
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
Isto implica:
- Slugs por idioma —
/blog/my-poste/fr/blog/mon-articleencaixam de forma natural - Publicação por idioma — publicar em inglês e manter o francês em rascunho
- Revisões por idioma — cada tradução tem o seu próprio histórico
- Sem misturar listas entre idiomas — as consultas de lista devolvem entradas só para um locale
Consultar conteúdo traduzido
Entrada única
Passe locale a getEmDashEntry para obter uma tradução específica. Se for omitido, usa-se o locale atual do pedido (definido pelo middleware i18n do 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>
Cadeia de recurso (fallback)
Se não existir conteúdo para o locale pedido, o EmDash segue a cadeia definida na configuração do Astro. Com fallback: { fr: "en" }:
- Tenta o locale pedido (
fr) - Tenta o locale de recurso (
en) - Tenta o locale predefinido
O fallback só se aplica a consultas de entrada única. As listas devolvem entradas só para o locale pedido, sem misturar.
Listagem de coleção
Filtre uma coleção 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>
Seletor de idioma
Use getTranslations para construir um seletor que ligue às traduções existentes da entrada atual:
---
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 devolve todas as variantes de idioma do mesmo grupo de tradução:
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" },
// ]
Gerir traduções na administração
Lista de conteúdo
Com o i18n ativo, a lista mostra:
- Uma coluna de idioma com o locale de cada entrada
- Um filtro de idioma na barra de ferramentas
Criar traduções
Abra qualquer entrada no editor. Na barra lateral aparece o painel Translations. Para cada locale configurado:
- «Translate» se ainda não existir tradução: ao clicar, cria-se
- «Edit» se já existir: ao clicar, navega para essa tradução
- O locale atual aparece com uma marca de verificação
Ao criar uma tradução, a nova entrada é preenchida com dados do locale de origem e recebe um slug predefinido {slug-origem}-{locale}. Ajuste o slug e o conteúdo e guarde.
Publicação por idioma
Cada tradução tem o seu próprio estado. Publique, retire ou agende de forma independente. O francês pode estar em rascunho enquanto o inglês está em produção.
API de conteúdo
Parâmetro locale
Todas as rotas da API de conteúdo aceitam o parâmetro de consulta opcional locale:
GET /_emdash/api/content/posts?locale=fr
GET /_emdash/api/content/posts/my-post?locale=fr
Se for omitido, usa-se o locale predefinido configurado.
Criar traduções por API
Crie uma tradução passando locale e translationOf ao endpoint de criação:
POST /_emdash/api/content/posts
Content-Type: application/json
{
"locale": "fr",
"translationOf": "01ABC...",
"data": {
"title": "Mon Article",
"slug": "mon-article"
}
}
A nova entrada partilha o translation_group da origem e começa como rascunho.
Listar traduções
Obtenha todas as traduções de uma entrada:
GET /_emdash/api/content/posts/01ABC.../translations
Devolve o ID do grupo de tradução e um array de variantes com os respetivos IDs, slugs e estados.
CLI
A CLI aceita a opção --locale nos comandos de conteúdo:
# 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...
Conteúdo multilingue em seed
Os ficheiros de seed expressam traduções com locale e 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" }
}
]
}
}
A entrada do locale de origem deve aparecer antes das suas traduções no seed para que translationOf se resolva corretamente.
Campos traduzíveis
Cada campo tem a opção translatable (por predefinição true). Ao criar uma tradução:
- Os campos traduzíveis são preenchidos a partir do locale de origem para edição
- Os não traduzíveis são copiados e mantidos sincronizados em todas as traduções do grupo
Os campos de sistema como status, published_at e author_id são sempre por idioma e não se sincronizam.
Estratégia de URL
O EmDash não gere URLs por idioma; o Astro gere o encaminhamento. Padrões habituais:
# 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
Use getRelativeLocaleUrl de astro:i18n para construir URLs corretas em qualquer modo de encaminhamento.
Importar conteúdo multilingue
WordPress com WPML ou Polylang
A origem de importação WordPress deteta WPML e Polylang automaticamente. Se forem detetados, o conteúdo importado inclui metadados de locale e grupo de tradução.
Ficheiros WXR
As exportações WXR não incluem metadados WPML/Polylang. Importe como um único locale e crie traduções manualmente, ou use --locale para atribuir um locale a todos os itens 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 passos
- Querying Content — Referência completa de consultas
- Working with Content — Gestão de conteúdo na administração
- Astro i18n routing — Configuração de encaminhamento Astro