В настоящее время я работаю над проектом, в котором у нас есть навык умного дома Alexa. Недавно мы добавили настраиваемую модель голосового взаимодействия в этот навык для обработки некоторых намерений, которые не поддерживаются в Smart Home API.

На данный момент мы поддерживаем только английский язык на пользовательской стороне вещей, но я знал, что есть возможность улучшить то, как мы справляемся с этим — я хотел упростить вещи, чтобы:

  • Ответы больше не сохранялись в виде жестко закодированных строк в коде.
  • Для тех, кто не является разработчиком, был способ легко добавлять, редактировать и обновлять ответы.
  • У нас было решение, которое легко позволяло нам добавлять переводы для новых локалей.

Вот как я к этому подошел.

Примечание. Мы используем версию 2 alexa-skills-kit-for-nodejs SDK.

Общий ресурс

Первое, что мне было нужно, — это способ эффективного хранения и совместного использования ресурсов. Я рассматривал различные варианты, такие как Google Sheets, но в итоге остановился на Airtable.

Airtable кажется очень удобным для пользователя, допускает несколько соавторов, но, что наиболее важно, имеет хороший API — если в электронную таблицу вносятся какие-либо изменения, мы можем легко получить любые обновления.

Я создал новое рабочее пространство в Airtable и создал базу для своих ответов — база, по сути, представляет собой электронную таблицу, и в дальнейшем я буду называть ее электронной таблицей.

В электронной таблице я создал 3 вкладки. Нашим базовым языком для этого навыка является английский, поэтому он будет содержать ответы для всех наших намерений. Варианты (или переопределения) для других локалей находятся на отдельной вкладке для этой локали — эти вкладки содержат только отличия от базовой вкладки (en). Если в будущем я захочу добавить новую локаль, я создам вкладку для этой локали и добавлю любые варианты.

Каждая вкладка имеет 3 столбца.

  • намерениеName — имя намерения, в котором используется этот ответ.
  • responseName — имя этого ответа.
  • responseString — ответ.

После создания электронной таблицы ею можно поделиться с другими членами команды, независимо от их технического уровня — другими разработчиками, владельцами продукта и т. д.

Создание наших активов

Вместо того, чтобы обращаться к API Airtable для получения строк ответа на каждый запрос, я хотел создать скрипт, который извлекал бы их из API и сохранял бы ответы для каждой локали в формате JSON (для использования с модулем i18next).

Моим обоснованием для этого было:

  • Я не хотел вводить ненужную задержку.
  • Я хотел контролировать обновления — например, я не хотел, чтобы изменения в электронной таблице сразу же были доступны в продакшене (например, в случае ошибки).

На данный момент скрипт запускается вручную (я добавил его как скрипт NPM). В будущем мы можем запустить его как часть процесса развертывания.

SUPPORTED_LOCALES — это массив наших поддерживаемых языков, включая, например, en, en-US и en-CA.

Когда мы запускаем этот скрипт, мы обращаемся к API Airtable, чтобы получить данные с каждого имени вкладки, которое указано в массиве SUPPORTED_LOCALES. Затем мы преобразуем данные в нужный нам формат и, наконец, сохраняем их в виде файла JSON.

Например, наш сгенерированный файл JSON может выглядеть примерно так:

Еще одна вещь, которую вам нужно сделать, это экспортировать все ваши строки ресурсов — я сохраняю файлы JSON в папке i18n, поэтому я создал там файл index.js со следующим:

Добавляем i18next в наш проект

Andrea Muttoni из Amazon написала отличный пост о добавлении i18n в ваш проект Alexa, используя блестящий модуль i18next. Я придерживался аналогичного подхода, однако кое-что подправил, чтобы лучше соответствовать нашим потребностям.

Я не использую шаблоны sprintf, которые использует Андреа. Вместо этого я использую шаблоны с {{double}} фигурными скобками, которые работают из коробки. Основной причиной этого были названные переменные, облегчающие работу разработчиков и сотрудников.

Я использую подход с использованием перехватчика для получения локали по каждому запросу и инициации i18next.

Вот как выглядит мой перехватчик:

Вы заметите, что он немного отличается от перехватчика Андреа. Я также использую Ramda, поскольку мы используем его во всем нашем проекте, но пусть это вас не смущает, поскольку вы можете легко удалить эту зависимость.

Предполагая, что мы уже сгенерировали наши активы, этот перехватчик делает следующее:

  • Получает локаль из входящего запроса (lng).
  • Получает резервный язык (fallbackLng — язык, указанный первым в нашем массиве SUPPORTED_LOCALES).
  • Инициализирует i18next с помощью lng, fallbackLng и нашего сгенерированного resources (сгенерированного с помощью скрипта generate-assets.js — см. выше, где мы создаем index.js).
  • Добавляет функцию для получения соответствующего ответа на атрибуты сеанса, чтобы упростить использование в обработчиках.

После того, как вы настроите это, просто не забудьте добавить его в addRequestInterceptors для вашего навыка. 😉

использование

Итак, вы сгенерировали ресурсы и настроили перехватчик. Следующий шаг — использовать его в ваших обработчиках.

Первое, что нам нужно сделать, это получить t из ваших атрибутов сеанса. Например:

const { t } = await attributesManager.getSessionAttributes()

Сделав это, вы можете использовать t для получения строки ответа. Вы можете сделать это так:

t('intentName.responseName', { variable, anotherVariable })

Обратите внимание, что мы ориентируемся на строку ответа, используя значения, которые мы определили бы как intentName и responseName в электронной таблице.

Мы также можем передать необязательный объект, где имена ключей соответствуют именам переменных любого шаблона, который мы настроили в электронной таблице.

Итак, это может выглядеть так:

return responseBuilder
  .speak(t('intentName.responseName', { variable }))
  .getResponse()

Простой! 😎

Заворачивать

В целом, я очень доволен таким подходом к нашему проекту. Когда придет время добавить переводы на другие языки, это сделает нашу жизнь намного проще. Все намного проще в управлении, и мне очень нравится тот факт, что каждый может внести свой вклад, независимо от своего технического уровня.

Я надеюсь, что это (расплывчатое) руководство будет полезно для других в подобной ситуации. Не стесняйтесь комментировать или задавать любые вопросы.