From 59937f935194546311ef8de44bcc23ac3741b575 Mon Sep 17 00:00:00 2001 From: Jake Kelly Date: Fri, 28 Sep 2018 13:36:54 -0600 Subject: [PATCH 1/5] Add i18next dependencies --- lambda/custom/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lambda/custom/package.json b/lambda/custom/package.json index 4aaa0e8..32e9f13 100644 --- a/lambda/custom/package.json +++ b/lambda/custom/package.json @@ -5,7 +5,9 @@ "main": "index.js", "dependencies": { "ask-sdk-core": "^2.0.0", - "ask-sdk-model": "^1.0.0" + "ask-sdk-model": "^1.0.0", + "i18next": "^11.8.0", + "i18next-sprintf-postprocessor": "^0.2.2" }, "devDependencies": {}, "author": "Amazon.com", From 5a6a47f4447c2147fd19b4a51e6ec72429d7d19a Mon Sep 17 00:00:00 2001 From: Jake Kelly Date: Fri, 28 Sep 2018 13:43:57 -0600 Subject: [PATCH 2/5] Add file structure and localization interceptor --- lambda/custom/index.js | 45 +++++++++++++++++++++++++++++++---- lambda/custom/languages/en.js | 0 2 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 lambda/custom/languages/en.js diff --git a/lambda/custom/index.js b/lambda/custom/index.js index a2fdec9..f130f73 100644 --- a/lambda/custom/index.js +++ b/lambda/custom/index.js @@ -26,6 +26,12 @@ /* eslint-disable no-console */ const Alexa = require('ask-sdk-core'); +const i18n = require('i18next'); +const sprintf = require('i18next-sprintf-postprocessor'); + +const languageStrings = { + 'en': require('./languages/en.js'), +}; /* INTENT HANDLERS */ @@ -45,7 +51,7 @@ const CouchPotatoIntent = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; - return request.type === 'IntentRequest' + return request.type === 'IntentRequest' && request.intent.name === 'CouchPotatoIntent'; }, handle(handlerInput) { @@ -143,7 +149,7 @@ const HelpHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; - return request.type === 'IntentRequest' + return request.type === 'IntentRequest' && request.intent.name === 'AMAZON.HelpIntent'; }, handle(handlerInput) { @@ -299,7 +305,37 @@ function getSlotValues(filledSlots) { }, this); return slotValues; -} +}; + +const LocalizationInterceptor = { + process(handlerInput) { + const localizationClient = i18n.use(sprintf).init({ + lng: handlerInput.requestEnvelope.request.locale, + resources: languageStrings, + }); + localizationClient.localize = function localize() { + const args = arguments; + const values = []; + for (let i = 1; i < args.length; i += 1) { + 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)]; + } + return value; + }; + const attributes = handlerInput.attributesManager.getRequestAttributes(); + attributes.t = function translate(...args) { + return localizationClient.localize(...args); + }; + }, +}; + exports.handler = skillBuilder .addRequestHandlers( @@ -309,7 +345,8 @@ exports.handler = skillBuilder CompletedRecommendationIntent, HelpHandler, ExitHandler, - SessionEndedRequestHandler, + SessionEndedRequestHandler, ) + .addRequestInterceptors(LocalizationInterceptor) .addErrorHandlers(ErrorHandler) .lambda(); diff --git a/lambda/custom/languages/en.js b/lambda/custom/languages/en.js new file mode 100644 index 0000000..e69de29 From 3888e2288e4a1fcd24b60f345e99e10b5990c0c9 Mon Sep 17 00:00:00 2001 From: Jake Kelly Date: Fri, 28 Sep 2018 15:58:40 -0600 Subject: [PATCH 3/5] Update to use i18next --- lambda/custom/index.js | 100 +++++++++++++++++----------------- lambda/custom/languages/en.js | 45 +++++++++++++++ 2 files changed, 94 insertions(+), 51 deletions(-) diff --git a/lambda/custom/index.js b/lambda/custom/index.js index f130f73..e92d2ff 100644 --- a/lambda/custom/index.js +++ b/lambda/custom/index.js @@ -40,9 +40,13 @@ const LaunchRequestHandler = { return handlerInput.requestEnvelope.request.type === 'LaunchRequest'; }, handle(handlerInput) { - return handlerInput.responseBuilder - .speak('Welcome to Decision Tree. I will recommend the best job for you. Do you want to start your career or be a couch potato?') - .reprompt('Do you want a career or to be a couch potato?') + const attributesManager = handlerInput.attributesManager; + const responseBuilder = handlerInput.responseBuilder; + const requestAttributes = handlerInput.attributesManager.getRequestAttributes(); + + return responseBuilder + .speak(requestAttributes.t('WELCOME_MESSAGE')) + .reprompt(requestAttributes.t('WELCOME_REPROMPT')) .getResponse(); }, }; @@ -55,8 +59,12 @@ const CouchPotatoIntent = { && request.intent.name === 'CouchPotatoIntent'; }, handle(handlerInput) { - return handlerInput.responseBuilder - .speak('You don\'t want to start your career? Have fun wasting away on the couch.') + const attributesManager = handlerInput.attributesManager; + const responseBuilder = handlerInput.responseBuilder; + const requestAttributes = handlerInput.attributesManager.getRequestAttributes(); + + return responseBuilder + .speak(requestAttributes.t('COUCH_POTATO_RESPONSE')) .getResponse(); }, }; @@ -75,12 +83,16 @@ const InProgressRecommendationIntent = { for (const slotName of Object.keys(handlerInput.requestEnvelope.request.intent.slots)) { const currentSlot = currentIntent.slots[slotName]; + const attributesManager = handlerInput.attributesManager; + const responseBuilder = handlerInput.responseBuilder; + const requestAttributes = handlerInput.attributesManager.getRequestAttributes(); + if (currentSlot.confirmationStatus !== 'CONFIRMED' && currentSlot.resolutions && currentSlot.resolutions.resolutionsPerAuthority[0]) { if (currentSlot.resolutions.resolutionsPerAuthority[0].status.code === 'ER_SUCCESS_MATCH') { if (currentSlot.resolutions.resolutionsPerAuthority[0].values.length > 1) { - prompt = 'Which would you like'; + prompt = requestAttributes.t('DECISION_PROMPT'); const size = currentSlot.resolutions.resolutionsPerAuthority[0].values.length; currentSlot.resolutions.resolutionsPerAuthority[0].values @@ -90,7 +102,7 @@ const InProgressRecommendationIntent = { prompt += '?'; - return handlerInput.responseBuilder + return responseBuilder .speak(prompt) .reprompt(prompt) .addElicitSlotDirective(currentSlot.name) @@ -98,9 +110,9 @@ const InProgressRecommendationIntent = { } } else if (currentSlot.resolutions.resolutionsPerAuthority[0].status.code === 'ER_SUCCESS_NO_MATCH') { if (requiredSlots.indexOf(currentSlot.name) > -1) { - prompt = `What ${currentSlot.name} are you looking for`; + prompt = requestAttributes.t(UNMATCHED_SLOT_PROMPT, currentSlot.name); - return handlerInput.responseBuilder + return responseBuilder .speak(prompt) .reprompt(prompt) .addElicitSlotDirective(currentSlot.name) @@ -110,7 +122,7 @@ const InProgressRecommendationIntent = { } } - return handlerInput.responseBuilder + return responseBuilder .addDelegateDirective(currentIntent) .getResponse(); }, @@ -128,19 +140,17 @@ const CompletedRecommendationIntent = { const filledSlots = handlerInput.requestEnvelope.request.intent.slots; const slotValues = getSlotValues(filledSlots); + const attributesManager = handlerInput.attributesManager; + const responseBuilder = handlerInput.responseBuilder; + const requestAttributes = handlerInput.attributesManager.getRequestAttributes(); const key = `${slotValues.salaryImportance.resolved}-${slotValues.personality.resolved}-${slotValues.bloodTolerance.resolved}-${slotValues.preferredSpecies.resolved}`; - const occupation = options[slotsToOptionsMap[key]]; + const occupation = requestAttributes.options[slotsToOptionsMap[key]]; - const speechOutput = `So you want to be ${slotValues.salaryImportance.resolved - }. You are an ${slotValues.personality.resolved - }, you like ${slotValues.preferredSpecies.resolved - } and you ${slotValues.bloodTolerance.resolved === 'high' ? 'can' : "can't" - } tolerate blood ` + - `. You should consider being a ${occupation.name}`; - return handlerInput.responseBuilder - .speak(speechOutput) + + return responseBuilder + .speak(requestAttributes.t('COMPLETED_RECOMMENDATION_MESSAGE', slotValues.salaryImportance.resolved, slotValues.personality.resolved, slotValues.preferredSpecies.resolved, (slotValues.bloodTolerance.resolved === 'high' ? 'can' : "can't"), occupation.name); .getResponse(); }, }; @@ -153,9 +163,13 @@ const HelpHandler = { && request.intent.name === 'AMAZON.HelpIntent'; }, handle(handlerInput) { - return handlerInput.responseBuilder - .speak('This is Decision Tree. I can help you find the perfect job. You can say, recommend a job.') - .reprompt('Would you like a career or do you want to be a couch potato?') + const attributesManager = handlerInput.attributesManager; + const responseBuilder = handlerInput.responseBuilder; + const requestAttributes = handlerInput.attributesManager.getRequestAttributes(); + + return responseBuilder + .speak(requestAttributes.t('HELP_MESSAGE') + .reprompt(requestAttributes.t('HELP_REPROMPT')) .getResponse(); }, }; @@ -169,8 +183,12 @@ const ExitHandler = { || request.intent.name === 'AMAZON.StopIntent'); }, handle(handlerInput) { - return handlerInput.responseBuilder - .speak('Bye') + const attributesManager = handlerInput.attributesManager; + const responseBuilder = handlerInput.responseBuilder; + const requestAttributes = handlerInput.attributesManager.getRequestAttributes(); + + return responseBuilder + .speak(requestAttributes.t('EXIT_MESSAGE') .getResponse(); }, }; @@ -194,9 +212,13 @@ const ErrorHandler = { handle(handlerInput, error) { console.log(`Error handled: ${error.message}`); - return handlerInput.responseBuilder - .speak('Sorry, I can\'t understand the command. Please say again.') - .reprompt('Sorry, I can\'t understand the command. Please say again.') + const attributesManager = handlerInput.attributesManager; + const responseBuilder = handlerInput.responseBuilder; + const requestAttributes = handlerInput.attributesManager.getRequestAttributes(); + + return responseBuilder + .speak(requestAttributes.t('ERROR_MESSAGE') + .reprompt(requestAttributes.t('ERROR_MESSAGE')) .getResponse(); }, }; @@ -239,30 +261,6 @@ const slotsToOptionsMap = { 'very-extrovert-high-people': 5, }; -const options = [ - { name: 'Actor', description: '' }, - { name: 'Animal Control Worker', description: '' }, - { name: 'Animal Shelter Manager', description: '' }, - { name: 'Artist', description: '' }, - { name: 'Court Reporter', description: '' }, - { name: 'Doctor', description: '' }, - { name: 'Geoscientist', description: '' }, - { name: 'Investment Banker', description: '' }, - { name: 'Lighthouse Keeper', description: '' }, - { name: 'Marine Ecologist', description: '' }, - { name: 'Park Naturalist', description: '' }, - { name: 'Pet Groomer', description: '' }, - { name: 'Physical Therapist', description: '' }, - { name: 'Security Guard', description: '' }, - { name: 'Social Media Engineer', description: '' }, - { name: 'Software Engineer', description: '' }, - { name: 'Teacher', description: '' }, - { name: 'Veterinary', description: '' }, - { name: 'Veterinary Dentist', description: '' }, - { name: 'Zookeeper', description: '' }, - { name: 'Zoologist', description: '' }, -]; - /* HELPER FUNCTIONS */ function getSlotValues(filledSlots) { diff --git a/lambda/custom/languages/en.js b/lambda/custom/languages/en.js index e69de29..83a2428 100644 --- a/lambda/custom/languages/en.js +++ b/lambda/custom/languages/en.js @@ -0,0 +1,45 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Licensed under the Amazon Software License +// http://aws.amazon.com/asl/ + +module.exports = { + translation: { + WELCOME_MESSAGE: 'Welcome to Decision Tree. I will recommend the best job for you. Do you want to start your career or be a couch potato?', + WELCOME_REPROMPT: 'Do you want a career or to be a couch potato?', + COUCH_POTATO_RESPONSE: 'You don\'t want to start your career? Have fun wasting away on the couch.', + DECISION_PROMPT: 'Which would you like, ', + UNMATCHED_SLOT_PROMPT: `What %s are you looking for?`, + COMPLETED_RECOMMENDATION_MESSAGE: `So you want to be %s. You are an %s, you like %s and you %s tolerate blood. You should consider being a %s.`, + EXIT_MESSAGE: 'Goodbye!', + ERROR_MESSAGE: 'Sorry, I can\'t understand the command. Please say again.', + GET_ANSWER_ABBREVIATION: `The %s of %s is %s.`, + GET_CURRENT_SCORE: `Your current score is %s out of %s.`, + GET_FINAL_SCORE: `Your final score is %s out of %s.`, + REPROMPT_SPEECH: `Which other state or capital would you like to know about?`, + HELP_MESSAGE: 'This is Decision Tree. I can help you find the perfect job. You can say, recommend a job.', + HELP_REPROMPT: 'Would you like a career or do you want to be a couch potato?', + }, + options = [ + { name: 'Actor', description: '' }, + { name: 'Animal Control Worker', description: '' }, + { name: 'Animal Shelter Manager', description: '' }, + { name: 'Artist', description: '' }, + { name: 'Court Reporter', description: '' }, + { name: 'Doctor', description: '' }, + { name: 'Geoscientist', description: '' }, + { name: 'Investment Banker', description: '' }, + { name: 'Lighthouse Keeper', description: '' }, + { name: 'Marine Ecologist', description: '' }, + { name: 'Park Naturalist', description: '' }, + { name: 'Pet Groomer', description: '' }, + { name: 'Physical Therapist', description: '' }, + { name: 'Security Guard', description: '' }, + { name: 'Social Media Engineer', description: '' }, + { name: 'Software Engineer', description: '' }, + { name: 'Teacher', description: '' }, + { name: 'Veterinary', description: '' }, + { name: 'Veterinary Dentist', description: '' }, + { name: 'Zookeeper', description: '' }, + { name: 'Zoologist', description: '' }, + ], +}; From 5fcdd9e9bdc8722df63f41f9471b1864da8f9daa Mon Sep 17 00:00:00 2001 From: Jake Kelly Date: Fri, 28 Sep 2018 16:26:24 -0600 Subject: [PATCH 4/5] Fix typo --- lambda/custom/languages/en.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambda/custom/languages/en.js b/lambda/custom/languages/en.js index 83a2428..d7cb3f0 100644 --- a/lambda/custom/languages/en.js +++ b/lambda/custom/languages/en.js @@ -19,7 +19,7 @@ module.exports = { HELP_MESSAGE: 'This is Decision Tree. I can help you find the perfect job. You can say, recommend a job.', HELP_REPROMPT: 'Would you like a career or do you want to be a couch potato?', }, - options = [ + options: [ { name: 'Actor', description: '' }, { name: 'Animal Control Worker', description: '' }, { name: 'Animal Shelter Manager', description: '' }, From 9bca58c5e23d04743b5a204dab3b8809c43769b5 Mon Sep 17 00:00:00 2001 From: Jake Kelly Date: Fri, 28 Sep 2018 16:27:31 -0600 Subject: [PATCH 5/5] Fix typos --- lambda/custom/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lambda/custom/index.js b/lambda/custom/index.js index e92d2ff..ae205da 100644 --- a/lambda/custom/index.js +++ b/lambda/custom/index.js @@ -150,7 +150,7 @@ const CompletedRecommendationIntent = { return responseBuilder - .speak(requestAttributes.t('COMPLETED_RECOMMENDATION_MESSAGE', slotValues.salaryImportance.resolved, slotValues.personality.resolved, slotValues.preferredSpecies.resolved, (slotValues.bloodTolerance.resolved === 'high' ? 'can' : "can't"), occupation.name); + .speak(requestAttributes.t('COMPLETED_RECOMMENDATION_MESSAGE', slotValues.salaryImportance.resolved, slotValues.personality.resolved, slotValues.preferredSpecies.resolved, (slotValues.bloodTolerance.resolved === 'high' ? 'can' : "can't"), occupation.name)) .getResponse(); }, }; @@ -168,7 +168,7 @@ const HelpHandler = { const requestAttributes = handlerInput.attributesManager.getRequestAttributes(); return responseBuilder - .speak(requestAttributes.t('HELP_MESSAGE') + .speak(requestAttributes.t('HELP_MESSAGE')) .reprompt(requestAttributes.t('HELP_REPROMPT')) .getResponse(); }, @@ -188,7 +188,7 @@ const ExitHandler = { const requestAttributes = handlerInput.attributesManager.getRequestAttributes(); return responseBuilder - .speak(requestAttributes.t('EXIT_MESSAGE') + .speak(requestAttributes.t('EXIT_MESSAGE')) .getResponse(); }, }; @@ -217,7 +217,7 @@ const ErrorHandler = { const requestAttributes = handlerInput.attributesManager.getRequestAttributes(); return responseBuilder - .speak(requestAttributes.t('ERROR_MESSAGE') + .speak(requestAttributes.t('ERROR_MESSAGE')) .reprompt(requestAttributes.t('ERROR_MESSAGE')) .getResponse(); },