Bot Builder v4 でボット開発 : 多言語対応を行う - 翻訳サービスの利用

前回は主にボットから送るメッセージの多言語対応を見ていきました。
今回はユーザーから送られてくるフリーテキストの多言語対応を考えます。

# 何を翻訳する必要があるか

**テキストプロンプトに対してのフリーテキスト**

ボットを通じて予定を作ったり、メールを送信するなど、ユーザーのインプットをそのまま受け取ればいい場合は、翻訳の必要はありません。

**ボットに対する指示**

一方で「キャンセル」や「設定を変える」または「天気を確認したい」などボットに対する命令は、対処が必要です。

- 翻訳サービスを使って自然言語処理エンジンが理解できるものに変換する
    - 翻訳対象をどのように選定するかが課題
- 自然言語処理エンジンを言語ごとに用意する
    - サポートする言語の数だけエンジンが必要となる
    - エンジンで対応していない言語の処理ができない

今回は Bing 翻訳サービスを使ってみます。

# Bing 翻訳サービスの準備

1\. [Azure ポータル](https://portal.azure.com)　にログインして、「リソースの追加」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/467bfd5f-fd83-951b-f9b1-bcd55117c15e.png)

2\. Translator Text を検索して、「作成」。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/528de531-4e6f-fd20-8057-8833eb861621.png)

3\. 任意の名前をつけて「作成」。価格プランはテストのため、無料のものを選択。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/9fc7f1f0-ddfe-5bbc-de4f-7615669a12a2.png)

4\. 作成が完了したら「Keys」メニューよりキーを確認。

# ミドルウェアでの翻訳実行 

翻訳処理はボット本体での作業ではないため、前回作成した SetLanguageMiddleware に翻訳機能を入れてみます。翻訳サービスの利用は、マイクロソフト製の SDK がなかったため、今回は [CognitiveServices.Translator.Client](https://github.com/Nordes/CognitiveServices.Translator.Client) を使ってみました。

1\. Visual Studio Code の統合コンソールより、以下のコマンドを実行してパッケージを追加。

```shell
dotnet add package CognitiveServices.Translator.Client
dotnet restore
```
2\. appsettings.json を以下のコードと差し替えて、キー情報を更新。

```json
{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*",
  "botFilePath": "./MyBot.bot",
  "CognitiveServices": {
    "Name": "myfirstbottranslator",
    "SubscriptionKey": "first key here",
    "SubscriptionKeyAlternate": "Second key here"
  }
}
```

3\. SetLanguageMiddleware.cs の OnTurnAsync を以下の様に変えて翻訳を追加。

```csharp
using System.Linq;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using CognitiveServices.Translator;
using CognitiveServices.Translator.Translate;

public class SetLanguageMiddleware : IMiddleware
{
    // ユーザープロファイルへのプロパティアクセサー
    private readonly IStatePropertyAccessor<UserProfile> userProfile;
    private ITranslateClient translator;

    public SetLanguageMiddleware(IStatePropertyAccessor<UserProfile> userProfile, ITranslateClient translator)
    {
        this.userProfile = userProfile;
        this.translator= translator;
    }
    public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default(CancellationToken))
    {
        // ユーザーの言語設定によって CurrentCulture を設定
        var profile = await userProfile.GetAsync(turnContext, () => new UserProfile(), cancellationToken);

        if (!string.IsNullOrEmpty(profile.Language))
        {
            Thread.CurrentThread.CurrentCulture = new CultureInfo(profile.Language);
            Thread.CurrentThread.CurrentUICulture = new CultureInfo(profile.Language);
            // 言語が日本語でない場合は日本語に変換する
            if (profile.Language != "ja-JP" && !string.IsNullOrEmpty(turnContext.Activity.Text))
            {
                var translateParams = new RequestParameter
                {
                    From = profile.Language,
                    To = new[] { "ja" },
                    IncludeAlignment = true,
                };
                // 元のインプットを変換したものに差し替え。
                turnContext.Activity.Text =
                (await translator.TranslateAsync(
                    new RequestContent(turnContext.Activity.Text), translateParams)
                ).First().Translations.First().Text;
            }
        }

        await next.Invoke(cancellationToken);
    }
}
```

4\. Startup.cs の AddBot<MyBot> の最後を変更してミドルウェアに TranslatorClient を渡す。

```csharp
var cognitiveServiceConfig  = new CognitiveServicesConfig();
Configuration.GetSection("CognitiveServices").Bind(cognitiveServiceConfig);
    options.Middleware.Add(new SetLanguageMiddleware(
    userState.CreateProperty<UserProfile>("UserProfile"), 
    new TranslateClient(cognitiveServiceConfig)));
```

# テスト

では早速テストをしてみます。

1\. F5 キーを押下してデバッグ実行。エミュレーターより検証。プロファイルを登録すると選択しても、匿名になってしまう。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/4cb97ab2-a192-409e-d937-16c500cf4445.png)

2\. WelcomeDialog.cs の CheckProfileAsync にブレークポイントを置いて再度検証。Yes! という返信が翻訳されることで条件に一致しないため、意図せぬ動作となる。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/847605b1-8d5e-13e4-d18b-d0facddf6b36.png)

# 考えらる対策

ユーザーのインプットは正しく日本語に翻訳されていますが、結果的に後続のコードで問題が発生しました。この場合特定のキーワードについては翻訳しない策が考えられます。

既にボット側が利用する文字列はリソースとして作成したので、ここではリソースにある文字列を翻訳対象から外すようにしてみます。

## 実装

1\. SetLanguageMiddleware.cs でリソースを使えるように変更。

- IoC コンテナを使えるよう、IServiceProvider を引数に指定
- serviceProvider.GetService にて指定したサービスを取得

```csharp
using System;
using System.Linq;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Schema;
using CognitiveServices.Translator;
using CognitiveServices.Translator.Translate;
using System.Resources;
using System.Collections;
using Microsoft.Extensions.Localization;

public class SetLanguageMiddleware : IMiddleware
{
    // ユーザープロファイルへのプロパティアクセサー
    private readonly IStatePropertyAccessor<UserProfile> userProfile;
    private readonly ITranslateClient translator;
    private readonly IServiceProvider serviceProvider;
    public SetLanguageMiddleware(IStatePropertyAccessor<UserProfile> userProfile,
        ITranslateClient translator,
        IServiceProvider serviceProvider)
    {
        this.userProfile = userProfile;
        this.translator = translator;
        this.serviceProvider = serviceProvider;
    }
    public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default(CancellationToken))
    {
        // ユーザーの言語設定によって CurrentCulture を設定
        var profile = await userProfile.GetAsync(turnContext, () => new UserProfile(), cancellationToken);

        if (!string.IsNullOrEmpty(profile.Language))
        {
            Thread.CurrentThread.CurrentCulture = new CultureInfo(profile.Language);
            Thread.CurrentThread.CurrentUICulture = new CultureInfo(profile.Language);
            // 言語が日本語でない場合は日本語に変換する
            if (profile.Language != "ja-JP" && !string.IsNullOrEmpty(turnContext.Activity.Text))
            {
                // WelcomeDialog のリソースとして localizer を取得            
                var localizer = (IStringLocalizer<WelcomeDialog>)serviceProvider.GetService(
                    typeof(IStringLocalizer<WelcomeDialog>));
                var strings = localizer.GetAllStrings();
                // ユーザー入力がリソースと一致しない場合は翻訳する
                if (strings.Where(x => x == turnContext.Activity.Text).FirstOrDefault() == null)
                {
                    var translateParams = new RequestParameter
                    {
                        From = profile.Language,
                        To = new[] { "ja" },
                        IncludeAlignment = true,
                    };
                    // 元のインプットを変換したものに差し替え。
                    turnContext.Activity.Text =
                    (await translator.TranslateAsync(
                        new RequestContent(turnContext.Activity.Text), translateParams)
                    ).First().Translations.First().Text;
                }
            }
        }

        await next.Invoke(cancellationToken);
    }
}
```
2\. Startup.cs を変更して SetLanguageMiddleware に IServiceProvider を渡す。

```csharp
options.Middleware.Add(new SetLanguageMiddleware(
    userState.CreateProperty<UserProfile>("UserProfile"), 
    new TranslateClient(cognitiveServiceConfig),
    services.BuildServiceProvider()));
```

3\. F5 を押下してデバッグ実行。先ほどと同じテストを実施。Yes! をクリックした際、次の処理に進むことを確認。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/e305943a-f7a3-b845-708c-04dc36b89ca8.png)

## 残る課題

上記のコードにはいくつかの課題があります。

- WelcomeDialog のリソースしか処理できないので、全てのリソースを処理するよう対応が必要
- 全てのリソースを追加しても、今後リソースが増えた場合に管理が必要となる
- 一方、管理を避けるため全てを 1 リソースにすると柔軟性が減る
- リソースに一致しないが翻訳したくないものが対応できない

# 翻訳サービスを MyBot クラスで実行

次にミドルウェアではなく、MyBot 本体側で翻訳してみます。

1\. SetLanguageMiddleware.cs を元のコードに戻す。

```csharp
using System.Linq;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using CognitiveServices.Translator;
using CognitiveServices.Translator.Translate;

public class SetLanguageMiddleware : IMiddleware
{
    // ユーザープロファイルへのプロパティアクセサー
    private readonly IStatePropertyAccessor<UserProfile> userProfile;
    public SetLanguageMiddleware(IStatePropertyAccessor<UserProfile> userProfile)
    {
        this.userProfile = userProfile;
    }
    public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default(CancellationToken))
    {
        // ユーザーの言語設定によって CurrentCulture を設定
        var profile = await userProfile.GetAsync(turnContext, () => new UserProfile(), cancellationToken);

        if (!string.IsNullOrEmpty(profile.Language))
        {
            Thread.CurrentThread.CurrentCulture = new CultureInfo(profile.Language);
            Thread.CurrentThread.CurrentUICulture = new CultureInfo(profile.Language);            
        }

        await next.Invoke(cancellationToken);
    }
}
```

2\. Startup.cs を変更。

- ミドルウェアに渡していた引数を元に戻す
- 翻訳サービスを IoC コンテナに登録

```csharp
using System;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.AI.Luis;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Integration;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Bot.Configuration;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using CognitiveServices.Translator.Extension;
using System.Globalization;
using Microsoft.AspNetCore.Localization;
using CognitiveServices.Translator;
using CognitiveServices.Translator.Configuration;

namespace myfirstbot
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            var secretKey = Configuration.GetSection("botFileSecret")?.Value;
            var botFilePath = Configuration.GetSection("botFilePath")?.Value;

            // 構成ファイルの読込み
            var botConfig = BotConfiguration.Load(botFilePath ?? @".\BotConfiguration.bot", secretKey);
            // 構成ファイルより LuisService を取得
            var luisService = (LuisService)botConfig.Services.Where(x => x.Type == "luis").First();
            // 構成情報より LuisApplication を作成
            var luisApp = new LuisApplication(luisService.AppId, luisService.AuthoringKey, luisService.GetEndpoint());
            var luisRecognizer = new LuisRecognizer(luisApp);
            services.AddSingleton(sp => luisRecognizer);
            services.AddSingleton(sp => botConfig ?? throw new InvalidOperationException($"The .bot config file could not be loaded. ({botConfig})"));

            services.AddBot<MyBot>(options =>
            {
                //options.Middleware.Add(new MyLoggingMiddleware());
                //options.Middleware.Add(new MyMiddleware());

                // Endpoint を構成ファイルより取得
                EndpointService endpointService = (EndpointService)botConfig.Services.Where(x => x.Type == "endpoint").First();
                // 認証として　AppId と AppPassword を使うように設定
                options.CredentialProvider = new SimpleCredentialProvider(endpointService.AppId, endpointService.AppPassword);

                // ストレージとしてインメモリを利用
                IStorage dataStore = new MemoryStorage();
                var userState = new UserState(dataStore);
                var conversationState = new ConversationState(dataStore);
                options.State.Add(userState);
                options.State.Add(conversationState);
                options.Middleware.Add(new SetLanguageMiddleware(
                    userState.CreateProperty<UserProfile>("UserProfile")));
            });

            // MyStateAccessors を IoC コンテナに登録
            services.AddSingleton(sp =>
            {
                // AddBot で登録した options を取得。
                var options = sp.GetRequiredService<IOptions<BotFrameworkOptions>>().Value;
                if (options == null)
                {
                    throw new InvalidOperationException("BotFrameworkOptions を事前に構成してください。");
                }
                var userState = options.State.OfType<UserState>().FirstOrDefault();
                if (userState == null)
                {
                    throw new InvalidOperationException("UserState を事前に定義してください。");
                }

                var conversationState = options.State.OfType<ConversationState>().FirstOrDefault();
                if (conversationState == null)
                {
                    throw new InvalidOperationException("ConversationState を事前に定義してください。");
                }

                var accessors = new MyStateAccessors(userState, conversationState)
                {
                    // DialogState を作成
                    ConversationDialogState = conversationState.CreateProperty<DialogState>("DialogState"),
                    // UserProfile を作成
                    UserProfile = userState.CreateProperty<UserProfile>("UserProfile"),
                };

                return accessors;
            });

            // リソースファイルが存在するフォルダの相対パス
            services.AddLocalization(o => o.ResourcesPath = "Resources");
            //　ローカライゼーションの設定
            services.Configure<RequestLocalizationOptions>(options =>
            {
                var supportedCultures = new[]
                {
                    new CultureInfo("en-US"),
                    new CultureInfo("ja-JP"),
                };
                // 既定とサポートされるカルチャーの設定
                options.DefaultRequestCulture = new RequestCulture("ja-JP", "ja-JP");
                options.SupportedCultures = supportedCultures;
                options.SupportedUICultures = supportedCultures;
            });

            // 翻訳サービスを追加
            services.AddCognitiveServicesTranslator(Configuration);
            services.AddScoped<ITranslateClient, TranslateClient>();

            // ダイアログも IoC コンテナに登録
            services.AddScoped<LoginDialog, LoginDialog>();
            services.AddScoped<MenuDialog, MenuDialog>();
            services.AddScoped<PhotoUpdateDialog, PhotoUpdateDialog>();
            services.AddScoped<ProfileDialog, ProfileDialog>();
            services.AddScoped<ScheduleDialog, ScheduleDialog>();
            services.AddScoped<SelectLanguageDialog, SelectLanguageDialog>();
            services.AddScoped<WeatherDialog, WeatherDialog>();
            services.AddScoped<WelcomeDialog, WelcomeDialog>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseBotFramework();
        }
    }
}
```

3\. MyBot.cs を以下のコードに差し替え。

- 翻訳サービスを利用
- dialogContext.ActiveDialog.Id から現在実行中のダイアログを確認して、対応するリソースを取得
- リソースとユーザーインプットが一致すれば LUIS に投げないなどロジックを追加
- MyBot 自体もリソース化

```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CognitiveServices.Translator;
using CognitiveServices.Translator.Translate;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.AI.Luis;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.Localization;

public class MyBot : IBot
{
    private MyStateAccessors accessors;
    private IRecognizer luisRecognizer;
    private DialogSet dialogs;
    private IStringLocalizer<MyBot> localizer;
    private ITranslateClient translator;
    private IServiceProvider serviceProvider;

    // DI で MyStateAccessors および luisRecognizer は自動解決
    public MyBot(MyStateAccessors accessors, IRecognizer luisRecognizer,
        IStringLocalizer<MyBot> localizer, IServiceProvider serviceProvider,
        ITranslateClient translator)
    {
        this.accessors = accessors;
        this.luisRecognizer = luisRecognizer;
        this.dialogs = new DialogSet(accessors.ConversationDialogState);
        this.localizer = localizer;
        this.translator = translator;
        this.serviceProvider = serviceProvider;
        // コンポーネントダイアログを追加
        dialogs.Add((WelcomeDialog)serviceProvider.GetService(typeof(WelcomeDialog)));
        dialogs.Add((ProfileDialog)serviceProvider.GetService(typeof(ProfileDialog)));
        dialogs.Add((MenuDialog)serviceProvider.GetService(typeof(MenuDialog)));
        dialogs.Add((WeatherDialog)serviceProvider.GetService(typeof(WeatherDialog)));
        dialogs.Add((ScheduleDialog)serviceProvider.GetService(typeof(ScheduleDialog)));
        dialogs.Add((PhotoUpdateDialog)serviceProvider.GetService(typeof(PhotoUpdateDialog)));
    }

    // 指定したクラスに対応するリソースを取得
    public List<string> GetResourceStrings<T>()
    {
        var localizer = (IStringLocalizer<T>)serviceProvider.GetService(
                    typeof(IStringLocalizer<T>));
        return localizer.GetAllStrings().Select(x => x.Value).ToList();
    }

    private async Task ContinueDialog(ITurnContext turnContext, DialogContext dialogContext,
        UserProfile userProfile, CancellationToken cancellationToken)
    {
        // まず ContinueDialogAsync を実行して既存のダイアログがあれば継続実行。
        var results = await dialogContext.ContinueDialogAsync(cancellationToken);

        // DialogTurnStatus が Complete または Empty の場合、メニューへ。
        if (results.Status == DialogTurnStatus.Complete || results.Status == DialogTurnStatus.Empty)
        {
            await turnContext.SendActivityAsync(MessageFactory.Text(String.Format(localizer["welcome"], userProfile.Name)));
            // メニューの表示
            await dialogContext.BeginDialogAsync(nameof(MenuDialog), null, cancellationToken);
        }
    }

    public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
    {
        // DialogSet からコンテキストを作成
        var dialogContext = await dialogs.CreateContextAsync(turnContext, cancellationToken);
        // プロファイルを取得
        var userProfile = await accessors.UserProfile.GetAsync(turnContext, () => new UserProfile(), cancellationToken);

        // ユーザーからメッセージが来た場合
        if (turnContext.Activity.Type == ActivityTypes.Message)
        {
            if (turnContext.Activity.Attachments != null)
            {
                // 添付ファイルのアドレスを取得
                var attachment = turnContext.Activity.Attachments.First();
                var attachmentUrl = attachment.ContentUrl;
                // PhotoUpdateDialog に対して画像のアドレスを渡す
                await dialogContext.BeginDialogAsync(nameof(PhotoUpdateDialog), attachmentUrl, cancellationToken);
            }
            else if (string.IsNullOrEmpty(turnContext.Activity.Text))
            {
                // Text がないためダイアログをそのまま継続
                await dialogContext.ContinueDialogAsync(cancellationToken);
            }
            else
            {
                // 現在のダイアログのリソースを取得
                List<string> strings = dialogContext.ActiveDialog == null ? new List<string>() :
                (List<string>)typeof(MyBot).GetMethod("GetResourceStrings").MakeGenericMethod(
                    new Type[] { Type.GetType( dialogContext.ActiveDialog.Id) }
                ).Invoke(this, null);

                // リソースのテキストだった場合そのままダイアログを実行
                if (strings.Where(x => x == turnContext.Activity.Text).FirstOrDefault() != null)
                {
                    await ContinueDialog(turnContext, dialogContext, userProfile, cancellationToken);
                }
                else
                {
                    // 日本語以外の言語の処理のため元のテキストを保存
                    var originalInput = turnContext.Activity.Text;

                    // 言語が日本語でない場合は一旦翻訳
                    if (userProfile.Language != "ja-JP" && !string.IsNullOrEmpty(turnContext.Activity.Text))
                    {
                        var translateParams = new RequestParameter
                        {
                            From = userProfile.Language,
                            To = new[] { "ja" },
                            IncludeAlignment = true,
                        };

                        // LUIS に投げるために、元のインプットを変換したものに差し替え。
                        turnContext.Activity.Text =
                        (await translator.TranslateAsync(
                            new RequestContent(turnContext.Activity.Text), translateParams)
                        ).First().Translations.First().Text;
                    }

                    var luisResult = await luisRecognizer.RecognizeAsync(turnContext, cancellationToken);
                    var topIntent = luisResult?.GetTopScoringIntent();
                    if (topIntent != null && topIntent.HasValue)
                    {
                        if (topIntent.Value.intent == "Cancel")
                        {
                            await turnContext.SendActivityAsync(localizer["cancel"], cancellationToken: cancellationToken);
                            await dialogContext.CancelAllDialogsAsync(cancellationToken);
                            await dialogContext.BeginDialogAsync(nameof(MenuDialog), null, cancellationToken);
                        }
                        else if (topIntent.Value.intent == "Logout")
                        {
                            // アダプターを取得
                            var botAdapter = (BotFrameworkAdapter)turnContext.Adapter;
                            // 指定した接続をログアウト
                            await botAdapter.SignOutUserAsync(turnContext, "AzureAdv2", cancellationToken: cancellationToken);
                            await turnContext.SendActivityAsync(localizer["loggedout"], cancellationToken: cancellationToken);
                            var results = await dialogContext.ContinueDialogAsync(cancellationToken);

                            // DialogTurnStatus が Complete または Empty の場合、メニューへ。
                            if (results.Status == DialogTurnStatus.Complete || results.Status == DialogTurnStatus.Empty)
                                // メニューの表示
                                await dialogContext.BeginDialogAsync(nameof(MenuDialog), null, cancellationToken);
                        }
                        else if (topIntent.Value.intent == "Schedule")
                        {
                            await dialogContext.BeginDialogAsync(nameof(ScheduleDialog), null, cancellationToken);
                        }
                        else if (topIntent.Value.intent == "Profile")
                        {
                            await dialogContext.BeginDialogAsync(nameof(ProfileDialog), null, cancellationToken);
                        }
                        else if (topIntent.Value.intent == "Weather")
                        {
                            var day = luisResult.Entities["day"] == null ? null : luisResult.Entities["day"][0][0].ToString();
                            await dialogContext.BeginDialogAsync(nameof(WeatherDialog), day, cancellationToken);
                        }
                        else
                        {
                            // ヘルプの場合は使い方を言って、そのまま処理継続
                            if (topIntent.Value.intent == "Help")
                            {
                                await turnContext.SendActivityAsync(localizer["help"], cancellationToken: cancellationToken);
                            }

                            // LUIS で分類できなかったため、元に戻す
                            turnContext.Activity.Text = originalInput;
                            // まず ContinueDialogAsync を実行して既存のダイアログがあれば継続実行。
                            await ContinueDialog(turnContext, dialogContext, userProfile, cancellationToken);
                        }
                    }
                }
            }

            // ユーザーに応答できなかった場合
            if (!turnContext.Responded)
            {
                await turnContext.SendActivityAsync(localizer["resetall"], cancellationToken: cancellationToken);
                await dialogContext.CancelAllDialogsAsync(cancellationToken);
                await dialogContext.BeginDialogAsync(nameof(MenuDialog), null, cancellationToken);
            }
        }
        // ユーザーとボットが会話に参加した
        else if (turnContext.Activity.Type == ActivityTypes.ConversationUpdate)
        {
            // turnContext より Activity を取得
            var activity = turnContext.Activity.AsConversationUpdateActivity();
            // ユーザーの参加に対してだけ、プロファイルダイアログを開始
            if (activity.MembersAdded.Any(member => member.Id != activity.Recipient.Id))
            {
                if (userProfile == null || string.IsNullOrEmpty(userProfile.Name))
                {
                    //await turnContext.SendActivityAsync("ようこそ MyBot へ！");
                    await dialogContext.BeginDialogAsync(nameof(WelcomeDialog), null, cancellationToken);
                    //await dialogContext.BeginDialogAsync(nameof(ProfileDialog), null, cancellationToken);
                }
                else
                {
                    await turnContext.SendActivityAsync(MessageFactory.Text(string.Format(localizer["welcome"], userProfile.Name)));
                    if (userProfile.HasCat)
                        await turnContext.SendActivityAsync(MessageFactory.Text(string.Format(localizer["howarecats"], userProfile.CatNum)));

                    // メニューの表示
                    await dialogContext.BeginDialogAsync(nameof(MenuDialog), null, cancellationToken);
                }
            }
        }
        else if (turnContext.Activity.Type == ActivityTypes.Event || turnContext.Activity.Type == ActivityTypes.Invoke)
        {
            // Event または Invoke で戻ってきた場合ダイアログを続ける
            await dialogContext.ContinueDialogAsync(cancellationToken);
            if (!turnContext.Responded)
            {
                await turnContext.SendActivityAsync(localizer["resetall"], cancellationToken: cancellationToken);
                await dialogContext.CancelAllDialogsAsync(cancellationToken);
                await dialogContext.BeginDialogAsync(nameof(MenuDialog), null, cancellationToken);
            }
        }

        // 最後に現在の UserProfile と DialogState を保存
        await accessors.UserState.SaveChangesAsync(turnContext, false, cancellationToken);
        await accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
    }
}
```
4\. Resources フォルダに MyBot.ja.resx と MyBot.en.resx を追加。

```xml:MyBot.ja.resx
<?xml version="1.0" encoding="utf-8"?>
<root>
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" />
              </xsd:sequence>
              <xsd:attribute name="name" use="required" type="xsd:string" />
              <xsd:attribute name="type" type="xsd:string" />
              <xsd:attribute name="mimetype" type="xsd:string" />
              <xsd:attribute ref="xml:space" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string" />
              <xsd:attribute name="name" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
              <xsd:attribute ref="xml:space" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" />
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <data name="cancel" xml:space="preserve">
    <value>キャンセルします</value>
  </data>
  <data name="help" xml:space="preserve">
    <value>天気と予定が確認できます。</value>
  </data>
  <data name="howarecats" xml:space="preserve">
    <value>{0}匹の猫は元気ですか？</value>
  </data>
  <data name="loggedout" xml:space="preserve">
    <value>ログアウトしました。</value>
  </data>
  <data name="resetall" xml:space="preserve">
    <value>わかりませんでした。全てキャンセルします。</value>
  </data>
  <data name="welcome" xml:space="preserve">
    <value>ようこそ '{0}' さん！</value>
  </data>
</root>
```
```xml:MyBot.en.resx
<?xml version="1.0" encoding="utf-8"?>
<root>
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" />
              </xsd:sequence>
              <xsd:attribute name="name" use="required" type="xsd:string" />
              <xsd:attribute name="type" type="xsd:string" />
              <xsd:attribute name="mimetype" type="xsd:string" />
              <xsd:attribute ref="xml:space" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string" />
              <xsd:attribute name="name" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
              <xsd:attribute ref="xml:space" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" />
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <data name="cancel" xml:space="preserve">
    <value>Cancel</value>
  </data>
  <data name="help" xml:space="preserve">
    <value>You can check weather and your schedule</value>
  </data>
  <data name="howarecats" xml:space="preserve">
    <value>How are your {0} cats doing?</value>
  </data>
  <data name="loggedout" xml:space="preserve">
    <value>Logged out</value>
  </data>
  <data name="resetall" xml:space="preserve">
    <value>Sorry but I couldn't understand. I cancell all dialogs.</value>
  </data>
  <data name="welcome" xml:space="preserve">
    <value>Welcome {0}!</value>
  </data>
</root>
```
5\. myfirstbot.csproj ファイルに埋め込みリソースを追加します。

```xml
<EmbeddedResource Update="Resources\MyBot.en.resx">
  <Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resources\MyBot.ja.resx">
  <Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
```

6\. 実際に動作するか確認。

# まとめ

開発中のボットの性質を理解した上でどのタイミングで翻訳を実行するか慎重に検討する必要がありそうです。状況に応じてより良い方法を考えてみてください。次回は多言語化された QnA ボットの利用を見ていきます。

[次の記事](https://qiita.com/kenakamu/items/8e0195b671d629c479c2)
[目次に戻る](https://qiita.com/kenakamu/items/6dc043cfc1f199032883)

[この記事のサンプルコード](https://github.com/kenakamu/botbuilderv4completeguide/tree/article17)