-A package by wyatt studio
+This package aims to facilitate and improve the internationalization of your applications. It allows, among other things, to load translation files on the fly during the execution of the application.
## Features
-TODO: List what your package can do. Maybe include images, gifs, or videos.
+* Load translation files
+ + [x] Load translation files from assets
+ + [x] Load translation files from the network
+ + [ ] Load translation files from the file system
+ + [ ] Load translation files from multiple sources
-## Getting started
+* Supports multiple formats
+ + [x] Supports JSON format
+ + [x] Supports YAML format
+ + [x] Supports ARB formats
+ + [ ] Supports CSV format
+ + [x] Supports custom formats parsers (see Parser class)
-TODO: List prerequisites and provide or point to information on how to
-start using the package.
+* Usage
+ + [x] Detects the current locale
+ + [x] Act as a LocalizationDelegate
+ + [x] Act as a DataSource (in the sense of the Wyatt Architecture)
+
+* Other
+ + [ ] Generate translation constants from fallback translation files
## Usage
-TODO: Include short and useful examples for package users. Add longer examples
-to `/example` folder.
+You can use this package as a LocalizationDelegate or as a DataSource.
+
+### As a LocalizationDelegate
+
+It is recommended to use this package as a LocalizationDelegate. This allows you to use the `context.i18n` method to translate your strings. It follows the standard of the `flutter_localizations` package and you can use it in the MaterialApp widget as follows:
```dart
-const like = 'sample';
+class MyApp extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'Flutter Demo',
+ theme: ThemeData(
+ primarySwatch: Colors.blue,
+ ),
+ localizationsDelegates: [
+ I18nDelegate(
+ dataSource: NetworkI18nDataSourceImpl(
+ baseUri: 'https://i18n.wyatt-studio.fr/apps/flutter_demo',
+ ),
+ localeTransformer: (locale) => locale.languageCode,
+ ),
+ GlobalMaterialLocalizations.delegate,
+ GlobalWidgetsLocalizations.delegate
+ ],
+ supportedLocales: [
+ Locale('en'),
+ Locale('fr'),
+ ],
+ home: MyHomePage(title: 'Flutter Demo Home Page'),
+ );
+ }
+}
```
-## Additional information
+And in your widgets:
-TODO: Tell users more about the package: where to find more information, how to
-contribute to the package, how to file issues, what response they can expect
-from the package authors, and more.
+```dart
+Text(context.i18n('youHavePushed', {'count': 42})),
+// => 'You have pushed the button this many times: 42' in English
+// => 'Vous avez appuyé sur le bouton ce nombre de fois: 42' in French
+```
+
+### As a DataSource
+
+This gives you more control over the internationalization of your application. You can handle the loading of the translation files yourself.
+
+For example, if you want to create a Repository that will handle the loading of the translation files, you can do it.
+
+Provide DataSource with GetIt:
+
+```dart
+// Initialize i18n
+final I18nDataSource i18nDataSource =
+ await AssetsI18nDataSourceImpl.withSystemLocale(
+ basePath: 'l10n',
+ baseName: 'intl',
+ localeTransformer: (locale) => locale.languageCode,
+);
+
+// Initialize real sources/services
+GetIt.I.registerLazySingleton(
+ () => i18nDataSource,
+);
+```
+
+Create a Repository:
+
+```dart
+class I18nRepository {
+ I18nRepository({
+ required this.dataSource,
+ });
+
+ final I18nDataSource dataSource;
+
+ Future load() async {
+ final i18n = await dataSource.load();
+ return i18n;
+ }
+}
+```
+
+And use it in your cubit:
+
+```dart
+class MyCubit extends Cubit {
+ MyCubit({
+ required this.i18nRepository,
+ }) : super(MyState());
+
+ final I18nRepository i18nRepository;
+
+ Future loadI18n() async {
+ final i18n = await i18nRepository.load();
+ emit(state.copyWith(i18n: i18n));
+ }
+}
+```
+
+> Note: you should create a cache system to avoid reloading the translation files every time.
diff --git a/packages/wyatt_i18n/example/lib/main.dart b/packages/wyatt_i18n/example/lib/main.dart
index d5b685b0..fa13f3f7 100644
--- a/packages/wyatt_i18n/example/lib/main.dart
+++ b/packages/wyatt_i18n/example/lib/main.dart
@@ -29,36 +29,32 @@ class App extends StatelessWidget {
static const String title = 'Wyatt i18n Example';
@override
- Widget build(BuildContext context) {
- final I18nDataSource dataSource = NetworkDataSourceImpl(
- baseUri: Uri.parse(
- 'https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/raw/commit/75f561a19e0484e67e511dbf29601ec5f58544aa/packages/wyatt_i18n/example/assets/',
- ),
- );
-
- final I18nRepository repository =
- I18nRepositoryImpl(dataSource: dataSource);
-
- return MaterialApp(
- title: title,
- theme: ThemeData(
- primarySwatch: Colors.blue,
- ),
- localizationsDelegates: [
- I18nDelegate(repository: repository),
- GlobalMaterialLocalizations.delegate,
- GlobalWidgetsLocalizations.delegate
- ],
- home: Scaffold(
- appBar: AppBar(
- title: const Text(title),
+ Widget build(BuildContext context) => MaterialApp(
+ title: title,
+ theme: ThemeData(
+ primarySwatch: Colors.blue,
),
- body: Builder(
- builder: (ctx) => Center(
- child: Text(ctx.i18n('youHavePushed', {'count': 654})),
+ localizationsDelegates: [
+ I18nDelegate(
+ dataSource: NetworkI18nDataSourceImpl(
+ baseUri: Uri.parse(
+ 'https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/raw/commit/75f561a19e0484e67e511dbf29601ec5f58544aa/packages/wyatt_i18n/example/assets/',
+ ),
+ localeTransformer: (locale) => locale.languageCode,
+ ),
+ ),
+ GlobalMaterialLocalizations.delegate,
+ GlobalWidgetsLocalizations.delegate
+ ],
+ home: Scaffold(
+ appBar: AppBar(
+ title: const Text(title),
+ ),
+ body: Builder(
+ builder: (ctx) => Center(
+ child: Text(ctx.i18n('youHavePushed', {'count': 654})),
+ ),
),
),
- ),
- );
- }
+ );
}
diff --git a/packages/wyatt_i18n/lib/src/core/enums/format.dart b/packages/wyatt_i18n/lib/src/core/enums/format.dart
index 8b043174..f428e25d 100644
--- a/packages/wyatt_i18n/lib/src/core/enums/format.dart
+++ b/packages/wyatt_i18n/lib/src/core/enums/format.dart
@@ -14,11 +14,10 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-import 'package:wyatt_i18n/src/core/utils/arb_parser.dart';
-import 'package:wyatt_i18n/src/core/utils/json_parser.dart';
import 'package:wyatt_i18n/src/core/utils/parser.dart';
-
-import 'package:wyatt_i18n/src/core/utils/yaml_parser.dart';
+import 'package:wyatt_i18n/src/core/utils/parsers/arb_parser.dart';
+import 'package:wyatt_i18n/src/core/utils/parsers/json_parser.dart';
+import 'package:wyatt_i18n/src/core/utils/parsers/yaml_parser.dart';
/// Enum for i18n file formats and extensions.
///
diff --git a/packages/wyatt_i18n/lib/src/core/utils/intl_utils.dart b/packages/wyatt_i18n/lib/src/core/utils/intl_utils.dart
new file mode 100644
index 00000000..1721168d
--- /dev/null
+++ b/packages/wyatt_i18n/lib/src/core/utils/intl_utils.dart
@@ -0,0 +1,24 @@
+// Copyright (C) 2023 WYATT GROUP
+// Please see the AUTHORS file for details.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+import 'package:intl/intl_standalone.dart'
+ if (dart.library.html) 'package:intl/intl_browser.dart';
+
+/// Utility class for internationalization.
+abstract class IntlUtils {
+ /// Returns system locale.
+ static Future getSystemLocale() => findSystemLocale();
+}
diff --git a/packages/wyatt_i18n/lib/src/core/utils/arb_parser.dart b/packages/wyatt_i18n/lib/src/core/utils/parsers/arb_parser.dart
similarity index 95%
rename from packages/wyatt_i18n/lib/src/core/utils/arb_parser.dart
rename to packages/wyatt_i18n/lib/src/core/utils/parsers/arb_parser.dart
index 1a064a70..2ec303b6 100644
--- a/packages/wyatt_i18n/lib/src/core/utils/arb_parser.dart
+++ b/packages/wyatt_i18n/lib/src/core/utils/parsers/arb_parser.dart
@@ -14,8 +14,8 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-import 'package:wyatt_i18n/src/core/utils/json_parser.dart';
import 'package:wyatt_i18n/src/core/utils/parser.dart';
+import 'package:wyatt_i18n/src/core/utils/parsers/json_parser.dart';
/// {@template arb_parser}
/// A class that parses a given input of type [String] into a given output
diff --git a/packages/wyatt_i18n/lib/src/core/utils/i18n_file_parser.dart b/packages/wyatt_i18n/lib/src/core/utils/parsers/i18n_file_parser.dart
similarity index 95%
rename from packages/wyatt_i18n/lib/src/core/utils/i18n_file_parser.dart
rename to packages/wyatt_i18n/lib/src/core/utils/parsers/i18n_file_parser.dart
index 3314c581..3cc371e1 100644
--- a/packages/wyatt_i18n/lib/src/core/utils/i18n_file_parser.dart
+++ b/packages/wyatt_i18n/lib/src/core/utils/parsers/i18n_file_parser.dart
@@ -20,8 +20,8 @@ import 'package:wyatt_i18n/src/domain/entities/tokens.dart';
import 'package:wyatt_i18n/wyatt_i18n.dart';
/// {@template i18n_file_parser}
-/// This class is responsible for parsing the [I18nFile] and returning the
-/// translated string.
+/// This class is responsible for parsing the [I18n] and returning the
+/// translated string using the [arguments] provided and the [IcuParser].
/// {@endtemplate}
class I18nFileParser extends Parser {
/// {@macro i18n_file_parser}
@@ -30,8 +30,8 @@ class I18nFileParser extends Parser {
this.arguments = const {},
}) : super();
- /// The [I18nFile] to be parsed.
- final I18nFile i18n;
+ /// The [I18n] to be parsed.
+ final I18n i18n;
/// The arguments to be used in the translation.
final Map arguments;
diff --git a/packages/wyatt_i18n/lib/src/core/utils/icu_parser.dart b/packages/wyatt_i18n/lib/src/core/utils/parsers/icu_parser.dart
similarity index 96%
rename from packages/wyatt_i18n/lib/src/core/utils/icu_parser.dart
rename to packages/wyatt_i18n/lib/src/core/utils/parsers/icu_parser.dart
index 0eb6d84a..192e752b 100644
--- a/packages/wyatt_i18n/lib/src/core/utils/icu_parser.dart
+++ b/packages/wyatt_i18n/lib/src/core/utils/parsers/icu_parser.dart
@@ -17,6 +17,7 @@
// ignore_for_file: avoid_dynamic_calls, inference_failure_on_untyped_parameter
import 'package:petitparser/petitparser.dart' hide Token;
+import 'package:wyatt_i18n/src/core/utils/parser.dart' as wyatt;
import 'package:wyatt_i18n/src/domain/entities/tokens.dart';
/// {@template icu_parser}
@@ -24,7 +25,7 @@ import 'package:wyatt_i18n/src/domain/entities/tokens.dart';
/// See https://unicode-org.github.io/icu/userguide/format_parse/messages/
/// for the syntax.
/// {@endtemplate}
-class IcuParser {
+class IcuParser extends wyatt.Parser?> {
/// {@macro icu_parser}
IcuParser() {
// There is a cycle here, so we need the explicit
@@ -167,12 +168,13 @@ class IcuParser {
Parser get parameter => (openCurly & id & closeCurly)
.map((result) => Argument(result[1] as String));
- List? parse(String message) {
+ @override
+ List? parse(String input) {
final parsed = (compound | pluralOrGenderOrSelect | simpleText | empty)
.map(
(result) => List.from(result is List ? result : [result]),
)
- .parse(message);
+ .parse(input);
return parsed.isSuccess ? parsed.value : null;
}
diff --git a/packages/wyatt_i18n/lib/src/core/utils/json_parser.dart b/packages/wyatt_i18n/lib/src/core/utils/parsers/json_parser.dart
similarity index 100%
rename from packages/wyatt_i18n/lib/src/core/utils/json_parser.dart
rename to packages/wyatt_i18n/lib/src/core/utils/parsers/json_parser.dart
diff --git a/packages/wyatt_i18n/lib/src/core/utils/parsers/locale_parser.dart b/packages/wyatt_i18n/lib/src/core/utils/parsers/locale_parser.dart
new file mode 100644
index 00000000..03192204
--- /dev/null
+++ b/packages/wyatt_i18n/lib/src/core/utils/parsers/locale_parser.dart
@@ -0,0 +1,50 @@
+// Copyright (C) 2023 WYATT GROUP
+// Please see the AUTHORS file for details.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+import 'package:flutter/widgets.dart';
+import 'package:wyatt_i18n/wyatt_i18n.dart';
+
+/// {@template locale_parser}
+/// A parser that parses a [String] into a [Locale].
+/// Example:
+/// ```dart
+/// final parser = LocaleParser();
+/// final locale = parser.parse('en-US');
+/// // locale.languageCode == 'en'
+/// // locale.countryCode == 'US'
+/// ```
+/// {@endtemplate}
+class LocaleParser extends Parser {
+ /// {@macro locale_parser}
+ const LocaleParser() : super();
+
+ @override
+ Locale parse(String input) {
+ final languageParts = RegExp('([a-z]*)[_-]?([a-z|A-Z]*)').firstMatch(input);
+ final languageCode = languageParts?.group(1);
+ final countryCode = languageParts?.group(2);
+ if (languageCode == null) {
+ throw ParserException("Can't parse locale tag '$input'", null);
+ }
+
+ return Locale(languageCode, countryCode);
+ }
+
+ /// Serializes the given [locale] into a [String].
+ String serialize(Locale locale) => locale.countryCode == null
+ ? locale.languageCode
+ : '${locale.languageCode}_${locale.countryCode}';
+}
diff --git a/packages/wyatt_i18n/lib/src/core/utils/yaml_parser.dart b/packages/wyatt_i18n/lib/src/core/utils/parsers/yaml_parser.dart
similarity index 100%
rename from packages/wyatt_i18n/lib/src/core/utils/yaml_parser.dart
rename to packages/wyatt_i18n/lib/src/core/utils/parsers/yaml_parser.dart
diff --git a/packages/wyatt_i18n/lib/src/core/utils/utils.dart b/packages/wyatt_i18n/lib/src/core/utils/utils.dart
index d822ac44..000da567 100644
--- a/packages/wyatt_i18n/lib/src/core/utils/utils.dart
+++ b/packages/wyatt_i18n/lib/src/core/utils/utils.dart
@@ -14,9 +14,9 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-export 'arb_parser.dart';
-export 'i18n_file_parser.dart';
-export 'icu_parser.dart';
-export 'json_parser.dart';
export 'parser.dart';
-export 'yaml_parser.dart';
+export 'parsers/arb_parser.dart';
+export 'parsers/i18n_file_parser.dart';
+export 'parsers/icu_parser.dart';
+export 'parsers/json_parser.dart';
+export 'parsers/yaml_parser.dart';
diff --git a/packages/wyatt_i18n/lib/src/data/data.dart b/packages/wyatt_i18n/lib/src/data/data.dart
index 876c86da..9fdb7a4a 100644
--- a/packages/wyatt_i18n/lib/src/data/data.dart
+++ b/packages/wyatt_i18n/lib/src/data/data.dart
@@ -14,6 +14,6 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-export 'data_sources/assets_file_data_source_impl.dart';
-export 'data_sources/network_data_source_impl.dart';
-export 'repositories/i18n_repository_impl.dart';
+export '../domain/entities/i18n.dart';
+export 'data_sources/assets_i18n_data_source_impl.dart';
+export 'data_sources/network_i18n_data_source_impl.dart';
diff --git a/packages/wyatt_i18n/lib/src/data/data_sources/assets_file_data_source_impl.dart b/packages/wyatt_i18n/lib/src/data/data_sources/assets_i18n_data_source_impl.dart
similarity index 57%
rename from packages/wyatt_i18n/lib/src/data/data_sources/assets_file_data_source_impl.dart
rename to packages/wyatt_i18n/lib/src/data/data_sources/assets_i18n_data_source_impl.dart
index 5cfecc4e..008d8dc9 100644
--- a/packages/wyatt_i18n/lib/src/data/data_sources/assets_file_data_source_impl.dart
+++ b/packages/wyatt_i18n/lib/src/data/data_sources/assets_i18n_data_source_impl.dart
@@ -15,31 +15,61 @@
// along with this program. If not, see .
import 'package:flutter/services.dart' show AssetBundle, rootBundle;
-import 'package:wyatt_i18n/src/core/enums/format.dart';
-import 'package:wyatt_i18n/src/core/exceptions/exceptions.dart';
+import 'package:flutter/widgets.dart';
import 'package:wyatt_i18n/src/core/utils/assets_utils.dart';
-import 'package:wyatt_i18n/src/domain/data_sources/i18n_data_source.dart';
+import 'package:wyatt_i18n/src/core/utils/intl_utils.dart';
+import 'package:wyatt_i18n/src/core/utils/parsers/locale_parser.dart';
+import 'package:wyatt_i18n/wyatt_i18n.dart';
-/// {@template assets_file_data_source_impl}
+/// {@template assets_i18n_data_source_impl}
/// Implementation of [I18nDataSource] that loads i18n files from the assets.
///
/// The [basePath] is the folder where the i18n files are located.
/// The [baseName] is the name of the i18n files without the extension.
/// The [format] is the format of the i18n files.
+/// The [defaultLocale] is the default locale to use when the locale is not
+/// specified.
+/// The [separator] is the separator to use when the locale is specified.
///
/// For example, if the i18n files are located in the `assets/l10n/` and are
/// named `i18n.en.arb`, `i18n.fr.arb`, etc., then the [basePath]
/// is `l10n` and the [baseName] is `i18n` and the [format] is [Format.arb].
/// {@endtemplate}
-class AssetsFileDataSourceImpl extends I18nDataSource {
- /// {@macro assets_file_data_source_impl}
- AssetsFileDataSourceImpl({
+class AssetsI18nDataSourceImpl extends I18nDataSource {
+ /// {@macro assets_i18n_data_source_impl}
+ AssetsI18nDataSourceImpl({
this.basePath = '',
this.baseName = 'i18n',
super.format = Format.arb,
- super.defaultLocale = 'en',
+ super.defaultLocale = const Locale('en'),
+ super.separator = '.',
+ super.localeTransformer,
}) : super();
+ /// Creates a new instance of [AssetsI18nDataSourceImpl] with the system
+ /// locale as the default locale.
+ /// So, the [defaultLocale] is the system locale and you don't need to
+ /// specify it when you load the i18n file.
+ // ignore: long-parameter-list
+ static Future withSystemLocale({
+ String basePath = '',
+ String baseName = 'i18n',
+ Format format = Format.arb,
+ String separator = '.',
+ LocaleTransformer? localeTransformer,
+ }) async {
+ final locale = await IntlUtils.getSystemLocale();
+
+ return AssetsI18nDataSourceImpl(
+ basePath: basePath,
+ baseName: baseName,
+ format: format,
+ defaultLocale: const LocaleParser().parse(locale),
+ separator: separator,
+ localeTransformer: localeTransformer,
+ );
+ }
+
/// The folder where the i18n files are located.
///
/// Note: The path is relative to the `assets` folder. So, `assets/l10n/`
@@ -53,15 +83,20 @@ class AssetsFileDataSourceImpl extends I18nDataSource {
final AssetBundle assetBundle = rootBundle;
/// Tries to load the i18n file from the given [locale].
- Future _tryLoad(String? locale) async {
+ Future _tryLoad(Locale? locale) async {
String? content;
final ext = format.name;
- final path = AssetsUtils.cleanPath(
- locale == null
- ? '$basePath/$baseName.$defaultLocale.$ext'
- : '$basePath/$baseName.$locale.$ext',
- );
+ final defaultLocaleString = localeTransformer?.call(defaultLocale) ??
+ const LocaleParser().serialize(defaultLocale);
+
+ final localeString = locale == null
+ ? defaultLocaleString
+ : localeTransformer?.call(locale) ??
+ const LocaleParser().serialize(locale);
+
+ final path =
+ AssetsUtils.cleanPath('$basePath/$baseName.$localeString.$ext');
try {
if (await AssetsUtils.assetExists(path)) {
@@ -76,7 +111,7 @@ class AssetsFileDataSourceImpl extends I18nDataSource {
if (content == null && locale != null) {
try {
final fallbackPath = AssetsUtils.cleanPath(
- '$basePath/$baseName.$defaultLocale.$ext',
+ '$basePath/$baseName.$defaultLocaleString.$ext',
);
content = await assetBundle.loadString(fallbackPath);
} catch (_) {
@@ -90,12 +125,19 @@ class AssetsFileDataSourceImpl extends I18nDataSource {
throw SourceNotFoundException(path, format: format);
}
- return content;
+ /// Parse the i18n file.
+ final parsedData = format.parser.parse(content);
+
+ return I18n(
+ unparsedData: content,
+ data: parsedData,
+ locale: locale,
+ );
}
/// Tries to load the i18n file from a given [uri].
/// This method is used when the [uri] is not null.
- Future _tryLoadUri(Uri uri) async {
+ Future _tryLoadUri(Uri uri) async {
String? content;
try {
content = await assetBundle.loadString(uri.toString());
@@ -103,7 +145,13 @@ class AssetsFileDataSourceImpl extends I18nDataSource {
throw SourceNotFoundException(uri.toString(), format: format);
}
- return content;
+ /// Parse the i18n file.
+ final parsedData = format.parser.parse(content);
+
+ return I18n(
+ unparsedData: content,
+ data: parsedData,
+ );
}
/// Loads the i18n file from Assets folder.
@@ -111,7 +159,8 @@ class AssetsFileDataSourceImpl extends I18nDataSource {
/// The i18n file must be in [basePath], named [baseName] +
/// `..` and must be specified in the `pubspec.yaml` file.
@override
- Future load({required String? locale}) async => _tryLoad(locale);
+ Future load({Locale? locale}) async =>
+ super.currentI18nFile = await _tryLoad(locale);
/// Loads the i18n file from Assets folder.
///
@@ -119,5 +168,6 @@ class AssetsFileDataSourceImpl extends I18nDataSource {
/// from the given [uri]. In this case, the [basePath] and the
/// [baseName] are ignored. And there is no fallback.
@override
- Future loadFrom(Uri uri) async => _tryLoadUri(uri);
+ Future loadFrom(Uri uri) async =>
+ super.currentI18nFile = await _tryLoadUri(uri);
}
diff --git a/packages/wyatt_i18n/lib/src/data/data_sources/network_data_source_impl.dart b/packages/wyatt_i18n/lib/src/data/data_sources/network_i18n_data_source_impl.dart
similarity index 65%
rename from packages/wyatt_i18n/lib/src/data/data_sources/network_data_source_impl.dart
rename to packages/wyatt_i18n/lib/src/data/data_sources/network_i18n_data_source_impl.dart
index 559ecfad..64bd3de4 100644
--- a/packages/wyatt_i18n/lib/src/data/data_sources/network_data_source_impl.dart
+++ b/packages/wyatt_i18n/lib/src/data/data_sources/network_i18n_data_source_impl.dart
@@ -15,19 +15,26 @@
// along with this program. If not, see .
import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
import 'package:http/http.dart' as http;
import 'package:wyatt_i18n/src/core/enums/format.dart';
import 'package:wyatt_i18n/src/core/exceptions/exceptions.dart';
import 'package:wyatt_i18n/src/core/utils/assets_utils.dart';
+import 'package:wyatt_i18n/src/core/utils/intl_utils.dart';
+import 'package:wyatt_i18n/src/core/utils/parsers/locale_parser.dart';
import 'package:wyatt_i18n/src/domain/data_sources/i18n_data_source.dart';
+import 'package:wyatt_i18n/src/domain/entities/i18n.dart';
-/// {@template network_data_source_impl}
+/// {@template network_i18n_data_source_impl}
/// Implementation of [I18nDataSource] that loads i18n files from the network.
///
/// The [baseUri] is the base uri where the i18n files are located.
/// The [baseName] is the name of the i18n files without the extension.
/// The [fallbackAssetPath] is the path to the fallback i18n files.
/// The [format] is the format of the i18n files.
+/// The [defaultLocale] is the default locale to use when the locale is not
+/// specified.
+/// The [separator] is the separator to use when the locale is specified.
///
/// For example, if the i18n files are located at `https://example.com/i18n/`
/// and are named `i18n.en.arb`, `i18n.fr.arb`, etc., then the [baseUri] is
@@ -42,16 +49,42 @@ import 'package:wyatt_i18n/src/domain/data_sources/i18n_data_source.dart';
/// and are named `i18n.arb`, `i18n.en.arb`, `i18n.fr.arb`, etc., then the
/// [fallbackAssetPath] is `l10n`.
/// {@endtemplate}
-class NetworkDataSourceImpl extends I18nDataSource {
- /// {@macro network_data_source_impl}
- const NetworkDataSourceImpl({
+class NetworkI18nDataSourceImpl extends I18nDataSource {
+ /// {@macro network_i18n_data_source_impl}
+ NetworkI18nDataSourceImpl({
required this.baseUri,
this.baseName = 'i18n',
this.fallbackAssetPath = '',
super.format = Format.arb,
- super.defaultLocale = 'en',
+ super.defaultLocale = const Locale('en'),
+ super.separator = '.',
+ super.localeTransformer,
}) : super();
+ /// Creates a new instance of [NetworkI18nDataSourceImpl] with the system
+ /// locale as the default locale.
+ /// So, the [defaultLocale] is the system locale and you don't need to
+ /// specify it when you load the i18n file.
+ // ignore: long-parameter-list
+ static Future withSystemLocale({
+ required Uri baseUri,
+ String baseName = 'i18n',
+ Format format = Format.arb,
+ String separator = '.',
+ LocaleTransformer? localeTransformer,
+ }) async {
+ final locale = await IntlUtils.getSystemLocale();
+
+ return NetworkI18nDataSourceImpl(
+ baseUri: baseUri,
+ baseName: baseName,
+ format: format,
+ defaultLocale: const LocaleParser().parse(locale),
+ separator: separator,
+ localeTransformer: localeTransformer,
+ );
+ }
+
/// The base uri where the i18n files are located.
final Uri baseUri;
@@ -68,18 +101,26 @@ class NetworkDataSourceImpl extends I18nDataSource {
final String fallbackAssetPath;
/// Tries to load the i18n file from the given [locale].
- Future _tryLoad(String? locale, {Uri? overrideUri}) async {
+ Future _tryLoad(Locale? locale, {Uri? overrideUri}) async {
String? content;
final ext = format.name;
+ final defaultLocaleString = localeTransformer?.call(defaultLocale) ??
+ const LocaleParser().serialize(defaultLocale);
+
+ final localeString = locale == null
+ ? defaultLocaleString
+ : localeTransformer?.call(locale) ??
+ const LocaleParser().serialize(locale);
+
/// If the locale is null, then we try to load the default i18n file from
/// the fallback asset path.
/// Otherwise, we try to load the i18n file for the given locale from the
/// base uri.
final path = AssetsUtils.cleanPath(
locale == null
- ? '$fallbackAssetPath/$baseName.$defaultLocale.$ext'
- : overrideUri?.toString() ?? '$baseUri/$baseName.$locale.$ext',
+ ? '$fallbackAssetPath/$baseName.$defaultLocaleString.$ext'
+ : overrideUri?.toString() ?? '$baseUri/$baseName.$localeString.$ext',
);
if (locale == null) {
@@ -108,7 +149,7 @@ class NetworkDataSourceImpl extends I18nDataSource {
if (content == null && locale != null) {
try {
final fallbackPath = AssetsUtils.cleanPath(
- '$fallbackAssetPath/$baseName.$defaultLocale.$ext',
+ '$fallbackAssetPath/$baseName.$defaultLocaleString.$ext',
);
content = await rootBundle.loadString(fallbackPath);
} catch (_) {
@@ -122,7 +163,14 @@ class NetworkDataSourceImpl extends I18nDataSource {
throw SourceNotFoundException(path, format: format);
}
- return content;
+ /// Parse the i18n file.
+ final parsedData = format.parser.parse(content);
+
+ return I18n(
+ unparsedData: content,
+ data: parsedData,
+ locale: locale,
+ );
}
/// Loads the i18n file for the given [locale].
@@ -130,12 +178,14 @@ class NetworkDataSourceImpl extends I18nDataSource {
/// If the [locale] is null, then the default i18n file is loaded from the
/// fallback asset path.
@override
- Future load({required String? locale}) async => _tryLoad(locale);
+ Future load({Locale? locale}) async =>
+ super.currentI18nFile = await _tryLoad(locale);
/// Loads the i18n file from the given [uri].
///
/// If the fetch fails, then the fallback i18n files are loaded from the
/// [fallbackAssetPath] in the root bundle.
@override
- Future loadFrom(Uri uri) async => _tryLoad(null, overrideUri: uri);
+ Future loadFrom(Uri uri) async =>
+ super.currentI18nFile = await _tryLoad(null, overrideUri: uri);
}
diff --git a/packages/wyatt_i18n/lib/src/data/repositories/i18n_repository_impl.dart b/packages/wyatt_i18n/lib/src/data/repositories/i18n_repository_impl.dart
deleted file mode 100644
index d3d9b9d2..00000000
--- a/packages/wyatt_i18n/lib/src/data/repositories/i18n_repository_impl.dart
+++ /dev/null
@@ -1,157 +0,0 @@
-// Copyright (C) 2023 WYATT GROUP
-// Please see the AUTHORS file for details.
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-import 'package:wyatt_architecture/wyatt_architecture.dart';
-import 'package:wyatt_i18n/wyatt_i18n.dart';
-import 'package:wyatt_type_utils/wyatt_type_utils.dart' hide Option;
-
-/// {@template i18n_repository_impl}
-/// The default implementation of [I18nRepository].
-/// {@endtemplate}
-class I18nRepositoryImpl extends I18nRepository {
- /// {@macro i18n_repository_impl}
- I18nRepositoryImpl({
- required this.dataSource,
- }) : super();
-
- /// The data source used to load the i18n file.
- final I18nDataSource dataSource;
-
- /// The current i18n instance.
- I18nFile _i18n = const I18nFile.empty();
-
- @override
- I18nFile get i18n => _i18n;
-
- Future _parse(
- String content,
- Parser> parser, {
- String? locale,
- bool strict = false,
- }) async {
- final parsed = parser.parse(content);
-
- String parsedLocale;
-
- /// Checks if the locale is present in the parsed data.
- /// If not, throws an exception.
- /// If yes, sets the locale to the parsed locale.
- if (parsed.containsKey('@@locale')) {
- if (strict) {
- /// Checks if the parsed locale is a string.
- if (parsed['@@locale'] is! String) {
- throw NoLocaleException();
- }
-
- /// Checks if the parsed locale is the same as the given locale.
- if (locale != null && parsed['@@locale'] as String != locale) {
- throw InvalidLocaleException(locale, parsed['@@locale'] as String);
- }
- }
- parsedLocale = parsed['@@locale'] as String;
- } else {
- if (strict) {
- /// Throws an exception if the locale is not present in the parsed data.
- throw NoLocaleException();
- } else {
- /// Sets the locale to the given locale.
- /// If the given locale is null, sets the locale to 'null'.
- /// This is done to prevent the locale from being null.
- /// It should never be null.
- parsedLocale = locale ?? 'null';
- }
- }
-
- return I18nFile(
- locale: parsedLocale,
- unparsedData: content,
- data: parsed,
- );
- }
-
- @override
- FutureOrResult load({
- required String? locale,
- bool strict = false,
- Parser>? parser,
- }) async =>
- await Result.tryCatchAsync(
- () async {
- final content = await dataSource.load(locale: locale);
-
- return _i18n = await _parse(
- content,
- parser ?? dataSource.format.parser,
- locale: locale,
- strict: strict,
- );
- },
- (error) => error,
- );
-
- @override
- FutureOrResult loadFrom(
- Uri uri, {
- Parser>? parser,
- }) async =>
- await Result.tryCatchAsync(
- () async {
- final content = await dataSource.loadFrom(uri);
-
- /// Strict is always true when loading from a uri. Because
- /// the locale is not given and can't be inferred.
- return _i18n = await _parse(
- content,
- parser ?? dataSource.format.parser,
- strict: true,
- );
- },
- (error) => error,
- );
-
- @override
- Result get(
- String key, [
- Map arguments = const {},
- ]) {
- final I18nFileParser parser =
- I18nFileParser(i18n: _i18n, arguments: arguments);
-
- if (_i18n.containsKey(key)) {
- return Result.tryCatch(
- () => parser.parse(_i18n[key] as String, key: key),
- (error) => error,
- );
- } else {
- throw KeyNotFoundException(key, arguments);
- }
- }
-
- @override
- Result getI18n() =>
- Result.conditional(!_i18n.isEmpty, _i18n, NotLoadedException());
-
- @override
- Result getLocale() =>
- Result.conditional(!_i18n.isEmpty, _i18n.locale, NotLoadedException());
-
- @override
- Result setI18n(I18nFile i18n) {
- _i18n = i18n;
-
- return const Ok(null);
- }
-}
diff --git a/packages/wyatt_i18n/lib/src/domain/data_sources/i18n_data_source.dart b/packages/wyatt_i18n/lib/src/domain/data_sources/i18n_data_source.dart
index 942e35b9..95531071 100644
--- a/packages/wyatt_i18n/lib/src/domain/data_sources/i18n_data_source.dart
+++ b/packages/wyatt_i18n/lib/src/domain/data_sources/i18n_data_source.dart
@@ -14,8 +14,11 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
+import 'package:flutter/widgets.dart';
import 'package:wyatt_architecture/wyatt_architecture.dart';
-import 'package:wyatt_i18n/src/core/enums/format.dart';
+import 'package:wyatt_i18n/wyatt_i18n.dart';
+
+typedef LocaleTransformer = String Function(Locale);
/// {@template i18n_data_source}
/// Base class for i18n data sources.
@@ -26,24 +29,62 @@ import 'package:wyatt_i18n/src/core/enums/format.dart';
/// {@endtemplate}
abstract class I18nDataSource extends BaseDataSource {
/// {@macro i18n_data_source}
- const I18nDataSource({
+ I18nDataSource({
this.format = Format.arb,
- this.defaultLocale = 'en',
+ this.defaultLocale = const Locale('en'),
+ this.separator = '.',
+ this.localeTransformer,
});
/// The format of the i18n file.
final Format format;
/// The default locale.
- final String defaultLocale;
+ /// This is used when the locale is `null` in the [load] method.
+ Locale defaultLocale;
+
+ /// The separator used in file name.
+ /// This is used to separate the baseName and the locale.
+ ///
+ /// For example, if the baseName is `i18n` and the locale is `en`, if the
+ /// separator is `_`, the path will be `i18n_en`.
+ final String separator;
+
+ /// The list of parsers used to parse the i18n file.
+ /// This is used to parse the i18n file into a [Map].
+ // final List>> decoders;
+
+ /// The current i18n file loaded from the source.
+ /// This is used to avoid loading the same file twice.
+ I18n? currentI18nFile;
+
+ /// Function used to transform the [Locale] into a string.
+ /// This is used to get the file name from the [Locale].
+ ///
+ /// For example, if the [Locale] is `en_US`, the file name will be `en_us`.
+ ///
+ /// But maybe you want to use `en-us` instead. In this case, you can use
+ /// ```dart
+ /// localeTransformer: (locale) => locale.toString().replaceAll('_', '-'),
+ /// ```
+ /// to replace the `_` with `-`.
+ ///
+ /// If you want to use `en` instead of `en_US`, you can use
+ /// ```dart
+ /// localeTransformer: (locale) => locale.languageCode,
+ /// ```
+ ///
+ /// If this is `null`, the default transformer will be used.
+ /// The default transformer will use the LocaleParser serializer method.
+ LocaleTransformer? localeTransformer;
/// Loads the i18n file from the source.
/// If [locale] is not `null`, it will load the file with the given [locale].
/// Otherwise, it will load the file from the default location.
- Future load({required String? locale});
+ Future load({Locale? locale});
/// Loads the i18n file from the source.
///
/// This method is used to load the i18n file from the given [Uri].
- Future loadFrom(Uri uri);
+ Future loadFrom(Uri uri);
}
diff --git a/packages/wyatt_i18n/lib/src/domain/domain.dart b/packages/wyatt_i18n/lib/src/domain/domain.dart
index d6f3a7f2..7a92cdf3 100644
--- a/packages/wyatt_i18n/lib/src/domain/domain.dart
+++ b/packages/wyatt_i18n/lib/src/domain/domain.dart
@@ -16,5 +16,3 @@
export 'data_sources/i18n_data_source.dart';
export 'entities/i18n.dart';
-export 'entities/i18n_file.dart';
-export 'repositories/i18n_repository.dart';
diff --git a/packages/wyatt_i18n/lib/src/domain/entities/i18n.dart b/packages/wyatt_i18n/lib/src/domain/entities/i18n.dart
index 42280fc7..dde2bcd7 100644
--- a/packages/wyatt_i18n/lib/src/domain/entities/i18n.dart
+++ b/packages/wyatt_i18n/lib/src/domain/entities/i18n.dart
@@ -14,47 +14,104 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
+import 'package:equatable/equatable.dart';
+import 'package:flutter/widgets.dart';
import 'package:wyatt_architecture/wyatt_architecture.dart';
-import 'package:wyatt_i18n/wyatt_i18n.dart';
+import 'package:wyatt_i18n/src/core/exceptions/exceptions.dart';
+import 'package:wyatt_i18n/src/core/utils/parsers/i18n_file_parser.dart';
+import 'package:wyatt_i18n/src/core/utils/parsers/locale_parser.dart';
-/// {@template i18n}
-/// This class is used to store the translations of the application.
-/// This entity is used by the I18nDelegate and Flutter's Localizations
-/// widget to provide the translations to the application.
+/// {@template i18n_file}
+/// Data structure for i18n files.
/// {@endtemplate}
-class I18n extends Entity {
- /// {@macro i18n}
- I18n({
- required this.i18nRepository,
+class I18n extends Entity with EquatableMixin {
+ /// {@macro i18n_file}
+ const I18n({
+ required this.unparsedData,
+ required this.data,
+ this.locale,
}) : super();
- final I18nRepository i18nRepository;
+ /// Creates an empty i18n file.
+ const I18n.empty() : this(unparsedData: '', data: const {});
- /// Get the translation of the given [key].
- /// If the [key] is not found, the [key] itself is returned.
- /// If the [key] is found, the translation is returned.
- /// If [args] is not null, the translation is formatted with the
- /// given arguments.
- String get(String key, [Map? args]) {
- final result = i18nRepository.get(key, args ?? const {});
+ /// The locale of the i18n file.
+ final Locale? locale;
- return result.fold(
- (value) => value,
- (error) => key,
- );
+ /// The unparsed data of the i18n file.
+ final String unparsedData;
+
+ /// The data of the i18n file.
+ final Map data;
+
+ /// Gets the locale of the i18n file.
+ /// If [strict] is true, it will throw an exception if the locale is not
+ /// present in the i18n file.
+ /// If [strict] is false, it will return the locale of the i18n file if it is
+ /// present, otherwise it will return the locale passed to the constructor.
+ Locale getLocale({bool strict = false}) {
+ if (strict) {
+ if (data.containsKey('@@locale')) {
+ if (data['@@locale'] is! String) {
+ throw NoLocaleException();
+ }
+
+ return const LocaleParser().parse(data['@@locale'] as String);
+ } else {
+ throw NoLocaleException();
+ }
+ } else {
+ if (data.containsKey('@@locale')) {
+ if (data['@@locale'] is! String) {
+ if (locale != null) {
+ return locale!;
+ } else {
+ throw NoLocaleException();
+ }
+ }
+
+ return const LocaleParser().parse(data['@@locale'] as String);
+ } else {
+ if (locale != null) {
+ return locale!;
+ } else {
+ throw NoLocaleException();
+ }
+ }
+ }
}
- /// Get the translation of the given [key].
- ///
- /// Note: arguments are not supported.
- String operator [](String key) => get(key);
+ /// Gets the value of a key.
+ /// If the key is not present in the i18n file, it will throw a
+ /// [KeyNotFoundException].
+ /// If the key is present in the i18n file, it will return the value of the
+ /// key parsed with ICU message format.
+ String get(String key, [Map? args]) {
+ final arguments = args ?? const {};
+
+ /// The parser used to parse the i18n file with ICU message format.
+ final I18nFileParser parser =
+ I18nFileParser(i18n: this, arguments: arguments);
+
+ if (containsKey(key)) {
+ return parser.parse(this[key] as String, key: key);
+ } else {
+ throw KeyNotFoundException(key, arguments);
+ }
+ }
/// Get the translation of the given [key].
String call(String key, [Map? args]) => get(key, args);
- /// Load the translations from the given [locale].
- /// If the [locale] is not found, the default locale is loaded.
- Future load(String locale) async {
- await i18nRepository.load(locale: locale);
- }
+ /// Checks if the i18n file contains a key.
+ bool containsKey(String key) => data.containsKey(key);
+
+ /// Gets the value of a key.
+ dynamic operator [](String key) => data[key];
+
+ /// Checks if the i18n file is empty.
+ bool get isEmpty => data.isEmpty && unparsedData.isEmpty && locale == null;
+
+ @override
+ List