NAME
CGI::Lingua - Create a multilingual web page
VERSION
Version 0.87
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.
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";
}
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";
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";
}
Make it faster with a cache
Finding the country of an IP address can be slow. Give CGI::Lingua a 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);
Let the visitor choose with a "lang" parameter
If you pass a CGI::Info object, a 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
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
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;
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
}
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";
}
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);
DESCRIPTION
This section explains, in simple steps, how CGI::Lingua finds the answer.
Finding the language
When you first call a language method (for example "language"), CGI::Lingua looks for the visitor's language in this order:
- 1. The
langparameter of the CGI::Info object, if you gave one to "new" withinfo. - 2. The
HTTP_ACCEPT_LANGUAGEenvironment variable. The web server sets this from the browser'sAccept-Languageheader, for examplefr-CA,fr;q=0.9,en;q=0.8. Languages with a higherqvalue are tried first. A language withq=0means "not acceptable" (RFC 7231) and is never chosen. Some browsers still send the old tagen-uk; it is treated asen-gb(the code for the United Kingdom isgb), with a warning. - 3. The
LANGenvironment variable. This is used when you run the program on the command line, for exampleLANG=fr_FR.UTF-8. - 4. The visitor's country (see below). CGI::Lingua uses the official language of that country. This step is skipped when you give
dont_use_ipto "new".
It then compares the answer with your supported list. If a visitor asks for en-us and you only support en-gb, CGI::Lingua still chooses English, because the base language is the same. If nothing matches, "language" returns the string 'Unknown'.
Finding the country
"country" tries these sources in order, and stops at the first answer:
- 1.
GEOIP_COUNTRY_CODE(set by Apache's mod_geoip). - 2.
HTTP_CF_IPCOUNTRY(set by Cloudflare). - 3. A local database: IP::Country, then Geo::IP, then Geo::IPfree, if they are installed.
- 4. The geoplugin.net web service.
- 5. A Whois look-up (Net::Whois::IP, then Net::Whois::IANA).
Steps 3 to 5 use the visitor's IP address from REMOTE_ADDR. Private addresses (such as 192.168.1.1) and loopback addresses (such as 127.0.0.1) have no country, so the answer is undef.
Remembering answers
Each object remembers its answers, so a second call to the same method is fast. If you give a cache to "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 supported list starts with those answers.
SUBROUTINES/METHODS
new
Creates a CGI::Lingua object.
You must tell it which languages your website supports, with supported. Each language is a short code, such as 'en' (English), 'fr' (French) or '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:
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.
supported_languagesis 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
'en','en-gb','en_gb','es-419'or'zh-Hant'. Other entries (undef, references,'', or words such as'english') could never match, so they are dropped with the warning"Ignoring '...' in the supported list: not a language code". An empty list is allowed; "language" then always returns'Unknown'.cache(optional)An object with
get(),set()andremove()methods, such as a 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 CHI object created withon_get_error => 'die'), CGI::Lingua warns"Cache get failed: ..."(orset/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
gb<script>as a country, or a hash where a language name should be) is removed and warned about with"Discarding malformed cache entry for ...", and the answer is worked out again.config_file(optional)The path to a configuration file. It is read by Object::Configure. Values in this file replace the arguments that you give to
new()(see "COMMON PITFALLS").logger(optional)Where to send messages. This can be an object with
warn(),info()anderror()methods, or any value that Object::Configure accepts (for example an array reference, which collects the messages). It is always changed into a Log::Abstraction object. Without a logger, warnings go to Carp.info(optional)A CGI::Info object (or any object with a
lang()method). Alangparameter in the request is then used before the browser's settings. The parameter comes from the visitor, so it is checked like theAccept-Languageheader (see "ENCODING"); a value that fails is ignored with the warning"lang parameter contains invalid characters; ignoring".dont_use_ip(optional, default false)When true, CGI::Lingua never guesses the language from the visitor's IP address.
syslog(optional)Passed on to the logging configuration.
debug(optional, default false)When true, I18N::AcceptLanguage prints debug information.
If you call 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 "COMMON PITFALLS").
API SPECIFICATION
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 supported or its other name supported_languages. A string must be 2 to 5 characters long.
Output
{
type => 'object',
isa => 'CGI::Lingua',
}
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(),
});
MESSAGES
new() dies (with "croak" in Carp) 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
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
language
Returns the name of the language to show to the visitor, in English, for example 'English', 'French' or 'Japanese'. The language is always one of the languages in your supported list.
Variants are handled sensibly. If a visitor asks for American English (en-us) and your site only has British English (en-gb), language() returns 'English'.
If none of the languages that the visitor wants is in your supported list, language() returns the string 'Unknown'. It never returns undef.
API SPECIFICATION
Input
{}
Output
{
type => 'string',
min => 1,
}
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"
MESSAGES
The first call to "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 en-uk into en-gb:
"Resetting country code to GB for ..."
preferred_language
Another name for "language". It takes no arguments and returns the same value.
API SPECIFICATION
Input
{}
Output
{
type => 'string',
min => 1,
}
name
Another name for "language", so that a CGI::Lingua object can be used where a Locale::Object::Language object is expected.
API SPECIFICATION
Input
{}
Output
{
type => 'string',
min => 1,
}
sublanguage
Returns the name of the variant (usually a country) of the chosen language, for example 'United Kingdom' when the chosen language is en-gb.
Returns undef when there is no variant, for example when the chosen language is just en, or when no language was found.
API SPECIFICATION
Input
{}
Output
{
type => 'string',
min => 1,
optional => 1,
}
EXAMPLE
local $ENV{HTTP_ACCEPT_LANGUAGE} = 'en-gb';
my $l = CGI::Lingua->new(supported => ['en-gb']);
print $l->sublanguage(); # "United Kingdom"
language_code_alpha2
Returns the two-letter code (ISO 639-1) of the chosen language, for example 'en' when the chosen language is en-gb.
Returns undef when none of the languages that the visitor wants is in your supported list (that is, when "language" returns 'Unknown').
API SPECIFICATION
Input
{}
Output
{
type => 'string',
min => 2,
max => 2,
optional => 1,
}
EXAMPLE
local $ENV{HTTP_ACCEPT_LANGUAGE} = 'en-gb';
my $l = CGI::Lingua->new(supported => ['en-gb']);
print $l->language_code_alpha2(); # "en"
code_alpha2
Another name for "language_code_alpha2", kept so that old programs still work.
API SPECIFICATION
Input
{}
Output
{
type => 'string',
min => 2,
max => 2,
optional => 1,
}
sublanguage_code_alpha2
Returns the two-letter code of the variant of the chosen language, in lower case, for example 'gb' when the chosen language is en-gb.
Returns undef when there is no variant.
API SPECIFICATION
Input
{}
Output
{
type => 'string',
min => 2,
max => 2,
optional => 1,
}
EXAMPLE
local $ENV{HTTP_ACCEPT_LANGUAGE} = 'en-gb';
my $l = CGI::Lingua->new(supported => ['en-gb']);
print $l->sublanguage_code_alpha2(); # "gb"
requested_language
Returns, in English, the language that the visitor asked for, 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 'English (United Kingdom)'. A variant that is not a known country code is shown as it was sent, for example 'English (Unknown: zz)'.
Returns 'Unknown' when the visitor's language cannot be found at all. It never returns undef.
API SPECIFICATION
Input
{}
Output
{
type => 'string',
min => 1,
}
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"
country
Returns the two-letter country code (ISO 3166-1) of the visitor, in lower case, for example 'us', 'gb' or 'fr'.
Returns undef when the country cannot be found, for example when REMOTE_ADDR is not set, is not a valid IP address, or is a private (192.168.1.1) or loopback (127.0.0.1) address.
In one special case it returns the string 'Unknown': when a source says the address is in the European Union (EU), which is not a country.
Two answers are corrected: hk (Hong Kong) is returned as cn, and a Whois record that says US with the state PR is returned as pr (Puerto Rico has its own country code; RT#131347).
See "Finding the country" for the order in which the sources are tried. If you have none of IP::Country, Geo::IP or Geo::IPfree installed, every look-up goes over the network, so please use a cache (see "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 GeoIP.dat databases, as used by Geo::IP, are no longer updated (Debian's geoip-database package is frozen at 2019-12-24), so they can give the wrong country for addresses that have been reallocated since then.
API SPECIFICATION
Input
{}
Output
{
type => 'string',
matches => qr/^(?:[a-z][a-z]|Unknown)$/,
optional => 1,
}
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"
MESSAGES
These are warnings. 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"
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
locale
Returns a 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 best guess. It is not always right.
It tries these, in order:
- 1. A language tag such as
en-GBinside theHTTP_USER_AGENTstring. - 2. HTTP::BrowserDetect, if it is installed.
- 3. "country".
- 4.
GEOIP_COUNTRY_CODE.
Returns undef when nothing is found.
API SPECIFICATION
Input
{}
Output
{
type => 'object',
isa => 'Locale::Object::Country',
optional => 1,
}
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"
}
MESSAGES
"HTTP_USER_AGENT contains invalid characters or exceeds length limit; ignoring"
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
time_zone
Returns the visitor's time zone, as an IANA time zone name, for example 'Europe/London' or 'America/New_York'.
When REMOTE_ADDR is set, it uses Geo::IP if a database is installed, and otherwise asks the ip-api.com web service (this needs LWP::Simple::WithCache or LWP::Simple, and JSON::Parse).
When REMOTE_ADDR is not set (for example on the command line), it returns the time zone of the computer that runs the program.
Returns undef when the time zone cannot be found.
API SPECIFICATION
Input
{}
Output
{
type => 'string',
matches => qr/^[A-Za-z][A-Za-z0-9_+\-\/]*$/,
optional => 1,
}
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"
MESSAGES
These are warnings. 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: ..."
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
is_rtl
Returns 1 if the chosen language is written from right to left, and 0 if it is not.
The right-to-left languages are Arabic (ar), Dhivehi (dv), Persian (fa), Hebrew (he), Kurdish (ku), Pashto (ps), Sindhi (sd), Uyghur (ug), Urdu (ur) and Yiddish (yi).
When no language was found, it returns 0.
API SPECIFICATION
Input
{}
Output
{
type => 'boolean',
memberof => [0, 1],
}
EXAMPLE
local $ENV{HTTP_ACCEPT_LANGUAGE} = 'ar';
my $l = CGI::Lingua->new(supported => ['ar', 'en']);
print $l->is_rtl(); # 1
text_direction
Returns 'rtl' (right to left) or 'ltr' (left to right) for the chosen language. You can use the value directly in the HTML dir attribute.
When no language was found, it returns 'ltr'.
API SPECIFICATION
Input
{}
Output
{
type => 'string',
memberof => ['ltr', 'rtl'],
}
EXAMPLE
local $ENV{HTTP_ACCEPT_LANGUAGE} = 'he';
my $l = CGI::Lingua->new(supported => ['he', 'en']);
print '<html dir="', $l->text_direction(), '">'; # <html dir="rtl">
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.
plural_category($n) tells you which form to use for the number $n in the chosen language. It returns one of 'zero', 'one', 'two', 'few', 'many' or '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: 'one' for 1, otherwise 'other'.
When no language was found, it always returns 'other'.
$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 Locale::CLDR.
API SPECIFICATION
Input
[
{ type => 'number', min => 0 },
]
Output
{
type => 'string',
memberof => ['zero', 'one', 'two', 'few', 'many', 'other'],
}
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"
MESSAGES
"plural_category: $n must be defined"
- dies (croak) when $n is undef
translation_file($dir, $ext)
Finds the translation file for the chosen language in the directory $dir, and returns its path.
It tries these file names, in order, and returns the first one that is a readable regular file:
$ext is the file extension. It is 'json' if you do not give it. You can write it with or without the dot ('po' or '.po').
Returns undef when no such file exists, when no language was found, when $dir is undef, or when $dir or $ext is unsafe (see below). A directory, a device (such as /dev/urandom), a broken symbolic link or a file you cannot read is never returned, even if it has the right name.
For safety, $dir must be a non-empty string (not a reference) without .. or a null byte, and $ext may only contain letters, digits and -. Control characters in a rejected value are shown as \xNN in the warning.
API SPECIFICATION
Input
[
{ type => 'string', min => 1 },
{ type => 'string', min => 1, matches => qr/^\.?[A-Za-z0-9-]+$/, optional => 1 },
]
Output
{
type => 'string',
optional => 1,
}
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');
MESSAGES
These are warnings. translation_file() does not die.
"translation_file: unsafe directory '...' rejected"
"translation_file: unsafe extension '...' rejected"
COMMON PITFALLS
Configuration files and environment variables win over your arguments
"new" passes its arguments to Object::Configure. If a configuration file (
config_file) or an environment variable such asCGI__Lingua__supportedsets a value, that value replaces the value you gave tonew().Values are replaced, not merged. If you pass
supported => ['en', 'fr']and the configuration file sayssupported: [de], the object supports onlyde, noten,frandde. For nested values (hashes inside hashes) see the rules in Object::Configure.A copy made with
$object->new()keeps old answersWhen you call
new()on an object, the copy starts with every answer that the original has already worked out. So if the original has already called "language", the copy returns the same language, even if you give the copy a differentsupportedlist: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
CGI::Lingua->new().'Unknown' is not the same as undef
Some methods return the string
'Unknown'and some returnundefwhen they do not know the answer.'Unknown'is a true value in Perl, soif($l->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
eq 'Unknown'ordefined(), as the table shows.undefreally 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 undefplural_category() returns 'other' when there is no language
When no supported language was found,
plural_category(1)returns'other', not'one'. Have an'other'form for every message.plural_category(undef)dies.supported => 0andsupported => ''count as missingA false value for
supportedis treated as if you did not give it, sonew()dies with "You must give a list of supported languages".Answers are worked out once, then remembered
The first call to a language method reads
%ENVand remembers the answer. If you change%ENVafterwards, the object does not notice. Create a new object for each web request.country() reads REMOTE_ADDR when you call it
country()reads$ENV{REMOTE_ADDR}when you call it, not when you create the object. In tests, callcountry()whileREMOTE_ADDRstill 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 }A bad header is ignored completely
If
HTTP_ACCEPT_LANGUAGEis longer than 256 characters, or contains a character that is not allowed (see "ENCODING"), the whole header is ignored with a warning. CGI::Lingua then usesLANGor the IP address instead.The supported list holds tags, not names
supported => ['english', 'fr']supports only French:'english'is not a language tag, so it is dropped (with a warning). Use'en'.Language names are in English
"language" returns
'French', not'Francais', and'German', not'Deutsch'. Use "language_code_alpha2" if you want to show the name in the language itself.The logger you pass is replaced
Object::Configure always changes
loggerinto a new Log::Abstraction object, so$l->{logger}is not the object you passed.Whole objects are only cached when REMOTE_ADDR is set
The answers of an object are read from the cache in "new", and written to it when the object is destroyed, only when
REMOTE_ADDRis set. On the command line, only smaller look-ups (such as code-to-name) are cached.Local geo databases can be out of date
Old GeoIP.dat files can give the wrong country for some addresses (see "country").
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 ('French'), country names ('Reunion'), codes and time zone names. You do not need to decode them.
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 local:
$CGI::Lingua::ZONE_FILEThe file that "time_zone" reads when there is no
REMOTE_ADDR(command-line use). The default is /etc/timezone. Only a readable regular file is used, and only its first 256 bytes are read; otherwise DateTime::TimeZone is used.local $CGI::Lingua::ZONE_FILE = '/srv/myapp/timezone';@CGI::Lingua::GEOIP_DATThe places where "country" and "time_zone" look for the legacy MaxMind GeoIP.dat used by Geo::IP; the first readable regular file is used. The default is /usr/share/GeoIP/GeoIP.dat and /usr/local/share/GeoIP/GeoIP.dat (and c:/GeoIP/GeoIP.dat first, on Windows). A file that Geo::IP cannot open is skipped with the warning
"Can't open ... with Geo::IP; not using it".local @CGI::Lingua::GEOIP_DAT = ('/opt/geo/GeoIP.dat');
LIMITATIONS
is_rtl() covers primary-script RTL languages only
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. Azerbaijaniaz, 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.plural_category() uses embedded CLDR rules, not Locale::CLDR
The embedded rules cover ~70 languages and truncate fractional
$nto an integer. For full CLDR v42 accuracy (including fractional forms and languages not in the table) install and useLocale::CLDRdirectly.The logger is always a Log::Abstraction object
The
loggerargument can be an object withwarn(),info()anderror()methods, or any value that Object::Configure accepts (such as an array reference or a hash reference of options). In every case Object::Configure replaces it with a Log::Abstraction object. An object that is missing one of the three methods is rejected by "new".es-419 sublanguage returns undef
Three-part regional codes such as
es-419(Latin American Spanish) do not resolve to asublanguage()value because ISO 3166-1 does not define '419'. This is a known limitation of the Locale::Object layer.Whois lookups are slow and unreliable
Without
IP::Country,Geo::IP, orGeo::IPfreeinstalled,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.Private methods are accessible from outside the package
The
_*methods use the naming convention for privacy but Perl does not enforce it.Sub::PrivateorSub::Protectedshould be added once all white-box tests (t/function.t,t/extended_tests.t) are updated to use the public API exclusively.IPv4-mapped IPv6 addresses are normalised to IPv4
REMOTE_ADDRvalues in the form::ffff:a.b.c.d(RFC 4291 section 2.5.5) are silently rewritten to the embeddeda.b.c.dIPv4 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.EU country code is irresolvable (with one exception)
IP addresses that Whois reports as country
EUare mapped to'Unknown'unless they fall within Baidu's known subnet (RT-86809). There is no ISO 3166-1 country code for the European Union.country() does not cache undef results
When
country()cannot determine a country (private IPs, loopback, unresolvable addresses), it returnsundefwithout storing the result. A second call on the same object repeats the full validation pipeline. This is intentional:country()readsREMOTE_ADDRat call time rather than at construction time, so cachingundefwould return a wrong answer ifREMOTE_ADDRchanges between calls. In practice this is rarely a problem becausecountry()is called once per request and CGI applications typically create a fresh object per request.
AUTHOR
Nigel Horne, <njh at nigelhorne.com>
BUGS
If HTTP_ACCEPT_LANGUAGE contains a sub-tag with a 3-digit UN M.49 region code (e.g. es-419 for Latin American Spanish), sublanguage() returns undef because ISO 3166-1 does not define numeric codes.
Uses 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 bug-cgi-lingua at rt.cpan.org, or through the web interface at 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.
SEE ALSO
VWF - Versatile Web Framework https://github.com/nigelhorne/vwf
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:
MetaCPAN
RT: CPAN's request tracker
CPANTS
CPAN Testers' Matrix
CPAN Testers Dependencies
FORMAL SPECIFICATION
This section describes each method in the Z notation. You do not need it to use the module.
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 ¬in; {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 ≠ ⊥))
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) ¬in; RESTORABLE(f)))
│ ⇒ key(ip) ¬in; 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? ¬in; LANGTAG ∪ seq LANGTAG
∨ (info? ≠ ⊥ ∧ ¬ can(info?, lang) ∧ ¬ can(info?, AUTOLOAD)) ] ⇒ croak
language
┌─ Language ────────────────────────────────────────────────────
│ EnsureResolved
│ result! : LANGNAME ∪ {Unknown}
├───────────────────────────────────────────────────────────────
│ result! = slanguage'
└───────────────────────────────────────────────────────────────
PreferredLanguage ≙ Language
Name ≙ Language
sublanguage
┌─ Sublanguage ─────────────────────────────────────────────────
│ EnsureResolved
│ result! : CNAME ∪ {⊥}
├───────────────────────────────────────────────────────────────
│ result! = sublanguage'
└───────────────────────────────────────────────────────────────
language_code_alpha2
┌─ LanguageCodeAlpha2 ──────────────────────────────────────────
│ EnsureResolved
│ result! : LC2 ∪ {⊥}
├───────────────────────────────────────────────────────────────
│ result! = code2'
│ slanguage' = Unknown ⇒ result! = ⊥
└───────────────────────────────────────────────────────────────
CodeAlpha2 ≙ LanguageCodeAlpha2
sublanguage_code_alpha2
┌─ SublanguageCodeAlpha2 ───────────────────────────────────────
│ EnsureResolved
│ result! : CC2 ∪ {⊥}
├───────────────────────────────────────────────────────────────
│ result! = subcode2'
└───────────────────────────────────────────────────────────────
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.
└───────────────────────────────────────────────────────────────
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 ¬in; 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 ¬in; CC2 ↦ ⊥
└───────────────────────────────────────────────────────────────
where CC2↑ is CC2 in upper case, and canon(::ffff:a.b.c.d) = a.b.c.d.
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!
└───────────────────────────────────────────────────────────────
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! ¬in; ZONE ⇒ result! = ⊥
│ timezone' = result!
└───────────────────────────────────────────────────────────────
is_rtl
┌─ IsRtl ───────────────────────────────────────────────────────
│ EnsureResolved
│ result! : {0, 1}
├───────────────────────────────────────────────────────────────
│ result! = 1 ⇔ code2' ∈ RTL_LANGS
└───────────────────────────────────────────────────────────────
text_direction
┌─ TextDirection ───────────────────────────────────────────────
│ IsRtl
│ dir! : DIR
├───────────────────────────────────────────────────────────────
│ dir! = (if result! = 1 then rtl else ltr)
└───────────────────────────────────────────────────────────────
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' ¬in; dom PLURAL_RULES ∪ {⊥} ⇒ result! = english(trunc n?)
└───────────────────────────────────────────────────────────────
PluralError ≙ [ n? = ⊥ ] ⇒ croak
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)
└───────────────────────────────────────────────────────────────
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).
LICENSE AND COPYRIGHT
Copyright 2010-2026 Nigel Horne.
Usage is subject to the GPL2 licence terms. If you use it, please let me know.