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
2 changes: 1 addition & 1 deletion bin/generate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ class CodegenLoader extends AssetLoader{
const CodegenLoader();

@override
Future<Map<String, dynamic>?> load(String path, Locale locale) {
Future<Map<String, dynamic>> load({Locale? locale}) {
return Future.value(mapLocales[locale.toString()]);
}

Expand Down
8 changes: 6 additions & 2 deletions example/lib/generated/codegen_loader.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import 'dart:ui';

import 'package:easy_localization/easy_localization.dart' show AssetLoader;

class CodegenLoader extends AssetLoader {
class CodegenLoader implements AssetLoader {
const CodegenLoader();

@override
Future<Map<String, dynamic>> load(String fullPath, Locale locale) {
Future<Map<String, dynamic>> load(Locale locale) {
return Future.value(mapLocales[locale.toString()]);
}

Expand Down Expand Up @@ -310,4 +310,8 @@ class CodegenLoader extends AssetLoader {
"ru_RU": ru_RU,
"ru": ru
};

@override
// TODO: implement path
String get path => throw UnimplementedError();
}
11 changes: 6 additions & 5 deletions example/lib/lang_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class LanguageView extends StatelessWidget {
'',
style: TextStyle(color: Colors.black),
),
backgroundColor: Colors.white,
// backgroundColor: Colors.white,
iconTheme: IconThemeData(color: Colors.black),
elevation: 0,
),
Expand All @@ -29,7 +29,7 @@ class LanguageView extends StatelessWidget {
child: Text(
'Choose language',
style: TextStyle(
color: Colors.blue,
color: Theme.of(context).primaryColor,
fontFamily: 'Montserrat',
fontWeight: FontWeight.w700,
fontSize: 18,
Expand Down Expand Up @@ -98,10 +98,11 @@ class _SwitchListTileMenuItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(left: 10, right: 10, top: 5),
margin: EdgeInsets.symmetric(vertical: 2, horizontal: 24),
decoration: BoxDecoration(
border:
isSelected(context) ? Border.all(color: Colors.blueAccent) : null,
border: isSelected(context)
? Border.all(color: Theme.of(context).primaryColor)
: null,
),
child: ListTile(
dense: true,
Expand Down
36 changes: 17 additions & 19 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,8 @@ import 'lang_view.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.ensureInitialized();

runApp(EasyLocalization(
supportedLocales: [
Locale('en', 'US'),
Locale('ar', 'DZ'),
Locale('de', 'DE'),
Locale('ru', 'RU')
],
path: 'resources/langs',
child: MyApp(),
// fallbackLocale: Locale('en', 'US'),
// startLocale: Locale('de', 'DE'),
// saveLocale: false,
// useOnlyLangCode: true,

await EasyLocalization.ensureInitialized(
assetLoader: RootBundleAssetLoader(path: 'resources/langs/json'),
// optional assetLoader default used is RootBundleAssetLoader which uses flutter's assetloader
// install easy_localization_loader for enable custom loaders
// assetLoader: RootBundleAssetLoader()
Expand All @@ -35,18 +21,30 @@ void main() async {
// assetLoader: XmlAssetLoader() //multiple files
// assetLoader: XmlSingleAssetLoader() //single file
// assetLoader: CodegenLoader()
));
);

runApp(
EasyLocalization(
child: MyApp(),
// fallbackLocale: Locale('en', 'US'),
// startLocale: Locale('ar', 'DZ'),
supportedLocales: [Locale('en', 'US'), Locale('ar', 'DZ')],
saveLocale: false,
// useOnlyLangCode: true,
),
);
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
print('supportedLocales => ${context.supportedLocales}');
return MaterialApp(
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: MyHomePage(title: 'Easy localization'),
);
Expand Down Expand Up @@ -94,7 +92,7 @@ class _MyHomePageState extends State<MyHomePage> {
},
child: Icon(
Icons.language,
color: Colors.white,
// color: Colors.white,
),
),
],
Expand Down
2 changes: 1 addition & 1 deletion example/linux/flutter/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ add_custom_command(
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
linux-x64 ${CMAKE_BUILD_TYPE}
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
Expand Down
6 changes: 5 additions & 1 deletion example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ publish_to: none
version: 1.0.0+1

environment:
sdk: ">=2.12.0-0 <4.0.0"
sdk: ">=3.1.0 <4.0.0"

dependencies:
flutter:
Expand Down Expand Up @@ -49,6 +49,10 @@ flutter:
# To add assets to your application, add an assets section, like this:
assets:
- resources/langs/
- resources/langs/json/
- resources/langs/yml/
- resources/langs/xml/

# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
22 changes: 11 additions & 11 deletions example/test/widget_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ import 'package:example/main.dart';

void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(MyApp());
// // Build our app and trigger a frame.
// await tester.pumpWidget(MyApp());

// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// // Verify that our counter starts at 0.
// expect(find.text('0'), findsOneWidget);
// expect(find.text('1'), findsNothing);

// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// // Tap the '+' icon and trigger a frame.
// await tester.tap(find.byIcon(Icons.add));
// await tester.pump();

// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
// // Verify that our counter has incremented.
// expect(find.text('0'), findsNothing);
// expect(find.text('1'), findsOneWidget);
});
}
11 changes: 11 additions & 0 deletions i18n/ar/ar.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"test": "اختبار",
"day": {
"zero": "{} يوم",
"one": "{} يوم",
"two": "{} أيام",
"few": "{} أيام",
"many": "{} يوم",
"other": "{} يوم"
}
}
11 changes: 11 additions & 0 deletions i18n/ar_en/ar.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"test": "اختبار",
"day": {
"zero": "{} يوم",
"one": "{} يوم",
"two": "{} أيام",
"few": "{} أيام",
"many": "{} يوم",
"other": "{} يوم"
}
}
22 changes: 22 additions & 0 deletions i18n/ar_en/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"test": "test",
"day": {
"zero": "{} days",
"one": "{} day",
"two": "{} days",
"few": "{} few days",
"many": "{} many days",
"other": "{} other days"
},
"hat": {
"zero": "no hats",
"one": "one hat",
"two": "two hats",
"few": "few hats",
"many": "many hats",
"other": "other hats"
},
"hat_other": {
"other": "other hats"
}
}
1 change: 1 addition & 0 deletions lib/easy_localization.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export 'package:easy_localization/src/easy_localization_app.dart';
export 'package:easy_localization/src/asset_loader.dart';
export 'package:easy_localization/src/public.dart';
export 'package:easy_localization/src/public_ext.dart';
export 'package:easy_localization/src/easy_localization_storage_interface.dart';
export 'package:intl/intl.dart';
66 changes: 47 additions & 19 deletions lib/src/asset_loader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,64 @@ import 'dart:ui';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';

/// abstract class used to building your Custom AssetLoader
/// Example:
/// ```
///class FileAssetLoader extends AssetLoader {
/// @override
/// Future<Map<String, dynamic>> load(String path, Locale locale) async {
/// final file = File(path);
/// return json.decode(await file.readAsString());
/// }
///}
/// ```
/// 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});
Comment on lines +7 to +32
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:

}

///
/// default used is RootBundleAssetLoader which uses flutter's assetloader
/// 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));
}
Comment on lines +36 to 66
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.

}
Loading