Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[V4]: Migrate to EasyLocalization v4 with improved initialization, asset loading, and storage #742

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from

Conversation

aissat
Copy link
Owner

@aissat aissat commented Jan 26, 2025

Hello everyone,

I have a suggestion for updating the next version of our project. I've written code to explain my idea and would greatly appreciate assistance in refining and implementing it.

Key Changes

  1. Initialization Overhaul

    • Remove supportedLocales and path from EasyLocalization widget.
    • Add ensureInitialized to load AssetLoader and storage early.
  2. AssetLoader Modernization

    • Update RootBundleAssetLoader to require path during initialization.
    • Introduce CachedAssetLoader for performance optimization.
  3. Storage Abstraction

    • Replace direct SharedPreferences usage with IEasyLocalizationStorage.
    • Add SharedPreferencesStorage as a default implementation.

Code Changes

1. Updated Initialization (main.dart)

void main() async {
+  WidgetsFlutterBinding.ensureInitialized();
+  await EasyLocalization.ensureInitialized(
+    assetLoader: RootBundleAssetLoader('assets/translations'),
+    storage: SharedPreferencesStorage(),
+  );
  runApp(
    EasyLocalization(
-      supportedLocales: const [Locale('en'), Locale('es')],
-      path: 'assets/translations',
      child: MyApp(),
    ),
  );
}

2. New SharedPreferencesStorage Class

class SharedPreferencesStorage implements IEasyLocalizationStorage {
  @override
  Future<void> init() async => await SharedPreferences.getInstance();

  @override
  T getValue<T>(String key) => SharedPreferences.getInstance().then((prefs) => prefs.get(key) as T);

  @override
  Future<void> setValue<T>(String key, T value) async {
    final prefs = await SharedPreferences.getInstance();
    if (value is String) prefs.setString(key, value);
  }

  @override
  Future<void> removeValue(String key) async => (await SharedPreferences.getInstance()).remove(key);

  @override
  Future<void> close() async {}
}

3. Optimized CachedAssetLoader

class CachedAssetLoader extends RootBundleAssetLoader {
  final Map<Locale, Map<String, dynamic>> _cache = {};

  @override
  Future<Map<String, dynamic>> load({Locale? locale}) async {
    assert(locale != null, 'Locale must not be null');
    if (_cache.containsKey(locale)) return _cache[locale]!;
    final data = await super.load(locale: locale);
    _cache[locale!] = data;
    return data;
  }
}

Migration Guide (MIGRATION.md)

## Migrating from v3.x to v4.x

1. **Move Initialization**  
   Replace `EasyLocalization` widget parameters with `ensureInitialized`:  
   ```dart
   await EasyLocalization.ensureInitialized(
     assetLoader: RootBundleAssetLoader('assets/translations'),
   );
  1. Remove Deprecated Parameters
    Delete supportedLocales and path from the EasyLocalization widget.

  2. Update AssetLoaders
    Custom loaders must now accept a path and implement load().

  3. Adopt New Storage
    Implement IEasyLocalizationStorage for locale persistence.
    Example: SharedPreferencesStorage.


Summary of Benefits

  • Faster Startup: Only active locale is loaded initially.
  • Lower Memory Usage: Unused locales are not cached by default.
  • Cleaner Architecture: Separation of asset loading, storage, and UI logic.
  • Better Maintainability: Clear migration path and reduced boilerplate.

Task:

  • update docs
  • update tests
  • update example

Summary by CodeRabbit

Release Notes for Easy Localization v4.0.0-dev.0

  • New Features

    • Added support for more flexible localization asset loading
    • Enhanced internationalization (i18n) with expanded language support
    • Introduced new storage interface for locale management
  • Breaking Changes

    • Updated Dart SDK requirement to >=3.1.0
    • Refactored asset loading mechanism
    • Removed direct dependency on SharedPreferences
  • Improvements

    • Streamlined localization configuration
    • Added support for multiple localization file formats
    • Improved theme integration for language selection
  • Bug Fixes

    • Enhanced error handling for locale loading
    • Improved locale selection logic
  • Documentation

    • Updated package metadata
    • Added topics for better discoverability

Copy link

coderabbitai bot commented Jan 26, 2025

Walkthrough

The pull request introduces significant updates to the Easy Localization package, focusing on enhancing localization asset loading, storage management, and internationalization support. The changes include modifications to method signatures, asset loading mechanisms, and test infrastructure. Key updates involve introducing a new storage interface, updating asset loader functionality, expanding supported locales, and refactoring the localization controller to provide more flexible locale management.

Changes

File Change Summary
bin/generate.dart Modified load method signature in CodegenLoader
example/lib/lang_view.dart UI styling updates with theme-based color and margin adjustments
example/lib/main.dart Simplified localization configuration, added Material Design 3 support
example/linux/flutter/CMakeLists.txt Dynamic platform specification in build process
example/pubspec.yaml Updated Dart SDK version, added new asset paths
lib/easy_localization.dart Added export for storage interface
lib/src/asset_loader.dart Enhanced asset loading with improved error handling
lib/src/easy_localization_app.dart Refactored localization initialization and delegate
lib/src/easy_localization_controller.dart Centralized locale management, added storage interface
lib/src/easy_localization_storage_interface.dart New storage interface for localization
pubspec.yaml Version bump, updated SDK requirements, added topics
i18n/ar/ar.json, i18n/ar_en/ar.json, i18n/ar_en/en.json New internationalization JSON files with translations and pluralization rules

Poem

🐰 Locales dancing, translations bright,
Easy Localization takes its flight!
From Arabic to English, words take wing,
A polyglot rabbit starts to sing 🌍
Internationalization's new delight! 🎉


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🔭 Outside diff range comments (1)
example/lib/lang_view.dart (1)

Line range hint 39-43: Avoid hardcoded indices for locale selection.

Using hardcoded indices (context.supportedLocales[1]) is fragile and could break if the locale order changes.

Consider using locale codes instead:

- locale: context.supportedLocales[1] //BuildContext extension method
+ locale: context.supportedLocales.firstWhere(
+   (l) => l.languageCode == 'ar',
+   orElse: () => context.supportedLocales.first,
+ )
🧹 Nitpick comments (16)
lib/src/easy_localization_controller.dart (4)

3-3: Remove or clarify commented-out import
The newly added imports for path utilities, asset loader, and storage interface align with the refactor. However, the commented-out SharedPreferences import may cause confusion if left unused. Consider removing it to keep the codebase clean.

Also applies to: 8-8, 11-13


18-19: Revisit mutability of _storage
If the storage implementation isn’t meant to change at runtime, consider making _storage a late final to prevent accidental reassignment. This clarifies intent and may reduce the chance of concurrency issues.


154-158: Remove lingering SharedPreferences references
The commented-out code suggests a previous approach to locale persistence. Since storage is now handled via _storage, removing these lines can reduce clutter and confusion.


189-240: Clarify asset-based loading for cross-platform usage

  • Reading locales from a directory via dart:io is incompatible with platforms like web. Ensure that this path-based approach is either gated by platform checks or supplemented with a fallback.
  • Throwing io.PathExistsException might be misleading; a custom exception or a more descriptive message can improve debugging.
lib/src/easy_localization_storage_interface.dart (1)

1-19: Add typed getters to avoid runtime type errors
Using a generic method getValue<T>() is flexible but may introduce runtime type mismatches. Consider providing typed methods (e.g., getString()) or robust runtime checks. Also ensure close() is reliably invoked to release resources (if needed).

test/root_bundle_asset_loader_test.dart (1)

5-6: Remove TODO comment.

The comment "Replace with your package and file name" should be removed as it's no longer needed.

test/easy_localization_ctl_test.dart (1)

7-15: Consider using a mock logger instead of print override.

While the print override works, using a mock logger would be more maintainable and allow for better verification of log messages.

Consider this approach:

class MockLogger {
  final List<String> logs = [];
  void debug(String message) => logs.add(message);
}

final mockLogger = MockLogger();
// Inject mockLogger into EasyLocalizationController
example/lib/lang_view.dart (1)

101-105: Add semantic labels for accessibility.

The language selection tiles should have semantic labels for better accessibility.

Add semantics to the ListTile:

 child: ListTile(
     dense: true,
+    semanticLabel: 'Select $title language',
     title: Text(
       title,
     ),
example/lib/main.dart (2)

26-35: Consider removing commented-out configuration options.

The commented options for fallbackLocale and startLocale should either be implemented or removed to maintain clean code.


41-41: Consider removing debug print statement.

The debug print statement for supported locales should be removed before production deployment.

lib/src/easy_localization_app.dart (1)

104-109: Remove commented-out code.

The commented-out properties should be removed as they're now handled by the controller.

test/easy_localization_context_test.dart (1)

43-44: Consider adding error handling test cases for asset loader initialization.

The initialization of EasyLocalization with RootBundleAssetLoader should include test cases for error scenarios, such as invalid paths or missing assets.

 await EasyLocalization.ensureInitialized(
-      assetLoader: const RootBundleAssetLoader('i18n'));
+      assetLoader: const RootBundleAssetLoader('i18n')).catchError((error) {
+        expect(error, isA<AssetLoaderException>());
+      });
test/easy_localization_test.dart (1)

75-85: Remove commented code to improve readability.

The commented parameters are no longer needed with the new initialization pattern.

 var r1 = EasyLocalizationController(
       forceLocale: const Locale('en'),
-      // path: 'path/en.json',
-      // supportedLocales: const [Locale('en')],
       useOnlyLangCode: true,
       useFallbackTranslations: false,
       saveLocale: false,
       onLoadError: (FlutterError e) {
         log(e.toString());
       },
-      // assetLoader: const JsonAssetLoader()
test/easy_localization_widget_test.dart (1)

71-78: Consider enabling logger levels for debugging.

The commented logger levels could be useful for debugging. Consider keeping them enabled in test environment.

 TestWidgetsFlutterBinding.ensureInitialized();
 SharedPreferences.setMockInitialValues({});
-  // EasyLocalization.logger.enableLevels = <LevelMessages>[
-  //   LevelMessages.error,
-  //   LevelMessages.warning,
-  // ];
+  EasyLocalization.logger.enableLevels = <LevelMessages>[
+    LevelMessages.error,
+    LevelMessages.warning,
+  ];
i18n/ar/ar.json (1)

1-11: Add metadata and more comprehensive translations.

Consider enhancing the JSON file with:

  1. Metadata about the language and region
  2. More comprehensive test cases
  3. Documentation for pluralization rules
 {
+  "_metadata": {
+    "language": "Arabic",
+    "country": "General",
+    "pluralization_rules": "Arabic has six plural forms"
+  },
   "test": "اختبار",
   "day": {
     "zero": "{} يوم",
     "one": "{} يوم",
     "two": "{} أيام",
     "few": "{} أيام",
     "many": "{} يوم",
     "other": "{} يوم"
   }
 }
example/pubspec.yaml (1)

Line range hint 31-34: Address the commented loader package dependency issue.

There's a FIXME comment regarding the easy_localization_loader package dependencies issue that needs to be resolved.

Would you like me to help investigate and resolve the dependency issue with the easy_localization_loader package? I can help create an issue to track this.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 820c9d8 and b5a12c3.

⛔ Files ignored due to path filters (1)
  • example/lib/generated/codegen_loader.g.dart is excluded by !**/generated/**
📒 Files selected for processing (21)
  • bin/generate.dart (1 hunks)
  • example/lib/lang_view.dart (3 hunks)
  • example/lib/main.dart (3 hunks)
  • example/linux/flutter/CMakeLists.txt (1 hunks)
  • example/pubspec.yaml (2 hunks)
  • example/test/widget_test.dart (1 hunks)
  • i18n/ar/ar.json (1 hunks)
  • i18n/ar_en/ar.json (1 hunks)
  • i18n/ar_en/en.json (1 hunks)
  • lib/easy_localization.dart (1 hunks)
  • lib/src/asset_loader.dart (1 hunks)
  • lib/src/easy_localization_app.dart (7 hunks)
  • lib/src/easy_localization_controller.dart (4 hunks)
  • lib/src/easy_localization_storage_interface.dart (1 hunks)
  • pubspec.yaml (1 hunks)
  • test/easy_localization_context_test.dart (1 hunks)
  • test/easy_localization_ctl_test.dart (1 hunks)
  • test/easy_localization_test.dart (7 hunks)
  • test/easy_localization_widget_test.dart (9 hunks)
  • test/root_bundle_asset_loader_test.dart (1 hunks)
  • test/utils/test_asset_loaders.dart (2 hunks)
✅ Files skipped from review due to trivial changes (2)
  • example/test/widget_test.dart
  • i18n/ar_en/en.json
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (25)
lib/src/easy_localization_controller.dart (5)

1-2: Ensure platform compatibility when using dart:io
While this import is necessary for reading directories on platforms that support I/O, be aware that web builds don’t fully support dart:io. If web compatibility is intended, consider a conditional import or alternative approach.


26-29: Initialize late fields thoroughly
Ensure _assetLoader and _locale are always assigned during initialization. If the controller initialization is skipped in certain conditions (e.g., testing), referencing these fields prematurely can throw runtime errors.


102-117: Validate fallback translation flow
When loading fallback translations, the code merges base and fallback data. This approach appears correct, but keep in mind that _listtranslationData is purged after its first use, forcing subsequent calls to rely solely on _assetLoader. Verify that this matches your intended behavior for multi-locale or repeated fallback scenarios.


162-185: Confirm one-time vs. repeated initEasyLocation calls
This static method sets _storage and _assetLoader globally. If initEasyLocation can be invoked multiple times, ensure that repeated calls won’t overwrite existing values or inadvertently reset partially loaded data.


241-241: Locale deletion confirmed
Removing the saved locale updates both the in-memory state and _storage. If an application flow requires resetting to a fallback or re-initializing after deletion, consider prompting re-initialization or reloading to avoid unexpected locale states.

Also applies to: 244-244

lib/easy_localization.dart (1)

7-7: Export interface for custom storage
Exporting easy_localization_storage_interface.dart allows external code to implement custom storage strategies, extending the library’s flexibility without tying users to a single persistence layer.

test/easy_localization_ctl_test.dart (1)

50-51: Add test cases for core functionality.

The TODO comment indicates missing test cases. Based on the PR objectives, tests should be added for:

  • Asset loading initialization
  • Storage interface integration
  • Cached asset loader functionality

Let me help verify what needs to be tested:

✅ Verification successful

Add test cases for core functionality

The review comment is accurate. The codebase shows several untested core components:

  • Asset Loaders:
    • CodegenLoader
    • ImmutableAssetLoader
    • ImmutableJsonAssetLoader
    • JsonAssetLoader
  • Storage interface implementation and integration
  • EasyLocalizationController functionality (currently has commented-out test cases)

The existing test coverage only covers RootBundleAssetLoader and basic Localization.load() scenarios.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Find core functionality that needs test coverage
echo "Searching for core functionality..."
rg -t dart "class.*Storage|class.*AssetLoader|class.*Controller" --no-filename

# Find existing test coverage
echo -e "\nSearching for existing tests..."
rg -t dart "^.*test.*Storage|^.*test.*AssetLoader|^.*test.*Controller" --no-filename

Length of output: 840


Script:

#!/bin/bash
# Find all test files
echo "All test files:"
fd -e dart "_test.dart$"

echo -e "\nController implementation:"
ast-grep --pattern 'class EasyLocalizationController {
  $$$
}'

echo -e "\nExisting test cases (broader search):"
rg -t dart "test.*\b(Storage|AssetLoader|Controller|load|save|init)" -A 2

Length of output: 2722

test/utils/test_asset_loaders.dart (3)

5-14: LGTM! Well-structured test utility class.

The ImmutableAssetLoader provides a clean, minimal implementation suitable for testing scenarios.


17-25: LGTM! Constructor and method signature updates align with new interface.

The changes properly implement the new AssetLoader interface while maintaining the test utility's immutable nature.


Line range hint 29-103: LGTM! Comprehensive test data with proper null safety.

The changes properly implement the new AssetLoader interface while maintaining a robust test data structure. The null-safe access to locale?.languageCode is a good practice.

example/lib/main.dart (2)

Line range hint 11-24: LGTM! Proper initialization setup with asset loader configuration.

The initialization is correctly configured with the new ensureInitialized method, providing a clear path for asset loading.


47-47: LGTM! Material 3 adoption.

Good practice to adopt Material 3 for modern UI components and features.

lib/src/easy_localization_app.dart (3)

65-67: Verify impact of changing saveLocale default to false.

This change might affect existing applications that rely on locale persistence. Consider documenting this breaking change in migration guide.


82-87: LGTM! Well-structured initialization method.

The ensureInitialized method properly handles both assetLoader and storage initialization.


171-172: LGTM! Proper centralization of locale management.

Good practice to centralize supported locales in the controller and validate against it.

Also applies to: 194-194

bin/generate.dart (1)

247-247: LGTM! Method signature properly updated for null safety.

The load method correctly implements the new AssetLoader interface with proper null safety.

test/easy_localization_context_test.dart (2)

66-236: 🛠️ Refactor suggestion

Consider keeping essential test cases from commented code.

While removing SharedPreferences dependency is good, some of the commented test cases cover important functionality like fallback locale and device locale handling. Consider preserving these tests with the new storage interface.


60-61: Verify test coverage for supported locales.

The test only checks the length of supported locales but not their specific values. Consider adding more detailed assertions.

 expect(_context.supportedLocales.length, 4);
+expect(_context.supportedLocales, containsAll([
+  const Locale("ar", "DZ"),
+  const Locale("en"),
+  const Locale("ar"),
+  const Locale("en", "US")
+]));
✅ Verification successful

The review suggestion is accurate and should be implemented

The suggested assertion precisely matches the actual supported locales found in the i18n directory:

  • ar-DZ
  • ar
  • en
  • en-US
🏁 Scripts executed

The following scripts were executed for the analysis:

Error: No shell scripts found


Script:

#!/bin/bash
# First, let's look at the test file content
cat test/easy_localization_context_test.dart

Length of output: 8339


Script:

#!/bin/bash
# Look for locale definitions in the source code
rg -l "supportedLocales" --type dart

# Also check for any i18n configuration files
fd "i18n"

Length of output: 414


Script:

#!/bin/bash
# Check i18n directory content
ls -la i18n/

# Look at the initialization code in the main implementation file
cat lib/src/easy_localization_app.dart

Length of output: 7980

test/easy_localization_test.dart (1)

26-48: LGTM! Good addition of asset loader test cases.

The new test group for asset loader properly verifies error cases for path and supported locales.

i18n/ar_en/ar.json (1)

1-11: Verify Arabic pluralization rules and placeholder consistency.

The JSON structure looks good, but there are a few points to consider:

  1. The placeholder {} is consistently used across all plural forms
  2. The plural forms follow Arabic grammar rules with appropriate forms for zero, one, two, few, many, and other cases

Let's validate the JSON structure and encoding:

✅ Verification successful

Arabic pluralization rules and placeholders are correctly implemented

The plural forms and placeholders are consistent across all Arabic translation files and follow the correct Arabic grammar rules.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Verify JSON validity and UTF-8 encoding
file="i18n/ar_en/ar.json"
if [ -f "$file" ]; then
    # Check JSON syntax
    jq '.' "$file" > /dev/null
    
    # Verify UTF-8 encoding
    file -i "$file" | grep -q "charset=utf-8"
    
    # Print the normalized content for visual inspection
    jq -r '.' "$file"
fi

Length of output: 430


Script:

#!/bin/bash
# Check for other Arabic translation files and references
echo "Looking for other Arabic translation files:"
fd -e json . i18n | grep -i "ar"

echo -e "\nChecking for files referencing Arabic translations:"
rg -l "ar\.json" --type ts --type js

Length of output: 519


Script:

#!/bin/bash
# Compare plural rules across Arabic translation files
echo "=== Standard Arabic (ar.json) ==="
jq -r '.day | select(.)' i18n/ar.json 2>/dev/null || echo "No 'day' key found"

echo -e "\n=== Algerian Arabic (ar-DZ.json) ==="
jq -r '.day | select(.)' i18n/ar-DZ.json 2>/dev/null || echo "No 'day' key found"

echo -e "\n=== Arabic in ar/ directory ==="
jq -r '.day | select(.)' i18n/ar/ar.json 2>/dev/null || echo "No 'day' key found"

echo -e "\n=== Arabic in ar_en/ directory (current file) ==="
jq -r '.day | select(.)' i18n/ar_en/ar.json 2>/dev/null || echo "No 'day' key found"

Length of output: 1023

pubspec.yaml (3)

8-8: Version bump indicates breaking changes.

The version change to 4.0.0-dev.0 correctly follows semantic versioning for breaking changes, as indicated in the PR objectives regarding initialization changes.


51-55: Asset paths align with new localization structure.

The flutter assets section correctly includes the new i18n directory structure, which supports the improved asset loading mechanism mentioned in the PR objectives.


33-33: Verify compatibility impact of SDK constraint.

The SDK constraint has been updated to require Dart >=3.1.0. This is a significant change that might affect existing users.

✅ Verification successful

SDK constraint change aligns with existing Dart 3 support

The package already supports Dart 3 since version 3.0.2 as documented in the changelog, making this constraint change a formalization of existing compatibility.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check pub points and SDK compatibility
curl -s "https://pub.dev/api/packages/easy_localization" | \
jq -r '.versions[] | select(.version | contains("3.")) | .pubspec.environment.sdk' | \
sort -V | tail -n 1

Length of output: 179


Script:

#!/bin/bash
# Check for migration guides and changelog entries
echo "=== Checking for CHANGELOG.md ==="
if [ -f CHANGELOG.md ]; then
    head -n 20 CHANGELOG.md
fi

echo -e "\n=== Checking for migration guides ==="
fd -t f -e md migration
fd -t f -e md guide

echo -e "\n=== Checking for Dart 3.0 related changes ==="
rg -i "dart 3" -A 2 -B 2

Length of output: 942

example/linux/flutter/CMakeLists.txt (1)

85-85: LGTM! Platform-agnostic build configuration.

The change to use ${FLUTTER_TARGET_PLATFORM} makes the build configuration more flexible and platform-agnostic.

example/pubspec.yaml (1)

52-54: Resource paths support multiple localization formats.

The addition of paths for JSON, YML, and XML formats aligns with the improved asset loading mechanism. However, ensure that the corresponding loader implementations are available and tested.

Consider documenting the supported formats and their usage in the migration guide.

Comment on lines +20 to +24
static late List<Map<String, dynamic>>? _listtranslationData;

static List<Locale> _supportedLocales = [];

static List<Locale> get supportedLocales => _supportedLocales;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid static storage for ephemeral data
_listtranslationData is cleared after a single usage. Having it as a static variable can cause confusion or unexpected behavior if multiple instances or dynamic re-initializations occur. Consider a more localized data structure or a dedicated caching solution.

required this.useOnlyLangCode,
required this.onLoadError,
Locale? startLocale,
Locale? fallbackLocale,
List<Locale>? supportedLocales,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enforce consistent handling of _supportedLocales

  1. The assertion requiring non-null storage if saveLocale is true is a good safeguard against misconfiguration.
  2. If _supportedLocales is intended to be passed into the controller, consider removing or uncommenting line 52 to avoid confusion.
  3. Confirm that _supportedLocales is fully populated prior to referencing it for fallback logic, especially if initEasyLocation is called after constructor usage.

Also applies to: 48-50, 52-52, 57-57, 64-64, 71-71

Comment on lines +125 to +140
late final Map<String, dynamic> translationData;

if (useOnlyLangCode) {
data = await assetLoader.load(path, Locale(locale.languageCode));
} else {
data = await assetLoader.load(path, locale);
try {
if (_listtranslationData != null) {
var index = _supportedLocales.indexOf(locale);
translationData = _listtranslationData![index];
_listtranslationData?.clear();
_listtranslationData = null;
} else {
translationData = await _assetLoader.load(locale: locale);
}
} catch (e) {
EasyLocalization.logger.error('loadTranslationData $e ');
}

if (data == null) return {};

return data;
return translationData;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle indexOf result and repeated loads

  • If _supportedLocales.indexOf(locale) returns -1, accessing _listtranslationData![index] will throw an error. Consider a safe check or a default handler for unknown locales.
  • Clearing _listtranslationData at first use prevents subsequent reuse. If fallback locales or repeated loading are required, reevaluate whether this data should remain available.

Comment on lines +7 to +32
/// Abstract class for loading assets.
abstract class AssetLoader {
const AssetLoader();
Future<Map<String, dynamic>?> load(String path, Locale locale);
/// Path to the assets directory.
/// Example:
/// ```dart
/// path: 'assets/translations',
/// path: 'assets/translations/lang.csv',
/// ```
final String? path;

/// List of supported locales.
/// {@macro flutter.widgets.widgetsApp.supportedLocales}
final List<Locale>? supportedLocales;

/// Constructor for [AssetLoader].
///
/// [path] is the path to the assets directory.
/// [supportedLocales] is a list of locales that the assets support.
const AssetLoader({this.path, this.supportedLocales})
: assert(path != null || supportedLocales != null,
'path or supportedLocales must not be null');

/// Loads the assets for the given [locale].
///
/// Returns a map of loaded assets.
Future<Map<String, dynamic>> load({Locale? locale});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider making locale required in load() method.

While the class structure and documentation look good, making locale optional in the load() method seems inconsistent since implementations like RootBundleAssetLoader require it and throw an error if it's null.

Consider updating the method signature to:

- Future<Map<String, dynamic>> load({Locale? locale});
+ Future<Map<String, dynamic>> load({required Locale locale});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Abstract class for loading assets.
abstract class AssetLoader {
const AssetLoader();
Future<Map<String, dynamic>?> load(String path, Locale locale);
/// Path to the assets directory.
/// Example:
/// ```dart
/// path: 'assets/translations',
/// path: 'assets/translations/lang.csv',
/// ```
final String? path;
/// List of supported locales.
/// {@macro flutter.widgets.widgetsApp.supportedLocales}
final List<Locale>? supportedLocales;
/// Constructor for [AssetLoader].
///
/// [path] is the path to the assets directory.
/// [supportedLocales] is a list of locales that the assets support.
const AssetLoader({this.path, this.supportedLocales})
: assert(path != null || supportedLocales != null,
'path or supportedLocales must not be null');
/// Loads the assets for the given [locale].
///
/// Returns a map of loaded assets.
Future<Map<String, dynamic>> load({Locale? locale});
/// Abstract class for loading assets.
abstract class AssetLoader {
/// Path to the assets directory.
/// Example:

Comment on lines +36 to 66
/// The `RootBundleAssetLoader` class is a subclass of `AssetLoader` that uses Flutter's asset loader
/// to load localized JSON files.
///
class RootBundleAssetLoader extends AssetLoader {
const RootBundleAssetLoader();
// A custom asset loader that loads assets from the root bundle

String getLocalePath(String basePath, Locale locale) {
return '$basePath/${locale.toStringWithSeparator(separator: "-")}.json';
const RootBundleAssetLoader(String path) : super(path: path);

/// Returns the path for the specified locale
///
/// The [locale] parameter represents the desired locale.
/// The returned path is based on the [path] of the asset loader
/// and the [locale] with a separator ("-") between language and country.
String getLocalePath(Locale locale) {
return '$path/${locale.toStringWithSeparator(separator: "-")}.json';
}

///
/// Loads the localized JSON file for the given `locale`.
///
/// Throws an `ArgumentError` if the `locale` is `null`.
///
@override
Future<Map<String, dynamic>?> load(String path, Locale locale) async {
var localePath = getLocalePath(path, locale);
Future<Map<String, dynamic>> load({Locale? locale}) async {
if (locale == null) throw ArgumentError.notNull('locale');
var localePath = getLocalePath(locale);
EasyLocalization.logger.debug('Load asset from $path');

// Load the asset as a string and decode it as JSON
return json.decode(await rootBundle.loadString(localePath));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling for JSON parsing and file loading.

The implementation should handle potential errors when loading and parsing JSON files.

Consider adding try-catch blocks:

 @override
 Future<Map<String, dynamic>> load({Locale? locale}) async {
   if (locale == null) throw ArgumentError.notNull('locale');
   var localePath = getLocalePath(locale);
   EasyLocalization.logger.debug('Load asset from $path');

-  // Load the asset as a string and decode it as JSON
-  return json.decode(await rootBundle.loadString(localePath));
+  try {
+    final jsonString = await rootBundle.loadString(localePath);
+    return json.decode(jsonString) as Map<String, dynamic>;
+  } on FlutterError catch (e) {
+    throw AssetLoadException('Failed to load $localePath: ${e.message}');
+  } on FormatException catch (e) {
+    throw AssetLoadException('Invalid JSON in $localePath: ${e.message}');
+  }
 }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +1 to +31
import 'package:easy_localization/src/asset_loader.dart';

import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
// Replace with your package and file name

void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('RootBundleAssetLoader', () {
const path = 'i18n'; // Replace with the actual asset path
const locale = Locale('en');
late RootBundleAssetLoader assetLoader;

setUp(() async {
assetLoader = const RootBundleAssetLoader(path);
await assetLoader.load(locale: locale);
});

test('getLocalePath returns the correct path', () async {
const expectedPath = '$path/en.json';
var result = assetLoader.getLocalePath(locale);

expect(result, expectedPath);
});

test('load throws an error when the locale is null', () {
assetLoader = const RootBundleAssetLoader(path);
expect(() => assetLoader.load(locale: null), throwsArgumentError);
});
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance test coverage with more test cases.

The test file needs additional test cases to cover error scenarios and actual asset loading.

Consider adding these test cases:

test('load throws AssetLoadException when file not found', () {
  assetLoader = const RootBundleAssetLoader('invalid/path');
  expect(
    () => assetLoader.load(locale: locale),
    throwsA(isA<AssetLoadException>()),
  );
});

test('load throws AssetLoadException for invalid JSON', () {
  // Mock rootBundle.loadString to return invalid JSON
  expect(
    () => assetLoader.load(locale: locale),
    throwsA(isA<AssetLoadException>()),
  );
});

Comment on lines +366 to +676
// expect(
// EasyLocalization.of(_context)!.locale, const Locale('en', 'US'));
// });
// },
// );

// testWidgets(
// '[EasyLocalization] device locale test',
// (WidgetTester tester) async {
// await tester.runAsync(() async {
// await tester.pumpWidget(EasyLocalization(
// // 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
// await tester.pump();

// expect(EasyLocalization.of(_context)!.deviceLocale.toString(),
// Platform.localeName);
// });
// },
// );

// testWidgets(
// '[EasyLocalization] reset device locale test',
// (WidgetTester tester) async {
// await tester.runAsync(() async {
// await tester.pumpWidget(EasyLocalization(
// // path: '../../i18n',
// // supportedLocales: const [
// // Locale('en', 'US'),
// // Locale('ar', 'DZ')
// // ], // Locale('en', 'US'), Locale('ar','DZ')
// 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, const Locale('ar', 'DZ'));
// // reset to device locale
// await _context.resetLocale();
// await tester.pump();
// expect(
// EasyLocalization.of(_context)!.locale, const Locale('en', 'US'));
// });
// },
// );

// testWidgets(
// '[EasyLocalization] device locale test',
// (WidgetTester tester) async {
// await tester.runAsync(() async {
// await tester.pumpWidget(EasyLocalization(
// // 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
// await tester.pumpAndSettle();

// expect(EasyLocalization.of(_context)!.deviceLocale.toString(),
// Platform.localeName);
// });
// },
// );

// testWidgets(
// '[EasyLocalization] reset device locale test',
// (WidgetTester tester) async {
// await tester.runAsync(() async {
// await tester.pumpWidget(EasyLocalization(
// // path: '../../i18n',
// // supportedLocales: const [
// // Locale('en', 'US'),
// // Locale('ar', 'DZ')
// // ], // Locale('en', 'US'), Locale('ar','DZ')
// 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, const Locale('ar', 'DZ'));
// // reset to device locale
// await _context.resetLocale();
// await tester.pumpAndSettle();
// expect(
// EasyLocalization.of(_context)!.locale, const Locale('en', 'US'));
// });
// },
// );
// });
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Clean up commented test groups.

Large sections of commented code make the file hard to maintain. Consider either removing them entirely or preserving essential tests with the new storage interface.

@aissat aissat closed this Jan 26, 2025
@aissat aissat reopened this Jan 26, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant