0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Bot Builder v4 でボット開発 : 多言語対応を行う - アダプティブカードの多言語化

Last updated at Posted at 2018-11-17

前回はリソースファイルを使ってボットの多言語化を始めました。今回はアダプティブカードの多言語化と、前回やり残したダイアログの多言語化を行います。

アダプティブカードの多言語化

今回のボットでは JSON ファイルからアダプティブカードを作っています。アダプティブカードの中身は翻訳しないといけない箇所が多いため、ここでは言語別のアダプティブカードを作って、ユーザーが指定した言語で読み込むように変更します。

1. AdaptiveJsons フォルダ配下に ja-JP フォルダを作成し、既存の JSON を全て移動。
image.png

2. 同様に en-US フォルダを作成して、既存の JSON をコピー。英語に翻訳。
image.png

3. json ファイルを読み取るコードに言語を追加。ProfileDialog.cs の場合は以下の様に変更。

var userProfile = await accessors.UserProfile.GetAsync(stepContext.Context, 
    ()=> new UserProfile(), cancellationToken);

var adaptiveCardJson = File.ReadAllText(
    $"./AdaptiveJsons/{userProfile.Language}/Profile.json");

残りのダイアログの多言語化

前回の記事同様、残りのダイアログも多言語化対応します。

1. LoginDialog 用の RESX ファイルを追加。

LoginDialog.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="signindialog" xml:space="preserve">
    <value>サインイン ダイアログ</value>
  </data>
  <data name="signin" xml:space="preserve">
    <value>サインイン</value>
  </data>
  <data name="failed" xml:space="preserve">
    <value>サインインに失敗しました</value>
  </data>
</root>
LoginDialog.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="signindialog" xml:space="preserve">
    <value>Signin Dialog</value>
  </data>
  <data name="signin" xml:space="preserve">
    <value>Signin</value>
  </data>
  <data name="failed" xml:space="preserve">
    <value>Failed to signin</value>
  </data>
</root>

2. LoginDialog.cs でリソースを使うように変更。

using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.Localization;

public class LoginDialog : ComponentDialog
{
    private const string connectionName = "AzureAdv2";
    private IStringLocalizer<LoginDialog> localizer;

    public LoginDialog(IStringLocalizer<LoginDialog> localizer) : base(nameof(LoginDialog))
    {
        this.localizer = localizer;
        // ウォーターフォールのステップを定義。処理順にメソッドを追加。
        var waterfallSteps = new WaterfallStep[]
        {
            LoginAsync,
            CompleteLoginAsync,
        };

        // ウォーターフォールダイアログと各種プロンプトを追加
        AddDialog(new WaterfallDialog("logindialog", waterfallSteps));
        AddDialog(new OAuthPrompt(
                "login",
                new OAuthPromptSettings
                {
                    ConnectionName = connectionName,
                    Text = localizer["signindialog"],
                    Title = localizer["signin"],
                    Timeout = 300000, // 5分でタイムアウトするように設定
                }));
    }

    private async Task<DialogTurnResult> LoginAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        return await stepContext.BeginDialogAsync("login", cancellationToken: cancellationToken);
    }
    private async Task<DialogTurnResult> CompleteLoginAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // ログインの結果よりトークンを取得
        var tokenResponse = (TokenResponse)stepContext.Result;

        if (string.IsNullOrEmpty(tokenResponse.Token))
        {
            await stepContext.Context.SendActivityAsync(localizer["failed"], cancellationToken: cancellationToken);
            return await stepContext.EndDialogAsync("", cancellationToken);
        }
        else
            // 戻り値としてトークンを返す
            return await stepContext.EndDialogAsync(tokenResponse.Token, cancellationToken);
    }
}

3. MenuDialog 用の RESX を追加。

MenuDialog.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="checkweather" xml:space="preserve">
    <value>天気を確認</value>
  </data>
  <data name="checkschedule" xml:space="preserve">
    <value>予定を確認。</value>
  </data>
  <data name="choicemenu" xml:space="preserve">
    <value>今日はなにをしますか?</value>
  </data>
</root>
MenuDialog.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="checkweather" xml:space="preserve">
    <value>Check weather</value>
  </data>
  <data name="checkschedule" xml:space="preserve">
    <value>Check schedule</value>
  </data>
  <data name="choicemenu" xml:space="preserve">
    <value>What do you want to do today?</value>
  </data>
</root>

4. MenuDialog.cs を以下のコードに差し替え。

  • リソースを使うため、一部のプロパティを静的から通常に変更
  • コンストラクタで初期化
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Dialogs.Choices;
using System.Linq;
using System;
using Microsoft.Extensions.Localization;

public class MenuDialog : ComponentDialog
{
    // メニューの作成。表示される文字と子ダイアログの名称をセットで登録
    private Dictionary<string, string> menus;

    private IStringLocalizer<MenuDialog> localizer;
    // ChoiceFactory で選択肢に設定する IList<Choice> を作成
    private IList<Choice> choices;
    public MenuDialog(IServiceProvider serviceProvider, IStringLocalizer<MenuDialog> localizer) : base(nameof(MenuDialog))
    {
        this.localizer = localizer;
        menus = new Dictionary<string, string>(){
            { localizer["checkweather"], nameof(WeatherDialog) },
            { localizer["checkschedule"], nameof(ScheduleDialog) }
        };

        choices = ChoiceFactory.ToChoices(menus.Select(x => x.Key).ToList());
        // ウォーターフォールのステップを定義。処理順にメソッドを追加。
        var waterfallSteps = new WaterfallStep[]
        {
            ShowMenuAsync,
            ProcessInputAsync,
            LoopMenu
        };

        // ウォーターフォールダイアログと各種プロンプトを追加
        AddDialog(new WaterfallDialog("menu", waterfallSteps));
        AddDialog(new ChoicePrompt("choice"));
        AddDialog((WeatherDialog)serviceProvider.GetService(typeof(WeatherDialog)));
        AddDialog((ScheduleDialog)serviceProvider.GetService(typeof(ScheduleDialog)));
    }

    public async Task<DialogTurnResult> ShowMenuAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // Choice プロンプトでメニューを表示
        return await stepContext.PromptAsync(
            "choice",
            new PromptOptions
            {
                Prompt = MessageFactory.Text(localizer["choicemenu"]),
                Choices = choices,
            },
            cancellationToken);
    }

    private async Task<DialogTurnResult> ProcessInputAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // 答えを確認して次のダイアログ名を取得
        var choice = (FoundChoice)stepContext.Result;
        var dialogId = menus[choice.Value];
        // 子ダイアログの実行
        return await stepContext.BeginDialogAsync(dialogId, null, cancellationToken);
    }

    private async Task<DialogTurnResult> LoopMenu(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // スタックに乗せないように、Replace でメニューを再表示
        return await stepContext.ReplaceDialogAsync("menu", null, cancellationToken);
    }
}

5. PhotoUpdateDialog 用の RESX を追加。

PhotoUpdateDialog.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="failed" xml:space="preserve">
    <value>サインインに失敗しました。</value>
  </data>
</root>
PhotoUpdateDialog.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="failed" xml:space="preserve">
    <value>Failed to signin</value>
  </data>
</root>

6. PhotoUpdateDialog.cs のコードを以下に差し替え。

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.Localization;

public class PhotoUpdateDialog : ComponentDialog
{
    private IServiceProvider serviceProvider;
    private IStringLocalizer<PhotoUpdateDialog> localizer;

    public PhotoUpdateDialog(IServiceProvider serviceProvider, IStringLocalizer<PhotoUpdateDialog> localizer) : base(nameof(PhotoUpdateDialog))
    {
        this.serviceProvider = serviceProvider;
        this.localizer = localizer;

        // ウォーターフォールのステップを定義。処理順にメソッドを追加。
        var waterfallSteps = new WaterfallStep[]
        {
            LoginAsync,
            UpdatePhotoAsync,
            GetPhotoAsync,
        };

        // ウォーターフォールダイアログと各種プロンプトを追加
        AddDialog(new WaterfallDialog("updatephoto", waterfallSteps));
        AddDialog((LoginDialog)serviceProvider.GetService(typeof(LoginDialog)));
    }

    private async Task<DialogTurnResult> LoginAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // 認証ダイアログはテキストがないと落ちるため、ダミーを設定
        stepContext.Context.Activity.Text = "dummy";
        return await stepContext.BeginDialogAsync(nameof(LoginDialog), cancellationToken: cancellationToken);       
    }
    private async Task<DialogTurnResult> UpdatePhotoAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // ログインの結果よりトークンを取得
        var accessToken = (string)stepContext.Result;
        // 親ダイアログより渡されたイメージを取得

        // 添付ファイルを取得して MemoryStream に格納
        var connector = new ConnectorClient(new Uri(stepContext.Context.Activity.ServiceUrl));
        var image = await connector.HttpClient.GetStreamAsync(stepContext.Options.ToString());
        if (!string.IsNullOrEmpty(accessToken))
        {
            var graphClient = new MSGraphService(accessToken);
            await graphClient.UpdatePhotoAsync(image);
        }
        else
            await stepContext.Context.SendActivityAsync(localizer["failed"], cancellationToken: cancellationToken);

        return await stepContext.NextAsync(accessToken, cancellationToken);
    }

    private async Task<DialogTurnResult> GetPhotoAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // 前の処理よりトークンを取得
        var accessToken = (string)stepContext.Result;
        if (!string.IsNullOrEmpty(accessToken))
        {
            // 返信の作成
            var reply = stepContext.Context.Activity.CreateReply();
            // 現在の写真を取得
            var graphClient = new MSGraphService(accessToken);
            var image = await graphClient.GetPhotoAsync();
            byte[] buffer = new byte[16 * 1024];
            using (MemoryStream ms = new MemoryStream())
            {
                int read;
                while ((read = image.Read(buffer, 0, buffer.Length)) > 0)
                {
                    ms.Write(buffer, 0, read);
                }

                var image64 = System.Convert.ToBase64String(ms.ToArray());
                // 返信に画像を設定
                reply.Attachments.Add(new Attachment(
                    contentType: "image/png",
                    contentUrl: $"data:image/png;base64,{image64}"
                    ));
            }
            await stepContext.Context.SendActivityAsync(reply, cancellationToken);
        }
        else
            await stepContext.Context.SendActivityAsync(localizer["failed"], cancellationToken: cancellationToken);

        return await stepContext.EndDialogAsync(true, cancellationToken);
    }
}

7. ProfileDialog 用 RESX を追加。

ProfileDialog.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="save" xml:space="preserve">
    <value>プロファイルを保存します。</value>
  </data>
</root>
ProfileDialog.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="save" xml:space="preserve">
    <value>Save profile information</value>
  </data>
</root>

8. ProfileDialog.cs のコードを差し替え。

  • SummaryStepAsync で言語情報を受け渡し
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.Localization;
using Newtonsoft.Json;

public class ProfileDialog : ComponentDialog
{
    private MyStateAccessors accessors;
    private IStringLocalizer<ProfileDialog> localizer;

    public ProfileDialog(MyStateAccessors accessors, IStringLocalizer<ProfileDialog> localizer) : base(nameof(ProfileDialog))
    {
        this.accessors = accessors;
        this.localizer = localizer;

        // ウォーターフォールのステップを定義。処理順にメソッドを追加。
        var waterfallSteps = new WaterfallStep[]
        {
            //AdaptiveAsync,
            ProfileStepAsync,
            SummaryStepAsync,
        };
        // ウォーターフォールダイアログと各種プロンプトを追加
        AddDialog(new WaterfallDialog("profile", waterfallSteps));
        AddDialog(new TextPrompt("adaptive", AdaptiveCardResponseValidators.ValidateInput));
    }

    private async Task<DialogTurnResult> ProfileStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        var userProfile = await accessors.UserProfile.GetAsync(stepContext.Context, ()=> new UserProfile(), cancellationToken);
        var adaptiveCardJson = File.ReadAllText($"./AdaptiveJsons/{userProfile.Language}/Profile.json");
        var adaptiveCardAttachment = new Attachment()
        {
            ContentType = "application/vnd.microsoft.card.adaptive",
            Content = JsonConvert.DeserializeObject(adaptiveCardJson),
        };

        // 返信をコンテキストより作成
        var reply = stepContext.Context.Activity.CreateReply();
        reply.Attachments = new List<Attachment>() { adaptiveCardAttachment };

        // 戻り値は複雑な型だが一旦 TextPrompt ダイアログを使って送信
        return await stepContext.PromptAsync("adaptive", new PromptOptions
        {
            Prompt = reply,
            Validations = new NumberRange() { MinValue = 0, MaxValue = 120 }
        },
        cancellationToken: cancellationToken);
    }

    private async Task<DialogTurnResult> SummaryStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        var input = JsonConvert.DeserializeObject<UserProfile>(stepContext.Context.Activity.Value.ToString());
        // 使用言語は現在のプロファイルよりコピー
        var userProfile = await accessors.UserProfile.GetAsync(stepContext.Context, ()=> new UserProfile(), cancellationToken);
        input.Language = userProfile.Language;
        await stepContext.Context.SendActivityAsync(MessageFactory.Text(localizer["save"]));
        await accessors.UserProfile.SetAsync(stepContext.Context, input, cancellationToken);
        return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
    }
}

9. ScheduleDialog 用の RESX を追加。

ScheduleDialog.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="failed" xml:space="preserve">
    <value>サインインに失敗しました。</value>
  </data>
</root>
ScheduleDialog.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="failed" xml:space="preserve">
    <value>Failed to signin</value>
  </data>
</root>

10. ScheduleDialog.cs を以下のコードと差し替え。

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.Localization;

public class ScheduleDialog : ComponentDialog
{
    private IStringLocalizer<ScheduleDialog> localizer;

    public ScheduleDialog(IServiceProvider serviceProvider, IStringLocalizer<ScheduleDialog> localizer) : base(nameof(ScheduleDialog))
    {
        this.localizer = localizer;
        // ウォーターフォールのステップを定義。処理順にメソッドを追加。
        var waterfallSteps = new WaterfallStep[]
        {
            LoginAsync,
            GetScheduleAsync,
        };

        // ウォーターフォールダイアログと各種プロンプトを追加
        AddDialog(new WaterfallDialog("schedule", waterfallSteps));
        AddDialog((LoginDialog)serviceProvider.GetService(typeof(LoginDialog)));
    }

    private async Task<DialogTurnResult> LoginAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        return await stepContext.BeginDialogAsync(nameof(LoginDialog), cancellationToken: cancellationToken);
    }
    private async Task<DialogTurnResult> GetScheduleAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // ログインの結果よりトークンを取得
        var accessToken = (string)stepContext.Result;

        if (!string.IsNullOrEmpty(accessToken))
        {
            var graphClient = new MSGraphService(accessToken);
            var events = await graphClient.GetScheduleAsync();
            events.ForEach(async x =>
            {
                await stepContext.Context.SendActivityAsync($"{System.DateTime.Parse(x.Start.DateTime).ToString("HH:mm")}-{System.DateTime.Parse(x.End.DateTime).ToString("HH:mm")} : {x.Subject}", cancellationToken: cancellationToken);
            });
        }
        else
            await stepContext.Context.SendActivityAsync(localizer["failed"], cancellationToken: cancellationToken);

        return await stepContext.EndDialogAsync(true, cancellationToken);
    }
}

11. WeatherDialog 用の RESX 追加。

WeatherDialog.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="end" xml:space="preserve">
    <value>終了</value>
  </data>
  <data name="today" xml:space="preserve">
    <value>今日</value>
  </data>
</root>
WeatherDialog.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="end" xml:space="preserve">
    <value>end</value>
  </data>
  <data name="today" xml:space="preserve">
    <value>today</value>
  </data>
</root>

12. WeatherDialog.cs を以下のコードと差し替え。

  • localizer を使うために、プロパティの位置変更
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.Localization;
using Newtonsoft.Json;

public class WeatherDialog : ComponentDialog
{
    // 既定で今日の天気を表示
    private string date;
    private MyStateAccessors accessors;
    private IStringLocalizer<WeatherDialog> localizer;

    public WeatherDialog(MyStateAccessors accessors, IStringLocalizer<WeatherDialog> localizer) : base(nameof(WeatherDialog))
    {
        this.accessors = accessors;
        this.localizer = localizer;
        date = localizer["today"];
        // ウォーターフォールのステップを定義。処理順にメソッドを追加。
        var waterfallSteps = new WaterfallStep[]
        {
            ShowWeatherAsync,
            CheckUserInputAsync,
            CheckDateAsync,
        };

        // ウォーターフォールダイアログと各種プロンプトを追加
        AddDialog(new WaterfallDialog("weather", waterfallSteps));
        AddDialog(new TextPrompt("date"));
    }

    private async Task<DialogTurnResult> ShowWeatherAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        var userProfile = await accessors.UserProfile.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);

        // アダプティブカードの定義を JSON ファイルより読込み、対象日を変更
        var adaptiveCardJson = File.ReadAllText($"./AdaptiveJsons/{userProfile.Language}/Weather.json").Replace("{0}", date);
        var adaptiveCardAttachment = new Attachment()
        {
            ContentType = "application/vnd.microsoft.card.adaptive",
            Content = JsonConvert.DeserializeObject(adaptiveCardJson),
        };

        // 返信をコンテキストより作成
        var reply = stepContext.Context.Activity.CreateReply();
        reply.Attachments = new List<Attachment>() { adaptiveCardAttachment };

        // 戻り値は文字型のため、TextPrompt ダイアログを使って送信
        return await stepContext.PromptAsync("date", new PromptOptions
        {
            Prompt = reply
        },
        cancellationToken: cancellationToken);
    }

    private async Task<DialogTurnResult> CheckUserInputAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        var input = stepContext.Result.ToString();

        if (input == localizer["end"])
            return await stepContext.EndDialogAsync();

        var userProfile = await accessors.UserProfile.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);
        // 他の日を選択するようにアダプティブカードを作成
        var adaptiveCardJson = File.ReadAllText($"./AdaptiveJsons/{userProfile.Language}/WeatherDateChoice.json");
        var adaptiveCardAttachment = new Attachment()
        {
            ContentType = "application/vnd.microsoft.card.adaptive",
            Content = JsonConvert.DeserializeObject(adaptiveCardJson),
        };

        var reply = stepContext.Context.Activity.CreateReply();
        reply.Attachments = new List<Attachment>() { adaptiveCardAttachment };

        // 戻り値は文字型のため、TextPrompt ダイアログを使って送信
        return await stepContext.PromptAsync("date", new PromptOptions
        {
            Prompt = reply
        },
        cancellationToken: cancellationToken);
    }

    private async Task<DialogTurnResult> CheckDateAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // 結果を date に指定してダイアログを再利用
        date = stepContext.Result.ToString();
        return await stepContext.ReplaceDialogAsync("weather");
    }
}

13. myfirstbot.csproj にリソースの情報を追加。

<ItemGroup>
  <EmbeddedResource Update="Resources\LoginDialog.en.resx">
    <Generator>PublicResXFileCodeGenerator</Generator>
  </EmbeddedResource>
  <EmbeddedResource Update="Resources\LoginDialog.ja.resx">
    <Generator>PublicResXFileCodeGenerator</Generator>
  </EmbeddedResource>
  <EmbeddedResource Update="Resources\MenuDialog.en.resx">
    <Generator>PublicResXFileCodeGenerator</Generator>
  </EmbeddedResource>
  <EmbeddedResource Update="Resources\MenuDialog.ja.resx">
    <Generator>PublicResXFileCodeGenerator</Generator>
  </EmbeddedResource>
  <EmbeddedResource Update="Resources\PhotoUpdateDialog.en.resx">
    <Generator>PublicResXFileCodeGenerator</Generator>
  </EmbeddedResource>
  <EmbeddedResource Update="Resources\PhotoUpdateDialog.ja.resx">
    <Generator>PublicResXFileCodeGenerator</Generator>
  </EmbeddedResource>
  <EmbeddedResource Update="Resources\ProfileDialog.en.resx">
    <Generator>PublicResXFileCodeGenerator</Generator>
  </EmbeddedResource>
  <EmbeddedResource Update="Resources\ProfileDialog.ja.resx">
    <Generator>PublicResXFileCodeGenerator</Generator>
  </EmbeddedResource>
  <EmbeddedResource Update="Resources\ScheduleDialog.en.resx">
    <Generator>PublicResXFileCodeGenerator</Generator>
  </EmbeddedResource>
  <EmbeddedResource Update="Resources\ScheduleDialog.ja.resx">
    <Generator>PublicResXFileCodeGenerator</Generator>
  </EmbeddedResource>
  <EmbeddedResource Update="Resources\WeatherDialog.en.resx">
    <Generator>PublicResXFileCodeGenerator</Generator>
  </EmbeddedResource>
  <EmbeddedResource Update="Resources\WeatherDialog.ja.resx">
    <Generator>PublicResXFileCodeGenerator</Generator>
  </EmbeddedResource>
  <EmbeddedResource Update="Resources\WelcomeDialog.en.resx">
    <Generator>PublicResXFileCodeGenerator</Generator>
  </EmbeddedResource>
  <EmbeddedResource Update="Resources\WelcomeDialog.ja.resx">
    <Generator>PublicResXFileCodeGenerator</Generator>
  </EmbeddedResource>
</ItemGroup>

テスト

一通りの変更が終わったら、テストをしてみます。

1. F5 キーを押下してデバッグ実行。エミュレーターで接続。言語選択で「English」を選択後、プロファイル登録の確認で「Yes!」を選択。
image.png

2. カードが英語で出ることを確認。必要な情報を入力して「Register」をクリック。
image.png

3. プロファイル保存後、話かけたらメニューが出ることを確認。
image.png

4. Check weather をクリックして、正しく英語で情報が出るか確認。
image.png

5. 次に日本でテスト。すると一部英語のままになっていることを確認。
image.png

英語は正しく出ましたが、日本語が正しく出ない問題が発生したため、この問題を対策する必要があります。

言語指定のタイミングを検討

現在言語の指定は Bot のミドルウェアで実行されています。この場合以下の順序でコードが実行されるため問題が出ます。

  • サーバー起動時に Startup.cs のコードが実行
  • Bot にアクセスした際、MyBot のコンストラクタが実行
  • 各ダイアログのコンストラクタが実行
    • このタイミングでメニューなど一部値がその時点での言語で初期化
  • ミドルウェアが実行
    • このタイミングで言語が設定

よって、言語が設定された後に必要な初期化をするようにします。

1. MenuDialog.cs のコードを以下に書き換え。

  • InitializeAsync に必要な初期化をいれてウォーターフォールで初めに呼び出し
  • 後のメソッドで参照する変数を静的に指定
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Dialogs.Choices;
using System.Linq;
using System;
using Microsoft.Extensions.Localization;

public class MenuDialog : ComponentDialog
{    
    static private Dictionary<string, string> menus;
    static private IList<Choice> choices;
    private IStringLocalizer<MenuDialog> localizer;
    
    public MenuDialog(IServiceProvider serviceProvider, IStringLocalizer<MenuDialog> localizer) : base(nameof(MenuDialog))
    {
        this.localizer = localizer;
        // ウォーターフォールのステップを定義。処理順にメソッドを追加。
        var waterfallSteps = new WaterfallStep[]
        {
            InitializeAsync,
            ShowMenuAsync,
            ProcessInputAsync,
            LoopMenu
        };

        // ウォーターフォールダイアログと各種プロンプトを追加
        AddDialog(new WaterfallDialog("menu", waterfallSteps));
        AddDialog(new ChoicePrompt("choice"));
        AddDialog((WeatherDialog)serviceProvider.GetService(typeof(WeatherDialog)));
        AddDialog((ScheduleDialog)serviceProvider.GetService(typeof(ScheduleDialog)));
    }

    public async Task<DialogTurnResult> InitializeAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // メニューの作成。表示される文字と子ダイアログの名称をセットで登録
        menus = new Dictionary<string, string>(){
            { localizer["checkweather"], nameof(WeatherDialog) },
            { localizer["checkschedule"], nameof(ScheduleDialog) },
        };
  
        // ChoiceFactory で選択肢に設定する IList<Choice> を作成
        choices = ChoiceFactory.ToChoices(menus.Select(x => x.Key).ToList());
        return await stepContext.NextAsync();
    }

    public async Task<DialogTurnResult> ShowMenuAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // Choice プロンプトでメニューを表示
        return await stepContext.PromptAsync(
            "choice",
            new PromptOptions
            {
                Prompt = MessageFactory.Text(localizer["choicemenu"]),
                Choices = choices,
            },
            cancellationToken);
    }

    private async Task<DialogTurnResult> ProcessInputAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // 答えを確認して次のダイアログ名を取得
        var choice = (FoundChoice)stepContext.Result;
        var dialogId = menus[choice.Value];
        // 子ダイアログの実行
        return await stepContext.BeginDialogAsync(dialogId, null, cancellationToken);
    }

    private async Task<DialogTurnResult> LoopMenu(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // スタックに乗せないように、Replace でメニューを再表示
        return await stepContext.ReplaceDialogAsync("menu", null, cancellationToken);
    }
}

2. F5 キーを押下してデバッグ実行。再度日本語をテスト。
image.png

まとめ

今回はアダプティブカードの JSON の多言語化について検討しました。また全てのダイアログのリソースを作りましたが、結構手間です。リソースファイルは ResXManager などツールで自動翻訳もできるものがあるので、大規模環境や複数言語のリソースを使う場合は検討してください。次回はユーザーインプットの翻訳について考えていきます。

次の記事へ
目次へ戻る

この記事のサンプルコード

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?