До этой недели чат-виджет Kookee понятия не имел, на каком он языке. Сайт мог передать locale, и виджет отдавал его дальше в API, но кнопки и «How can we help?» на Главной оставались английскими, если сайт не переводил каждую надпись сам, а часть текста, например сообщения об ошибках, перевести было вообще нельзя.
Уровнем ниже было ещё хуже. API сравнивал языковые теги как обычные строки, поэтому бразильский сайт, который передавал pt-BR в проект с центром помощи на pt, не находил совпадения и получал язык по умолчанию.
Виджет выглядел многоязычным, только если сайт делал половину работы и писал тег в точности правильно. Я переделал, как виджет выбирает язык и что потом с ним делает. Изменения вышли сегодня, и вот правила, на которых я остановился.

1. Сначала решает страница, потом браузер
Большинство виджетов читают navigator.language — язык браузера посетителя. Это очевидный сигнал и обычно неверный: он говорит о компьютере посетителя, а не о странице, которую тот читает. Немецкий разработчик на английском сайте с документацией получил бы немецкий виджет на английской странице, и этот виджет предлагал бы статьи, которые всё равно загрузил бы на английском.
Страница сама сообщает свой язык: <html lang>. Его выставляют большинство фреймворков и CMS, и это решение владельца сайта, а не догадка. Поэтому виджет читает его первым. Браузер получает право голоса, только если страница ничего не объявляет, и только для языка, на который виджет переведён. Если нет ни того ни другого, виджет берёт язык проекта по умолчанию.
Сайт по-прежнему может переопределить всё это через data-locale или locale в React. А когда переключатель языков на сайте меняет <html lang> без перезагрузки, виджет это замечает и переключается следом.
2. Языковой тег — это семейство, а не строка
pt-BR, pt, PT-br и pt_BR — это всё португальский, и виджет, который работает на чужих сайтах, должен принимать любой из этих вариантов. Вариант с подчёркиванием приходит с серверов, которые записывают локали в стиле POSIX. В некоторых тегах есть не только регион, например ca-ES-valencia.
Я обрабатываю это в двух местах. Виджет чистит тег перед отправкой: приводит его к каноническому виду, оставляет язык и регион и отбрасывает всё, что вообще не языковой тег, потому что запрос, который API отклонит, оставит посетителя со сломанным виджетом. Затем сервер сопоставляет тег с языками проекта: сначала ищет точное совпадение, потом базовый язык, потом несколько устаревших кодов, например nb и nn для норвежского и iw для иврита.
Тег, который ни с чем не совпал, остаётся как есть. Он не превращается втихую в какой-то другой язык. Для него действует тот же запасной язык, что и для недостающего перевода.
3. Ваш контент — на языке по умолчанию, текст виджета — на английском
Виджет показывает два вида текста, и запасной язык у них должен быть разный.
Ваш контент (статьи центра помощи, коллекции, новости на Главной) без перевода показывается на языке проекта по умолчанию. Английская статья на французской странице — не идеал, но это лучше пустой вкладки «Помощь», и так API Kookee уже поступал с недостающим переводом.
Собственный текст виджета без перевода показывается на английском. В виджете пять языков, и для шестого кнопки будут английскими, пока вы не передадите виджету свои слова через labels.
Так что немецкая страница в проекте с немецкими статьями показывает немецкие статьи с английскими кнопками. Я долго сомневался и оставил так. Смешанный экран честно показывает, что есть. Прятать статью из-за того, что у виджета нет немецкого перевода кнопки «Back», было бы хуже.
Главная на скриншоте вверху показывает оба правила сразу. Текст виджета и приветствие, которое страница передала в коде, на русском, а объявление, написанное только по-английски, показывается на английском.
Пока я над этим работал, нашлись две старые ошибки в том же месте. Список мог показать одну статью дважды, по разу на каждом языке, а на языке без переведённых категорий вкладка «Помощь» целиком теряла коллекции. Обе исправлены: в списке теперь одна версия каждой статьи, а категории без перевода показываются на языке по умолчанию, как и всё остальное.
4. Отвечать на языке, на котором пишет посетитель
Всё, что выше, — догадки о посетителе. Его сообщение — не догадка. Если человек пишет по-испански на странице, которая объявлена английской, он хочет получить ответ на испанском.
Поэтому ассистент теперь отвечает на языке последнего сообщения посетителя и переходит на язык виджета, только когда по сообщению язык не понять, как в случае с «hi» или названием продукта. Статьи он по-прежнему ищет на любом языке, на котором они есть, и ссылается на них без перевода.

5. Смена языка не должна стоить разговора
В одностраничном приложении посетитель может сменить язык без перезагрузки. Тогда Главная и Помощь загружаются заново на новом языке, а разговор остаётся на месте.
Статья, открытая в этот момент, закрывается, и посетитель возвращается на её вкладку. Она загружена на старом языке, а её адрес может отличаться от языка к языку, поэтому перезагружать её на месте ненадёжно. Шаг назад — небольшая цена, а разговор остаётся нетронутым.
Если переключатель языков на сайте не трогает <html lang>, вызовите вместо этого KookeeChat.setLocale('fr').
6. Переводы лежат в самом виджете
Я мог бы отдавать переводы из API, по одному языку на запрос. Скрипт остался бы меньше, но каждый просмотр страницы начинался бы со вспышки английского, пока грузятся переводы, а старая копия виджета сломалась бы в тот день, когда я переименую ключ.
Поэтому все пять языков лежат в самом файле. Это стоило около 7 КБ в gzip: скрипт вырос со 149,0 до 156,2 КБ. Шестой язык — это один файл и релиз, а до тех пор выручает labels.
Чего я делать не стал
Выбор языка внутри виджета. На вашем сайте уже есть переключатель языков, а второй, в углу страницы, рано или поздно с ним разойдётся. Виджет следует вашему.
Переводы текстов в панели управления. Заголовок, приветствие, подсказки и ссылки на вкладке «Настроить» сохраняются на одном языке. Сайт на нескольких языках может оставить их пустыми, и тогда сработают переведённые тексты по умолчанию, или задать их для каждой страницы в коде, и тогда они перекроют сохранённые значения:
<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>
Больше пяти языков. Каждой новой строке в виджете нужен перевод на все его языки, и я лучше оставлю пять языков, которые могу проверить, чем тридцать, которые не могу.
Как подключить
Тег script подхватывает это сам, на сайте ничего менять не нужно. В React обновитесь до @kookee/react 1.12.0 и @kookee/sdk 1.12.0. Если ваша страница задаёт <html lang>, виджет уже следует ему.
Одно изменение в поведении: если сайт не передаёт виджету язык, теперь виджет берёт тот, что объявлен на странице. Чтобы виджет оставался английским, что бы ни говорила страница, задайте data-locale="en". На вкладке Установка страницы Виджет для сайта есть список «Язык», который пропишет это за вас.

Все настройки описаны в статье Показывайте виджет на языке посетителей, а setLocale() — в статье Управляйте чат-виджетом из JavaScript. Готовый портал получил свои языки позже, об этом — в посте Многоязычный центр помощи без пустых страниц.