diff --git a/assets/fonts/Vazir-Medium-FD.ttf b/assets/fonts/Vazir-Medium-FD.ttf new file mode 100644 index 0000000..d2dfe27 Binary files /dev/null and b/assets/fonts/Vazir-Medium-FD.ttf differ diff --git a/lang/en.json b/lang/en.json new file mode 100644 index 0000000..ed12588 --- /dev/null +++ b/lang/en.json @@ -0,0 +1,12 @@ +{ + "home_title": "Smart Washing", + "home_sub_title":"Machine", + "mode":"Mode", + "minutes":"minutes", + "choose_water":"Choose water", + "tip_save":"Please save choice", + "current":"Current", + "standard":"Standard", + "gentle":"Gentle", + "fast":"Fast" +} \ No newline at end of file diff --git a/lang/fa.json b/lang/fa.json new file mode 100644 index 0000000..eba2095 --- /dev/null +++ b/lang/fa.json @@ -0,0 +1,12 @@ +{ + "home_title": "ماشین لباسشویی", + "home_sub_title":"هوشمند", + "mode":"حالت", + "minutes":"دقیقه", + "choose_water":"انتخاب آب", + "tip_save":"لطفا انتخاب خود را ذخیره کنید", + "current":"فعلی", + "standard":"استاندارد", + "gentle":"آهسته", + "fast":"سریع" +} \ No newline at end of file diff --git a/lib/core/lang/app_localizations.dart b/lib/core/lang/app_localizations.dart new file mode 100644 index 0000000..4380ae6 --- /dev/null +++ b/lib/core/lang/app_localizations.dart @@ -0,0 +1,67 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class AppLocalizations { + final Locale locale; + + AppLocalizations(this.locale); + + // Helper method to keep the code in the widgets concise + // Localizations are accessed using an InheritedWidget "of" syntax + static AppLocalizations of(BuildContext context) { + return Localizations.of(context, AppLocalizations); + } + + // Static member to have a simple access to the delegate from the MaterialApp + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + Map _localizedStrings; + + Future load() async { + // Load the language JSON file from the "lang" folder + String jsonString = + await rootBundle.loadString('lang/${locale.languageCode}.json'); + Map jsonMap = json.decode(jsonString); + + _localizedStrings = jsonMap.map((key, value) { + return MapEntry(key, value.toString()); + }); + + return true; + } + + // This method will be called from every widget which needs a localized text + String translate(String key) { + return _localizedStrings[key]; + } +} + +// LocalizationsDelegate is a factory for a set of localized resources +// In this case, the localized strings will be gotten in an AppLocalizations object +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + // This delegate instance will never change (it doesn't even have fields!) + // It can provide a constant constructor. + const _AppLocalizationsDelegate(); + + @override + bool isSupported(Locale locale) { + // Include all of your supported language codes here + return ['en', 'fa'].contains(locale.languageCode); + } + + @override + Future load(Locale locale) async { + // AppLocalizations class is where the JSON loading actually runs + AppLocalizations localizations = new AppLocalizations(locale); + await localizations.load(); + return localizations; + } + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} diff --git a/lib/main.dart b/lib/main.dart index 3dc4c27..991ae99 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_whirlpool/screens/main/main_screen.dart'; import 'package:flutter_whirlpool/view_models/dev_view_model.dart'; +import 'package:flutter_whirlpool/view_models/language_view_model.dart'; import 'package:flutter_whirlpool/view_models/main_view_model.dart'; import 'package:flutter_whirlpool/view_models/service_locator.dart'; import 'package:flutter_whirlpool/view_models/timer_view_model.dart'; import 'package:provider/provider.dart'; +import 'core/lang/app_localizations.dart'; + void main() { ServiceLocator.init(); @@ -18,22 +22,42 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MultiProvider( - providers: [ - ChangeNotifierProvider( - create: (_) => ServiceLocator.get()), - ChangeNotifierProvider( - create: (_) => ServiceLocator.get()), - ChangeNotifierProvider( - create: (_) => ServiceLocator.get()), - ], - child: MaterialApp( - title: 'Smart Washing Machine', - debugShowCheckedModeBanner: false, - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: MainScreen(), - ), - ); + providers: [ + ChangeNotifierProvider( + create: (_) => ServiceLocator.get()), + ChangeNotifierProvider( + create: (_) => ServiceLocator.get()), + ChangeNotifierProvider( + create: (_) => ServiceLocator.get()), + ChangeNotifierProvider( + create: (_) => ServiceLocator.get()), + ], + child: Consumer( + builder: (context, model, child) { + return MaterialApp( + locale: model.appLocal, + title: 'Smart Washing Machine', + // List all of the app's supported locales here + supportedLocales: [ + Locale('en', 'US'), + Locale('fa', 'IR'), + ], + // These delegates make sure that the localization data for the proper language is loaded + localizationsDelegates: [ + // A class which loads the translations from JSON files + AppLocalizations.delegate, + // Built-in localization of basic text for Material widgets + GlobalMaterialLocalizations.delegate, + // Built-in localization for text direction LTR/RTL + GlobalWidgetsLocalizations.delegate, + ], + debugShowCheckedModeBanner: false, + theme: ThemeData( + primarySwatch: Colors.blue, + fontFamily: model.isRTL ? "Vazir" : null), + home: MainScreen(), + ); + }, + )); } } diff --git a/lib/screens/main/main_screen.dart b/lib/screens/main/main_screen.dart index 3e132fe..0aee1b4 100644 --- a/lib/screens/main/main_screen.dart +++ b/lib/screens/main/main_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_whirlpool/core/lang/app_localizations.dart'; import 'package:flutter_whirlpool/models/mode_item_model.dart'; import 'package:flutter_whirlpool/screens/main/mode_tile.dart'; import 'package:flutter_whirlpool/screens/main/top_bar.dart'; @@ -8,18 +9,22 @@ import 'package:flutter_whirlpool/screens/water_drawer/water_drawer.dart'; import 'package:flutter_whirlpool/shared/colors.dart'; import 'package:flutter_whirlpool/shared/consts.dart'; import 'package:flutter_whirlpool/shared/widgets.dart'; +import 'package:flutter_whirlpool/view_models/language_view_model.dart'; import 'package:flutter_whirlpool/view_models/main_view_model.dart'; +import 'package:flutter_whirlpool/view_models/service_locator.dart'; import 'package:provider/provider.dart'; class MainScreen extends StatelessWidget { - static const margin = EdgeInsets.only( - left: GLOBAL_EDGE_MARGIN_VALUE, + static const margin = EdgeInsetsDirectional.only( + start: GLOBAL_EDGE_MARGIN_VALUE, ); const MainScreen({Key key}) : super(key: key); @override Widget build(BuildContext context) { + bool _isRtl = ServiceLocator.get()?.isRTL ?? false; + return Container( color: CustomColors.primaryColor, child: Scaffold( @@ -32,8 +37,10 @@ class MainScreen extends StatelessWidget { ), drawer: ClipRRect( borderRadius: BorderRadius.only( - topRight: Radius.circular(45), - bottomRight: Radius.circular(45), + topRight: _isRtl ? Radius.circular(0) : Radius.circular(45), + bottomRight: _isRtl ? Radius.circular(0) : Radius.circular(45), + bottomLeft: _isRtl ? Radius.circular(45) : Radius.circular(0), + topLeft: _isRtl ? Radius.circular(45) : Radius.circular(0), ), child: Drawer( child: WaterDrawer(), @@ -44,10 +51,10 @@ class MainScreen extends StatelessWidget { child: Container( child: Stack( children: [ - Positioned( - right: 0, + PositionedDirectional( + end: 0, child: Transform.translate( - offset: Offset(100, 120), + offset: Offset(_isRtl ? -100 : 100, 120), child: WashingMachineCase( width: 380, height: 380, @@ -61,9 +68,9 @@ class MainScreen extends StatelessWidget { Padding( padding: margin, child: Text( - 'Smart Washing', + AppLocalizations.of(context).translate("home_title"), style: TextStyle( - fontSize: 28, + fontSize: _isRtl ? 22 : 28, color: CustomColors.headerColor, fontWeight: FontWeight.w800, ), @@ -73,9 +80,10 @@ class MainScreen extends StatelessWidget { Padding( padding: margin, child: Text( - 'Machine', + AppLocalizations.of(context) + .translate("home_sub_title"), style: TextStyle( - fontSize: 26, + fontSize: _isRtl ? 20 : 26, color: CustomColors.headerColor, fontWeight: FontWeight.w400, ), @@ -262,7 +270,7 @@ class _ModesList extends StatelessWidget { Padding( padding: MainScreen.margin, child: Text( - 'Mode', + AppLocalizations.of(context).translate("mode"), style: TextStyle( fontSize: 23, color: CustomColors.headerColor, @@ -284,7 +292,7 @@ class _ModesList extends StatelessWidget { return ModeTile( pressed: viewModel?.selectedMode == item, indicatorColor: item.color, - name: item.name, + name: AppLocalizations.of(context).translate(item.name), minutes: item.minutes, disabled: viewModel.modeStatus == ModeStatus.running, onTap: () => viewModel.selectMode(item), diff --git a/lib/screens/main/mode_tile.dart b/lib/screens/main/mode_tile.dart index 8a047db..ed4c5b7 100644 --- a/lib/screens/main/mode_tile.dart +++ b/lib/screens/main/mode_tile.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_whirlpool/core/lang/app_localizations.dart'; import 'package:flutter_whirlpool/shared/colors.dart'; import 'package:flutter_whirlpool/shared/consts.dart'; import 'package:flutter_whirlpool/shared/widgets.dart'; @@ -30,8 +31,8 @@ class ModeTile extends StatelessWidget { onTap: this.onTap, width: 120, disabled: disabled, - margin: const EdgeInsets.only( - left: GLOBAL_EDGE_MARGIN_VALUE, + margin: const EdgeInsetsDirectional.only( + start: GLOBAL_EDGE_MARGIN_VALUE, top: 10, bottom: 10, ), @@ -62,7 +63,7 @@ class ModeTile extends StatelessWidget { height: 6, ), Text( - '$minutes minutes', + '$minutes ${AppLocalizations.of(context).translate("minutes")}', style: TextStyle( fontSize: 13, color: CustomColors.headerColor.withAlpha(120), diff --git a/lib/screens/main/top_bar.dart b/lib/screens/main/top_bar.dart index 068e950..0bff158 100644 --- a/lib/screens/main/top_bar.dart +++ b/lib/screens/main/top_bar.dart @@ -5,6 +5,7 @@ import 'package:flutter_whirlpool/shared/colors.dart'; import 'package:flutter_whirlpool/shared/consts.dart'; import 'package:flutter_whirlpool/shared/widgets.dart'; import 'package:flutter_whirlpool/view_models/dev_view_model.dart'; +import 'package:flutter_whirlpool/view_models/language_view_model.dart'; import 'package:provider/provider.dart'; class TopBar extends StatelessWidget { @@ -13,10 +14,9 @@ class TopBar extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - margin: EdgeInsets.fromLTRB( + margin: EdgeInsetsDirectional.fromSTEB( GLOBAL_EDGE_MARGIN_VALUE, DRAWER_BUTTON_MARGIN_TOP, 18, 10), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ Consumer(builder: (context, viewModel, _) { @@ -31,6 +31,23 @@ class TopBar extends StatelessWidget { }, ); }), + SizedBox( + width: 25, + ), + Consumer(builder: (context, viewModel, _) { + return NeumorphicIconButton( + icon: Icon( + Icons.translate, + color: CustomColors.textColor, + ), + onTap: () { + viewModel.appLocal = viewModel.appLocal; + }, + ); + }), + Expanded( + child: Container(), + ), TimerPanel() ], ), diff --git a/lib/screens/water_drawer/water_drawer.dart b/lib/screens/water_drawer/water_drawer.dart index 45dc580..7a37d82 100644 --- a/lib/screens/water_drawer/water_drawer.dart +++ b/lib/screens/water_drawer/water_drawer.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter_whirlpool/core/lang/app_localizations.dart'; import 'package:flutter_whirlpool/screens/water_drawer/water_slider.dart'; import 'package:flutter_whirlpool/shared/colors.dart'; import 'package:flutter_whirlpool/shared/consts.dart'; import 'package:flutter_whirlpool/shared/widgets.dart'; +import 'package:flutter_whirlpool/view_models/language_view_model.dart'; import 'package:flutter_whirlpool/view_models/main_view_model.dart'; +import 'package:flutter_whirlpool/view_models/service_locator.dart'; import 'package:provider/provider.dart'; class WaterDrawer extends StatelessWidget { @@ -11,14 +14,16 @@ class WaterDrawer extends StatelessWidget { @override Widget build(BuildContext context) { + bool _isRtl = ServiceLocator.get()?.isRTL ?? false; + return Consumer( builder: (context, viewModel, _) { return Container( color: CustomColors.primaryColor, child: SafeArea( child: Padding( - padding: EdgeInsets.only( - left: GLOBAL_EDGE_MARGIN_VALUE, + padding: EdgeInsetsDirectional.only( + start: GLOBAL_EDGE_MARGIN_VALUE, top: DRAWER_BUTTON_MARGIN_TOP, ), child: Column( @@ -35,7 +40,7 @@ class WaterDrawer extends StatelessWidget { ), SizedBox(height: 35), Text( - 'Choose water', + AppLocalizations.of(context).translate("choose_water"), style: TextStyle( fontSize: 28, color: CustomColors.headerColor, @@ -44,7 +49,7 @@ class WaterDrawer extends StatelessWidget { ), SizedBox(height: 3), Text( - 'Please save choice', + AppLocalizations.of(context).translate("tip_save"), style: TextStyle( fontSize: 16, color: CustomColors.headerColor, @@ -62,21 +67,21 @@ class WaterDrawer extends StatelessWidget { ), ), SizedBox(height: 80), - RichText( - text: TextSpan( - text: 'Current ', - style: TextStyle( - color: CustomColors.headerColor, - fontWeight: FontWeight.w300, - fontSize: 18, - ), - children: [ - TextSpan( - text: viewModel.waterValue.toStringAsFixed(0), - style: TextStyle(fontWeight: FontWeight.w700), - ), - ], - ), + Row( + children: [ + Text( + '${AppLocalizations.of(context).translate("current")} ', + style: TextStyle( + color: CustomColors.headerColor, + fontWeight: FontWeight.w300, + fontSize: 18, + )), + Text(viewModel.waterValue.toStringAsFixed(0), + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 18, + )) + ], ), SizedBox(height: 40), ], diff --git a/lib/screens/water_drawer/water_slider.dart b/lib/screens/water_drawer/water_slider.dart index df07468..adffdc6 100644 --- a/lib/screens/water_drawer/water_slider.dart +++ b/lib/screens/water_drawer/water_slider.dart @@ -88,7 +88,9 @@ class _WaterSliderState extends State ), ), Padding( - padding: const EdgeInsets.only(left: 12), + padding: const EdgeInsetsDirectional.only( + start: 12, + ), child: _SliderLegend( controller: _animationController.view, min: widget.minValue, @@ -197,8 +199,8 @@ class _SliderLegend extends StatelessWidget { double top = topOffset + i * bottomMargin; result.add( - Positioned( - left: 0, + PositionedDirectional( + start: 0, top: top, child: Container( width: markStep ? longLineWidth.value : shortLineWidth.value, @@ -210,8 +212,8 @@ class _SliderLegend extends StatelessWidget { if (markStep) { result.add( - Positioned( - left: 80.0 + 8.0, + PositionedDirectional( + start: 80.0 + 8.0, top: top - 6, child: Opacity( opacity: opacity.value, @@ -290,9 +292,9 @@ class _WaterSlideState extends State<_WaterSlide> { @override Widget build(BuildContext context) { - return Positioned( + return PositionedDirectional( bottom: -_yOffset, - left: 0, + start: 0, child: GestureDetector( onVerticalDragUpdate: _onDragUpdate, child: Stack( diff --git a/lib/view_models/language_view_model.dart b/lib/view_models/language_view_model.dart new file mode 100644 index 0000000..098fb70 --- /dev/null +++ b/lib/view_models/language_view_model.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class LanguageViewModel extends ChangeNotifier { + Locale get appLocal => _appLocale ?? Locale("en"); + bool get isRTL => + (_appLocale?.languageCode?.compareTo("fa") ?? -1) == 0 ? true : false; + + set appLocal(Locale type) { + if (type == Locale("en")) { + _appLocale = Locale("fa"); + } else { + _appLocale = Locale("en"); + } + notifyListeners(); + } + + Locale _appLocale = Locale('en'); +} diff --git a/lib/view_models/main_view_model.dart b/lib/view_models/main_view_model.dart index e3ff2c9..2642a3b 100644 --- a/lib/view_models/main_view_model.dart +++ b/lib/view_models/main_view_model.dart @@ -21,17 +21,17 @@ class MainViewModel with ChangeNotifier { List nodes = const [ ModeItemModel( - name: 'Standard', + name: 'standard', minutes: 32, color: Color.fromRGBO(61, 111, 252, 1), ), ModeItemModel( - name: 'Gentle', + name: 'gentle', minutes: 24, color: Color.fromRGBO(50, 197, 253, 1), ), ModeItemModel( - name: 'Fast', + name: 'fast', minutes: 12, color: Color.fromRGBO(253, 133, 53, 1), ), diff --git a/lib/view_models/service_locator.dart b/lib/view_models/service_locator.dart index 13be8f2..42ac630 100644 --- a/lib/view_models/service_locator.dart +++ b/lib/view_models/service_locator.dart @@ -1,5 +1,6 @@ import 'package:flutter_whirlpool/screens/main/washing_machine/washing_machine_controller.dart'; import 'package:flutter_whirlpool/view_models/dev_view_model.dart'; +import 'package:flutter_whirlpool/view_models/language_view_model.dart'; import 'package:flutter_whirlpool/view_models/main_view_model.dart'; import 'package:flutter_whirlpool/view_models/timer_view_model.dart'; import 'package:get_it/get_it.dart'; @@ -13,6 +14,7 @@ class ServiceLocator { WashingMachineController(ballsCount: 16)); getIt.registerSingleton(TimerViewModel()); getIt.registerSingleton(MainViewModel()); + getIt.registerSingleton(LanguageViewModel()); } static T get() { diff --git a/pubspec.lock b/pubspec.lock index ca48272..ec98dbf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,20 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.13" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" async: dependency: transitive description: @@ -29,20 +43,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.3" - clock: + collection: dependency: transitive description: - name: clock + name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" - collection: + version: "1.14.12" + convert: dependency: transitive description: - name: collection + name: convert url: "https://pub.dartlang.org" source: hosted - version: "1.14.12" + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.4" cupertino_icons: dependency: "direct main" description: @@ -50,18 +71,16 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.3" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -74,6 +93,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.0.2" + image: + dependency: transitive + description: + name: image + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.12" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.1" matcher: dependency: transitive description: @@ -101,7 +134,14 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.6.4" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.0" provider: dependency: "direct main" description: @@ -177,6 +217,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.8" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "3.6.1" sdks: dart: ">=2.6.0 <3.0.0" flutter: ">=1.12.1" diff --git a/pubspec.yaml b/pubspec.yaml index c25217f..bf451ae 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,6 +23,8 @@ dependencies: quiver: 2.1.3 get_it: ^4.0.2 box2d_flame: ^0.4.5 + flutter_localizations: + sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. @@ -45,9 +47,10 @@ flutter: uses-material-design: true # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg + assets: + - assets/ + - lang/en.json + - lang/fa.json # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. @@ -60,17 +63,10 @@ flutter: # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 + fonts: + - family: Vazir + fonts: + - asset: assets/fonts/Vazir-Medium-FD.ttf # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages