feat(i18n)!: rename few files + add clearer documentation

This commit is contained in:
Hugo Pointcheval 2023-03-01 17:27:19 +01:00
parent 55ee89fb26
commit 6c7e561fde
Signed by: hugo
GPG Key ID: 3AAC487E131E00BC
22 changed files with 510 additions and 429 deletions

View File

@ -23,28 +23,131 @@
<img src="https://img.shields.io/badge/SDK-Flutter-blue?style=flat-square" alt="SDK: Flutter" />
</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
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>(
() => 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.

View File

@ -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})),
),
),
),
),
);
}
);
}

View File

@ -14,11 +14,10 @@
// 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_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.
///

View 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();
}

View File

@ -14,8 +14,8 @@
// 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_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

View File

@ -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<String, String> {
/// {@macro i18n_file_parser}
@ -30,8 +30,8 @@ class I18nFileParser extends Parser<String, String> {
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<String, dynamic> arguments;

View File

@ -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<String, List<Token>?> {
/// {@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<Token>? parse(String message) {
@override
List<Token>? parse(String input) {
final parsed = (compound | pluralOrGenderOrSelect | simpleText | empty)
.map(
(result) => List<Token>.from(result is List ? result : [result]),
)
.parse(message);
.parse(input);
return parsed.isSuccess ? parsed.value : null;
}

View File

@ -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}';
}

View File

@ -14,9 +14,9 @@
// You should have received a copy of the GNU General Public License
// 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 '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';

View File

@ -14,6 +14,6 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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';

View File

@ -15,31 +15,61 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<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.
///
/// 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<String> _tryLoad(String? locale) async {
Future<I18n> _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<String> _tryLoadUri(Uri uri) async {
Future<I18n> _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] +
/// `.<locale>.<extension>` and must be specified in the `pubspec.yaml` file.
@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.
///
@ -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<String> loadFrom(Uri uri) async => _tryLoadUri(uri);
Future<I18n> loadFrom(Uri uri) async =>
super.currentI18nFile = await _tryLoadUri(uri);
}

View File

@ -15,19 +15,26 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<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.
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<String> _tryLoad(String? locale, {Uri? overrideUri}) async {
Future<I18n> _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<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].
///
/// If the fetch fails, then the fallback i18n files are loaded from the
/// [fallbackAssetPath] in the root bundle.
@override
Future<String> loadFrom(Uri uri) async => _tryLoad(null, overrideUri: uri);
Future<I18n> loadFrom(Uri uri) async =>
super.currentI18nFile = await _tryLoad(null, overrideUri: uri);
}

View File

@ -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);
}
}

View File

@ -14,8 +14,11 @@
// 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_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<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.
/// 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<String> load({required String? locale});
Future<I18n> load({Locale? locale});
/// Loads the i18n file from the source.
///
/// This method is used to load the i18n file from the given [Uri].
Future<String> loadFrom(Uri uri);
Future<I18n> loadFrom(Uri uri);
}

View File

@ -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';

View File

@ -14,47 +14,104 @@
// 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: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<String, dynamic>? args]) {
final result = i18nRepository.get(key, args ?? const <String, dynamic>{});
/// 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<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();
}
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<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].
String call(String key, [Map<String, dynamic>? args]) => get(key, args);
/// Load the translations from the given [locale].
/// If the [locale] is not found, the default locale is loaded.
Future<void> 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<Object?> get props => [locale, unparsedData, data];
}

View File

@ -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];
}

View File

@ -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();
}

View File

@ -18,21 +18,21 @@ import 'package:flutter/cupertino.dart';
import 'package:wyatt_i18n/wyatt_i18n.dart';
/// {@template i18n_delegate}
/// A [LocalizationsDelegate] that loads the [I18n] instance.
/// A [LocalizationsDelegate] that loads the [I18n] instance from
/// the [I18nDataSource].
/// {@endtemplate}
class I18nDelegate extends LocalizationsDelegate<I18n> {
/// {@macro i18n_delegate}
I18nDelegate({
required this.repository,
required this.dataSource,
});
/// The [I18nRepository] that will be used to load the i18n file.
final I18nRepository repository;
final I18nDataSource dataSource;
/// The current locale.
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 can just return `true`.
@override
@ -40,9 +40,10 @@ class I18nDelegate extends LocalizationsDelegate<I18n> {
@override
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

View File

@ -18,6 +18,4 @@
/// 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.
void main() {
}
void main() {}