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>
477 lines
12 KiB
Dart
477 lines
12 KiB
Dart
// ignore: unused_import
|
||
import 'package:intl/intl.dart' as intl;
|
||
import 'app_localizations.dart';
|
||
|
||
// ignore_for_file: type=lint
|
||
|
||
/// The translations for Russian (`ru`).
|
||
class AppLocalizationsRu extends AppLocalizations {
|
||
AppLocalizationsRu([String locale = 'ru']) : super(locale);
|
||
|
||
@override
|
||
String get appTitle => 'FoodAI';
|
||
|
||
@override
|
||
String get greetingMorning => 'Доброе утро';
|
||
|
||
@override
|
||
String get greetingAfternoon => 'Добрый день';
|
||
|
||
@override
|
||
String get greetingEvening => 'Добрый вечер';
|
||
|
||
@override
|
||
String get caloriesUnit => 'ккал';
|
||
|
||
@override
|
||
String get gramsUnit => 'г';
|
||
|
||
@override
|
||
String get goalLabel => 'цель:';
|
||
|
||
@override
|
||
String get consumed => 'Потреблено';
|
||
|
||
@override
|
||
String get remaining => 'Осталось';
|
||
|
||
@override
|
||
String get exceeded => 'Превышение';
|
||
|
||
@override
|
||
String get proteinLabel => 'Белки';
|
||
|
||
@override
|
||
String get fatLabel => 'Жиры';
|
||
|
||
@override
|
||
String get carbsLabel => 'Углеводы';
|
||
|
||
@override
|
||
String get today => 'Сегодня';
|
||
|
||
@override
|
||
String get yesterday => 'Вчера';
|
||
|
||
@override
|
||
String get mealsSection => 'Приёмы пищи';
|
||
|
||
@override
|
||
String get addDish => 'Добавить блюдо';
|
||
|
||
@override
|
||
String get scanDish => 'Сканировать';
|
||
|
||
@override
|
||
String get menu => 'Меню';
|
||
|
||
@override
|
||
String get dishHistory => 'История блюд';
|
||
|
||
@override
|
||
String get recommendCook => 'Рекомендуем приготовить';
|
||
|
||
@override
|
||
String get camera => 'Камера';
|
||
|
||
@override
|
||
String get gallery => 'Галерея';
|
||
|
||
@override
|
||
String get analyzingPhoto => 'Анализируем фото...';
|
||
|
||
@override
|
||
String get inQueue => 'Вы в очереди';
|
||
|
||
@override
|
||
String queuePosition(int position) {
|
||
return 'Позиция $position';
|
||
}
|
||
|
||
@override
|
||
String get processing => 'Обрабатываем...';
|
||
|
||
@override
|
||
String get upgradePrompt => 'Хотите без очереди? Upgrade →';
|
||
|
||
@override
|
||
String get recognitionFailed => 'Не удалось распознать. Попробуйте ещё раз.';
|
||
|
||
@override
|
||
String get dishRecognition => 'Распознавание блюд';
|
||
|
||
@override
|
||
String get all => 'Все';
|
||
|
||
@override
|
||
String get dishRecognized => 'Блюдо распознано';
|
||
|
||
@override
|
||
String get recognizing => 'Распознаётся…';
|
||
|
||
@override
|
||
String get recognitionError => 'Ошибка распознавания';
|
||
|
||
@override
|
||
String get dishResultTitle => 'Распознано блюдо';
|
||
|
||
@override
|
||
String get selectDish => 'Выберите блюдо';
|
||
|
||
@override
|
||
String get dishNotRecognized => 'Блюдо не распознано';
|
||
|
||
@override
|
||
String get tryAgain => 'Попробовать снова';
|
||
|
||
@override
|
||
String get nutritionApproximate =>
|
||
'КБЖУ приблизительные — определены по фото.';
|
||
|
||
@override
|
||
String get portion => 'Порция';
|
||
|
||
@override
|
||
String get mealType => 'Приём пищи';
|
||
|
||
@override
|
||
String get dateLabel => 'Дата';
|
||
|
||
@override
|
||
String get addToJournal => 'Добавить в журнал';
|
||
|
||
@override
|
||
String get addFailed => 'Не удалось добавить. Попробуйте ещё раз.';
|
||
|
||
@override
|
||
String get historyTitle => 'История распознавания';
|
||
|
||
@override
|
||
String get historyLoadError => 'Не удалось загрузить историю';
|
||
|
||
@override
|
||
String get retry => 'Повторить';
|
||
|
||
@override
|
||
String get noHistory => 'Нет распознаваний';
|
||
|
||
@override
|
||
String get profileTitle => 'Профиль';
|
||
|
||
@override
|
||
String get edit => 'Изменить';
|
||
|
||
@override
|
||
String get bodyParams => 'ПАРАМЕТРЫ ТЕЛА';
|
||
|
||
@override
|
||
String get goalActivity => 'ЦЕЛЬ И АКТИВНОСТЬ';
|
||
|
||
@override
|
||
String get nutrition => 'ПИТАНИЕ';
|
||
|
||
@override
|
||
String get settings => 'НАСТРОЙКИ';
|
||
|
||
@override
|
||
String get height => 'Рост';
|
||
|
||
@override
|
||
String get weight => 'Вес';
|
||
|
||
@override
|
||
String get age => 'Возраст';
|
||
|
||
@override
|
||
String get gender => 'Пол';
|
||
|
||
@override
|
||
String get genderMale => 'Мужской';
|
||
|
||
@override
|
||
String get genderFemale => 'Женский';
|
||
|
||
@override
|
||
String get goalLoss => 'Похудение';
|
||
|
||
@override
|
||
String get goalMaintain => 'Поддержание';
|
||
|
||
@override
|
||
String get goalGain => 'Набор массы';
|
||
|
||
@override
|
||
String get activityLow => 'Низкая';
|
||
|
||
@override
|
||
String get activityMedium => 'Средняя';
|
||
|
||
@override
|
||
String get activityHigh => 'Высокая';
|
||
|
||
@override
|
||
String get calorieGoal => 'Норма калорий';
|
||
|
||
@override
|
||
String get mealTypes => 'Приёмы пищи';
|
||
|
||
@override
|
||
String get formulaNote => 'Рассчитано по формуле Миффлина-Сан Жеора';
|
||
|
||
@override
|
||
String get language => 'Язык';
|
||
|
||
@override
|
||
String get notSet => 'Не задано';
|
||
|
||
@override
|
||
String get calorieHint => 'Укажите параметры тела для расчёта нормы калорий';
|
||
|
||
@override
|
||
String get logout => 'Выйти из аккаунта';
|
||
|
||
@override
|
||
String get editProfile => 'Редактировать профиль';
|
||
|
||
@override
|
||
String get cancel => 'Отмена';
|
||
|
||
@override
|
||
String get save => 'Сохранить';
|
||
|
||
@override
|
||
String get nameLabel => 'Имя';
|
||
|
||
@override
|
||
String get heightCm => 'Рост (см)';
|
||
|
||
@override
|
||
String get weightKg => 'Вес (кг)';
|
||
|
||
@override
|
||
String get birthDate => 'Дата рождения';
|
||
|
||
@override
|
||
String get nameRequired => 'Введите имя';
|
||
|
||
@override
|
||
String get profileUpdated => 'Профиль обновлён';
|
||
|
||
@override
|
||
String get profileSaveFailed => 'Не удалось сохранить';
|
||
|
||
@override
|
||
String get mealTypeBreakfast => 'Завтрак';
|
||
|
||
@override
|
||
String get mealTypeSecondBreakfast => 'Второй завтрак';
|
||
|
||
@override
|
||
String get mealTypeLunch => 'Обед';
|
||
|
||
@override
|
||
String get mealTypeAfternoonSnack => 'Полдник';
|
||
|
||
@override
|
||
String get mealTypeDinner => 'Ужин';
|
||
|
||
@override
|
||
String get mealTypeSnack => 'Перекус';
|
||
|
||
@override
|
||
String get navHome => 'Главная';
|
||
|
||
@override
|
||
String get navProducts => 'Продукты';
|
||
|
||
@override
|
||
String get navRecipes => 'Рецепты';
|
||
|
||
@override
|
||
String get addFromReceiptOrPhoto => 'Добавить из чека или фото';
|
||
|
||
@override
|
||
String get chooseMethod => 'Выберите способ';
|
||
|
||
@override
|
||
String get photoReceipt => 'Сфотографировать чек';
|
||
|
||
@override
|
||
String get photoReceiptSubtitle => 'Распознаем все продукты из чека';
|
||
|
||
@override
|
||
String get photoProducts => 'Сфотографировать продукты';
|
||
|
||
@override
|
||
String get photoProductsSubtitle => 'Холодильник, стол, полка — до 3 фото';
|
||
|
||
@override
|
||
String get addPackagedFood => 'Добавить готовый продукт';
|
||
|
||
@override
|
||
String get scanBarcode => 'Сканировать штрихкод';
|
||
|
||
@override
|
||
String get portionWeightG => 'Вес порции (г)';
|
||
|
||
@override
|
||
String get productNotFound => 'Продукт не найден';
|
||
|
||
@override
|
||
String get enterManually => 'Ввести вручную';
|
||
|
||
@override
|
||
String get perHundredG => 'на 100 г';
|
||
|
||
@override
|
||
String get searchFoodHint => 'Поиск продуктов и блюд...';
|
||
|
||
@override
|
||
String get recentlyUsedLabel => 'Недавно использованные';
|
||
|
||
@override
|
||
String get productsSection => 'Продукты';
|
||
|
||
@override
|
||
String get dishesSection => 'Блюда';
|
||
|
||
@override
|
||
String noResultsForQuery(String query) {
|
||
return 'По запросу \"$query\" ничего не найдено';
|
||
}
|
||
|
||
@override
|
||
String get servingsLabel => 'Порций';
|
||
|
||
@override
|
||
String get addToDiary => 'Добавить в дневник';
|
||
|
||
@override
|
||
String get scanDishPhoto => 'Сканировать фото';
|
||
|
||
@override
|
||
String planningForDate(String date) {
|
||
return 'Планирование на $date';
|
||
}
|
||
|
||
@override
|
||
String get markAsEaten => 'Отметить как съеденное';
|
||
|
||
@override
|
||
String get plannedMealLabel => 'Запланировано';
|
||
|
||
@override
|
||
String get generateWeekLabel => 'Запланировать неделю';
|
||
|
||
@override
|
||
String get generateWeekSubtitle =>
|
||
'AI составит меню с завтраком, обедом и ужином на всю неделю';
|
||
|
||
@override
|
||
String get generatingMenu => 'Генерируем меню...';
|
||
|
||
@override
|
||
String get dayPlannedLabel => 'День запланирован';
|
||
|
||
@override
|
||
String get planMenuButton => 'Спланировать меню';
|
||
|
||
@override
|
||
String get planMenuTitle => 'Что запланировать?';
|
||
|
||
@override
|
||
String get planOptionSingleMeal => '1 приём пищи';
|
||
|
||
@override
|
||
String get planOptionSingleMealDesc => 'Выберите день и приём пищи';
|
||
|
||
@override
|
||
String get planOptionDay => '1 день';
|
||
|
||
@override
|
||
String get planOptionDayDesc => 'Все приёмы пищи за день';
|
||
|
||
@override
|
||
String get planOptionDays => 'Несколько дней';
|
||
|
||
@override
|
||
String get planOptionDaysDesc => 'Настроить период';
|
||
|
||
@override
|
||
String get planOptionWeek => 'Неделя';
|
||
|
||
@override
|
||
String get planOptionWeekDesc => '7 дней сразу';
|
||
|
||
@override
|
||
String get planSelectDate => 'Выберите дату';
|
||
|
||
@override
|
||
String get planSelectMealType => 'Приём пищи';
|
||
|
||
@override
|
||
String get planSelectRange => 'Выберите период';
|
||
|
||
@override
|
||
String get planGenerateButton => 'Запланировать';
|
||
|
||
@override
|
||
String get planGenerating => 'Генерирую план…';
|
||
|
||
@override
|
||
String get planSuccess => 'Меню запланировано!';
|
||
|
||
@override
|
||
String get planProductsTitle => 'Продукты для меню';
|
||
|
||
@override
|
||
String get planProductsSubtitle =>
|
||
'AI учтёт выбранные продукты при составлении рецептов';
|
||
|
||
@override
|
||
String get planProductsEmpty => 'Продукты не добавлены';
|
||
|
||
@override
|
||
String get planProductsEmptyMessage =>
|
||
'Добавьте продукты, которые есть у вас дома — AI подберёт рецепты из того, что уже есть';
|
||
|
||
@override
|
||
String get planProductsAddProducts => 'Добавить продукты';
|
||
|
||
@override
|
||
String get planProductsContinue => 'Продолжить';
|
||
|
||
@override
|
||
String get planProductsSkip => 'Пропустить выбор продуктов';
|
||
|
||
@override
|
||
String get planProductsSkipNoProducts => 'Планировать без продуктов';
|
||
|
||
@override
|
||
String get planProductsSelectAll => 'Выбрать все';
|
||
|
||
@override
|
||
String get planProductsDeselectAll => 'Снять всё';
|
||
|
||
@override
|
||
String get recentScans => 'Последние сканирования';
|
||
|
||
@override
|
||
String get seeAllScans => 'Все';
|
||
|
||
@override
|
||
String get productJobHistoryTitle => 'История сканирования';
|
||
|
||
@override
|
||
String get jobTypeReceipt => 'Чек';
|
||
|
||
@override
|
||
String get jobTypeProducts => 'Продукты';
|
||
|
||
@override
|
||
String get scanSubmitting => 'Отправка...';
|
||
|
||
@override
|
||
String get processingProducts => 'Обработка...';
|
||
}
|