diff --git a/lambda/custom/index.js b/lambda/custom/index.js index a2fdec9..ae205da 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 */ @@ -34,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(); }, }; @@ -45,12 +55,16 @@ const CouchPotatoIntent = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; - return request.type === 'IntentRequest' + return request.type === 'IntentRequest' && 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(); }, }; @@ -69,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 @@ -84,7 +102,7 @@ const InProgressRecommendationIntent = { prompt += '?'; - return handlerInput.responseBuilder + return responseBuilder .speak(prompt) .reprompt(prompt) .addElicitSlotDirective(currentSlot.name) @@ -92,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) @@ -104,7 +122,7 @@ const InProgressRecommendationIntent = { } } - return handlerInput.responseBuilder + return responseBuilder .addDelegateDirective(currentIntent) .getResponse(); }, @@ -122,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(); }, }; @@ -143,13 +159,17 @@ const HelpHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; - return request.type === 'IntentRequest' + return request.type === 'IntentRequest' && 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(); }, }; @@ -163,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(); }, }; @@ -188,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(); }, }; @@ -233,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) { @@ -299,7 +303,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 +343,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..d7cb3f0 --- /dev/null +++ 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: '' }, + ], +}; 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",