Backend:
- Migration 002: product_recognition_jobs table with JSONB images column
and job_type CHECK ('receipt' | 'products')
- New Kafka topics: ai.products.paid / ai.products.free
- ProductJob model, ProductJobRepository (mirrors dish job pattern)
- itemEnricher extracted from Handler — shared by HTTP handler and worker
- ProductSSEBroker: PG LISTEN on product_job_update channel
- ProductWorkerPool: 5 workers, branches on job_type to call
RecognizeReceipt or RecognizeProducts per image in parallel
- Handler: RecognizeReceipt and RecognizeProducts now return 202 Accepted
instead of blocking; 4 new endpoints: GET /ai/product-jobs,
/product-jobs/history, /product-jobs/{id}, /product-jobs/{id}/stream
- cmd/worker: extended to run ProductWorkerPool alongside dish WorkerPool
- cmd/server: wires productJobRepository + productSSEBroker; both SSE
brokers started in App.Start()
Flutter client:
- ProductJobCreated, ProductJobResult, ProductJobSummary, ProductJobEvent
models + submitReceiptRecognition/submitProductsRecognition/stream methods
- Shared _openSseStream helper eliminates duplicate SSE parsing loop
- ScanScreen: replace blocking AI calls with async submit + navigate to
ProductJobWatchScreen
- ProductJobWatchScreen: watches SSE stream, navigates to /scan/confirm
when done, shows error on failure
- ProductsScreen: prepends _RecentScansSection (hidden when empty); compact
horizontal list of recent scans with "See all" → history
- ProductJobHistoryScreen: full list of all product recognition jobs
- New routes: /scan/product-job-watch, /products/job-history
- L10n: 7 new keys in all 12 ARB files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
177 lines
6.4 KiB
Plaintext
177 lines
6.4 KiB
Plaintext
{
|
||
"@@locale": "ja",
|
||
"appTitle": "FoodAI",
|
||
"greetingMorning": "おはようございます",
|
||
"greetingAfternoon": "こんにちは",
|
||
"greetingEvening": "こんばんは",
|
||
"caloriesUnit": "kcal",
|
||
"gramsUnit": "g",
|
||
"goalLabel": "目標:",
|
||
"consumed": "摂取済み",
|
||
"remaining": "残り",
|
||
"exceeded": "超過",
|
||
"proteinLabel": "タンパク質",
|
||
"fatLabel": "脂質",
|
||
"carbsLabel": "炭水化物",
|
||
"today": "今日",
|
||
"yesterday": "昨日",
|
||
"mealsSection": "食事",
|
||
"addDish": "料理を追加",
|
||
"scanDish": "スキャン",
|
||
"menu": "メニュー",
|
||
"dishHistory": "料理履歴",
|
||
"recommendCook": "おすすめレシピ",
|
||
"camera": "カメラ",
|
||
"gallery": "ギャラリー",
|
||
"analyzingPhoto": "写真を分析中...",
|
||
"inQueue": "順番待ち中",
|
||
"queuePosition": "{position}番目",
|
||
"@queuePosition": {
|
||
"placeholders": {
|
||
"position": {
|
||
"type": "int"
|
||
}
|
||
}
|
||
},
|
||
"processing": "処理中...",
|
||
"upgradePrompt": "順番をスキップ?アップグレード →",
|
||
"recognitionFailed": "認識に失敗しました。もう一度お試しください。",
|
||
"dishRecognition": "料理認識",
|
||
"all": "すべて",
|
||
"dishRecognized": "料理を認識しました",
|
||
"recognizing": "認識中…",
|
||
"recognitionError": "認識エラー",
|
||
"dishResultTitle": "料理を認識しました",
|
||
"selectDish": "料理を選択",
|
||
"dishNotRecognized": "料理を認識できませんでした",
|
||
"tryAgain": "もう一度試す",
|
||
"nutritionApproximate": "栄養値は概算です — 写真から推定されました。",
|
||
"portion": "量",
|
||
"mealType": "食事タイプ",
|
||
"dateLabel": "日付",
|
||
"addToJournal": "日記に追加",
|
||
"addFailed": "追加に失敗しました。もう一度お試しください。",
|
||
"historyTitle": "認識履歴",
|
||
"historyLoadError": "履歴の読み込みに失敗しました",
|
||
"retry": "再試行",
|
||
"noHistory": "認識履歴がありません",
|
||
"profileTitle": "プロフィール",
|
||
"edit": "編集",
|
||
"bodyParams": "身体パラメータ",
|
||
"goalActivity": "目標 & 活動",
|
||
"nutrition": "栄養",
|
||
"settings": "設定",
|
||
"height": "身長",
|
||
"weight": "体重",
|
||
"age": "年齢",
|
||
"gender": "性別",
|
||
"genderMale": "男性",
|
||
"genderFemale": "女性",
|
||
"goalLoss": "体重減少",
|
||
"goalMaintain": "維持",
|
||
"goalGain": "筋肉増量",
|
||
"activityLow": "低い",
|
||
"activityMedium": "普通",
|
||
"activityHigh": "高い",
|
||
"calorieGoal": "カロリー目標",
|
||
"mealTypes": "食事タイプ",
|
||
"formulaNote": "ミフリン・セントジョー式で計算",
|
||
"language": "言語",
|
||
"notSet": "未設定",
|
||
"calorieHint": "カロリー目標を計算するために身体パラメータを入力してください",
|
||
"logout": "ログアウト",
|
||
"editProfile": "プロフィールを編集",
|
||
"cancel": "キャンセル",
|
||
"save": "保存",
|
||
"nameLabel": "名前",
|
||
"heightCm": "身長(cm)",
|
||
"weightKg": "体重(kg)",
|
||
"birthDate": "生年月日",
|
||
"nameRequired": "名前を入力してください",
|
||
"profileUpdated": "プロフィールを更新しました",
|
||
"profileSaveFailed": "保存に失敗しました",
|
||
"mealTypeBreakfast": "朝食",
|
||
"mealTypeSecondBreakfast": "第二朝食",
|
||
"mealTypeLunch": "昼食",
|
||
"mealTypeAfternoonSnack": "おやつ",
|
||
"mealTypeDinner": "夕食",
|
||
"mealTypeSnack": "間食",
|
||
"navHome": "ホーム",
|
||
"navProducts": "食品",
|
||
"navRecipes": "レシピ",
|
||
"addFromReceiptOrPhoto": "レシートや写真から追加",
|
||
"chooseMethod": "方法を選択",
|
||
"photoReceipt": "レシートを撮影",
|
||
"photoReceiptSubtitle": "レシートから全商品を認識",
|
||
"photoProducts": "食品を撮影",
|
||
"photoProductsSubtitle": "冷蔵庫・テーブル・棚 — 最大3枚",
|
||
"addPackagedFood": "パッケージ食品を追加",
|
||
"scanBarcode": "バーコードをスキャン",
|
||
"portionWeightG": "1食分の重さ(g)",
|
||
"productNotFound": "商品が見つかりません",
|
||
"enterManually": "手動で入力",
|
||
"perHundredG": "100gあたり",
|
||
"searchFoodHint": "食品と料理を検索...",
|
||
"recentlyUsedLabel": "最近使用",
|
||
"productsSection": "食品",
|
||
"dishesSection": "料理",
|
||
"noResultsForQuery": "「{query}」の検索結果はありません",
|
||
"@noResultsForQuery": {
|
||
"placeholders": {
|
||
"query": {
|
||
"type": "String"
|
||
}
|
||
}
|
||
},
|
||
"servingsLabel": "人前",
|
||
"addToDiary": "日記に追加",
|
||
"scanDishPhoto": "写真をスキャン",
|
||
"planningForDate": "",
|
||
"@planningForDate": {
|
||
"placeholders": {
|
||
"date": {
|
||
"type": "String"
|
||
}
|
||
}
|
||
},
|
||
"markAsEaten": "食べた印をつける",
|
||
"plannedMealLabel": "予定済み",
|
||
"generateWeekLabel": "週を計画する",
|
||
"generateWeekSubtitle": "AIが一週間の朝食・昼食・夕食のメニューを作成します",
|
||
"generatingMenu": "メニューを生成中...",
|
||
"dayPlannedLabel": "日の計画済み",
|
||
"planMenuButton": "食事を計画する",
|
||
"planMenuTitle": "何を計画する?",
|
||
"planOptionSingleMeal": "1食",
|
||
"planOptionSingleMealDesc": "日と食事タイプを選択",
|
||
"planOptionDay": "1日",
|
||
"planOptionDayDesc": "1日分の全食事",
|
||
"planOptionDays": "数日",
|
||
"planOptionDaysDesc": "期間をカスタマイズ",
|
||
"planOptionWeek": "1週間",
|
||
"planOptionWeekDesc": "7日分まとめて",
|
||
"planSelectDate": "日付を選択",
|
||
"planSelectMealType": "食事タイプ",
|
||
"planSelectRange": "期間を選択",
|
||
"planGenerateButton": "計画する",
|
||
"planGenerating": "プランを生成中…",
|
||
"planSuccess": "メニューが計画されました!",
|
||
"planProductsTitle": "メニューの食材",
|
||
"planProductsSubtitle": "AIはレシピ生成時に選択した食材を考慮します",
|
||
"planProductsEmpty": "食材が追加されていません",
|
||
"planProductsEmptyMessage": "家にある食材を追加してください — AIが手持ちの食材でレシピを提案します",
|
||
"planProductsAddProducts": "食材を追加",
|
||
"planProductsContinue": "続ける",
|
||
"planProductsSkip": "食材選択をスキップ",
|
||
"planProductsSkipNoProducts": "食材なしでプランニング",
|
||
"planProductsSelectAll": "すべて選択",
|
||
"planProductsDeselectAll": "すべて解除",
|
||
"recentScans": "最近のスキャン",
|
||
"seeAllScans": "すべて表示",
|
||
"productJobHistoryTitle": "スキャン履歴",
|
||
"jobTypeReceipt": "レシート",
|
||
"jobTypeProducts": "商品",
|
||
"scanSubmitting": "送信中...",
|
||
"processingProducts": "処理中..."
|
||
}
|