Look, I get it. You don’t like the Perl pro­gram­ming lan­guage or have oth­er­wise dis­re­gard­ed it as dead.” (Or per­haps you haven’t, in which case please check out my oth­er blog posts!) It has weird noisy syn­tax, mix­ing reg­u­lar expres­sions, sig­ils on vari­able names, var­i­ous braces and brack­ets for data struc­tures, and a menagerie of cryp­tic spe­cial vari­ables. It’s old: 34 years in December, with a his­to­ry of (some­times ama­teur) devel­op­ers that have used and abused that syn­tax to ship code of ques­tion­able qual­i­ty. Maybe you grudg­ing­ly accept its util­i­ty but think it should die grace­ful­ly, main­tained only to run lega­cy applications.

But you know what? Perl’s still going. It’s had a steady cadence of year­ly releas­es for the past decade, intro­duc­ing new fea­tures and fenc­ing in bad behav­ior while main­tain­ing an admirable lev­el of back­ward com­pat­i­bil­i­ty. Yes, there was a too-​long adven­ture devel­op­ing what start­ed as Perl 6, but that lan­guage now has its own iden­ti­ty as Raku and even has facil­i­ties for mix­ing Perl with its native code or vice versa.

And then there’s CPAN, the Comprehensive Perl Archive Network: a continually-​updated col­lec­tion of over 200,000 open-​source mod­ules writ­ten by over 14,000 authors, the best of which are well-​tested and ‑doc­u­ment­ed (apply­ing peer pres­sure to those that fall short), pre­sent­ed through a search engine and front-​end built by scores of con­trib­u­tors. Through CPAN you can find dis­tri­b­u­tions for things like:

All of this is avail­able through a mature instal­la­tion tool­chain that doesn’t break from month to month.

Finally and most impor­tant­ly, there’s the glob­al Perl com­mu­ni­ty. The COVID-​19 pan­dem­ic has put a damper on the hun­dreds of glob­al Perl Mongers groups’ mee­tups, but that hasn’t stopped the year­ly Perl and Raku Conference from meet­ing vir­tu­al­ly. (In the past there have also been year­ly European and Asian con­fer­ences, occa­sion­al for­ays into South America and Russia, as well as hackathons and work­shops world­wide.) There are IRC servers and chan­nels for chat, mail­ing lists galore, blogs (yes, apart from this one), and a quirky social net­work that pre­dates Facebook and Twitter.

So no, Perl isn’t dead or even dying, but if you don’t like it and favor some­thing new­er, that’s OK! Technologies can coex­ist on their own mer­its and advo­cates of one don’t have to beat down their con­tem­po­raries to be suc­cess­ful. Perl hap­pens to be battle-​tested (to bor­row a term from my friend Curtis Ovid” Poe), it runs large parts of the Web (speak­ing from direct and ongo­ing expe­ri­ence in the host­ing busi­ness here), and it’s still evolv­ing to meet the needs of its users.


Cover image by Alan Levine, licensed CC BY 2.0

clear light bulb planter on gray rock

Twitter recent­ly rec­om­mend­ed a tweet to me (all hail the algo­rithm) tout­ing what the author viewed as the top 5 web devel­op­ment stacks.”

JavaScript/​Node.js options dom­i­nat­ed the four-​letter acronyms as expect­ed, but the fifth one sur­prised me: LAMP, the com­bi­na­tion of the Linux oper­at­ing sys­tem, Apache web serv­er, MySQL rela­tion­al data­base, and Perl, PHP, or Python pro­gram­ming lan­guages. A quick web search for sim­i­lar lists yield­ed sim­i­lar results. Clearly, this meme (in the Dawkins sense) has out­last­ed its pop­u­lar­iza­tion by tech pub­lish­er O’Reilly in the 2000s.

Originally coined in 1998 dur­ing the dot-​com” bub­ble, I had thought that the term LAMP” had fad­ed with devel­op­ers in the inter­ven­ing decades with the rise of language-​specific web frame­works for:

Certainly on the Perl side (with which I’m most famil­iar), the com­mu­ni­ty has long since rec­om­mend­ed the use of a frame­work built on the PSGI spec­i­fi­ca­tion, dep­re­cat­ing 1990s-​era CGI scripts and the mod_​perl Apache exten­sion. Although general-​purpose web servers like Apache or Nginx may be part of an over­all sys­tem, they’re typ­i­cal­ly used as prox­ies or load bal­ancers for Perl-​specific servers either pro­vid­ed by the frame­work or a third-​party mod­ule.

Granted, PHP still relies on web server-​specific mod­ules, APIs, or vari­a­tions of the FastCGI pro­to­col for inter­fac­ing with a web serv­er. And Python web appli­ca­tions typ­i­cal­ly make use of its WSGI pro­to­col either as a web serv­er exten­sion or, like the Perl exam­ples above, as a prox­ied stand­alone serv­er. But all of these are deploy­ment details and do lit­tle to describe how devel­op­ers imple­ment and extend a web application’s structure.

Note how the var­i­ous four-​letter JavaScript stacks (e.g., MERN, MEVN, MEAN, PERN) dif­fer­en­ti­ate them­selves most­ly by fron­tend frame­work (e.g., Angular, React, Vue.js) and maybe by the (rela­tion­al or NoSQL) data­base (e.g., MongoDB, MySQL, PostgreSQL). All how­ev­er seem stan­dard­ized on the Node.js run­time and Express back­end web frame­work, which could, in the­o­ry, be replaced with non-​JavaScript options like the more mature LAMP-​associated lan­guages and frame­works. (Or if you pre­fer lan­guages that don’t start with P”, there’s C#, Go, Java, Ruby, etc.)

My point is that LAMP” as the name of a web devel­op­ment stack has out­lived its use­ful­ness. It’s at once too spe­cif­ic (about oper­at­ing sys­tem and web serv­er details that are often abstract­ed away for devel­op­ers) and too broad (cov­er­ing three sep­a­rate pro­gram­ming lan­guages and not the frame­works they favor). It also leaves out oth­er non-​JavaScript back-​end lan­guages and their asso­ci­at­ed frameworks.

The ques­tion is: what can replace it? I’d pro­pose NoJS” as rem­i­nis­cent of NoSQL,” but that inac­cu­rate­ly excludes JavaScript from its nec­es­sary role in the front-​end. NJSB” doesn’t exact­ly roll off the tongue, either, and still has the same ambi­gu­i­ty prob­lem as LAMP.”

How about pithy sort-​of-​acronyms pat­terned like database-​frontend-​backend? Here are some Perl examples:

  • MRDancer: MySQL, React, and Dancer (I use this at work. Yes, the M could also stand for MongoDB. Naming things is hard.)
  • MRMojo: MongoDB, React, and Mojolicious
  • PACat: PostgreSQL, Angular, and Catalyst
  • etc.

Ultimately it comes down to com­mu­ni­ty and indus­try adop­tion. If you’re involved with back-​end web devel­op­ment, please let me know in the com­ments if you agree or dis­agree that LAMP” is still a use­ful term, and if not, what should replace it.

brown wooden arrow signed

A mentee asked me over the week­end if there was a way with­in a Mojolicious web appli­ca­tion to store the routes sep­a­rate­ly from the main appli­ca­tion class. Here’s one way. These instruc­tions assume you’re using Perl 5.34 and Mojolicious 9.19 (the lat­est as of this writ­ing) via the ter­mi­nal com­mand line on a Linux, Unix, or macOS sys­tem; make the appro­pri­ate changes if this does­n’t apply to you.

First, if you haven’t already, cre­ate your Mojolicious app at your shell prompt:

$ mojo generate app Local::RouteDemo
  [mkdir] /Users/mgardner/Projects/blog/local_route_demo/script
  [write] /Users/mgardner/Projects/blog/local_route_demo/script/local_route_demo
  [chmod] /Users/mgardner/Projects/blog/local_route_demo/script/local_route_demo 744
  [mkdir] /Users/mgardner/Projects/blog/local_route_demo/lib/Local
  [write] /Users/mgardner/Projects/blog/local_route_demo/lib/Local/RouteDemo.pm
  [exist] /Users/mgardner/Projects/blog/local_route_demo
  [write] /Users/mgardner/Projects/blog/local_route_demo/local-route_demo.yml
  [mkdir] /Users/mgardner/Projects/blog/local_route_demo/lib/Local/RouteDemo/Controller
  [write] /Users/mgardner/Projects/blog/local_route_demo/lib/Local/RouteDemo/Controller/Example.pm
  [mkdir] /Users/mgardner/Projects/blog/local_route_demo/t
  [write] /Users/mgardner/Projects/blog/local_route_demo/t/basic.t
  [mkdir] /Users/mgardner/Projects/blog/local_route_demo/public
  [write] /Users/mgardner/Projects/blog/local_route_demo/public/index.html
  [mkdir] /Users/mgardner/Projects/blog/local_route_demo/templates/layouts
  [write] /Users/mgardner/Projects/blog/local_route_demo/templates/layouts/default.html.ep
  [mkdir] /Users/mgardner/Projects/blog/local_route_demo/templates/example
  [write] /Users/mgardner/Projects/blog/local_route_demo/templates/example/welcome.html.ep
$ cd local_route_demo

Create a new Perl mod­ule in your edi­tor for stor­ing your routes. Here we’re using Local::RouteDemo::Routes:

$ touch lib/Local/RouteDemo/Routes.pm
$ $EDITOR lib/Local/RouteDemo/Routes.pm

Make the mod­ule with a func­tion that will cre­ate the routes you want, giv­en a Mojolicious::Routes object. Here we’re just bring­ing over the default route cre­at­ed when we cre­at­ed our app:

package Local::RouteDemo::Routes;
use strict;
use warnings qw(all -experimental::signatures);
use feature 'signatures';
use Exporter 'import';
our @EXPORT_OK = qw(make_routes);

sub make_routes ($router) {
    $router->get('/')->to('Example#welcome');
    # add more routes here

    return;
}

1;

Adjust the appli­ca­tion class to load your new Routes mod­ule and call its export­ed function:

package Local::RouteDemo;
use Mojo::Base 'Mojolicious', -signatures;
use Local::RouteDemo::Routes 'make_routes';

# This method will run once at server start
sub startup ($self) {

    # Load configuration from config file
    my $config = $self->plugin('NotYAMLConfig');

    # Configure the application
    $self->secrets($config->{secrets});

    # Make routes
    make_routes($self->routes);

    return;
}

1;

Finally, run your tests and/​or man­u­al­ly test your routes to be sure every­thing works OK:

$ prove -vlr t
t/basic.t .. [2021-06-07 12:21:55.36917] [58779] [debug] [elVGykGVWlOt] GET "/"
[2021-06-07 12:21:55.36972] [58779] [debug] [elVGykGVWlOt] Routing to controller "Local::RouteDemo::Controller::Example" and action "welcome"
[2021-06-07 12:21:55.37137] [58779] [debug] [elVGykGVWlOt] Rendering template "example/welcome.html.ep"
[2021-06-07 12:21:55.37343] [58779] [debug] [elVGykGVWlOt] Rendering template "layouts/default.html.ep"
[2021-06-07 12:21:55.37495] [58779] [debug] [elVGykGVWlOt] 200 OK (0.005772s, 173.250/s)

ok 1 - GET /
ok 2 - 200 OK
ok 3 - content is similar
1..3
ok
All tests successful.
Files=1, Tests=3,  1 wallclock secs ( 0.02 usr  0.01 sys +  0.38 cusr  0.11 csys =  0.52 CPU)
Result: PASS
$ script/local_route_demo get /
[2021-06-07 12:22:29.55930] [58889] [debug] [f3YoaFhkwJ42] GET "/"
[2021-06-07 12:22:29.55990] [58889] [debug] [f3YoaFhkwJ42] Routing to controller "Local::RouteDemo::Controller::Example" and action "welcome"
[2021-06-07 12:22:29.56059] [58889] [debug] [f3YoaFhkwJ42] Rendering template "example/welcome.html.ep"
[2021-06-07 12:22:29.56269] [58889] [debug] [f3YoaFhkwJ42] Rendering template "layouts/default.html.ep"
[2021-06-07 12:22:29.56432] [58889] [debug] [f3YoaFhkwJ42] 200 OK (0.005004s, 199.840/s)
<!DOCTYPE html>
<html>
  <head><title>Welcome</title></head>
  <body><h2>Welcome to the Mojolicious real-time web framework!</h2>
<p>
  This page was generated from the template "templates/example/welcome.html.ep"
  and the layout "templates/layouts/default.html.ep",
  <a href="/">click here</a> to reload the page or
  <a href="/index.html">here</a> to move forward to a static page.
</p>
</body>
</html>

You can find a git repos­i­to­ry of this work on GitHub, and here’s a com­mit of all the changes made to the default Mojolicious appli­ca­tion so you can see the differences.

Update

Joel Berger from the Mojolicious project told me at The Perl and Raku Conference that it would be more idiomat­ic to use a Mojolicious plu­g­in rather than a plain mod­ule with an export, so here you go:

lib/Local/RouteDemo.pm:

package Local::RouteDemo;
use Mojo::Base 'Mojolicious', -signatures;

# This method will run once at server start
sub startup ($self) {

    # Load configuration from config file
    my $config = $self->plugin('NotYAMLConfig');

    # Configure the application
    $self->secrets($config->{secrets});

    # Add routes from plugin
    $self->plugin('Local::RouteDemo::Plugin::Routes');

    return;
}

1;

lib/Local/RouteDemo/Plugin/Routes.pm:

package Local::RouteDemo::Plugin::Routes;
use Mojo::Base 'Mojolicious::Plugin', -signatures;

sub register ($self, $app, $conf) {
    my $r = $app->routes;

    $r->get('/')->to('Example#welcome');
    # add more routes here

    return;
}

1;

This week we con­sid­ered a view­er’s pull request, added admin­is­tra­tor login, and start­ed on adding the SQLite data­base that will store the admin­is­tra­tor’s accep­tance of assign­ments. We also shored up file upload per­mis­sions for authen­ti­cat­ed users only and added a logout link, learn­ing about some more Mojolicious helpers.

You can find the whole series here.

Today Gábor and I con­tin­ued our work on a man­age­ment appli­ca­tion for his train­ing cours­es pow­ered by Mojolicious. Big accom­plish­ment this time: we went from a sin­gle upload form for exer­cis­es going to a pre­set file all the way to a multi-​file form where every exer­cise has its own list of files for sub­mis­sion. We also had a lit­tle detour into Docker-land where we sim­pli­fied his launch com­mand into docker-compose up.

Head on over to Perl Maven if you’d like to catch up with pre­vi­ous install­ments or reg­is­ter with Zoom for the next one sched­uled on Sunday, May 30.

text

Last week’s arti­cle received a com­ment on a pri­vate Facebook group that amount­ed to just use JavaScript’s built-​in for­mat­ting.” So what would that look like?

#!/usr/bin/env perl

use Mojolicious::Lite -signatures;
use DateTime;

get '/' =>
    sub ($c) { $c->render( template => 'index', date => DateTime->today ) };

helper localize_date => sub ( $c, $date = DateTime->today, $style = 'full' ) {
    my $date_params = join ',' => $date->year, $date->month_0, $date->day;
    return
        qq<new Date($date_params).toLocaleString( [], {dateStyle: "$style"})>;
};

app->start;
__DATA__
@@ index.html.ep
% layout 'default';
% title 'Today';
<ul>
    <li><script>
        document.write(<%== localize_date $date %>)
    </script></li>
    % for my $style ( qw(long medium short) ) {
    <li><script>
        document.write(<%== localize_date $date, $style %>)
    </script></li>
    % }
</ul>
@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
    <head><title><%= title %></title></head>
    <body><%= content %></body>
</html>

It’s struc­tured much like the Perl-​only solu­tion, with a default "/" route and a localize_date Mojolicious helper to do the for­mat­ting. I opt­ed to out­put a piece of JavaScript from the helper on lines 11 through 14 since it could be repeat­ed sev­er­al times in a doc­u­ment. You could instead declare a func­tion in the default lay­out’s HTML <head> on line 38 that would receive a date and a for­mat­ting style, out­putting the result­ing for­mat­ted date.

In the tem­plate’s list from lines 22 through 31 I decid­ed to use JavaScript document.write method calls to add our gen­er­at­ed code. This has a slew of caveats but works for our exam­ple here.

Worth not­ing is the dou­ble equals sign (<%== %>) when embed­ding a Perl expres­sion. This pre­vents Mojolicious from XML-​escaping spe­cial char­ac­ters, e.g., replac­ing "quotes" with &quot;, <angle brack­ets> with &lt; and &gt;, etc.. This is impor­tant when return­ing HTML and JavaScript code.

I also chose to use the JavaScript Date objec­t’s toLocaleString() method for my for­mat­ting on line 12. There are oth­er ways to do this:

Note that line 10 builds the para­me­ters for JavaScript’s Date con­struc­tor using the year, month_0, and day meth­ods of our Perl DateTime object; month_0 because the Date con­struc­tor takes its month as an inte­ger from 0 to 11 rather than 1 to 12. JavaScript Dates can be con­struct­ed in many ways; this seemed the sim­plest with­out hav­ing to explain things like epochs and incon­sis­tent parsing.

Why are we using Perl DateTimes and a helper any­way? I’m assum­ing that our dates are com­ing from the back­end of our appli­ca­tion, pos­si­bly inflat­ed from a data­base col­umn. If your dates are strict­ly on the fron­tend, you might decide to put your for­mat­ting code there in a JavaScript func­tion, per­haps using a JavaScript-​based tem­plat­ing library.

The bot­tom line is to do what­ev­er makes sense for your sit­u­a­tion. I pre­fer the Perl solu­tion because I like the lan­guage and its ecosys­tem and per­haps have accli­mat­ed to its quirks. The com­pli­ca­tions of JavaScript brows­er sup­port, com­pet­ing frame­works, and lay­ers of tool­ing make my head hurt. Despite this, I’m still learn­ing; if you have any com­ments or sug­ges­tions, please leave them below.

Gábor’s already post­ed today’s video!

It’s real­ly inter­est­ing to see our dif­fer­ent devel­op­ment styles work togeth­er. Gábor’s all about try­ing things imme­di­ate­ly and search­ing the entire web for answers, while I like to read the doc­u­men­ta­tion and work out its writ­ers’ intent.

I’m also embar­rassed that we end­ed on a cliffhang­er” with an unre­solved error, though Gábor post­ed that he man­aged to resolve it.

Western and eastern hemispheres of the Earth

When we’re writ­ing soft­ware for a glob­al audi­ence, it’s nice if we can pro­vide it accord­ing to their native lan­guages and con­ven­tions. Translating all of the text can be a huge under­tak­ing, but we can start small by mak­ing sure that when we show the day and date it appears as the user expects. For exam­ple, to me it’s Tuesday, April 20, 2021; to my friend Paul in the UK it’s Tuesday, 20 April 2021 (note the dif­fer­ence in order), and to my oth­er friend Gabór in Israel it’s יום שלישי, 20 באפריל 2021 (note the dif­fer­ent direc­tion of the text, dif­fer­ent lan­guage, and char­ac­ter set).

Thankfully, we have a num­ber of tools to assist us:

  • The DateTime::Locale library, which enables our Perl soft­ware to rep­re­sent dates and times glob­al­ly and con­tains a cat­a­log of locales. It works with the DateTime library for stor­ing our dates as objects that can be eas­i­ly manip­u­lat­ed and formatted.
  • The HTTP Accept-​Language head­er, which lets a web brows­er com­mu­ni­cate to the serv­er what nat­ur­al lan­guages and locale vari­ants the user understands.
  • The HTTP::AcceptLanguage mod­ule, which helps us parse the Accept-​Language head­er and select a com­pat­i­ble locale.

Our sam­ple code uses the Mojolicious frame­work and is very sim­ple; almost half of it is just HTML web page tem­plates. You could eas­i­ly adapt it to oth­er frame­works or tem­plat­ing systems.

#!/usr/bin/env perl

use Mojolicious::Lite -signatures;
use DateTime;
use DateTime::Locale;
use HTTP::AcceptLanguage;

my %locales
    = map { $_ => DateTime::Locale->load($_) } DateTime::Locale->codes;

get '/' =>
    sub ($c) { $c->render( template => 'index', date => DateTime->today ) };

helper localize_date => sub ( $c, $date = DateTime->today, $format = 'full' )
{
    my $locale = $locales{ HTTP::AcceptLanguage->new(
            $c->req->headers->accept_language )->match( keys %locales ) };

    my $method_name = "date_format_$format";
    return $date->clone->set_locale($locale)
        ->format_cldr( $locale->$method_name );
};

app->start;
__DATA__
@@ index.html.ep
% layout 'default';
% title 'Today';
<ul>
    <li><%= localize_date $date %></li>
    % for my $format ( qw(long medium short) ) {
    <li><%= localize_date $date, $format %></li>
    % }
</ul>
@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
    <head><title><%= title %></title></head>
    <body><%= content %></body>
</html>

Lines 1 through 5 tell our code to use the Perl inter­preter in our exe­cu­tion PATH and load our pre­req­ui­site mod­ules. Note we’re using the micro ver­sion of Mojolicious, Mojolicious::Lite; lat­er you can grow your appli­ca­tion into a well-​structured Mojolicious app. We’re also using Perl sub­rou­tine sig­na­tures, which requires Perl 5.20 or lat­er (released in 2014).

Lines 7 and 8 pre­load all of the avail­able DateTime::Locale objects so that we can serve requests faster with­out hav­ing to load a new locale every time. We cre­ate a hash where the keys are the locale iden­ti­fiers (for exam­ple, en-US for United States English), and the val­ues are the locale objects.

Line 10 begins our route han­dler for HTTP GET requests on the default / route in our web appli­ca­tion. When a brows­er hits the home page of our app, it will exe­cute the code in the anony­mous sub in line 11, which is passed the con­troller object as $c. It’s a very sim­ple han­dler that ren­ders a tem­plate called index (described below), pass­ing it a date object with today’s date.

Lines 13 through 23 are where the smarts of our appli­ca­tion lie. It’s a helper that we’ll call from our tem­plate to local­ize a date object, and it’s anoth­er anony­mous sub. This time it’s passed a Mojolicious con­troller as $c, a $date para­me­ter that defaults to today, and a $format para­me­ter that defaults to full’.

Lines 14 through 18 in the helper get our locale. Working from the inside out, we get the HTTP Accept-​Language head­er from the request on line 16, cre­ate a new HTTP::AcceptLanguage object in line 15 for pars­ing that head­er, and then match it against the keys in our glob­al %locales hash in line 17. That matched key then looks up the appro­pri­ate DateTime::Locale object from the hash.

DateTime only allows you to set a locale at object con­struc­tion time, so in line 19 we cre­ate a new object from the old one, set­ting its locale to our newly-​discovered $locale object. Finally, in lines 21 and 22 we deter­mine what method to call on that object to retrieve the CLDR (Common Locale Data Repository) for­mat­ting pat­tern for the request­ed for­mat and then return the for­mat­ted date.

Finally, line 25 starts the appli­ca­tion. To run it using the devel­op­ment serv­er includ­ed with Mojolicious, do this at the com­mand line:

$ morbo perl_date_locale.pl

There are oth­er options for deploy­ing your appli­ca­tion, includ­ing Mojolicious’ built-​in web serv­er, inside a con­tain­er, using oth­er web servers, etc.

The rest of the above script is in the __DATA__ por­tion and con­tains two pseudo-​files that Mojolicious knows how to read in the absence of actu­al tem­plates and lay­outs. First on line 28 is the actu­al index.html.ep HTML page, which uses Mojolicious’ Embedded Perl (ep) tem­plat­ing sys­tem to select a lay­out of shared HTML to use (the layouts/default.html.ep file start­ing on line 39).

Lines 32 through 37 ren­der an HTML unordered list that runs through the var­i­ous for­mat­ting options avail­able to our localize_date helper, first with the default full’ for­mat­ting, and then a loop through long’, medi­um’, and short’. Note that we call our helper as an expres­sion, with an equals (=) sign after the per­cent (%) sign.

If you want to test dif­fer­ent locales with­out chang­ing your brows­er or oper­at­ing sys­tem set­tings, you can invoke the script from the com­mand line along with the HTTP request and head­ers to pass along. Here’s an exam­ple using German:

$ perl perl_date_locale.pl get -H 'Accept-Language: de' /
[2021-04-17 16:39:57.81379] [5425] [debug] [LcCSBKMVd90t] GET "/"
[2021-04-17 16:39:57.81408] [5425] [debug] [LcCSBKMVd90t] Routing to a callback
[2021-04-17 16:39:57.81610] [5425] [debug] [LcCSBKMVd90t] Rendering template "index.html.ep" from DATA section
[2021-04-17 16:39:57.81714] [5425] [debug] [LcCSBKMVd90t] Rendering template "layouts/default.html.ep" from DATA section
[2021-04-17 16:39:57.81792] [5425] [debug] [LcCSBKMVd90t] 200 OK (0.004118s, 242.836/s)
<!DOCTYPE html>
<html>
    <head><title>Today</title></head>
    <body>
<ul>
    <li>Sonntag, 18. April 2021</li>
    <li>18. April 2021</li>
    <li>18.04.2021</li>
    <li>18.04.21</li>
</ul>
</body>
</html>

And here’s Japanese:

$ perl perl_date_locale.pl get -H 'Accept-Language: ja' /
[2021-04-17 16:40:56.10840] [5478] [debug] [Wmr6cN5KUJlP] GET "/"
[2021-04-17 16:40:56.10874] [5478] [debug] [Wmr6cN5KUJlP] Routing to a callback
[2021-04-17 16:40:56.11101] [5478] [debug] [Wmr6cN5KUJlP] Rendering template "index.html.ep" from DATA section
[2021-04-17 16:40:56.11255] [5478] [debug] [Wmr6cN5KUJlP] Rendering template "layouts/default.html.ep" from DATA section
[2021-04-17 16:40:56.11360] [5478] [debug] [Wmr6cN5KUJlP] 200 OK (0.005164s, 193.648/s)
<!DOCTYPE html>
<html>
    <head><title>Today</title></head>
    <body>
<ul>
    <li>2021年4月18日日曜日</li>
    <li>2021年4月18日</li>
    <li>2021/04/18</li>
    <li>2021/04/18</li>
</ul>
</body>
</html>

A full list of sup­port­ed locales is pro­vid­ed in the DateTime::Locale::Catalog documentation.

I hope this arti­cle has helped demon­strate that it’s not too hard to make your Perl web appli­ca­tions respect glob­al audi­ences, if only with dates. For more on local­iza­tion and Perl, start with the Locale::Maketext framework.