Internacionalização (i18n)

Nesta página

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-post e /fr/blog/mon-article encaixam 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" }:

  1. Tenta o locale pedido (fr)
  2. Tenta o locale de recurso (en)
  3. 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