Compare commits
	
		
			3 Commits
		
	
	
		
			75f561a19e
			...
			0f8f9abcf4
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 0f8f9abcf4 | |||
| 17ece11170 | |||
| cbbde8db85 | 
@ -58,7 +58,13 @@ class MalformedValueException extends ClientException {
 | 
			
		||||
      : super('Key `$key` references a malformed value. ($value)');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Exception thrown when the parser fails.
 | 
			
		||||
class ParserException extends ClientException {
 | 
			
		||||
  ParserException(String message, StackTrace? stackTrace)
 | 
			
		||||
      : super('$message\n\n$stackTrace');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Exception thrown when the i18n is not loaded.
 | 
			
		||||
class NotLoadedException extends ClientException {
 | 
			
		||||
  NotLoadedException() : super('I18n not loaded.');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										40
									
								
								packages/wyatt_i18n/lib/src/core/utils/assets_utils.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								packages/wyatt_i18n/lib/src/core/utils/assets_utils.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,40 @@
 | 
			
		||||
// 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 'dart:convert';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
 | 
			
		||||
class AssetsUtils {
 | 
			
		||||
  /// Checks if the given [assetPath] is a local asset.
 | 
			
		||||
  ///
 | 
			
		||||
  /// In fact, this method loads the asset and checks if it is null.
 | 
			
		||||
  static Future<bool> assetExists(String assetPath) async {
 | 
			
		||||
    final encoded = utf8.encoder.convert(
 | 
			
		||||
      Uri(path: Uri.encodeFull(assetPath)).path,
 | 
			
		||||
    );
 | 
			
		||||
    final asset = await ServicesBinding.instance.defaultBinaryMessenger
 | 
			
		||||
        .send('flutter/assets', encoded.buffer.asByteData());
 | 
			
		||||
 | 
			
		||||
    return asset != null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Cleans the given [path] by removing all the consecutive slashes.
 | 
			
		||||
  /// For example, `///a///b///c///` becomes `/a/b/c/`.
 | 
			
		||||
  /// It also removes the first slash if it exists.
 | 
			
		||||
  static String cleanPath(String path) =>
 | 
			
		||||
      path.replaceAll(RegExp(r'\/+'), '/').replaceFirst(RegExp(r'^\/'), '');
 | 
			
		||||
}
 | 
			
		||||
@ -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 'dart:convert';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/services.dart' show ServicesBinding, rootBundle;
 | 
			
		||||
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:wyatt_i18n/src/core/utils/assets_utils.dart';
 | 
			
		||||
import 'package:wyatt_i18n/src/domain/data_sources/i18n_data_source.dart';
 | 
			
		||||
 | 
			
		||||
/// {@template assets_file_data_source_impl}
 | 
			
		||||
@ -34,10 +33,11 @@ import 'package:wyatt_i18n/src/domain/data_sources/i18n_data_source.dart';
 | 
			
		||||
/// {@endtemplate}
 | 
			
		||||
class AssetsFileDataSourceImpl extends I18nDataSource {
 | 
			
		||||
  /// {@macro assets_file_data_source_impl}
 | 
			
		||||
  const AssetsFileDataSourceImpl({
 | 
			
		||||
  AssetsFileDataSourceImpl({
 | 
			
		||||
    this.basePath = 'assets',
 | 
			
		||||
    this.baseName = 'i18n',
 | 
			
		||||
    super.format = Format.arb,
 | 
			
		||||
    super.defaultLocale = 'en',
 | 
			
		||||
  }) : super();
 | 
			
		||||
 | 
			
		||||
  /// The folder where the i18n files are located.
 | 
			
		||||
@ -46,29 +46,23 @@ class AssetsFileDataSourceImpl extends I18nDataSource {
 | 
			
		||||
  /// The name of the i18n files without the extension.
 | 
			
		||||
  final String baseName;
 | 
			
		||||
 | 
			
		||||
  /// Checks if the given [assetPath] is a local asset.
 | 
			
		||||
  ///
 | 
			
		||||
  /// In fact, this method loads the asset and checks if it is null.
 | 
			
		||||
  Future<bool> assetExists(String assetPath) async {
 | 
			
		||||
    final encoded = utf8.encoder.convert(
 | 
			
		||||
      Uri(path: Uri.encodeFull(assetPath)).path,
 | 
			
		||||
    );
 | 
			
		||||
    final asset = await ServicesBinding.instance.defaultBinaryMessenger
 | 
			
		||||
        .send('flutter/assets', encoded.buffer.asByteData());
 | 
			
		||||
 | 
			
		||||
    return asset != null;
 | 
			
		||||
  }
 | 
			
		||||
  /// The asset bundle used to load the i18n files.
 | 
			
		||||
  final AssetBundle assetBundle = rootBundle;
 | 
			
		||||
 | 
			
		||||
  /// Tries to load the i18n file from the given [locale].
 | 
			
		||||
  Future<String> _tryLoad(String? locale) async {
 | 
			
		||||
    String? content;
 | 
			
		||||
    final ext = format.name;
 | 
			
		||||
    final path = locale == null
 | 
			
		||||
        ? '$basePath/$baseName.$ext'
 | 
			
		||||
        : '$basePath/$baseName.$locale.$ext';
 | 
			
		||||
 | 
			
		||||
    final path = AssetsUtils.cleanPath(
 | 
			
		||||
      locale == null
 | 
			
		||||
          ? '$basePath/$baseName.$defaultLocale.$ext'
 | 
			
		||||
          : '$basePath/$baseName.$locale.$ext',
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      if (await assetExists(path)) {
 | 
			
		||||
        content = await rootBundle.loadString(path);
 | 
			
		||||
      if (await AssetsUtils.assetExists(path)) {
 | 
			
		||||
        content = await assetBundle.loadString(path);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (_) {
 | 
			
		||||
      content = null;
 | 
			
		||||
@ -78,7 +72,10 @@ class AssetsFileDataSourceImpl extends I18nDataSource {
 | 
			
		||||
    /// default i18n file.
 | 
			
		||||
    if (content == null && locale != null) {
 | 
			
		||||
      try {
 | 
			
		||||
        content = await rootBundle.loadString('$basePath/$baseName.$ext');
 | 
			
		||||
        final fallbackPath = AssetsUtils.cleanPath(
 | 
			
		||||
          '$basePath/$baseName.$defaultLocale.$ext',
 | 
			
		||||
        );
 | 
			
		||||
        content = await assetBundle.loadString(fallbackPath);
 | 
			
		||||
      } catch (_) {
 | 
			
		||||
        throw SourceNotFoundException(path, format: format);
 | 
			
		||||
      }
 | 
			
		||||
@ -98,7 +95,7 @@ class AssetsFileDataSourceImpl extends I18nDataSource {
 | 
			
		||||
  Future<String> _tryLoadUri(Uri uri) async {
 | 
			
		||||
    String? content;
 | 
			
		||||
    try {
 | 
			
		||||
      content = await rootBundle.loadString(uri.toString());
 | 
			
		||||
      content = await assetBundle.loadString(uri.toString());
 | 
			
		||||
    } catch (_) {
 | 
			
		||||
      throw SourceNotFoundException(uri.toString(), format: format);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -14,21 +14,117 @@
 | 
			
		||||
// 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/services.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/domain/data_sources/i18n_data_source.dart';
 | 
			
		||||
 | 
			
		||||
/// {@template network_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.
 | 
			
		||||
///
 | 
			
		||||
/// 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
 | 
			
		||||
/// `https://example.com/i18n/` and the [baseName] is `i18n` and the
 | 
			
		||||
/// [format] is [Format.arb].
 | 
			
		||||
///
 | 
			
		||||
/// If the i18n file for the given locale is not found or if the fetch fails,
 | 
			
		||||
/// then the fallback i18n files are loaded from the [fallbackAssetPath] in
 | 
			
		||||
/// the root bundle.
 | 
			
		||||
///
 | 
			
		||||
/// For example, if the fallback i18n files are located in the `assets` and are
 | 
			
		||||
/// named `i18n.arb`, `i18n.en.arb`, `i18n.fr.arb`, etc., then the
 | 
			
		||||
/// [fallbackAssetPath] is `assets`.
 | 
			
		||||
/// {@endtemplate}
 | 
			
		||||
class NetworkDataSourceImpl extends I18nDataSource {
 | 
			
		||||
  const NetworkDataSourceImpl({super.format = Format.arb}) : super();
 | 
			
		||||
  
 | 
			
		||||
  @override
 | 
			
		||||
  Future<String> load({required String? locale}) {
 | 
			
		||||
    // TODO(wyatt): implement load
 | 
			
		||||
    throw UnimplementedError();
 | 
			
		||||
  /// {@macro network_data_source_impl}
 | 
			
		||||
  const NetworkDataSourceImpl({
 | 
			
		||||
    required this.baseUri,
 | 
			
		||||
    this.baseName = 'i18n',
 | 
			
		||||
    this.fallbackAssetPath = 'assets',
 | 
			
		||||
    super.format = Format.arb,
 | 
			
		||||
    super.defaultLocale = 'en',
 | 
			
		||||
  }) : super();
 | 
			
		||||
 | 
			
		||||
  /// The base uri where the i18n files are located.
 | 
			
		||||
  final Uri baseUri;
 | 
			
		||||
 | 
			
		||||
  /// The name of the i18n files without the extension.
 | 
			
		||||
  final String baseName;
 | 
			
		||||
 | 
			
		||||
  /// The path to the fallback i18n files.
 | 
			
		||||
  ///
 | 
			
		||||
  /// This is used when the i18n file for the given locale is not found or
 | 
			
		||||
  /// if the fetch fails.
 | 
			
		||||
  final String fallbackAssetPath;
 | 
			
		||||
 | 
			
		||||
  /// Tries to load the i18n file from the given [locale].
 | 
			
		||||
  Future<String> _tryLoad(String? locale, {Uri? overrideUri}) async {
 | 
			
		||||
    String? content;
 | 
			
		||||
    final ext = format.name;
 | 
			
		||||
 | 
			
		||||
    /// 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',
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (locale == null) {
 | 
			
		||||
      /// Load the default i18n file from the fallback asset path.
 | 
			
		||||
      try {
 | 
			
		||||
        if (await AssetsUtils.assetExists(path)) {
 | 
			
		||||
          content = await rootBundle.loadString(path);
 | 
			
		||||
        }
 | 
			
		||||
      } catch (_) {
 | 
			
		||||
        content = null;
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      /// Load the i18n file for the given locale from the base uri.
 | 
			
		||||
      try {
 | 
			
		||||
        final response = await http.get(Uri.parse(path));
 | 
			
		||||
        if (response.statusCode >= 200 && response.statusCode < 300) {
 | 
			
		||||
          content = response.body;
 | 
			
		||||
        }
 | 
			
		||||
      } catch (_) {
 | 
			
		||||
        content = null;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// If the i18n file is not found, then we try to load the
 | 
			
		||||
    /// default i18n file.
 | 
			
		||||
    if (content == null && locale != null) {
 | 
			
		||||
      try {
 | 
			
		||||
        final fallbackPath = AssetsUtils.cleanPath(
 | 
			
		||||
          '$fallbackAssetPath/$baseName.$defaultLocale.$ext',
 | 
			
		||||
        );
 | 
			
		||||
        content = await rootBundle.loadString(fallbackPath);
 | 
			
		||||
      } catch (_) {
 | 
			
		||||
        throw SourceNotFoundException(path, format: format);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// If the default i18n file is not found, then we throw an exception.
 | 
			
		||||
    /// This case should happen only if the locale is null.
 | 
			
		||||
    if (content == null) {
 | 
			
		||||
      throw SourceNotFoundException(path, format: format);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return content;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<String> loadFrom(Uri uri) {
 | 
			
		||||
    // TODO(wyatt): implement loadFrom
 | 
			
		||||
    throw UnimplementedError();
 | 
			
		||||
  }
 | 
			
		||||
  Future<String> load({required String? locale}) async => _tryLoad(locale);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<String> loadFrom(Uri uri) async => _tryLoad(null, overrideUri: uri);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -15,18 +15,27 @@
 | 
			
		||||
// 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/i18n_parser.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.
 | 
			
		||||
  I18n _i18n = const I18n.empty();
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  I18n get i18n => _i18n;
 | 
			
		||||
 | 
			
		||||
  Future<I18n> _parse(
 | 
			
		||||
    String content,
 | 
			
		||||
    Parser<String, Map<String, dynamic>> parser, {
 | 
			
		||||
@ -129,4 +138,19 @@ class I18nRepositoryImpl extends I18nRepository {
 | 
			
		||||
      throw KeyNotFoundException(key, arguments);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Result<I18n, 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(I18n i18n) {
 | 
			
		||||
    _i18n = i18n;
 | 
			
		||||
 | 
			
		||||
    return const Ok(null);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -17,23 +17,33 @@
 | 
			
		||||
import 'package:wyatt_architecture/wyatt_architecture.dart';
 | 
			
		||||
import 'package:wyatt_i18n/src/core/enums/format.dart';
 | 
			
		||||
 | 
			
		||||
/// {@template i18n_data_source}
 | 
			
		||||
/// Base class for i18n data sources.
 | 
			
		||||
/// 
 | 
			
		||||
///
 | 
			
		||||
/// This class is used to load i18n files from a source.
 | 
			
		||||
/// It returns a [Future] that resolves to a [String] containing the i18n file
 | 
			
		||||
/// contents.
 | 
			
		||||
/// {@endtemplate}
 | 
			
		||||
abstract class I18nDataSource extends BaseDataSource {
 | 
			
		||||
  const I18nDataSource({this.format = Format.arb});
 | 
			
		||||
  /// {@macro i18n_data_source}
 | 
			
		||||
  const I18nDataSource({
 | 
			
		||||
    this.format = Format.arb,
 | 
			
		||||
    this.defaultLocale = 'en',
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  /// The format of the i18n file.
 | 
			
		||||
  final Format format;
 | 
			
		||||
 | 
			
		||||
  /// The default locale.
 | 
			
		||||
  final String defaultLocale;
 | 
			
		||||
 | 
			
		||||
  /// 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});
 | 
			
		||||
 | 
			
		||||
  /// 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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -39,4 +39,6 @@ class I18n extends Entity {
 | 
			
		||||
  bool containsKey(String key) => data.containsKey(key);
 | 
			
		||||
 | 
			
		||||
  dynamic operator [](String key) => data[key];
 | 
			
		||||
 | 
			
		||||
  bool get isEmpty => data.isEmpty && unparsedData.isEmpty && locale.isEmpty;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -19,10 +19,18 @@ import 'package:wyatt_i18n/src/core/utils/parser.dart';
 | 
			
		||||
import 'package:wyatt_i18n/src/domain/entities/i18n.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.
 | 
			
		||||
  I18n 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
 | 
			
		||||
@ -44,8 +52,26 @@ abstract class I18nRepository extends BaseRepository {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  /// 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(I18n i18n);
 | 
			
		||||
 | 
			
		||||
  /// Gets the current i18n instance.
 | 
			
		||||
  ///
 | 
			
		||||
  /// This method is used to get the current i18n instance.
 | 
			
		||||
  Result<I18n, AppException> getI18n();
 | 
			
		||||
 | 
			
		||||
  /// Gets the current locale.
 | 
			
		||||
  ///
 | 
			
		||||
  /// This method is used to get the current locale.
 | 
			
		||||
  Result<String, AppException> getLocale();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@ environment:
 | 
			
		||||
dependencies:
 | 
			
		||||
  collection: ^1.17.0
 | 
			
		||||
  flutter: {sdk: flutter}
 | 
			
		||||
  http: ^0.13.5
 | 
			
		||||
  intl: ^0.18.0
 | 
			
		||||
  path: ^1.8.0
 | 
			
		||||
  petitparser: ^5.1.0
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user