Parcourir la source

Add support for extension compatibility (#8081)

The compatibility does support only a minimum version of FreshRSS. If we need
something a bit more clever in the future, it is possible to handle a rule
with a bit more complexity.

See https://github.com/FreshRSS/FreshRSS/issues/5903

* Update app/Controllers/extensionController.php

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>

* Update app/i18n/pl/admin.php

Co-authored-by: Inverle <inverle@proton.me>

* Minor move phpstan-type

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
Co-authored-by: Inverle <inverle@proton.me>
Alexis Degrugillier il y a 5 mois
Parent
commit
eee8b8c03f

+ 2 - 2
README.fr.md

@@ -233,7 +233,7 @@ Voir le [dépôt dédié à ces extensions](https://github.com/FreshRSS/Extensio
 | English (United States) (en-US) | ■■■■■■■■■■ 100% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fen-US+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Español (es) | ■■■■■■■■・・ 88% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fes+%2F%28TODO%7CDIRTY%29%24%2F) |
 | فارسی (fa) | ■■■■■■■■■・ 94% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffa+%2F%28TODO%7CDIRTY%29%24%2F) |
-| Suomi (fi) | ■■■■■■■■■・ 97% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffi+%2F%28TODO%7CDIRTY%29%24%2F) |
+| Suomi (fi) | ■■■■■■■■■・ 96% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffi+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Français (fr) | ■■■■■■■■■■ 100% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffr+%2F%28TODO%7CDIRTY%29%24%2F) |
 | עברית (he) | ■■■■・・・・・・ 44% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fhe+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Magyar (hu) | ■■■■■■■■■・ 96% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fhu+%2F%28TODO%7CDIRTY%29%24%2F) |
@@ -241,7 +241,7 @@ Voir le [dépôt dédié à ces extensions](https://github.com/FreshRSS/Extensio
 | Italiano (it) | ■■■■■■■■■・ 97% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fit+%2F%28TODO%7CDIRTY%29%24%2F) |
 | 日本語 (ja) | ■■■■■■■■■・ 92% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fja+%2F%28TODO%7CDIRTY%29%24%2F) |
 | 한국어 (ko) | ■■■■■■■■・・ 85% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fko+%2F%28TODO%7CDIRTY%29%24%2F) |
-| Latviešu (lv) | ■■■■■■■■・・ 80% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Flv+%2F%28TODO%7CDIRTY%29%24%2F) |
+| Latviešu (lv) | ■■■■■■■・・・ 79% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Flv+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Nederlands (nl) | ■■■■■■■■■・ 96% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fnl+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Occitan (oc) | ■■■■■■■・・・ 78% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Foc+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Polski (pl) | ■■■■■■■■■■ 100% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpl+%2F%28TODO%7CDIRTY%29%24%2F) |

+ 2 - 2
README.md

@@ -129,7 +129,7 @@ See the [repository dedicated to those extensions](https://github.com/FreshRSS/E
 | English (United States) (en-US) | ■■■■■■■■■■ 100% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fen-US+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Español (es) | ■■■■■■■■・・ 88% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fes+%2F%28TODO%7CDIRTY%29%24%2F) |
 | فارسی (fa) | ■■■■■■■■■・ 94% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffa+%2F%28TODO%7CDIRTY%29%24%2F) |
-| Suomi (fi) | ■■■■■■■■■・ 97% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffi+%2F%28TODO%7CDIRTY%29%24%2F) |
+| Suomi (fi) | ■■■■■■■■■・ 96% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffi+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Français (fr) | ■■■■■■■■■■ 100% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffr+%2F%28TODO%7CDIRTY%29%24%2F) |
 | עברית (he) | ■■■■・・・・・・ 44% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fhe+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Magyar (hu) | ■■■■■■■■■・ 96% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fhu+%2F%28TODO%7CDIRTY%29%24%2F) |
@@ -137,7 +137,7 @@ See the [repository dedicated to those extensions](https://github.com/FreshRSS/E
 | Italiano (it) | ■■■■■■■■■・ 97% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fit+%2F%28TODO%7CDIRTY%29%24%2F) |
 | 日本語 (ja) | ■■■■■■■■■・ 92% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fja+%2F%28TODO%7CDIRTY%29%24%2F) |
 | 한국어 (ko) | ■■■■■■■■・・ 85% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fko+%2F%28TODO%7CDIRTY%29%24%2F) |
-| Latviešu (lv) | ■■■■■■■■・・ 80% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Flv+%2F%28TODO%7CDIRTY%29%24%2F) |
+| Latviešu (lv) | ■■■■■■■・・・ 79% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Flv+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Nederlands (nl) | ■■■■■■■■■・ 96% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fnl+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Occitan (oc) | ■■■■■■■・・・ 78% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Foc+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Polski (pl) | ■■■■■■■■■■ 100% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpl+%2F%28TODO%7CDIRTY%29%24%2F) |

+ 15 - 4
app/Controllers/extensionController.php

@@ -3,6 +3,8 @@ declare(strict_types=1);
 
 /**
  * The controller to manage extensions.
+ *
+ * @phpstan-type ExtensionFullMetadata array{name:string,entrypoint:string,author:string,description:string,version:string,type:'system'|'user',url:string,method:string,directory:string,compatibility:string}
  */
 class FreshRSS_extension_Controller extends FreshRSS_ActionController {
 	/**
@@ -40,7 +42,7 @@ class FreshRSS_extension_Controller extends FreshRSS_ActionController {
 
 	/**
 	 * Fetch extension list from GitHub
-	 * @return list<array{name:string,author:string,description:string,version:string,entrypoint:string,type:'system'|'user',url:string,method:string,directory:string}>
+	 * @phpstan-return list<ExtensionFullMetadata>
 	 */
 	protected function getAvailableExtensionList(): array {
 		$extensionListUrl = 'https://raw.githubusercontent.com/FreshRSS/Extensions/master/extensions.json';
@@ -82,7 +84,10 @@ class FreshRSS_extension_Controller extends FreshRSS_ActionController {
 			if (isset($extension['version']) && is_numeric($extension['version'])) {
 				$extension['version'] = (string)$extension['version'];
 			}
-			$keys = ['author', 'description', 'directory', 'entrypoint', 'method', 'name', 'type', 'url', 'version'];
+			if (!array_key_exists('compatibility', $extension)) {
+				$extension['compatibility'] = '✔';
+			}
+			$keys = ['author', 'description', 'directory', 'entrypoint', 'method', 'name', 'type', 'url', 'version', 'compatibility'];
 			$extension = array_intersect_key($extension, array_flip($keys));	// Keep only valid keys
 			$extension = array_filter($extension, 'is_string');
 			foreach ($keys as $key) {
@@ -93,10 +98,16 @@ class FreshRSS_extension_Controller extends FreshRSS_ActionController {
 			if (!in_array($extension['type'], ['system', 'user'], true) || trim($extension['name']) === '') {
 				continue;
 			}
-			/** @var array{name:string,author:string,description:string,version:string,entrypoint:string,type:'system'|'user',url:string,method:string,directory:string} $extension */
+			/** @var ExtensionFullMetadata $extension */
 			$extensions[] = $extension;
 		}
-		return $extensions;
+
+		return array_map(static function (array $extension) {
+			if ($extension['compatibility'] !== '✔') {
+				$extension['compatibility'] = version_compare($extension['compatibility'], FRESHRSS_VERSION, '>=') ? '✔' : '✘';
+			}
+			return $extension;
+		}, $extensions);
 	}
 
 	/**

+ 4 - 1
app/Models/View.php

@@ -1,6 +1,9 @@
 <?php
 declare(strict_types=1);
 
+/**
+ * @phpstan-import-type ExtensionFullMetadata from FreshRSS_extension_Controller
+ */
 class FreshRSS_View extends Minz_View {
 
 	// Main views
@@ -123,7 +126,7 @@ class FreshRSS_View extends Minz_View {
 	public bool $selectorSuccess;
 
 	// Extensions
-	/** @var array<array{name:string,author:string,description:string,version:string,entrypoint:string,type:'system'|'user',url:string,method:string,directory:string}> */
+	/** @var list<ExtensionFullMetadata> */
 	public array $available_extensions;
 	public ?Minz_Extension $ext_details = null;
 	/** @var array{system:array<Minz_Extension>,user:array<Minz_Extension>} */

+ 1 - 0
app/i18n/cs/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'Nejsou naistalována žádná rozšíření',
 		'empty_list_help' => 'Check the logs to determine the reason behind the empty extension list.',	// TODO
 		'enabled' => 'Povoleno',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => 'Nainstalováno',
 		'name' => 'Název',
 		'no_configure_view' => 'Toto rozšíření nemá žádná nastavení.',

+ 1 - 0
app/i18n/de/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'Es gibt keine installierte Erweiterung.',
 		'empty_list_help' => 'Siehe Protokolle für weitere Infos, warum die Erweiterungsliste leer ist.',
 		'enabled' => 'Aktiviert',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => 'Installiert',
 		'name' => 'Name',	// IGNORE
 		'no_configure_view' => 'Diese Erweiterung kann nicht konfiguriert werden.',

+ 1 - 0
app/i18n/el/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'Δεν υπάρχουν εγκατεστημένες επεκτάσεις',
 		'empty_list_help' => 'Check the logs to determine the reason behind the empty extension list.',	// TODO
 		'enabled' => 'Ενεργοποιημένες',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => 'Εγκατεστημένες',
 		'name' => 'Όνομα',
 		'no_configure_view' => 'Αυτή η επέκταση δεν μπορεί να ρυθμιστεί.',

+ 1 - 0
app/i18n/en-US/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'There are no installed extensions',	// IGNORE
 		'empty_list_help' => 'Check the logs to determine the reason behind the empty extension list.',	// IGNORE
 		'enabled' => 'Enabled',	// IGNORE
+		'is_compatible' => 'Is compatible',	// IGNORE
 		'latest' => 'Installed',	// IGNORE
 		'name' => 'Name',	// IGNORE
 		'no_configure_view' => 'This extension cannot be configured.',	// IGNORE

+ 1 - 0
app/i18n/en/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'There are no installed extensions',
 		'empty_list_help' => 'Check the logs to determine the reason behind the empty extension list.',
 		'enabled' => 'Enabled',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => 'Installed',
 		'name' => 'Name',
 		'no_configure_view' => 'This extension cannot be configured.',

+ 1 - 0
app/i18n/es/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'No hay extensiones instaladas',
 		'empty_list_help' => 'Check the logs to determine the reason behind the empty extension list.',	// TODO
 		'enabled' => 'Activado',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => 'Instalado',
 		'name' => 'Nombre',
 		'no_configure_view' => 'Esta extensión no puede ser configurada.',

+ 1 - 0
app/i18n/fa/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => ' هیچ برنامه افزودنی نصب شده ای وجود ندارد',
 		'empty_list_help' => 'لاگ‌ها را بررسی کنید تا دلیل خالی بودن لیست افزونه‌ها مشخص شود',
 		'enabled' => ' فعال است',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => ' نصب شده است',
 		'name' => ' نام',
 		'no_configure_view' => ' این برنامه افزودنی قابل پیکربندی نیست.',

+ 1 - 0
app/i18n/fi/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'Asennettuja laajennuksia ei ole',
 		'empty_list_help' => 'Voit tarkistaa lokeista, miksi laajennusluettelo on tyhjä.',
 		'enabled' => 'Käytössä',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => 'Asennettu',
 		'name' => 'Nimi',
 		'no_configure_view' => 'Tätä laajennusta ei voi määrittää.',

+ 1 - 0
app/i18n/fr/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'Aucune extension installée',
 		'empty_list_help' => 'Vérifiez les logs pour déterminer pourquoi la liste des extensions est vide.',
 		'enabled' => 'Activée',
+		'is_compatible' => 'Est compatible',
 		'latest' => 'Installée',
 		'name' => 'Nom',
 		'no_configure_view' => 'Cette extension n’a pas à être configurée',

+ 1 - 0
app/i18n/he/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'There is no installed extension',
 		'empty_list_help' => 'Check the logs to determine the reason behind the empty extension list.',	// TODO
 		'enabled' => 'Enabled',	// TODO
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => 'Installed',	// TODO
 		'name' => 'Name',	// TODO
 		'no_configure_view' => 'This extension cannot be configured.',	// TODO

+ 1 - 0
app/i18n/hu/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'Nincsenek telepített kiegészítők',
 		'empty_list_help' => 'Ellenőrizd a naplókat, hogy megállapítsd az üres bővítménylista mögött meghúzódó okot.',
 		'enabled' => 'Bekapcsolva',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => 'Telepítve',
 		'name' => 'Név',
 		'no_configure_view' => 'Ezt a kiegészítőt nem lehet konfigurálni.',

+ 1 - 0
app/i18n/id/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'Tidak ada ekstensi yang terpasang',
 		'empty_list_help' => 'Periksa log untuk menemukan alasan daftar ekstensi yang kosong.',
 		'enabled' => 'Diaktifkan',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => 'Terpasang',
 		'name' => 'Nama',
 		'no_configure_view' => 'Ekstensi ini tidak dapat dikonfigurasi.',

+ 1 - 0
app/i18n/it/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'Non ci sono estensioni installate',
 		'empty_list_help' => 'Controllare i log per determinare il motivo della lista estensioni vuota.',
 		'enabled' => 'Abilitata',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => 'Installato',
 		'name' => 'Nome',
 		'no_configure_view' => 'Questa estensioni non può essere configurata.',

+ 1 - 0
app/i18n/ja/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'インストールされている拡張機能はありません',
 		'empty_list_help' => '拡張機能リストが表示されない原因を特定するために、ログを確認してください。',
 		'enabled' => '有効',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => 'インストール済み',
 		'name' => '名前',
 		'no_configure_view' => 'この拡張機能は設定できません.',

+ 1 - 0
app/i18n/ko/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => '설치된 확장 기능이 없습니다',
 		'empty_list_help' => 'Check the logs to determine the reason behind the empty extension list.',	// TODO
 		'enabled' => '활성화됨',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => '설치됨',
 		'name' => '이름',
 		'no_configure_view' => '이 확장 기능은 설정이 없습니다.',

+ 1 - 0
app/i18n/lv/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'Nav instalētu paplašinājumu',
 		'empty_list_help' => 'Check the logs to determine the reason behind the empty extension list.',	// TODO
 		'enabled' => 'Ieslēgts',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => 'Instalēts',
 		'name' => 'Vārds',
 		'no_configure_view' => 'Šo paplašinājumu nevar konfigurēt.',

+ 1 - 0
app/i18n/nl/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'Er zijn geïnstalleerde uitbreidingen',
 		'empty_list_help' => 'Controleer de logbestanden om de reden voor de lege extensielijst te achterhalen.',
 		'enabled' => 'Ingeschakeld',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => 'Geïnstalleerd',
 		'name' => 'Naam',
 		'no_configure_view' => 'Deze uitbreiding kan niet worden geconfigureerd.',

+ 1 - 0
app/i18n/oc/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'Cap d’extensions pas installadas',
 		'empty_list_help' => 'Check the logs to determine the reason behind the empty extension list.',	// TODO
 		'enabled' => 'Activada',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => 'Installada',
 		'name' => 'Nom',
 		'no_configure_view' => 'Aquesta extension se pòt pas configurar.',

+ 1 - 0
app/i18n/pl/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'Brak zainstalowanych rozszerzeń',
 		'empty_list_help' => 'Sprawdź dziennik, aby ustalić powód pustej listy rozszerzeń.',
 		'enabled' => 'Włączone',
+		'is_compatible' => 'Jest kompatybilne',
 		'latest' => 'Zainstalowane',
 		'name' => 'Nazwa',
 		'no_configure_view' => 'To rozszerzenie nie jest konfigurowalne.',

+ 1 - 0
app/i18n/pt-BR/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'Não há extensões instaladas',
 		'empty_list_help' => 'Check the logs to determine the reason behind the empty extension list.',	// TODO
 		'enabled' => 'Habilitada',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => 'Instalado',
 		'name' => 'Nome',
 		'no_configure_view' => 'Esta extensão não pode ser configurada.',

+ 1 - 0
app/i18n/pt-PT/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'Não existem extensões instaladas',
 		'empty_list_help' => 'Check the logs to determine the reason behind the empty extension list.',	// TODO
 		'enabled' => 'Habilitada',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => 'Instalado',
 		'name' => 'Nome',
 		'no_configure_view' => 'Esta extensão não pode ser configurada.',

+ 1 - 0
app/i18n/ru/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'Нет установленных расширений',
 		'empty_list_help' => 'Check the logs to determine the reason behind the empty extension list.',	// TODO
 		'enabled' => 'Включены',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => 'Установлено',
 		'name' => 'Название',
 		'no_configure_view' => 'Это расширение не требует настройки.',

+ 1 - 0
app/i18n/sk/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'Žiadne nainštalované rozšírenia',
 		'empty_list_help' => 'Check the logs to determine the reason behind the empty extension list.',	// TODO
 		'enabled' => 'Povolené',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => 'Nainštalované',
 		'name' => 'Názov',
 		'no_configure_view' => 'Toto rozšírenie nemá nastavenia.',

+ 1 - 0
app/i18n/tr/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'Yüklü eklenti yok',
 		'empty_list_help' => 'Eklenti listesinin neden boş olduğunu belirlemek için günlükleri kontrol edin.',
 		'enabled' => 'Etkin',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => 'Yüklü',
 		'name' => 'İsim',
 		'no_configure_view' => 'Bu eklenti yapılandırılamaz.',

+ 1 - 0
app/i18n/uk/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => 'Розширень не встановлено',
 		'empty_list_help' => 'Щоб виявити причину порожнього списку розширень, перегляньте журнали.',
 		'enabled' => 'Увімкнено',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => 'Встановлено',
 		'name' => 'Назва',
 		'no_configure_view' => 'Розширення не налаштовується.',

+ 1 - 0
app/i18n/zh-CN/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => '没有已安装的扩展',
 		'empty_list_help' => 'Check the logs to determine the reason behind the empty extension list.',	// TODO
 		'enabled' => '已启用',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => '已安装',
 		'name' => '名称',
 		'no_configure_view' => '此扩展无法配置。',

+ 1 - 0
app/i18n/zh-TW/admin.php

@@ -118,6 +118,7 @@ return array(
 		'empty_list' => '沒有已安裝的擴充功能',
 		'empty_list_help' => 'Check the logs to determine the reason behind the empty extension list.',	// TODO
 		'enabled' => '已啟用',
+		'is_compatible' => 'Is compatible',	// TODO
 		'latest' => '已安裝',
 		'name' => '名稱',
 		'no_configure_view' => '此擴充功能不能配置。',

+ 2 - 0
app/views/extension/index.phtml

@@ -48,6 +48,7 @@
 					<th><?= _t('admin.extensions.version') ?></th>
 					<th><?= _t('admin.extensions.author') ?></th>
 					<th><?= _t('admin.extensions.description') ?></th>
+					<th><?= _t('admin.extensions.is_compatible') ?></th>
 				</tr>
 				<?php foreach ($this->available_extensions as $ext) { ?>
 					<tr>
@@ -68,6 +69,7 @@
 								<?php } ?>
 							<?php } ?>
 						</td>
+						<td><?= $ext['compatibility'] ?></td>
 					</tr>
 				<?php } ?>
 			</table>

+ 3 - 1
lib/Minz/Extension.php

@@ -3,6 +3,8 @@ declare(strict_types=1);
 
 /**
  * The extension base class.
+ *
+ * @phpstan-type ExtensionMetadata array{name:string,entrypoint:string,author?:string,description?:string,version?:string,type?:'system'|'user',path:string}
  */
 abstract class Minz_Extension {
 	private string $name;
@@ -41,7 +43,7 @@ abstract class Minz_Extension {
 	 * - version: a version for the current extension.
 	 * - type: "system" or "user" (default).
 	 *
-	 * @param array{'name':string,'entrypoint':string,'path':string,'author'?:string,'description'?:string,'version'?:string,'type'?:'system'|'user'} $meta_info
+	 * @param ExtensionMetadata $meta_info
 	 * contains information about the extension.
 	 */
 	final public function __construct(array $meta_info) {

+ 4 - 5
lib/Minz/ExtensionManager.php

@@ -5,6 +5,7 @@ declare(strict_types=1);
  * An extension manager to load extensions present in CORE_EXTENSIONS_PATH and THIRDPARTY_EXTENSIONS_PATH.
  *
  * @todo see coding style for methods!!
+ * @phpstan-import-type ExtensionMetadata from Minz_Extension
  */
 final class Minz_ExtensionManager {
 
@@ -81,7 +82,7 @@ final class Minz_ExtensionManager {
 				continue;
 			}
 			$meta_raw_content = file_get_contents($metadata_filename) ?: '';
-			/** @var array{'name':string,'entrypoint':string,'path':string,'author'?:string,'description'?:string,'version'?:string,'type'?:'system'|'user'}|null $meta_json */
+			/** @var ExtensionMetadata|null $meta_json */
 			$meta_json = json_decode($meta_raw_content, true);
 			if (!is_array($meta_json) || !self::isValidMetadata($meta_json)) {
 				// metadata.json is not a json file? Invalid!
@@ -109,8 +110,7 @@ final class Minz_ExtensionManager {
 	 * If the extension class name is `TestExtension`, entry point will be `Test`.
 	 * `entry_point` must be composed of alphanumeric characters.
 	 *
-	 * @param array{'name':string,'entrypoint':string,'path':string,'author'?:string,'description'?:string,'version'?:string,'type'?:'system'|'user'} $meta
-	 * is an array of values.
+	 * @param ExtensionMetadata $meta is an array of values.
 	 * @return bool true if the array is valid, false else.
 	 */
 	private static function isValidMetadata(array $meta): bool {
@@ -121,8 +121,7 @@ final class Minz_ExtensionManager {
 	/**
 	 * Load the extension source code based on info metadata.
 	 *
-	 * @param array{'name':string,'entrypoint':string,'path':string,'author'?:string,'description'?:string,'version'?:string,'type'?:'system'|'user'} $info
-	 * an array containing information about extension.
+	 * @param ExtensionMetadata $info an array containing information about extension.
 	 * @return Minz_Extension|null an extension inheriting from Minz_Extension.
 	 */
 	private static function load(array $info): ?Minz_Extension {

+ 4 - 0
p/themes/base-theme/frss.css

@@ -433,6 +433,10 @@ button.as-link[disabled] {
 	overflow-x: auto;
 }
 
+.table-wrapper th {
+	white-space: nowrap;
+}
+
 table {
 	margin: 0.5rem 0;
 	max-width: 100%;

+ 4 - 0
p/themes/base-theme/frss.rtl.css

@@ -433,6 +433,10 @@ button.as-link[disabled] {
 	overflow-x: auto;
 }
 
+.table-wrapper th {
+	white-space: nowrap;
+}
+
 table {
 	margin: 0.5rem 0;
 	max-width: 100%;