This commit is contained in:
Phuoc Nguyen
2025-10-17 17:22:28 +07:00
parent 2125e85d40
commit 628c81ce13
86 changed files with 31339 additions and 1710 deletions

View File

@@ -0,0 +1,540 @@
/// Form Validators for Vietnamese Locale
///
/// Provides validation utilities for forms with Vietnamese-specific
/// validations for phone numbers, email, passwords, etc.
library;
/// Form field validators
class Validators {
Validators._();
// ========================================================================
// Required Field Validators
// ========================================================================
/// Validate required field
static String? required(String? value, {String? fieldName}) {
if (value == null || value.trim().isEmpty) {
return fieldName != null
? '$fieldName là bắt buộc'
: 'Trường này là bắt buộc';
}
return null;
}
// ========================================================================
// Phone Number Validators
// ========================================================================
/// Validate Vietnamese phone number
///
/// Accepts formats:
/// - 0xxx xxx xxx (10 digits starting with 0)
/// - +84xxx xxx xxx (starts with +84)
/// - 84xxx xxx xxx (starts with 84)
static String? phone(String? value) {
if (value == null || value.trim().isEmpty) {
return 'Vui lòng nhập số điện thoại';
}
final cleaned = value.replaceAll(RegExp(r'\D'), '');
// Check if starts with valid Vietnamese mobile prefix
final vietnamesePattern = RegExp(r'^(0|\+?84)(3|5|7|8|9)[0-9]{8}$');
if (!vietnamesePattern.hasMatch(value.replaceAll(RegExp(r'[^\d+]'), ''))) {
return 'Số điện thoại không hợp lệ';
}
if (cleaned.length < 10 || cleaned.length > 11) {
return 'Số điện thoại phải có 10 chữ số';
}
return null;
}
/// Validate phone number (optional)
static String? phoneOptional(String? value) {
if (value == null || value.trim().isEmpty) {
return null; // Optional, so null is valid
}
return phone(value);
}
// ========================================================================
// Email Validators
// ========================================================================
/// Validate email address
static String? email(String? value) {
if (value == null || value.trim().isEmpty) {
return 'Vui lòng nhập email';
}
final emailRegex = RegExp(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
);
if (!emailRegex.hasMatch(value)) {
return 'Email không hợp lệ';
}
return null;
}
/// Validate email (optional)
static String? emailOptional(String? value) {
if (value == null || value.trim().isEmpty) {
return null;
}
return email(value);
}
// ========================================================================
// Password Validators
// ========================================================================
/// Validate password strength
///
/// Requirements:
/// - At least 8 characters
/// - At least 1 uppercase letter
/// - At least 1 lowercase letter
/// - At least 1 number
/// - At least 1 special character
static String? password(String? value) {
if (value == null || value.trim().isEmpty) {
return 'Vui lòng nhập mật khẩu';
}
if (value.length < 8) {
return 'Mật khẩu phải có ít nhất 8 ký tự';
}
if (!RegExp(r'[A-Z]').hasMatch(value)) {
return 'Mật khẩu phải có ít nhất 1 chữ hoa';
}
if (!RegExp(r'[a-z]').hasMatch(value)) {
return 'Mật khẩu phải có ít nhất 1 chữ thường';
}
if (!RegExp(r'[0-9]').hasMatch(value)) {
return 'Mật khẩu phải có ít nhất 1 số';
}
if (!RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(value)) {
return 'Mật khẩu phải có ít nhất 1 ký tự đặc biệt';
}
return null;
}
/// Validate password confirmation
static String? confirmPassword(String? value, String? password) {
if (value == null || value.trim().isEmpty) {
return 'Vui lòng xác nhận mật khẩu';
}
if (value != password) {
return 'Mật khẩu không khớp';
}
return null;
}
/// Simple password validator (minimum length only)
static String? passwordSimple(String? value, {int minLength = 6}) {
if (value == null || value.trim().isEmpty) {
return 'Vui lòng nhập mật khẩu';
}
if (value.length < minLength) {
return 'Mật khẩu phải có ít nhất $minLength ký tự';
}
return null;
}
// ========================================================================
// OTP Validators
// ========================================================================
/// Validate OTP code
static String? otp(String? value, {int length = 6}) {
if (value == null || value.trim().isEmpty) {
return 'Vui lòng nhập mã OTP';
}
if (value.length != length) {
return 'Mã OTP phải có $length chữ số';
}
if (!RegExp(r'^[0-9]+$').hasMatch(value)) {
return 'Mã OTP chỉ được chứa số';
}
return null;
}
// ========================================================================
// Text Length Validators
// ========================================================================
/// Validate minimum length
static String? minLength(String? value, int min, {String? fieldName}) {
if (value == null || value.trim().isEmpty) {
return fieldName != null
? '$fieldName là bắt buộc'
: 'Trường này là bắt buộc';
}
if (value.length < min) {
return fieldName != null
? '$fieldName phải có ít nhất $min ký tự'
: 'Phải có ít nhất $min ký tự';
}
return null;
}
/// Validate maximum length
static String? maxLength(String? value, int max, {String? fieldName}) {
if (value != null && value.length > max) {
return fieldName != null
? '$fieldName không được vượt quá $max ký tự'
: 'Không được vượt quá $max ký tự';
}
return null;
}
/// Validate length range
static String? lengthRange(
String? value,
int min,
int max, {
String? fieldName,
}) {
if (value == null || value.trim().isEmpty) {
return fieldName != null
? '$fieldName là bắt buộc'
: 'Trường này là bắt buộc';
}
if (value.length < min || value.length > max) {
return fieldName != null
? '$fieldName phải có từ $min đến $max ký tự'
: 'Phải có từ $min đến $max ký tự';
}
return null;
}
// ========================================================================
// Number Validators
// ========================================================================
/// Validate number
static String? number(String? value, {String? fieldName}) {
if (value == null || value.trim().isEmpty) {
return fieldName != null
? '$fieldName là bắt buộc'
: 'Trường này là bắt buộc';
}
if (double.tryParse(value) == null) {
return fieldName != null
? '$fieldName phải là số'
: 'Giá trị phải là số';
}
return null;
}
/// Validate integer
static String? integer(String? value, {String? fieldName}) {
if (value == null || value.trim().isEmpty) {
return fieldName != null
? '$fieldName là bắt buộc'
: 'Trường này là bắt buộc';
}
if (int.tryParse(value) == null) {
return fieldName != null
? '$fieldName phải là số nguyên'
: 'Giá trị phải là số nguyên';
}
return null;
}
/// Validate positive number
static String? positiveNumber(String? value, {String? fieldName}) {
final numberError = number(value, fieldName: fieldName);
if (numberError != null) return numberError;
final num = double.parse(value!);
if (num <= 0) {
return fieldName != null
? '$fieldName phải lớn hơn 0'
: 'Giá trị phải lớn hơn 0';
}
return null;
}
/// Validate number range
static String? numberRange(
String? value,
double min,
double max, {
String? fieldName,
}) {
final numberError = number(value, fieldName: fieldName);
if (numberError != null) return numberError;
final num = double.parse(value!);
if (num < min || num > max) {
return fieldName != null
? '$fieldName phải từ $min đến $max'
: 'Giá trị phải từ $min đến $max';
}
return null;
}
// ========================================================================
// Date Validators
// ========================================================================
/// Validate date format (dd/MM/yyyy)
static String? date(String? value) {
if (value == null || value.trim().isEmpty) {
return 'Vui lòng nhập ngày';
}
final dateRegex = RegExp(r'^\d{2}/\d{2}/\d{4}$');
if (!dateRegex.hasMatch(value)) {
return 'Định dạng ngày không hợp lệ (dd/MM/yyyy)';
}
try {
final parts = value.split('/');
final day = int.parse(parts[0]);
final month = int.parse(parts[1]);
final year = int.parse(parts[2]);
final date = DateTime(year, month, day);
if (date.day != day || date.month != month || date.year != year) {
return 'Ngày không hợp lệ';
}
} catch (e) {
return 'Ngày không hợp lệ';
}
return null;
}
/// Validate age (must be at least 18 years old)
static String? age(String? value, {int minAge = 18}) {
final dateError = date(value);
if (dateError != null) return dateError;
try {
final parts = value!.split('/');
final birthDate = DateTime(
int.parse(parts[2]),
int.parse(parts[1]),
int.parse(parts[0]),
);
final today = DateTime.now();
final age = today.year -
birthDate.year -
(today.month > birthDate.month ||
(today.month == birthDate.month && today.day >= birthDate.day)
? 0
: 1);
if (age < minAge) {
return 'Bạn phải từ $minAge tuổi trở lên';
}
return null;
} catch (e) {
return 'Ngày sinh không hợp lệ';
}
}
// ========================================================================
// Address Validators
// ========================================================================
/// Validate Vietnamese address
static String? address(String? value) {
if (value == null || value.trim().isEmpty) {
return 'Vui lòng nhập địa chỉ';
}
if (value.length < 10) {
return 'Địa chỉ quá ngắn';
}
return null;
}
// ========================================================================
// Tax ID Validators
// ========================================================================
/// Validate Vietnamese Tax ID (Mã số thuế)
/// Format: 10 or 13 digits
static String? taxId(String? value) {
if (value == null || value.trim().isEmpty) {
return 'Vui lòng nhập mã số thuế';
}
final cleaned = value.replaceAll(RegExp(r'\D'), '');
if (cleaned.length != 10 && cleaned.length != 13) {
return 'Mã số thuế phải có 10 hoặc 13 chữ số';
}
return null;
}
/// Validate tax ID (optional)
static String? taxIdOptional(String? value) {
if (value == null || value.trim().isEmpty) {
return null;
}
return taxId(value);
}
// ========================================================================
// URL Validators
// ========================================================================
/// Validate URL
static String? url(String? value) {
if (value == null || value.trim().isEmpty) {
return 'Vui lòng nhập URL';
}
final urlRegex = RegExp(
r'^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$',
);
if (!urlRegex.hasMatch(value)) {
return 'URL không hợp lệ';
}
return null;
}
// ========================================================================
// Combination Validators
// ========================================================================
/// Combine multiple validators
static String? Function(String?) combine(
List<String? Function(String?)> validators,
) {
return (String? value) {
for (final validator in validators) {
final error = validator(value);
if (error != null) return error;
}
return null;
};
}
// ========================================================================
// Custom Pattern Validators
// ========================================================================
/// Validate against custom regex pattern
static String? pattern(
String? value,
RegExp pattern,
String errorMessage,
) {
if (value == null || value.trim().isEmpty) {
return 'Trường này là bắt buộc';
}
if (!pattern.hasMatch(value)) {
return errorMessage;
}
return null;
}
// ========================================================================
// Match Validators
// ========================================================================
/// Validate that value matches another value
static String? match(String? value, String? matchValue, String fieldName) {
if (value == null || value.trim().isEmpty) {
return 'Vui lòng nhập $fieldName';
}
if (value != matchValue) {
return '$fieldName không khớp';
}
return null;
}
}
/// Password strength enum
enum PasswordStrength {
weak,
medium,
strong,
veryStrong,
}
/// Password strength calculator
class PasswordStrengthCalculator {
/// Calculate password strength
static PasswordStrength calculate(String password) {
if (password.isEmpty) return PasswordStrength.weak;
var score = 0;
// Length check
if (password.length >= 8) score++;
if (password.length >= 12) score++;
if (password.length >= 16) score++;
// Character variety check
if (RegExp(r'[a-z]').hasMatch(password)) score++;
if (RegExp(r'[A-Z]').hasMatch(password)) score++;
if (RegExp(r'[0-9]').hasMatch(password)) score++;
if (RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(password)) score++;
// Return strength based on score
if (score <= 2) return PasswordStrength.weak;
if (score <= 4) return PasswordStrength.medium;
if (score <= 6) return PasswordStrength.strong;
return PasswordStrength.veryStrong;
}
/// Get strength label in Vietnamese
static String getLabel(PasswordStrength strength) {
switch (strength) {
case PasswordStrength.weak:
return 'Yếu';
case PasswordStrength.medium:
return 'Trung bình';
case PasswordStrength.strong:
return 'Mạnh';
case PasswordStrength.veryStrong:
return 'Rất mạnh';
}
}
}