NodeBBの多言語対応：サーバーサイドとクライアントサイドの実装

NodeBBのプラグイン開発において、多言語対応は重要な要素の一つです（NodeBBは執筆時点で、49言語に対応しています）。この記事では、NodeBBの多言語対応の実装方法について、サーバーサイドとクライアントサイドの両面から説明します。

## 多言語対応の基本構造

NodeBBの多言語対応は、以下のような構造で実装されます：

```
plugins/
  nodebb-plugin-caiz/
    languages/
      en-US/
        caiz.json
        global.json
      ja/
        caiz.json
        global.json
    library.js
    plugin.json
```

各言語の翻訳ファイルは、`languages`ディレクトリ内の言語コード（例：`en-US`、`ja`）のサブディレクトリに配置されます。翻訳ファイルはJSON形式で、キーと値のペアで構成されます。

NodeBBで使用可能な言語コードは、[NodeBBのリポジトリ](https://github.com/NodeBB/NodeBB/tree/d161eb6f6c37e62c092668a7a750f24f7d51378e/public/language)の`public/language`ディレクトリ以下にあるフォルダ名を参考にすることができます。例えば：

- `en-US`: アメリカ英語
- `en-GB`: イギリス英語
- `ja`: 日本語
- `zh-CN`: 中国語（簡体字）
- `zh-TW`: 中国語（繁体字）

これらの言語コードは、ISO 639-1（2文字の言語コード）やISO 639-2（3文字の言語コード）に基づいています。また、地域の違いを表すために、ハイフンで区切って国コードを追加することもできます（例：`en-US`、`pt-BR`）。

## プラグイン設定の更新

多言語対応の実装で最も重要なのは、`plugin.json`の設定です：

```json
{
  "id": "nodebb-plugin-caiz",
  "name": "NodeBB Plugin for Caiz",
  "description": "NodeBB Plugin for Caiz",
  "version": "1.0.0",
  "library": "./library.js",
  "languages": "languages",
  "scripts": [
    "static/client.js"
  ]
}
```

重要な設定：

- `languages`: 翻訳ファイルのディレクトリをプラグインルートから指定（この例では`languages`ディレクトリ）
- `scripts`: クライアントサイドのJavaScriptファイルを指定

`languages`プロパティを指定することで、NodeBBは自動的に指定されたディレクトリ内の翻訳ファイルを読み込みます。各言語の翻訳ファイルは、言語コードのサブディレクトリ内に配置する必要があります。

翻訳ファイルの例（`languages/ja/caiz.json`）：

```json
{
  "reading": "読み込み中",
  "create_community": "コミュニティを作成",
  "create_community_message": "新しくコミュニティを作成する",
  "community_name": "コミュニティ名",
  "community_description": "コミュニティの説明",
  "submit_create": "作成",
  "follow": "フォロー中",
  "unfollow": "未フォロー",
  "following": "フォローする",
  "unfollowing": "フォロー解除する",
  "follow_success": "フォローしました",
  "unfollow_success": "フォロー解除しました",
  "error.generic": "エラーが発生しました"
}
```

このようなファイルの場合、 `[[caiz.reading]]` で文字が取得できます。

## クライアントサイドの実装

クライアントサイドでの多言語対応は、`translator`モジュールを使用して実装します。そのままだと使いづらいので、 `async` で取り出すと良いでしょう。

```javascript
async function getTranslate() {
    return new Promise((resolve, reject) => {
        require(['translator'], resolve);
    });
}

document.addEventListener('DOMContentLoaded', async () => {
    const translator = await getTranslate();
    const messageKeys = [
        'caiz:reading',
        'caiz:follow',
        'caiz:unfollow',
        'caiz:follow_success',
        'caiz:unfollow_success',
        'caiz:error.generic',
        'caiz:unfollowing',
        'caiz:following',
    ];

    const messages = Object.fromEntries(
        await Promise.all(
            messageKeys.map(key => 
                new Promise((resolve) => 
                    translator.translate(`[[${key}]]`, (t) => resolve([key, t]))
                )
            )
        )
    );

    const getText = (key) => messages[key] || key;
});
```

このコードでの実行内容です。

1. `translator`モジュールを非同期で読み込み
2. 必要な翻訳キーのリストを定義
3. `translator.translate`を使用して各キーの翻訳を取得
4. 翻訳をオブジェクトとして保存
5. `getText`関数で翻訳を取得

この結果、 `getText` で i18n した結果が得られます。

```javascript
getText('caiz:reading'); // 読み込み中
```

## テンプレートでの使用

テンプレート `*.tpl` では、以下のように翻訳を使用します。

```html
<div class="modal-header">
    <h5 class="modal-title">[[caiz:create_community]]</h5>
    <button type="button" class="close" data-dismiss="modal">
        <span>&times;</span>
    </button>
</div>
```

## 注意点

1. **翻訳キーの命名規則**
   - 翻訳ファイル名をプレフィックスとして使用（例：`caiz.json`の場合は`caiz:`）
   - 階層構造を使用して整理（例：`error.generic`）

2. **翻訳の読み込みタイミング**
   - サーバーサイド：アプリケーション起動時
   - クライアントサイド：ページ読み込み時

3. **エラーハンドリング**
   - 翻訳が見つからない場合のフォールバック処理
   - ログ出力によるデバッグ支援

## まとめ

NodeBBの多言語対応は、`plugin.json`の`languages`プロパティで翻訳ファイルの場所を指定するだけで、基本的な設定は完了します。翻訳ファイルの構造を適切に設計し、命名規則を統一することで、メンテナンス性の高い多言語対応を実現できます。 

[goofmint/caiz](https://github.com/goofmint/caiz)