Hasta esta semana, el widget de chat de Kookee no tenía ni idea de en qué idioma estaba. Un sitio podía pasar un locale y el widget se lo entregaba a la API, pero los botones y el «¿Cómo podemos ayudarte?» de Inicio seguían en inglés salvo que el sitio tradujera cada uno por su cuenta, y algunos textos, como los mensajes de error, no se podían traducir de ninguna manera.
Un nivel más abajo era peor. La API comparaba las etiquetas de idioma como simples cadenas, así que un sitio brasileño que pasaba pt-BR a un proyecto con el centro de ayuda escrito en pt no encontraba coincidencia y recibía el idioma predeterminado.
El widget solo parecía multilingüe si el sitio hacía la mitad del trabajo y escribía la etiqueta exactamente bien. Rehíce la forma en que elige un idioma y lo que hace con él. Salió hoy, y estas son las reglas con las que me quedé.

1. Primero decide la página, después el navegador
La mayoría de los widgets leen navigator.language, el idioma del navegador del visitante. Es la señal obvia y, por lo general, la equivocada, porque describe el equipo del visitante, no la página que está leyendo. Un desarrollador alemán en un sitio de documentación en inglés tendría un widget en alemán sobre una página en inglés, que le ofrecería artículos que luego cargaría en inglés de todos modos.
La página ya dice en qué idioma está: <html lang>. La mayoría de los frameworks y CMS lo definen, y es una decisión del dueño del sitio, no una suposición. Así que el widget lee eso primero. El navegador solo opina cuando la página no declara nada, y solo para un idioma que el widget de verdad incluye. Si no hay ninguno de los dos, usa el idioma predeterminado del proyecto.
Aun así, un sitio puede imponerse a todo esto con data-locale, o con locale en React. Y cuando su selector de idioma cambia <html lang> sin recargar, el widget lo detecta y lo sigue.
2. Una etiqueta de idioma es una familia, no una cadena
pt-BR, pt, PT-br y pt_BR significan portugués, y un widget que funciona en sitios ajenos tiene que aceptarlas todas. La forma con guion bajo viene de servidores que escriben los locales al estilo POSIX. Algunas etiquetas llevan más que una región, como ca-ES-valencia.
Lo resolví en dos lugares. El widget limpia la etiqueta antes de enviar nada: la normaliza a su forma canónica, conserva el idioma y la región, y descarta todo lo que no sea una etiqueta de idioma, porque una petición que la API rechaza dejaría al visitante con un widget roto. Después, el servidor compara la etiqueta con los idiomas del proyecto: primero busca una coincidencia exacta, luego el idioma base y luego algunos códigos antiguos, como nb y nn para el noruego e iw para el hebreo.
Una etiqueta que no coincide con nada se queda como está. No se convierte sin avisar en otro idioma. Recurre al mismo respaldo que una traducción que falta.
3. Tus textos recurren a tu idioma; los del widget, al inglés
Un widget muestra dos tipos de texto, y cada uno necesita un respaldo distinto.
Tu contenido (artículos de ayuda, colecciones, novedades en Inicio) recurre al idioma predeterminado de tu proyecto. Un artículo en inglés en una página en francés no es lo ideal, pero es mejor que una pestaña Ayuda vacía, y es lo que la API ya hacía con una traducción que falta.
El texto propio del widget recurre al inglés. El widget incluye cinco idiomas, y para un sexto los botones están en inglés hasta que le des tus propias palabras con labels.
Así, una página en alemán de un proyecto con artículos en alemán muestra artículos en alemán con botones en inglés. Le di muchas vueltas y lo dejé así. Una pantalla mezclada es honesta sobre lo que existe. Ocultar un artículo porque el widget no tiene la palabra alemana para «Atrás» sería peor.
La captura de Inicio de arriba muestra las dos reglas a la vez. El texto del widget y el saludo que la página pasó en su código están en ruso, y el anuncio, escrito solo en inglés, se muestra en inglés.
Trabajar en esto sacó a la luz dos errores antiguos en el mismo lugar. Una lista podía mostrar el mismo artículo dos veces, una en cada idioma, y un idioma sin categorías traducidas hacía que Ayuda perdiera por completo sus colecciones. Los dos están corregidos: una lista ahora contiene una sola versión de cada artículo, y las categorías recurren al respaldo como todo lo demás.
4. Responde en el idioma en que escribe el visitante
Todo lo anterior es una suposición sobre el visitante. Su mensaje no lo es. Si alguien escribe en español en una página que dice estar en inglés, quiere la respuesta en español.
Así que el asistente ahora responde en el idioma del último mensaje del visitante, y solo recurre al idioma del widget cuando el mensaje no lo deja claro, como pasa con «hi» o con el nombre de un producto. Sigue buscando en tus artículos en el idioma en que existan y los cita tal como están.

5. Cambiar de idioma no debería costar la conversación
En una aplicación de una sola página, el visitante puede cambiar de idioma sin recargar. Cuando eso pasa, Inicio y Ayuda se recargan en el nuevo idioma y la conversación se queda donde estaba.
Un artículo abierto en ese momento se cierra y vuelve a su pestaña. Se cargó en el idioma anterior, y su dirección puede cambiar de un idioma a otro, así que recargarlo ahí mismo no es fiable. Retroceder un paso es un precio pequeño, y la conversación queda intacta.
Los sitios cuyo selector de idioma no toca <html lang> pueden llamar a KookeeChat.setLocale('fr').
6. Los textos van dentro del widget
Podría haber servido las traducciones desde la API, un idioma por petición. El script habría sido más pequeño, pero cada vista de página empezaría con un destello de inglés mientras cargan los textos, y una copia antigua del widget se rompería el día en que cambiara el nombre de una clave.
Así que los cinco idiomas van en el archivo. Eso costó unos 7 KB comprimidos con gzip: el script pasó de 149,0 a 156,2 KB. Añadir un sexto idioma es un archivo y una nueva versión, y labels cubre lo que haga falta mientras tanto.
Lo que dejé fuera
Un selector de idioma dentro del widget. Tu sitio ya tiene un selector de idioma, y un segundo selector en una esquina de la página tarde o temprano dirá otra cosa. El widget sigue al tuyo.
Textos traducidos en el panel. El título, el saludo, las sugerencias y los enlaces de la pestaña Personalizar se guardan en un solo idioma. Un sitio en varios idiomas puede dejarlos vacíos, para que se usen los textos predeterminados traducidos del widget, o definirlos por página en el código, donde tienen prioridad sobre los valores guardados:
<script async src="https://kookee.dev/chat/latest.js"
data-api-key="YOUR_API_KEY"
data-locale="es"
data-greeting="¿En qué podemos ayudarte?"></script>
Más de cinco idiomas. Cada texto nuevo del widget necesita una traducción en cada idioma que incluye, y prefiero tener cinco que pueda revisar que treinta que no.
Cómo activarlo
Con la etiqueta script, llega sin tocar tu sitio. En React, actualiza a @kookee/react 1.12.0 y @kookee/sdk 1.12.0. Si tu página define <html lang>, el widget ya lo sigue.
Un cambio de comportamiento: un sitio que no define ningún idioma ahora recibe el que declara su página. Para mantener el widget en inglés diga lo que diga la página, define data-locale="en". La pestaña Instalación de la página Widget del sitio tiene un selector de idioma que lo escribe por ti.

Muestra el widget en el idioma de tus visitantes cubre todas las opciones, y Controla el widget de chat desde JavaScript explica setLocale(). El portal alojado recibió sus propios idiomas más tarde, en Un centro de ayuda multilingüe sin páginas vacías.