diff --git a/CHANGELOG.md b/CHANGELOG.md index e99b64f..99ec8f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.0 + +* Production release + ## 0.0.7 * Added `when` and `setValidator` diff --git a/README.md b/README.md index 0eaa16f..49996cb 100644 --- a/README.md +++ b/README.md @@ -268,6 +268,111 @@ After that, execute a validation normaly: expect(result.isValid, isTrue); ``` +You can use `byField` using nested params syntax: + +```dart +final validator = CustomerValidator(); + +final postCodeValidator = validator.byField(customer, 'address.postcode')(); +expect(postCodeValidator, null); // is valid + +``` + +There are several ways to customize or internationalize the failure message in validation. + +All validations have the `message` parameter for customization, with the possibility of receiving arguments to make the message more dynamic. + +```dart + ruleFor((entity) => entity.name, key: 'name') + .isEmpty(message: "'{PropertyName}' can not be empty." ) +``` + +Please note that the `{PropertyName}` is an exclusive parameter of the `isEmpty` validation that will be internally changed to the validation's `key`, which in this case is `name`. +Each validation can have different parameters such as `{PropertyValue}` or `{ComparisonValue}`, so please check the documentation of each one to know the available parameters. + +### Default Messages + +By default, validation messages are in English, but you can change the language in the global properties of `LucidValidation`. + + +```dart +LucidValidation.global.culture = Culture('pt', 'BR'); +``` + +If you’d like to contribute a translation of `LucidValidation’s` default messages, please open a pull request that adds a language file to the project. + + +You can also customize the default messages by overriding the `LanguageManager`: + +```dart +class CustomLanguageManager extends LanguageManager { + CustomLanguageManager(){ + addTranslation(Culture('pt', 'PR'), Language.code.equalTo, 'Custom message here'); + } +} +... +// change manager +LucidValidation.global.languageManager = CustomLanguageManager(); + +``` + +## Flutter Configuration + +Podemos criar um `Delegate` para automatizar a internacionalização diretamente no Flutter. + +Para criar um `Delegate` siga esses passos: + +```dart +class LucidLocalizationDelegate extends LocalizationsDelegate { + const LucidLocalizationDelegate(); + + static final delegate = LucidLocalizationDelegate(); + + @override + bool isSupported(Locale locale) { + return LucidValidation.global.languageManager.isSupported( + locale.languageCode, + locale.countryCode, + ); + } + + @override + Future load(Locale locale) async { + print(locale); + final culture = Culture(locale.languageCode, locale.countryCode ?? ''); + LucidValidation.global.culture = culture; + return culture; + } + + @override + bool shouldReload(LocalizationsDelegate old) { + return true; + } +} +``` + +Agora basta adicionar no `MaterialApp` ou `CupertinoApp`: + +```dart + @override + Widget build(BuildContext context) { + return MaterialApp( + supportedLocales: const [ + Locale('en', 'US'), + Locale('pt', 'BR'), + ], + localizationsDelegates: [ + LucidLocalizationDelegate.delegate, + // + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + + ], + ... + ); + } +``` + ## Creating Custom Rules diff --git a/example/lib/domain/validations/extensions.dart b/example/lib/domain/validations/extensions.dart index 39651ba..1ca1ac7 100644 --- a/example/lib/domain/validations/extensions.dart +++ b/example/lib/domain/validations/extensions.dart @@ -12,11 +12,10 @@ extension CustomValidPasswordValidator on SimpleValidationBuilder { } extension CustomValidPhoneValidator on SimpleValidationBuilder { - SimpleValidationBuilder customValidPhone(String message) { + SimpleValidationBuilder customValidPhone() { return matchesPattern( r'^\(?(\d{2})\)?\s?9?\d{4}-?\d{4}$', - message: message, - code: 'invalid_phone', + code: 'validPhone', ); } } diff --git a/example/lib/domain/validations/language_manager.dart b/example/lib/domain/validations/language_manager.dart new file mode 100644 index 0000000..09a7722 --- /dev/null +++ b/example/lib/domain/validations/language_manager.dart @@ -0,0 +1,10 @@ +import 'package:lucid_validation/lucid_validation.dart'; + +class CustomLanguageManager extends LanguageManager { + CustomLanguageManager() { + addTranslation(Culture('pt', 'BR'), 'passwordEqualTo', "'{PropertyName}' deve ser igual."); + addTranslation(Culture('pt'), 'passwordEqualTo', "'{PropertyName}' deve ser igual."); + addTranslation(Culture('en', 'US'), 'passwordEqualTo', "'{PropertyName}' must be equal."); + addTranslation(Culture('en'), 'passwordEqualTo', "'{PropertyName}' must be equal."); + } +} diff --git a/example/lib/domain/validations/register_param_validation.dart b/example/lib/domain/validations/register_param_validation.dart index ccfaabf..f698da9 100644 --- a/example/lib/domain/validations/register_param_validation.dart +++ b/example/lib/domain/validations/register_param_validation.dart @@ -13,9 +13,9 @@ class RegisterParamValidation extends LucidValidator { ruleFor((registerParamDto) => registerParamDto.confirmPassword, key: 'confirmPassword') // .customValidPassword() - .equalTo((registerParamDto) => registerParamDto.password, message: 'Password and confirm password must match'); + .equalTo((registerParamDto) => registerParamDto.password, code: 'passwordEqualTo'); ruleFor((registerParamDto) => registerParamDto.phone, key: 'phone') // - .customValidPhone('Phone invalid format'); + .customValidPhone(); } } diff --git a/example/lib/main.dart b/example/lib/main.dart index 16421ce..70e5e43 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,22 +1,74 @@ import 'package:example/presentation/login_page/login_page.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:lucid_validation/lucid_validation.dart'; + +import 'domain/validations/language_manager.dart'; void main() { + LucidValidation.global.languageManager = CustomLanguageManager(); + runApp(const MyApp()); } +final globalLocale = ValueNotifier(Locale('en')); + class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - useMaterial3: true, - ), - home: const LoginPage(), + return ValueListenableBuilder( + valueListenable: globalLocale, + builder: (context, value, child) { + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Flutter Demo', + locale: value, + supportedLocales: const [ + Locale('en', 'US'), + Locale('pt', 'BR'), + ], + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + LucidLocalizationDelegate.delegate, + ], + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + home: const LoginPage(), + ); + }, + ); + } +} + +class LucidLocalizationDelegate extends LocalizationsDelegate { + const LucidLocalizationDelegate(); + + static final delegate = LucidLocalizationDelegate(); + + @override + bool isSupported(Locale locale) { + return LucidValidation.global.languageManager.isSupported( + locale.languageCode, + locale.countryCode, ); } + + @override + Future load(Locale locale) async { + print(locale); + final culture = Culture(locale.languageCode, locale.countryCode ?? ''); + LucidValidation.global.culture = culture; + return culture; + } + + @override + bool shouldReload(LocalizationsDelegate old) { + return true; + } } diff --git a/example/lib/presentation/login_page/login_page.dart b/example/lib/presentation/login_page/login_page.dart index e288bf6..301a81f 100644 --- a/example/lib/presentation/login_page/login_page.dart +++ b/example/lib/presentation/login_page/login_page.dart @@ -1,5 +1,6 @@ import 'package:example/domain/dtos/login_param_dto.dart'; import 'package:example/domain/validations/login_param_validation.dart'; +import 'package:example/main.dart'; import 'package:example/presentation/register_page/register_page.dart'; import 'package:flutter/material.dart'; @@ -35,6 +36,27 @@ class _LoginPageState extends State { @override Widget build(BuildContext context) { return Scaffold( + appBar: AppBar( + title: const Text('Login'), + actions: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Is English'), + ValueListenableBuilder( + valueListenable: globalLocale, + builder: (context, _, __) { + return Switch( + value: globalLocale.value.languageCode == 'en', + onChanged: (value) { + globalLocale.value = value ? Locale('en', 'US') : Locale('pt', 'BR'); + }, + ); + }), + ], + ) + ], + ), body: Form( key: formKey, child: Padding( diff --git a/example/lib/presentation/register_page/register_page.dart b/example/lib/presentation/register_page/register_page.dart index aa03b8b..467626c 100644 --- a/example/lib/presentation/register_page/register_page.dart +++ b/example/lib/presentation/register_page/register_page.dart @@ -1,5 +1,6 @@ import 'package:example/domain/dtos/register_param_dto.dart'; import 'package:example/domain/validations/register_param_validation.dart'; +import 'package:example/main.dart'; import 'package:flutter/material.dart'; class RegisterPage extends StatefulWidget { @@ -53,6 +54,27 @@ class _RegisterPageState extends State { @override Widget build(BuildContext context) { return Scaffold( + appBar: AppBar( + title: const Text('Login'), + actions: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Is English'), + ValueListenableBuilder( + valueListenable: globalLocale, + builder: (context, _, __) { + return Switch( + value: globalLocale.value.languageCode == 'en', + onChanged: (value) { + globalLocale.value = value ? Locale('en', 'US') : Locale('pt', 'BR'); + }, + ); + }), + ], + ) + ], + ), body: Padding( padding: const EdgeInsets.all(12.0), child: Column( diff --git a/example/pubspec.lock b/example/pubspec.lock index 003ad8d..5f10222 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -70,11 +70,24 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" leak_tracker: dependency: transitive description: @@ -113,7 +126,7 @@ packages: path: ".." relative: true source: path - version: "0.0.6" + version: "0.0.7" matcher: dependency: transitive description: @@ -211,10 +224,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.2.4" sdks: dart: ">=3.4.3 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 251b1f4..4efb7de 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -30,6 +30,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter lucid_validation: path: ../ diff --git a/lib/lucid_validation.dart b/lib/lucid_validation.dart index eebe024..84059f7 100644 --- a/lib/lucid_validation.dart +++ b/lib/lucid_validation.dart @@ -65,20 +65,20 @@ export 'src/validations/validations.dart'; sealed class LucidValidation { static final global = _GlobalConfig( - language: EnglishLanguage(), languageManager: DefaultLanguageManager(), cascadeMode: CascadeMode.continueExecution, + culture: Culture('en'), ); } class _GlobalConfig { LanguageManager languageManager; + Culture culture; CascadeMode cascadeMode; - Language language; _GlobalConfig({ required this.languageManager, required this.cascadeMode, - required this.language, + required this.culture, }); } diff --git a/lib/src/localization/culture.dart b/lib/src/localization/culture.dart new file mode 100644 index 0000000..4921cff --- /dev/null +++ b/lib/src/localization/culture.dart @@ -0,0 +1,17 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +class Culture { + final String languageCode; + final String countryCode; + + Culture(this.languageCode, [this.countryCode = '']); + + @override + bool operator ==(covariant Culture other) { + if (identical(this, other)) return true; + + return other.languageCode == languageCode && other.countryCode == countryCode; + } + + @override + int get hashCode => languageCode.hashCode ^ countryCode.hashCode; +} diff --git a/lib/src/localization/language.dart b/lib/src/localization/language.dart index 4fb989e..ffabf9b 100644 --- a/lib/src/localization/language.dart +++ b/lib/src/localization/language.dart @@ -1,6 +1,4 @@ abstract class Language { - final String culture; - static const code = ( equalTo: 'equalTo', greaterThan: 'greaterThan', @@ -33,7 +31,7 @@ abstract class Language { exclusiveBetweenDatetime: 'exclusiveBetweenDatetime', ); - Language(this.culture, [Map translations = const {}]) { + Language([Map translations = const {}]) { _translations.addAll(translations); } diff --git a/lib/src/localization/language_manager.dart b/lib/src/localization/language_manager.dart index 2c463a7..1a6d227 100644 --- a/lib/src/localization/language_manager.dart +++ b/lib/src/localization/language_manager.dart @@ -1,27 +1,72 @@ import '../../lucid_validation.dart'; import 'languages/portuguese_brazillian_language.dart'; +import 'languages/spanish_language.dart'; -final _avaliableLanguages = { - 'pt_BR': PortugueseBrasillianLanguage(), - 'pt': PortugueseBrasillianLanguage(), - 'en': EnglishLanguage(), - 'en_US': EnglishLanguage(), +final _avaliableLanguages = { + Culture('pt'): PortugueseBrasillianLanguage(), + Culture('pt', 'BR'): PortugueseBrasillianLanguage(), + Culture('en'): EnglishLanguage(), + Culture('en', 'US'): EnglishLanguage(), + Culture('en', 'UA'): EnglishLanguage(), + Culture('en', 'NZ'): EnglishLanguage(), + Culture('en', 'ZA'): EnglishLanguage(), + Culture('en', 'GB'): EnglishLanguage(), + Culture('en', 'CA'): EnglishLanguage(), + Culture('en', 'AU'): EnglishLanguage(), + Culture('en', 'IE'): EnglishLanguage(), + Culture('en', 'IN'): EnglishLanguage(), + Culture('en', 'SG'): EnglishLanguage(), + Culture('en', 'PH'): EnglishLanguage(), + Culture('en', 'MY'): EnglishLanguage(), + Culture('en', 'PK'): EnglishLanguage(), + Culture('en', 'NG'): EnglishLanguage(), + Culture('en', 'KE'): EnglishLanguage(), + Culture('en', 'GH'): EnglishLanguage(), + Culture('en', 'UG'): EnglishLanguage(), + Culture('en', 'TT'): EnglishLanguage(), + Culture('es'): SpanishLanguage(), + Culture('es', 'ES'): SpanishLanguage(), + Culture('es', 'MX'): SpanishLanguage(), + Culture('es', 'AR'): SpanishLanguage(), + Culture('es', 'CR'): SpanishLanguage(), + Culture('es', 'DO'): SpanishLanguage(), + Culture('es', 'CO'): SpanishLanguage(), + Culture('es', 'CL'): SpanishLanguage(), + Culture('es', 'PY'): SpanishLanguage(), + Culture('es', 'SV'): SpanishLanguage(), + Culture('es', 'NI'): SpanishLanguage(), + Culture('es', 'GT'): SpanishLanguage(), + Culture('es', 'PA'): SpanishLanguage(), + Culture('es', 'VE'): SpanishLanguage(), + Culture('es', 'PE'): SpanishLanguage(), + Culture('es', 'EC'): SpanishLanguage(), + Culture('es', 'UY'): SpanishLanguage(), + Culture('es', 'PO'): SpanishLanguage(), + Culture('es', 'HN'): SpanishLanguage(), + Culture('es', 'PR'): SpanishLanguage(), }; abstract class LanguageManager { - final _globalTranslations = >{}; + final _globalTranslations = >{}; - Language get currentLanguage => LucidValidation.global.language; - - void addTranslation(String culture, String code, String value) { + void addTranslation(Culture culture, String code, String value) { if (!_globalTranslations.containsKey(culture)) { _globalTranslations[culture] = {}; } _globalTranslations[culture]![code] = value; } + List avaliableCultures() { + return _avaliableLanguages.keys.toList(); + } + + bool isSupported(String languageCode, String? countryCode) { + return _avaliableLanguages.containsKey(Culture(languageCode, countryCode ?? '')); + } + String translate(String key, {Map parameters = const {}, String? defaultMessage}) { - final culture = currentLanguage.culture; + final culture = LucidValidation.global.culture; + final currentLanguage = getLanguage(culture); final translations = _globalTranslations[culture] ?? {}; var message = defaultMessage ?? translations[key] ?? currentLanguage.getTranslation(key) ?? key; for (var key in parameters.keys) { @@ -31,8 +76,8 @@ abstract class LanguageManager { return message; } - Language getLanguage(String culture) { - return _avaliableLanguages[culture] ?? LucidValidation.global.language; + Language getLanguage(Culture culture) { + return _avaliableLanguages[culture] ?? _avaliableLanguages[Culture('en')]!; } } diff --git a/lib/src/localization/languages/english_language.dart b/lib/src/localization/languages/english_language.dart index a3a8329..0b8dd80 100644 --- a/lib/src/localization/languages/english_language.dart +++ b/lib/src/localization/languages/english_language.dart @@ -1,5 +1,5 @@ import '../language.dart'; class EnglishLanguage extends Language { - EnglishLanguage() : super('en'); + EnglishLanguage(); } diff --git a/lib/src/localization/languages/portuguese_brazillian_language.dart b/lib/src/localization/languages/portuguese_brazillian_language.dart index bcfe954..22fe282 100644 --- a/lib/src/localization/languages/portuguese_brazillian_language.dart +++ b/lib/src/localization/languages/portuguese_brazillian_language.dart @@ -1,5 +1,30 @@ import '../language.dart'; class PortugueseBrasillianLanguage extends Language { - PortugueseBrasillianLanguage() : super('pt_BR'); + PortugueseBrasillianLanguage() + : super({ + Language.code.equalTo: "'{PropertyName}' deve ser igual a '{ComparisonValue}'.", + Language.code.greaterThan: "'{PropertyName}' deve ser maior que '{ComparisonValue}'.", + Language.code.isEmpty: "'{PropertyName}' deve estar vazio.", + Language.code.isNotNull: "'{PropertyName}' não pode ser nulo.", + Language.code.isNull: "'{PropertyName}' deve ser nulo.", + Language.code.lessThan: "'{PropertyName}' deve ser menor que '{ComparisonValue}'.", + Language.code.matchesPattern: "'{PropertyName}' não está no formato correto.", + Language.code.max: "'{PropertyName}' deve ser menor ou igual a {MaxValue}. Você digitou {PropertyValue}.", + Language.code.maxLength: "O tamanho de '{PropertyName}' deve ser de {MaxLength} caracteres ou menos. Você digitou {TotalLength} caracteres.", + Language.code.min: "'{PropertyName}' deve ser maior ou igual a {MinValue}. Você digitou {PropertyValue}.", + Language.code.minLength: "O tamanho de '{PropertyName}' deve ser de pelo menos {MinLength} caracteres. Você digitou {TotalLength} caracteres.", + Language.code.mustHaveLowercase: "'{PropertyName}' deve ter pelo menos uma letra minúscula.", + Language.code.mustHaveNumber: "'{PropertyName}' deve ter pelo menos um dígito ('0'-'9').", + Language.code.mustHaveSpecialCharacter: "'{PropertyName}' deve ter pelo menos um caractere não alfanumérico.", + Language.code.mustHaveUppercase: "'{PropertyName}' deve ter pelo menos uma letra maiúscula.", + Language.code.notEmpty: "'{PropertyName}' não pode estar vazio.", + Language.code.notEqualTo: "'{PropertyName}' não pode ser igual a '{ComparisonValue}'.", + Language.code.range: "'{PropertyName}' deve estar entre {From} e {To}. Você digitou {PropertyValue}.", + Language.code.validCEP: "'{PropertyName}' não é um CEP válido.", + Language.code.validCPF: "'{PropertyName}' não é um CPF válido.", + Language.code.validCNPJ: "'{PropertyName}' não é um CNPJ válido.", + Language.code.validCreditCard: "'{PropertyName}' não é um número de cartão de crédito válido.", + Language.code.validEmail: "'{PropertyName}' não é um endereço de e-mail válido.", + }); } diff --git a/lib/src/localization/languages/spanish_language.dart b/lib/src/localization/languages/spanish_language.dart new file mode 100644 index 0000000..82749fa --- /dev/null +++ b/lib/src/localization/languages/spanish_language.dart @@ -0,0 +1,30 @@ +import '../language.dart'; + +class SpanishLanguage extends Language { + SpanishLanguage() + : super({ + Language.code.equalTo: "'{PropertyName}' debe ser igual a '{ComparisonValue}'.", + Language.code.greaterThan: "'{PropertyName}' debe ser mayor que '{ComparisonValue}'.", + Language.code.isEmpty: "'{PropertyName}' debe estar vacío.", + Language.code.isNotNull: "'{PropertyName}' no puede ser nulo.", + Language.code.isNull: "'{PropertyName}' debe ser nulo.", + Language.code.lessThan: "'{PropertyName}' debe ser menor que '{ComparisonValue}'.", + Language.code.matchesPattern: "'{PropertyName}' no tiene el formato correcto.", + Language.code.max: "'{PropertyName}' debe ser menor o igual a {MaxValue}. Has ingresado {PropertyValue}.", + Language.code.maxLength: "El tamaño de '{PropertyName}' debe ser de {MaxLength} caracteres o menos. Has ingresado {TotalLength} caracteres.", + Language.code.min: "'{PropertyName}' debe ser mayor o igual a {MinValue}. Has ingresado {PropertyValue}.", + Language.code.minLength: "El tamaño de '{PropertyName}' debe ser de al menos {MinLength} caracteres. Has ingresado {TotalLength} caracteres.", + Language.code.mustHaveLowercase: "'{PropertyName}' debe tener al menos una letra minúscula.", + Language.code.mustHaveNumber: "'{PropertyName}' debe tener al menos un dígito ('0'-'9').", + Language.code.mustHaveSpecialCharacter: "'{PropertyName}' debe tener al menos un carácter no alfanumérico.", + Language.code.mustHaveUppercase: "'{PropertyName}' debe tener al menos una letra mayúscula.", + Language.code.notEmpty: "'{PropertyName}' no puede estar vacío.", + Language.code.notEqualTo: "'{PropertyName}' no puede ser igual a '{ComparisonValue}'.", + Language.code.range: "'{PropertyName}' debe estar entre {From} y {To}. Has ingresado {PropertyValue}.", + Language.code.validCEP: "'{PropertyName}' no es un CEP válido.", + Language.code.validCPF: "'{PropertyName}' no es un CPF válido.", + Language.code.validCNPJ: "'{PropertyName}' no es un CNPJ válido.", + Language.code.validCreditCard: "'{PropertyName}' no es un número de tarjeta de crédito válido.", + Language.code.validEmail: "'{PropertyName}' no es una dirección de correo electrónico válida.", + }); +} diff --git a/lib/src/localization/localization.dart b/lib/src/localization/localization.dart index 427c48c..9bce991 100644 --- a/lib/src/localization/localization.dart +++ b/lib/src/localization/localization.dart @@ -1,3 +1,4 @@ +export 'culture.dart'; export 'language.dart'; export 'language_manager.dart'; export 'languages/english_language.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 6f88ecf..0d9eb0b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "A Dart/Flutter package for building strongly typed validation rule repository: https://github.com/Flutterando/lucid_validation homepage: https://pub.dev/documentation/lucid_validation/latest/ -version: 0.0.7 +version: 1.0.0 environment: sdk: ">=3.0.0 <4.0.0" @@ -12,5 +12,5 @@ environment: dev_dependencies: - lints: ^3.0.0 - test: ^1.25.8 + lints: ">=3.0.0 <4.0.0" + test: ">=1.20.0 <2.0.0"