diff --git a/samples/17.multilingual-bot/LICENSE b/samples/17.multilingual-bot/LICENSE new file mode 100644 index 000000000..21071075c --- /dev/null +++ b/samples/17.multilingual-bot/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/samples/17.multilingual-bot/README.md b/samples/17.multilingual-bot/README.md new file mode 100644 index 000000000..16ddd7477 --- /dev/null +++ b/samples/17.multilingual-bot/README.md @@ -0,0 +1,85 @@ +# Multilingual Bot + +Bot Framework v4 multilingual bot sample + +This sample will present the user with a set of cards to pick their choice of language. The user can either change language by invoking the option cards, or by entering the language code (_en_/_es_). The bot will then acknowledge the selection. + +This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to translate incoming and outgoing text using a custom middleware and the [Microsoft Translator Text API](https://docs.microsoft.com/en-us/azure/cognitive-services/translator/). + +## Concepts introduced in this sample + +Translation Middleware: We create a translation middleware that can translate text from bot to user and from user to bot, allowing the creation of multi-lingual bots. + +The middleware is driven by user state. This means that users can specify their language preference, and the middleware automatically will intercept messages back and forth and present them to the user in their preferred language. + +Users can change their language preference anytime, and since this gets written to the user state, the middleware will read this state and instantly modify its behavior to honor the newly selected preferred language. + +The [Microsoft Translator Text API](https://docs.microsoft.com/en-us/azure/cognitive-services/translator/), Microsoft Translator Text API is a cloud-based machine translation service. With this API you can translate text in near real-time from any app or service through a simple REST API call. +The API uses the most modern neural machine translation technology, as well as offering statistical machine translation technology. + +## Prerequisites + +- Java 1.8+ +- Install [Maven](https://maven.apache.org/) +- An account on [Azure](https://azure.microsoft.com) if you want to deploy to Azure. +- [Microsoft Translator Text API key](https://docs.microsoft.com/en-us/azure/cognitive-services/translator/translator-text-how-to-signup) + + To consume the Microsoft Translator Text API, first obtain a key following the instructions in the [Microsoft Translator Text API documentation](https://docs.microsoft.com/en-us/azure/cognitive-services/translator/translator-text-how-to-signup). + + Paste the key in the `TranslatorKey` setting in the `application.properties` file. + +## To try this sample + +- From the root of this project folder: + - Build the sample using `mvn package` + - Run it by using `java -jar .\target\bot-multilingual-sample.jar` + +## Testing the bot using Bot Framework Emulator + +[Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. + +- Install the latest Bot Framework Emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) + +### Connect to the bot using Bot Framework Emulator + +- Launch Bot Framework Emulator +- File -> Open Bot +- Enter a Bot URL of `http://localhost:3978/api/messages` + +### Creating a custom middleware + +Translation Middleware: We create a translation middleware than can translate text from bot to user and from user to bot, allowing the creation of multilingual bots. +Users can specify their language preference, which is stored in the user state. The translation middleware translates to and from the user's preferred language. + +### Microsoft Translator Text API + +The [Microsoft Translator Text API](https://docs.microsoft.com/en-us/azure/cognitive-services/translator/), Microsoft Translator Text API is a cloud-based machine translation service. With this API you can translate text in near real-time from any app or service through a simple REST API call. +The API uses the most modern neural machine translation technology, as well as offering statistical machine translation technology. + +## Deploy this bot to Azure + +To learn more about deploying a bot to Azure, see [Deploy your bot to Azure](https://aka.ms/azuredeployment) for a complete list of deployment instructions. + +### Add `TranslatorKey` to Application Settings + +If you used the `application.properties` file to store your `TranslatorKey` then you'll need to add this key and its value to the Application Settings for your deployed bot. + +- Log into the [Azure portal](https://portal.azure.com) +- In the left nav, click on `Bot Services` +- Click the `` Name to display the bots Web App Settings +- Click the `Application Settings` +- Scroll to the `Application settings` section +- Click `+ Add new setting` +- Add the key `TranslatorKey` with a value of the Translator Text API `Authentication key` created from the steps above + +## Further reading + +- [Bot Framework Documentation](https://docs.botframework.com) +- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) +- [Bot State](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-storage-concept?view=azure-bot-service-4.0) +- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) +- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) +- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) +- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest) +- [Azure Portal](https://portal.azure.com) +- [Spring Boot](https://spring.io/projects/spring-boot) diff --git a/samples/17.multilingual-bot/deploymentTemplates/new-rg-parameters.json b/samples/17.multilingual-bot/deploymentTemplates/new-rg-parameters.json new file mode 100644 index 000000000..ead339093 --- /dev/null +++ b/samples/17.multilingual-bot/deploymentTemplates/new-rg-parameters.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "groupLocation": { + "value": "" + }, + "groupName": { + "value": "" + }, + "appId": { + "value": "" + }, + "appSecret": { + "value": "" + }, + "botId": { + "value": "" + }, + "botSku": { + "value": "" + }, + "newAppServicePlanName": { + "value": "" + }, + "newAppServicePlanSku": { + "value": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + } + }, + "newAppServicePlanLocation": { + "value": "" + }, + "newWebAppName": { + "value": "" + } + } +} \ No newline at end of file diff --git a/samples/17.multilingual-bot/deploymentTemplates/preexisting-rg-parameters.json b/samples/17.multilingual-bot/deploymentTemplates/preexisting-rg-parameters.json new file mode 100644 index 000000000..b6f5114fc --- /dev/null +++ b/samples/17.multilingual-bot/deploymentTemplates/preexisting-rg-parameters.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appId": { + "value": "" + }, + "appSecret": { + "value": "" + }, + "botId": { + "value": "" + }, + "botSku": { + "value": "" + }, + "newAppServicePlanName": { + "value": "" + }, + "newAppServicePlanSku": { + "value": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + } + }, + "appServicePlanLocation": { + "value": "" + }, + "existingAppServicePlan": { + "value": "" + }, + "newWebAppName": { + "value": "" + } + } +} \ No newline at end of file diff --git a/samples/17.multilingual-bot/deploymentTemplates/template-with-new-rg.json b/samples/17.multilingual-bot/deploymentTemplates/template-with-new-rg.json new file mode 100644 index 000000000..3a0e81219 --- /dev/null +++ b/samples/17.multilingual-bot/deploymentTemplates/template-with-new-rg.json @@ -0,0 +1,183 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "groupLocation": { + "type": "string", + "metadata": { + "description": "Specifies the location of the Resource Group." + } + }, + "groupName": { + "type": "string", + "metadata": { + "description": "Specifies the name of the Resource Group." + } + }, + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "metadata": { + "description": "The name of the App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "newAppServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan. Defaults to \"westus\"." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "appServicePlanName": "[parameters('newAppServicePlanName')]", + "resourcesLocation": "[parameters('newAppServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]", + "resourceGroupId": "[concat(subscription().id, '/resourceGroups/', parameters('groupName'))]" + }, + "resources": [ + { + "name": "[parameters('groupName')]", + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2018-05-01", + "location": "[parameters('groupLocation')]", + "properties": {} + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2018-05-01", + "name": "storageDeployment", + "resourceGroup": "[parameters('groupName')]", + "dependsOn": [ + "[resourceId('Microsoft.Resources/resourceGroups/', parameters('groupName'))]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "variables": {}, + "resources": [ + { + "comments": "Create a new App Service Plan", + "type": "Microsoft.Web/serverfarms", + "name": "[variables('appServicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "properties": { + "name": "[variables('appServicePlanName')]" + } + }, + { + "comments": "Create a Web App using the new App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2015-08-01", + "location": "[variables('resourcesLocation')]", + "kind": "app", + "dependsOn": [ + "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "serverFarmId": "[variables('appServicePlanName')]", + "siteConfig": { + "appSettings": [ + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "10.14.1" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ], + "outputs": {} + } + } + } + ] +} \ No newline at end of file diff --git a/samples/17.multilingual-bot/deploymentTemplates/template-with-preexisting-rg.json b/samples/17.multilingual-bot/deploymentTemplates/template-with-preexisting-rg.json new file mode 100644 index 000000000..34a026819 --- /dev/null +++ b/samples/17.multilingual-bot/deploymentTemplates/template-with-preexisting-rg.json @@ -0,0 +1,154 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Defaults to \"\"." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "defaultValue": "F0", + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The name of the new App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "appServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan." + } + }, + "existingAppServicePlan": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the existing App Service Plan used to create the Web App for the bot." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", + "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", + "resourcesLocation": "[parameters('appServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" + }, + "resources": [ + { + "comments": "Create a new App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "condition": "[not(variables('useExistingAppServicePlan'))]", + "name": "[variables('servicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "properties": { + "name": "[variables('servicePlanName')]" + } + }, + { + "comments": "Create a Web App using an App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2015-08-01", + "location": "[variables('resourcesLocation')]", + "kind": "app", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", + "siteConfig": { + "appSettings": [ + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "10.14.1" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ] +} \ No newline at end of file diff --git a/samples/17.multilingual-bot/pom.xml b/samples/17.multilingual-bot/pom.xml new file mode 100644 index 000000000..137e34808 --- /dev/null +++ b/samples/17.multilingual-bot/pom.xml @@ -0,0 +1,238 @@ + + + + 4.0.0 + + com.microsoft.bot.sample + bot-multilingual + sample + jar + + ${project.groupId}:${project.artifactId} + This package contains a Java Multi-Lingual Bot sample using Spring Boot. + http://maven.apache.org + + + org.springframework.boot + spring-boot-starter-parent + 2.4.0 + + + + + + MIT License + http://www.opensource.org/licenses/mit-license.php + + + + + + Bot Framework Development + + Microsoft + https://dev.botframework.com/ + + + + + 1.8 + 1.8 + 1.8 + com.microsoft.bot.sample.multilingual.Application + + + + + org.springframework.boot + spring-boot-starter-test + 2.4.0 + test + + + junit + junit + 4.13.1 + test + + + org.junit.vintage + junit-vintage-engine + test + + + + org.slf4j + slf4j-api + + + org.apache.logging.log4j + log4j-api + 2.11.0 + + + org.apache.logging.log4j + log4j-core + 2.13.2 + + + + com.microsoft.bot + bot-integration-spring + 4.6.0-preview8 + compile + + + + + + build + + true + + + + + src/main/resources + false + + + + + maven-compiler-plugin + 3.8.1 + + + maven-war-plugin + 3.2.3 + + src/main/webapp + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + com.microsoft.bot.sample.multilingual.Application + + + + + + com.microsoft.azure + azure-webapp-maven-plugin + 1.7.0 + + V2 + ${groupname} + ${botname} + + + JAVA_OPTS + -Dserver.port=80 + + + + linux + jre8 + jre8 + + + + + ${project.basedir}/target + + *.jar + + + + + + + + + + + + publish + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + maven-war-plugin + 3.2.3 + + src/main/webapp + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + true + + true + ossrh + https://oss.sonatype.org/ + true + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + 8 + false + + + + attach-javadocs + + jar + + + + + + + + + diff --git a/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/Application.java b/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/Application.java new file mode 100644 index 000000000..c1052cd4b --- /dev/null +++ b/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/Application.java @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.multilingual; + +import com.microsoft.bot.builder.ConversationState; +import com.microsoft.bot.builder.Storage; +import com.microsoft.bot.builder.UserState; +import com.microsoft.bot.integration.AdapterWithErrorHandler; +import com.microsoft.bot.integration.BotFrameworkHttpAdapter; +import com.microsoft.bot.integration.Configuration; +import com.microsoft.bot.integration.spring.BotController; +import com.microsoft.bot.integration.spring.BotDependencyConfiguration; +import com.microsoft.bot.sample.multilingual.translation.MicrosoftTranslator; +import com.microsoft.bot.sample.multilingual.translation.TranslationMiddleware; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +/** + * This is the starting point of the Sprint Boot Bot application. + * + * This class also provides overrides for dependency injections. A class that + * extends the {@link com.microsoft.bot.builder.Bot} interface should be + * annotated with @Component. + * + * @see MultiLingualBot + */ +@SpringBootApplication + +// Use the default BotController to receive incoming Channel messages. A custom +// controller could be used by eliminating this import and creating a new +// RestController. +// The default controller is created by the Spring Boot container using +// dependency injection. The default route is /api/messages. +@Import({BotController.class}) + +public class Application extends BotDependencyConfiguration { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + /** + * Returns a custom Adapter that provides error handling. + * + * @param configuration The Configuration object to use. + * @return An error handling BotFrameworkHttpAdapter. + */ + @Override + public BotFrameworkHttpAdapter getBotFrameworkHttpAdaptor(Configuration configuration) { + Storage storage = this.getStorage(); + ConversationState conversationState = this.getConversationState(storage); + + BotFrameworkHttpAdapter adapter = new AdapterWithErrorHandler(configuration, conversationState); + TranslationMiddleware translationMiddleware = this.getTranslationMiddleware(configuration); + adapter.use(translationMiddleware); + return adapter; + } + + /** + * Create the Microsoft Translator responsible for making calls to the Cognitive Services translation service. + * @param configuration The Configuration object to use. + * @return MicrosoftTranslator + */ + @Bean + public MicrosoftTranslator getMicrosoftTranslator(Configuration configuration) { + return new MicrosoftTranslator(configuration); + } + + /** + * Create the Translation Middleware that will be added to the middleware pipeline in the AdapterWithErrorHandler. + * @param configuration The Configuration object to use. + * @return TranslationMiddleware + */ + @Bean + public TranslationMiddleware getTranslationMiddleware(Configuration configuration) { + Storage storage = this.getStorage(); + UserState userState = this.getUserState(storage); + MicrosoftTranslator microsoftTranslator = this.getMicrosoftTranslator(configuration); + return new TranslationMiddleware(microsoftTranslator, userState); + } +} diff --git a/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/MultiLingualBot.java b/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/MultiLingualBot.java new file mode 100644 index 000000000..d549c3f91 --- /dev/null +++ b/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/MultiLingualBot.java @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.multilingual; + +import com.codepoetics.protonpack.collectors.CompletableFutures; +import com.google.common.base.Strings; + +import com.microsoft.bot.builder.ActivityHandler; +import com.microsoft.bot.builder.UserState; +import com.microsoft.bot.builder.StatePropertyAccessor; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.CardAction; +import com.microsoft.bot.schema.SuggestedActions; +import com.microsoft.bot.schema.ActionTypes; +import com.microsoft.bot.schema.Attachment; +import com.microsoft.bot.schema.Serialization; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +/** + * This bot demonstrates how to use Microsoft Translator. + * More information can be found + * here https://docs.microsoft.com/en-us/azure/cognitive-services/translator/translator-info-overview. + */ +@Component +public class MultiLingualBot extends ActivityHandler { + private static final String WELCOME_TEXT = + new StringBuilder("This bot will introduce you to translation middleware. ") + .append("Say 'hi' to get started.").toString(); + + private static final String ENGLISH_ENGLISH = "en"; + private static final String ENGLISH_SPANISH = "es"; + private static final String SPANISH_ENGLISH = "in"; + private static final String SPANISH_SPANISH = "it"; + + private UserState userState; + private StatePropertyAccessor languagePreference; + + /** + * Creates a Multilingual bot. + * @param withUserState User state object. + */ + public MultiLingualBot(UserState withUserState) { + if (withUserState == null) { + throw new IllegalArgumentException("userState"); + } + this.userState = withUserState; + + this.languagePreference = userState.createProperty("LanguagePreference"); + } + + /** + * This method is executed when a user is joining to the conversation. + * @param membersAdded A list of all the members added to the conversation, + * as described by the conversation update activity. + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + @Override + protected CompletableFuture onMembersAdded(List membersAdded, + TurnContext turnContext) { + return MultiLingualBot.sendWelcomeMessage(turnContext); + } + + /** + * This method is executed when the turnContext receives a message activity. + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + @Override + protected CompletableFuture onMessageActivity(TurnContext turnContext) { + if (MultiLingualBot.isLanguageChangeRequested(turnContext.getActivity().getText())) { + String currentLang = turnContext.getActivity().getText().toLowerCase(); + String lang = currentLang.equals(ENGLISH_ENGLISH) || currentLang.equals(SPANISH_ENGLISH) + ? ENGLISH_ENGLISH : ENGLISH_SPANISH; + + // If the user requested a language change through the suggested actions with values "es" or "en", + // simply change the user's language preference in the user state. + // The translation middleware will catch this setting and translate both ways to the user's + // selected language. + // If Spanish was selected by the user, the reply below will actually be shown in spanish to the user. + return languagePreference.set(turnContext, lang) + .thenCompose(task -> { + Activity reply = MessageFactory.text(String.format("Your current language code is: %s", lang)); + return turnContext.sendActivity(reply); + }) + // Save the user profile updates into the user state. + .thenCompose(task -> userState.saveChanges(turnContext, false)); + } else { + // Show the user the possible options for language. If the user chooses a different language + // than the default, then the translation middleware will pick it up from the user state and + // translate messages both ways, i.e. user to bot and bot to user. + Activity reply = MessageFactory.text("Choose your language:"); + CardAction esAction = new CardAction() { + { + setTitle("Español"); + setType(ActionTypes.POST_BACK); + setValue(ENGLISH_SPANISH); + } + }; + CardAction enAction = new CardAction() { + { + setTitle("English"); + setType(ActionTypes.POST_BACK); + setValue(ENGLISH_ENGLISH); + } + }; + List actions = new ArrayList<>(Arrays.asList(esAction, enAction)); + SuggestedActions suggestedActions = new SuggestedActions() { + { + setActions(actions); + } + }; + reply.setSuggestedActions(suggestedActions); + return turnContext.sendActivity(reply).thenApply(resourceResponse -> null); + } + } + + private static CompletableFuture sendWelcomeMessage(TurnContext turnContext) { + return turnContext.getActivity().getMembersAdded().stream() + .filter(member -> !StringUtils.equals(member.getId(), turnContext.getActivity().getRecipient().getId())) + .map(channel -> { + Attachment welcomeCard = MultiLingualBot.createAdaptiveCardAttachment(); + Activity response = MessageFactory.attachment(welcomeCard); + return turnContext.sendActivity(response) + .thenCompose(task -> turnContext.sendActivity(MessageFactory.text(WELCOME_TEXT))); + }) + .collect(CompletableFutures.toFutureList()) + .thenApply(resourceResponse -> null); + } + + /** + * Load attachment from file. + * @return the welcome adaptive card + */ + private static Attachment createAdaptiveCardAttachment() { + // combine path for cross platform support + try ( + InputStream input = Thread.currentThread().getContextClassLoader() + .getResourceAsStream("cards/welcomeCard.json") + ) { + String adaptiveCardJson = IOUtils.toString(input, StandardCharsets.UTF_8.toString()); + + return new Attachment() {{ + setContentType("application/vnd.microsoft.card.adaptive"); + setContent(Serialization.jsonToTree(adaptiveCardJson)); + }}; + } catch (IOException e) { + e.printStackTrace(); + return new Attachment(); + } + } + + /** + * Checks whether the utterance from the user is requesting a language change. + * In a production bot, we would use the Microsoft Text Translation API language + * detection feature, along with detecting language names. + * For the purpose of the sample, we just assume that the user requests language + * changes by responding with the language code through the suggested action presented + * above or by typing it. + * @param utterance utterance the current turn utterance. + * @return the utterance. + */ + private static Boolean isLanguageChangeRequested(String utterance) { + if (Strings.isNullOrEmpty(utterance)) { + return false; + } + + utterance = utterance.toLowerCase().trim(); + return utterance.equals(ENGLISH_SPANISH) || utterance.equals(ENGLISH_ENGLISH) + || utterance.equals(SPANISH_SPANISH) || utterance.equals(SPANISH_ENGLISH); + } +} diff --git a/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/translation/MicrosoftTranslator.java b/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/translation/MicrosoftTranslator.java new file mode 100644 index 000000000..f4a2b347a --- /dev/null +++ b/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/translation/MicrosoftTranslator.java @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.multilingual.translation; + +import java.io.Reader; +import java.io.StringReader; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import com.microsoft.bot.integration.Configuration; +import com.microsoft.bot.sample.multilingual.translation.model.TranslatorResponse; + +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Strings; + +import org.slf4j.LoggerFactory; + +/** + * A helper class wrapper for the Microsoft Translator API. + */ +public class MicrosoftTranslator { + private static final String HOST = "https://api.cognitive.microsofttranslator.com"; + private static final String PATH = "/translate?api-version=3.0"; + private static final String URI_PARAMS = "&to="; + + private static String key; + + /** + * @param configuration The configuration class with the translator key stored. + */ + public MicrosoftTranslator(Configuration configuration) { + String translatorKey = configuration.getProperty("TranslatorKey"); + + if (translatorKey == null) { + throw new IllegalArgumentException("key"); + } + + MicrosoftTranslator.key = translatorKey; + } + + /** + * Helper method to translate text to a specified language. + * @param text Text that will be translated. + * @param targetLocale targetLocale Two character language code, e.g. "en", "es". + * @return The first translation result + */ + public CompletableFuture translate(String text, String targetLocale) { + return CompletableFuture.supplyAsync(() -> { + // From Cognitive Services translation documentation: + // https://docs.microsoft.com/en-us/azure/cognitive-services/Translator/quickstart-translator?tabs=java + String body = String.format("[{ \"Text\": \"%s\" }]", text); + + String uri = new StringBuilder(MicrosoftTranslator.HOST) + .append(MicrosoftTranslator.PATH) + .append(MicrosoftTranslator.URI_PARAMS) + .append(targetLocale).toString(); + + RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), body); + + OkHttpClient client = new OkHttpClient(); + Request request = new Request.Builder() + .url(uri) + .header("Ocp-Apim-Subscription-Key", MicrosoftTranslator.key) + .post(requestBody) + .build(); + + try { + Response response = client.newCall(request).execute(); + + if (!response.isSuccessful()) { + String message = new StringBuilder("The call to the translation service returned HTTP status code ") + .append(response.code()) + .append(".").toString(); + throw new Exception(message); + } + + ObjectMapper objectMapper = new ObjectMapper(); + Reader reader = new StringReader(response.body().string()); + TranslatorResponse[] result = objectMapper.readValue(reader, TranslatorResponse[].class); + + return result[0].getTranslations().get(0).getText(); + + } catch (Exception e) { + LoggerFactory.getLogger(MicrosoftTranslator.class).error("findPackages", e); + throw new CompletionException(e); + } + }); + } +} diff --git a/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/translation/TranslationMiddleware.java b/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/translation/TranslationMiddleware.java new file mode 100644 index 000000000..b36653cdb --- /dev/null +++ b/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/translation/TranslationMiddleware.java @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.multilingual.translation; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import com.google.common.base.Strings; +import com.microsoft.bot.builder.Middleware; +import com.microsoft.bot.builder.NextDelegate; +import com.microsoft.bot.builder.StatePropertyAccessor; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.UserState; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; + +/** + * Middleware for translating text between the user and bot. + * Uses the Microsoft Translator Text API. + */ +public class TranslationMiddleware implements Middleware { + private MicrosoftTranslator translator; + private StatePropertyAccessor languageStateProperty; + + /** + * Initializes a new instance of the {@link TranslationMiddleware} class. + * @param withTranslator Translator implementation to be used for text translation. + * @param userState State property for current language. + */ + public TranslationMiddleware(MicrosoftTranslator withTranslator, UserState userState) { + if (withTranslator == null) { + throw new IllegalArgumentException("withTranslator"); + } + this.translator = withTranslator; + if (userState == null) { + throw new IllegalArgumentException("userState"); + } + + this.languageStateProperty = userState.createProperty("LanguagePreference"); + } + + /** + * Processes an incoming activity. + * @param turnContext Context object containing information for a single turn of conversation with a user. + * @param next The delegate to call to continue the bot middleware pipeline. + * @return A Task representing the asynchronous operation. + */ + public CompletableFuture onTurn(TurnContext turnContext, NextDelegate next) { + if (turnContext == null) { + throw new IllegalArgumentException("turnContext"); + } + + return this.shouldTranslate(turnContext).thenCompose(translate -> { + if (translate) { + if (turnContext.getActivity().getType() == ActivityTypes.MESSAGE) { + return this.translator.translate( + turnContext.getActivity().getText(), + TranslationSettings.DEFAULT_LANGUAGE) + .thenApply(text -> { + turnContext.getActivity().setText(text); + return CompletableFuture.completedFuture(null); + }); + } + } + return CompletableFuture.completedFuture(null); + }).thenCompose(task -> { + turnContext.onSendActivities((newContext, activities, nextSend) -> { + return this.languageStateProperty.get(turnContext, () -> TranslationSettings.DEFAULT_LANGUAGE).thenCompose(userLanguage -> { + Boolean shouldTranslate = !userLanguage.equals(TranslationSettings.DEFAULT_LANGUAGE); + + // Translate messages sent to the user to user language + if (shouldTranslate) { + ArrayList> tasks = new ArrayList>(); + for (Activity activity : activities.stream().filter(a -> a.getType() == ActivityTypes.MESSAGE).collect(Collectors.toList())) { + tasks.add(this.translateMessageActivity(activity, userLanguage)); + } + + if (!Arrays.asList(tasks).isEmpty()) { + CompletableFuture.allOf(tasks.toArray(new CompletableFuture[tasks.size()])).join(); + } + } + + return nextSend.get(); + }); + }); + + turnContext.onUpdateActivity((newContext, activity, nextUpdate) -> { + return this.languageStateProperty.get(turnContext, () -> TranslationSettings.DEFAULT_LANGUAGE).thenCompose(userLanguage -> { + Boolean shouldTranslate = !userLanguage.equals(TranslationSettings.DEFAULT_LANGUAGE); + + // Translate messages sent to the user to user language + if (activity.getType() == ActivityTypes.MESSAGE) { + if (shouldTranslate) { + this.translateMessageActivity(activity, userLanguage); + } + } + + return nextUpdate.get(); + }); + }); + + return next.next(); + }); + } + + private CompletableFuture translateMessageActivity(Activity activity, String targetLocale) { + if (activity.getType() == ActivityTypes.MESSAGE) { + return this.translator.translate(activity.getText(), targetLocale).thenAccept(text -> { + activity.setText(text); + }); + } + return CompletableFuture.completedFuture(null); + } + + private CompletableFuture shouldTranslate(TurnContext turnContext) { + return this.languageStateProperty.get(turnContext, () -> TranslationSettings.DEFAULT_LANGUAGE).thenApply(userLanguage -> { + if (Strings.isNullOrEmpty(userLanguage)) { + userLanguage = TranslationSettings.DEFAULT_LANGUAGE; + } + return !userLanguage.equals(TranslationSettings.DEFAULT_LANGUAGE); + }); + } +} diff --git a/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/translation/TranslationSettings.java b/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/translation/TranslationSettings.java new file mode 100644 index 000000000..9f216d220 --- /dev/null +++ b/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/translation/TranslationSettings.java @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.multilingual.translation; + +/** + * General translation settings and constants. + */ +public class TranslationSettings { + public static final String DEFAULT_LANGUAGE = "en"; +} diff --git a/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/translation/model/TranslatorResponse.java b/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/translation/model/TranslatorResponse.java new file mode 100644 index 000000000..c4f056eb8 --- /dev/null +++ b/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/translation/model/TranslatorResponse.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.multilingual.translation.model; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Array of translated results from Translator API v3. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class TranslatorResponse { + @JsonProperty("translations") + private List translations; + + /** + * Gets the translation results. + * @return A list of {@link TranslatorResult} + */ + public List getTranslations() { + return this.translations; + } + + /** + * Sets the translation results. + * @param withTranslations A list of {@link TranslatorResult} + */ + public void setTranslations(List withTranslations) { + this.translations = withTranslations; + } +} diff --git a/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/translation/model/TranslatorResult.java b/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/translation/model/TranslatorResult.java new file mode 100644 index 000000000..6021ee88c --- /dev/null +++ b/samples/17.multilingual-bot/src/main/java/com/microsoft/bot/sample/multilingual/translation/model/TranslatorResult.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.multilingual.translation.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Translation result from Translator API v3. + */ +public class TranslatorResult { + @JsonProperty("text") + private String text; + + @JsonProperty("to") + private String to; + + /** + * Gets the translation result text. + * @return Translation result. + */ + public String getText() { + return this.text; + } + + /** + * Sets the translation result text. + * @param withText Translation result. + */ + public void setText(String withText) { + this.text = withText; + } + + /** + * Gets the target language locale. + * @return Locale. + */ + public String getTo() { + return this.to; + } + + /** + * Sets the target language locale. + * @param withTo Target locale. + */ + public void setTo(String withTo) { + this.to = withTo; + } +} diff --git a/samples/17.multilingual-bot/src/main/resources/application.properties b/samples/17.multilingual-bot/src/main/resources/application.properties new file mode 100644 index 000000000..bbbe15889 --- /dev/null +++ b/samples/17.multilingual-bot/src/main/resources/application.properties @@ -0,0 +1,4 @@ +MicrosoftAppId= +MicrosoftAppPassword= +TranslatorKey= +server.port=3978 diff --git a/samples/17.multilingual-bot/src/main/resources/cards/welcomeCard.json b/samples/17.multilingual-bot/src/main/resources/cards/welcomeCard.json new file mode 100644 index 000000000..47e5614df --- /dev/null +++ b/samples/17.multilingual-bot/src/main/resources/cards/welcomeCard.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.0", + "body": [ + { + "type": "Image", + "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU", + "size": "stretch" + }, + { + "type": "TextBlock", + "spacing": "medium", + "size": "default", + "weight": "bolder", + "text": "Welcome to Bot Framework!", + "wrap": true, + "maxLines": 0 + }, + { + "type": "TextBlock", + "size": "default", + "isSubtle": true, + "text": "Now that you have successfully run your bot, follow the links in this Adaptive Card to expand your knowledge of Bot Framework.", + "wrap": true, + "maxLines": 0 + } + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "Get an overview", + "url": "https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0" + }, + { + "type": "Action.OpenUrl", + "title": "Ask a question", + "url": "https://stackoverflow.com/questions/tagged/botframework" + }, + { + "type": "Action.OpenUrl", + "title": "Learn how to deploy", + "url": "https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-deploy-azure?view=azure-bot-service-4.0" + } + ] +} \ No newline at end of file diff --git a/samples/17.multilingual-bot/src/main/resources/log4j2.json b/samples/17.multilingual-bot/src/main/resources/log4j2.json new file mode 100644 index 000000000..67c0ad530 --- /dev/null +++ b/samples/17.multilingual-bot/src/main/resources/log4j2.json @@ -0,0 +1,18 @@ +{ + "configuration": { + "name": "Default", + "appenders": { + "Console": { + "name": "Console-Appender", + "target": "SYSTEM_OUT", + "PatternLayout": {"pattern": "[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n"} + } + }, + "loggers": { + "root": { + "level": "debug", + "appender-ref": {"ref": "Console-Appender","level": "debug"} + } + } + } +} diff --git a/samples/17.multilingual-bot/src/main/webapp/META-INF/MANIFEST.MF b/samples/17.multilingual-bot/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 000000000..254272e1c --- /dev/null +++ b/samples/17.multilingual-bot/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Class-Path: + diff --git a/samples/17.multilingual-bot/src/main/webapp/WEB-INF/web.xml b/samples/17.multilingual-bot/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..383c19004 --- /dev/null +++ b/samples/17.multilingual-bot/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,12 @@ + + + dispatcher + + org.springframework.web.servlet.DispatcherServlet + + + contextConfigLocation + /WEB-INF/spring/dispatcher-config.xml + + 1 + \ No newline at end of file diff --git a/samples/17.multilingual-bot/src/main/webapp/index.html b/samples/17.multilingual-bot/src/main/webapp/index.html new file mode 100644 index 000000000..c46330130 --- /dev/null +++ b/samples/17.multilingual-bot/src/main/webapp/index.html @@ -0,0 +1,418 @@ + + + + + + + Multi-lingual Bot Sample + + + + + +
+
+
+
Multi-lingual Bot Sample
+
+
+
+
+
Your bot is ready!
+
You can test your bot in the Bot Framework Emulator
+ by connecting to http://localhost:3978/api/messages.
+ +
Visit Azure + Bot Service to register your bot and add it to
+ various channels. The bot's endpoint URL typically looks + like this:
+
https://your_bots_hostname/api/messages
+
+
+
+
+ +
+ + + diff --git a/samples/17.multilingual-bot/src/test/java/com/microsoft/bot/sample/multilingual/ApplicationTest.java b/samples/17.multilingual-bot/src/test/java/com/microsoft/bot/sample/multilingual/ApplicationTest.java new file mode 100644 index 000000000..18d372838 --- /dev/null +++ b/samples/17.multilingual-bot/src/test/java/com/microsoft/bot/sample/multilingual/ApplicationTest.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.multilingual; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class ApplicationTest { + + @Test + public void contextLoads() { + } + +}