Manual/Referencia

Internacionalización (i18n)

La interfaz de Meratyn es traducible. Las cadenas de la interfaz viven en catálogos de mensajes JSON en lugar de estar codificadas en los componentes, así que la app puede distribuirse en cualquier idioma añadiendo un catálogo. La capa está construida sobre react-i18next y vive en apps/mappview/src/i18n/.

La aplicación de gestión tiene un catálogo separado en apps/admin/src/i18n/; actualmente incluye inglés y español porque sus textos y su cadencia de lanzamiento difieren del espacio de trabajo de mapas. El idioma de un usuario con sesión iniciada se guarda en su perfil de Meratyn y solo lo devuelve GET /api/users/me, de modo que los perfiles públicos no exponen la preferencia. UserLanguageSync aplica ese ajuste a i18next y a los atributos lang y dir del documento. Los visitantes sin sesión recurren al idioma de su navegador. Añade nuevos idiomas de Meratyn a su languages.ts, a los catálogos de configuración regional y a la validación de idiomas soportados del servicio, juntos.

El escritorio distribuye 19 catálogos de configuración regional: el catálogo base en inglés más 18 configuraciones regionales seleccionables no inglesas. Cada catálogo lleva el conjunto completo de claves actual del inglés. Cuando una traducción nativa aún está pendiente de revisión, el catálogo contiene deliberadamente el valor fuente en inglés en lugar de omitir la clave. bun run --filter admin check:i18n rechaza nuevo texto JSX codificado, y la prueba de catálogos mantiene todos los catálogos y sus marcadores de interpolación alineados.

Cómo funciona

apps/mappview/src/i18n/
├── index.ts          # init de i18next, autodescubrimiento de catálogos, detección de idioma
├── languages.ts      # nombres amigables de idiomas + ayudantes de resolución
├── i18next.d.ts      # tipa las claves de t() contra el catálogo en inglés
└── locales/
    ├── en.json       # catálogo base en inglés (fuente de verdad)
    ├── zh.json       # Chino simplificado
    ├── es.json       # Español
    ├── fr.json       # Francés
    ├── de.json       # Alemán
    ├── pt.json       # Portugués
    ├── it.json       # Italiano
    ├── nl.json       # Neerlandés
    ├── ja.json       # Japonés
    ├── ko.json       # Coreano
    ├── ru.json       # Ruso
    ├── tr.json       # Turco
    ├── id.json       # Indonesio
    ├── hi.json       # Hindi
    ├── ar.json       # Árabe (derecha a izquierda)
    ├── th.json       # Tailandés
    ├── ka.json       # Georgiano
    ├── fa.json       # Persa (derecha a izquierda)
    └── vi.json       # Vietnamita

Los catálogos que no están en inglés se sembraron con traducciones asistidas por máquina y están abiertos a revisión y corrección de hablantes nativos. Los catálogos persa y vietnamita están completos estructuralmente, pero conservan valores de respaldo explícitos en inglés donde la traducción aún no cubre una clave específica de Meratyn; las pruebas mantienen visible esa cobertura sin afirmar que la interfaz esté completamente traducida.

  • en.json es la fuente de verdad. Toda clave usada por t() debe existir aquí; i18next.d.ts tipa la función t() contra él, así que una clave faltante o mal escrita es un error de compilación (bun run typecheck).
  • Los catálogos se autodescubren. index.ts toma cada locales/*.json vía import.meta.glob, así que añadir locales/fr.json es todo lo que hace falta para que el francés sea seleccionable — sin cambios de código.
  • El inglés va en el bundle; las demás configuraciones regionales se cargan con pereza. en.json viaja en el chunk de la app (es el respaldo y la fuente de tipos). Cada otro catálogo es un chunk separado (i18n-locale-<code>) importado solo cuando se convierte en el idioma activo — la configuración regional inicial se espera antes del primer render (i18nReady en index.ts), y cambiar de idioma (useLanguage.ts) carga el catálogo destino antes de aplicarlo. Los 18 chunks de catálogos no ingleses suman varios MB, así que se evita embarcarlos todos al arrancar. El service worker mantiene esos chunks fuera de la precache del app-shell y los cachea con CacheFirst en su primer uso (ver maximumFileSizeToCacheInBytes / HEAVY_PRECACHE_IGNORES en vite.config.ts): el idioma activo funciona sin conexión tras su primera carga en línea, y el inglés funciona sin conexión incondicionalmente.
  • Los catálogos se mantienen estructuralmente completos. La suite de pruebas exige que cada catálogo que no esté en inglés contenga el conjunto de claves del inglés (tras normalizar formas de plural). El fallbackLng: "en" en tiempo de ejecución sigue siendo un respaldo defensivo para chunks en caché obsoletos y cambios futuros del catálogo.

Detección de idioma

Al arrancar, el idioma inicial se resuelve en orden de prioridad (getInitialLanguage en index.ts):

  1. el parámetro de consulta ?locale= o ?lang= (para incrustaciones — coherente con los parámetros existentes ?theme= / ?maponly),
  2. el idioma persistido en los ajustes de escritorio (campo language),
  3. los idiomas preferidos del navegador (navigator.languages),
  4. el predeterminado, en.

Solo se honran los idiomas que tienen catálogo; cualquier otro pasa a la regla siguiente. El selector dentro de la app (el menú Ajustes → Idioma, o la pestaña Diseño del diálogo de Ajustes) cambia el idioma en vivo y registra la elección para que sobreviva a las recargas.

El parámetro ?locale hace que las incrustaciones reconozcan el idioma, por ejemplo:

http://localhost:5173/?maponly&locale=es

Usar traducciones en los componentes

import { useTranslation } from "react-i18next";

function Example() {
  const { t } = useTranslation();
  return <button aria-label={t("common.cancel")}>{t("common.save")}</button>;
}
  • Interpolación: t("settings.env.removeAria", { name }) contra un valor de catálogo como "Remove {{name}}".
  • Pluralización: define key_one / key_other y llama a t("settings.env.variablesCount", { count }).
  • Texto enriquecido (enlaces, negritas): usa el componente <Trans> con components con nombre, p. ej. un valor de catálogo "… in <tokenLink>Meratyn Share settings</tokenLink>." renderizado con <Trans i18nKey="…" components={{ tokenLink: <a href="…" /> }} />.
  • Constantes a nivel de módulo no pueden llamar a t() (no hay hook en ámbito). Guarda una clave de catálogo estable en la constante en lugar de la cadena en inglés y resuélvela con t(item.labelKey) en el sitio de render. Ver SECTION_ITEMS en features/settings/dialog.tsx, el registro de comandos en features/toolbar/commands.ts y los catálogos de menús bajo features/toolbar/menus/.

No traduzcas: registros de desarrollador (console.*), URLs, IDs de almacén, nombres/extensión de formatos de archivo ni valores de datos persistidos.

Añadir un idioma nuevo

  1. Copia apps/mappview/src/i18n/locales/en.json a locales/<code>.json (p. ej. fr.json, pt-BR.json) y traduce los valores. Conserva cada clave del inglés; usa el valor en inglés mientras la traducción está pendiente.
  2. Usa las categorías de plural CLDR de tu idioma para las claves plurales: conserva solo las formas que tu idioma tenga (p. ej. indonesio/japonés/coreano/chino eliminan _one y conservan _other; ruso añade _few/_many; árabe lleva las seis formas). La prueba de catálogos compara claves normalizadas de plural, así que una configuración regional puede llevar más o menos formas plurales que en sin fallar.
  3. Añade una entrada correspondiente en LANGUAGE_NAMES (languages.ts) para que el selector muestre un nombre nativo amigable. La prueba de catálogos exige que los códigos del registro y del catálogo coincidan.
  4. Cuando el inglés incorpore claves, ejecuta bun scripts/i18n/sync-catalogs.ts en <code> para cada idioma afectado. El script reconstruye el catálogo en el orden de las claves inglesas, conserva solo valores con marcadores compatibles y rellena las nuevas claves con un respaldo inglés explícito; nunca copia valores por posición.
  5. Eso es todo. El idioma aparece en Ajustes → Idioma (y en la pestaña Diseño del diálogo de Ajustes) y funciona con ?locale=<code>.

Idiomas de derecha a izquierda

El árabe (ar) y el persa (fa) son catálogos de derecha a izquierda, y la dirección de la disposición está cableada para que más configuraciones regionales RTL sean aditivas:

  • Registro de direcciones. RTL_LANGUAGES en languages.ts lista los subtags base de derecha a izquierda (ar, he, fa, ur); languageDirection(code) resuelve cualquier etiqueta igual que lo hace resolveLanguage (ar-SA → rtl, desconocido → ltr).
  • Espejo del documento. index.ts se suscribe al evento languageChanged de i18next — registrado antes de init, así que también se dispara para el idioma inicial — y ajusta document.documentElement.lang y .dir. Con dir="rtl" el shell basado en flex se espeja automáticamente: Capas a la derecha, Estilo a la izquierda.
  • Fuente persa. index.css carga el subconjunto árabe autoalojado de @fontsource-variable/vazirmatn solo cuando está activo html:lang(fa). Los nombres de producto, archivos y coordenadas latinos siguen usando Geist, mientras los glifos persas no dependen de una fuente instalada en el sistema.
  • Dirección de la interfaz base. App.tsx envuelve el shell en DirectionProvider (reexportado desde @mappview/ui), así que los menús y submenús se abren hacia la dirección de lectura y las flechas, deslizadores y pestañas siguen el orden espejado.
  • Utilidades lógicas. Los componentes usan las utilidades lógicas de espaciado/posición de Tailwind (ms-*/me-*, ps-*/pe-*, text-start/text-end, border-s/border-e, start-*/end-*) en lugar de las formas físicas ml-*/left-*, para que los estilos giren con la dirección del documento. Usa las formas lógicas en código nuevo, y no mezcles una utilidad física y una lógica para el mismo lado en un elemento — tailwind-merge las trata como grupos distintos, así que ambas se aplicarían. El centrado simétrico (left-1/2 + -translate-x-1/2), las superposiciones ancladas al mapa y los desplazamientos de píxeles calculados en JS permanecen físicos a propósito.
  • El chrome del mapa sigue en ltr. MapLibre ancla sus controles con CSS físico, así que index.css fija .maplibregl-control-container { direction: ltr } — el chrome del mapa es idéntico en todos los idiomas. El resto del subárbol .maplibregl-map se deja deliberadamente libre para espejarse, así que las superposiciones de la app portaladas en el contenedor del mapa (p. ej. el presentador de story maps) siguen la disposición RTL. El texto árabe en las ventanas emergentes se forma correctamente mediante el algoritmo bidi de Unicode, y MapLibre moldea el árabe y reordena las etiquetas bidireccionales del mapa base por sí mismo, también en las compilaciones sin conexión.

Añadir otro idioma de derecha a izquierda es la misma receta que cualquier idioma, más su subtag base en RTL_LANGUAGES si aún no está listado.

Mantener el formato consciente de la configuración regional

El formato de números, fechas y coordenadas debe seguir usando la configuración regional en tiempo de ejecución vía APIs Intl (p. ej. new Intl.NumberFormat(undefined, …) / Intl.DateTimeFormat) en lugar de formato codificado, para que los valores se rendericen según las convenciones regionales del usuario.

Limitaciones conocidas

Algunos plugins envuelven controles de MapLibre de terceros que renderizan su propia interfaz (p. ej. el control maplibre-gl-esri-wayback del plugin Esri Wayback, con etiquetas como “Only versions with local changes”). Esas cadenas viven dentro de la biblioteca externa y nunca pasan por el conducto de react-i18next de Meratyn, así que permanecen en inglés sin importar el idioma seleccionado. Traducirlas exige un cambio en la biblioteca externa (o un parche mantenido); no puede hacerse desde un catálogo de configuración regional aquí.