Vai al contenuto
21 maggio 20268 min readTutorials

Internazionalizzare un sito Astro con Locize

Astro è il framework web static-first per siti content-driven — routing file-based, zero JavaScript per default, "island" per interattività opt-in, una modalità SSR-on-demand per le parti che la richiedono. A partire da v4.0 (fine 2023, stabile in v5+, attuale in v6.x), Astro include il routing i18n integrato: prefissazione delle URL (/en/, /de/), l'helper Astro.currentLocale, rilevamento della lingua del browser e tag SEO <link rel="alternate" hreflang> automatici.

Quello che l'i18n integrato di Astro non fornisce è una funzione di traduzione (t()), pluralizzazione o interpolazione dei messaggi. La ricetta i18n ufficiale di Astro mostra il pattern canonico "build it yourself": importate un file JSON per locale, scrivete cinque righe di helper, fatto. Ed è esattamente qui che Locize si inseriscelocize-cli scarica gli ultimi JSON pubblicati da Locize in src/i18n/locales/{lng}/{ns}.json al build, un piccolo modulo ui.ts li assembla in un albero piatto di lookup, e la build statica di Astro li raccoglie al momento della compilazione.

Questo articolo guida l'integrazione end-to-end. Il codice completo è disponibile su github.com/locize/locize-astro-example.

Se invece siete su Nuxt, seguite il walkthrough di Nuxt 4. Se siete su React Router v7 (modalità Framework), vedete il walkthrough di React Router v7 — entrambi coprono integrazioni più ricche (editor in-context, saveMissing a runtime) che non si adattano direttamente al modello static-by-default di Astro.

TL;DR

  • Configurate il routing i18n integrato di Astro in astro.config.mjs — lista delle locale + prefixDefaultLocale: true per URL uniformi.
  • Usate rotte di pagina dinamiche [lang]/ con getStaticPaths così che ogni locale venga pre-renderizzata staticamente.
  • Sincronizzate le traduzioni nell'app al build tramite il comando download di locize-cli. Nessun hop CDN a runtime, nessuna considerazione SSR.
  • Scrivete un helper useTranslations(lang) di cinque righe in src/i18n/utils.ts. Astro deliberatamente non spedisce una t(); questo è il pattern canonico dei loro stessi docs.
  • Fate arrivare le nuove chiavi via npm run syncLocales, l'estrazione statica di i18next-cli o l'app web di Locize — a seconda di cosa si adatta al vostro workflow. Non è possibile un saveMissing a runtime dal layer statico.
  • Vi serve saveMissing a runtime, fetch live tramite CDN o l'editor in-context? Montate una qualsiasi island React/Vue/Svelte via @astrojs/<framework> e usate i18next-locize-backend + il pacchetto locize dentro l'island.
  • Esempio completo funzionante: github.com/locize/locize-astro-example

Come si incastrano i pezzi

Astro è static-by-default. Le pagine vengono pre-renderizzate in HTML al build e servite come file statici (o, in modalità SSR, tramite un adapter on demand). L'i18n integrato di Astro possiede il layer di routing — prefissazione URL, redirect locale-aware, tag hreflang, helper di rilevamento del browser — ma resta deliberatamente fuori dal layer di traduzione dei messaggi. Ecco perché ogni tutorial i18n di Astro finisce con "ora scrivete il vostro helper t()": la filosofia di Astro è darvi primitive, non opinioni sulla traduzione.

Locize è il livello di gestione delle traduzioni. L'integrazione è solo in bundle a runtime:

  • Al build: locize-cli scarica gli ultimi JSON pubblicati dal CDN di Locize in src/i18n/locales/{lng}/{ns}.json. I file JSON committati vengono importati direttamente da src/i18n/ui.ts al caricamento del modulo e assemblati in un albero piatto di lookup.
  • t() è TypeScript locale — cinque righe che gestiscono il fallback di locale + l'interpolazione in stile {name}. Nessuna libreria a runtime, nessuna peer dep.
  • Gli aggiornamenti richiedono una rebuild — uguale a qualsiasi altra modifica di contenuto Astro. Eseguite npm run downloadLocales e fate il redeploy.

Quest'ultimo punto è il compromesso. Un giro «traduzione pubblicata in Locize → gli utenti finali la vedono» significa rilanciare la build. Se preferite un fetch live tramite CDN (pubblichi → la pagina successiva lo prende, senza redeploy), montate una framework island e usate i18next-locize-backend al suo interno — il walkthrough di React Router v7 copre quella forma, e potete adottare lo stesso pattern per-island in Astro. Ci torneremo alla fine.

Iniziamo

Prerequisiti

Node.js 22+ (Astro 6 lo richiede), npm/pnpm/yarn e una familiarità di base con Astro. L'esempio sotto punta ad Astro 6.3.

Layout del progetto

Generate una nuova app Astro, oppure fate git clone dell'esempio:

npm create astro@latest my-app
cd my-app

La forma della directory rilevante dopo aver cablato i18n + Locize:

my-app/
├── astro.config.mjs            — config dell'i18n integrato di Astro
├── package.json                — script download/sync di locize-cli
└── src/
    ├── pages/
    │   ├── index.astro         — root /, redirige a /en/
    │   └── [lang]/
    │       ├── index.astro     — home, renderizzata per ogni locale via getStaticPaths
    │       └── second.astro    — pagina secondaria, stesso pattern
    ├── layouts/
    │   └── Layout.astro<html>/<head>/<body> condivisi
    ├── components/
    │   └── LanguagePicker.astro — scambia il prefisso /{lang}/
    └── i18n/
        ├── ui.ts               — assembla i JSON namespaced in un albero piatto
        ├── utils.ts            — helper getLangFromUrl + useTranslations
        └── locales/            — target di download di locize-cli (un JSON per ns)
            ├── en/
            │   ├── common.json
            │   ├── index.json
            │   └── second.json
            └── de/
                ├── common.json
                ├── index.json
                └── second.json

Installare le dipendenze

npm install astro
npm install --save-dev locize-cli

Tutto qui. Nessuna libreria di traduzione a runtime, nessun pacchetto di integrazione con il framework. L'i18n integrato di Astro + locize-cli + ~50 righe di helper coprono l'intera storia.

Configurare l'i18n integrato di Astro in astro.config.mjs

import { defineConfig } from 'astro/config'

export default defineConfig({
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'de'],
    routing: {
      prefixDefaultLocale: true   // /en/, /de/ — URL uniformi
    }
  }
})

Astro ora possiede:

  • Il routing URL per ogni pagina sotto src/pages/[lang]//en/, /de/, /en/second, /de/second, ecc.
  • Astro.currentLocale disponibile gratuitamente in ogni pagina/componente.
  • Rilevamento della lingua del browser via Astro.preferredLocale e Astro.preferredLocaleList (solo per rotte SSR/on-demand).
  • Tag <link rel="alternate" hreflang> automatici quando si fa opt-in.

Con prefixDefaultLocale: true, ogni locale ottiene un prefisso URL — uniforme, pulito per la SEO e facile da gestire con rotte dinamiche [lang]/ + getStaticPaths. (Se preferite servire la locale di default su / senza prefisso, impostatelo a false e usate pagine file-based invece di [lang]/. L'esempio usa la forma prefissata perché scala meglio.)

Scaricare le traduzioni da Locize al build

Aggiungete a package.json:

{
  "scripts": {
    "downloadLocales": "locize download --project-id=<your-id> --ver=latest --cdn-type=standard --clean=true --path=./src/i18n/locales",
    "syncLocales":     "locize sync --project-id=<your-id> --ver=latest --cdn-type=standard --api-key=<your-write-key> --path=./src/i18n/locales --dry=true"
  }
}

Eseguite npm run downloadLocales prima di npm run build (manualmente, in CI o come hook prebuild) così che i JSON in bundle siano sempre freschi.

Standard vs Pro CDN. Locize offre due infrastrutture CDN (confronto completo in Tipi di CDN: Standard vs Pro): Standard su api.lite.locize.app è basato su BunnyCDN, gratuito per volumi mensili di download generosi, solo pubblico (default per i nuovi progetti); Pro su api.locize.app è basato su CloudFront, a pagamento, supporta Private Downloads + controllo cache personalizzato. Allineate --cdn-type nel flag CLI a ciò che è configurato nel vostro progetto.

L'albero dei messaggi — src/i18n/ui.ts

Locize emette un JSON per namespace per lingua. Astro non ha una nozione di namespace i18n, quindi assembliamo il layout on-disk in un singolo albero piatto di lookup al momento del caricamento del modulo, con il namespace incorporato come prefisso in ogni chiave:

import enCommon from './locales/en/common.json' with { type: 'json' }
import enIndex  from './locales/en/index.json'  with { type: 'json' }
import enSecond from './locales/en/second.json' with { type: 'json' }
import deCommon from './locales/de/common.json' with { type: 'json' }
import deIndex  from './locales/de/index.json'  with { type: 'json' }
import deSecond from './locales/de/second.json' with { type: 'json' }

type Messages = Record<string, string>

function prefix (ns: string, obj: Messages): Messages {
  return Object.fromEntries(
    Object.entries(obj).map(([k, v]) => [`${ns}.${k}`, v])
  )
}

export const ui = {
  en: { ...prefix('common', enCommon), ...prefix('index', enIndex), ...prefix('second', enSecond) },
  de: { ...prefix('common', deCommon), ...prefix('index', deIndex), ...prefix('second', deSecond) }
} as const

export const defaultLang = 'en'

export const languages = {
  en: 'English',
  de: 'Deutsch'
} as const

export type Lang = keyof typeof ui
export type TranslationKey = keyof typeof ui[typeof defaultLang]

L'output appare così:

ui.en = {
  'common.headTitle': 'Locize + Astro',
  'common.skipToContent': 'Skip to content',
  'index.title': 'Hello, {name}!',
  'second.title': 'The second page',
  // ...
}

TranslationKey deriva dall'albero della locale di default, così TypeScript autocompleta la chiave nei file .astro e respinge i refusi in fase di compilazione.

L'helper t()src/i18n/utils.ts

import { ui, defaultLang, type Lang, type TranslationKey } from './ui'

export function getLangFromUrl (url: URL): Lang {
  const [, lang] = url.pathname.split('/')
  if (lang && lang in ui) return lang as Lang
  return defaultLang
}

export function useTranslations (lang: Lang) {
  return function t (
    key: TranslationKey,
    values?: Record<string, string | number>
  ): string {
    const raw: string = ui[lang][key] ?? ui[defaultLang][key] ?? key
    if (!values) return raw
    return raw.replace(/\{(\w+)\}/g, (_, k: string) =>
      values[k] !== undefined ? String(values[k]) : `{${k}}`
    )
  }
}

Cinque righe di logica. Catena di fallback: la chiave richiesta nella locale attiva, poi la locale di default, poi la stringa della chiave grezza. Interpolazione opzionale in stile {name} passa i valori tramite il secondo argomento. Questa è l'intera API a runtime.

Questo pattern è lo stesso che mostra la ricetta i18n ufficiale di Astro — ci abbiamo solo stratificato sopra il layout namespaced on-disk di Locize.

Usare t() nelle pagine

Le rotte dinamiche [lang]/ + getStaticPaths fanno sì che Astro pre-renderizzi un file HTML per ciascuna locale al build:

---
// src/pages/[lang]/index.astro
import Layout from '../../layouts/Layout.astro'
import { ui, type Lang } from '../../i18n/ui'
import { useTranslations } from '../../i18n/utils'
import { getRelativeLocaleUrl } from 'astro:i18n'

export function getStaticPaths () {
  return Object.keys(ui).map(lang => ({ params: { lang } }))
}

const { lang } = Astro.params as { lang: Lang }
const t = useTranslations(lang)
---

<Layout lang={lang} title={t('common.headTitle')}>
  <h1>{t('index.title', { name: 'Astro' })}</h1>
  <p>{t('index.subtitle')}</p>
  <p>
    <a href={getRelativeLocaleUrl(lang, 'second/')}>
      {t('index.goto.second')}
    </a>
  </p>
</Layout>

La funzione getStaticPaths mappa ogni locale a un percorso statico; lang arriva come parametro di rotta. Lavoro puramente in compile-time — nessun runtime lato server, nessuna considerazione sull'hydration.

Il language picker

La ricetta i18n dei docs di Astro mostra un picker semplice che scambia il prefisso di locale sulla URL corrente:

---
// src/components/LanguagePicker.astro
import { languages, type Lang } from '../i18n/ui'

interface Props {
  currentLang: Lang
}
const { currentLang } = Astro.props

function pathForLang (target: Lang): string {
  const { pathname } = Astro.url
  const parts = pathname.split('/')
  // "/en/foo/bar" → ["", "en", "foo", "bar"]
  parts[1] = target
  return parts.join('/')
}
---

<nav>
  <ul>
    {Object.entries(languages).map(([lang, label]) => (
      <li>
        {lang === currentLang ? (
          <strong>{label}</strong>
        ) : (
          <a href={pathForLang(lang as Lang)}>{label}</a>
        )}
      </li>
    ))}
  </ul>
</nav>

Inseritelo in src/layouts/Layout.astro e sarà disponibile su ogni pagina.

Tre modi per far arrivare nuove chiavi in Locize

Astro è static-by-default — le pagine sono pre-renderizzate, quindi un push saveMissing a runtime dagli utenti in produzione non è possibile senza un adapter SSR, e comunque Astro scoraggia le scritture dal layer statico. Ci sono invece tre strade praticabili:

  1. npm run syncLocales (cablato in package.json, togliete il flag --dry=true per spingere davvero). locize-cli legge i vostri src/i18n/locales/{lng}/{ns}.json locali e carica qualsiasi chiave non ancora in Locize. Manuale o triggerato da CI, nessuna write-key nel browser.
  2. Estrazione statica via i18next-cli o simili — scansiona il vostro sorgente .astro per chiamate t('…'), scrive le nuove chiavi nei JSON locali, poi sync verso l'alto come sopra.
  3. L'app web di Locize — aggiungete chiavi direttamente nell'editor e portatele giù con downloadLocales prima della prossima build.

Scegliete quella che si adatta al vostro workflow. Nessuna richiede di spedire una write API key nel vostro artefatto di build.

Cosa questa forma intenzionalmente non include

Tre cose che potreste aspettarvi dai walkthrough di Nuxt o React Router v7 sono deliberatamente assenti qui:

  • Nessun saveMissing a runtime. Vedi sopra — le build statiche di Astro non possono spingere, e un'app Astro SSR lo farebbe da dentro un'island, non dal layer statico.
  • Nessun editor in-context sulle pagine statiche. L'editor di locize ha bisogno di un DOM che si re-renderizzi quando l'editor aggiorna una stringa. L'output statico di Astro non ha quel gancio.
  • Nessun fetch live tramite CDN. Le traduzioni sono messe in bundle al build. Per servire una traduzione fresca, eseguite npm run downloadLocales e fate il redeploy. Questo si allinea al modello static-first di Astro e mantiene l'artefatto di build autocontenuto.

Tutte e tre funzionano dentro le framework island. Montate un'island React, Vue, Svelte, Solid o Preact tramite la corrispondente integrazione @astrojs/<framework>, e dentro quell'island usate i18next-locize-backend + il pacchetto locize direttamente. Il layer statico di Astro gestisce il routing + il chrome della pagina; le island gestiscono le parti che richiedono un runtime.

Quando aggiungere una framework island

L'integrazione solo-statica copre la maggior parte dei siti di contenuto — blog, pagine marketing, docs, landing page. Aggiungete una framework island quando vi serve:

  • Aggiornamenti di traduzione live senza redeploy. Usate i18next-locize-backend dentro l'island per recuperare le traduzioni dal CDN di Locize a ogni visita (o per rotta, se mettete in cache).
  • Editing in-context per i traduttori. L'overlay ?incontext=true del pacchetto locize ha bisogno di un runtime che si re-renderizzi sui cambi di stringa — disponibile solo dentro un'island idratata.
  • saveMissing a runtime. Stesso vincolo — un'island idratata può spingere nuove chiavi; l'HTML statico no.

Il pattern appare così (per un'island React):

---
// src/pages/[lang]/interactive.astro
import Layout from '../../layouts/Layout.astro'
import I18nIsland from '../../components/I18nIsland.tsx'
---

<Layout lang={Astro.currentLocale}>
  <I18nIsland client:load lang={Astro.currentLocale} />
</Layout>
// src/components/I18nIsland.tsx
import i18next from 'i18next'
import Backend from 'i18next-locize-backend'
import { initReactI18next, useTranslation } from 'react-i18next'

i18next
  .use(Backend)
  .use(initReactI18next)
  .init({
    fallbackLng: 'en',
    saveMissing: import.meta.env.DEV,
    backend: {
      projectId: import.meta.env.PUBLIC_LOCIZE_PROJECT_ID,
      apiKey: import.meta.env.DEV ? import.meta.env.LOCIZE_API_KEY : undefined,
      version: 'latest'
    }
  })

export default function I18nIsland ({ lang }: { lang: string }) {
  const { t, i18n } = useTranslation()
  if (i18n.language !== lang) i18n.changeLanguage(lang)
  return <h2>{t('island.heading')}</h2>
}

client:load dice ad Astro di idratare questo componente immediatamente al caricamento della pagina; potete usare client:visible per posticipare l'hydration finché l'island non entra nel viewport. La configurazione precisa di i18next-locize-backend è documentata nel walkthrough di React Router v7 — lo stesso cablaggio del backend funziona in qualsiasi contesto React/Vue/Svelte/Solid, incluso dentro un'island Astro.

🎉 Complimenti

Un setup funzionante Astro 6 + Locize vi offre:

  • i18n static-first senza flash di contenuti non tradotti (tutto è pre-renderizzato per locale)
  • Prefissazione URL pulita per la SEO via il routing i18n integrato di Astro
  • Sincronizzazione delle traduzioni al build via locize-cli — serverless-friendly, nessun hop CDN per richiesta
  • t() type-safe con autocomplete sulle chiavi di traduzione
  • Una via di fuga verso island dinamiche quando vi servono aggiornamenti a runtime o editing in-context

🧑‍💻 Il codice completo: github.com/locize/locize-astro-example.

I fondatori di Locize sono anche i creatori di i18next — usando Locize sostenete direttamente il futuro di i18next.

Vedi anche