Tutoriales//10 min de lectura

SvelteKit i18n: Creando aplicaciones multilingües con seguridad de tipos en Svelte 5

Eray Gündoğmuş
Compartir

SvelteKit i18n: Creando aplicaciones multilingües con seguridad de tipos en Svelte 5

La arquitectura de SvelteKit se presta sorprendentemente bien a la internacionalización. Sus funciones de carga del lado del servidor se ejecutan antes del renderizado, lo que hace que la detección de idioma y la carga de mensajes sean una integración natural. El enrutamiento basado en URL con jerarquías de diseño significa que puedes prefijar las rutas con /en/ o /de/ sin pelear con el framework. Y las runas de Svelte 5 — $state, $derived, $effect — te ofrecen cambio de idioma reactivo sin el código repetitivo de stores que requería Svelte 4.

Esta guía explica cómo construir una app multilingüe de SvelteKit lista para producción con Svelte 5. Cubriremos la detección de idioma, funciones de traducción con seguridad de tipos, pluralización usando la API Intl, y consideraciones de SEO. También analizaremos honestamente las ventajas y desventajas entre empaquetar archivos de traducción versus servirlos desde una CDN.

Configuración del proyecto

Comienza con un proyecto SvelteKit nuevo (plantilla TypeScript):

npx sv create my-i18n-app
cd my-i18n-app
npm install

Para esta guía mantendremos las dependencias al mínimo — las APIs de Intl integradas en los navegadores modernos gestionan el formateo, y escribiremos una capa de traducción delgada nosotros mismos. Si necesitas soporte para el formato de mensajes ICU, librerías como @messageformat/core pueden integrarse.

Estructura de carpetas que buscamos:

src/
  lib/
    i18n/
      index.ts          # Core translation utilities
      types.ts          # Generated/shared types
      locales/
        en.json
        de.json
        fr.json
  hooks.server.ts       # Locale detection
  routes/
    [locale]/
      +layout.server.ts # Load translations
      +layout.svelte    # Provide translations to tree
      +page.svelte

Define los idiomas soportados en una configuración compartida:

// src/lib/i18n/config.ts
export const SUPPORTED_LOCALES = ['en', 'de', 'fr'] as const;
export type Locale = typeof SUPPORTED_LOCALES[number];
export const DEFAULT_LOCALE: Locale = 'en';

export function isSupportedLocale(locale: string): locale is Locale {
  return SUPPORTED_LOCALES.includes(locale as Locale);
}

Detección de idioma y enrutamiento

El archivo hooks.server.ts de SvelteKit es el lugar adecuado para detectar el idioma del usuario antes de que se ejecute cualquier ruta. Leeremos primero desde la URL, con fallback al encabezado Accept-Language y luego al idioma predeterminado.

// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { DEFAULT_LOCALE, isSupportedLocale } from '$lib/i18n/config';

export const handle: Handle = async ({ event, resolve }) => {
  // Extract locale from URL path segment
  const pathLocale = event.url.pathname.split('/')[1];

  const locale = isSupportedLocale(pathLocale)
    ? pathLocale
    : detectFromHeader(event.request.headers.get('accept-language'));

  event.locals.locale = locale;

  return resolve(event, {
    transformPageChunk: ({ html }) => html.replace('%lang%', locale),
  });
};

function detectFromHeader(header: string | null): import('$lib/i18n/config').Locale {
  if (!header) return DEFAULT_LOCALE;

  const accepted = header
    .split(',')
    .map((part) => {
      const [lang, q = 'q=1'] = part.trim().split(';');
      return { lang: lang.split('-')[0].trim(), q: parseFloat(q.split('=')[1]) };
    })
    .sort((a, b) => b.q - a.q);

  for (const { lang } of accepted) {
    if (isSupportedLocale(lang)) return lang;
  }

  return DEFAULT_LOCALE;
}

Agrega el campo locale al tipo locals de tu app:

// src/app.d.ts
import type { Locale } from '$lib/i18n/config';

declare global {
  namespace App {
    interface Locals {
      locale: Locale;
    }
  }
}

Para el enrutamiento basado en URL, crea un segmento [locale] en el nivel superior y agrega un matcher de parámetros para validarlo:

// src/params/locale.ts
import { isSupportedLocale } from '$lib/i18n/config';

export function match(param: string): boolean {
  return isSupportedLocale(param);
}

Referencia este matcher en tu directorio de rutas: src/routes/[locale=locale]/. SvelteKit solo hará coincidir rutas donde el segmento sea una cadena de idioma válida.

Cargando traducciones

En +layout.server.ts dentro del segmento [locale], carga las traducciones para el idioma actual. Esto se ejecuta en el servidor antes de que se renderice la página, por lo que no habrá destellos de contenido sin traducir.

// src/routes/[locale=locale]/+layout.server.ts
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ params, fetch }) => {
  const locale = params.locale;

  // Option A: Bundled JSON (imported directly)
  const messages = await import(`$lib/i18n/locales/${locale}.json`);

  // Option B: CDN endpoint (no redeploy needed for translation updates)
  // const res = await fetch(`https://cdn.example.com/translations/${locale}.json`);
  // const messages = await res.json();

  return { locale, messages: messages.default };
};

En el componente de diseño, pon las traducciones a disposición de los componentes hijos:

<!-- src/routes/[locale=locale]/+layout.svelte -->
<script lang="ts">
  import { setContext } from 'svelte';
  import { createTranslator } from '$lib/i18n';
  import type { LayoutData } from './$types';

  const { data }: { data: LayoutData } = $props();

  const t = createTranslator(data.messages);
  setContext('t', t);
  setContext('locale', data.locale);
</script>

<slot />

Usando traducciones en componentes con runas de Svelte 5

Con Svelte 5, puedes usar $state y $derived para hacer las traducciones reactivas cuando cambia el idioma. Aquí hay un patrón de store de traducción que funciona limpiamente con las runas:

// src/lib/i18n/index.ts
import type { Messages } from './types';

export function createTranslator(messages: Messages) {
  return function t(key: keyof Messages, params?: Record<string, string | number>): string {
    const template = messages[key] ?? key;

    if (!params) return template;

    return Object.entries(params).reduce<string>(
      (result, [k, v]) => result.replaceAll(`{${k}}`, String(v)),
      template
    );
  };
}

En un componente, obtén el traductor desde el contexto:

<!-- src/routes/[locale=locale]/+page.svelte -->
<script lang="ts">
  import { getContext } from 'svelte';
  import type { Translator } from '$lib/i18n/types';

  const t = getContext<Translator>('t');
  const locale = getContext<string>('locale');

  // Svelte 5: derived state from context
  let greeting = $derived(t('home.greeting', { name: 'World' }));
</script>

<h1>{greeting}</h1>
<p>{t('home.description')}</p>

Para cambiar de idioma sin recargar completamente la página, actualiza el traductor de forma reactiva usando $state:

<script lang="ts">
  import { createTranslator } from '$lib/i18n';
  import type { Messages } from '$lib/i18n/types';

  let messages = $state<Messages>({} as Messages);
  let t = $derived(createTranslator(messages));

  async function switchLocale(newLocale: string) {
    const res = await fetch(`/api/translations/${newLocale}`);
    messages = await res.json();
    // Update URL without reload
    history.pushState({}, '', `/${newLocale}${window.location.pathname.replace(/^\/[^/]+/, '')}`);
  }
</script>

Seguridad de tipos

Una función de traducción con seguridad de tipos detecta claves faltantes en tiempo de compilación y habilita el autocompletado en el IDE. Genera los tipos desde tu archivo de idioma base:

// src/lib/i18n/types.ts
// Auto-generated from en.json — do not edit manually
import type en from './locales/en.json';

export type Messages = typeof en;
export type MessageKey = keyof Messages;
export type Translator = (key: MessageKey, params?: Record<string, string | number>) => string;

Puedes automatizar este paso. Un script simple para regenerar los tipos:

// scripts/generate-i18n-types.ts
import { writeFileSync } from 'fs';

const output = `// Auto-generated from en.json — do not edit manually
import type en from './locales/en.json';

export type Messages = typeof en;
export type MessageKey = keyof Messages;
export type Translator = (key: MessageKey, params?: Record<string, string | number>) => string;
`;

writeFileSync('src/lib/i18n/types.ts', output);
console.log('Types generated.');

Conéctalo a tu proceso de build en package.json:

{
  "scripts": {
    "generate:i18n": "tsx scripts/generate-i18n-types.ts",
    "prebuild": "npm run generate:i18n",
    "predev": "npm run generate:i18n"
  }
}

Ahora t('home.greetng') produce un error de TypeScript porque 'home.greetng' no existe en Messages. Los errores tipográficos se detectan en tiempo de compilación en lugar de ser descubiertos por los usuarios en producción.

Esta es una área donde plataformas como Better i18n agregan valor real: generan tipos de SDK desde tu esquema de traducción automáticamente, de modo que cada vez que un traductor agrega o renombra una clave, tus tipos de TypeScript se actualizan sin necesidad de ejecutar scripts manualmente.

Si estás construyendo aplicaciones con React o Next.js junto a Svelte, encontrarás que los mismos patrones con seguridad de tipos aplican — consulta React i18n: The Complete Guide to React Internationalization y Next.js App Router i18n: Server Components and RSC Patterns para implementaciones específicas de cada framework.

Pluralización y formateo

La API Intl integrada en JavaScript maneja la pluralización correctamente en todos los idiomas sin dependencias externas:

// src/lib/i18n/format.ts

export function pluralize(
  locale: string,
  count: number,
  forms: Record<Intl.LDMLPluralRule, string>
): string {
  const rule = new Intl.PluralRules(locale).select(count);
  const template = forms[rule] ?? forms.other;
  return template.replace('{count}', String(count));
}

export function formatNumber(locale: string, value: number, options?: Intl.NumberFormatOptions): string {
  return new Intl.NumberFormat(locale, options).format(value);
}

export function formatDate(locale: string, date: Date, options?: Intl.DateTimeFormatOptions): string {
  return new Intl.DateTimeFormat(locale, options).format(date);
}

Uso en un componente:

<script lang="ts">
  import { pluralize, formatNumber, formatDate } from '$lib/i18n/format';
  import { getContext } from 'svelte';

  const locale = getContext<string>('locale');

  const itemCount = 3;
  const price = 1499.99;
  const publishedAt = new Date('2024-06-15');

  let itemLabel = $derived(
    pluralize(locale, itemCount, {
      one: '{count} item',
      other: '{count} items',
      zero: 'no items',
      two: '{count} items',
      few: '{count} items',
      many: '{count} items',
    })
  );

  let formattedPrice = $derived(formatNumber(locale, price, { style: 'currency', currency: 'USD' }));
  let formattedDate = $derived(formatDate(locale, publishedAt, { dateStyle: 'long' }));
</script>

<p>{itemLabel}</p>
<p>{formattedPrice}</p>
<p>{formattedDate}</p>

Para el idioma alemán, 1499.99 se muestra como 1.499,99 $ y la fecha como 15. Juni 2024 — sin librerías adicionales.

SEO para SvelteKit multilingüe

Los motores de búsqueda necesitan señales explícitas sobre las versiones en otros idiomas de tus páginas. Agrega etiquetas hreflang y metadatos localizados en el <svelte:head> de tu diseño. Hacerlo correctamente es esencial para el posicionamiento multilingüe — nuestra guía completa de SEO i18n, etiquetas hreflang y URLs de idioma cubre cada detalle de implementación y los errores comunes.

<!-- src/routes/[locale=locale]/+layout.svelte -->
<script lang="ts">
  import { page } from '$app/stores';
  import { SUPPORTED_LOCALES } from '$lib/i18n/config';

  const { data }: { data: LayoutData } = $props();

  // Build alternate URLs for all supported locales
  let alternates = $derived(
    SUPPORTED_LOCALES.map((loc) => ({
      locale: loc,
      url: $page.url.href.replace(`/${data.locale}/`, `/${loc}/`),
    }))
  );
</script>

<svelte:head>
  {#each alternates as { locale, url }}
    <link rel="alternate" hreflang={locale} href={url} />
  {/each}
  <link rel="alternate" hreflang="x-default" href={alternates.find(a => a.locale === 'en')?.url} />
</svelte:head>

<slot />

Para el prerenderizado, configura SvelteKit para generar todas las variantes de idioma:

// svelte.config.js
const config = {
  kit: {
    prerender: {
      entries: [
        '/en',
        '/de',
        '/fr',
        '/en/about',
        '/de/about',
        '/fr/about',
        // ... or generate dynamically
      ],
    },
  },
};

Para entradas de prerenderizado dinámicas, usa un +page.server.ts con la exportación entries:

// src/routes/[locale=locale]/+page.server.ts
import { SUPPORTED_LOCALES } from '$lib/i18n/config';

export function entries() {
  return SUPPORTED_LOCALES.map((locale) => ({ locale }));
}

CDN primero vs traducciones empaquetadas

Ambos enfoques funcionan — la elección correcta depende de tu flujo de trabajo.

Traducciones empaquetadas (importadas desde JSON):

  • Sin solicitudes HTTP adicionales por carga de página
  • Traducciones vinculadas en versión al despliegue
  • Funciona sin conexión y sin dependencias externas
  • Corregir un error tipográfico en una traducción requiere un redespliegue

Entrega por CDN:

  • Las actualizaciones de traducciones se publican sin redesplegar la app
  • Los traductores pueden publicar cambios de forma independiente a los ingenieros
  • Agrega una solicitud de red (mitigada con caché agresivo)
  • Requiere una estrategia de invalidación de caché

Para la mayoría de los equipos que están comenzando con i18n, el JSON empaquetado es más sencillo. A medida que tu equipo de traducción crece y el ritmo de actualizaciones de contenido aumenta, el acoplamiento con el despliegue se vuelve doloroso. Lanzar una versión de código solo para corregir una etiqueta de botón en alemán no escala. Automatizar i18n en tu pipeline de CI/CD es el siguiente paso natural una vez que superas el enfoque empaquetado.

Este es el problema central que plataformas como Better i18n están diseñadas para resolver: tus mensajes de traducción viven en su CDN, servidos a través de URLs versionadas con TTLs de caché largos. Los ingenieros actualizan el código, los traductores actualizan las traducciones — de forma independiente, sin bloquearse mutuamente. La página de características explica cómo funciona esto en detalle, incluyendo cómo se gestiona la invalidación de caché al publicar traducciones.

También existe un punto intermedio: obtener las traducciones en tiempo de build desde una CDN y empaquetar el resultado en el artefacto de despliegue. Obtienes cargas locales rápidas sin empaquetar archivos de idioma en tu repositorio, y aún necesitas un redespliegue para las actualizaciones.

Resumen

SvelteKit y Svelte 5 forman una base sólida para aplicaciones multilingües:

  • Hooks gestionan la detección de idioma en el servidor antes de que ocurra cualquier renderizado
  • Segmentos de ruta dinámicos con matchers de parámetros te dan un enrutamiento de idioma limpio basado en URL
  • Funciones de carga obtienen las traducciones una vez por diseño y las pasan por el árbol de componentes
  • Runas de Svelte 5 ($state, $derived) hacen el cambio de idioma reactivo sin suscripciones manuales a stores
  • Los tipos de TypeScript generados desde tu idioma base detectan claves faltantes o con errores tipográficos en tiempo de compilación
  • Las APIs Intl manejan la pluralización y el formateo de números/fechas correctamente para cada idioma
  • Las etiquetas hreflang de svelte:head y las rutas de idioma prerenderizadas mantienen informados a los motores de búsqueda

La arquitectura descrita aquí maneja la mayoría de los requisitos de producción. El principal vacío que deja es operacional: a medida que tu app crece, gestionar archivos de traducción dentro del repositorio crea fricción entre ingenieros y traductores. La entrega por CDN y la generación automática de tipos son las dos áreas donde las herramientas diseñadas específicamente para este propósito generan mayor valor.

Si estás construyendo una app de SvelteKit con un equipo de traducción real, echa un vistazo a la integración de Better i18n con Svelte — proporciona un SDK con seguridad de tipos, entrega por CDN lista para usar, y un flujo de revisión basado en Git para que los cambios de traducción pasen por el mismo proceso de revisión que el código.

Better i18n es una plataforma de localización centrada en el desarrollador, construida para equipos de frontend modernos. SDKs con seguridad de tipos, flujos de trabajo basados en Git, entrega por CDN y traducción con IA con aplicación de glosario — sin archivos de idioma en tu repositorio.


Lleva tu app al mundo con better-i18n

better-i18n combina traducciones impulsadas por IA, flujos de trabajo nativos de git y entrega por CDN global en una plataforma centrada en el desarrollador. Deja de gestionar hojas de cálculo y empieza a publicar en todos los idiomas.

Empieza gratis → · Explora las características · Lee la documentación

Comments

Loading comments...