Mejores Prácticas de Paraglide JS + Svelte 5
Trata la configuración regional como estado de solicitud/compilación resuelto por un compilador, no como estado reactivo de la aplicación — deja que la navegación de documento completo de Paraglide maneje los cambios de locale, y deja que los runes de Svelte 5 se encarguen de todo lo demás. Paraglide JS es la integración i18n oficial de SvelteKit: compila messages/{locale}.json en funciones ESM tree-shakable y completamente tipadas, lo que reduce los bundles i18n hasta ~70% en comparación con las bibliotecas en tiempo de ejecución (≈47 KB vs ≈205 KB en el benchmark publicado con 5 locales / 200 mensajes), sin cascadas asíncronas y sin necesidad de conectar un proveedor i18n a los componentes de Svelte 5.
1. Tabla de Decisiones de Referencia Rápida
| Decisión | Valor recomendado por defecto | Por qué |
|---|---|---|
| Ruta de instalación | npx sv add paraglide="languageTags:en,de" en una nueva app SvelteKit | Configura el plugin de Vite, hooks, atributos de app.html, .gitignore en un solo paso |
| Paquete | @inlang/paraglide-js v2.x | El adaptador v1 @inlang/paraglide-js-adapter-sveltekit está obsoleto — usa el paquete principal directamente |
| Estrategia (SSR + URLs localizadas) | ["url", "cookie", "baseLocale"] | La URL es la fuente de verdad para SEO; la cookie persiste la elección del usuario de manera visible en el servidor; baseLocale siempre se resuelve al final |
| Estrategia (añadir anulación almacenada) | ["localStorage", "cookie", "preferredLanguage", "url", "baseLocale"] | localStorage solo es invisible para la primera solicitud SSR; la cookie actúa como puente entre servidor/cliente |
| Comportamiento del cambio de locale | Por defecto setLocale(locale) (navegación de documento completo) | La reactividad del framework no vuelve a renderizar <html lang>, dir, <title> ni las URLs localizadas; la navegación sí lo hace |
| AsyncLocalStorage | Mantenerlo habilitado (por defecto) | Vercel Edge y Cloudflare Workers (compatibles con Node) lo soportan; deshabilitarlo corre el riesgo de filtrar la configuración regional entre solicitudes concurrentes |
| Declaraciones TS | emitTsDeclarations: true (TypeScript ≥ 5.6) | Mantiene sincronizados los servidores de lenguaje Svelte + TypeScript cuando las claves cambian durante el desarrollo |
| Claves de mensaje | Claves planas, estables y legibles para humanos (calm_green_otter) | Recomendación oficial; las claves anidadas funcionan pero se prefieren las planas |
| Prerenderizado | export const prerender = true + entries() por locale | SSG necesita que cada locale se resuelva en un archivo URL distinto |
| Reactividad para locale | Ninguna en los componentes por defecto | getLocale() tiene ámbito de solicitud; se renderiza un nuevo documento por locale, por lo que los runes no necesitan rastrearlo |
2. Configuración y Ajustes (Paraglide 2.x + SvelteKit + Svelte 5)
2.1 Ruta más rápida
npx sv create my-app
npx sv add paraglide="languageTags:en,de,demo:yes"
El complemento de la CLI de Svelte configura el proyecto inlang, el plugin de Vite, los hooks handle/reroute de SvelteKit, %lang%/%dir% en app.html y una página de demostración opcional.
2.2 Configuración manual (boilerplate completo)
Instala y configura el proyecto, luego conecta cuatro archivos:
vite.config.ts — plugin con estrategia explícita:
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { paraglideVitePlugin } from '@inlang/paraglide-js';
export default defineConfig({
plugins: [
sveltekit(),
paraglideVitePlugin({
project: './project.inlang',
outdir: './src/lib/paraglide',
emitTsDeclarations: true,
strategy: ['url', 'cookie', 'baseLocale'],
}),
],
});
emitTsDeclarations: true regenera los .d.ts a medida que editas los mensajes para que los servidores Svelte y TypeScript se mantengan sincronizados; requiere TypeScript 5.6+ (establece false para la ruta de inferencia JSDoc más rápida).
src/app.html — reemplaza el lang codificado:
<!doctype html>
<html lang="%lang%" dir="%dir%">
src/hooks.server.ts — middleware del servidor con locale de ámbito de solicitud:
import type { Handle } from '@sveltejs/kit';
import { paraglideMiddleware } from '$lib/paraglide/server';
import { getTextDirection } from '$lib/paraglide/runtime';
const paraglideHandle: Handle = ({ event, resolve }) =>
paraglideMiddleware(event.request, ({ request: localizedRequest, locale }) => {
event.request = localizedRequest;
return resolve(event, {
transformPageChunk: ({ html }) =>
html.replace('%lang%', locale).replace('%dir%', getTextDirection(locale)),
});
});
export const handle: Handle = paraglideHandle;
El middleware detecta la configuración regional (estrategias en orden), redirige las solicitudes de documentos cuando la URL no coincide con la detectada, deslocaliza /de/about → /about antes de que SvelteKit lo vea y envuelve la solicitud en AsyncLocalStorage para que getLocale() devuelva la configuración regional correcta por solicitud concurrente.
src/hooks.ts — límite de enrutamiento (debe ser hooks.ts, no hooks.server.ts):
import type { Reroute } from '@sveltejs/kit';
import { deLocalizeUrl } from '$lib/paraglide/runtime';
export const reroute: Reroute = (request) => deLocalizeUrl(request.url).pathname;
// project.inlang/paraglide.config.ts
import { defineConfig } from '@inlang/paraglide-js';
export default defineConfig({
outdir: './src/lib/paraglide',
strategy: ['url', 'cookie', 'baseLocale'],
urlPatterns: [
{ pattern: '/', localized: [['en', '/']] },
{
pattern: '/:path(.*)?',
localized: [
['en', '/en/:path(.*)?'],
['de', '/de/:path(.*)?'],
],
},
],
});
Precedencia: banderas de CLI / opciones del plugin > archivo de configuración > valores predeterminados integrados. El modo de observación recarga la configuración en caliente.
3. Patrones de Uso Principales (Mensajes, Locale, Enrutamiento)
3.1 Mensajes
import { m } from '$lib/paraglide/messages';
import type { LocalizedString } from '$lib/paraglide/runtime';
m.hello_world(); // "¡Hola Mundo!"
m.greeting({ name: 'Ada' }); // "¡Hola Ada!" — los parámetros están tipados
m.greeting({ name: 'Ada' }, { locale: 'de' }); // forzar locale para una llamada
const title: LocalizedString = m.welcome_title(); // tipo de cadena seguro para la marca
Los mensajes residen en messages/{locale}.json; los locales se declaran en project.inlang/settings.json (baseLocale + locales). Prefiere claves planas y estables; las claves anidadas se soportan mediante m['user.profile.title'](). El acceso dinámico a claves anula el tree-shaking — mapea los IDs dinámicos a referencias de mensajes explícitas en su lugar.
3.2 Obtener y establecer la configuración regional
import { getLocale, locales, setLocale, getTextDirection } from '$lib/paraglide/runtime';
getLocale(); // ej. "de" — ámbito de solicitud en el servidor
getTextDirection(); // "ltr" | "rtl" para el locale actual
setLocale('de'); // navegación de documento completo a la URL localizada
setLocale() actualiza tus estrategias configuradas y luego realiza una navegación de documento completo — a la URL localizada cuando la estrategia url está activa, de lo contrario una recarga. Esto es intencional: <html lang>, dir, <title>, metadatos y la URL localizada son estado a nivel de documento que la reactividad del framework no puede actualizar.
La opción de escape setLocale(locale, { reload: false }) existe solo para superficies completamente renderizadas en el cliente (widgets incrustados, páginas de extensiones) que deben preservar el estado en memoria no restaurable. Nunca lo uses para selectores de locale ordinarios, en documentos SSR/SSG, o en rutas cuya estrategia incluya url — deja la URL antigua y el shell en su lugar, por lo que getLocale() seguirá resolviendo la configuración regional antigua.
3.3 URLs y enlaces localizados
import { localizeHref, deLocalizeUrl, localizeUrl } from '$lib/paraglide/runtime';
localizeHref('/blog'); // "/de/blog" para el locale actual
localizeHref('/blog', { locale: 'de' }); // explícito
localizeUrl('https://example.com/about', { locale: 'de' }).href; // formulario de URL completa
Los patrones de URL utilizan la sintaxis estándar de URLPattern; el orden importa — coloca patrones específicos antes que comodines y pruébalos en urlpattern.com.
3.4 Selector de locale de Svelte 5 (idiomático)
<script lang="ts">
import { page } from '$app/state'; // Svelte 5: objeto, no un store
import { resolve } from '$app/paths';
import { locales, localizeHref } from '$lib/paraglide/runtime';
</script>
<nav aria-label="Idiomas">
{#each locales as locale (locale)}
<a
href={resolve(localizeHref(page.url.pathname, { locale }))}
data-sveltekit-reload
aria-current={page.params.locale === locale ? 'page' : undefined}
>
{locale}
</a>
{/each}
</nav>
Dos detalles de Svelte 5 importan aquí: $app/state expone page como un objeto reactivo (el store $page y $app/stores son el patrón heredado), y data-sveltekit-reload en enlaces entre locales mantiene sincronizados la URL localizada, el documento renderizado en el servidor y el locale en tiempo de ejecución — también es necesario para el rastreo prerenderizado.
4. Texto Enriquecido con Snippets de Svelte 5 (ParaglideMessage)
El marcado permite a los traductores controlar la estructura mientras tu aplicación controla el renderizado. Los nombres de las etiquetas son definidos por la aplicación — nada se asigna a HTML automáticamente.
// messages/en.json
{
"cta": "{#link to=|/docs|}Lee la documentación{/link}",
"welcome": "{#b}Hola {name}{/b}{#icon/}"
}
<script lang="ts">
import { ParaglideMessage } from '@inlang/paraglide-js-svelte';
import { m } from '$lib/paraglide/messages';
const name = $props().name; // o cualquier $state
</script>
<ParaglideMessage message={m.welcome} inputs={{ name }}>
{#snippet b({ children })}
<strong>{@render children?.()}</strong>
{/snippet}
{#snippet icon({ attributes })}
<Icon variant={attributes?.variant} />
{/snippet}
</ParaglideMessage>
<ParaglideMessage> usa message.parts() cuando existe marcado y recurre a message() para texto plano; la sintaxis {#snippet tag({ children, options, attributes })} es exactamente el patrón de snippets de Svelte 5 — sin slots, sin el legado de <slot>. Ten en cuenta la sintaxis literal to=|/docs| (delimitada por tuberías) y que m.welcome() solo devuelve texto plano sin marcado.
5. Integración de Runes de Svelte 5: Qué es Reactivo (y Qué No)
El modelo mental clave: Paraglide posee la configuración regional como estado de documento/solicitud; los runes de Svelte 5 poseen el estado de la UI. Dado que los cambios de locale envían un nuevo documento, rara vez necesitas runes para rastrear getLocale(). Patrones que surgen en la práctica:
Valores derivados dependientes de locale dentro de un solo documento (por ejemplo, una superficie solo de cliente que usa reload: false, o un panel de vista previa que renderiza múltiples locales explícitamente):
<script lang="ts">
import { getLocale, locales } from '$lib/paraglide/runtime';
import { m } from '$lib/paraglide/messages';
let { locale = getLocale() } = $props(); // props tipados
// $derived se reevalúa cuando la prop `locale` cambia — no cuando
// el locale interno de Paraglide cambia (no lo hace, sin navegación):
const greeting = $derived(m.greeting({ name: 'Ada' }, { locale }));
const isRtl = $derived(getTextDirection(locale) === 'rtl');
</script>
Pasar el locale forzado hacia abajo en lugar de leer el estado global es el patrón compatible con runes: las opciones { locale } en cada llamada de mensaje hacen que los componentes sean funciones puras de las props, que es exactamente como Svelte 5 quiere que fluya el estado. Reserva $effect para efectos secundarios reales (por ejemplo, persistir un locale de vista previa) — nunca asignes a un $derived, y no escribas $state desde dentro de $effect para "empujar" el locale; pasa props en su lugar.
Estado global del cliente (por ejemplo, "último locale visto" para una herramienta de traducción dentro de la aplicación):
// src/lib/locale-preview.svelte.ts — los archivos .svelte.ts soportan runes en el ámbito del módulo
export const preview = $state({ locale: 'en' as string | undefined });
El $state a nivel de módulo en archivos .svelte.ts se comparte entre todos los importadores — está bien para singletons de toda la aplicación, pero recuerda que no es cómo funciona el locale de producción (esa es la cadena de estrategias + navegación de documento).
6. SSR, Hidratación y SSG
6.1 Reglas de SSR
- Un orden de cadena
handle: colocaparaglideHandleprimero (o antes de los manejadores de autenticación que necesitenevent.localsclaveados por locale) para que los hooks aguas abajo vean la solicitud deslocalizada. - Detrás de un proxy de terminación TLS, pasa
effectiveRequestUrlpara que la coincidencia de URL y las redirecciones utilicen el origen orientado al navegador. - Mantén
AsyncLocalStorageactivado; es lo que hace quegetLocale()sea seguro bajo solicitudes concurrentes y SSR por streaming. La alternativadisableAsyncLocalStoragees solo para entornos de ejecución sinnode:async_hooksy con aislamiento de solicitud garantizado — usarlo en otros lugares puede filtrar la configuración regional entre usuarios.
6.2 Corrección de la hidratación
El servidor y el cliente deben resolver la configuración regional desde el mismo orden de estrategias. La discrepancia clásica: el servidor resuelve desde la URL, pero la estrategia del cliente lee localStorage primero, que no existe en el momento de SSR. Soluciónalo incluyendo una estrategia cookie visible en el servidor cuando una elección persistida debe dar forma a la primera respuesta.
6.3 SSG (adapter-static / prerender)
El prerenderizado nunca ejecuta el middleware — establece la configuración regional por página durante la compilación:
// src/routes/[locale]/+layout.ts
import { setLocale, assertIsLocale } from '$lib/paraglide/runtime';
import type { LayoutLoad } from './$types';
export const prerender = true;
export function load({ params }: { params: { locale: string } }) {
setLocale(assertIsLocale(params.locale));
}
// src/routes/[locale]/+page.ts
import { locales } from '$lib/paraglide/runtime';
export const prerender = true;
export function entries() {
return locales.map((locale) => ({ locale }));
}
SSG requiere que cada locale tenga un prefijo de URL (configura urlPatterns para que /en/… y /de/… existan ambos), de lo contrario SvelteKit no puede emitir archivos distintos por locale. Para el fallback SPA del adaptador estático, establece kit.paths.relative = false para evitar 404 de assets con prefijo de locale, y usa generateStaticLocalizedUrls() para sitemaps.
7. Rendimiento y Tree-Shaking
- Llama a las funciones de mensaje directamente.
m.hello()se compila en una pequeña función ESM; tu bundler elimina automáticamente los mensajes no utilizados y sus traducciones. El tamaño del bundle se mantiene plano a medida que crece el catálogo (benchmark: 47 KB tanto si el proyecto tiene 200 como 1,000 mensajes).