diff --git a/libraries/botbuilder-dialogs/src/prompts/choicePrompt.ts b/libraries/botbuilder-dialogs/src/prompts/choicePrompt.ts index f60491d074..10f2c1b10c 100644 --- a/libraries/botbuilder-dialogs/src/prompts/choicePrompt.ts +++ b/libraries/botbuilder-dialogs/src/prompts/choicePrompt.ts @@ -83,10 +83,7 @@ export class ChoicePrompt extends Prompt { protected async onPrompt(context: TurnContext, state: any, options: PromptOptions, isRetry: boolean): Promise { // Determine locale - let locale: string = PromptCultureModels.mapToNearestLanguage(context.activity.locale || this.defaultLocale); - if (!locale || !this.choiceDefaults.hasOwnProperty(locale)) { - locale = 'en-us'; - } + const locale = this.determineCulture(context.activity); // Format prompt to send let prompt: Partial; @@ -111,7 +108,7 @@ export class ChoicePrompt extends Prompt { const utterance: string = activity.text; const choices: any[] = (this.style === ListStyle.suggestedAction ? ChoiceFactory.toChoices(options.choices) : options.choices)|| []; const opt: FindChoicesOptions = this.recognizerOptions || {}; - opt.locale = activity.locale || opt.locale || this.defaultLocale || 'en-us'; + opt.locale = this.determineCulture(activity, opt); const results: any[] = recognizeChoices(utterance, choices, opt); if (Array.isArray(results) && results.length > 0) { result.succeeded = true; @@ -120,4 +117,15 @@ export class ChoicePrompt extends Prompt { return result; } + + private determineCulture(activity: Activity, opt: FindChoicesOptions = null): string { + const optLocale = opt && opt.locale ? opt.locale : null; + let culture = PromptCultureModels.mapToNearestLanguage(activity.locale || optLocale || this.defaultLocale || PromptCultureModels.English.locale); + if (!culture || !this.choiceDefaults[culture]) + { + culture = PromptCultureModels.English.locale; + } + + return culture; + } } diff --git a/libraries/botbuilder-dialogs/src/prompts/promptCultureModels.ts b/libraries/botbuilder-dialogs/src/prompts/promptCultureModels.ts index df1f29cc21..abe694d296 100644 --- a/libraries/botbuilder-dialogs/src/prompts/promptCultureModels.ts +++ b/libraries/botbuilder-dialogs/src/prompts/promptCultureModels.ts @@ -115,12 +115,35 @@ export class PromptCultureModels { noInLanguage: 'No', } + private static getSupportedCultureCodes(): string[] { + return this.getSupportedCultures().map((c): string => c.locale); + } + /** * Use Recognizers-Text to normalize various potential Locale strings to a standard. + * @remarks This is mostly a copy/paste from https://github.com/microsoft/Recognizers-Text/blob/master/JavaScript/packages/recognizers-text/src/culture.ts#L39 + * This doesn't directly use Recognizers-Text's MapToNearestLanguage because if they add language support before we do, it will break our prompts. * @param cultureCode Represents locale. Examples: "en-US, en-us, EN". * @returns Normalized locale. */ - public static mapToNearestLanguage = (cultureCode: string): string => Culture.mapToNearestLanguage(cultureCode); + public static mapToNearestLanguage(cultureCode: string): string { + if (cultureCode !== undefined) { + cultureCode = cultureCode.toLowerCase(); + let supportedCultureCodes = this.getSupportedCultureCodes(); + + if (supportedCultureCodes.indexOf(cultureCode) < 0) { + let culturePrefix = cultureCode.split('-')[0].trim(); + + supportedCultureCodes.forEach(function(supportedCultureCode): void { + if (supportedCultureCode.startsWith(culturePrefix)) { + cultureCode = supportedCultureCode; + } + }); + } + } + + return cultureCode; + } public static getSupportedCultures = (): PromptCultureModel[] => [ diff --git a/libraries/botbuilder-dialogs/tests/choicePrompt.test.js b/libraries/botbuilder-dialogs/tests/choicePrompt.test.js index 8bfe4b0490..ae3d6b0162 100644 --- a/libraries/botbuilder-dialogs/tests/choicePrompt.test.js +++ b/libraries/botbuilder-dialogs/tests/choicePrompt.test.js @@ -1,5 +1,6 @@ const { ActivityTypes, CardFactory, ConversationState, MemoryStorage, TestAdapter } = require('botbuilder-core'); const { ChoicePrompt, ChoiceFactory, DialogSet, ListStyle, DialogTurnStatus } = require('../'); +const { PromptCultureModels } = require('../'); const assert = require('assert'); const answerMessage = { text: `red`, type: 'message' }; @@ -385,6 +386,50 @@ describe('ChoicePrompt', function () { })); }); + it('should default to english locale', async function () { + const locales = [ + null, + '', + 'not-supported' + ]; + await Promise.all(locales.map(async (testLocale) => { + const adapter = new TestAdapter(async (turnContext) => { + const dc = await dialogs.createContext(turnContext); + + const results = await dc.continueDialog(); + if (results.status === DialogTurnStatus.empty) { + await dc.prompt('prompt', { prompt: 'Please choose a color.', choices: stringChoices }); + } else if (results.status === DialogTurnStatus.complete) { + const selectedChoice = results.result; + await turnContext.sendActivity(selectedChoice.value); + } + await convoState.saveChanges(turnContext); + }); + const convoState = new ConversationState(new MemoryStorage()); + + const dialogState = convoState.createProperty('dialogState'); + const dialogs = new DialogSet(dialogState); + const choicePrompt = new ChoicePrompt('prompt', async (prompt) => { + assert(prompt); + if (!prompt.recognized.succeeded) { + await prompt.context.sendActivity('bad input.'); + } + return prompt.recognized.succeeded; + }, null); + dialogs.add(choicePrompt); + + await adapter.send({ text: 'Hello', type: ActivityTypes.Message, locale: testLocale }) + .assertReply((activity) => { + const expectedChoices = ChoiceFactory.inline(stringChoices, null, null, { + inlineOr: PromptCultureModels.English.inlineOr, + inlineOrMore: PromptCultureModels.English.inlineOrMore, + inlineSeparator: PromptCultureModels.English.separator, + }).text; + assert.strictEqual(activity.text, `Please choose a color.${ expectedChoices }`); + }); + })); + }); + it('should accept and recognize custom locale dict', async function() { const adapter = new TestAdapter(async (turnContext) => { const dc = await dialogs.createContext(turnContext); diff --git a/libraries/botbuilder-dialogs/tests/confirmPrompt.test.js b/libraries/botbuilder-dialogs/tests/confirmPrompt.test.js index 7b54ed3952..18b0b214d6 100644 --- a/libraries/botbuilder-dialogs/tests/confirmPrompt.test.js +++ b/libraries/botbuilder-dialogs/tests/confirmPrompt.test.js @@ -521,6 +521,41 @@ describe('ConfirmPrompt', function () { .assertReply(`The result found is 'true'.`); }); + it('should recognize valid number and default to en if locale is null.', async function () { + const adapter = new TestAdapter(async (turnContext) => { + + turnContext.activity.locale = null; + + const dc = await dialogs.createContext(turnContext); + + const results = await dc.continueDialog(); + if (results.status === DialogTurnStatus.empty) { + await dc.prompt('prompt', { + prompt: { text: 'Please confirm.', type: ActivityTypes.Message }, + retryPrompt: { text: 'Please confirm, say "yes" or "no" or something like that.', type: ActivityTypes.Message } + }); + } else if (results.status === DialogTurnStatus.complete) { + await turnContext.sendActivity(`The result found is '${ results.result }'.`); + } + await convoState.saveChanges(turnContext); + }); + + const convoState = new ConversationState(new MemoryStorage()); + + const dialogState = convoState.createProperty('dialogState'); + const dialogs = new DialogSet(dialogState); + const prompt = new ConfirmPrompt('prompt'); + prompt.choiceOptions = { includeNumbers: true }; + dialogs.add(prompt); + + await adapter.send('Hello') + .assertReply('Please confirm. (1) Yes or (2) No') + .send('lala') + .assertReply('Please confirm, say "yes" or "no" or something like that. (1) Yes or (2) No') + .send('1') + .assertReply(`The result found is 'true'.`); + }); + it('should recogize valid number and default to en if locale invalid string.', async function () { const adapter = new TestAdapter(async (turnContext) => {