<?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>DEV Community: Konstantin Iliev</title>
    <description>The latest articles on DEV Community by Konstantin Iliev (@konstantin_iliev).</description>
    <link>https://dev.to/konstantin_iliev</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4229443%2Fac5958b2-ddd4-4b45-9d06-c77569b2efae.jpg</url>
      <title>DEV Community: Konstantin Iliev</title>
      <link>https://dev.to/konstantin_iliev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/konstantin_iliev"/>
    <language>en</language>
    <item>
      <title>A world clock that doesn't trust your clock: lessons from 64 500 city pages in plain PHP</title>
      <dc:creator>Konstantin Iliev</dc:creator>
      <pubDate>Fri, 06 Nov 2026 00:39:18 +0000</pubDate>
      <link>https://dev.to/konstantin_iliev/a-world-clock-that-doesnt-trust-your-clock-lessons-from-64-500-city-pages-in-plain-php-37a6</link>
      <guid>https://dev.to/konstantin_iliev/a-world-clock-that-doesnt-trust-your-clock-lessons-from-64-500-city-pages-in-plain-php-37a6</guid>
      <description>&lt;p&gt;I built &lt;a href="https://whattimeisit.zone" rel="noopener noreferrer"&gt;WhatTimeIsIt.zone&lt;/a&gt;, a site that answers one question for about 64 500 places in 245 countries and territories: what time is it there right now? Each place has its own page with the time, the time zone, the next clock change, sunrise and sunset, the moon, the weather and the next public holiday. There are also 25+ small tools (a time zone converter, a time zone map, holidays in 247 countries, calendars and so on), in English and Bulgarian.&lt;/p&gt;

&lt;p&gt;The stack is deliberately boring: plain PHP 8 and MariaDB on a shared hosting plan with LiteSpeed, no framework, no front-end framework, a few small vanilla JavaScript files. A &lt;code&gt;git push&lt;/code&gt; to &lt;code&gt;main&lt;/code&gt; deploys it.&lt;/p&gt;

&lt;p&gt;"Show the time" sounds like a one-liner, but it turned out to be full of small traps. Here are the ones I found most interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Your device clock is often wrong, so measure it
&lt;/h2&gt;

&lt;p&gt;A clock in the browser is only as good as &lt;code&gt;Date.now()&lt;/code&gt;, and the device clock can be off by seconds or even minutes. So the page measures it. It asks the server for the time five times and keeps the answer with the fastest round trip, because that one has the smallest error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;samples&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;one&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/time&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="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-store&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="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;rtt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="c1"&gt;// The server's time when the answer arrived, minus the device time&lt;/span&gt;
        &lt;span class="nx"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;rtt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rtt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;skew&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rtt&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;one&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;done&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;});&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;The skew is then added to every tick of the clock, and the page tells you the result: "Your clock is exact (±0.01 s)" or "Your clock is 1.3 s fast". The precision is half of the best round trip. When you come back to the tab after more than ten minutes, it measures again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fodslhzg3doxriqn6cbx7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fodslhzg3doxriqn6cbx7.png" alt="The clock for London with the line &amp;quot;Your clock is exact (±0.01 s)&amp;quot;" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Your browser's time zone data can be out of date too
&lt;/h2&gt;

&lt;p&gt;This one surprised me. &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt; uses the time zone database that ships with the browser's copy of ICU, and that copy can be years old. While testing I found that with ICU 74 (time zone data 2023c), Kazakhstan was an hour ahead (it moved to a single UTC+5 in 2024), and Morocco, British Columbia and Paraguay were wrong after their recent changes. A newer zone such as &lt;code&gt;America/Coyhaique&lt;/code&gt; (2025) was simply unknown.&lt;/p&gt;

&lt;p&gt;The same applies on the server: PHP's &lt;code&gt;intl&lt;/code&gt; extension has its own ICU copy, separate from the IANA database that &lt;code&gt;DateTimeZone&lt;/code&gt; uses.&lt;/p&gt;

&lt;p&gt;So the site has one source of truth: the IANA database bundled with PHP, which I keep up to date (a weekly job reports which version the hosting runs). The server computes the offset; ICU only formats. In the browser, each page carries the offsets of the zones it shows: the current one and any change in the next three days. The clock never asks the browser what the offset is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Z = { "Asia/Almaty": [300], "Europe/London": [0], ... }&lt;/span&gt;
&lt;span class="c1"&gt;// A zone with a change in the next 3 days also gets [moment in ms, new offset],&lt;/span&gt;
&lt;span class="c1"&gt;// e.g. on 26 March 2027: "Europe/London": [0, [1806195600000, 60]]&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;zoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Z&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tz&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&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="nx"&gt;o&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// With a known offset, shift the moment and let Intl format it as UTC&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;zoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tz&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;o&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tz&lt;/span&gt;&lt;span class="p"&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;ts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="nx"&gt;e4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UTC&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zone names from ICU ("Eastern European Summer Time") are shown only when ICU agrees with PHP on the offset. Otherwise the name could be wrong too.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. PHP's sunrise was a minute or two off
&lt;/h2&gt;

&lt;p&gt;Every page shows sunrise and sunset, computed on the server with no API. I started with PHP's &lt;code&gt;date_sun_info()&lt;/code&gt; and then checked it against independent astronomy libraries. In my tests, PHP returned sunrise and sunset when the sun was about 1.1° below the horizon, instead of the standard 0.833° (atmospheric refraction plus the radius of the sun's disc). That is 1 to 2 minutes of difference, which is exactly the kind of thing people notice on a site about time. The site now refines both moments to −0.833°.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Cleaning up 64 500 place names
&lt;/h2&gt;

&lt;p&gt;The places come from GeoNames. It is a great dataset, but in countries with a non-Latin script it writes names in a scholarly transcription: Thāne, Ghāziābād, Orël, Pátra, Ḩamāh. English speakers (and Wikipedia) write Thane, Ghaziabad, Oryol, Patras, Hama.&lt;/p&gt;

&lt;p&gt;GeoNames also links many places to their English Wikipedia article, so the title of that article is a good English name. But you can't take it blindly. When I tried it on every country, I got Potenza → Potentia (the link pointed to the ancient city), Svoge → Svogue and Middletown → Middletown Township. So the rule is narrow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;only for names with these marks, in countries whose script is not Latin (Japan is left out: Wikipedia writes Kōfu too);&lt;/li&gt;
&lt;li&gt;take the Wikipedia title if it is the same name once the marks are removed, or a very close spelling;&lt;/li&gt;
&lt;li&gt;accept a more distant spelling (Orël → Oryol) only if the GeoNames English name agrees;&lt;/li&gt;
&lt;li&gt;reject titles about something else: a district, an airport, a bigger city;&lt;/li&gt;
&lt;li&gt;otherwise just remove the marks (Bārākpur → Barakpur).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The comparison folds both names first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;fold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;\Normalizer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;\Normalizer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FORM_D&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;mb_strtolower&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/\p{Mn}+/u'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// drop the marks&lt;/span&gt;
    &lt;span class="nv"&gt;$s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/[‘’'`ʻʼ]/u"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;             &lt;span class="c1"&gt;// and apostrophes&lt;/span&gt;
    &lt;span class="nv"&gt;$s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/[\-–.]/u'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/\s+/u'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$s&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;Before writing anything, I ran the rules as a Python prototype on all places, then ported them to PHP and checked that both gave exactly the same 5 501 changes: 3 469 names from Wikipedia, 120 from GeoNames and 1 912 with the marks removed. The URLs did not change, since addresses on the site are permanent.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. A second language with 58 000 names nobody wrote
&lt;/h2&gt;

&lt;p&gt;The Bulgarian version needs every place name in Cyrillic. GeoNames has a real Bulgarian name for about 10% of the places, so the rest are transliterated: from the Ukrainian, Serbian, Greek or Russian name where it exists (never Russian for places in Ukraine), otherwise from the Latin name with rules for the language of each country (Spanish, Polish, pinyin and so on). English words in names follow Bulgarian spelling rules too, so "Village" doesn't come out letter by letter.&lt;/p&gt;

&lt;p&gt;Compared with the 6 641 places that do have a real Bulgarian name, 63% of the automatic names match exactly and 93% differ by at most two letters. Not perfect, but much better than showing Latin letters on a Bulgarian page.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Small things that kept it fast
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Each page carries only its own CSS&lt;/strong&gt;, inlined. A tool page gets the shared styles plus its own file, nothing else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The day and night map on the home page uses no map library.&lt;/strong&gt; The land is a 39 KB SVG (10 KB compressed) from Natural Earth via world-atlas, and the night is computed from the sun's position in three steps: below the horizon, civil and nautical twilight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Best time to call" is 24 grid cells and a line of text.&lt;/strong&gt; The page shows the city's working day next to yours and the hours you share.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxad2yucxeitu092arzaf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxad2yucxeitu092arzaf.png" alt="The day and night map with the largest cities" width="799" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp0pqh9s93tqcvs5we6e8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp0pqh9s93tqcvs5we6e8.png" alt="The &amp;quot;Best time to call London&amp;quot; block: 24 hours for London and for the visitor" width="799" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The site is also an installable PWA that keeps working offline, and a &lt;a href="https://apps.microsoft.com/detail/9PG7RTZC69B6" rel="noopener noreferrer"&gt;Windows app in the Microsoft Store&lt;/a&gt;. The Store app is a PWABuilder package that opens the live site, so changes to the site need no new package.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd like to hear
&lt;/h2&gt;

&lt;p&gt;If you have run into stale time zone data in a browser or on a server, I'd love to hear how you dealt with it. And if a time tool you need is missing, tell me in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I wrote this post with the help of AI, based on the project's code and my notes. I checked the facts and numbers before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>php</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
