feat(admin): Reiter „Kompatibilität“ übersetzt und übersichtlicher

Sichtbarer Name „Kompatibilität“ (Slug compatibility bleibt) in Reiter, Hinweisen, Hilfe-Tab, CLI-Hilfe, Guides und readme. Oben eine Übersicht aller Plugins mit Gesamtzustand, Version, Zustand (Wort und Icon), Freigabe-Art in Klartext und Sprunglink; je Plugin erst Zustand und nächster Schritt, dann Aktionen, dann aufklappbare Details (offen bei Handlungsbedarf, ohne JavaScript). Tabellen mit Beschriftung und Kartenansicht bis 375 px, Buttons mit Plugin-Namen im zugänglichen Namen, Kontraste mit Reserve. Doku: Divi leert beim Prüfen den Cache einmal komplett.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-11-06 17:39:47 +01:00
co-authored by Claude Opus 5.5
parent 661d3945fd
commit f337a57f26
15 changed files with 507 additions and 100 deletions
+216 -58
View File
@@ -22,14 +22,16 @@ use Throwable;
defined('ABSPATH') || exit;
/**
* Tab "Compatibility": one section per third-party plugin of Compatibility::profiles() – its version, overall state
* (word and icon, never colour alone), what the tier means for a release, the capabilities, tested and released
* versions, the latest check report and "Prüfung starten" (Admin\CheckRoute). A profile that is not active is one
* line and never asks for its matrix. Everything that comes from a third-party plugin (version, problem texts, step
* labels) is escaped on output. Profiles hook in: lit_mcp_compat_profile_load($id) runs before any output (see
* loadProfiles()), lit_mcp_compat_profile_intro($id) opens the section above the capability table (next step,
* notices), lit_mcp_compat_profile_extra($id) closes it, and the filter lit_mcp_compat_explanation($text, $id,
* ReleaseState) replaces the generic sentence next to the overall state.
* Tab "Kompatibilität" (slug "compatibility"): an overview table of all third-party plugins of
* Compatibility::profiles() (version, state as word and icon – never colour alone –, kind of release, link to the
* section), then one section per active plugin: state and next step first, then the actions ("Prüfung starten",
* Admin\CheckRoute, release, withdrawal) and, folded, the details (capabilities, tested and released versions, the
* latest check report). A profile that is not active is one overview row and never asks for its matrix. Everything
* that comes from a third-party plugin (version, problem texts, step labels) is escaped on output. Profiles hook in:
* lit_mcp_compat_profile_load($id) runs before any output (see loadProfiles()),
* lit_mcp_compat_profile_intro($id) opens the section below the state notice (next step, notices),
* lit_mcp_compat_profile_extra($id) closes it, and the filter lit_mcp_compat_explanation($text, $id, ReleaseState)
* replaces the generic sentence next to the overall state.
*/
final class CompatibilityTab implements AdminTab
{
@@ -94,7 +96,7 @@ final class CompatibilityTab implements AdminTab
public function label(): string
{
return __('Compatibility', 'wp-lit-mcp');
return __('Kompatibilität', 'wp-lit-mcp');
}
public function registerSettings(): void
@@ -227,71 +229,213 @@ final class CompatibilityTab implements AdminTab
public function render(): void
{
foreach ($this->compatibility->profiles() as $profile) {
$this->renderProfile($profile);
$profiles = $this->compatibility->profiles();
$this->renderOverview($profiles);
foreach ($profiles as $profile) {
if ($this->compatibility->active($profile)) {
$this->renderProfile($profile);
}
}
}
private function renderProfile(Profile $profile): void
/**
* The whole page at a glance: one sentence on whether anything needs attention, then one row per plugin. Rows of
* active plugins link to their section; an inactive plugin is one short row.
*
* @param list<Profile> $profiles
*/
private function renderOverview(array $profiles): void
{
printf('<section class="lit-mcp-compat" id="lit-mcp-compat-%s">', esc_attr($profile->id));
printf('<h2>%s</h2>', esc_html($profile->label));
$active = array_filter($profiles, fn (Profile $profile): bool => $this->compatibility->active($profile));
$attention = count(array_filter(
$active,
fn (Profile $profile): bool => $this->compatibility->matrix($profile)->state() !== ReleaseState::Released
));
echo '<p>' . esc_html__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Hier siehst du, ob LIT MCP zu deinen Plugins passt. Was nicht passt, sperrt LIT MCP; alles andere läuft weiter.',
'wp-lit-mcp'
) . '</p>';
echo '<h2>' . esc_html__('Übersicht', 'wp-lit-mcp') . '</h2>';
[$class, $text] = match (true) {
$active === [] => ['info', __('Kein unterstütztes Plugin ist aktiv. Es gibt nichts zu tun.', 'wp-lit-mcp')],
$attention === 0 => [
'success',
__('Alles in Ordnung: Alle aktiven Plugins sind freigegeben.', 'wp-lit-mcp'),
],
default => ['warning', sprintf(
/* translators: %d: number of plugins */
_n(
'%d aktives Plugin braucht deine Aufmerksamkeit.',
'%d aktive Plugins brauchen deine Aufmerksamkeit.',
$attention,
'wp-lit-mcp'
),
$attention
)],
};
printf('<div class="notice notice-%s inline"><p>%s</p></div>', esc_attr($class), esc_html($text));
$headings = [
__('Plugin', 'wp-lit-mcp'),
__('Version', 'wp-lit-mcp'),
__('Zustand', 'wp-lit-mcp'),
__('Freigabe', 'wp-lit-mcp'),
__('Details', 'wp-lit-mcp'),
];
echo '<table class="widefat striped lit-mcp-compat-overview"><caption class="screen-reader-text">'
. esc_html__('Alle unterstützten Plugins und ihr Zustand', 'wp-lit-mcp') . '</caption><thead><tr>';
foreach ($headings as $heading) {
echo '<th scope="col">' . esc_html($heading) . '</th>';
}
echo '</tr></thead><tbody>';
foreach ($profiles as $profile) {
$this->renderOverviewRow($profile, $headings);
}
echo '</tbody></table>';
}
/** @param list<string> $headings */
private function renderOverviewRow(Profile $profile, array $headings): void
{
printf(
'<tr><th scope="row" data-label="%s">%s</th>',
esc_attr($headings[0]),
esc_html($profile->label)
);
if (! $this->compatibility->active($profile)) {
echo '<p>' . esc_html__('nicht installiert oder nicht aktiv', 'wp-lit-mcp') . '</p></section>';
$none = '<span aria-hidden="true">–</span><span class="screen-reader-text">'
. esc_html__('keine Angabe', 'wp-lit-mcp') . '</span>';
printf(
'<td data-label="%1$s">%5$s</td><td data-label="%2$s">%6$s</td><td data-label="%3$s">%5$s</td>'
. '<td data-label="%4$s">%5$s</td></tr>',
esc_attr($headings[1]),
esc_attr($headings[2]),
esc_attr($headings[3]),
esc_attr($headings[4]),
$none, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above.
'<span class="lit-mcp-state lit-mcp-status-minus"><span class="dashicons dashicons-minus"'
. ' aria-hidden="true"></span> <strong>' . esc_html__('nicht aktiv', 'wp-lit-mcp')
. '</strong></span>'
);
return;
}
printf(
'<td data-label="%1$s">%5$s</td><td data-label="%2$s">%6$s</td><td data-label="%3$s">%7$s</td>'
. '<td data-label="%4$s"><a href="#lit-mcp-compat-%8$s">%9$s<span class="screen-reader-text">'
. ' %10$s</span></a></td></tr>',
esc_attr($headings[1]),
esc_attr($headings[2]),
esc_attr($headings[3]),
esc_attr($headings[4]),
esc_html($this->compatibility->version($profile) ?? __('nicht erkannt', 'wp-lit-mcp')),
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in stateBadge().
self::stateBadge($this->compatibility->matrix($profile)->state()),
esc_html(self::tierHint($profile->tier)),
esc_attr($profile->id),
esc_html__('Details', 'wp-lit-mcp'),
esc_html(sprintf(
/* translators: %s: plugin name */
__('zu %s', 'wp-lit-mcp'),
$profile->label
))
);
}
private function renderProfile(Profile $profile): void
{
$matrix = $this->compatibility->matrix($profile);
$problems = $this->compatibility->problems($profile);
$state = $matrix->state();
echo '<table class="form-table" role="presentation"><tbody>';
printf(
'<tr><th scope="row">%s</th><td>%s</td></tr>',
esc_html__('Version', 'wp-lit-mcp'),
esc_html($this->compatibility->version($profile) ?? __('nicht erkannt', 'wp-lit-mcp'))
'<section class="lit-mcp-compat" id="lit-mcp-compat-%1$s" aria-labelledby="lit-mcp-compat-%1$s-title">',
esc_attr($profile->id)
);
printf('<h2 id="lit-mcp-compat-%s-title">%s</h2>', esc_attr($profile->id), esc_html($profile->label));
// 1. State and next step.
printf(
'<tr><th scope="row">%s</th><td>%s – %s</td></tr>',
esc_html__('Gesamtzustand', 'wp-lit-mcp'),
'<div class="notice notice-%s inline"><p>%s %s</p>',
esc_attr(self::noticeClass($state)),
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in stateBadge().
self::stateBadge($state),
esc_html($this->explanation($profile, $state))
);
if ($problems !== []) {
echo '<tr><th scope="row">' . esc_html__('Probleme', 'wp-lit-mcp') . '</th><td>';
self::renderList($problems);
echo '</td></tr>';
}
echo '</div>';
printf(
'<tr><th scope="row">%s</th><td>%s</td></tr>',
esc_html__('Freigabe', 'wp-lit-mcp'),
esc_html(self::tierHint($profile->tier))
'<p class="description">%s</p>',
esc_html(sprintf(
/* translators: 1: plugin version, 2: how releases work for this plugin */
__('Installiert: Version %1$s. %2$s', 'wp-lit-mcp'),
$this->compatibility->version($profile) ?? __('nicht erkannt', 'wp-lit-mcp'),
self::tierExplanation($profile->tier)
))
);
printf(
'<tr><th scope="row">%s</th><td>%s</td></tr>',
esc_html__('Getestete Versionen', 'wp-lit-mcp'),
$profile->testedVersions === [] ? '–' : esc_html(implode(', ', $profile->testedVersions))
);
printf(
'<tr><th scope="row">%s</th><td>%s</td></tr>',
esc_html__('Freigegebene Versionen', 'wp-lit-mcp'),
esc_html($this->releasedVersions($profile)) ?: '–'
);
echo '</tbody></table>';
do_action(self::INTRO_ACTION, $profile->id);
$this->renderCapabilities($profile, $matrix);
// 2. Actions.
$report = ($this->reports)($profile)->latest();
$this->renderForms($profile, $report);
// 3. Details, folded while everything is fine.
printf(
'<details class="lit-mcp-compat-details"%s><summary>%s'
. '<span class="screen-reader-text"> %s</span></summary>',
$state === ReleaseState::Released ? '' : ' open',
esc_html__('Details', 'wp-lit-mcp'),
esc_html(sprintf(
/* translators: %s: plugin name */
__('zu %s', 'wp-lit-mcp'),
$profile->label
))
);
$this->renderCapabilities($profile, $matrix);
$this->renderVersions($profile);
if ($report !== null) {
$this->renderReport($profile, $report);
}
echo '</details>';
do_action(self::EXTRA_ACTION, $profile->id);
echo '</section>';
}
private static function noticeClass(ReleaseState $state): string
{
return match ($state) {
ReleaseState::Released => 'success',
ReleaseState::Untested => 'warning',
ReleaseState::Incompatible => 'error',
};
}
private function renderVersions(Profile $profile): void
{
$rows = [
__('Im Plugin getestet', 'wp-lit-mcp') => $profile->testedVersions === []
? '–' : esc_html(implode(', ', $profile->testedVersions)),
__('Von dir freigegeben', 'wp-lit-mcp') => esc_html($this->releasedVersions($profile)) ?: '–',
];
echo '<h3>' . esc_html__('Versionen', 'wp-lit-mcp') . '</h3>';
echo '<table class="widefat striped lit-mcp-compat-versions"><caption class="screen-reader-text">'
. esc_html(sprintf(
/* translators: %s: plugin name */
__('Getestete und freigegebene Versionen von %s', 'wp-lit-mcp'),
$profile->label
)) . '</caption><tbody>';
foreach ($rows as $heading => $value) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $value is escaped above.
printf('<tr><th scope="row">%s</th><td>%s</td></tr>', esc_html($heading), $value);
}
echo '</tbody></table>';
}
private function renderCapabilities(Profile $profile, CompatibilityMatrix $matrix): void
{
$extra = $this->extraColumns($profile);
@@ -300,12 +444,7 @@ final class CompatibilityTab implements AdminTab
$headings[] = $column['heading'];
}
// Open when something needs attention; a released profile keeps the long table folded.
printf(
'<details class="lit-mcp-compat-details"%s><summary>%s</summary>',
$matrix->state() === ReleaseState::Released ? '' : ' open',
esc_html__('Fähigkeiten im Detail', 'wp-lit-mcp')
);
echo '<h3>' . esc_html__('Fähigkeiten', 'wp-lit-mcp') . '</h3>';
echo '<table class="widefat striped lit-mcp-compat-features"><caption class="screen-reader-text">'
. esc_html(sprintf(
/* translators: %s: plugin name */
@@ -320,7 +459,7 @@ final class CompatibilityTab implements AdminTab
foreach ($profile->capabilities as $capability) {
// data-label: on phones each row becomes a card and the cell names itself.
printf(
'<tr><td data-label="%s">%s</td><td data-label="%s">%s</td>',
'<tr><th scope="row" data-label="%s">%s</th><td data-label="%s">%s</td>',
esc_attr($headings[0]),
esc_html($capability->label()),
esc_attr($headings[1]),
@@ -337,7 +476,7 @@ final class CompatibilityTab implements AdminTab
}
echo '</tr>';
}
echo '</tbody></table></details>';
echo '</tbody></table>';
}
/** @return list<array{heading: string, cell: Closure, code?: bool}> */
@@ -385,6 +524,7 @@ final class CompatibilityTab implements AdminTab
$fresh = $report !== null && $report->freshFor($version, Plugin::VERSION, $this->stateKey($profile, $version));
$releasable = $this->releasable($profile, $version);
echo '<h3>' . esc_html__('Aktionen', 'wp-lit-mcp') . '</h3><div class="lit-mcp-compat-actions">';
printf('<form method="post" action="%s">', esc_url(CheckRoute::url($profile->id)));
printf('<input type="hidden" name="_wpnonce" value="%s">', esc_attr(wp_create_nonce('wp_rest')));
// The check is the one thing to do while a release is due without a usable report.
@@ -395,7 +535,6 @@ final class CompatibilityTab implements AdminTab
false,
['aria-label' => self::namedFor($profile, __('Prüfung starten', 'wp-lit-mcp'))]
);
echo '<p class="description">' . esc_html__('Die Prüfung dauert einige Sekunden.', 'wp-lit-mcp') . '</p>';
echo '</form>';
if ($releasable && $fresh) {
@@ -407,11 +546,6 @@ final class CompatibilityTab implements AdminTab
sprintf(__('%1$s %2$s freigeben', 'wp-lit-mcp'), $profile->label, $version),
'primary'
);
} elseif ($releasable) {
echo '<p>' . esc_html__(
'Freigeben geht erst nach einer vollständig grünen Prüfung dieser Version – Prüfung erneut ausführen.',
'wp-lit-mcp'
) . '</p>';
}
// A release applies whatever the overall state: one incompatible capability must not hide the way back.
@@ -425,6 +559,16 @@ final class CompatibilityTab implements AdminTab
true
);
}
echo '</div>';
echo '<p class="description">' . esc_html__('Die Prüfung dauert einige Sekunden.', 'wp-lit-mcp') . '</p>';
if ($releasable && ! $fresh) {
echo '<p>' . esc_html__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Freigeben geht erst nach einer vollständig grünen Prüfung dieser Version. Führe die Prüfung erneut aus.',
'wp-lit-mcp'
) . '</p>';
}
}
/** "<visible text>: <profile>" – the accessible name starts with the visible text (WCAG 2.5.3). */
@@ -467,7 +611,7 @@ final class CompatibilityTab implements AdminTab
esc_html__('Letzte Prüfung', 'wp-lit-mcp'),
esc_html(sprintf(
/* translators: 1: plugin version, 2: LIT MCP version, 3: date */
__('Version %1$s mit LIT MCP %2$s, %3$s', 'wp-lit-mcp'),
__('Geprüft: Version %1$s mit LIT MCP %2$s, %3$s', 'wp-lit-mcp'),
$report->version,
$report->pluginVersion,
date_i18n((string) get_option('date_format'), (int) strtotime($report->createdAt))
@@ -556,7 +700,7 @@ final class CompatibilityTab implements AdminTab
return match ($state) {
ReleaseState::Released => __('Alle Fähigkeiten sind nutzbar.', 'wp-lit-mcp'),
ReleaseState::Untested => __(
'Diese Version ist nicht geprüft. Die ungetesteten Fähigkeiten sind gesperrt, bis sie freigegeben ist.',
'Diese Version ist nicht geprüft. Die ungetesteten Fähigkeiten sind gesperrt, bis du sie freigibst.',
'wp-lit-mcp'
),
ReleaseState::Incompatible => __(
@@ -566,15 +710,29 @@ final class CompatibilityTab implements AdminTab
};
}
/** Short and in plain words: the tier's name never reaches the screen. */
private static function tierHint(Tier $tier): string
{
return match ($tier) {
Tier::ReleasePerVersion => __('Freigabe pro Version nötig', 'wp-lit-mcp'),
Tier::LockOnFailure => __(
'Freigabe wird dokumentiert, Nutzung automatisch, solange die Prüfung grün ist',
Tier::LockOnFailure => __('Sperrt automatisch bei Problemen', 'wp-lit-mcp'),
Tier::ReportOnly => __('Nur Anzeige', 'wp-lit-mcp'),
};
}
private static function tierExplanation(Tier $tier): string
{
return match ($tier) {
Tier::ReleasePerVersion => __(
'Jede neue Version musst du einmal freigeben, bevor LIT MCP damit schreibt.',
'wp-lit-mcp'
),
Tier::ReportOnly => __('nur Anzeige', 'wp-lit-mcp'),
Tier::LockOnFailure => __(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'LIT MCP sperrt betroffene Fähigkeiten selbst, sobald die Prüfung Probleme findet. Eine Freigabe hält nur fest, dass du diese Version geprüft hast.',
'wp-lit-mcp'
),
Tier::ReportOnly => __('Probleme werden nur angezeigt; nichts wird gesperrt.', 'wp-lit-mcp'),
};
}
}
+1 -1
View File
@@ -114,7 +114,7 @@ final class SettingsPage
'wp-lit-mcp'
),
__('Passt es zu Divi, Pods & Co.?', 'wp-lit-mcp') => __(
'Tab „Compatibility“: Fremd-Plugins prüfen, Versionen freigeben und sehen, was gesperrt ist.',
'Tab „Kompatibilität“: Fremd-Plugins prüfen, Versionen freigeben und sehen, was gesperrt ist.',
'wp-lit-mcp'
),
];
@@ -87,7 +87,7 @@ final class DiviAdminExtras
))
)),
esc_url(SettingsPage::url(CompatibilityTab::SLUG)),
esc_html__('Details im Reiter „Compatibility“', 'wp-lit-mcp')
esc_html__('Details im Reiter „Kompatibilität“', 'wp-lit-mcp')
);
}
@@ -298,6 +298,8 @@ final class DiviAdminExtras
/** E1: what the automatic release does and how to switch it off. */
private function renderAutomationNote(): void
{
echo '<details class="lit-mcp-hint"><summary>'
. esc_html__('So funktioniert die automatische Freigabe', 'wp-lit-mcp') . '</summary>';
echo '<p class="description">' . esc_html__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Automatik: Ist die installierte Divi-Version im Plugin nicht als getestet hinterlegt, gibt LIT MCP jede Fähigkeit einzeln frei, wenn alle benutzten Divi-Funktionen vorhanden sind, ihre Aufrufe (Signaturen) und die Fingerabdrücke nachgebauter Divi-Abläufe unverändert sind und alle Konstanten definiert sind. Das prüft Aufrufformen, nicht das Verhalten von Divi. Fehlt für eine Stelle die Referenz, ist die Fähigkeit ungetestet; weicht etwas ab, ist sie inkompatibel.',
@@ -311,7 +313,7 @@ final class DiviAdminExtras
'wp-lit-mcp'
),
'<code>lit_mcp_divi_auto_release</code>'
) . '</p>';
) . '</p></details>';
}
private function version(): string
@@ -72,7 +72,7 @@ final class DiviCheckCommand
}
/**
* Prüft die installierte Divi-Version wie der Reiter „Compatibility“: Manifeste, Fingerabdrücke, Modulkatalog,
* Prüft die installierte Divi-Version wie der Reiter „Kompatibilität“: Manifeste, Fingerabdrücke, Modulkatalog,
* Format und Schreibtest auf einer Wegwerfseite. Gibt danach den Zustand je Fähigkeit aus und gibt nichts frei.
* Exit-Code 0: Prüfung grün und alle Fähigkeiten nutzbar; 1: Prüfung rot; 2: Prüfung grün, aber mindestens
* eine Fähigkeit nicht nutzbar (z. B. Version noch nicht in DiviReleases::TESTED).
@@ -24,7 +24,7 @@ final class ReleaseStatus
public const SOURCE_AUTO = CompatibilityMatrix::SOURCE_AUTO;
private const FEATURE_UNTESTED = 'Achtung: %1$s ist auf dieser Website für %2$s noch nicht für LIT MCP '
. 'freigegeben. Das ist gesperrt, bis ein Administrator unter Einstellungen → LIT MCP → Compatibility die '
. 'freigegeben. Das ist gesperrt, bis ein Administrator unter Einstellungen → LIT MCP → Kompatibilität die '
. 'Prüfung ausführt und die Version freigibt. Es wurde nichts geändert.';
private const FEATURE_UNTESTED_HINT = 'Gesperrt: %s. Andere Divi-Werkzeuge funktionieren weiter, sofern ihre '
. 'Fähigkeiten freigegeben sind. Den Nutzer bitten, die Divi-Prüfung auszuführen und die Version freizugeben.';