CGI-Lingua

 view release on metacpan or  search on metacpan

README.md  view on Meta::CPAN


    Input:  none beyond $self
    Returns: Str - e.g. "English (United Kingdom)" or "Unknown"

## country

Returns the two-character country code of the remote end in lowercase.

If [IP::Country](https://metacpan.org/pod/IP%3A%3ACountry), [Geo::IPfree](https://metacpan.org/pod/Geo%3A%3AIPfree) or [Geo::IP](https://metacpan.org/pod/Geo%3A%3AIP) is installed,
CGI::Lingua will make use of that, otherwise, it will do a Whois lookup.
If you do not have any of those installed I recommend you use the
caching capability of CGI::Lingua.

### API SPECIFICATION

    Input:  none beyond $self
    Returns: Str (2 lowercase chars) | undef
      'Unknown' is only returned in the Baidu-EU special case via _handle_eu_country.

### EXAMPLE

    # With mod_geoip (fastest - no IP lookup at all):
    local $ENV{GEOIP_COUNTRY_CODE} = 'DE';
    print $l->country();   # "de"

    # With REMOTE_ADDR and IP::Country installed:
    local $ENV{REMOTE_ADDR} = '8.8.8.8';
    print $l->country();   # "us" (depends on geo database)

### MESSAGES

    "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"
    "Can't determine country from LAN connection X"
    "Can't determine country from loopback connection X"
    "cache contains a numeric country: N"
    "IP matches to a numeric country"

### PSEUDOCODE

    1. Return cached _country if set
    2. Check GEOIP_COUNTRY_CODE env var (mod_geoip); validate /^[A-Z]{2}$/
    3. Check HTTP_CF_IPCOUNTRY (Cloudflare); skip 'XX'; validate /^[A-Z]{2}$/
    4. Untaint and validate REMOTE_ADDR; return undef if absent or invalid
    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 or LWP::Simple)
    11. Last resort: Net::Whois::IP then Net::Whois::IANA
    12. Sanitise: discard numeric, normalise HK->CN, handle EU special case
    13. Store in CHI cache; return result

## locale

HTTP doesn't have a way of transmitting a browser's localisation information
which would be useful for default currency, date formatting, etc.

This method attempts to detect the information, but it is a best guess
and is not 100% reliable.  But it's better than nothing ;-)

Returns a [Locale::Object::Country](https://metacpan.org/pod/Locale%3A%3AObject%3A%3ACountry) object.

### EXAMPLE

    local $ENV{REMOTE_ADDR} = '8.8.8.8';
    my $locale = $l->locale();
    if (defined $locale) {
        print $locale->name();          # e.g. "United States"
        print $locale->currency_code(); # e.g. "USD"
    }

### API SPECIFICATION

    Input:  none beyond $self
    Returns: Locale::Object::Country | undef

### PSEUDOCODE

    1. Return cached _locale immediately if already computed
    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

## time\_zone

Returns the timezone of the web client.

If [Geo::IP](https://metacpan.org/pod/Geo%3A%3AIP) is installed,
CGI::Lingua will make use of that, otherwise it will use [ip-api.com](https://metacpan.org/pod/ip-api.com)

### API SPECIFICATION

    Input:  none beyond $self
    Returns: Str (IANA timezone name) | undef

### EXAMPLE

    local $ENV{REMOTE_ADDR} = '8.8.8.8';
    my $tz = $l->time_zone();
    print $tz // 'unknown';   # e.g. "America/New_York"

### MESSAGES

    "Couldn't determine the timezone"
    "LWP::Simple::WithCache and LWP::Simple are both absent; cannot contact ip-api.com"
      Returns undef rather than croaking; install either LWP variant to enable ip-api lookups.

### PSEUDOCODE

    1. Return cached _timezone immediately if already computed
    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 is installed

README.md  view on Meta::CPAN

- CPANTS

    [http://cpants.cpanauthors.org/dist/CGI-Lingua](http://cpants.cpanauthors.org/dist/CGI-Lingua)

- CPAN Testers' Matrix

    [http://matrix.cpantesters.org/?dist=CGI-Lingua](http://matrix.cpantesters.org/?dist=CGI-Lingua)

- CPAN Testers Dependencies

    [http://deps.cpantesters.org/?module=CGI::Lingua](http://deps.cpantesters.org/?module=CGI::Lingua)

# FORMAL SPECIFICATION

## new

    new : Class × Params → CGI::Lingua
    ∀ p : Params • p.supported ≠ ∅ ⟹ result.language ∈ (p.supported ∪ {'Unknown'})

## language

    language : CGI::Lingua → Str
    result ∈ {name(l) | l ∈ supported} ∪ {'Unknown'}

## sublanguage

    sublanguage : CGI::Lingua -> Str | undef
    result = country_name(sublanguage_code_alpha2(self))
             when sublanguage_code_alpha2(self) is defined,
             undef otherwise

## language\_code\_alpha2

    language_code_alpha2 : CGI::Lingua -> Str(2) | undef
    result = base_code(matched_supported_entry)
             when a supported language was matched, undef otherwise

## sublanguage\_code\_alpha2

    sublanguage_code_alpha2 : CGI::Lingua -> Str(2) | undef
    result = variety_code(matched_supported_entry) | undef

## requested\_language

    requested_language : CGI::Lingua -> Str
    result = name(base) + " (" + name(variety) + ")"
             when variety is known,
           = name(base)   when no variety,
           = 'Unknown'    when no language detected

## country

    country : CGI::Lingua -> Str(2,lowercase) | undef
    -- 'Unknown' returned only in the EU/Baidu special case
    result = lc(code) where code satisfies ISO 3166-1 alpha-2
             | undef when IP is private, loopback, or unresolvable

## locale

    locale : CGI::Lingua -> Locale::Object::Country | undef
    -- Best-guess detection; not guaranteed accurate.
    result = first defined value from:
        1. UA parenthetical language tag
        2. HTTP::BrowserDetect country
        3. country() IP lookup
        4. GEOIP_COUNTRY_CODE env var

## time\_zone

    time_zone : CGI::Lingua -> Str | undef
    result is an IANA timezone name (e.g. 'Europe/London') or undef

## is\_rtl

    is_rtl : CGI::Lingua → Bool
    is_rtl(s) ≙ language_code_alpha2(s) ∈ RTL_LANGS

## text\_direction

    text_direction : CGI::Lingua → {'rtl', 'ltr'}
    text_direction(s) ≙ is_rtl(s) ? 'rtl' : 'ltr'

## plural\_category

    plural_category : CGI::Lingua x N -> PluralCategory
    plural_category(s, n) = PLURAL_RULES[language_code_alpha2(s)](trunc(n))
    -- Falls back to English rule (n=1 -> 'one'; else 'other')
    -- when language_code_alpha2(s) is undef or not in the rules table.

## translation\_file

    translation_file : CGI::Lingua × Path × Ext → Path | undef
    translation_file(s, d, e) ≙
      first p ∈ candidates(s) • ∃ file d/p.e
      where candidates(s) = [lang(s)-sublang(s), lang(s)] \ {undef}

# ACKNOWLEDGEMENTS

# LICENSE AND COPYRIGHT

Copyright 2010-2026 Nigel Horne.

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



( run in 4.364 seconds using v1.01-cache-2.11-cpan-0fb53d1c279 )