diff --git a/lib/common/app-translation/app-translation.dart b/lib/common/app-translation/app-translation.dart new file mode 100644 index 0000000..4eca709 --- /dev/null +++ b/lib/common/app-translation/app-translation.dart @@ -0,0 +1,58 @@ +import 'package:get/get.dart'; + +import '../../screens/home/title/title.translation.dart' as home_title; +import '../../screens/home/todo/add-task/add-task.translation.dart' + as home_add_task; +import '../../screens/home/todo/filter-panel/filter-panel.translation.dart' + as home_filter_panel; +import '../remaining-items/remaining-items.translation.dart' as remaining_items; + +class AppTranslation extends GetxService { + AppTranslation(); + + Map> translationsKeys = + >{ + 'en': en, + 'uk': uk, + 'ru': ru, + }; + + void _combineTranslations(Map enTranslations, + Map ukTranslations, Map ruTranslations) { + translationsKeys['en']!.addAll(enTranslations); + translationsKeys['uk']!.addAll(ukTranslations); + translationsKeys['ru']!.addAll(ruTranslations); + } + + Map> combineAllTranslations() { + _combineTranslations( + home_title.en, + home_title.uk, + home_title.ru, + ); + + _combineTranslations( + home_add_task.en, + home_add_task.uk, + home_add_task.ru, + ); + + _combineTranslations( + home_filter_panel.en, + home_filter_panel.uk, + home_filter_panel.ru, + ); + + _combineTranslations( + remaining_items.en, + remaining_items.uk, + remaining_items.ru, + ); + + return translationsKeys; + } +} + +final Map en = {}; +final Map uk = {}; +final Map ru = {}; diff --git a/lib/common/app-translation/translation.extensions.dart b/lib/common/app-translation/translation.extensions.dart new file mode 100644 index 0000000..3c1f8b3 --- /dev/null +++ b/lib/common/app-translation/translation.extensions.dart @@ -0,0 +1,14 @@ +import 'package:get/get.dart'; + +abstract class Translation {} + +extension Translations on Translation { + String get st => '$this'; + String get tr => st.tr; +} + +extension MapEnumExtensions on Map { + Map get st => Map.fromEntries(entries.map( + (MapEntry entry) => + MapEntry(entry.key.st, entry.value))); +} diff --git a/lib/common/remaining-items/remaining-items.extensions.dart b/lib/common/remaining-items/remaining-items.extensions.dart new file mode 100644 index 0000000..6dbbaeb --- /dev/null +++ b/lib/common/remaining-items/remaining-items.extensions.dart @@ -0,0 +1,30 @@ +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; + +import '../app-translation/translation.extensions.dart'; +import 'remaining-items.translation.dart'; + +extension RemainingItemsFormatter on int { + String get remainingItemsText => _getPlural( + this, + one: RemainingItemsTranslationNames.oneItem.tr, + few: RemainingItemsTranslationNames.fewItems.tr, + many: RemainingItemsTranslationNames.manyItems.tr, + ).trim(); + + String _getPlural(int value, + {required String one, required String few, required String many}) { + if (value > 0) { + return Intl.plural( + value, + zero: '', + one: '$value $one', + few: '$value $few', + many: '$value $many', + other: '$value $many', + locale: Get.locale?.languageCode, + ); + } + return ''; + } +} diff --git a/lib/common/remaining-items/remaining-items.translation.dart b/lib/common/remaining-items/remaining-items.translation.dart new file mode 100644 index 0000000..c0a39c6 --- /dev/null +++ b/lib/common/remaining-items/remaining-items.translation.dart @@ -0,0 +1,25 @@ +import '../../../../common/app-translation/translation.extensions.dart'; + +enum RemainingItemsTranslationNames implements Translation { + oneItem, + fewItems, + manyItems, +} + +final Map en = { + RemainingItemsTranslationNames.oneItem: 'item left', + RemainingItemsTranslationNames.fewItems: 'items left', + RemainingItemsTranslationNames.manyItems: 'items left', +}.st; + +final Map ru = { + RemainingItemsTranslationNames.oneItem: 'элемент остался', + RemainingItemsTranslationNames.fewItems: 'элемента осталось', + RemainingItemsTranslationNames.manyItems: 'элементов осталось', +}.st; + +final Map uk = { + RemainingItemsTranslationNames.oneItem: 'елемент залишився', + RemainingItemsTranslationNames.fewItems: 'елементи залишились', + RemainingItemsTranslationNames.manyItems: 'елементів залишилось', +}.st; diff --git a/lib/main.dart b/lib/main.dart index 8472f82..b44ef4c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'common/app-translation/app-translation.dart'; import 'screens/home/home.binding.dart'; import 'screens/home/home.screen.dart'; void main() { + Get.lazyPut(AppTranslation.new); + runApp(const MyApp()); } @@ -14,5 +17,8 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) => GetMaterialApp( initialBinding: HomeBinding(), home: const HomeScreen(), + translationsKeys: Get.find().combineAllTranslations(), + locale: const Locale('en'), + fallbackLocale: const Locale('en'), ); } diff --git a/lib/screens/home/home.binding.dart b/lib/screens/home/home.binding.dart index 25cb19c..a616bf0 100644 --- a/lib/screens/home/home.binding.dart +++ b/lib/screens/home/home.binding.dart @@ -1,4 +1,6 @@ import 'package:get/get.dart'; +import '../../common/app-translation/app-translation.dart'; +import 'language-switcher/language-switcher.controller.dart'; import 'todo/add-task/add-task.controller.dart'; import 'todo/filter-panel/filter-panel.controller.dart'; import 'todo/task-list-item/task-list-item.controller.dart'; @@ -8,7 +10,9 @@ class HomeBinding extends Bindings { @override Future dependencies() async { Get + ..lazyPut(AppTranslation.new) ..lazyPut(TodoService.new) + ..lazyPut(LanguageSwitcherController.new) ..lazyPut(() => AddTaskController(Get.find())) ..lazyPut(() => FilterPanelController(Get.find())) ..lazyPut(() => TaskListItemController(Get.find(), Get.find())); diff --git a/lib/screens/home/home.screen.dart b/lib/screens/home/home.screen.dart index 2bff3c6..970b0fc 100644 --- a/lib/screens/home/home.screen.dart +++ b/lib/screens/home/home.screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'language-switcher/language-switcher.widget.dart'; import 'todo/todo.widget.dart'; import '../home/title/title.widget.dart'; @@ -10,6 +11,12 @@ class HomeScreen extends StatelessWidget { body: ListView( children: const [ Center(child: TitleWidget()), + Padding( + padding: EdgeInsets.only(bottom: 20), + child: Center( + child: LanguageSwitcherWidget(), + ), + ), TodoWidget(), ], ), diff --git a/lib/screens/home/language-switcher/language-switcher.controller.dart b/lib/screens/home/language-switcher/language-switcher.controller.dart new file mode 100644 index 0000000..a1e6107 --- /dev/null +++ b/lib/screens/home/language-switcher/language-switcher.controller.dart @@ -0,0 +1,11 @@ +import 'package:get/get.dart'; +import 'language-switcher.enum.dart'; + +class LanguageSwitcherController extends GetxController { + final Rx currentLanguage = Language.en.obs; + + void updateLocale(Language language) { + currentLanguage.value = language; + Get.updateLocale(language.locale); + } +} diff --git a/lib/screens/home/language-switcher/language-switcher.enum.dart b/lib/screens/home/language-switcher/language-switcher.enum.dart new file mode 100644 index 0000000..9030316 --- /dev/null +++ b/lib/screens/home/language-switcher/language-switcher.enum.dart @@ -0,0 +1,12 @@ +import 'dart:ui'; + +enum Language { + en(Locale('en'), 'English'), + ru(Locale('ru'), 'Русский'), + uk(Locale('uk'), 'Українська'); + + const Language(this.locale, this.title); + + final Locale locale; + final String title; +} diff --git a/lib/screens/home/language-switcher/language-switcher.widget.dart b/lib/screens/home/language-switcher/language-switcher.widget.dart new file mode 100644 index 0000000..6d1a44b --- /dev/null +++ b/lib/screens/home/language-switcher/language-switcher.widget.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'language-switcher.controller.dart'; +import 'language-switcher.enum.dart'; + +class LanguageSwitcherWidget extends GetView { + const LanguageSwitcherWidget({super.key}); + + @override + Widget build(BuildContext context) => Obx(() => DropdownButton( + value: controller.currentLanguage.value, + onChanged: (Language? language) { + if (language != null) { + controller.updateLocale(language); + } + }, + items: Language.values + .map>( + (Language language) => DropdownMenuItem( + value: language, + child: Text(language.title), + )) + .toList(), + )); +} diff --git a/lib/screens/home/title/title.translation.dart b/lib/screens/home/title/title.translation.dart new file mode 100644 index 0000000..9eba73c --- /dev/null +++ b/lib/screens/home/title/title.translation.dart @@ -0,0 +1,17 @@ +import '../../../common/app-translation/translation.extensions.dart'; + +enum TitleTranslationNames implements Translation { + title, +} + +final Map en = { + TitleTranslationNames.title: 'todos', +}.st; + +final Map ru = { + TitleTranslationNames.title: 'Список задач', +}.st; + +final Map uk = { + TitleTranslationNames.title: 'Список завдань', +}.st; diff --git a/lib/screens/home/title/title.widget.dart b/lib/screens/home/title/title.widget.dart index b3d815d..82ac4a2 100644 --- a/lib/screens/home/title/title.widget.dart +++ b/lib/screens/home/title/title.widget.dart @@ -1,14 +1,18 @@ import 'package:flutter/material.dart'; +import 'title.translation.dart'; +import '../../../common/app-translation/translation.extensions.dart'; + class TitleWidget extends StatelessWidget { const TitleWidget({super.key}); @override - Widget build(BuildContext context) => const Padding( - padding: EdgeInsets.only(top: 50, bottom: 20), + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.only(top: 50, bottom: 20), child: Text( - 'todos', - style: TextStyle( + TitleTranslationNames.title.tr, + textAlign: TextAlign.center, + style: const TextStyle( fontSize: 80, fontWeight: FontWeight.w300, color: Colors.red, diff --git a/lib/screens/home/todo/add-task/add-task.translation.dart b/lib/screens/home/todo/add-task/add-task.translation.dart new file mode 100644 index 0000000..d7d62c0 --- /dev/null +++ b/lib/screens/home/todo/add-task/add-task.translation.dart @@ -0,0 +1,17 @@ +import '../../../../common/app-translation/translation.extensions.dart'; + +enum AddTaskTranslationNames implements Translation { + title, +} + +final Map en = { + AddTaskTranslationNames.title: 'What needs to be done?', +}.st; + +final Map ru = { + AddTaskTranslationNames.title: 'Что нужно сделать?', +}.st; + +final Map uk = { + AddTaskTranslationNames.title: 'Що потрібно зробити?', +}.st; diff --git a/lib/screens/home/todo/add-task/add-task.widget.dart b/lib/screens/home/todo/add-task/add-task.widget.dart index 73cf924..07b48d8 100644 --- a/lib/screens/home/todo/add-task/add-task.widget.dart +++ b/lib/screens/home/todo/add-task/add-task.widget.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import '../../../../common/app-translation/translation.extensions.dart'; import '../../../../common/input/input.widget.dart'; import 'add-task.controller.dart'; +import 'add-task.translation.dart'; class AddTaskWidget extends GetView { const AddTaskWidget({super.key}); @@ -16,7 +18,7 @@ class AddTaskWidget extends GetView { child: InputWidget( textController: controller.textController, focusNode: controller.focusNode, - hintText: 'What needs to be done?', + hintText: AddTaskTranslationNames.title.tr, onSubmitted: controller.addTask, ), ); diff --git a/lib/screens/home/todo/filter-panel/filter-panel.enum.dart b/lib/screens/home/todo/filter-panel/filter-panel.enum.dart index 49370fc..7907b2a 100644 --- a/lib/screens/home/todo/filter-panel/filter-panel.enum.dart +++ b/lib/screens/home/todo/filter-panel/filter-panel.enum.dart @@ -1,9 +1,14 @@ +import '../../../../common/app-translation/translation.extensions.dart'; +import 'filter-panel.translation.dart'; + enum FilterType { - all('All'), - active('Active'), - completed('Completed'); + all(FilterPanelTranslationNames.all), + active(FilterPanelTranslationNames.active), + completed(FilterPanelTranslationNames.completed); + + const FilterType(this.translationKey); - const FilterType(this.type); + final FilterPanelTranslationNames translationKey; - final String type; + String get type => translationKey.tr; } diff --git a/lib/screens/home/todo/filter-panel/filter-panel.translation.dart b/lib/screens/home/todo/filter-panel/filter-panel.translation.dart new file mode 100644 index 0000000..4d105e9 --- /dev/null +++ b/lib/screens/home/todo/filter-panel/filter-panel.translation.dart @@ -0,0 +1,29 @@ +import '../../../../common/app-translation/translation.extensions.dart'; + +enum FilterPanelTranslationNames implements Translation { + clearItems, + all, + active, + completed +} + +final Map en = { + FilterPanelTranslationNames.clearItems: 'Clear completed', + FilterPanelTranslationNames.all: 'All', + FilterPanelTranslationNames.active: 'Active', + FilterPanelTranslationNames.completed: 'Completed', +}.st; + +final Map ru = { + FilterPanelTranslationNames.clearItems: 'Очистить выполненные', + FilterPanelTranslationNames.all: 'Все', + FilterPanelTranslationNames.active: 'Активные', + FilterPanelTranslationNames.completed: 'Выполненные', +}.st; + +final Map uk = { + FilterPanelTranslationNames.clearItems: 'Очистити виконані', + FilterPanelTranslationNames.all: 'Усі', + FilterPanelTranslationNames.active: 'Активні', + FilterPanelTranslationNames.completed: 'Виконані', +}.st; diff --git a/lib/screens/home/todo/filter-panel/filter-panel.widget.dart b/lib/screens/home/todo/filter-panel/filter-panel.widget.dart index adab1bd..3b68ccb 100644 --- a/lib/screens/home/todo/filter-panel/filter-panel.widget.dart +++ b/lib/screens/home/todo/filter-panel/filter-panel.widget.dart @@ -1,16 +1,17 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import '../../../../common/app-translation/translation.extensions.dart'; import '../../../../common/button/button.widget.dart'; +import '../../../../common/remaining-items/remaining-items.extensions.dart'; import 'filter-panel-buttons/filter-panel-buttons.dart'; import 'filter-panel.controller.dart'; +import 'filter-panel.translation.dart'; class FilterPanelWidget extends GetView { const FilterPanelWidget({super.key}); @override Widget build(BuildContext context) => Obx(() { - final bool isMobile = MediaQuery.of(context).size.width < 600; - if (controller.tasks.isEmpty) { return const SizedBox(); } @@ -23,23 +24,21 @@ class FilterPanelWidget extends GetView { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '${controller.activeCount} items left', + controller.activeCount.remainingItemsText, style: const TextStyle(fontSize: 14, color: Colors.grey), ), - if (!isMobile) const FilterPanelButtons(), ButtonWidget( onTap: controller.clearCompleted, - title: 'Clear completed', + title: FilterPanelTranslationNames.clearItems.tr, ), ], ), - if (isMobile) - const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FilterPanelButtons(), - ], - ), + const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FilterPanelButtons(), + ], + ), ], ), ); diff --git a/pubspec.lock b/pubspec.lock index 3c9ba11..efd253f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -256,6 +256,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" jsdaddy_custom_lints: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index f556000..5fd214c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: sdk: flutter get: ^4.6.6 cupertino_icons: ^1.0.8 + intl: ^0.19.0 dev_dependencies: flutter_test: