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