NAME

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

SYNOPSIS

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

# Case Conversions & Comparison
my $upper  = $adb->uc("ığdır");                         # "IĞDIR"
my $lower  = $adb->lc("İSTANBUL");                      # "istanbul"
my $title  = $adb->ucfirst("istanbul büyükşehir");      # "İstanbul Büyükşehir"
my $folded = $adb->fold("İSTANBUL");                    # "istanbul"
my $same   = $adb->ieq("İstanbul", "istanbul");         # 1

# Unicode Collation Algorithm (UCA) Sorting
my @sorted = $adb->sort(["İzmir", "Ankara", "Van", "Şanlıurfa", "Bursa", "Çanakkale"]);
# => ("Ankara", "Bursa", "Çanakkale", "İzmir", "Şanlıurfa", "Van")

# Text Normalization & Transliteration (Turkish rules: ü -> u, ç -> c)
my $clean = $adb->normalize("<p>Kâr &amp; zarar &ccedil;izelgesi</p>"); # "Kar zarar cizelgesi"
my $ascii = $adb->to_ascii("müller");                   # "muller"   (Turkish: ü -> u)
my $slug  = $adb->to_ascii("İstanbul Kâr & Zarar!", 1); # "istanbul_kar_zarar"

# UTF-8 Safe Substring (character-based, safe for multibyte chars)
my $sub = $adb->substring("Çanakkale", 0, 4);           # "Çana"

# Number to Written Text (Invoices / Cheques)
my $text = $adb->num2text(1234.56);
# => "Bin İki Yüz Otuz Dört TL Elli Altı KR" (Note: "Bin", not "Bir Bin")

# Number & Currency Formatting
my $num  = $adb->format_number(1234567.89);            # "1.234.567,89"
my $curr = $adb->format_currency(1234.50, "EUR");       # "1.234,50 €"

# Date Formatting & Parsing
my $date = $adb->format_date(time(), "full");          # "Cuma, 28 Ağustos 2026"
my $ep   = $adb->parse_date("28.08.2026");             # Unix timestamp

# Pluralization (CLDR)
my $msg  = $adb->plural(5, { one => "{count} ürün", other => "{count} ürün" });

# Search Token Normalization
my $norm = $adb->normalize_word("Türkiye'de", 1);      # "turkiye turkiyede"

# =========================================================================
# 2. CROSS-LANGUAGE COMPARISON & STANDALONE USAGE:
# =========================================================================
use AmberDB::Locale;

my $tr = AmberDB::Locale->new(language => "tr");
my $de = AmberDB::Locale->new(language => "de");
my $en = AmberDB::Locale->new(language => "en");
my $fr = AmberDB::Locale->new(language => "fr");
my $ru = AmberDB::Locale->new(language => "ru");

# --- A. ASCII Transliteration Differences (to_ascii) ---
$tr->to_ascii("müller");          # "muller"    (Turkish rule: ü -> u)
$de->to_ascii("müller");          # "mueller"   (German DIN 5007 rule: ü -> ue)
$de->to_ascii("Große Straße");    # "Grosse Strasse" (ß -> ss)
$tr->to_ascii("çarşı");           # "carsi"     (ç -> c, ş -> s, ı -> i)
$fr->to_ascii("façade Noël");     # "facade Noel"

# --- B. Case Conversion Differences (uc / lc) ---
$tr->uc("istanbul");              # "İSTANBUL"  (Turkish dotted i -> İ)
$en->uc("istanbul");              # "ISTANBUL"  (Standard English i -> I)
$tr->lc("IĞDIR");                 # "ığdır"     (Turkish dotless I -> ı)
$en->lc("IĞDIR");                 # "iğdır"     (Standard English I -> i)
$de->uc("weiß");                  # "WEISS"     (German ß -> SS)

# --- C. Number Formatting Differences (format_number) ---
$tr->format_number(1234567.89);   # "1.234.567,89" (Group: dot, Dec: comma)
$de->format_number(1234567.89);   # "1.234.567,89" (Group: dot, Dec: comma)
$en->format_number(1234567.89);   # "1,234,567.89" (Group: comma, Dec: dot)
$fr->format_number(1234567.89);   # "1 234 567,89" (Group: space, Dec: comma)

# --- D. Written Number Differences (num2text) ---
$tr->num2text(1000);              # "Bin TL"           (Turkish: "Bin", no "Bir" prefix)
$en->num2text(1000);              # "One Thousand USD" (English: requires "One" prefix)
$de->num2text(1000);              # "Eins Tausend EUR" (German: requires "Eins" prefix)

# --- E. Pluralization Differences (plural) ---
# English: 2 forms (one, other)
$en->plural(1, { one => "{count} item", other => "{count} items" }); # "1 item"
$en->plural(5, { one => "{count} item", other => "{count} items" }); # "5 items"

# 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 яблоко"

DESCRIPTION

AmberDB::Locale is a comprehensive, high-performance, locale-aware text processing engine designed for multilingual Perl applications. It provides a unified API for:

  • Locale-aware case conversion (e.g., Turkish I/ı, İ/i, German ß -> SS).

  • 100% standard Unicode Collation Algorithm (UCA) sorting via Unicode::Collate::Locale.

  • ASCII transliteration and URL slug generation with language-specific rules (e.g. German DIN 5007-2 ü -> ue vs. Turkish ü -> u).

  • Written number and cheque printing conversion supporting integer, decimal, negative numbers, and Eastern Arabic/Persian numerals.

  • Precision number and ISO 4217 currency formatting with customizable decimal and group separators.

  • Bidirectional date/time formatting with full pattern tokens (YYYY, MMMM, dddd, HH:mm:ss) and parsing.

  • Unicode NFKC case-folding and phonetic search token normalization for high-performance inverted indexes.

  • CLDR-standard plural form evaluation across Western, Slavic, and Eastern languages.

  • UTF-8 character-safe substring extraction preventing multi-byte corruption.

Language-specific datasets and rule tables are decoupled into modular packages (e.g. AmberDB::Locale::Lang::tr, AmberDB::Locale::Lang::de, AmberDB::Locale::Lang::en, AmberDB::Locale::Lang::ru, AmberDB::Locale::Lang::fr, AmberDB::Locale::Lang::es, AmberDB::Locale::Lang::az, AmberDB::Locale::Lang::ar, AmberDB::Locale::Lang::ja).

Inheritance Note: AmberDB inherits from AmberDB::Locale via use parent. When an AmberDB instance is constructed, it automatically initializes its locale subsystem from $adb->config('language'). All methods documented below can be called directly on $adb (e.g. $adb->format_currency(...)).

CONSTRUCTOR

new([%options | $hashref | $language_code])

Creates and returns an 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 English 'en')
my $lang = AmberDB::Locale->new();

Supported language codes include "tr", "en", "de", "fr", "es", "ru", "az", "ar", "ja" and common aliases (such as "turkish", "tr_tr", "english", "german", etc.). If an unsupported language is specified, a warning is issued and the instance falls back to "en".

METHODS

Case Conversions & Comparison

uc($string)

Converts $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)

lc($string)

Converts $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)

ucfirst($string)

Capitalizes the first letter of each word in $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"

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"

ieq($str1, $str2)

Performs a locale-aware, case-insensitive comparison between $str1 and $str2. Returns 1 if they are equal under locale rules, 0 otherwise.

$tr->ieq("İstanbul", "istanbul"); # 1 (true)
$tr->ieq("IĞDIR", "ığdır");       # 1 (true)
$tr->ieq("Ankara", "İzmir");      # 0 (false)

Sorting

sort(\@list [, $field_or_index])

Sorts an array reference \@list using the Unicode Collation Algorithm (UCA) tailored for the active locale.

# 1. Simple array of strings (respects Turkish alphabetical ordering: Ç, Ğ, İ, Ö, Ş, Ü)
my @sorted = $tr->sort(["İzmir", "Ankara", "Van", "Şanlıurfa", "Bursa", "Çanakkale"]);
# => ("Ankara", "Bursa", "Çanakkale", "İzmir", "Şanlıurfa", "Van")

# 2. Array of hash references (sort by hash key)
my @products = (
    { id => 1, title => "Şemsiye" },
    { id => 2, title => "Ayna" },
    { id => 3, title => "Çanta" }
);
my @sorted_products = $tr->sort(\@products, "title");
# => ({ id => 2, title => "Ayna" }, { id => 3, title => "Çanta" }, { id => 1, title => "Şemsiye" })

# 3. Array of array references (sort by element column index)
my @rows = (
    [ 101, "Van" ],
    [ 102, "Adana" ],
    [ 103, "Çorum" ]
);
my @sorted_rows = $tr->sort(\@rows, 1);
# => ([102, "Adana"], [103, "Çorum"], [101, "Van"])

Text Normalization & Transliteration

normalize($string)

Cleans and normalizes $string by decoding HTML entities, stripping HTML tags, mapping locale-specific accents (such as circumflex vowels â, î, û), filtering characters outside the locale's safe character set, and collapsing whitespace.

my $clean = $tr->normalize('<p>Kâr &amp; zarar &ccedil;izelgesi</p>');
# => "Kar zarar cizelgesi"

to_ascii($string [, $nonspace])

Transliterates localized text into plain ASCII characters according to per-language phonetic and transliteration conventions.

  • Turkish (tr>): ç -> c, ğ -> g, ı/İ -> i, ö -> o, ş -> s, ü -> u

    $tr->to_ascii("müller");          # "muller"
    $tr->to_ascii("çarşı");           # "carsi"
  • German (de>, DIN 5007-2): ä -> ae, ö -> oe, ü -> ue, ß -> ss

    $de->to_ascii("müller");          # "mueller"
    $de->to_ascii("Große Straße");    # "Grosse Strasse"
  • French (fr>):

    $fr->to_ascii("façade Noël");     # "facade Noel"
  • Spanish (es>):

    $es->to_ascii("año niño");        # "ano nino"

If $nonspace = 1 (slug mode), the string is lowercased and spaces/punctuation are converted to single underscores:

$tr->to_ascii("İstanbul", 1);               # "istanbul"
$tr->to_ascii("Kâr & Zarar Tablosu!", 1);   # "kar_zarar_tablosu"

first_char($string)

Returns the normalized, uppercase first character of $string for alphabetical indexing (e.g. A-Z catalog directories). Returns "0-9" if the string begins with a digit.

$tr->first_char("  çarşı  ");    # "Ç"
$tr->first_char("123abc");       # "0-9"
$tr->first_char("İzmir");        # "İ"

UTF-8 Encoding & Slicing

utf_encode($string) / utf_decode($string)

Converts a Perl Unicode character string into raw UTF-8 octet bytes (utf_encode) or decodes raw UTF-8 bytes into Perl Unicode characters (utf_decode). Safe against double-encoding.

my $bytes = $lang->utf_encode($unicode_str);
my $chars = $lang->utf_decode($raw_bytes);

substring($string, [$offset], $length)

Extracts a substring from $string based on character count rather than byte count. Prevents cutting multibyte UTF-8 characters in half. Works transparently on both decoded Unicode strings and raw UTF-8 byte strings.

$tr->substring("Çanakkale", 0, 4); # "Çana" (4 characters, 5 bytes)
$tr->substring("İstanbul", 2, 3);  # "tan"

# Default offset is 0 if omitted:
$tr->substring("Şanlıurfa", 5);     # "Şanlı"

Search Engine Tokenization & Regex

search_pattern($query)

Converts a search query string into a locale-aware regex pattern. Replaces locale-specific casing characters with regex character classes.

my $pat = $tr->search_pattern("sırdaş");
# => pattern matching both "sırdaş", "SIRDAŞ", "Sırdaş" under Turkish rules

search_regex($string, $pattern)

Performs a case-insensitive, locale-aware regex match of $pattern inside $string. Returns 1 on match, 0 otherwise.

my $found = $tr->search_regex("İstanbul Boğazı", "istanbul"); # 1

normalize_word($word, [$mode_write])

Normalizes a single search token according to locale phonetic assimilation, clitic/apostrophe stripping, and final-devoicing rules.

  • Write Mode ($mode_write = 1): Generates both the root token and joined compound token to index clitic variants (e.g. "Türkiye'de" -> "turkiye turkiyede").

  • Read Mode ($mode_write = 0 or omitted): Strips clitics and suffixes to resolve the base root (e.g. "Türkiye'de" -> "turkiye"; single-letter prefixes like "T-Shirt" resolve to "tshirt").

my $write_tokens = $tr->normalize_word("Türkiye'de", 1); # "turkiye turkiyede"
my $query_token  = $tr->normalize_word("Türkiye'de", 0); # "turkiye"

Number & Currency Processing

num2text($number [, %options])

Converts numeric values (integers or floating-point decimals) into written words in the target locale. Ideal for generating formal banking receipts, invoices, and cheques.

# Turkish rules: "1000" is "Bin TL" (NOT "Bir Bin TL")
$tr->num2text(0);       # "Sıfır"
$tr->num2text(1);       # "Bir TL"
$tr->num2text(100);     # "Yüz TL"
$tr->num2text(1000);    # "Bin TL"
$tr->num2text(1234.56); # "Bin İki Yüz Otuz Dört TL Elli Altı KR"
$tr->num2text(-42);     # "Eksi Kırk İki TL"

# English rules: requires "One Thousand"
$en->num2text(1000);    # "One Thousand USD"
$en->num2text(1234.56); # "One Thousand Two Hundred Thirty Four USD Fifty Six cent"

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

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

Options:

  • currency => { main => "EUR", sub => "cent" }: Custom currency labels.

    $tr->num2text(99.99, currency => { main => "EUR", sub => "cent" });
    # => "Doksan Dokuz EUR Doksan Dokuz cent"
  • numbers => \%custom_hash: Overrides number word definitions with custom dictionaries.

format_number($number [, %options])

Formats $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: decimals, decimal_sep, group_sep.

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"

Date & Time Operations

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:

  • YYYY, YY - 4-digit / 2-digit year

  • MMMM, MMM, MM, M - Full month name, short month, 2-digit month, 1-digit month

  • DD, D - 2-digit day, 1-digit day

  • dddd, ddd - Full day name, short day name

  • HH, H - Hour (2-digit / 1-digit)

  • mm, m - Minute (2-digit / 1-digit)

  • ss, s - Second (2-digit / 1-digit)

parse_date($string [, %options])

Parses a localized date string (e.g. "28.08.2026" or "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 }

HTML Entity Decoding

decode_entities($string)

Decodes numeric (hex &#x...;, decimal &#...;) and named HTML entities in $string, incorporating both universal entities and locale-specific extra entities.

$tr->decode_entities("&amp; &lt; &gt; &#x20AC; &ccedil;");
# => "& < > € ç"

Pluralization

plural($count, \%forms)

Selects and interpolates the appropriate plural form from \%forms based on CLDR plural rules for the active locale.

# English (2 forms: one, other)
my $en = AmberDB::Locale->new(language => "en");
$en->plural(1, { one => "{count} item", other => "{count} items" }); # "1 item"
$en->plural(5, { one => "{count} item", other => "{count} items" }); # "5 items"

# Russian (4 forms: one, few, many, other)
my $ru = AmberDB::Locale->new(language => "ru");
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 яблоко"  (ends in 1, except 11)

# Turkish (regular count)
my $tr = AmberDB::Locale->new(language => "tr");
$tr->plural(5, { one => "{count} ürün", other => "{count} ürün" }); # "5 ürün"

Placeholders {count} or {n} in template strings are automatically replaced with formatted number values.

Accessors

language()

Returns the active language tag (e.g., "tr", "en", "de").

months()

Returns an array reference containing the 12 localized month names.

days()

Returns an array reference containing the 7 localized day names starting from Sunday.

AUTHOR

Maruf Cetin <marufcetin@gmail.com>

LICENSE AND COPYRIGHT

Copyright (C) 2017-2026 Maruf Cetin.

This library is free software; you can redistribute it and/or modify it under the terms of the Artistic License 2.0.

1 POD Error

The following errors were encountered while parsing the POD:

Around line 1163:

Non-ASCII character seen before =encoding in '$adb->uc("ığdır");'. Assuming UTF-8