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