feat: UI-feinschliff, scan-vorlagenerkennung, einmal-laden, DE/EN-sprachen

UI:
- Ein-/Ausklappen jetzt mit grossem +/- Icon statt kleinem Pfeil.
- "Entfernen" ist ein Papierkorb-Symbol (dashicon).
- Aktiver Tab klar gekennzeichnet (Akzent-Unterstrich + Farbe).
- 20px Abstand zwischen Tabs und Inhalt.

Funktionen:
- Scan erkennt Anbieter, fuer die es eine Vorlage gibt ("Vorlage verfuegbar"),
  und "Vorlage uebernehmen" fuellt die komplette Vorlage statt nur Host/Pattern.
- Platzhalter: Checkbox "Diesen Dienst kuenftig immer laden" (Standard AN).
  Abgewaehlt -> Inhalt wird nur einmal geladen, keine dauerhafte Einwilligung.

i18n:
- Sprachumschaltung: Deutsch fuer alle de_* Locales, Englisch fuer alle anderen
  (plugin_locale-Filter). Vollstaendige englische Uebersetzung (126 Strings,
  inkl. Vorlagentexte/Empfaenger) als gdpr-content-blocker-en_US.po/.mo.
- Helper-Skripte (extract/build) in hilfsdaten/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-06-07 15:06:16 +02:00
parent ecb5e1bd22
commit 3c37bf63cc
10 changed files with 752 additions and 16 deletions

View File

@@ -28,7 +28,7 @@
function setExpanded( box, open ) { function setExpanded( box, open ) {
$( '.cb-service-grid', box ).toggle( open ); $( '.cb-service-grid', box ).toggle( open );
const $btn = $( '.cb-service-toggle', box ); const $btn = $( '.cb-service-toggle', box );
$btn.attr( 'aria-expanded', open ? 'true' : 'false' ).text( open ? '' : '' ); $btn.attr( 'aria-expanded', open ? 'true' : 'false' ).text( open ? '' : '+' );
} }
function bindToggle( box ) { function bindToggle( box ) {
$( '.cb-service-toggle', box ).on( 'click', function () { $( '.cb-service-toggle', box ).on( 'click', function () {
@@ -211,21 +211,33 @@
$ex.append( $( '<code></code>' ).css( { fontSize: '11px', wordBreak: 'break-all' } ).text( sample ) ); $ex.append( $( '<code></code>' ).css( { fontSize: '11px', wordBreak: 'break-all' } ).text( sample ) );
$tr.append( $ex ); $tr.append( $ex );
// Does a ready-made template exist for this finding?
const preset = ( f.preset && cbAdmin.presets && cbAdmin.presets[ f.preset ] )
? cbAdmin.presets[ f.preset ]
: null;
// Status // Status
const $st = $( '<td></td>' ); const $st = $( '<td></td>' );
if ( f.covered ) { if ( f.covered ) {
$st.append( $( '<span></span>' ).css( { color: '#1a7f37', fontWeight: '600' } ).text( '✓ ' + ( i18n.covered || 'covered' ) ) ); $st.append( $( '<span></span>' ).css( { color: '#1a7f37', fontWeight: '600' } ).text( '✓ ' + ( i18n.covered || 'covered' ) ) );
} else if ( preset ) {
$st.append( $( '<span></span>' ).css( { color: '#2043B7', fontWeight: '600' } ).text( '★ ' + ( i18n.templateAvail || 'Vorlage verfügbar' ) ) );
} }
$tr.append( $st ); $tr.append( $st );
// Action: take over as a new service (only useful for uncovered third parties) // Action: take over as a new service (only useful for uncovered third parties)
const $act = $( '<td></td>' ); const $act = $( '<td></td>' );
if ( f.third_party && ! f.covered ) { if ( f.third_party && ! f.covered ) {
// Prefill from the matching preset (full data) if there is one,
// otherwise just seed host + pattern.
const data = preset
? preset
: { name: f.host, match_pattern: f.suggested_pattern || f.host, third_country: true };
$( '<button type="button" class="button button-small"></button>' ) $( '<button type="button" class="button button-small"></button>' )
.text( i18n.addService || 'Add as service' ) .text( preset ? ( i18n.useTemplate || 'Vorlage übernehmen' ) : ( i18n.addService || 'Add as service' ) )
.on( 'click', function () { .on( 'click', function () {
activateTab( 'services' ); activateTab( 'services' );
const $box = addServiceRow( { name: f.host, match_pattern: f.suggested_pattern || f.host, third_country: true } ); const $box = addServiceRow( data );
if ( $box && $box.length ) { if ( $box && $box.length ) {
$box[ 0 ].scrollIntoView( { behavior: 'smooth', block: 'center' } ); $box[ 0 ].scrollIntoView( { behavior: 'smooth', block: 'center' } );
$box.find( '.cb-input-id' ).trigger( 'focus' ); $box.find( '.cb-input-id' ).trigger( 'focus' );

View File

@@ -117,6 +117,22 @@
outline-offset: 2px; outline-offset: 2px;
} }
/* "Remember this service" checkbox under the button */
.cb-blocker .cb-blocker__remember {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
margin-top: 0.8em;
font-size: 0.8rem;
opacity: 0.85;
cursor: pointer;
}
.cb-blocker .cb-blocker__remember input {
margin: 0;
cursor: pointer;
}
/* ── Revoke button ── */ /* ── Revoke button ── */
.cb-revoke-btn { .cb-revoke-btn {
display: inline-block; display: inline-block;

View File

@@ -71,9 +71,17 @@
const serviceId = btn.dataset.cbId; const serviceId = btn.dataset.cbId;
if ( ! serviceId ) return; if ( ! serviceId ) return;
grantConsent( serviceId );
const wrapper = btn.closest( '.cb-blocker' ); const wrapper = btn.closest( '.cb-blocker' );
// "Remember" checkbox (default checked): persist consent only when
// ticked; otherwise load this embed once without storing consent.
const remember = wrapper
? wrapper.querySelector( '.cb-blocker__remember-cb' )
: null;
if ( ! remember || remember.checked ) {
grantConsent( serviceId );
}
if ( wrapper ) { if ( wrapper ) {
loadContent( wrapper ); loadContent( wrapper );
} }

View File

@@ -38,6 +38,17 @@ require_once CB_DIR . 'includes/class-autodetect.php';
require_once CB_DIR . 'includes/class-license.php'; require_once CB_DIR . 'includes/class-license.php';
require_once CB_DIR . 'includes/class-updater.php'; require_once CB_DIR . 'includes/class-updater.php';
// Language policy: German source strings for any German locale (de_*), English
// for everything else. We map the plugin's locale accordingly and ship an
// en_US translation. Applies to admin and frontend (filter runs for both).
add_filter( 'plugin_locale', 'cb_plugin_locale', 10, 2 );
function cb_plugin_locale( $locale, $domain ) {
if ( $domain !== 'gdpr-content-blocker' ) {
return $locale;
}
return str_starts_with( (string) $locale, 'de' ) ? 'de_DE' : 'en_US';
}
add_action( 'plugins_loaded', 'cb_init' ); add_action( 'plugins_loaded', 'cb_init' );
function cb_init(): void { function cb_init(): void {

View File

@@ -244,6 +244,10 @@ class CB_Renderer {
$name $name
) )
. '</button>' . '</button>'
. '<label class="cb-blocker__remember">'
. '<input type="checkbox" class="cb-blocker__remember-cb" checked> '
. esc_html__( 'Diesen Dienst künftig immer laden', 'gdpr-content-blocker' )
. '</label>'
. '</div>' . '</div>'
. '</div>'; . '</div>';
} }

View File

@@ -137,6 +137,7 @@ class CB_Settings {
return; return;
} }
wp_enqueue_style( 'wp-color-picker' ); wp_enqueue_style( 'wp-color-picker' );
wp_enqueue_style( 'dashicons' );
wp_enqueue_script( wp_enqueue_script(
'cb-admin', 'cb-admin',
CB_URL . 'assets/admin.js', CB_URL . 'assets/admin.js',
@@ -164,6 +165,8 @@ class CB_Settings {
'thirdParty' => __( 'Drittanbieter', 'gdpr-content-blocker' ), 'thirdParty' => __( 'Drittanbieter', 'gdpr-content-blocker' ),
'firstParty' => __( 'eigene Domain', 'gdpr-content-blocker' ), 'firstParty' => __( 'eigene Domain', 'gdpr-content-blocker' ),
'addService' => __( 'Als Dienst übernehmen', 'gdpr-content-blocker' ), 'addService' => __( 'Als Dienst übernehmen', 'gdpr-content-blocker' ),
'useTemplate' => __( 'Vorlage übernehmen', 'gdpr-content-blocker' ),
'templateAvail' => __( 'Vorlage verfügbar', 'gdpr-content-blocker' ),
'scannedPages'=> __( 'Gescannte Seiten:', 'gdpr-content-blocker' ), 'scannedPages'=> __( 'Gescannte Seiten:', 'gdpr-content-blocker' ),
'foundOn' => __( 'Gefunden auf', 'gdpr-content-blocker' ), 'foundOn' => __( 'Gefunden auf', 'gdpr-content-blocker' ),
], ],
@@ -180,7 +183,7 @@ class CB_Settings {
'id' => 'google-maps', 'id' => 'google-maps',
'name' => 'Google Maps', 'name' => 'Google Maps',
'match_pattern' => 'google.com/maps', 'match_pattern' => 'google.com/maps',
'recipient' => 'Google Ireland Ltd., Irland / Google LLC, USA', 'recipient' => __( 'Google Ireland Ltd., Irland / Google LLC, USA', 'gdpr-content-blocker' ),
'third_country' => true, 'third_country' => true,
'sets_cookie' => true, 'sets_cookie' => true,
'loads_script' => true, 'loads_script' => true,
@@ -191,7 +194,7 @@ class CB_Settings {
'id' => 'youtube', 'id' => 'youtube',
'name' => 'YouTube', 'name' => 'YouTube',
'match_pattern' => 'youtube', 'match_pattern' => 'youtube',
'recipient' => 'Google Ireland Ltd., Irland / Google LLC, USA', 'recipient' => __( 'Google Ireland Ltd., Irland / Google LLC, USA', 'gdpr-content-blocker' ),
'third_country' => true, 'third_country' => true,
'sets_cookie' => true, 'sets_cookie' => true,
'loads_script' => true, 'loads_script' => true,
@@ -202,7 +205,7 @@ class CB_Settings {
'id' => 'openstreetmap', 'id' => 'openstreetmap',
'name' => 'OpenStreetMap', 'name' => 'OpenStreetMap',
'match_pattern' => 'openstreetmap.org', 'match_pattern' => 'openstreetmap.org',
'recipient' => 'OpenStreetMap Foundation, Großbritannien', 'recipient' => __( 'OpenStreetMap Foundation, Großbritannien', 'gdpr-content-blocker' ),
'third_country' => true, 'third_country' => true,
'sets_cookie' => false, 'sets_cookie' => false,
'loads_script' => true, 'loads_script' => true,
@@ -213,7 +216,7 @@ class CB_Settings {
'id' => 'vimeo', 'id' => 'vimeo',
'name' => 'Vimeo', 'name' => 'Vimeo',
'match_pattern' => 'player.vimeo.com', 'match_pattern' => 'player.vimeo.com',
'recipient' => 'Vimeo LLC, USA', 'recipient' => __( 'Vimeo LLC, USA', 'gdpr-content-blocker' ),
'third_country' => true, 'third_country' => true,
'sets_cookie' => true, 'sets_cookie' => true,
'loads_script' => true, 'loads_script' => true,
@@ -406,6 +409,57 @@ class CB_Settings {
.cb-admin-wrap .cb-switch input:checked + .cb-switch__slider::before { .cb-admin-wrap .cb-switch input:checked + .cb-switch__slider::before {
transform: translateX(18px); transform: translateX(18px);
} }
/* expand/collapse +/- icon */
.cb-admin-wrap .cb-service-toggle {
width: 30px;
height: 30px;
font-size: 22px;
line-height: 1;
font-weight: 400;
color: #2043B7;
background: #f0f3fc;
border: 1px solid #c7d2f5;
border-radius: 6px;
padding: 0;
}
.cb-admin-wrap .cb-service-toggle:hover {
background: #e3e9fb;
}
/* trash remove button */
.cb-admin-wrap .cb-remove-service {
background: none;
border: none;
cursor: pointer;
padding: 4px;
color: #888;
display: inline-flex;
align-items: center;
}
.cb-admin-wrap .cb-remove-service:hover {
color: #b32d2e;
}
.cb-admin-wrap .cb-remove-service .dashicons {
font-size: 20px;
width: 20px;
height: 20px;
}
/* active tab indicator */
.cb-admin-wrap .nav-tab-wrapper {
margin-bottom: 0;
}
.cb-admin-wrap .nav-tab-active,
.cb-admin-wrap .nav-tab-active:focus,
.cb-admin-wrap .nav-tab-active:hover {
background: #fff;
color: #2043B7;
border-bottom: 3px solid #2043B7;
font-weight: 600;
margin-bottom: -1px;
}
/* 20px breathing room between tabs and content */
.cb-admin-wrap .cb-tab-content {
padding-top: 20px;
}
.cb-admin-wrap .cb-field label { .cb-admin-wrap .cb-field label {
display: block; display: block;
font-weight: 600; font-weight: 600;
@@ -449,7 +503,7 @@ class CB_Settings {
?> ?>
<div class="cb-service-box" data-cb-index="<?php echo $idx; ?>"> <div class="cb-service-box" data-cb-index="<?php echo $idx; ?>">
<div class="cb-service-head"> <div class="cb-service-head">
<button type="button" class="cb-service-toggle" aria-expanded="false" title="<?php esc_attr_e( 'Details anzeigen/ausblenden', 'gdpr-content-blocker' ); ?>"></button> <button type="button" class="cb-service-toggle" aria-expanded="false" aria-label="<?php esc_attr_e( 'Details anzeigen/ausblenden', 'gdpr-content-blocker' ); ?>">+</button>
<span class="cb-service-title"><?php echo esc_html( $headname ); ?></span> <span class="cb-service-title"><?php echo esc_html( $headname ); ?></span>
<label class="cb-switch" title="<?php esc_attr_e( 'Blocker aktiv/inaktiv', 'gdpr-content-blocker' ); ?>"> <label class="cb-switch" title="<?php esc_attr_e( 'Blocker aktiv/inaktiv', 'gdpr-content-blocker' ); ?>">
@@ -458,8 +512,8 @@ class CB_Settings {
<span class="cb-switch__slider"></span> <span class="cb-switch__slider"></span>
</label> </label>
<button type="button" class="button-link-delete cb-remove-service"> <button type="button" class="cb-remove-service" aria-label="<?php esc_attr_e( 'Dienst entfernen', 'gdpr-content-blocker' ); ?>" title="<?php esc_attr_e( 'Dienst entfernen', 'gdpr-content-blocker' ); ?>">
<?php esc_html_e( 'Entfernen', 'gdpr-content-blocker' ); ?> <span class="dashicons dashicons-trash"></span>
</button> </button>
</div> </div>
<div class="cb-service-grid" style="display:none;"> <div class="cb-service-grid" style="display:none;">
@@ -710,11 +764,14 @@ class CB_Settings {
wp_send_json_error( [ 'message' => $msg ] ); wp_send_json_error( [ 'message' => $msg ] );
} }
// Annotate each finding: is it already covered by a configured service? // Annotate each finding: already covered by a configured service? And is
// there a ready-made template (preset) for it?
$services = self::get_services(); $services = self::get_services();
$presets = self::get_presets();
$findings = is_array( $data['findings'] ?? null ) ? $data['findings'] : []; $findings = is_array( $data['findings'] ?? null ) ? $data['findings'] : [];
foreach ( $findings as &$f ) { foreach ( $findings as &$f ) {
$f['covered'] = self::is_covered( $f, $services ); $f['covered'] = self::is_covered( $f, $services );
$f['preset'] = self::match_preset( $f, $presets ); // preset key or ''
} }
unset( $f ); unset( $f );
@@ -724,12 +781,17 @@ class CB_Settings {
] ); ] );
} }
/** True if any configured service's match_pattern matches this finding. */ /** Collect host + sample URLs of a finding for substring matching. */
private static function is_covered( array $finding, array $services ): bool { private static function finding_haystacks( array $finding ): array {
$haystacks = array_merge( return array_merge(
[ (string) ( $finding['host'] ?? '' ) ], [ (string) ( $finding['host'] ?? '' ) ],
array_map( 'strval', (array) ( $finding['sample_urls'] ?? [] ) ) array_map( 'strval', (array) ( $finding['sample_urls'] ?? [] ) )
); );
}
/** True if any configured service's match_pattern matches this finding. */
private static function is_covered( array $finding, array $services ): bool {
$haystacks = self::finding_haystacks( $finding );
foreach ( $services as $svc ) { foreach ( $services as $svc ) {
$pattern = $svc['match_pattern'] ?? ''; $pattern = $svc['match_pattern'] ?? '';
if ( $pattern === '' ) { if ( $pattern === '' ) {
@@ -743,4 +805,21 @@ class CB_Settings {
} }
return false; return false;
} }
/** Return the key of a preset whose match_pattern fits this finding, or ''. */
private static function match_preset( array $finding, array $presets ): string {
$haystacks = self::finding_haystacks( $finding );
foreach ( $presets as $key => $preset ) {
$pattern = $preset['match_pattern'] ?? '';
if ( $pattern === '' ) {
continue;
}
foreach ( $haystacks as $h ) {
if ( $h !== '' && str_contains( $h, $pattern ) ) {
return (string) $key;
}
}
}
return '';
}
} }

View File

@@ -0,0 +1,386 @@
msgid ""
msgstr ""
"Project-Id-Version: GDPR Content Blocker\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: en_US\n"
msgid "Ungültige Antwort vom Lizenzserver."
msgstr "Invalid response from the license server."
msgid "Keine Berechtigung."
msgstr "Insufficient permissions."
msgid "Bitte einen Lizenzschlüssel eingeben."
msgstr "Please enter a license key."
msgid "Lizenz erfolgreich aktiviert."
msgstr "License activated successfully."
msgid "Alle Plätze dieser Lizenz sind belegt. Geben Sie eine Domain frei, um diese Seite zu aktivieren."
msgstr "All seats for this license are in use. Release a domain to activate this site."
msgid "Aktivierung fehlgeschlagen."
msgstr "Activation failed."
msgid "Bitte eine gültige Domain zum Freigeben auswählen."
msgstr "Please select a valid domain to release."
msgid "Domain %s freigegeben und diese Seite aktiviert."
msgstr "Released %s and activated this site."
msgid "Lizenz nicht mehr gültig."
msgstr "License is no longer valid."
msgid "GDPR Content Blocker ist nicht lizenziert. Der Schutz bleibt aktiv, aber für Updates und Support bitte %s hinterlegen."
msgstr "GDPR Content Blocker is not licensed. Protection stays active, but please add a %s for updates and support."
msgid "Lizenzschlüssel"
msgstr "License key"
msgid "Aktiv"
msgstr "Active"
msgid "Ungültig"
msgstr "Invalid"
msgid "Alle Plätze belegt"
msgstr "All seats in use"
msgid "Nicht aktiviert"
msgstr "Not activated"
msgid "Status"
msgstr "Status"
msgid "Domain"
msgstr "Domain"
msgid "Domain freigeben"
msgstr "Release domain"
msgid "Diese Lizenz ist bereits auf den genannten Domains aktiv. Wählen Sie eine zum Freigeben aus sie wird deaktiviert und diese Seite stattdessen aktiviert."
msgstr "This license is already active on the domains listed. Choose one to release — it will be deactivated and this site activated instead."
msgid "Freigeben und diese Seite aktivieren"
msgstr "Release and activate this site"
msgid "Aus Sicherheitsgründen verdeckt. Zum Ändern bitte zuerst deaktivieren."
msgstr "Hidden for security. To change it, deactivate first."
msgid "Lizenz deaktivieren"
msgstr "Deactivate license"
msgid "Erneut versuchen"
msgstr "Try again"
msgid "Lizenz aktivieren"
msgstr "Activate license"
msgid "Einwilligung für externe Inhalte widerrufen"
msgstr "Withdraw consent for external content"
msgid "Betrifft nur die Freigabe externer Einbettungen (z. B. Karten, Videos). Cookie-Einstellungen werden separat verwaltet."
msgstr "Affects only the release of external embeds (e.g. maps, videos). Cookie settings are managed separately."
msgid "Übermittlung in ein Drittland außerhalb der EU/des EWR"
msgstr "Transfer to a third country outside the EU/EEA"
msgid "Empfänger"
msgstr "Recipient"
msgid "Zweck"
msgstr "Purpose"
msgid "Setzt Cookies"
msgstr "Sets cookies"
msgid "Ja"
msgstr "Yes"
msgid "Nein"
msgstr "No"
msgid "Datenschutz"
msgstr "Privacy policy"
msgid "Um diesen Inhalt von %s zu laden, ist Ihre Einwilligung erforderlich. Dabei werden personenbezogene Daten (z. B. Ihre IP-Adresse) an den Anbieter übertragen."
msgstr "Loading this content from %s requires your consent. Personal data (e.g. your IP address) will be transmitted to the provider."
msgid "⚠ Datenübermittlung in ein Drittland außerhalb der EU/des EWR"
msgstr "⚠ Data transfer to a third country outside the EU/EEA"
msgid "Datenschutzerklärung des Anbieters"
msgstr "Provider's privacy policy"
msgid "Empfänger:"
msgstr "Recipient:"
msgid "Zweck:"
msgstr "Purpose:"
msgid "%s jetzt laden"
msgstr "Load %s now"
msgid "Diesen Dienst künftig immer laden"
msgstr "Always load this service from now on"
msgid "GDPR Content Blocker"
msgstr "GDPR Content Blocker"
msgid "Dienst wirklich entfernen?"
msgstr "Really remove this service?"
msgid "Neuer Dienst"
msgstr "New service"
msgid "Scanne Webseite …"
msgstr "Scanning website …"
msgid "Scan fehlgeschlagen:"
msgstr "Scan failed:"
msgid "Keine externen Einbindungen gefunden."
msgstr "No external resources found."
msgid "Anbieter / Host"
msgstr "Provider / Host"
msgid "Typ"
msgstr "Type"
msgid "Anzahl"
msgstr "Count"
msgid "Beispiel-URL"
msgstr "Example URL"
msgid "Aktion"
msgstr "Action"
msgid "abgedeckt"
msgstr "covered"
msgid "Drittanbieter"
msgstr "third party"
msgid "eigene Domain"
msgstr "own domain"
msgid "Als Dienst übernehmen"
msgstr "Add as service"
msgid "Vorlage übernehmen"
msgstr "Use template"
msgid "Vorlage verfügbar"
msgstr "Template available"
msgid "Gescannte Seiten:"
msgstr "Scanned pages:"
msgid "Gefunden auf"
msgstr "Found on"
msgid "Google Ireland Ltd., Irland / Google LLC, USA"
msgstr "Google Ireland Ltd., Ireland / Google LLC, USA"
msgid "Darstellung interaktiver Karten und Standortinformationen."
msgstr "Display of interactive maps and location information."
msgid "Einbettung und Wiedergabe von Videos."
msgstr "Embedding and playback of videos."
msgid "OpenStreetMap Foundation, Großbritannien"
msgstr "OpenStreetMap Foundation, United Kingdom"
msgid "Darstellung interaktiver Karten."
msgstr "Display of interactive maps."
msgid "Vimeo LLC, USA"
msgstr "Vimeo LLC, USA"
msgid "Einstellungen gespeichert."
msgstr "Settings saved."
msgid "Dienste"
msgstr "Services"
msgid "Scan"
msgstr "Scan"
msgid "Darstellung"
msgstr "Appearance"
msgid "Lizenz"
msgstr "License"
msgid "Über das Plugin"
msgstr "About the plugin"
msgid "+ Leeren Dienst hinzufügen"
msgstr "+ Add empty service"
msgid "— Vorlage einfügen —"
msgstr "— Insert template —"
msgid "Dienste speichern"
msgstr "Save services"
msgid "Der Lizenzserver besucht Ihre Webseite und listet alle eingebundenen Drittanbieter-Ressourcen (iframes, Skripte, Schriften, Bilder, …) auf. So sehen Sie auf einen Blick, welche externen Dienste Sie blockieren sollten. Erfordert eine aktive Lizenz."
msgstr "The license server visits your website and lists all embedded third-party resources (iframes, scripts, fonts, images, …). This shows at a glance which external services you should block. Requires an active license."
msgid "Webseite scannen"
msgstr "Scan website"
msgid "Darstellung speichern"
msgstr "Save appearance"
msgid "neu"
msgstr "new"
msgid "Details anzeigen/ausblenden"
msgstr "Show/hide details"
msgid "Blocker aktiv/inaktiv"
msgstr "Blocker on/off"
msgid "Dienst entfernen"
msgstr "Remove service"
msgid "Interner Name (Slug)"
msgstr "Internal name (slug)"
msgid "Anbietername"
msgstr "Provider name"
msgid "Erkennungsmuster (Domain/Pfad)"
msgstr "Detection pattern (domain/path)"
msgid "Empfänger (inkl. Land)"
msgstr "Recipient (incl. country)"
msgid "Datenschutz-URL des Anbieters"
msgstr "Provider's privacy policy URL"
msgid "Verarbeitungszweck"
msgstr "Processing purpose"
msgid "Individueller Platzhaltertext (leer = Standard)"
msgstr "Custom placeholder text (empty = default)"
msgid "Datenübermittlung in Drittland (außerhalb EU/EWR)"
msgstr "Data transfer to a third country (outside EU/EEA)"
msgid "Lädt externe Skripte"
msgstr "Loads external scripts"
msgid "Textfarbe Platzhalter"
msgstr "Placeholder text color"
msgid "Hintergrundfarbe Platzhalter"
msgstr "Placeholder background color"
msgid "Button: Hintergrundfarbe"
msgstr "Button: background color"
msgid "Button: Textfarbe"
msgstr "Button: text color"
msgid "Button Hover: Hintergrundfarbe"
msgstr "Button hover: background color"
msgid "Button Hover: Textfarbe"
msgstr "Button hover: text color"
msgid "Custom CSS"
msgstr "Custom CSS"
msgid "Wird nach den CSS-Variablen eingebunden und kann diese überschreiben. Tipp: denselben Präfix verwenden, z. B. .cb-blocker .cb-blocker__button { … }"
msgstr "Loaded after the CSS variables and can override them. Tip: use the same prefix, e.g. .cb-blocker .cb-blocker__button { … }"
msgid "Funktionen & Shortcodes"
msgstr "Features & shortcodes"
msgid "Widerruf-Button (Pflicht in der Datenschutzerklärung)"
msgstr "Withdrawal link (required in the privacy policy)"
msgid "Rendert einen gut sichtbaren Link, der die Einwilligung für externe Einbettungen widerruft und die Seite neu lädt (Art. 7 Abs. 3 DSGVO). Betrifft NICHT die Cookie-Einwilligung eines separaten Cookie-Plugins. Optionen: text=\"…\", style=\"link|button\", note=\"yes|no\". In die Datenschutzerklärung einfügen."
msgstr "Renders a clearly visible link that withdraws consent for external embeds and reloads the page (Art. 7(3) GDPR). Does NOT affect the cookie consent of a separate cookie plugin. Options: text=\"…\", style=\"link|button\", note=\"yes|no\". Add it to your privacy policy."
msgid "Inhalt manuell blockieren"
msgstr "Block content manually"
msgid "Umschließt ein iframe und ersetzt es durch den Platzhalter des angegebenen Dienstes. Verwenden Sie den internen Namen aus dem Tab „Dienste\". Ideal für Inhalte, die nicht automatisch erkannt werden (z. B. per JavaScript nachgeladene iframes)."
msgstr "Wraps an iframe and replaces it with the placeholder of the given service. Use the internal name from the \"Services\" tab. Ideal for content that isn't detected automatically (e.g. iframes loaded via JavaScript)."
msgid "Dienste-Übersicht (für die Datenschutzerklärung)"
msgstr "Services overview (for the privacy policy)"
msgid "Listet alle konfigurierten Dienste mit Empfänger, Drittland-Hinweis, Zweck und Datenschutz-Link auf. Ideal zum Einbinden in die Datenschutzerklärung (Art. 13 DSGVO)."
msgstr "Lists all configured services with recipient, third-country note, purpose and privacy link. Ideal for inclusion in your privacy policy (Art. 13 GDPR)."
msgid "Automatische Erkennung"
msgstr "Automatic detection"
msgid "iframes im Server-HTML, deren URL auf das „Erkennungsmuster\" eines Dienstes passt, werden automatisch blockiert. Greift nur bei statischem HTML für JS-iframes den Shortcode oben nutzen."
msgstr "iframes in the server HTML whose URL matches a service's \"detection pattern\" are blocked automatically. Works only for static HTML — for JS-injected iframes use the shortcode above."
msgid "Webseiten-Scan"
msgstr "Website scan"
msgid "Im Tab „Scan\" listet der Lizenzserver alle eingebundenen Drittanbieter auf. Erfordert eine aktive Lizenz."
msgstr "In the \"Scan\" tab the license server lists all embedded third parties. Requires an active license."
msgid "CSS-Klassen"
msgstr "CSS classes"
msgid "Eigene Regeln im Custom-CSS mit Präfix .cb-blocker schreiben, damit sie das Theme überschreiben."
msgstr "Write your own rules in Custom CSS with the .cb-blocker prefix so they override the theme."
msgid "Version %s"
msgstr "Version %s"
msgid "GDPR Content Blocker lädt externe Einbindungen (z. B. Google Maps, YouTube, OpenStreetMap, Vimeo) erst nach aktiver Einwilligung der Besucher. Vor dem Klick wird keine Verbindung zum Drittanbieter aufgebaut es werden also keine personenbezogenen Daten (z. B. die IP-Adresse) übertragen. So lassen sich externe Inhalte DSGVO-konform einbinden, ohne ein schweres Consent-Tool."
msgstr "GDPR Content Blocker loads external embeds (e.g. Google Maps, YouTube, OpenStreetMap, Vimeo) only after the visitor actively consents. Before the click no connection to the third party is made — so no personal data (e.g. the IP address) is transmitted. This lets you embed external content in a GDPR-compliant way without a heavy consent tool."
msgid "Funktionen"
msgstr "Features"
msgid "Echtes Click-to-Load (kein vorab geladenes iframe)"
msgstr "True click-to-load (no preloaded iframe)"
msgid "Granulare Einwilligung pro Dienst"
msgstr "Granular consent per service"
msgid "Art.-13-konformer Platzhalter mit Empfänger, Zweck, Drittland-Hinweis und Datenschutz-Link"
msgstr "Art.-13-compliant placeholder with recipient, purpose, third-country note and privacy link"
msgid "Einfacher Widerruf per Shortcode (Art. 7 Abs. 3 DSGVO)"
msgstr "Easy withdrawal via shortcode (Art. 7(3) GDPR)"
msgid "Automatische Erkennung gängiger Anbieter + manueller Shortcode"
msgstr "Automatic detection of common providers + manual shortcode"
msgid "Rechtliches"
msgstr "Legal"
msgid "Impressum"
msgstr "Imprint"
msgid "Datenschutzerklärung"
msgstr "Privacy policy"
msgid "Hinweis: Dieses Plugin ist ein technisches Hilfsmittel und ersetzt keine Rechtsberatung. Für die rechtskonforme Konfiguration (Empfänger, Zwecke, Datenschutzerklärung) ist der Seitenbetreiber verantwortlich."
msgstr "Note: This plugin is a technical tool and does not constitute legal advice. The site operator is responsible for the legally compliant configuration (recipients, purposes, privacy policy)."
msgid "Entwickelt von %s"
msgstr "Developed by %s"
msgid "Bitte zuerst eine Lizenz aktivieren (Tab „Lizenz\")."
msgstr "Please activate a license first (\"License\" tab)."
msgid "Unerwartete Antwort vom Lizenzserver."
msgstr "Unexpected response from the license server."

191
hilfsdaten/build-en-mo.py Normal file
View File

@@ -0,0 +1,191 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Build gdpr-content-blocker-en_US.po + .mo from strings.txt (German keys in
file order) + the EN list below (same order)."""
import struct, sys, os
EN = [
"Invalid response from the license server.",
"Insufficient permissions.",
"Please enter a license key.",
"License activated successfully.",
"All seats for this license are in use. Release a domain to activate this site.",
"Activation failed.",
"Please select a valid domain to release.",
"Released %s and activated this site.",
"License is no longer valid.",
"GDPR Content Blocker is not licensed. Protection stays active, but please add a %s for updates and support.",
"License key",
"Active",
"Invalid",
"All seats in use",
"Not activated",
"Status",
"Domain",
"Release domain",
"This license is already active on the domains listed. Choose one to release — it will be deactivated and this site activated instead.",
"Release and activate this site",
"Hidden for security. To change it, deactivate first.",
"Deactivate license",
"Try again",
"Activate license",
"Withdraw consent for external content",
"Affects only the release of external embeds (e.g. maps, videos). Cookie settings are managed separately.",
"Transfer to a third country outside the EU/EEA",
"Recipient",
"Purpose",
"Sets cookies",
"Yes",
"No",
"Privacy policy",
"Loading this content from %s requires your consent. Personal data (e.g. your IP address) will be transmitted to the provider.",
"⚠ Data transfer to a third country outside the EU/EEA",
"Provider's privacy policy",
"Recipient:",
"Purpose:",
"Load %s now",
"Always load this service from now on",
"GDPR Content Blocker",
"Really remove this service?",
"New service",
"Scanning website …",
"Scan failed:",
"No external resources found.",
"Provider / Host",
"Type",
"Count",
"Example URL",
"Action",
"covered",
"third party",
"own domain",
"Add as service",
"Use template",
"Template available",
"Scanned pages:",
"Found on",
"Google Ireland Ltd., Ireland / Google LLC, USA",
"Display of interactive maps and location information.",
"Embedding and playback of videos.",
"OpenStreetMap Foundation, United Kingdom",
"Display of interactive maps.",
"Vimeo LLC, USA",
"Settings saved.",
"Services",
"Scan",
"Appearance",
"License",
"About the plugin",
"+ Add empty service",
"— Insert template —",
"Save services",
"The license server visits your website and lists all embedded third-party resources (iframes, scripts, fonts, images, …). This shows at a glance which external services you should block. Requires an active license.",
"Scan website",
"Save appearance",
"new",
"Show/hide details",
"Blocker on/off",
"Remove service",
"Internal name (slug)",
"Provider name",
"Detection pattern (domain/path)",
"Recipient (incl. country)",
"Provider's privacy policy URL",
"Processing purpose",
"Custom placeholder text (empty = default)",
"Data transfer to a third country (outside EU/EEA)",
"Loads external scripts",
"Placeholder text color",
"Placeholder background color",
"Button: background color",
"Button: text color",
"Button hover: background color",
"Button hover: text color",
"Custom CSS",
"Loaded after the CSS variables and can override them. Tip: use the same prefix, e.g. .cb-blocker .cb-blocker__button { … }",
"Features & shortcodes",
"Withdrawal link (required in the privacy policy)",
"Renders a clearly visible link that withdraws consent for external embeds and reloads the page (Art. 7(3) GDPR). Does NOT affect the cookie consent of a separate cookie plugin. Options: text=\"\", style=\"link|button\", note=\"yes|no\". Add it to your privacy policy.",
"Block content manually",
"Wraps an iframe and replaces it with the placeholder of the given service. Use the internal name from the \"Services\" tab. Ideal for content that isn't detected automatically (e.g. iframes loaded via JavaScript).",
"Services overview (for the privacy policy)",
"Lists all configured services with recipient, third-country note, purpose and privacy link. Ideal for inclusion in your privacy policy (Art. 13 GDPR).",
"Automatic detection",
"iframes in the server HTML whose URL matches a service's \"detection pattern\" are blocked automatically. Works only for static HTML — for JS-injected iframes use the shortcode above.",
"Website scan",
"In the \"Scan\" tab the license server lists all embedded third parties. Requires an active license.",
"CSS classes",
"Write your own rules in Custom CSS with the .cb-blocker prefix so they override the theme.",
"Version %s",
"GDPR Content Blocker loads external embeds (e.g. Google Maps, YouTube, OpenStreetMap, Vimeo) only after the visitor actively consents. Before the click no connection to the third party is made — so no personal data (e.g. the IP address) is transmitted. This lets you embed external content in a GDPR-compliant way without a heavy consent tool.",
"Features",
"True click-to-load (no preloaded iframe)",
"Granular consent per service",
"Art.-13-compliant placeholder with recipient, purpose, third-country note and privacy link",
"Easy withdrawal via shortcode (Art. 7(3) GDPR)",
"Automatic detection of common providers + manual shortcode",
"Legal",
"Imprint",
"Privacy policy",
"Note: This plugin is a technical tool and does not constitute legal advice. The site operator is responsible for the legally compliant configuration (recipients, purposes, privacy policy).",
"Developed by %s",
"Please activate a license first (\"License\" tab).",
"Unexpected response from the license server.",
]
base = sys.argv[1]
strings_file = os.path.join(base, 'hilfsdaten', 'strings.txt')
de = []
with open(strings_file, encoding='utf-8') as f:
for line in f:
if line.startswith('MSGID\t'):
de.append(line[len('MSGID\t'):].rstrip('\n'))
if len(de) != len(EN):
print(f"MISMATCH: {len(de)} German vs {len(EN)} English"); sys.exit(1)
entries = {}
header = ("Project-Id-Version: GDPR Content Blocker\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: en_US\n")
entries[''] = header
for d, e in zip(de, EN):
entries[d] = e
def po_escape(s):
return s.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n')
# write .po
po_path = os.path.join(base, 'gdpr-content-blocker', 'languages', 'gdpr-content-blocker-en_US.po')
with open(po_path, 'w', encoding='utf-8') as f:
f.write('msgid ""\nmsgstr ""\n')
for line in header.rstrip('\n').split('\n'):
f.write('"%s\\n"\n' % po_escape(line))
f.write('\n')
for d, e in zip(de, EN):
f.write('msgid "%s"\n' % po_escape(d))
f.write('msgstr "%s"\n\n' % po_escape(e))
# write .mo
def write_mo(entries, out):
keys = sorted(entries.keys())
ids = b''; strs = b''; offsets = []
for k in keys:
kb = k.encode('utf-8'); vb = entries[k].encode('utf-8')
offsets.append((len(ids), len(kb), len(strs), len(vb)))
ids += kb + b'\x00'; strs += vb + b'\x00'
keystart = 7 * 4 + 16 * len(keys)
valuestart = keystart + len(ids)
ko = []; vo = []
for o1, l1, o2, l2 in offsets:
ko += [l1, o1 + keystart]; vo += [l2, o2 + valuestart]
out_b = struct.pack("Iiiiiii", 0x950412de, 0, len(keys), 7 * 4, 7 * 4 + len(keys) * 8, 0, 0)
out_b += struct.pack("i" * len(ko), *ko) + struct.pack("i" * len(vo), *vo) + ids + strs
with open(out, 'wb') as f:
f.write(out_b)
mo_path = os.path.join(base, 'gdpr-content-blocker', 'languages', 'gdpr-content-blocker-en_US.mo')
write_mo(entries, mo_path)
print(f"OK: {len(de)} strings -> {os.path.basename(po_path)} + .mo")

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env python3
"""Extract translatable strings (text-domain gdpr-content-blocker) from the plugin PHP."""
import re, sys, os, glob
FUNCS = r"(?:esc_html__|esc_html_e|esc_attr__|esc_attr_e|esc_attr_e|__|_e|esc_attr_e)"
# capture first single-quoted arg
PAT = re.compile(FUNCS + r"\s*\(\s*'((?:[^'\\]|\\.)*)'\s*,\s*'gdpr-content-blocker'")
def unescape(s):
return s.replace("\\'", "'").replace('\\\\', '\\')
base = sys.argv[1]
seen = []
seenset = set()
for path in glob.glob(os.path.join(base, '**', '*.php'), recursive=True):
with open(path, encoding='utf-8') as f:
txt = f.read()
for m in PAT.finditer(txt):
s = unescape(m.group(1))
if s not in seenset:
seenset.add(s)
seen.append(s)
out = sys.argv[2] if len(sys.argv) > 2 else 'strings.txt'
with open(out, 'w', encoding='utf-8') as f:
f.write(f"# {len(seen)} unique strings\n")
for s in seen:
f.write("MSGID\t" + s + "\n")
print(f"{len(seen)} strings written to {out}")