diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 708efa26..8b196abd 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +1,3 @@ -# github: [aissat] -# liberapay: aissat -custom: ['https://www.buymeacoffee.com/aissat','https://www.paypal.me/aissatabdo'] +custom: ['https://www.buymeacoffee.com/aissat'] +ko_fi: aissat open_collective: flutter_easy_localization diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 1679137c..00000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,80 +0,0 @@ -name: Test and Publish - -on: - push: - branches: ['*'] - tags: ['v*','V*'] - - pull_request: - branches: ['*'] - tags: ['v*','V*'] - -jobs: - test: - name: Test - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - uses: actions/setup-java@v1 - with: - java-version: '12.x' - - uses: subosito/flutter-action@v1 - with: - channel: 'dev' # or: 'dev' or 'beta' - - - name: Install packages dependencies - run: flutter pub get - - - name: Analyze the project's Dart code - run: flutter analyze - - - name: Run tests - run: flutter test - - - name: Run tests coverage - run: flutter test --coverage - - - name: Coveralls GitHub Action - uses: coverallsapp/github-action@v1.0.1 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - - publish: - if: "(contains(github.event.head_commit.message, '[pub]') && - contains(' - refs/heads/master - refs/heads/develop - refs/tags - refs/tags/releases', - github.ref)) || - startsWith(' - refs/tags/ - refs/tags/releases/', - github.ref)" - - name: Publish - needs: [test] - runs-on: ubuntu-latest - - container: - image: google/dart:latest - - 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 - - - name: code format - run: dart format lib/*/*.dart lib/*.dart - - name: Publish pkg - run: dart pub publish --force \ No newline at end of file diff --git a/.github/workflows/pr_check.yml b/.github/workflows/pr_check.yml new file mode 100644 index 00000000..b1fca1b9 --- /dev/null +++ b/.github/workflows/pr_check.yml @@ -0,0 +1,11 @@ +name: PR Check + +on: + workflow_dispatch: + pull_request: + +jobs: + test: + name: Test + uses: ./.github/workflows/test.yml + secrets: inherit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..fd302fc5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,39 @@ +name: Release to pub.dev + +on: + workflow_dispatch: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+*' + +jobs: + test: + name: Test + uses: ./.github/workflows/test.yml + secrets: inherit + + publish: + needs: [test] + name: Publish + permissions: + id-token: write # This is required for authentication using OIDC + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - uses: actions/checkout@v3 + + - uses: dart-lang/setup-dart@v1 + + - uses: subosito/flutter-action@v2 + with: + flutter-version: "3.7.12" + + - name: Install dependencies + run: dart pub get + + - name: code format + run: dart format lib/*/*.dart lib/*.dart + + - name: Publish + run: dart pub publish --force diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..c8d744f1 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,30 @@ +name: Test + +on: + workflow_call: + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: '12.x' + + - uses: subosito/flutter-action@v2 + with: + flutter-version: "3.7.12" + + - name: Install packages dependencies + run: flutter pub get + + - name: Analyze the project's Dart code + run: flutter analyze + + - name: Run tests + run: flutter test + + - name: Run tests coverage + run: flutter test --coverage \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 14d55d4a..50101194 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +### [3.0.3] + +- replace log() with stdout.writeln() + +### [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/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..4570e50d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,11 @@ +# Contributing + +## Release process + + 1. Make sure that the changelog is updated + + 2. Make sure that the version in pubspec.yaml is correct + + 3. Create a release in the github UI. Name the release like the version, but with a v (3.7.5 -> v3.7.5). Name the tag like the release + + 4. A pipeline will run and deploy the new version to pub.dev diff --git a/README.md b/README.md index 2e34f19e..cf248fc3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Stand With Palestine](https://raw.githubusercontent.com/TheBSD/StandWithPalestine/main/banner-no-action.svg)](https://thebsd.github.io/StandWithPalestine) +

Easy and Fast internationalization for your Flutter Apps @@ -19,6 +21,7 @@ Easy and Fast internationalization for your Flutter Apps ![GitHub license](https://img.shields.io/github/license/aissat/easy_localization?style=flat-square) ![Sponsors](https://img.shields.io/opencollective/all/flutter_easy_localization?style=flat-square) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square) +[![StandWithPalestine](https://raw.githubusercontent.com/TheBSD/StandWithPalestine/main/badges/StandWithPalestine.svg)](https://github.com/TheBSD/StandWithPalestine/blob/main/docs/README.md) ## Why easy_localization? @@ -192,6 +195,8 @@ Text('title').tr() //Text widget print('title'.tr()); //String var title = tr('title') //Static function + +Text(context.tr('title')) //Extension on BuildContext ``` #### Arguments: @@ -241,11 +246,13 @@ You can use extension methods of [String] or [Text] widget, you can also use `pl #### Arguments: -| Name | Type | Description | -| ------ | -------------- | ---------------------------------------------------------------------------------------------------------------------------- | -| value | `num` | Number value for pluralization | -| args | `List` | List of localized strings. Replaces `{}` left to right | -| format | `NumberFormat` | Formats a numeric value using a [NumberFormat](https://pub.dev/documentation/intl/latest/intl/NumberFormat-class.html) class | +| Name | Type | Description | +| --------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| value | `num` | Number value for pluralization | +| args | `List` | List of localized strings. Replaces `{}` left to right | +| namedArgs | `Map` | Map of localized strings. Replaces the name keys `{key_name}` according to its name | +| name | `String` | Name of number value. Replaces `{$name}` to value | +| format | `NumberFormat` | Formats a numeric value using a [NumberFormat](https://pub.dev/documentation/intl/latest/intl/NumberFormat-class.html) class | Example: @@ -270,6 +277,12 @@ Example: "one": "{} has {} dollar", "many": "{} has {} dollars", "other": "{} has {} dollars" + }, + "money_named_args": { + "zero": "{name} has no money", + "one": "{name} has {money} dollar", + "many": "{name} has {money} dollars", + "other": "{name} has {money} dollars" } } ``` @@ -285,8 +298,15 @@ 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 + +//Static function with named arguments +var money = plural('money_named_args', 10.23, namedArgs: {'name': 'Jane', 'money': '10.23'}) // output: Jane has 10.23 dollars +var money = plural('money_named_args', 10.23, namedArgs: {'name': 'Jane'}, name: 'money') // output: Jane has 10.23 dollars ``` ### 🔥 Linked translations: @@ -378,7 +398,7 @@ Get device locale Example: ```dart -print(${context.deviceLocale.toString()}) // OUTPUT: en_US +print(context.deviceLocale.toString()) // OUTPUT: en_US ``` ### 🔥 Delete save locale `deleteSaveLocale()` diff --git a/analysis_options.yaml b/analysis_options.yaml index 33b50d99..e88416f8 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,2 +1,2 @@ #https://dart.dev/guides/language/analysis-options -include: package:pedantic/analysis_options.yaml +include: package:flutter_lints/flutter.yaml \ No newline at end of file diff --git a/bin/generate.dart b/bin/generate.dart index 03bc7b8e..33a28c1f 100644 --- a/bin/generate.dart +++ b/bin/generate.dart @@ -31,7 +31,7 @@ bool _isHelpCommand(List args) { void _printHelperDisplay() { var parser = _generateArgParser(null); - print(parser.usage); + stdout.writeln(parser.usage); } GenerateOptions _generateOption(List args) { @@ -109,7 +109,7 @@ void handleLangFiles(GenerateOptions options) async { Directory(path.join(current.path, output.path, options.outputFile)); if (!await sourcePath.exists()) { - printError('Source path does not exist'); + stderr.writeln('Source path does not exist'); return; } @@ -117,7 +117,7 @@ void handleLangFiles(GenerateOptions options) async { if (options.sourceFile != null) { final sourceFile = File(path.join(source.path, options.sourceFile)); if (!await sourceFile.exists()) { - printError('Source file does not exist (${sourceFile.toString()})'); + stderr.writeln('Source file does not exist (${sourceFile.toString()})'); return; } files = [sourceFile]; @@ -129,7 +129,7 @@ void handleLangFiles(GenerateOptions options) async { if (files.isNotEmpty) { generateFile(files, outputPath, options); } else { - printError('Source path empty'); + stderr.writeln('Source path empty'); } } @@ -162,12 +162,12 @@ void generateFile(List files, Directory outputPath, // await _writeCsv(classBuilder, files); // break; default: - printError('Format not support'); + stderr.writeln('Format not supported'); } generatedFile.writeAsStringSync(classBuilder.toString()); - printInfo('All done! File generated in ${outputPath.path}'); + stdout.writeln('All done! File generated in ${outputPath.path}'); } class _NestedTranslationObject { @@ -315,7 +315,7 @@ Future _writeJson( var gFile = ''' // DO NOT EDIT. This is code generated via package:easy_localization/generate.dart -// ignore_for_file: prefer_single_quotes +// ignore_for_file: prefer_single_quotes, avoid_renaming_method_parameters import 'dart:ui'; @@ -325,7 +325,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()]); } @@ -341,12 +341,12 @@ class CodegenLoader extends AssetLoader{ Map? data = json.decode(await fileData.readAsString()); - final mapString = JsonEncoder.withIndent(' ').convert(data); + final mapString = const JsonEncoder.withIndent(' ').convert(data); gFile += 'static const Map $localeName = $mapString;\n'; } gFile += - 'static const Map> mapLocales = \{${listLocales.join(', ')}\};'; + 'static const Map> mapLocales = {${listLocales.join(', ')}};'; classBuilder.writeln(gFile); } @@ -369,11 +369,3 @@ class CodegenLoader extends AssetLoader{ // ' static const Map> mapLocales = \{${listLocales.join(', ')}\};'); // } - -void printInfo(String info) { - print('\u001b[32measy localization: $info\u001b[0m'); -} - -void printError(String error) { - print('\u001b[31m[ERROR] easy localization: $error\u001b[0m'); -} 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/lib/main.dart b/example/lib/main.dart index e02c9a0a..073c3c49 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:easy_localization/easy_localization.dart'; //import 'package:easy_localization_loader/easy_localization_loader.dart'; // import custom loaders import 'package:flutter/material.dart'; diff --git a/example/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc index d38195aa..e71a16d2 100644 --- a/example/linux/flutter/generated_plugin_registrant.cc +++ b/example/linux/flutter/generated_plugin_registrant.cc @@ -2,6 +2,8 @@ // Generated file. Do not edit. // +// clang-format off + #include "generated_plugin_registrant.h" diff --git a/example/linux/flutter/generated_plugin_registrant.h b/example/linux/flutter/generated_plugin_registrant.h index 9bf74789..e0f0a47b 100644 --- a/example/linux/flutter/generated_plugin_registrant.h +++ b/example/linux/flutter/generated_plugin_registrant.h @@ -2,6 +2,8 @@ // Generated file. Do not edit. // +// clang-format off + #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake index 51436ae8..2e1de87a 100644 --- a/example/linux/flutter/generated_plugins.cmake +++ b/example/linux/flutter/generated_plugins.cmake @@ -5,6 +5,9 @@ list(APPEND FLUTTER_PLUGIN_LIST ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -13,3 +16,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index 287b6a9d..724bb2ac 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,7 +5,7 @@ import FlutterMacOS import Foundation -import shared_preferences_macos +import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 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_app.dart b/lib/src/easy_localization_app.dart index 98f120a0..e5c9ec8c 100644 --- a/lib/src/easy_localization_app.dart +++ b/lib/src/easy_localization_app.dart @@ -65,6 +65,7 @@ class EasyLocalization extends StatefulWidget { /// Class loader for localization files. /// You can use custom loaders from [Easy Localization Loader](https://github.com/aissat/easy_localization_loader) or create your own class. /// @Default value `const RootBundleAssetLoader()` + // ignore: prefer_typing_uninitialized_variables final assetLoader; /// Save locale in device storage. @@ -94,8 +95,10 @@ class EasyLocalization extends StatefulWidget { } @override + // ignore: library_private_types_in_public_api _EasyLocalizationState createState() => _EasyLocalizationState(); + // ignore: library_private_types_in_public_api static _EasyLocalizationProvider? of(BuildContext context) => _EasyLocalizationProvider.of(context); @@ -207,11 +210,11 @@ class _EasyLocalizationProvider extends InheritedWidget { // Locale get startLocale => parent.startLocale; /// Change app locale - Future setLocale(Locale _locale) async { + Future setLocale(Locale locale) async { // Check old locale - if (_locale != _localeState.locale) { - assert(parent.supportedLocales.contains(_locale)); - await _localeState.setLocale(_locale); + if (locale != _localeState.locale) { + assert(parent.supportedLocales.contains(locale)); + await _localeState.setLocale(locale); } } diff --git a/lib/src/easy_localization_controller.dart b/lib/src/easy_localization_controller.dart index 0b8a5a22..2f63f0c8 100644 --- a/lib/src/easy_localization_controller.dart +++ b/lib/src/easy_localization_controller.dart @@ -14,6 +14,7 @@ class EasyLocalizationController extends ChangeNotifier { Locale? _fallbackLocale; final Function(FlutterError e) onLoadError; + // ignore: prefer_typing_uninitialized_variables final assetLoader; final String path; final bool useFallbackTranslations; @@ -45,17 +46,36 @@ 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 = supportedLocales.firstWhere( - (locale) => _checkInitLocale(locale, _deviceLocale), - orElse: () => _getFallbackLocale(supportedLocales, fallbackLocale)); + _locale = selectLocaleFrom( + supportedLocales, + _deviceLocale, + fallbackLocale: fallbackLocale, + ); } } + @visibleForTesting + static Locale selectLocaleFrom( + List supportedLocales, + Locale deviceLocale, { + Locale? fallbackLocale, + }) { + final selectedLocale = supportedLocales.firstWhere( + (locale) => locale.supports(deviceLocale), + orElse: () => _getFallbackLocale(supportedLocales, fallbackLocale), + ); + return selectedLocale; + } + //Get fallback Locale - Locale _getFallbackLocale( + static Locale _getFallbackLocale( List supportedLocales, Locale? fallbackLocale) { //If fallbackLocale not set then return first from supportedLocales if (fallbackLocale != null) { @@ -65,22 +85,25 @@ class EasyLocalizationController extends ChangeNotifier { } } - bool _checkInitLocale(Locale locale, Locale? _deviceLocale) { - // If supported locale not contain countryCode then check only languageCode - if (locale.countryCode == null) { - return (locale.languageCode == _deviceLocale!.languageCode); - } else { - return (locale == _deviceLocale); - } - } - Future loadTranslations() async { Map data; try { - data = await loadTranslationData(_locale); + data = Map.from(await loadTranslationData(_locale)); _translations = Translations(data); if (useFallbackTranslations && _fallbackLocale != null) { - data = await loadTranslationData(_fallbackLocale!); + Map? baseLangData; + if (_locale.countryCode != null && _locale.countryCode!.isNotEmpty) { + baseLangData = + await loadBaseLangTranslationData(Locale(locale.languageCode)); + } + data = Map.from(await loadTranslationData(_fallbackLocale!)); + if (baseLangData != null) { + try { + data.addAll(baseLangData); + } on UnsupportedError { + data = Map.of(data)..addAll(baseLangData); + } + } _fallbackTranslations = Translations(data); } } on FlutterError catch (e) { @@ -90,12 +113,29 @@ class EasyLocalizationController extends ChangeNotifier { } } - Future loadTranslationData(Locale locale) async { + Future?> loadBaseLangTranslationData( + Locale locale) async { + try { + return await loadTranslationData(Locale(locale.languageCode)); + } on FlutterError catch (e) { + // Disregard asset not found FlutterError when attempting to load base language fallback + EasyLocalization.logger.warning(e.message); + } + return null; + } + + 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; @@ -110,24 +150,24 @@ class EasyLocalizationController extends ChangeNotifier { Future _saveLocale(Locale? locale) async { if (!saveLocale) return; - final _preferences = await SharedPreferences.getInstance(); - await _preferences.setString('locale', locale.toString()); + final preferences = await SharedPreferences.getInstance(); + await preferences.setString('locale', locale.toString()); EasyLocalization.logger('Locale $locale saved'); } static Future initEasyLocation() async { - final _preferences = await SharedPreferences.getInstance(); - final _strLocale = _preferences.getString('locale'); - _savedLocale = _strLocale != null ? _strLocale.toLocale() : null; - final _foundPlatformLocale = await findSystemLocale(); - _deviceLocale = _foundPlatformLocale.toLocale(); + final preferences = await SharedPreferences.getInstance(); + final strLocale = preferences.getString('locale'); + _savedLocale = strLocale?.toLocale(); + final foundPlatformLocale = await findSystemLocale(); + _deviceLocale = foundPlatformLocale.toLocale(); EasyLocalization.logger.debug('Localization initialized'); } Future deleteSaveLocale() async { _savedLocale = null; - final _preferences = await SharedPreferences.getInstance(); - await _preferences.remove('locale'); + final preferences = await SharedPreferences.getInstance(); + await preferences.remove('locale'); EasyLocalization.logger('Saved locale deleted'); } @@ -139,3 +179,25 @@ class EasyLocalizationController extends ChangeNotifier { await setLocale(_deviceLocale); } } + +@visibleForTesting +extension LocaleExtension on Locale { + bool supports(Locale locale) { + if (this == locale) { + return true; + } + if (languageCode != locale.languageCode) { + return false; + } + if (countryCode != null && + countryCode!.isNotEmpty && + countryCode != locale.countryCode) { + return false; + } + if (scriptCode != null && scriptCode != locale.scriptCode) { + return false; + } + + return true; + } +} 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 3dfa8341..57742ee3 100644 --- a/lib/src/localization.dart +++ b/lib/src/localization.dart @@ -1,8 +1,6 @@ -import 'dart:ui'; - import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/widgets.dart'; -import 'package:intl/intl.dart'; + import 'plural_rules.dart'; import 'translations.dart'; @@ -10,11 +8,11 @@ class Localization { Translations? _translations, _fallbackTranslations; late Locale _locale; - final RegExp _replaceArgRegex = RegExp(r'{}'); + final RegExp _replaceArgRegex = RegExp('{}'); final RegExp _linkKeyMatcher = RegExp(r'(?:@(?:\.[a-z]+)?:(?:[\w\-_|.]+|\([\w\-_|.]+\)))'); final RegExp _linkKeyPrefixMatcher = RegExp(r'^@(?:\.([a-z]+))?:'); - final RegExp _bracketsMatcher = RegExp(r'[()]'); + final RegExp _bracketsMatcher = RegExp('[()]'); final _modifiers = { 'upper': (String? val) => val!.toUpperCase(), 'lower': (String? val) => val!.toLowerCase(), @@ -97,7 +95,9 @@ class Localization { String _replaceArgs(String res, List? args) { if (args == null || args.isEmpty) return res; - args.forEach((String str) => res = res.replaceFirst(_replaceArgRegex, str)); + for (var str in args) { + res = res.replaceFirst(_replaceArgRegex, str); + } return res; } @@ -113,24 +113,33 @@ class Localization { return pluralRules[locale]; } - String plural(String key, num value, - {List? args, NumberFormat? format}) { - late var pluralCase; - late var res; - var pluralRule = _pluralRule(_locale.languageCode, value); + static PluralCase _pluralCaseFallback(num value) { switch (value) { case 0: - pluralCase = PluralCase.ZERO; - break; + return PluralCase.ZERO; case 1: - pluralCase = PluralCase.ONE; - break; + return PluralCase.ONE; case 2: - pluralCase = PluralCase.TWO; - break; + return PluralCase.TWO; default: - pluralCase = pluralRule!(); + return PluralCase.OTHER; } + } + + String plural( + String key, + num value, { + List? args, + Map? namedArgs, + String? name, + NumberFormat? format, + }) { + + late String res; + + final pluralRule = _pluralRule(_locale.languageCode, value); + final pluralCase = pluralRule != null ? pluralRule() : _pluralCaseFallback(value); + switch (pluralCase) { case PluralCase.ZERO: res = _resolvePlural(key, 'zero'); @@ -154,30 +163,38 @@ class Localization { throw ArgumentError.value(value, 'howMany', 'Invalid plural argument'); } - return _replaceArgs( - res, args ?? [format == null ? '$value' : format.format(value)]); + final formattedValue = format == null ? '$value' : format.format(value); + + if (name != null) { + namedArgs = {...?namedArgs, name: formattedValue}; + } + res = _replaceNamedArgs(res, namedArgs); + + return _replaceArgs(res, args ?? [formattedValue]); } String _gender(String key, {required String gender}) { - return _resolve(key + '.$gender'); + return _resolve('$key.$gender'); } String _resolvePlural(String key, String subKey) { + if (subKey == 'other') return _resolve('$key.other'); + final tag = '$key.$subKey'; - var resource = _resolve(tag); - if (resource == tag && subKey != 'other') { + var resource = _resolve(tag, logging: false, fallback: _fallbackTranslations != null); + if (resource == tag) { resource = _resolve('$key.other'); } return resource; } - String _resolve(String key, {bool logging = true}) { + String _resolve(String key, {bool logging = true, bool fallback = true}) { var resource = _translations?.get(key); if (resource == null) { if (logging) { EasyLocalization.logger.warning('Localization key [$key] not found'); } - if (_fallbackTranslations == null) { + if (_fallbackTranslations == null || !fallback) { return key; } else { resource = _fallbackTranslations?.get(key); @@ -192,4 +209,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 0a8ac5db..ba419496 100644 --- a/lib/src/public.dart +++ b/lib/src/public.dart @@ -33,11 +33,21 @@ import 'localization.dart'; /// {@endtemplate} String tr( String key, { + BuildContext? context, List? args, Map? namedArgs, String? gender, }) { - return Localization.instance.tr(key, args: args, namedArgs: namedArgs, gender: gender); + return context != null + ? Localization.of(context)! + .tr(key, args: args, namedArgs: namedArgs, gender: gender) + : Localization.instance + .tr(key, args: args, namedArgs: namedArgs, gender: gender); +} + +bool trExists(String key) { + return Localization.instance + .exists(key); } /// {@template plural} @@ -45,6 +55,9 @@ String tr( /// [key] Localization key /// [value] Number value for pluralization /// [BuildContext] The location in the tree where this widget builds +/// [args] List of localized strings. Replaces {} left to right +/// [namedArgs] Map of localized strings. Replaces the name keys {key_name} according to its name +/// [name] Name of number value. Replaces {$name} to value /// [format] Formats a numeric value using a [NumberFormat](https://pub.dev/documentation/intl/latest/intl/NumberFormat-class.html) class /// /// Example: @@ -69,6 +82,12 @@ String tr( /// "one": "{} has {} dollar", /// "many": "{} has {} dollars", /// "other": "{} has {} dollars" +/// }, +/// "money_named_args": { +/// "zero": "{name} has no money", +/// "one": "{name} has {money} dollar", +/// "many": "{name} has {money} dollars", +/// "other": "{name} has {money} dollars" /// } /// } /// ``` @@ -78,13 +97,21 @@ String tr( /// print('day'.plural(21)); // output: 21 день /// var money = plural('money', 10.23) // output: You have 10.23 dollars /// var money = plural('money_args', 10.23, args: ['John', '10.23']) // output: John has 10.23 dollars +/// var money = plural('money_named_args', 10.23, namedArgs: {'name': 'Jane'}, name: 'money') // output: Jane has 10.23 dollars /// ``` /// {@endtemplate} String plural( String key, num value, { List? args, + BuildContext? context, + Map? namedArgs, + String? name, NumberFormat? format, }) { - return Localization.instance.plural(key, value, args: args, format: format); + return context != null + ? Localization.of(context)!.plural(key, value, + args: args, namedArgs: namedArgs, name: name, format: format) + : Localization.instance.plural(key, value, + args: args, namedArgs: namedArgs, name: name, format: format); } diff --git a/lib/src/public_ext.dart b/lib/src/public_ext.dart index e49585ee..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) /// ``` @@ -14,11 +16,13 @@ extension TextTranslateExtension on Text { /// {@macro tr} Text tr( {List? args, + BuildContext? context, Map? namedArgs, String? gender}) => Text( ez.tr( data ?? '', + context: context, args: args, namedArgs: namedArgs, gender: gender, @@ -39,14 +43,20 @@ extension TextTranslateExtension on Text { /// {@macro plural} Text plural( num value, { + BuildContext? context, List? args, + Map? namedArgs, + String? name, NumberFormat? format, }) => Text( ez.plural( data ?? '', value, + context: context, args: args, + namedArgs: namedArgs, + name: name, format: format, ), key: key, @@ -65,7 +75,7 @@ extension TextTranslateExtension on Text { /// Strings extension method for access to [tr()] and [plural()] /// Example : -/// ```drat +/// ```dart /// 'title'.tr() /// 'day'.plural(21) /// ``` @@ -75,23 +85,38 @@ extension StringTranslateExtension on String { List? args, Map? namedArgs, String? gender, + BuildContext? context, }) => - ez.tr(this, args: args, namedArgs: namedArgs, gender: gender); + ez.tr(this, + context: context, args: args, namedArgs: namedArgs, gender: gender); + + bool trExists() => ez.trExists(this); /// {@macro plural} String plural( num value, { List? args, + BuildContext? context, + Map? namedArgs, + String? name, NumberFormat? format, }) => - ez.plural(this, value, args: args, format: format); + ez.plural( + this, + value, + context: context, + args: args, + namedArgs: namedArgs, + name: name, + format: format, + ); } /// BuildContext extension method for access to [locale], [supportedLocales], [fallbackLocale], [delegates] and [deleteSaveLocale()] /// /// Example : /// -/// ```drat +/// ```dart /// context.locale = Locale('en', 'US'); /// print(context.locale.toString()); /// @@ -143,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/lib/src/widgets.dart b/lib/src/widgets.dart index 80f3146c..0993a1a6 100644 --- a/lib/src/widgets.dart +++ b/lib/src/widgets.dart @@ -2,35 +2,36 @@ import 'package:flutter/material.dart'; class FutureErrorWidget extends StatelessWidget { final String msg; - const FutureErrorWidget({this.msg = 'Loading ...'}); + const FutureErrorWidget({Key? key, this.msg = 'Loading ...'}) + : super(key: key); @override Widget build(BuildContext context) { return Container( color: Colors.white, child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( + const Icon( Icons.error_outline, color: Colors.red, size: 64, textDirection: TextDirection.ltr, ), - SizedBox(height: 20), - Text( + const SizedBox(height: 20), + const Text( 'Easy Localization:', textAlign: TextAlign.center, textDirection: TextDirection.ltr, style: TextStyle( fontWeight: FontWeight.w700, color: Colors.red, fontSize: 25.0), ), - SizedBox(height: 10), + const SizedBox(height: 10), Text( '"$msg"', textAlign: TextAlign.center, textDirection: TextDirection.ltr, - style: TextStyle( + style: const TextStyle( fontWeight: FontWeight.w500, color: Colors.red, fontSize: 14.0), ), - SizedBox(height: 30), + const SizedBox(height: 30), // Center( // child: CircularProgressIndicator() // ), diff --git a/packages/easy_logger/analysis_options.yaml b/packages/easy_logger/analysis_options.yaml index 86bd4221..966440a0 100644 --- a/packages/easy_logger/analysis_options.yaml +++ b/packages/easy_logger/analysis_options.yaml @@ -1,9 +1,7 @@ #https://dart.dev/guides/language/analysis-options -include: package:pedantic/analysis_options.yaml +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/packages/easy_logger/pubspec.yaml b/packages/easy_logger/pubspec.yaml index db995324..42cc609e 100644 --- a/packages/easy_logger/pubspec.yaml +++ b/packages/easy_logger/pubspec.yaml @@ -12,6 +12,6 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0-nullsafety.3 + flutter_lints: ^2.0.1 flutter: \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 4f5baf68..ebedd847 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,18 +5,18 @@ homepage: https://github.com/aissat/easy_localization issue_tracker: https://github.com/aissat/easy_localization/issues # publish_to: none -version: 3.0.1 +version: 3.0.3 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.17.0-0 <=0.17.0' - args: ^2.0.0 - path: '>=1.8.0-0 <=1.8.0' + shared_preferences: '>=2.0.0 <3.0.0' + intl: '>=0.17.0-0 <=0.19.0' + args: ^2.3.1 + path: ^1.8.1 easy_logger: ^0.0.2 flutter_localizations: sdk: flutter @@ -25,8 +25,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 - -flutter: - assets: - - i18n/ \ No newline at end of file + flutter_lints: ^2.0.1 diff --git a/test/easy_localization_context_test.dart b/test/easy_localization_context_test.dart index 1239ebdd..c0348a97 100644 --- a/test/easy_localization_context_test.dart +++ b/test/easy_localization_context_test.dart @@ -10,26 +10,30 @@ import 'package:shared_preferences/shared_preferences.dart'; late BuildContext _context; class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return MaterialApp( locale: context.locale, supportedLocales: context.supportedLocales, localizationsDelegates: context.localizationDelegates, - home: MyWidget(), + home: const MyWidget(), ); } } class MyWidget extends StatelessWidget { + const MyWidget({Key? key}) : super(key: key); + @override Widget build(context) { _context = context; return Scaffold( body: Column( children: [ - Text('test').tr(), - Text('day').plural(1), + const Text('test').tr(), + const Text('day').plural(1), ], ), ); @@ -56,20 +60,20 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', saveLocale: false, useOnlyLangCode: true, - supportedLocales: [Locale('ar')], - fallbackLocale: Locale('ar'), - child: MyApp(), + supportedLocales: const [Locale('ar')], + fallbackLocale: const Locale('ar'), + child: const MyApp(), )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump await tester.pump(); - expect(_context.supportedLocales, [Locale('ar')]); - expect(_context.locale, Locale('ar')); - expect(_context.fallbackLocale, Locale('ar')); + expect(_context.supportedLocales, [const Locale('ar')]); + expect(_context.locale, const Locale('ar')); + expect(_context.fallbackLocale, const Locale('ar')); }); }, ); @@ -79,21 +83,21 @@ 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') , - supportedLocales: [ + supportedLocales: const [ Locale('ar') ], // Locale('en', 'US'), Locale('ar','DZ') - child: MyApp(), + child: const MyApp(), )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump await tester.pump(); - expect(_context.supportedLocales, [Locale('ar')]); - expect(_context.locale, Locale('ar')); + expect(_context.supportedLocales, [const Locale('ar')]); + expect(_context.locale, const Locale('ar')); expect(_context.fallbackLocale, null); }); }, @@ -111,19 +115,19 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', // fallbackLocale:Locale('en') , - supportedLocales: [ + supportedLocales: const [ Locale('en', 'US'), Locale('ar', 'DZ') ], // Locale('en', 'US'), Locale('ar','DZ') - child: MyApp(), + child: const MyApp(), )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump await tester.pump(); - expect(_context.locale, Locale('ar', 'DZ')); + expect(_context.locale, const Locale('ar', 'DZ')); await _context.deleteSaveLocale(); }); }, @@ -134,19 +138,19 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', // fallbackLocale:Locale('en') , - supportedLocales: [ + supportedLocales: const [ Locale('en', 'US'), Locale('ar', 'DZ') ], // Locale('en', 'US'), Locale('ar','DZ') - child: MyApp(), + child: const MyApp(), )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump await tester.pump(); - expect(_context.locale, Locale('en', 'US')); + expect(_context.locale, const Locale('en', 'US')); }); }, ); @@ -156,12 +160,12 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', - supportedLocales: [ + path: '../../i18n', + supportedLocales: const [ Locale('en', 'US'), Locale('ar', 'DZ') ], // Locale('en', 'US'), Locale('ar','DZ') - child: MyApp(), + child: const MyApp(), )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump @@ -177,23 +181,23 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', - supportedLocales: [ + path: '../../i18n', + supportedLocales: const [ Locale('en', 'US'), Locale('ar', 'DZ') ], // Locale('en', 'US'), Locale('ar','DZ') - startLocale: Locale('ar', 'DZ'), - child: MyApp(), + startLocale: const Locale('ar', 'DZ'), + child: const MyApp(), )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump await tester.pump(); - expect(_context.locale, Locale('ar', 'DZ')); + expect(_context.locale, const Locale('ar', 'DZ')); // reset to device locale await _context.resetLocale(); await tester.pump(); - expect(_context.locale, Locale('en', 'US')); + expect(_context.locale, const Locale('en', 'US')); }); }, ); @@ -203,12 +207,12 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', - supportedLocales: [ + path: '../../i18n', + supportedLocales: const [ Locale('en', 'US'), Locale('ar', 'DZ') ], // Locale('en', 'US'), Locale('ar','DZ') - child: MyApp(), + child: const MyApp(), )); await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump @@ -224,23 +228,23 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', - supportedLocales: [ + path: '../../i18n', + supportedLocales: const [ Locale('en', 'US'), Locale('ar', 'DZ') ], // Locale('en', 'US'), Locale('ar','DZ') - startLocale: Locale('ar', 'DZ'), - child: MyApp(), + startLocale: const Locale('ar', 'DZ'), + child: const MyApp(), )); await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump await tester.pumpAndSettle(); - expect(_context.locale, Locale('ar', 'DZ')); + expect(_context.locale, const Locale('ar', 'DZ')); // reset to device locale await _context.resetLocale(); await tester.pumpAndSettle(); - expect(_context.locale, Locale('en', 'US')); + expect(_context.locale, const Locale('en', 'US')); }); }, ); diff --git a/test/easy_localization_language_specific_test.dart b/test/easy_localization_language_specific_test.dart new file mode 100644 index 00000000..2dce5467 --- /dev/null +++ b/test/easy_localization_language_specific_test.dart @@ -0,0 +1,65 @@ +import 'dart:developer'; + +import 'package:easy_localization/src/easy_localization_controller.dart'; +import 'package:easy_localization/src/localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'utils/test_asset_loaders.dart'; + +void main() { + group('language-specific-plurals', () { + var r = EasyLocalizationController( + forceLocale: const Locale('fb'), + supportedLocales: [const Locale('en'), const Locale('ru'), const Locale('fb')], + fallbackLocale: const Locale('fb'), + path: 'path', + useOnlyLangCode: true, + useFallbackTranslations: true, + onLoadError: (FlutterError e) { + log(e.toString()); + }, + saveLocale: false, + assetLoader: const JsonAssetLoader()); + + setUpAll(() async { + await r.loadTranslations(); + + }); + + test('english one', () async { + Localization.load(const Locale('en'), + translations: r.translations, + fallbackTranslations: r.fallbackTranslations); + expect(Localization.instance.plural('hat', 1), 'one hat'); + }); + test('english other', () async { + Localization.load(const Locale('en'), + translations: r.translations, + fallbackTranslations: r.fallbackTranslations); + expect(Localization.instance.plural('hat', 2), 'other hats'); + expect(Localization.instance.plural('hat', 0), 'other hats'); + expect(Localization.instance.plural('hat', 3), 'other hats'); + }); + test('russian one', () async { + Localization.load(const Locale('ru'), + translations: r.translations, + fallbackTranslations: r.fallbackTranslations); + expect(Localization.instance.plural('hat', 1), 'one hat'); + }); + test('russian few', () async { + Localization.load(const Locale('ru'), + translations: r.translations, + fallbackTranslations: r.fallbackTranslations); + expect(Localization.instance.plural('hat', 2), 'few hats'); + expect(Localization.instance.plural('hat', 3), 'few hats'); + }); + test('russian many', () async { + Localization.load(const Locale('ru'), + translations: r.translations, + fallbackTranslations: r.fallbackTranslations); + expect(Localization.instance.plural('hat', 0), 'many hats'); + expect(Localization.instance.plural('hat', 5), 'many hats'); + }); + }); +} \ No newline at end of file diff --git a/test/easy_localization_logger_test.dart b/test/easy_localization_logger_test.dart index b0483812..68385b91 100644 --- a/test/easy_localization_logger_test.dart +++ b/test/easy_localization_logger_test.dart @@ -1,17 +1,8 @@ -import 'dart:async'; - import 'package:easy_localization/easy_localization.dart'; import 'package:easy_logger/easy_logger.dart'; import 'package:flutter_test/flutter_test.dart'; -List printLog = []; -dynamic overridePrint(Function() testFn) => () { - var spec = ZoneSpecification(print: (_, __, ___, String msg) { - // Add to log instead of printing to stdout - printLog.add(msg); - }); - return Zone.current.fork(specification: spec).run(testFn); - }; +import 'easy_localization_utils_test.dart'; void main() async { group('Logger testing', () { diff --git a/test/easy_localization_test.dart b/test/easy_localization_test.dart index c66a637b..5dff2aa3 100644 --- a/test/easy_localization_test.dart +++ b/test/easy_localization_test.dart @@ -1,6 +1,4 @@ -import 'dart:async'; import 'dart:developer'; -import 'dart:ui'; import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/src/easy_localization_controller.dart'; @@ -8,35 +6,27 @@ 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:intl/intl.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'easy_localization_utils_test.dart'; import 'utils/test_asset_loaders.dart'; -var printLog = []; -dynamic overridePrint(Function() testFn) => () { - var spec = ZoneSpecification(print: (_, __, ___, String msg) { - // Add to log instead of printing to stdout - printLog.add(msg); - }); - return Zone.current.fork(specification: spec).run(testFn); - }; - void main() { group('localization', () { var r1 = EasyLocalizationController( - forceLocale: Locale('en'), + forceLocale: const Locale('en'), path: 'path/en.json', - supportedLocales: [Locale('en')], + supportedLocales: const [Locale('en')], useOnlyLangCode: true, useFallbackTranslations: false, saveLocale: false, onLoadError: (FlutterError e) { log(e.toString()); }, - assetLoader: JsonAssetLoader()); + assetLoader: const JsonAssetLoader()); var r2 = EasyLocalizationController( - forceLocale: Locale('en', 'us'), - supportedLocales: [Locale('en', 'us')], + forceLocale: const Locale('en', 'us'), + supportedLocales: const [Locale('en', 'us')], path: 'path/en-us.json', useOnlyLangCode: false, useFallbackTranslations: false, @@ -44,7 +34,7 @@ void main() { log(e.toString()); }, saveLocale: false, - assetLoader: JsonAssetLoader()); + assetLoader: const JsonAssetLoader()); setUpAll(() async { EasyLocalization.logger.enableLevels = [ LevelMessages.error, @@ -53,7 +43,7 @@ void main() { await r1.loadTranslations(); await r2.loadTranslations(); - Localization.load(Locale('en'), translations: r1.translations); + Localization.load(const Locale('en'), translations: r1.translations); }); test('is a localization object', () { expect(Localization.instance, isInstanceOf()); @@ -68,29 +58,53 @@ void main() { test('load() succeeds', () async { expect( - Localization.load(Locale('en'), translations: r1.translations), true); + Localization.load(const Locale('en'), translations: r1.translations), + true); }); test('load() with fallback succeeds', () async { expect( - Localization.load(Locale('en'), + Localization.load(const Locale('en'), translations: r1.translations, fallbackTranslations: r2.translations), true); }); + test('merge fallbackLocale with locale without country code succeeds', + () async { + await EasyLocalizationController( + forceLocale: const Locale('es', 'AR'), + supportedLocales: const [ + Locale('en'), + Locale('es'), + Locale('es', 'AR') + ], + path: 'path/en-us.json', + useOnlyLangCode: false, + useFallbackTranslations: true, + fallbackLocale: const Locale('en'), + onLoadError: (FlutterError e) { + throw e; + }, + saveLocale: false, + assetLoader: const ImmutableJsonAssetLoader(), + ).loadTranslations(); + }); + test('localeFromString() succeeds', () async { - expect(Locale('ar'), 'ar'.toLocale()); - expect(Locale('ar', 'DZ'), 'ar_DZ'.toLocale()); + 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( - Locale.fromSubtags( + const Locale.fromSubtags( languageCode: 'ar', scriptCode: 'Arab', countryCode: 'DZ'), 'ar_Arab_DZ'.toLocale()); }); test('load() Failed assertion', () async { try { - Localization.load(Locale('en'), translations: null); + Localization.load(const Locale('en'), translations: null); } on AssertionError catch (e) { // throw AssertionError('Expected ArgumentError'); expect(e, isAssertionError); @@ -99,26 +113,125 @@ void main() { test('load() correctly sets locale path', () async { expect( - Localization.load(Locale('en'), translations: r1.translations), true); + Localization.load(const Locale('en'), translations: r1.translations), + true); expect(Localization.instance.tr('path'), 'path/en.json'); }); test('load() respects useOnlyLangCode', () async { expect( - Localization.load(Locale('en'), translations: r1.translations), true); + Localization.load(const Locale('en'), translations: r1.translations), + true); expect(Localization.instance.tr('path'), 'path/en.json'); expect( - Localization.load(Locale('en', 'us'), translations: r2.translations), + Localization.load(const Locale('en', 'us'), + translations: r2.translations), true); 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'); + const en2 = Locale('en', ''); + const enUS = Locale('en', 'US'); + const enGB = Locale('en', 'GB'); + expect(en.supports(enUS), isTrue); + expect(en2.supports(enUS), isTrue); + expect(enUS.supports(enUS), isTrue); + expect(enGB.supports(enUS), isFalse); + + const zh = Locale('zh', ''); + const zh2 = Locale('zh', ''); + const zhCN = Locale('zh', 'CN'); + const zhHans = + Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'); + const zhHant = + Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'); + const zhHansCN = Locale.fromSubtags( + languageCode: 'zh', scriptCode: 'Hans', countryCode: 'CN'); + expect(zh.supports(zhHansCN), isTrue); + expect(zh2.supports(zhHansCN), isTrue); + expect(zhCN.supports(zhHansCN), isTrue); + expect(zhHans.supports(zhHansCN), isTrue); + expect(zhHant.supports(zhHansCN), isFalse); + expect(zhHansCN.supports(zhHansCN), isTrue); + }); + + test('select locale from device locale', () { + const en = Locale('en', ''); + const zh = Locale('zh', ''); + const zhHans = + Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'); + const zhHant = + Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'); + const zhHansCN = Locale.fromSubtags( + languageCode: 'zh', scriptCode: 'Hans', countryCode: 'CN'); + + expect( + EasyLocalizationController.selectLocaleFrom([en, zh], zhHansCN), + zh, + ); + expect( + EasyLocalizationController.selectLocaleFrom( + [zhHant, zhHans], zhHansCN), + zhHans, + ); + }); + }); + group('tr', () { var r = EasyLocalizationController( - forceLocale: Locale('en'), - supportedLocales: [Locale('en'), Locale('fb')], - fallbackLocale: Locale('fb'), + forceLocale: const Locale('en'), + supportedLocales: const [Locale('en'), Locale('fb')], + fallbackLocale: const Locale('fb'), path: 'path', useOnlyLangCode: true, useFallbackTranslations: true, @@ -126,11 +239,11 @@ void main() { log(e.toString()); }, saveLocale: false, - assetLoader: JsonAssetLoader()); + assetLoader: const JsonAssetLoader()); setUpAll(() async { await r.loadTranslations(); - Localization.load(Locale('en'), + Localization.load(const Locale('en'), translations: r.translations, fallbackTranslations: r.fallbackTranslations); }); @@ -299,12 +412,25 @@ void main() { }); group('plural', () { - // setUpAll(() async { - // await Localization.load(Locale('en-US'), - // path: 'path', - // useOnlyLangCode: true, - // assetLoader: JsonAssetLoader()); - // }); + var r = EasyLocalizationController( + forceLocale: const Locale('fb'), + supportedLocales: [const Locale('fb')], + fallbackLocale: const Locale('fb'), + path: 'path', + useOnlyLangCode: true, + useFallbackTranslations: true, + onLoadError: (FlutterError e) { + log(e.toString()); + }, + saveLocale: false, + assetLoader: const JsonAssetLoader()); + + setUpAll(() async { + await r.loadTranslations(); + Localization.load(const Locale('fb'), + translations: r.translations, + fallbackTranslations: r.fallbackTranslations); + }); test('zero', () { expect(Localization.instance.plural('hat', 0), 'no hats'); @@ -333,6 +459,16 @@ void main() { expect(Localization.instance.plural('hat_other', 1), 'other hats'); }); + test('two as fallback and fallback translations priority', + overridePrint(() { + printLog = []; + expect( + Localization.instance.plural('test_fallback_plurals', 2), + 'fallback two', + ); + expect(printLog, isEmpty); + })); + test('with number format', () { expect( Localization.instance @@ -354,6 +490,38 @@ void main() { expect(Localization.instance.plural('money', 3, args: ['John', '3']), 'John has 3 dollars'); }); + + test('zero with named args', () { + expect( + Localization.instance.plural('money_named_args', 0, + namedArgs: {'name': 'John', 'money': '0'}), + 'John has no money', + ); + }); + + test('one with named args', () { + expect( + Localization.instance.plural('money_named_args', 1, + namedArgs: {'name': 'John', 'money': '1'}), + 'John has 1 dollar', + ); + }); + + test('other with named args', () { + expect( + Localization.instance.plural('money_named_args', 3, + namedArgs: {'name': 'John', 'money': '3'}), + 'John has 3 dollars', + ); + }); + + test('named args and value name', () { + expect( + Localization.instance.plural('money_named_args', 3, + namedArgs: {'name': 'John'}, name: 'money'), + 'John has 3 dollars', + ); + }); }); group('extensions', () { @@ -368,6 +536,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 28db5fe3..13e78026 100644 --- a/test/easy_localization_utils_test.dart +++ b/test/easy_localization_utils_test.dart @@ -18,31 +18,37 @@ void main() { group('Locales', () { test('localeFromString only language code', () { var locale = 'en'.toLocale(); - expect(locale, Locale('en')); + expect(locale, const Locale('en')); }); test('localeFromString language code and country code', () { var locale = 'en_US'.toLocale(); - expect(locale, Locale('en', 'US')); + 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( locale, - Locale.fromSubtags( + const Locale.fromSubtags( languageCode: 'zh', scriptCode: 'Hant', countryCode: 'HK')); }); test('localeToString', () { - var locale = Locale.fromSubtags( + var locale = const Locale.fromSubtags( languageCode: 'zh', scriptCode: 'Hant', countryCode: 'HK'); var string = locale.toStringWithSeparator(); expect(string, 'zh_Hant_HK'); }); test('localeToString custom separator', () { - var locale = Locale.fromSubtags( + var locale = const Locale.fromSubtags( languageCode: 'zh', scriptCode: 'Hant', countryCode: 'HK'); var string = locale.toStringWithSeparator(separator: '|'); expect(string, 'zh|Hant|HK'); diff --git a/test/easy_localization_widget_test.dart b/test/easy_localization_widget_test.dart index ee654449..3444837d 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,28 +11,59 @@ 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({ + this.child = const MyWidget(), + Key? key, + }) : super(key: key); + + final Widget child; + @override Widget build(BuildContext context) { return MaterialApp( locale: EasyLocalization.of(context)!.locale, supportedLocales: EasyLocalization.of(context)!.supportedLocales, localizationsDelegates: EasyLocalization.of(context)!.delegates, - home: MyWidget(), + home: child, ); } } class MyWidget extends StatelessWidget { + const MyWidget({Key? key}) : super(key: key); + + @override + Widget build(context) { + _context = context; + return Scaffold( + body: Column( + children: [ + const Text('test').tr(), + const Text('day').plural(1), + ], + ), + ); + } +} + +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('test').tr(), - Text('day').plural(1), + Text(_contextTranslationValue), + Text(_contextPluralValue), ], ), ); @@ -52,9 +84,9 @@ void main() async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( path: 'path', - supportedLocales: [Locale('en', 'US')], - assetLoader: JsonAssetLoader(), - child: MyApp(), + supportedLocales: const [Locale('en', 'US')], + assetLoader: const JsonAssetLoader(), + child: const MyApp(), )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump @@ -64,8 +96,8 @@ void main() async { expect(Localization.instance, isInstanceOf()); expect(Localization.instance, Localization.of(_context)); expect(EasyLocalization.of(_context)!.supportedLocales, - [Locale('en', 'US')]); - expect(EasyLocalization.of(_context)!.locale, Locale('en', 'US')); + [const Locale('en', 'US')]); + expect(EasyLocalization.of(_context)!.locale, const Locale('en', 'US')); final trFinder = find.text('test'); expect(trFinder, findsOneWidget); @@ -73,12 +105,8 @@ void main() async { expect(pluralFinder, findsOneWidget); expect(tr('test'), 'test'); - expect(plural('day', 1), '1 day'); - expect(plural('day', 2), '2 days'); - expect(plural('day', 3), '3 other days'); expect('test'.tr(), 'test'); - expect('day'.plural(1), '1 day'); }); }, ); @@ -88,27 +116,24 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', - assetLoader: RootBundleAssetLoader(), - supportedLocales: [Locale('en', 'US')], - child: MyApp(), + path: '../../i18n', + assetLoader: const RootBundleAssetLoader(), + supportedLocales: const [Locale('en', 'US')], + child: const MyApp(), )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump await tester.pump(); expect(EasyLocalization.of(_context)!.supportedLocales, - [Locale('en', 'US')]); - expect(EasyLocalization.of(_context)!.locale, Locale('en', 'US')); + [const Locale('en', 'US')]); + expect(EasyLocalization.of(_context)!.locale, const Locale('en', 'US')); final trFinder = find.text('test'); expect(trFinder, findsOneWidget); final pluralFinder = find.text('1 day'); expect(pluralFinder, findsOneWidget); expect(tr('test'), 'test'); - expect(plural('day', 1), '1 day'); - expect(plural('day', 2), '2 days'); - expect(plural('day', 3), '3 other days'); }); }, ); @@ -118,17 +143,17 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', - supportedLocales: [Locale('en', 'US')], - child: MyApp(), + path: '../../i18n', + supportedLocales: const [Locale('en', 'US')], + child: const MyApp(), )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump await tester.pump(); expect(EasyLocalization.of(_context)!.supportedLocales, - [Locale('en', 'US')]); - expect(EasyLocalization.of(_context)!.locale, Locale('en', 'US')); + [const Locale('en', 'US')]); + expect(EasyLocalization.of(_context)!.locale, const Locale('en', 'US')); final trFinder = find.text('test'); expect(trFinder, findsOneWidget); @@ -136,9 +161,6 @@ void main() async { expect(pluralFinder, findsOneWidget); expect(tr('test'), 'test'); - expect(plural('day', 1), '1 day'); - expect(plural('day', 2), '2 days'); - expect(plural('day', 3), '3 other days'); }); }, ); @@ -148,8 +170,8 @@ void main() async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( path: 'i18', - supportedLocales: [Locale('en', 'US')], - child: MyApp(), + supportedLocales: const [Locale('en', 'US')], + child: const MyApp(), )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump @@ -166,22 +188,22 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', - supportedLocales: [Locale('en', 'US')], - child: MyApp(), + path: '../../i18n', + supportedLocales: const [Locale('en', 'US')], + child: const MyApp(), )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump await tester.pump(); expect(EasyLocalization.of(_context)!.supportedLocales, - [Locale('en', 'US')]); - expect(EasyLocalization.of(_context)!.locale, Locale('en', 'US')); + [const Locale('en', 'US')]); + expect(EasyLocalization.of(_context)!.locale, const Locale('en', 'US')); - var l = Locale('en', 'US'); + var l = const Locale('en', 'US'); await EasyLocalization.of(_context)!.setLocale(l); await tester.pump(); - expect(EasyLocalization.of(_context)!.locale, Locale('en', 'US')); + expect(EasyLocalization.of(_context)!.locale, const Locale('en', 'US')); final trFinder = find.text('test'); expect(trFinder, findsOneWidget); @@ -189,17 +211,14 @@ void main() async { expect(pluralFinder, findsOneWidget); expect(tr('test'), 'test'); - expect(plural('day', 1), '1 day'); - expect(plural('day', 2), '2 days'); - expect(plural('day', 3), '3 other days'); - expect(EasyLocalization.of(_context)!.locale, Locale('en', 'US')); + expect(EasyLocalization.of(_context)!.locale, const Locale('en', 'US')); - l = Locale('ar', 'DZ'); + l = const Locale('ar', 'DZ'); expect(() async { await EasyLocalization.of(_context)!.setLocale(l); }, throwsAssertionError); await tester.pump(); - expect(EasyLocalization.of(_context)!.locale, Locale('en', 'US')); + expect(EasyLocalization.of(_context)!.locale, const Locale('en', 'US')); }); }, ); @@ -209,9 +228,9 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', - supportedLocales: [Locale('en', 'US'), Locale('ar', 'DZ')], - child: MyApp(), + path: '../../i18n', + supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], + child: const MyApp(), )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump @@ -219,8 +238,8 @@ void main() async { expect(Localization.of(_context), isInstanceOf()); expect(EasyLocalization.of(_context)!.supportedLocales, - [Locale('en', 'US'), Locale('ar', 'DZ')]); - expect(EasyLocalization.of(_context)!.locale, Locale('en', 'US')); + [const Locale('en', 'US'), const Locale('ar', 'DZ')]); + expect(EasyLocalization.of(_context)!.locale, const Locale('en', 'US')); var trFinder = find.text('test'); expect(trFinder, findsOneWidget); @@ -228,32 +247,29 @@ void main() async { expect(pluralFinder, findsOneWidget); expect(tr('test'), 'test'); - expect(plural('day', 1), '1 day'); - expect(plural('day', 2), '2 days'); - expect(plural('day', 3), '3 other days'); - var l = Locale('en', 'US'); + var l = const Locale('en', 'US'); await EasyLocalization.of(_context)!.setLocale(l); await tester.pump(); expect(EasyLocalization.of(_context)!.locale, l); - l = Locale('ar', 'DZ'); + l = const Locale('ar', 'DZ'); await EasyLocalization.of(_context)!.setLocale(l); // await tester.idle(); await tester.pump(); expect(EasyLocalization.of(_context)!.locale, l); - l = Locale('en', 'US'); + l = const Locale('en', 'US'); await EasyLocalization.of(_context)!.setLocale(l); // await tester.idle(); await tester.pump(); expect(EasyLocalization.of(_context)!.locale, l); - l = Locale('en', 'UK'); + l = const Locale('en', 'UK'); expect(() async => {await EasyLocalization.of(_context)!.setLocale(l)}, throwsAssertionError); - l = Locale('ar', 'DZ'); + l = const Locale('ar', 'DZ'); await EasyLocalization.of(_context)!.setLocale(l); // await tester.idle(); await tester.pump(); @@ -267,22 +283,23 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', - supportedLocales: [Locale('en', 'US'), Locale('ar', 'DZ')], - child: MyApp(), + path: '../../i18n', + supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], + child: const MyApp(), )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump await tester.pump(); - await EasyLocalization.of(_context)!.setLocale(Locale('ar', 'DZ')); + await EasyLocalization.of(_context)! + .setLocale(const Locale('ar', 'DZ')); await tester.pump(); expect(EasyLocalization.of(_context)!.supportedLocales, - [Locale('en', 'US'), Locale('ar', 'DZ')]); - expect(EasyLocalization.of(_context)!.locale, Locale('ar', 'DZ')); + [const Locale('en', 'US'), const Locale('ar', 'DZ')]); + expect(EasyLocalization.of(_context)!.locale, const Locale('ar', 'DZ')); var trFinder = find.text('اختبار'); expect(trFinder, findsOneWidget); @@ -307,24 +324,21 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', saveLocale: false, useOnlyLangCode: true, - supportedLocales: [ - Locale('en'), - Locale('ar') - ], - child: MyApp(), // Locale('en', 'US'), Locale('ar','DZ') + supportedLocales: const [Locale('en'), Locale('ar')], + child: const MyApp(), // Locale('en', 'US'), Locale('ar','DZ') )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump await tester.pump(); expect(EasyLocalization.of(_context)!.supportedLocales, - [Locale('en'), Locale('ar')]); - expect(EasyLocalization.of(_context)!.locale, Locale('en')); + [const Locale('en'), const Locale('ar')]); + expect(EasyLocalization.of(_context)!.locale, const Locale('en')); - var l = Locale('en'); + var l = const Locale('en'); await EasyLocalization.of(_context)!.setLocale(l); expect(EasyLocalization.of(_context)!.locale, l); }); @@ -336,24 +350,21 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', saveLocale: false, useOnlyLangCode: true, - supportedLocales: [ - Locale('en'), - Locale('ar') - ], - child: MyApp(), // Locale('en', 'US'), Locale('ar','DZ') + supportedLocales: const [Locale('en'), Locale('ar')], + child: const MyApp(), // Locale('en', 'US'), Locale('ar','DZ') )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump await tester.pump(); expect(EasyLocalization.of(_context)!.supportedLocales, - [Locale('en'), Locale('ar')]); - expect(EasyLocalization.of(_context)!.locale, Locale('en')); + [const Locale('en'), const Locale('ar')]); + expect(EasyLocalization.of(_context)!.locale, const Locale('en')); - var l = Locale('en'); + var l = const Locale('en'); await EasyLocalization.of(_context)!.setLocale(l); expect(EasyLocalization.of(_context)!.locale, l); }); @@ -365,20 +376,22 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', saveLocale: false, useOnlyLangCode: true, - supportedLocales: [Locale('ar')], - fallbackLocale: Locale('ar'), - child: MyApp(), + supportedLocales: const [Locale('ar')], + fallbackLocale: const Locale('ar'), + child: const MyApp(), )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump await tester.pump(); - expect(EasyLocalization.of(_context)!.supportedLocales, [Locale('ar')]); - expect(EasyLocalization.of(_context)!.locale, Locale('ar')); - expect(EasyLocalization.of(_context)!.fallbackLocale, Locale('ar')); + expect(EasyLocalization.of(_context)!.supportedLocales, + [const Locale('ar')]); + expect(EasyLocalization.of(_context)!.locale, const Locale('ar')); + expect( + EasyLocalization.of(_context)!.fallbackLocale, const Locale('ar')); }); }, ); @@ -388,21 +401,20 @@ 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') , - supportedLocales: [ - Locale('ar') - ], - child: MyApp(), // Locale('en', 'US'), Locale('ar','DZ') + supportedLocales: const [Locale('ar')], + child: const MyApp(), // Locale('en', 'US'), Locale('ar','DZ') )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump await tester.pump(); - expect(EasyLocalization.of(_context)!.supportedLocales, [Locale('ar')]); - expect(EasyLocalization.of(_context)!.locale, Locale('ar')); + expect(EasyLocalization.of(_context)!.supportedLocales, + [const Locale('ar')]); + expect(EasyLocalization.of(_context)!.locale, const Locale('ar')); expect(EasyLocalization.of(_context)!.fallbackLocale, null); }); }, @@ -420,19 +432,19 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', // fallbackLocale:Locale('en') , - supportedLocales: [Locale('en'), Locale('ar')], - child: MyApp(), // + supportedLocales: const [Locale('en'), Locale('ar')], + child: const MyApp(), // )); // await tester.idle(); - await tester.pump(Duration(seconds: 2)); + await tester.pump(const Duration(seconds: 2)); // The async delegator load will require build on the next frame. Thus, pump await tester.pump(); expect(EasyLocalization.of(_context)!.supportedLocales, - [Locale('en'), Locale('ar')]); - expect(EasyLocalization.of(_context)!.locale, Locale('en')); + [const Locale('en'), const Locale('ar')]); + expect(EasyLocalization.of(_context)!.locale, const Locale('en')); expect(EasyLocalization.of(_context)!.fallbackLocale, null); }); }, @@ -442,19 +454,20 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', // fallbackLocale:Locale('en') , - supportedLocales: [Locale('en', 'US'), Locale('ar', 'DZ')], - child: MyApp(), // + supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], + child: const MyApp(), // )); // await tester.idle(); - await tester.pump(Duration(seconds: 2)); + await tester.pump(const Duration(seconds: 2)); // The async delegator load will require build on the next frame. Thus, pump await tester.pump(); expect(EasyLocalization.of(_context)!.supportedLocales, - [Locale('en', 'US'), Locale('ar', 'DZ')]); - expect(EasyLocalization.of(_context)!.locale, Locale('en', 'US')); + [const Locale('en', 'US'), const Locale('ar', 'DZ')]); + expect( + EasyLocalization.of(_context)!.locale, const Locale('en', 'US')); expect(EasyLocalization.of(_context)!.fallbackLocale, null); }); }, @@ -464,20 +477,21 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', - startLocale: Locale('ar', 'DZ'), + path: '../../i18n', + startLocale: const Locale('ar', 'DZ'), // fallbackLocale:Locale('en') , - supportedLocales: [Locale('en', 'US'), Locale('ar', 'DZ')], - child: MyApp(), // + supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], + child: const MyApp(), // )); // await tester.idle(); - await tester.pump(Duration(seconds: 2)); + await tester.pump(const Duration(seconds: 2)); // The async delegator load will require build on the next frame. Thus, pump await tester.pump(); expect(EasyLocalization.of(_context)!.supportedLocales, - [Locale('en', 'US'), Locale('ar', 'DZ')]); - expect(EasyLocalization.of(_context)!.locale, Locale('ar', 'DZ')); + [const Locale('en', 'US'), const Locale('ar', 'DZ')]); + expect( + EasyLocalization.of(_context)!.locale, const Locale('ar', 'DZ')); expect(EasyLocalization.of(_context)!.fallbackLocale, null); }); }, @@ -497,23 +511,20 @@ 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, - supportedLocales: [ - Locale('en'), - Locale('ar') - ], - child: MyApp(), // Locale('en', 'US'), Locale('ar','DZ') + supportedLocales: const [Locale('en'), Locale('ar')], + child: const MyApp(), // Locale('en', 'US'), Locale('ar','DZ') )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump await tester.pump(); expect(EasyLocalization.of(_context)!.supportedLocales, - [Locale('en'), Locale('ar')]); - expect(EasyLocalization.of(_context)!.locale, Locale('ar')); + [const Locale('en'), const Locale('ar')]); + expect(EasyLocalization.of(_context)!.locale, const Locale('ar')); expect(EasyLocalization.of(_context)!.fallbackLocale, null); }); }, @@ -533,22 +544,20 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', saveLocale: true, // fallbackLocale:Locale('en') , - supportedLocales: [ - Locale('en', 'US'), - Locale('ar', 'DZ') - ], - child: MyApp(), // Locale('en', 'US'), Locale('ar','DZ') + supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], + child: const MyApp(), // Locale('en', 'US'), Locale('ar','DZ') )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump await tester.pump(); expect(EasyLocalization.of(_context)!.supportedLocales, - [Locale('en', 'US'), Locale('ar', 'DZ')]); - expect(EasyLocalization.of(_context)!.locale, Locale('ar', 'DZ')); + [const Locale('en', 'US'), const Locale('ar', 'DZ')]); + expect( + EasyLocalization.of(_context)!.locale, const Locale('ar', 'DZ')); expect(EasyLocalization.of(_context)!.fallbackLocale, null); }); }, @@ -559,24 +568,23 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', saveLocale: false, // fallbackLocale:Locale('en') , - supportedLocales: [ - Locale('en', 'US'), - Locale('ar', 'DZ') - ], - child: MyApp(), // Locale('en', 'US'), Locale('ar','DZ') + supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], + child: const MyApp(), // Locale('en', 'US'), Locale('ar','DZ') )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump await tester.pump(); expect(EasyLocalization.of(_context)!.supportedLocales, - [Locale('en', 'US'), Locale('ar', 'DZ')]); - expect(EasyLocalization.of(_context)!.locale, Locale('en', 'US')); + [const Locale('en', 'US'), const Locale('ar', 'DZ')]); + expect( + EasyLocalization.of(_context)!.locale, const Locale('en', 'US')); - await EasyLocalization.of(_context)!.setLocale(Locale('en', 'US')); + await EasyLocalization.of(_context)! + .setLocale(const Locale('en', 'US')); }); }, ); @@ -593,19 +601,17 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', // fallbackLocale:Locale('en') , - supportedLocales: [ - Locale('en', 'US'), - Locale('ar', 'DZ') - ], - child: MyApp(), // Locale('en', 'US'), Locale('ar','DZ') + supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], + child: const MyApp(), // Locale('en', 'US'), Locale('ar','DZ') )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump await tester.pump(); - expect(EasyLocalization.of(_context)!.locale, Locale('ar', 'DZ')); + expect( + EasyLocalization.of(_context)!.locale, const Locale('ar', 'DZ')); await EasyLocalization.of(_context)!.deleteSaveLocale(); }); }, @@ -616,19 +622,17 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', + path: '../../i18n', // fallbackLocale:Locale('en') , - supportedLocales: [ - Locale('en', 'US'), - Locale('ar', 'DZ') - ], - child: MyApp(), // Locale('en', 'US'), Locale('ar','DZ') + supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], + child: const MyApp(), // Locale('en', 'US'), Locale('ar','DZ') )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump await tester.pump(); - expect(EasyLocalization.of(_context)!.locale, Locale('en', 'US')); + expect( + EasyLocalization.of(_context)!.locale, const Locale('en', 'US')); }); }, ); @@ -638,12 +642,9 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', - supportedLocales: [ - Locale('en', 'US'), - Locale('ar', 'DZ') - ], - child: MyApp(), // Locale('en', 'US'), Locale('ar','DZ') + path: '../../i18n', + supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], + child: const MyApp(), // Locale('en', 'US'), Locale('ar','DZ') )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump @@ -660,23 +661,25 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', - supportedLocales: [ + path: '../../i18n', + supportedLocales: const [ Locale('en', 'US'), Locale('ar', 'DZ') ], // Locale('en', 'US'), Locale('ar','DZ') - startLocale: Locale('ar', 'DZ'), - child: MyApp(), + startLocale: const Locale('ar', 'DZ'), + child: const MyApp(), )); // await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump await tester.pump(); - expect(EasyLocalization.of(_context)!.locale, Locale('ar', 'DZ')); + expect( + EasyLocalization.of(_context)!.locale, const Locale('ar', 'DZ')); // reset to device locale await _context.resetLocale(); await tester.pump(); - expect(EasyLocalization.of(_context)!.locale, Locale('en', 'US')); + expect( + EasyLocalization.of(_context)!.locale, const Locale('en', 'US')); }); }, ); @@ -686,12 +689,9 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', - supportedLocales: [ - Locale('en', 'US'), - Locale('ar', 'DZ') - ], - child: MyApp(), // Locale('en', 'US'), Locale('ar','DZ') + path: '../../i18n', + supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], + child: const MyApp(), // Locale('en', 'US'), Locale('ar','DZ') )); await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump @@ -708,23 +708,120 @@ void main() async { (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget(EasyLocalization( - path: 'i18n', - supportedLocales: [ + path: '../../i18n', + supportedLocales: const [ Locale('en', 'US'), Locale('ar', 'DZ') ], // Locale('en', 'US'), Locale('ar','DZ') - startLocale: Locale('ar', 'DZ'), - child: MyApp(), + startLocale: const Locale('ar', 'DZ'), + child: const MyApp(), )); await tester.idle(); // The async delegator load will require build on the next frame. Thus, pump await tester.pumpAndSettle(); - expect(EasyLocalization.of(_context)!.locale, Locale('ar', 'DZ')); + expect( + EasyLocalization.of(_context)!.locale, const Locale('ar', 'DZ')); // reset to device locale await _context.resetLocale(); await tester.pumpAndSettle(); - expect(EasyLocalization.of(_context)!.locale, Locale('en', 'US')); + expect( + EasyLocalization.of(_context)!.locale, const Locale('en', 'US')); + }); + }, + ); + }); + + 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, + ); }); }, ); diff --git a/test/utils/test_asset_loaders.dart b/test/utils/test_asset_loaders.dart index 5020cb19..0dacf425 100644 --- a/test/utils/test_asset_loaders.dart +++ b/test/utils/test_asset_loaders.dart @@ -2,6 +2,17 @@ import 'dart:ui'; import 'package:easy_localization/src/asset_loader.dart'; +class ImmutableJsonAssetLoader extends AssetLoader { + const ImmutableJsonAssetLoader(); + + @override + Future> load(String fullPath, Locale locale) { + return Future.value(const { + 'test': 'test', + }); + } +} + class JsonAssetLoader extends AssetLoader { const JsonAssetLoader(); @@ -33,14 +44,17 @@ class JsonAssetLoader extends AssetLoader { 'many': 'many hats', 'other': 'other hats' }, - 'hat_other': { - 'other': 'other hats' - }, + 'hat_other': {'other': 'other hats'}, 'money': { 'zero': '{} has no money', 'one': '{} has {} dollar', 'other': '{} has {} dollars', }, + 'money_named_args': { + 'zero': '{name} has no money', + 'one': '{name} has {money} dollar', + 'other': '{name} has {money} dollars', + }, 'nested_periods': { 'Processing': 'Processing', 'Processing.': 'Processing.', @@ -70,9 +84,22 @@ class JsonAssetLoader extends AssetLoader { } } }, - 'path': '$fullPath', + 'path': fullPath, 'test_missing_fallback': (locale.languageCode == 'fb' ? 'fallback!' : null), + 'test_fallback_plurals': (locale.languageCode == 'fb' + ? { + 'zero': 'fallback zero', + 'one': 'fallback one', + 'two': 'fallback two', + 'few': 'fallback few', + 'many': 'fallback many', + 'other': 'fallback other', + } + : { + 'one': '{} second', + 'other': '{} seconds', + }), }); } }