378 lines
11 KiB
Dart
378 lines
11 KiB
Dart
/// Data Formatters for Vietnamese Locale
|
|
///
|
|
/// Provides formatting utilities for currency, dates, phone numbers,
|
|
/// and other data types commonly used in the app.
|
|
library;
|
|
|
|
import 'package:intl/intl.dart';
|
|
|
|
/// Currency formatter for Vietnamese Dong (VND)
|
|
class CurrencyFormatter {
|
|
CurrencyFormatter._();
|
|
|
|
/// Format amount as Vietnamese currency (e.g., "100,000 ₫")
|
|
static String format(double amount, {bool showSymbol = true}) {
|
|
final formatter = NumberFormat.currency(
|
|
locale: 'vi_VN',
|
|
symbol: showSymbol ? '₫' : '',
|
|
decimalDigits: 0,
|
|
);
|
|
return formatter.format(amount);
|
|
}
|
|
|
|
/// Format amount with custom precision
|
|
static String formatWithDecimals(
|
|
double amount, {
|
|
int decimalDigits = 2,
|
|
bool showSymbol = true,
|
|
}) {
|
|
final formatter = NumberFormat.currency(
|
|
locale: 'vi_VN',
|
|
symbol: showSymbol ? '₫' : '',
|
|
decimalDigits: decimalDigits,
|
|
);
|
|
return formatter.format(amount);
|
|
}
|
|
|
|
/// Format as compact currency (e.g., "1.5M ₫")
|
|
static String formatCompact(double amount, {bool showSymbol = true}) {
|
|
final formatter = NumberFormat.compactCurrency(
|
|
locale: 'vi_VN',
|
|
symbol: showSymbol ? '₫' : '',
|
|
decimalDigits: 1,
|
|
);
|
|
return formatter.format(amount);
|
|
}
|
|
|
|
/// Parse currency string to double
|
|
static double? parse(String value) {
|
|
try {
|
|
// Remove currency symbol and spaces
|
|
final cleaned = value.replaceAll(RegExp(r'[₫\s,]'), '');
|
|
return double.tryParse(cleaned);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Date and time formatter
|
|
class DateFormatter {
|
|
DateFormatter._();
|
|
|
|
/// Format date as "dd/MM/yyyy" (Vietnamese format)
|
|
static String formatDate(DateTime date) {
|
|
final formatter = DateFormat('dd/MM/yyyy', 'vi_VN');
|
|
return formatter.format(date);
|
|
}
|
|
|
|
/// Format date as "dd-MM-yyyy"
|
|
static String formatDateDash(DateTime date) {
|
|
final formatter = DateFormat('dd-MM-yyyy', 'vi_VN');
|
|
return formatter.format(date);
|
|
}
|
|
|
|
/// Format time as "HH:mm"
|
|
static String formatTime(DateTime date) {
|
|
final formatter = DateFormat('HH:mm', 'vi_VN');
|
|
return formatter.format(date);
|
|
}
|
|
|
|
/// Format date and time as "dd/MM/yyyy HH:mm"
|
|
static String formatDateTime(DateTime date) {
|
|
final formatter = DateFormat('dd/MM/yyyy HH:mm', 'vi_VN');
|
|
return formatter.format(date);
|
|
}
|
|
|
|
/// Format date as "dd/MM/yyyy lúc HH:mm"
|
|
static String formatDateTimeVN(DateTime date) {
|
|
final formatter = DateFormat('dd/MM/yyyy', 'vi_VN');
|
|
final timeFormatter = DateFormat('HH:mm', 'vi_VN');
|
|
return '${formatter.format(date)} lúc ${timeFormatter.format(date)}';
|
|
}
|
|
|
|
/// Format as relative time (e.g., "2 giờ trước")
|
|
static String formatRelative(DateTime date) {
|
|
final now = DateTime.now();
|
|
final difference = now.difference(date);
|
|
|
|
if (difference.inSeconds < 60) {
|
|
return 'Vừa xong';
|
|
} else if (difference.inMinutes < 60) {
|
|
return '${difference.inMinutes} phút trước';
|
|
} else if (difference.inHours < 24) {
|
|
return '${difference.inHours} giờ trước';
|
|
} else if (difference.inDays < 7) {
|
|
return '${difference.inDays} ngày trước';
|
|
} else if (difference.inDays < 30) {
|
|
final weeks = (difference.inDays / 7).floor();
|
|
return '$weeks tuần trước';
|
|
} else if (difference.inDays < 365) {
|
|
final months = (difference.inDays / 30).floor();
|
|
return '$months tháng trước';
|
|
} else {
|
|
final years = (difference.inDays / 365).floor();
|
|
return '$years năm trước';
|
|
}
|
|
}
|
|
|
|
/// Format as day of week (e.g., "Thứ Hai")
|
|
static String formatDayOfWeek(DateTime date) {
|
|
final formatter = DateFormat('EEEE', 'vi_VN');
|
|
return formatter.format(date);
|
|
}
|
|
|
|
/// Format as month and year (e.g., "Tháng 10 năm 2024")
|
|
static String formatMonthYear(DateTime date) {
|
|
final formatter = DateFormat('MMMM yyyy', 'vi_VN');
|
|
return formatter.format(date);
|
|
}
|
|
|
|
/// Format as full date with day of week (e.g., "Thứ Hai, 17/10/2024")
|
|
static String formatFullDate(DateTime date) {
|
|
final dayOfWeek = formatDayOfWeek(date);
|
|
final dateStr = formatDate(date);
|
|
return '$dayOfWeek, $dateStr';
|
|
}
|
|
|
|
/// Parse date string in format "dd/MM/yyyy"
|
|
static DateTime? parseDate(String dateStr) {
|
|
try {
|
|
final formatter = DateFormat('dd/MM/yyyy');
|
|
return formatter.parse(dateStr);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Parse datetime string in format "dd/MM/yyyy HH:mm"
|
|
static DateTime? parseDateTime(String dateTimeStr) {
|
|
try {
|
|
final formatter = DateFormat('dd/MM/yyyy HH:mm');
|
|
return formatter.parse(dateTimeStr);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Phone number formatter for Vietnamese phone numbers
|
|
class PhoneFormatter {
|
|
PhoneFormatter._();
|
|
|
|
/// Format phone number as "(0xxx) xxx xxx"
|
|
static String format(String phone) {
|
|
// Remove all non-digit characters
|
|
final cleaned = phone.replaceAll(RegExp(r'\D'), '');
|
|
|
|
if (cleaned.isEmpty) return '';
|
|
|
|
// Handle Vietnamese phone number formats
|
|
if (cleaned.startsWith('84')) {
|
|
// +84 format
|
|
final local = cleaned.substring(2);
|
|
if (local.length >= 9) {
|
|
return '(+84${local.substring(0, 2)}) ${local.substring(2, 5)} ${local.substring(5)}';
|
|
}
|
|
} else if (cleaned.startsWith('0')) {
|
|
// 0xxx format
|
|
if (cleaned.length >= 10) {
|
|
return '(${cleaned.substring(0, 4)}) ${cleaned.substring(4, 7)} ${cleaned.substring(7)}';
|
|
}
|
|
}
|
|
|
|
return phone; // Return original if format doesn't match
|
|
}
|
|
|
|
/// Format as international number (+84xxx xxx xxx)
|
|
static String formatInternational(String phone) {
|
|
final cleaned = phone.replaceAll(RegExp(r'\D'), '');
|
|
|
|
if (cleaned.isEmpty) return '';
|
|
|
|
if (cleaned.startsWith('0')) {
|
|
// Convert 0xxx to +84xxx
|
|
final local = cleaned.substring(1);
|
|
if (local.length >= 9) {
|
|
return '+84${local.substring(0, 2)} ${local.substring(2, 5)} ${local.substring(5)}';
|
|
}
|
|
} else if (cleaned.startsWith('84')) {
|
|
final local = cleaned.substring(2);
|
|
if (local.length >= 9) {
|
|
return '+84${local.substring(0, 2)} ${local.substring(2, 5)} ${local.substring(5)}';
|
|
}
|
|
}
|
|
|
|
return phone;
|
|
}
|
|
|
|
/// Remove formatting to get clean phone number
|
|
static String clean(String phone) {
|
|
return phone.replaceAll(RegExp(r'\D'), '');
|
|
}
|
|
|
|
/// Convert to E.164 format (+84xxxxxxxxx)
|
|
static String toE164(String phone) {
|
|
final cleaned = clean(phone);
|
|
|
|
if (cleaned.startsWith('0')) {
|
|
return '+84${cleaned.substring(1)}';
|
|
} else if (cleaned.startsWith('84')) {
|
|
return '+$cleaned';
|
|
} else if (cleaned.startsWith('+84')) {
|
|
return cleaned;
|
|
}
|
|
|
|
return '+84$cleaned';
|
|
}
|
|
|
|
/// Mask phone number (e.g., "0xxx xxx ***")
|
|
static String mask(String phone) {
|
|
final cleaned = clean(phone);
|
|
|
|
if (cleaned.length >= 10) {
|
|
return '${cleaned.substring(0, 4)} ${cleaned.substring(4, 7)} ***';
|
|
}
|
|
|
|
return phone;
|
|
}
|
|
}
|
|
|
|
/// Number formatter
|
|
class NumberFormatter {
|
|
NumberFormatter._();
|
|
|
|
/// Format number with thousand separators
|
|
static String format(num number, {int decimalDigits = 0}) {
|
|
final formatter = NumberFormat('#,###', 'vi_VN');
|
|
if (decimalDigits > 0) {
|
|
return formatter.format(number);
|
|
}
|
|
return formatter.format(number.round());
|
|
}
|
|
|
|
/// Format as percentage
|
|
static String formatPercentage(
|
|
double value, {
|
|
int decimalDigits = 0,
|
|
bool showSymbol = true,
|
|
}) {
|
|
final formatter = NumberFormat.percentPattern('vi_VN');
|
|
formatter.maximumFractionDigits = decimalDigits;
|
|
formatter.minimumFractionDigits = decimalDigits;
|
|
|
|
final result = formatter.format(value / 100);
|
|
return showSymbol ? result : result.replaceAll('%', '');
|
|
}
|
|
|
|
/// Format as compact number (e.g., "1.5K")
|
|
static String formatCompact(num number) {
|
|
final formatter = NumberFormat.compact(locale: 'vi_VN');
|
|
return formatter.format(number);
|
|
}
|
|
|
|
/// Format file size
|
|
static String formatBytes(int bytes, {int decimals = 2}) {
|
|
if (bytes <= 0) return '0 B';
|
|
|
|
const suffixes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
final i = (bytes.bitLength - 1) ~/ 10;
|
|
final value = bytes / (1 << (i * 10));
|
|
|
|
return '${value.toStringAsFixed(decimals)} ${suffixes[i]}';
|
|
}
|
|
|
|
/// Format duration (e.g., "1:30:45")
|
|
static String formatDuration(Duration duration) {
|
|
final hours = duration.inHours;
|
|
final minutes = duration.inMinutes.remainder(60);
|
|
final seconds = duration.inSeconds.remainder(60);
|
|
|
|
if (hours > 0) {
|
|
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
|
} else {
|
|
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Text formatter utilities
|
|
class TextFormatter {
|
|
TextFormatter._();
|
|
|
|
/// Capitalize first letter
|
|
static String capitalize(String text) {
|
|
if (text.isEmpty) return text;
|
|
return text[0].toUpperCase() + text.substring(1);
|
|
}
|
|
|
|
/// Capitalize each word
|
|
static String capitalizeWords(String text) {
|
|
if (text.isEmpty) return text;
|
|
return text.split(' ').map((word) => capitalize(word)).join(' ');
|
|
}
|
|
|
|
/// Truncate text with ellipsis
|
|
static String truncate(
|
|
String text,
|
|
int maxLength, {
|
|
String ellipsis = '...',
|
|
}) {
|
|
if (text.length <= maxLength) return text;
|
|
return text.substring(0, maxLength - ellipsis.length) + ellipsis;
|
|
}
|
|
|
|
/// Remove diacritics from Vietnamese text
|
|
static String removeDiacritics(String text) {
|
|
const withDiacritics =
|
|
'àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ';
|
|
const withoutDiacritics =
|
|
'aaaaaaaaaaaaaaaaaeeeeeeeeeeeiiiiioooooooooooooooooouuuuuuuuuuuyyyyyd';
|
|
|
|
var result = text.toLowerCase();
|
|
for (var i = 0; i < withDiacritics.length; i++) {
|
|
result = result.replaceAll(withDiacritics[i], withoutDiacritics[i]);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// Create URL-friendly slug
|
|
static String slugify(String text) {
|
|
var slug = removeDiacritics(text);
|
|
slug = slug.toLowerCase();
|
|
slug = slug.replaceAll(RegExp(r'[^\w\s-]'), '');
|
|
slug = slug.replaceAll(RegExp(r'[-\s]+'), '-');
|
|
return slug;
|
|
}
|
|
}
|
|
|
|
/// Loyalty tier formatter
|
|
class LoyaltyFormatter {
|
|
LoyaltyFormatter._();
|
|
|
|
/// Format tier name in Vietnamese
|
|
static String formatTier(String tier) {
|
|
switch (tier.toLowerCase()) {
|
|
case 'diamond':
|
|
return 'Kim Cương';
|
|
case 'platinum':
|
|
return 'Bạch Kim';
|
|
case 'gold':
|
|
return 'Vàng';
|
|
default:
|
|
return tier;
|
|
}
|
|
}
|
|
|
|
/// Format points with label
|
|
static String formatPoints(int points) {
|
|
return '${NumberFormatter.format(points)} điểm';
|
|
}
|
|
|
|
/// Format points progress (e.g., "1,200 / 5,000 điểm")
|
|
static String formatPointsProgress(int current, int target) {
|
|
return '${NumberFormatter.format(current)} / ${NumberFormatter.format(target)} điểm';
|
|
}
|
|
}
|