From fe67189ac723aded314269510961afd9a8a43959 Mon Sep 17 00:00:00 2001 From: Sanders Date: Wed, 27 May 2026 10:36:03 +0300 Subject: [PATCH] Locale --- l10n.yaml | 3 + lib/l10n/app_en.arb | 67 +++++ lib/l10n/app_localizations.dart | 272 ++++++++++++++++++ lib/l10n/app_localizations_en.dart | 100 +++++++ lib/l10n/app_localizations_ru.dart | 104 +++++++ lib/l10n/app_ru.arb | 67 +++++ lib/src/app/app.dart | 10 +- lib/src/app/l10n/l10n.dart | 8 + .../presentation/screens/accounts_screen.dart | 8 +- .../screens/analytics_screen.dart | 8 +- .../presentation/screens/home_screen.dart | 28 +- .../presentation/widgets/account_tabs.dart | 3 +- .../widgets/category_donut_card.dart | 17 +- .../presentation/widgets/month_header.dart | 7 +- .../presentation/widgets/month_kpi_card.dart | 8 +- .../widgets/transactions_section.dart | 19 +- .../presentation/screens/profile_screen.dart | 10 +- lib/src/shared/widgets/app_bottom_nav.dart | 20 +- .../shared/widgets/placeholder_screen.dart | 3 +- pubspec.yaml | 1 + 20 files changed, 696 insertions(+), 67 deletions(-) create mode 100644 l10n.yaml create mode 100644 lib/l10n/app_en.arb create mode 100644 lib/l10n/app_localizations.dart create mode 100644 lib/l10n/app_localizations_en.dart create mode 100644 lib/l10n/app_localizations_ru.dart create mode 100644 lib/l10n/app_ru.arb create mode 100644 lib/src/app/l10n/l10n.dart diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..8cad7e6 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,3 @@ +arb-dir: lib/l10n +template-arb-file: app_ru.arb +output-localization-file: app_localizations.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..2fc4a73 --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,67 @@ +{ + "@@locale": "en", + + "appTitle": "NewBudget", + + "navHome": "Home", + "navAnalytics": "Analytics", + "navAccounts": "Accounts", + "navProfile": "Profile", + + "allAccounts": "All", + + "comingSoon": "Coming soon", + + "budget": "BUDGET", + "balance": "BALANCE", + "income": "INCOME", + "expenses": "EXPENSES", + + "transactions": "Transactions", + "transactionCount": "{count, plural, one{{count} transaction} other{{count} transactions}}", + "@transactionCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + + "allCategoriesFilter": "All categories · all types", + + "moreCategories": "{count, plural, one{+ {count} more →} other{+ {count} more →}}", + "@moreCategories": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + + "todayWithDate": "Today · {date}", + "@todayWithDate": { + "placeholders": { + "date": { + "type": "String" + } + } + }, + + "yesterdayWithDate": "Yesterday · {date}", + "@yesterdayWithDate": { + "placeholders": { + "date": { + "type": "String" + } + } + }, + + "analyticsTitle": "Reports", + "analyticsSubtitle": "Analytics", + + "accountsSubtitle": "Management", + + "profileSubtitle": "Settings", + "darkTheme": "Dark theme", + "profileHint": "User profile, currency, locale and other settings will appear here." +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart new file mode 100644 index 0000000..9ee8bc1 --- /dev/null +++ b/lib/l10n/app_localizations.dart @@ -0,0 +1,272 @@ +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_en.dart'; +import 'app_localizations_ru.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('en'), + Locale('ru'), + ]; + + /// No description provided for @appTitle. + /// + /// In ru, this message translates to: + /// **'NewBudget'** + String get appTitle; + + /// No description provided for @navHome. + /// + /// In ru, this message translates to: + /// **'Главная'** + String get navHome; + + /// No description provided for @navAnalytics. + /// + /// In ru, this message translates to: + /// **'Аналитика'** + String get navAnalytics; + + /// No description provided for @navAccounts. + /// + /// In ru, this message translates to: + /// **'Счета'** + String get navAccounts; + + /// No description provided for @navProfile. + /// + /// In ru, this message translates to: + /// **'Профиль'** + String get navProfile; + + /// No description provided for @allAccounts. + /// + /// In ru, this message translates to: + /// **'Все'** + String get allAccounts; + + /// No description provided for @comingSoon. + /// + /// In ru, this message translates to: + /// **'Скоро'** + String get comingSoon; + + /// No description provided for @budget. + /// + /// In ru, this message translates to: + /// **'БЮДЖЕТ'** + String get budget; + + /// No description provided for @balance. + /// + /// In ru, this message translates to: + /// **'БАЛАНС'** + String get balance; + + /// No description provided for @income. + /// + /// In ru, this message translates to: + /// **'ДОХОДЫ'** + String get income; + + /// No description provided for @expenses. + /// + /// In ru, this message translates to: + /// **'РАСХОДЫ'** + String get expenses; + + /// No description provided for @transactions. + /// + /// In ru, this message translates to: + /// **'Транзакции'** + String get transactions; + + /// No description provided for @transactionCount. + /// + /// In ru, this message translates to: + /// **'{count, plural, one{{count} операция} few{{count} операции} many{{count} операций} other{{count} операций}}'** + String transactionCount(int count); + + /// No description provided for @allCategoriesFilter. + /// + /// In ru, this message translates to: + /// **'Все категории · все типы'** + String get allCategoriesFilter; + + /// No description provided for @moreCategories. + /// + /// In ru, this message translates to: + /// **'{count, plural, one{+ ещё {count} категория →} few{+ ещё {count} категории →} many{+ ещё {count} категорий →} other{+ ещё {count} категорий →}}'** + String moreCategories(int count); + + /// No description provided for @todayWithDate. + /// + /// In ru, this message translates to: + /// **'Сегодня · {date}'** + String todayWithDate(String date); + + /// No description provided for @yesterdayWithDate. + /// + /// In ru, this message translates to: + /// **'Вчера · {date}'** + String yesterdayWithDate(String date); + + /// No description provided for @analyticsTitle. + /// + /// In ru, this message translates to: + /// **'Отчёты'** + String get analyticsTitle; + + /// No description provided for @analyticsSubtitle. + /// + /// In ru, this message translates to: + /// **'Аналитика'** + String get analyticsSubtitle; + + /// No description provided for @accountsSubtitle. + /// + /// In ru, this message translates to: + /// **'Управление'** + String get accountsSubtitle; + + /// No description provided for @profileSubtitle. + /// + /// In ru, this message translates to: + /// **'Настройки'** + String get profileSubtitle; + + /// No description provided for @darkTheme. + /// + /// In ru, this message translates to: + /// **'Тёмная тема'** + String get darkTheme; + + /// No description provided for @profileHint. + /// + /// In ru, this message translates to: + /// **'Здесь появится профиль пользователя, валюта, локаль и другие настройки.'** + String get profileHint; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['en', 'ru'].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 'en': + return AppLocalizationsEn(); + case 'ru': + return AppLocalizationsRu(); + } + + 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/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart new file mode 100644 index 0000000..94c8fd0 --- /dev/null +++ b/lib/l10n/app_localizations_en.dart @@ -0,0 +1,100 @@ +// 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 => 'NewBudget'; + + @override + String get navHome => 'Home'; + + @override + String get navAnalytics => 'Analytics'; + + @override + String get navAccounts => 'Accounts'; + + @override + String get navProfile => 'Profile'; + + @override + String get allAccounts => 'All'; + + @override + String get comingSoon => 'Coming soon'; + + @override + String get budget => 'BUDGET'; + + @override + String get balance => 'BALANCE'; + + @override + String get income => 'INCOME'; + + @override + String get expenses => 'EXPENSES'; + + @override + String get transactions => 'Transactions'; + + @override + String transactionCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count transactions', + one: '$count transaction', + ); + return '$_temp0'; + } + + @override + String get allCategoriesFilter => 'All categories · all types'; + + @override + String moreCategories(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '+ $count more →', + one: '+ $count more →', + ); + return '$_temp0'; + } + + @override + String todayWithDate(String date) { + return 'Today · $date'; + } + + @override + String yesterdayWithDate(String date) { + return 'Yesterday · $date'; + } + + @override + String get analyticsTitle => 'Reports'; + + @override + String get analyticsSubtitle => 'Analytics'; + + @override + String get accountsSubtitle => 'Management'; + + @override + String get profileSubtitle => 'Settings'; + + @override + String get darkTheme => 'Dark theme'; + + @override + String get profileHint => + 'User profile, currency, locale and other settings will appear here.'; +} diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart new file mode 100644 index 0000000..b697021 --- /dev/null +++ b/lib/l10n/app_localizations_ru.dart @@ -0,0 +1,104 @@ +// 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 => 'NewBudget'; + + @override + String get navHome => 'Главная'; + + @override + String get navAnalytics => 'Аналитика'; + + @override + String get navAccounts => 'Счета'; + + @override + String get navProfile => 'Профиль'; + + @override + String get allAccounts => 'Все'; + + @override + String get comingSoon => 'Скоро'; + + @override + String get budget => 'БЮДЖЕТ'; + + @override + String get balance => 'БАЛАНС'; + + @override + String get income => 'ДОХОДЫ'; + + @override + String get expenses => 'РАСХОДЫ'; + + @override + String get transactions => 'Транзакции'; + + @override + String transactionCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count операций', + many: '$count операций', + few: '$count операции', + one: '$count операция', + ); + return '$_temp0'; + } + + @override + String get allCategoriesFilter => 'Все категории · все типы'; + + @override + String moreCategories(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '+ ещё $count категорий →', + many: '+ ещё $count категорий →', + few: '+ ещё $count категории →', + one: '+ ещё $count категория →', + ); + return '$_temp0'; + } + + @override + String todayWithDate(String date) { + return 'Сегодня · $date'; + } + + @override + String yesterdayWithDate(String date) { + return 'Вчера · $date'; + } + + @override + String get analyticsTitle => 'Отчёты'; + + @override + String get analyticsSubtitle => 'Аналитика'; + + @override + String get accountsSubtitle => 'Управление'; + + @override + String get profileSubtitle => 'Настройки'; + + @override + String get darkTheme => 'Тёмная тема'; + + @override + String get profileHint => + 'Здесь появится профиль пользователя, валюта, локаль и другие настройки.'; +} diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb new file mode 100644 index 0000000..fa045d6 --- /dev/null +++ b/lib/l10n/app_ru.arb @@ -0,0 +1,67 @@ +{ + "@@locale": "ru", + + "appTitle": "NewBudget", + + "navHome": "Главная", + "navAnalytics": "Аналитика", + "navAccounts": "Счета", + "navProfile": "Профиль", + + "allAccounts": "Все", + + "comingSoon": "Скоро", + + "budget": "БЮДЖЕТ", + "balance": "БАЛАНС", + "income": "ДОХОДЫ", + "expenses": "РАСХОДЫ", + + "transactions": "Транзакции", + "transactionCount": "{count, plural, one{{count} операция} few{{count} операции} many{{count} операций} other{{count} операций}}", + "@transactionCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + + "allCategoriesFilter": "Все категории · все типы", + + "moreCategories": "{count, plural, one{+ ещё {count} категория →} few{+ ещё {count} категории →} many{+ ещё {count} категорий →} other{+ ещё {count} категорий →}}", + "@moreCategories": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + + "todayWithDate": "Сегодня · {date}", + "@todayWithDate": { + "placeholders": { + "date": { + "type": "String" + } + } + }, + + "yesterdayWithDate": "Вчера · {date}", + "@yesterdayWithDate": { + "placeholders": { + "date": { + "type": "String" + } + } + }, + + "analyticsTitle": "Отчёты", + "analyticsSubtitle": "Аналитика", + + "accountsSubtitle": "Управление", + + "profileSubtitle": "Настройки", + "darkTheme": "Тёмная тема", + "profileHint": "Здесь появится профиль пользователя, валюта, локаль и другие настройки." +} diff --git a/lib/src/app/app.dart b/lib/src/app/app.dart index 3f0b96b..cf2147b 100644 --- a/lib/src/app/app.dart +++ b/lib/src/app/app.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:new_budget/l10n/app_localizations.dart'; import 'router/app_router.dart'; import 'theme/app_theme.dart'; @@ -21,12 +21,8 @@ class NewBudgetApp extends ConsumerWidget { darkTheme: AppTheme.dark(), themeMode: themeMode, routerConfig: router, - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [Locale('ru'), Locale('en')], + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, ); } } diff --git a/lib/src/app/l10n/l10n.dart b/lib/src/app/l10n/l10n.dart new file mode 100644 index 0000000..b51a1cb --- /dev/null +++ b/lib/src/app/l10n/l10n.dart @@ -0,0 +1,8 @@ +import 'package:flutter/widgets.dart'; +import 'package:new_budget/l10n/app_localizations.dart'; + +export 'package:new_budget/l10n/app_localizations.dart'; + +extension AppLocalizationsX on BuildContext { + AppLocalizations get l10n => AppLocalizations.of(this)!; +} diff --git a/lib/src/features/accounts/presentation/screens/accounts_screen.dart b/lib/src/features/accounts/presentation/screens/accounts_screen.dart index 26daf54..edb896c 100644 --- a/lib/src/features/accounts/presentation/screens/accounts_screen.dart +++ b/lib/src/features/accounts/presentation/screens/accounts_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../../../../app/l10n/l10n.dart'; import '../../../../shared/widgets/placeholder_screen.dart'; class AccountsScreen extends StatelessWidget { @@ -7,9 +8,10 @@ class AccountsScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return const PlaceholderScreen( - subtitle: 'Управление', - title: 'Счета', + final l10n = context.l10n; + return PlaceholderScreen( + subtitle: l10n.accountsSubtitle, + title: l10n.navAccounts, ); } } diff --git a/lib/src/features/analytics/presentation/screens/analytics_screen.dart b/lib/src/features/analytics/presentation/screens/analytics_screen.dart index 6f4a994..b1fc7c0 100644 --- a/lib/src/features/analytics/presentation/screens/analytics_screen.dart +++ b/lib/src/features/analytics/presentation/screens/analytics_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../../../../app/l10n/l10n.dart'; import '../../../../shared/widgets/placeholder_screen.dart'; class AnalyticsScreen extends StatelessWidget { @@ -7,9 +8,10 @@ class AnalyticsScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return const PlaceholderScreen( - subtitle: 'Аналитика', - title: 'Отчёты', + final l10n = context.l10n; + return PlaceholderScreen( + subtitle: l10n.analyticsSubtitle, + title: l10n.analyticsTitle, ); } } diff --git a/lib/src/features/home/presentation/screens/home_screen.dart b/lib/src/features/home/presentation/screens/home_screen.dart index c6dbb72..347f50c 100644 --- a/lib/src/features/home/presentation/screens/home_screen.dart +++ b/lib/src/features/home/presentation/screens/home_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; +import '../../../../app/l10n/l10n.dart'; import '../../../../app/theme/app_colors.dart'; import '../../../categories/domain/entities/category.dart'; import '../../../transactions/domain/entities/transaction.dart'; @@ -22,10 +23,12 @@ class HomeScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final p = context.palette; + final l10n = context.l10n; + final locale = Localizations.localeOf(context).toString(); final categories = ref.watch(mockCategoriesProvider); final categoryById = {for (final c in categories) c.id: c}; final txs = ref.watch(filteredTransactionsProvider); - final groups = _groupByDay(txs); + final groups = _groupByDay(txs, locale, l10n); return Scaffold( backgroundColor: p.paper, @@ -91,7 +94,11 @@ class _DayGroup { final int totalSpentMinor; } -List<_DayGroup> _groupByDay(List txs) { +List<_DayGroup> _groupByDay( + List txs, + String locale, + AppLocalizations l10n, +) { final sorted = [...txs]..sort((a, b) => b.date.compareTo(a.date)); final map = >{}; for (final t in sorted) { @@ -101,12 +108,12 @@ List<_DayGroup> _groupByDay(List txs) { final keys = map.keys.toList()..sort((a, b) => b.compareTo(a)); final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); - final fmt = DateFormat('d MMMM', 'ru'); + final fmt = DateFormat('d MMMM', locale); return [ for (final k in keys) _DayGroup( - label: _dayLabel(k, today, fmt), + label: _dayLabel(k, today, fmt, l10n), items: map[k]!, totalSpentMinor: map[k]! .where((t) => t.amount > 0 && t.note != null) @@ -117,19 +124,22 @@ List<_DayGroup> _groupByDay(List txs) { } bool _isExpense(Transaction t) { - // Помечаем расходы — для day-итога. (Type-based filter.) return t.amount > 0 && t.categoryId != null && _isExpenseType(t); } bool _isExpenseType(Transaction t) { - // Доступ к энам — без импорта в этой утилите. return t.type.name == 'expense'; } -String _dayLabel(DateTime day, DateTime today, DateFormat fmt) { +String _dayLabel( + DateTime day, + DateTime today, + DateFormat fmt, + AppLocalizations l10n, +) { final diff = today.difference(day).inDays; final base = fmt.format(day); - if (diff == 0) return 'Сегодня · $base'; - if (diff == 1) return 'Вчера · $base'; + if (diff == 0) return l10n.todayWithDate(base); + if (diff == 1) return l10n.yesterdayWithDate(base); return base; } diff --git a/lib/src/features/home/presentation/widgets/account_tabs.dart b/lib/src/features/home/presentation/widgets/account_tabs.dart index 7601958..733251e 100644 --- a/lib/src/features/home/presentation/widgets/account_tabs.dart +++ b/lib/src/features/home/presentation/widgets/account_tabs.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../app/l10n/l10n.dart'; import '../../../../app/theme/app_colors.dart'; import '../../../accounts/domain/entities/account.dart'; import '../_mock_data.dart'; @@ -22,7 +23,7 @@ class AccountTabs extends ConsumerWidget { children: [ _Pill( icon: Icons.account_balance_wallet_outlined, - label: 'Все', + label: context.l10n.allAccounts, active: selected == kAllAccountsId, onTap: () => ref.read(selectedAccountProvider.notifier).state = kAllAccountsId, diff --git a/lib/src/features/home/presentation/widgets/category_donut_card.dart b/lib/src/features/home/presentation/widgets/category_donut_card.dart index f03ce5e..c87dfbe 100644 --- a/lib/src/features/home/presentation/widgets/category_donut_card.dart +++ b/lib/src/features/home/presentation/widgets/category_donut_card.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../app/l10n/l10n.dart'; import '../../../../app/theme/app_colors.dart'; import '../../../categories/domain/entities/category.dart'; import '../_mock_data.dart'; @@ -15,6 +16,7 @@ class CategoryDonutCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final p = context.palette; + final l10n = context.l10n; final summary = ref.watch(monthSummaryProvider); final categories = ref.watch(mockCategoriesProvider); final selectedCat = ref.watch(selectedCategoryFilterProvider); @@ -53,7 +55,7 @@ class CategoryDonutCard extends ConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - 'РАСХОДЫ', + l10n.expenses, style: TextStyle( fontSize: 9, color: p.ink2, @@ -88,6 +90,7 @@ class _Legend extends StatelessWidget { @override Widget build(BuildContext context) { final p = context.palette; + final l10n = context.l10n; final top = entries.take(4).toList(); final restCount = entries.length - top.length; @@ -135,21 +138,11 @@ class _Legend extends StatelessWidget { Padding( padding: const EdgeInsets.only(top: 4), child: Text( - '+ ещё $restCount ${_word(restCount)} →', + l10n.moreCategories(restCount), style: TextStyle(fontSize: 10, color: p.ink2), ), ), ], ); } - - String _word(int n) { - final mod10 = n % 10; - final mod100 = n % 100; - if (mod10 == 1 && mod100 != 11) return 'категория'; - if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) { - return 'категории'; - } - return 'категорий'; - } } diff --git a/lib/src/features/home/presentation/widgets/month_header.dart b/lib/src/features/home/presentation/widgets/month_header.dart index 6b48178..fd42c22 100644 --- a/lib/src/features/home/presentation/widgets/month_header.dart +++ b/lib/src/features/home/presentation/widgets/month_header.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import '../../../../app/l10n/l10n.dart'; import '../../../../app/theme/app_colors.dart'; class MonthHeader extends StatelessWidget { @@ -9,8 +10,10 @@ class MonthHeader extends StatelessWidget { @override Widget build(BuildContext context) { final p = context.palette; + final l10n = context.l10n; + final locale = Localizations.localeOf(context).toString(); final now = DateTime.now(); - final monthName = DateFormat.MMMM('ru').format(now); + final monthName = DateFormat.MMMM(locale).format(now); final monthCap = '${monthName[0].toUpperCase()}${monthName.substring(1)}'; final title = '$monthCap ${now.year}'; @@ -24,7 +27,7 @@ class MonthHeader extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'БЮДЖЕТ', + l10n.budget, style: TextStyle( fontSize: 11, color: p.ink2, diff --git a/lib/src/features/home/presentation/widgets/month_kpi_card.dart b/lib/src/features/home/presentation/widgets/month_kpi_card.dart index 449d0bc..b5ae30a 100644 --- a/lib/src/features/home/presentation/widgets/month_kpi_card.dart +++ b/lib/src/features/home/presentation/widgets/month_kpi_card.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../app/l10n/l10n.dart'; import '../../../../app/theme/app_colors.dart'; import '../month_summary.dart'; import 'money_text.dart'; @@ -11,6 +12,7 @@ class MonthKpiCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final p = context.palette; + final l10n = context.l10n; final s = ref.watch(monthSummaryProvider); return Container( @@ -30,7 +32,7 @@ class MonthKpiCard extends ConsumerWidget { children: [ Expanded( child: Text( - 'БАЛАНС', + l10n.balance, style: TextStyle( fontSize: 11, color: p.ink2, @@ -53,7 +55,7 @@ class MonthKpiCard extends ConsumerWidget { children: [ Expanded( child: _SubKpi( - label: 'ДОХОДЫ', + label: l10n.income, value: s.incomeMinor, color: p.positive, withSign: true, @@ -63,7 +65,7 @@ class MonthKpiCard extends ConsumerWidget { const SizedBox(width: 12), Expanded( child: _SubKpi( - label: 'РАСХОДЫ', + label: l10n.expenses, value: -s.expensesMinor, color: p.negative, withSign: true, diff --git a/lib/src/features/home/presentation/widgets/transactions_section.dart b/lib/src/features/home/presentation/widgets/transactions_section.dart index 24a9339..d936d7c 100644 --- a/lib/src/features/home/presentation/widgets/transactions_section.dart +++ b/lib/src/features/home/presentation/widgets/transactions_section.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../app/l10n/l10n.dart'; import '../../../../app/theme/app_colors.dart'; import '../_mock_data.dart'; import '../month_summary.dart'; @@ -12,13 +13,14 @@ class TransactionsSectionHeader extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final p = context.palette; + final l10n = context.l10n; final count = ref.watch(filteredTransactionsProvider).length; return Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: Row( children: [ Text( - 'Транзакции', + l10n.transactions, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, @@ -27,23 +29,13 @@ class TransactionsSectionHeader extends ConsumerWidget { ), const Spacer(), Text( - '$count ${_opsWord(count)}', + l10n.transactionCount(count), style: TextStyle(fontSize: 11, color: p.ink2), ), ], ), ); } - - String _opsWord(int n) { - final mod10 = n % 10; - final mod100 = n % 100; - if (mod10 == 1 && mod100 != 11) return 'операция'; - if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) { - return 'операции'; - } - return 'операций'; - } } class CategoryFilterPill extends ConsumerWidget { @@ -52,6 +44,7 @@ class CategoryFilterPill extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final p = context.palette; + final l10n = context.l10n; final categories = ref.watch(mockCategoriesProvider); final selectedId = ref.watch(selectedCategoryFilterProvider); final activeCat = selectedId == null @@ -79,7 +72,7 @@ class CategoryFilterPill extends ConsumerWidget { Expanded( child: activeCat == null ? Text( - 'Все категории · все типы', + l10n.allCategoriesFilter, style: TextStyle(fontSize: 13, color: p.ink), ) : Row( diff --git a/lib/src/features/profile/presentation/screens/profile_screen.dart b/lib/src/features/profile/presentation/screens/profile_screen.dart index 8ac16f7..c30e9b9 100644 --- a/lib/src/features/profile/presentation/screens/profile_screen.dart +++ b/lib/src/features/profile/presentation/screens/profile_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../app/l10n/l10n.dart'; import '../../../../app/theme/app_colors.dart'; import '../../../../app/theme/theme_mode_controller.dart'; import '../../../../shared/widgets/placeholder_screen.dart'; @@ -11,12 +12,13 @@ class ProfileScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final p = context.palette; + final l10n = context.l10n; final mode = ref.watch(themeModeControllerProvider); final controller = ref.read(themeModeControllerProvider.notifier); return PlaceholderScreen( - subtitle: 'Настройки', - title: 'Профиль', + subtitle: l10n.profileSubtitle, + title: l10n.navProfile, body: ListView( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), children: [ @@ -29,7 +31,7 @@ class ProfileScreen extends ConsumerWidget { children: [ _Row( icon: Icons.dark_mode_outlined, - title: 'Тёмная тема', + title: l10n.darkTheme, trailing: Switch( value: mode == ThemeMode.dark, activeThumbColor: p.accent, @@ -42,7 +44,7 @@ class ProfileScreen extends ConsumerWidget { ), const SizedBox(height: 12), Text( - 'Здесь появится профиль пользователя, валюта, локаль и другие настройки.', + l10n.profileHint, style: TextStyle(fontSize: 12, color: p.ink2), ), ], diff --git a/lib/src/shared/widgets/app_bottom_nav.dart b/lib/src/shared/widgets/app_bottom_nav.dart index a333caf..f6a35f8 100644 --- a/lib/src/shared/widgets/app_bottom_nav.dart +++ b/lib/src/shared/widgets/app_bottom_nav.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../../app/l10n/l10n.dart'; import '../../app/theme/app_colors.dart'; class AppBottomNav extends StatelessWidget { @@ -12,16 +13,17 @@ class AppBottomNav extends StatelessWidget { final int activeIndex; final ValueChanged onTap; - static const _items = <_NavItem>[ - _NavItem(icon: Icons.home_outlined, label: 'Главная'), - _NavItem(icon: Icons.bar_chart_outlined, label: 'Аналитика'), - _NavItem(icon: Icons.account_balance_wallet_outlined, label: 'Счета'), - _NavItem(icon: Icons.person_outline, label: 'Профиль'), - ]; - @override Widget build(BuildContext context) { final p = context.palette; + final l10n = context.l10n; + final items = <_NavItem>[ + _NavItem(icon: Icons.home_outlined, label: l10n.navHome), + _NavItem(icon: Icons.bar_chart_outlined, label: l10n.navAnalytics), + _NavItem(icon: Icons.account_balance_wallet_outlined, label: l10n.navAccounts), + _NavItem(icon: Icons.person_outline, label: l10n.navProfile), + ]; + return Container( decoration: BoxDecoration( color: p.paper, @@ -33,10 +35,10 @@ class AppBottomNav extends StatelessWidget { height: 60, child: Row( children: [ - for (var i = 0; i < _items.length; i++) + for (var i = 0; i < items.length; i++) Expanded( child: _NavTab( - item: _items[i], + item: items[i], active: i == activeIndex, onTap: () => onTap(i), ), diff --git a/lib/src/shared/widgets/placeholder_screen.dart b/lib/src/shared/widgets/placeholder_screen.dart index 0cda3b0..6191c5d 100644 --- a/lib/src/shared/widgets/placeholder_screen.dart +++ b/lib/src/shared/widgets/placeholder_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../../app/l10n/l10n.dart'; import '../../app/theme/app_colors.dart'; class PlaceholderScreen extends StatelessWidget { @@ -53,7 +54,7 @@ class PlaceholderScreen extends StatelessWidget { child: body ?? Center( child: Text( - 'Скоро', + context.l10n.comingSoon, style: TextStyle( fontSize: 14, color: p.ink2, diff --git a/pubspec.yaml b/pubspec.yaml index c3c4b34..93d57ec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,3 +57,4 @@ dev_dependencies: flutter: uses-material-design: true + generate: true