Visual Studio Code 拡張機能のメッセージを言語設定に合わせてローカライズしてみる

> 本記事は、`vscode-nls 2.0.2` と `vscode-nls-dev 2.1.6` をベースに書かれた内容となります。
> 2020/1/13 現在、`vscode-nls 4.1.1` と `vscode-nls-dev 3.3.1` が最新となりますが、最新バージョンでは、`nls.config(process.env.VSCODE_NLS_CONFIG);` の記述は機能しなくなっています。
>
> また、[microsoft/vscode-nls](https://github.com/Microsoft/vscode-nls) と、それを利用した[i18n-sample](https://github.com/Microsoft/vscode-extension-samples/tree/master/i18n-sample) サンプルをベースにした本記事は、シンプルではないという指摘を頂いたり、使用すべきではないと注意喚起されている記事も見受けますので、vscode 拡張機能のローカライズに取り組む場合は、[Visual Studio Code 拡張のローカライズ対応方法](https://qiita.com/wraith13/items/8f873a1867a5cc2865a8#_reference-730777a16404cd524909) を参考にして頂ければと思います。
> 


# Visual Studio Code 拡張機能のメッセージを言語設定に合わせてローカライズしてみる

昨年の [Visual Studio Code Advent Calendar 2016](https://qiita.com/advent-calendar/2016/vscode) で [VS Code 拡張機能のコマンドタイトル名をローカライズしてみる](https://qiita.com/satokaz/items/dbb009597630b2a946f5) という記事を書いてみました。
この時は、[vscode-nls](https://github.com/Microsoft/vscode-nls) モジュールの存在などは知っていたものの、どのように利用するかまでは理解できていなかった...

## 拡張機能のサンプル

VS Code の API を利用して拡張機能を作成するにあたり参考になるのが [Microsoft/vscode-extension-samples](https://github.com/Microsoft/vscode-extension-samples) で公開されるサンプルです。

この中に、拡張機能で利用するメッセージをローカライズするためのサンプル [i18n-sample](https://github.com/Microsoft/vscode-extension-samples/tree/master/i18n-sample) が含まれており、このサンプルの手法を利用することで、拡張機能のメッセージをローカライズすることができます。
これで、やっと理解できた。

### vscode-extension-samples の clone

サンプルから、いくつかのファイルを拝借するので、まずは、Microsoft/vscode-extension-samples リポジトリを git clone し、手元に置いておきます:

```sh 
$ git clone https://github.com/Microsoft/vscode-extension-samples.git
```

### hello world を利用してメッセージをローカライズしてみる

今回も、下記で公開されている入門用 “Hello World” 拡張機能を利用してみたいと思います。

* [Example - Hello World](https://code.visualstudio.com/docs/extensions/example-hello-world)

yo code を実行し、拡張機能の雛形を作成します。
ここでは、拡張機能の名前に `hello-world` とつけてみます。

この拡張機能は、`Hello World` コマンドを追加し、このコマンドを実行すると上部に "Hello World!" と表示して終了するだけの拡張機能です。

今回は、`Hello Wolrd` コマンド及びその実行結果として表示される "Hello World!" を、ローカライズしてみるお話です。

`Hello World` コマンド:

![alt](https://qiita-image-store.s3.amazonaws.com/0/65800/30db40ac-826d-c0ea-41fe-9b279a21894b.png)

`Hello World` コマンドの実行結果:

![alt](https://qiita-image-store.s3.amazonaws.com/0/65800/6a20c547-910d-5088-49c0-b3a62c956fcf.png)

### ローカライズに必要な要件

ローカライズを提供するためには、下記のような要件が必要になります。ここでは、日本語へのローカライズを前提としています。

1. `言語を構成する (Configure Language)` で、`”locale”:”ja”` を設定し、VS Code の表示言語を日本語に設定します
2. ローカライズに利用される全てのファイルは `i18n` フォルダの下に配置します
3. このフォルダ自体は、手作業で作成します。(vscode-nls-dev モジュールを使って [Transifix](https://www.transifex.com) プロジェクトから、ローカライズされたファイルを pull することもできるように作られているようですが、プロジェクトなどは一切関係ないため今回は利用しません)
4. `i18n` フォルダの下には、ローカライズする言語を表すサブフォルダを配置します。フォルダ名は、[ISO 639-3](https://ja.wikipedia.org/wiki/ISO_639-3) の規約に従います。例えば、日本語の場合は、`jpn` となります
5. 言語名フォルダの下に、拡張機能のソースコードの配置と同じディレクトリ構造 (例えば、out/) に倣って `<ファイル名>.i18n.json` ファイルを作成します。この json ファイルの中身は、ローカライズ対象となるテキストの `”key”:”value”` のペアとなり、このファイルから該当する `key` のペアとなる `value` すなわち、ローカライズされたテキストが表示に利用されます
6. 拡張機能のトップレベルに `package.nls.json` ファイルがある場合は、`package.i18n.json` というファイル名を持つ、各言語用のパッケージを用意する必要があります。(日本語メッセージを作成する場合: `i18n/jpn/package.i18n.json`)
`package.nls.json` については、[Visual Studio Code Advent Calendar 2016 5日目 - VS Code 拡張機能のコマンドタイトル名をローカライズしてみる](https://qiita.com/satokaz/items/dbb009597630b2a946f5#packagenlsjson-と-packagenlsjajson) を参照してください

この条件に従い、メッセージをローカライズして行きます。

### npm package のインストール

`yo code` で拡張機能の雛形を作成し、すぐに実行することはできますが、それだけではメッセージをローカライズすることができません。

まずは、ローカライズに必要な下記の npm パッケージを “Hello World” 拡張機能のローカルリポジトリにインストールします。

```shell
$ cd hello-world
$ npm -SD install gulp gulp-typescript del run-sequence vsce vscode-nls-dev 
```

* [vsce](https://github.com/Microsoft/vscode-vsce) は、拡張機能をパッケージにしたり Marketplace へ公開するためなどに利用されます。
* [vscode-nls-dev](https://github.com/Microsoft/vscode-nls-dev) は、Javascript や Typescript から外部にある文字列を抽出することを目的としたモジュールです。

続いて、配布に必要な下記の npm パケージを “Hello World” 拡張機能のローカルリポジトリにインストールします。

```shell
$ npm -S install vscode-nls
```

* [vscode-nls](https://github.com/Microsoft/vscode-nls) は、ローカライズとそれに利用するリソースを外部化するための npm モジュールです。主に VS Code 拡張機能のローカライズに利用されます。

hello-world/package.json の `devDependencies` と `dependencies` は下記のようになります。

```json
  "devDependencies": {
    "@types/mocha": "^2.2.42",
    "@types/node": "^7.0.43",
    "del": "^3.0.0",
    "gulp": "^3.9.1",
    "gulp-typescript": "^3.2.3",
    "run-sequence": "^2.2.0",
    "typescript": "^2.6.1",
    "vsce": "^1.35.0",
    "vscode": "^1.1.6",
    "vscode-nls-dev": "^2.1.6"
  },
  "dependencies": {
    "vscode-nls": "^2.0.2"
  }
```

### ファイルの配置

続いてローカライズを行うために必要なファイルを配置します。
[ローカライズに必要な要件](#ローカライズに必要な要件)にもありましたが、ローカライズされたメッセージを格納するファイルを作成し配置する必要があります。

* hello-world/i18/jpn/out ディレクトリを作成します。このディレクトリに、コード内のメッセージをローカライズするために必要な `extension.i18n.json` ファイルを配置します(作成は後ほど)。また、拡張機能パッケージのメッセージ(コマンド名や設定情報など)をローカライズに必要な `package.i18n.json` ファイル(こちらは、i18/jpn/package.i18n.json として)を配置します(作成は後ほど)
* [i18n-sample/gulpfile.js](https://github.com/Microsoft/vscode-extension-samples/blob/master/i18n-sample/gulpfile.js) を hello-world ディレクトリのトップレベルに配置します

最終的なファイルの配置は下記のようになります:

```
hello-world
   |—i18n/
   |——jpn/
   |———package.i18n.json (作成は後ほど)
   |———out/
   |————extension.i18n.json (作成は後ほど)
   |-gulpfile.js
```

試しに、gulp task を実行して見ます。
メッセージとしては、特に何もなく、各タスクが実行されている事だけがわかります。

```shell
$ gulp
[18:32:39] Using gulpfile ~/hello-world/gulpfile.js
[18:32:39] Starting 'default'...
[18:32:39] Starting 'build'...
[18:32:39] Starting 'clean'...
[18:32:39] Finished 'clean' after 14 ms
[18:32:39] Starting 'internal-nls-compile'...
[18:32:40] Finished 'internal-nls-compile' after 932 ms
[18:32:40] Starting 'add-i18n'...
[18:32:40] Finished 'add-i18n' after 4.89 ms
[18:32:40] Finished 'build' after 953 ms
[18:32:40] Finished 'default' after 954 ms
```

この時、hello-world/out ディレクトリに変化があります。
明らかにローカライズに関連しそうなファイルができています。

```
hello-world
   |—out
   |——test/
   |——extension.js
   |——extension.js.map
   |——extension.nls.ja.json  // このファイルが作成される
   |——extension.nls.json     // このファイルが作成される
```

中身は、それぞれ下記のようになりますが、ローカライズに必要なファイルが作成されていないため、空に等しい内容となっています。

extension.nls.ja.json:

```json
[]
```

extension.nls.json:

```json
{
	"messages": [],
	"keys": []
}
```

でも、なんとなくローカライズの仕組みが見えてきました。

## ローカライズ手順

準備が整ったので、'Hello World!' 拡張機能をローカライズして見ます。
まずは、`extension.ts` を編集して行きます。`extension.ts` には、コマンドを実行すると表示されるメッセージが実装されており、その部分を変更して行きます。

### extension.ts に下記を追加

`vscode-nls` モジュールを import し、nls.config() を localize として割り当てる記述を extension.ts に追加します。

```typescript
import * as nls from 'vscode-nls';
const localize = nls.config(process.env.VSCODE_NLS_CONFIG)();
```

コメントを除いたコードは、下記のようになります。

```typescript
'use strict';
import * as vscode from 'vscode';

import * as nls from 'vscode-nls'; // 追加された行
const localize = nls.config(process.env.VSCODE_NLS_CONFIG)(); // 追加された行

export function activate(context: vscode.ExtensionContext) {
    console.log('Congratulations, your extension "hello-world" is now active!');
    let disposable = vscode.commands.registerCommand('extension.sayHello', () => {
        vscode.window.showInformationMessage('Hello World!');
    });
    context.subscriptions.push(disposable);
}

export function deactivate() {
}
```

"Hello World" 拡張機能で表示されるメッセージ 'Hello World!' を出力する処理は下記のように実装されています。

```typescript
        vscode.window.showInformationMessage('Hello World!');
```

この 'Hello World!' の部分をローカライズします。
ソースコード上のこの部分を、'こんにちは世界！' に書き換えたら終了です。いや、それだとダメですね。

### ローカライズしたいメッセージを localize() で囲う

まずは、ローカライズしたいメッセージを `localize()` で囲います。`localize()` は、`key` と `message` の 2 つの引数が必要になります。
`key` には、`message` を判別するための固有の文字列を。`message` には、デフォルトで表示する文字列を割り当てます。

下記のように変更します:

```typescript
        vscode.window.showInformationMessage(localize('helloWorld.text', 'Hello World!'));
```

ここでは、`helloWorld.text` という `key` が設定されているならば、その値を表示し、設定されていなければ、'Hello World!' を表示するという意味になります。
書いただけでは何も起きません。

試しに実行して見ましょう。ここで一つ気をつけなければいけない事は、ローカライズを反映するには、通常の F5 キーを押したデバッグのトランスパイルではなく、都度、変更を反映させるために、必ず `gulp build` を実行しなければならない点です。(本項の説明が、`i18n-sample/gulpfile.js` の利用を想定しているため、このような手順になっています)

統合ターミナルで `gulp build` を実行後、out/ ディレクトリ配下が再生成されます。その後、`F5 キー`でデバッグ実行する事が可能です。

また、`gulp build` 実行時に、extension.js が書き換えられます。この時に、gulp から `vscode-nls-dev` モジュールが呼び出されます。

localize() で囲んだ部分が下記のように変化している事がわかります:

out/extension.js: 

```javascript
        vscode.window.showInformationMessage(localize(0, null));
```

### どのようにローカライズを行なっているか？

1. ソースコード中の `vscode-nls` モジュールを利用した記述を対象に、`vscode-nls-dev` モジュールを使って書き換えを行います。書き換えは、トランスパイルで生成された JavaScript ファイルを、gulp を使って、後から書き換えます

2. 例えば、下記のように記述された呼び出しは、

    ```ts 
    localize(‘helloWorld.text’, ‘Hello World!’)
    ``` 

    このように書き換えられます:

    ```ts
    localize(0、null)
    ```

    最初のパラメータ (この例では 0) は、メッセージファイル内の `key` をあらわし、`key` に設定された `value` に置き換えられます。`localize()` が複数存在する場合は、最初のパラメータとなる数字が増えて行きます。

3. `i18n` フォルダに配置されたファイルの内容は、`”key”:”value”` のペアからなる配列に変換され利用されます

### ローカライズファイルの配置

`作成は後ほど`と説明していた 2 つのファイルを作成します。

```
hello-world
   |—i18n/
   |——jpn/
   |———package.i18n.json (作成は後ほど)
   |———out/
   |————extension.i18n.json (作成は後ほど)
   |-gulpfile.js
```

まずは、下記の内容で `hello-world/i18n/jpn/out/extension.i18n.json` を作成します。

```json
{
    "helloWorld.text": "こんにちは世界！"
}
```

こレは、言語設定が、`ja` の場合、`jpn` ディレクトリ配下の `extension.i18n.json` ファイルに設定された`helloWorld.text` の値となる、`こんにちは世界！` を表示することを想定しています。

### 実行してみる

ここまでくれば、もう少し。
あとは、ターミナルから `gulp build` を実行します。

```
$ gulp build
```

そして、`F5 キー`でデバッグを実行し、コマンドパレットから `Hello World` コマンドを実行します:

![alt](https://qiita-image-store.s3.amazonaws.com/0/65800/978dc47e-9b2b-02b3-75fc-c6f5be91cfeb.png)

表示できました。

`gulp build` で生成される、`out/extension.nls.ja.json` と `out/extension.nls.json` ファイルの中身を見てみると下記のようになっています:

extension.nls.ja.json:

```json
[
	"こんにちは世界！"
]
```

extension.nls.json:

```json
{
	"messages": [
		"Hello World!"
	],
	"keys": [
		"helloWorld.text"
	]
}
```

ちなみに、英語ロケールで実行すると、下記のように英語のメッセージが表示されます。

![alt](https://qiita-image-store.s3.amazonaws.com/0/65800/8990d21b-b823-14ba-cc43-b77f5502eded.png)

このように、ソースコード中のメッセージをローカライズして行きます。

ローカライズしたいメッセージを localize() で囲い、ローカライズしたいメッセージを判別するための固有の文字列を割り当てた `key` を定義し、その値に表示する文字列を割り当てます。

## コマンドのローカライズ

続いて、コマンドのローカライズを行います。

コマンドおよびコマンド名の定義は、`package.json` 内の `"contributes"` で定義される、`"commands"` で行われます。

'Hello World' 拡張機能は、下記のようになっています。これは、コマンド名として `"Hello World"` を割り当て、これが選択された場合に実行されるコマンドは `extension.sayHello` である事がわかります。

```json
  "contributes": {
    "commands": [
      {
        "command": "extension.sayHello",
        "title": "Hello World"
      }
    ]
  },
```

ここでも、テキストを直接書き換えれば対応完了ですね。

基本的な対応については、[VS Code 拡張機能のコマンドタイトル名をローカライズしてみる](https://qiita.com/satokaz/items/dbb009597630b2a946f5)の手順と大きく変わりませんので、パクって行きます。


### まず、package.json の変更:

まずは、コマンドの `title` を `%extension.sayHello.title%` のように `%` で囲まれた文字列に置き換えます。これがキーになります

```json
  "contributes": {
    "commands": [
      {
        "command": "extension.sayHello",
        "title": "%extension.sayHello.title%"
      }
    ]
  },
```

### 続いて packaje.nls.json の作成

package.json で設定したローカライズ用のキーにマッピングするための値を、下記のように追加します。
このファイルを用意することで、VS Code の言語が英語(en)に設定されているか、英語以外のローカライズ情報が用意されていない場合にこのファイルに記載されている情報が利用されることになります

```json
{
  "extension.sayHello.title": "Hello World"
}
```

### 最後にローカライズ用の値が格納された i18n/jpn/packahe.i18n.json を作成

ここだけが、前回の記事と異なる部分です。
ローカライズされたメッセージを格納するファイルは、`packahe.i18n.json` として、i18n/jpn に配置し、言語名を含んだ `package.nls.ja.json` ファイルを作成する必要はありません。

`gulp build` の実行により、`packahe.i18n.json` ファイルから `package.nls.ja.json` ファイルを自動生成してくれます。

下記の例では、"extension.sayHello.title": に、"こんにちは世界！ってやってみよー" を指定することで、package.json に定義した "%extension.sayHello.title%" が置きかわるという仕組みです。

```json
{
  "extension.sayHello.title": "こんにちは世界！ってやってみよー"
}
```

### gulp build を実行

最後に `gulp build` を実行し、`F5 キー` でデバッグを実行してみましょう。
下記のように、コマンドが "こんにちは世界！ってやってみよー" となります。

![alt](https://qiita-image-store.s3.amazonaws.com/0/65800/13989a04-82b2-1975-536c-d7e002beb943.gif)

また、`packaje.nls.json` でデフォルトとなるコマンド名を定義しているため、ローカライズされたコマンドの下に、"Hello World" とも表示されています。
そのため、コマンドパレットでは、日本語化されたコマンド名と英語のコマンド名のどちらでも検索して実行する事が可能です。


## パッケージ化してみる

せっかくなので、作成した 'Hello World' 拡張機能を VS Code へインストールできるように、拡張機能パッケージ化してみましょう。

パッケージ化も、ローカライズのために利用している gulpfile.js を使って行う事が可能です。下記のように実行してください:

```shell
$ gulp package
```

拡張機能をシングルファイルにパッケージ化した `hello-world-0.0.1.vsix` ファイルが生成されます。

`拡張機能: VSIX からのインストール` コマンドを実行し、`hello-world-0.0.1.vsix` を読み込むことで VS Code へ拡張機能をインストールする事ができます。

ちなみに、README.md が、ちゃんと編集されているかをチェックしており、そのままだとパッケージ化がエラーとなります。急いでいる場合は、冒頭にある下記の一文を削除すれば回避できます。

```
This is the README for your extension "hello-world". After writing up a brief description, we recommend including the following sections.
```