Internacionalización
La web documental de este módulo se publica en https://i18n.meekojs.com y vive en apps/i18n. Incluye guías y API, con un laboratorio de interpolación, plurales, selectores, formatos y RTL usando catálogos reales de español, inglés y árabe. Se inicia con pnpm i18n:dev y se construye con pnpm i18n:build. Comparte la base documental privada packages/docs con la web de UI y no necesita una base de datos.
@meekojs/i18n es un módulo independiente de Meeko. Los catálogos se validan y compilan al preparar la aplicación; sus claves y parámetros se generan para TypeScript. La configuración no obliga a usar UI ni instala una segunda copia del framework.
Configurar catálogos#
// meeko.config.ts
import { defineMeekoConfig } from '@meekojs/meeko/framework'
export default defineMeekoConfig({
modules: ['@meekojs/i18n'],
i18n: {
defaultLocale: 'es',
catalogs: {
es: { common: 'i18n/es/common.json' },
en: { common: 'i18n/en/common.json' },
},
},
})El módulo se registra por su nombre y sus opciones van en la clave i18n (su configKey), tipada sin importar nada: Meeko carga sus tipos en .meeko/types/config.d.ts (véase Módulos). '@meekojs/i18n' resuelve el subpath @meekojs/i18n/module.
Cada locale necesita un namespace common. Los idiomas deben declarar los mismos namespaces, claves y contratos de parámetros. Las rutas de los catálogos deben permanecer dentro del proyecto. Los catálogos se cargan por idioma y namespace; routes permite añadir namespaces a IDs de ruta concretos. global lista namespaces que se cargan siempre junto a common, en todas las rutas, sus versiones /en/… y la página de error, como los textos de un Shell compartido (global: ['shell']); cada nombre debe existir en todos los idiomas y no puede ser common. directions permite marcar idiomas rtl.
Una clave cuyo namespace no se cargó en la ruta muestra la propia clave en lugar de romper el documento, y el módulo avisa una vez por idioma y clave en el servidor (registro y prerender) y, con meeko dev, también en el navegador: [meeko:i18n] Missing message …. createI18n lanza ReferenceError salvo que reciba missing(key, locale).
Mensajes y parámetros tipados#
{
"welcome": "Hola {name}",
"ready": "Listo",
"items": {
"plural": "total",
"forms": {
"one": "{owner} tiene {count} elemento",
"other": "{owner} tiene {count} elementos"
}
}
}const { t } = useI18n()
t('welcome', { name: 'Diego' })
t('ready')
t('items', { total: 3, owner: 'El equipo' })Los parámetros requeridos no pueden omitirse ni ser null o undefined. Los selectores de plural requieren number; {count} es el alias numérico que el runtime proporciona a sus formas. Los parámetros adicionales utilizados por cualquier forma también son obligatorios. select requiere su selector y los parámetros de sus formas. Toda estructura plural o select debe declarar other.
meeko check detecta claves desconocidas, parámetros faltantes y tipos incorrectos. Al preparar el módulo, dos idiomas que usan {name} y {user} para la misma clave producen un error de contrato. Las categorías de plural pueden variar entre idiomas. Los registros antiguos de claves declaradas como true mantienen su contrato abierto; los registros generados nuevos incluyen los parámetros.
Los tipos I18n, I18nMessageKey, I18nMessageTags<'items'>, I18nMessageValues<'items'>, I18nRichPart, I18nTranslateOptions e I18nValue están disponibles mediante import type desde @meekojs/i18n. Sin un registro generado, createI18n conserva una API abierta y las validaciones de ejecución.
Una clave dinámica de tipo unión necesita parámetros válidos para todas sus alternativas. Si una de ellas requiere valores, no pueden omitirse aunque otra sea un mensaje estático. Puedes estrechar la clave con un if para pasar sólo los parámetros de ese caso.
Mensajes con marcado#
Para intercalar un elemento o un componente en una frase, escribe la etiqueta en el mensaje y píntalo con <I18nT>, que pide un snippet por etiqueta. La frase queda entera en el catálogo, así que cada idioma ordena sus palabras:
{ "built": "Hecho con <code>.meeko</code> por {name}. Lee <a>la guía</a>." }<I18nT key="built" bind:values="{ name: author.value }">
<snippet name="code" params="text"><code>{text}</code></snippet>
<snippet name="a" params="text"><UILink to="/guide">{text}</UILink></snippet>
</I18nT>Cada <name>…</name> o <name/> es un snippet que recibe su texto, con los parámetros ya sustituidos, y pertenece al componente que lo escribe: lee sus señales y sus eventos y conserva su <style scoped>. <I18nT> no añade elementos alrededor; una etiqueta sin snippet muestra su texto. key y values se tipan como en t(), y locale pide el mensaje en otro idioma. Funciona en SSR, prerender e hidratación; cambiar values actualiza el texto sin volver a crear los nodos de los snippets, y un cambio de forma de un plural cambia los trozos de la frase.
Seguridad. No se usa innerHTML. Sólo el texto del catálogo forma etiquetas, antes de sustituir los parámetros: un valor con <img onerror=…> se pinta como texto. Las etiquetas no admiten atributos: el href y las clases los decide el snippet, no el traductor.
meeko check exige los snippets de las etiquetas de ese mensaje y rechaza los que no tiene (I18nMessageTags<K>). Al preparar el módulo, dos idiomas con etiquetas distintas para la misma clave o una etiqueta dentro de otra son un error. Lo que no forma una pareja (Array<T>, <a href=x>) es texto. t() devuelve el texto tal cual, con las etiquetas; i18n.rich(key, values?, options?) devuelve los trozos, { text, tag? }, para pintarlos por tu cuenta.
Texto en otro idioma#
Cada documento carga sólo el catálogo del idioma de su URL. Para un texto escrito en otro idioma, como una sugerencia en el idioma sugerido, declara un namespace pequeño en multilingual, que se carga en todos los idiomas:
i18n: {
defaultLocale: 'es',
catalogs: {
es: { common: 'i18n/es/common.json', suggestion: 'i18n/es/suggestion.json' },
en: { common: 'i18n/en/common.json', suggestion: 'i18n/en/suggestion.json' },
},
multilingual: ['suggestion'],
}t('suggestion.go', {}, { locale: 'en' }) es síncrono y usa las reglas de plural de ese idioma, igual en SSR e hidratación. Una clave de otro namespace se trata como un mensaje sin cargar. Cada nombre debe existir en todos los idiomas y no puede ser common. Cada namespace añade un chunk por idioma a cada documento.
El módulo no incluye un componente de sugerencia: multilingual resuelve también otros textos en otro idioma y cada sitio conserva su diseño. La receta está en Sugerir el idioma sin redirigir.
En <script server>#
useI18n() no existe en el <script server>, que también se ejecuta en la petición de datos de una navegación, sin aplicación ni plugins. usePageI18n(), autoimportado en el código de servidor, da el idioma de la URL y el traductor:
<script server lang="ts">
const { params } = usePage()
const { locale, t } = await usePageI18n()
export const doc = await readDoc(params.slug, locale)
export const head = { title: t('docs.title', { name: doc.title }) }
</script>locale se tipa con los idiomas de los catálogos (I18nLocale) y están cargados todos los namespaces de todos los idiomas, que en el servidor no pesan en ningún bundle. Como usePage(), sólo vale en el código de nivel superior de una página o de un middleware de ruta de servidor: en otro sitio da MEEK073. Se importa de @meekojs/i18n/server, junto a los tipos PageI18n e I18nLocale.
Formatos y cambio de idioma#
useI18n() expone locale, defaultLocale, locales, dir, t, rich, has, number, date, localePath, switchLocalePath, pageLocales, localeName, directionOf, suggestedLocale, rememberLocale y switchLocale. Los formateadores Intl se reutilizan por opciones. Especifica timeZone al formatear fechas que deban coincidir entre SSR y navegador. switchLocale guarda el idioma en la cookie y navega a la ruta equivalente, con su búsqueda y fragmento, en un documento nuevo.
Rutas localizadas#
Con strategy: 'prefix_except_default' (la opción por defecto), el idioma por defecto no lleva prefijo y cada otro idioma vive bajo el suyo: /guide y /en/guide. El módulo genera las rutas de cada idioma a partir de las páginas existentes con meeko.extendRoutes: comparten archivo, layouts, middleware y opciones de ruta (render, prerenderEntries, sitemapEntries), así que el SSR, la hidratación, el prerender, el sitemap y los tipos de rutas las tratan como cualquier otra. /es/guide no existe.
i18n: {
defaultLocale: 'es',
catalogs: { es: { common: 'app/locales/es/common.json' }, en: { common: 'app/locales/en/common.json' } },
exclude: ['/admin/*'], // sin versiones en otros idiomas ni hreflang
partial: { ar: ['/playground'] }, // un idioma con traducción parcial sólo tiene estas rutas
cookie: { name: 'meeko-locale', domain: 'example.com' }, // o false
hreflang: true, // <link rel="alternate" hreflang> y x-default
ogLocale: true, // og:locale y og:locale:alternate; o { es: 'es_MX' }, o false
sitemap: true, // alternativas xhtml:link en sitemap.xml
}Una página del proyecto en la URL de otro idioma, como app/pages/en/legal.meeko, sustituye a la ruta generada; así se escribe la prosa de una página en cada idioma. Las rutas cuyo primer segmento ya es un idioma o [locale] no se vuelven a localizar. Los namespaces de routes se declaran con el ID sin prefijo y se cargan también en sus versiones /en/….
Enlaza con <MeekoLink to="/guide"> (o <UILink to>): con Meeko 1.8 el módulo registra un resolvedor de enlaces que añade el prefijo del idioma del documento y conserva el tipado de to, aria-current y la navegación de cliente; hreflang="es" fuerza otro idioma y bind:resolve="false" deja la ruta tal cual. localePath('/guide') sirve para <a>, código y URL con fragmento. Un selector usa enlaces reales a switchLocalePath(code) para cada idioma de pageLocales(), con localeName(code) como texto; al seguirlos, el módulo guarda la elección en la cookie.
hreflang, sitemap y sugerencia de idioma#
Cada página localizada lleva <html lang dir>, un <link rel="alternate" hreflang> por idioma con versión de la página y x-default hacia el idioma por defecto, con URL absolutas a partir de site (o MEEKO_PUBLIC_ORIGIN al construir; sin ninguno, el origen de la petición). Con sitemap: true, el sitemap.xml de Meeko lista cada versión con sus xhtml:link, mediante el hook sitemap:resolved. Una aplicación que sirve el sitemap desde una base de datos lo escribe con sitemapXml.
También emite og:locale y un og:locale:alternate por cada otra versión de la página, con la región más probable de cada idioma (es_ES, en_US). ogLocale: { es: 'es_MX' } la elige y ogLocale: false los desactiva; un og:locale de la página o del layout gana.
La cookie con domain se comparte entre subdominios. En un host que no pertenece a ese dominio (localhost, una vista previa), donde el navegador la rechazaría, se escribe sin Domain, así que no hace falta configurarla por entorno.
El módulo nunca redirige según Accept-Language ni la cookie: cada URL tiene un único contenido que los buscadores indexan y las cachés comparten. suggestedLocale() devuelve, sólo en el navegador, el idioma de la cookie o de navigator.languages cuando no es el del documento; llámalo en onMounted para mostrar un aviso, y rememberLocale(locale) al descartarlo. negotiateLocale(header, locales) y readLocaleCookie(cookies, name, locales) están disponibles para el servidor.
Los catálogos de un paquete enlazado (por ejemplo, un Shell compartido en node_modules/meeko-docs/locales/…) se aceptan aunque su ruta real esté fuera del proyecto; una ruta escrita fuera del proyecto se rechaza.
Validación#
pnpm -F @meekojs/i18n build
pnpm -F @meekojs/i18n check
pnpm -F @meekojs/i18n testLas pruebas incluyen contratos de texto, select y plural, consumidores TypeScript y componentes .meeko, build SSR, eliminación de tipos al desactivar el módulo, rutas, cookie y negociación de idioma, <I18nT> en SSR, hidratación y actualización con valores hostiles, y builds estáticos con rutas /en/…, hreflang, og:locale, usePageI18n, namespaces perezosos, globales (también en la página de error) y multilingües, claves sin cargar y sitemap localizado.
Guía de integración#
Necesita Meeko 1.8 (meeko.extendRoutes y el hook sitemap:resolved). Para un sitemap dinámico (sitemap: false y sitemapXml), localeAlternates(pathname, routing, available?) da las alternativas de cada URL.
Esta guía es la receta para localizar una web Meeko con español sin prefijo (las URL de siempre) e inglés bajo /en/….
1. Declarar los idiomas y la estrategia#
// meeko.config.ts
import { defineMeekoConfig } from '@meekojs/meeko/framework'
export default defineMeekoConfig({
// Origen público: URL absolutas de hreflang y del sitemap (o MEEKO_PUBLIC_ORIGIN al construir).
site: 'https://meekojs.com',
sitemap: true,
modules: ['@meekojs/i18n'],
i18n: {
defaultLocale: 'es',
// Por defecto: es sin prefijo y en con /en/…
strategy: 'prefix_except_default',
catalogs: {
es: { common: 'app/locales/es/common.json', docs: 'app/locales/es/docs.json' },
en: { common: 'app/locales/en/common.json', docs: 'app/locales/en/docs.json' },
},
// Namespaces que se cargan de forma perezosa en una ruta (ID sin prefijo de idioma).
routes: { '/docs/[...slug]': ['docs'] },
// Rutas sin versión en otros idiomas ni hreflang.
exclude: ['/admin/*'],
// Cookie del idioma elegido, compartida entre subdominios (sólo para sugerir).
cookie: { name: 'meeko-locale', domain: 'meekojs.com' },
},
})El módulo se registra por su nombre y sus opciones van en la clave i18n, tipada sin importar nada. Con esta configuración:
- Cada página existente tiene su versión inglesa sin duplicar archivos:
app/pages/guide.meekosirve/guidey/en/guide;app/pages/docs/[...slug].meeko,/docs/…y/en/docs/…. Las rutas generadas comparten archivo, layouts, middleware,render,prerenderEntriesysitemapEntries; las páginas de layers y módulos también se localizan. /es/guideno existe (404): el idioma por defecto nunca lleva prefijo.<html lang dir>sigue al idioma de la URL y cada página localizada declara<link rel="alternate" hreflang>por idioma másx-default(el idioma por defecto).sitemap.xmllista cada versión con sus alternativasxhtml:link.partial: { ar: ['/playground'] }limita un idioma con traducción parcial a esas rutas.
2. Extraer los textos a catálogos#
- Un archivo JSON por idioma y namespace:
app/locales/<idioma>/<namespace>.json. Todos los idiomas declaran los mismos namespaces, las mismas claves y los mismos parámetros; el módulo falla al preparar si no. commonse carga en todas las rutas: navegación, cabecera, pie, botones, accesibilidad, estados vacíos y errores. Los namespaces deglobal(global: ['shell']) se cargan siempre junto acommon: en todas las rutas, sus versiones/en/…y la página de error, en SSR, prerender, hidratación y navegación de cliente. Los demás namespaces se cargan sólo en las rutas deroutes.- Una clave sin cargar (de un namespace que la ruta no carga) muestra la propia clave en lugar de romper la página, y avisa una vez por idioma y clave en el servidor (registro, prerender) y, con
meeko dev, también en la consola del navegador. Lanzar un error tumbaría el documento entero, incluida la página de error. Si aparece[meeko:i18n] Missing message …, añade su namespace aglobalo a la ruta enroutes. - Las claves son globales (no se repiten entre namespaces), así que empiezan por un prefijo de su área:
nav.*,actions.*,a11y.*,footer.*,search.*encommon;docs.*endocs;dashboard.*endashboard. El JSON anidado se aplana con puntos:{ "nav": { "guide": "Guía" } }esnav.guide. - Parámetros con
{name}; plurales y selectores con{ "plural": "total", "forms": { "=0": …, "one": …, "other": … } }y{ "select": "role", "forms": { …, "other": … } }. meeko checkcomprueba las claves y los parámetros de cadat().- Un Shell o layer compartido guarda sus catálogos en su paquete y la aplicación los declara como un namespace más, con la ruta dentro de
node_modules, y lo añade aglobalpara que sus textos estén en todas las páginas, incluida la de error. Una ruta escrita fuera del proyecto se rechaza; un paquete enlazado por pnpm se acepta.
i18n: {
defaultLocale: 'es',
catalogs: {
es: { common: 'app/locales/es/common.json', shell: 'node_modules/meeko-docs/locales/es/shell.json' },
en: { common: 'app/locales/en/common.json', shell: 'node_modules/meeko-docs/locales/en/shell.json' },
},
// Cada nombre existe en todos los idiomas de catalogs y no repite common.
global: ['shell'],
}<script lang="ts">
const { t } = useI18n()
</script>
<template>
<button type="button">{t('actions.copy')}</button>
<p>{t('search.results', { total: 3 })}</p>
</template>useI18n se autoimporta en los componentes (import { useI18n } from '@meekojs/i18n' en archivos .ts).
Mensajes con marcado#
Para intercalar un elemento o un componente, escribe la etiqueta en el mensaje y pinta el mensaje con <I18nT>. La frase queda entera en el catálogo, así que cada idioma ordena sus palabras:
{ "footer": { "built": "Hecho con <code>.meeko</code> por {name}. Lee <a>la guía</a>." } }{ "footer": { "built": "Made by {name} with <code>.meeko</code>. Read <a>the guide</a>." } }<template>
<p>
<I18nT
key="footer.built"
bind:values="{ name: author.value }"
>
<snippet
name="code"
params="text"
>
<code>{text}</code>
</snippet>
<snippet
name="a"
params="text"
>
<UILink to="/guide">{text}</UILink>
</snippet>
</I18nT>
</p>
</template>- Cada etiqueta
<name>…</name>(o<name/>, sin texto) es un snippet que recibe su texto, con los parámetros ya sustituidos. Pertenece a tu componente: lee sus señales y sus eventos y conserva su<style scoped>.<I18nT>no añade elementos alrededor y una etiqueta sin snippet muestra su texto. - Es seguro. No se usa
innerHTML. Sólo el texto del catálogo forma etiquetas: un valor con<img onerror=…>se pinta como texto. Las etiquetas no admiten atributos: elhrefy las clases los decide el snippet, no el traductor. meeko checkexige los snippets de las etiquetas de ese mensaje y rechaza los que no tiene. Todos los idiomas deben usar las mismas etiquetas y una etiqueta no puede contener otra; si no, el módulo falla al preparar.t()devuelve el texto tal cual, con las etiquetas. Lo que no forma una pareja (Array<T>,<a href=x>) sigue siendo texto también en<I18nT>.i18n.rich(key, values?)devuelve los trozos ({ text, tag? }) si lo pintas por tu cuenta.
Funciona en SSR, prerender e hidratación, y al cambiar values actualiza el texto sin volver a crear los nodos de los snippets.
3. Localizar la prosa de las páginas .meeko#
Elige por tipo de contenido:
- Interfaz y textos cortos (títulos de sección, etiquetas, descripciones de
DocsPage): claves de catálogo cont(). - Una página de prosa escrita a mano: un archivo por idioma.
app/pages/en/legal.meekosustituye a la ruta/en/legalque el módulo generaría desdeapp/pages/legal.meeko; las demás páginas siguen reutilizando el archivo español. - Documentación generada desde Markdown o base de datos: el
<script server>de la página lee el idioma conusePageI18n()y pide el contenido de ese idioma (con fallback al idioma por defecto si falta la traducción):
<script server lang="ts">
const { params } = usePage()
const { locale, t } = await usePageI18n()
export const doc = await readDoc(params.slug, locale)
export const head = { title: t('docs.title', { name: doc.title }) }
</script>useI18n() no existe ahí: ese bloque también se ejecuta en la petición de datos de una navegación, sin aplicación ni plugins. usePageI18n() se autoimporta en el código de servidor (@meekojs/i18n/server), toma el idioma de la URL, lo tipa con los idiomas de los catálogos y tiene todos los namespaces cargados. Como usePage(), sólo vale en el código de nivel superior de una página.
Sin traducción, una ruta /en/… muestra el mismo contenido que la española; usa exclude o partial para no publicar versiones que aún no existen.
4. Enlazar#
Escribe los enlaces internos con <MeekoLink to> (o <UILink to> / <UIButton to>) y la ruta sin prefijo: el módulo registra un resolvedor de enlaces (app.resolveLink, Meeko 1.8) que añade el prefijo del idioma del documento en el SSR, el prerender, la hidratación y la navegación del cliente.
<script lang="ts">
const i18n = useI18n()
</script>
<template>
<!-- /guide en español, /en/guide en inglés: tipado de `to`, aria-current y navegación de cliente -->
<MeekoLink to="/guide">{i18n.t('nav.guide')}</MeekoLink>
<MeekoLink
to="/docs/[...slug]"
bind:params="{ slug: 'intro' }"
bind:search="{ tab: 'setup' }"
>
Intro
</MeekoLink>
<UILink to="/installation">{i18n.t('actions.start')}</UILink>
<!-- Otro idioma: hreflang elige la versión (y queda en el <a>) -->
<MeekoLink
to="/guide"
hreflang="es"
>
Versión en español
</MeekoLink>
<!-- Sin prefijo, tal cual -->
<MeekoLink
to="/guide"
bind:resolve="false"
>
Siempre /guide
</MeekoLink>
<!-- Con fragmento o una ruta que no es de la aplicación: localePath -->
<a bind:href="i18n.localePath('/docs/intro#setup')">Intro</a>
</template>- Reglas del resolvedor.
to="/guide"da/guideen el idioma por defecto y/en/guideen inglés.hreflangelige un idioma configurado (exacto o por idioma base:en-USesen); uno desconocido se ignora. Elhrefqueda como lo construyehref()cuandotoya nombra un idioma (/en/legal,/[locale]/…), cuando la ruta está enexcludey cuando el idioma de destino no tiene versión de ella (partial): enlaza a la versión por defecto.bind:resolve="false"lo desactiva en un enlace. - Estado activo.
aria-currentcompara elhrefresuelto con la ruta, así que<MeekoLink to="/guide">está activo en/en/guide. Fuera de<MeekoLink>, compara rutas sin prefijo:stripLocale(route.path.value, i18n.locales)(bajo/en, el ID de ruta activo es/en/guide). - Un enlace del mismo idioma es una navegación de cliente; uno a otro idioma (por ejemplo con
hreflang), una navegación completa que guarda la cookie del idioma. localePath(path, locale?)sigue disponible para<a>, URL con fragmento y código: conserva búsqueda y fragmento.
5. Selector de idioma y hreflang#
<script lang="ts">
const i18n = useI18n()
</script>
<template>
<nav aria-label="Idioma">
<a
each:of="i18n.pageLocales()"
each:as="code"
each:key="code"
bind:href="i18n.switchLocalePath(code)"
bind:hreflang="code"
bind:lang="code"
bind:aria-current="code === i18n.locale ? 'page' : undefined"
>
{i18n.localeName(code)}
</a>
</nav>
</template>switchLocalePath(code)es la ruta equivalente de la página actual (con búsqueda y fragmento) ypageLocales()los idiomas que tienen versión de ella. Usa enlaces reales: funcionan sin JavaScript y los buscadores los siguen. Al seguirlos, el módulo guarda la elección en la cookie.hreflang,x-default,langydirno necesitan código: los añade el módulo (hreflang: falselo desactiva). Declarasitepara que las URL sean absolutas y estables.og:localey unog:locale:alternatepor cada otra versión de la página, también. Usan la región más probable de cada idioma (es_ES,en_US);ogLocale: { es: 'es_MX' }la elige yogLocale: falselos desactiva. Unog:localede la página o del layout gana.- La cookie entre subdominios. Con
cookie: { name: 'meeko-locale', domain: 'meekojs.com' }, los subdominios comparten la elección. En un host que no pertenece a ese dominio (localhost, una vista previa) la cookie se escribe sinDomain, así que no hace falta configurarla por entorno.
6. Sugerir el idioma sin redirigir#
Las URL sin prefijo siempre sirven el idioma por defecto: el módulo nunca redirige según Accept-Language ni la cookie. Una redirección haría que los buscadores (que suelen rastrear sin Accept-Language o en inglés) no vieran nunca una de las versiones, rompería la correspondencia hreflang ↔ URL y obligaría a variar la caché por cabecera. En su lugar, muestra una sugerencia, escrita en el idioma que ofrece. Sólo se carga el catálogo del idioma del documento, así que declara sus textos como un namespace pequeño de multilingual, que se carga en todos los idiomas:
i18n: {
defaultLocale: 'es',
catalogs: {
es: { common: 'app/locales/es/common.json', suggestion: 'app/locales/es/suggestion.json' },
en: { common: 'app/locales/en/common.json', suggestion: 'app/locales/en/suggestion.json' },
},
multilingual: ['suggestion'],
}<script lang="ts">
const i18n = useI18n()
const suggestion = signal<string | undefined>(undefined)
onMounted(() => {
suggestion.value = i18n.suggestedLocale()
})
</script>
<template>
<aside
if:when="suggestion.value"
bind:lang="suggestion.value"
>
<p role="status">{i18n.t('suggestion.message', {}, { locale: suggestion.value! })}</p>
<a bind:href="i18n.switchLocalePath(suggestion.value!)">{i18n.t('suggestion.go', {}, { locale: suggestion.value! })}</a>
<button
type="button"
on:click="() => { i18n.rememberLocale(i18n.locale); suggestion.value = undefined }"
>
{i18n.t('suggestion.stay')}
</button>
</aside>
</template>t(key, values, { locale }) es síncrono y usa las reglas de plural de ese idioma, igual en SSR e hidratación. Sólo conoce los namespaces de multilingual: otra clave se trata como un mensaje sin cargar. Cada namespace de multilingual añade un chunk por idioma a cada documento, así que debe ser pequeño. Haz que el aviso flote (position: fixed) para que su llegada tras hidratar no desplace la página.
suggestedLocale() lee la cookie y, sin ella, navigator.languages; en el servidor devuelve undefined, por eso se llama en onMounted (el HTML de SSR y la hidratación coinciden). En el servidor, negotiateLocale(request.headers.get('accept-language'), locales) y readLocaleCookie(...) sirven para otras decisiones, nunca para cambiar el contenido de una URL.
API#
useI18n() devuelve:
| Miembro | Descripción |
|---|---|
locale, defaultLocale, locales, dir |
Idioma del documento, idioma sin prefijo, idiomas configurados y dirección. |
t(key, values?, { locale }?) |
Mensaje con clave y parámetros tipados desde los catálogos; en otro idioma, con multilingual. |
rich(key, values?, { locale }?) |
El mensaje partido por sus etiquetas ({ text, tag? }), la base de <I18nT>. |
has(key, { locale }?) |
Si la clave tiene mensaje en los catálogos cargados, para una clave que llega de un dato. |
number(value, options?), date(value, options?) |
Intl.NumberFormat y Intl.DateTimeFormat del idioma, cacheados por opciones. |
localePath(path, locale?) |
Ruta interna en un idioma (por defecto, el actual). |
switchLocalePath(locale) |
Ruta equivalente de la página actual en otro idioma. Reactiva. |
pageLocales() |
Idiomas con versión de la página actual. Reactiva. |
localeName(locale, displayLocale?) |
Nombre del idioma (English, Español), por defecto en ese idioma. |
directionOf(locale) |
ltr o rtl. |
suggestedLocale() |
Idioma preferido por el lector si no es el actual; sólo en el navegador. |
rememberLocale(locale) |
Guarda el idioma en la cookie. |
switchLocale(locale) |
Guarda el idioma y navega a la ruta equivalente (documento nuevo). |
<I18nT key values? locale?> pinta un mensaje con marcado, con un snippet por etiqueta. usePageI18n(), de @meekojs/i18n/server, da el idioma y el traductor en un <script server>. Los dos se autoimportan.
Exports de la raíz: useI18n, createI18n, i18nContext, localeOf, localizedPath, stripLocale, negotiateLocale, readLocaleCookie y los tipos I18n, I18nCookie, I18nDirection, I18nMessageKey, I18nMessageTags, I18nMessageValues, I18nRichPart, I18nTranslateOptions, I18nValue, CreateI18nOptions. De @meekojs/i18n/server: usePageI18n y los tipos PageI18n e I18nLocale. '@meekojs/i18n' en modules resuelve el subpath @meekojs/i18n/module.
Opciones del módulo: defaultLocale, catalogs, global, multilingual, routes, directions, strategy, exclude, partial, cookie, hreflang, ogLocale y sitemap (véanse las secciones anteriores).
SSR, prerender y Cloudflare#
El idioma sale de la URL, así que el SSR, la hidratación, el prerender y el adaptador estático producen el mismo HTML para cada URL sin leer cabeceras. Los catálogos se compilan al construir y se cargan por idioma y namespace con import(); el runtime sólo usa Intl y funciona igual en Node, Workers, Bun y Deno.