這是簡單的數學:你到達的客戶越多,他們發現和參與你的技能的可能性就越大。因此,我們始終建議開發人員以儘可能多的語言本地化和發佈技能。將代碼擴展到其他語言的過程可能是一個耗時的過程,雖然是值得的。翻譯需要時間,如果您不瞭解多種語言,則可能需要聘請外部翻譯人員。根據您構建代碼的方式,它可能會在後端開發方面增加顯着的加班時間。
在今天的帖子中,我分享了一些關於如何使用針對Node.js 的Alexa技能套件(ASK)軟件開發套件簡化Alexa技能的本地化過程的一些技巧。我將教您如何簡化後端開發,以便您可以保留單個AWS Lambda函數,無論您最終支持多少種語言。我還分享了一些如何管理可本地化內容的選項。國際化,也稱爲“i18n”,因爲“i”和“n”之間有18個字母,這是一個描述這個過程的長詞。請注意,“國際化”,“i18n”和“本地化”將在本文中互換使用。
Alexa技能本地化的目標
爲了使我們的代碼可以維護並且可以使用將來的語言輕鬆擴展,我們希望:
- 將我們的字符串與代碼邏輯分開
- 可以輕鬆地從我們的代碼邏輯中選擇一個字符串,但保持我們的代碼DRY
- 能夠在我們的字符串中支持通配符,以便我們可以在以後用代碼填充變量(例如,如果我們提供名稱
'Andrea'
,'Hello %s!'
將變成'Hello Andrea!'
) - 能夠添加相同響應的多個版本(例如'hello','hi','hey')並讓代碼隨機選擇一個
如何使用Node.js和AWS Lambda實現本地化
首先,將您的字符串與邏輯分開。即使在文件方面。您的工作目錄應如下所示:
/
├── i18n/ // your language strings are here
│ ├── en.js
│ ├── de.js
│ ├── fr.js
│ ├── it.js
│ └── es.js
├── lib/
│ └── ... // your other logic
├── node_modules/
│ └── ... // your npm modules
└── index.js // your lambda entry point
我們按如下方式構建語言文件。您會注意到字符串ID可以包含字符串或字符串數組。如果您選擇使用字符串數組,則本地化庫將自動從數組中選擇一個隨機值,從而幫助您提供各種技能響應。
您還會注意到我們可以以'%s'(對於類似字符串的變量)或'%d'(對於類似數字的變量)的形式向字符串添加通配符。它們的工作原理很簡單:您只需要爲字符串中的每個通配符傳遞一個額外的參數,例如:requestAttributes.t('GREETING_WITH_NAME','Andrea')。
// en.js
module.exports = {
translation : {
'SKILL_NAME' : 'Super Welcome', // <- can either be a string...
'GREETING' : [ // <- or an array of strings.
'Hello there',
'Hey',
'Hi!'
],
'GREETING_WITH_NAME' : [
'Hey %s', // --> That %s is a wildcard. It will
'Hi there, %s', // get turned into a name in our code.
'Hello, %s' // e.g. requestAttributes.t('GREETING_WITH_NAME', 'Andrea')
],
// ...more...
}
}
同樣,另一個語言環境將具有相同的鍵,但具有不同的值,如下所示:
// it.js
module.exports = {
translation : {
'SKILL_NAME' : 'Iper Benvenuto',
'GREETING' : [
'Ciao!',
'Ehila!',
'Buongiorno'
],
'GREETING_WITH_NAME' : [
'Ciao %s', // --> That %s is a wildcard. It will
'Ehila %s', // get turned into a name in our code.
'Buongiorno, %s' // e.g. requestAttributes.t('GREETING_WITH_NAME', 'Andrea')
],
// ...more...
}
}
爲了完成上述工作,我們需要兩個節點模塊:i18next
和i18next-sprintf-postprocessor
。
- 如果要從無服務器應用程序存儲庫創建lambda,請確保選擇' alexa-skills-kit-nodejs- howto skill '模板而不是事實技能,因爲前者已經將這些模塊作爲依賴項。
- 如果您使用ASK命令行界面(CLI)進行本地開發,只需像使用其他任何npm模塊一樣安裝它們:
npm i —save i18next i18next-sprintf-postprocessor
在我們的主要部分,index.js
我們需要兩個節點模塊:
// in the index.js file, we add i18next and
// i18next-sprintf-postprocessor as dependencies
const i18n = require('i18next');
const sprintf = require('i18next-sprintf-postprocessor');
我們還需要在index.js文件中聚合這些語言文件。
// further down the index.js
const languageStrings = {
'en' : require('./i18n/en'),
'it' : require('./i18n/it'),
// ... etc
}
現在我們需要一些代碼來適應通用(和開源)i18next本地化框架,並使其與SDK一起使用。添加以下更新LocalizationInterceptor如下。攔截器將自動解析傳入的請求,檢測用戶的語言環境並選擇要使用的正確語言字符串。它還將結合i18next的強大功能,即i18next-sprintf-postprocessor 的sprintf功能,並在特定鍵(如'GREETING')具有可能響應數組時隨機自動選擇響應。
// inside the index.js
const LocalizationInterceptor = {
process(handlerInput) {
const localizationClient = i18n.use(sprintf).init({
lng: handlerInput.requestEnvelope.request.locale,
fallbackLng: 'en', // fallback to EN if locale doesn't exist
resources: languageStrings
});
localizationClient.localize = function () {
const args = arguments;
let values = [];
for (var i = 1; i < args.length; i++) {
values.push(args[i]);
}
const value = i18n.t(args[0], {
returnObjects: true,
postProcess: 'sprintf',
sprintf: values
});
if (Array.isArray(value)) {
return value[Math.floor(Math.random() * value.length)];
} else {
return value;
}
}
const attributes = handlerInput.attributesManager.getRequestAttributes();
attributes.t = function (...args) { // pass on arguments to the localizationClient
return localizationClient.localize(...args);
};
},
};
在實例化Skill Builder對象時,不要忘記註冊LocalizationInterceptor:
// at the bottom of the index.js
const skillBuilder = Alexa.SkillBuilders.standard();
exports.handler = skillBuilder
.addRequestHandlers(
// your intent handlers here
)
.addRequestInterceptors(LocalizationInterceptor) // <-- ADD THIS LINE
.addErrorHandlers(ErrorHandler)
.lambda();
我們添加請求攔截器的原因是,每當請求進入我們處理任何請求之前,SDK都會運行此命令,並使用傳入語言環境的本地化字符串預填充請求屬性。
如何在處理程序中使用本地化字符串
現在設置已經完成,在處理程序中使用它是一件輕而易舉的事!這裏有些例子:
// IN THE CASE OF A SIMPLE GREETING, WITHOUT THE NAME
const LaunchRequestHandler = {
canHandle(handlerInput) {
const request = handlerInput.requestEnvelope.request;
return request.type === 'LaunchRequest';
},
async handle(handlerInput) {
// we get the translator 't' function from the request attributes
const requestAttributes = handlerInput.attributesManager.getRequestAttributes();
// we call it using requestAttributes.t and reference the string key we want as the argument.
const speechOutput = requestAttributes.t('GREETING');
// -> speechOutput will now contain a 'GREETING' at random, such as 'Hello'
return handlerInput.responseBuilder
.speak(speechOutput)
.getResponse();
},
};
// IN THE CASE WE HAVE THE USER'S NAME
const LaunchRequestHandler = {
canHandle(handlerInput) {
const request = handlerInput.requestEnvelope.request;
return request.type === 'LaunchRequest';
},
async handle(handlerInput) {
const requestAttributes = handlerInput.attributesManager.getRequestAttributes();
const sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
const username = sessionAttributes.username // <-- let's assume = 'Andrea'
const speechOutput = requestAttributes.t('GREETING_WITH_NAME', username); // < -- note the second argument
// -> speechOutput now contains a 'GREETING_WITH_NAME' at random, such as 'Hello, %s'
// and filled with the attribute we provided as the second argument 'username', i.e. 'Hello, Andrea'.
return handlerInput.responseBuilder
.speak(speechOutput)
.getResponse();
},
};
關於i18next框架的好處是你不需要使用數組,你可以根據需要使用靜態字符串,同樣你也不需要使用通配符'%s'。
爲同一語言區域設置添加不同的字符串
在某些情況下,我們希望某些語言文件涵蓋多種語言。例如:en.js可能涵蓋en-US,en-GB,en-IN。在其他情況下,我們可能希望覆蓋特定的字符串,使其更具文化相關性(例如運動鞋與運動鞋,精彩對抗和令人敬畏等)。我們怎麼做?
比方說,我們想要爲我們的英國用戶覆蓋帶有自定義en-GB問候語的en.js。簡單!我們將有一個專用的en-GB.js文件,只有改變的鍵/值對。i18next將自動選擇最具體的可用語言字符串(類似於CSS選擇器)。例如,如果有一個“en-GB”字符串可用,它將通過“en”等效字符串選擇它。
/
├── i18n/
│ ├── en.js
│ ├── en-GB.js // <-- we add a special en-GB file
...
└── index.js
我們的語言文件如下所示:
// en.js
module.exports = {
translation : {
// ... other strings
'SNEAKER_COMPLIMENT' : 'Those are awesome sneakers!' // <-- default
// ... other strings
}
然後我們將添加一個en-GB文件,它只覆蓋我們想要爲en-GB更改的字符串。
// en-GB.js
module.exports = {
translation : {
'SNEAKER_COMPLIMENT' : 'Those are sick trainers!' // <--
}
然後,在我們的index.js文件中,我們將確保將其添加到languageStrings對象:
// inside the index.js
const languageStrings = {
'en' : require('./i18n/en'),
'en-GB' : require('./i18n/en-GB'),
// ... etc
}
我們完成了!
展望未來
雖然這種方法非常適合簡單的技能,但隨着您的技能變得越來越複雜,您的字符串列表將會增加,維護和本地化所需的工作量也會增加。如果發生這種情況,可以使用以下任何一種方法來提升本地化策略。
字符串提取
有時你的後端已經在整個地方使用硬編碼字符串進行語音輸出,有時這些字符串太多而無法手動處理。 幸運的是,Node.js中有很多本地化支持庫(甚至是i18next掃描程序),它們可以掃描您的代碼,提取翻譯鍵/值,並將它們合併到i18n資源文件中,準備以上述格式使用。爲了自動化該過程,您甚至可以使用gulp來運行字符串提取任務並以編程方式生成字符串資源。
字符串管理
使用CMS或適當的本地化框架可能是值得的,該框架允許外部(非技術)人員協作併爲單個字符串提供翻譯,而無需訪問您的生產環境。然後,您將導出這些字符串並生成您的技能將讀取的語言字符串文件。
一些技能開發人員使用基於雲的用戶友好型數據庫,並要求他們的beta測試人員提供字符串資源和翻譯。其他人更喜歡更多的i18n特定服務,您可以將字符串的翻譯衆包,然後通過API進行管理。無論您選擇什麼,您的技能處理的字符串越多,集中管理這些資源就越有必要。
翻譯API
當後端在不受支持的語言環境中獲取傳入請求時的後備策略是使用基於機器學習的翻譯服務,如Amazon Translate。在基於Node.js的AWS Lambda函數中,您將使用AWS SDK獲取AWS.Translate實例,該實例傳遞源語言,目標語言和要翻譯的文本等參數。幸運的是,AWS Lambda可以非常輕鬆地連接到其他AWS服務:它已經將AWS SDK作爲執行環境的一部分包含在內,因此您不必手動將其添加爲依賴項。此外,它會自動將SDK所需的憑據設置爲與您的功能相關聯的IAM角色(您無需執行任何其他步驟)。