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