# 機能設計書 69-i18n（多言語）サポート

## 概要

本ドキュメントは、Decap CMSのi18n（多言語）サポート機能の設計を定義する。本機能は、エントリの多言語化を実現し、単一ファイル・複数ファイル・複数フォルダの3つのファイル構造で多言語コンテンツを管理する。

### 本機能の処理概要

i18nサポート機能は、CMSで管理するコンテンツを複数の言語（ロケール）で作成・編集・保存するための基盤機能である。コレクション設定でi18nを有効化し、ロケール一覧、デフォルトロケール、ファイル構造（single_file/multiple_files/multiple_folders）を指定することで、多言語コンテンツの管理が可能になる。

**業務上の目的・背景**：グローバルなWebサイトでは複数言語でのコンテンツ配信が必要であり、同一コンテンツの多言語版を効率的に管理する仕組みが求められる。i18n機能により、デフォルト言語のコンテンツを基に他言語版の翻訳・管理を一元的に行える。

**機能の利用シーン**：エントリ編集画面でi18nトグルを有効にすると、デュアルペイン表示となり、デフォルトロケールと選択されたロケールを並べて編集できる。フィールドごとに「翻訳対象」「複製」「非表示」を設定可能。

**主要な処理内容**：
1. コレクションのi18n設定からロケール情報を取得（getI18nInfo）
2. フィールドのi18n属性による翻訳可否判定（translate/duplicate/none）
3. ファイルパスの多言語展開（getFilePath/getFilePaths）
4. 多言語エントリの読込・マージ（getI18nEntry/groupEntries）
5. 多言語エントリの保存用ファイル生成（getI18nFiles）
6. ローカルバックアップの多言語対応（getI18nBackup/formatI18nBackup）
7. duplicateフィールドのデフォルト値複製（duplicateI18nFields/duplicateDefaultI18nFields）
8. プレビュー用のロケール切替（getPreviewEntry）
9. 多言語データのシリアライズ（serializeI18n）

**関連システム・外部連携**：バックエンド（GitHub/GitLab等）と連携し、多言語ファイルの読み書きを行う。エディタインターフェースのi18nトグルUIと連携。

**権限による制御**：特になし。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 6 | エントリ編集画面 | 主画面 | i18n有効時のデュアルペイン編集、ロケール切替 |
| 7 | エントリ新規作成画面 | 主画面 | i18n有効時の多言語エントリ新規作成 |

## 機能種別

データ管理 / 多言語化 / ファイルパス変換

## 入力仕様

### 入力パラメータ

#### getI18nInfo

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| collection | Collection | Yes | コレクション定義（i18n設定含む） | - |

#### getFilePath

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| structure | I18N_STRUCTURE | Yes | ファイル構造（MULTIPLE_FOLDERS/MULTIPLE_FILES/SINGLE_FILE） | - |
| extension | string | Yes | ファイル拡張子 | - |
| path | string | Yes | 元のファイルパス | - |
| slug | string | Yes | エントリスラッグ | - |
| locale | string | Yes | ロケールコード | - |

### 入力データソース

- コレクション設定: i18n設定（structure, locales, default_locale）
- フィールド定義: 各フィールドのi18n属性（translate/duplicate/none）
- バックエンド: 各ロケールのファイル内容

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| I18nInfo | { locales, defaultLocale, structure } | i18n設定情報 |
| filePaths | string[] | 多言語展開されたファイルパス一覧 |
| entryValue | EntryValue | マージされた多言語エントリ値 |
| i18nFiles | { path, slug, raw }[] | 保存用の多言語ファイル一覧 |

### 出力先

- Redux Store: マージされた多言語エントリデータ
- バックエンド: 各ロケールの保存ファイル

## 処理フロー

### 処理シーケンス

```
1. i18n設定確認
   └─ hasI18n(collection)でi18n有効か判定
2. i18n情報取得
   └─ getI18nInfo()でstructure/locales/defaultLocaleを取得
3. エントリ読込
   ├─ SINGLE_FILE: 単一ファイル内のロケールごとのデータを分離
   ├─ MULTIPLE_FILES: locale付き拡張子のファイルをそれぞれ読込
   └─ MULTIPLE_FOLDERS: locale/slug構造のファイルをそれぞれ読込
4. データマージ
   └─ mergeValues()でデフォルトロケール+他ロケールのデータを統合
5. フィールド制御
   ├─ translate: 各ロケールで独立編集
   ├─ duplicate: デフォルトロケールの値を自動複製
   └─ none: デフォルトロケール以外では非表示
6. エントリ保存
   └─ getI18nFiles()で各ロケールの保存ファイルを生成
7. シリアライズ
   └─ serializeI18n()で各ロケールデータをシリアライズ
```

### フローチャート

```mermaid
flowchart TD
    A[hasI18n?] -->|Yes| B[getI18nInfo]
    A -->|No| Z[通常のエントリ処理]
    B --> C{structure}
    C -->|SINGLE_FILE| D[単一ファイル読込]
    C -->|MULTIPLE_FILES| E[locale付き拡張子ファイル群読込]
    C -->|MULTIPLE_FOLDERS| F[locale/slugフォルダ群読込]
    D --> G[mergeSingleFileValue]
    E --> H[mergeValues]
    F --> H
    G --> I[統合されたEntryValue]
    H --> I
    I --> J[エディタ表示]
    J --> K[保存時: getI18nFiles]
    K --> L{structure}
    L -->|SINGLE_FILE| M[全ロケールを1ファイルに統合]
    L -->|MULTIPLE系| N[各ロケールごとにファイル生成]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-69-01 | 3つのファイル構造 | SINGLE_FILE: 1ファイルに全ロケール、MULTIPLE_FILES: locale付き拡張子、MULTIPLE_FOLDERS: locale/slugフォルダ構造 | i18n有効時 |
| BR-69-02 | フィールドi18n属性 | translate: 各ロケール独立編集、duplicate: デフォルト値複製、none: デフォルトロケール以外非表示 | フィールドにi18n属性がある場合 |
| BR-69-03 | デフォルトロケールフォールバック | デフォルトロケールのエントリが見つからない場合、最初に見つかったエントリを使用しwarning | エントリ読込時 |
| BR-69-04 | MULTIPLE_FOLDERSパス変換 | `path.replace('/${slug}', '/${locale}/${slug}')` | MULTIPLE_FOLDERS構造時 |
| BR-69-05 | MULTIPLE_FILESパス変換 | `path.replace(extension, locale.extension)` | MULTIPLE_FILES構造時 |
| BR-69-06 | duplicate値自動複製 | duplicateI18nFieldsで再帰的にduplicateフィールドの値をデフォルトロケールから各ロケールにコピー | フィールドi18n=duplicate時 |
| BR-69-07 | データパス分離 | デフォルトロケール: ['data']、他ロケール: ['i18n', locale, 'data'] | 常時 |
| BR-69-08 | ファイルデプス補正 | MULTIPLE_FOLDERS構造ではdepthに+1（フォルダ階層が1段深くなるため） | MULTIPLE_FOLDERS構造時 |

### 計算ロジック

- ファイルパス変換: structure別のパス生成ロジック（getFilePath関数）
- データパス: `locale !== defaultLocale ? ['i18n', locale, 'data'] : ['data']`

## データベース操作仕様

### 操作別データベース影響一覧

該当なし（Gitリポジトリへのファイル操作はバックエンド層で行われる）。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | ロケールファイル欠損 | MULTIPLE系構造で特定ロケールのファイルが存在しない場合 | nullとしてフィルタ（非nullのみマージ） |
| - | デフォルトロケール欠損 | デフォルトロケールのエントリが見つからない場合 | 最初のエントリを使用してconsole.warn |

### リトライ仕様

リトライ処理は行わない。

## トランザクション仕様

該当なし（各ロケールファイルは独立して保存。原子性は保証されない）。

## パフォーマンス要件

- MULTIPLE系構造ではPromise.allでロケールファイルの並列読込
- SINGLE_FILE構造では1ファイルの読込のみで済むため高速

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

- ロケールコードはファイルパスの一部として使用されるため、パストラバーサル対策が必要（バックエンド側で実施）

## 備考

- i18nデータパスは`['i18n', locale, 'data']`の形式でImmutableMapに格納される
- groupEntries関数はエントリ一覧取得時に多言語エントリをグルーピングする

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | i18n.ts | `packages/decap-cms-core/src/lib/i18n.ts` | I18N_STRUCTURE enum（SINGLE_FILE/MULTIPLE_FILES/MULTIPLE_FOLDERS）とI18N_FIELD enum（TRANSLATE/DUPLICATE/NONE）の定義 |

**読解のコツ**: i18nデータは`entry.i18n.{locale}.data`パスに格納される。デフォルトロケールは`entry.data`に格納される。この2階層の理解が重要。

#### Step 2: i18n設定情報の取得を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | i18n.ts | `packages/decap-cms-core/src/lib/i18n.ts` | hasI18n、getI18nInfo、getI18nFilesDepthの設定取得関数群 |

**主要処理フロー**:
- **12行目**: I18N定数 = 'i18n'
- **13-17行目**: I18N_STRUCTURE enum定義
- **19-23行目**: I18N_FIELD enum定義
- **25-27行目**: hasI18n - collection.has(I18N)
- **35-41行目**: getI18nInfo - structure/locales/defaultLocale取得
- **43-49行目**: getI18nFilesDepth - MULTIPLE_FOLDERSでdepth+1

#### Step 3: フィールドのi18n制御を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | i18n.ts | `packages/decap-cms-core/src/lib/i18n.ts` | isFieldTranslatable/isFieldDuplicate/isFieldHiddenのフィールド制御 |

**主要処理フロー**:
- **51-54行目**: isFieldTranslatable - locale !== defaultLocale && i18n === 'translate'
- **56-59行目**: isFieldDuplicate - locale !== defaultLocale && i18n === 'duplicate'
- **61-64行目**: isFieldHidden - locale !== defaultLocale && i18n === 'none'
- **66-68行目**: getLocaleDataPath - ['i18n', locale, 'data']
- **70-73行目**: getDataPath - ロケール別データパス取得

#### Step 4: ファイルパス変換を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | i18n.ts | `packages/decap-cms-core/src/lib/i18n.ts` | getFilePath/getFilePaths/normalizeFilePathのパス変換ロジック |

**主要処理フロー**:
- **75-91行目**: getFilePath - structure別のパス生成
- **93-110行目**: getLocaleFromPath - パスからロケールを抽出
- **112-129行目**: getFilePaths - 全ロケールのパス一覧生成
- **131-141行目**: normalizeFilePath - ロケール部分を除去して正規化

#### Step 5: エントリ読込・保存を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | i18n.ts | `packages/decap-cms-core/src/lib/i18n.ts` | getI18nEntry, getI18nFiles, groupEntriesのメインロジック |

**主要処理フロー**:
- **143-188行目**: getI18nFiles - 保存用ファイル生成（SINGLE_FILE/MULTIPLE系の分岐）
- **298-328行目**: getI18nEntry - エントリ読込とマージ
- **330-352行目**: groupEntries - エントリ一覧のグルーピング
- **391-429行目**: duplicateI18nFields - duplicateフィールドの再帰的値複製
- **438-454行目**: serializeI18n - 各ロケールデータのシリアライズ

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

```
i18n.ts
    │
    ├─ hasI18n(collection) → boolean
    ├─ getI18nInfo(collection) → { locales, defaultLocale, structure }
    │
    ├─ getFilePath(structure, extension, path, slug, locale) → string
    ├─ getFilePaths(collection, extension, path, slug) → string[]
    ├─ getLocaleFromPath(structure, extension, path) → string
    ├─ normalizeFilePath(structure, path, locale) → string
    │
    ├─ getI18nEntry(collection, extension, path, slug, getEntryValue)
    │   ├─ SINGLE_FILE → mergeSingleFileValue()
    │   └─ MULTIPLE系 → Promise.all(locales.map(getEntryValue)) → mergeValues()
    │
    ├─ getI18nFiles(collection, extension, entryDraft, entryToRaw, path, slug)
    │   ├─ SINGLE_FILE → 全ロケールデータを1ファイルに
    │   └─ MULTIPLE系 → 各ロケールごとにファイル生成
    │
    ├─ duplicateI18nFields(entryDraft, field, locales, defaultLocale)
    │   └─ 再帰的にduplicate属性のフィールド値を複製
    │
    └─ serializeI18n(collection, entry, serializeValues)
        └─ 各ロケールのデータをシリアライズ
```

### データフロー図

```
[入力]                         [処理]                          [出力]

コレクション設定 ──────▶ getI18nInfo()
  (i18n: {structure,           │
   locales, default_locale})   ▼

バックエンド ──────────▶ getI18nEntry() ──▶ マージされたEntryValue
  (各ロケールファイル)         │                (data + i18n.{locale}.data)
                              ▼
エディタ編集 ──────────▶ duplicateI18nFields()
  (duplicate値複製)            │
                              ▼
保存処理 ────────────▶ getI18nFiles() ──▶ ロケール別ファイル
                              │                (path, slug, raw)
                              ▼
                        serializeI18n() ──▶ シリアライズ済みデータ
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| i18n.ts | `packages/decap-cms-core/src/lib/i18n.ts` | ソース | i18nの全ロジック（約455行） |
| collections.ts | `packages/decap-cms-core/src/reducers/collections.ts` | ソース | selectEntrySlugの参照（mergeValues内で使用） |
