# 通知設計書 14-国際化メッセージ通知

## 概要

本ドキュメントは、RuoYiシステムにおける国際化メッセージ通知（i18n）の設計について記載する。MessageUtils.message()を通じてロケールに応じた国際化メッセージを取得・表示する機構を定義する。

### 本通知の処理概要

国際化メッセージ通知は、システム内の各種メッセージ（エラーメッセージ、バリデーションメッセージ、操作結果メッセージ等）をユーザーのロケール設定に応じて適切な言語で表示する機構である。MessageUtils.message()メソッドを介してSpringのMessageSourceから国際化メッセージを取得し、プレースホルダーに動的な値を埋め込んで表示する。

**業務上の目的・背景**：RuoYiは中国語ベースのシステムであるが、国際的な利用やローカライズの拡張性を考慮し、メッセージの国際化基盤が整備されている。エラーメッセージや操作ガイダンスをユーザーの言語設定に応じて表示することで、ユーザビリティを向上させ、多言語環境でのシステム運用を可能にする。

**通知の送信タイミング**：ログインエラー、パスワードリトライ制限、権限エラー、バリデーションエラー等、システム内でメッセージを表示する必要がある場面でMessageUtils.message()が呼び出され、ロケールに応じたメッセージが取得・返却される。

**通知の受信者**：HTTPリクエストを送信したクライアント（ブラウザ、APIクライアント）が受信者となる。HTMLページへのレンダリングまたはJSONレスポンスとして返却される。

**通知内容の概要**：messages.propertiesファイルに定義されたメッセージキーに対応するメッセージが取得される。プレースホルダー（{0}、{1}等）がある場合は、引数で渡された値が埋め込まれる。例：「密码输入错误{0}次」→「密码输入错误3次」

**期待されるアクション**：ユーザーは表示されたメッセージを確認し、適切な対応を行う（パスワード再入力、権限確認、入力値修正等）。システム管理者は必要に応じてメッセージ定義を追加・修正する。

## 通知種別

アプリ内通知（HTTPレスポンス/画面表示）

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 同期 |
| 優先度 | 中 |
| リトライ | 無 |

### 送信先決定ロジック

HTTPリクエストを送信したクライアントに対して、レスポンス（HTMLまたはJSON）の一部として返却する。ロケールはセッションに保存され、リクエストパラメータ「lang」で切り替え可能。

## 通知テンプレート

### メッセージファイル

| 項目 | 内容 |
|-----|------|
| ファイルパス | static/i18n/messages.properties |
| 文字エンコーディング | UTF-8 |
| デフォルトロケール | zh_CN（簡体字中国語） |

### メッセージテンプレート例

```properties
# エラーメッセージ
user.jcaptcha.error=验证码错误
user.not.exists=用户不存在/密码错误
user.password.not.match=用户不存在/密码错误
user.password.retry.limit.count=密码输入错误{0}次
user.password.retry.limit.exceed=密码输入错误{0}次，帐户锁定10分钟
user.password.delete=对不起，您的账号已被删除
user.blocked=用户已封禁，请联系管理员
role.blocked=角色已封禁，请联系管理员
login.blocked=很遗憾，访问IP已被列入系统黑名单
user.logout.success=退出成功

# 権限メッセージ
no.permission=您没有数据的权限，请联系管理员添加权限 [{0}]
no.create.permission=您没有创建数据的权限，请联系管理员添加权限 [{0}]
no.update.permission=您没有修改数据的权限，请联系管理员添加权限 [{0}]
no.delete.permission=您没有删除数据的权限，请联系管理员添加权限 [{0}]
no.export.permission=您没有导出数据的权限，请联系管理员添加权限 [{0}]
no.view.permission=您没有查看数据的权限，请联系管理员添加权限 [{0}]

# バリデーションメッセージ
not.null=* 必须填写
length.not.valid=长度必须在{min}到{max}个字符之间
user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成，且必须以非数字开头
user.password.not.valid=* 5-50个字符
user.email.not.valid=邮箱格式错误
user.mobile.phone.number.not.valid=手机号格式错误

# 成功メッセージ
user.login.success=登录成功
user.register.success=注册成功

# ファイルアップロード
upload.exceed.maxSize=上传的文件大小超出限制的文件大小！<br/>允许的文件最大大小是：{0}MB！
upload.filename.exceed.length=上传的文件名最长{0}个字符
```

### 添付ファイル

該当なし

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| code | メッセージキー | 呼び出し元で指定 | Yes |
| args | プレースホルダー引数 | 呼び出し元で指定 | No |
| locale | ロケール | LocaleContextHolder.getLocale() | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| 認証処理 | ログイン失敗 | キャプチャエラー、パスワード誤り等 | SysLoginService, SysPasswordService |
| 認証処理 | ログイン成功 | 認証成功時 | SysLoginService |
| 認証処理 | ログアウト | ログアウト処理時 | LogoutFilter |
| 認証処理 | パスワードリトライ超過 | maxRetryCount超過時 | SysPasswordService |
| 権限チェック | 権限エラー | 必要な権限がない場合 | PermissionUtils |
| 例外処理 | BaseException発生 | 国際化対応例外発生時 | BaseException.getMessage() |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| メッセージキー未定義 | プロパティファイルにキーが存在しない場合はdefaultMessageを返却 |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[メッセージ取得要求] --> B[MessageUtils.message呼び出し]
    B --> C[SpringUtils.getBean - MessageSource取得]
    C --> D[LocaleContextHolder.getLocale - ロケール取得]
    D --> E[MessageSource.getMessage]
    E --> F{メッセージ存在?}
    F -->|Yes| G[プレースホルダー置換]
    F -->|No| H[デフォルトメッセージ返却]
    G --> I[国際化メッセージ返却]
    H --> I
    I --> J[呼び出し元で使用]
```

## データベース参照・更新仕様

### 参照テーブル一覧

該当なし（メッセージはプロパティファイルから取得）

### 更新テーブル一覧

該当なし

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| メッセージキー未定義 | プロパティファイルにキーが存在しない | defaultMessageを返却（BaseExceptionの場合） |
| ロケールファイル未存在 | 対応するロケールのプロパティファイルがない | デフォルトロケールのファイルを使用 |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | なし（同期処理のため） |
| リトライ間隔 | - |
| リトライ対象エラー | - |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 制限なし |
| 1日あたり上限 | 制限なし |

### 配信時間帯

制限なし（システム稼働中は常時）

## セキュリティ考慮事項

- メッセージプロパティファイルにはセンシティブ情報を含めない
- プレースホルダーに挿入される値はサニタイズ済みであることを前提とする
- ロケール切り替えパラメータ（lang）の値はSpringにより安全に処理される

## 備考

- デフォルトロケールはConstants.DEFAULT_LOCALE（Locale.SIMPLIFIED_CHINESE）
- ロケール変更はリクエストパラメータ「lang」で可能（LocaleChangeInterceptor）
- ロケール情報はセッションに保存される（SessionLocaleResolver）
- BaseExceptionはMessageUtils.message()を内部で呼び出し、国際化メッセージを自動取得する

---

## コードリーディングガイド

本通知を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

まず、国際化メッセージの設定と定義を理解することが重要である。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | messages.properties | `ruoyi-admin/src/main/resources/static/i18n/messages.properties` | メッセージキーと値の定義、プレースホルダー記法 |
| 1-2 | application.yml | `ruoyi-admin/src/main/resources/application.yml` | spring.messages.basename設定 |
| 1-3 | Constants.java | `ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java` | DEFAULT_LOCALE定義 |

**読解のコツ**: messages.propertiesでは{0}、{1}等のプレースホルダーが使用されており、MessageFormat形式でパラメータ置換が行われる。

#### Step 2: 国際化基盤を理解する

Spring i18n設定とユーティリティクラスを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | I18nConfig.java | `ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java` | LocaleResolver、LocaleChangeInterceptor設定 |
| 2-2 | MessageUtils.java | `ruoyi-common/src/main/java/com/ruoyi/common/utils/MessageUtils.java` | message()メソッドの実装 |
| 2-3 | SpringUtils.java | `ruoyi-common/src/main/java/com/ruoyi/common/utils/spring/SpringUtils.java` | Bean取得ユーティリティ |

**主要処理フロー**:

**I18nConfig.java**:
1. **20-27行目**: `localeResolver()` - SessionLocaleResolver生成、デフォルトロケール設定
2. **29-35行目**: `localeChangeInterceptor()` - ロケール変更インターセプター、パラメータ名「lang」
3. **37-41行目**: `addInterceptors()` - インターセプター登録

**MessageUtils.java**:
1. **21行目**: `public static String message(String code, Object... args)` - メッセージ取得メソッド
2. **23行目**: `MessageSource messageSource = SpringUtils.getBean(MessageSource.class)` - MessageSource取得
3. **24行目**: `return messageSource.getMessage(code, args, LocaleContextHolder.getLocale())` - ロケールに応じたメッセージ取得

#### Step 3: 使用箇所を理解する

MessageUtils.message()の呼び出し箇所を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | BaseException.java | `ruoyi-common/src/main/java/com/ruoyi/common/exception/base/BaseException.java` | getMessage()での国際化メッセージ取得 |
| 3-2 | SysLoginService.java | `ruoyi-framework/src/main/java/com/ruoyi/framework/shiro/service/SysLoginService.java` | ログイン処理でのメッセージ使用 |
| 3-3 | SysPasswordService.java | `ruoyi-framework/src/main/java/com/ruoyi/framework/shiro/service/SysPasswordService.java` | パスワード検証でのメッセージ使用 |
| 3-4 | PermissionUtils.java | `ruoyi-common/src/main/java/com/ruoyi/common/utils/security/PermissionUtils.java` | 権限エラーメッセージ取得 |

**主要処理フロー**:

**BaseException.java getMessage() (63-76行目)**:
1. **67行目**: `if (!StringUtils.isEmpty(code))` - コードが設定されている場合
2. **69行目**: `message = MessageUtils.message(code, args)` - 国際化メッセージを取得
3. **71-74行目**: メッセージがnullの場合はdefaultMessageを使用

### プログラム呼び出し階層図

```
サービス層 (SysLoginService等)
    │
    ├─ MessageUtils.message("user.password.retry.limit.count", retryCount)
    │      │
    │      ├─ SpringUtils.getBean(MessageSource.class)
    │      │
    │      └─ messageSource.getMessage(code, args, locale)
    │             │
    │             └─ messages.properties参照
    │
    └─ BaseException発生時
           │
           └─ BaseException.getMessage()
                  │
                  └─ MessageUtils.message(code, args)
```

### データフロー図

```
[設定]                         [処理]                              [出力]

messages.properties ───────▶ MessageSource ──┐
(メッセージ定義)              (Spring Bean)    │
                                              ▼
リクエストパラメータ ────────▶ LocaleChangeInterceptor ▶ セッション保存
(lang=en / lang=zh_CN)                                     │
                                                           ▼
                                              LocaleContextHolder.getLocale()
                                                           │
                                                           ▼
MessageUtils.message() ──────────────────────▶ getMessage(code, args, locale)
                                                           │
                                                           ▼
                                                    国際化メッセージ
                                                    "密码输入错误3次"
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| MessageUtils.java | `ruoyi-common/src/main/java/com/ruoyi/common/utils/MessageUtils.java` | ソース | 国際化メッセージ取得ユーティリティ |
| I18nConfig.java | `ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java` | ソース | i18n設定クラス |
| SpringUtils.java | `ruoyi-common/src/main/java/com/ruoyi/common/utils/spring/SpringUtils.java` | ソース | Spring Bean取得ユーティリティ |
| BaseException.java | `ruoyi-common/src/main/java/com/ruoyi/common/exception/base/BaseException.java` | ソース | 国際化対応基底例外クラス |
| Constants.java | `ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java` | ソース | 定数定義（DEFAULT_LOCALE） |
| messages.properties | `ruoyi-admin/src/main/resources/static/i18n/messages.properties` | 設定 | 国際化メッセージ定義 |
| application.yml | `ruoyi-admin/src/main/resources/application.yml` | 設定 | spring.messages.basename設定 |
| SysLoginService.java | `ruoyi-framework/src/main/java/com/ruoyi/framework/shiro/service/SysLoginService.java` | ソース | ログインサービス（メッセージ使用） |
| SysPasswordService.java | `ruoyi-framework/src/main/java/com/ruoyi/framework/shiro/service/SysPasswordService.java` | ソース | パスワードサービス（メッセージ使用） |
| PermissionUtils.java | `ruoyi-common/src/main/java/com/ruoyi/common/utils/security/PermissionUtils.java` | ソース | 権限ユーティリティ（メッセージ使用） |
| LogoutFilter.java | `ruoyi-framework/src/main/java/com/ruoyi/framework/shiro/web/filter/LogoutFilter.java` | ソース | ログアウトフィルター（メッセージ使用） |
| SysRegisterService.java | `ruoyi-framework/src/main/java/com/ruoyi/framework/shiro/service/SysRegisterService.java` | ソース | 登録サービス（メッセージ使用） |
