master #81
@ -23,28 +23,131 @@
|
|||||||
<img src="https://img.shields.io/badge/SDK-Flutter-blue?style=flat-square" alt="SDK: Flutter" />
|
<img src="https://img.shields.io/badge/SDK-Flutter-blue?style=flat-square" alt="SDK: Flutter" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
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
|
## 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
|
* Usage
|
||||||
start using the package.
|
+ [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
|
## Usage
|
||||||
|
|
||||||
TODO: Include short and useful examples for package users. Add longer examples
|
You can use this package as a LocalizationDelegate or as a DataSource.
|
||||||
to `/example` folder.
|
|
||||||
|
### 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
|
```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
|
```dart
|
||||||
contribute to the package, how to file issues, what response they can expect
|
Text(context.i18n('youHavePushed', {'count': 42})),
|
||||||
from the package authors, and more.
|
// => '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>(
|
||||||
|
() => i18nDataSource,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a Repository:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class I18nRepository {
|
||||||
|
I18nRepository({
|
||||||
|
required this.dataSource,
|
||||||
|
});
|
||||||
|
|
||||||
|
final I18nDataSource dataSource;
|
||||||
|
|
||||||
|
Future<I18n> load() async {
|
||||||
|
final i18n = await dataSource.load();
|
||||||
|
return i18n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And use it in your cubit:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class MyCubit extends Cubit<MyState> {
|
||||||
|
MyCubit({
|
||||||
|
required this.i18nRepository,
|
||||||
|
}) : super(MyState());
|
||||||
|
|
||||||
|
final I18nRepository i18nRepository;
|
||||||
|
|
||||||
|
Future<void> 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.
|
||||||
|
@ -29,23 +29,20 @@ class App extends StatelessWidget {
|
|||||||
static const String title = 'Wyatt i18n Example';
|
static const String title = 'Wyatt i18n Example';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => MaterialApp(
|
||||||
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,
|
title: title,
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
primarySwatch: Colors.blue,
|
primarySwatch: Colors.blue,
|
||||||
),
|
),
|
||||||
localizationsDelegates: [
|
localizationsDelegates: [
|
||||||
I18nDelegate(repository: repository),
|
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,
|
GlobalMaterialLocalizations.delegate,
|
||||||
GlobalWidgetsLocalizations.delegate
|
GlobalWidgetsLocalizations.delegate
|
||||||
],
|
],
|
||||||
@ -60,5 +57,4 @@ class App extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -14,11 +14,10 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
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/parser.dart';
|
||||||
|
import 'package:wyatt_i18n/src/core/utils/parsers/arb_parser.dart';
|
||||||
import 'package:wyatt_i18n/src/core/utils/yaml_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.
|
/// Enum for i18n file formats and extensions.
|
||||||
///
|
///
|
||||||
|
24
packages/wyatt_i18n/lib/src/core/utils/intl_utils.dart
Normal file
24
packages/wyatt_i18n/lib/src/core/utils/intl_utils.dart
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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<String> getSystemLocale() => findSystemLocale();
|
||||||
|
}
|
@ -14,8 +14,8 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
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/parser.dart';
|
||||||
|
import 'package:wyatt_i18n/src/core/utils/parsers/json_parser.dart';
|
||||||
|
|
||||||
/// {@template arb_parser}
|
/// {@template arb_parser}
|
||||||
/// A class that parses a given input of type [String] into a given output
|
/// A class that parses a given input of type [String] into a given output
|
@ -20,8 +20,8 @@ import 'package:wyatt_i18n/src/domain/entities/tokens.dart';
|
|||||||
import 'package:wyatt_i18n/wyatt_i18n.dart';
|
import 'package:wyatt_i18n/wyatt_i18n.dart';
|
||||||
|
|
||||||
/// {@template i18n_file_parser}
|
/// {@template i18n_file_parser}
|
||||||
/// This class is responsible for parsing the [I18nFile] and returning the
|
/// This class is responsible for parsing the [I18n] and returning the
|
||||||
/// translated string.
|
/// translated string using the [arguments] provided and the [IcuParser].
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
class I18nFileParser extends Parser<String, String> {
|
class I18nFileParser extends Parser<String, String> {
|
||||||
/// {@macro i18n_file_parser}
|
/// {@macro i18n_file_parser}
|
||||||
@ -30,8 +30,8 @@ class I18nFileParser extends Parser<String, String> {
|
|||||||
this.arguments = const {},
|
this.arguments = const {},
|
||||||
}) : super();
|
}) : super();
|
||||||
|
|
||||||
/// The [I18nFile] to be parsed.
|
/// The [I18n] to be parsed.
|
||||||
final I18nFile i18n;
|
final I18n i18n;
|
||||||
|
|
||||||
/// The arguments to be used in the translation.
|
/// The arguments to be used in the translation.
|
||||||
final Map<String, dynamic> arguments;
|
final Map<String, dynamic> arguments;
|
@ -17,6 +17,7 @@
|
|||||||
// ignore_for_file: avoid_dynamic_calls, inference_failure_on_untyped_parameter
|
// ignore_for_file: avoid_dynamic_calls, inference_failure_on_untyped_parameter
|
||||||
|
|
||||||
import 'package:petitparser/petitparser.dart' hide Token;
|
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';
|
import 'package:wyatt_i18n/src/domain/entities/tokens.dart';
|
||||||
|
|
||||||
/// {@template icu_parser}
|
/// {@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/
|
/// See https://unicode-org.github.io/icu/userguide/format_parse/messages/
|
||||||
/// for the syntax.
|
/// for the syntax.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
class IcuParser {
|
class IcuParser extends wyatt.Parser<String, List<Token>?> {
|
||||||
/// {@macro icu_parser}
|
/// {@macro icu_parser}
|
||||||
IcuParser() {
|
IcuParser() {
|
||||||
// There is a cycle here, so we need the explicit
|
// There is a cycle here, so we need the explicit
|
||||||
@ -167,12 +168,13 @@ class IcuParser {
|
|||||||
Parser get parameter => (openCurly & id & closeCurly)
|
Parser get parameter => (openCurly & id & closeCurly)
|
||||||
.map((result) => Argument(result[1] as String));
|
.map((result) => Argument(result[1] as String));
|
||||||
|
|
||||||
List<Token>? parse(String message) {
|
@override
|
||||||
|
List<Token>? parse(String input) {
|
||||||
final parsed = (compound | pluralOrGenderOrSelect | simpleText | empty)
|
final parsed = (compound | pluralOrGenderOrSelect | simpleText | empty)
|
||||||
.map(
|
.map(
|
||||||
(result) => List<Token>.from(result is List ? result : [result]),
|
(result) => List<Token>.from(result is List ? result : [result]),
|
||||||
)
|
)
|
||||||
.parse(message);
|
.parse(input);
|
||||||
|
|
||||||
return parsed.isSuccess ? parsed.value : null;
|
return parsed.isSuccess ? parsed.value : null;
|
||||||
}
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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<String, Locale> {
|
||||||
|
/// {@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}';
|
||||||
|
}
|
@ -14,9 +14,9 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
export 'arb_parser.dart';
|
|
||||||
export 'i18n_file_parser.dart';
|
|
||||||
export 'icu_parser.dart';
|
|
||||||
export 'json_parser.dart';
|
|
||||||
export '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';
|
||||||
|
@ -14,6 +14,6 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
export 'data_sources/assets_file_data_source_impl.dart';
|
export '../domain/entities/i18n.dart';
|
||||||
export 'data_sources/network_data_source_impl.dart';
|
export 'data_sources/assets_i18n_data_source_impl.dart';
|
||||||
export 'repositories/i18n_repository_impl.dart';
|
export 'data_sources/network_i18n_data_source_impl.dart';
|
||||||
|
@ -15,31 +15,61 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import 'package:flutter/services.dart' show AssetBundle, rootBundle;
|
import 'package:flutter/services.dart' show AssetBundle, rootBundle;
|
||||||
import 'package:wyatt_i18n/src/core/enums/format.dart';
|
import 'package:flutter/widgets.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/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.
|
/// Implementation of [I18nDataSource] that loads i18n files from the assets.
|
||||||
///
|
///
|
||||||
/// The [basePath] is the folder where the i18n files are located.
|
/// The [basePath] is the folder where the i18n files are located.
|
||||||
/// The [baseName] is the name of the i18n files without the extension.
|
/// The [baseName] is the name of the i18n files without the extension.
|
||||||
/// The [format] is the format of the 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 in the `assets/l10n/` and are
|
/// 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]
|
/// named `i18n.en.arb`, `i18n.fr.arb`, etc., then the [basePath]
|
||||||
/// is `l10n` and the [baseName] is `i18n` and the [format] is [Format.arb].
|
/// is `l10n` and the [baseName] is `i18n` and the [format] is [Format.arb].
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
class AssetsFileDataSourceImpl extends I18nDataSource {
|
class AssetsI18nDataSourceImpl extends I18nDataSource {
|
||||||
/// {@macro assets_file_data_source_impl}
|
/// {@macro assets_i18n_data_source_impl}
|
||||||
AssetsFileDataSourceImpl({
|
AssetsI18nDataSourceImpl({
|
||||||
this.basePath = '',
|
this.basePath = '',
|
||||||
this.baseName = 'i18n',
|
this.baseName = 'i18n',
|
||||||
super.format = Format.arb,
|
super.format = Format.arb,
|
||||||
super.defaultLocale = 'en',
|
super.defaultLocale = const Locale('en'),
|
||||||
|
super.separator = '.',
|
||||||
|
super.localeTransformer,
|
||||||
}) : super();
|
}) : 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<AssetsI18nDataSourceImpl> 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.
|
/// The folder where the i18n files are located.
|
||||||
///
|
///
|
||||||
/// Note: The path is relative to the `assets` folder. So, `assets/l10n/`
|
/// Note: The path is relative to the `assets` folder. So, `assets/l10n/`
|
||||||
@ -53,15 +83,20 @@ class AssetsFileDataSourceImpl extends I18nDataSource {
|
|||||||
final AssetBundle assetBundle = rootBundle;
|
final AssetBundle assetBundle = rootBundle;
|
||||||
|
|
||||||
/// Tries to load the i18n file from the given [locale].
|
/// Tries to load the i18n file from the given [locale].
|
||||||
Future<String> _tryLoad(String? locale) async {
|
Future<I18n> _tryLoad(Locale? locale) async {
|
||||||
String? content;
|
String? content;
|
||||||
final ext = format.name;
|
final ext = format.name;
|
||||||
|
|
||||||
final path = AssetsUtils.cleanPath(
|
final defaultLocaleString = localeTransformer?.call(defaultLocale) ??
|
||||||
locale == null
|
const LocaleParser().serialize(defaultLocale);
|
||||||
? '$basePath/$baseName.$defaultLocale.$ext'
|
|
||||||
: '$basePath/$baseName.$locale.$ext',
|
final localeString = locale == null
|
||||||
);
|
? defaultLocaleString
|
||||||
|
: localeTransformer?.call(locale) ??
|
||||||
|
const LocaleParser().serialize(locale);
|
||||||
|
|
||||||
|
final path =
|
||||||
|
AssetsUtils.cleanPath('$basePath/$baseName.$localeString.$ext');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (await AssetsUtils.assetExists(path)) {
|
if (await AssetsUtils.assetExists(path)) {
|
||||||
@ -76,7 +111,7 @@ class AssetsFileDataSourceImpl extends I18nDataSource {
|
|||||||
if (content == null && locale != null) {
|
if (content == null && locale != null) {
|
||||||
try {
|
try {
|
||||||
final fallbackPath = AssetsUtils.cleanPath(
|
final fallbackPath = AssetsUtils.cleanPath(
|
||||||
'$basePath/$baseName.$defaultLocale.$ext',
|
'$basePath/$baseName.$defaultLocaleString.$ext',
|
||||||
);
|
);
|
||||||
content = await assetBundle.loadString(fallbackPath);
|
content = await assetBundle.loadString(fallbackPath);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@ -90,12 +125,19 @@ class AssetsFileDataSourceImpl extends I18nDataSource {
|
|||||||
throw SourceNotFoundException(path, format: format);
|
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].
|
/// Tries to load the i18n file from a given [uri].
|
||||||
/// This method is used when the [uri] is not null.
|
/// This method is used when the [uri] is not null.
|
||||||
Future<String> _tryLoadUri(Uri uri) async {
|
Future<I18n> _tryLoadUri(Uri uri) async {
|
||||||
String? content;
|
String? content;
|
||||||
try {
|
try {
|
||||||
content = await assetBundle.loadString(uri.toString());
|
content = await assetBundle.loadString(uri.toString());
|
||||||
@ -103,7 +145,13 @@ class AssetsFileDataSourceImpl extends I18nDataSource {
|
|||||||
throw SourceNotFoundException(uri.toString(), format: format);
|
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.
|
/// Loads the i18n file from Assets folder.
|
||||||
@ -111,7 +159,8 @@ class AssetsFileDataSourceImpl extends I18nDataSource {
|
|||||||
/// The i18n file must be in [basePath], named [baseName] +
|
/// The i18n file must be in [basePath], named [baseName] +
|
||||||
/// `.<locale>.<extension>` and must be specified in the `pubspec.yaml` file.
|
/// `.<locale>.<extension>` and must be specified in the `pubspec.yaml` file.
|
||||||
@override
|
@override
|
||||||
Future<String> load({required String? locale}) async => _tryLoad(locale);
|
Future<I18n> load({Locale? locale}) async =>
|
||||||
|
super.currentI18nFile = await _tryLoad(locale);
|
||||||
|
|
||||||
/// Loads the i18n file from Assets folder.
|
/// 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
|
/// from the given [uri]. In this case, the [basePath] and the
|
||||||
/// [baseName] are ignored. And there is no fallback.
|
/// [baseName] are ignored. And there is no fallback.
|
||||||
@override
|
@override
|
||||||
Future<String> loadFrom(Uri uri) async => _tryLoadUri(uri);
|
Future<I18n> loadFrom(Uri uri) async =>
|
||||||
|
super.currentI18nFile = await _tryLoadUri(uri);
|
||||||
}
|
}
|
@ -15,19 +15,26 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:wyatt_i18n/src/core/enums/format.dart';
|
import 'package:wyatt_i18n/src/core/enums/format.dart';
|
||||||
import 'package:wyatt_i18n/src/core/exceptions/exceptions.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/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/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.
|
/// Implementation of [I18nDataSource] that loads i18n files from the network.
|
||||||
///
|
///
|
||||||
/// The [baseUri] is the base uri where the i18n files are located.
|
/// 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 [baseName] is the name of the i18n files without the extension.
|
||||||
/// The [fallbackAssetPath] is the path to the fallback i18n files.
|
/// The [fallbackAssetPath] is the path to the fallback i18n files.
|
||||||
/// The [format] is the format of the 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/`
|
/// 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
|
/// 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
|
/// and are named `i18n.arb`, `i18n.en.arb`, `i18n.fr.arb`, etc., then the
|
||||||
/// [fallbackAssetPath] is `l10n`.
|
/// [fallbackAssetPath] is `l10n`.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
class NetworkDataSourceImpl extends I18nDataSource {
|
class NetworkI18nDataSourceImpl extends I18nDataSource {
|
||||||
/// {@macro network_data_source_impl}
|
/// {@macro network_i18n_data_source_impl}
|
||||||
const NetworkDataSourceImpl({
|
NetworkI18nDataSourceImpl({
|
||||||
required this.baseUri,
|
required this.baseUri,
|
||||||
this.baseName = 'i18n',
|
this.baseName = 'i18n',
|
||||||
this.fallbackAssetPath = '',
|
this.fallbackAssetPath = '',
|
||||||
super.format = Format.arb,
|
super.format = Format.arb,
|
||||||
super.defaultLocale = 'en',
|
super.defaultLocale = const Locale('en'),
|
||||||
|
super.separator = '.',
|
||||||
|
super.localeTransformer,
|
||||||
}) : super();
|
}) : 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<NetworkI18nDataSourceImpl> 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.
|
/// The base uri where the i18n files are located.
|
||||||
final Uri baseUri;
|
final Uri baseUri;
|
||||||
|
|
||||||
@ -68,18 +101,26 @@ class NetworkDataSourceImpl extends I18nDataSource {
|
|||||||
final String fallbackAssetPath;
|
final String fallbackAssetPath;
|
||||||
|
|
||||||
/// Tries to load the i18n file from the given [locale].
|
/// Tries to load the i18n file from the given [locale].
|
||||||
Future<String> _tryLoad(String? locale, {Uri? overrideUri}) async {
|
Future<I18n> _tryLoad(Locale? locale, {Uri? overrideUri}) async {
|
||||||
String? content;
|
String? content;
|
||||||
final ext = format.name;
|
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
|
/// If the locale is null, then we try to load the default i18n file from
|
||||||
/// the fallback asset path.
|
/// the fallback asset path.
|
||||||
/// Otherwise, we try to load the i18n file for the given locale from the
|
/// Otherwise, we try to load the i18n file for the given locale from the
|
||||||
/// base uri.
|
/// base uri.
|
||||||
final path = AssetsUtils.cleanPath(
|
final path = AssetsUtils.cleanPath(
|
||||||
locale == null
|
locale == null
|
||||||
? '$fallbackAssetPath/$baseName.$defaultLocale.$ext'
|
? '$fallbackAssetPath/$baseName.$defaultLocaleString.$ext'
|
||||||
: overrideUri?.toString() ?? '$baseUri/$baseName.$locale.$ext',
|
: overrideUri?.toString() ?? '$baseUri/$baseName.$localeString.$ext',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (locale == null) {
|
if (locale == null) {
|
||||||
@ -108,7 +149,7 @@ class NetworkDataSourceImpl extends I18nDataSource {
|
|||||||
if (content == null && locale != null) {
|
if (content == null && locale != null) {
|
||||||
try {
|
try {
|
||||||
final fallbackPath = AssetsUtils.cleanPath(
|
final fallbackPath = AssetsUtils.cleanPath(
|
||||||
'$fallbackAssetPath/$baseName.$defaultLocale.$ext',
|
'$fallbackAssetPath/$baseName.$defaultLocaleString.$ext',
|
||||||
);
|
);
|
||||||
content = await rootBundle.loadString(fallbackPath);
|
content = await rootBundle.loadString(fallbackPath);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@ -122,7 +163,14 @@ class NetworkDataSourceImpl extends I18nDataSource {
|
|||||||
throw SourceNotFoundException(path, format: format);
|
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].
|
/// 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
|
/// If the [locale] is null, then the default i18n file is loaded from the
|
||||||
/// fallback asset path.
|
/// fallback asset path.
|
||||||
@override
|
@override
|
||||||
Future<String> load({required String? locale}) async => _tryLoad(locale);
|
Future<I18n> load({Locale? locale}) async =>
|
||||||
|
super.currentI18nFile = await _tryLoad(locale);
|
||||||
|
|
||||||
/// Loads the i18n file from the given [uri].
|
/// Loads the i18n file from the given [uri].
|
||||||
///
|
///
|
||||||
/// If the fetch fails, then the fallback i18n files are loaded from the
|
/// If the fetch fails, then the fallback i18n files are loaded from the
|
||||||
/// [fallbackAssetPath] in the root bundle.
|
/// [fallbackAssetPath] in the root bundle.
|
||||||
@override
|
@override
|
||||||
Future<String> loadFrom(Uri uri) async => _tryLoad(null, overrideUri: uri);
|
Future<I18n> loadFrom(Uri uri) async =>
|
||||||
|
super.currentI18nFile = await _tryLoad(null, overrideUri: uri);
|
||||||
}
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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<I18nFile> _parse(
|
|
||||||
String content,
|
|
||||||
Parser<String, Map<String, dynamic>> 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<I18nFile> load({
|
|
||||||
required String? locale,
|
|
||||||
bool strict = false,
|
|
||||||
Parser<String, Map<String, dynamic>>? parser,
|
|
||||||
}) async =>
|
|
||||||
await Result.tryCatchAsync<I18nFile, AppException, AppException>(
|
|
||||||
() 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<I18nFile> loadFrom(
|
|
||||||
Uri uri, {
|
|
||||||
Parser<String, Map<String, dynamic>>? parser,
|
|
||||||
}) async =>
|
|
||||||
await Result.tryCatchAsync<I18nFile, AppException, AppException>(
|
|
||||||
() 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<String, AppException> get(
|
|
||||||
String key, [
|
|
||||||
Map<String, dynamic> arguments = const {},
|
|
||||||
]) {
|
|
||||||
final I18nFileParser parser =
|
|
||||||
I18nFileParser(i18n: _i18n, arguments: arguments);
|
|
||||||
|
|
||||||
if (_i18n.containsKey(key)) {
|
|
||||||
return Result.tryCatch<String, AppException, AppException>(
|
|
||||||
() => parser.parse(_i18n[key] as String, key: key),
|
|
||||||
(error) => error,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw KeyNotFoundException(key, arguments);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Result<I18nFile, AppException> getI18n() =>
|
|
||||||
Result.conditional(!_i18n.isEmpty, _i18n, NotLoadedException());
|
|
||||||
|
|
||||||
@override
|
|
||||||
Result<String, AppException> getLocale() =>
|
|
||||||
Result.conditional(!_i18n.isEmpty, _i18n.locale, NotLoadedException());
|
|
||||||
|
|
||||||
@override
|
|
||||||
Result<void, AppException> setI18n(I18nFile i18n) {
|
|
||||||
_i18n = i18n;
|
|
||||||
|
|
||||||
return const Ok(null);
|
|
||||||
}
|
|
||||||
}
|
|
@ -14,8 +14,11 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:wyatt_architecture/wyatt_architecture.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}
|
/// {@template i18n_data_source}
|
||||||
/// Base class for i18n data sources.
|
/// Base class for i18n data sources.
|
||||||
@ -26,24 +29,62 @@ import 'package:wyatt_i18n/src/core/enums/format.dart';
|
|||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
abstract class I18nDataSource extends BaseDataSource {
|
abstract class I18nDataSource extends BaseDataSource {
|
||||||
/// {@macro i18n_data_source}
|
/// {@macro i18n_data_source}
|
||||||
const I18nDataSource({
|
I18nDataSource({
|
||||||
this.format = Format.arb,
|
this.format = Format.arb,
|
||||||
this.defaultLocale = 'en',
|
this.defaultLocale = const Locale('en'),
|
||||||
|
this.separator = '.',
|
||||||
|
this.localeTransformer,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The format of the i18n file.
|
/// The format of the i18n file.
|
||||||
final Format format;
|
final Format format;
|
||||||
|
|
||||||
/// The default locale.
|
/// 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<Parser<String, Map<String, dynamic>>> 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.
|
/// Loads the i18n file from the source.
|
||||||
/// If [locale] is not `null`, it will load the file with the given [locale].
|
/// If [locale] is not `null`, it will load the file with the given [locale].
|
||||||
/// Otherwise, it will load the file from the default location.
|
/// Otherwise, it will load the file from the default location.
|
||||||
Future<String> load({required String? locale});
|
Future<I18n> load({Locale? locale});
|
||||||
|
|
||||||
/// Loads the i18n file from the source.
|
/// Loads the i18n file from the source.
|
||||||
///
|
///
|
||||||
/// This method is used to load the i18n file from the given [Uri].
|
/// This method is used to load the i18n file from the given [Uri].
|
||||||
Future<String> loadFrom(Uri uri);
|
Future<I18n> loadFrom(Uri uri);
|
||||||
}
|
}
|
||||||
|
@ -16,5 +16,3 @@
|
|||||||
|
|
||||||
export 'data_sources/i18n_data_source.dart';
|
export 'data_sources/i18n_data_source.dart';
|
||||||
export 'entities/i18n.dart';
|
export 'entities/i18n.dart';
|
||||||
export 'entities/i18n_file.dart';
|
|
||||||
export 'repositories/i18n_repository.dart';
|
|
||||||
|
@ -14,47 +14,104 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:wyatt_architecture/wyatt_architecture.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}
|
/// {@template i18n_file}
|
||||||
/// This class is used to store the translations of the application.
|
/// Data structure for i18n files.
|
||||||
/// This entity is used by the I18nDelegate and Flutter's Localizations
|
|
||||||
/// widget to provide the translations to the application.
|
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
class I18n extends Entity {
|
class I18n extends Entity with EquatableMixin {
|
||||||
/// {@macro i18n}
|
/// {@macro i18n_file}
|
||||||
I18n({
|
const I18n({
|
||||||
required this.i18nRepository,
|
required this.unparsedData,
|
||||||
|
required this.data,
|
||||||
|
this.locale,
|
||||||
}) : super();
|
}) : super();
|
||||||
|
|
||||||
final I18nRepository i18nRepository;
|
/// Creates an empty i18n file.
|
||||||
|
const I18n.empty() : this(unparsedData: '', data: const {});
|
||||||
|
|
||||||
/// Get the translation of the given [key].
|
/// The locale of the i18n file.
|
||||||
/// If the [key] is not found, the [key] itself is returned.
|
final Locale? locale;
|
||||||
/// 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<String, dynamic>? args]) {
|
|
||||||
final result = i18nRepository.get(key, args ?? const <String, dynamic>{});
|
|
||||||
|
|
||||||
return result.fold(
|
/// The unparsed data of the i18n file.
|
||||||
(value) => value,
|
final String unparsedData;
|
||||||
(error) => key,
|
|
||||||
);
|
/// The data of the i18n file.
|
||||||
|
final Map<String, dynamic> 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the translation of the given [key].
|
return const LocaleParser().parse(data['@@locale'] as String);
|
||||||
///
|
} else {
|
||||||
/// Note: arguments are not supported.
|
throw NoLocaleException();
|
||||||
String operator [](String key) => get(key);
|
}
|
||||||
|
} 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<String, dynamic>? args]) {
|
||||||
|
final arguments = args ?? const <String, dynamic>{};
|
||||||
|
|
||||||
|
/// 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].
|
/// Get the translation of the given [key].
|
||||||
String call(String key, [Map<String, dynamic>? args]) => get(key, args);
|
String call(String key, [Map<String, dynamic>? args]) => get(key, args);
|
||||||
|
|
||||||
/// Load the translations from the given [locale].
|
/// Checks if the i18n file contains a key.
|
||||||
/// If the [locale] is not found, the default locale is loaded.
|
bool containsKey(String key) => data.containsKey(key);
|
||||||
Future<void> load(String locale) async {
|
|
||||||
await i18nRepository.load(locale: locale);
|
/// 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<Object?> get props => [locale, unparsedData, data];
|
||||||
}
|
}
|
||||||
|
@ -1,54 +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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import 'package:equatable/equatable.dart';
|
|
||||||
import 'package:wyatt_architecture/wyatt_architecture.dart';
|
|
||||||
|
|
||||||
/// {@template i18n_file}
|
|
||||||
/// Data structure for i18n files.
|
|
||||||
/// {@endtemplate}
|
|
||||||
class I18nFile extends Entity with EquatableMixin {
|
|
||||||
/// {@macro i18n_file}
|
|
||||||
const I18nFile({
|
|
||||||
required this.locale,
|
|
||||||
required this.unparsedData,
|
|
||||||
required this.data,
|
|
||||||
}) : super();
|
|
||||||
|
|
||||||
/// Creates an empty i18n file.
|
|
||||||
const I18nFile.empty() : this(locale: '', unparsedData: '', data: const {});
|
|
||||||
|
|
||||||
/// The locale of the i18n file.
|
|
||||||
final String locale;
|
|
||||||
|
|
||||||
/// The unparsed data of the i18n file.
|
|
||||||
final String unparsedData;
|
|
||||||
|
|
||||||
/// The data of the i18n file.
|
|
||||||
final Map<String, dynamic> data;
|
|
||||||
|
|
||||||
/// 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.isEmpty;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [locale, unparsedData, data];
|
|
||||||
}
|
|
@ -1,77 +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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import 'package:wyatt_architecture/wyatt_architecture.dart';
|
|
||||||
import 'package:wyatt_i18n/src/core/utils/parser.dart';
|
|
||||||
import 'package:wyatt_i18n/src/domain/entities/i18n_file.dart';
|
|
||||||
import 'package:wyatt_type_utils/wyatt_type_utils.dart';
|
|
||||||
|
|
||||||
/// {@template i18n_repository}
|
|
||||||
/// Base class for i18n repositories.
|
|
||||||
///
|
|
||||||
/// This class is used to manage i18n files.
|
|
||||||
/// {@endtemplate}
|
|
||||||
abstract class I18nRepository extends BaseRepository {
|
|
||||||
/// {@macro i18n_repository}
|
|
||||||
const I18nRepository() : super();
|
|
||||||
|
|
||||||
/// The current i18n file.
|
|
||||||
I18nFile get i18n;
|
|
||||||
|
|
||||||
/// Loads the i18n file from the source.
|
|
||||||
/// If [strict] is `true`, it will throw an NoLocaleException if the
|
|
||||||
/// `@@locale` key is not found in the i18n file, otherwise it will
|
|
||||||
/// set the locale to the given [locale].
|
|
||||||
/// If [parser] is not `null`, it will use the given parser to parse
|
|
||||||
/// the i18n file. Otherwise, it will use the default parser for the format.
|
|
||||||
FutureOrResult<I18nFile> load({
|
|
||||||
required String? locale,
|
|
||||||
bool strict = false,
|
|
||||||
Parser<String, Map<String, dynamic>>? parser,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Loads the i18n file from the given [uri].
|
|
||||||
/// If [parser] is not `null`, it will use the given parser to parse
|
|
||||||
/// the i18n file. Otherwise, it will use the default parser for the format.
|
|
||||||
FutureOrResult<I18nFile> loadFrom(
|
|
||||||
Uri uri, {
|
|
||||||
Parser<String, Map<String, dynamic>>? parser,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Gets the translation for the given [key].
|
|
||||||
///
|
|
||||||
/// If [arguments] is not `null`, it will replace the placeholders in the
|
|
||||||
/// translation with the given arguments.
|
|
||||||
Result<String, AppException> get(
|
|
||||||
String key, [
|
|
||||||
Map<String, dynamic> arguments = const {},
|
|
||||||
]);
|
|
||||||
|
|
||||||
/// Sets the current i18n instance.
|
|
||||||
///
|
|
||||||
/// This method is used to set the current i18n instance.
|
|
||||||
Result<void, AppException> setI18n(I18nFile i18n);
|
|
||||||
|
|
||||||
/// Gets the current i18n instance.
|
|
||||||
///
|
|
||||||
/// This method is used to get the current i18n instance.
|
|
||||||
Result<I18nFile, AppException> getI18n();
|
|
||||||
|
|
||||||
/// Gets the current locale.
|
|
||||||
///
|
|
||||||
/// This method is used to get the current locale.
|
|
||||||
Result<String, AppException> getLocale();
|
|
||||||
}
|
|
@ -18,21 +18,21 @@ import 'package:flutter/cupertino.dart';
|
|||||||
import 'package:wyatt_i18n/wyatt_i18n.dart';
|
import 'package:wyatt_i18n/wyatt_i18n.dart';
|
||||||
|
|
||||||
/// {@template i18n_delegate}
|
/// {@template i18n_delegate}
|
||||||
/// A [LocalizationsDelegate] that loads the [I18n] instance.
|
/// A [LocalizationsDelegate] that loads the [I18n] instance from
|
||||||
|
/// the [I18nDataSource].
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
class I18nDelegate extends LocalizationsDelegate<I18n> {
|
class I18nDelegate extends LocalizationsDelegate<I18n> {
|
||||||
/// {@macro i18n_delegate}
|
/// {@macro i18n_delegate}
|
||||||
I18nDelegate({
|
I18nDelegate({
|
||||||
required this.repository,
|
required this.dataSource,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The [I18nRepository] that will be used to load the i18n file.
|
final I18nDataSource dataSource;
|
||||||
final I18nRepository repository;
|
|
||||||
|
|
||||||
/// The current locale.
|
/// The current locale.
|
||||||
Locale? currentLocale;
|
Locale? currentLocale;
|
||||||
|
|
||||||
/// Since we are using the [I18nRepository] to load the i18n file,
|
/// Since we are using the [I18nDataSource] to load the i18n file,
|
||||||
/// we don't need to check if the locale is supported.
|
/// we don't need to check if the locale is supported.
|
||||||
/// We can just return `true`.
|
/// We can just return `true`.
|
||||||
@override
|
@override
|
||||||
@ -40,9 +40,10 @@ class I18nDelegate extends LocalizationsDelegate<I18n> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<I18n> load(Locale locale) async {
|
Future<I18n> load(Locale locale) async {
|
||||||
await repository.load(locale: locale.languageCode);
|
currentLocale = locale;
|
||||||
|
final i18n = await dataSource.load(locale: locale);
|
||||||
|
|
||||||
return I18n(i18nRepository: repository);
|
return i18n;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -18,6 +18,4 @@
|
|||||||
/// And update any widgets that are listening to the i18n map.
|
/// And update any widgets that are listening to the i18n map.
|
||||||
///
|
///
|
||||||
/// A top level BLoC is used to provide the i18n map to the app.
|
/// A top level BLoC is used to provide the i18n map to the app.
|
||||||
void main() {
|
void main() {}
|
||||||
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user