From 65f041af6e484c9c3c5c6df8d32a0bcdaec89d41 Mon Sep 17 00:00:00 2001
From: Marc Durdin
Date: Tue, 22 Sep 2026 10:37:53 +0200
Subject: [PATCH 1/2] refactor: support mac .pkg and cleanup download path
references
Consolidate references to downloads across various pages and DRY out
repeated content. Add support for .pkg downloads when they become
available (will be automatic based on content from download server).
Test-bot: skip
---
_content/10/index.php | 12 +-
_content/11/index.php | 10 +-
_content/12/index.php | 2 +-
_content/13/index.php | 3 +-
_content/14/index.php | 13 +-
_content/15/index.php | 13 +-
_content/developer/download.php | 4 +-
_content/downloads/_downloads.php | 16 +-
_content/downloads/all-versions/index.php | 1 -
_content/downloads/index.php | 2 +-
_content/downloads/pre-release/index.php | 17 +-
.../downloads/releases/_version_downloads.php | 17 +-
_content/linux/download.php | 2 -
_content/mac/download.php | 5 +-
_content/windows/download.php | 5 +-
_content/windows/keyboards.php | 1 -
_includes/2020/DownloadUI.php | 186 ++++++++++++++++++
_includes/2020/KeymanDownloadVersions.php | 41 ----
_includes/includes/ui/downloads.php | 116 -----------
19 files changed, 250 insertions(+), 216 deletions(-)
create mode 100644 _includes/2020/DownloadUI.php
delete mode 100644 _includes/2020/KeymanDownloadVersions.php
delete mode 100644 _includes/includes/ui/downloads.php
diff --git a/_content/10/index.php b/_content/10/index.php
index 5d4a2eca..ef5321a8 100644
--- a/_content/10/index.php
+++ b/_content/10/index.php
@@ -1,12 +1,12 @@
What's New in Keyman Desktop 10 for Windows?
@@ -58,7 +58,7 @@
supports Unicode 11.0 and BCP 47 language identifiers. Desktop 10 also includes additional user interface for Turkish.
What's New in Keyman 10 for macOS?
@@ -68,7 +68,7 @@
and easily install keyboard packages by double-clicking the kmp file.
Keyman for Android is also available on the Play Store.
= $playstoreTable ?>
@@ -92,7 +92,7 @@
Swift 4.0 app.
What's New in KeymanWeb 10?
@@ -104,7 +104,7 @@
Developer Software
Keyman Developer 10 allows you to create
diff --git a/_content/11/index.php b/_content/11/index.php
index 1482d7ac..7c88ab19 100644
--- a/_content/11/index.php
+++ b/_content/11/index.php
@@ -1,11 +1,11 @@
What's New in Keyman 11 for macOS?
@@ -92,7 +92,7 @@
Keyman for Android is available on the Play Store.
= $playstoreTable ?>
@@ -122,7 +122,7 @@
@@ -131,7 +131,7 @@
Developer Software
What's new in Keyman Developer 11?
diff --git a/_content/12/index.php b/_content/12/index.php
index 91db9d88..a784b67d 100644
--- a/_content/12/index.php
+++ b/_content/12/index.php
@@ -1,8 +1,8 @@
User Software
What's New in Keyman 14 for Windows?
@@ -98,7 +99,7 @@
What's New in Keyman 14 for macOS?
@@ -144,7 +145,7 @@
= $playstoreTable ?>
@@ -188,7 +189,7 @@
What's New in KeymanWeb 14?
@@ -208,7 +209,7 @@
Developer Software
What's new in Keyman Developer 14?
diff --git a/_content/15/index.php b/_content/15/index.php
index 79fc5237..89bc2e9c 100644
--- a/_content/15/index.php
+++ b/_content/15/index.php
@@ -1,11 +1,12 @@
User Software
What's New in Keyman 15 for Windows?
@@ -111,7 +112,7 @@
What's New in Keyman 15 for macOS?
@@ -146,7 +147,7 @@
= $playstoreTable ?>
@@ -184,7 +185,7 @@
What's New in KeymanWeb 15?
@@ -201,7 +202,7 @@
Developer Software
What's new in Keyman Developer 15?
diff --git a/_content/developer/download.php b/_content/developer/download.php
index 55853861..2b391220 100644
--- a/_content/developer/download.php
+++ b/_content/developer/download.php
@@ -1,8 +1,8 @@
Keyman Developer Command Line Tools
diff --git a/_content/downloads/_downloads.php b/_content/downloads/_downloads.php
index 2bd26a40..4a7d7b89 100644
--- a/_content/downloads/_downloads.php
+++ b/_content/downloads/_downloads.php
@@ -1,7 +1,9 @@
= _m_Downloads('available_on_play_store') ?>
@@ -20,10 +22,10 @@
= _m_Downloads('products_for_software_developers') ?>
diff --git a/_content/downloads/all-versions/index.php b/_content/downloads/all-versions/index.php
index d286c7b4..1d3063ad 100644
--- a/_content/downloads/all-versions/index.php
+++ b/_content/downloads/all-versions/index.php
@@ -1,6 +1,5 @@
Keyman version history (all products)
Keyman for Linux
@@ -64,8 +65,8 @@
sudo apt install keyman onboard-keyman
You can also sign up to access pre-release versions through Google Play.
@@ -74,10 +75,10 @@
Please register for the pre-release versions through the links below for Keyman beta or
alpha pre-releases on your iOS device. This will grant access to the respective app version
through Apple's TestFlight app, which facilitates direct installation on iOS devices.
-= iosTestFlightTable(); ?>
+= DownloadUI::iosTestFlightTable(); ?>
diff --git a/_content/downloads/releases/_version_downloads.php b/_content/downloads/releases/_version_downloads.php
index 75c43bae..a32a5508 100644
--- a/_content/downloads/releases/_version_downloads.php
+++ b/_content/downloads/releases/_version_downloads.php
@@ -1,9 +1,10 @@
Keyman for Android is also available on the Play Store.
@@ -144,10 +145,10 @@ function array_key_first(array $arr) {
Products for Software Developers
diff --git a/_content/linux/download.php b/_content/linux/download.php
index fb5facfa..d66ccfc2 100644
--- a/_content/linux/download.php
+++ b/_content/linux/download.php
@@ -1,7 +1,5 @@
Install directly from keyman.com
Install via Homebrew
diff --git a/_content/windows/download.php b/_content/windows/download.php
index 8a408255..f77e301d 100644
--- a/_content/windows/download.php
+++ b/_content/windows/download.php
@@ -1,6 +1,7 @@
diff --git a/_content/windows/keyboards.php b/_content/windows/keyboards.php
index ccf5f082..eb5c8e13 100644
--- a/_content/windows/keyboards.php
+++ b/_content/windows/keyboards.php
@@ -1,6 +1,5 @@
= 1073741824) {
+ $bytes = number_format($bytes / 1073741824, 2) . ' GB';
+ } elseif ($bytes >= 1048576) {
+ $bytes = number_format($bytes / 1048576, 2) . ' MB';
+ } elseif ($bytes >= 1024) {
+ $bytes = number_format($bytes / 1024, 2) . ' KB';
+ } elseif ($bytes > 1) {
+ $bytes = $bytes . ' bytes';
+ } elseif ($bytes == 1) {
+ $bytes = $bytes . ' byte';
+ } else {
+ $bytes = '0 bytes';
+ }
+
+ return $bytes;
+ }
+
+ private static function filenamePatterns($target, $tier) {
+ global $versions;
+ switch($target) {
+ case 'android': return 'keyman-$version.apk';
+ case 'android-engine': return 'keyman-engine-android-$version.zip';
+ case 'developer': return ['keymandeveloper-$version.exe', 'kmcomp-$version.exe'];
+ case 'ios': return 'keyman-ios-$version.ipa'; // not currently used
+ case 'ios-engine': return 'keyman-engine-ios-$version.zip';
+ case 'linux': return ''; // not currently used
+ case 'mac': return ['keyman-$version.pkg','keyman-$version.dmg'];
+ case 'web': return 'keymanweb-$version.zip';
+ case 'windows': return ['keyman-$version.exe', 'keymandesktop-$version.exe'];
+ default: throw new Exception("Invalid target $target");
+ }
+ }
+
+ private static function productName($target) {
+ switch($target) {
+ case 'android': return 'product_android';
+ case 'android-engine': return 'product_engine_android';
+ case 'developer': return 'product_developer';
+ case 'ios': return 'product_ios'; // not currently used
+ case 'ios-engine': return 'product_engine_ios';
+ case 'linux': return 'product_linux'; // not currently used
+ case 'mac': return 'product_macos'; // note mismatched platform name
+ case 'web': return 'product_keymanweb'; // note mismatched platform name
+ case 'windows': return 'product_windows';
+ default: throw new Exception("Invalid target $target");
+ }
+ }
+
+ private static function title($target) {
+ switch($target) {
+ case 'android': return 'Keyman for Android';
+ case 'android-engine': return 'Keyman Engine for Android';
+ case 'developer': return 'Keyman Developer';
+ case 'ios': return 'Keyman for iPhone and iPad'; // not currently used
+ case 'ios-engine': return 'Keyman Engine for iPhone and iPad';
+ case 'linux': return 'Keyman for Linux'; // not currently used
+ case 'mac': return 'Keyman for mac';
+ case 'web': return 'KeymanWeb';
+ case 'windows': return 'Keyman for Windows';
+ default: throw new Exception("Invalid target $target");
+ }
+ }
+
+
+ public static function downloadSection($platform, $tiers = '', $target = '') {
+
+ if($target == '') $target = $platform;
+
+ echo sprintf("%s
\n\n",
+ $target, _m_Downloads(DownloadUI::productName($target)));
+ $tiers = explode(' ',$tiers);
+ foreach($tiers as $tier) {
+ $filepatterns = DownloadUI::filenamePatterns($target, $tier);
+ echo DownloadUI::downloadLinks($platform, $tier, $filepatterns);
+ }
+ }
+
+ private static function downloadLinks($platform, $tier, $filepatterns) {
+ global $versions;
+ $product = DownloadUI::productName($platform);
+ echo sprintf("%s
\n\n", ucFirst(_m_Downloads($tier)));
+ if(!empty($versions->$platform->$tier)) {
+ if(!is_array($filepatterns)) $filepatterns = array($filepatterns);
+ foreach($filepatterns as $filepattern) {
+ $file = str_replace('$version', $versions->$platform->$tier->version, $filepattern);
+ $file = str_replace('$tier', $tier, $file);
+
+ if(!empty($versions->$platform->$tier->files->$file)) {
+ $fileData = $versions->$platform->$tier->files->$file;
+ $fileSize = DownloadUI::formatSizeUnits($fileData->size);
+ echo sprintf("- %s %s %s
\n",
+ KeymanHosts::Instance()->downloads_keyman_com ,
+ $platform, $tier,
+ $versions->$platform->$tier->version,
+ $file, $file, $tier,
+ _m_Downloads('released_date_size', $fileData->date, $fileSize));
+ }
+ }
+ }
+ echo sprintf("- %s
\n
\n",
+ KeymanHosts::Instance()->downloads_keyman_com, $platform, $tier,
+ _m_Downloads('all_product_releases', _m_Downloads($product), _m_Downloads($tier)));
+ }
+
+ public static function downloadLargeCTA($platform, $tier) {
+ global $versions;
+
+ if(empty($versions)) return false;
+
+ $found = false;
+
+ $filepatterns = DownloadUI::filenamePatterns($platform, $tier);
+ $title = DownloadUI::title($platform);
+ // CTA only supports the first download (which will typically be the right one?)
+ if(!is_array($filepatterns)) $filepatterns = [$filepatterns];
+ foreach($filepatterns as $filepattern) {
+ $file = str_replace('$version', $versions->$platform->$tier->version, $filepattern);
+ $file = str_replace('$tier', $tier, $file);
+
+ if(empty($versions->$platform->$tier->files->$file)) {
+ continue;
+ }
+
+ $found = true;
+
+ $fileData = $versions->$platform->$tier->files->$file;
+ $fileSize = DownloadUI::formatSizeUnits($fileData->size);
+ $host = KeymanHosts::Instance()->downloads_keyman_com;
+ $downloadSiteUrl = "$host/$platform/$tier/{$versions->$platform->$tier->version}/$file";
+ $downloadUrl = htmlentities("/go/app/download/$platform/{$versions->$platform->$tier->version}/$tier?url=".
+ rawurlencode($downloadSiteUrl));
+
+ echo <<
+
+
$title {$versions->$platform->$tier->version}
+
Released: {$fileData->date}
+
Size: $fileSize
+
+
+
Download Now
+
+
+
+END;
+ }
+
+ if(!$found) {
+ echo "No downloads found for $title.
";
+ return false;
+ }
+ }
+
+ public static function iosTestflightTable() {
+ $testflight = 'https://itunes.apple.com/us/app/testflight/id899247664?mt=8';
+ $testflight_beta_signup = 'https://testflight.apple.com/join/9W4XIoxQ';
+ $testflight_alpha_signup = 'https://testflight.apple.com/join/vnCV2EiH';
+ $testflightTable_cdn = cdn('img/testflight-64x64.png');
+ $testflight_about = 'https://developer.apple.com/testflight/testers/';
+ $testflightTable = <<
+ Available through TestFlight
+
+ Sign up here for beta-only Keyman access.
+ Sign up for here alpha-only Keyman access.
+ |
+END;
+ return $testflightTable;
+ }
+
+ }
+?>
\ No newline at end of file
diff --git a/_includes/2020/KeymanDownloadVersions.php b/_includes/2020/KeymanDownloadVersions.php
deleted file mode 100644
index 550c5616..00000000
--- a/_includes/2020/KeymanDownloadVersions.php
+++ /dev/null
@@ -1,41 +0,0 @@
- 'keyman-(\d+\.\d+\.\d+)\.apk', // Generally, use Play Store
- 'ios' => 'keyman-ios-(\d+\.\d+\.\d+)\.ipa', // Always use App Store; included for completeness but not generally used
- 'mac' => 'keyman-(\d+\.\d+\.\d+)\.dmg',
- 'linux' => '-', // Linux distribution is multiple archives, use a package manager
- 'windows' => 'keyman(desktop)?-(\d+\.\d+\.\d+(\.\d+)?).exe'
- ];
-
- static function getDownloadUrls() {
- if(empty(self::$versions))
- self::$versions = @json_decode(Util::call_downloads_keyman_com('/api/version/2.0', 'downloads.keyman.com-api_version_2.0.json'));
- return self::$versions;
- }
-
- static function getDownloadUrl($platform, $tier = 'stable') {
- $versions = self::getDownloadUrls();
- if(empty($versions->$platform->$tier)) {
- return null;
- }
- $files = $versions->$platform->$tier;
- $pattern = self::filenamePatterns[$platform];
- foreach($files->files as $name => $details) {
- //echo $name;
- if(preg_match("/^$pattern\$/", $name)) {
- return KeymanHosts::Instance()->downloads_keyman_com . "/$platform/$tier/{$files->version}/{$name}";
- }
- }
- return null;
- }
- }
diff --git a/_includes/includes/ui/downloads.php b/_includes/includes/ui/downloads.php
deleted file mode 100644
index 29974e56..00000000
--- a/_includes/includes/ui/downloads.php
+++ /dev/null
@@ -1,116 +0,0 @@
-= 1073741824) {
- $bytes = number_format($bytes / 1073741824, 2) . ' GB';
- } elseif ($bytes >= 1048576) {
- $bytes = number_format($bytes / 1048576, 2) . ' MB';
- } elseif ($bytes >= 1024) {
- $bytes = number_format($bytes / 1024, 2) . ' KB';
- } elseif ($bytes > 1) {
- $bytes = $bytes . ' bytes';
- } elseif ($bytes == 1) {
- $bytes = $bytes . ' byte';
- } else {
- $bytes = '0 bytes';
- }
-
- return $bytes;
- }
-
- function downloadSection($product, $platform, $filepattern, $tiers = '', $target = '') {
- if($target == '') $target = $platform;
-
- echo sprintf("%s
\n\n",
- $target, _m_Downloads($product));
- $tiers = explode(' ',$tiers);
- foreach($tiers as $tier) {
- echo downloadLinks($product, $platform, $tier, $filepattern);
- }
- }
-
- function downloadLinks($product, $platform, $tier, $filepatterns) {
- global $versions;
- echo sprintf("%s
\n\n", ucFirst(_m_Downloads($tier)));
- if(!empty($versions->$platform->$tier)) {
- if(!is_array($filepatterns)) $filepatterns = array($filepatterns);
- foreach($filepatterns as $filepattern) {
- $file = str_replace('$version', $versions->$platform->$tier->version, $filepattern);
- $file = str_replace('$tier', $tier, $file);
-
- if(!empty($versions->$platform->$tier->files->$file)) {
- $fileData = $versions->$platform->$tier->files->$file;
- $fileSize = formatSizeUnits($fileData->size);
- echo sprintf("- %s %s %s
\n",
- KeymanHosts::Instance()->downloads_keyman_com ,
- $platform, $tier,
- $versions->$platform->$tier->version,
- $file, $file, $tier,
- _m_Downloads('released_date_size', $fileData->date, $fileSize));
- }
- }
- }
- echo sprintf("- %s
\n
\n",
- KeymanHosts::Instance()->downloads_keyman_com, $platform, $tier,
- _m_Downloads('all_product_releases', _m_Downloads($product), _m_Downloads($tier)));
- }
-
- function downloadLargeCTA($product, $platform, $tier, $filepattern) {
- global $versions;
-
- if(empty($versions)) return;
- $file = str_replace('$version', $versions->$platform->$tier->version, $filepattern);
- $file = str_replace('$tier', $tier, $file);
-
- if(empty($versions->$platform->$tier->files->$file)) {
- echo "No downloads found for $product.
";
- return false;
- }
-
- $fileData = $versions->$platform->$tier->files->$file;
- $fileSize = formatSizeUnits($fileData->size);
- $host = KeymanHosts::Instance()->downloads_keyman_com;
- $downloadSiteUrl = "$host/$platform/$tier/{$versions->$platform->$tier->version}/$file";
- $downloadUrl = htmlentities("/go/app/download/$platform/{$versions->$platform->$tier->version}/$tier?url=".
- rawurlencode($downloadSiteUrl));
-
- echo <<
-
-
$product {$versions->$platform->$tier->version}
-
Released: {$fileData->date}
-
Size: $fileSize
-
-
-
Download Now
-
-
-
-END;
- }
-
- function iosTestflightTable() {
- $testflight = 'https://itunes.apple.com/us/app/testflight/id899247664?mt=8';
- $testflight_beta_signup = 'https://testflight.apple.com/join/9W4XIoxQ';
- $testflight_alpha_signup = 'https://testflight.apple.com/join/vnCV2EiH';
- $testflightTable_cdn = cdn('img/testflight-64x64.png');
- $testflight_about = 'https://developer.apple.com/testflight/testers/';
- $testflightTable = <<
- Available through TestFlight
-
- Sign up here for beta-only Keyman access.
- Sign up for here alpha-only Keyman access.
- |
-END;
- return $testflightTable;
- }
-?>
\ No newline at end of file
From 9195f35ce85a14a6e7fa22721f2bdf93b965ff3f Mon Sep 17 00:00:00 2001
From: Marc Durdin
Date: Wed, 23 Sep 2026 09:47:12 +0200
Subject: [PATCH 2/2] fix: use kmcomp-version.zip, not .exe
Co-authored-by: Ross Cruickshank
---
_includes/2020/DownloadUI.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/_includes/2020/DownloadUI.php b/_includes/2020/DownloadUI.php
index aff40e69..77fa5fab 100644
--- a/_includes/2020/DownloadUI.php
+++ b/_includes/2020/DownloadUI.php
@@ -34,7 +34,7 @@ private static function filenamePatterns($target, $tier) {
switch($target) {
case 'android': return 'keyman-$version.apk';
case 'android-engine': return 'keyman-engine-android-$version.zip';
- case 'developer': return ['keymandeveloper-$version.exe', 'kmcomp-$version.exe'];
+ case 'developer': return ['keymandeveloper-$version.exe', 'kmcomp-$version.zip'];
case 'ios': return 'keyman-ios-$version.ipa'; // not currently used
case 'ios-engine': return 'keyman-engine-ios-$version.zip';
case 'linux': return ''; // not currently used