Locale / 言語コードシステム
BCP 47準拠の多言語対応と言語コード管理仕様
07 - Locale / 言語コードシステム
対象バージョン: BluePeriod (Tauri V2) 最終更新: 2026-04-24 関連Issue: BCP 47 Language Support 実装Report: BCP 47実装レポート
1. 概要
BluePeriodにおけるLocale(言語コード)は、UI表示、プロジェクト管理、原稿(Manuscript)の多言語管理、パブリッシュ/Explore機能など、システムのほぼ全層に深く統合されている。本ドキュメントは、言語コードが「どこで」「どのように」使われているのかを包括的に記述し、言語追加時の判断基準を提供する。
1.1 三層の言語管理
BluePeriodは概念的に3つの独立した言語レイヤーを持つ:
| レイヤー | 目的 | 格納先 | デフォルト |
|---|---|---|---|
| UI言語 | インターフェースの表示言語 | Cookie (NEXT_LOCALE), i18next | en(フォールバック) |
| プロジェクト原典言語 | 作品の書かれた言語(プロジェクト単位) | IndexedDB structuredPlot.language | ブラウザ言語から自動判定 |
| 原稿バージョン言語 | 各セクションの原稿がどの言語で書かれているか | IndexedDB manuscripts[sectionId].versions[lang] | プロジェクト原典言語に従う |
2. 言語コード仕様
2.1 BCP 47形式
BluePeriodはBCP 47言語タグ形式を採用している:
| 形式 | 例 | 用途 |
|---|---|---|
| 言語のみ | en, ja, ko | 一般的な言語指定 |
| 言語 + スクリプト | zh-Hans, zh-Hant | 中国語の簡体字/繁体字区別 |
| 言語 + 地域 | en-US, pt-BR | 地域バリエーション |
バリデーション正規表現: /^[a-z]{2,3}(-[A-Z][a-z]{3})?(-[A-Z]{2})?$/
後方互換性: 既存のISO 639-1コード(
en,ja等)はBCP 47としても有効なため、既存データの移行なしで動作する。
2.2 サポート言語の一元管理
ファイル: next-app/public/languages/supported-languages.json
言語データはJSONファイルで一元管理され、原典言語・翻訳言語の両方で使用する:
[
{ "code": "en", "name": "English", "nativeName": "English", "uiSupported": true },
{ "code": "ja", "name": "Japanese", "nativeName": "日本語", "uiSupported": true },
{ "code": "zh-Hans", "name": "Chinese (Simplified)", "nativeName": "简体中文", "uiSupported": true },
{ "code": "zh-Hant", "name": "Chinese (Traditional)", "nativeName": "繁體中文", "uiSupported": false },
{ "code": "ko", "name": "Korean", "nativeName": "한국어", "uiSupported": false }
]| フィールド | 説明 |
|---|---|
code | BCP 47言語タグ |
name | 英語名(フォールバック表示用) |
nativeName | ネイティブ名(ネイティブ名表示用) |
uiSupported | UI言語として対応しているか(i18nextリソース存在) |
初期10言語: en, ja, zh-Hans, zh-Hant, ko, es, fr, de, pt, ru
UI対応: en, ja, zh-Hans の3言語のみ(uiSupported: true)
2.3 UI言語(i18n)
| コード | 言語 | 翻訳ファイル |
|---|---|---|
en | English | next-app/src/i18n/locales/en.json |
ja | 日本語 | next-app/src/i18n/locales/ja.json |
zh | 简体中文 | next-app/src/i18n/locales/zh.json |
注意: i18nextのリソースキーは
zhだが、supported-languages.jsonではzh-Hansとして登録。toUiLanguageKey()でzh-Hans→zhにフォールバックする。
翻訳ファイルには language.{code} キーが含まれ、フル表示モードの括弧内に使用される:
// ja.json の例
"language.en": "英語",
"language.ja": "日本語",
"language.zh-Hans": "簡体字中国語",
"language.ko": "韓国語"3. BCP 47ユーティリティ
3.1 コア関数(src/lib/i18n/bcp47.ts)
| 関数 | 役割 |
|---|---|
isValidBcp47(tag) | BCP 47タグのバリデーション |
toUiLanguageKey(bcp47) | i18next用フォールバック(zh-Hans → zh) |
getNativeName(tag) | ネイティブ名取得(未登録ならIntl.DisplayNamesフォールバック) |
getLanguageDisplayName(tag, t) | フル表示名の生成 |
3.2 サポート言語アクセス(src/lib/i18n/supportedLanguages.ts)
JSONを直接importする同期API:
| 関数 | 役割 |
|---|---|
getSupportedLanguages() | 全言語リスト取得 |
getUiLanguages() | uiSupported: true の言語のみ |
getLanguageByCode(code) | コードから言語情報取得 |
3.3 言語名の3層表示方式
表示コンテキストに応じて3つのモードを使い分ける:
| モード | 形式 | 例(UI日本語時) |
|---|---|---|
| フル | {nativeName} ({i18n名}) | 한국어 (韓国語) |
| ショート | BCP 47コード大文字 | KO |
| フォールバック | Intl.DisplayNames | Italian |
ネイティブ名とi18n名が同一の場合(例: UI日本語時の 日本語)は括弧を省略してnativeNameのみ表示。
フォールバック順序:
- i18n翻訳キー
language.{code}→ 2. JSONname→ 3.Intl.DisplayNames
4. オフライン(ローカル)の言語処理
4.1 i18n設定
ファイル: next-app/src/i18n/config.ts
| 設定項目 | 値 |
|---|---|
| ライブラリ | i18next + react-i18next + i18next-browser-languagedetector |
| フォールバック言語 | en |
| 検出順序 | cookie → navigator → htmlTag |
| Cookie名 | NEXT_LOCALE |
| キャッシュ先 | Cookie |
4.2 ミドルウェア(言語自動検出)
ファイル: next-app/src/proxy.ts
[リクエスト受信]
↓
Accept-Language ヘッダー取得
↓
accept-language-parser で解析
↓
サポート言語から最適なものを選択
↓
Cookie 'NEXT_LOCALE' に保存(1年有効)
↓
[レスポンス返却]- ライブラリ:
accept-language-parser - デフォルトフォールバック:
en - Cookie属性:
path=/,maxAge=1年,sameSite=lax
4.3 データ構造
StructuredPlot(プロジェクト原典言語)
ファイル: next-app/src/lib/types.ts
export interface StructuredPlot {
/** BCP 47 language tag @example 'ja' @example 'zh-Hans' */
language: string
parts?: Part[]
chapters?: Chapter[] // 後方互換用
}初期値: next-app/src/stores/projectStateAtoms.ts
{ language: 'ja', parts: [] }Manuscript(原稿バージョン言語)
export interface Manuscript {
[sectionId: string]: {
versions: {
[lang: string]: ManuscriptVersion // キーがBCP 47言語タグ
}
}
}各 ManuscriptVersion は以下のメタデータを持つ:
content: 原稿テキストisOriginal: 原典かどうかのフラグsourceLang: 翻訳元言語(翻訳版の場合)model: 生成に使用したAIモデルgeneratedAt: 生成日時
PlotTranslation(プロット翻訳)
plotTranslations: {
[lang: string]: {
[id: string]: PlotTranslation // キーがBCP 47言語タグ → アイテムID
}
}4.4 状態管理(Atoms)
| Atom | ファイル | デフォルト | 用途 |
|---|---|---|---|
languageAtom | settingsAtoms.ts | 'en' | グローバルUI言語 |
selectedLanguageAtom | readingStateAtoms.ts | 'ja' | 読書モードでの選択言語 |
structuredPlotAtom | projectStateAtoms.ts | { language: 'ja' } | プロジェクトの原典言語 |
publishStateAtom | publishStateAtoms.ts | { originalLanguage: 'ja' } | パブリッシュ時の原典言語 |
永続化:
selectedLanguageAtom→localStorageキーreading-language(atomWithStorage使用)languageAtom→ i18next経由でCookieに永続化
全デフォルト値(
'ja','en')はBCP 47として有効。
4.5 MCPツール
ファイル: next-app/src/server/api/mcp/index.ts
以下のMCPツールが lang / language パラメータを持つ:
| ツール名 | パラメータ | 説明 |
|---|---|---|
blueperiod_create_project | language | BCP 47 language tag (default: ja) |
blueperiod_get_raw_manuscript | lang | BCP 47 language tag (default: ja) |
blueperiod_edit_manuscript_structured | lang | BCP 47 language tag (default: ja) |
blueperiod_edit_manuscript_delimiter | lang | BCP 47 language tag (default: ja) |
blueperiod_update_manuscript | lang | BCP 47 language tag (default: ja) |
blueperiod_create_manuscript | lang | BCP 47 language tag (default: ja) |
MCPブリッジ: next-app/src/hooks/useMcpBridge.ts
- すべての
langパラメータは(args.lang as string) || 'ja'のパターンでフォールバック
AIエージェントツール定義: next-app/src/lib/features/editorial/tools/manuscript-tools.ts
- Zodスキーマで
z.string().optional().describe("BCP 47 language tag (default: ja)...")と定義
4.6 ManuscriptService
ファイル: next-app/src/lib/features/editorial/services/manuscript-service.ts
getContent(get, sectionId, lang = 'ja') // BCP 47言語タグで原稿を取得
updateContent(set, sectionId, lang, content, options?) // BCP 47言語タグで原稿を更新4.7 プロジェクト作成
ファイル: next-app/src/components/ProjectMetadataDialog.tsx
- 新規プロジェクト作成時に言語選択ドロップダウンを表示
detectBrowserLanguage()でnavigator.languagesから最も近いサポート言語を自動判定getSupportedLanguages()から動的に選択肢を生成- 編集モード時は言語選択フィールドは非表示
サービス層: next-app/src/lib/features/projects/local-project-service.ts
language: params.language || 'ja' // フォームからの入力、未指定時は'ja'5. オンライン(Supabase / Explore)の言語処理
5.1 データベーステーブル定義
ファイル: doc/system/04_database_schema_supabase.md
published_projects テーブルの言語関連フィールド:
| カラム | 型 | 説明 |
|---|---|---|
title | text | グローバル(英語)のタイトル(フォールバック値) |
synopsis | text | グローバル(英語)のあらすじ(フォールバック値) |
headline | text | グローバル(英語)の見出し(フォールバック値) |
original_language | text | BCP 47言語タグ(例: 'ja', 'zh-Hans') |
localizations | JSONB | 言語別メタデータマップ(唯一のソース) |
default_language | text | デフォルト表示言語 |
title_original | text | (DB未DROP: コードからは完全に除去済み) |
synopsis_original | text | (DB未DROP: コードからは完全に除去済み) |
headline_original | text | (DB未DROP: コードからは完全に除去済み) |
tags | text[] | (DB未DROP: コードからは完全に除去済み) |
設計上の特徴: localizations JSONBが唯一の多言語メタデータソース。各言語のデータは localizations[BCP47_TAG] でアクセスする。
Supabase
text型のため文字数制限なし。BCP 47タグをそのまま格納可能。
5.2 TypeScript型定義
ファイル: next-app/src/lib/types/supabase.ts
export type PublishedProject = {
id: string
title: string
synopsis: string | null
headline: string | null
original_language: string | null
localizations: Json | null // Record<string, ProjectLocalization>
default_language: string | null
// ...
}5.3 メタデータ表示ロジック
ファイル: next-app/src/lib/i18n/metadataUtils.ts
getDisplayMetadata(project, preferredLang) は以下のフォールバックチェーンで表示メタデータを決定:
localizations[preferredLang]
→ localizations[default_language || original_language]
→ localizations["en"] (GLOBAL_LANGUAGE)
→ トップレベル title/headline/synopsis (localizationsがnull/emptyの場合のみ)原典言語のメタデータは localizations[original_language] で直接アクセス可能。
5.4 利用可能言語の抽出
ファイル: next-app/src/lib/publishUtils.ts
export function extractAvailableLanguages(manuscripts: Manuscript): string[]Manuscriptの versions オブジェクトのキーを走査し、利用可能な言語リストを返す。BCP 47タグをそのまま返却。
5.5 パブリッシュ処理
ファイル: next-app/src/lib/features/publish/service.ts
パブリッシュ時に以下の言語関連データをSupabaseに Upsert:
localizations(唯一の多言語メタデータソース)original_languagetitle,headline,synopsis(フォールバック値)
6. GUIコンポーネント
6.1 統一言語表示コンポーネント
ファイル: next-app/src/components/ui/language-label.tsx
| コンポーネント | モード | 表示 | 用途 |
|---|---|---|---|
LanguageLabel mode="full" | フル | 日本語 or 한국어 (韓国語) | 翻訳先選択、UI言語設定 |
LanguageLabel mode="code" | ショート | JA + Tooltip | バッジ、極小領域 |
6.2 OriginalLanguageBadge
ファイル: next-app/src/components/ui/original-language-badge.tsx
- BCP 47コードを大文字表示(
JA,ZH-HANS等) - ホバーでTooltipにフル表示(
side="top"、collisionPadding={8}) - Globe アイコン付きバッジ
- 言語コードが不正な場合はコードそのものをフォールバック表示
6.3 言語切替ボタン(プロジェクト詳細)
ファイル: next-app/src/components/details/ProjectDetailsContainer.tsx
3モードの切替:
| モード | 動作 |
|---|---|
auto | 現在のUI言語に基づいて自動判定 |
original | プロジェクトの原典言語で表示 |
global | 英語で表示 |
BCP 47タグをそのまま保持(lang.split('-')[0] による正規化は廃止済み)。
切替フロー:
auto → [現在のUI言語]
↓ クリック
↓ isOriginal ? global : original
↓ トグル
original ↔ global6.4 目次翻訳表示
ファイル: next-app/src/components/reader/TableOfContents.tsx
3段階フォールバックによる翻訳タイトル検索:
- BCP 47完全一致(
zh-Hans→plotTranslations['zh-Hans']) - base言語フォールバック(
zh-Hans→plotTranslations['zh']) - 逆方向フォールバック(
zh→zh-Hans/zh-Hantを順次確認)
6.5 ProjectInfoCard
ファイル: next-app/src/components/details/ProjectInfoCard.tsx
OriginalLanguageBadgeで原典言語を表示extractAvailableLanguagesで利用可能言語のバッジを表示
6.6 言語表示の適用マッピング
| 箇所 | モード | 表示 |
|---|---|---|
| ManuscriptCard バッジ・ドロップダウン | フル | 日本語 / 한국어 (韓国語) |
| LeftPanel 翻訳先選択 | フル | 한국어 (韓国語) |
| SettingsPageClient UI言語 | フル | 日本語 / English |
| ReaderHeader 言語切替 | フル | 日本語 / English |
| OriginalLanguageBadge (Explore) | ショート + Tooltip | JA(ホバーでフル) |
| ProjectInfoCard Available | ショート + Tooltip | JA(ホバーでフル) |
6.7 UI言語選択(設定画面)
ファイル: next-app/src/app/settings/SettingsPageClient.tsx
getUiLanguages()から動的にSelectItemを生成lang.code.split('-')[0]をvalueに使用(i18nextのリソースキーzhに対応)- 新しいUI言語の追加: 翻訳JSON追加 + JSONの
uiSupportedをtrueに変更するのみ
7. 言語コードのデータフロー
7.1 プロジェクト作成フロー
[ユーザー] → 新規プロジェクト作成
↓
ProjectMetadataDialog
detectBrowserLanguage() → navigator.languages から最適な言語を自動判定
言語選択ドロップダウン表示
↓
local-project-service.ts
language: params.language || 'ja'
↓
structuredPlot.language = 'zh-Hans' 等
↓
IndexedDB (projects) に保存7.2 原稿書き込みフロー(MCP経由)
[AIエージェント] → blueperiod_update_manuscript
projectId, sectionId, content, lang='zh-Hans'
↓
MCP Bridge (useMcpBridge.ts)
lang = args.lang || 'ja'
↓
ManuscriptService.updateContent()
manuscripts[sectionId].versions['zh-Hans'] = { content, isOriginal: true }
↓
IndexedDB (manuscripts) に保存7.3 言語検出フロー(初回アクセス)
[ブラウザ] → HTTPリクエスト (Accept-Language: ja,en-US;q=0.9,zh-CN;q=0.7)
↓
proxy.ts (ミドルウェア)
accept-language-parser でサポート言語から最適なものを選択
↓
検出結果: 'ja'
↓
Cookie 'NEXT_LOCALE' = 'ja' (1年有効)
↓
i18next が Cookie を読み取り UI言語を設定7.4 読書モードでの言語切替フロー
[読者] → 言語切替ボタンをクリック
↓
ProjectDetailsContainer.toggleLanguage()
auto → original | global
↓
displayLanguage = lang(BCP 47タグをそのまま保持)
↓
TableOfContents.getTitle()
plotTranslations[displayLanguage][id]?.title
↓ 未ヒット時: base言語フォールバック → 逆方向フォールバック
↓
Manuscript[sectionId].versions[displayLanguage]?.content
↓
[表示更新]7.5 パブリッシュフロー
[作者] → パブリッシュ実行
↓
publish/service.ts
localizations = PublishWizardで構築済みの言語別メタデータマップ
original_language = structuredPlot.language(BCP 47タグ)
↓
Supabase: published_projects に Upsert(localizations のみ書き込み)
↓
[読者] → Explore で閲覧
↓
metadataUtils.getDisplayMetadata()
フォールバックチェーン: localizations[preferredLang]
→ localizations[original_language] → localizations["en"]8. 言語コードが使用される全箇所のマッピング
8.1 定義・設定
| 箇所 | ファイル | 役割 |
|---|---|---|
| i18n設定 | src/i18n/config.ts | UI翻訳のリソース定義、言語検出設定 |
| ミドルウェア | src/proxy.ts | Accept-Language解析、Cookie設定 |
| サポート言語データ | public/languages/supported-languages.json | 言語一元管理 |
| 翻訳ファイル | src/i18n/locales/{en,ja,zh}.json | UI翻訳テキスト + language.* キー |
8.2 型・スキーマ
| 箇所 | ファイル | 役割 |
|---|---|---|
| StructuredPlot.language | src/lib/types.ts | BCP 47言語タグ |
| Manuscript[sectionId].versions[lang] | src/lib/types.ts | 原稿の言語バージョン(BCP 47キー) |
| ManuscriptVersion.isOriginal/sourceLang | src/lib/types.ts | 原典・翻訳元の追跡 |
| PublishedProject.original_language | src/lib/types/supabase.ts | Supabase側の原典言語 |
| plot-service schema | src/lib/features/ai/plot-service.ts | BCP 47正規表現でバリデーション |
8.3 状態管理(Atoms)
| 箇所 | ファイル | デフォルト |
|---|---|---|
structuredPlotAtom | src/stores/projectStateAtoms.ts | 'ja' |
manuscriptContentChangeAtom | src/stores/projectStateAtoms.ts | lang パラメータで管理 |
languageAtom | src/stores/settingsAtoms.ts | 'en' |
selectedLanguageAtom | src/stores/readingStateAtoms.ts | 'ja' |
publishStateAtom.originalLanguage | src/stores/publishStateAtoms.ts | 'ja' |
8.4 サービス層
| 箇所 | ファイル | 役割 |
|---|---|---|
ManuscriptService.getContent() | src/lib/features/editorial/services/manuscript-service.ts | lang指定で原稿取得 |
ManuscriptService.updateContent() | 同上 | lang指定で原稿更新 |
LocalProjectService | src/lib/features/projects/local-project-service.ts | プロジェクト作成時のlanguage設定 |
8.5 MCPツール
| 箇所 | ファイル | パラメータ |
|---|---|---|
| MCPスキーマ定義 | src/server/api/mcp/index.ts | BCP 47 language tag |
| MCPブリッジ | src/hooks/useMcpBridge.ts | `args.lang |
| エージェントツール定義 | src/lib/features/editorial/tools/manuscript-tools.ts | BCP 47 language tag |
| プロジェクトツール定義 | src/lib/features/projects/tools.ts | BCP 47 language tag |
8.6 ユーティリティ
| 箇所 | ファイル | 役割 |
|---|---|---|
| BCP 47ユーティリティ | src/lib/i18n/bcp47.ts | バリデーション・フォールバック・表示名生成 |
| サポート言語ローダー | src/lib/i18n/supportedLanguages.ts | JSONからの言語データアクセス |
| 言語表示コンポーネント | src/components/ui/language-label.tsx | フル/ショート表示の統一コンポーネント |
extractAvailableLanguages() | src/lib/publishUtils.ts | 利用可能言語の抽出 |
getDisplayMetadata() | src/lib/i18n/metadataUtils.ts | 優先言語に基づく表示データ選択 |
8.7 GUIコンポーネント
| 箇所 | ファイル | 役割 |
|---|---|---|
OriginalLanguageBadge | src/components/ui/original-language-badge.tsx | BCP 47コード表示 + Tooltip |
LanguageLabel | src/components/ui/language-label.tsx | フル/ショート統一表示 |
ProjectDetailsContainer | src/components/details/ProjectDetailsContainer.tsx | 3モード言語切替 |
TableOfContents | src/components/details/TableOfContents.tsx | 翻訳タイトル3段階フォールバック |
SettingsPageClient | src/app/settings/SettingsPageClient.tsx | UI言語選択(動的生成) |
ProjectInfoCard | src/components/details/ProjectInfoCard.tsx | 言語バッジ群表示 |
ManuscriptCard | src/components/ManuscriptCard.tsx | 原稿言語バージョン表示・切替 |
LeftPanel | src/components/LeftPanel.tsx | 翻訳先言語選択 |
ReaderHeader | src/components/reader/ReaderHeader.tsx | 読書モード言語切替 |
9. 解決済みの課題
以下はBCP 47対応(2026-04-24)により解決された課題の記録:
| 課題 | 解決内容 |
|---|---|
z.string().length(2) でBCP 47タグが弾かれる | BCP 47正規表現に緩和 |
lang.split('-')[0] で zh-Hans が zh に正規化 | 正規化を廃止、BCP 47タグをそのまま保持 |
['en', 'ja', 'zh'] が複数ファイルに分散 | supported-languages.json で一元管理 |
| プロジェクト作成時に言語選択不可 | ProjectMetadataDialog に言語選択ドロップダウン追加 |
LanguageFlag switch文のハードコード | 統一 LanguageLabel コンポーネントに集約 |
| 翻訳言語リストのハードコード | getSupportedLanguages() から動的生成 |
lang.toUpperCase() と3項演算子の散在 | getLanguageDisplayName() / LanguageLabel に集約 |
| 国旗絵文字の使用 | テキストのみの表示に統一(Globeアイコンは維持) |
OriginalLanguageBadge が英語名のみ | BCP 47コード大文字 + Tooltip に変更 |
未解決・将来課題
| 課題 | 状況 |
|---|---|
既存 zh データの zh-Hans への移行 | 強制移行は行わない。ユーザーが必要に応じて個別対応 |
10. 関連ドキュメント
| ドキュメント | 関連内容 |
|---|---|
02_technology_stack.md | i18next, accept-language-parser の技術選定 |
04_database_schema.md | IndexedDBスキーマの言語フィールド |
04_database_schema_supabase.md | Supabaseテーブルの言語フィールド |
06_development_guidelines.md | i18n実装ガイドライン |
09_design_system.md | UIコンポーネントの設計方針 |
19_mcp_architecture.md | MCPツールのアーキテクチャ |
20_ai_agent_tool_architecture_overview.md | AIエージェントツールの設計 |
| BCP47 Issue | BCP 47対応の課題(クローズ済み) |
| BCP47 Plan | 実装計画(完了済み) |
| BCP47 Report | 実装レポート |