feat: Flutter client localisation (12 languages)

Add flutter_localizations + intl, 12 ARB files (en/ru/es/de/fr/it/pt/zh/ja/ko/ar/hi),
replace all hardcoded Russian UI strings with AppLocalizations, detect system locale
on first launch, localise bottom nav bar labels, document rule in CLAUDE.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dbastrikin
2026-03-19 22:22:52 +02:00
parent 9357c194eb
commit 54b10d51e2
40 changed files with 5919 additions and 267 deletions

View File

@@ -91,3 +91,53 @@ Single- or two-letter names are only allowed when the abbreviation *is* the full
This rule applies to all languages in this repo (Go and Dart). This rule applies to all languages in this repo (Go and Dart).
In Go, name variables to avoid shadowing the `error` built-in and the `context` package — In Go, name variables to avoid shadowing the `error` built-in and the `context` package —
use descriptive prefixes: `parseError`, `requestContext`, etc. use descriptive prefixes: `parseError`, `requestContext`, etc.
## Flutter Client Localisation
**Rule:** Every UI string in `client/` must go through `AppLocalizations`.
Never hardcode user-visible text in Dart source files.
### Current setup
- `flutter_localizations` (sdk: flutter) + `intl: ^0.20.2` in `client/pubspec.yaml`
- `generate: true` under `flutter:` in `client/pubspec.yaml`
- `client/l10n.yaml` — generator config (arb-dir, template-arb-file, output-class)
- Generated class: `package:food_ai/l10n/app_localizations.dart`
### Supported languages
`en`, `ru`, `es`, `de`, `fr`, `it`, `pt`, `zh`, `ja`, `ko`, `ar`, `hi`
### ARB files
All translation files live in `client/lib/l10n/`:
- `app_en.arb` — English (template / canonical)
- `app_ru.arb`, `app_es.arb`, `app_de.arb`, `app_fr.arb`, `app_it.arb`
- `app_pt.arb`, `app_zh.arb`, `app_ja.arb`, `app_ko.arb`, `app_ar.arb`, `app_hi.arb`
### Usage pattern
```dart
import 'package:food_ai/l10n/app_localizations.dart';
// Inside build():
final l10n = AppLocalizations.of(context)!;
Text(l10n.someKey)
```
### Adding new strings
1. Add the key + English value to `client/lib/l10n/app_en.arb` (template file).
2. Add the same key with the correct translation to **all other 11 ARB files**.
3. Run `flutter gen-l10n` inside `client/` to regenerate `AppLocalizations`.
4. Use `l10n.<newKey>` in the widget.
For parameterised strings use the ICU placeholder syntax in ARB files:
```json
"queuePosition": "Position {position}",
"@queuePosition": { "placeholders": { "position": { "type": "int" } } }
```
Then call `l10n.queuePosition(n)` in Dart.

4
client/l10n.yaml Normal file
View File

@@ -0,0 +1,4 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:food_ai/l10n/app_localizations.dart';
import 'core/locale/language_provider.dart';
import 'core/router/app_router.dart'; import 'core/router/app_router.dart';
import 'core/theme/app_theme.dart'; import 'core/theme/app_theme.dart';
@@ -10,12 +12,16 @@ class App extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(routerProvider); final router = ref.watch(routerProvider);
final languageCode = ref.watch(languageProvider);
return MaterialApp.router( return MaterialApp.router(
title: 'FoodAI', title: 'FoodAI',
theme: appTheme(), theme: appTheme(),
routerConfig: router, routerConfig: router,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
locale: Locale(languageCode),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
); );
} }
} }

View File

@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../l10n/app_localizations.dart';
import '../auth/auth_provider.dart'; import '../auth/auth_provider.dart';
import '../../features/auth/login_screen.dart'; import '../../features/auth/login_screen.dart';
import '../../features/auth/register_screen.dart'; import '../../features/auth/register_screen.dart';
@@ -208,15 +210,17 @@ class MainShell extends ConsumerWidget {
orElse: () => 0, orElse: () => 0,
); );
final l10n = AppLocalizations.of(context)!;
return Scaffold( return Scaffold(
body: child, body: child,
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: BottomNavigationBar(
currentIndex: currentIndex, currentIndex: currentIndex,
onTap: (index) => context.go(_tabs[index]), onTap: (index) => context.go(_tabs[index]),
items: [ items: [
const BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.home), icon: const Icon(Icons.home),
label: 'Главная', label: l10n.navHome,
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Badge( icon: Badge(
@@ -224,19 +228,19 @@ class MainShell extends ConsumerWidget {
label: Text('$expiringCount'), label: Text('$expiringCount'),
child: const Icon(Icons.kitchen), child: const Icon(Icons.kitchen),
), ),
label: 'Продукты', label: l10n.navProducts,
), ),
const BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.calendar_month), icon: const Icon(Icons.calendar_month),
label: 'Меню', label: l10n.menu,
), ),
const BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.menu_book), icon: const Icon(Icons.menu_book),
label: 'Рецепты', label: l10n.navRecipes,
), ),
const BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.person), icon: const Icon(Icons.person),
label: 'Профиль', label: l10n.profileTitle,
), ),
], ],
), ),

View File

@@ -56,3 +56,23 @@ final todayJobsProvider =
StateNotifierProvider<TodayJobsNotifier, AsyncValue<List<DishJobSummary>>>( StateNotifierProvider<TodayJobsNotifier, AsyncValue<List<DishJobSummary>>>(
(ref) => TodayJobsNotifier(ref.read(recognitionServiceProvider)), (ref) => TodayJobsNotifier(ref.read(recognitionServiceProvider)),
); );
// ── All recognition jobs (history screen) ─────────────────────
class AllJobsNotifier extends StateNotifier<AsyncValue<List<DishJobSummary>>> {
final RecognitionService _service;
AllJobsNotifier(this._service) : super(const AsyncValue.loading()) {
load();
}
Future<void> load() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => _service.listAllJobs());
}
}
final allJobsProvider =
StateNotifierProvider<AllJobsNotifier, AsyncValue<List<DishJobSummary>>>(
(ref) => AllJobsNotifier(ref.read(recognitionServiceProvider)),
);

View File

@@ -2,9 +2,11 @@ import 'dart:math' as math;
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:food_ai/l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
import '../../core/storage/local_preferences_provider.dart'; import '../../core/storage/local_preferences_provider.dart';
import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors.dart';
@@ -24,6 +26,7 @@ class HomeScreen extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final homeSummaryState = ref.watch(homeProvider); final homeSummaryState = ref.watch(homeProvider);
final profile = ref.watch(profileProvider).valueOrNull; final profile = ref.watch(profileProvider).valueOrNull;
final userName = profile?.name; final userName = profile?.name;
@@ -100,7 +103,7 @@ class HomeScreen extends ConsumerWidget {
_QuickActionsRow(), _QuickActionsRow(),
if (recommendations.isNotEmpty) ...[ if (recommendations.isNotEmpty) ...[
const SizedBox(height: 20), const SizedBox(height: 20),
_SectionTitle('Рекомендуем приготовить'), _SectionTitle(l10n.recommendCook),
const SizedBox(height: 12), const SizedBox(height: 12),
_RecommendationsRow(recipes: recommendations), _RecommendationsRow(recipes: recommendations),
], ],
@@ -120,26 +123,25 @@ class _AppBar extends StatelessWidget {
final String? userName; final String? userName;
const _AppBar({this.userName}); const _AppBar({this.userName});
String get _greetingBase { String _greetingBase(AppLocalizations l10n) {
final hour = DateTime.now().hour; final hour = DateTime.now().hour;
if (hour < 12) return 'Доброе утро'; if (hour < 12) return l10n.greetingMorning;
if (hour < 18) return 'Добрый день'; if (hour < 18) return l10n.greetingAfternoon;
return 'Добрый вечер'; return l10n.greetingEvening;
}
String get _greeting {
final name = userName;
if (name != null && name.isNotEmpty) return '$_greetingBase, $name!';
return _greetingBase;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context); final theme = Theme.of(context);
final base = _greetingBase(l10n);
final greeting = (userName != null && userName!.isNotEmpty)
? '$base, $userName!'
: base;
return SliverAppBar( return SliverAppBar(
pinned: false, pinned: false,
floating: true, floating: true,
title: Text(_greeting, style: theme.textTheme.titleMedium), title: Text(greeting, style: theme.textTheme.titleMedium),
); );
} }
} }
@@ -160,11 +162,6 @@ class _DateSelector extends StatefulWidget {
} }
class _DateSelectorState extends State<_DateSelector> { class _DateSelectorState extends State<_DateSelector> {
static const _weekDayShort = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
static const _monthNames = [
'января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря',
];
// Total days available in the past (index 0 = today, index N-1 = oldest) // Total days available in the past (index 0 = today, index N-1 = oldest)
static const _totalDays = 365; static const _totalDays = 365;
static const _pillWidth = 48.0; static const _pillWidth = 48.0;
@@ -172,12 +169,10 @@ class _DateSelectorState extends State<_DateSelector> {
late final ScrollController _scrollController; late final ScrollController _scrollController;
String _formatSelectedDate(DateTime date) { String _formatSelectedDate(DateTime date, String localeCode) {
final now = DateTime.now(); final now = DateTime.now();
final dayName = _weekDayShort[date.weekday - 1];
final month = _monthNames[date.month - 1];
final yearSuffix = date.year != now.year ? ' ${date.year}' : ''; final yearSuffix = date.year != now.year ? ' ${date.year}' : '';
return '$dayName, ${date.day} $month$yearSuffix'; return DateFormat('EEE, d MMMM', localeCode).format(date) + yearSuffix;
} }
// Index in the reversed list: 0 = today, 1 = yesterday, … // Index in the reversed list: 0 = today, 1 = yesterday, …
@@ -253,6 +248,8 @@ class _DateSelectorState extends State<_DateSelector> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final localeCode = Localizations.localeOf(context).toString();
final theme = Theme.of(context); final theme = Theme.of(context);
final today = DateTime.now(); final today = DateTime.now();
final todayNormalized = DateTime(today.year, today.month, today.day); final todayNormalized = DateTime(today.year, today.month, today.day);
@@ -276,7 +273,7 @@ class _DateSelectorState extends State<_DateSelector> {
), ),
Expanded( Expanded(
child: Text( child: Text(
_formatSelectedDate(widget.selectedDate), _formatSelectedDate(widget.selectedDate, localeCode),
style: theme.textTheme.labelMedium style: theme.textTheme.labelMedium
?.copyWith(fontWeight: FontWeight.w600), ?.copyWith(fontWeight: FontWeight.w600),
textAlign: TextAlign.center, textAlign: TextAlign.center,
@@ -290,7 +287,7 @@ class _DateSelectorState extends State<_DateSelector> {
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
), ),
child: Text( child: Text(
'Сегодня', l10n.today,
style: theme.textTheme.labelMedium style: theme.textTheme.labelMedium
?.copyWith(color: theme.colorScheme.primary), ?.copyWith(color: theme.colorScheme.primary),
), ),
@@ -336,7 +333,7 @@ class _DateSelectorState extends State<_DateSelector> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
_weekDayShort[date.weekday - 1], DateFormat('EEE', localeCode).format(date),
style: theme.textTheme.labelSmall?.copyWith( style: theme.textTheme.labelSmall?.copyWith(
color: isSelected color: isSelected
? theme.colorScheme.onPrimary ? theme.colorScheme.onPrimary
@@ -386,21 +383,22 @@ class _CaloriesCard extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (dailyGoal == 0) return const SizedBox.shrink(); if (dailyGoal == 0) return const SizedBox.shrink();
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context); final theme = Theme.of(context);
final logged = loggedCalories.toInt(); final logged = loggedCalories.toInt();
final rawProgress = dailyGoal > 0 ? loggedCalories / dailyGoal : 0.0; final rawProgress = dailyGoal > 0 ? loggedCalories / dailyGoal : 0.0;
final isOverGoal = rawProgress > 1.0; final isOverGoal = rawProgress > 1.0;
final ringColor = _ringColorFor(rawProgress, goalType); final ringColor = _ringColorFor(rawProgress, goalType);
final String secondaryLabel; final String secondaryValue;
final Color secondaryColor; final Color secondaryColor;
if (isOverGoal) { if (isOverGoal) {
final overBy = (loggedCalories - dailyGoal).toInt(); final overBy = (loggedCalories - dailyGoal).toInt();
secondaryLabel = '+$overBy перебор'; secondaryValue = '+$overBy ${l10n.caloriesUnit}';
secondaryColor = AppColors.error; secondaryColor = AppColors.error;
} else { } else {
final remaining = (dailyGoal - loggedCalories).toInt(); final remaining = (dailyGoal - loggedCalories).toInt();
secondaryLabel = 'осталось $remaining'; secondaryValue = '$remaining ${l10n.caloriesUnit}';
secondaryColor = AppColors.textSecondary; secondaryColor = AppColors.textSecondary;
} }
@@ -433,14 +431,14 @@ class _CaloriesCard extends StatelessWidget {
), ),
), ),
Text( Text(
'ккал', l10n.caloriesUnit,
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
color: AppColors.textSecondary, color: AppColors.textSecondary,
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'цель: $dailyGoal', '${l10n.goalLabel} $dailyGoal',
style: theme.textTheme.labelSmall?.copyWith( style: theme.textTheme.labelSmall?.copyWith(
color: AppColors.textSecondary, color: AppColors.textSecondary,
), ),
@@ -456,20 +454,20 @@ class _CaloriesCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_CalorieStat( _CalorieStat(
label: 'Потреблено', label: l10n.consumed,
value: '$logged ккал', value: '$logged ${l10n.caloriesUnit}',
valueColor: ringColor, valueColor: ringColor,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_CalorieStat( _CalorieStat(
label: isOverGoal ? 'Превышение' : 'Осталось', label: isOverGoal ? l10n.exceeded : l10n.remaining,
value: secondaryLabel, value: secondaryValue,
valueColor: secondaryColor, valueColor: secondaryColor,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_CalorieStat( _CalorieStat(
label: 'Цель', label: l10n.goalLabel.replaceAll(':', '').trim(),
value: '$dailyGoal ккал', value: '$dailyGoal ${l10n.caloriesUnit}',
valueColor: AppColors.textPrimary, valueColor: AppColors.textPrimary,
), ),
], ],
@@ -532,28 +530,29 @@ class _MacrosRow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Row( return Row(
children: [ children: [
Expanded( Expanded(
child: _MacroChip( child: _MacroChip(
label: 'Белки', label: l10n.proteinLabel,
value: '${proteinG.toStringAsFixed(1)} г', value: '${proteinG.toStringAsFixed(1)} ${l10n.gramsUnit}',
color: Colors.blue, color: Colors.blue,
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: _MacroChip( child: _MacroChip(
label: 'Жиры', label: l10n.fatLabel,
value: '${fatG.toStringAsFixed(1)} г', value: '${fatG.toStringAsFixed(1)} ${l10n.gramsUnit}',
color: Colors.orange, color: Colors.orange,
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: _MacroChip( child: _MacroChip(
label: 'Углеводы', label: l10n.carbsLabel,
value: '${carbsG.toStringAsFixed(1)} г', value: '${carbsG.toStringAsFixed(1)} ${l10n.gramsUnit}',
color: Colors.green, color: Colors.green,
), ),
), ),
@@ -712,11 +711,12 @@ class _DailyMealsSection extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context); final theme = Theme.of(context);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('Приёмы пищи', style: theme.textTheme.titleSmall), Text(l10n.mealsSection, style: theme.textTheme.titleSmall),
const SizedBox(height: 8), const SizedBox(height: 8),
...mealTypeIds.map((mealTypeId) { ...mealTypeIds.map((mealTypeId) {
final mealTypeOption = mealTypeById(mealTypeId); final mealTypeOption = mealTypeById(mealTypeId);
@@ -743,6 +743,8 @@ Future<void> _pickAndShowDishResult(
WidgetRef ref, WidgetRef ref,
String mealTypeId, String mealTypeId,
) async { ) async {
final l10n = AppLocalizations.of(context)!;
// 1. Choose image source. // 1. Choose image source.
final source = await showModalBottomSheet<ImageSource>( final source = await showModalBottomSheet<ImageSource>(
context: context, context: context,
@@ -752,12 +754,12 @@ Future<void> _pickAndShowDishResult(
children: [ children: [
ListTile( ListTile(
leading: const Icon(Icons.camera_alt), leading: const Icon(Icons.camera_alt),
title: const Text('Камера'), title: Text(l10n.camera),
onTap: () => Navigator.pop(context, ImageSource.camera), onTap: () => Navigator.pop(context, ImageSource.camera),
), ),
ListTile( ListTile(
leading: const Icon(Icons.photo_library), leading: const Icon(Icons.photo_library),
title: const Text('Галерея'), title: Text(l10n.gallery),
onTap: () => Navigator.pop(context, ImageSource.gallery), onTap: () => Navigator.pop(context, ImageSource.gallery),
), ),
], ],
@@ -778,7 +780,7 @@ Future<void> _pickAndShowDishResult(
// 3. Show progress dialog. // 3. Show progress dialog.
// Capture root navigator before await to avoid GoRouter inner-navigator issues. // Capture root navigator before await to avoid GoRouter inner-navigator issues.
final rootNavigator = Navigator.of(context, rootNavigator: true); final rootNavigator = Navigator.of(context, rootNavigator: true);
final progressNotifier = _DishProgressNotifier(); final progressNotifier = _DishProgressNotifier(initialMessage: l10n.analyzingPhoto);
showDialog( showDialog(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
@@ -809,11 +811,11 @@ Future<void> _pickAndShowDishResult(
switch (event) { switch (event) {
case DishJobQueued(): case DishJobQueued():
progressNotifier.update( progressNotifier.update(
message: 'Вы в очереди #${event.position + 1} · ~${event.estimatedSeconds} сек', message: '${l10n.inQueue} · ${l10n.queuePosition(event.position + 1)}',
showUpgrade: event.position > 0, showUpgrade: event.position > 0,
); );
case DishJobProcessing(): case DishJobProcessing():
progressNotifier.update(message: 'Обрабатываем...'); progressNotifier.update(message: l10n.processing);
case DishJobDone(): case DishJobDone():
rootNavigator.pop(); // close dialog rootNavigator.pop(); // close dialog
if (!context.mounted) return; if (!context.mounted) return;
@@ -825,6 +827,7 @@ Future<void> _pickAndShowDishResult(
dish: event.result, dish: event.result,
preselectedMealType: mealTypeId.isNotEmpty ? mealTypeId : null, preselectedMealType: mealTypeId.isNotEmpty ? mealTypeId : null,
jobId: jobCreated.jobId, jobId: jobCreated.jobId,
targetDate: targetDate,
onAdded: () => Navigator.pop(sheetContext), onAdded: () => Navigator.pop(sheetContext),
), ),
); );
@@ -836,7 +839,7 @@ Future<void> _pickAndShowDishResult(
SnackBar( SnackBar(
content: Text(event.error), content: Text(event.error),
action: SnackBarAction( action: SnackBarAction(
label: 'Повторить', label: l10n.retry,
onPressed: () => _pickAndShowDishResult(context, ref, mealTypeId), onPressed: () => _pickAndShowDishResult(context, ref, mealTypeId),
), ),
), ),
@@ -849,9 +852,7 @@ Future<void> _pickAndShowDishResult(
if (context.mounted) { if (context.mounted) {
rootNavigator.pop(); // close dialog rootNavigator.pop(); // close dialog
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(content: Text(l10n.recognitionFailed)),
content: Text('Не удалось распознать. Попробуйте ещё раз.'),
),
); );
} }
} }
@@ -872,7 +873,10 @@ class _DishProgressState {
} }
class _DishProgressNotifier extends ChangeNotifier { class _DishProgressNotifier extends ChangeNotifier {
_DishProgressState _state = const _DishProgressState(message: 'Анализируем фото...'); late _DishProgressState _state;
_DishProgressNotifier({required String initialMessage})
: _state = _DishProgressState(message: initialMessage);
_DishProgressState get state => _state; _DishProgressState get state => _state;
@@ -889,6 +893,7 @@ class _DishProgressDialog extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return ListenableBuilder( return ListenableBuilder(
listenable: notifier, listenable: notifier,
builder: (context, _) { builder: (context, _) {
@@ -903,7 +908,7 @@ class _DishProgressDialog extends StatelessWidget {
if (state.showUpgrade) ...[ if (state.showUpgrade) ...[
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
'Хотите без очереди? Upgrade →', l10n.upgradePrompt,
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
@@ -931,6 +936,7 @@ class _MealCard extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context); final theme = Theme.of(context);
final totalCalories = entries.fold<double>( final totalCalories = entries.fold<double>(
0.0, (sum, entry) => sum + (entry.calories ?? 0)); 0.0, (sum, entry) => sum + (entry.calories ?? 0));
@@ -946,12 +952,12 @@ class _MealCard extends ConsumerWidget {
Text(mealTypeOption.emoji, Text(mealTypeOption.emoji,
style: const TextStyle(fontSize: 20)), style: const TextStyle(fontSize: 20)),
const SizedBox(width: 8), const SizedBox(width: 8),
Text(mealTypeOption.label, Text(mealTypeLabel(mealTypeOption.id, l10n),
style: theme.textTheme.titleSmall), style: theme.textTheme.titleSmall),
const Spacer(), const Spacer(),
if (totalCalories > 0) if (totalCalories > 0)
Text( Text(
'${totalCalories.toInt()} ккал', '${totalCalories.toInt()} ${l10n.caloriesUnit}',
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
@@ -959,7 +965,7 @@ class _MealCard extends ConsumerWidget {
IconButton( IconButton(
icon: const Icon(Icons.add, size: 20), icon: const Icon(Icons.add, size: 20),
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
tooltip: 'Добавить блюдо', tooltip: l10n.addDish,
onPressed: () => _pickAndShowDishResult( onPressed: () => _pickAndShowDishResult(
context, ref, mealTypeOption.id), context, ref, mealTypeOption.id),
), ),
@@ -990,6 +996,7 @@ class _DiaryEntryTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context); final theme = Theme.of(context);
final calories = entry.calories?.toInt(); final calories = entry.calories?.toInt();
final hasProtein = (entry.proteinG ?? 0) > 0; final hasProtein = (entry.proteinG ?? 0) > 0;
@@ -1016,7 +1023,7 @@ class _DiaryEntryTile extends StatelessWidget {
children: [ children: [
if (calories != null) if (calories != null)
Text( Text(
'$calories ккал', '$calories ${l10n.caloriesUnit}',
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
@@ -1095,6 +1102,7 @@ class _TodayJobsWidget extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context); final theme = Theme.of(context);
final visibleJobs = jobs.take(3).toList(); final visibleJobs = jobs.take(3).toList();
final hasMore = jobs.length > 3; final hasMore = jobs.length > 3;
@@ -1104,7 +1112,7 @@ class _TodayJobsWidget extends ConsumerWidget {
children: [ children: [
Row( Row(
children: [ children: [
Text('Распознавания', style: theme.textTheme.titleSmall), Text(l10n.dishRecognition, style: theme.textTheme.titleSmall),
const Spacer(), const Spacer(),
if (hasMore) if (hasMore)
TextButton( TextButton(
@@ -1114,7 +1122,7 @@ class _TodayJobsWidget extends ConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
), ),
child: Text( child: Text(
'Все', l10n.all,
style: theme.textTheme.labelMedium?.copyWith( style: theme.textTheme.labelMedium?.copyWith(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
), ),
@@ -1139,6 +1147,7 @@ class _JobTile extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context); final theme = Theme.of(context);
final isDone = job.status == 'done'; final isDone = job.status == 'done';
@@ -1162,13 +1171,13 @@ class _JobTile extends ConsumerWidget {
final dishName = job.result?.candidates.isNotEmpty == true final dishName = job.result?.candidates.isNotEmpty == true
? job.result!.best.dishName ? job.result!.best.dishName
: null; : null;
final subtitle = dishName ?? (isFailed ? (job.error ?? 'Ошибка') : 'Обрабатывается…'); final subtitle = dishName ?? (isFailed ? (job.error ?? l10n.recognitionError) : l10n.recognizing);
return Card( return Card(
child: ListTile( child: ListTile(
leading: Icon(statusIcon, color: statusColor), leading: Icon(statusIcon, color: statusColor),
title: Text( title: Text(
dishName ?? (isProcessing ? 'Распознаётся…' : 'Ошибка'), dishName ?? (isProcessing ? l10n.recognizing : l10n.recognitionError),
style: theme.textTheme.bodyMedium, style: theme.textTheme.bodyMedium,
), ),
subtitle: Text( subtitle: Text(
@@ -1193,6 +1202,8 @@ class _JobTile extends ConsumerWidget {
dish: job.result!, dish: job.result!,
preselectedMealType: job.targetMealType, preselectedMealType: job.targetMealType,
jobId: job.id, jobId: job.id,
targetDate: job.targetDate,
createdAt: job.createdAt,
onAdded: () => Navigator.pop(sheetContext), onAdded: () => Navigator.pop(sheetContext),
), ),
); );
@@ -1210,12 +1221,13 @@ class _QuickActionsRow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Row( return Row(
children: [ children: [
Expanded( Expanded(
child: _ActionButton( child: _ActionButton(
icon: Icons.document_scanner_outlined, icon: Icons.document_scanner_outlined,
label: 'Сканировать', label: l10n.scanDish,
onTap: () => context.push('/scan'), onTap: () => context.push('/scan'),
), ),
), ),
@@ -1223,7 +1235,7 @@ class _QuickActionsRow extends StatelessWidget {
Expanded( Expanded(
child: _ActionButton( child: _ActionButton(
icon: Icons.calendar_month_outlined, icon: Icons.calendar_month_outlined,
label: 'Меню', label: l10n.menu,
onTap: () => context.push('/menu'), onTap: () => context.push('/menu'),
), ),
), ),
@@ -1231,7 +1243,7 @@ class _QuickActionsRow extends StatelessWidget {
Expanded( Expanded(
child: _ActionButton( child: _ActionButton(
icon: Icons.history, icon: Icons.history,
label: 'История', label: l10n.dishHistory,
onTap: () => context.push('/scan/history'), onTap: () => context.push('/scan/history'),
), ),
), ),
@@ -1309,6 +1321,7 @@ class _RecipeCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context); final theme = Theme.of(context);
return SizedBox( return SizedBox(
@@ -1343,7 +1356,7 @@ class _RecipeCard extends StatelessWidget {
Padding( Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 8, 6), padding: const EdgeInsets.fromLTRB(8, 0, 8, 6),
child: Text( child: Text(
'${recipe.calories!.toInt()} ккал', '${recipe.calories!.toInt()} ${l10n.caloriesUnit}',
style: theme.textTheme.labelSmall?.copyWith( style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant), color: theme.colorScheme.onSurfaceVariant),
), ),

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:food_ai/l10n/app_localizations.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors.dart';
@@ -1151,6 +1152,7 @@ class _MealTypesStepContent extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context); final theme = Theme.of(context);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -1178,7 +1180,7 @@ class _MealTypesStepContent extends StatelessWidget {
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Text( child: Text(
mealTypeOption.label, mealTypeLabel(mealTypeOption.id, l10n),
style: theme.textTheme.bodyLarge?.copyWith( style: theme.textTheme.bodyLarge?.copyWith(
color: isSelected color: isSelected
? accentColor ? accentColor

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:food_ai/l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/auth/auth_provider.dart'; import '../../core/auth/auth_provider.dart';
@@ -15,16 +16,17 @@ class ProfileScreen extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final state = ref.watch(profileProvider); final state = ref.watch(profileProvider);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Профиль'), title: Text(l10n.profileTitle),
actions: [ actions: [
if (state.hasValue) if (state.hasValue)
TextButton( TextButton(
onPressed: () => _openEdit(context, state.value!), onPressed: () => _openEdit(context, state.value!),
child: const Text('Изменить'), child: Text(l10n.edit),
), ),
], ],
), ),
@@ -33,7 +35,7 @@ class ProfileScreen extends ConsumerWidget {
error: (_, __) => Center( error: (_, __) => Center(
child: FilledButton( child: FilledButton(
onPressed: () => ref.read(profileProvider.notifier).load(), onPressed: () => ref.read(profileProvider.notifier).load(),
child: const Text('Повторить'), child: Text(l10n.retry),
), ),
), ),
data: (user) => _ProfileBody(user: user), data: (user) => _ProfileBody(user: user),
@@ -58,6 +60,28 @@ class _ProfileBody extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
String? genderLabel(String? gender) => switch (gender) {
'male' => l10n.genderMale,
'female' => l10n.genderFemale,
_ => null,
};
String? goalLabel(String? goal) => switch (goal) {
'lose' => l10n.goalLoss,
'maintain' => l10n.goalMaintain,
'gain' => l10n.goalGain,
_ => null,
};
String? activityLabel(String? activity) => switch (activity) {
'low' => l10n.activityLow,
'moderate' => l10n.activityMedium,
'high' => l10n.activityHigh,
_ => null,
};
return ListView( return ListView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 32), padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
children: [ children: [
@@ -65,50 +89,49 @@ class _ProfileBody extends ConsumerWidget {
const SizedBox(height: 24), const SizedBox(height: 24),
// Body params // Body params
_SectionLabel('ПАРАМЕТРЫ ТЕЛА'), _SectionLabel(l10n.bodyParams),
const SizedBox(height: 6), const SizedBox(height: 6),
_InfoCard(children: [ _InfoCard(children: [
_InfoRow('Рост', user.heightCm != null ? '${user.heightCm} см' : null), _InfoRow(l10n.height, user.heightCm != null ? '${user.heightCm} см' : null),
const Divider(height: 1, indent: 16), const Divider(height: 1, indent: 16),
_InfoRow('Вес', _InfoRow(l10n.weight,
user.weightKg != null ? '${_fmt(user.weightKg!)} кг' : null), user.weightKg != null ? '${_fmt(user.weightKg!)} кг' : null),
const Divider(height: 1, indent: 16), const Divider(height: 1, indent: 16),
_InfoRow('Возраст', user.age != null ? '${user.age} лет' : null), _InfoRow(l10n.age, user.age != null ? '${user.age}' : null),
const Divider(height: 1, indent: 16), const Divider(height: 1, indent: 16),
_InfoRow('Пол', _genderLabel(user.gender)), _InfoRow(l10n.gender, genderLabel(user.gender)),
]), ]),
if (user.heightCm == null && user.weightKg == null) ...[ if (user.heightCm == null && user.weightKg == null) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
_HintBanner('Укажите параметры тела для расчёта нормы калорий'), _HintBanner(l10n.calorieHint),
], ],
const SizedBox(height: 16), const SizedBox(height: 16),
// Goal & activity // Goal & activity
_SectionLabel('ЦЕЛЬ И АКТИВНОСТЬ'), _SectionLabel(l10n.goalActivity),
const SizedBox(height: 6), const SizedBox(height: 6),
_InfoCard(children: [ _InfoCard(children: [
_InfoRow('Цель', _goalLabel(user.goal)), _InfoRow(l10n.goalLabel.replaceAll(':', '').trim(), goalLabel(user.goal)),
const Divider(height: 1, indent: 16), const Divider(height: 1, indent: 16),
_InfoRow('Активность', _activityLabel(user.activity)), _InfoRow('Активность', activityLabel(user.activity)),
]), ]),
const SizedBox(height: 16), const SizedBox(height: 16),
// Calories + meal types // Calories + meal types
_SectionLabel('ПИТАНИЕ'), _SectionLabel(l10n.nutrition),
const SizedBox(height: 6), const SizedBox(height: 6),
_InfoCard(children: [ _InfoCard(children: [
_InfoRow( _InfoRow(
'Норма калорий', l10n.calorieGoal,
user.dailyCalories != null user.dailyCalories != null
? '${user.dailyCalories} ккал/день' ? '${user.dailyCalories} ${l10n.caloriesUnit}/день'
: null, : null,
), ),
const Divider(height: 1, indent: 16), const Divider(height: 1, indent: 16),
_InfoRow( _InfoRow(
'Приёмы пищи', l10n.mealTypes,
user.mealTypes user.mealTypes
.map((mealTypeId) => .map((mealTypeId) => mealTypeLabel(mealTypeId, l10n))
mealTypeById(mealTypeId)?.label ?? mealTypeId)
.join(', '), .join(', '),
), ),
]), ]),
@@ -117,7 +140,7 @@ class _ProfileBody extends ConsumerWidget {
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(horizontal: 4),
child: Text( child: Text(
'Рассчитано по формуле Миффлина-Сан Жеора', l10n.formulaNote,
style: Theme.of(context).textTheme.labelSmall?.copyWith( style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: AppColors.textSecondary, color: AppColors.textSecondary,
), ),
@@ -127,11 +150,11 @@ class _ProfileBody extends ConsumerWidget {
const SizedBox(height: 16), const SizedBox(height: 16),
// Settings // Settings
_SectionLabel('НАСТРОЙКИ'), _SectionLabel(l10n.settings),
const SizedBox(height: 6), const SizedBox(height: 6),
_InfoCard(children: [ _InfoCard(children: [
_InfoRow( _InfoRow(
'Язык', l10n.language,
ref.watch(supportedLanguagesProvider).valueOrNull?[ ref.watch(supportedLanguagesProvider).valueOrNull?[
user.preferences['language'] as String? ?? 'ru'] ?? user.preferences['language'] as String? ?? 'ru'] ??
'Русский', 'Русский',
@@ -144,28 +167,10 @@ class _ProfileBody extends ConsumerWidget {
); );
} }
static String _fmt(double w) => static String _fmt(double weight) =>
w == w.truncateToDouble() ? w.toInt().toString() : w.toStringAsFixed(1); weight == weight.truncateToDouble()
? weight.toInt().toString()
static String? _genderLabel(String? g) => switch (g) { : weight.toStringAsFixed(1);
'male' => 'Мужской',
'female' => 'Женский',
_ => null,
};
static String? _goalLabel(String? g) => switch (g) {
'lose' => 'Похудение',
'maintain' => 'Поддержание',
'gain' => 'Набор массы',
_ => null,
};
static String? _activityLabel(String? a) => switch (a) {
'low' => 'Низкая',
'moderate' => 'Средняя',
'high' => 'Высокая',
_ => null,
};
} }
// ── Header ──────────────────────────────────────────────────── // ── Header ────────────────────────────────────────────────────
@@ -241,6 +246,7 @@ class _InfoRow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context); final theme = Theme.of(context);
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
@@ -249,7 +255,7 @@ class _InfoRow extends StatelessWidget {
Text(label, style: theme.textTheme.bodyMedium), Text(label, style: theme.textTheme.bodyMedium),
const Spacer(), const Spacer(),
Text( Text(
value ?? 'Не задано', value ?? l10n.notSet,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
color: value != null color: value != null
? AppColors.textPrimary ? AppColors.textPrimary
@@ -296,31 +302,33 @@ class _HintBanner extends StatelessWidget {
class _LogoutButton extends ConsumerWidget { class _LogoutButton extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
return OutlinedButton( return OutlinedButton(
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: AppColors.error, foregroundColor: AppColors.error,
side: const BorderSide(color: AppColors.error), side: const BorderSide(color: AppColors.error),
), ),
onPressed: () => _confirmLogout(context, ref), onPressed: () => _confirmLogout(context, ref),
child: const Text('Выйти из аккаунта'), child: Text(l10n.logout),
); );
} }
Future<void> _confirmLogout(BuildContext context, WidgetRef ref) async { Future<void> _confirmLogout(BuildContext context, WidgetRef ref) async {
final l10n = AppLocalizations.of(context)!;
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (dialogContext) => AlertDialog(
title: const Text('Выйти из аккаунта?'), title: Text('${l10n.logout}?'),
content: const Text('Вы будете перенаправлены на экран входа.'), content: const Text('Вы будете перенаправлены на экран входа.'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(ctx).pop(false), onPressed: () => Navigator.of(dialogContext).pop(false),
child: const Text('Отмена'), child: Text(l10n.cancel),
), ),
TextButton( TextButton(
style: TextButton.styleFrom(foregroundColor: AppColors.error), style: TextButton.styleFrom(foregroundColor: AppColors.error),
onPressed: () => Navigator.of(ctx).pop(true), onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('Выйти'), child: Text(l10n.logout),
), ),
], ],
), ),
@@ -343,9 +351,9 @@ class EditProfileSheet extends ConsumerStatefulWidget {
class _EditProfileSheetState extends ConsumerState<EditProfileSheet> { class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
late final TextEditingController _nameCtrl; late final TextEditingController _nameController;
late final TextEditingController _heightCtrl; late final TextEditingController _heightController;
late final TextEditingController _weightCtrl; late final TextEditingController _weightController;
DateTime? _selectedDob; DateTime? _selectedDob;
String? _gender; String? _gender;
String? _goal; String? _goal;
@@ -357,30 +365,32 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final u = widget.user; final user = widget.user;
_nameCtrl = TextEditingController(text: u.name); _nameController = TextEditingController(text: user.name);
_heightCtrl = TextEditingController(text: u.heightCm?.toString() ?? ''); _heightController = TextEditingController(text: user.heightCm?.toString() ?? '');
_weightCtrl = TextEditingController( _weightController = TextEditingController(
text: u.weightKg != null ? _fmt(u.weightKg!) : ''); text: user.weightKg != null ? _fmt(user.weightKg!) : '');
_selectedDob = _selectedDob =
u.dateOfBirth != null ? DateTime.tryParse(u.dateOfBirth!) : null; user.dateOfBirth != null ? DateTime.tryParse(user.dateOfBirth!) : null;
_gender = u.gender; _gender = user.gender;
_goal = u.goal; _goal = user.goal;
_activity = u.activity; _activity = user.activity;
_language = u.preferences['language'] as String? ?? 'ru'; _language = user.preferences['language'] as String? ?? 'ru';
_mealTypes = List<String>.from(u.mealTypes); _mealTypes = List<String>.from(user.mealTypes);
} }
@override @override
void dispose() { void dispose() {
_nameCtrl.dispose(); _nameController.dispose();
_heightCtrl.dispose(); _heightController.dispose();
_weightCtrl.dispose(); _weightController.dispose();
super.dispose(); super.dispose();
} }
String _fmt(double w) => String _fmt(double weight) =>
w == w.truncateToDouble() ? w.toInt().toString() : w.toStringAsFixed(1); weight == weight.truncateToDouble()
? weight.toInt().toString()
: weight.toStringAsFixed(1);
Future<void> _pickDob() async { Future<void> _pickDob() async {
final now = DateTime.now(); final now = DateTime.now();
@@ -395,13 +405,14 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
} }
Future<void> _save() async { Future<void> _save() async {
final l10n = AppLocalizations.of(context)!;
if (!_formKey.currentState!.validate()) return; if (!_formKey.currentState!.validate()) return;
setState(() => _saving = true); setState(() => _saving = true);
final req = UpdateProfileRequest( final request = UpdateProfileRequest(
name: _nameCtrl.text.trim(), name: _nameController.text.trim(),
heightCm: int.tryParse(_heightCtrl.text), heightCm: int.tryParse(_heightController.text),
weightKg: double.tryParse(_weightCtrl.text), weightKg: double.tryParse(_weightController.text),
dateOfBirth: _selectedDob != null dateOfBirth: _selectedDob != null
? '${_selectedDob!.year.toString().padLeft(4, '0')}-' ? '${_selectedDob!.year.toString().padLeft(4, '0')}-'
'${_selectedDob!.month.toString().padLeft(2, '0')}-' '${_selectedDob!.month.toString().padLeft(2, '0')}-'
@@ -414,7 +425,7 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
mealTypes: _mealTypes, mealTypes: _mealTypes,
); );
final ok = await ref.read(profileProvider.notifier).update(req); final ok = await ref.read(profileProvider.notifier).update(request);
if (!mounted) return; if (!mounted) return;
setState(() => _saving = false); setState(() => _saving = false);
@@ -422,17 +433,18 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
if (ok) { if (ok) {
Navigator.of(context).pop(); Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Профиль обновлён')), SnackBar(content: Text(l10n.profileUpdated)),
); );
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Не удалось сохранить')), SnackBar(content: Text(l10n.profileSaveFailed)),
); );
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context); final theme = Theme.of(context);
final bottomInset = MediaQuery.viewInsetsOf(context).bottom; final bottomInset = MediaQuery.viewInsetsOf(context).bottom;
@@ -459,12 +471,11 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row( child: Row(
children: [ children: [
Text('Редактировать профиль', Text(l10n.editProfile, style: theme.textTheme.titleMedium),
style: theme.textTheme.titleMedium),
const Spacer(), const Spacer(),
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: const Text('Отмена'), child: Text(l10n.cancel),
), ),
], ],
), ),
@@ -479,11 +490,13 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
children: [ children: [
// Name // Name
TextFormField( TextFormField(
controller: _nameCtrl, controller: _nameController,
decoration: const InputDecoration(labelText: 'Имя'), decoration: InputDecoration(labelText: l10n.nameLabel),
textCapitalization: TextCapitalization.words, textCapitalization: TextCapitalization.words,
validator: (v) => validator: (value) =>
(v == null || v.trim().isEmpty) ? 'Введите имя' : null, (value == null || value.trim().isEmpty)
? l10n.nameRequired
: null,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@@ -492,17 +505,16 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
children: [ children: [
Expanded( Expanded(
child: TextFormField( child: TextFormField(
controller: _heightCtrl, controller: _heightController,
decoration: decoration: InputDecoration(labelText: l10n.heightCm),
const InputDecoration(labelText: 'Рост (см)'),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
inputFormatters: [ inputFormatters: [
FilteringTextInputFormatter.digitsOnly FilteringTextInputFormatter.digitsOnly
], ],
validator: (v) { validator: (value) {
if (v == null || v.isEmpty) return null; if (value == null || value.isEmpty) return null;
final n = int.tryParse(v); final number = int.tryParse(value);
if (n == null || n < 100 || n > 250) { if (number == null || number < 100 || number > 250) {
return '100250'; return '100250';
} }
return null; return null;
@@ -512,19 +524,18 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: TextFormField( child: TextFormField(
controller: _weightCtrl, controller: _weightController,
decoration: decoration: InputDecoration(labelText: l10n.weightKg),
const InputDecoration(labelText: 'Вес (кг)'),
keyboardType: const TextInputType.numberWithOptions( keyboardType: const TextInputType.numberWithOptions(
decimal: true), decimal: true),
inputFormatters: [ inputFormatters: [
FilteringTextInputFormatter.allow( FilteringTextInputFormatter.allow(
RegExp(r'[0-9.]')) RegExp(r'[0-9.]'))
], ],
validator: (v) { validator: (value) {
if (v == null || v.isEmpty) return null; if (value == null || value.isEmpty) return null;
final n = double.tryParse(v); final number = double.tryParse(value);
if (n == null || n < 30 || n > 300) { if (number == null || number < 30 || number > 300) {
return '30300'; return '30300';
} }
return null; return null;
@@ -539,14 +550,13 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
InkWell( InkWell(
onTap: _pickDob, onTap: _pickDob,
child: InputDecorator( child: InputDecorator(
decoration: decoration: InputDecoration(labelText: l10n.birthDate),
const InputDecoration(labelText: 'Дата рождения'),
child: Text( child: Text(
_selectedDob != null _selectedDob != null
? '${_selectedDob!.day.toString().padLeft(2, '0')}.' ? '${_selectedDob!.day.toString().padLeft(2, '0')}.'
'${_selectedDob!.month.toString().padLeft(2, '0')}.' '${_selectedDob!.month.toString().padLeft(2, '0')}.'
'${_selectedDob!.year}' '${_selectedDob!.year}'
: 'Не задано', : l10n.notSet,
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
), ),
), ),
@@ -554,34 +564,34 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
const SizedBox(height: 20), const SizedBox(height: 20),
// Gender // Gender
Text('Пол', style: theme.textTheme.labelMedium), Text(l10n.gender, style: theme.textTheme.labelMedium),
const SizedBox(height: 8), const SizedBox(height: 8),
SegmentedButton<String>( SegmentedButton<String>(
segments: const [ segments: [
ButtonSegment(value: 'male', label: Text('Мужской')), ButtonSegment(value: 'male', label: Text(l10n.genderMale)),
ButtonSegment(value: 'female', label: Text('Женский')), ButtonSegment(value: 'female', label: Text(l10n.genderFemale)),
], ],
selected: _gender != null ? {_gender!} : const {}, selected: _gender != null ? {_gender!} : const {},
emptySelectionAllowed: true, emptySelectionAllowed: true,
onSelectionChanged: (s) => onSelectionChanged: (selection) =>
setState(() => _gender = s.isEmpty ? null : s.first), setState(() => _gender = selection.isEmpty ? null : selection.first),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// Goal // Goal
Text('Цель', style: theme.textTheme.labelMedium), Text(l10n.goalLabel.replaceAll(':', '').trim(),
style: theme.textTheme.labelMedium),
const SizedBox(height: 8), const SizedBox(height: 8),
SegmentedButton<String>( SegmentedButton<String>(
segments: const [ segments: [
ButtonSegment(value: 'lose', label: Text('Похудение')), ButtonSegment(value: 'lose', label: Text(l10n.goalLoss)),
ButtonSegment( ButtonSegment(value: 'maintain', label: Text(l10n.goalMaintain)),
value: 'maintain', label: Text('Поддержание')), ButtonSegment(value: 'gain', label: Text(l10n.goalGain)),
ButtonSegment(value: 'gain', label: Text('Набор')),
], ],
selected: _goal != null ? {_goal!} : const {}, selected: _goal != null ? {_goal!} : const {},
emptySelectionAllowed: true, emptySelectionAllowed: true,
onSelectionChanged: (s) => onSelectionChanged: (selection) =>
setState(() => _goal = s.isEmpty ? null : s.first), setState(() => _goal = selection.isEmpty ? null : selection.first),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
@@ -589,21 +599,20 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
Text('Активность', style: theme.textTheme.labelMedium), Text('Активность', style: theme.textTheme.labelMedium),
const SizedBox(height: 8), const SizedBox(height: 8),
SegmentedButton<String>( SegmentedButton<String>(
segments: const [ segments: [
ButtonSegment(value: 'low', label: Text('Низкая')), ButtonSegment(value: 'low', label: Text(l10n.activityLow)),
ButtonSegment( ButtonSegment(value: 'moderate', label: Text(l10n.activityMedium)),
value: 'moderate', label: Text('Средняя')), ButtonSegment(value: 'high', label: Text(l10n.activityHigh)),
ButtonSegment(value: 'high', label: Text('Высокая')),
], ],
selected: _activity != null ? {_activity!} : const {}, selected: _activity != null ? {_activity!} : const {},
emptySelectionAllowed: true, emptySelectionAllowed: true,
onSelectionChanged: (s) => setState( onSelectionChanged: (selection) => setState(
() => _activity = s.isEmpty ? null : s.first), () => _activity = selection.isEmpty ? null : selection.first),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// Meal types // Meal types
Text('Приёмы пищи', style: theme.textTheme.labelMedium), Text(l10n.mealTypes, style: theme.textTheme.labelMedium),
const SizedBox(height: 8), const SizedBox(height: 8),
Wrap( Wrap(
spacing: 8, spacing: 8,
@@ -613,7 +622,7 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
_mealTypes.contains(mealTypeOption.id); _mealTypes.contains(mealTypeOption.id);
return FilterChip( return FilterChip(
label: Text( label: Text(
'${mealTypeOption.emoji} ${mealTypeOption.label}'), '${mealTypeOption.emoji} ${mealTypeLabel(mealTypeOption.id, l10n)}'),
selected: isSelected, selected: isSelected,
onSelected: (selected) { onSelected: (selected) {
setState(() { setState(() {
@@ -632,21 +641,20 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
// Language // Language
ref.watch(supportedLanguagesProvider).when( ref.watch(supportedLanguagesProvider).when(
data: (languages) => DropdownButtonFormField<String>( data: (languages) => DropdownButtonFormField<String>(
decoration: const InputDecoration( decoration: InputDecoration(labelText: l10n.language),
labelText: 'Язык интерфейса'),
initialValue: _language, initialValue: _language,
items: languages.entries items: languages.entries
.map((e) => DropdownMenuItem( .map((entry) => DropdownMenuItem(
value: e.key, value: entry.key,
child: Text(e.value), child: Text(entry.value),
)) ))
.toList(), .toList(),
onChanged: (v) => setState(() => _language = v), onChanged: (value) => setState(() => _language = value),
), ),
loading: () => const Center( loading: () => const Center(
child: CircularProgressIndicator()), child: CircularProgressIndicator()),
error: (_, __) => error: (_, __) =>
const Text('Не удалось загрузить языки'), Text(l10n.historyLoadError),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
@@ -660,7 +668,7 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white), strokeWidth: 2, color: Colors.white),
) )
: const Text('Сохранить'), : Text(l10n.save),
), ),
], ],
), ),

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:food_ai/l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/storage/local_preferences_provider.dart'; import '../../core/storage/local_preferences_provider.dart';
@@ -17,6 +18,8 @@ class DishResultSheet extends ConsumerStatefulWidget {
required this.onAdded, required this.onAdded,
this.preselectedMealType, this.preselectedMealType,
this.jobId, this.jobId,
this.targetDate,
this.createdAt,
}); });
final DishResult dish; final DishResult dish;
@@ -24,6 +27,13 @@ class DishResultSheet extends ConsumerStatefulWidget {
final String? preselectedMealType; final String? preselectedMealType;
final String? jobId; final String? jobId;
/// The diary date to add the entry to (YYYY-MM-DD).
/// Falls back to [createdAt] date, then today.
final String? targetDate;
/// Job creation timestamp used as fallback date when [targetDate] is null.
final DateTime? createdAt;
@override @override
ConsumerState<DishResultSheet> createState() => _DishResultSheetState(); ConsumerState<DishResultSheet> createState() => _DishResultSheetState();
} }
@@ -32,6 +42,7 @@ class _DishResultSheetState extends ConsumerState<DishResultSheet> {
late int _selectedIndex; late int _selectedIndex;
late int _portionGrams; late int _portionGrams;
late String _mealType; late String _mealType;
late DateTime _selectedDiaryDate;
bool _saving = false; bool _saving = false;
final TextEditingController _portionController = TextEditingController(); final TextEditingController _portionController = TextEditingController();
@@ -43,9 +54,19 @@ class _DishResultSheetState extends ConsumerState<DishResultSheet> {
_portionGrams = widget.dish.candidates.isNotEmpty _portionGrams = widget.dish.candidates.isNotEmpty
? widget.dish.candidates.first.weightGrams ? widget.dish.candidates.first.weightGrams
: 300; : 300;
_mealType = widget.preselectedMealType ?? _mealType = widget.preselectedMealType ?? kAllMealTypes.first.id;
kAllMealTypes.first.id;
_portionController.text = '$_portionGrams'; _portionController.text = '$_portionGrams';
if (widget.targetDate != null) {
final parts = widget.targetDate!.split('-');
_selectedDiaryDate = DateTime(
int.parse(parts[0]), int.parse(parts[1]), int.parse(parts[2]),
);
} else if (widget.createdAt != null) {
final createdAtDate = widget.createdAt!;
_selectedDiaryDate = DateTime(createdAtDate.year, createdAtDate.month, createdAtDate.day);
} else {
_selectedDiaryDate = DateTime.now();
}
} }
@override @override
@@ -79,6 +100,17 @@ class _DishResultSheetState extends ConsumerState<DishResultSheet> {
}); });
} }
Future<void> _pickDate() async {
final now = DateTime.now();
final pickedDate = await showDatePicker(
context: context,
initialDate: _selectedDiaryDate,
firstDate: now.subtract(const Duration(days: 365)),
lastDate: now,
);
if (pickedDate != null) setState(() => _selectedDiaryDate = pickedDate);
}
void _onPortionEdited(String value) { void _onPortionEdited(String value) {
final parsed = int.tryParse(value); final parsed = int.tryParse(value);
if (parsed != null && parsed >= 10) { if (parsed != null && parsed >= 10) {
@@ -90,8 +122,8 @@ class _DishResultSheetState extends ConsumerState<DishResultSheet> {
if (_saving) return; if (_saving) return;
setState(() => _saving = true); setState(() => _saving = true);
final selectedDate = ref.read(selectedDateProvider); final l10n = AppLocalizations.of(context)!;
final dateString = formatDateForDiary(selectedDate); final dateString = formatDateForDiary(_selectedDiaryDate);
final scaledCalories = _scale(_selected.calories); final scaledCalories = _scale(_selected.calories);
final scaledProtein = _scale(_selected.proteinG); final scaledProtein = _scale(_selected.proteinG);
@@ -120,7 +152,7 @@ class _DishResultSheetState extends ConsumerState<DishResultSheet> {
if (mounted) { if (mounted) {
setState(() => _saving = false); setState(() => _saving = false);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Не удалось добавить. Попробуйте ещё раз.')), SnackBar(content: Text(l10n.addFailed)),
); );
} }
} }
@@ -128,6 +160,7 @@ class _DishResultSheetState extends ConsumerState<DishResultSheet> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context); final theme = Theme.of(context);
final hasCandidates = widget.dish.candidates.isNotEmpty; final hasCandidates = widget.dish.candidates.isNotEmpty;
@@ -150,7 +183,7 @@ class _DishResultSheetState extends ConsumerState<DishResultSheet> {
padding: const EdgeInsets.fromLTRB(20, 0, 8, 0), padding: const EdgeInsets.fromLTRB(20, 0, 8, 0),
child: Row( child: Row(
children: [ children: [
Text('Распознано блюдо', style: theme.textTheme.titleMedium), Text(l10n.dishResultTitle, style: theme.textTheme.titleMedium),
const Spacer(), const Spacer(),
IconButton( IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
@@ -179,7 +212,7 @@ class _DishResultSheetState extends ConsumerState<DishResultSheet> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'КБЖУ приблизительные — определены по фото.', l10n.nutritionApproximate,
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
@@ -199,6 +232,11 @@ class _DishResultSheetState extends ConsumerState<DishResultSheet> {
if (value != null) setState(() => _mealType = value); if (value != null) setState(() => _mealType = value);
}, },
), ),
const SizedBox(height: 20),
_DatePickerRow(
date: _selectedDiaryDate,
onTap: _pickDate,
),
const SizedBox(height: 16), const SizedBox(height: 16),
], ],
) )
@@ -207,13 +245,13 @@ class _DishResultSheetState extends ConsumerState<DishResultSheet> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
'Блюдо не распознано', l10n.dishNotRecognized,
style: theme.textTheme.titleMedium, style: theme.textTheme.titleMedium,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
FilledButton( FilledButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('Попробовать снова'), child: Text(l10n.tryAgain),
), ),
], ],
), ),
@@ -232,7 +270,7 @@ class _DishResultSheetState extends ConsumerState<DishResultSheet> {
width: 20, width: 20,
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(strokeWidth: 2),
) )
: const Text('Добавить в журнал'), : Text(l10n.addToJournal),
), ),
), ),
), ),
@@ -241,6 +279,47 @@ class _DishResultSheetState extends ConsumerState<DishResultSheet> {
} }
} }
// ---------------------------------------------------------------------------
// Date picker row
// ---------------------------------------------------------------------------
class _DatePickerRow extends StatelessWidget {
final DateTime date;
final VoidCallback onTap;
const _DatePickerRow({required this.date, required this.onTap});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final target = DateTime(date.year, date.month, date.day);
final String label;
if (target == today) {
label = l10n.today;
} else if (target == today.subtract(const Duration(days: 1))) {
label = l10n.yesterday;
} else {
label = '${date.day.toString().padLeft(2, '0')}.${date.month.toString().padLeft(2, '0')}.${date.year}';
}
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: InputDecorator(
decoration: InputDecoration(
labelText: l10n.dateLabel,
border: const OutlineInputBorder(),
suffixIcon: const Icon(Icons.calendar_today_outlined),
),
child: Text(label),
),
);
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Candidates selector // Candidates selector
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -258,11 +337,12 @@ class _CandidatesSection extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context); final theme = Theme.of(context);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('Выберите блюдо', style: theme.textTheme.titleMedium), Text(l10n.selectDish, style: theme.textTheme.titleMedium),
const SizedBox(height: 8), const SizedBox(height: 8),
...candidates.asMap().entries.map((entry) { ...candidates.asMap().entries.map((entry) {
final index = entry.key; final index = entry.key;
@@ -378,6 +458,7 @@ class _NutritionCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context); final theme = Theme.of(context);
return Card( return Card(
child: Padding( child: Padding(
@@ -388,7 +469,7 @@ class _NutritionCard extends StatelessWidget {
Row( Row(
children: [ children: [
Text( Text(
'${calories.toInt()} ккал', '${calories.toInt()} ${l10n.caloriesUnit}',
style: theme.textTheme.titleLarge?.copyWith( style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
@@ -396,7 +477,7 @@ class _NutritionCard extends StatelessWidget {
), ),
const Spacer(), const Spacer(),
Tooltip( Tooltip(
message: 'Приблизительные значения на основе фото', message: l10n.nutritionApproximate,
child: Text( child: Text(
'', '',
style: theme.textTheme.titleLarge?.copyWith( style: theme.textTheme.titleLarge?.copyWith(
@@ -411,18 +492,18 @@ class _NutritionCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
_MacroChip( _MacroChip(
label: 'Белки', label: l10n.proteinLabel,
value: '${proteinG.toStringAsFixed(1)} г', value: '${proteinG.toStringAsFixed(1)} ${l10n.gramsUnit}',
color: Colors.blue, color: Colors.blue,
), ),
_MacroChip( _MacroChip(
label: 'Жиры', label: l10n.fatLabel,
value: '${fatG.toStringAsFixed(1)} г', value: '${fatG.toStringAsFixed(1)} ${l10n.gramsUnit}',
color: Colors.orange, color: Colors.orange,
), ),
_MacroChip( _MacroChip(
label: 'Углеводы', label: l10n.carbsLabel,
value: '${carbsG.toStringAsFixed(1)} г', value: '${carbsG.toStringAsFixed(1)} ${l10n.gramsUnit}',
color: Colors.green, color: Colors.green,
), ),
], ],
@@ -482,11 +563,12 @@ class _PortionRow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context); final theme = Theme.of(context);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('Порция', style: theme.textTheme.titleSmall), Text(l10n.portion, style: theme.textTheme.titleSmall),
const SizedBox(height: 8), const SizedBox(height: 8),
Row( Row(
children: [ children: [
@@ -502,9 +584,9 @@ class _PortionRow extends StatelessWidget {
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly], inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: onChanged, onChanged: onChanged,
decoration: const InputDecoration( decoration: InputDecoration(
suffixText: 'г', suffixText: l10n.gramsUnit,
border: OutlineInputBorder(), border: const OutlineInputBorder(),
), ),
), ),
), ),
@@ -535,11 +617,12 @@ class _MealTypeDropdown extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context); final theme = Theme.of(context);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('Приём пищи', style: theme.textTheme.titleSmall), Text(l10n.mealType, style: theme.textTheme.titleSmall),
const SizedBox(height: 8), const SizedBox(height: 8),
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
initialValue: selected, initialValue: selected,
@@ -550,7 +633,7 @@ class _MealTypeDropdown extends StatelessWidget {
.map((mealTypeOption) => DropdownMenuItem( .map((mealTypeOption) => DropdownMenuItem(
value: mealTypeOption.id, value: mealTypeOption.id,
child: Text( child: Text(
'${mealTypeOption.emoji} ${mealTypeOption.label}'), '${mealTypeOption.emoji} ${mealTypeLabel(mealTypeOption.id, l10n)}'),
)) ))
.toList(), .toList(),
onChanged: onChanged, onChanged: onChanged,

View File

@@ -1,21 +1,23 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:food_ai/l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../home/home_provider.dart'; import '../home/home_provider.dart';
import '../scan/dish_result_screen.dart'; import '../scan/dish_result_screen.dart';
import 'recognition_service.dart'; import 'recognition_service.dart';
/// Full-screen page showing all of today's unlinked dish recognition jobs. /// Full-screen page showing all dish recognition jobs.
class RecognitionHistoryScreen extends ConsumerWidget { class RecognitionHistoryScreen extends ConsumerWidget {
const RecognitionHistoryScreen({super.key}); const RecognitionHistoryScreen({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final jobsState = ref.watch(todayJobsProvider); final l10n = AppLocalizations.of(context)!;
final jobsState = ref.watch(allJobsProvider);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('История распознавания'), title: Text(l10n.historyTitle),
), ),
body: jobsState.when( body: jobsState.when(
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
@@ -23,20 +25,18 @@ class RecognitionHistoryScreen extends ConsumerWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Text('Не удалось загрузить историю'), Text(l10n.historyLoadError),
const SizedBox(height: 12), const SizedBox(height: 12),
FilledButton( FilledButton(
onPressed: () => ref.invalidate(todayJobsProvider), onPressed: () => ref.invalidate(allJobsProvider),
child: const Text('Повторить'), child: Text(l10n.retry),
), ),
], ],
), ),
), ),
data: (jobs) { data: (jobs) {
if (jobs.isEmpty) { if (jobs.isEmpty) {
return const Center( return Center(child: Text(l10n.noHistory));
child: Text('Нет распознаваний за сегодня'),
);
} }
return ListView.separated( return ListView.separated(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@@ -58,6 +58,7 @@ class _HistoryJobTile extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context); final theme = Theme.of(context);
final isDone = job.status == 'done'; final isDone = job.status == 'done';
@@ -84,11 +85,11 @@ class _HistoryJobTile extends ConsumerWidget {
final String titleText; final String titleText;
if (isDone) { if (isDone) {
titleText = dishName ?? 'Блюдо распознано'; titleText = dishName ?? l10n.dishRecognized;
} else if (isProcessing) { } else if (isProcessing) {
titleText = 'Распознаётся…'; titleText = l10n.recognizing;
} else { } else {
titleText = 'Ошибка распознавания'; titleText = l10n.recognitionError;
} }
final contextParts = [ final contextParts = [
@@ -118,6 +119,8 @@ class _HistoryJobTile extends ConsumerWidget {
dish: job.result!, dish: job.result!,
preselectedMealType: job.targetMealType, preselectedMealType: job.targetMealType,
jobId: job.id, jobId: job.id,
targetDate: job.targetDate,
createdAt: job.createdAt,
onAdded: () => Navigator.pop(sheetContext), onAdded: () => Navigator.pop(sheetContext),
), ),
); );

View File

@@ -282,7 +282,16 @@ class RecognitionService {
/// Returns today's recognition jobs that have not yet been linked to a diary entry. /// Returns today's recognition jobs that have not yet been linked to a diary entry.
Future<List<DishJobSummary>> listTodayUnlinkedJobs() async { Future<List<DishJobSummary>> listTodayUnlinkedJobs() async {
final data = await _client.get('/ai/jobs') as List<dynamic>; final data = await _client.getList('/ai/jobs');
return data
.map((element) =>
DishJobSummary.fromJson(element as Map<String, dynamic>))
.toList();
}
/// Returns all recognition jobs for the current user, newest first.
Future<List<DishJobSummary>> listAllJobs() async {
final data = await _client.getList('/ai/jobs/history');
return data return data
.map((element) => .map((element) =>
DishJobSummary.fromJson(element as Map<String, dynamic>)) DishJobSummary.fromJson(element as Map<String, dynamic>))

100
client/lib/l10n/app_ar.arb Normal file
View File

@@ -0,0 +1,100 @@
{
"@@locale": "ar",
"appTitle": "FoodAI",
"greetingMorning": "صباح الخير",
"greetingAfternoon": "مساء الخير",
"greetingEvening": "مساء النور",
"caloriesUnit": "سعرة",
"gramsUnit": "غ",
"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": "الطول (سم)",
"weightKg": "الوزن (كغ)",
"birthDate": "تاريخ الميلاد",
"nameRequired": "أدخل الاسم",
"profileUpdated": "تم تحديث الملف الشخصي",
"profileSaveFailed": "فشل الحفظ",
"mealTypeBreakfast": "الإفطار",
"mealTypeSecondBreakfast": "الإفطار الثاني",
"mealTypeLunch": "الغداء",
"mealTypeAfternoonSnack": "وجبة العصر",
"mealTypeDinner": "العشاء",
"mealTypeSnack": "وجبة خفيفة",
"navHome": "الرئيسية",
"navProducts": "المنتجات",
"navRecipes": "الوصفات"
}

100
client/lib/l10n/app_de.arb Normal file
View File

@@ -0,0 +1,100 @@
{
"@@locale": "de",
"appTitle": "FoodAI",
"greetingMorning": "Guten Morgen",
"greetingAfternoon": "Guten Tag",
"greetingEvening": "Guten Abend",
"caloriesUnit": "kcal",
"gramsUnit": "g",
"goalLabel": "Ziel:",
"consumed": "Verzehrt",
"remaining": "Verbleibend",
"exceeded": "Überschritten",
"proteinLabel": "Protein",
"fatLabel": "Fett",
"carbsLabel": "Kohlenhydrate",
"today": "Heute",
"yesterday": "Gestern",
"mealsSection": "Mahlzeiten",
"addDish": "Gericht hinzufügen",
"scanDish": "Scannen",
"menu": "Menü",
"dishHistory": "Gerichtverlauf",
"recommendCook": "Wir empfehlen zu kochen",
"camera": "Kamera",
"gallery": "Galerie",
"analyzingPhoto": "Foto wird analysiert...",
"inQueue": "Sie sind in der Warteschlange",
"queuePosition": "Position {position}",
"@queuePosition": {
"placeholders": {
"position": { "type": "int" }
}
},
"processing": "Verarbeitung...",
"upgradePrompt": "Warteschlange überspringen? Upgrade →",
"recognitionFailed": "Erkennung fehlgeschlagen. Erneut versuchen.",
"dishRecognition": "Gerichterkennung",
"all": "Alle",
"dishRecognized": "Gericht erkannt",
"recognizing": "Wird erkannt…",
"recognitionError": "Erkennungsfehler",
"dishResultTitle": "Gericht erkannt",
"selectDish": "Gericht auswählen",
"dishNotRecognized": "Gericht nicht erkannt",
"tryAgain": "Erneut versuchen",
"nutritionApproximate": "Nährwerte sind ungefähr — aus dem Foto geschätzt.",
"portion": "Portion",
"mealType": "Mahlzeittyp",
"dateLabel": "Datum",
"addToJournal": "Zum Tagebuch hinzufügen",
"addFailed": "Hinzufügen fehlgeschlagen. Erneut versuchen.",
"historyTitle": "Erkennungsverlauf",
"historyLoadError": "Verlauf konnte nicht geladen werden",
"retry": "Wiederholen",
"noHistory": "Noch keine Erkennungen",
"profileTitle": "Profil",
"edit": "Bearbeiten",
"bodyParams": "KÖRPERPARAMETER",
"goalActivity": "ZIEL & AKTIVITÄT",
"nutrition": "ERNÄHRUNG",
"settings": "EINSTELLUNGEN",
"height": "Größe",
"weight": "Gewicht",
"age": "Alter",
"gender": "Geschlecht",
"genderMale": "Männlich",
"genderFemale": "Weiblich",
"goalLoss": "Gewichtsverlust",
"goalMaintain": "Gewicht halten",
"goalGain": "Muskelaufbau",
"activityLow": "Niedrig",
"activityMedium": "Mittel",
"activityHigh": "Hoch",
"calorieGoal": "Kalorienziel",
"mealTypes": "Mahlzeittypen",
"formulaNote": "Berechnet mit der Mifflin-St Jeor Formel",
"language": "Sprache",
"notSet": "Nicht festgelegt",
"calorieHint": "Körperparameter eingeben, um das Kalorienziel zu berechnen",
"logout": "Abmelden",
"editProfile": "Profil bearbeiten",
"cancel": "Abbrechen",
"save": "Speichern",
"nameLabel": "Name",
"heightCm": "Größe (cm)",
"weightKg": "Gewicht (kg)",
"birthDate": "Geburtsdatum",
"nameRequired": "Name eingeben",
"profileUpdated": "Profil aktualisiert",
"profileSaveFailed": "Speichern fehlgeschlagen",
"mealTypeBreakfast": "Frühstück",
"mealTypeSecondBreakfast": "Zweites Frühstück",
"mealTypeLunch": "Mittagessen",
"mealTypeAfternoonSnack": "Nachmittagssnack",
"mealTypeDinner": "Abendessen",
"mealTypeSnack": "Snack",
"navHome": "Startseite",
"navProducts": "Produkte",
"navRecipes": "Rezepte"
}

100
client/lib/l10n/app_en.arb Normal file
View File

@@ -0,0 +1,100 @@
{
"@@locale": "en",
"appTitle": "FoodAI",
"greetingMorning": "Good morning",
"greetingAfternoon": "Good afternoon",
"greetingEvening": "Good evening",
"caloriesUnit": "kcal",
"gramsUnit": "g",
"goalLabel": "goal:",
"consumed": "Consumed",
"remaining": "Remaining",
"exceeded": "Exceeded",
"proteinLabel": "Protein",
"fatLabel": "Fat",
"carbsLabel": "Carbs",
"today": "Today",
"yesterday": "Yesterday",
"mealsSection": "Meals",
"addDish": "Add dish",
"scanDish": "Scan",
"menu": "Menu",
"dishHistory": "Dish history",
"recommendCook": "We recommend cooking",
"camera": "Camera",
"gallery": "Gallery",
"analyzingPhoto": "Analyzing photo...",
"inQueue": "You are in queue",
"queuePosition": "Position {position}",
"@queuePosition": {
"placeholders": {
"position": { "type": "int" }
}
},
"processing": "Processing...",
"upgradePrompt": "Skip the queue? Upgrade →",
"recognitionFailed": "Recognition failed. Try again.",
"dishRecognition": "Dish recognition",
"all": "All",
"dishRecognized": "Dish recognized",
"recognizing": "Recognizing…",
"recognitionError": "Recognition error",
"dishResultTitle": "Dish recognized",
"selectDish": "Select dish",
"dishNotRecognized": "Dish not recognized",
"tryAgain": "Try again",
"nutritionApproximate": "Nutrition is approximate — estimated from photo.",
"portion": "Portion",
"mealType": "Meal type",
"dateLabel": "Date",
"addToJournal": "Add to journal",
"addFailed": "Failed to add. Try again.",
"historyTitle": "Recognition history",
"historyLoadError": "Failed to load history",
"retry": "Retry",
"noHistory": "No recognitions yet",
"profileTitle": "Profile",
"edit": "Edit",
"bodyParams": "BODY PARAMS",
"goalActivity": "GOAL & ACTIVITY",
"nutrition": "NUTRITION",
"settings": "SETTINGS",
"height": "Height",
"weight": "Weight",
"age": "Age",
"gender": "Gender",
"genderMale": "Male",
"genderFemale": "Female",
"goalLoss": "Weight loss",
"goalMaintain": "Maintenance",
"goalGain": "Muscle gain",
"activityLow": "Low",
"activityMedium": "Medium",
"activityHigh": "High",
"calorieGoal": "Calorie goal",
"mealTypes": "Meal types",
"formulaNote": "Calculated using the Mifflin-St Jeor formula",
"language": "Language",
"notSet": "Not set",
"calorieHint": "Enter body params to calculate calorie goal",
"logout": "Log out",
"editProfile": "Edit profile",
"cancel": "Cancel",
"save": "Save",
"nameLabel": "Name",
"heightCm": "Height (cm)",
"weightKg": "Weight (kg)",
"birthDate": "Date of birth",
"nameRequired": "Enter name",
"profileUpdated": "Profile updated",
"profileSaveFailed": "Failed to save",
"mealTypeBreakfast": "Breakfast",
"mealTypeSecondBreakfast": "Second breakfast",
"mealTypeLunch": "Lunch",
"mealTypeAfternoonSnack": "Afternoon snack",
"mealTypeDinner": "Dinner",
"mealTypeSnack": "Snack",
"navHome": "Home",
"navProducts": "Products",
"navRecipes": "Recipes"
}

100
client/lib/l10n/app_es.arb Normal file
View File

@@ -0,0 +1,100 @@
{
"@@locale": "es",
"appTitle": "FoodAI",
"greetingMorning": "Buenos días",
"greetingAfternoon": "Buenas tardes",
"greetingEvening": "Buenas noches",
"caloriesUnit": "kcal",
"gramsUnit": "g",
"goalLabel": "meta:",
"consumed": "Consumido",
"remaining": "Restante",
"exceeded": "Excedido",
"proteinLabel": "Proteínas",
"fatLabel": "Grasas",
"carbsLabel": "Carbohidratos",
"today": "Hoy",
"yesterday": "Ayer",
"mealsSection": "Comidas",
"addDish": "Añadir plato",
"scanDish": "Escanear",
"menu": "Menú",
"dishHistory": "Historial de platos",
"recommendCook": "Recomendamos cocinar",
"camera": "Cámara",
"gallery": "Galería",
"analyzingPhoto": "Analizando foto...",
"inQueue": "Estás en la cola",
"queuePosition": "Posición {position}",
"@queuePosition": {
"placeholders": {
"position": { "type": "int" }
}
},
"processing": "Procesando...",
"upgradePrompt": "¿Saltar la cola? Actualiza →",
"recognitionFailed": "Reconocimiento fallido. Inténtalo de nuevo.",
"dishRecognition": "Reconocimiento de platos",
"all": "Todos",
"dishRecognized": "Plato reconocido",
"recognizing": "Reconociendo…",
"recognitionError": "Error de reconocimiento",
"dishResultTitle": "Plato reconocido",
"selectDish": "Selecciona un plato",
"dishNotRecognized": "Plato no reconocido",
"tryAgain": "Intentar de nuevo",
"nutritionApproximate": "Los valores nutricionales son aproximados — estimados a partir de la foto.",
"portion": "Porción",
"mealType": "Tipo de comida",
"dateLabel": "Fecha",
"addToJournal": "Añadir al diario",
"addFailed": "Error al añadir. Inténtalo de nuevo.",
"historyTitle": "Historial de reconocimientos",
"historyLoadError": "Error al cargar el historial",
"retry": "Reintentar",
"noHistory": "Sin reconocimientos aún",
"profileTitle": "Perfil",
"edit": "Editar",
"bodyParams": "PARÁMETROS CORPORALES",
"goalActivity": "OBJETIVO Y ACTIVIDAD",
"nutrition": "NUTRICIÓN",
"settings": "AJUSTES",
"height": "Altura",
"weight": "Peso",
"age": "Edad",
"gender": "Género",
"genderMale": "Masculino",
"genderFemale": "Femenino",
"goalLoss": "Pérdida de peso",
"goalMaintain": "Mantenimiento",
"goalGain": "Ganancia muscular",
"activityLow": "Baja",
"activityMedium": "Media",
"activityHigh": "Alta",
"calorieGoal": "Objetivo calórico",
"mealTypes": "Tipos de comida",
"formulaNote": "Calculado con la fórmula de Mifflin-St Jeor",
"language": "Idioma",
"notSet": "No establecido",
"calorieHint": "Introduce los parámetros corporales para calcular el objetivo calórico",
"logout": "Cerrar sesión",
"editProfile": "Editar perfil",
"cancel": "Cancelar",
"save": "Guardar",
"nameLabel": "Nombre",
"heightCm": "Altura (cm)",
"weightKg": "Peso (kg)",
"birthDate": "Fecha de nacimiento",
"nameRequired": "Introduce el nombre",
"profileUpdated": "Perfil actualizado",
"profileSaveFailed": "Error al guardar",
"mealTypeBreakfast": "Desayuno",
"mealTypeSecondBreakfast": "Segundo desayuno",
"mealTypeLunch": "Almuerzo",
"mealTypeAfternoonSnack": "Merienda",
"mealTypeDinner": "Cena",
"mealTypeSnack": "Aperitivo",
"navHome": "Inicio",
"navProducts": "Productos",
"navRecipes": "Recetas"
}

100
client/lib/l10n/app_fr.arb Normal file
View File

@@ -0,0 +1,100 @@
{
"@@locale": "fr",
"appTitle": "FoodAI",
"greetingMorning": "Bonjour",
"greetingAfternoon": "Bon après-midi",
"greetingEvening": "Bonsoir",
"caloriesUnit": "kcal",
"gramsUnit": "g",
"goalLabel": "objectif :",
"consumed": "Consommé",
"remaining": "Restant",
"exceeded": "Dépassé",
"proteinLabel": "Protéines",
"fatLabel": "Lipides",
"carbsLabel": "Glucides",
"today": "Aujourd'hui",
"yesterday": "Hier",
"mealsSection": "Repas",
"addDish": "Ajouter un plat",
"scanDish": "Scanner",
"menu": "Menu",
"dishHistory": "Historique des plats",
"recommendCook": "Nous recommandons de cuisiner",
"camera": "Appareil photo",
"gallery": "Galerie",
"analyzingPhoto": "Analyse de la photo...",
"inQueue": "Vous êtes en file d'attente",
"queuePosition": "Position {position}",
"@queuePosition": {
"placeholders": {
"position": { "type": "int" }
}
},
"processing": "Traitement...",
"upgradePrompt": "Passer la file ? Passez à Premium →",
"recognitionFailed": "Reconnaissance échouée. Réessayez.",
"dishRecognition": "Reconnaissance de plats",
"all": "Tous",
"dishRecognized": "Plat reconnu",
"recognizing": "Reconnaissance en cours…",
"recognitionError": "Erreur de reconnaissance",
"dishResultTitle": "Plat reconnu",
"selectDish": "Sélectionner un plat",
"dishNotRecognized": "Plat non reconnu",
"tryAgain": "Réessayer",
"nutritionApproximate": "Les valeurs nutritionnelles sont approximatives — estimées à partir de la photo.",
"portion": "Portion",
"mealType": "Type de repas",
"dateLabel": "Date",
"addToJournal": "Ajouter au journal",
"addFailed": "Échec de l'ajout. Réessayez.",
"historyTitle": "Historique des reconnaissances",
"historyLoadError": "Impossible de charger l'historique",
"retry": "Réessayer",
"noHistory": "Aucune reconnaissance pour l'instant",
"profileTitle": "Profil",
"edit": "Modifier",
"bodyParams": "PARAMÈTRES CORPORELS",
"goalActivity": "OBJECTIF & ACTIVITÉ",
"nutrition": "NUTRITION",
"settings": "PARAMÈTRES",
"height": "Taille",
"weight": "Poids",
"age": "Âge",
"gender": "Sexe",
"genderMale": "Masculin",
"genderFemale": "Féminin",
"goalLoss": "Perte de poids",
"goalMaintain": "Maintien",
"goalGain": "Prise de masse",
"activityLow": "Faible",
"activityMedium": "Moyenne",
"activityHigh": "Élevée",
"calorieGoal": "Objectif calorique",
"mealTypes": "Types de repas",
"formulaNote": "Calculé avec la formule de Mifflin-St Jeor",
"language": "Langue",
"notSet": "Non défini",
"calorieHint": "Saisissez les paramètres corporels pour calculer l'objectif calorique",
"logout": "Se déconnecter",
"editProfile": "Modifier le profil",
"cancel": "Annuler",
"save": "Enregistrer",
"nameLabel": "Nom",
"heightCm": "Taille (cm)",
"weightKg": "Poids (kg)",
"birthDate": "Date de naissance",
"nameRequired": "Saisir le nom",
"profileUpdated": "Profil mis à jour",
"profileSaveFailed": "Échec de l'enregistrement",
"mealTypeBreakfast": "Petit-déjeuner",
"mealTypeSecondBreakfast": "Deuxième petit-déjeuner",
"mealTypeLunch": "Déjeuner",
"mealTypeAfternoonSnack": "Goûter",
"mealTypeDinner": "Dîner",
"mealTypeSnack": "Collation",
"navHome": "Accueil",
"navProducts": "Produits",
"navRecipes": "Recettes"
}

100
client/lib/l10n/app_hi.arb Normal file
View File

@@ -0,0 +1,100 @@
{
"@@locale": "hi",
"appTitle": "FoodAI",
"greetingMorning": "सुप्रभात",
"greetingAfternoon": "नमस्ते",
"greetingEvening": "शुभ संध्या",
"caloriesUnit": "कैलोरी",
"gramsUnit": "ग्रा",
"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": "ऊंचाई (सेमी)",
"weightKg": "वज़न (किग्रा)",
"birthDate": "जन्म तिथि",
"nameRequired": "नाम दर्ज करें",
"profileUpdated": "प्रोफ़ाइल अपडेट हुई",
"profileSaveFailed": "सहेजने में विफल",
"mealTypeBreakfast": "नाश्ता",
"mealTypeSecondBreakfast": "दूसरा नाश्ता",
"mealTypeLunch": "दोपहर का भोजन",
"mealTypeAfternoonSnack": "शाम का नाश्ता",
"mealTypeDinner": "रात का खाना",
"mealTypeSnack": "स्नैक",
"navHome": "होम",
"navProducts": "उत्पाद",
"navRecipes": "रेसिपी"
}

100
client/lib/l10n/app_it.arb Normal file
View File

@@ -0,0 +1,100 @@
{
"@@locale": "it",
"appTitle": "FoodAI",
"greetingMorning": "Buongiorno",
"greetingAfternoon": "Buon pomeriggio",
"greetingEvening": "Buonasera",
"caloriesUnit": "kcal",
"gramsUnit": "g",
"goalLabel": "obiettivo:",
"consumed": "Consumato",
"remaining": "Rimanente",
"exceeded": "Superato",
"proteinLabel": "Proteine",
"fatLabel": "Grassi",
"carbsLabel": "Carboidrati",
"today": "Oggi",
"yesterday": "Ieri",
"mealsSection": "Pasti",
"addDish": "Aggiungi piatto",
"scanDish": "Scansiona",
"menu": "Menu",
"dishHistory": "Cronologia piatti",
"recommendCook": "Consigliamo di cucinare",
"camera": "Fotocamera",
"gallery": "Galleria",
"analyzingPhoto": "Analisi foto in corso...",
"inQueue": "Sei in coda",
"queuePosition": "Posizione {position}",
"@queuePosition": {
"placeholders": {
"position": { "type": "int" }
}
},
"processing": "Elaborazione...",
"upgradePrompt": "Salta la coda? Aggiorna →",
"recognitionFailed": "Riconoscimento fallito. Riprova.",
"dishRecognition": "Riconoscimento piatti",
"all": "Tutti",
"dishRecognized": "Piatto riconosciuto",
"recognizing": "Riconoscimento in corso…",
"recognitionError": "Errore di riconoscimento",
"dishResultTitle": "Piatto riconosciuto",
"selectDish": "Seleziona un piatto",
"dishNotRecognized": "Piatto non riconosciuto",
"tryAgain": "Riprova",
"nutritionApproximate": "I valori nutrizionali sono approssimativi — stimati dalla foto.",
"portion": "Porzione",
"mealType": "Tipo di pasto",
"dateLabel": "Data",
"addToJournal": "Aggiungi al diario",
"addFailed": "Aggiunta fallita. Riprova.",
"historyTitle": "Cronologia riconoscimenti",
"historyLoadError": "Impossibile caricare la cronologia",
"retry": "Riprova",
"noHistory": "Nessun riconoscimento ancora",
"profileTitle": "Profilo",
"edit": "Modifica",
"bodyParams": "PARAMETRI CORPOREI",
"goalActivity": "OBIETTIVO & ATTIVITÀ",
"nutrition": "NUTRIZIONE",
"settings": "IMPOSTAZIONI",
"height": "Altezza",
"weight": "Peso",
"age": "Età",
"gender": "Sesso",
"genderMale": "Maschio",
"genderFemale": "Femmina",
"goalLoss": "Perdita di peso",
"goalMaintain": "Mantenimento",
"goalGain": "Aumento muscolare",
"activityLow": "Bassa",
"activityMedium": "Media",
"activityHigh": "Alta",
"calorieGoal": "Obiettivo calorico",
"mealTypes": "Tipi di pasto",
"formulaNote": "Calcolato con la formula di Mifflin-St Jeor",
"language": "Lingua",
"notSet": "Non impostato",
"calorieHint": "Inserisci i parametri corporei per calcolare l'obiettivo calorico",
"logout": "Disconnetti",
"editProfile": "Modifica profilo",
"cancel": "Annulla",
"save": "Salva",
"nameLabel": "Nome",
"heightCm": "Altezza (cm)",
"weightKg": "Peso (kg)",
"birthDate": "Data di nascita",
"nameRequired": "Inserisci il nome",
"profileUpdated": "Profilo aggiornato",
"profileSaveFailed": "Salvataggio fallito",
"mealTypeBreakfast": "Colazione",
"mealTypeSecondBreakfast": "Seconda colazione",
"mealTypeLunch": "Pranzo",
"mealTypeAfternoonSnack": "Merenda",
"mealTypeDinner": "Cena",
"mealTypeSnack": "Spuntino",
"navHome": "Home",
"navProducts": "Prodotti",
"navRecipes": "Ricette"
}

100
client/lib/l10n/app_ja.arb Normal file
View File

@@ -0,0 +1,100 @@
{
"@@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": "レシピ"
}

100
client/lib/l10n/app_ko.arb Normal file
View File

@@ -0,0 +1,100 @@
{
"@@locale": "ko",
"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": "Mifflin-St Jeor 공식으로 계산",
"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": "레시피"
}

View File

@@ -0,0 +1,738 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'app_localizations_ar.dart';
import 'app_localizations_de.dart';
import 'app_localizations_en.dart';
import 'app_localizations_es.dart';
import 'app_localizations_fr.dart';
import 'app_localizations_hi.dart';
import 'app_localizations_it.dart';
import 'app_localizations_ja.dart';
import 'app_localizations_ko.dart';
import 'app_localizations_pt.dart';
import 'app_localizations_ru.dart';
import 'app_localizations_zh.dart';
// ignore_for_file: type=lint
/// Callers can lookup localized strings with an instance of AppLocalizations
/// returned by `AppLocalizations.of(context)`.
///
/// Applications need to include `AppLocalizations.delegate()` in their app's
/// `localizationDelegates` list, and the locales they support in the app's
/// `supportedLocales` list. For example:
///
/// ```dart
/// import 'l10n/app_localizations.dart';
///
/// return MaterialApp(
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
/// supportedLocales: AppLocalizations.supportedLocales,
/// home: MyApplicationHome(),
/// );
/// ```
///
/// ## Update pubspec.yaml
///
/// Please make sure to update your pubspec.yaml to include the following
/// packages:
///
/// ```yaml
/// dependencies:
/// # Internationalization support.
/// flutter_localizations:
/// sdk: flutter
/// intl: any # Use the pinned version from flutter_localizations
///
/// # Rest of dependencies
/// ```
///
/// ## iOS Applications
///
/// iOS applications define key application metadata, including supported
/// locales, in an Info.plist file that is built into the application bundle.
/// To configure the locales supported by your app, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects Runner folder.
///
/// Next, select the Information Property List item, select Add Item from the
/// Editor menu, then select Localizations from the pop-up menu.
///
/// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale
/// you wish to add from the pop-up menu in the Value field. This list should
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
/// property.
abstract class AppLocalizations {
AppLocalizations(String locale)
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
static AppLocalizations? of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
///
/// Returns a list of localizations delegates containing this delegate along with
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
/// and GlobalWidgetsLocalizations.delegate.
///
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
Locale('ar'),
Locale('de'),
Locale('en'),
Locale('es'),
Locale('fr'),
Locale('hi'),
Locale('it'),
Locale('ja'),
Locale('ko'),
Locale('pt'),
Locale('ru'),
Locale('zh'),
];
/// No description provided for @appTitle.
///
/// In en, this message translates to:
/// **'FoodAI'**
String get appTitle;
/// No description provided for @greetingMorning.
///
/// In en, this message translates to:
/// **'Good morning'**
String get greetingMorning;
/// No description provided for @greetingAfternoon.
///
/// In en, this message translates to:
/// **'Good afternoon'**
String get greetingAfternoon;
/// No description provided for @greetingEvening.
///
/// In en, this message translates to:
/// **'Good evening'**
String get greetingEvening;
/// No description provided for @caloriesUnit.
///
/// In en, this message translates to:
/// **'kcal'**
String get caloriesUnit;
/// No description provided for @gramsUnit.
///
/// In en, this message translates to:
/// **'g'**
String get gramsUnit;
/// No description provided for @goalLabel.
///
/// In en, this message translates to:
/// **'goal:'**
String get goalLabel;
/// No description provided for @consumed.
///
/// In en, this message translates to:
/// **'Consumed'**
String get consumed;
/// No description provided for @remaining.
///
/// In en, this message translates to:
/// **'Remaining'**
String get remaining;
/// No description provided for @exceeded.
///
/// In en, this message translates to:
/// **'Exceeded'**
String get exceeded;
/// No description provided for @proteinLabel.
///
/// In en, this message translates to:
/// **'Protein'**
String get proteinLabel;
/// No description provided for @fatLabel.
///
/// In en, this message translates to:
/// **'Fat'**
String get fatLabel;
/// No description provided for @carbsLabel.
///
/// In en, this message translates to:
/// **'Carbs'**
String get carbsLabel;
/// No description provided for @today.
///
/// In en, this message translates to:
/// **'Today'**
String get today;
/// No description provided for @yesterday.
///
/// In en, this message translates to:
/// **'Yesterday'**
String get yesterday;
/// No description provided for @mealsSection.
///
/// In en, this message translates to:
/// **'Meals'**
String get mealsSection;
/// No description provided for @addDish.
///
/// In en, this message translates to:
/// **'Add dish'**
String get addDish;
/// No description provided for @scanDish.
///
/// In en, this message translates to:
/// **'Scan'**
String get scanDish;
/// No description provided for @menu.
///
/// In en, this message translates to:
/// **'Menu'**
String get menu;
/// No description provided for @dishHistory.
///
/// In en, this message translates to:
/// **'Dish history'**
String get dishHistory;
/// No description provided for @recommendCook.
///
/// In en, this message translates to:
/// **'We recommend cooking'**
String get recommendCook;
/// No description provided for @camera.
///
/// In en, this message translates to:
/// **'Camera'**
String get camera;
/// No description provided for @gallery.
///
/// In en, this message translates to:
/// **'Gallery'**
String get gallery;
/// No description provided for @analyzingPhoto.
///
/// In en, this message translates to:
/// **'Analyzing photo...'**
String get analyzingPhoto;
/// No description provided for @inQueue.
///
/// In en, this message translates to:
/// **'You are in queue'**
String get inQueue;
/// No description provided for @queuePosition.
///
/// In en, this message translates to:
/// **'Position {position}'**
String queuePosition(int position);
/// No description provided for @processing.
///
/// In en, this message translates to:
/// **'Processing...'**
String get processing;
/// No description provided for @upgradePrompt.
///
/// In en, this message translates to:
/// **'Skip the queue? Upgrade →'**
String get upgradePrompt;
/// No description provided for @recognitionFailed.
///
/// In en, this message translates to:
/// **'Recognition failed. Try again.'**
String get recognitionFailed;
/// No description provided for @dishRecognition.
///
/// In en, this message translates to:
/// **'Dish recognition'**
String get dishRecognition;
/// No description provided for @all.
///
/// In en, this message translates to:
/// **'All'**
String get all;
/// No description provided for @dishRecognized.
///
/// In en, this message translates to:
/// **'Dish recognized'**
String get dishRecognized;
/// No description provided for @recognizing.
///
/// In en, this message translates to:
/// **'Recognizing…'**
String get recognizing;
/// No description provided for @recognitionError.
///
/// In en, this message translates to:
/// **'Recognition error'**
String get recognitionError;
/// No description provided for @dishResultTitle.
///
/// In en, this message translates to:
/// **'Dish recognized'**
String get dishResultTitle;
/// No description provided for @selectDish.
///
/// In en, this message translates to:
/// **'Select dish'**
String get selectDish;
/// No description provided for @dishNotRecognized.
///
/// In en, this message translates to:
/// **'Dish not recognized'**
String get dishNotRecognized;
/// No description provided for @tryAgain.
///
/// In en, this message translates to:
/// **'Try again'**
String get tryAgain;
/// No description provided for @nutritionApproximate.
///
/// In en, this message translates to:
/// **'Nutrition is approximate — estimated from photo.'**
String get nutritionApproximate;
/// No description provided for @portion.
///
/// In en, this message translates to:
/// **'Portion'**
String get portion;
/// No description provided for @mealType.
///
/// In en, this message translates to:
/// **'Meal type'**
String get mealType;
/// No description provided for @dateLabel.
///
/// In en, this message translates to:
/// **'Date'**
String get dateLabel;
/// No description provided for @addToJournal.
///
/// In en, this message translates to:
/// **'Add to journal'**
String get addToJournal;
/// No description provided for @addFailed.
///
/// In en, this message translates to:
/// **'Failed to add. Try again.'**
String get addFailed;
/// No description provided for @historyTitle.
///
/// In en, this message translates to:
/// **'Recognition history'**
String get historyTitle;
/// No description provided for @historyLoadError.
///
/// In en, this message translates to:
/// **'Failed to load history'**
String get historyLoadError;
/// No description provided for @retry.
///
/// In en, this message translates to:
/// **'Retry'**
String get retry;
/// No description provided for @noHistory.
///
/// In en, this message translates to:
/// **'No recognitions yet'**
String get noHistory;
/// No description provided for @profileTitle.
///
/// In en, this message translates to:
/// **'Profile'**
String get profileTitle;
/// No description provided for @edit.
///
/// In en, this message translates to:
/// **'Edit'**
String get edit;
/// No description provided for @bodyParams.
///
/// In en, this message translates to:
/// **'BODY PARAMS'**
String get bodyParams;
/// No description provided for @goalActivity.
///
/// In en, this message translates to:
/// **'GOAL & ACTIVITY'**
String get goalActivity;
/// No description provided for @nutrition.
///
/// In en, this message translates to:
/// **'NUTRITION'**
String get nutrition;
/// No description provided for @settings.
///
/// In en, this message translates to:
/// **'SETTINGS'**
String get settings;
/// No description provided for @height.
///
/// In en, this message translates to:
/// **'Height'**
String get height;
/// No description provided for @weight.
///
/// In en, this message translates to:
/// **'Weight'**
String get weight;
/// No description provided for @age.
///
/// In en, this message translates to:
/// **'Age'**
String get age;
/// No description provided for @gender.
///
/// In en, this message translates to:
/// **'Gender'**
String get gender;
/// No description provided for @genderMale.
///
/// In en, this message translates to:
/// **'Male'**
String get genderMale;
/// No description provided for @genderFemale.
///
/// In en, this message translates to:
/// **'Female'**
String get genderFemale;
/// No description provided for @goalLoss.
///
/// In en, this message translates to:
/// **'Weight loss'**
String get goalLoss;
/// No description provided for @goalMaintain.
///
/// In en, this message translates to:
/// **'Maintenance'**
String get goalMaintain;
/// No description provided for @goalGain.
///
/// In en, this message translates to:
/// **'Muscle gain'**
String get goalGain;
/// No description provided for @activityLow.
///
/// In en, this message translates to:
/// **'Low'**
String get activityLow;
/// No description provided for @activityMedium.
///
/// In en, this message translates to:
/// **'Medium'**
String get activityMedium;
/// No description provided for @activityHigh.
///
/// In en, this message translates to:
/// **'High'**
String get activityHigh;
/// No description provided for @calorieGoal.
///
/// In en, this message translates to:
/// **'Calorie goal'**
String get calorieGoal;
/// No description provided for @mealTypes.
///
/// In en, this message translates to:
/// **'Meal types'**
String get mealTypes;
/// No description provided for @formulaNote.
///
/// In en, this message translates to:
/// **'Calculated using the Mifflin-St Jeor formula'**
String get formulaNote;
/// No description provided for @language.
///
/// In en, this message translates to:
/// **'Language'**
String get language;
/// No description provided for @notSet.
///
/// In en, this message translates to:
/// **'Not set'**
String get notSet;
/// No description provided for @calorieHint.
///
/// In en, this message translates to:
/// **'Enter body params to calculate calorie goal'**
String get calorieHint;
/// No description provided for @logout.
///
/// In en, this message translates to:
/// **'Log out'**
String get logout;
/// No description provided for @editProfile.
///
/// In en, this message translates to:
/// **'Edit profile'**
String get editProfile;
/// No description provided for @cancel.
///
/// In en, this message translates to:
/// **'Cancel'**
String get cancel;
/// No description provided for @save.
///
/// In en, this message translates to:
/// **'Save'**
String get save;
/// No description provided for @nameLabel.
///
/// In en, this message translates to:
/// **'Name'**
String get nameLabel;
/// No description provided for @heightCm.
///
/// In en, this message translates to:
/// **'Height (cm)'**
String get heightCm;
/// No description provided for @weightKg.
///
/// In en, this message translates to:
/// **'Weight (kg)'**
String get weightKg;
/// No description provided for @birthDate.
///
/// In en, this message translates to:
/// **'Date of birth'**
String get birthDate;
/// No description provided for @nameRequired.
///
/// In en, this message translates to:
/// **'Enter name'**
String get nameRequired;
/// No description provided for @profileUpdated.
///
/// In en, this message translates to:
/// **'Profile updated'**
String get profileUpdated;
/// No description provided for @profileSaveFailed.
///
/// In en, this message translates to:
/// **'Failed to save'**
String get profileSaveFailed;
/// No description provided for @mealTypeBreakfast.
///
/// In en, this message translates to:
/// **'Breakfast'**
String get mealTypeBreakfast;
/// No description provided for @mealTypeSecondBreakfast.
///
/// In en, this message translates to:
/// **'Second breakfast'**
String get mealTypeSecondBreakfast;
/// No description provided for @mealTypeLunch.
///
/// In en, this message translates to:
/// **'Lunch'**
String get mealTypeLunch;
/// No description provided for @mealTypeAfternoonSnack.
///
/// In en, this message translates to:
/// **'Afternoon snack'**
String get mealTypeAfternoonSnack;
/// No description provided for @mealTypeDinner.
///
/// In en, this message translates to:
/// **'Dinner'**
String get mealTypeDinner;
/// No description provided for @mealTypeSnack.
///
/// In en, this message translates to:
/// **'Snack'**
String get mealTypeSnack;
/// No description provided for @navHome.
///
/// In en, this message translates to:
/// **'Home'**
String get navHome;
/// No description provided for @navProducts.
///
/// In en, this message translates to:
/// **'Products'**
String get navProducts;
/// No description provided for @navRecipes.
///
/// In en, this message translates to:
/// **'Recipes'**
String get navRecipes;
}
class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) {
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) => <String>[
'ar',
'de',
'en',
'es',
'fr',
'hi',
'it',
'ja',
'ko',
'pt',
'ru',
'zh',
].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'ar':
return AppLocalizationsAr();
case 'de':
return AppLocalizationsDe();
case 'en':
return AppLocalizationsEn();
case 'es':
return AppLocalizationsEs();
case 'fr':
return AppLocalizationsFr();
case 'hi':
return AppLocalizationsHi();
case 'it':
return AppLocalizationsIt();
case 'ja':
return AppLocalizationsJa();
case 'ko':
return AppLocalizationsKo();
case 'pt':
return AppLocalizationsPt();
case 'ru':
return AppLocalizationsRu();
case 'zh':
return AppLocalizationsZh();
}
throw FlutterError(
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.',
);
}

View File

@@ -0,0 +1,289 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Arabic (`ar`).
class AppLocalizationsAr extends AppLocalizations {
AppLocalizationsAr([String locale = 'ar']) : 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 => 'تخطي قائمة الانتظار؟ ترقية →';
@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 => 'الوصفات';
}

View File

@@ -0,0 +1,290 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for German (`de`).
class AppLocalizationsDe extends AppLocalizations {
AppLocalizationsDe([String locale = 'de']) : super(locale);
@override
String get appTitle => 'FoodAI';
@override
String get greetingMorning => 'Guten Morgen';
@override
String get greetingAfternoon => 'Guten Tag';
@override
String get greetingEvening => 'Guten Abend';
@override
String get caloriesUnit => 'kcal';
@override
String get gramsUnit => 'g';
@override
String get goalLabel => 'Ziel:';
@override
String get consumed => 'Verzehrt';
@override
String get remaining => 'Verbleibend';
@override
String get exceeded => 'Überschritten';
@override
String get proteinLabel => 'Protein';
@override
String get fatLabel => 'Fett';
@override
String get carbsLabel => 'Kohlenhydrate';
@override
String get today => 'Heute';
@override
String get yesterday => 'Gestern';
@override
String get mealsSection => 'Mahlzeiten';
@override
String get addDish => 'Gericht hinzufügen';
@override
String get scanDish => 'Scannen';
@override
String get menu => 'Menü';
@override
String get dishHistory => 'Gerichtverlauf';
@override
String get recommendCook => 'Wir empfehlen zu kochen';
@override
String get camera => 'Kamera';
@override
String get gallery => 'Galerie';
@override
String get analyzingPhoto => 'Foto wird analysiert...';
@override
String get inQueue => 'Sie sind in der Warteschlange';
@override
String queuePosition(int position) {
return 'Position $position';
}
@override
String get processing => 'Verarbeitung...';
@override
String get upgradePrompt => 'Warteschlange überspringen? Upgrade →';
@override
String get recognitionFailed => 'Erkennung fehlgeschlagen. Erneut versuchen.';
@override
String get dishRecognition => 'Gerichterkennung';
@override
String get all => 'Alle';
@override
String get dishRecognized => 'Gericht erkannt';
@override
String get recognizing => 'Wird erkannt…';
@override
String get recognitionError => 'Erkennungsfehler';
@override
String get dishResultTitle => 'Gericht erkannt';
@override
String get selectDish => 'Gericht auswählen';
@override
String get dishNotRecognized => 'Gericht nicht erkannt';
@override
String get tryAgain => 'Erneut versuchen';
@override
String get nutritionApproximate =>
'Nährwerte sind ungefähr — aus dem Foto geschätzt.';
@override
String get portion => 'Portion';
@override
String get mealType => 'Mahlzeittyp';
@override
String get dateLabel => 'Datum';
@override
String get addToJournal => 'Zum Tagebuch hinzufügen';
@override
String get addFailed => 'Hinzufügen fehlgeschlagen. Erneut versuchen.';
@override
String get historyTitle => 'Erkennungsverlauf';
@override
String get historyLoadError => 'Verlauf konnte nicht geladen werden';
@override
String get retry => 'Wiederholen';
@override
String get noHistory => 'Noch keine Erkennungen';
@override
String get profileTitle => 'Profil';
@override
String get edit => 'Bearbeiten';
@override
String get bodyParams => 'KÖRPERPARAMETER';
@override
String get goalActivity => 'ZIEL & AKTIVITÄT';
@override
String get nutrition => 'ERNÄHRUNG';
@override
String get settings => 'EINSTELLUNGEN';
@override
String get height => 'Größe';
@override
String get weight => 'Gewicht';
@override
String get age => 'Alter';
@override
String get gender => 'Geschlecht';
@override
String get genderMale => 'Männlich';
@override
String get genderFemale => 'Weiblich';
@override
String get goalLoss => 'Gewichtsverlust';
@override
String get goalMaintain => 'Gewicht halten';
@override
String get goalGain => 'Muskelaufbau';
@override
String get activityLow => 'Niedrig';
@override
String get activityMedium => 'Mittel';
@override
String get activityHigh => 'Hoch';
@override
String get calorieGoal => 'Kalorienziel';
@override
String get mealTypes => 'Mahlzeittypen';
@override
String get formulaNote => 'Berechnet mit der Mifflin-St Jeor Formel';
@override
String get language => 'Sprache';
@override
String get notSet => 'Nicht festgelegt';
@override
String get calorieHint =>
'Körperparameter eingeben, um das Kalorienziel zu berechnen';
@override
String get logout => 'Abmelden';
@override
String get editProfile => 'Profil bearbeiten';
@override
String get cancel => 'Abbrechen';
@override
String get save => 'Speichern';
@override
String get nameLabel => 'Name';
@override
String get heightCm => 'Größe (cm)';
@override
String get weightKg => 'Gewicht (kg)';
@override
String get birthDate => 'Geburtsdatum';
@override
String get nameRequired => 'Name eingeben';
@override
String get profileUpdated => 'Profil aktualisiert';
@override
String get profileSaveFailed => 'Speichern fehlgeschlagen';
@override
String get mealTypeBreakfast => 'Frühstück';
@override
String get mealTypeSecondBreakfast => 'Zweites Frühstück';
@override
String get mealTypeLunch => 'Mittagessen';
@override
String get mealTypeAfternoonSnack => 'Nachmittagssnack';
@override
String get mealTypeDinner => 'Abendessen';
@override
String get mealTypeSnack => 'Snack';
@override
String get navHome => 'Startseite';
@override
String get navProducts => 'Produkte';
@override
String get navRecipes => 'Rezepte';
}

View File

@@ -0,0 +1,289 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get appTitle => 'FoodAI';
@override
String get greetingMorning => 'Good morning';
@override
String get greetingAfternoon => 'Good afternoon';
@override
String get greetingEvening => 'Good evening';
@override
String get caloriesUnit => 'kcal';
@override
String get gramsUnit => 'g';
@override
String get goalLabel => 'goal:';
@override
String get consumed => 'Consumed';
@override
String get remaining => 'Remaining';
@override
String get exceeded => 'Exceeded';
@override
String get proteinLabel => 'Protein';
@override
String get fatLabel => 'Fat';
@override
String get carbsLabel => 'Carbs';
@override
String get today => 'Today';
@override
String get yesterday => 'Yesterday';
@override
String get mealsSection => 'Meals';
@override
String get addDish => 'Add dish';
@override
String get scanDish => 'Scan';
@override
String get menu => 'Menu';
@override
String get dishHistory => 'Dish history';
@override
String get recommendCook => 'We recommend cooking';
@override
String get camera => 'Camera';
@override
String get gallery => 'Gallery';
@override
String get analyzingPhoto => 'Analyzing photo...';
@override
String get inQueue => 'You are in queue';
@override
String queuePosition(int position) {
return 'Position $position';
}
@override
String get processing => 'Processing...';
@override
String get upgradePrompt => 'Skip the queue? Upgrade →';
@override
String get recognitionFailed => 'Recognition failed. Try again.';
@override
String get dishRecognition => 'Dish recognition';
@override
String get all => 'All';
@override
String get dishRecognized => 'Dish recognized';
@override
String get recognizing => 'Recognizing…';
@override
String get recognitionError => 'Recognition error';
@override
String get dishResultTitle => 'Dish recognized';
@override
String get selectDish => 'Select dish';
@override
String get dishNotRecognized => 'Dish not recognized';
@override
String get tryAgain => 'Try again';
@override
String get nutritionApproximate =>
'Nutrition is approximate — estimated from photo.';
@override
String get portion => 'Portion';
@override
String get mealType => 'Meal type';
@override
String get dateLabel => 'Date';
@override
String get addToJournal => 'Add to journal';
@override
String get addFailed => 'Failed to add. Try again.';
@override
String get historyTitle => 'Recognition history';
@override
String get historyLoadError => 'Failed to load history';
@override
String get retry => 'Retry';
@override
String get noHistory => 'No recognitions yet';
@override
String get profileTitle => 'Profile';
@override
String get edit => 'Edit';
@override
String get bodyParams => 'BODY PARAMS';
@override
String get goalActivity => 'GOAL & ACTIVITY';
@override
String get nutrition => 'NUTRITION';
@override
String get settings => 'SETTINGS';
@override
String get height => 'Height';
@override
String get weight => 'Weight';
@override
String get age => 'Age';
@override
String get gender => 'Gender';
@override
String get genderMale => 'Male';
@override
String get genderFemale => 'Female';
@override
String get goalLoss => 'Weight loss';
@override
String get goalMaintain => 'Maintenance';
@override
String get goalGain => 'Muscle gain';
@override
String get activityLow => 'Low';
@override
String get activityMedium => 'Medium';
@override
String get activityHigh => 'High';
@override
String get calorieGoal => 'Calorie goal';
@override
String get mealTypes => 'Meal types';
@override
String get formulaNote => 'Calculated using the Mifflin-St Jeor formula';
@override
String get language => 'Language';
@override
String get notSet => 'Not set';
@override
String get calorieHint => 'Enter body params to calculate calorie goal';
@override
String get logout => 'Log out';
@override
String get editProfile => 'Edit profile';
@override
String get cancel => 'Cancel';
@override
String get save => 'Save';
@override
String get nameLabel => 'Name';
@override
String get heightCm => 'Height (cm)';
@override
String get weightKg => 'Weight (kg)';
@override
String get birthDate => 'Date of birth';
@override
String get nameRequired => 'Enter name';
@override
String get profileUpdated => 'Profile updated';
@override
String get profileSaveFailed => 'Failed to save';
@override
String get mealTypeBreakfast => 'Breakfast';
@override
String get mealTypeSecondBreakfast => 'Second breakfast';
@override
String get mealTypeLunch => 'Lunch';
@override
String get mealTypeAfternoonSnack => 'Afternoon snack';
@override
String get mealTypeDinner => 'Dinner';
@override
String get mealTypeSnack => 'Snack';
@override
String get navHome => 'Home';
@override
String get navProducts => 'Products';
@override
String get navRecipes => 'Recipes';
}

View File

@@ -0,0 +1,290 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Spanish Castilian (`es`).
class AppLocalizationsEs extends AppLocalizations {
AppLocalizationsEs([String locale = 'es']) : super(locale);
@override
String get appTitle => 'FoodAI';
@override
String get greetingMorning => 'Buenos días';
@override
String get greetingAfternoon => 'Buenas tardes';
@override
String get greetingEvening => 'Buenas noches';
@override
String get caloriesUnit => 'kcal';
@override
String get gramsUnit => 'g';
@override
String get goalLabel => 'meta:';
@override
String get consumed => 'Consumido';
@override
String get remaining => 'Restante';
@override
String get exceeded => 'Excedido';
@override
String get proteinLabel => 'Proteínas';
@override
String get fatLabel => 'Grasas';
@override
String get carbsLabel => 'Carbohidratos';
@override
String get today => 'Hoy';
@override
String get yesterday => 'Ayer';
@override
String get mealsSection => 'Comidas';
@override
String get addDish => 'Añadir plato';
@override
String get scanDish => 'Escanear';
@override
String get menu => 'Menú';
@override
String get dishHistory => 'Historial de platos';
@override
String get recommendCook => 'Recomendamos cocinar';
@override
String get camera => 'Cámara';
@override
String get gallery => 'Galería';
@override
String get analyzingPhoto => 'Analizando foto...';
@override
String get inQueue => 'Estás en la cola';
@override
String queuePosition(int position) {
return 'Posición $position';
}
@override
String get processing => 'Procesando...';
@override
String get upgradePrompt => '¿Saltar la cola? Actualiza →';
@override
String get recognitionFailed => 'Reconocimiento fallido. Inténtalo de nuevo.';
@override
String get dishRecognition => 'Reconocimiento de platos';
@override
String get all => 'Todos';
@override
String get dishRecognized => 'Plato reconocido';
@override
String get recognizing => 'Reconociendo…';
@override
String get recognitionError => 'Error de reconocimiento';
@override
String get dishResultTitle => 'Plato reconocido';
@override
String get selectDish => 'Selecciona un plato';
@override
String get dishNotRecognized => 'Plato no reconocido';
@override
String get tryAgain => 'Intentar de nuevo';
@override
String get nutritionApproximate =>
'Los valores nutricionales son aproximados — estimados a partir de la foto.';
@override
String get portion => 'Porción';
@override
String get mealType => 'Tipo de comida';
@override
String get dateLabel => 'Fecha';
@override
String get addToJournal => 'Añadir al diario';
@override
String get addFailed => 'Error al añadir. Inténtalo de nuevo.';
@override
String get historyTitle => 'Historial de reconocimientos';
@override
String get historyLoadError => 'Error al cargar el historial';
@override
String get retry => 'Reintentar';
@override
String get noHistory => 'Sin reconocimientos aún';
@override
String get profileTitle => 'Perfil';
@override
String get edit => 'Editar';
@override
String get bodyParams => 'PARÁMETROS CORPORALES';
@override
String get goalActivity => 'OBJETIVO Y ACTIVIDAD';
@override
String get nutrition => 'NUTRICIÓN';
@override
String get settings => 'AJUSTES';
@override
String get height => 'Altura';
@override
String get weight => 'Peso';
@override
String get age => 'Edad';
@override
String get gender => 'Género';
@override
String get genderMale => 'Masculino';
@override
String get genderFemale => 'Femenino';
@override
String get goalLoss => 'Pérdida de peso';
@override
String get goalMaintain => 'Mantenimiento';
@override
String get goalGain => 'Ganancia muscular';
@override
String get activityLow => 'Baja';
@override
String get activityMedium => 'Media';
@override
String get activityHigh => 'Alta';
@override
String get calorieGoal => 'Objetivo calórico';
@override
String get mealTypes => 'Tipos de comida';
@override
String get formulaNote => 'Calculado con la fórmula de Mifflin-St Jeor';
@override
String get language => 'Idioma';
@override
String get notSet => 'No establecido';
@override
String get calorieHint =>
'Introduce los parámetros corporales para calcular el objetivo calórico';
@override
String get logout => 'Cerrar sesión';
@override
String get editProfile => 'Editar perfil';
@override
String get cancel => 'Cancelar';
@override
String get save => 'Guardar';
@override
String get nameLabel => 'Nombre';
@override
String get heightCm => 'Altura (cm)';
@override
String get weightKg => 'Peso (kg)';
@override
String get birthDate => 'Fecha de nacimiento';
@override
String get nameRequired => 'Introduce el nombre';
@override
String get profileUpdated => 'Perfil actualizado';
@override
String get profileSaveFailed => 'Error al guardar';
@override
String get mealTypeBreakfast => 'Desayuno';
@override
String get mealTypeSecondBreakfast => 'Segundo desayuno';
@override
String get mealTypeLunch => 'Almuerzo';
@override
String get mealTypeAfternoonSnack => 'Merienda';
@override
String get mealTypeDinner => 'Cena';
@override
String get mealTypeSnack => 'Aperitivo';
@override
String get navHome => 'Inicio';
@override
String get navProducts => 'Productos';
@override
String get navRecipes => 'Recetas';
}

View File

@@ -0,0 +1,290 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for French (`fr`).
class AppLocalizationsFr extends AppLocalizations {
AppLocalizationsFr([String locale = 'fr']) : super(locale);
@override
String get appTitle => 'FoodAI';
@override
String get greetingMorning => 'Bonjour';
@override
String get greetingAfternoon => 'Bon après-midi';
@override
String get greetingEvening => 'Bonsoir';
@override
String get caloriesUnit => 'kcal';
@override
String get gramsUnit => 'g';
@override
String get goalLabel => 'objectif :';
@override
String get consumed => 'Consommé';
@override
String get remaining => 'Restant';
@override
String get exceeded => 'Dépassé';
@override
String get proteinLabel => 'Protéines';
@override
String get fatLabel => 'Lipides';
@override
String get carbsLabel => 'Glucides';
@override
String get today => 'Aujourd\'hui';
@override
String get yesterday => 'Hier';
@override
String get mealsSection => 'Repas';
@override
String get addDish => 'Ajouter un plat';
@override
String get scanDish => 'Scanner';
@override
String get menu => 'Menu';
@override
String get dishHistory => 'Historique des plats';
@override
String get recommendCook => 'Nous recommandons de cuisiner';
@override
String get camera => 'Appareil photo';
@override
String get gallery => 'Galerie';
@override
String get analyzingPhoto => 'Analyse de la photo...';
@override
String get inQueue => 'Vous êtes en file d\'attente';
@override
String queuePosition(int position) {
return 'Position $position';
}
@override
String get processing => 'Traitement...';
@override
String get upgradePrompt => 'Passer la file ? Passez à Premium →';
@override
String get recognitionFailed => 'Reconnaissance échouée. Réessayez.';
@override
String get dishRecognition => 'Reconnaissance de plats';
@override
String get all => 'Tous';
@override
String get dishRecognized => 'Plat reconnu';
@override
String get recognizing => 'Reconnaissance en cours…';
@override
String get recognitionError => 'Erreur de reconnaissance';
@override
String get dishResultTitle => 'Plat reconnu';
@override
String get selectDish => 'Sélectionner un plat';
@override
String get dishNotRecognized => 'Plat non reconnu';
@override
String get tryAgain => 'Réessayer';
@override
String get nutritionApproximate =>
'Les valeurs nutritionnelles sont approximatives — estimées à partir de la photo.';
@override
String get portion => 'Portion';
@override
String get mealType => 'Type de repas';
@override
String get dateLabel => 'Date';
@override
String get addToJournal => 'Ajouter au journal';
@override
String get addFailed => 'Échec de l\'ajout. Réessayez.';
@override
String get historyTitle => 'Historique des reconnaissances';
@override
String get historyLoadError => 'Impossible de charger l\'historique';
@override
String get retry => 'Réessayer';
@override
String get noHistory => 'Aucune reconnaissance pour l\'instant';
@override
String get profileTitle => 'Profil';
@override
String get edit => 'Modifier';
@override
String get bodyParams => 'PARAMÈTRES CORPORELS';
@override
String get goalActivity => 'OBJECTIF & ACTIVITÉ';
@override
String get nutrition => 'NUTRITION';
@override
String get settings => 'PARAMÈTRES';
@override
String get height => 'Taille';
@override
String get weight => 'Poids';
@override
String get age => 'Âge';
@override
String get gender => 'Sexe';
@override
String get genderMale => 'Masculin';
@override
String get genderFemale => 'Féminin';
@override
String get goalLoss => 'Perte de poids';
@override
String get goalMaintain => 'Maintien';
@override
String get goalGain => 'Prise de masse';
@override
String get activityLow => 'Faible';
@override
String get activityMedium => 'Moyenne';
@override
String get activityHigh => 'Élevée';
@override
String get calorieGoal => 'Objectif calorique';
@override
String get mealTypes => 'Types de repas';
@override
String get formulaNote => 'Calculé avec la formule de Mifflin-St Jeor';
@override
String get language => 'Langue';
@override
String get notSet => 'Non défini';
@override
String get calorieHint =>
'Saisissez les paramètres corporels pour calculer l\'objectif calorique';
@override
String get logout => 'Se déconnecter';
@override
String get editProfile => 'Modifier le profil';
@override
String get cancel => 'Annuler';
@override
String get save => 'Enregistrer';
@override
String get nameLabel => 'Nom';
@override
String get heightCm => 'Taille (cm)';
@override
String get weightKg => 'Poids (kg)';
@override
String get birthDate => 'Date de naissance';
@override
String get nameRequired => 'Saisir le nom';
@override
String get profileUpdated => 'Profil mis à jour';
@override
String get profileSaveFailed => 'Échec de l\'enregistrement';
@override
String get mealTypeBreakfast => 'Petit-déjeuner';
@override
String get mealTypeSecondBreakfast => 'Deuxième petit-déjeuner';
@override
String get mealTypeLunch => 'Déjeuner';
@override
String get mealTypeAfternoonSnack => 'Goûter';
@override
String get mealTypeDinner => 'Dîner';
@override
String get mealTypeSnack => 'Collation';
@override
String get navHome => 'Accueil';
@override
String get navProducts => 'Produits';
@override
String get navRecipes => 'Recettes';
}

View File

@@ -0,0 +1,290 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Hindi (`hi`).
class AppLocalizationsHi extends AppLocalizations {
AppLocalizationsHi([String locale = 'hi']) : 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 => 'कतार छोड़ें? अपग्रेड करें →';
@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 => 'रेसिपी';
}

View File

@@ -0,0 +1,290 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Italian (`it`).
class AppLocalizationsIt extends AppLocalizations {
AppLocalizationsIt([String locale = 'it']) : super(locale);
@override
String get appTitle => 'FoodAI';
@override
String get greetingMorning => 'Buongiorno';
@override
String get greetingAfternoon => 'Buon pomeriggio';
@override
String get greetingEvening => 'Buonasera';
@override
String get caloriesUnit => 'kcal';
@override
String get gramsUnit => 'g';
@override
String get goalLabel => 'obiettivo:';
@override
String get consumed => 'Consumato';
@override
String get remaining => 'Rimanente';
@override
String get exceeded => 'Superato';
@override
String get proteinLabel => 'Proteine';
@override
String get fatLabel => 'Grassi';
@override
String get carbsLabel => 'Carboidrati';
@override
String get today => 'Oggi';
@override
String get yesterday => 'Ieri';
@override
String get mealsSection => 'Pasti';
@override
String get addDish => 'Aggiungi piatto';
@override
String get scanDish => 'Scansiona';
@override
String get menu => 'Menu';
@override
String get dishHistory => 'Cronologia piatti';
@override
String get recommendCook => 'Consigliamo di cucinare';
@override
String get camera => 'Fotocamera';
@override
String get gallery => 'Galleria';
@override
String get analyzingPhoto => 'Analisi foto in corso...';
@override
String get inQueue => 'Sei in coda';
@override
String queuePosition(int position) {
return 'Posizione $position';
}
@override
String get processing => 'Elaborazione...';
@override
String get upgradePrompt => 'Salta la coda? Aggiorna →';
@override
String get recognitionFailed => 'Riconoscimento fallito. Riprova.';
@override
String get dishRecognition => 'Riconoscimento piatti';
@override
String get all => 'Tutti';
@override
String get dishRecognized => 'Piatto riconosciuto';
@override
String get recognizing => 'Riconoscimento in corso…';
@override
String get recognitionError => 'Errore di riconoscimento';
@override
String get dishResultTitle => 'Piatto riconosciuto';
@override
String get selectDish => 'Seleziona un piatto';
@override
String get dishNotRecognized => 'Piatto non riconosciuto';
@override
String get tryAgain => 'Riprova';
@override
String get nutritionApproximate =>
'I valori nutrizionali sono approssimativi — stimati dalla foto.';
@override
String get portion => 'Porzione';
@override
String get mealType => 'Tipo di pasto';
@override
String get dateLabel => 'Data';
@override
String get addToJournal => 'Aggiungi al diario';
@override
String get addFailed => 'Aggiunta fallita. Riprova.';
@override
String get historyTitle => 'Cronologia riconoscimenti';
@override
String get historyLoadError => 'Impossibile caricare la cronologia';
@override
String get retry => 'Riprova';
@override
String get noHistory => 'Nessun riconoscimento ancora';
@override
String get profileTitle => 'Profilo';
@override
String get edit => 'Modifica';
@override
String get bodyParams => 'PARAMETRI CORPOREI';
@override
String get goalActivity => 'OBIETTIVO & ATTIVITÀ';
@override
String get nutrition => 'NUTRIZIONE';
@override
String get settings => 'IMPOSTAZIONI';
@override
String get height => 'Altezza';
@override
String get weight => 'Peso';
@override
String get age => 'Età';
@override
String get gender => 'Sesso';
@override
String get genderMale => 'Maschio';
@override
String get genderFemale => 'Femmina';
@override
String get goalLoss => 'Perdita di peso';
@override
String get goalMaintain => 'Mantenimento';
@override
String get goalGain => 'Aumento muscolare';
@override
String get activityLow => 'Bassa';
@override
String get activityMedium => 'Media';
@override
String get activityHigh => 'Alta';
@override
String get calorieGoal => 'Obiettivo calorico';
@override
String get mealTypes => 'Tipi di pasto';
@override
String get formulaNote => 'Calcolato con la formula di Mifflin-St Jeor';
@override
String get language => 'Lingua';
@override
String get notSet => 'Non impostato';
@override
String get calorieHint =>
'Inserisci i parametri corporei per calcolare l\'obiettivo calorico';
@override
String get logout => 'Disconnetti';
@override
String get editProfile => 'Modifica profilo';
@override
String get cancel => 'Annulla';
@override
String get save => 'Salva';
@override
String get nameLabel => 'Nome';
@override
String get heightCm => 'Altezza (cm)';
@override
String get weightKg => 'Peso (kg)';
@override
String get birthDate => 'Data di nascita';
@override
String get nameRequired => 'Inserisci il nome';
@override
String get profileUpdated => 'Profilo aggiornato';
@override
String get profileSaveFailed => 'Salvataggio fallito';
@override
String get mealTypeBreakfast => 'Colazione';
@override
String get mealTypeSecondBreakfast => 'Seconda colazione';
@override
String get mealTypeLunch => 'Pranzo';
@override
String get mealTypeAfternoonSnack => 'Merenda';
@override
String get mealTypeDinner => 'Cena';
@override
String get mealTypeSnack => 'Spuntino';
@override
String get navHome => 'Home';
@override
String get navProducts => 'Prodotti';
@override
String get navRecipes => 'Ricette';
}

View File

@@ -0,0 +1,288 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Japanese (`ja`).
class AppLocalizationsJa extends AppLocalizations {
AppLocalizationsJa([String locale = 'ja']) : super(locale);
@override
String get appTitle => 'FoodAI';
@override
String get greetingMorning => 'おはようございます';
@override
String get greetingAfternoon => 'こんにちは';
@override
String get greetingEvening => 'こんばんは';
@override
String get caloriesUnit => 'kcal';
@override
String get gramsUnit => 'g';
@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 => '順番をスキップ?アップグレード →';
@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 => '身長cm';
@override
String get weightKg => '体重kg';
@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 => 'レシピ';
}

View File

@@ -0,0 +1,288 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Korean (`ko`).
class AppLocalizationsKo extends AppLocalizations {
AppLocalizationsKo([String locale = 'ko']) : super(locale);
@override
String get appTitle => 'FoodAI';
@override
String get greetingMorning => '좋은 아침이에요';
@override
String get greetingAfternoon => '안녕하세요';
@override
String get greetingEvening => '좋은 저녁이에요';
@override
String get caloriesUnit => 'kcal';
@override
String get gramsUnit => 'g';
@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 => '대기열 건너뛰기? 업그레이드 →';
@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 => 'Mifflin-St Jeor 공식으로 계산';
@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 => '키 (cm)';
@override
String get weightKg => '체중 (kg)';
@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 => '레시피';
}

View File

@@ -0,0 +1,290 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Portuguese (`pt`).
class AppLocalizationsPt extends AppLocalizations {
AppLocalizationsPt([String locale = 'pt']) : super(locale);
@override
String get appTitle => 'FoodAI';
@override
String get greetingMorning => 'Bom dia';
@override
String get greetingAfternoon => 'Boa tarde';
@override
String get greetingEvening => 'Boa noite';
@override
String get caloriesUnit => 'kcal';
@override
String get gramsUnit => 'g';
@override
String get goalLabel => 'meta:';
@override
String get consumed => 'Consumido';
@override
String get remaining => 'Restante';
@override
String get exceeded => 'Excedido';
@override
String get proteinLabel => 'Proteínas';
@override
String get fatLabel => 'Gorduras';
@override
String get carbsLabel => 'Carboidratos';
@override
String get today => 'Hoje';
@override
String get yesterday => 'Ontem';
@override
String get mealsSection => 'Refeições';
@override
String get addDish => 'Adicionar prato';
@override
String get scanDish => 'Escanear';
@override
String get menu => 'Menu';
@override
String get dishHistory => 'Histórico de pratos';
@override
String get recommendCook => 'Recomendamos cozinhar';
@override
String get camera => 'Câmera';
@override
String get gallery => 'Galeria';
@override
String get analyzingPhoto => 'Analisando foto...';
@override
String get inQueue => 'Você está na fila';
@override
String queuePosition(int position) {
return 'Posição $position';
}
@override
String get processing => 'Processando...';
@override
String get upgradePrompt => 'Pular a fila? Faça upgrade →';
@override
String get recognitionFailed => 'Reconhecimento falhou. Tente novamente.';
@override
String get dishRecognition => 'Reconhecimento de pratos';
@override
String get all => 'Todos';
@override
String get dishRecognized => 'Prato reconhecido';
@override
String get recognizing => 'Reconhecendo…';
@override
String get recognitionError => 'Erro de reconhecimento';
@override
String get dishResultTitle => 'Prato reconhecido';
@override
String get selectDish => 'Selecionar prato';
@override
String get dishNotRecognized => 'Prato não reconhecido';
@override
String get tryAgain => 'Tentar novamente';
@override
String get nutritionApproximate =>
'Os valores nutricionais são aproximados — estimados pela foto.';
@override
String get portion => 'Porção';
@override
String get mealType => 'Tipo de refeição';
@override
String get dateLabel => 'Data';
@override
String get addToJournal => 'Adicionar ao diário';
@override
String get addFailed => 'Falha ao adicionar. Tente novamente.';
@override
String get historyTitle => 'Histórico de reconhecimentos';
@override
String get historyLoadError => 'Falha ao carregar o histórico';
@override
String get retry => 'Tentar novamente';
@override
String get noHistory => 'Nenhum reconhecimento ainda';
@override
String get profileTitle => 'Perfil';
@override
String get edit => 'Editar';
@override
String get bodyParams => 'PARÂMETROS CORPORAIS';
@override
String get goalActivity => 'OBJETIVO & ATIVIDADE';
@override
String get nutrition => 'NUTRIÇÃO';
@override
String get settings => 'CONFIGURAÇÕES';
@override
String get height => 'Altura';
@override
String get weight => 'Peso';
@override
String get age => 'Idade';
@override
String get gender => 'Gênero';
@override
String get genderMale => 'Masculino';
@override
String get genderFemale => 'Feminino';
@override
String get goalLoss => 'Perda de peso';
@override
String get goalMaintain => 'Manutenção';
@override
String get goalGain => 'Ganho muscular';
@override
String get activityLow => 'Baixa';
@override
String get activityMedium => 'Média';
@override
String get activityHigh => 'Alta';
@override
String get calorieGoal => 'Meta calórica';
@override
String get mealTypes => 'Tipos de refeição';
@override
String get formulaNote => 'Calculado com a fórmula de Mifflin-St Jeor';
@override
String get language => 'Idioma';
@override
String get notSet => 'Não definido';
@override
String get calorieHint =>
'Insira os parâmetros corporais para calcular a meta calórica';
@override
String get logout => 'Sair';
@override
String get editProfile => 'Editar perfil';
@override
String get cancel => 'Cancelar';
@override
String get save => 'Salvar';
@override
String get nameLabel => 'Nome';
@override
String get heightCm => 'Altura (cm)';
@override
String get weightKg => 'Peso (kg)';
@override
String get birthDate => 'Data de nascimento';
@override
String get nameRequired => 'Insira o nome';
@override
String get profileUpdated => 'Perfil atualizado';
@override
String get profileSaveFailed => 'Falha ao salvar';
@override
String get mealTypeBreakfast => 'Café da manhã';
@override
String get mealTypeSecondBreakfast => 'Segundo café da manhã';
@override
String get mealTypeLunch => 'Almoço';
@override
String get mealTypeAfternoonSnack => 'Lanche da tarde';
@override
String get mealTypeDinner => 'Jantar';
@override
String get mealTypeSnack => 'Petisco';
@override
String get navHome => 'Início';
@override
String get navProducts => 'Produtos';
@override
String get navRecipes => 'Receitas';
}

View File

@@ -0,0 +1,289 @@
// 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 => 'Рецепты';
}

View File

@@ -0,0 +1,288 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Chinese (`zh`).
class AppLocalizationsZh extends AppLocalizations {
AppLocalizationsZh([String locale = 'zh']) : 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 => '跳过队列?升级 →';
@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 => '食谱';
}

100
client/lib/l10n/app_pt.arb Normal file
View File

@@ -0,0 +1,100 @@
{
"@@locale": "pt",
"appTitle": "FoodAI",
"greetingMorning": "Bom dia",
"greetingAfternoon": "Boa tarde",
"greetingEvening": "Boa noite",
"caloriesUnit": "kcal",
"gramsUnit": "g",
"goalLabel": "meta:",
"consumed": "Consumido",
"remaining": "Restante",
"exceeded": "Excedido",
"proteinLabel": "Proteínas",
"fatLabel": "Gorduras",
"carbsLabel": "Carboidratos",
"today": "Hoje",
"yesterday": "Ontem",
"mealsSection": "Refeições",
"addDish": "Adicionar prato",
"scanDish": "Escanear",
"menu": "Menu",
"dishHistory": "Histórico de pratos",
"recommendCook": "Recomendamos cozinhar",
"camera": "Câmera",
"gallery": "Galeria",
"analyzingPhoto": "Analisando foto...",
"inQueue": "Você está na fila",
"queuePosition": "Posição {position}",
"@queuePosition": {
"placeholders": {
"position": { "type": "int" }
}
},
"processing": "Processando...",
"upgradePrompt": "Pular a fila? Faça upgrade →",
"recognitionFailed": "Reconhecimento falhou. Tente novamente.",
"dishRecognition": "Reconhecimento de pratos",
"all": "Todos",
"dishRecognized": "Prato reconhecido",
"recognizing": "Reconhecendo…",
"recognitionError": "Erro de reconhecimento",
"dishResultTitle": "Prato reconhecido",
"selectDish": "Selecionar prato",
"dishNotRecognized": "Prato não reconhecido",
"tryAgain": "Tentar novamente",
"nutritionApproximate": "Os valores nutricionais são aproximados — estimados pela foto.",
"portion": "Porção",
"mealType": "Tipo de refeição",
"dateLabel": "Data",
"addToJournal": "Adicionar ao diário",
"addFailed": "Falha ao adicionar. Tente novamente.",
"historyTitle": "Histórico de reconhecimentos",
"historyLoadError": "Falha ao carregar o histórico",
"retry": "Tentar novamente",
"noHistory": "Nenhum reconhecimento ainda",
"profileTitle": "Perfil",
"edit": "Editar",
"bodyParams": "PARÂMETROS CORPORAIS",
"goalActivity": "OBJETIVO & ATIVIDADE",
"nutrition": "NUTRIÇÃO",
"settings": "CONFIGURAÇÕES",
"height": "Altura",
"weight": "Peso",
"age": "Idade",
"gender": "Gênero",
"genderMale": "Masculino",
"genderFemale": "Feminino",
"goalLoss": "Perda de peso",
"goalMaintain": "Manutenção",
"goalGain": "Ganho muscular",
"activityLow": "Baixa",
"activityMedium": "Média",
"activityHigh": "Alta",
"calorieGoal": "Meta calórica",
"mealTypes": "Tipos de refeição",
"formulaNote": "Calculado com a fórmula de Mifflin-St Jeor",
"language": "Idioma",
"notSet": "Não definido",
"calorieHint": "Insira os parâmetros corporais para calcular a meta calórica",
"logout": "Sair",
"editProfile": "Editar perfil",
"cancel": "Cancelar",
"save": "Salvar",
"nameLabel": "Nome",
"heightCm": "Altura (cm)",
"weightKg": "Peso (kg)",
"birthDate": "Data de nascimento",
"nameRequired": "Insira o nome",
"profileUpdated": "Perfil atualizado",
"profileSaveFailed": "Falha ao salvar",
"mealTypeBreakfast": "Café da manhã",
"mealTypeSecondBreakfast": "Segundo café da manhã",
"mealTypeLunch": "Almoço",
"mealTypeAfternoonSnack": "Lanche da tarde",
"mealTypeDinner": "Jantar",
"mealTypeSnack": "Petisco",
"navHome": "Início",
"navProducts": "Produtos",
"navRecipes": "Receitas"
}

100
client/lib/l10n/app_ru.arb Normal file
View File

@@ -0,0 +1,100 @@
{
"@@locale": "ru",
"appTitle": "FoodAI",
"greetingMorning": "Доброе утро",
"greetingAfternoon": "Добрый день",
"greetingEvening": "Добрый вечер",
"caloriesUnit": "ккал",
"gramsUnit": "г",
"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": "Хотите без очереди? Upgrade →",
"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": "Рост (см)",
"weightKg": "Вес (кг)",
"birthDate": "Дата рождения",
"nameRequired": "Введите имя",
"profileUpdated": "Профиль обновлён",
"profileSaveFailed": "Не удалось сохранить",
"mealTypeBreakfast": "Завтрак",
"mealTypeSecondBreakfast": "Второй завтрак",
"mealTypeLunch": "Обед",
"mealTypeAfternoonSnack": "Полдник",
"mealTypeDinner": "Ужин",
"mealTypeSnack": "Перекус",
"navHome": "Главная",
"navProducts": "Продукты",
"navRecipes": "Рецепты"
}

100
client/lib/l10n/app_zh.arb Normal file
View File

@@ -0,0 +1,100 @@
{
"@@locale": "zh",
"appTitle": "FoodAI",
"greetingMorning": "早上好",
"greetingAfternoon": "下午好",
"greetingEvening": "晚上好",
"caloriesUnit": "千卡",
"gramsUnit": "克",
"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": "身高(厘米)",
"weightKg": "体重(千克)",
"birthDate": "出生日期",
"nameRequired": "请输入姓名",
"profileUpdated": "资料已更新",
"profileSaveFailed": "保存失败",
"mealTypeBreakfast": "早餐",
"mealTypeSecondBreakfast": "第二早餐",
"mealTypeLunch": "午餐",
"mealTypeAfternoonSnack": "下午茶",
"mealTypeDinner": "晚餐",
"mealTypeSnack": "零食",
"navHome": "首页",
"navProducts": "食品",
"navRecipes": "食谱"
}

View File

@@ -1,9 +1,12 @@
import 'dart:ui' show PlatformDispatcher;
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'app.dart'; import 'app.dart';
import 'core/locale/language_provider.dart';
import 'core/storage/local_preferences.dart'; import 'core/storage/local_preferences.dart';
import 'core/storage/local_preferences_provider.dart'; import 'core/storage/local_preferences_provider.dart';
import 'firebase_options.dart'; import 'firebase_options.dart';
@@ -16,12 +19,22 @@ void main() async {
final sharedPreferences = await SharedPreferences.getInstance(); final sharedPreferences = await SharedPreferences.getInstance();
// Detect system language on first launch.
const supportedLanguageCodes = {
'en', 'ru', 'es', 'de', 'fr', 'it', 'pt', 'zh', 'ja', 'ko', 'ar', 'hi',
};
final systemLanguageCode = PlatformDispatcher.instance.locale.languageCode;
final initialLanguage = supportedLanguageCodes.contains(systemLanguageCode)
? systemLanguageCode
: 'en';
runApp( runApp(
ProviderScope( ProviderScope(
overrides: [ overrides: [
localPreferencesProvider.overrideWithValue( localPreferencesProvider.overrideWithValue(
LocalPreferences(sharedPreferences), LocalPreferences(sharedPreferences),
), ),
languageProvider.overrideWith((ref) => initialLanguage),
], ],
child: const App(), child: const App(),
), ),

View File

@@ -1,24 +1,24 @@
import 'package:food_ai/l10n/app_localizations.dart';
/// A configurable meal type that the user tracks throughout the day. /// A configurable meal type that the user tracks throughout the day.
class MealTypeOption { class MealTypeOption {
final String id; final String id;
final String label;
final String emoji; final String emoji;
const MealTypeOption({ const MealTypeOption({
required this.id, required this.id,
required this.label,
required this.emoji, required this.emoji,
}); });
} }
/// All meal types available for selection. /// All meal types available for selection.
const kAllMealTypes = [ const kAllMealTypes = [
MealTypeOption(id: 'breakfast', label: 'Завтрак', emoji: '🌅'), MealTypeOption(id: 'breakfast', emoji: '🌅'),
MealTypeOption(id: 'second_breakfast', label: 'Второй завтрак', emoji: ''), MealTypeOption(id: 'second_breakfast', emoji: ''),
MealTypeOption(id: 'lunch', label: 'Обед', emoji: '🍽️'), MealTypeOption(id: 'lunch', emoji: '🍽️'),
MealTypeOption(id: 'afternoon_snack', label: 'Полдник', emoji: '🥗'), MealTypeOption(id: 'afternoon_snack', emoji: '🥗'),
MealTypeOption(id: 'dinner', label: 'Ужин', emoji: '🌙'), MealTypeOption(id: 'dinner', emoji: '🌙'),
MealTypeOption(id: 'snack', label: 'Перекус', emoji: '🍎'), MealTypeOption(id: 'snack', emoji: '🍎'),
]; ];
/// Default meal type IDs assigned to new users. /// Default meal type IDs assigned to new users.
@@ -27,3 +27,14 @@ const kDefaultMealTypeIds = ['breakfast', 'lunch', 'dinner'];
/// Returns the [MealTypeOption] for the given [id], or null if not found. /// Returns the [MealTypeOption] for the given [id], or null if not found.
MealTypeOption? mealTypeById(String id) => MealTypeOption? mealTypeById(String id) =>
kAllMealTypes.where((option) => option.id == id).firstOrNull; kAllMealTypes.where((option) => option.id == id).firstOrNull;
/// Returns the localised label for the given meal type [id].
String mealTypeLabel(String id, AppLocalizations l10n) => switch (id) {
'breakfast' => l10n.mealTypeBreakfast,
'second_breakfast' => l10n.mealTypeSecondBreakfast,
'lunch' => l10n.mealTypeLunch,
'afternoon_snack' => l10n.mealTypeAfternoonSnack,
'dinner' => l10n.mealTypeDinner,
'snack' => l10n.mealTypeSnack,
_ => id,
};

View File

@@ -125,10 +125,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.1"
checked_yaml: checked_yaml:
dependency: transitive dependency: transitive
description: description:
@@ -350,6 +350,11 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0" version: "5.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@@ -592,6 +597,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.2" version: "0.2.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
io: io:
dependency: transitive dependency: transitive
description: description:
@@ -676,26 +689,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.17" version: "0.12.19"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.1" version: "0.13.0"
meta: meta:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.16.0" version: "1.17.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@@ -1057,10 +1070,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.6" version: "0.7.10"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:

View File

@@ -9,6 +9,9 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_localizations:
sdk: flutter
intl: ^0.20.1
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
@@ -52,3 +55,4 @@ dev_dependencies:
flutter: flutter:
uses-material-design: true uses-material-design: true
generate: true