From 54b10d51e2c70b19312201fd143a5db60b0fc393 Mon Sep 17 00:00:00 2001 From: dbastrikin Date: Thu, 19 Mar 2026 22:22:52 +0200 Subject: [PATCH] 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 --- CLAUDE.md | 50 ++ client/l10n.yaml | 4 + client/lib/app.dart | 6 + client/lib/core/router/app_router.dart | 30 +- client/lib/features/home/home_provider.dart | 20 + client/lib/features/home/home_screen.dart | 143 ++-- .../onboarding/onboarding_screen.dart | 4 +- .../lib/features/profile/profile_screen.dart | 272 +++---- .../lib/features/scan/dish_result_screen.dart | 133 +++- .../scan/recognition_history_screen.dart | 27 +- .../features/scan/recognition_service.dart | 11 +- client/lib/l10n/app_ar.arb | 100 +++ client/lib/l10n/app_de.arb | 100 +++ client/lib/l10n/app_en.arb | 100 +++ client/lib/l10n/app_es.arb | 100 +++ client/lib/l10n/app_fr.arb | 100 +++ client/lib/l10n/app_hi.arb | 100 +++ client/lib/l10n/app_it.arb | 100 +++ client/lib/l10n/app_ja.arb | 100 +++ client/lib/l10n/app_ko.arb | 100 +++ client/lib/l10n/app_localizations.dart | 738 ++++++++++++++++++ client/lib/l10n/app_localizations_ar.dart | 289 +++++++ client/lib/l10n/app_localizations_de.dart | 290 +++++++ client/lib/l10n/app_localizations_en.dart | 289 +++++++ client/lib/l10n/app_localizations_es.dart | 290 +++++++ client/lib/l10n/app_localizations_fr.dart | 290 +++++++ client/lib/l10n/app_localizations_hi.dart | 290 +++++++ client/lib/l10n/app_localizations_it.dart | 290 +++++++ client/lib/l10n/app_localizations_ja.dart | 288 +++++++ client/lib/l10n/app_localizations_ko.dart | 288 +++++++ client/lib/l10n/app_localizations_pt.dart | 290 +++++++ client/lib/l10n/app_localizations_ru.dart | 289 +++++++ client/lib/l10n/app_localizations_zh.dart | 288 +++++++ client/lib/l10n/app_pt.arb | 100 +++ client/lib/l10n/app_ru.arb | 100 +++ client/lib/l10n/app_zh.arb | 100 +++ client/lib/main.dart | 13 + client/lib/shared/models/meal_type.dart | 27 +- client/pubspec.lock | 33 +- client/pubspec.yaml | 4 + 40 files changed, 5919 insertions(+), 267 deletions(-) create mode 100644 client/l10n.yaml create mode 100644 client/lib/l10n/app_ar.arb create mode 100644 client/lib/l10n/app_de.arb create mode 100644 client/lib/l10n/app_en.arb create mode 100644 client/lib/l10n/app_es.arb create mode 100644 client/lib/l10n/app_fr.arb create mode 100644 client/lib/l10n/app_hi.arb create mode 100644 client/lib/l10n/app_it.arb create mode 100644 client/lib/l10n/app_ja.arb create mode 100644 client/lib/l10n/app_ko.arb create mode 100644 client/lib/l10n/app_localizations.dart create mode 100644 client/lib/l10n/app_localizations_ar.dart create mode 100644 client/lib/l10n/app_localizations_de.dart create mode 100644 client/lib/l10n/app_localizations_en.dart create mode 100644 client/lib/l10n/app_localizations_es.dart create mode 100644 client/lib/l10n/app_localizations_fr.dart create mode 100644 client/lib/l10n/app_localizations_hi.dart create mode 100644 client/lib/l10n/app_localizations_it.dart create mode 100644 client/lib/l10n/app_localizations_ja.dart create mode 100644 client/lib/l10n/app_localizations_ko.dart create mode 100644 client/lib/l10n/app_localizations_pt.dart create mode 100644 client/lib/l10n/app_localizations_ru.dart create mode 100644 client/lib/l10n/app_localizations_zh.dart create mode 100644 client/lib/l10n/app_pt.arb create mode 100644 client/lib/l10n/app_ru.arb create mode 100644 client/lib/l10n/app_zh.arb diff --git a/CLAUDE.md b/CLAUDE.md index 87be1a0..80d51b7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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.` 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. diff --git a/client/l10n.yaml b/client/l10n.yaml new file mode 100644 index 0000000..1437ccc --- /dev/null +++ b/client/l10n.yaml @@ -0,0 +1,4 @@ +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart +output-class: AppLocalizations diff --git a/client/lib/app.dart b/client/lib/app.dart index 769bdb7..b7608b2 100644 --- a/client/lib/app.dart +++ b/client/lib/app.dart @@ -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, ); } } diff --git a/client/lib/core/router/app_router.dart b/client/lib/core/router/app_router.dart index 566a7e4..d224f7a 100644 --- a/client/lib/core/router/app_router.dart +++ b/client/lib/core/router/app_router.dart @@ -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, ), ], ), diff --git a/client/lib/features/home/home_provider.dart b/client/lib/features/home/home_provider.dart index 347087f..c33f199 100644 --- a/client/lib/features/home/home_provider.dart +++ b/client/lib/features/home/home_provider.dart @@ -56,3 +56,23 @@ final todayJobsProvider = StateNotifierProvider>>( (ref) => TodayJobsNotifier(ref.read(recognitionServiceProvider)), ); + +// ── All recognition jobs (history screen) ───────────────────── + +class AllJobsNotifier extends StateNotifier>> { + final RecognitionService _service; + + AllJobsNotifier(this._service) : super(const AsyncValue.loading()) { + load(); + } + + Future load() async { + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() => _service.listAllJobs()); + } +} + +final allJobsProvider = + StateNotifierProvider>>( + (ref) => AllJobsNotifier(ref.read(recognitionServiceProvider)), +); diff --git a/client/lib/features/home/home_screen.dart b/client/lib/features/home/home_screen.dart index 5451375..30f20c3 100644 --- a/client/lib/features/home/home_screen.dart +++ b/client/lib/features/home/home_screen.dart @@ -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 _pickAndShowDishResult( WidgetRef ref, String mealTypeId, ) async { + final l10n = AppLocalizations.of(context)!; + // 1. Choose image source. final source = await showModalBottomSheet( context: context, @@ -752,12 +754,12 @@ Future _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 _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 _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 _pickAndShowDishResult( dish: event.result, preselectedMealType: mealTypeId.isNotEmpty ? mealTypeId : null, jobId: jobCreated.jobId, + targetDate: targetDate, onAdded: () => Navigator.pop(sheetContext), ), ); @@ -836,7 +839,7 @@ Future _pickAndShowDishResult( SnackBar( content: Text(event.error), action: SnackBarAction( - label: 'Повторить', + label: l10n.retry, onPressed: () => _pickAndShowDishResult(context, ref, mealTypeId), ), ), @@ -849,9 +852,7 @@ Future _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( 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), ), diff --git a/client/lib/features/onboarding/onboarding_screen.dart b/client/lib/features/onboarding/onboarding_screen.dart index c9fa291..c268744 100644 --- a/client/lib/features/onboarding/onboarding_screen.dart +++ b/client/lib/features/onboarding/onboarding_screen.dart @@ -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 diff --git a/client/lib/features/profile/profile_screen.dart b/client/lib/features/profile/profile_screen.dart index efd12d1..6df1ce6 100644 --- a/client/lib/features/profile/profile_screen.dart +++ b/client/lib/features/profile/profile_screen.dart @@ -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 _confirmLogout(BuildContext context, WidgetRef ref) async { + final l10n = AppLocalizations.of(context)!; final confirmed = await showDialog( 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 { final _formKey = GlobalKey(); - 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 { @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.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.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 _pickDob() async { final now = DateTime.now(); @@ -395,13 +405,14 @@ class _EditProfileSheetState extends ConsumerState { } Future _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 { 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 { 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 { 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 { 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 { 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 '100–250'; } return null; @@ -512,19 +524,18 @@ class _EditProfileSheetState extends ConsumerState { 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 '30–300'; } return null; @@ -539,14 +550,13 @@ class _EditProfileSheetState extends ConsumerState { 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 { const SizedBox(height: 20), // Gender - Text('Пол', style: theme.textTheme.labelMedium), + Text(l10n.gender, style: theme.textTheme.labelMedium), const SizedBox(height: 8), SegmentedButton( - 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( - 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 { Text('Активность', style: theme.textTheme.labelMedium), const SizedBox(height: 8), SegmentedButton( - 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 { _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 { // Language ref.watch(supportedLanguagesProvider).when( data: (languages) => DropdownButtonFormField( - 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 { child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white), ) - : const Text('Сохранить'), + : Text(l10n.save), ), ], ), diff --git a/client/lib/features/scan/dish_result_screen.dart b/client/lib/features/scan/dish_result_screen.dart index bcb1760..190bb28 100644 --- a/client/lib/features/scan/dish_result_screen.dart +++ b/client/lib/features/scan/dish_result_screen.dart @@ -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 createState() => _DishResultSheetState(); } @@ -32,6 +42,7 @@ class _DishResultSheetState extends ConsumerState { 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 { _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 { }); } + Future _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 { 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 { 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 { @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 { 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 { ), const SizedBox(height: 8), Text( - 'КБЖУ приблизительные — определены по фото.', + l10n.nutritionApproximate, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -199,6 +232,11 @@ class _DishResultSheetState extends ConsumerState { 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 { 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 { width: 20, child: CircularProgressIndicator(strokeWidth: 2), ) - : const Text('Добавить в журнал'), + : Text(l10n.addToJournal), ), ), ), @@ -241,6 +279,47 @@ class _DishResultSheetState extends ConsumerState { } } +// --------------------------------------------------------------------------- +// 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( 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, diff --git a/client/lib/features/scan/recognition_history_screen.dart b/client/lib/features/scan/recognition_history_screen.dart index 71189cc..8ef6449 100644 --- a/client/lib/features/scan/recognition_history_screen.dart +++ b/client/lib/features/scan/recognition_history_screen.dart @@ -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), ), ); diff --git a/client/lib/features/scan/recognition_service.dart b/client/lib/features/scan/recognition_service.dart index 1b42ae6..590bf05 100644 --- a/client/lib/features/scan/recognition_service.dart +++ b/client/lib/features/scan/recognition_service.dart @@ -282,7 +282,16 @@ class RecognitionService { /// Returns today's recognition jobs that have not yet been linked to a diary entry. Future> listTodayUnlinkedJobs() async { - final data = await _client.get('/ai/jobs') as List; + final data = await _client.getList('/ai/jobs'); + return data + .map((element) => + DishJobSummary.fromJson(element as Map)) + .toList(); + } + + /// Returns all recognition jobs for the current user, newest first. + Future> listAllJobs() async { + final data = await _client.getList('/ai/jobs/history'); return data .map((element) => DishJobSummary.fromJson(element as Map)) diff --git a/client/lib/l10n/app_ar.arb b/client/lib/l10n/app_ar.arb new file mode 100644 index 0000000..b6bcebb --- /dev/null +++ b/client/lib/l10n/app_ar.arb @@ -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": "الوصفات" +} diff --git a/client/lib/l10n/app_de.arb b/client/lib/l10n/app_de.arb new file mode 100644 index 0000000..664ae23 --- /dev/null +++ b/client/lib/l10n/app_de.arb @@ -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" +} diff --git a/client/lib/l10n/app_en.arb b/client/lib/l10n/app_en.arb new file mode 100644 index 0000000..441a71d --- /dev/null +++ b/client/lib/l10n/app_en.arb @@ -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" +} diff --git a/client/lib/l10n/app_es.arb b/client/lib/l10n/app_es.arb new file mode 100644 index 0000000..8759694 --- /dev/null +++ b/client/lib/l10n/app_es.arb @@ -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" +} diff --git a/client/lib/l10n/app_fr.arb b/client/lib/l10n/app_fr.arb new file mode 100644 index 0000000..a136d19 --- /dev/null +++ b/client/lib/l10n/app_fr.arb @@ -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" +} diff --git a/client/lib/l10n/app_hi.arb b/client/lib/l10n/app_hi.arb new file mode 100644 index 0000000..cd4c153 --- /dev/null +++ b/client/lib/l10n/app_hi.arb @@ -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": "रेसिपी" +} diff --git a/client/lib/l10n/app_it.arb b/client/lib/l10n/app_it.arb new file mode 100644 index 0000000..1c6e770 --- /dev/null +++ b/client/lib/l10n/app_it.arb @@ -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" +} diff --git a/client/lib/l10n/app_ja.arb b/client/lib/l10n/app_ja.arb new file mode 100644 index 0000000..e37130e --- /dev/null +++ b/client/lib/l10n/app_ja.arb @@ -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": "レシピ" +} diff --git a/client/lib/l10n/app_ko.arb b/client/lib/l10n/app_ko.arb new file mode 100644 index 0000000..a0c8542 --- /dev/null +++ b/client/lib/l10n/app_ko.arb @@ -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": "레시피" +} diff --git a/client/lib/l10n/app_localizations.dart b/client/lib/l10n/app_localizations.dart new file mode 100644 index 0000000..3921ace --- /dev/null +++ b/client/lib/l10n/app_localizations.dart @@ -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, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s 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(context, AppLocalizations); + } + + static const LocalizationsDelegate 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> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + 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 { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => [ + '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.', + ); +} diff --git a/client/lib/l10n/app_localizations_ar.dart b/client/lib/l10n/app_localizations_ar.dart new file mode 100644 index 0000000..7eec8fe --- /dev/null +++ b/client/lib/l10n/app_localizations_ar.dart @@ -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 => 'الوصفات'; +} diff --git a/client/lib/l10n/app_localizations_de.dart b/client/lib/l10n/app_localizations_de.dart new file mode 100644 index 0000000..5d5b13d --- /dev/null +++ b/client/lib/l10n/app_localizations_de.dart @@ -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'; +} diff --git a/client/lib/l10n/app_localizations_en.dart b/client/lib/l10n/app_localizations_en.dart new file mode 100644 index 0000000..919b09c --- /dev/null +++ b/client/lib/l10n/app_localizations_en.dart @@ -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'; +} diff --git a/client/lib/l10n/app_localizations_es.dart b/client/lib/l10n/app_localizations_es.dart new file mode 100644 index 0000000..a113dbd --- /dev/null +++ b/client/lib/l10n/app_localizations_es.dart @@ -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'; +} diff --git a/client/lib/l10n/app_localizations_fr.dart b/client/lib/l10n/app_localizations_fr.dart new file mode 100644 index 0000000..de40238 --- /dev/null +++ b/client/lib/l10n/app_localizations_fr.dart @@ -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'; +} diff --git a/client/lib/l10n/app_localizations_hi.dart b/client/lib/l10n/app_localizations_hi.dart new file mode 100644 index 0000000..d1b28c5 --- /dev/null +++ b/client/lib/l10n/app_localizations_hi.dart @@ -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 => 'रेसिपी'; +} diff --git a/client/lib/l10n/app_localizations_it.dart b/client/lib/l10n/app_localizations_it.dart new file mode 100644 index 0000000..e141f74 --- /dev/null +++ b/client/lib/l10n/app_localizations_it.dart @@ -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'; +} diff --git a/client/lib/l10n/app_localizations_ja.dart b/client/lib/l10n/app_localizations_ja.dart new file mode 100644 index 0000000..42e56d2 --- /dev/null +++ b/client/lib/l10n/app_localizations_ja.dart @@ -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 => 'レシピ'; +} diff --git a/client/lib/l10n/app_localizations_ko.dart b/client/lib/l10n/app_localizations_ko.dart new file mode 100644 index 0000000..1677d25 --- /dev/null +++ b/client/lib/l10n/app_localizations_ko.dart @@ -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 => '레시피'; +} diff --git a/client/lib/l10n/app_localizations_pt.dart b/client/lib/l10n/app_localizations_pt.dart new file mode 100644 index 0000000..49f8579 --- /dev/null +++ b/client/lib/l10n/app_localizations_pt.dart @@ -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'; +} diff --git a/client/lib/l10n/app_localizations_ru.dart b/client/lib/l10n/app_localizations_ru.dart new file mode 100644 index 0000000..a196ad6 --- /dev/null +++ b/client/lib/l10n/app_localizations_ru.dart @@ -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 => 'Рецепты'; +} diff --git a/client/lib/l10n/app_localizations_zh.dart b/client/lib/l10n/app_localizations_zh.dart new file mode 100644 index 0000000..d47d2c5 --- /dev/null +++ b/client/lib/l10n/app_localizations_zh.dart @@ -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 => '食谱'; +} diff --git a/client/lib/l10n/app_pt.arb b/client/lib/l10n/app_pt.arb new file mode 100644 index 0000000..2a436b5 --- /dev/null +++ b/client/lib/l10n/app_pt.arb @@ -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" +} diff --git a/client/lib/l10n/app_ru.arb b/client/lib/l10n/app_ru.arb new file mode 100644 index 0000000..2dd9571 --- /dev/null +++ b/client/lib/l10n/app_ru.arb @@ -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": "Рецепты" +} diff --git a/client/lib/l10n/app_zh.arb b/client/lib/l10n/app_zh.arb new file mode 100644 index 0000000..f31399d --- /dev/null +++ b/client/lib/l10n/app_zh.arb @@ -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": "食谱" +} diff --git a/client/lib/main.dart b/client/lib/main.dart index 802c0e3..4bc4067 100644 --- a/client/lib/main.dart +++ b/client/lib/main.dart @@ -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(), ), diff --git a/client/lib/shared/models/meal_type.dart b/client/lib/shared/models/meal_type.dart index c5958aa..3032708 100644 --- a/client/lib/shared/models/meal_type.dart +++ b/client/lib/shared/models/meal_type.dart @@ -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, +}; diff --git a/client/pubspec.lock b/client/pubspec.lock index ed85303..5a1e04c 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -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: diff --git a/client/pubspec.yaml b/client/pubspec.yaml index a634a08..9df4d46 100644 --- a/client/pubspec.yaml +++ b/client/pubspec.yaml @@ -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