<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Fotis Papadogeorgopoulos</title>
    <description>The latest articles on Forem by Fotis Papadogeorgopoulos (@isfotis).</description>
    <link>https://forem.com/isfotis</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F139879%2Fdea158c5-de23-47ee-a99d-472ed0b75d34.jpg</url>
      <title>Forem: Fotis Papadogeorgopoulos</title>
      <link>https://forem.com/isfotis</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/isfotis"/>
    <language>en</language>
    <item>
      <title>HTTPS for Local Sites</title>
      <dc:creator>Fotis Papadogeorgopoulos</dc:creator>
      <pubDate>Mon, 27 Jul 2020 12:11:29 +0000</pubDate>
      <link>https://forem.com/isfotis/https-for-local-sites-3o0p</link>
      <guid>https://forem.com/isfotis/https-for-local-sites-3o0p</guid>
      <description>&lt;p&gt;I recently set up &lt;a href="https://pi-hole.net/" rel="noopener noreferrer"&gt;pi-hole&lt;/a&gt;, a local DNS sinkhole. It redirects lookups for ad-related domains into the void (and any other domain, via a blocklist). This post is not about pi-hole itself though. Rather, it is about adding HTTPs to the local interface.&lt;/p&gt;

&lt;p&gt;Let’s define the problem better. The pi-hole runs on a Raspberry Pi, on the local network. It does not listen on the public internet, and its port is not exposed to it. Pi-hole offers an admin interface as a web UI. It is available locally, under its IP address, or a domain that you can configure locally. I would like to have HTTPS, because accessing sensitive data over an unencrypted connection, local as it may be, seems icky.&lt;/p&gt;

&lt;p&gt;In order to enable HTTPS, we need some kind of domain, and a certificate for that domain. We also do not want to go for a self-signed certificate solution.&lt;/p&gt;

&lt;p&gt;We thus have to solve the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Getting a domain to point to our IP&lt;/li&gt;
&lt;li&gt;Getting a certificate for that domain&lt;/li&gt;
&lt;li&gt;Auto-renewing that certificate when it expires&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I will stress the automation part. Historically, I have been &lt;em&gt;really&lt;/em&gt; bad at remembering to update certificates, or restart servers after certificates auto-renew. You might have more consistency than I do!&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting a domain
&lt;/h2&gt;

&lt;p&gt;I decided to use a sub-domain of &lt;a href="https://fotis.xyz/posts/https-for-local-sites/fotis.xyz" rel="noopener noreferrer"&gt;fotis.xyz&lt;/a&gt;, in this case &lt;code&gt;pi-hole.fotis.xyz&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We could then resolve it via a DNS record to our local IP address, or do it per-device via a hosts file. I went with the hosts file, because I only really administrate the pi-hole through my laptop, and I can always access the admin via IP from other devices (albeit unencrypted). This solves the domain issue.&lt;/p&gt;

&lt;p&gt;Worrying about DHCP leases and changing IP for the pi-hole is out of scope for this post :)&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting a certificate for that domain
&lt;/h2&gt;

&lt;p&gt;This is the interesting part, where I had gotten stuck in the past. I want to use &lt;a href="https://letsencrypt.org/" rel="noopener noreferrer"&gt;LetsEncrypt&lt;/a&gt;, because they are awesome LetsEncrypt uses a mechanism called ACME (Automatic Certificate Management Environment) to issue challenges, that the server must answer to prove that you own the domain.&lt;/p&gt;

&lt;p&gt;The “regular” challenge mechanism, that I was familiar with at least, is over HTTP. The client uses a well-known address like &lt;code&gt;http://&amp;lt;YOUR_DOMAIN&amp;gt;/.well-known/acme-challenge/&amp;lt;TOKEN&amp;gt;&lt;/code&gt; to place the challenge token, which LetsEncrypt then validates. This process can be manual or automated, for example with plugins for popular servers, such as Apache and Nginx.&lt;/p&gt;

&lt;p&gt;However, this does not work for sites that are resolved locally. For example, my local Raspberry Pi only listens on local addresses. The HTTP port 80 is not exposed to the internet. An HTTP ACME challenge would fail, because the pi cannot be reached. It also means I must resolve the domain to the local address at a public DNS level, instead of locally.&lt;/p&gt;

&lt;p&gt;That’s when I found the &lt;a href="https://letsencrypt.org/docs/challenge-types/#dns-01-challenge" rel="noopener noreferrer"&gt;DNS Challenge mechanism&lt;/a&gt;. The ACME client makes the request, and gets a challenge token to place in a TXT DNS record (e.g. TXT for &lt;code&gt;_acme-challenge&lt;/code&gt;). LetsEncrypt then validates the presence of that record, to verify our control. This is promising, because the Pi can communicate outwards, and nothing needs to connect to its HTTP port!&lt;/p&gt;

&lt;h2&gt;
  
  
  Automation
&lt;/h2&gt;

&lt;p&gt;Now, you could copy this TXT record manually, but that sucks for the short renewal period of LetsEncrypt. Historically, as I mentioned, I have been bad at staying on top of it.&lt;/p&gt;

&lt;p&gt;Surprise! Most DNS providers offer an API these days, to add records and so on. So you can perform the addition of the TXT record as part of the challenge pipeline, with some artisanal scripting. This verifies that you have control of the domain, without caring about the ip it resolves to (or that ip needing to be listening on the internet).&lt;/p&gt;

&lt;p&gt;To avoid artisanal scripting, and make this reproducible, I used &lt;a href="https://letsencrypt.org/docs/challenge-types/" rel="noopener noreferrer"&gt;acme.sh&lt;/a&gt;, an ACME client for the command line. I have used &lt;a href="https://certbot.eff.org/" rel="noopener noreferrer"&gt;Certbot&lt;/a&gt; in the past, but I was a bit lazy about managing Python this time around! acme.sh has integrations to many DNS providers, including the one I use, &lt;a href="https://dnsimple.com/" rel="noopener noreferrer"&gt;DNSimple&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;h3&amp;gt;Warning&amp;lt;/h3&amp;gt;
 &amp;lt;p&amp;gt; This has some security implications, such as your web server having access to editing DNS. Depending on the granularity of the API tokens, this can be dangerous!&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt; &lt;a href="https://letsencrypt.org/docs/challenge-types/" rel="noopener noreferrer"&gt;The LetsEncrypt documentation outlines the pros and cons of each challenge type.&lt;/a&gt; &lt;/p&gt; 

&lt;p&gt; &lt;a href="https://www.eff.org/deeplinks/2018/02/technical-deep-dive-securing-automation-acme-dns-challenge-validation" rel="noopener noreferrer"&gt;This post by Joona Hoikkala on the EFF blog discusses different mitigation strategies.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A more applied example
&lt;/h2&gt;

&lt;p&gt;The following is a guide (mostly to myself) about setting this up with pi-hole specifically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install acme.sh
&lt;/h3&gt;

&lt;p&gt;First, &lt;a href="https://github.com/acmesh-official/acme.sh#1-how-to-install" rel="noopener noreferrer"&gt;install acme.sh via their instructions&lt;/a&gt;. I used the “pipe to shell” method, but I know not everyone likes that. Remember to restart the terminal session for it to appear in the PATH.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set up lighttpd
&lt;/h3&gt;

&lt;p&gt;We must respond to the host name that we define (e.g. pi-hole.fotis.xyz) from our Raspberry Pi. pi-hole uses &lt;a href="https://www.lighttpd.net/" rel="noopener noreferrer"&gt;lighttpd&lt;/a&gt; for the HTTP UI already, so it made sense to me to reuse it.&lt;/p&gt;

&lt;p&gt;I added the following file under &lt;code&gt;/etc/lighttpd/external.conf&lt;/code&gt;, adapted from &lt;a href="https://scotthelme.co.uk/securing-dns-across-all-of-my-devices-with-pihole-dns-over-https-1-1-1-1/" rel="noopener noreferrer"&gt;Scott Helme’s post on pi-hole&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install this under /etc/lighttpd/external.conf&lt;/span&gt;
&lt;span class="nv"&gt;$HTTP&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"host"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"pi-hole.fotis.xyz"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;# Ensure the Pi-hole Block Page knows that this is not a blocked domain&lt;/span&gt;
  setenv.add-environment &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"fqdn"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

  &lt;span class="c"&gt;# Enable the SSL engine with a LE cert, only for this specific host&lt;/span&gt;
  &lt;span class="nv"&gt;$SERVER&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"socket"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;":443"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    ssl.engine &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"enable"&lt;/span&gt;
    ssl.ca-file &lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="s2"&gt;"/home/pi/cert.pem"&lt;/span&gt;
    &lt;span class="c"&gt;# This file does not come right off acme.sh, it is merged from the two files&lt;/span&gt;
    ssl.pemfile &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/home/pi/merged.pem"&lt;/span&gt;
    ssl.honor-cipher-order &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"enable"&lt;/span&gt;
    ssl.cipher-list &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"&lt;/span&gt;
    ssl.use-compression &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"disable"&lt;/span&gt;
    ssl.use-sslv2 &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"disable"&lt;/span&gt;
    ssl.use-sslv3 &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"disable"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="c"&gt;# Redirect HTTP to HTTPS&lt;/span&gt;
  &lt;span class="nv"&gt;$HTTP&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"scheme"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"http"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$HTTP&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"host"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ &lt;span class="s2"&gt;".*"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      url.redirect &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;".*"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"https://%0&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Getting the certificate
&lt;/h3&gt;

&lt;p&gt;Now we must get the certificate. We want to use the &lt;a href="https://github.com/acmesh-official/acme.sh/wiki/dnsapi#25-use-dnsimple-api" rel="noopener noreferrer"&gt;DNS challenge mode of acme.sh&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We first get a token from DNSimple (this will vary on your provider).  &lt;/p&gt;

&lt;p&gt;I stored it in a &lt;code&gt;.env&lt;/code&gt; file locally on the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;DNSimple_OAUTH_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sdfsdfsdfljlbjkljlkjsdfoiwje"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then &lt;code&gt;source&lt;/code&gt;d that file, to export the variables on the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt; .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we are ready to issue the ACME challenge, and hopefully acme.sh will do the rest, including editing the DNS records:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;acme.sh &lt;span class="nt"&gt;--issue&lt;/span&gt; &lt;span class="nt"&gt;--dns&lt;/span&gt; dns_dnsimple &lt;span class="nt"&gt;-d&lt;/span&gt; pi-hole.fotis.xyz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once it is set up, acme.sh handles auto-renewal via a cron job. No more email snoozing, yay!&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing the certificate
&lt;/h3&gt;

&lt;p&gt;acme.sh will store the certificates in its internal folders (under &lt;code&gt;/home/pi/.acme-sh&lt;/code&gt;). We are not supposed to rely on that structure, so it provides an &lt;code&gt;--install-cert&lt;/code&gt; command to move the certificates to the correct place.&lt;/p&gt;

&lt;p&gt;I used the following command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/home/pi/.acme.sh/acme.sh &lt;span class="nt"&gt;--install-cert&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; pi-hole.fotis.xyz &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--key-file&lt;/span&gt;       /home/pi/key.pem  &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--fullchain-file&lt;/span&gt; /home/pi/cert.pem &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--reloadcmd&lt;/span&gt;     &lt;span class="s2"&gt;"cat /home/pi/key.pem /home/pi/cert.pem &amp;gt; /home/pi/merged.pem &amp;amp;&amp;amp; sudo service lighttpd restart"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some things to note here. First of all, the &lt;code&gt;home/pi/key.pem&lt;/code&gt; and &lt;code&gt;/home/pi/cert.pem&lt;/code&gt; locations are meant to match the location where lighttpd expects them. I was confused by the terminology of acme.sh and of lighttpd, so I just named them in a way that made sense to me!&lt;/p&gt;

&lt;p&gt;The “reloadcmd” argument is the tricky bit. It will run whenever the new certificate is installed, to prompt the server to use it. This command depends on your server. In my case, &lt;strong&gt;lighttpd needs the private key and chain file to be merged&lt;/strong&gt; , so I do this as part of the setup script.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hosts resolution
&lt;/h3&gt;

&lt;p&gt;I edited the hosts file (&lt;code&gt;/etc/hosts&lt;/code&gt;) on my own machine, to point the domain pi-hole.fotis.xyz to the known IP address of the pi-hole.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Replace x.x with your local address :)&lt;/span&gt;
192.168.x.x pi-hole.fotis.xyz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I can now visit pi-hole.fotis.xyz locally, and see it secured, hooray!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ffotis.xyz%2Fimg%2Fposts%2Fhttps-for-local-sites%2Fpadlock.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ffotis.xyz%2Fimg%2Fposts%2Fhttps-for-local-sites%2Fpadlock.jpg" alt="The website with a secure padlock sign, and the raspberry logo in the center."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;I hope this short guide on setting up HTTPS for local sites was useful! In addition to security, many modern browser APIs require HTTPS to function, so I suspect I will be using it again in the near future. Did I miss anything? Have you had a different way of doing this? &lt;a href="https://fotis.xyz/about#contact" rel="noopener noreferrer"&gt;Get in touch, and let’s talk!&lt;/a&gt;&lt;/p&gt;

</description>
      <category>https</category>
      <category>web</category>
      <category>security</category>
      <category>raspberrypi</category>
    </item>
    <item>
      <title>A toLocaleString Mystery</title>
      <dc:creator>Fotis Papadogeorgopoulos</dc:creator>
      <pubDate>Fri, 24 Jul 2020 07:10:50 +0000</pubDate>
      <link>https://forem.com/isfotis/a-tolocalestring-mystery-lel</link>
      <guid>https://forem.com/isfotis/a-tolocalestring-mystery-lel</guid>
      <description>&lt;p&gt;Recently, at work, one of our tests started failing. Our site is available in 11 languages, and the months for Azerbaijani (with the Latin script) had inconsistent capitalisation!&lt;/p&gt;

&lt;p&gt;After investigating, and a little bit of good guessing, it turned out to be an issue with the localisation data in the browser and Node themselves.&lt;/p&gt;

&lt;p&gt;This post digs into how I went about investigating that issue, with entirely too many diversions along the way. I hope it gives you a fun insight into how localisation data ends up in JS APIs, and how to spot errors!&lt;/p&gt;


  
    &lt;h3&gt;Numeronyms&lt;/h3&gt;
     &lt;p&gt; Internationalisation and Localisation are commonly referred to as i18n and l10n respectively. These are called numeronyms, representing the start and end letters, as well as the number of letters omitted. For example, &lt;code&gt;i - 18 letters - n&lt;/code&gt;. &lt;/p&gt; &lt;p&gt; Numeronyms can be useful as a short representation and provide more accurate terms for web search. For the purposes of this post, I'll type them out long-form, but I wanted to point it out in case you run into it in the links. &lt;/p&gt; &lt;p&gt; One day, I might shorten my name to Fotis P16s... &lt;/p&gt; 
  


&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;Let’s frame the problem.&lt;/p&gt;

&lt;p&gt;We have a function that provides a list of months (in the Gregorian calendar), localised for one of the languages and scripts that we support. For English US, that would be “January, February, March…”.&lt;/p&gt;

&lt;p&gt;JavaScript environments, whether web browsers such as Chrome and Firefox, or Node, provide a set of APIs for localisation and internationalisation. Two common ones are the &lt;code&gt;Intl&lt;/code&gt; namespace of APIs, and the &lt;code&gt;Date&lt;/code&gt; object with its &lt;code&gt;toLocaleString&lt;/code&gt; method. We use &lt;code&gt;toLocaleString&lt;/code&gt; specifically to get a localised month, for each month of the calendar.&lt;/p&gt;

&lt;p&gt;However, the result of calling those APIs can vary depending on the data that each browser has available.&lt;/p&gt;

&lt;p&gt;Because that possibility can sometimes be unexpected (especially for people that have not worked with multiple languages or scripts before), last year we added a series of tests to verify the localisation of months.&lt;/p&gt;

&lt;p&gt;Then, at some later point, our tests started failing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;AssertionError: expected [ Array(12) ] to deeply equal [ Array(12) ]
&lt;/span&gt;&lt;span class="gi"&gt;+ expected - actual
&lt;/span&gt;
[
&lt;span class="gd"&gt;-  "yanvar"
&lt;/span&gt;&lt;span class="gi"&gt;+  "Yanvar"
&lt;/span&gt;    "Fevral"
&lt;span class="gd"&gt;-  "mart"
&lt;/span&gt;&lt;span class="gi"&gt;+  "Mart"
&lt;/span&gt;    "Aprel"
    "May"
    "İyun"
    "İyul"
    "Avqust"
    "Sentyabr"
    "Oktyabr"
    "Noyabr"
&lt;span class="gd"&gt;-  "dekabr"
&lt;/span&gt;&lt;span class="gi"&gt;+  "Dekabr"
&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;In other words: the months for Azerbaijani with the Latin script, Yanvar (January), Mart (March) and Dekabr (December) were lower case, while all the other months were capitalised.&lt;/p&gt;

&lt;h2&gt;
  
  
  First step, checking our own function
&lt;/h2&gt;

&lt;p&gt;Before going down the path that the data might be wrong, let’s make sure that our own function is not doing anything absurd.&lt;/p&gt;

&lt;p&gt;The function itself is provided below, a small wrapper around calling &lt;code&gt;toLocaleString&lt;/code&gt; for 12 Dates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;getArrayOfMonths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localeTag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Months for Date are 0.=11&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;months&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;month&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dateobj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1970&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;dateobj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toLocaleString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localeTag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;month&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;long&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;months&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;(There are subtleties to getting a list of months this way, which may make the results wrong or unidiomatic. In our use, those are fine, but I am listing an example with noun cases at the end of the article.)&lt;/p&gt;

&lt;p&gt;Running this function in Firefox and Node (with localisation data, more on that later!) brings up the same results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Node&lt;/span&gt;
&lt;span class="c1"&gt;// NODE_ICU_DATA=node_modules/full-icu node    &lt;/span&gt;
&lt;span class="c1"&gt;// Welcome to Node.js v12.16.3.&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;getArrayOfMonths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;az-AZ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yanvar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Fevral&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Aprel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;May&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;İyun&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;İyul&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Avqust&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sentyabr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Oktyabr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Noyabr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dekabr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;// Firefox&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;getArrayOfMonths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;az-AZ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yanvar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Fevral&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Aprel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;May&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;İyun&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;İyul&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Avqust&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sentyabr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Oktyabr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;…&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Firefox and Node having the same inconsistent capitalisation was already tipping me off.&lt;/strong&gt; They are different engines, so them processing the data in the same odd way seemed too good to be a coincidence.&lt;/p&gt;

&lt;p&gt;Chrome only prints out English months, but that is as-intended, because it does not support Azerbaijani in &lt;code&gt;Intl&lt;/code&gt;/&lt;code&gt;toLocaleString&lt;/code&gt; yet, and I did not specify a fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding if a locale is supported with Intl
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Intl&lt;/code&gt; family of APIs is really powerful. They have a bunch of namespaces and constructors to account for different lingustic artefacts and locales. For example, there is &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat"&gt;&lt;code&gt;Intl.DateTimeFormat&lt;/code&gt;&lt;/a&gt; for formatting dates and times (day month year? month day year? fight!).&lt;/p&gt;

&lt;p&gt;One useful function is &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/supportedLocalesOf"&gt;&lt;code&gt;Intl.DateTimeFormat.supportedLocalesOf&lt;/code&gt;&lt;/a&gt;. It takes an array of locales as &lt;a href="https://tools.ietf.org/html/bcp47"&gt;BCP 47 language tags&lt;/a&gt;, such as &lt;code&gt;en-GB&lt;/code&gt; (English as used in Great Britan) or &lt;code&gt;el-GR&lt;/code&gt; (Hellenic/Greek as used in Greece) as an argument, and returns an array of the ones that are supported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;supportedLocalesOf&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;az-AZ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en-GB&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;el-GR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;az-AZ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en-GB&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;el-GR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Here I would go on a tangent about locales being a complex interaction of languages, regions and scripts, but this post has already too many diversions, and I don’t feel qualified to give you good examples.&lt;/p&gt;

&lt;p&gt;To account for these interactions, BCP 47 tags have optional components for scripts, region or country codes, variants, and also reserved extensions. I found this &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#Locale_identification_and_negotiation"&gt;article from MDN on locale identification&lt;/a&gt; useful for a short explanation.&lt;/p&gt;

&lt;p&gt;Azerbaijani (as far as my searching shows, I might be wrong) has both a Latin and Cyrillic script. Those would be &lt;code&gt;az-Latn-AZ&lt;/code&gt; and &lt;code&gt;az-Cyrl-AZ&lt;/code&gt; respectively. As far as I can tell, &lt;code&gt;az-AZ&lt;/code&gt; defaults to Latin, but I am not sure if that is an artefact of a specific data source.&lt;/p&gt;

&lt;h2&gt;
  
  
  A past Chrome bug with supportedLocalesOf
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;When I started seeing issues with Azerbaijani in particular, I was already on my toes about issues with data.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;About a year ago, we had run into a bug with Azerbaijani and Chrome, which claimed it supported it via &lt;code&gt;supportedLocalesOf&lt;/code&gt;, but would give placeholder months.&lt;/p&gt;

&lt;p&gt;In particular, this was the behaviour from this function back then (circa July 2019):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;supportedLocalesOf&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;az-AZ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;az-AZ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;// Means it is supported&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;getArrayOfMonths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;az-AZ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;M0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;M1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;M2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;M3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="nx"&gt;M11&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;In other words, ‘az-AZ’ was purportedly supported, but the months were these odd &lt;code&gt;M0&lt;/code&gt; to &lt;code&gt;M11&lt;/code&gt; months, which seemed like internal placeholders. If Azerbaijani was unsupported, I would expect &lt;code&gt;supportedLocalesOf&lt;/code&gt; to not report it, and also the months to be in English GB (because that is my system locale, and I did not specify a fallback).&lt;/p&gt;

&lt;p&gt;After double- and triple-checking with colleagues and different platforms, &lt;a href="https://bugs.chromium.org/p/chromium/issues/detail?id=984922"&gt;I filed a bug in Chromium&lt;/a&gt;, and it was confirmed! It was eventually fixed, and &lt;code&gt;supportedLocalesOf&lt;/code&gt; reports Azerbaijani as unsupported.&lt;/p&gt;

&lt;p&gt;Long story short, Azerbaijani being unsupported indicates to me that the localisation data might be incomplete. I have referenced “the data” multiple times now; let’s dive into what that data is, and where it comes from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Localisation data: ICU, CLDR, oh my!
&lt;/h2&gt;

&lt;p&gt;Let’s take a few different Intl APIs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DateTimeFormat, uhm, formatting (as is bugging us so far)&lt;/li&gt;
&lt;li&gt;Pluralization (e.g. apple, 2 of them = two apples, or more complex changes for languages that differentiate between “one”, “a handful”, and “many”)&lt;/li&gt;
&lt;li&gt;Locale names (e.g. saying that “Greek” is “Ελληνικά” in Greek)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can imagine that all of the underlying data (calendars, names of months, pluralization rules) must be coming from somewhere!&lt;/p&gt;

&lt;p&gt;Indeed, there is a standard resource for these in the &lt;a href="http://userguide.icu-project.org/icudata"&gt;ICU (International Components for Unicode) data&lt;/a&gt;. Quoting from the site:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ICU makes use of a wide variety of data tables to provide many of its services. Examples include converter mapping tables, collation rules, transliteration rules, break iterator rules and dictionaries, and other locale data. Additional data can be provided by users, either as customizations of ICU’s data or as new data altogether.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A related data-set is the &lt;a href="http://cldr.unicode.org/"&gt;CLDR (Unicode Common Locale Data Repository)&lt;/a&gt;. Quoting again from the site:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The Unicode CLDR provides key building blocks for software to support the world’s languages, with the largest and most extensive standard repository of locale data available. This data is used by a wide spectrum of companies for their software internationalization and localization, adapting software to the conventions of different languages for such common software tasks. It includes: …&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The ICU data-set uses CLDR itself for many things, with a few differences:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Data which is NOT sourced from CLDR includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Conversion Data&lt;/li&gt;
&lt;li&gt;Break Iterator Dictionary Data ( Thai, CJK, etc )&lt;/li&gt;
&lt;li&gt;Break Iterator Rule Data (as of this writing, it is manually kept in sync with the CLDR datasets)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those data come in different formats, such as XML (LDML), categorised by locale (roughly, that I can tell). The &lt;a href="http://cldr.unicode.org/#TOC-How-to-Use-"&gt;ICU data seems more commonly used by higher-level libraries&lt;/a&gt;, because the format is more compact.&lt;/p&gt;

&lt;p&gt;With this data available, browsers have enough information to provide the richer &lt;code&gt;Intl&lt;/code&gt; and &lt;code&gt;Date&lt;/code&gt; localisation APIs.&lt;/p&gt;


  
    &lt;h3&gt;Handwaving&lt;/h3&gt;
     &lt;p&gt; Here are some things I am handwaving at this point. &lt;/p&gt; &lt;p&gt; I use ICU and CLDR rather interchangeably. As far as I can tell, the ICU data is derived from the CLDR data. I found better links for the CLDR sources, so I am digging into those. &lt;/p&gt; &lt;p&gt; I am also not 100% clear on whether &lt;em&gt;all&lt;/em&gt; browsers use the ICU/CLDR data at the moment, or use some other source. I could not find anything normative about the data source in the specs (I would find that surprising anyway), and I am bad at going through issue trackers. &lt;/p&gt; &lt;p&gt; I found one tracking issue about Firefox transitioning to the CLDR data, and at least my testing seems to support that. Perhaps the CLDR data version would be useful for browsers to expose? Not as an API, rather an `about:` config or something similar in the UI. &lt;/p&gt; &lt;p&gt; Node definitely uses the ICU data, and gets its own following section for it. &lt;/p&gt; 
  


&lt;h2&gt;
  
  
  Excerpt from the CLDR Data
&lt;/h2&gt;

&lt;p&gt;For example, here is the top-level directory structure from one &lt;a href="https://unicode.org/Public/cldr/35.1/"&gt;download of the CLDR data&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; tree &lt;span class="nt"&gt;-L&lt;/span&gt; 1 cldr-common-35.1/
cldr-common-35.1/common/
├── annotations
├── annotationsDerived
├── bcp47
├── casing
├── collation
├── dtd
├── main
├── properties
├── rbnf
├── segments
├── subdivisions
├── supplemental
├── transforms
├── uca
└── validity
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;An excerpt from the &lt;code&gt;main&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; cldr-exploration tree &lt;span class="nt"&gt;-L&lt;/span&gt; 1 cldr-common-35.1/common/main
cldr-common-35.1/common/main
├── af_NA.xml
├── af.xml
├── af_ZA.xml
├── agq_CM.xml
├── agq.xml
├── ak_GH.xml
├── ak.xml
├── am_ET.xml
├── am.xml
├── ar_001.xml
├── ar_AE.xml
├── ar_BH.xml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And here is part of the data for English (&lt;code&gt;common/main/en.xml&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;monthWidth&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"wide"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;month&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;January&lt;span class="nt"&gt;&amp;lt;/month&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;month&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;February&lt;span class="nt"&gt;&amp;lt;/month&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;month&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;March&lt;span class="nt"&gt;&amp;lt;/month&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;month&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;April&lt;span class="nt"&gt;&amp;lt;/month&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;month&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"5"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;May&lt;span class="nt"&gt;&amp;lt;/month&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;month&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;June&lt;span class="nt"&gt;&amp;lt;/month&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;month&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"7"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;July&lt;span class="nt"&gt;&amp;lt;/month&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;month&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;August&lt;span class="nt"&gt;&amp;lt;/month&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;month&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"9"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;September&lt;span class="nt"&gt;&amp;lt;/month&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;month&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;October&lt;span class="nt"&gt;&amp;lt;/month&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;month&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"11"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;November&lt;span class="nt"&gt;&amp;lt;/month&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;month&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"12"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;December&lt;span class="nt"&gt;&amp;lt;/month&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/monthWidth&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h2&gt;
  
  
  ICU and Node
&lt;/h2&gt;

&lt;p&gt;If you have tried to work with internationalisation in Node, you might have run into the ICU data yourself.&lt;/p&gt;

&lt;p&gt;Up until version 13 (a few months ago), Node only had a base English locale loaded. The ICU data takes up space on the order of tens of megabytes, and so Node for the longest time did not come with them installed.&lt;/p&gt;

&lt;p&gt;To get correct localisations in Node, you had to either a) build Node yourself with the &lt;code&gt;full-icu&lt;/code&gt; dataset loaded, or b) install the correct build of the icu data locally, and provide the path via &lt;code&gt;NODE_ICU_DATA&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It was messy, and probably still exists as an arcane parameter in current and aging codebases. Watch tests fail because &lt;code&gt;NODE_ICU_DATA&lt;/code&gt; is not supplied, ugh.&lt;/p&gt;

&lt;p&gt;Node getting the full ICU data built-in from version 13 was one of my favourite features, and if you’ve read this far, at least someone else might now understand my excitement!&lt;/p&gt;

&lt;p&gt;If you are curious:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/nodejs/node/pull/29522"&gt;The issue for providing ICU data by default&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nodejs.org/api/intl.html#intl_internationalization_support"&gt;Node’s internationalisation guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either way, now that we have gone through all the abbreviations, we’re in a good spot to find the data and investigate it!&lt;/p&gt;

&lt;h2&gt;
  
  
  Digging into the CLDR data
&lt;/h2&gt;

&lt;p&gt;Time to dig into the CLDR data, to validate whether the months in Azerbaijani show up capitalised, uncapitalised, or inconsistent.&lt;/p&gt;

&lt;p&gt;To check for any changes (and in the case of our test, regressions), I downloaded &lt;a href="http://cldr.unicode.org/index/downloads"&gt;CLDR versions&lt;/a&gt; 35.1, 36.1 and 37.&lt;/p&gt;

&lt;p&gt;I started browsing through the directories and quickly got lost because my search skills are bad.&lt;/p&gt;

&lt;p&gt;I then decided to go with a more drastic approach, and headed to the command line. In my case Gnome Terminal on Linux, but iTerm on MacOS or Windows Subsystem for Linux would work just as well, if you want to follow along.&lt;/p&gt;

&lt;p&gt;There is a nice utility called &lt;a href="https://github.com/BurntSushi/ripgrep"&gt;&lt;code&gt;ripgrep&lt;/code&gt;&lt;/a&gt; which can search through files very fast. It is written in Rust and is lovely, though to be honest I just didn’t remember the &lt;code&gt;grep&lt;/code&gt; flags any more.&lt;/p&gt;

&lt;p&gt;Anyway, I went searching through the files. I used “Yanvar” capital case and “yanvar” lower case for the known issues, as well as “Oktyabr” capital case and “oktyabr” lower case as a control.&lt;/p&gt;

&lt;p&gt;The results from ripgrep across three versions follow, and then a long-form explanation of them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Yanvar capital case - 1 result from version 35.1&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;  az-AZ-exploration rg &lt;span class="s2"&gt;"Yanvar"&lt;/span&gt; cldr&lt;span class="k"&gt;*&lt;/span&gt;/&lt;span class="k"&gt;**&lt;/span&gt;/az.xml 
cldr-common-35.1/common/main/az.xml
1412:  &amp;lt;month &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;Yanvar&amp;lt;/month&amp;gt;

&lt;span class="c"&gt;# Yanvar lower case - two results for version 36.1 and 37, one for 35.1&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;  az-AZ-exploration rg &lt;span class="s2"&gt;"yanvar"&lt;/span&gt; cldr&lt;span class="k"&gt;*&lt;/span&gt;/&lt;span class="k"&gt;**&lt;/span&gt;/az.xml
cldr-common-37.0/common/main/az.xml
1360:  &amp;lt;month &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;yanvar&amp;lt;/month&amp;gt;
1404:  &amp;lt;month &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;yanvar&amp;lt;/month&amp;gt;

cldr-common-36.1/common/main/az.xml
1360:  &amp;lt;month &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;yanvar&amp;lt;/month&amp;gt;
1404:  &amp;lt;month &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;yanvar&amp;lt;/month&amp;gt;

cldr-common-35.1/common/main/az.xml
1368:  &amp;lt;month &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;yanvar&amp;lt;/month&amp;gt;

&lt;span class="c"&gt;# Oktyabr capital case - one result for each version&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;  az-AZ-exploration rg &lt;span class="s2"&gt;"Oktyabr"&lt;/span&gt; cldr&lt;span class="k"&gt;*&lt;/span&gt;/&lt;span class="k"&gt;**&lt;/span&gt;/az.xml
cldr-common-37.0/common/main/az.xml
1413:  &amp;lt;month &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"10"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;Oktyabr&amp;lt;/month&amp;gt;

cldr-common-36.1/common/main/az.xml
1413:  &amp;lt;month &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"10"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;Oktyabr&amp;lt;/month&amp;gt;

cldr-common-35.1/common/main/az.xml
1421:  &amp;lt;month &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"10"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;Oktyabr&amp;lt;/month&amp;gt;

&lt;span class="c"&gt;# Oktyabr lower case - one result for each version&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;  az-AZ-exploration rg &lt;span class="s2"&gt;"oktyabr"&lt;/span&gt; cldr&lt;span class="k"&gt;*&lt;/span&gt;/&lt;span class="k"&gt;**&lt;/span&gt;/az.xml
cldr-common-37.0/common/main/az.xml
1369:  &amp;lt;month &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"10"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;oktyabr&amp;lt;/month&amp;gt;

cldr-common-36.1/common/main/az.xml
1369:  &amp;lt;month &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"10"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;oktyabr&amp;lt;/month&amp;gt;

cldr-common-35.1/common/main/az.xml
1377:  &amp;lt;month &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"10"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;oktyabr&amp;lt;/month&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;We have a winner! From version 36 onward, we get “yanvar” as lower case for January, while “Fevral” for February stays capitalised for all versions. The same pattern repeats with March and December. Version 35, by comparison, has both Yanvar and Fevral (and all the other months) capitalised.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data sources
&lt;/h3&gt;

&lt;p&gt;Something I found interesting: the data for months appears in two places, once in a “months” entry, and once in a “calendar” entry (again, for the Gregorian calendar).&lt;/p&gt;

&lt;p&gt;The “months” entry has consistent capitalisation throughout. They are all lower case; “yanvar”, “fevral” and so on.&lt;/p&gt;

&lt;p&gt;This hints to me that Firefox and Node use the “calendar” entry for the names of the months in this case. It makes sense, because if you’ll recall our original function, we go through a &lt;code&gt;Date&lt;/code&gt; object’s &lt;code&gt;toLocaleString&lt;/code&gt;, which deals with dates directly, rather than canonical names or anything of that sort.&lt;/p&gt;

&lt;h3&gt;
  
  
  Changelog, Contributions
&lt;/h3&gt;

&lt;p&gt;I was curious as to what changed in version 36 onward.&lt;/p&gt;

&lt;p&gt;Diving into the &lt;a href="http://cldr.unicode.org/index/downloads/cldr-36"&gt;Changelog for the CLDR data version 36&lt;/a&gt; we find the following line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Additionally, the following locales had at least a 15% increase in basic coverage: az (Azerbaijani / Latin script), so (Somali / Latin script).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The inconsistent months might have been entered accidentally, or were caused somehow when the coverage was expanded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future steps
&lt;/h2&gt;

&lt;p&gt;This is all many words, for a simple change in our codebase at least: change the test to match the data (3 line change), alongside a description about why that is ok (200 words in the PR, however many words is this post).&lt;/p&gt;

&lt;p&gt;I am not keen on capitalising the months ourselves (today’s hotfix is tomorrow’s footgun), but we might do that specifically for Azerbaijani, with an inverse test case to notify us when the data is updated.&lt;/p&gt;

&lt;p&gt;Another thing I am looking into, is contributing the consistent capitalisation into the CLDR. Ideally, I would like to submit it as something to be approved by a native speaker, because who the heck am I to say what the capitalisation of months in Azerbaijani should be!&lt;/p&gt;

&lt;p&gt;I have not really researched the CLDR process, so this might all be simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Long story short: sometimes, it is the data.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This whole process was some of the most fun I’ve had at work this month! I love it when the different abstraction layers (specs, JS APIs, JS hosts, CLDR data, bugs, messiness) fall into place. Localisation and internationalisation take a lot of material effort, so diving into it makes me appreciate it much more.&lt;/p&gt;

&lt;p&gt;In this case, I am also fond of our team’s past selves. We had the tests in place, and had already gone into the ICU/CLDR rabbit hole a year ago, filing the Chrome bug. It was both a time-saver, and brought a smile to my face.&lt;/p&gt;

&lt;p&gt;I hope I managed to impart at least a glimpse of that fun to you, and that you found something interesting here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fotis.xyz/about#contact"&gt;I’ll be happy to discuss this post and any linked resources!&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix: When this method of getting months goes wrong
&lt;/h2&gt;

&lt;p&gt;As mentioned previously, we go through a &lt;code&gt;Date&lt;/code&gt; object’s &lt;code&gt;toLocaleString&lt;/code&gt; to get the array of months.&lt;/p&gt;

&lt;p&gt;However, because the formatting happens in the context of a date, languages with different cases might inflect the month.&lt;/p&gt;

&lt;p&gt;When running this function for Greek, we get the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;getArrayOfMonths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;el-GR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ιανουαρίου&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Φεβρουαρίου&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Μαρτίου&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Απριλίου&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Μαΐου&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ιουνίου&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ιουλίου&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Αυγούστου&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Σεπτεμβρίου&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Οκτωβρίου&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Νοεμβρίου&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Δεκεμβρίου&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;All of these months are in the genitive case (denoting possession). This is the equivalent of saying “x of January”, “y of February” and so on in English. In our site, we use this function in the context of birthdays, so it ends up being ok! If, however, we wanted to only list the months, it would technically be wrong (we’d need the nominative case). Make sure to test for your use-case, and beware of tutorials only assuming English language rules.&lt;/p&gt;

&lt;p&gt;I am not certain how I would go about listing the months in the nominative, at least with the &lt;code&gt;Date&lt;/code&gt; object. &lt;code&gt;Intl&lt;/code&gt; has a draft (Stage 3) family of APIs called &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames"&gt;&lt;code&gt;Intl.DisplayNames&lt;/code&gt;&lt;/a&gt; that “enables the consistent translation of language, region and script display names”. Would something similar for month names be desirable? I’m not sure! Let me know if you know of an approach.&lt;/p&gt;

</description>
      <category>internationalization</category>
      <category>localization</category>
      <category>i18n</category>
      <category>javascript</category>
    </item>
    <item>
      <title>HTML details is awesome</title>
      <dc:creator>Fotis Papadogeorgopoulos</dc:creator>
      <pubDate>Sun, 24 Feb 2019 14:32:12 +0000</pubDate>
      <link>https://forem.com/isfotis/html-details-is-awesome-1c20</link>
      <guid>https://forem.com/isfotis/html-details-is-awesome-1c20</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally posted on &lt;a href="https://fotis.xyz/posts/details-is-awesome/" rel="noopener noreferrer"&gt;https://fotis.xyz/posts/details-is-awesome/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A while back, I read the slides and notes of a talk by &lt;a href="https://twitter.com/muanchiou" rel="noopener noreferrer"&gt;Mu-An Chiou&lt;/a&gt;, titled &lt;a href="https://github.com/muan/details-on-details" rel="noopener noreferrer"&gt;"Details on &amp;lt;details&amp;gt;"&lt;/a&gt;. It's a fantastic overview of how to use the &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; element of HTML. &lt;/p&gt;

&lt;p&gt;In short, &lt;code&gt;details&lt;/code&gt;, and its pair &lt;code&gt;summary&lt;/code&gt;, are a way to implement native collapsible sections, without relying on Javascript. Toggling content is one of the most common actions on the web, so being able to provide that without scripts is appealing. It is not just collapsible sections though; you can make panels, menus, and other imaginative things with it!&lt;/p&gt;

&lt;p&gt;I had stashed that knowledge at the back of my mind. Then, some weeks ago, I was implementing a collapsible section for the webmentions on this blog:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fhdi65xwmq03zb4wapqob.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fhdi65xwmq03zb4wapqob.jpg" alt="A webmentions section, with a button to show all comments"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This seemed like a great use case for details. I would get the native functionality of toggling, and could then work with styling it. Let's go over how to implement it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing a "show all" section
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Markup
&lt;/h3&gt;

&lt;p&gt;You nest &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt; inside of &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;. &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt; is what appears as the "toggle button", regardless of the section being collapsed or not. Anything after that will be hidden and only appear when the widget is open.&lt;/p&gt;

&lt;p&gt;Here is what an initial attempt at our comments section would look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;details&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;summary&amp;gt;&lt;/span&gt;
    Show all (2)
  &lt;span class="nt"&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ol&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;I am a comment&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;I am another comment.&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ol&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/details&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/fpapado/embed/bZbbPr?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Styling
&lt;/h3&gt;

&lt;p&gt;There are a couple of extra things to know about styling details. &lt;/p&gt;

&lt;p&gt;The arrow is styled with the &lt;code&gt;list-style&lt;/code&gt; properties on &lt;code&gt;summary&lt;/code&gt;, except in Chrome, which uses &lt;code&gt;::-webkit-details-marker&lt;/code&gt;. You could consider resetting them and then relying on a unified &lt;code&gt;::before&lt;/code&gt; selector.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;open&lt;/code&gt; attribute is added when the summary is open. You can query that as &lt;code&gt;details[open]&lt;/code&gt; to style the container further. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details" rel="noopener noreferrer"&gt;The demo on MDN&lt;/a&gt; shows the open attribute in practice.&lt;/p&gt;

&lt;p&gt;I also found that on Safari, the &lt;code&gt;summary&lt;/code&gt; element cannot be a flexbox container, even if defined in CSS. I wanted to do that to show author images next to the "Show all" text. I added an extra div wrapper inside of summary, to act as a flexbox wrapper.&lt;/p&gt;

&lt;p&gt;After adding some more styles to our details, it can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;details&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"details-reset"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;summary&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"summary fg-highlight cursor-default"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-wrapper-for-safari"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Show all (2)&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"author-list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"author"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://fotis.xyz/img/logo.png"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"author-image"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"author"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://fotis.xyz/img/logo.png"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"author-image"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;ol&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;I am a comment&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;I am another comment&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ol&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/details&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--highlight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#da0993&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/**
  * Styles and resets for the &amp;lt;details&amp;gt; element.
  * @see https://github.com/primer/primer/blob/master/modules/primer-buttons/lib/button.scss#L206-L213
*/&lt;/span&gt;

&lt;span class="c"&gt;/* Remove marker added by the display: list-item browser default */&lt;/span&gt;
&lt;span class="nc"&gt;.details-reset&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;summary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;list-style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Remove marker added by details polyfill */&lt;/span&gt;
&lt;span class="nc"&gt;.details-reset&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;summary&lt;/span&gt;&lt;span class="nd"&gt;::before&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Remove marker added by Chrome */&lt;/span&gt;
&lt;span class="nc"&gt;.details-reset&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;summary&lt;/span&gt;&lt;span class="nd"&gt;::-webkit-details-marker&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Other styles */&lt;/span&gt;
&lt;span class="nc"&gt;.flex-wrapper-for-safari&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.fg-highlight&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--highlight&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.cursor-default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.author-list&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.author&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.author-image&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;gray&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/fpapado/embed/JzPPqQ?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Focus affordances
&lt;/h3&gt;

&lt;p&gt;In reality, our section summary has two distinct elements; the "show all" button, and the profile pictures of commenters. The whole summary is interactable as far as input methods and highlighting are concerned. This makes sense to start with, and is a benefit of using semantic HTML.&lt;/p&gt;

&lt;p&gt;I think we can do a bit better, by enhancing the focus, so that it highlights the action more clearly. If the user focuses (by keyboard or otherwise) on the section, I would like to only highlight the "show all" part. This "nested focus" pattern is something I took out of &lt;a href="https://inclusive-components.design/collapsible-sections#stylingthebutton" rel="noopener noreferrer"&gt;Heydon Pickering's "Collapsible Sections" article&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;details&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"details-reset"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;summary&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"nested-focus fg-highlight cursor-default"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-wrapper-for-safari"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"nested-focus-target"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Show all (2)&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"author-list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"author"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://fotis.xyz/img/logo.png"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"author-image"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"author"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://fotis.xyz/img/logo.png"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"author-image"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;ol&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;I am a comment&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;I am another comment&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ol&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/details&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--highlight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#da0993&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.nested-focus&lt;/span&gt;&lt;span class="nd"&gt;:focus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* NOTE: The only good reason to hide an outline is if you enhance it */&lt;/span&gt;
  &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.nested-focus&lt;/span&gt;&lt;span class="nd"&gt;:focus&lt;/span&gt; &lt;span class="nc"&gt;.nested-focus-target&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--highlight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/** The rest is the same as above */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/fpapado/embed/mobbZE?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Other things to consider would be &lt;code&gt;user-select&lt;/code&gt; and &lt;code&gt;cursor&lt;/code&gt;. &lt;code&gt;user-select&lt;/code&gt; in particular is an open question, so I left it intact. I did give the &lt;code&gt;summary&lt;/code&gt; a &lt;code&gt;cursor: default&lt;/code&gt;, though there are certainly different ways to prioritise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Polyfills
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/javan/details-element-polyfill" rel="noopener noreferrer"&gt;polyfill for details&lt;/a&gt; is great. It is small in size (just under 1.5kB), and covers IE and Edge well. It is also a good example of a layered polyfill, where parts of the functionality are polyfilled depending on what is supported. Give the source code a read, I learned a bunch from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.scottohara.me/blog/2018/09/03/details-and-summary.html" rel="noopener noreferrer"&gt;Scott O'Hara investigated the use of the details element&lt;/a&gt; with various screen reader and browser combinations. I would urge you to read that piece for more information and readouts. A summary would be that screen reader support is good, with a few edge cases pending.&lt;/p&gt;

&lt;p&gt;(I would love to fill more on this after looking into it myself, and checking the status of the bugs mentioned by Scott).&lt;/p&gt;

&lt;p&gt;Apart from Scott's notes, there are a couple of things I would point out for collapsible sections.&lt;/p&gt;

&lt;h3&gt;
  
  
  Avoid changing the display text
&lt;/h3&gt;

&lt;p&gt;You might notice in the example above that I avoided changing the "Show all" text when expanding. You could do this with Javascript, or even some CSS conditional on the &lt;code&gt;open&lt;/code&gt; attribute. Beware though! In toggle buttons, it is common to not change the text, since the state is already communicated via &lt;code&gt;aria-expanded&lt;/code&gt; (&lt;a href="https://inclusive-components.design/toggle-button/" rel="noopener noreferrer"&gt;Heydon Pickering writes about toggle buttons&lt;/a&gt;). I think collapsible sections with the details element should follow the same idea, since the state is communicated natively.&lt;/p&gt;

&lt;p&gt;If I did want to change the display text, I would try to always expose the same text to screen readers, and keep parity with toggle buttons.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;details&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- This is exposed to AT --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"visually-hidden"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Show all (2)&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- These will be hidden from AT --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;aria-hidden=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"summary-open"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Hide all&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;aria-hidden=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"summary-closed"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Show all (2)&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;I am a comment&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;I am another comment.&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/details&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Hide visually, but keep in the tree for AT */&lt;/span&gt;
&lt;span class="nc"&gt;.visually-hidden&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;details&lt;/span&gt; &lt;span class="nc"&gt;.summary-open&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;details&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;open&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="nc"&gt;.summary-open&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inline&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;details&lt;/span&gt; &lt;span class="nc"&gt;.summary-closed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inline&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;details&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;open&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="nc"&gt;.summary-closed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, keeping the &lt;a href="https://www.w3.org/TR/using-aria/" rel="noopener noreferrer"&gt;rules of ARIA usage&lt;/a&gt; in mind, and gauging the value of this feature, I would not implement it. It is not a big deal if "Show all" persists visually, and I like that the presentation is unified. Also, the arrow is a good indicator already, so if you need this feature, then consider not removing it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Avoid interactive content in &lt;code&gt;summary&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/whatwg/html/issues/2272" rel="noopener noreferrer"&gt;Interactive content in summary is an open question&lt;/a&gt; on the spec. I also would question the need for that. I would not place, say, a &lt;code&gt;button&lt;/code&gt; inside a &lt;code&gt;button&lt;/code&gt;, and in the same spirit I would not nest something interactive inside summary. Your mileage may vary, and it might be a chance to rework the widget, if you have this use case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The details element is a great part of HTML. It offers a lot of commonplace functionality without relying on JS. This is important when it comes to resilience. Between requests failing, or a heavy load on the browser (such as when hydrating server-rendered markup), being able to collapse sections without relying on main scripts is valuable.&lt;/p&gt;

&lt;p&gt;There are even more &lt;a href="https://github.com/muan/details-on-details#githubs-details-dependent-custom-elements" rel="noopener noreferrer"&gt;interesting uses of details by Github&lt;/a&gt;, such as modals and menus. It has a couple styling hooks of its own, and even some UX questions to explore. The polyfill is well-encompasing, and the accessibility story pretty solid.&lt;/p&gt;

&lt;p&gt;Have you tried using the details element in your projects? How did you find it? Let me know!&lt;/p&gt;

&lt;h2&gt;
  
  
  More resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/muan/details-on-details#githubs-details-dependent-custom-elements" rel="noopener noreferrer"&gt;Mu-An's presentation and notes&lt;/a&gt; that set me on this path. It offers a concise explanation and guidance, perfect for bookmarking and referring to, say, when writing an article :)&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://github.com/muan/details-on-details#githubs-details-dependent-custom-elements" rel="noopener noreferrer"&gt;MDN guide on details&lt;/a&gt; has a good summary (no pun intended)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://html.spec.whatwg.org/multipage/interactive-elements.html#the-details-element" rel="noopener noreferrer"&gt;The spec for details&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/javan/details-element-polyfill" rel="noopener noreferrer"&gt;The polyfill for details&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Heydon Pickering's Inclusive Components has a chapter that explains the various &lt;a href="https://inclusive-components.design/collapsible-sections" rel="noopener noreferrer"&gt;affordances that Collapsible Sections provide&lt;/a&gt;. He also goes into details about how to implement one with Javascript. Give it a read, it is good stuff.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>resilience</category>
      <category>html</category>
      <category>css</category>
      <category>a11y</category>
    </item>
  </channel>
</rss>
