PowerApps コンポーネントフレームワーク : ローカライゼーション API サンプル

[前回](https://qiita.com/kenakamu/items/ef1b0e9250817ac6e814) までは field と dataset タイプのカスタムコントロールを開発初してきました。今後は他のサンプルを紹介しながら各種機能を見ていきます。まず今回は多言語対応のサンプルとして [localization API component](https://docs.microsoft.com/en-us/powerapps/developer/component-framework/sample-controls/localization-api-control) サンプルを解説します。

このサンプルは、ボタンをクリックすると数字が大きくなる [Increment component](https://docs.microsoft.com/en-us/powerapps/developer/component-framework/sample-controls/increment-control) を多言語したもので、完成イメージは以下の通りです。
![](https://docs.microsoft.com/en-us/powerapps/developer/component-framework/media/localization-api-control.png)

# RESX リソースファイル

PCF プロジェクトではリソースファイルとして resx を指定できます。resx の利用は参照場所によって異なります。

## マニフェスト内

取得したい文字列に対するキー名を直接指定します。

```xml
display-name-key="TS_LocalizationAPI_Display_Key"
description-key="TS_LocalizationAPI_Desc_Key"
```

## ts コード内

コンテキストが提供する resources オブジェクトから取得できます。
[参照: Resouces](https://docs.microsoft.com/en-us/powerapps/developer/component-framework/reference/resources)

- context.resources.getResource: リソースファイル自体を取得
- context.resources.getString: resx からキーに対応する文字列を取得

# プロジェクトの作成

今回も既に作成した SampleSolution に追加するように開発していきます。

1\. 前回まで作ってきた PCFControls フォルダに移動して、フィールド用のコントロールを作成。

```shell
mkdir LocalizationControl
cd LocalizationControl
```

2\. 以下のコマンドを実行してプロジェクトを作成。

- 名前空間とプロジェクト名の指定
- template 引数は field を指定

```shell
pac pcf init --namespace SampleNamespace --name TSLocalizationAPI --template field
```

3\. npm パッケージをリストアして任意の IDE でフォルダを開く。ここでは Visual Studio Code を利用。

```shell
npm install
code .
```

# コントロールマニフェストの編集とリソースの追加

ローカライゼーションのリソースは RESX として作成し、コントロールマニフェストより参照します。またマニフェスト内のコントロール名や説明をローカライズする場合は、RESX に指定したキーを使います。

1\. ControlManifest.Input.xml を以下の様に編集。

- 名前や説明をリソースファイルか取得するようキー名のみ設定
- resources に resx を追加。

```xml:ControlManifest.Input.xml
<?xml version="1.0" encoding="utf-8" ?>
<manifest>
  <control namespace="SampleNamespace" constructor="TSLocalizationAPI" version="1.0.0" display-name-key="TS_LocalizationAPI_Display_Key" description-key="TS_LocalizationAPI_Desc_Key" control-type="standard">
    <type-group name="numbers">
      <type>Whole.None</type>
      <type>Currency</type>
      <type>FP</type>
      <type>Decimal</type>
    </type-group>
    <property name="value" display-name-key="value_Display_Key" description-key="value_Desc_Key" of-type-group="numbers" usage="bound" required="true" />
    <resources>
      <code path="index.ts" order="1" />
      <css path="css/TS_LocalizationAPI.css" order="1" />
      <resx path="strings/TSLocalizationAPI.1033.resx" version="1.0.0" />
      <resx path="strings/TSLocalizationAPI.1041.resx" version="1.0.0" />
    </resources>
  </control>
</manifest>
```

2\. css フォルダと strings フォルダを作成。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/b5f56817-04f3-ca4a-cf45-cfa8131319f1.png)

3\. css フォルダに TS_LocalizationAPI.css ファイルを追加。

```css:TS_LocalizationAPI.css
.SampleNamespace\.TSLocalizationAPI button.LocalizationSample_Button_Style {
	text-decoration: none;
    display: inline-block;
    font-size: 14px;
    margin: 4px 6px;
    cursor: pointer;
	color: white;
    border-radius: 0px;
	background-color: rgb(59, 121, 183);
    border: none;
    padding: 5px;
    text-align: center;
}

.SampleNamespace\.TSLocalizationAPI button.LocalizationSample_Input_Error_Style {
	color: red;
}
```

4\. strings フォルダに TSLocalizationAPI.1033.resx を追加。

- マニフェストに指定したキーに対応する value/comment を追加

```xml:TSLocalizationAPI.1033.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="PCF_LocalizationSample_ButtonLabel" xml:space="preserve">
        <value>Increment</value>
        <comment>Label for TSLocalizationAPI's Button</comment>
    </data>
    <data name="TS_LocalizationAPI_Display_Key" xml:space="preserve">
        <value>Sample Localization Control</value>
        <comment>Localization Sample Localized Control Name</comment>
    </data>
    <data name="TS_LocalizationAPI_Desc_Key" xml:space="preserve">
        <value>This control showcases usage of localization.</value>
        <comment>Localization Sample Localized Control Description</comment>
    </data>
    <data name="value_Display_Key" xml:space="preserve">
        <value>Value</value>
        <comment>Localization Sample Control Main Property Localized Name</comment>
    </data>
    <data name="value_Desc_Key" xml:space="preserve">
        <value>Shows the field that the control is mapped to.</value>
        <comment>Localization Sample Control Main Property Localized Description</comment>
    </data>
</root>
```

5\. 同様に TSLocalizationAPI.1041.resx を追加。

```xml:TSLocalizationAPI.1041.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="PCF_LocalizationSample_ButtonLabel" xml:space="preserve">
        <value>増加</value>
        <comment> TSLocalizationAPI ボタンのラベル</comment>
    </data>
    <data name="TS_LocalizationAPI_Display_Key" xml:space="preserve">
        <value>ローカライゼーションサンプルコントロール</value>
        <comment>ローカライゼーションサンプルコントロールの名前</comment>
    </data>
    <data name="TS_LocalizationAPI_Desc_Key" xml:space="preserve">
        <value>ローカライゼーションの使い方を示すサンプル</value>
        <comment>ローカライゼーションサンプルコントロールの説明</comment>
    </data>
    <data name="value_Display_Key" xml:space="preserve">
        <value>バインドされたフィールドの値</value>
        <comment>値用の文字列</comment>
    </data>
    <data name="value_Desc_Key" xml:space="preserve">
        <value>バインドされたフィールドの値が表示される</value>
        <comment>説明用の文字列</comment>
    </data>
</root>
```

6\. ターミナルより以下コマンドでビルドを実行。

```shell
npm run build
```

# index.ts の編集

1\. まずは import の追加。

```typescript
import { IInputs, IOutputs } from "./generated/ManifestTypes";
```

2\. クラスとプロパティの定義。

```typescript
export class TSLocalizationAPI implements ComponentFramework.StandardControl<IInputs, IOutputs> {
	// 現在の値を持つプロパティ 
	private _value: number;
	// PCF への変更通知コールバック
	private _notifyOutputChanged: () => void;
	// ラベル用
	private label: HTMLInputElement;
	// ボタン用
	private button: HTMLButtonElement;
	// カスタムコントロールを保持するコンテナ
	private _container: HTMLDivElement;
}
```

3\. init 初期化メソッドを追加。

- context.resources.getString() : リソースファイルからキーに対応する文字列の取得。

```typescript
/**
 * init メソッド
 * @param context : 各種オブジェクトや API へのアクセスを提供するコンテキスト
 * @param notifyOutputChanged : 出力変更通知のコールバック
 * @param state : 前回保存したステート
 * @param container : UI コントロールを保持するコンテナ
 */
public init ( context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container: HTMLDivElement )
{
	// ラベルの作成と blur イベントの設定
	this.label = document.createElement( "input" );
	this.label.setAttribute( "type", "label" );
	this.label.addEventListener( "blur", this.onInputBlur.bind( this ) );
	// ボタンの作成。
	this.button = document.createElement( "button" );
	// リソースよりローカライズされた文字列の取得
	this.button.innerHTML = context.resources.getString( "PCF_LocalizationSample_ButtonLabel" );
	this.button.classList.add( "LocalizationSample_Button_Style" );
	this._notifyOutputChanged = notifyOutputChanged;
	// クリックイベントの設定
	this.button.addEventListener( "click", this.onButtonClick.bind( this ) );
	// コンテナの作成と要素の追加
	this._container = document.createElement( "div" );
	this._container.appendChild( this.label );
	this._container.appendChild( this.button );
	container.appendChild( this._container );
}
```
3\. インプットとボタン用のコールバックメソッドを追加。

```typescript
private onButtonClick ( event: Event ): void
{
	this._value = this._value + 1;
	this._notifyOutputChanged();
}

private onInputBlur ( event: Event ): void
{
	let inputNumber = Number( this.label.value );
	this._value = isNaN( inputNumber ) ? ( this.label.value as any ) as number : inputNumber;
	this._notifyOutputChanged();
}
```

4\. PCF からの変更通知を処理する updateView メソッドを追加。

- context.parameters.value: フィールドからの値を保持。raw, formatted, error 等
- context.parameters.value.error でエラーを取得

```typescript
public updateView ( context: ComponentFramework.Context<IInputs> ): void
{
	// 現在の値を取得してラベルの表示
	this._value = context.parameters.value.raw;
	this.label.value = this._value != null ? this._value.toString() : "";
	if ( context.parameters.value.error )
	{
		this.label.classList.add( "LocalizationSample_Input_Error_Style" );
	}
	else
	{
		this.label.classList.remove( "LocalizationSample_Input_Error_Style" );
	}
}
```

5\. PCF 側に値を返す getOutputs メソッドを追加。

```typescript
public getOutputs (): IOutputs
{
	// custom code goes here - remove the line below and return the correct output
	let result: IOutputs = {
		value: this._value
	};
	return result;
}
```

6\. コントロール破棄時に呼ばれる destroy を追加。特に今回は処理なし。

```typescript
public destroy (): void
{
}
```

7\. ```npm run build``` でビルド。エラーがないことを確認。

# コントロールのパッケージ化と配布

これまでと同じ方法でパッケージ化と配布を実行。

1\. SampleSolution フォルダに移動し、以下のコマンドを実行。

```shell
pac solution add-reference --path ../LocalizationControl
```

2\. SampleSolution\Other\Solution.xml でバージョン情報を更新。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/e61112ec-caaf-4e2f-071d-857c76778f1e.png)

3\. 以下コマンドでパッケージをビルド。

```shell
msbuild /restore
msbuild /p:configuration=Release
```

4\. https://make.powerapps.com に接続。管理者権限でログイン。すでに SampleSolution を入れた環境で Solutions を選択し、Import をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/a95148b3-2956-e530-46ff-ac00e0369ed1.png)

5\. インポート時にパッケージが更新される旨が表示されるので、そのまま次へ。
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/22d7d8c8-e333-55c5-3672-be51dbf755c0.png"/>

6\. 次の画面も既定のまま「インポート」をクリック。
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/96b6d3ea-eb24-e326-4d55-48882b22368c.png"/>

7\. インポートが終わったら任意の数値列にコントロールを設定。ユーザーの言語によって表示される内容変わることを確認。
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/d59a44dd-12e4-ab57-292f-601b4286d22a.png"/>

[フォーム:日本語]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/3f11f771-780b-4322-f11b-f159a5d55d0a.png)
[フォーム:英語]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/a4196c69-e0e0-3d84-0745-b1ef2e7fd6ef.png)

# ソリューションの多言語サポート

PCF コントロールだけでなく、ソリューション自体も多言語化がサポートされます。
方法はシンプルで、Other\Solution.xml ファイルに直接書き込みます。

```xml:Solution.xml
<LocalizedNames>
  <!-- Localized Solution Name in language code -->
  <LocalizedName description="SampleSolution" languagecode="1033" />
  <LocalizedName description="サンプルソリューション" languagecode="1041" />
</LocalizedNames>

<LocalizedNames>
  <!-- Localized Cds Publisher Name in language code-->
  <LocalizedName description="SamplePublisher" languagecode="1033" />
  <LocalizedName description="サンプル発行元" languagecode="1041" />
</LocalizedNames>

<Descriptions>
  <!-- Description of Cds Publisher in language code -->
  <Description description="SamplePublisher" languagecode="1033" />
  <Description description="サンプル発行元" languagecode="1041" />
</Descriptions>
```

# まとめ

resx は C# 開発に慣れている人であれば容易に使えますが、Visual Studio Code での操作が不便なため、Visual Studio で直接作成、編集したり RESX Manager 等のツールを利用したほうがいいかもしれません。

次回はステート管理の機能のサンプルである Control State API component を見ていきます。

[目次へ戻る](https://qiita.com/kenakamu/items/5dd4c64b14c453fa7d85)
[次の記事へ](https://qiita.com/kenakamu/items/489d8c9dafbcc18829e0)

# 参考

[RESX Manager](https://webgallery.microsoft.com/apps/RESXManager/install)