ponshu-room-lite/docs/ARB_MIGRATION_GUIDE.md

560 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ARBファイル移行ガイド
**対象**: 現在のMap-based翻訳システム → ARB (Application Resource Bundle) への移行
**推奨タイミング**: 翻訳キーが100個を超えたとき、または3言語目追加時
---
## 🎯 なぜARBへ移行すべきか
### 現在の実装 (Map-based) の限界
```dart
// lib/utils/translations.dart (109行)
static const Map<String, Map<String, String>> _translations = {
'home': {'ja': 'ホーム', 'en': 'Home'},
'save': {'ja': '保存', 'en': 'Save'},
// ... 61個のキー
};
```
**問題点:**
1. ✗ ファイルが肥大化 (200キー超えると500行以上)
2. ✗ 翻訳者がDartコードを触る必要がある
3. ✗ パラメータ埋め込みが弱い (`"Welcome, ${name}"` は手動実装)
4. ✗ 複数形対応が困難 (`1 item` vs `2 items`)
5. ✗ IDE補完が弱い (タイポに気づきにくい)
### ARB形式のメリット
```json
// lib/l10n/app_ja.arb
{
"@@locale": "ja",
"welcomeMessage": "ようこそ、{name}さん",
"@welcomeMessage": {
"description": "ユーザーへの歓迎メッセージ",
"placeholders": {
"name": { "type": "String" }
}
}
}
```
**メリット:**
1. ✓ 翻訳者フレンドリー (JSON形式、説明付き)
2. ✓ Flutter公式標準 (長期サポート保証)
3. ✓ 型安全な自動生成コード
4. ✓ パラメータ・複数形対応
5. ✓ IDE補完が効く
6. ✓ 翻訳管理ツールと連携可能 (Crowdin, Lokaliseなど)
---
## 📋 移行手順 (推定時間: 2-3時間)
### Step 1: 設定ファイル作成 (5分)
#### 1-1. `l10n.yaml` を作成
```yaml
# l10n.yaml (プロジェクトルート)
arb-dir: lib/l10n
template-arb-file: app_ja.arb
output-localization-file: app_localizations.dart
output-dir: lib/l10n/generated
synthetic-package: false
```
#### 1-2. `pubspec.yaml` を更新
```yaml
# pubspec.yaml
flutter:
generate: true # この行を追加
uses-material-design: true
assets:
- assets/
```
---
### Step 2: 既存の翻訳をARBファイルに変換 (30分)
#### 2-1. `lib/l10n/app_ja.arb` を作成
```json
{
"@@locale": "ja",
"_comment_navigation": "=== ナビゲーション ===",
"home": "ホーム",
"@home": {
"description": "ホームタブのラベル"
},
"scan": "スキャン",
"@scan": {
"description": "スキャンタブのラベル"
},
"sommelier": "ソムリエ",
"map": "マップ",
"myPage": "マイページ",
"promo": "販促",
"analytics": "分析",
"shop": "店舗",
"_comment_actions": "=== 共通アクション ===",
"save": "保存",
"cancel": "キャンセル",
"delete": "削除",
"close": "閉じる",
"ok": "OK",
"confirm": "確認",
"_comment_home": "=== ホーム画面 ===",
"menuCreation": "お品書き作成",
"searchPlaceholder": "銘柄・酒蔵・都道府県...",
"sort": "並び替え",
"_comment_params": "=== パラメータ付き ===",
"welcomeMessage": "ようこそ、{name}さん",
"@welcomeMessage": {
"description": "ユーザーへの歓迎メッセージ",
"placeholders": {
"name": {
"type": "String",
"example": "太郎"
}
}
},
"itemCount": "{count, plural, =0{お酒がありません} =1{{count}件のお酒} other{{count}件のお酒}}",
"@itemCount": {
"description": "お酒の件数表示",
"placeholders": {
"count": {
"type": "int",
"example": "5"
}
}
}
}
```
#### 2-2. `lib/l10n/app_en.arb` を作成
```json
{
"@@locale": "en",
"home": "Home",
"scan": "Scan",
"sommelier": "Sommelier",
"map": "Map",
"myPage": "My Page",
"promo": "Promo",
"analytics": "Analytics",
"shop": "Shop",
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"close": "Close",
"ok": "OK",
"confirm": "Confirm",
"menuCreation": "Menu Creation",
"searchPlaceholder": "Brand, Brewery, Prefecture...",
"sort": "Sort",
"welcomeMessage": "Welcome, {name}",
"itemCount": "{count, plural, =0{No sake} =1{1 sake} other{{count} sake}}"
}
```
#### 2-3. 自動変換スクリプト (Optional)
```dart
// tools/convert_to_arb.dart
import 'dart:io';
import 'dart:convert';
void main() {
// translations.dartから読み込み
final translations = {
'home': {'ja': 'ホーム', 'en': 'Home'},
'save': {'ja': '保存', 'en': 'Save'},
// ... (現在の61キーをコピー)
};
// ARB形式に変換
final jaArb = {'@@locale': 'ja'};
final enArb = {'@@locale': 'en'};
translations.forEach((key, values) {
jaArb[key] = values['ja'];
enArb[key] = values['en'];
});
// ファイルに書き込み
File('lib/l10n/app_ja.arb').writeAsStringSync(
JsonEncoder.withIndent(' ').convert(jaArb)
);
File('lib/l10n/app_en.arb').writeAsStringSync(
JsonEncoder.withIndent(' ').convert(enArb)
);
print('✅ ARBファイル生成完了');
}
```
実行:
```bash
dart run tools/convert_to_arb.dart
```
---
### Step 3: コード生成 (5分)
```bash
# 依存パッケージ取得 & コード生成
flutter pub get
# 自動的に lib/l10n/generated/app_localizations.dart が生成される
```
生成されるファイル:
```
lib/l10n/generated/
├── app_localizations.dart # メインクラス
├── app_localizations_ja.dart # 日本語実装
└── app_localizations_en.dart # 英語実装
```
---
### Step 4: アプリ設定を更新 (10分)
#### 4-1. `main.dart` を更新
```dart
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; // 追加
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final locale = ref.watch(localeProvider);
return MaterialApp(
// 追加: localizationsDelegates
localizationsDelegates: const [
AppLocalizations.delegate, // 追加
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
// 追加: supportedLocales
supportedLocales: const [
Locale('ja'),
Locale('en'),
],
locale: locale,
// ... 既存のコード
);
}
}
```
---
### Step 5: 各画面のコードを置き換え (1-2時間)
#### Before (Map-based)
```dart
class HomeScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userProfile = ref.watch(userProfileProvider);
final t = Translations(userProfile.locale);
return Scaffold(
appBar: AppBar(
title: Text(t['menuCreation']),
),
body: Text(t['welcomeMessage']), // パラメータ非対応
);
}
}
```
#### After (ARB-based)
```dart
class HomeScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.menuCreation),
),
body: Text(l10n.welcomeMessage('太郎')), // パラメータ対応
);
}
}
```
#### 一括置換パターン
```regex
# 検索
t\['(\w+)'\]
# 置換
l10n.$1
```
---
### Step 6: 旧コードの削除 (5分)
```bash
# translations.dartを削除
rm lib/utils/translations.dart
# Gitでコミット
git add .
git commit -m "feat: Migrate to ARB-based localization"
```
---
## 🎯 移行後の使い方
### 基本的な使い方
```dart
// BuildContext経由でアクセス
final l10n = AppLocalizations.of(context)!;
Text(l10n.home) // "ホーム" or "Home"
Text(l10n.save) // "保存" or "Save"
```
### パラメータ付き翻訳
```dart
// 1つのパラメータ
Text(l10n.welcomeMessage('太郎')) // "ようこそ、太郎さん"
// 複数のパラメータ
Text(l10n.dateRange(startDate, endDate))
```
### 複数形対応
```dart
// 件数に応じて自動切り替え
Text(l10n.itemCount(0)) // "お酒がありません"
Text(l10n.itemCount(1)) // "1件のお酒"
Text(l10n.itemCount(5)) // "5件のお酒"
```
### 日付・通貨フォーマット
```dart
import 'package:intl/intl.dart';
// 日付
final formatter = DateFormat.yMd(l10n.localeName);
formatter.format(DateTime.now()); // "2026/01/20" (ja) or "1/20/2026" (en)
// 通貨
final currency = NumberFormat.currency(locale: l10n.localeName, symbol: '¥');
currency.format(1500); // "¥1,500"
```
---
## 🧪 移行後のテスト
### 1. 翻訳漏れチェック
```dart
// test/l10n_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'dart:convert';
import 'dart:io';
void test('All translation keys exist in all locales', () {
final jaFile = File('lib/l10n/app_ja.arb');
final enFile = File('lib/l10n/app_en.arb');
final jaKeys = (jsonDecode(jaFile.readAsStringSync()) as Map)
.keys
.where((k) => !k.startsWith('@') && !k.startsWith('_'))
.toSet();
final enKeys = (jsonDecode(enFile.readAsStringSync()) as Map)
.keys
.where((k) => !k.startsWith('@') && !k.startsWith('_'))
.toSet();
expect(jaKeys, equals(enKeys), reason: '翻訳漏れがあります');
});
```
実行:
```bash
flutter test test/l10n_test.dart
```
### 2. 手動テスト
1. アプリを起動
2. 設定で言語を「English」に変更
3. 全画面を確認
4. パラメータ付き翻訳が正しく動作するか確認
---
## 📊 移行前後の比較
| 項目 | Map-based (現在) | ARB-based (移行後) |
|------|------------------|-------------------|
| **ファイル構成** | 1ファイル (109行) | 2ファイル (ja/en) |
| **翻訳者の作業** | Dartコードを編集 | JSONファイルのみ |
| **パラメータ埋め込み** | 手動実装 | 自動対応 |
| **複数形** | 非対応 | 完全対応 |
| **型安全性** | 弱い (String返却) | 強い (生成コード) |
| **IDE補完** | なし | あり |
| **翻訳管理ツール** | 非対応 | 対応 |
| **学習コスト** | 低い | 中程度 |
| **長期メンテナンス** | 困難 (肥大化) | 容易 |
---
## 🚨 注意事項
### 1. BuildContextが必要
```dart
// ❌ NG: Providerから直接アクセス不可
final localeProvider = Provider<String>((ref) {
final l10n = AppLocalizations.of(???); // BuildContextがない
return l10n.home;
});
// ✅ OK: Widget内でアクセス
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
return Text(l10n.home);
}
```
**解決策**: BuildContext不要な場面では、localeプロバイダーから言語コードを取得して分岐:
```dart
final locale = ref.watch(localeProvider);
final text = locale.languageCode == 'en' ? 'Home' : 'ホーム';
```
### 2. 生成ファイルはGit管理不要
```.gitignore
# ARB生成ファイルは無視
lib/l10n/generated/
```
### 3. ビルド時に自動生成される
```bash
# pubspec.yamlを変更したら必ず実行
flutter pub get
```
---
## 🔧 トラブルシューティング
### Q1: `AppLocalizations`が見つからない
**原因**: コード生成されていない
**解決策**:
```bash
flutter clean
flutter pub get
flutter pub run build_runner build --delete-conflicting-outputs
```
### Q2: 翻訳が反映されない
**原因**: ARBファイルの構文エラー
**解決策**: JSONバリデーターでチェック
```bash
# jqコマンドでバリデーション
jq . lib/l10n/app_ja.arb
```
### Q3: パラメータが表示されない
**原因**: プレースホルダー定義が不足
**解決策**: `@` で始まるメタデータを追加
```json
"welcomeMessage": "Welcome, {name}",
"@welcomeMessage": {
"placeholders": {
"name": {"type": "String"}
}
}
```
---
## 📚 参考資料
- [Flutter公式: Internationalizing Flutter apps](https://docs.flutter.dev/ui/accessibility-and-localization/internationalization)
- [ARB Format Specification](https://github.com/google/app-resource-bundle/wiki/ApplicationResourceBundleSpecification)
- [intl Package](https://pub.dev/packages/intl)
- [flutter_localizations](https://api.flutter.dev/flutter/flutter_localizations/flutter_localizations-library.html)
---
## ✅ チェックリスト
移行完了時に確認:
- [ ] l10n.yaml作成済み
- [ ] pubspec.yamlに `generate: true` 追加済み
- [ ] app_ja.arb / app_en.arb作成済み
- [ ] flutter pub get実行済み
- [ ] 生成ファイル確認 (lib/l10n/generated/)
- [ ] main.dartにlocalizationsDelegates追加済み
- [ ] 全画面でt['key'] → l10n.key に置き換え済み
- [ ] 翻訳漏れテスト実行済み
- [ ] 手動テストで動作確認済み
- [ ] translations.dart削除済み
- [ ] Gitコミット済み
---
**移行タイミングの目安:**
- ✅ 今すぐ (基盤が整う)
- ✅ 翻訳キー100個超え時
- ✅ 3言語目追加時
- ❌ 現在 (まだ61キーのみ、Map実装で十分)