AmberDB

 view release on metacpan or  search on metacpan

lib/AmberDB/Locale.pm  view on Meta::CPAN

        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

AmberDB::Locale - Multilingual text processing, collation, number/currency formatting, and search normalization engine

=head1 SYNOPSIS

  # =========================================================================
  # 1. DIRECT USAGE VIA AMBERDB INSTANCE ($adb inherits AmberDB::Locale):
  # Reads active language from config (default is 'gb' or configured language)
  # =========================================================================
  my $adb = AmberDB->new(cfg => { language => "gb" });

lib/AmberDB/Locale.pm  view on Meta::CPAN

  # German rules:
  $de->num2text(1000);    # "Eins Tausend EUR"

Accepts Eastern Arabic (C<٠١٢٣٤٥٦٧٨٩>) and Persian (C<۰۱۲۳۴۵۶۷۸۹>) digits automatically.

Options:

=over 4

=item * C<currency =E<gt> { main =E<gt> "EUR", sub =E<gt> "cent" }>: Custom currency labels.

  $tr->num2text(99.99, currency => { main => "EUR", sub => "cent" });
  # => "Doksan Dokuz EUR Doksan Dokuz cent"

=item * C<numbers =E<gt> \%custom_hash>: Overrides number word definitions with custom dictionaries.

=back

=head3 format_number($number [, %options])

Formats C<$number> with locale-specific decimal and thousand grouping separators.

  # Turkish conventions (group: dot, decimal: comma)
  $tr->format_number(1234567.89);                # "1.234.567,89"
  $tr->format_number(1234567.89, decimals => 0); # "1.234.568"
  $tr->format_number(1234567.89, decimals => 3); # "1.234.567,890"

  # German conventions (group: dot, decimal: comma)
  $de->format_number(1234567.89);                # "1.234.567,89"

  # English conventions (group: comma, decimal: dot)
  my $en = AmberDB::Locale->new(language => "en");
  $en->format_number(1234567.89);                # "1,234,567.89"

  # French conventions (group: space, decimal: comma)
  my $fr = AmberDB::Locale->new(language => "fr");
  $fr->format_number(1234567.89);                # "1 234 567,89"

Available options: C<decimals>, C<decimal_sep>, C<group_sep>.

=head3 format_currency($amount [, $currency_code | %options])

Formats monetary amounts using locale conventions or specific ISO 4217 currency settings.

  # Default Turkish currency (TRY)
  $tr->format_currency(1234.50);                    # "₺1.234,50"

  # Explicit ISO code
  $tr->format_currency(1234.50, 'EUR');             # "1.234,50 €"
  $tr->format_currency(1234.50, currency => 'USD'); # "$1.234,50"

Custom formatting overrides:

  $tr->format_currency(100, symbol => 'TL', position => 'suffix', space => 1);
  # => "100,00 TL"

=head3 ISO 4217 Currency Dictionary

C<AmberDB::Locale> integrates a master dictionary of ISO 4217 currency definitions, numeric codes, currency symbols, and default subunit decimal precision (implemented internally via C<AmberDB::Locale::Currency>).

Direct dictionary lookups, symbol conversions, and select dropdown lists can be accessed via:

  use AmberDB::Locale::Currency;

  # Symbol and name lookups
  my $sym  = AmberDB::Locale::Currency->symbol('TRY'); # '₺'
  my $name = AmberDB::Locale::Currency->name('USD');   # 'US Dollar'
  my $info = AmberDB::Locale::Currency->by_code('EUR');
  # => { num => '978', name => 'Euro', symbol => '€', digits => 2 }

  # Dropdown options for UI forms
  my @options = AmberDB::Locale::Currency->all();
  # => ( [ 'TRY', 'Turkish Lira' ], [ 'USD', 'US Dollar' ], ... )

  # List active ISO codes
  my @codes = AmberDB::Locale::Currency->active_codes();
  # => ('TRY', 'USD', 'EUR', 'GBP', 'RUB', 'AZN', 'SAR', 'JPY', 'CHF', 'CAD', 'AUD', 'CNY')

Supported helper methods:

=over 4

=item * C<AmberDB::Locale::Currency-E<gt>by_code($iso_code)> - Returns the currency definition hash reference for the given 3-letter ISO 4217 code (case-insensitive), containing C<num>, C<name>, C<symbol>, and C<digits>.

=item * C<AmberDB::Locale::Currency-E<gt>symbol($iso_code)> - Returns the currency symbol for the given ISO code (e.g. C<'₺'>, C<'$'>, C<'€'>, C<'£'>, C<'₽'>, C<'¥'>). If the code is unknown, returns the uppercase code itself.

=item * C<AmberDB::Locale::Currency-E<gt>name($iso_code)> - Returns the English currency name for the given ISO code.

=item * C<AmberDB::Locale::Currency-E<gt>all()> - Returns a list of 2-element array references C<[ $code, $name ]> ordered by priority, suitable for rendering HTML C<E<lt>selectE<gt>> form dropdowns.

=item * C<AmberDB::Locale::Currency-E<gt>active_codes()> - Returns the list of active 3-letter ISO 4217 currency codes supported by the dictionary.

=back

=head2 Date & Time Operations

=head3 format_date($time_or_string [, $pattern_or_style])

Formats a Unix epoch timestamp or ISO date string into a localized date/time representation.

  my $epoch = 1787832600; # 2026-08-28 14:30:00

  # Standard styles:
  $tr->format_date($epoch);             # "28.08.2026" (short, default)
  $tr->format_date($epoch, 'medium');   # "28 AÄŸu 2026"
  $tr->format_date($epoch, 'long');     # "28 AÄŸustos 2026"
  $tr->format_date($epoch, 'full');     # "Cuma, 28 AÄŸustos 2026"
  $tr->format_date($epoch, 'time');     # "14:30"
  $tr->format_date($epoch, 'datetime'); # "28.08.2026 14:30"

  # Custom format pattern tokens:
  $tr->format_date($epoch, 'YYYY-MM-DD'); # "2026-08-28"
  $tr->format_date($epoch, 'DD/MM/YYYY'); # "28/08/2026"

  # Input can also be ISO date strings:
  $tr->format_date("2026-08-28", 'full'); # "Cuma, 28 AÄŸustos 2026"

Supported pattern tokens:

=over 4

=item * C<YYYY>, C<YY> - 4-digit / 2-digit year

=item * C<MMMM>, C<MMM>, C<MM>, C<M> - Full month name, short month, 2-digit month, 1-digit month

=item * C<DD>, C<D> - 2-digit day, 1-digit day

=item * C<dddd>, C<ddd> - Full day name, short day name

=item * C<HH>, C<H> - Hour (2-digit / 1-digit)

=item * C<mm>, C<m> - Minute (2-digit / 1-digit)

=item * C<ss>, C<s> - Second (2-digit / 1-digit)

=back

=head3 parse_date($string [, %options])

Parses a localized date string (e.g. C<"28.08.2026"> or C<"2026-08-28 14:30:00">) back into a Unix epoch timestamp or component hash.

  my $epoch = $tr->parse_date("28.08.2026"); # Unix timestamp

  my $hash = $tr->parse_date("28.08.2026", hash => 1);
  # => { year => 2026, month => 8, day => 28, hour => 0, minute => 0, second => 0 }

=head2 HTML Entity Decoding

=head3 decode_entities($string)



( run in 0.642 second using v1.01-cache-2.11-cpan-e623d60df62 )