原文https://developer.amazon.com/zh/blogs/alexa/post/285a6778-0ed0-4467-a602-d9893eae34d7/how-to-localize-your-alexa-skills
这是简单的数学:你到达的客户越多,他们发现和参与你的技能的可能性就越大。因此,我们始终建议开发人员以尽可能多的语言本地化和发布技能。将代码扩展到其他语言的过程可能是一个耗时的过程,虽然是值得的。翻译需要时间,如果您不了解多种语言,则可能需要聘请外部翻译人员。根据您构建代码的方式,它可能会在后端开发方面增加显着的加班时间。
在今天的帖子中,我分享了一些关于如何使用针对Node.js 的Alexa技能套件(ASK)软件开发套件简化Alexa技能的本地化过程的一些技巧。我将教您如何简化后端开发,以便您可以保留单个AWS Lambda函数,无论您最终支持多少种语言。我还分享了一些如何管理可本地化内容的选项。国际化,也称为“i18n”,因为“i”和“n”之间有18个字母,这是一个描述这个过程的长词。请注意,“国际化”,“i18n”和“本地化”将在本文中互换使用。
为了使我们的代码可以维护并且可以使用将来的语言轻松扩展,我们希望:
'Andrea'
,'Hello %s!'
将变成'Hello Andrea!'
)首先,将您的字符串与逻辑分开。即使在文件方面。您的工作目录应如下所示:
/
├── 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
。
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进行管理。无论您选择什么,您的技能处理的字符串越多,集中管理这些资源就越有必要。
当后端在不受支持的语言环境中获取传入请求时的后备策略是使用基于机器学习的翻译服务,如Amazon Translate。在基于Node.js的AWS Lambda函数中,您将使用AWS SDK获取AWS.Translate实例,该实例传递源语言,目标语言和要翻译的文本等参数。幸运的是,AWS Lambda可以非常轻松地连接到其他AWS服务:它已经将AWS SDK作为执行环境的一部分包含在内,因此您不必手动将其添加为依赖项。此外,它会自动将SDK所需的凭据设置为与您的功能相关联的IAM角色(您无需执行任何其他步骤)。