Internazionalizzazione (i18n)

In questa pagina

EmDash si integra con il routing i18n integrato di Astro per fornire la gestione dei contenuti multilingue. Astro gestisce il routing degli URL e il rilevamento della locale; EmDash gestisce l’archiviazione e il recupero dei contenuti tradotti.

Ogni traduzione è una voce di contenuto completa e indipendente con il proprio slug, stato e cronologia delle revisioni. La versione francese di un post può essere in bozza mentre la versione inglese è pubblicata.

Configurazione

Abilita i18n aggiungendo un blocco i18n alla tua configurazione Astro. EmDash legge la stessa configurazione per l’elenco delle locale, la locale predefinita e la catena di 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 non è presente nella configurazione Astro, tutte le funzionalità i18n sono disabilitate e EmDash si comporta come un CMS monolingue.

Come funzionano le traduzioni

EmDash utilizza un modello riga per locale. Ogni traduzione è la propria riga nel database con il proprio ID, slug e stato, collegata ad altre traduzioni tramite un identificatore translation_group condiviso. Una tabella posts con tre traduzioni appare così:

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

Questo design significa:

  • Slug per locale/blog/my-post e /fr/blog/mon-article funzionano naturalmente
  • Pubblicazione per locale — pubblica la versione inglese mantenendo il francese in bozza
  • Revisioni per locale — ogni traduzione ha la propria cronologia delle revisioni
  • Query mono-locale — le query di elenco restituiscono voci per una sola locale

Interrogare contenuti tradotti

Voce singola

Passa locale a getEmDashEntry per recuperare una traduzione specifica. Se omesso, usa per impostazione predefinita la locale corrente della richiesta (impostata dal middleware i18n di 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>

Catena di fallback

Quando non esiste contenuto per la locale richiesta, EmDash segue la catena di fallback definita nella configurazione Astro. Con fallback: { fr: "en" }:

  1. Prova la locale richiesta (fr)
  2. Prova la locale di fallback (en)
  3. Prova la locale predefinita

Il fallback si applica solo alle query di voce singola. Le query di elenco restituiscono voci solo per la locale richiesta.

I menu sono per locale — lo stesso name (ad es. "primary") può esistere in più locale, tutti collegati tramite un translation_group condiviso. Le voci del menu risolvono i riferimenti al contenuto rispetto alla versione del contenuto referenziato nella locale attiva.

Il seguente componente recupera il menu principale per la locale attiva:

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

Crea traduzioni di un menu esistente dall’elenco Menus dell’admin — le voci vengono clonate con reference_id intatto (memorizza il translation_group del contenuto referenziato), quindi i link del nuovo menu puntano automaticamente al contenuto corretto per locale.

Tassonomie (categorie, tag)

I termini sono per locale. Le definizioni (_emdash_taxonomy_defs) sono anch’esse per locale, quindi label / labelSingular possono essere tradotti. La colonna pivot content_taxonomies.taxonomy_id memorizza il translation_group del termine, quindi una singola assegnazione copre ogni locale del contenuto.

Il seguente esempio recupera categorie e termini di un post per la locale attiva:

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

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

Tradurre un contenuto eredita automaticamente le assegnazioni dei termini dalla fonte — devi tradurre i termini stessi solo una volta, e ogni post che li usa li risolve alla locale corretta in fase di lettura.

Elenco collection

Filtra una collection per 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>

Selettore lingua

Usa getTranslations per costruire un selettore lingua che collega alle traduzioni esistenti della voce corrente:

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

La funzione getTranslations restituisce tutte le varianti di locale nello stesso gruppo di traduzione:

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

Gestire le traduzioni nell’Admin

Elenco contenuti

Quando i18n è abilitato, l’elenco contenuti mostra:

  • Una colonna locale che visualizza la locale di ogni voce
  • Un filtro locale nella barra degli strumenti per passare tra le locale

Creare traduzioni

Apri qualsiasi voce di contenuto nell’editor. La barra laterale visualizza un pannello Translations con tutte le locale configurate. Per ogni locale:

  • “Translate” appare per le locale senza traduzione — clicca per crearne una
  • “Edit” appare per le locale con traduzione esistente — clicca per navigare
  • La locale corrente è contrassegnata con un segno di spunta

Quando crei una traduzione, la nuova voce viene precompilata con i dati dalla locale sorgente e riceve uno slug predefinito di {source-slug}-{locale}. Regola slug e contenuto secondo necessità, quindi salva.

Pubblicazione per locale

Ogni traduzione ha il proprio stato. Pubblica, annulla la pubblicazione o programma le traduzioni in modo indipendente. La versione francese può essere in bozza mentre la versione inglese è online.

API contenuti

Parametro locale

Tutte le route dell’API contenuti accettano un parametro query locale opzionale:

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

Se omesso, usa per impostazione predefinita la locale predefinita configurata.

Creare traduzioni via API

Crea una traduzione passando locale e translationOf all’endpoint di creazione contenuti:

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

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

La nuova voce condivise il translation_group della voce sorgente e inizia come bozza.

Elencare traduzioni

Recupera tutte le traduzioni per una determinata voce:

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

Restituisce l’ID del gruppo di traduzione e un array di varianti locale con i loro ID, slug e stati.

CLI

La CLI supporta flag --locale sui comandi contenuti:

# Elenca post in francese
emdash content list posts --locale fr

# Ottieni una voce specifica in francese
emdash content get posts my-post --locale fr

# Crea una traduzione francese di una voce esistente
emdash content create posts --locale fr --translation-of 01ABC...

Seeding contenuti multilingue

I file seed esprimono le traduzioni 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" }
      }
    ]
  }
}

La voce della locale sorgente deve apparire prima delle sue traduzioni nel file seed affinché i riferimenti translationOf si risolvano correttamente.

Traducibilità dei campi

Ogni campo ha un’impostazione translatable (predefinito: true). Quando crei una traduzione:

  • I campi traducibili vengono precompilati dalla locale sorgente per la modifica
  • I campi non traducibili vengono copiati e mantenuti sincronizzati in tutte le traduzioni del gruppo

I campi di sistema come status, published_at e author_id sono sempre per locale e non vengono mai sincronizzati.

Strategia URL

EmDash non gestisce gli URL delle locale — Astro gestisce il routing. Modelli comuni:

# prefix-other-locales (predefinito Astro)
/blog/my-post          → en (locale predefinita, senza prefisso)
/fr/blog/mon-article   → fr

# prefix-always
/en/blog/my-post       → en
/fr/blog/mon-article   → fr

Usa getRelativeLocaleUrl da astro:i18n per costruire URL corrette indipendentemente dalla modalità di routing.

Sitemap

La sitemap per collection su /sitemap-{collection}.xml è consapevole della locale. Quando i18n è abilitato, ogni traduzione viene emessa come propria voce <url>, con il prefisso locale risolto tramite getRelativeLocaleUrl di Astro. L’impostazione prefixDefaultLocale e qualsiasi mappatura personalizzata del path della locale vengono rispettate automaticamente.

Le traduzioni sorelle sono collegate incrociatamente con alternative xhtml:link in modo che i motori di ricerca servano la lingua corretta a ogni utente:

<url>
  <loc>https://example.com/blog/hello</loc>
  <lastmod>2026-05-28T16:33:15.461Z</lastmod>
  <xhtml:link rel="alternate" hreflang="en" href="https://example.com/blog/hello" />
  <xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr/blog/bonjour" />
  <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/blog/hello" />
</url>

Le sorelle sono raggruppate per translation_group, quindi una riga aggiunta successivamente (una nuova variante locale di un post esistente) appare automaticamente come alternativa su ogni altra variante. I siti con una singola locale producono una sitemap semplice senza namespace xhtml.

Importare contenuti multilingue

Importa contenuti WordPress tramite lo strumento di migrazione dell’admin — vedi Content Import e Migrate from WordPress. Un’esportazione WXR non contiene la struttura di locale e gruppo di traduzione che WPML o Polylang aggiungono, quindi i contenuti importati arrivano nella tua locale predefinita.

Per costruire traduzioni dai contenuti importati, crea la voce tradotta e collegala all’originale:

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

Questo è lo stesso flusso di lavoro --locale / --translation-of mostrato in Seeding contenuti multilingue sopra, applicato dopo il completamento dell’importazione.

Prossimi passi