File Coverage

File:blib/lib/CGI/Lingua.pm
Coverage:90.1%

linestmtbrancondsubtimecode
1package CGI::Lingua;
2
3
77
77
77
2157220
61
1811
use warnings;
4
77
77
77
121
43
763
use strict;
5
77
77
77
3856
489891
141
use autodie qw(:all);
6
7
77
77
77
611951
77
905
use Carp ();    # fully qualified calls: nothing imported into this package
8
77
77
77
8795
4667150
1574
use Object::Configure 0.23;
9
77
77
77
236
364
1157
use Params::Get 0.15;   # 0.15 fast-path: unblessed hashref returned directly
10
77
77
77
127
66
1129
use Readonly;
11
77
77
77
126
63
393
use Scalar::Util ();    # fully qualified, so $obj->blessed() is not a method
12
77
77
77
9864
170832
835
use JSON::PP ();
13
77
221
use Class::Autouse qw{
14        Locale::Language
15        Locale::Object::Country
16        Locale::Object::DB
17        I18N::AcceptLanguage
18
77
77
7773
246152
};
19
20our $VERSION = '0.87';
21
22# Post-release roadmap (from the 0.87 gap analysis).
23#
24# Upstream services
25# TODO: geoplugin.net has no free tier any more, so that look-up is dead code
26#       for nearly everyone.  Make the web providers configurable
27#       (geo_providers => [...]) with a built-in client for a free HTTPS
28#       service, or drop geoplugin.
29# TODO: ip-api.com is plain HTTP and limited to 45 requests a minute; answers
30#       are validated but can be read and altered in transit.  Allow an API key
31#       / HTTPS provider and add rate limiting or back-off.
32# TODO: Support MaxMind GeoLite2 (GeoIP2::Database::Reader or
33#       IP::Geolocation::MMDB); GeoIP.dat (frozen 2019) and Geo::IPfree (known
34#       wrong entries) are the only local databases today.
35# TODO: Add a timeout option: a slow upstream holds the request for LWP's
36#       default of 180 seconds.
37#
38# Features
39# TODO: native_name() ("Francais" for French, in that language's script),
40#       plus bcp47() / lang_attribute() for the HTML lang attribute.
41# TODO: Number, currency and date formatting helpers from the negotiated
42#       locale (CLDR via Locale::CLDR, optional).
43# TODO: Script detection (zh-Hant / zh-Hans, sr-Latn) and RTL by script
44#       rather than by language.
45# TODO: Proper 3-letter and UN M.49 regions (es-419, en-029); these are the
46#       remaining TODO tests (t/es_419.t, t/en_029.t).
47# TODO: A Content-Language / Vary: Accept-Language header helper, and RFC
48#       4647 "lookup" fallback chains.
49# TODO: PSGI / Plack: middleware or a from_env(\%env) constructor that does
50#       not depend on %ENV, removing the "country() reads REMOTE_ADDR when
51#       called" pitfall.
52#
53# Technical debt
54# TODO: Split this file into ::Negotiate, ::Geo (one class per provider) and
55#       ::Cache; most of the mocking pitfalls in the tests come from the
56#       coupling.
57# TODO: Replace the three geo sentinels and the two package sentinels
58#       ($_locale_object_db_ok, $_have_dvip) with one capability probe, cached
59#       per process.
60# TODO: Enforce privacy with Sub::Private once t/function.t and
61#       t/extended_tests.t stop calling _-methods directly.
62# TODO: Structured log events (a code plus fields) instead of English
63#       strings, so applications can translate and filter them.
64# TODO: t/integration.t takes about 15 seconds because of its ~45 child
65#       processes; run them in parallel or reuse one child per combination.
66
67# Gathering magic strings here makes behavioural changes one-edit operations.
68
69Readonly my $CACHE_TTL_LONG      => '1 month';
70Readonly my $CACHE_TTL_SHORT     => '1 hour';
71Readonly my $CACHE_NS            => 'CGI::Lingua:';    # namespace prefix for every key
72Readonly my $BROKEN_GEOIPFREE    => '45.128.139.41';  # https://github.com/bricas/geo-ipfree/issues/10
73Readonly my $BAIDU_SUBNET        => '185.10.104.0/22';# RT-86809: Baidu misreports as EU
74Readonly my $DEPRECATED_EN_UK    => 'en-uk';          # some browsers still send this
75Readonly my $CANONICAL_EN_GB     => 'en-gb';
76Readonly my $ACCEPT_LANG_MAX     => 256;              # max bytes we accept from the header
77Readonly my $UA_MAX              => 512;              # max bytes we accept from HTTP_USER_AGENT
78Readonly my $GEO_UNKNOWN         => -1;               # geo-module sentinel: not yet probed
79Readonly my $GEO_ABSENT          =>  0;               # geo-module sentinel: unavailable
80Readonly my $GEO_PRESENT         =>  1;               # geo-module sentinel: loaded OK
81
82# Shapes of the values CGI::Lingua keeps in the cache.  Anything read back
83# that does not match is treated as poisoned and discarded.  Language and
84# country names from Locale::Language, Locale::Codes and Locale::Object use
85# only these characters (e.g. "Cote D'Ivoire", "Modern Greek (1453-)",
86# "Falkland Islands (The) [Malvinas]"); none of them use < > " & ; / =.
87Readonly my $NAME_RE      => qr/^[A-Za-z][A-Za-z0-9 ,.:'()\[\]\-]{0,99}\z/a;     # ":" for "English (Unknown: zz)"
88Readonly my $NAME_CODE_RE => qr/^[A-Za-z][A-Za-z0-9 ,.'()\[\]\-]{0,99}=[a-z]{2,3}\z/a;
89Readonly my $LANG_CODE_RE => qr/^[a-z]{2,3}\z/a;
90Readonly my $COUNTRY_RE   => qr/^(?:[a-z]{2}|Unknown)\z/a;
91
92# A language list from HTTP_ACCEPT_LANGUAGE or a lang= parameter: the RFC 7231
93# characters plus "*", spaces and tabs (never CR or LF), at most
94# $ACCEPT_LANG_MAX bytes.  Captures the untainted value.
95Readonly my $ACCEPT_LANG_RE => qr/^([A-Za-z0-9\-,;=.* \t]{1,$ACCEPT_LANG_MAX})\z/a;
96
97# A supported-list entry: a language tag such as "en", "en-gb", "es-419" or
98# "zh-Hant" (an underscore is accepted for "en_gb").  "english" is not one.
99Readonly my $SUPPORTED_RE => qr/^[A-Za-z]{2,3}(?:[-_][A-Za-z0-9]{2,8})*\z/a;
100
101# An IP address as accepted from REMOTE_ADDR, before Data::Validate::IP checks
102# it properly: dotted quad, or IPv6 including ::ffff:a.b.c.d.  Captures it.
103Readonly my $IP_SHAPE_RE => qr/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|[0-9a-fA-F:]{2,39}|[0-9a-fA-F:]{2,30}:(?:\d{1,3}\.){3}\d{1,3})\z/a;
104
105# Fields that DESTROY saves and new() restores, with the shape each must have
106# time_zone(): IANA zone names (e.g. "America/New_York", "Etc/GMT+8", "UTC")
107Readonly my $ZONE_RE       => qr/^[A-Za-z][A-Za-z0-9_+\-\/]{0,50}\z/a;
108Readonly my $ZONE_FILE_MAX => 256;   # bytes read from $ZONE_FILE; a zone name is far shorter
109
110# File holding the system time zone, read by time_zone() when there is no
111# REMOTE_ADDR.  A package variable (not Readonly) so tests can point it at a
112# hostile file with "local $CGI::Lingua::ZONE_FILE = ...".
113our $ZONE_FILE = '/etc/timezone';
114
115# Where _load_geoip() looks for the legacy MaxMind GeoIP.dat used by Geo::IP;
116# the first readable regular file wins.  A package variable so a test or a
117# site can point it elsewhere ("local @CGI::Lingua::GEOIP_DAT = ...").
118our @GEOIP_DAT = (
119        (($^O eq 'MSWin32') ? ('c:/GeoIP/GeoIP.dat') : ()),
120        '/usr/share/GeoIP/GeoIP.dat',
121        '/usr/local/share/GeoIP/GeoIP.dat',
122);
123
124Readonly my %RESTORABLE => (
125        _slanguage               => $NAME_RE,
126        _rlanguage               => $NAME_RE,
127        _sublanguage             => $NAME_RE,
128        _slanguage_code_alpha2   => $LANG_CODE_RE,
129        _sublanguage_code_alpha2 => $LANG_CODE_RE,
130        _country                 => $COUNTRY_RE,
131);
132
133# Package-level sentinel for Locale::Object's SQLite database.  undef = not yet
134# probed; 0 = database absent (Windows installers often omit it); 1 = available.
135# Package-level (not per-object) because the database either exists on the
136# filesystem or it doesn't — there is no per-request variability.
137my $_locale_object_db_ok;
138
139# Package-level sentinel for Data::Validate::IP / NetAddr::IP availability.
140# NetAddr::IP::UtilPP fails to build on Windows (mask4to6 bad-argument error),
141# which cascades to Data::Validate::IP.  undef = not yet probed; 0 = broken;
142# 1 = available.  On first country() call we try to load the module and, if it
143# fails, install pure-Perl aliases for the four functions we use.
144my $_have_dvip;
145
146# Short-name overrides used when Locale::Object's database is absent and we
147# fall back to Locale::Codes::Country.  Locale::Codes carries full ISO official
148# names (e.g. "United Kingdom of Great Britain and Northern Ireland") while
149# Locale::Object returns the common short form ("United Kingdom").  Only
150# entries that differ are listed; everything else comes from Locale::Codes.
151my %COUNTRY_SHORT_NAMES = (
152        bo => 'Bolivia',
153        cd => 'Democratic Republic of the Congo',
154        fk => 'Falkland Islands',
155        fm => 'Micronesia',
156        gb => 'United Kingdom',
157        ir => 'Iran',
158        kp => 'North Korea',
159        kr => 'South Korea',
160        md => 'Moldova',
161        nl => 'Netherlands',
162        ps => 'Palestine',
163        tw => 'Taiwan',
164        tz => 'Tanzania',
165        us => 'United States',
166        ve => 'Venezuela',
167);
168
169Readonly my %RTL_LANGS           => (map { $_ => 1 }  # ISO 639-1 codes whose primary script is RTL
170        qw(ar dv fa he ku ps sd ug ur yi));
171
172 - 180
=head1 NAME

CGI::Lingua - Create a multilingual web page

=head1 VERSION

Version 0.87

=cut
181
182 - 537
=head1 SYNOPSIS

Your website tells CGI::Lingua which languages it can show.
CGI::Lingua looks at what the visitor's web browser asks for,
and tells your website which of its languages to use.

=head2 Pick a language for the page

    use CGI::Lingua;

    # The site has pages in English and French
    my $l = CGI::Lingua->new(supported => ['en', 'fr']);

    my $language = $l->language();    # 'English', 'French' or 'Unknown'
    if($language eq 'English') {
        print "<p>Hello</p>\n";
    } elsif($language eq 'French') {
        print "<p>Bonjour</p>\n";
    } else {
        # The visitor wants a language that the site does not have
        my $wanted = $l->requested_language();    # e.g. 'German'
        print "<p>Sorry, this page is not available in $wanted.</p>\n";
    }

=head2 Use the short code to choose a file or template

    my $l = CGI::Lingua->new(supported => ['en-gb', 'en-us', 'fr']);

    my $code    = $l->language_code_alpha2() // 'en';    # 'en' or 'fr'
    my $variant = $l->sublanguage_code_alpha2();         # 'gb', 'us' or undef
    my $template = defined($variant) ? "$code-$variant.tmpl" : "$code.tmpl";

=head2 Show different content for different countries

    my $l = CGI::Lingua->new(supported => ['en']);

    my $country = $l->country() // '';    # e.g. 'us', 'ca', 'gb'
    if($country eq 'us') {
        print "Call us on 1-800-555-0100\n";
    } elsif($country eq 'ca') {
        print "Call us on 1-888-555-0100\n";
    } else {
        print "Email us at help\@example.com\n";
    }

=head2 Make it faster with a cache

Finding the country of an IP address can be slow.
Give CGI::Lingua a L<CHI> cache so that the work is only done once
for each visitor.

    use CHI;
    use CGI::Lingua;

    my $cache = CHI->new(
        driver    => 'File',
        root_dir  => '/var/cache/myapp',
        namespace => 'CGI::Lingua',
    );
    my $l = CGI::Lingua->new(supported => ['en', 'fr'], cache => $cache);

=head2 Let the visitor choose with a "lang" parameter

If you pass a L<CGI::Info> object, a C<lang=fr> parameter in the URL
is used before the browser's settings.

    use CGI::Info;
    use CGI::Lingua;

    my $info = CGI::Info->new();
    my $l = CGI::Lingua->new(supported => ['en', 'fr', 'de'], info => $info);
    print $l->language();    # 'German' for https://example.com/page?lang=de

=head2 Set the HTML "lang" and "dir" attributes

    my $l = CGI::Lingua->new(supported => ['en', 'ar', 'he']);
    my $code = $l->language_code_alpha2() // 'en';
    printf qq{<html lang="%s" dir="%s">\n}, $code, $l->text_direction();
    # <html lang="ar" dir="rtl"> for an Arabic-speaking visitor

=head2 Choose the right plural form

    my $l = CGI::Lingua->new(supported => ['en', 'ru']);
    my %messages = (
        one  => '%d file',
        few  => '%d files (few)',
        many => '%d files (many)',
        other => '%d files',
    );
    my $n = 3;
    printf $messages{$l->plural_category($n)} . "\n", $n;

=head2 Load a translation file

    # Looks for /var/www/i18n/en-gb.json, then /var/www/i18n/en.json
    my $l = CGI::Lingua->new(supported => ['en-gb', 'en', 'fr']);
    if(my $file = $l->translation_file('/var/www/i18n')) {
        # read and use $file
    }

=head2 Guess the visitor's time zone and locale

    my $l = CGI::Lingua->new(supported => ['en']);
    my $tz = $l->time_zone() // 'UTC';     # e.g. 'America/New_York'
    if(my $locale = $l->locale()) {         # a Locale::Object::Country
        print 'Currency: ', $locale->currency()->code(), "\n";
    }

=head2 Do not guess from the IP address

    # Only use what the browser asks for; never look up the IP address
    my $l = CGI::Lingua->new(supported => ['en', 'fr'], dont_use_ip => 1);

=head1 DESCRIPTION

This section explains, in simple steps, how CGI::Lingua finds the answer.

=head2 Finding the language

When you first call a language method (for example L</language>),
CGI::Lingua looks for the visitor's language in this order:

=over 4

=item 1. The C<lang> parameter of the L<CGI::Info> object, if you gave one
to L</new> with C<info>.

=item 2. The C<HTTP_ACCEPT_LANGUAGE> environment variable.
The web server sets this from the browser's C<Accept-Language> header,
for example C<fr-CA,fr;q=0.9,en;q=0.8>.
Languages with a higher C<q> value are tried first.
A language with C<q=0> means "not acceptable" (RFC 7231) and is never chosen.
Some browsers still send the old tag C<en-uk>; it is treated as C<en-gb>
(the code for the United Kingdom is C<gb>), with a warning.

=item 3. The C<LANG> environment variable.
This is used when you run the program on the command line,
for example C<LANG=fr_FR.UTF-8>.

=item 4. The visitor's country (see below).
CGI::Lingua uses the official language of that country.
This step is skipped when you give C<dont_use_ip> to L</new>.

=back

It then compares the answer with your C<supported> list.
If a visitor asks for C<en-us> and you only support C<en-gb>,
CGI::Lingua still chooses English, because the base language is the same.
If nothing matches, L</language> returns the string C<'Unknown'>.

=head2 Finding the country

L</country> tries these sources in order, and stops at the first answer:

=over 4

=item 1. C<GEOIP_COUNTRY_CODE> (set by Apache's mod_geoip).

=item 2. C<HTTP_CF_IPCOUNTRY> (set by Cloudflare).

=item 3. A local database: L<IP::Country>, then L<Geo::IP>, then L<Geo::IPfree>,
if they are installed.

=item 4. The geoplugin.net web service.

=item 5. A Whois look-up (L<Net::Whois::IP>, then L<Net::Whois::IANA>).

=back

Steps 3 to 5 use the visitor's IP address from C<REMOTE_ADDR>.
Private addresses (such as C<192.168.1.1>) and loopback addresses
(such as C<127.0.0.1>) have no country, so the answer is C<undef>.

=head2 Remembering answers

Each object remembers its answers, so a second call to the same method is fast.
If you give a C<cache> to L</new>, answers are also stored in the cache when the
object is destroyed. The next object created for the same IP address, the same
requested language and the same C<supported> list starts with those answers.

=head1 SUBROUTINES/METHODS

=head2 new

Creates a CGI::Lingua object.

You must tell it which languages your website supports, with C<supported>.
Each language is a short code, such as C<'en'> (English),
C<'fr'> (French) or C<'en-gb'> (British English).

You can give the arguments as a list, as a hash reference,
as a single array reference (the supported list), or as a single string
(one supported language):

    CGI::Lingua->new(supported => ['en', 'fr']);
    CGI::Lingua->new({ supported => ['en', 'fr'] });
    CGI::Lingua->new(['en', 'fr']);
    CGI::Lingua->new('en');

The arguments are:

=over 4

=item * C<supported> (required)

The languages your website supports: one short code (a string of 2 to 5
characters) or a reference to an array of language tags.
C<supported_languages> is another name for the same argument.

Each entry in the array must look like a language tag: two or three letters,
optionally followed by subtags, such as C<'en'>, C<'en-gb'>, C<'en_gb'>,
C<'es-419'> or C<'zh-Hant'>. Other entries (C<undef>, references, C<''>, or
words such as C<'english'>) could never match, so they are dropped with the
warning C<"Ignoring '...' in the supported list: not a language code">.
An empty list is allowed; L</language> then always returns C<'Unknown'>.

=item * C<cache> (optional)

An object with C<get()>, C<set()> and C<remove()> methods, such as a L<CHI> object.
CGI::Lingua stores its answers here so that later requests are faster.
If a call to the cache dies (for example a full disc, an unreachable server,
or a L<CHI> object created with C<< on_get_error =E<gt> 'die' >>), CGI::Lingua
warns C<"Cache get failed: ..."> (or C<set> / C<remove>) and carries on as if
the value was not cached. Any method that uses the cache can give this warning.

Everything read back from the cache is checked before it is used, because a
cache can be shared with, or written by, other programs. A value that does not
have the shape CGI::Lingua stores (for example C<< gb<script> >> as a country, or
a hash where a language name should be) is removed and warned about with
C<"Discarding malformed cache entry for ...">, and the answer is worked out
again.

=item * C<config_file> (optional)

The path to a configuration file. It is read by L<Object::Configure>.
Values in this file B<replace> the arguments that you give to C<new()>
(see L</COMMON PITFALLS>).

=item * C<logger> (optional)

Where to send messages. This can be an object with C<warn()>, C<info()> and
C<error()> methods, or any value that L<Object::Configure> accepts
(for example an array reference, which collects the messages).
It is always changed into a L<Log::Abstraction> object.
Without a logger, warnings go to L<Carp>.

=item * C<info> (optional)

A L<CGI::Info> object (or any object with a C<lang()> method).
A C<lang> parameter in the request is then used before the browser's
settings. The parameter comes from the visitor, so it is checked like the
C<Accept-Language> header (see L</ENCODING>); a value that fails is ignored with
the warning C<"lang parameter contains invalid characters; ignoring">.

=item * C<dont_use_ip> (optional, default false)

When true, CGI::Lingua never guesses the language from the visitor's
IP address.

=item * C<syslog> (optional)

Passed on to the logging configuration.

=item * C<debug> (optional, default false)

When true, L<I18N::AcceptLanguage> prints debug information.

=back

If you call C<new()> on an existing object, you get a copy of that object.
The arguments you give replace the values in the copy.
The copy also keeps any answers that the original has already worked out
(see L</COMMON PITFALLS>).

=head3 API SPECIFICATION

=head4 Input

    {
        supported           => { type => 'string|arrayref', optional => 0 },
        supported_languages => { type => 'string|arrayref', optional => 1 },
        cache               => { type => 'object', can => ['get', 'set'], optional => 1 },
        config_file         => { type => 'string', optional => 1 },
        logger              => { type => 'object|arrayref|hashref|string', optional => 1 },
        info                => { type => 'object', can => 'lang', optional => 1 },
        dont_use_ip         => { type => 'boolean', optional => 1 },
        syslog              => { type => 'boolean|hashref', optional => 1 },
        debug               => { type => 'boolean', optional => 1 },
    }

You must give C<supported> or its other name C<supported_languages>.
A string must be 2 to 5 characters long.

=head4 Output

    {
        type => 'object',
        isa  => 'CGI::Lingua',
    }

=head3 EXAMPLE

    # The most common form: a list of supported codes
    my $l = CGI::Lingua->new(supported => ['en', 'fr', 'de']);

    # One supported language
    my $l = CGI::Lingua->new(supported => 'en');

    # With a cache, a logger and a CGI::Info object
    use CHI;
    use CGI::Info;
    my $l = CGI::Lingua->new({
        supported => ['en', 'fr'],
        cache     => CHI->new(driver => 'File', root_dir => '/var/cache/myapp'),
        logger    => \my @messages,
        info      => CGI::Info->new(),
    });

=head3 MESSAGES

C<new()> dies (with L<Carp/croak>) with one of these messages:

    "You must give a list of supported languages"
        - supported is missing, undef, 0 or the empty string
    "List of supported languages must be an array ref"
        - supported is a reference, but not to an array
    "Supported languages must be the short code"
        - supported is a string shorter than 2 or longer than 5 characters
    "Logger must be a blessed object with warn/info/error methods"
        - logger is an object that is missing one of these methods
    "CGI::Lingua use ->new() not ::new() to instantiate"
        - new() was called as a function, with arguments
    "info must be an object with a lang() method"
        - info is not an object, or has neither lang() nor AUTOLOAD

It warns, and carries on, with:

    "Ignoring '...' in the supported list: not a language code"
        - an entry of the supported list is not a language tag
    "Cache get failed: ..."
        - the cache died while looking up saved answers
    "Discarding malformed cache entry for ..."
        - the saved answers were not in the shape CGI::Lingua writes

=head3 PSEUDOCODE

    1. Read the arguments with Params::Get
    2. If called on an object, return a copy with the new arguments on top
    3. If logger is an object, check that it has warn, info and error
    4. Merge in config_file and environment settings with Object::Configure
    5. Check supported (required; string of 2-5 characters, or an arrayref)
    6. If there is a cache and REMOTE_ADDR is set, try to load saved answers
       from the cache (JSON); if found, return them as an object
    7. Otherwise return a new object with no answers yet

=cut
538
539sub new
540{
541
2089
48241289
        my $class = shift;
542
2089
3262
        my $params = Params::Get::get_params('supported', @_);
543
544        # Handle ::new() misuse
545
2085
32222
        if(!defined($class)) {
546
3
7
                if($params) {
547                        # Object::Configure has not run yet, so the logger may still be in
548                        # one of its raw forms (arrayref, hashref, file name)
549
3
4
                        my $logger = $params->{'logger'};
550
3
8
                        if(Scalar::Util::blessed($logger) && $logger->can('error')) {
551
0
0
                                $logger->error(__PACKAGE__ . ' use ->new() not ::new() to instantiate');
552                        }
553
3
19
                        Carp::croak(__PACKAGE__ . ' use ->new() not ::new() to instantiate');
554                }
555
0
0
                $class = __PACKAGE__;
556        } elsif(ref($class)) {
557                # Clone: overlay new params onto existing object state
558
6
21
                $params->{_supported} ||= $params->{supported} if defined $params->{'supported'};
559
6
6
6
6
11
24
                return bless { %{$class}, %{$params} }, ref($class);
560        }
561
562        # Validate blessed logger objects before Object::Configure runs.
563        # Non-blessed values (arrayrefs, hashrefs) are valid config forms that
564        # Object::Configure knows how to convert into a Log::Abstraction instance.
565
2076
2525
        if(defined $params->{'logger'} && Scalar::Util::blessed($params->{'logger'})) {
566
8
61
                unless(
567                        $params->{'logger'}->can('warn')
568                        && $params->{'logger'}->can('info')
569                        && $params->{'logger'}->can('error')
570                ) {
571
4
171
                        Carp::croak('Logger must be a blessed object with warn/info/error methods');
572                }
573        }
574
575        # Object::Configure runs evals and file tests; keep them off the caller's $@ and $!
576
2072
2072
2072
1410
8772
2866
        $params = do { local ($@, $!); Object::Configure::configure($class, $params) };
577
578        # Normalise supported / supported_languages alias
579
2072
4931906
        $params->{'supported'} ||= $params->{'supported_languages'};
580
2072
2669
        if(defined($params->{supported})) {
581                # Validate supported type/length
582
2062
3543
                if(ref($params->{supported})) {
583
1027
1598
                        if(ref($params->{supported}) ne 'ARRAY') {
584
249
2977
                                Carp::croak('List of supported languages must be an array ref');
585                        }
586                } elsif((length($params->{supported}) < 2) || (length($params->{supported}) > 5)) {
587
189
2492
                        Carp::croak('Supported languages must be the short code');
588                }
589        } else {
590
10
20
                if(my $logger = $params->{'logger'}) {
591
10
24
                        $logger->error('You must give a list of supported languages');
592                }
593
10
4248
                Carp::croak('You must give a list of supported languages');
594        }
595
596
1624
1136
        my $cache = $params->{cache};
597
1624
1188
        my $info  = $params->{info};
598
599        # info is asked for lang() on every request.  CGI::Info provides lang()
600        # through AUTOLOAD, so can('lang') alone is not enough of a test.
601
1624
1873
        if(defined($info) && !(Scalar::Util::blessed($info) && ($info->can('lang') || $info->can('AUTOLOAD')))) {
602
3
29
                Carp::croak('info must be an object with a lang() method');
603        }
604
605        # Keep only entries that are language tags ("en", "en-gb", "es-419").
606        # undef, references (including a list that contains itself), '' and words
607        # such as "english" could never match and would make I18N::AcceptLanguage
608        # warn; say so rather than drop them silently.
609
1621
1042
        my @supported;
610
1621
775
1780
828
        for my $item (ref($params->{supported}) ? @{$params->{supported}} : ($params->{'supported'})) {
611
2762
142475
                my $entry = $item;      # a copy: the loop variable aliases the caller's array
612
2762
7112
                if(defined($entry) && !ref($entry) && ($entry =~ $SUPPORTED_RE)) {
613
1537
6954
                        if(lc($entry) eq $DEPRECATED_EN_UK) {
614                                # Same rule as the header: en-uk is en-gb
615
2
10
                                my $msg = "Resetting country code to GB for $entry";
616
2
16
                                if(Scalar::Util::blessed($params->{'logger'}) && $params->{'logger'}->can('warn')) {
617
2
8
                                        $params->{'logger'}->warn($msg);
618                                } else {
619
0
0
                                        Carp::carp($msg);
620                                }
621
2
461
                                $entry = $CANONICAL_EN_GB;
622                        }
623
1537
3595
                        push @supported, $entry;
624
1537
1605
                        next;
625                }
626
1225
5062
                my $msg = q{Ignoring '} . _printable($entry) . q{' in the supported list: not a language code};
627
1225
2559
                if(Scalar::Util::blessed($params->{'logger'}) && $params->{'logger'}->can('warn')) {
628
1225
4671
                        $params->{'logger'}->warn($msg);
629                } else {
630
0
0
                        Carp::carp($msg);
631                }
632        }
633
634        my $self = bless {
635
1621
7692
                %{$params},
636                _supported       => \@supported,
637                _cache           => $cache,
638                _info            => $info,
639                _syslog          => $params->{syslog},
640                _dont_use_ip     => $params->{dont_use_ip} || 0,
641                _have_ipcountry  => $GEO_UNKNOWN,
642                _have_geoip      => $GEO_UNKNOWN,
643                _have_geoipfree  => $GEO_UNKNOWN,
644
1621
157161
                _debug           => $params->{debug} || 0,
645        }, $class;
646
647        # Try to restore the answers saved by DESTROY for this visitor.  Only the
648        # fields in %RESTORABLE are taken from the cache, each checked against
649        # its pattern: the cache can be written by others (world-writable /tmp,
650        # unauthenticated Redis), so a hostile entry must not set the logger,
651        # the supported list, dont_use_ip, or an HTML/path payload as a language.
652
1621
9554
        if($cache && $ENV{'REMOTE_ADDR'}) {
653
77
126
                my $key = _build_cache_key($ENV{'REMOTE_ADDR'}, $params, $class, $info);
654                # No key means REMOTE_ADDR is not a valid address: do not cache
655
77
167
                if(defined($key) && (my $frozen = _cache_call($params, $cache, 'get', $key))) {
656                        # JSON::PP rather than Storable::thaw: Storable can run code via
657                        # STORABLE_thaw hooks in a crafted blob; JSON cannot.  A blob that
658                        # is not JSON (e.g. a legacy Storable entry) is quietly rebuilt.
659
23
23
23
23
23
19
16
17
24
42
                        my $rc = do { local $@; eval { local $SIG{__DIE__}; JSON::PP::decode_json($frozen) } };
660
23
9872
                        if(ref($rc) eq 'HASH') {
661                                my @bad = grep {
662
18
108
47
559
                                        defined($rc->{$_}) && (ref($rc->{$_}) || ($rc->{$_} !~ $RESTORABLE{$_}))
663                                } sort keys %RESTORABLE;
664
18
44
                                if(@bad) {
665
5
8
                                        my $msg = "Discarding malformed cache entry for $key";
666
5
33
                                        if(Scalar::Util::blessed($params->{'logger'}) && $params->{'logger'}->can('warn')) {
667
5
20
                                                $params->{'logger'}->warn($msg);
668                                        } else {
669
0
0
                                                Carp::carp($msg);
670                                        }
671
5
2046
                                        _cache_call($params, $cache, 'remove', $key);
672                                } else {
673
13
78
15
196
                                        $self->{$_} = $rc->{$_} for grep { defined $rc->{$_} } keys %RESTORABLE;
674
675                                        # A lang= CGI parameter overrides the browser, so the
676                                        # cached language choice may be stale: work it out again
677
13
30
                                        if(_info_lang($info)) {
678
0
0
0
0
                                                delete @{$self}{qw(_slanguage _rlanguage _sublanguage
679                                                        _slanguage_code_alpha2 _sublanguage_code_alpha2)};
680                                        }
681                                }
682                        }
683                }
684        }
685
686
1621
3103
        return $self;
687}
688# ── _build_cache_key ──────────────────────────────────────────────────────────
689# Purpose:      Produce a deterministic string key for the per-request cache
690#               entry stored in new() and DESTROY().
691# Entry:        $addr  â€” IP string (not yet taint-checked; used read-only here)
692#               $params — constructor params hashref
693#               $class  â€” package name
694#               $info   â€” optional CGI::Info object
695# Exit:         A plain string key of the form "ip/lang/lang1/lang2/..."
696sub _build_cache_key
697{
698
169
56544
        my ($addr, $params, $class, $info) = @_;
699
700        # Every part of the key comes from the visitor or the caller, and cache
701        # backends such as Memcached treat CR, LF and spaces in a key as protocol.
702        # So the key is built only from checked values, and there is no key (and
703        # so no caching) when the address is not a valid one.
704
169
209
        my $ip = _untaint_ip($addr);
705
169
945
        return undef unless defined($ip);
706
169
137
        my $key = "$ip/";
707
708        # Include the requested language (if determinable) so different
709        # Accept-Language values get distinct cache slots for the same IP.
710        # Both sources are validated; spaces are not significant in the header.
711
169
123
        my $l;
712
169
199
        if(defined($l = _info_lang($info)) || defined($l = $class->_what_language())) {
713
124
133
                $l =~ s/[ \t]+//g;
714
124
165
                $key .= "$l/" if length($l);
715        }
716
717        # Fix: was ref($params->{'supported'} eq 'ARRAY') — eq was inside ref(),
718        # so ref() always received a boolean (1 or ''), never the arrayref itself.
719        # Only entries that new() would keep are used.
720
169
168
238
195
        my @supported = ref($params->{'supported'}) eq 'ARRAY' ? @{$params->{supported}} : ($params->{'supported'});
721
169
317
156
903
        $key .= join('/', grep { defined($_) && !ref($_) && ($_ =~ $SUPPORTED_RE) } @supported);
722
723
169
814
        return $key;
724}
725
726# ── _untaint_ip ──────────────────────────────────────────────────────────
727# Purpose:      Check that a value has the shape of an IP address and return
728#               an untainted copy.  The single place REMOTE_ADDR is checked,
729#               so country(), time_zone() and the cache key always agree.
730# Entry:        $raw — any scalar.
731# Exit:         The address, or undef.  \z, not $, so "1.2.3.4\n" is refused.
732# Notes:        Shape only: country() then range-checks with is_ipv4/is_ipv6.
733sub _untaint_ip
734{
735
455
370
        my $raw = shift;
736
455
820
        return undef unless defined($raw) && !ref($raw);
737
455
649
        return $raw =~ $IP_SHAPE_RE ? $1 : undef;
738}
739
740# ── _cache_call ──────────────────────────────────────────────────────────
741# Purpose:      Call get/set/remove on the caller-supplied cache without
742#               letting a cache failure take down the request.  The cache only
743#               saves time, so a broken backend (full disc, unreachable Redis,
744#               CHI with on_get_error => 'die') must degrade to "not cached".
745# Entry:        $self   â€” a CGI::Lingua object, or new()'s params hashref
746#                         (used only to find a logger);
747#               $cache  â€” the cache object; $method — 'get', 'set' or 'remove';
748#               @args   â€” passed to the method unchanged.
749# Exit:         The method's scalar result, or undef if it died.
750# Side Effects: Warns "Cache <method> failed: <error>" on failure.
751sub _cache_call
752{
753
445
6972
        my ($self, $cache, $method, @args) = @_;
754
755
445
514
        local $@;
756
445
261
        my $rc;
757
445
445
445
408
251
538
892
32164
        return $rc if eval { local $SIG{__DIE__}; $rc = $cache->$method(@args); 1 };
758
759
37
11484
        my $err = $@ || 'unknown error';
760
37
125
        $err =~ s/\s+\z//;
761
37
52
        my $msg = "Cache $method failed: $err";
762
37
96
        if(Scalar::Util::blessed($self)) {
763
32
49
                $self->_warn({ warning => $msg });
764        } elsif((ref($self) eq 'HASH') && Scalar::Util::blessed($self->{'logger'}) && $self->{'logger'}->can('warn')) {
765
5
9
                $self->{'logger'}->warn($msg);
766        } else {
767
0
0
                Carp::carp($msg);
768        }
769
37
5064
        return;
770}
771
772# ── _info_lang ───────────────────────────────────────────────────────────
773# Purpose:      Ask the CGI::Info-style object for the lang= parameter without
774#               letting a broken object (an AUTOLOAD that dies) end the request.
775# Entry:        $info — the info object, or undef;
776#               $self — optional object to warn through when the value is bad.
777# Exit:         The validated, untainted lang value, or undef if there is none,
778#               the call died, or the value is not a plausible language list.
779sub _info_lang
780{
781
186
166
        my ($info, $self) = @_;
782
783
186
468
        return undef unless $info;
784
13
10
        local $@;
785
13
10
        my $lang;
786
13
13
13
10
52
30
24
43
        return undef unless eval { local $SIG{__DIE__}; $lang = $info->lang(); 1 };
787
10
31
        return undef unless defined($lang) && length($lang);
788
789        # lang= comes from the query string, so it is as hostile as any header:
790        # same character set and length cap as HTTP_ACCEPT_LANGUAGE
791
6
22
        return $1 if !ref($lang) && ($lang =~ $ACCEPT_LANG_RE);
792
1
13
        $self->_warn({ warning => 'lang parameter contains invalid characters; ignoring' }) if ref($self);
793
1
7
        return undef;
794}
795
796# ── _cache_get_valid ─────────────────────────────────────────────────────
797# Purpose:      Read a value from the cache and use it only if it has the
798#               shape CGI::Lingua itself stores.  The cache may be shared or
799#               writable by others; a poisoned entry ("<script>", a hashref,
800#               "../../etc") must never become a language or country.
801# Entry:        $key — full cache key; $re — the pattern the value must match.
802# Exit:         The value, or undef on a miss or a malformed value.
803# Side Effects: A malformed value is removed and "Discarding malformed cache
804#               entry for <key>" is warned (the value itself is not logged:
805#               it is attacker-controlled).
806sub _cache_get_valid
807{
808
101
341
        my ($self, $key, $re) = @_;
809
810
101
290
        my $value = $self->_cache_call($self->{_cache}, 'get', $key);
811
101
308
        return $value if !defined($value) || (!ref($value) && ($value =~ $re));
812
813
3
8
        $self->_warn({ warning => "Discarding malformed cache entry for $key" });
814
3
10
        $self->_cache_call($self->{_cache}, 'remove', $key);
815
3
5
        return;
816}
817
818# Some of the information takes a long time to work out, so cache what we can
819sub DESTROY {
820        # Destructors run at arbitrary points, e.g. while the caller is examining $@
821
1616
341594
        local ($@, $!);
822
1616
11632
        if(defined($^V) && ($^V ge 'v5.14.0')) {
823
1616
2339
                return if ${^GLOBAL_PHASE} eq 'DESTRUCT';
824        }
825
1616
11372
        return unless $ENV{'REMOTE_ADDR'};
826
827
380
293
        my $self = shift;
828
380
393
        return unless ref($self);
829
830
380
557
        my $cache = $self->{_cache};
831
380
12140
        return unless $cache;
832
833        my $key = _build_cache_key(
834                $ENV{'REMOTE_ADDR'},
835                { supported => $self->{_supported} },
836                ref($self),
837                $self->{_info},
838
79
183
        );
839
79
106
        return unless defined($key);    # REMOTE_ADDR is not a valid address
840
79
98
        return if $self->_cache_call($cache, 'get', $key);
841
842
59
98
        $self->_debug("Storing self in cache as $key");
843
844        # Serialise only the computed state — not loggers, file handles, or
845        # geo-module objects (they are re-initialised on next construction).
846        # JSON::PP is used instead of Storable so that a compromised cache backend
847        # cannot deliver a blob that executes code via STORABLE_thaw hooks.
848        # Only the fields new() will accept back (see %RESTORABLE).  _sublanguage
849        # used to be left out, so sublanguage() was undef after a restore.
850
59
354
1689
1039
        my %state = map { $_ => $self->{$_} } keys %RESTORABLE;
851
852
59
187
        $self->_cache_call($cache, 'set', $key, JSON::PP::encode_json(\%state), $CACHE_TTL_LONG);
853}
854
855 - 906
=head2 language

Returns the name of the language to show to the visitor, in English,
for example C<'English'>, C<'French'> or C<'Japanese'>.
The language is always one of the languages in your C<supported> list.

Variants are handled sensibly.
If a visitor asks for American English (C<en-us>)
and your site only has British English (C<en-gb>),
C<language()> returns C<'English'>.

If none of the languages that the visitor wants is in your C<supported> list,
C<language()> returns the string C<'Unknown'>.
It never returns C<undef>.

=head3 API SPECIFICATION

=head4 Input

    {}

=head4 Output

    {
        type => 'string',
        min  => 1,
    }

=head3 EXAMPLE

    local $ENV{HTTP_ACCEPT_LANGUAGE} = 'fr,en;q=0.9';
    my $l = CGI::Lingua->new(supported => ['en', 'fr']);
    print $l->language();   # "French"

    local $ENV{HTTP_ACCEPT_LANGUAGE} = 'de';
    my $l = CGI::Lingua->new(supported => ['en', 'fr']);
    print $l->language();   # "Unknown"

=head3 MESSAGES

The first call to L</language> (or any other language method) warns, and
ignores the value, when one of its inputs is not acceptable:

    "lang parameter contains invalid characters; ignoring"
    "HTTP_ACCEPT_LANGUAGE contains invalid characters; ignoring"
    "LANG contains invalid characters; ignoring"

It also warns when it changes the deprecated tag C<en-uk> into C<en-gb>:

    "Resetting country code to GB for ..."

=cut
907
908sub language {
909
283
11249
        my $self = $_[0];
910
911
283
628
        $self->_find_language() unless $self->{_slanguage};
912
283
1326
        return $self->{_slanguage};
913}
914
915 - 932
=head2 preferred_language

Another name for L</language>. It takes no arguments and returns the same value.

=head3 API SPECIFICATION

=head4 Input

    {}

=head4 Output

    {
        type => 'string',
        min  => 1,
    }

=cut
933
934sub preferred_language
935{
936
9
18
        my $self = shift;
937
9
18
        return $self->language(@_);
938}
939
940 - 958
=head2 name

Another name for L</language>, so that a CGI::Lingua object can be used where a
L<Locale::Object::Language> object is expected.

=head3 API SPECIFICATION

=head4 Input

    {}

=head4 Output

    {
        type => 'string',
        min  => 1,
    }

=cut
959
960sub name {
961
9
16
        my $self = $_[0];
962
9
17
        return $self->language();
963}
964
965 - 993
=head2 sublanguage

Returns the name of the variant (usually a country) of the chosen language,
for example C<'United Kingdom'> when the chosen language is C<en-gb>.

Returns C<undef> when there is no variant, for example when the chosen
language is just C<en>, or when no language was found.

=head3 API SPECIFICATION

=head4 Input

    {}

=head4 Output

    {
        type     => 'string',
        min      => 1,
        optional => 1,
    }

=head3 EXAMPLE

    local $ENV{HTTP_ACCEPT_LANGUAGE} = 'en-gb';
    my $l = CGI::Lingua->new(supported => ['en-gb']);
    print $l->sublanguage();   # "United Kingdom"

=cut
994
995sub sublanguage {
996
62
777
        my $self = $_[0];
997
998
62
111
        $self->_trace('Entered sublanguage');
999
62
1741
        $self->_find_language() unless $self->{_slanguage};
1000
62
129
        $self->_trace('Leaving sublanguage ', ($self->{_sublanguage} || 'undef'));
1001
62
1463
        return $self->{_sublanguage};
1002}
1003
1004 - 1033
=head2 language_code_alpha2

Returns the two-letter code (ISO 639-1) of the chosen language,
for example C<'en'> when the chosen language is C<en-gb>.

Returns C<undef> when none of the languages that the visitor wants is in your
C<supported> list (that is, when L</language> returns C<'Unknown'>).

=head3 API SPECIFICATION

=head4 Input

    {}

=head4 Output

    {
        type     => 'string',
        min      => 2,
        max      => 2,
        optional => 1,
    }

=head3 EXAMPLE

    local $ENV{HTTP_ACCEPT_LANGUAGE} = 'en-gb';
    my $l = CGI::Lingua->new(supported => ['en-gb']);
    print $l->language_code_alpha2();   # "en"

=cut
1034
1035sub language_code_alpha2 {
1036
115
95
        my $self = $_[0];
1037
1038
115
149
        $self->_trace('Entered language_code_alpha2');
1039
115
3842
        $self->_find_language() unless $self->{_slanguage};
1040
115
138
        $self->_trace('language_code_alpha2 returns ', $self->{_slanguage_code_alpha2});
1041
115
3112
        return $self->{_slanguage_code_alpha2};
1042}
1043
1044 - 1063
=head2 code_alpha2

Another name for L</language_code_alpha2>, kept so that old programs still work.

=head3 API SPECIFICATION

=head4 Input

    {}

=head4 Output

    {
        type     => 'string',
        min      => 2,
        max      => 2,
        optional => 1,
    }

=cut
1064
1065sub code_alpha2 {
1066
16
564
        my $self = $_[0];
1067
16
24
        return $self->language_code_alpha2();
1068}
1069
1070 - 1098
=head2 sublanguage_code_alpha2

Returns the two-letter code of the variant of the chosen language,
in lower case, for example C<'gb'> when the chosen language is C<en-gb>.

Returns C<undef> when there is no variant.

=head3 API SPECIFICATION

=head4 Input

    {}

=head4 Output

    {
        type     => 'string',
        min      => 2,
        max      => 2,
        optional => 1,
    }

=head3 EXAMPLE

    local $ENV{HTTP_ACCEPT_LANGUAGE} = 'en-gb';
    my $l = CGI::Lingua->new(supported => ['en-gb']);
    print $l->sublanguage_code_alpha2();   # "gb"

=cut
1099
1100sub sublanguage_code_alpha2 {
1101
51
784
        my $self = $_[0];
1102
1103
51
69
        $self->_find_language() unless $self->{_slanguage};
1104
51
110
        return $self->{_sublanguage_code_alpha2};
1105}
1106
1107 - 1145
=head2 requested_language

Returns, in English, the language that the visitor asked for,
B<whether or not your site supports it>.
Use it to tell the visitor that their language is not available.

If the visitor asked for a variant, it is shown in brackets,
for example C<'English (United Kingdom)'>. A variant that is not a known
country code is shown as it was sent, for example C<'English (Unknown: zz)'>.

Returns C<'Unknown'> when the visitor's language cannot be found at all.
It never returns C<undef>.

=head3 API SPECIFICATION

=head4 Input

    {}

=head4 Output

    {
        type => 'string',
        min  => 1,
    }

=head3 EXAMPLE

    local $ENV{HTTP_ACCEPT_LANGUAGE} = 'en-gb';
    my $l = CGI::Lingua->new(supported => ['en']);
    print $l->language();             # "English"
    print $l->requested_language();   # "English (United Kingdom)"

    local $ENV{HTTP_ACCEPT_LANGUAGE} = 'de';
    my $l = CGI::Lingua->new(supported => ['en']);
    print $l->language();             # "Unknown"
    print $l->requested_language();   # "German"

=cut
1146
1147sub requested_language {
1148
77
8352
        my $self = $_[0];
1149
1150
77
119
        $self->_find_language() unless $self->{_rlanguage};
1151        # _find_language() can leave it undef; the API promises a string
1152
77
155
        $self->{_rlanguage} //= 'Unknown';
1153
77
157
        return $self->{_rlanguage};
1154}
1155
1156# ── _find_language ─────────────────────────────────────────────────────────
1157# Purpose:      Populate _slanguage, _rlanguage, _sublanguage, and the
1158#               various code fields by working through the detection pipeline:
1159#               Accept-Language header → I18N::AcceptLanguage → IP country.
1160# Entry:        $self->{_slanguage} must be undef (guards repeated calls).
1161# Exit:         $self->{_slanguage} is set to a language name or 'Unknown'.
1162# Side Effects: Populates _rlanguage, _sublanguage, *_code_alpha2 fields.
1163sub _find_language
1164{
1165
322
1006
        my $self = shift;
1166        # Called by every language accessor; the evals and file tests below must
1167        # not change the caller's $@ or $!
1168
322
1068
        local ($@, $!);
1169
1170
322
481
        $self->_trace('Entered _find_language');
1171
1172
322
10190
        $self->{_rlanguage} = 'Unknown';
1173
322
330
        $self->{_slanguage} = 'Unknown';
1174
1175
322
433
        my $http_accept_language = $self->_what_language();
1176
1177        # RFC 7231 section 5.3.1: q=0 means "not acceptable", so drop those tags
1178        # before matching, or "fr;q=0, en;q=0.5" would choose French.  A q value
1179        # outside the RFC grammar (0 to 1, at most three decimals: "q=abc",
1180        # "q=1e309", "q=-1") makes the tag malformed, so drop it too, rather than
1181        # let I18N::AcceptLanguage guess and warn.
1182
322
630
        if(defined($http_accept_language) && ($http_accept_language =~ /;\s*q\s*=/i)) {
1183
46
44
                my @acceptable;
1184
46
117
                for my $tag (split(/\s*,\s*/, $http_accept_language)) {
1185
105
216
                        if($tag =~ /;\s*q\s*=\s*(\S*?)\s*\z/i) {
1186
71
59
                                my $q = $1;
1187
71
150
                                next unless $q =~ /^(?:0(?:\.[0-9]{0,3})?|1(?:\.0{0,3})?)\z/;
1188
66
149
                                next if $q == 0;
1189                        }
1190
92
97
                        push @acceptable, $tag;
1191                }
1192
46
84
                $http_accept_language = @acceptable ? join(',', @acceptable) : undef;
1193
46
91
                $self->_debug('Accept-Language after dropping unacceptable tags: ', $http_accept_language // '(none)');
1194        }
1195
1196
322
1671
        if(defined($http_accept_language)) {
1197                $self->_debug(
1198                        "language wanted: $http_accept_language, "
1199                        . 'languages supported: '
1200
237
237
276
564
                        . join(', ', @{$self->{_supported}} // '')
1201                );
1202
1203                # Normalise the deprecated en-uk tag that some browsers send, wherever
1204                # it is in the list (not only when it is the whole header)
1205
237
6461
                my $sent = $http_accept_language;
1206
237
262
                if($http_accept_language =~ s/(?<![A-Za-z0-9-])\Q$DEPRECATED_EN_UK\E(?![A-Za-z0-9-])/$CANONICAL_EN_GB/gi) {
1207
7
66
                        $self->_warn({ warning => "Resetting country code to GB for $sent" });
1208                }
1209
1210                # Run the header through the Accept-Language resolver
1211
237
2797
                my ($l, $requested_sublanguage) =
1212                        $self->_accept_language_match($http_accept_language);
1213
1214                # Resolve the matched code to a full language/sublanguage
1215
237
308
                if($l) {
1216
204
269
                        return if $self->_resolve_match($l, $requested_sublanguage, $http_accept_language);
1217                } elsif($http_accept_language =~ /;/) {
1218                        # e.g. de-DE,de;q=0.9,en-US;q=0.8 and we support none of those
1219                        $self->_notice(
1220                                __PACKAGE__, ': ', __LINE__,
1221                                ": couldn't honour HTTP_ACCEPT_LANGUAGE=$http_accept_language,"
1222                                . ' supported languages are: '
1223
4
4
5
12
                                . join(',', @{$self->{_supported}})
1224                        );
1225                }
1226
1227                # Last-chance: 2-char or xx-xx header where we have no match
1228
35
267
                if(
1229                        ((!$self->{_rlanguage}) || ($self->{_rlanguage} eq 'Unknown'))
1230                        && ((length($http_accept_language) == 2) || ($http_accept_language =~ /^..-..$/))
1231                ) {
1232
23
61
                        $self->{_rlanguage} = $self->_code2language($http_accept_language) || 'Unknown';
1233                }
1234
35
86826
                $self->{_slanguage} = 'Unknown';
1235        }
1236
1237
120
186
        return if $self->{_dont_use_ip};
1238
1239        # Fall back to the official language of the visitor's country
1240
92
164
        $self->_find_language_from_ip($http_accept_language);
1241}
1242
1243# ── _accept_language_match ────────────────────────────────────────────────
1244# Purpose:      Run I18N::AcceptLanguage strict matching plus two fallback
1245#               left-to-right scan passes against $self->{_supported}.
1246# Entry:        $http_accept_language — validated, untainted Accept-Language value.
1247# Exit:         Returns ($matched_code, $requested_sublanguage) or (undef, undef).
1248# Side Effects: Logs debug messages.
1249sub _accept_language_match
1250{
1251
245
409
        my ($self, $http_accept_language) = @_;
1252
1253
245
739
        my $i18n = I18N::AcceptLanguage->new(debug => $self->{_debug}, strict => 1);
1254
245
17837
        my $l;
1255        {
1256                # Suppress I18N::AcceptLanguage's uninitialized-value warnings (RT 74338)
1257
245
245
151
480
                local $SIG{__WARN__} = _warn_filter(qr/^Use of uninitialized value/);
1258
245
395
                $l = $i18n->accepts($http_accept_language, $self->{_supported});
1259        }
1260
1261        # I18N-AcceptLanguage strict mode can return a sublanguage variant when
1262        # the request contains a sublanguage we don't support; force a retry.
1263
245
12550
        if($l && ($http_accept_language =~ /-/) && ($http_accept_language !~ qr/$l/i)) {
1264
4
6
                $self->_debug('Forcing fallback');
1265
4
83
                undef $l;
1266        }
1267
1268
245
197
        my $requested_sublanguage;
1269
245
250
        if(!$l) {
1270                # Sort tokens by q-value once; both scan passes share the ordered list
1271
41
69
                my $sorted = $self->_sorted_tokens($http_accept_language);
1272                # First fallback: scan for xx-yy pairs, try base language xx
1273
41
67
                ($l, $requested_sublanguage) =
1274                        $self->_scan_sublanguage_pairs($i18n, $sorted);
1275
41
60
                if(!$l) {
1276                        # Second fallback: scan plain tokens without sublanguages
1277
36
72
                        $l = $self->_scan_plain_tokens($i18n, $sorted);
1278
36
65
                        undef $requested_sublanguage if $l;
1279                }
1280        }
1281
1282
245
492
        return ($l, $requested_sublanguage);
1283}
1284
1285# ── _sorted_tokens ────────────────────────────────────────────────────────
1286# Purpose:      Parse an Accept-Language header into tokens sorted by
1287#               descending quality value so fallback scans honour q= priority.
1288# Entry:        $header — validated Accept-Language string.
1289# Exit:         Arrayref of [$language_tag, $quality] pairs, highest q first.
1290sub _sorted_tokens
1291{
1292
56
420
        my ($self, $header) = @_;
1293
56
52
        my @tokens;
1294
56
356
        for my $token (split /,/, $header) {
1295
5088
3563
                $token =~ s/^\s+|\s+$//g;
1296
5088
2496
                my $q = 1.0;
1297
5088
2928
                if($token =~ s/;\s*q\s*=\s*(\d+(?:\.\d+)?)//) {
1298
28
54
                        $q = $1 + 0;
1299                }
1300
5088
3343
                $token =~ s/^\s+|\s+$//g;
1301
5088
4843
                push @tokens, [$token, $q] if length $token;
1302        }
1303
56
5033
334
2638
        return [sort { $b->[1] <=> $a->[1] } @tokens];
1304}
1305
1306# ── _scan_sublanguage_pairs ───────────────────────────────────────────────
1307# Purpose:      Walk q-sorted tokens looking for xx-yy pairs; try accepting
1308#               the base language xx from the supported list.
1309# Entry:        $i18n — I18N::AcceptLanguage instance;
1310#               $sorted — arrayref from _sorted_tokens.
1311# Exit:         ($matched_code, $sublanguage_code) or (undef, undef).
1312# Side Effects: Debug logging.
1313sub _scan_sublanguage_pairs
1314{
1315
43
47
        my ($self, $i18n, $sorted) = @_;
1316
1317
43
67
        $self->_debug(__PACKAGE__, ': ', __LINE__, ': scan q-sorted tokens for xx-yy pairs');
1318
43
43
1382
69
        for my $entry (@{$sorted}) {
1319
52
52
237
55
                my ($tag) = @{$entry};
1320
52
92
                next unless $tag =~ /^(..)-(..)$/;
1321
16
26
                my ($base, $sub) = ($1, $2);
1322
16
29
                $self->_debug(__PACKAGE__, ': ', __LINE__, ": see if $base is supported");
1323
16
365
                if($i18n->accepts($base, $self->{_supported})) {
1324
6
196
                        $self->_debug("Fallback to $base as sublanguage $sub is not supported");
1325
6
116
                        return ($base, $sub);
1326                }
1327        }
1328
37
123
        return (undef, undef);
1329}
1330
1331# ── _scan_plain_tokens ────────────────────────────────────────────────────
1332# Purpose:      Walk q-sorted tokens that have no sublanguage suffix and try
1333#               accepting each against the supported list.
1334# Entry:        $i18n — I18N::AcceptLanguage instance;
1335#               $sorted — arrayref from _sorted_tokens.
1336# Exit:         Matched code string, or undef.
1337# Side Effects: Debug logging.
1338sub _scan_plain_tokens
1339{
1340
39
47
        my ($self, $i18n, $sorted) = @_;
1341
1342
39
54
        $self->_debug(__PACKAGE__, ': ', __LINE__, ': scan q-sorted tokens for plain alternatives');
1343
39
39
1221
42
        for my $entry (@{$sorted}) {
1344
47
47
100
42
                my ($tag) = @{$entry};
1345
47
95
                next if $tag =~ /^..-../;    # already tried in the pair scan
1346
39
62
                $self->_debug(__PACKAGE__, ': ', __LINE__, ": see if $tag is supported");
1347
39
1199
                if($i18n->accepts($tag, $self->{_supported})) {
1348
3
34
                        $self->_debug("Fallback to $tag as best alternative");
1349
3
66
                        return $tag;
1350                }
1351        }
1352
36
1096
        return;
1353}
1354
1355# ── _resolve_match ────────────────────────────────────────────────────────
1356# Purpose:      Given a matched code $l (possibly xx or xx-yy), populate all
1357#               of _slanguage, _rlanguage, _sublanguage and their code fields.
1358# Entry:        $l — 2-char or xx-yy language code; $requested_sublanguage —
1359#               2-char variety code or undef; $http_accept_language — full header.
1360# Exit:         Returns true (1) if the caller should return immediately.
1361# Side Effects: Mutates $self->{_slanguage}, _rlanguage, _sublanguage, etc.
1362sub _resolve_match
1363{
1364
209
647
        my ($self, $l, $requested_sublanguage, $http_accept_language) = @_;
1365
1366
209
304
        $self->_debug("l: $l");
1367
1368
209
6055
        if($l !~ /^..-../) {
1369                # Base-language match (e.g. 'en') — no sublanguage component
1370
129
188
                return $self->_resolve_base_match($l, $requested_sublanguage, $http_accept_language);
1371        } elsif($l =~ /(.+)-(..)$/) {
1372                # Sublanguage match (e.g. 'en-gb') — resolve both language and variant
1373
78
131
                return $self->_resolve_sublanguage_match($l, $1, $2, $http_accept_language);
1374        }
1375
2
5
        return 0;
1376}
1377
1378# ── _resolve_base_match ───────────────────────────────────────────────────
1379# Purpose:      Handle the case where a base-language code matched (no hyphen).
1380#               Sets _slanguage, _rlanguage; appends sublanguage name to rlanguage
1381#               when the client requested one we don't support.
1382# Entry:        $l — 2-char code; $requested_sublanguage — optional; $header.
1383# Exit:         1 to signal caller should return, 0 otherwise.
1384# Side Effects: Mutates slanguage, rlanguage, slanguage_code_alpha2.
1385sub _resolve_base_match
1386{
1387
133
291
        my ($self, $l, $requested_sublanguage, $header) = @_;
1388
1389
133
197
        $self->{_slanguage} = $self->_code2language($l);
1390
133
348092
        return 0 unless $self->{_slanguage};
1391
1392
128
224
        $self->_debug("_slanguage: $self->{_slanguage}");
1393
128
3790
        $self->{_slanguage_code_alpha2} = $l;
1394
128
123
        $self->{_rlanguage}             = $self->{_slanguage};
1395
1396        # Attempt to name the sublanguage the client actually asked for
1397
128
82
        my $sl;
1398
128
248
        if($header =~ /..-(..)$/) {
1399
9
14
                $self->_debug($1);
1400
9
218
                $sl = $self->_code2country($1);
1401
9
33
                $requested_sublanguage //= $1;
1402        } elsif($header =~ /..-([a-z]{2,3})$/i) {
1403
1
2
                if($_locale_object_db_ok // 1) {
1404
1
1
1
3
                        eval { $sl = Locale::Object::Country->new(code_alpha3 => $1) };
1405
1
260
                        if($@) {
1406
0
0
                                $_locale_object_db_ok = 0 if $@ =~ /database was not in/;
1407
0
0
                                $self->_info($@);
1408                        } else {
1409
1
2
                                $_locale_object_db_ok = 1;
1410                        }
1411                }
1412        }
1413
1414
128
202
        if($sl) {
1415
8
17
                $self->{_rlanguage} .= ' (' . $sl->name() . ')';
1416        } elsif($requested_sublanguage) {
1417
5
10
                if(my $c = $self->_code2countryname($requested_sublanguage)) {
1418
2
12
                        $self->{_rlanguage} .= " ($c)";
1419                } else {
1420
3
57
                        $self->{_rlanguage} .= " (Unknown: $requested_sublanguage)";
1421                }
1422        }
1423
128
342
        return 1;
1424}
1425
1426# ── _resolve_sublanguage_match ────────────────────────────────────────────
1427# Purpose:      Handle the case where the full xx-yy code matched in the
1428#               supported list.  Resolves the variety name and caches results.
1429# Entry:        $l — full code e.g. 'en-gb'; $alpha2 — 'en'; $variety — 'gb';
1430#               $header — full Accept-Language value.
1431# Exit:         1 to signal caller should return, 0 otherwise.
1432# Side Effects: Mutates _slanguage, _rlanguage, _sublanguage and code fields;
1433#               writes to cache.
1434sub _resolve_sublanguage_match
1435{
1436
81
247
        my ($self, $l, $alpha2, $variety, $header) = @_;
1437
1438
81
109
        my $i18n    = I18N::AcceptLanguage->new(strict => 1);
1439
81
995
        my $accepts = $i18n->accepts($l, $self->{_supported});
1440
81
2880
        $self->_debug('accepts = ', $accepts // 'undef');
1441
1442        # $l is an entry of the supported list with a hyphen (e.g. 'en-gb'), so a
1443        # strict match always gives back that same hyphenated entry.  (An earlier
1444        # branch for a hyphen-less answer could never run, and was removed.)
1445
81
2055
        delete $self->{_slanguage} if $accepts;
1446
1447        # Accepts returned something but we couldn't resolve a language name —
1448        # try harder using the variety code directly
1449
81
118
        $self->{_rlanguage} = $self->_code2language($alpha2);
1450
81
351681
        $self->_debug("_rlanguage: $self->{_rlanguage}");
1451
1452
81
2127
        return 0 unless $accepts;
1453
1454
80
115
        $self->_debug("http_accept_language = $header");
1455
80
1952
        $l =~ /(..)-(..)/;
1456
80
79
        $variety = lc($2);
1457
1458        # Skip numeric/region codes like en-029
1459
80
292
        if(($variety =~ /[a-z]{2,3}/) && !defined($self->{_sublanguage})) {
1460
80
139
                $self->_get_closest($alpha2, $alpha2);
1461
80
107
                $self->_debug("Find the country code for $variety");
1462
1463
80
1864
                my ($from_cache, $language_name, $db_error);
1464
80
126
                if($self->{_cache}) {
1465
22
32
                        $from_cache = $self->_cache_get_valid($CACHE_NS . "variety:$variety", $NAME_CODE_RE);
1466                }
1467
1468
80
191
                if(defined($from_cache)) {
1469
6
11
                        $self->_debug("$variety is in cache as $from_cache");
1470                        # Cache stores "countryname=langcode" (e.g. "United Kingdom=en").
1471                        # Splitting on = gives the country name as the first field.
1472
6
84
                        ($language_name) = split(/=/, $from_cache);
1473                        # A poisoned entry such as "=en" has an empty name; treat it as a miss
1474
6
15
                        undef $language_name unless defined($language_name) && length($language_name);
1475                } elsif($_locale_object_db_ok // 1) {
1476                        # Locale::Object's SQLite database is absent on some Windows
1477                        # installations; the sentinel avoids repeated failed new() calls.
1478                        # Keep the error in a lexical: $@ itself may still hold an
1479                        # unrelated error from the caller when this branch is skipped.
1480
74
57
                        eval {
1481
74
272
                                my $db = Locale::Object::DB->new();
1482
74
74
89784
139
                                my @results = @{$db->lookup(
1483                                        table         => 'country',
1484                                        result_column => 'name',
1485                                        search_column => 'code_alpha2',
1486                                        value         => $variety
1487                                )};
1488
74
9975
                                $_locale_object_db_ok = 1;
1489
74
98
                                if(defined($results[0])) {
1490
73
114
                                        $language_name = $self->_code2countryname($variety);
1491                                } else {
1492
1
1
                                        $self->_debug("Can't find the country code for $variety in Locale::Object::DB");
1493                                }
1494
74
385
                                1;
1495                        } or $db_error = ($@ || 'unknown error');
1496
74
99
                        if($db_error) {
1497
0
0
                                $_locale_object_db_ok = 0
1498                                        if $db_error =~ /database was not in/;
1499                                # fall through: $language_name stays undef, caught below
1500                        }
1501                }
1502
1503
80
178
                if($db_error || !defined($language_name)) {
1504
1
2
                        $self->_warn({ warning => $db_error }) if $db_error;
1505                        # Locale::Object DB may be absent (common on Windows CI); fall back to
1506                        # the short-name table / Locale::Codes before giving up.
1507
1
2
                        $language_name = $self->_country_short_name($variety);
1508                }
1509
80
121
                if(!defined($language_name)) {
1510
1
1
                        $self->_debug(__PACKAGE__, ': ', __LINE__, ': setting sublanguage to Unknown');
1511
1
32
                        $self->{_sublanguage} = 'Unknown';
1512
1
3
                        $self->_warn({ warning => "Can't determine values for $header" });
1513                } else {
1514
79
94
                        $self->{_sublanguage} = $language_name;
1515
79
119
                        $self->_debug('variety name ', $self->{_sublanguage});
1516
79
2191
                        if($self->{_cache} && !defined($from_cache)) {
1517                                # Store "countryname=langcode" so future cache hits return the country
1518                                # name in the first field.  Previously this stored the language name
1519                                # ("English=en" for en-gb) which was wrong — the cache-hit branch
1520                                # split on = and used the first field as the sublanguage (country) name.
1521
16
30
                                $self->_debug("Set variety:$variety to $language_name=$self->{_slanguage_code_alpha2}");
1522
16
377
                                $self->_cache_call($self->{_cache}, 'set',
1523                                        $CACHE_NS . "variety:$variety",
1524                                        "$language_name=$self->{_slanguage_code_alpha2}",
1525                                        $CACHE_TTL_LONG
1526                                );
1527                        }
1528                }
1529        }
1530
1531
80
101
        if(defined($self->{_sublanguage})) {
1532
80
128
                $self->{_rlanguage} = "$self->{_slanguage} ($self->{_sublanguage})";
1533
80
86
                $self->{_sublanguage_code_alpha2} = $variety;
1534
80
2311
                return 1;
1535        }
1536
0
0
        return 0;
1537}
1538
1539# ── _find_language_from_ip ────────────────────────────────────────────────
1540# Purpose:      Fall back to the visitor's IP country when the Accept-Language
1541#               header produced no usable match.  Looks up the official language
1542#               of the country and checks it against the supported list.
1543# Entry:        $http_accept_language — may be undef if no header was present.
1544# Exit:         Mutates _slanguage, _rlanguage via _get_closest if a match found.
1545# Side Effects: Calls country(); may write to cache.
1546sub _find_language_from_ip
1547{
1548
103
812
        my ($self, $http_accept_language) = @_;
1549
1550
103
196
        my $country = $self->country();
1551
1552        # If country() returned nothing, try to derive from the LANG env var
1553
103
175
        if(!defined($country) && (my $c = $self->_what_language())) {
1554
29
77
                if($c =~ /^(..)_(..)/) {
1555
3
4
                        $country = $2;
1556                } elsif($c =~ /^(..)$/) {
1557
16
16
                        $country = $1;
1558                }
1559        }
1560
103
164
        return unless defined $country;
1561
1562
62
94
        $self->_debug("country: $country");
1563
1564
62
628
        my ($language_name, $language_code2, $from_cache);
1565
62
148
        if($self->{_cache}) {
1566
6
7
                $from_cache = $self->_cache_get_valid($CACHE_NS . 'language_name:' . $country, $NAME_CODE_RE);
1567        }
1568
1569
62
163
        if($from_cache) {
1570
2
3
                $self->_debug("$country is in cache as $from_cache");
1571
2
35
                ($language_name, $language_code2) = split(/=/, $from_cache);
1572        } else {
1573
60
208
                my $l = $self->_code2country(uc($country));
1574
60
188
                if($l) {
1575
51
136
                        $l = ($l->languages_official)[0];
1576
51
21264
                        if(defined $l) {
1577
50
88
                                $language_name  = $l->name;
1578
50
192
                                $language_code2 = $l->code_alpha2;
1579
50
259
                                $self->_debug("Official language: $language_name") if $language_name;
1580                        }
1581                }
1582        }
1583
1584        # REMOTE_ADDR is absent when the country came from LANG (command-line use)
1585
62
675
        my $ip = $ENV{'REMOTE_ADDR'} // '(none)';
1586
62
99
        return unless $language_name;
1587
1588
52
187
        if((!defined($self->{_rlanguage})) || ($self->{_rlanguage} eq 'Unknown')) {
1589
44
64
                $self->{_rlanguage} = $language_name;
1590        }
1591
1592
52
166
        unless((exists $self->{_slanguage}) && ($self->{_slanguage} ne 'Unknown')) {
1593
50
45
                my $code;
1594
1595
50
176
                if($language_name && $language_code2 && !defined($http_accept_language)) {
1596                        # Fast-path for search engines that hit with no Accept-Language
1597
40
71
                        $self->_debug("Fast assign to $language_code2");
1598
40
166
                        $code = $language_code2;
1599                } else {
1600
10
16
                        $self->_debug("Call language2code on $self->{_rlanguage}");
1601
10
305
                        $code = Locale::Language::language2code($self->{_rlanguage});
1602
1603
10
88773
                        unless($code) {
1604
1
4
                                if($http_accept_language && ($http_accept_language ne $self->{_rlanguage})) {
1605
1
2
                                        $self->_debug("Call language2code on $http_accept_language");
1606
1
3
                                        $code = Locale::Language::language2code($http_accept_language);
1607                                }
1608
1
14
                                unless($code) {
1609                                        # Norwegian (Nynorsk) — strip the parenthetical qualifier
1610
1
2
                                        if($self->{_rlanguage} =~ /(.+)\s\(.+/) {
1611
0
0
                                                if((!defined($http_accept_language)) || ($1 ne $self->{_rlanguage})) {
1612
0
0
                                                        $self->_debug("Call language2code on $1");
1613
0
0
                                                        $code = Locale::Language::language2code($1);
1614                                                }
1615                                        }
1616
1
2
                                        unless($code) {
1617
1
3
                                                $self->_warn({
1618                                                        warning => "Can't determine code from IP $ip for requested language $self->{_rlanguage}"
1619                                                });
1620                                        }
1621                                }
1622                        }
1623                }
1624
1625
50
81
                if($code) {
1626
49
136
                        $self->_get_closest($code, $language_code2);
1627
49
176
                        unless($self->{_slanguage}) {
1628                                $self->_warn({
1629                                        warning => "Couldn't determine closest language for $language_name in "
1630
1
1
1
3
                                                . join(', ', @{$self->{_supported}})
1631                                });
1632                        } else {
1633
48
196
                                $self->_debug("language set to $self->{_slanguage}, code set to $code");
1634                        }
1635                }
1636        }
1637
1638
52
835
        if(!defined($self->{_slanguage_code_alpha2})) {
1639
11
12
                $self->_debug("Can't determine slanguage_code_alpha2");
1640        } elsif(!defined($from_cache) && $self->{_cache} && defined($self->{_slanguage_code_alpha2})) {
1641
3
8
                $self->_debug("Set $country to $language_name=$self->{_slanguage_code_alpha2}");
1642
3
64
                $self->_cache_call($self->{_cache}, 'set',
1643                        $CACHE_NS . 'language_name:' . $country,
1644                        "$language_name=$self->{_slanguage_code_alpha2}",
1645                        $CACHE_TTL_LONG
1646                );
1647        }
1648}
1649
1650# ── _get_closest ─────────────────────────────────────────────────────────
1651# Purpose:      If $language_string matches the base language of any supported
1652#               entry, set _slanguage and _slanguage_code_alpha2.
1653# Entry:        $language_string — base code e.g. 'en'; $alpha2 — same or variant.
1654# Exit:         Mutates _slanguage and _slanguage_code_alpha2 on match.
1655sub _get_closest
1656{
1657
135
208
        my ($self, $language_string, $alpha2) = @_;
1658
1659        # Map each supported entry to its base language code
1660        my %base_languages =
1661
135
341
135
118
685
190
                map { /^(.+)-/ ? ($1 => $_) : ($_ => $_) } @{$self->{_supported}};
1662
1663
135
272
        if(exists $base_languages{$language_string}) {
1664
124
151
                $self->{_slanguage}             = $self->{_rlanguage};
1665
124
273
                $self->{_slanguage_code_alpha2} = $alpha2;
1666        }
1667}
1668
1669# ── _what_language ────────────────────────────────────────────────────────
1670# Purpose:      Return the raw (validated, untainted) Accept-Language string,
1671#               consulting in priority order: cached value, CGI lang= param,
1672#               HTTP_ACCEPT_LANGUAGE env var, LANG env var (local/debug mode).
1673# Entry:        May be called as a class method (no $self->{...} access) or
1674#               as an object method.
1675# Exit:         A validated language string, or undef if nothing available.
1676# Side Effects: Caches result in $self->{_what_language} on object calls.
1677sub _what_language {
1678
562
3591
        my $self = $_[0];
1679
1680
562
547
        if(ref($self)) {
1681
394
398
                $self->_trace('Entered _what_language');
1682
394
10141
                if(defined($self->{_what_language})) {
1683
29
42
                        $self->_trace('_what_language: returning cached value: ', $self->{_what_language});
1684
29
860
                        return $self->{_what_language};
1685                }
1686
365
476
                if(my $info = $self->{_info}) {
1687
4
12
                        if(my $rc = _info_lang($info, $self)) {
1688
2
13
                                $self->_trace("_what_language set language to $rc from the lang argument");
1689
2
61
                                return $self->{_what_language} = $rc;
1690                        }
1691                }
1692        }
1693
1694
531
709
        if(my $raw_lang = $ENV{'HTTP_ACCEPT_LANGUAGE'}) {
1695                # Validate and untaint — RFC 7231 §5.3.5 character set plus * wildcard.
1696                # Spaces and tabs only, not \s: CR and LF would carry header, log-line
1697                # and Memcached-command injection into the cache key and messages.
1698
418
547
                if($raw_lang =~ $ACCEPT_LANG_RE) {
1699
367
1380
                        my $rc = $1;    # untainted
1700
367
376
                        if(ref($self)) {
1701
244
435
                                return $self->{_what_language} = $rc;
1702                        }
1703
123
256
                        return $rc;
1704                } elsif(ref($self)) {
1705
51
239
                        $self->_warn({ warning => 'HTTP_ACCEPT_LANGUAGE contains invalid characters; ignoring' });
1706                }
1707        }
1708
1709
164
4924
        if(defined($ENV{'LANG'})) {
1710                # Running locally (debug mode) — derive from system locale.
1711                # Apply the same untainting discipline as HTTP_ACCEPT_LANGUAGE: only
1712                # alphanumeric, hyphen, underscore, and dot are legitimate in a POSIX
1713                # locale name (e.g. "en_US.UTF-8", "de_DE", "ja").  Anything else is
1714                # either malformed or an injection attempt; discard it silently.
1715
19
42
                if($ENV{'LANG'} =~ /^([A-Za-z0-9_.\-]{1,$ACCEPT_LANG_MAX})$/a) {
1716
8
141
                        my $rc = $1;    # untainted
1717
8
16
                        if(ref($self)) {
1718
8
19
                                return $self->{_what_language} = $rc;
1719                        }
1720
0
0
                        return $rc;
1721                } elsif(ref($self)) {
1722
11
153
                        $self->_warn({ warning => 'LANG contains invalid characters; ignoring' });
1723                }
1724        }
1725
156
2694
        return;
1726}
1727
1728 - 1827
=head2 country

Returns the two-letter country code (ISO 3166-1) of the visitor,
in lower case, for example C<'us'>, C<'gb'> or C<'fr'>.

Returns C<undef> when the country cannot be found, for example when
C<REMOTE_ADDR> is not set, is not a valid IP address, or is a private
(C<192.168.1.1>) or loopback (C<127.0.0.1>) address.

In one special case it returns the string C<'Unknown'>:
when a source says the address is in the European Union (C<EU>),
which is not a country.

Two answers are corrected: C<hk> (Hong Kong) is returned as C<cn>, and a Whois
record that says C<US> with the state C<PR> is returned as C<pr> (Puerto Rico
has its own country code; RT#131347).

See L</Finding the country> for the order in which the sources are tried.
If you have none of L<IP::Country>, L<Geo::IP> or L<Geo::IPfree> installed,
every look-up goes over the network, so please use a C<cache> (see L</new>).

The answer is remembered, so a second call on the same object is fast.

Note that as of October 2026 geoplugin.net, one of the remote
fallbacks, no longer has a free tier: it answers with an HTTP 403 and
a "please upgrade to a paid plan" message, which contains no country
code, so the lookup falls through to Whois.
The geoplugin.net code is kept in case that changes.

Legacy MaxMind F<GeoIP.dat> databases, as used by L<Geo::IP>, are no
longer updated (Debian's C<geoip-database> package is frozen at
2019-12-24), so they can give the wrong country for addresses that
have been reallocated since then.

=head3 API SPECIFICATION

=head4 Input

    {}

=head4 Output

    {
        type     => 'string',
        matches  => qr/^(?:[a-z][a-z]|Unknown)$/,
        optional => 1,
    }

=head3 EXAMPLE

    # Behind Apache mod_geoip: no IP look-up is needed
    local $ENV{GEOIP_COUNTRY_CODE} = 'DE';
    my $l = CGI::Lingua->new(supported => ['en']);
    print $l->country();   # "de"

    # From the IP address (the answer depends on your geo database)
    local $ENV{REMOTE_ADDR} = '8.8.8.8';
    my $l = CGI::Lingua->new(supported => ['en']);
    print $l->country() // 'not known';   # "us"

=head3 MESSAGES

These are warnings. C<country()> does not die.

    "GEOIP_COUNTRY_CODE contains an invalid country code; ignoring"
    "HTTP_CF_IPCOUNTRY contains an invalid country code; ignoring"
    "X.X.X.X isn't a valid IP address"
    "cache contains a numeric country: N"
    "IP matches to a numeric country"
    "geoplugin returned unparseable JSON: ..."
    "Discarding malformed country code '...'"
    "geoplugin lookup failed: ..."
    "Cache get failed: ...", "Cache set failed: ...", "Cache remove failed: ..."
    "Discarding malformed cache entry for ..."

These are debug messages, sent only to the logger:

    "Can't determine country from LAN connection X"
    "Can't determine country from loopback connection X"

=head3 PSEUDOCODE

    1. Return the remembered answer if there is one
    2. Check GEOIP_COUNTRY_CODE env var (mod_geoip); validate /^[A-Z]{2}\z/
    3. Check HTTP_CF_IPCOUNTRY (Cloudflare); skip 'XX'; validate /^[A-Z]{2}\z/
    4. Untaint and validate REMOTE_ADDR; return undef if absent or invalid;
       change ::ffff:a.b.c.d into a.b.c.d
    5. Skip private and loopback IPs (return undef)
    6. Check CHI cache; return cached value if present
    7. Try IP::Country::Fast (local DB, fastest)
    8. Try Geo::IP (local DB)
    9. Try Geo::IPfree (local DB, skip $BROKEN_GEOIPFREE)
    10. Try geoplugin.net JSON API (LWP::Simple::WithCache;
        no free tier as of October 2026, so normally yields nothing)
    11. Last resort: Net::Whois::IP then Net::Whois::IANA
    12. Sanitise: discard numeric, normalise HK->CN, handle EU special case,
        discard anything that is not two lower-case letters
    13. Store in CHI cache; return result

=cut
1828
1829sub country {
1830
369
8601
        my $self = shift;
1831
369
1056
        local ($@, $!); # evals and lazy requires below must not leak to the caller
1832
1833
369
502
        $self->_trace(__PACKAGE__, ': Entered country()');
1834
1835        # Return cached result immediately if a previous call already resolved it.
1836        # Note: undef results (private/loopback IPs) are NOT cached here because
1837        # country() reads REMOTE_ADDR at call time, not construction time; caching
1838        # undef would give wrong answers if REMOTE_ADDR changes between calls on
1839        # the same object (the documented lazy-read design).  See LIMITATIONS.
1840
369
8502
        if($self->{_country}) {
1841
40
77
                $self->_trace('quick return: ', $self->{_country});
1842
40
392
                return $self->{_country};
1843        }
1844
1845        # mod_geoip: validate against ISO 3166-1 alpha-2 before trusting
1846
329
2465
        if(defined($ENV{'GEOIP_COUNTRY_CODE'})) {
1847
53
110
                if($ENV{'GEOIP_COUNTRY_CODE'} =~ /^([A-Z]{2})\z/a) {
1848
27
49
                        $self->{_country} = lc($1);
1849
27
273
                        return $self->{_country};
1850                } else {
1851
26
46
                        $self->_warn({ warning => 'GEOIP_COUNTRY_CODE contains an invalid country code; ignoring' });
1852                }
1853        }
1854
1855        # Cloudflare: 'XX' means Cloudflare couldn't determine country — skip it
1856
302
1327
        if(($ENV{'HTTP_CF_IPCOUNTRY'}) && ($ENV{'HTTP_CF_IPCOUNTRY'} ne 'XX')) {
1857
15
35
                if($ENV{'HTTP_CF_IPCOUNTRY'} =~ /^([A-Z]{2})\z/a) {
1858
5
11
                        $self->{_country} = lc($1);
1859
5
17
                        return $self->{_country};
1860                } else {
1861
10
20
                        $self->_warn({ warning => 'HTTP_CF_IPCOUNTRY contains an invalid country code; ignoring' });
1862                }
1863        }
1864
1865
297
289
        my $raw_ip = $ENV{'REMOTE_ADDR'};
1866
297
381
        return undef unless defined $raw_ip;
1867
1868        # Validate and untaint the IP address before passing to any geo module
1869
246
277
        my $ip = _untaint_ip($raw_ip);
1870
246
1339
        unless(defined($ip)) {
1871
24
53
                $self->_warn({ warning => "$raw_ip isn't a valid IP address" });
1872
24
1231
                return undef;
1873        }
1874
1875        # Data::Validate::IP depends on NetAddr::IP, which fails to build on Windows
1876        # (NetAddr::IP::UtilPP::mask4to6 bad-argument error).  Try to load it once;
1877        # on failure install pure-Perl aliases for the four bare function names used
1878        # below so the rest of the function is unchanged on both platforms.
1879        # Import only those four: a plain import() installs all 29 of the module's
1880        # exports here, where they would become methods of every object.
1881
222
266
        if(!defined($_have_dvip)) {
1882
44
79
                local $SIG{__DIE__};
1883
44
44
0
0
40
735
0
0
                if(eval { require Data::Validate::IP; Data::Validate::IP->import(qw(is_ipv4 is_ipv6 is_private_ip is_loopback_ip)); 1 }) {
1884
0
0
                        $_have_dvip = 1;
1885                } else {
1886
44
2861
                        $_have_dvip = 0;
1887
77
77
77
259868
107
242752
                        no warnings 'redefine';
1888
44
92
                        *CGI::Lingua::is_ipv4       = \&_is_ipv4;
1889
44
60
                        *CGI::Lingua::is_ipv6       = \&_is_ipv6;
1890
44
42
                        *CGI::Lingua::is_private_ip  = \&_is_private_ip;
1891
44
97
                        *CGI::Lingua::is_loopback_ip = \&_is_loopback_ip;
1892                }
1893        }
1894
1895
222
264
        if(!is_ipv4($ip)) {
1896
17
36
                $self->_debug("$ip isn't IPv4. Is it IPv6?");
1897
17
350
                if($ip eq '::1') {
1898
4
6
                        $ip = '127.0.0.1';    # normalise loopback
1899                } elsif($ip =~ /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i) {
1900
9
11
                        $ip = $1;             # normalise IPv4-mapped IPv6 (::ffff:a.b.c.d) to plain IPv4
1901                        # \d{1,3} matches 0-999; validate range before geo lookups because
1902                        # some inet_aton implementations wrap out-of-range octets modulo 256,
1903                        # turning 999.999.999.999 into a real routable address.
1904
9
20
                        unless(is_ipv4($ip)) {
1905
3
8
                                $self->_warn({ warning => "$ip isn't a valid IP address" });
1906
3
478
                                return undef;
1907                        }
1908                } elsif(!is_ipv6($ip)) {
1909
1
3
                        $self->_warn({ warning => "$ip isn't a valid IP address" });
1910
1
3
                        return undef;
1911                }
1912        }
1913
218
1792
        if(is_private_ip($ip)) {
1914
8
23
                $self->_debug("Can't determine country from LAN connection $ip");
1915
8
148
                return undef;
1916        }
1917
210
239
        if(is_loopback_ip($ip)) {
1918
72
116
                $self->_debug("Can't determine country from loopback connection $ip");
1919
72
2325
                return undef;
1920        }
1921
1922        # Cache look-up — skip for LAN/loopback (already returned above)
1923
138
261
        if($self->{_cache}) {
1924
21
43
                $self->{_country} = $self->_cache_call($self->{_cache}, 'get', $CACHE_NS . "country:$ip");
1925
21
43
                if(defined($self->{_country})) {
1926
11
63
                        if(!ref($self->{_country}) && ($self->{_country} !~ /\D/)) {
1927
8
26
                                $self->_warn({ warning => 'cache contains a numeric country: ' . $self->{_country} });
1928
8
520
                                $self->_cache_call($self->{_cache}, 'remove', $CACHE_NS . "country:$ip");
1929
8
10
                                delete $self->{_country};
1930                        } elsif(ref($self->{_country}) || ($self->{_country} !~ $COUNTRY_RE)) {
1931                                # Poisoned entry ("gb<script>", a hashref): same checks as a
1932                                # fresh look-up, or the cache would bypass them
1933
2
14
                                $self->_warn({ warning => "Discarding malformed cache entry for ${CACHE_NS}country:$ip" });
1934
2
24
                                $self->_cache_call($self->{_cache}, 'remove', $CACHE_NS . "country:$ip");
1935
2
4
                                delete $self->{_country};
1936                        } else {
1937
1
5
                                $self->_debug("Get $ip from cache = $self->{_country}");
1938
1
33
                                return $self->{_country};
1939                        }
1940                }
1941
20
37
                $self->_debug("$ip isn't in the cache");
1942        }
1943
1944        # Try IP::Country first (fastest, local database)
1945
137
646
        if($self->{_have_ipcountry} == $GEO_UNKNOWN) {
1946
45
45
144
220
                if(eval { require IP::Country::Fast }) {
1947                        # Require the concrete class directly; IP::Country->import() is not needed
1948                        # because IP::Country::Fast->new() is a fully-qualified class method call.
1949
0
0
                        $self->{_have_ipcountry} = $GEO_PRESENT;
1950
0
0
                        $self->{_ipcountry}      = IP::Country::Fast->new();
1951                } else {
1952
45
4154
                        $self->{_have_ipcountry} = $GEO_ABSENT;
1953                }
1954        }
1955
137
990
        $self->_debug("have_ipcountry $self->{_have_ipcountry}");
1956
1957
137
1936
        if($self->{_have_ipcountry}) {
1958
32
45
                $self->{_country} = $self->{_ipcountry}->inet_atocc($ip);
1959
32
73
                if($self->{_country}) {
1960
27
35
                        $self->{_country} = lc($self->{_country});
1961                } elsif(is_ipv4($ip)) {
1962
5
6
                        $self->_debug("$ip is not known by IP::Country");
1963                }
1964        }
1965
1966        # Try Geo::IP if IP::Country gave nothing
1967
137
244
        unless(defined($self->{_country})) {
1968
106
110
                if($self->{_have_geoip} == $GEO_UNKNOWN) {
1969
46
168
                        $self->_load_geoip();
1970                }
1971
106
210
                if($self->{_have_geoip} == $GEO_PRESENT) {
1972
1
3
                        $self->{_country} = $self->{_geoip}->country_code_by_addr($ip);
1973                }
1974
1975                # Geo::IPfree has a known-broken entry for $BROKEN_GEOIPFREE
1976
106
455
                if(!defined($self->{_country}) && ($ip ne $BROKEN_GEOIPFREE)) {
1977
105
292
                        if($self->{_have_geoipfree} == $GEO_UNKNOWN) {
1978
45
45
99
224
                                eval { require Geo::IPfree };
1979
45
3323
                                unless($@) {
1980                                        # No ->import(): Geo::IPfree uses only OO (->LookUp); the old
1981                                        # Geo::IPfree::IP->import() call was a wrong package and could
1982                                        # clobber Test::Mockingbird mocks on some versions.
1983
0
0
                                        $self->{_have_geoipfree} = $GEO_PRESENT;
1984
0
0
                                        $self->{_geoipfree}      = Geo::IPfree->new();
1985                                } else {
1986
45
57
                                        $self->{_have_geoipfree} = $GEO_ABSENT;
1987                                }
1988                        }
1989
105
249
                        if($self->{_have_geoipfree} == $GEO_PRESENT) {
1990
1
4
                                if(my $country = ($self->{_geoipfree}->LookUp($ip))[0]) {
1991
1
4
                                        $self->{_country} = lc($country);
1992                                }
1993                        }
1994                }
1995        }
1996
1997        # 'eu' is not a real country — discard
1998
137
397
        if($self->{_country} && ($self->{_country} eq 'eu')) {
1999
2
2
                delete $self->{_country};
2000        }
2001
2002        # Remote JSON lookup via geoplugin
2003
137
211
        if((!$self->{_country}) &&
2004
110
84
538
33639
           (eval { require LWP::Simple::WithCache; require JSON::Parse })) {
2005
76
735
                $self->_debug("Look up $ip on geoplugin");
2006
2007                # A timeout or connection error must not take down the request;
2008                # fall through to Whois instead
2009
76
76
76
1115
108
131
                my $data = eval { local $SIG{__DIE__}; LWP::Simple::WithCache::get("https://www.geoplugin.net/json.gp?ip=$ip") };
2010
76
506846
                $self->_warn({ warning => "geoplugin lookup failed: $@" }) if $@;
2011
76
95
                if($data) {
2012
32
32
37
2594
                        eval { $self->{_country} = JSON::Parse::parse_json($data)->{'geoplugin_countryCode'} };
2013
32
57
                        $self->_warn({ warning => "geoplugin returned unparseable JSON: $@" }) if $@;
2014                }
2015
2016                # Check the answer now rather than at the end: a junk value (an object,
2017                # "GB<script>") is true, so it would stop the Whois fallback below and
2018                # then be thrown away, leaving no country at all
2019
76
764
                my $v = $self->{_country};
2020
76
192
                if(defined($v) && (ref($v) || $v !~ /^[A-Za-z]{2}\z/)) {
2021
10
89
                        if(!ref($v) && length($v) && ($v !~ /\D/)) {
2022
4
8
                                $self->_warn({ warning => 'IP matches to a numeric country' });
2023                        } elsif(ref($v) || length($v)) {
2024
4
12
                                $self->_warn({ warning => q{Discarding malformed country code '} . lc($v) . q{'} });
2025                        }
2026
10
302
                        delete $self->{_country};
2027                }
2028        }
2029
2030        # Last resort: Whois
2031
137
2308
        unless($self->{_country}) {
2032
95
138
                $self->_resolve_country_via_whois($ip);
2033        }
2034
2035        # Sanitise and normalise whatever we found
2036
137
240
        if($self->{_country}) {
2037
82
178
                if($self->{_country} !~ /\D/) {
2038
2
6
                        $self->_warn({ warning => 'IP matches to a numeric country' });
2039
2
253
                        delete $self->{_country};
2040                } else {
2041
80
103
                        $self->{_country} = lc($self->{_country});
2042
2043                        # Legacy mappings
2044
80
175
                        if($self->{_country} eq 'hk') {
2045
3
4
                                $self->{_country} = 'cn';    # HK is no longer a separate country in Whois
2046                        } elsif($self->{_country} eq 'eu') {
2047
7
8
                                $self->_handle_eu_country($ip);
2048                        }
2049
2050
80
645
                        if($self->{_country} && ($self->{_country} !~ /\D/)) {
2051
0
0
                                $self->_warn({ warning => "cache contains a numeric country: $self->{_country}" });
2052
0
0
                                delete $self->{_country};
2053                        } elsif($self->{_country} &&
2054                                $self->{_country} ne 'Unknown' &&
2055                                ($self->{_country} !~ /^[a-z]{2}$/)) {
2056                                # Reject anything that is not exactly 2 lowercase ASCII letters,
2057                                # unless it is the 'Unknown' sentinel written by _handle_eu_country
2058                                # for EU addresses that do not map to a specific country.
2059                                # Guards against Whois CRLF injection leftovers ("gbx-header: evil")
2060                                # and XSS payloads in JSON API responses ("gb<script>...</script>").
2061
1
3
                                $self->_warn({ warning => "Discarding malformed country code '$self->{_country}'" });
2062
1
2
                                delete $self->{_country};
2063                        } elsif($self->{_country} && $self->{_cache}) {
2064
10
16
                                $self->_debug("Set $ip to $self->{_country}");
2065                                $self->_cache_call($self->{_cache}, 'set',
2066                                        $CACHE_NS . "country:$ip",
2067                                        $self->{_country},
2068
10
202
                                        $CACHE_TTL_SHORT
2069                                );
2070                        }
2071                }
2072        }
2073
2074
137
283
        return $self->{_country};
2075}
2076
2077# ── _resolve_country_via_whois ─────────────────────────────────────────────
2078# Purpose:      Attempt Net::Whois::IP then Net::Whois::IANA as a last resort.
2079# Entry:        $ip — validated, untainted IP string.
2080# Exit:         Sets $self->{_country} if a result was found.
2081# Side Effects: Network I/O; logs debug messages.
2082sub _resolve_country_via_whois
2083{
2084
75
351
        my ($self, $ip) = @_;
2085
2086
75
185
        $self->_debug("Look up $ip on Whois");
2087
2088
75
1342
        require Net::Whois::IP;
2089        # No ->import(): whoisip_query is called fully-qualified, so import is unneeded
2090        # and on some versions reinstalls the real function, clobbering Test::Mockingbird mocks.
2091
2092
75
6853
        my $whois;
2093
75
52
        eval {
2094                # Catch connection timeouts by converting Carp::carp into a die
2095
75
2
224
20
                local $SIG{__WARN__} = sub { die $_[0] };
2096
75
135
                $whois = Net::Whois::IP::whoisip_query($ip);
2097        };
2098
2099
75
1133041
        unless($@ || !defined($whois) || (ref($whois) ne 'HASH')) {
2100
54
77
                if(defined($whois->{Country})) {
2101
51
81
                        $self->{_country} = $whois->{Country};
2102                } elsif(defined($whois->{country})) {
2103
3
4
                        $self->{_country} = $whois->{country};
2104                }
2105
54
78
                if($self->{_country}) {
2106
54
181
                        if($self->{_country} eq 'EU') {
2107
7
8
                                delete $self->{_country};
2108                        } elsif(($self->{_country} eq 'US') && defined($whois->{'StateProv'}) && ($whois->{'StateProv'} eq 'PR')) {
2109                                # RT#131347: Puerto Rico is not the US
2110
2
2
                                $self->{_country} = 'pr';
2111                        }
2112                }
2113        }
2114
2115
75
88
        if($self->{_country}) {
2116
47
102
                $self->_debug("Found $ip on Net::Whois::IP as ", $self->{_country});
2117
47
537
                $self->{_country} = _clean_country_code($self->{_country});
2118                # _clean_country_code returns undef for malformed values (e.g. CRLF
2119                # injection leftovers); if so, fall through to the IANA look-up.
2120
47
98
                return if defined $self->{_country};
2121
2
2
                delete $self->{_country};
2122        }
2123
2124
30
63
        $self->_debug("Look up $ip on IANA");
2125
2126
30
691
        require Net::Whois::IANA;
2127        # No ->import(): Net::Whois::IANA->new() is a class method; import not needed.
2128
2129        # The whole IANA exchange is in one eval: new(), the query and the parse
2130        # of the answer can all die on a network or protocol error, and this is
2131        # the last resort, so a failure simply means "no country"
2132
30
5151
        my $country;
2133
30
21
        if(eval {
2134
30
37
                local $SIG{__DIE__};
2135
30
58
                my $iana = Net::Whois::IANA->new();
2136
29
168
                $iana->whois_query(-ip => $ip);
2137
28
89786
                $country = $iana->country();
2138
28
123
                1;
2139        }) {
2140
28
34
                $self->{_country} = $country;
2141
28
113
                $self->_debug("IANA reports $ip as ", $self->{_country});
2142        } else {
2143
2
24
                $self->_debug("IANA look-up of $ip failed: $@");
2144        }
2145
2146
30
411
        if($self->{_country}) {
2147
16
28
                $self->{_country} = _clean_country_code($self->{_country});
2148
16
34
                delete $self->{_country} unless defined $self->{_country};
2149        }
2150}
2151
2152# ── _clean_country_code ───────────────────────────────────────────────────
2153# Purpose:      Strip carriage returns and trailing "#…" comments that some
2154#               Whois servers append to their country field
2155#               (e.g. "US\r", "GB # United Kingdom").
2156# Entry:        $raw — raw country string from a Whois response.
2157# Exit:         Cleaned 2-char country code string.
2158sub _clean_country_code
2159{
2160
81
5463
        my ($raw) = @_;
2161
81
136
        return unless defined($raw);
2162
80
95
        $raw =~ s/[\r\n]//g;
2163        # Accept exactly 2 alpha chars, optionally followed by whitespace and a
2164        # comment (e.g. "GB # United Kingdom").  Anything else (CRLF injection
2165        # leftovers, embedded headers) returns undef so the caller can discard it
2166        # rather than propagating a malformed string through the geo pipeline.
2167
80
262
        if($raw =~ /^([A-Za-z]{2})\s*(?:#.*)?$/) {
2168
66
110
                return $1;
2169        }
2170
14
19
        return;
2171}
2172
2173# ── _in_baidu_subnet ──────────────────────────────────────────────────────
2174# Purpose:      Pure-Perl fallback check for the Baidu IPv4 subnet
2175#               185.10.104.0/22.  Used when Net::Subnet is absent (e.g. on
2176#               Windows where its Socket6 dependency fails to build).
2177# Entry:        $ip — untainted IPv4 string.
2178# Exit:         Returns true if $ip is within the /22 block, false otherwise.
2179sub _in_baidu_subnet
2180{
2181
33
6245
        my $ip = shift;
2182
33
117
        return 0 unless defined($ip) && $ip =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z/a;
2183        # pack('C') wraps octets above 255 (300 becomes 44), which would put a
2184        # malformed address such as 185.10.104.300 inside the subnet
2185
28
112
28
99
        return 0 if grep { $_ > 255 } ($1, $2, $3, $4);
2186        # pack/unpack avoids signed-integer overflow on 32-bit Perl
2187
27
70
        my $n   = unpack('N', pack('C4', $1, $2, $3, $4));
2188
27
28
        my $net = unpack('N', pack('C4', 185, 10, 104, 0));
2189
27
47
        return (($n & 0xFFFFFC00) == ($net & 0xFFFFFC00));
2190}
2191
2192# ── _handle_eu_country ────────────────────────────────────────────────────
2193# Purpose:      Resolve the ambiguous 'eu' country code.  RT-86809 shows that
2194#               Baidu reports itself as EU when it is actually in CN.  All
2195#               other 'eu' addresses are logged as Unknown.
2196# Entry:        $ip — validated, untainted IP string.
2197# Exit:         Sets $self->{_country} to 'cn' or 'Unknown'.
2198# Side Effects: Optionally loads Net::Subnet; writes info log entry.
2199sub _handle_eu_country
2200{
2201
11
24
        my ($self, $ip) = @_;
2202
2203        # Prefer Net::Subnet for correctness; fall back to the pure-Perl helper
2204        # when it is absent (Socket6, its indirect dep, fails to build on Windows).
2205
11
16
        my $in_baidu;
2206
11
11
0
0
8
366
0
0
        if(eval { require Net::Subnet; Net::Subnet->import(); 1 }) {
2207
0
0
                $in_baidu = subnet_matcher($BAIDU_SUBNET)->($ip);
2208        } else {
2209
11
399
                $in_baidu = _in_baidu_subnet($ip);
2210        }
2211
2212
11
23
        if($in_baidu) {
2213
5
33
                $self->{_country} = 'cn';
2214        } else {
2215
6
14
                $self->_info("$ip has country of eu");
2216
6
174
                $self->{_country} = 'Unknown';
2217        }
2218}
2219
2220# ── _load_geoip ───────────────────────────────────────────────────────────
2221# Purpose:      Probe for the Geo::IP database file and the Geo::IP module;
2222#               set _have_geoip and initialise _geoip on success.
2223# Entry:        _have_geoip must be GEO_UNKNOWN.
2224# Exit:         _have_geoip set to GEO_PRESENT or GEO_ABSENT.
2225# Side Effects: Requires Geo::IP; opens GeoIP.dat.
2226sub _load_geoip
2227{
2228
58
74
        my $self = shift;
2229
2230        # Check for the database file before even trying to load the module
2231        # (avoids noisy errors on Windows — CPANTESTERS report 54117bd0)
2232
58
115
76
482
        my ($dat) = grep { -f $_ && -r _ } @GEOIP_DAT;
2233
58
73
        unless(defined($dat)) {
2234
57
73
                $self->{_have_geoip} = $GEO_ABSENT;
2235
57
144
                return;
2236        }
2237
2238
1
1
1
0
1
2
4
0
        unless(eval { local $SIG{__DIE__}; require Geo::IP; 1 }) {
2239
1
7
                $self->{_have_geoip} = $GEO_ABSENT;
2240
1
4
                return;
2241        }
2242
2243        # No ->import(): Geo::IP->open() is a class method.  GEOIP_STANDARD = 0
2244        # (the constant cannot be used by name).  A corrupt or truncated file makes
2245        # open() die or return undef; treat that as no database, rather than let
2246        # country() call a method on undef later.
2247
0
0
0
0
0
0
        my $geoip = eval { local $SIG{__DIE__}; Geo::IP->open($dat, 0) };
2248
0
0
        if(Scalar::Util::blessed($geoip)) {
2249
0
0
                $self->{_have_geoip} = $GEO_PRESENT;
2250
0
0
                $self->{_geoip}      = $geoip;
2251        } else {
2252
0
0
                $self->_warn({ warning => "Can't open $dat with Geo::IP; not using it" });
2253
0
0
                $self->{_have_geoip} = $GEO_ABSENT;
2254        }
2255}
2256
2257 - 2317
=head2 locale

Returns a L<Locale::Object::Country> object for the visitor's country.
You can use it to find, for example, the currency or the country's name.

HTTP does not send a browser's local settings (such as the currency or the
date format), so this is a B<best guess>. It is not always right.

It tries these, in order:

=over 4

=item 1. A language tag such as C<en-GB> inside the C<HTTP_USER_AGENT> string.

=item 2. L<HTTP::BrowserDetect>, if it is installed.

=item 3. L</country>.

=item 4. C<GEOIP_COUNTRY_CODE>.

=back

Returns C<undef> when nothing is found.

=head3 API SPECIFICATION

=head4 Input

    {}

=head4 Output

    {
        type     => 'object',
        isa      => 'Locale::Object::Country',
        optional => 1,
    }

=head3 EXAMPLE

    local $ENV{REMOTE_ADDR} = '8.8.8.8';
    my $l = CGI::Lingua->new(supported => ['en']);
    if(my $locale = $l->locale()) {
        print $locale->name(), "\n";              # e.g. "United States"
        print $locale->currency()->code(), "\n";  # e.g. "USD"
    }

=head3 MESSAGES

    "HTTP_USER_AGENT contains invalid characters or exceeds length limit; ignoring"

=head3 PSEUDOCODE

    1. Return the remembered answer if there is one
    2. Parse HTTP_USER_AGENT parenthetical for xx-YY language tag
    3. Try HTTP::BrowserDetect on the full User-Agent string
    4. Fall back to country() IP lookup
    5. Fall back to GEOIP_COUNTRY_CODE env var (ISO 3166-1 validated)
    6. Return undef if all strategies fail

=cut
2318
2319sub locale {
2320
32
1313
        my $self = shift;
2321
32
99
        local ($@, $!); # evals below must not leak to the caller
2322
2323
32
70
        return $self->{_locale} if $self->{_locale};
2324
2325        # Validate and untaint HTTP_USER_AGENT before passing to any parser.
2326        # The User-Agent header is attacker-controlled; apply the same discipline
2327        # as HTTP_ACCEPT_LANGUAGE.  Printable ASCII (0x20-0x7e), bounded length.
2328
30
24
        my $agent;
2329
30
56
        if(defined(my $raw_agent = $ENV{'HTTP_USER_AGENT'})) {
2330
18
38
                if($raw_agent =~ /^([\x20-\x7e]{1,$UA_MAX})$/a) {
2331
13
154
                        $agent = $1;    # untainted
2332                } else {
2333
5
80
                        $self->_warn({ warning => 'HTTP_USER_AGENT contains invalid characters or exceeds length limit; ignoring' });
2334                }
2335        }
2336
2337        # First try: parse the language tag from the User-Agent parenthetical
2338
30
540
        if(defined($agent) && ($agent =~ /\((.+)\)/)) {
2339
12
28
                foreach(split(/;/, $1)) {
2340
25
17
                        my $candidate = $_;
2341
25
60
                        $candidate =~ s/^\s+|\s+$//g;    # trim both ends
2342
2343
25
57
                        if($candidate =~ /^[a-zA-Z]{2}-([a-zA-Z]{2})$/) {
2344
5
11
                                local $SIG{__WARN__} = undef;
2345
5
11
                                if(my $c = $self->_code2country($1)) {
2346
5
10
                                        $self->{_locale} = $c;
2347
5
18
                                        return $c;
2348                                }
2349                        }
2350                }
2351
2352                # Second try: HTTP::BrowserDetect (works for more User-Agents)
2353
7
7
7
1404
                if(eval { require HTTP::BrowserDetect }) {
2354
6
24022
                        HTTP::BrowserDetect->import();
2355
6
14
                        my $browser = HTTP::BrowserDetect->new($agent);
2356                        # Validate country() result before use — the return value comes from
2357                        # the third-party module and is not yet untainted or range-checked.
2358
6
579
                        if($browser) {
2359
6
10
                                my $bc = $browser->country() // '';
2360
6
195
                                if($bc =~ /^([A-Za-z]{2})$/a) {
2361
1
3
                                        if(my $c = $self->_code2country($1)) {
2362
1
3
                                                $self->{_locale} = $c;
2363
1
8
                                                return $c;
2364                                        }
2365                                }
2366                        }
2367                }
2368        }
2369
2370        # Third try: IP address
2371
24
165
        my $country = $self->country();
2372
24
35
        if($country) {
2373
10
12
                $country =~ s/[\r\n]//g;
2374
10
10
                my $c;
2375
10
10
                eval {
2376
10
0
26
0
                        local $SIG{__WARN__} = sub { die $_[0] };
2377
10
18
                        $c = $self->_code2country($country);
2378                };
2379
10
20
                unless($@) {
2380
10
14
                        if($c) {
2381
10
12
                                $self->{_locale} = $c;
2382
10
25
                                return $c;
2383                        }
2384                }
2385        }
2386
2387        # Fourth try: mod_geoip env var — apply the same ISO 3166-1 validation
2388        # used in country() to guard against spoofed or malformed values
2389
14
24
        if(defined($ENV{'GEOIP_COUNTRY_CODE'})) {
2390
3
9
                if($ENV{'GEOIP_COUNTRY_CODE'} =~ /^([A-Z]{2})\z/a) {
2391
0
0
                        if(my $c = $self->_code2country(lc($1))) {
2392
0
0
                                $self->{_locale} = $c;
2393
0
0
                                return $c;
2394                        }
2395                }
2396        }
2397
14
30
        return undef;
2398}
2399
2400 - 2463
=head2 time_zone

Returns the visitor's time zone, as an IANA time zone name,
for example C<'Europe/London'> or C<'America/New_York'>.

When C<REMOTE_ADDR> is set, it uses L<Geo::IP> if a database is installed,
and otherwise asks the ip-api.com web service (this needs
L<LWP::Simple::WithCache> or L<LWP::Simple>, and L<JSON::Parse>).

When C<REMOTE_ADDR> is not set (for example on the command line),
it returns the time zone of the computer that runs the program.

Returns C<undef> when the time zone cannot be found.

=head3 API SPECIFICATION

=head4 Input

    {}

=head4 Output

    {
        type     => 'string',
        matches  => qr/^[A-Za-z][A-Za-z0-9_+\-\/]*$/,
        optional => 1,
    }

=head3 EXAMPLE

    local $ENV{REMOTE_ADDR} = '8.8.8.8';
    my $l = CGI::Lingua->new(supported => ['en']);
    print $l->time_zone() // 'unknown';   # e.g. "America/New_York"

=head3 MESSAGES

These are warnings. C<time_zone()> does not die.

    "Couldn't determine the timezone"
    "X.X.X.X isn't a valid IP address"
    "LWP::Simple::WithCache and LWP::Simple are both absent; cannot contact ip-api.com"
    "JSON::Parse is absent; cannot read ip-api.com answers"
    "ip-api.com returned unparseable JSON: ..."
    "DateTime::TimeZone::Local failed: ..."
    "Discarding malformed timezone '...'"
    "ip-api.com lookup failed: ..."

=head3 PSEUDOCODE

    1. Return the remembered answer if there is one
    2. If REMOTE_ADDR is set:
       a. Untaint and validate the IP
       b. Try Geo::IP->time_zone() (local DB)
       c. Try LWP::Simple::WithCache + JSON::Parse against ip-api.com
       d. Fall back to LWP::Simple + JSON::Parse against ip-api.com
       e. Warn and return undef if neither LWP variant, or JSON::Parse,
          is installed (the warning names the missing module)
    3. If REMOTE_ADDR is absent (local/CLI mode):
       a. Read /etc/timezone if readable
       b. Fall back to DateTime::TimeZone::Local->TimeZone()->name()
    4. Discard the answer if it does not look like an IANA name
    5. Warn "Couldn't determine the timezone" and return undef if all fail

=cut
2464
2465sub time_zone {
2466
113
2548
        my $self = shift;
2467
113
311
        local ($@, $!); # evals, open() and HTTP calls must not leak to the caller
2468
2469
113
146
        $self->_trace('Entered time_zone');
2470
2471
112
2138
        if($self->{_timezone}) {
2472
3
7
                $self->_trace('quick return: ', $self->{_timezone});
2473
3
85
                return $self->{_timezone};
2474        }
2475
2476
109
90
        my $raw_ip = $ENV{'REMOTE_ADDR'};
2477
2478
109
98
        if(defined $raw_ip) {
2479                # Untaint before any external use — kept in sync with country()'s pattern,
2480                # including the mixed-notation branch for ::ffff:a.b.c.d addresses.
2481
40
49
                my $ip = _untaint_ip($raw_ip);
2482
40
222
                unless(defined($ip)) {
2483
5
12
                        $self->_warn({ warning => "$raw_ip isn't a valid IP address" });
2484
5
15
                        return undef;
2485                }
2486
2487
35
62
                if($self->{_have_geoip} == $GEO_UNKNOWN) {
2488
9
38
                        $self->_load_geoip();
2489                }
2490
35
86
                if($self->{_have_geoip} == $GEO_PRESENT) {
2491
1
1
3
3
                        eval { $self->{_timezone} = $self->{_geoip}->time_zone($ip) };
2492                }
2493
2494
35
144
                unless($self->{_timezone}) {
2495
34
34
26
30
111
34868
                        if(eval { require LWP::Simple::WithCache; require JSON::Parse }) {
2496
24
44
                                $self->_debug("Look up $ip on ip-api.com");
2497
2498
24
24
24
316
37
44
                                my $data = eval { local $SIG{__DIE__}; LWP::Simple::WithCache::get("http://ip-api.com/json/$ip") };
2499
24
267
                                $self->_warn({ warning => "ip-api.com lookup failed: $@" }) if $@;
2500
24
305
                                if($data) {
2501
16
16
14
85
                                        eval { $self->{_timezone} = JSON::Parse::parse_json($data)->{'timezone'} };
2502
16
34
                                        $self->_warn({ warning => "ip-api.com returned unparseable JSON: $@" }) if $@;
2503                                }
2504
10
5
952
13
                        } elsif(eval { require LWP::Simple; require JSON::Parse }) {
2505
2
4
                                $self->_debug("Look up $ip on ip-api.com");
2506
2507
2
2
2
28
4
5
                                my $data = eval { local $SIG{__DIE__}; LWP::Simple::get("http://ip-api.com/json/$ip") };
2508
2
84
                                $self->_warn({ warning => "ip-api.com lookup failed: $@" }) if $@;
2509
2
4
                                if($data) {
2510
2
2
1
11
                                        eval { $self->{_timezone} = JSON::Parse::parse_json($data)->{'timezone'} };
2511
2
6
                                        $self->_warn({ warning => "ip-api.com returned unparseable JSON: $@" }) if $@;
2512                                }
2513                        } else {
2514                                # A module is missing — degrade gracefully rather than killing the
2515                                # entire request with a croak; caller can check for undef.  Name the
2516                                # module that is really missing: an LWP may be present without JSON::Parse.
2517
8
8
2
6
1
596
21
5
172
2
                                if(eval { require LWP::Simple::WithCache; 1 } || eval { require LWP::Simple; 1 }) {
2518
3
8
                                        $self->_warn({ warning => 'JSON::Parse is absent; cannot read ip-api.com answers' });
2519                                } else {
2520
5
219
                                        $self->_warn({ warning => 'LWP::Simple::WithCache and LWP::Simple are both absent; cannot contact ip-api.com' });
2521                                }
2522                        }
2523                }
2524        } else {
2525                # Local connection — read from /etc/timezone or DateTime::TimeZone
2526
69
63
                if(defined(my $tz = $self->_read_zone_file())) {
2527
56
37
                        $self->{_timezone} = $tz;
2528                } else {
2529                        # DateTime::TimeZone::Local::TimeZone() is not available on all
2530                        # platforms/versions (absent on some Windows Perl builds); guard
2531                        # so time_zone() degrades to undef rather than dying.
2532
13
12
                        eval {
2533
13
19
                                local $SIG{__DIE__};
2534
13
30
                                require DateTime::TimeZone::Local;
2535
13
28
                                $self->{_timezone} = DateTime::TimeZone::Local->TimeZone()->name();
2536                        };
2537
13
71
                        $self->_warn({ warning => "DateTime::TimeZone::Local failed: $@" }) if $@;
2538                }
2539        }
2540
2541        # Validate the timezone string against a permissive but bounded IANA pattern.
2542        # Rejects XSS payloads (e.g. "Europe/London<script>...") from hostile JSON
2543        # responses while accepting all real IANA zone names (e.g. "America/New_York",
2544        # "Etc/GMT+8", "UTC").
2545
104
211
        if(defined($self->{_timezone}) &&
2546           (ref($self->{_timezone}) || ($self->{_timezone} !~ $ZONE_RE))) {
2547
4
29
                $self->_warn({ warning => "Discarding malformed timezone '" . _printable($self->{_timezone}) . "'" });
2548
4
223
                delete $self->{_timezone};
2549        }
2550
2551
104
302
        unless(defined($self->{_timezone})) {
2552
27
47
                $self->_warn({ warning => "Couldn't determine the timezone" });
2553        }
2554
104
1003
        return $self->{_timezone};
2555}
2556
2557# ── _read_zone_file ──────────────────────────────────────────────────────
2558# Purpose:      Read the system time zone name from $ZONE_FILE (normally
2559#               /etc/timezone) for command-line use.
2560# Entry:        none.
2561# Exit:         The zone name, or undef if the file is missing, unreadable,
2562#               not a regular file, empty, or does not hold a zone name.
2563# Notes:        Only a regular file is opened, and at most $ZONE_FILE_MAX bytes
2564#               are read: a symlink to /dev/zero or /dev/urandom, or a huge
2565#               file, must not hang the request or exhaust memory.  CORE::
2566#               calls bypass autodie, so a failure returns undef, not a die.
2567sub _read_zone_file
2568{
2569
69
40
        my $self = shift;
2570
2571
69
486
        return undef unless -f $ZONE_FILE && -r _;
2572
61
541
        CORE::open(my $fin, '<', $ZONE_FILE) or return undef;
2573
61
67
        my $got = CORE::read($fin, my $buf, $ZONE_FILE_MAX);
2574
61
833
        CORE::close($fin);
2575
61
47
        return undef unless $got;
2576
2577        # First word of the file; surrounding spaces and newlines are not part of it
2578
60
81
        my ($zone) = $buf =~ /\A\s*(\S+)/;
2579
60
111
        unless(defined($zone) && ($zone =~ $ZONE_RE)) {
2580
4
16
                $self->_debug("$ZONE_FILE does not hold a time zone name");
2581
4
15
                return undef;
2582        }
2583
56
215
        return $zone;
2584}
2585
2586 - 2616
=head2 is_rtl

Returns C<1> if the chosen language is written from right to left,
and C<0> if it is not.

The right-to-left languages are Arabic (C<ar>), Dhivehi (C<dv>),
Persian (C<fa>), Hebrew (C<he>), Kurdish (C<ku>), Pashto (C<ps>),
Sindhi (C<sd>), Uyghur (C<ug>), Urdu (C<ur>) and Yiddish (C<yi>).

When no language was found, it returns C<0>.

=head3 API SPECIFICATION

=head4 Input

    {}

=head4 Output

    {
        type     => 'boolean',
        memberof => [0, 1],
    }

=head3 EXAMPLE

    local $ENV{HTTP_ACCEPT_LANGUAGE} = 'ar';
    my $l = CGI::Lingua->new(supported => ['ar', 'en']);
    print $l->is_rtl();   # 1

=cut
2617
2618sub is_rtl
2619{
2620
16
38
        my $self = shift;
2621
16
32
        return $RTL_LANGS{$self->language_code_alpha2() // ''} ? 1 : 0;
2622}
2623
2624 - 2650
=head2 text_direction

Returns C<'rtl'> (right to left) or C<'ltr'> (left to right) for the chosen
language. You can use the value directly in the HTML C<dir> attribute.

When no language was found, it returns C<'ltr'>.

=head3 API SPECIFICATION

=head4 Input

    {}

=head4 Output

    {
        type     => 'string',
        memberof => ['ltr', 'rtl'],
    }

=head3 EXAMPLE

    local $ENV{HTTP_ACCEPT_LANGUAGE} = 'he';
    my $l = CGI::Lingua->new(supported => ['he', 'en']);
    print '<html dir="', $l->text_direction(), '">';   # <html dir="rtl">

=cut
2651
2652sub text_direction
2653{
2654
6
23
        my $self = shift;
2655
6
11
        return $self->is_rtl() ? 'rtl' : 'ltr';
2656}
2657
2658 - 2709
=head2 plural_category($n)

Many languages use a different word form for different numbers.
English has two forms ("1 file", "2 files"); Russian has more;
Japanese has only one.

C<plural_category($n)> tells you which form to use for the number C<$n>
in the chosen language.
It returns one of C<'zero'>, C<'one'>, C<'two'>, C<'few'>, C<'many'> or
C<'other'>. These are the Unicode CLDR plural category names.

The rules for about 70 languages are built in, including Arabic (6 forms),
Slavic languages (3 or 4 forms), Celtic languages (up to 6 forms), Hebrew,
Maltese, Romanian, Latvian, Lithuanian and Slovenian.
Other languages use the English rule: C<'one'> for 1, otherwise C<'other'>.

When no language was found, it always returns C<'other'>.

C<$n> should be a whole number that is zero or more.
A number with a fractional part is cut down to a whole number first
(2.7 becomes 2).
For full CLDR rules, including fractions, use L<Locale::CLDR>.

=head3 API SPECIFICATION

=head4 Input

    [
        { type => 'number', min => 0 },
    ]

=head4 Output

    {
        type     => 'string',
        memberof => ['zero', 'one', 'two', 'few', 'many', 'other'],
    }

=head3 EXAMPLE

    local $ENV{HTTP_ACCEPT_LANGUAGE} = 'ru';
    my $l = CGI::Lingua->new(supported => ['ru']);
    print $l->plural_category(1);    # "one"
    print $l->plural_category(3);    # "few"
    print $l->plural_category(11);   # "many"

=head3 MESSAGES

    "plural_category: $n must be defined"
        - dies (croak) when $n is undef

=cut
2710
2711# CLDR plural category rules (https://unicode.org/cldr/charts/42/supplemental/language_plural_rules.html).
2712# Values are coderefs: ($n) → category string.  Languages absent from this
2713# table fall back to the standard one/other rule inside plural_category().
2714my %PLURAL_RULES = (
2715
2716        # ── No plural distinction (always 'other') ────────────────────────────
2717        (map { $_ => sub { 'other' } }
2718                qw(az bm bo dz id ig ii in ja jbo jv jw kde kea km ko lkt lo ms
2719                   my nqo root sah ses sg th to vi wo yo zh)),
2720
2721        # ── Standard: n == 1 → 'one', else 'other' ───────────────────────────
2722        (map { $_ => sub { int($_[0]) == 1 ? 'one' : 'other' } }
2723                qw(af an ast bg bn brx ca cgg da de el en eo es et eu fi
2724                   gl gsw gu ha haw hu ia it kcg kk kl lb lg mas ml mn mr
2725                   nb nd ne nl nn nyn om or pa pap rm rof rwk saq seh sn so
2726                   sq ss ssy st sv sw ta te teo tig tk tl tn ts ur wae xh xog)),
2727
2728        # ── French / Portuguese-BR: n ≤ 1 → 'one' ────────────────────────────
2729        (map { $_ => sub { int($_[0]) <= 1 ? 'one' : 'other' } } qw(fr pt_BR)),
2730
2731        # ── Arabic: zero/one/two/few/many/other ───────────────────────────────
2732        ar => sub {
2733                my $n    = int($_[0]);
2734                my $m100 = $n % 100;
2735                return 'zero'  if $n == 0;
2736                return 'one'   if $n == 1;
2737                return 'two'   if $n == 2;
2738                return 'few'   if $m100 >= 3  && $m100 <= 10;
2739                return 'many'  if $m100 >= 11 && $m100 <= 99;
2740                return 'other';
2741        },
2742
2743        # ── Hebrew: one/two/many/other ────────────────────────────────────────
2744        he => sub {
2745                my $n = int($_[0]);
2746                return 'one'  if $n == 1;
2747                return 'two'  if $n == 2;
2748                return 'many' if $n != 0 && $n % 10 == 0;
2749                return 'other';
2750        },
2751
2752        # ── Russian / Ukrainian / Belarusian: one/few/many ────────────────────
2753        (map { $_ => sub {
2754                my $n    = int($_[0]);
2755                my $m10  = $n % 10;
2756                my $m100 = $n % 100;
2757                return 'one' if $m10 == 1 && $m100 != 11;
2758                return 'few' if $m10 >= 2 && $m10 <= 4 && ($m100 < 10 || $m100 >= 20);
2759                return 'many';
2760        } } qw(ru uk be)),
2761
2762        # ── Polish: one/few/many ──────────────────────────────────────────────
2763        pl => sub {
2764                my $n    = int($_[0]);
2765                my $m10  = $n % 10;
2766                my $m100 = $n % 100;
2767                return 'one' if $n == 1;
2768                return 'few' if $m10 >= 2 && $m10 <= 4 && ($m100 < 10 || $m100 >= 20);
2769                return 'many';
2770        },
2771
2772        # ── Czech / Slovak: one/few/other ────────────────────────────────────
2773        (map { $_ => sub {
2774                my $n = int($_[0]);
2775                return 'one' if $n == 1;
2776                return 'few' if $n >= 2 && $n <= 4;
2777                return 'other';
2778        } } qw(cs sk)),
2779
2780        # ── Romanian: one/few/other ───────────────────────────────────────────
2781        ro => sub {
2782                my $n    = int($_[0]);
2783                my $m100 = $n % 100;
2784                return 'one' if $n == 1;
2785                return 'few' if $n == 0 || ($m100 >= 1 && $m100 <= 19);
2786                return 'other';
2787        },
2788
2789        # ── Latvian: zero/one/other ───────────────────────────────────────────
2790        lv => sub {
2791                my $n    = int($_[0]);
2792                my $m10  = $n % 10;
2793                my $m100 = $n % 100;
2794                return 'zero'  if $m10 == 0 || ($m100 >= 11 && $m100 <= 19);
2795                return 'one'   if $m10 == 1 && $m100 != 11;
2796                return 'other';
2797        },
2798
2799        # ── Lithuanian: one/few/other ─────────────────────────────────────────
2800        lt => sub {
2801                my $n    = int($_[0]);
2802                my $m10  = $n % 10;
2803                my $m100 = $n % 100;
2804                return 'one' if $m10 == 1 && ($m100 < 10 || $m100 >= 20);
2805                return 'few' if $m10 >= 2 && ($m100 < 10 || $m100 >= 20);
2806                return 'other';
2807        },
2808
2809        # ── Slovenian: one/two/few/other ──────────────────────────────────────
2810        sl => sub {
2811                my $m100 = int($_[0]) % 100;
2812                return 'one'   if $m100 == 1;
2813                return 'two'   if $m100 == 2;
2814                return 'few'   if $m100 == 3 || $m100 == 4;
2815                return 'other';
2816        },
2817
2818        # ── Welsh: zero/one/two/few/many/other ───────────────────────────────
2819        cy => sub {
2820                my $n = int($_[0]);
2821                return 'zero'  if $n == 0;
2822                return 'one'   if $n == 1;
2823                return 'two'   if $n == 2;
2824                return 'few'   if $n == 3;
2825                return 'many'  if $n == 6;
2826                return 'other';
2827        },
2828
2829        # ── Irish: one/two/few/many/other ────────────────────────────────────
2830        ga => sub {
2831                my $n = int($_[0]);
2832                return 'one'  if $n == 1;
2833                return 'two'  if $n == 2;
2834                return 'few'  if $n >= 3 && $n <= 6;
2835                return 'many' if $n >= 7 && $n <= 10;
2836                return 'other';
2837        },
2838
2839        # ── Maltese: one/two/few/many/other ──────────────────────────────────
2840        mt => sub {
2841                my $n    = int($_[0]);
2842                my $m100 = $n % 100;
2843                return 'one'  if $n == 1;
2844                return 'two'  if $n == 2;
2845                return 'few'  if $n == 0 || ($m100 >= 3  && $m100 <= 10);
2846                return 'many' if $m100 >= 11 && $m100 <= 19;
2847                return 'other';
2848        },
2849);
2850
2851sub plural_category
2852{
2853
31
570
        my ($self, $n) = @_;
2854
31
67
        Carp::croak('plural_category: $n must be defined') unless defined $n;
2855
28
33
        my $code = $self->language_code_alpha2() // return 'other';
2856
26
0
34
0
        my $rule = $PLURAL_RULES{$code} // sub { int($_[0]) == 1 ? 'one' : 'other' };
2857
26
30
        return $rule->($n);
2858}
2859
2860 - 2924
=head2 translation_file($dir, $ext)

Finds the translation file for the chosen language in the directory C<$dir>,
and returns its path.

It tries these file names, in order, and returns the first one that is a
readable regular file:

=over 4

=item 1. C<$dir/$lang-$sublang.$ext>  (for example F<en-gb.json>)

=item 2. C<$dir/$lang.$ext>           (for example F<en.json>)

=back

C<$ext> is the file extension. It is C<'json'> if you do not give it.
You can write it with or without the dot (C<'po'> or C<'.po'>).

Returns C<undef> when no such file exists, when no language was found,
when C<$dir> is C<undef>, or when C<$dir> or C<$ext> is unsafe (see below).
A directory, a device (such as F</dev/urandom>), a broken symbolic link or a
file you cannot read is never returned, even if it has the right name.

For safety, C<$dir> must be a non-empty string (not a reference) without
C<..> or a null byte, and C<$ext> may only contain letters, digits and C<->.
Control characters in a rejected value are shown as C<\xNN> in the warning.

=head3 API SPECIFICATION

=head4 Input

    [
        { type => 'string', min => 1 },
        { type => 'string', min => 1, matches => qr/^\.?[A-Za-z0-9-]+$/, optional => 1 },
    ]

=head4 Output

    {
        type     => 'string',
        optional => 1,
    }

=head3 EXAMPLE

    local $ENV{HTTP_ACCEPT_LANGUAGE} = 'en-gb';
    my $l = CGI::Lingua->new(supported => ['en-gb', 'en']);

    my $path = $l->translation_file('/var/www/i18n');
    # '/var/www/i18n/en-gb.json' if it exists,
    # else '/var/www/i18n/en.json' if it exists,
    # else undef

    # A different extension
    my $po = $l->translation_file('/var/www/i18n', 'po');

=head3 MESSAGES

These are warnings. C<translation_file()> does not die.

    "translation_file: unsafe directory '...' rejected"
    "translation_file: unsafe extension '...' rejected"

=cut
2925
2926sub translation_file
2927{
2928
46
11447
        my ($self, $dir, $ext) = @_;
2929
46
236
        local $!;       # a failed file test sets $!
2930
46
63
        return undef unless defined $dir;
2931
2932        # Reject traversal attempts in the directory argument.  A real translation
2933        # directory never needs '..' or null bytes; an empty name would mean "/",
2934        # and a reference would be used as the string "ARRAY(0x...)".
2935
45
204
        if(ref($dir) || !length($dir) || $dir =~ /\.\./ || $dir =~ /\x00/) {
2936
9
13
                $self->_warn({ warning => "translation_file: unsafe directory '" . _printable($dir) . "' rejected" });
2937
9
309
                return undef;
2938        }
2939
2940
36
80
        $ext //= 'json';
2941
36
36
        $ext =~ s/^\.//;    # accept '.json' or 'json'
2942
2943        # Valid extensions are letters, digits and hyphens only (e.g. 'json', 'po',
2944        # 'yml'); \z, not $, so "json\n" is refused too
2945
36
77
        unless($ext =~ /^[A-Za-z0-9\-]+\z/) {
2946
9
14
                $self->_warn({ warning => "translation_file: unsafe extension '" . _printable($ext) . "' rejected" });
2947
9
492
                return undef;
2948        }
2949
2950        # The codes become part of a path, so check their shape even though they
2951        # come from the negotiated language
2952
27
40
        my $lang = $self->language_code_alpha2();
2953
27
71
        return undef unless defined($lang) && ($lang =~ $LANG_CODE_RE);
2954
26
112
        my @candidates;
2955
26
40
        if(defined(my $sub = $self->sublanguage_code_alpha2())) {
2956
4
9
                push @candidates, "$lang-$sub" if $sub =~ $LANG_CODE_RE;
2957        }
2958
26
44
        push @candidates, $lang;
2959
2960        # A translation file must be a readable regular file: a directory, a
2961        # device such as /dev/urandom, a dangling symlink or an unreadable file
2962        # named en.json is not one, and handing it back would only fail later
2963
26
26
        for my $code (@candidates) {
2964
29
35
                my $path = "$dir/$code.$ext";
2965
29
250
                return $path if -f $path && -r _;
2966        }
2967
11
32
        return undef;
2968}
2969
2970# ── _printable ────────────────────────────────────────────────────────────
2971# Purpose:      Make a caller-supplied value safe to put in a log message:
2972#               control and non-ASCII characters (newlines that would forge
2973#               log lines, NULs, terminal escapes) become \xNN.
2974# Entry:        $value — any scalar; a reference is shown as its type.
2975# Exit:         A printable ASCII string.
2976sub _printable
2977{
2978
1247
762
        my $value = shift;
2979
1247
965
        return 'undef' unless defined($value);
2980
1245
920
        return ref($value) . ' reference' if ref($value);
2981
1239
244
1261
622
        $value =~ s/([^\x20-\x7e])/sprintf('\\x%02x', ord($1))/ge;
2982
1239
1188
        return $value;
2983}
2984
2985# ── _code2language ────────────────────────────────────────────────────────
2986# Purpose:      Translate a 2-char language code to its English name, with
2987#               optional CHI caching.
2988# Entry:        $code — 2-char ISO 639-1 code; must be defined and non-empty.
2989# Exit:         Human-readable language name string, or undef.
2990# Side Effects: Reads/writes cache.
2991sub _code2language
2992{
2993
242
1259
        my ($self, $code) = @_;
2994
2995
242
230
        return unless $code;
2996
240
256
        if(defined($self->{_country})) {
2997
3
7
                $self->_debug("_code2language $code, country ", $self->{_country});
2998        } else {
2999
237
261
                $self->_debug("_code2language $code");
3000        }
3001
3002
240
6459
        unless($self->{_cache}) {
3003
188
272
                return Locale::Language::code2language($code);
3004        }
3005
3006
52
95
        if(my $from_cache = $self->_cache_get_valid($CACHE_NS . "code2language:$code", $NAME_RE)) {
3007
8
14
                $self->_trace("_code2language found in cache $from_cache");
3008
8
151
                return $from_cache;
3009        }
3010
3011        # Compute, cache, then return the value separately —
3012        # CHI->set() is not guaranteed to return the stored value across all drivers
3013
44
80
        $self->_trace('_code2language not in cache, storing');
3014
44
1120
        my $name = Locale::Language::code2language($code);
3015
44
434967
        if(defined $name) {
3016
43
89
                $self->_cache_call($self->{_cache}, 'set', $CACHE_NS . "code2language:$code", $name, $CACHE_TTL_LONG);
3017        }
3018
44
65
        return $name;
3019}
3020
3021# ── _code2country ─────────────────────────────────────────────────────────
3022# Purpose:      Translate a 2-char country code to a Locale::Object::Country
3023#               object, suppressing the expected "No result found" warning.
3024# Entry:        $code — 2-char ISO 3166-1 alpha-2 code (any case).
3025# Exit:         Locale::Object::Country object, or undef.
3026# Side Effects: None beyond the Locale::Object::Country look-up.
3027sub _code2country
3028{
3029
165
601
        my ($self, $code) = @_;
3030
3031
165
243
        return unless $code;
3032
163
203
        if($self->{_country}) {
3033
47
95
                $self->_trace(">_code2country $code, country ", $self->{_country});
3034        } else {
3035
116
142
                $self->_trace(">_code2country $code");
3036        }
3037
3038
163
3684
        my $rc;
3039
163
364
        if($_locale_object_db_ok // 1) {
3040                # Suppress the routine "No result found" warning; catch the database-
3041                # absent exception that Windows installations sometimes throw.
3042
163
320
                local $SIG{__WARN__} = _warn_filter(qr/No result found in country table/);
3043
163
163
135
669
                eval { $rc = Locale::Object::Country->new(code_alpha2 => $code) };
3044
163
10547397
                if($@) {
3045
0
0
                        $_locale_object_db_ok = 0
3046                                if $@ =~ /database was not in/;
3047
0
0
                        $rc = undef;
3048                } else {
3049
163
897
                        $_locale_object_db_ok = 1;
3050                }
3051        }
3052
163
440
        $self->_trace('<_code2country ', $code || 'undef');
3053
163
5290
        return $rc;
3054}
3055
3056# ── _country_short_name ───────────────────────────────────────────────────
3057# Purpose:      Return the common short English name for an ISO 3166-1 alpha-2
3058#               code when Locale::Object's database is unavailable.  Uses
3059#               %COUNTRY_SHORT_NAMES overrides for codes where Locale::Codes
3060#               returns the full ISO official name rather than the short form.
3061# Entry:        $code — 2-char country code (any case).
3062# Exit:         Short name string, or undef.
3063sub _country_short_name
3064{
3065
27
3203
        my ($self, $code) = @_;
3066
27
31
        return unless defined($code);
3067
25
35
        my $lc = lc($code);
3068
25
43
        return $COUNTRY_SHORT_NAMES{$lc} if exists $COUNTRY_SHORT_NAMES{$lc};
3069        # Locale::Object may have partially initialised Locale::Codes::Country as a
3070        # dependency before we get here; suppress the spurious 'redefine' warning
3071        # that some Perl/Locale::Codes combinations produce on first full load.
3072
14
14
12
22
        { local $SIG{__WARN__} = _warn_filter(qr/redefined/);
3073
14
725
          require Locale::Codes::Country; }
3074
14
57479
        return Locale::Codes::Country::code2country($lc, 'alpha-2');
3075}
3076
3077# ── _code2countryname ─────────────────────────────────────────────────────
3078# Purpose:      Translate a 2-char country code to its English name string,
3079#               with optional CHI caching.
3080# Entry:        $code — 2-char ISO 3166-1 alpha-2 code.
3081# Exit:         Country name string, or undef.
3082# Side Effects: Reads/writes cache.
3083sub _code2countryname
3084{
3085
86
591
        my ($self, $code) = @_;
3086
3087
86
104
        return unless $code;
3088
84
144
        $self->_trace(">_code2countryname $code");
3089
3090
84
3158
        unless($self->{_cache}) {
3091
63
109
                my $country = $self->_code2country($code);
3092
63
152
                return $country->name if defined($country);
3093
3
5
                return $self->_country_short_name($code);
3094        }
3095
3096
21
40
        if(my $from_cache = $self->_cache_get_valid($CACHE_NS . "code2countryname:$code", $NAME_RE)) {
3097
2
4
                $self->_trace("_code2countryname found in cache $from_cache");
3098
2
60
                return $from_cache;
3099        }
3100
3101
19
21
        my $name;
3102
19
34
        if(my $country = $self->_code2country($code)) {
3103
18
35
                $name = $country->name();
3104        } else {
3105                # Locale::Object database absent (common on Windows); fall back to
3106                # Locale::Codes::Country with a short-name correction table.
3107
1
2
                $name = $self->_country_short_name($code);
3108        }
3109
3110
19
115
        if(defined($name)) {
3111
18
27
                $self->_debug('_code2countryname not in cache, storing');
3112
18
431
                $self->_trace('<_code2countryname ', $name);
3113
18
429
                $self->_cache_call($self->{_cache}, 'set', $CACHE_NS . "code2countryname:$code", $name, $CACHE_TTL_LONG);
3114
18
32
                return $name;
3115        }
3116
1
2
        $self->_trace('<_code2countryname undef');
3117
1
32
        return;
3118}
3119
3120# ── _log ──────────────────────────────────────────────────────────────────
3121# Purpose:      Append a message to $self->{messages} and forward to the
3122#               optional logger object.
3123# Entry:        $level — log level string (debug/info/notice/warn/trace/error);
3124#               @messages — one or more strings to concatenate.
3125# Exit:         void
3126# Side Effects: Mutates $self->{messages}; calls logger method if set.
3127sub _log
3128{
3129
4461
4935
        my ($self, $level, @messages) = @_;
3130
3131
4461
5434
        return unless ref($self) && scalar(@messages);
3132
3133
4458
5403
        my $text = join('', grep defined, @messages);
3134
4458
3199
        return unless length($text);
3135
4455
4455
2306
5190
        push @{$self->{'messages'}}, { level => $level, message => $text };
3136
3137
4455
4145
        if(my $logger = $self->{'logger'}) {
3138
3925
3950
                $logger->$level($text);
3139        }
3140}
3141
3142
2308
2308
2863
3927
sub _debug  { my $self = shift; $self->_log('debug',  @_) }
3143
8
8
963
11
sub _info   { my $self = shift; $self->_log('info',   @_) }
3144
6
6
799
11
sub _notice { my $self = shift; $self->_log('notice', @_) }
3145
2111
2111
2098
2104
sub _trace  { my $self = shift; $self->_log('trace',  @_) }
3146
3147# ── _warn ─────────────────────────────────────────────────────────────────
3148# Purpose:      Emit a warning through the logger (if set) or via Carp::carp.
3149# Entry:        A single hashref argument: { warning => 'message text' }.
3150#               All callers MUST use this structured form — plain-string calls
3151#               silently lose the message when no logger is configured.
3152# Exit:         void
3153# Side Effects: Calls logger->warn() or carp().
3154sub _warn
3155{
3156
220
291
        my $self = shift;
3157
3158        # Parse once; both branches need the same $msg.
3159
220
269
        my $params = Params::Get::get_params('warning', @_);
3160
220
3497
        my $msg    = (ref($params) ? $params->{'warning'} : undef) // join('', grep defined, @_);
3161
3162
220
252
        if(defined($self->{'logger'})) {
3163
202
283
                $self->{'logger'}->warn($msg);
3164        } else {
3165
18
21
                $self->_log('warn', $msg);
3166
18
540
                Carp::carp($msg);
3167        }
3168}
3169
3170# ── _warn_filter ──────────────────────────────────────────────────────────
3171# Purpose:      Build a $SIG{__WARN__} handler that drops warnings matching
3172#               $skip and passes every other warning on to the handler that
3173#               was active when the filter was built.
3174# Entry:        $skip — compiled regex of warnings to drop.
3175# Exit:         Code reference suitable for "local $SIG{__WARN__} = ...".
3176# Notes:        A plain "warn" inside a __WARN__ handler bypasses every handler
3177#               and goes straight to STDERR, so the caller's handler (a logger,
3178#               Test::Warnings, ...) would never see the warning; hence the
3179#               explicit call to the previous handler.
3180sub _warn_filter
3181{
3182
422
318
        my $skip = shift;
3183
422
410
        my $prev = $SIG{__WARN__};
3184
3185        return sub {
3186
24
82587
                return if $_[0] =~ $skip;
3187
1
2
                if(ref($prev) eq 'CODE') {
3188
1
2
                        $prev->(@_);
3189                } else {
3190
0
0
                        warn @_;
3191                }
3192
422
1235
        };
3193}
3194
3195# ── Pure-Perl IP-validation helpers ──────────────────────────────────────────
3196# These are installed into the CGI::Lingua symbol table as is_ipv4() etc. when
3197# Data::Validate::IP / NetAddr::IP are unavailable (see country()).
3198
3199sub _is_ipv4 {
3200
255
17988
        my $ip = shift;
3201        # /a: without it \d also matches non-ASCII digits such as U+0661, which
3202        # then compare as 0 and make "\x{661}.\x{661}.\x{661}.\x{661}" look valid
3203
255
720
        return unless defined($ip) && $ip =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z/a;
3204
227
908
1297
2091
        return !(grep { $_ > 255 } ($1, $2, $3, $4));
3205}
3206
3207sub _is_ipv6 {
3208
21
5332
        my $ip = shift;
3209
21
53
        return unless defined($ip) && $ip =~ /:/;
3210        # Socket::inet_pton is core since Perl 5.14 and validates IPv6 reliably
3211
17
23
        require Socket;
3212
17
22
        if(defined &Socket::inet_pton) {
3213
11
53
                return defined Socket::inet_pton(Socket::AF_INET6(), $ip);
3214        }
3215        # Structural fallback: hex+colons, at least two colons, at most one ::
3216        # (tr/// counts characters, not "::" pairs, so count matches of /::/ instead)
3217
6
28
        return ($ip =~ /^[0-9a-fA-F:]+\z/ || $ip =~ /^[0-9a-fA-F:]+:\d{1,3}(?:\.\d{1,3}){3}\z/a)
3218                && (() = $ip =~ /::/g) <= 1
3219                && ($ip =~ tr/://) >= 2;
3220}
3221
3222sub _is_private_ip {
3223
236
3745
        my $ip = shift;
3224
236
220
        return unless defined($ip);
3225
235
443
        if($ip =~ /^(\d+)\.(\d+)/) {
3226
229
312
                my ($a, $b) = ($1 + 0, $2 + 0);
3227
229
238
                return 1 if $a == 10;
3228
224
273
                return 1 if $a == 172 && $b >= 16 && $b <= 31;
3229
221
767
                return 1 if $a == 192 && $b == 168;
3230
214
252
                return 1 if $a == 169 && $b == 254;     # link-local / APIPA
3231
213
269
                return 0;
3232        }
3233
6
10
        return 1 if $ip =~ /^fe[89ab][0-9a-f]:/i;  # fe80::/10 link-local
3234
5
7
        return 1 if $ip =~ /^f[cd]/i;              # fc00::/7 ULA
3235
4
5
        return 0;
3236}
3237
3238sub _is_loopback_ip {
3239
220
1905
        my $ip = shift;
3240
220
224
        return unless defined($ip);
3241
219
208
        return 1 if $ip eq '::1';
3242
217
315
        return $ip =~ /^127\./;
3243}
3244
3245 - 3936
=head1 COMMON PITFALLS

=over 4

=item * B<Configuration files and environment variables win over your arguments>

L</new> passes its arguments to L<Object::Configure>.
If a configuration file (C<config_file>) or an environment variable such as
C<CGI__Lingua__supported> sets a value, that value B<replaces> the value you
gave to C<new()>.

Values are replaced, not merged. If you pass C<< supported => ['en', 'fr'] >>
and the configuration file says C<supported: [de]>, the object supports only
C<de>, not C<en>, C<fr> and C<de>.
For nested values (hashes inside hashes) see the rules in L<Object::Configure>.

=item * B<A copy made with C<< $object->new() >> keeps old answers>

When you call C<new()> on an object, the copy starts with every answer that the
original has already worked out. So if the original has already called
L</language>, the copy returns the same language,
even if you give the copy a different C<supported> list:

    my $l = CGI::Lingua->new(supported => ['en', 'fr']);
    print $l->language();             # "French"
    my $copy = $l->new(supported => ['en']);
    print $copy->language();          # still "French"

Make the copy before you call any other method, or create a new object with
C<< CGI::Lingua->new() >>.

=item * B<'Unknown' is not the same as undef>

Some methods return the B<string> C<'Unknown'> and some return C<undef>
when they do not know the answer. C<'Unknown'> is a true value in Perl,
so C<if($l-E<gt>language())> is always true.

    Method                    When it does not know
    ------------------------  -----------------------------
    language()                'Unknown'  (never undef)
    requested_language()      'Unknown'  (never undef)
    sublanguage()             undef
    language_code_alpha2()    undef
    sublanguage_code_alpha2() undef
    country()                 undef  ('Unknown' for EU addresses)
    locale()                  undef
    time_zone()               undef
    translation_file()        undef
    is_rtl()                  0
    text_direction()          'ltr'
    plural_category()         'other'

Always test with C<eq 'Unknown'> or C<defined()>, as the table shows.

C<undef> really is one value, even in list context, so it is safe to build a
hash from the results:

    my %vars = (country => $l->country(), zone => $l->time_zone());
    # $vars{zone} is the time zone even when country() is undef

=item * B<plural_category() returns 'other' when there is no language>

When no supported language was found, C<plural_category(1)> returns
C<'other'>, not C<'one'>. Have an C<'other'> form for every message.
C<plural_category(undef)> dies.

=item * B<C<supported =E<gt> 0> and C<supported =E<gt> ''> count as missing>

A false value for C<supported> is treated as if you did not give it,
so C<new()> dies with "You must give a list of supported languages".

=item * B<Answers are worked out once, then remembered>

The first call to a language method reads C<%ENV> and remembers the answer.
If you change C<%ENV> afterwards, the object does not notice.
Create a new object for each web request.

=item * B<country() reads REMOTE_ADDR when you call it>

C<country()> reads C<$ENV{REMOTE_ADDR}> when you B<call> it,
not when you create the object. In tests, call C<country()> while
C<REMOTE_ADDR> still has the value you want:

    {
        local $ENV{REMOTE_ADDR} = '8.8.8.8';
        my $l = CGI::Lingua->new(supported => ['en']);
        $country = $l->country();    # correct: inside the block
    }

=item * B<A bad header is ignored completely>

If C<HTTP_ACCEPT_LANGUAGE> is longer than 256 characters, or contains a
character that is not allowed (see L</ENCODING>), the whole header is ignored
with a warning. CGI::Lingua then uses C<LANG> or the IP address instead.

=item * B<The supported list holds tags, not names>

C<< supported => ['english', 'fr'] >> supports only French: C<'english'> is not a
language tag, so it is dropped (with a warning). Use C<'en'>.

=item * B<Language names are in English>

L</language> returns C<'French'>, not C<'Francais'>, and C<'German'>, not
C<'Deutsch'>. Use L</language_code_alpha2> if you want to show the name in
the language itself.

=item * B<The logger you pass is replaced>

L<Object::Configure> always changes C<logger> into a new L<Log::Abstraction>
object, so C<< $l->{logger} >> is not the object you passed.

=item * B<Whole objects are only cached when REMOTE_ADDR is set>

The answers of an object are read from the cache in L</new>, and written to it
when the object is destroyed, only when C<REMOTE_ADDR> is set.
On the command line, only smaller look-ups (such as code-to-name) are cached.

=item * B<Local geo databases can be out of date>

Old F<GeoIP.dat> files can give the wrong country for some addresses
(see L</country>).

=back

=head1 ENCODING

CGI::Lingua works with ASCII text. This table shows what each input accepts.

    Input                   Accepted characters        Non-ASCII / UTF-8 / emoji
    ----------------------  -------------------------  -------------------------
    supported               language tags (en, en-gb,   no: entry is ignored,
                            es-419); a plain string     with a warning
                            must be 2-5 chars
    HTTP_ACCEPT_LANGUAGE    A-Z a-z 0-9 - , ; = . *     no: header is ignored
                            space and tab (never CR or
                            LF); max 256 chars
    lang (CGI::Info)        as HTTP_ACCEPT_LANGUAGE     no: parameter is ignored
    LANG                    A-Z a-z 0-9 _ . -           no: LANG is ignored
    HTTP_USER_AGENT         printable ASCII             no: user agent ignored
                            (0x20-0x7E), max 512 chars
    REMOTE_ADDR             IPv4 or IPv6 address        no: address is rejected
    GEOIP_COUNTRY_CODE      exactly two of A-Z          no: value is ignored
    HTTP_CF_IPCOUNTRY       exactly two of A-Z          no: value is ignored
    translation_file $dir   any, except ".." and NUL    yes, if your file system
                                                        supports it (pass bytes
                                                        encoded as the file
                                                        system expects)
    translation_file $ext   A-Z a-z 0-9 -               no: undef is returned
    plural_category $n      a number                    not applicable

All values that CGI::Lingua returns are plain ASCII: language names
(C<'French'>), country names (C<'Reunion'>), codes and time zone names.
You do not need to decode them.

=head1 CONFIGURATION VARIABLES

Two package variables say where CGI::Lingua looks for files on the local
machine. You do not normally need to change them; tests and unusual
installations can, with C<local>:

=over 4

=item * C<$CGI::Lingua::ZONE_FILE>

The file that L</time_zone> reads when there is no C<REMOTE_ADDR> (command-line
use). The default is F</etc/timezone>. Only a readable regular file is used,
and only its first 256 bytes are read; otherwise L<DateTime::TimeZone> is used.

    local $CGI::Lingua::ZONE_FILE = '/srv/myapp/timezone';

=item * C<@CGI::Lingua::GEOIP_DAT>

The places where L</country> and L</time_zone> look for the legacy MaxMind
F<GeoIP.dat> used by L<Geo::IP>; the first readable regular file is used. The
default is F</usr/share/GeoIP/GeoIP.dat> and F</usr/local/share/GeoIP/GeoIP.dat>
(and F<c:/GeoIP/GeoIP.dat> first, on Windows). A file that L<Geo::IP> cannot
open is skipped with the warning C<"Can't open ... with Geo::IP; not using it">.

    local @CGI::Lingua::GEOIP_DAT = ('/opt/geo/GeoIP.dat');

=back

=head1 LIMITATIONS

=over 4

=item * B<is_rtl() covers primary-script RTL languages only>

C<is_rtl()> returns true for the 10 ISO 639-1 codes whose overwhelmingly
dominant script is right-to-left.  Languages with script variants (e.g.
Azerbaijani C<az>, which uses Latin in modern Azerbaijan but Arabic in Iran)
are treated as LTR.  If you serve content in multiple scripts of the same
language, inspect the sublanguage or Accept-Language header directly.

=item * B<plural_category() uses embedded CLDR rules, not Locale::CLDR>

The embedded rules cover ~70 languages and truncate fractional C<$n> to an
integer.  For full CLDR v42 accuracy (including fractional forms and
languages not in the table) install and use C<Locale::CLDR> directly.

=item * B<The logger is always a Log::Abstraction object>

The C<logger> argument can be an object with C<warn()>, C<info()> and
C<error()> methods, or any value that L<Object::Configure> accepts (such as an
array reference or a hash reference of options).  In every case
L<Object::Configure> replaces it with a L<Log::Abstraction> object.  An object
that is missing one of the three methods is rejected by L</new>.

=item * B<es-419 sublanguage returns undef>

Three-part regional codes such as C<es-419> (Latin American Spanish) do not
resolve to a C<sublanguage()> value because ISO 3166-1 does not define '419'.
This is a known limitation of the Locale::Object layer.

=item * B<Whois lookups are slow and unreliable>

Without C<IP::Country>, C<Geo::IP>, or C<Geo::IPfree> installed, C<country()>
falls back to Whois queries against live RIPE/ARIN/IANA servers.  These can
time out under load.  Install at least one local geo-database module and enable
the CHI cache to avoid this.

=item * B<Private methods are accessible from outside the package>

The C<_*> methods use the naming convention for privacy but Perl does not enforce
it.  C<Sub::Private> or C<Sub::Protected> should be added once all white-box
tests (C<t/function.t>, C<t/extended_tests.t>) are updated to use the public API
exclusively.

=item * B<IPv4-mapped IPv6 addresses are normalised to IPv4>

C<REMOTE_ADDR> values in the form C<::ffff:a.b.c.d> (RFC 4291 section 2.5.5)
are silently rewritten to the embedded C<a.b.c.d> IPv4 address before any
geo-lookup.  This is correct for country detection purposes but means the raw
address string is not preserved in cache keys or log messages.

=item * B<EU country code is irresolvable (with one exception)>

IP addresses that Whois reports as country C<EU> are mapped to C<'Unknown'>
unless they fall within Baidu's known subnet (RT-86809).  There is no ISO
3166-1 country code for the European Union.

=item * B<country() does not cache undef results>

When C<country()> cannot determine a country (private IPs, loopback,
unresolvable addresses), it returns C<undef> without storing the result.  A
second call on the same object repeats the full validation pipeline.  This is
intentional: C<country()> reads C<REMOTE_ADDR> at call time rather than at
construction time, so caching C<undef> would return a wrong answer if
C<REMOTE_ADDR> changes between calls.  In practice this is rarely a problem
because C<country()> is called once per request and CGI applications typically
create a fresh object per request.

=back

=head1 AUTHOR

Nigel Horne, C<< <njh at nigelhorne.com> >>

=head1 BUGS

If C<HTTP_ACCEPT_LANGUAGE> contains a sub-tag with a 3-digit UN M.49 region
code (e.g. C<es-419> for Latin American Spanish), C<sublanguage()> returns
C<undef> because ISO 3166-1 does not define numeric codes.

Uses L<I18N::AcceptLanguage> to find the highest priority accepted language.
This means that if you support languages at a lower priority, it may be missed.

Please report any bugs or feature requests to C<bug-cgi-lingua at rt.cpan.org>,
or through the web interface at
L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=CGI-Lingua>.
I will be notified, and then you'll
automatically be notified of progress on your bug as I make changes.

=head1 SEE ALSO

=over 4

=item * L<Configure an Object at Runtime|Object::Configure>

=item * L<Test Dashboard|https://nigelhorne.github.io/CGI-Lingua/coverage/>

=item * VWF - Versatile Web Framework L<https://github.com/nigelhorne/vwf>

=item * L<HTTP::BrowserDetect>

=item * L<I18N::AcceptLanguage>

=item * L<Locale::Country>

=back

=head1 SUPPORT

This module is provided as-is without any warranty.

You can find documentation for this module with the perldoc command.

    perldoc CGI::Lingua

You can also look for information at:

=over 4

=item * MetaCPAN

L<https://metacpan.org/release/CGI-Lingua>

=item * RT: CPAN's request tracker

L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=CGI-Lingua>

=item * CPANTS

L<http://cpants.cpanauthors.org/dist/CGI-Lingua>

=item * CPAN Testers' Matrix

L<http://matrix.cpantesters.org/?dist=CGI-Lingua>

=item * CPAN Testers Dependencies

L<http://deps.cpantesters.org/?module=CGI::Lingua>

=back

=encoding utf-8

=head1 FORMAL SPECIFICATION

This section describes each method in the Z notation.
You do not need it to use the module.

=head2 Types and state

    [LANGTAG, LANGNAME, CNAME, IPADDR, PATH, ZONE, LOCALE]

    CC2      == { s : seq CHAR | #s = 2 ∧ ran s ⊆ 'a'..'z' }
    LC2      == CC2
    PLURAL   ::= zero | one | two | few | many | other
    GEO      ::= unknown | absent | present
    DIR      ::= ltr | rtl
    RTL_LANGS == { ar, dv, fa, he, ku, ps, sd, ug, ur, yi }

    âŠ¥ marks a value that has not been worked out yet (or is undef).

    â”Œâ”€ Lingua ──────────────────────────────────────────────────────
    â”‚ supported   : seq LANGTAG
    â”‚ slanguage   : LANGNAME ∪ {Unknown, ⊥}
    â”‚ rlanguage   : LANGNAME ∪ {Unknown, ⊥}
    â”‚ code2       : LC2 ∪ {⊥}
    â”‚ subcode2    : CC2 ∪ {⊥}
    â”‚ sublanguage : CNAME ∪ {⊥}
    â”‚ country     : CC2 ∪ {Unknown, ⊥}
    â”‚ locale      : LOCALE ∪ {⊥}
    â”‚ timezone    : ZONE ∪ {⊥}
    â”‚ ipc, gip, gipf : GEO
    â”‚ dont_use_ip : 𝔹
    â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    â”‚ slanguage ∉ {Unknown, ⊥} ⇒
    â”‚     code2 ≠ ⊥ ∧ (∃ t : ran supported • base(t) = code2)
    â”‚ slanguage = Unknown ⇒ code2 = ⊥ ∧ subcode2 = ⊥
    â”‚ subcode2 ≠ ⊥ ⇒ code2 ≠ ⊥
    â”‚ sublanguage ≠ ⊥ ⇒ subcode2 ≠ ⊥
    â”‚ (slanguage = ⊥) ⇔ (rlanguage = ⊥)
    â””───────────────────────────────────────────────────────────────

    request   : ENV ⇸ seq (LANGTAG × ℝ)   -- lang param, Accept-Language or LANG
    negotiate : seq (LANGTAG × ℝ) × seq LANGTAG ⇸ LANGTAG
    geo       : IPADDR ⇸ CC2              -- first answer of the geo sources
    official  : CC2 ⇸ LANGTAG             -- official language of a country

    â”Œâ”€ Resolve ─────────────────────────────────────────────────────
    â”‚ ΔLingua
    â”‚ env? : ENV
    â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    â”‚ slanguage = ⊥
    â”‚ let t == negotiate(request(env?), supported) •
    â”‚   t ≠ ⊥ ⇒ slanguage' = name(base(t)) ∧ code2' = base(t)
    â”‚          âˆ§ subcode2' = variety(t)
    â”‚   t = ⊥ ∧ ¬ dont_use_ip ∧ official(geo(env?.REMOTE_ADDR)) ∈ ran supported
    â”‚          â‡’ slanguage' = name(official(geo(env?.REMOTE_ADDR)))
    â”‚   otherwise slanguage' = Unknown
    â”‚ rlanguage' ≠ ⊥
    â””───────────────────────────────────────────────────────────────

    EnsureResolved ≙ (Resolve ∨ (ΞLingua ∧ slanguage ≠ ⊥))

=head2 new

    â”Œâ”€ New ─────────────────────────────────────────────────────────
    â”‚ Lingua'
    â”‚ supported? : LANGTAG ∪ seq LANGTAG
    â”‚ cache? : CACHE ∪ {⊥}
    â”‚ env? : ENV
    â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    â”‚ supported? ∈ LANGTAG ⇒ 2 ≤ #supported? ≤ 5
    â”‚ supported' = (if supported? ∈ LANGTAG then ⟨supported?⟩ else supported?)
    â”‚                 â†¾ SUPPORTED_TAGS        -- other entries dropped, with a warning
    â”‚ ipc' = gip' = gipf' = unknown
    â”‚ dont_use_ip' = dont_use_ip?             -- never taken from the cache
    â”‚ let ip == untaint_ip(env?.REMOTE_ADDR); saved == decode(cache?(key(ip))) •
    â”‚ (cache? ≠ ⊥ ∧ ip ≠ ⊥ ∧ key(ip) ∈ dom cache?
    â”‚      âˆ§ (∀ f : dom saved ∩ dom RESTORABLE • saved(f) ∈ RESTORABLE(f)))
    â”‚     â‡’ (∀ f : dom saved ∩ dom RESTORABLE • θLingua'.f = saved(f))
    â”‚ (cache? ≠ ⊥ ∧ ip ≠ ⊥ ∧ key(ip) ∈ dom cache?
    â”‚      âˆ§ (∃ f : dom saved ∩ dom RESTORABLE • saved(f) ∉ RESTORABLE(f)))
    â”‚     â‡’ key(ip) ∉ dom cache?'            -- poisoned entry removed, with a warning
    â”‚ otherwise
    â”‚     slanguage' = rlanguage' = country' = locale' = timezone' = ⊥
    â””───────────────────────────────────────────────────────────────

    RESTORABLE : FIELD ⇸ â„™ seq CHAR
    RESTORABLE ≙ { slanguage, rlanguage, sublanguage ↦ NAME,
                   slanguage_code_alpha2, sublanguage_code_alpha2 ↦ LANGCODE,
                   country ↦ CC2 ∪ {Unknown} }

    NewError ≙ [ supported? = ⊥ ∨ supported? ∉ LANGTAG ∪ seq LANGTAG
                 âˆ¨ (info? ≠ ⊥ ∧ ¬ can(info?, lang) ∧ ¬ can(info?, AUTOLOAD)) ] ⇒ croak

=head2 language

    â”Œâ”€ Language ────────────────────────────────────────────────────
    â”‚ EnsureResolved
    â”‚ result! : LANGNAME ∪ {Unknown}
    â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    â”‚ result! = slanguage'
    â””───────────────────────────────────────────────────────────────

    PreferredLanguage ≙ Language
    Name              â‰™ Language

=head2 sublanguage

    â”Œâ”€ Sublanguage ─────────────────────────────────────────────────
    â”‚ EnsureResolved
    â”‚ result! : CNAME ∪ {⊥}
    â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    â”‚ result! = sublanguage'
    â””───────────────────────────────────────────────────────────────

=head2 language_code_alpha2

    â”Œâ”€ LanguageCodeAlpha2 ──────────────────────────────────────────
    â”‚ EnsureResolved
    â”‚ result! : LC2 ∪ {⊥}
    â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    â”‚ result! = code2'
    â”‚ slanguage' = Unknown ⇒ result! = ⊥
    â””───────────────────────────────────────────────────────────────

    CodeAlpha2 ≙ LanguageCodeAlpha2

=head2 sublanguage_code_alpha2

    â”Œâ”€ SublanguageCodeAlpha2 ───────────────────────────────────────
    â”‚ EnsureResolved
    â”‚ result! : CC2 ∪ {⊥}
    â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    â”‚ result! = subcode2'
    â””───────────────────────────────────────────────────────────────

=head2 requested_language

    â”Œâ”€ RequestedLanguage ───────────────────────────────────────────
    â”‚ EnsureResolved
    â”‚ result! : seq CHAR
    â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    â”‚ result! = rlanguage'
    â”‚ -- rlanguage' = name(b) ⁀ " (" ⁀ cname(v) ⁀ ")" when the visitor
    â”‚ -- asked for base b with variety v; name(b) when no variety;
    â”‚ -- Unknown when nothing was asked for.
    â””───────────────────────────────────────────────────────────────

=head2 country

    â”Œâ”€ Country ─────────────────────────────────────────────────────
    â”‚ ΔLingua
    â”‚ env? : ENV
    â”‚ result! : CC2 ∪ {Unknown, ⊥}
    â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    â”‚ country ≠ ⊥ ⇒ result! = country ∧ θLingua' = θLingua
    â”‚ country = ⊥ ⇒
    â”‚   ( env?.GEOIP_COUNTRY_CODE ∈ CC2↑ ⇒ result! = lc(env?.GEOIP_COUNTRY_CODE) )
    â”‚   ( env?.HTTP_CF_IPCOUNTRY ∈ CC2↑ \ {XX} ⇒ result! = lc(env?.HTTP_CF_IPCOUNTRY) )
    â”‚   ( ip = canon(env?.REMOTE_ADDR) ∈ PUBLIC_IP ⇒ result! = norm(geo(ip)) )
    â”‚   ( ip ∉ PUBLIC_IP ⇒ result! = ⊥ )
    â”‚ country' = result!
    â”‚ ipc' ≠ unknown ∨ ipc' = ipc ;  gip' ≠ unknown ∨ gip' = gip
    â”‚ -- norm maps hk ↦ cn, eu ↦ Unknown (or cn for the Baidu subnet),
    â”‚ -- and anything ∉ CC2 ↦ ⊥
    â””───────────────────────────────────────────────────────────────

    where CC2↑ is CC2 in upper case, and canon(::ffff:a.b.c.d) = a.b.c.d.

=head2 locale

    â”Œâ”€ Locale ──────────────────────────────────────────────────────
    â”‚ ΔLingua
    â”‚ env? : ENV
    â”‚ result! : LOCALE ∪ {⊥}
    â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    â”‚ locale ≠ ⊥ ⇒ result! = locale ∧ θLingua' = θLingua
    â”‚ locale = ⊥ ⇒ result! = first ⊥-free of
    â”‚     âŸ¨ uatag(env?.HTTP_USER_AGENT), browserdetect(env?.HTTP_USER_AGENT),
    â”‚       loc(Country.result!), loc(env?.GEOIP_COUNTRY_CODE) ⟩
    â”‚ result! ≠ ⊥ ⇒ locale' = result!
    â””───────────────────────────────────────────────────────────────

=head2 time_zone

    â”Œâ”€ TimeZone ────────────────────────────────────────────────────
    â”‚ ΔLingua
    â”‚ env? : ENV
    â”‚ result! : ZONE ∪ {⊥}
    â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    â”‚ timezone ≠ ⊥ ⇒ result! = timezone
    â”‚ timezone = ⊥ ∧ env?.REMOTE_ADDR ≠ ⊥ ⇒
    â”‚     result! = first ⊥-free of ⟨ geoip_tz(ip), ipapi_tz(ip) ⟩
    â”‚ timezone = ⊥ ∧ env?.REMOTE_ADDR = ⊥ ⇒
    â”‚     result! = first ⊥-free of ⟨ etc_timezone, local_tz ⟩
    â”‚ result! ∉ ZONE ⇒ result! = ⊥
    â”‚ timezone' = result!
    â””───────────────────────────────────────────────────────────────

=head2 is_rtl

    â”Œâ”€ IsRtl ───────────────────────────────────────────────────────
    â”‚ EnsureResolved
    â”‚ result! : {0, 1}
    â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    â”‚ result! = 1 ⇔ code2' ∈ RTL_LANGS
    â””───────────────────────────────────────────────────────────────

=head2 text_direction

    â”Œâ”€ TextDirection ───────────────────────────────────────────────
    â”‚ IsRtl
    â”‚ dir! : DIR
    â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    â”‚ dir! = (if result! = 1 then rtl else ltr)
    â””───────────────────────────────────────────────────────────────

=head2 plural_category

    PLURAL_RULES : LC2 ⇸ (â„• → PLURAL)
    english == λ n : â„• • (if n = 1 then one else other)

    â”Œâ”€ PluralCategory ──────────────────────────────────────────────
    â”‚ EnsureResolved
    â”‚ n? : ℝ
    â”‚ result! : PLURAL
    â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    â”‚ n? ≠ ⊥
    â”‚ code2' = ⊥ ⇒ result! = other
    â”‚ code2' ∈ dom PLURAL_RULES ⇒ result! = PLURAL_RULES(code2')(trunc n?)
    â”‚ code2' ∉ dom PLURAL_RULES ∪ {⊥} ⇒ result! = english(trunc n?)
    â””───────────────────────────────────────────────────────────────

    PluralError ≙ [ n? = ⊥ ] ⇒ croak

=head2 translation_file

    â”Œâ”€ TranslationFile ─────────────────────────────────────────────
    â”‚ EnsureResolved
    â”‚ dir? : PATH ∪ {⊥}
    â”‚ ext? : seq CHAR ∪ {⊥}
    â”‚ files : â„™ PATH                       -- readable regular files
    â”‚ result! : PATH ∪ {⊥}
    â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    â”‚ e == (if ext? = ⊥ then "json" else strip_dot(ext?))
    â”‚ (dir? = ⊥ ∨ dir? = ⟨⟩ ∨ ".." ⊆ dir? ∨ NUL ∈ ran dir?
    â”‚     âˆ¨ ¬ (ran e ⊆ ALNUM ∪ {'-'})) ⇒ result! = ⊥
    â”‚ otherwise
    â”‚   cands == ⟨ code2' ⁀ "-" ⁀ subcode2' | subcode2' ≠ ⊥ ⟩ ⁀ ⟨ code2' | code2' ≠ ⊥ ⟩
    â”‚   result! = first p : ran cands • dir? ⁀ "/" ⁀ p ⁀ "." ⁀ e ∈ files
    â”‚             (⊥ if there is none)
    â””───────────────────────────────────────────────────────────────

=head1 STATE DIAGRAM

A CGI::Lingua object has several independent parts. Each part starts
empty and is filled in the first time a method needs it.
After that, the part does not change (it is remembered).

Part 1: the language (used by language(), sublanguage(),
language_code_alpha2(), sublanguage_code_alpha2(), requested_language(),
is_rtl(), text_direction(), plural_category(), translation_file())

                                  new()
                                    |
                                    v
                      +---------------------------+
                      |   LANGUAGE NOT CHECKED    |
                      |   _slanguage = undef      |
                      +---------------------------+
                                    |
                 first call to any language method:
                 read the lang param, HTTP_ACCEPT_LANGUAGE or LANG,
                 and compare it with the supported list
                                    |
                  +-----------------+------------------+
                  |                                    |
             a match                               no match
                  |                                    |
                  |                      +-------------+-------------+
                  |                      |                           |
                  |               dont_use_ip is false         dont_use_ip
                  |               side effect: country()       is true
                  |               is called (Part 2), and           |
                  |               the country's official            |
                  |               language is tried                 |
                  |                 |               |               |
                  |            supported     not supported          |
                  v                 v               v               v
           +----------------------------+   +------------------------------+
           |          MATCHED           |   |          UNMATCHED           |
           | language() = 'English' ... |   | language() = 'Unknown'       |
           | language_code_alpha2() set |   | language_code_alpha2() undef |
           +----------------------------+   +------------------------------+

        Both MATCHED and UNMATCHED are final: later calls return the same
        answers, even if %ENV changes.

Part 2: the country (used by country(), locale(), and the language IP path)

    +----------------------+   country(): REMOTE_ADDR missing, invalid,
    |  COUNTRY NOT KNOWN   |---------------------------------------------+
    |  _country = undef    |   private or loopback, or no source answers |
    +----------------------+<--------------------------------------------+
               |                (returns undef; NOT remembered, so the
               |                 next call tries again)
               | country(): a source gives a valid code
               | side effects: geo modules loaded, sentinels set,
               |               value stored in cache (if any)
               v
    +----------------------+
    |    COUNTRY KNOWN     |  later calls return the same value
    |  _country = 'xx'     |  (or 'Unknown' for EU addresses)
    +----------------------+

Part 3: each geo module (IP::Country, Geo::IP, Geo::IPfree)

    +-----------+  first use: module loads   +-----------+
    |  UNKNOWN  |--------------------------->|  PRESENT  |
    |   (-1)    |                            |    (1)    |
    +-----------+--------------------------->+-----------+
                   first use: module or      +-----------+
                   database is missing ----->|  ABSENT   |
                                             |    (0)    |
                                             +-----------+

Part 4: locale() and time_zone()

    +------------+  locale() / time_zone() finds a value  +------------+
    | NOT KNOWN  |--------------------------------------->|   KNOWN    |
    |  (undef)   |<-------+                               | remembered |
    +------------+        | nothing found: returns undef, +------------+
          |               | stays NOT KNOWN (time_zone()
          +---------------+ also warns)

Part 5: the life of the object

    new() ----------------------> ALIVE ----------------------> DESTROYED
      |   no cache entry, or        ^      object goes out of     side effect:
      |   no REMOTE_ADDR            |      scope (DESTROY)        if cache and
      |                             |                             REMOTE_ADDR are
      +---- valid cache entry ------+                             set and no entry
      |     found (the six answer   |                             exists yet, the
      |     fields restored, each   |                             answers are saved
      |     checked; geo sentinels  |                             to the cache as
      |     reset to UNKNOWN)       |                             JSON
      |                             |
      +---- malformed cache entry --+
      |     found (removed, warned;
      |     answers worked out again)
      |
      +---- REMOTE_ADDR not a valid address: the cache is not used
      |
      +---- bad arguments ---> croak (no object)

    $obj->new(...) makes a copy in the ALIVE state that keeps all the
    answers of $obj (all parts keep their current state).

=head1 LICENSE AND COPYRIGHT

Copyright 2010-2026 Nigel Horne.

Usage is subject to the GPL2 licence terms.
If you use it,
please let me know.

=cut
3937
39381; # End of CGI::Lingua