Visual Studio Code 拡張のローカライズ対応方法

## 始めに

@satokaz さんが先行して[同様の記事](#関連記事)を書いてくださってるのですが、 ["Keep It Simple, Stupid!"](https://ja.wikipedia.org/wiki/KISS%E3%81%AE%E5%8E%9F%E5%89%87) と大きくプリントされた金属バットを贈ってあげなければならないような [vscode-nls](https://github.com/Microsoft/vscode-nls) 及び [i18n-sample](https://github.com/Microsoft/vscode-extension-samples/tree/master/i18n-sample) を元に記述されており残念なことになってるので、 vscode-nls 及び i18n-sample をベースしないシンプルなローカライズ対応方法について Keep It Simple, Stupid!💢💢💢 と叫びながらまとめてみました。

なお、この記事は拡張のコード部分については TypeScript を前提に記述してますので、 JavaScript を利用されている場合は若干適当に読み替えてください。また、テーマやスニペットなどの拡張の場合のローカライズについては試しておらず、その周りの事情は私は把握しておりません。

## 対応内容まとめ

- `./package.json` 内のローカライズしたいテキストの内容を %key% の形に修正
- `./package.nls.json` ファイルを作成し、各 %key% に対する default のテキストを記述
- `./package.nls.`ロケール`.json` ( `./package.nls.en.json` とか `./package.nls.ja.json` )なファイルを作成し、そのロケールでの各 %key% に対するテキストを記述
- `./src/extension.ts` 内のコードで `vscode.env.language` にロケール( `en` とか `ja` といった文字列)が入ってるのでそれを見てテキストを出しわける

簡単にまとめるとと言うかホントにもうこれだけなんですが、一応、以下にもうちょっと詳細に書き記しておきます。

## 対応可能なロケール

2018年10現在は次のようになっています。

|表示言語|ロケール|
|---|---|
|English (US)|`en`|
|Simplified Chinese|`zh-CN`|
|Traditional Chinese|`zh-TW`|
|French|`fr`|
|German|`de`|
|Italian|`it`|
|Spanish|`es`|
|Japanese|`ja`|
|Korean|`ko`|
|Russian|`ru`|
|Bulgarian|`bg`|
|Hungarian|`hu`|
|Portuguese (Brazil)|`pt-br`|
|Turkish|`tr`|

オリジナルの表は[こちら](https://code.visualstudio.com/docs/getstarted/locales#_available-locales)にあります。

## ./package.json

`./package.json` にはさまざまな情報が含まれているのですが、2018年10月現在ローカライズ可能なのは

- ✅ コマンドのカテゴリ
- ✅ コマンドのタイトル
- ✅ 設定のタイトル
- ✅ 設定の default 値
- ✅ 設定の description

などであり

- 🚫 拡張の名前
- 🚫 拡張の description
- 🚫 拡張の readme

あたりはローカライズできません。( VS Code 組み込みの拡張は description がローカライズされてたりするのですが、これはランゲージパックを元に本体側が特別に処理しているモノであり、サードパーティー製の拡張ではできないっぽいです。ランゲージパックまで自作すればイケるかもですが・・・ )

ちなみにコマンドや設定まわりもローカライズこそはできるのですが、 VS Code 内での拡張情報表示が対応してくれておらず、 `コントリビューション` の情報を確認するとローカライズの為のキーが置換されずにそのまま表示されるという悲しいことになっています。この問題の詳細についてはこちらの [🦋issue](https://github.com/Microsoft/vscode/issues/54111) を参照してください。

### ./package.json のローカライズ対応方法

元のローカライズ対応前の `./package.json` の内容の一部がこんな感じだとします。

```json
"title": "Bark",
"category": "Dog",
```
それを次のように書き換えます。

```json
"title": "%bark-command-title%",
"category": "%dog-category%",
```

`%` で括った文字列がローカライズの為のキーとして使われ、JSONのキーとして利用可能な文字列であれば基本的になんでもいいハズです。( ただ使用する文字は ASCII の範囲内に限定しておいた方が無難だとは思います。 )

次に default のロケールテキストとして `./package.nls.json` ファイルを作成し次のように記述します。 

```json
{
    "bark-command-title": "Bark",
    "dog-category": "Dog"
}
```

最後に対応したい言語に合わせて `./package.nls.`ロケール`.json` なファイル( 日本語対応するなら `./package.nls.ja.json` ) を作成し次のように記述します。 

```json
{
    "bark-command-title": "吠える",
    "dog-category": "イヌ"
}
```

`./package.json` のローカライズ対応方法としては以上です。

## 拡張コード内のテキスト

基本的にはユーザーが `Configure Display Language` コマンドで `locale.json` 内の `locale` の値として設定した `en` とか `ja` といった文字列が `vscode.env.language` を見れば入ってるんでそれに合わせて表示するテキストを切り替えてやるだけです。

以上でお終いと言えばお終いなんですが以下に実際に私が採用してる具体的なコードを紹介しておきます。

`./src/extension.ts` の最初の方に次のような import 指定と共通コードを記述します。

```typescript
import localeEn from "../package.nls.json";
import localeJa from "../package.nls.ja.json";

export type LocaleKeyType = keyof typeof localeEn;

interface LocaleEntry
{
    [key : string] : string;
}
const localeTableKey = vscode.env.language;
const localeTable = Object.assign(localeEn, ((<{[key : string] : LocaleEntry}>{
    ja : localeJa
})[localeTableKey] || { }));
const localeString = (key : string) : string => localeTable[key] || key;
const localeMap = (key : LocaleKeyType) : string => localeString(key);
```

このコードでは `./package.json` のローカライズ対応用の JSON ファイルを流用且つコード上必要なローカライズのキーとテキストを書き足して利用しています。

JSON をそのまま import して利用する為、必要に応じて `./tsconfig.json` の `compilerOptions` のところで

```json
{
    "compilerOptions": {
        ...
        "resolveJsonModule": true,
        "esModuleInterop": true
    },
    "exclude": [
        ...
    ]
}
```

のように `resolveJsonModule` と `esModuleInterop` をともに `true` で指定してください。( 前者の指定がないとファイルが見つからないとエラーになり、後者の指定がないと default が export されてないとエラーになります。 )

なお、ここまでの編集を行って本来的には問題が解決してるハズなのに VS Code 上でエラーが出続けてしまうことがありますが、その時は VS Code を再起動すればエラーが消えます。

で、あとは先のコードで定義した `localeString()` 関数あるいは `localeMap()` 関数を使って

```typescript
console.log(localeString("dog-category"));
console.log(localeMap("dog-category"));
```

のような形で使うだけです。

`localeString()` 関数はロケール上の対応するテキストが見つからなかった時にキーをそのまま返すようにしてありますので、元の英語版のテキストをそのままキーとして流用すれば `./package.nls.json` への記述が省略できます。また、ロケールのテキストの参照の仕方としてどの言語であろうと最初に `./package.nls.json` の記述とマージしてしまうので、部分的な多言語対応も可能です。( 対応してない箇所は default のテキストが表示されます。 )

`localeMap()` 関数は引数のチェックがコンパイラによって行われ、 `./package.nls.json` に存在しないキーが指定されるとコンパイルエラーになりますし、 VS Code による入力補完も効きます。

## 対応サンプル

実際に私が日本語対応を行った拡張で修正箇所がわかりやすいように多言語対応の基礎部分と日本語対応部分を分けてコミットしてるので良ければ参考にしてください。

### [Zoom Bar](https://github.com/wraith13/zoombar-vscode)
- [🛠 多言語対応の基礎部分](https://github.com/wraith13/zoombar-vscode/commit/f2ca9b123d90e275229a0dc1c7eeea74b763e2c8)
- [🛠 日本語対応](https://github.com/wraith13/zoombar-vscode/commit/e0e75bbdf092dfc831ca66fc9776ea61e26f1aa5)

### [Unsaved Files](https://github.com/wraith13/unsaved-files-vscode)
- [🛠 多言語対応の基礎部分](https://github.com/wraith13/unsaved-files-vscode/commit/b32f6789924a1e911bea7dcd0b857af19d2943ff)
- [🛠 日本語対応](https://github.com/wraith13/unsaved-files-vscode/commit/793e1d93e53e4d2c89c841644d4be72331a8cae6)


## 関連記事

- [Visual Studio Code 拡張機能のメッセージを言語設定に合わせてローカライズしてみる](https://qiita.com/satokaz/items/f01ab4fc7f938904f5ae)
- [VS Code 拡張機能のコマンドタイトル名をローカライズしてみる](https://qiita.com/satokaz/items/dbb009597630b2a946f5)