diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 610e5281..e2ef197f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,18 +9,20 @@ on: branches: ['*'] tags: ['v*','V*'] + workflow_dispatch: + jobs: test: name: Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - uses: actions/setup-java@v1 with: java-version: '12.x' - uses: subosito/flutter-action@v1 with: - channel: 'dev' # or: 'dev' or 'beta' + channel: 'stable' # or: 'dev' or 'beta' - name: Install packages dependencies run: flutter pub get @@ -34,11 +36,6 @@ jobs: - name: Run tests coverage run: flutter test --coverage - - name: Coveralls GitHub Action - uses: coverallsapp/github-action@1.1.3 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - publish: if: "(contains(github.event.head_commit.message, '[pub]') && contains(' @@ -53,28 +50,27 @@ jobs: github.ref)" name: Publish + permissions: + id-token: write # This is required for authentication using OIDC needs: [test] runs-on: ubuntu-latest - - container: - image: google/dart:latest + timeout-minutes: 5 steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Dry run pub publish - run: dart pub publish --dry-run || true - - - name: Setup credentials - run: | - pwd - mkdir -p ~/.pub-cache - cat < ~/.pub-cache/credentials.json - {"accessToken":"${{ secrets.OAUTH_ACCESS_TOKEN }}","refreshToken":"${{ secrets.OAUTH_REFRESH_TOKEN }}","idToken":"${{ secrets.OAUTH_ID_TOKEN }}","tokenEndpoint":"https://accounts.google.com/o/oauth2/token","scopes":["openid","https://www.googleapis.com/auth/userinfo.email"],"expiration":1609800070574} - EOF + - uses: actions/checkout@v3 + - name: Run a one-line script + id: vars + run: echo "pkg_tag=$(cat pubspec.yaml | grep version | head -1 | awk -F= "{ print $2 }" | sed 's/[version:,\",]//g' | tr -d '[[:space:]]')" >> $GITHUB_OUTPUT + - uses: dart-lang/setup-dart@v1 + - uses: subosito/flutter-action@v2 + with: + channel: "stable" - - name: code format - run: dart format lib/*/*.dart lib/*.dart - - name: Publish pkg - run: dart pub publish --force \ No newline at end of file + - name: Add pub token + run: echo ${{secrets.OAUTH_ACCESS_TOKEN}} | dart pub token add https://pub.dev + - name: Install dependencies + run: dart pub get + - name: code format + run: dart format lib/*/*.dart lib/*.dart + - name: Publish pkg + run: dart pub publish --server=https://pub.dartlang.org -f diff --git a/CHANGELOG.md b/CHANGELOG.md index 14d55d4a..29e3a0b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +### [3.0.2] + +- support intl 18 +- support dart 3 +- added trExists extension +- fix: handle invalid saved local +- handle null returned by assetLoader +- improve parsing scriptCode from local string +- add tr-extension on build context + ### [3.0.1] - added option allowing skip keys of nested object diff --git a/README.md b/README.md index cd4556a2..91ec3189 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,8 @@ Text('title').tr() //Text widget print('title'.tr()); //String var title = tr('title') //Static function + +Text(context.tr('title')) //Extension on BuildContext ``` #### Arguments: @@ -293,6 +295,9 @@ print('day'.plural(21)); // output: 21 день //Static function var money = plural('money', 10.23) // output: You have 10.23 dollars +//Text widget with plural BuildContext extension +Text(context.plural('money', 10.23)) + //Static function with arguments var money = plural('money_args', 10.23, args: ['John', '10.23']) // output: John has 10.23 dollars diff --git a/bin/generate.dart b/bin/generate.dart index 591d182d..4c88f0bf 100644 --- a/bin/generate.dart +++ b/bin/generate.dart @@ -245,7 +245,7 @@ class CodegenLoader extends AssetLoader{ const CodegenLoader(); @override - Future> load(String fullPath, Locale locale ) { + Future?> load(String path, Locale locale) { return Future.value(mapLocales[locale.toString()]); } diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 6b4c0f78..4f8d4d24 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 11.0 diff --git a/example/ios/Podfile b/example/ios/Podfile index 6697f0a5..88359b22 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -10,78 +10,32 @@ project 'Runner', { 'Release' => :release, } -def parse_KV_file(file, separator='=') - file_abs_path = File.expand_path(file) - if !File.exists? file_abs_path - return []; +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end - generated_key_values = {} - skip_line_start_symbols = ["#", "/"] - File.foreach(file_abs_path) do |line| - next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } - plugin = line.split(pattern=separator) - if plugin.length == 2 - podname = plugin[0].strip() - path = plugin[1].strip() - podpath = File.expand_path("#{path}", file_abs_path) - generated_key_values[podname] = podpath - else - puts "Invalid plugin specification: #{line}" - end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches end - generated_key_values + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + target 'Runner' do use_frameworks! use_modular_headers! - # Flutter Pod - - copied_flutter_dir = File.join(__dir__, 'Flutter') - copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework') - copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec') - unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path) - # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet. - # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration. - # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. - - generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig') - unless File.exist?(generated_xcode_build_settings_path) - raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path) - cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR']; - - unless File.exist?(copied_framework_path) - FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir) - end - unless File.exist?(copied_podspec_path) - FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir) - end - end - - # Keep pod path relative so it can be checked into Podfile.lock. - pod 'Flutter', :path => 'Flutter' - - # Plugin Pods - - # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock - # referring to absolute paths on developers' machines. - system('rm -rf .symlinks') - system('mkdir -p .symlinks/plugins') - plugin_pods = parse_KV_file('../.flutter-plugins') - plugin_pods.each do |name, path| - symlink = File.join('.symlinks', 'plugins', name) - File.symlink(path, symlink) - pod name, :path => File.join(symlink, 'ios') - end + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end post_install do |installer| installer.pods_project.targets.each do |target| - target.build_configurations.each do |config| - config.build_settings['ENABLE_BITCODE'] = 'NO' - end + flutter_additional_ios_build_settings(target) end end diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 053177c8..3b06621d 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -168,7 +168,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -214,6 +214,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -245,6 +246,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -359,7 +361,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -442,7 +444,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -492,7 +494,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16..919434a6 100644 --- a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cf..3db53b6e 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/example/pubspec.yaml b/example/pubspec.yaml index d5362f20..a055d781 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -14,7 +14,7 @@ publish_to: none version: 1.0.0+1 environment: - sdk: ">=2.12.0-0 <3.0.0" + sdk: ">=2.12.0-0 <4.0.0" dependencies: flutter: @@ -28,8 +28,9 @@ dependencies: font_awesome_flutter: 9.0.0-nullsafety #custom loaders - easy_localization_loader: - git: https://github.com/aissat/easy_localization_loader.git +#fixme(DartAndrik): Commented due to [easy_localization_loader] package dependencies issue, uncomment after resolving. +# easy_localization_loader: +# git: https://github.com/aissat/easy_localization_loader.git dev_dependencies: flutter_test: diff --git a/lib/src/easy_localization_controller.dart b/lib/src/easy_localization_controller.dart index 6707aa41..2f63f0c8 100644 --- a/lib/src/easy_localization_controller.dart +++ b/lib/src/easy_localization_controller.dart @@ -46,7 +46,11 @@ class EasyLocalizationController extends ChangeNotifier { // If saved locale then get else if (saveLocale && _savedLocale != null) { EasyLocalization.logger('Saved locale loaded ${_savedLocale.toString()}'); - _locale = _savedLocale!; + _locale = selectLocaleFrom( + supportedLocales, + _savedLocale!, + fallbackLocale: fallbackLocale, + ); } else { // From Device Locale _locale = selectLocaleFrom( @@ -84,7 +88,7 @@ class EasyLocalizationController extends ChangeNotifier { Future loadTranslations() async { Map data; try { - data = await loadTranslationData(_locale); + data = Map.from(await loadTranslationData(_locale)); _translations = Translations(data); if (useFallbackTranslations && _fallbackLocale != null) { Map? baseLangData; @@ -92,7 +96,7 @@ class EasyLocalizationController extends ChangeNotifier { baseLangData = await loadBaseLangTranslationData(Locale(locale.languageCode)); } - data = await loadTranslationData(_fallbackLocale!); + data = Map.from(await loadTranslationData(_fallbackLocale!)); if (baseLangData != null) { try { data.addAll(baseLangData); @@ -120,12 +124,18 @@ class EasyLocalizationController extends ChangeNotifier { return null; } - Future loadTranslationData(Locale locale) async { + Future> loadTranslationData(Locale locale) async { + late Map? data; + if (useOnlyLangCode) { - return assetLoader.load(path, Locale(locale.languageCode)); + data = await assetLoader.load(path, Locale(locale.languageCode)); } else { - return assetLoader.load(path, locale); + data = await assetLoader.load(path, locale); } + + if (data == null) return {}; + + return data; } Locale get locale => _locale; diff --git a/lib/src/exceptions.dart b/lib/src/exceptions.dart new file mode 100644 index 00000000..0326e5be --- /dev/null +++ b/lib/src/exceptions.dart @@ -0,0 +1,6 @@ +class LocalizationNotFoundException implements Exception { + const LocalizationNotFoundException(); + + @override + String toString() => 'Localization not found for current context'; +} diff --git a/lib/src/localization.dart b/lib/src/localization.dart index dc5fa224..97fe73ac 100644 --- a/lib/src/localization.dart +++ b/lib/src/localization.dart @@ -206,4 +206,8 @@ class Localization { } return resource; } + + bool exists(String key){ + return _translations?.get(key) != null; + } } diff --git a/lib/src/public.dart b/lib/src/public.dart index 6fc1d74f..ba419496 100644 --- a/lib/src/public.dart +++ b/lib/src/public.dart @@ -45,6 +45,11 @@ String tr( .tr(key, args: args, namedArgs: namedArgs, gender: gender); } +bool trExists(String key) { + return Localization.instance + .exists(key); +} + /// {@template plural} /// function translate with pluralization /// [key] Localization key diff --git a/lib/src/public_ext.dart b/lib/src/public_ext.dart index f9b55b25..876099a1 100644 --- a/lib/src/public_ext.dart +++ b/lib/src/public_ext.dart @@ -1,3 +1,5 @@ +import 'package:easy_localization/src/exceptions.dart'; +import 'package:easy_localization/src/localization.dart'; import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; @@ -6,7 +8,7 @@ import 'public.dart' as ez; /// Text widget extension method for access to [tr()] and [plural()] /// Example : -/// ```drat +/// ```dart /// Text('title').tr() /// Text('day').plural(21) /// ``` @@ -73,7 +75,7 @@ extension TextTranslateExtension on Text { /// Strings extension method for access to [tr()] and [plural()] /// Example : -/// ```drat +/// ```dart /// 'title'.tr() /// 'day'.plural(21) /// ``` @@ -88,6 +90,8 @@ extension StringTranslateExtension on String { ez.tr(this, context: context, args: args, namedArgs: namedArgs, gender: gender); + bool trExists() => ez.trExists(this); + /// {@macro plural} String plural( num value, { @@ -112,7 +116,7 @@ extension StringTranslateExtension on String { /// /// Example : /// -/// ```drat +/// ```dart /// context.locale = Locale('en', 'US'); /// print(context.locale.toString()); /// @@ -164,4 +168,77 @@ extension BuildContextEasyLocalizationExtension on BuildContext { /// Reset locale to platform locale Future resetLocale() => EasyLocalization.of(this)!.resetLocale(); + + /// An extension method for translating your language keys. + /// Subscribes the widget on current [Localization] that provided from context. + /// Throws exception if [Localization] was not found. + /// + /// [key] Localization key + /// [args] List of localized strings. Replaces {} left to right + /// [namedArgs] Map of localized strings. Replaces the name keys {key_name} according to its name + /// [gender] Gender switcher. Changes the localized string based on gender string + /// + /// Example: + /// + /// ```json + /// { + /// "msg":"{} are written in the {} language", + /// "msg_named":"Easy localization is written in the {lang} language", + /// "msg_mixed":"{} are written in the {lang} language", + /// "gender":{ + /// "male":"Hi man ;) {}", + /// "female":"Hello girl :) {}", + /// "other":"Hello {}" + /// } + /// } + /// ``` + /// ```dart + /// Text(context.tr('msg', args: ['Easy localization', 'Dart']), // args + /// Text(context.tr('msg_named', namedArgs: {'lang': 'Dart'}), // namedArgs + /// Text(context.tr('msg_mixed', args: ['Easy localization'], namedArgs: {'lang': 'Dart'}), // args and namedArgs + /// Text(context.tr('gender', gender: _gender ? "female" : "male"), // gender + /// ``` + String tr( + String key, { + List? args, + Map? namedArgs, + String? gender, + }) { + final localization = Localization.of(this); + + if (localization == null) { + throw const LocalizationNotFoundException(); + } + + return localization.tr( + key, + args: args, + namedArgs: namedArgs, + gender: gender, + ); + } + + String plural( + String key, + num number, { + List? args, + Map? namedArgs, + String? name, + NumberFormat? format, + }) { + final localization = Localization.of(this); + + if (localization == null) { + throw const LocalizationNotFoundException(); + } + + return localization.plural( + key, + number, + args: args, + namedArgs: namedArgs, + name: name, + format: format, + ); + } } diff --git a/lib/src/utils.dart b/lib/src/utils.dart index b5c67ac0..eedf0e2e 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -6,7 +6,12 @@ Locale localeFromString(String localeString) { final localeList = localeString.split('_'); switch (localeList.length) { case 2: - return Locale(localeList.first, localeList.last); + return localeList.last.length == 4 // scriptCode length is 4 + ? Locale.fromSubtags( + languageCode: localeList.first, + scriptCode: localeList.last, + ) + : Locale(localeList.first, localeList.last); case 3: return Locale.fromSubtags( languageCode: localeList.first, @@ -39,7 +44,12 @@ extension StringToLocaleHelper on String { final localeList = split(separator); switch (localeList.length) { case 2: - return Locale(localeList.first, localeList.last); + return localeList.last.length == 4 // scriptCode length is 4 + ? Locale.fromSubtags( + languageCode: localeList.first, + scriptCode: localeList.last, + ) + : Locale(localeList.first, localeList.last); case 3: return Locale.fromSubtags( languageCode: localeList.first, diff --git a/packages/easy_logger/analysis_options.yaml b/packages/easy_logger/analysis_options.yaml index 14777566..966440a0 100644 --- a/packages/easy_logger/analysis_options.yaml +++ b/packages/easy_logger/analysis_options.yaml @@ -2,8 +2,6 @@ include: package:flutter_lints/flutter.yaml analyzer: - strong-mode: - implicit-casts: false errors: missing_required_param: warning missing_return: warning diff --git a/packages/easy_logger/lib/src/logger_printer.dart b/packages/easy_logger/lib/src/logger_printer.dart index 077882e4..6da7fa0a 100644 --- a/packages/easy_logger/lib/src/logger_printer.dart +++ b/packages/easy_logger/lib/src/logger_printer.dart @@ -1,51 +1,58 @@ +import 'package:flutter/foundation.dart'; + import '../easy_logger.dart'; /// Type for function printing/logging in [EasyLogger]. typedef EasyLogPrinter = Function(Object object, {String? name, LevelMessages? level, StackTrace? stackTrace}); -/// Default function printing. +/// Default debug-mode function printing. EasyLogPrinter easyLogDefaultPrinter = (Object object, {String? name, StackTrace? stackTrace, LevelMessages? level}) { - String _coloredString(String string) { - switch (level) { - case LevelMessages.debug: - // gray - return '\u001b[90m$string\u001b[0m'; - case LevelMessages.info: - // green - return '\u001b[32m$string\u001b[0m'; - case LevelMessages.warning: - // blue - return '\u001B[34m$string\u001b[0m'; - case LevelMessages.error: - // red - return '\u001b[31m$string\u001b[0m'; - default: - // gray - return '\u001b[90m$string\u001b[0m'; + final String levelName = level?.name != null ? '[${level?.name}] ' : ''; + final String tag = name != null ? '[$name] ' : ''; + + if (kDebugMode) { + print(_getColoredString(level, '$tag$levelName${object.toString()}')); + + if (stackTrace != null) { + print(_getColoredString(level, '__________________________________')); + print(_getColoredString(level, stackTrace.toString())); } } +}; + +String _getColoredString(LevelMessages? level, String string) { + switch (level) { + case LevelMessages.debug: + // gray + return '\u001b[90m$string\u001b[0m'; + case LevelMessages.info: + // green + return '\u001b[32m$string\u001b[0m'; + case LevelMessages.warning: + // blue + return '\u001B[34m$string\u001b[0m'; + case LevelMessages.error: + // red + return '\u001b[31m$string\u001b[0m'; + default: + // gray + return '\u001b[90m$string\u001b[0m'; + } +} - String _prepareObject() { - switch (level) { +extension _LevelMessagesExtension on LevelMessages { + String get name { + switch (this) { case LevelMessages.debug: - return _coloredString('[$name] [DEBUG] ${object.toString()}'); + return 'DEBUG'; case LevelMessages.info: - return _coloredString('[$name] [INFO] ${object.toString()}'); + return 'INFO'; case LevelMessages.warning: - return _coloredString('[$name] [WARNING] ${object.toString()}'); + return 'WARNING'; case LevelMessages.error: - return _coloredString('[$name] [ERROR] ${object.toString()}'); - default: - return _coloredString('[$name] ${object.toString()}'); + return 'ERROR'; } } - - print(_prepareObject()); - - if (stackTrace != null) { - print(_coloredString('__________________________________')); - print(_coloredString('${stackTrace.toString()}')); - } -}; +} diff --git a/pubspec.yaml b/pubspec.yaml index 8416aeae..fdf6e127 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,16 +5,16 @@ homepage: https://github.com/aissat/easy_localization issue_tracker: https://github.com/aissat/easy_localization/issues # publish_to: none -version: 3.0.2-dev.5 +version: 3.0.2 environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.12.0 <4.0.0' dependencies: flutter: sdk: flutter shared_preferences: '>=2.0.0 <3.0.0' - intl: ^0.18.0 + intl: '>=0.17.0-0 <=0.18.1' args: ^2.3.1 path: ^1.8.1 easy_logger: ^0.0.2 @@ -26,7 +26,3 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.1 - -flutter: - assets: - - i18n/ diff --git a/test/easy_localization_context_test.dart b/test/easy_localization_context_test.dart index 476b0b4b..c0348a97 100644 --- a/test/easy_localization_context_test.dart +++ b/test/easy_localization_context_test.dart @@ -60,7 +60,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', saveLocale: false, useOnlyLangCode: true, supportedLocales: const [Locale('ar')], @@ -83,7 +83,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', saveLocale: false, useOnlyLangCode: true, // fallbackLocale:Locale('en') , @@ -115,7 +115,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', // fallbackLocale:Locale('en') , supportedLocales: const [ Locale('en', 'US'), @@ -138,7 +138,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', // fallbackLocale:Locale('en') , supportedLocales: const [ Locale('en', 'US'), @@ -160,7 +160,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', supportedLocales: const [ Locale('en', 'US'), Locale('ar', 'DZ') @@ -181,7 +181,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', supportedLocales: const [ Locale('en', 'US'), Locale('ar', 'DZ') @@ -207,7 +207,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', supportedLocales: const [ Locale('en', 'US'), Locale('ar', 'DZ') @@ -228,7 +228,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', supportedLocales: const [ Locale('en', 'US'), Locale('ar', 'DZ') diff --git a/test/easy_localization_test.dart b/test/easy_localization_test.dart index bdf84c40..bfb01c6b 100644 --- a/test/easy_localization_test.dart +++ b/test/easy_localization_test.dart @@ -7,6 +7,7 @@ import 'package:easy_localization/src/localization.dart'; import 'package:easy_logger/easy_logger.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'utils/test_asset_loaders.dart'; @@ -103,6 +104,8 @@ void main() { test('localeFromString() succeeds', () async { expect(const Locale('ar'), 'ar'.toLocale()); expect(const Locale('ar', 'DZ'), 'ar_DZ'.toLocale()); + expect(const Locale.fromSubtags(languageCode: 'ar', scriptCode: 'Arab'), + 'ar_Arab'.toLocale()); expect( const Locale.fromSubtags( languageCode: 'ar', scriptCode: 'Arab', countryCode: 'DZ'), @@ -138,6 +141,52 @@ void main() { expect(Localization.instance.tr('path'), 'path/en-us.json'); }); + test('controller loads saved locale', () async { + SharedPreferences.setMockInitialValues({ + 'locale': 'en', + }); + await EasyLocalization.ensureInitialized(); + final controller = EasyLocalizationController( + supportedLocales: const [Locale('en'), Locale('fb')], + fallbackLocale: const Locale('fb'), + path: 'path', + useOnlyLangCode: true, + useFallbackTranslations: true, + onLoadError: (FlutterError e) { + log(e.toString()); + }, + saveLocale: true, + assetLoader: const JsonAssetLoader(), + ); + expect(controller.locale, const Locale('en')); + + SharedPreferences.setMockInitialValues({}); + }); + + /// E.g. if user saved a locale that was removed in a later version + test('controller loads fallback if saved locale is not supported', + () async { + SharedPreferences.setMockInitialValues({ + 'locale': 'de', + }); + await EasyLocalization.ensureInitialized(); + final controller = EasyLocalizationController( + supportedLocales: const [Locale('en'), Locale('fb')], + fallbackLocale: const Locale('fb'), + path: 'path', + useOnlyLangCode: true, + useFallbackTranslations: true, + onLoadError: (FlutterError e) { + log(e.toString()); + }, + saveLocale: true, + assetLoader: const JsonAssetLoader(), + ); + expect(controller.locale, const Locale('fb')); + + SharedPreferences.setMockInitialValues({}); + }); + group('locale', () { test('locale supports device locale', () { const en = Locale('en'); @@ -497,6 +546,11 @@ void main() { expect('test'.tr(), 'test'); }); + test('trExists', () { + expect('test'.trExists(), true); + expect('xyz'.trExists(), false); + }); + test('plural', () { expect('day'.plural(0), '0 days'); }); diff --git a/test/easy_localization_utils_test.dart b/test/easy_localization_utils_test.dart index 47b43b56..13e78026 100644 --- a/test/easy_localization_utils_test.dart +++ b/test/easy_localization_utils_test.dart @@ -26,6 +26,12 @@ void main() { expect(locale, const Locale('en', 'US')); }); + test('localeFromString language code and script code', () { + var locale = 'zh_Hant'.toLocale(); + expect(locale, + const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant')); + }); + test('localeFromString language, country, script code', () { var locale = 'zh_Hant_HK'.toLocale(); expect( diff --git a/test/easy_localization_widget_test.dart b/test/easy_localization_widget_test.dart index bbaf642a..4bca17c1 100644 --- a/test/easy_localization_widget_test.dart +++ b/test/easy_localization_widget_test.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; +import 'package:easy_localization/src/exceptions.dart'; import 'package:easy_localization/src/localization.dart'; import 'package:easy_logger/easy_logger.dart'; import 'package:flutter/material.dart'; @@ -10,9 +11,16 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'utils/test_asset_loaders.dart'; late BuildContext _context; +late String _contextTranslationValue; +late String _contextPluralValue; class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); + const MyApp({ + this.child = const MyWidget(), + Key? key, + }) : super(key: key); + + final Widget child; @override Widget build(BuildContext context) { @@ -20,7 +28,7 @@ class MyApp extends StatelessWidget { locale: EasyLocalization.of(context)!.locale, supportedLocales: EasyLocalization.of(context)!.supportedLocales, localizationsDelegates: EasyLocalization.of(context)!.delegates, - home: const MyWidget(), + home: child, ); } } @@ -42,6 +50,26 @@ class MyWidget extends StatelessWidget { } } +class MyLocalizedWidget extends StatelessWidget { + const MyLocalizedWidget({Key? key}) : super(key: key); + + @override + Widget build(context) { + _context = context; + _contextTranslationValue = context.tr('test'); + _contextPluralValue = context.plural('day', 1); + + return Scaffold( + body: Column( + children: [ + Text(_contextTranslationValue), + Text(_contextPluralValue), + ], + ), + ); + } +} + void main() async { SharedPreferences.setMockInitialValues({}); EasyLocalization.logger.enableLevels = [ @@ -92,7 +120,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', assetLoader: const RootBundleAssetLoader(), supportedLocales: const [Locale('en', 'US')], child: const MyApp(), @@ -122,7 +150,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', supportedLocales: const [Locale('en', 'US')], child: const MyApp(), )); @@ -170,7 +198,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', supportedLocales: const [Locale('en', 'US')], child: const MyApp(), )); @@ -213,7 +241,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], child: const MyApp(), )); @@ -271,7 +299,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], child: const MyApp(), )); @@ -312,7 +340,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', saveLocale: false, useOnlyLangCode: true, supportedLocales: const [Locale('en'), Locale('ar')], @@ -338,7 +366,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', saveLocale: false, useOnlyLangCode: true, supportedLocales: const [Locale('en'), Locale('ar')], @@ -364,7 +392,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', saveLocale: false, useOnlyLangCode: true, supportedLocales: const [Locale('ar')], @@ -389,7 +417,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', saveLocale: false, useOnlyLangCode: true, // fallbackLocale:Locale('en') , @@ -420,7 +448,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', // fallbackLocale:Locale('en') , supportedLocales: const [Locale('en'), Locale('ar')], child: const MyApp(), // @@ -442,7 +470,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', // fallbackLocale:Locale('en') , supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], child: const MyApp(), // @@ -465,7 +493,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', startLocale: const Locale('ar', 'DZ'), // fallbackLocale:Locale('en') , supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], @@ -499,7 +527,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', saveLocale: true, // fallbackLocale:Locale('en') , useOnlyLangCode: true, @@ -532,7 +560,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', saveLocale: true, // fallbackLocale:Locale('en') , supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], @@ -556,7 +584,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', saveLocale: false, // fallbackLocale:Locale('en') , supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], @@ -589,7 +617,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', // fallbackLocale:Locale('en') , supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], child: const MyApp(), // Locale('en', 'US'), Locale('ar','DZ') @@ -610,7 +638,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', // fallbackLocale:Locale('en') , supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], child: const MyApp(), // Locale('en', 'US'), Locale('ar','DZ') @@ -630,7 +658,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], child: const MyApp(), // Locale('en', 'US'), Locale('ar','DZ') )); @@ -649,7 +677,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', supportedLocales: const [ Locale('en', 'US'), Locale('ar', 'DZ') @@ -677,7 +705,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], child: const MyApp(), // Locale('en', 'US'), Locale('ar','DZ') )); @@ -696,7 +724,7 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', supportedLocales: const [ Locale('en', 'US'), Locale('ar', 'DZ') @@ -719,4 +747,99 @@ void main() async { }, ); }); + + group('Context extensions tests', () { + final testWidget = EasyLocalization( + path: '../../i18n', + supportedLocales: const [ + Locale('en', 'US'), + Locale('ar', 'DZ') + ], // Locale('en', 'US'), Locale('ar','DZ') + startLocale: const Locale('en', 'US'), + child: const MyApp( + child: MyLocalizedWidget(), + ), + ); + + testWidgets( + '[EasyLocalization] Throws LocalizationNotFoundException without EasyLocalization widget', + (WidgetTester tester) async { + await tester.pumpWidget(const MyLocalizedWidget()); + final exception = tester.takeException(); + + expect( + exception, + isA(), + ); + }, + ); + + testWidgets( + '[EasyLocalization] context.translate and context.plural text widgets are in the tree', + (WidgetTester tester) async { + await tester.runAsync(() async { + await tester.pumpWidget(testWidget); + + await tester.idle(); + // The async delegator load will require build on the next frame. Thus, pump + await tester.pumpAndSettle(); + + expect( + find.text(_contextTranslationValue), + findsOneWidget, + ); + expect( + find.text(_contextPluralValue), + findsOneWidget, + ); + }); + }, + ); + + testWidgets( + '[EasyLocalization] context.translate and context.plural provide relevant texts', + (WidgetTester tester) async { + await tester.runAsync(() async { + await tester.pumpWidget(testWidget); + + const expectedEnTranslateTextWidgetValue = 'test'; + const expectedArTranslateTextWidgetValue = 'اختبار'; + const expectedEnPluralTextWidgetValue = '1 day'; + const expectedArPluralTextWidgetValue = '1 يوم'; + const arabyLocale = Locale('ar', 'DZ'); + + await tester.idle(); + // The async delegator load will require build on the next frame. Thus, pump + + await tester.pumpAndSettle(); + final initialTranslationValue = _contextTranslationValue; + final initialPluralValue = _contextPluralValue; + + expect( + initialTranslationValue == expectedEnTranslateTextWidgetValue, + true, + ); + expect( + initialPluralValue == expectedEnPluralTextWidgetValue, + true, + ); + + EasyLocalization.of(_context)?.setLocale(arabyLocale); + + await tester.pumpAndSettle(); + + expect( + initialTranslationValue != _contextTranslationValue && + _contextTranslationValue == expectedArTranslateTextWidgetValue, + true, + ); + expect( + initialPluralValue != _contextPluralValue && + _contextPluralValue == expectedArPluralTextWidgetValue, + true, + ); + }); + }, + ); + }); }