AmberDB
view release on metacpan or search on metacpan
lib/AmberDB/Locale.pm view on Meta::CPAN
#
# USAGE:
# # New API (explicit language):
# my $lang = AmberDB::Locale->new(language => "gb");
# my $lang = AmberDB::Locale->new(language => "tr");
#
# # With AmberDB engine (language from cfg, defaults to "gb"):
# AmberDB->new(cfg => { language => "gb" });
# # then $self->uc($str) works on the inherited object
# -------------------------------------------------------
# -------------------------------------------------------
# Constructor
# -------------------------------------------------------
sub new {
my $class = shift;
my $lang;
# Handle calling conventions:
# 1. new(language => "gb") â named-param API
# 2. new({ language => "gb" }) â unblessed hash ref API
# 3. new("gb") â positional string API
# 4. new() â no args; use default ("gb")
if ( @_ ) {
if ( @_ == 1 && !ref( $_[0] ) ) {
$lang = $_[0];
}
elsif ( @_ == 1 && ref( $_[0] ) eq 'HASH' ) {
$lang = $_[0]->{language} || $_[0]->{lang};
}
elsif ( @_ % 2 == 0 && !ref( $_[0] ) ) {
my %args = @_;
$lang = $args{language} || $args{lang};
}
}
my %LANG_ALIAS = (
'turkish' => 'tr',
'tr_tr' => 'tr',
'tr-tr' => 'tr',
'english' => 'en',
'en_us' => 'en',
'en_gb' => 'gb',
'german' => 'de',
'de_de' => 'de',
'french' => 'fr',
'fr_fr' => 'fr',
'spanish' => 'es',
'es_es' => 'es',
'russian' => 'ru',
'ru_ru' => 'ru',
'arabic' => 'ar',
'ar_sa' => 'ar',
'azerbaijani' => 'az',
'az_az' => 'az',
'japanese' => 'ja',
'ja_jp' => 'ja',
'ja-jp' => 'ja',
'global' => 'gb',
'gb' => 'gb',
'gl' => 'gb',
'universal' => 'gb',
'uni' => 'gb',
'gb_base' => 'gb',
);
$lang = lc( $lang // '' );
$lang =~ s/[^\w-]//g;
my $norm_lang = $LANG_ALIAS{$lang} || ( split /[_-]/, $lang )[0] || 'gb';
# Fast path: instance cache lookup (keyed by class + normalized language)
my $cache_key = "${class}::${norm_lang}";
return $LOCALE_CACHE{$cache_key} if $LOCALE_CACHE{$cache_key};
my $self = bless { _lang => $norm_lang }, $class;
$self->_load_locale($norm_lang);
if ( $self->{_lang} ) {
$LOCALE_CACHE{$cache_key} = $self;
$LOCALE_CACHE{"${class}::$self->{_lang}"} = $self;
}
return $self;
}
# -------------------------------------------------------
# Load locale data from AmberDB::Locale::Lang::<lang>
# Falls back to 'gb' if requested locale not found.
# -------------------------------------------------------
sub _load_locale {
my ( $self, $lang ) = @_;
$lang ||= 'gb';
$self->{_lang} = $lang;
my $locale_class = "AmberDB::Locale::Lang::$lang";
my $loaded = eval {
( my $path = $locale_class ) =~ s|::|/|g;
require "$path.pm";
1;
};
unless ($loaded) {
if ( $lang ne 'gb' ) {
unless ( $WARNED_LOCALES{$lang}++ ) {
cluck "AmberDB::Locale: locale '$lang' not found, falling back to 'gb'\n";
}
$locale_class = 'AmberDB::Locale::Lang::gb';
require AmberDB::Locale::Lang::gb;
$self->{_lang} = 'gb';
}
}
$self->{_locale} = $locale_class->data();
# Initialize Unicode::Collate::Locale for UCA standard collation
eval {
require Unicode::Collate::Locale;
$self->{_collator} = Unicode::Collate::Locale->new( locale => $lang );
lib/AmberDB/Locale.pm view on Meta::CPAN
my ( $day, $month, $year, $hour, $min, $sec ) = ( 0, 0, 0, 0, 0, 0 );
if ( $str =~ /^(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{4})(?:\s+(\d{1,2}):(\d{1,2}):?(\d{1,2})?)?/ ) {
my ( $p1, $p2, $p3 ) = ( int($1), int($2), int($3) );
$hour = int( $4 // 0 );
$min = int( $5 // 0 );
$sec = int( $6 // 0 );
my $fmt = $self->{_locale}{date_format}{short} || 'DD.MM.YYYY';
if ( $fmt =~ /^MM/i ) {
$month = $p1;
$day = $p2;
$year = $p3;
}
else {
$day = $p1;
$month = $p2;
$year = $p3;
}
}
elsif ( $str =~ /^(\d{4})[.\/-](\d{1,2})[.\/-](\d{1,2})(?:\s+(\d{1,2}):(\d{1,2}):?(\d{1,2})?)?/ ) {
$year = int($1);
$month = int($2);
$day = int($3);
$hour = int( $4 // 0 );
$min = int( $5 // 0 );
$sec = int( $6 // 0 );
}
return unless $year && $month && $day;
if ( $opts{hash} ) {
return {
year => $year,
month => $month,
day => $day,
hour => $hour,
minute => $min,
second => $sec,
};
}
require Time::Local;
return eval { Time::Local::timelocal( $sec, $min, $hour, $day, $month - 1, $year ) };
}
# Private helper: securely evaluate sanitized CLDR plural expressions
sub _eval_plural_rule {
my ( $self, $cond, $count ) = @_;
return 0 unless defined $cond && length $cond;
my $n = abs( $count // 0 );
# Whitelist strictly only digits, 'n', whitespace, arithmetic and logical operators
return 0 unless $cond =~ /^[n0-9+\-*\/%&|!=<>()\s]+$/;
# Replace 'n' variable token with actual numeric value
( my $expr = $cond ) =~ s/\bn\b/$n/g;
# Safely evaluate numeric expression isolated from global DIE handlers
my $res = eval {
local $SIG{__DIE__} = sub {};
eval $expr; ## no critic
};
return $res ? 1 : 0;
}
# -------------------------------------------------------
# Evaluate CLDR plural rule and select template.
#
# my $text = $lang->plural(1, { one => "{count} ürün", other => "{count} ürün" });
# my $text = $lang->plural(5, { one => "{count} item", other => "{count} items" });
# -------------------------------------------------------
sub plural {
my ( $self, $count, $forms ) = @_;
return '' unless defined $forms;
$count //= 0;
my $form_key = 'other';
my $rule = $self->{_locale}{plural_rule} || 'one{n==1}other';
for my $key (qw(zero one two few many)) {
if ( $rule =~ /\b$key\{([^{}]+)\}/ ) {
my $cond = $1;
if ( $self->_eval_plural_rule( $cond, $count ) ) {
$form_key = $key;
last;
}
}
}
my $template;
if ( ref($forms) eq 'HASH' ) {
$template = $forms->{$form_key} // $forms->{other} // $forms->{one} // '';
}
else {
$template = "$forms";
}
my $fmt_count = $self->format_number($count, decimals => 0);
$template =~ s/\{count\}|\{n\}/$fmt_count/g;
return $template;
}
# -------------------------------------------------------
# Language tag accessor
# my $tag = $lang->language; # "tr", "en", "de" ...
# -------------------------------------------------------
sub language { return $_[0]->{_lang} }
1;
__END__
=encoding utf8
=head1 NAME
lib/AmberDB/Locale.pm view on Meta::CPAN
# Russian: 4 forms (one, few, many, other)
my %ru_apple = (
one => "{count} Ñблоко",
few => "{count} Ñблока",
many => "{count} Ñблок",
other => "{count} Ñблока"
);
$ru->plural(1, \%ru_apple); # "1 Ñблоко" (ends in 1, except 11)
$ru->plural(3, \%ru_apple); # "3 Ñблока" (ends in 2-4, except 12-14)
$ru->plural(5, \%ru_apple); # "5 Ñблок" (ends in 5-9, 0, or 11-14)
$ru->plural(21, \%ru_apple); # "21 Ñблоко"
=head1 DESCRIPTION
C<AmberDB::Locale> is a comprehensive, high-performance, locale-aware text processing engine designed for multilingual Perl applications. It provides a unified API for:
=over 4
=item * Locale-aware case conversion (e.g., Turkish C<I/ı>, C<İ/i>, German C<à -E<gt> SS>).
=item * 100% standard Unicode Collation Algorithm (UCA) sorting via C<Unicode::Collate::Locale>.
=item * ASCII transliteration and URL slug generation with language-specific rules (e.g. German DIN 5007-2 C<ü -E<gt> ue> vs. Turkish C<ü -E<gt> u>).
=item * Written number and cheque printing conversion supporting integer, decimal, negative numbers, and Eastern Arabic/Persian numerals.
=item * Precision number and ISO 4217 currency formatting with customizable decimal and group separators.
=item * Bidirectional date/time formatting with full pattern tokens (C<YYYY>, C<MMMM>, C<dddd>, C<HH:mm:ss>) and parsing.
=item * Unicode NFKC case-folding and phonetic search token normalization for high-performance inverted indexes.
=item * CLDR-standard plural form evaluation across Western, Slavic, and Eastern languages.
=item * UTF-8 character-safe substring extraction preventing multi-byte corruption.
=back
Language-specific datasets and rule tables are decoupled into modular packages (e.g. C<AmberDB::Locale::Lang::gb>, C<AmberDB::Locale::Lang::tr>, C<AmberDB::Locale::Lang::de>, C<AmberDB::Locale::Lang::en>, C<AmberDB::Locale::Lang::ru>, C<AmberDB::Loca...
B<Inheritance Note:> C<AmberDB> inherits from C<AmberDB::Locale> via C<use parent>. When an C<AmberDB> instance is constructed, it automatically initializes its locale subsystem from C<$adb-E<gt>config('language')>. All methods documented below can b...
=head1 CONSTRUCTOR
=head2 new([%options | $hashref | $language_code])
Creates and returns an C<AmberDB::Locale> instance configured for the specified language. Instances are cached internally for high-throughput reuse.
# 1. Named-parameter API (recommended)
my $lang = AmberDB::Locale->new(language => 'tr');
# 2. Hashref API
my $lang = AmberDB::Locale->new({ language => 'de' });
# 3. Positional string API
my $lang = AmberDB::Locale->new('fr');
# 4. Default (falls back to Global Base 'gb')
my $lang = AmberDB::Locale->new();
Supported language codes include C<"gb"> (Global Base - default), C<"tr">, C<"en">, C<"de">, C<"fr">, C<"es">, C<"ru">, C<"az">, C<"ar">, C<"ja"> and common aliases (such as C<"global">, C<"turkish">, C<"tr_tr">, C<"english">, C<"german">, etc.).
If an unsupported language is specified, a warning is issued and the instance falls back to C<"gb">.
=head1 METHODS
=head2 Case Conversions & Comparison
=head3 uc($string)
Converts C<$string> to uppercase according to locale-specific casing rules.
# Turkish dotted/dotless I handling:
$tr->uc("ıÄdır"); # "IÄDIR"
$tr->uc("istanbul"); # "İSTANBUL" (Turkish i -> İ)
# English standard casing:
$en->uc("istanbul"); # "ISTANBUL" (English i -> I)
# German sharp S:
$de->uc("straÃe"); # "STRASSE" (German à -> SS)
=head3 lc($string)
Converts C<$string> to lowercase according to locale-specific casing rules.
# Turkish dotted/dotless I handling:
$tr->lc("İSTANBUL"); # "istanbul" (Turkish İ -> i)
$tr->lc("IÄDIR"); # "ıÄdır" (Turkish I -> ı)
# English standard casing:
$en->lc("IÄDIR"); # "iÄdır" (English I -> i)
=head3 ucfirst($string)
Capitalizes the first letter of each word in C<$string> under locale rules. The string is first lowercased, and then the first character following word-starting delimiters (spaces, punctuation, brackets) is uppercased according to locale rules.
$tr->ucfirst("istanbul büyükÅehir belediyesi");
# => "İstanbul BüyükÅehir Belediyesi"
$tr->ucfirst("ahmet (ısparta) - izmir");
# => "Ahmet (Isparta) - İzmir"
=head3 fold($string)
Applies Unicode NFKC normalization and locale lowercasing to produce a case-folded string suitable for search indexing and matching.
my $key = $tr->fold("İSTANBUL"); # "istanbul"
=head3 ieq($str1, $str2)
Performs a locale-aware, case-insensitive comparison between C<$str1> and C<$str2>. Returns C<1> if they are equal under locale rules, C<0> otherwise.
$tr->ieq("İstanbul", "istanbul"); # 1 (true)
$tr->ieq("IÄDIR", "ıÄdır"); # 1 (true)
$tr->ieq("Ankara", "İzmir"); # 0 (false)
=head2 Sorting
=head3 sort(\@list [, $field_or_index])
Sorts an array reference C<\@list> using the Unicode Collation Algorithm (UCA) tailored for the active locale.
( run in 1.798 second using v1.01-cache-2.11-cpan-4716db65a44 )