Internacionalização (i18n)

Nesta página

EmDash integra-se com o roteamento i18n integrado do Astro para fornecer gestão de conteúdo multilíngue. O Astro lida com o roteamento de URL e a detecção de locale; o EmDash lida com o armazenamento e recuperação de conteúdo traduzido.

Cada tradução é uma entrada de conteúdo completa e independente com seu próprio slug, status e histórico de revisões. A versão francesa de um post pode estar em rascunho enquanto a versão em inglês está publicada.

Configuração

Ative o i18n adicionando um bloco i18n à sua configuração do Astro. O EmDash lê esta mesma configuração para sua lista de locales, locale padrão e cadeia de fallback.

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",
			}),
		}),
	],
});

Quando i18n não está presente na configuração do Astro, todos os recursos de i18n são desativados e o EmDash se comporta como um CMS de idioma único.

Como as traduções funcionam

O EmDash usa um modelo linha-por-locale. Cada tradução é sua própria linha no banco de dados com seu próprio ID, slug e status, vinculada a outras traduções por meio de um identificador translation_group compartilhado. Uma tabela de posts com três traduções se parece com isso:

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

Este design significa:

  • Slugs por locale/blog/my-post e /fr/blog/mon-article funcionam naturalmente
  • Publicação por locale — publique a versão em inglês mantendo o francês em rascunho
  • Revisões por locale — cada tradução tem seu próprio histórico de revisões
  • Consultas de locale único — consultas de lista retornam entradas apenas para um locale

Consultar conteúdo traduzido

Entrada única

Passe locale para getEmDashEntry para recuperar uma tradução específica. Quando omitido, o padrão é o locale atual da solicitação (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 fallback

Quando não existe conteúdo para o locale solicitado, o EmDash segue a cadeia de fallback definida em sua configuração do Astro. Dado fallback: { fr: "en" }:

  1. Tenta o locale solicitado (fr)
  2. Tenta o locale de fallback (en)
  3. Tenta o locale padrão

O fallback só se aplica a consultas de entrada única. Consultas de lista retornam entradas apenas para o locale solicitado.

Os menus são por locale — o mesmo name (ex. "primary") pode existir em vários locales, todos vinculados por meio de um translation_group compartilhado. Os itens do menu resolvem suas referências de conteúdo em relação à versão do locale ativo do conteúdo referenciado.

O seguinte componente busca o menu principal para o locale ativo:

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

Crie traduções de um menu existente a partir da lista de Menus do admin — os itens são clonados com reference_id intacto (armazena o translation_group do conteúdo referenciado), então os links do novo menu apontam automaticamente para o conteúdo correto por locale.

Taxonomias (categorias, tags)

Os termos são por locale. As definições (_emdash_taxonomy_defs) também são por locale, então label / labelSingular também podem ser traduzidos. O pivô content_taxonomies.taxonomy_id armazena o translation_group do termo, então uma única atribuição abrange cada locale do conteúdo.

O exemplo a seguir busca categorias e os termos de um post para o locale ativo:

---
import { getTaxonomyTerms, getEntryTerms } from "emdash";

const categories = await getTaxonomyTerms("category", {
  locale: Astro.currentLocale,
});
const terms = await getEntryTerms("posts", post.id, undefined, {
  locale: Astro.currentLocale,
});
---

Traduzir um conteúdo herda automaticamente as atribuições de termos da fonte — você só precisa traduzir os termos em si uma vez, e cada post que os usa será resolvido para o locale correto no momento da leitura.

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 de idioma que vincula à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>

A função getTranslations retorna todas as variantes de locale no 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" },
// ]

Gerenciar traduções no Admin

Lista de conteúdo

Quando o i18n está ativado, a lista de conteúdo mostra:

  • Uma coluna de locale exibindo o locale de cada entrada
  • Um filtro de locale na barra de ferramentas para alternar entre locales

Criar traduções

Abra qualquer entrada de conteúdo no editor. A barra lateral exibe um painel de Traduções listando todos os locales configurados. Para cada locale:

  • “Traduzir” aparece para locales sem tradução — clique para criar uma
  • “Editar” aparece para locales com tradução existente — clique para navegar até ela
  • O locale atual é marcado com uma marca de seleção

Ao criar uma tradução, a nova entrada é pré-preenchida com dados do locale de origem e recebe um slug padrão de {slug-origem}-{locale}. Ajuste o slug e o conteúdo conforme necessário e salve.

Publicação por locale

Cada tradução tem seu próprio status. Publique, despublique ou agende traduções de forma independente. A versão francesa pode estar em rascunho enquanto a versão em inglês está ao vivo.

API de conteúdo

Parâmetro locale

Todas as rotas da API de conteúdo aceitam um parâmetro de consulta locale opcional:

GET /_emdash/api/content/posts?locale=fr
GET /_emdash/api/content/posts/my-post?locale=fr

Quando omitido, o padrão é o locale padrão configurado.

Criar traduções via API

Crie uma tradução passando locale e translationOf para o endpoint de criação de conteúdo:

POST /_emdash/api/content/posts
Content-Type: application/json

{
  "locale": "fr",
  "translationOf": "01ABC...",
  "data": {
    "title": "Mon Article",
    "slug": "mon-article"
  }
}

A nova entrada compartilha o translation_group da entrada de origem e começa como rascunho.

Listar traduções

Recupere todas as traduções para uma entrada específica:

GET /_emdash/api/content/posts/01ABC.../translations

Retorna o ID do grupo de tradução e um array de variantes de locale com seus IDs, slugs e status.

CLI

A CLI suporta flags --locale em comandos de conteúdo:

# Listar posts em francês
emdash content list posts --locale fr

# Obter uma entrada específica em francês
emdash content get posts my-post --locale fr

# Criar uma tradução em francês de uma entrada existente
emdash content create posts --locale fr --translation-of 01ABC...

Seed de conteúdo multilíngue

Os arquivos seed expressam traduções usando 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 de suas traduções no arquivo seed para que as referências translationOf sejam resolvidas corretamente.

Traduzibilidade de campo

Cada campo tem uma configuração translatable (padrão: true). Ao criar uma tradução:

  • Campos traduzíveis são pré-preenchidos do locale de origem para edição
  • Campos não traduzíveis são copiados e mantidos sincronizados em todas as traduções do grupo

Campos do sistema como status, published_at e author_id são sempre por locale e nunca sincronizados.

Estratégia de URL

O EmDash não gerencia URLs de locale — o Astro lida com o roteamento. Padrões comuns:

# prefix-other-locales (padrão do Astro)
/blog/my-post          → en (locale padrão, sem prefixo)
/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 independentemente do modo de roteamento.

Importar conteúdo multilíngue

Importe conteúdo do WordPress através da ferramenta de migração do admin — veja Importação de conteúdo e Migrar do WordPress. Uma exportação WXR não carrega a estrutura de locale e grupo de tradução que WPML ou Polylang adicionam, então o conteúdo importado chega ao seu locale padrão.

Para construir traduções a partir do conteúdo importado, crie a entrada traduzida e vincule-a ao original:

emdash content create posts --locale fr --translation-of 01ABC...

Este é o mesmo fluxo de trabalho --locale / --translation-of mostrado em Seed de conteúdo multilíngue acima, aplicado após a conclusão da importação.

Próximos passos