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 inserisce — locize-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: trueper URL uniformi. - Usate rotte di pagina dinamiche
[lang]/congetStaticPathscosì che ogni locale venga pre-renderizzata staticamente. - Sincronizzate le traduzioni nell'app al build tramite il comando
downloaddilocize-cli. Nessun hop CDN a runtime, nessuna considerazione SSR. - Scrivete un helper
useTranslations(lang)di cinque righe insrc/i18n/utils.ts. Astro deliberatamente non spedisce unat(); 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 unsaveMissinga 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 usatei18next-locize-backend+ il pacchettolocizedentro 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-cliscarica gli ultimi JSON pubblicati dal CDN di Locize insrc/i18n/locales/{lng}/{ns}.json. I file JSON committati vengono importati direttamente dasrc/i18n/ui.tsal 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 downloadLocalese 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-appLa 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.jsonInstallare le dipendenze
npm install astro
npm install --save-dev locize-cliTutto 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.currentLocaledisponibile gratuitamente in ogni pagina/componente.- Rilevamento della lingua del browser via
Astro.preferredLocaleeAstro.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 suapi.locize.appè basato su CloudFront, a pagamento, supporta Private Downloads + controllo cache personalizzato. Allineate--cdn-typenel 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:
npm run syncLocales(cablato inpackage.json, togliete il flag--dry=trueper spingere davvero).locize-clilegge i vostrisrc/i18n/locales/{lng}/{ns}.jsonlocali e carica qualsiasi chiave non ancora in Locize. Manuale o triggerato da CI, nessuna write-key nel browser.- Estrazione statica via
i18next-clio simili — scansiona il vostro sorgente.astroper chiamatet('…'), scrive le nuove chiavi nei JSON locali, poi sync verso l'alto come sopra. - L'app web di Locize — aggiungete chiavi direttamente nell'editor e portatele giù con
downloadLocalesprima 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
saveMissinga 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
locizeha 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 downloadLocalese 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-backenddentro 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=truedel pacchettolocizeha bisogno di un runtime che si re-renderizzi sui cambi di stringa — disponibile solo dentro un'island idratata. saveMissinga 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
- Come internazionalizzare un'app Nuxt 4 con @nuxtjs/i18n e Locize — controparte lato Vue con integrazione più ricca (vue-i18n, editor in-context, saveMissing a runtime)
- Come internazionalizzare un'app React Router v7 con remix-i18next — controparte lato React con fetch live tramite CDN via
i18next-locize-backend - Routing i18n integrato di Astro — la primitiva di routing su cui questo walkthrough si fonda
- Ricetta i18n di Astro — il pattern build-it-yourself in cui Locize si inserisce
locize-cli— lo strumento di sincronizzazione al build