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).
In Go, name variables to avoid shadowing the `error` built-in and the `context` package —
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_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/theme/app_theme.dart';
@@ -10,12 +12,16 @@ class App extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(routerProvider);
final languageCode = ref.watch(languageProvider);
return MaterialApp.router(
title: 'FoodAI',
theme: appTheme(),
routerConfig: router,
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:go_router/go_router.dart';
import '../../l10n/app_localizations.dart';
import '../auth/auth_provider.dart';
import '../../features/auth/login_screen.dart';
import '../../features/auth/register_screen.dart';
@@ -208,15 +210,17 @@ class MainShell extends ConsumerWidget {
orElse: () => 0,
);
final l10n = AppLocalizations.of(context)!;
return Scaffold(
body: child,
bottomNavigationBar: BottomNavigationBar(
currentIndex: currentIndex,
onTap: (index) => context.go(_tabs[index]),
items: [
const BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Главная',
BottomNavigationBarItem(
icon: const Icon(Icons.home),
label: l10n.navHome,
),
BottomNavigationBarItem(
icon: Badge(
@@ -224,19 +228,19 @@ class MainShell extends ConsumerWidget {
label: Text('$expiringCount'),
child: const Icon(Icons.kitchen),
),
label: 'Продукты',
label: l10n.navProducts,
),
const BottomNavigationBarItem(
icon: Icon(Icons.calendar_month),
label: 'Меню',
BottomNavigationBarItem(
icon: const Icon(Icons.calendar_month),
label: l10n.menu,
),
const BottomNavigationBarItem(
icon: Icon(Icons.menu_book),
label: 'Рецепты',
BottomNavigationBarItem(
icon: const Icon(Icons.menu_book),
label: l10n.navRecipes,
),
const BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'Профиль',
BottomNavigationBarItem(
icon: const Icon(Icons.person),
label: l10n.profileTitle,
),
],
),

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:food_ai/l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/storage/local_preferences_provider.dart';
@@ -17,6 +18,8 @@ class DishResultSheet extends ConsumerStatefulWidget {
required this.onAdded,
this.preselectedMealType,
this.jobId,
this.targetDate,
this.createdAt,
});
final DishResult dish;
@@ -24,6 +27,13 @@ class DishResultSheet extends ConsumerStatefulWidget {
final String? preselectedMealType;
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
ConsumerState<DishResultSheet> createState() => _DishResultSheetState();
}
@@ -32,6 +42,7 @@ class _DishResultSheetState extends ConsumerState<DishResultSheet> {
late int _selectedIndex;
late int _portionGrams;
late String _mealType;
late DateTime _selectedDiaryDate;
bool _saving = false;
final TextEditingController _portionController = TextEditingController();
@@ -43,9 +54,19 @@ class _DishResultSheetState extends ConsumerState<DishResultSheet> {
_portionGrams = widget.dish.candidates.isNotEmpty
? widget.dish.candidates.first.weightGrams
: 300;
_mealType = widget.preselectedMealType ??
kAllMealTypes.first.id;
_mealType = widget.preselectedMealType ?? kAllMealTypes.first.id;
_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
@@ -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) {
final parsed = int.tryParse(value);
if (parsed != null && parsed >= 10) {
@@ -90,8 +122,8 @@ class _DishResultSheetState extends ConsumerState<DishResultSheet> {
if (_saving) return;
setState(() => _saving = true);
final selectedDate = ref.read(selectedDateProvider);
final dateString = formatDateForDiary(selectedDate);
final l10n = AppLocalizations.of(context)!;
final dateString = formatDateForDiary(_selectedDiaryDate);
final scaledCalories = _scale(_selected.calories);
final scaledProtein = _scale(_selected.proteinG);
@@ -120,7 +152,7 @@ class _DishResultSheetState extends ConsumerState<DishResultSheet> {
if (mounted) {
setState(() => _saving = false);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Не удалось добавить. Попробуйте ещё раз.')),
SnackBar(content: Text(l10n.addFailed)),
);
}
}
@@ -128,6 +160,7 @@ class _DishResultSheetState extends ConsumerState<DishResultSheet> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context);
final hasCandidates = widget.dish.candidates.isNotEmpty;
@@ -150,7 +183,7 @@ class _DishResultSheetState extends ConsumerState<DishResultSheet> {
padding: const EdgeInsets.fromLTRB(20, 0, 8, 0),
child: Row(
children: [
Text('Распознано блюдо', style: theme.textTheme.titleMedium),
Text(l10n.dishResultTitle, style: theme.textTheme.titleMedium),
const Spacer(),
IconButton(
icon: const Icon(Icons.close),
@@ -179,7 +212,7 @@ class _DishResultSheetState extends ConsumerState<DishResultSheet> {
),
const SizedBox(height: 8),
Text(
'КБЖУ приблизительные — определены по фото.',
l10n.nutritionApproximate,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
@@ -199,6 +232,11 @@ class _DishResultSheetState extends ConsumerState<DishResultSheet> {
if (value != null) setState(() => _mealType = value);
},
),
const SizedBox(height: 20),
_DatePickerRow(
date: _selectedDiaryDate,
onTap: _pickDate,
),
const SizedBox(height: 16),
],
)
@@ -207,13 +245,13 @@ class _DishResultSheetState extends ConsumerState<DishResultSheet> {
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Блюдо не распознано',
l10n.dishNotRecognized,
style: theme.textTheme.titleMedium,
),
const SizedBox(height: 8),
FilledButton(
onPressed: () => Navigator.pop(context),
child: const Text('Попробовать снова'),
child: Text(l10n.tryAgain),
),
],
),
@@ -232,7 +270,7 @@ class _DishResultSheetState extends ConsumerState<DishResultSheet> {
width: 20,
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
// ---------------------------------------------------------------------------
@@ -258,11 +337,12 @@ class _CandidatesSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Выберите блюдо', style: theme.textTheme.titleMedium),
Text(l10n.selectDish, style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
...candidates.asMap().entries.map((entry) {
final index = entry.key;
@@ -378,6 +458,7 @@ class _NutritionCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context);
return Card(
child: Padding(
@@ -388,7 +469,7 @@ class _NutritionCard extends StatelessWidget {
Row(
children: [
Text(
'${calories.toInt()} ккал',
'${calories.toInt()} ${l10n.caloriesUnit}',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
@@ -396,7 +477,7 @@ class _NutritionCard extends StatelessWidget {
),
const Spacer(),
Tooltip(
message: 'Приблизительные значения на основе фото',
message: l10n.nutritionApproximate,
child: Text(
'',
style: theme.textTheme.titleLarge?.copyWith(
@@ -411,18 +492,18 @@ class _NutritionCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_MacroChip(
label: 'Белки',
value: '${proteinG.toStringAsFixed(1)} г',
label: l10n.proteinLabel,
value: '${proteinG.toStringAsFixed(1)} ${l10n.gramsUnit}',
color: Colors.blue,
),
_MacroChip(
label: 'Жиры',
value: '${fatG.toStringAsFixed(1)} г',
label: l10n.fatLabel,
value: '${fatG.toStringAsFixed(1)} ${l10n.gramsUnit}',
color: Colors.orange,
),
_MacroChip(
label: 'Углеводы',
value: '${carbsG.toStringAsFixed(1)} г',
label: l10n.carbsLabel,
value: '${carbsG.toStringAsFixed(1)} ${l10n.gramsUnit}',
color: Colors.green,
),
],
@@ -482,11 +563,12 @@ class _PortionRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Порция', style: theme.textTheme.titleSmall),
Text(l10n.portion, style: theme.textTheme.titleSmall),
const SizedBox(height: 8),
Row(
children: [
@@ -502,9 +584,9 @@ class _PortionRow extends StatelessWidget {
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: onChanged,
decoration: const InputDecoration(
suffixText: 'г',
border: OutlineInputBorder(),
decoration: InputDecoration(
suffixText: l10n.gramsUnit,
border: const OutlineInputBorder(),
),
),
),
@@ -535,11 +617,12 @@ class _MealTypeDropdown extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Приём пищи', style: theme.textTheme.titleSmall),
Text(l10n.mealType, style: theme.textTheme.titleSmall),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
initialValue: selected,
@@ -550,7 +633,7 @@ class _MealTypeDropdown extends StatelessWidget {
.map((mealTypeOption) => DropdownMenuItem(
value: mealTypeOption.id,
child: Text(
'${mealTypeOption.emoji} ${mealTypeOption.label}'),
'${mealTypeOption.emoji} ${mealTypeLabel(mealTypeOption.id, l10n)}'),
))
.toList(),
onChanged: onChanged,

View File

@@ -1,21 +1,23 @@
import 'package:flutter/material.dart';
import 'package:food_ai/l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../home/home_provider.dart';
import '../scan/dish_result_screen.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 {
const RecognitionHistoryScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final jobsState = ref.watch(todayJobsProvider);
final l10n = AppLocalizations.of(context)!;
final jobsState = ref.watch(allJobsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('История распознавания'),
title: Text(l10n.historyTitle),
),
body: jobsState.when(
loading: () => const Center(child: CircularProgressIndicator()),
@@ -23,20 +25,18 @@ class RecognitionHistoryScreen extends ConsumerWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Не удалось загрузить историю'),
Text(l10n.historyLoadError),
const SizedBox(height: 12),
FilledButton(
onPressed: () => ref.invalidate(todayJobsProvider),
child: const Text('Повторить'),
onPressed: () => ref.invalidate(allJobsProvider),
child: Text(l10n.retry),
),
],
),
),
data: (jobs) {
if (jobs.isEmpty) {
return const Center(
child: Text('Нет распознаваний за сегодня'),
);
return Center(child: Text(l10n.noHistory));
}
return ListView.separated(
padding: const EdgeInsets.all(16),
@@ -58,6 +58,7 @@ class _HistoryJobTile extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context);
final isDone = job.status == 'done';
@@ -84,11 +85,11 @@ class _HistoryJobTile extends ConsumerWidget {
final String titleText;
if (isDone) {
titleText = dishName ?? 'Блюдо распознано';
titleText = dishName ?? l10n.dishRecognized;
} else if (isProcessing) {
titleText = 'Распознаётся…';
titleText = l10n.recognizing;
} else {
titleText = 'Ошибка распознавания';
titleText = l10n.recognitionError;
}
final contextParts = [
@@ -118,6 +119,8 @@ class _HistoryJobTile extends ConsumerWidget {
dish: job.result!,
preselectedMealType: job.targetMealType,
jobId: job.id,
targetDate: job.targetDate,
createdAt: job.createdAt,
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.
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
.map((element) =>
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:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'app.dart';
import 'core/locale/language_provider.dart';
import 'core/storage/local_preferences.dart';
import 'core/storage/local_preferences_provider.dart';
import 'firebase_options.dart';
@@ -16,12 +19,22 @@ void main() async {
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(
ProviderScope(
overrides: [
localPreferencesProvider.overrideWithValue(
LocalPreferences(sharedPreferences),
),
languageProvider.overrideWith((ref) => initialLanguage),
],
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.
class MealTypeOption {
final String id;
final String label;
final String emoji;
const MealTypeOption({
required this.id,
required this.label,
required this.emoji,
});
}
/// All meal types available for selection.
const kAllMealTypes = [
MealTypeOption(id: 'breakfast', label: 'Завтрак', emoji: '🌅'),
MealTypeOption(id: 'second_breakfast', label: 'Второй завтрак', emoji: ''),
MealTypeOption(id: 'lunch', label: 'Обед', emoji: '🍽️'),
MealTypeOption(id: 'afternoon_snack', label: 'Полдник', emoji: '🥗'),
MealTypeOption(id: 'dinner', label: 'Ужин', emoji: '🌙'),
MealTypeOption(id: 'snack', label: 'Перекус', emoji: '🍎'),
MealTypeOption(id: 'breakfast', emoji: '🌅'),
MealTypeOption(id: 'second_breakfast', emoji: ''),
MealTypeOption(id: 'lunch', emoji: '🍽️'),
MealTypeOption(id: 'afternoon_snack', emoji: '🥗'),
MealTypeOption(id: 'dinner', emoji: '🌙'),
MealTypeOption(id: 'snack', emoji: '🍎'),
];
/// 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.
MealTypeOption? mealTypeById(String id) =>
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
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
checked_yaml:
dependency: transitive
description:
@@ -350,6 +350,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@@ -592,6 +597,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
io:
dependency: transitive
description:
@@ -676,26 +689,26 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.16.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -1057,10 +1070,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.6"
version: "0.7.10"
typed_data:
dependency: transitive
description:

View File

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