From 80d1be427ba5f4445d3b912f35d7d493dc053b35 Mon Sep 17 00:00:00 2001 From: Jacob Moura <jacobaraujo7@gmail.com> Date: Tue, 27 Aug 2024 14:32:51 -0300 Subject: [PATCH] update doc and example --- README.md | 50 +++++++++++++++ .../lib/domain/validations/extensions.dart | 5 +- .../register_param_validation.dart | 4 +- example/lib/main.dart | 62 ++++++++++++++++--- .../presentation/login_page/login_page.dart | 22 +++++++ example/pubspec.lock | 19 +++++- example/pubspec.yaml | 2 + lib/lucid_validation.dart | 6 +- lib/src/localization/culture.dart | 17 +++++ lib/src/localization/language.dart | 4 +- lib/src/localization/language_manager.dart | 31 ++++++---- .../languages/english_language.dart | 2 +- .../portuguese_brazillian_language.dart | 27 +++++++- lib/src/localization/localization.dart | 1 + 14 files changed, 217 insertions(+), 35 deletions(-) create mode 100644 lib/src/localization/culture.dart diff --git a/README.md b/README.md index 5193d0e..59bc9df 100644 --- a/README.md +++ b/README.md @@ -268,6 +268,56 @@ 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(); + +``` + + + ## 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<String> { } extension CustomValidPhoneValidator on SimpleValidationBuilder<String> { - SimpleValidationBuilder<String> customValidPhone(String message) { + SimpleValidationBuilder<String> 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/register_param_validation.dart b/example/lib/domain/validations/register_param_validation.dart index ccfaabf..6805edc 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<RegisterParamDto> { ruleFor((registerParamDto) => registerParamDto.confirmPassword, key: 'confirmPassword') // .customValidPassword() - .equalTo((registerParamDto) => registerParamDto.password, message: 'Password and confirm password must match'); + .equalTo((registerParamDto) => registerParamDto.password); 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..2f2d2da 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,22 +1,70 @@ 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'; void main() { 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<Locale>( + 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<Culture> { + const LucidLocalizationDelegate(); + + static final delegate = LucidLocalizationDelegate(); + + @override + bool isSupported(Locale locale) { + return LucidValidation.global.languageManager.isSupported( + locale.languageCode, + locale.countryCode, ); } + + @override + Future<Culture> load(Locale locale) async { + print(locale); + final culture = Culture(locale.languageCode, locale.countryCode ?? ''); + LucidValidation.global.culture = culture; + return culture; + } + + @override + bool shouldReload(LocalizationsDelegate<Culture> 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<LoginPage> { @override Widget build(BuildContext context) { return Scaffold( + appBar: AppBar( + title: const Text('Login'), + actions: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Is English'), + ValueListenableBuilder<Locale>( + 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/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 edc6b7b..6d76495 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', @@ -27,7 +25,7 @@ abstract class Language { validEmail: 'validEmail', ); - Language(this.culture, [Map<String, String> translations = const {}]) { + Language([Map<String, String> translations = const {}]) { _translations.addAll(translations); } diff --git a/lib/src/localization/language_manager.dart b/lib/src/localization/language_manager.dart index 2c463a7..53dcd2a 100644 --- a/lib/src/localization/language_manager.dart +++ b/lib/src/localization/language_manager.dart @@ -1,27 +1,34 @@ import '../../lucid_validation.dart'; import 'languages/portuguese_brazillian_language.dart'; -final _avaliableLanguages = <String, Language>{ - 'pt_BR': PortugueseBrasillianLanguage(), - 'pt': PortugueseBrasillianLanguage(), - 'en': EnglishLanguage(), - 'en_US': EnglishLanguage(), +final _avaliableLanguages = <Culture, Language>{ + Culture('pt', 'BR'): PortugueseBrasillianLanguage(), + Culture('pt'): PortugueseBrasillianLanguage(), + Culture('en'): EnglishLanguage(), + Culture('en', 'US'): EnglishLanguage(), }; abstract class LanguageManager { - final _globalTranslations = <String, Map<String, String>>{}; + final _globalTranslations = <Culture, Map<String, String>>{}; - 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<Culture> avaliableCultures() { + return _avaliableLanguages.keys.toList(); + } + + bool isSupported(String languageCode, String? countryCode) { + return _avaliableLanguages.containsKey(Culture(languageCode, countryCode ?? '')); + } + String translate(String key, {Map<String, String> 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 +38,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/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';