feat: lokales Google-Fonts-Hosting + "Lizenz upgraden"-button (v1.3.0)

- Lizenz-Tab: bei vollen Plaetzen erscheint ein "Lizenz upgraden"-Button
  (https://lucas-orth.de/wp-plugins/gdpr-content-blocker/) zusaetzlich zur
  domain-freigabe.
- Neues Feature "Schriften": Google Fonts lokal hosten (button-modell).
  Admin klickt "Jetzt herunterladen & einbinden" -> plugin scannt die seiten,
  laedt googleapis-CSS + gstatic-schriftdateien einmalig in den uploads-ordner,
  schreibt die pfade lokal um und tauscht sie per output-buffer im quelltext.
  Abgedeckt: <link>, inline-@import/url() UND @import/url() in gleicher-origin
  externen CSS-dateien (mit absolutierung der restlichen relativen pfade).
  preconnect/dns-prefetch zu google-font-hosts werden entfernt (kein IP-leak).
  Toggle zum aus/einschalten + "Lokale Schriften loeschen".
- i18n-pipeline robuster: build-en-mo.py nutzt jetzt translations-en.json
  (lookup per quell-string, reihenfolge-unabhaengig) und bricht mit liste ab,
  falls eine uebersetzung fehlt. Alle neuen strings DE/EN ergaenzt (139).
- version 1.3.0 (header + CB_VERSION).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-06-08 07:16:19 +02:00
parent ca0f2f2c32
commit 377c64f8d0
10 changed files with 990 additions and 152 deletions

View File

@@ -250,6 +250,40 @@
$out.append( $table ); $out.append( $table );
} }
/* ── Local Google Fonts: download & embed ── */
$( '#cb-fonts-fetch' ).on( 'click', function () {
const i18n = cbAdmin.i18n || {};
const $btn = $( this ).prop( 'disabled', true );
const $st = $( '#cb-fonts-status' ).text( i18n.fontsFetching || 'Downloading…' );
$.post( cbAdmin.ajaxUrl, { action: 'cb_fonts_fetch', nonce: cbAdmin.fontsNonce } )
.done( function ( resp ) {
if ( ! resp || ! resp.success ) {
const msg = resp && resp.data && resp.data.message ? resp.data.message : 'Error';
$st.text( ( i18n.fontsError || 'Error:' ) + ' ' + msg );
return;
}
$st.text( resp.data.message || 'OK' );
setTimeout( function () { window.location.reload(); }, 900 );
} )
.fail( function ( xhr ) {
$st.text( ( i18n.fontsError || 'Error:' ) + ' ' + xhr.status );
} )
.always( function () {
$btn.prop( 'disabled', false );
} );
} );
$( '#cb-fonts-clear' ).on( 'click', function () {
const i18n = cbAdmin.i18n || {};
if ( ! window.confirm( i18n.fontsClearConfirm || 'Delete local fonts?' ) ) {
return;
}
$( this ).prop( 'disabled', true );
$.post( cbAdmin.ajaxUrl, { action: 'cb_fonts_clear', nonce: cbAdmin.fontsNonce } )
.always( function () { window.location.reload(); } );
} );
/* ── Activate a tab by key ── */ /* ── Activate a tab by key ── */
function activateTab( tab ) { function activateTab( tab ) {
$( '.cb-tab-content' ).hide(); $( '.cb-tab-content' ).hide();

View File

@@ -3,7 +3,7 @@
* Plugin Name: GDPR Content Blocker * Plugin Name: GDPR Content Blocker
* Plugin URI: https://lucas-orth.de * Plugin URI: https://lucas-orth.de
* Description: DSGVO-konformer Consent-Blocker für externe iframes. Lädt Drittinhalte erst nach aktiver Einwilligung. * Description: DSGVO-konformer Consent-Blocker für externe iframes. Lädt Drittinhalte erst nach aktiver Einwilligung.
* Version: 1.2.0 * Version: 1.3.0
* Author: Lucas Orth * Author: Lucas Orth
* Author URI: https://lucas-orth.de * Author URI: https://lucas-orth.de
* Text Domain: gdpr-content-blocker * Text Domain: gdpr-content-blocker
@@ -15,7 +15,7 @@
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
define( 'CB_VERSION', '1.2.0' ); define( 'CB_VERSION', '1.3.0' );
define( 'CB_FILE', __FILE__ ); define( 'CB_FILE', __FILE__ );
define( 'CB_DIR', plugin_dir_path( __FILE__ ) ); define( 'CB_DIR', plugin_dir_path( __FILE__ ) );
define( 'CB_URL', plugin_dir_url( __FILE__ ) ); define( 'CB_URL', plugin_dir_url( __FILE__ ) );
@@ -37,6 +37,7 @@ require_once CB_DIR . 'includes/class-renderer.php';
require_once CB_DIR . 'includes/class-autodetect.php'; 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';
require_once CB_DIR . 'includes/class-fonts.php';
// Language policy: German source strings for any German locale (de_*), English // Language policy: German source strings for any German locale (de_*), English
// for everything else. We map the plugin's locale accordingly and ship an // for everything else. We map the plugin's locale accordingly and ship an
@@ -59,6 +60,7 @@ function cb_init(): void {
CB_Autodetect::init(); CB_Autodetect::init();
CB_License::init(); CB_License::init();
CB_Updater::init(); CB_Updater::init();
CB_Fonts::init();
} }
// Schedule / clear the daily license re-check. // Schedule / clear the daily license re-check.

View File

@@ -0,0 +1,529 @@
<?php
defined( 'ABSPATH' ) || exit;
/**
* Optional: host Google Fonts locally to avoid the connection to Google's CDN
* (privacy + performance).
*
* Trigger model: "button". The admin clicks "download & embed"; the plugin
* visits the configured pages, finds every Google Fonts reference and downloads
* everything once:
* - <link href="…fonts.googleapis.com/css…"> (also protocol-relative)
* - @import / direct gstatic URLs inside inline <style> blocks
* - @import / url(…gstatic…) inside SAME-ORIGIN external CSS files
* The CSS + font files are stored under wp-content/uploads, all paths are
* rewritten to the local copies, and a remote→local URL map is saved. On the
* front end a lightweight output-buffer pass swaps the mapped URLs (no network).
*
* Anything not covered by the scanned pages simply stays remote until the next
* "download & embed" run (predictable, no per-visitor latency).
*/
class CB_Fonts {
const OPTION = 'cb_fonts';
const DIR = 'gdpr-content-blocker-fonts';
// Modern browser UA so fonts.googleapis.com returns woff2 (not legacy ttf).
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
const MAX_CSS = 40;
const MAX_FONTS = 200;
/* Per-build memoisation (avoid re-downloading across pages). */
private static array $css_done = [];
private static array $font_done = [];
private static array $map = [];
private static int $font_count = 0;
private static int $css_count = 0;
public static function init(): void {
add_action( 'template_redirect', [ __CLASS__, 'start_buffer' ], 5 );
add_action( 'admin_post_cb_save_fonts', [ __CLASS__, 'handle_save' ] );
add_action( 'wp_ajax_cb_fonts_fetch', [ __CLASS__, 'ajax_fetch' ] );
add_action( 'wp_ajax_cb_fonts_clear', [ __CLASS__, 'ajax_clear' ] );
}
public static function get_settings(): array {
$s = get_option( self::OPTION, [] );
$s = wp_parse_args( is_array( $s ) ? $s : [], [
'enabled' => false,
'map' => [],
'updated_at' => 0,
'count' => 0,
'css_count' => 0,
] );
if ( ! is_array( $s['map'] ) ) {
$s['map'] = [];
}
return $s;
}
public static function is_enabled(): bool {
$s = self::get_settings();
return ! empty( $s['enabled'] ) && ! empty( $s['map'] );
}
/* ───────────────────────── front-end rewrite ───────────────────────── */
public static function start_buffer(): void {
if ( is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
return;
}
if ( ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) ) {
return;
}
if ( ! self::is_enabled() ) {
return;
}
ob_start( [ __CLASS__, 'rewrite' ] );
}
public static function rewrite( string $html ): string {
if ( $html === '' ) {
return $html;
}
$map = self::get_settings()['map'];
if ( empty( $map ) ) {
return $html;
}
// Longest keys first so a shorter URL can't pre-empt a longer one.
uksort( $map, fn( $a, $b ) => strlen( (string) $b ) <=> strlen( (string) $a ) );
$html = str_replace( array_keys( $map ), array_values( $map ), $html );
// Drop now-pointless resource hints to Google's font hosts — a preconnect
// would still open a connection and leak the visitor's IP. Match any
// attribute order via look-aheads.
$out = preg_replace(
'#<link\b(?=[^>]*\brel=["\'](?:preconnect|dns-prefetch|preload)["\'])(?=[^>]*(?:fonts\.googleapis\.com|fonts\.gstatic\.com))[^>]*>\s*#i',
'',
$html
);
return $out === null ? $html : $out;
}
/* ───────────────────────── admin actions ───────────────────────── */
public static function handle_save(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Keine Berechtigung.', 'gdpr-content-blocker' ) );
}
check_admin_referer( 'cb_fonts', 'cb_fonts_nonce' );
$s = self::get_settings();
$s['enabled'] = ! empty( $_POST['cb_fonts_enabled'] );
update_option( self::OPTION, $s );
wp_safe_redirect( admin_url( 'options-general.php?page=gdpr-content-blocker&cb_tab=fonts&cb_saved=1' ) );
exit;
}
public static function ajax_fetch(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => __( 'Keine Berechtigung.', 'gdpr-content-blocker' ) ], 403 );
}
check_ajax_referer( 'cb_fonts', 'nonce' );
$res = self::build_cache();
if ( is_wp_error( $res ) ) {
wp_send_json_error( [ 'message' => $res->get_error_message() ] );
}
wp_send_json_success( $res );
}
public static function ajax_clear(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => __( 'Keine Berechtigung.', 'gdpr-content-blocker' ) ], 403 );
}
check_ajax_referer( 'cb_fonts', 'nonce' );
self::delete_dir();
$s = self::get_settings();
$s['map'] = [];
$s['count'] = 0;
$s['css_count'] = 0;
$s['updated_at'] = 0;
update_option( self::OPTION, $s );
wp_send_json_success( [ 'message' => __( 'Lokale Schriften gelöscht.', 'gdpr-content-blocker' ) ] );
}
/* ───────────────────────── download / cache ───────────────────────── */
private static function build_cache(): array|WP_Error {
self::$css_done = [];
self::$font_done = [];
self::$map = [];
self::$font_count = 0;
self::$css_count = 0;
$upload = wp_upload_dir();
if ( ! empty( $upload['error'] ) ) {
return new WP_Error( 'cb_upload', (string) $upload['error'] );
}
$dir = trailingslashit( $upload['basedir'] ) . self::DIR;
if ( ! wp_mkdir_p( $dir ) ) {
return new WP_Error( 'cb_mkdir', __( 'Upload-Verzeichnis konnte nicht erstellt werden.', 'gdpr-content-blocker' ) );
}
$home_host = self::host( home_url() );
$pages = CB_Settings::scan_urls();
foreach ( $pages as $page ) {
$html = self::http_get( $page );
if ( $html === null ) {
continue;
}
// 1) Google Fonts stylesheets referenced directly (link href / @import).
if ( preg_match_all( '#(?:https?:)?//fonts\.googleapis\.com/[^"\'()\s<>]+#i', $html, $g ) ) {
foreach ( array_unique( $g[0] ) as $found ) {
if ( isset( self::$map[ $found ] ) ) {
continue;
}
$local = self::process_googleapis_css( self::abs_url( $found ) );
if ( $local ) {
self::$map[ $found ] = $local;
}
}
}
// 2) Direct gstatic font URLs in the HTML (e.g. inline @font-face).
if ( preg_match_all( '#(?:https?:)?//fonts\.gstatic\.com/[^"\'()\s<>]+#i', $html, $gs ) ) {
foreach ( array_unique( $gs[0] ) as $found ) {
if ( isset( self::$map[ $found ] ) ) {
continue;
}
$local = self::download_font( self::abs_url( $found ) );
if ( $local ) {
self::$map[ $found ] = $local;
}
}
}
// 3) Same-origin external CSS files that themselves reference Google Fonts.
if ( preg_match_all( '#<link\b[^>]*>#i', $html, $links ) ) {
foreach ( $links[0] as $tag ) {
if ( self::$css_count >= self::MAX_CSS ) {
break;
}
$rel = self::attr( $tag, 'rel' );
$href = self::attr( $tag, 'href' );
if ( $href === '' || stripos( $rel, 'stylesheet' ) === false ) {
continue;
}
$abs = self::abs_url( $href );
$h = self::host( $abs );
if ( $h === 'fonts.googleapis.com' || $h !== $home_host ) {
continue; // googleapis handled in (1); only our own CSS files here
}
$cache_key = 'ext:' . $abs;
if ( isset( self::$css_done[ $cache_key ] ) ) {
continue;
}
self::$css_done[ $cache_key ] = true;
$css = self::http_get( $abs, true );
if ( $css === null
|| ( stripos( $css, 'fonts.googleapis.com' ) === false
&& stripos( $css, 'fonts.gstatic.com' ) === false ) ) {
continue;
}
$localized = self::localize_css( $css, $abs, true );
if ( $localized !== $css ) {
$url = self::save( 'css', $localized );
if ( $url ) {
self::$map[ $href ] = $url;
self::$css_count++;
}
}
}
}
}
$s = self::get_settings();
$s['map'] = self::$map;
$s['count'] = self::$font_count;
$s['css_count'] = self::$css_count;
$s['updated_at'] = time();
$s['enabled'] = true; // a successful build implies the feature is wanted
update_option( self::OPTION, $s );
return [
'fonts' => self::$font_count,
'css' => self::$css_count,
'mapped' => count( self::$map ),
'message' => sprintf(
/* translators: 1: number of stylesheets, 2: number of font files */
__( '%1$d Schrift-Stylesheet(s) und %2$d Schriftdatei(en) lokal gespeichert.', 'gdpr-content-blocker' ),
self::$css_count,
self::$font_count
),
];
}
/** Fetch a googleapis CSS, localise its gstatic URLs, store it; return local URL. */
private static function process_googleapis_css( string $absUrl ): ?string {
if ( array_key_exists( $absUrl, self::$css_done ) ) {
return self::$css_done[ $absUrl ];
}
if ( self::$css_count >= self::MAX_CSS || self::host( $absUrl ) !== 'fonts.googleapis.com' ) {
return null;
}
$css = self::http_get( $absUrl, true );
if ( $css === null ) {
self::$css_done[ $absUrl ] = null;
return null;
}
$css = self::localize_css( $css, $absUrl, false );
$url = self::save( 'css', $css );
if ( $url ) {
self::$css_count++;
}
self::$css_done[ $absUrl ] = $url;
return $url;
}
/** Download a single gstatic font file; return its local URL. */
private static function download_font( string $absUrl ): ?string {
if ( array_key_exists( $absUrl, self::$font_done ) ) {
return self::$font_done[ $absUrl ];
}
if ( self::$font_count >= self::MAX_FONTS || self::host( $absUrl ) !== 'fonts.gstatic.com' ) {
return null;
}
$bin = self::http_get( $absUrl );
if ( $bin === null ) {
self::$font_done[ $absUrl ] = null;
return null;
}
$url = self::save( self::ext_from_url( $absUrl ), $bin );
if ( $url ) {
self::$font_count++;
}
self::$font_done[ $absUrl ] = $url;
return $url;
}
/**
* Rewrite a CSS body: googleapis @import → local; gstatic url() → local file.
* When $absolutize is true (external theme CSS we copy elsewhere), all other
* relative url()s are made absolute against $baseUrl so they keep working.
*/
private static function localize_css( string $css, string $baseUrl, bool $absolutize ): string {
// @import url("…googleapis…") or @import "…googleapis…"
$css = preg_replace_callback(
'#@import\s+(?:url\(\s*)?([\'"]?)([^\'"\)\s]+)\1\s*\)?\s*;#i',
function ( array $m ): string {
$abs = self::abs_url( $m[2] );
if ( self::host( $abs ) !== 'fonts.googleapis.com' ) {
return $m[0];
}
$local = self::process_googleapis_css( $abs );
return $local ? '@import url("' . $local . '");' : $m[0];
},
$css
) ?? $css;
// url(…gstatic…woff2) → local
$css = preg_replace_callback(
'#url\(\s*([\'"]?)([^\'"\)\s]+)\1\s*\)#i',
function ( array $m ) use ( $baseUrl, $absolutize ): string {
$raw = $m[2];
$abs = self::abs_url( $raw, $baseUrl );
if ( self::host( $abs ) === 'fonts.gstatic.com' ) {
$local = self::download_font( $abs );
return $local ? 'url("' . $local . '")' : $m[0];
}
// Keep other resources working when we relocate the CSS file.
if ( $absolutize && ! preg_match( '#^(data:|https?://|//)#i', $raw ) ) {
return 'url("' . $abs . '")';
}
return $m[0];
},
$css
) ?? $css;
return $css;
}
/* ───────────────────────── small helpers ───────────────────────── */
/** GET a URL with a browser UA. Returns the body (string) or null on failure. */
private static function http_get( string $url, bool $css = false ): ?string {
$args = [
'timeout' => 20,
'redirection' => 3,
'user-agent' => self::UA,
];
if ( $css ) {
$args['headers'] = [ 'Accept' => 'text/css,*/*;q=0.1' ];
}
$resp = wp_remote_get( $url, $args );
if ( is_wp_error( $resp ) || wp_remote_retrieve_response_code( $resp ) !== 200 ) {
return null;
}
$body = wp_remote_retrieve_body( $resp );
return $body === '' ? null : $body;
}
/** Save bytes to the uploads font dir (deduped by content hash); return URL. */
private static function save( string $ext, string $contents ): ?string {
$upload = wp_upload_dir();
if ( ! empty( $upload['error'] ) ) {
return null;
}
$dir = trailingslashit( $upload['basedir'] ) . self::DIR;
if ( ! wp_mkdir_p( $dir ) ) {
return null;
}
$ext = preg_replace( '/[^a-z0-9]/', '', strtolower( $ext ) ) ?: 'bin';
$name = md5( $contents ) . '.' . $ext;
$path = trailingslashit( $dir ) . $name;
if ( ! file_exists( $path ) && file_put_contents( $path, $contents ) === false ) {
return null;
}
return trailingslashit( $upload['baseurl'] ) . self::DIR . '/' . $name;
}
private static function delete_dir(): void {
$upload = wp_upload_dir();
if ( ! empty( $upload['error'] ) ) {
return;
}
$dir = trailingslashit( $upload['basedir'] ) . self::DIR;
if ( ! is_dir( $dir ) ) {
return;
}
foreach ( (array) glob( trailingslashit( $dir ) . '*' ) as $file ) {
if ( is_file( $file ) ) {
@unlink( $file );
}
}
}
/** Resolve protocol-relative / relative URLs (and decode HTML entities). */
private static function abs_url( string $url, string $base = '' ): string {
$url = html_entity_decode( trim( $url ), ENT_QUOTES );
if ( str_starts_with( $url, '//' ) ) {
return 'https:' . $url;
}
if ( preg_match( '#^(data:|https?://)#i', $url ) ) {
return $url;
}
if ( $base === '' ) {
return $url;
}
// Relative → absolute against $base.
$b = wp_parse_url( $base );
$scheme = $b['scheme'] ?? 'https';
$host = $b['host'] ?? '';
$port = isset( $b['port'] ) ? ':' . $b['port'] : '';
$path = $b['path'] ?? '/';
if ( str_starts_with( $url, '/' ) ) {
$full = $url;
} else {
$full = preg_replace( '#/[^/]*$#', '/', $path ) . $url;
}
$segs = [];
foreach ( explode( '/', $full ) as $seg ) {
if ( $seg === '..' ) {
array_pop( $segs );
} elseif ( $seg !== '.' && $seg !== '' ) {
$segs[] = $seg;
}
}
return $scheme . '://' . $host . $port . '/' . implode( '/', $segs );
}
private static function host( string $url ): string {
return strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) );
}
private static function ext_from_url( string $url ): string {
$path = (string) wp_parse_url( $url, PHP_URL_PATH );
$ext = strtolower( pathinfo( $path, PATHINFO_EXTENSION ) );
return in_array( $ext, [ 'woff2', 'woff', 'ttf', 'otf', 'eot', 'svg' ], true ) ? $ext : 'woff2';
}
/** Read a single attribute value out of an HTML tag string. */
private static function attr( string $tag, string $name ): string {
$re = '#\b' . preg_quote( $name, '#' ) . '\s*=\s*("([^"]*)"|\'([^\']*)\'|([^\s>]+))#i';
if ( preg_match( $re, $tag, $m ) ) {
if ( ( $m[2] ?? '' ) !== '' ) {
return $m[2];
}
if ( ( $m[3] ?? '' ) !== '' ) {
return $m[3];
}
return $m[4] ?? '';
}
return '';
}
/* ───────────────────────── settings tab ───────────────────────── */
public static function render_tab(): void {
$s = self::get_settings();
$has_cache = ! empty( $s['map'] );
$when = $s['updated_at']
? wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), (int) $s['updated_at'] )
: '';
?>
<p class="description" style="max-width:760px;">
<?php esc_html_e( 'Bindet Google Fonts lokal ein: Das Plugin lädt die Schrift-Stylesheets und Schriftdateien einmalig herunter, speichert sie in Ihrem Uploads-Ordner und ersetzt die Pfade im Seitenquelltext. So wird beim Seitenaufruf keine Verbindung mehr zu Google aufgebaut (kein IP-Transfer) und die Schriften laden schneller.', 'gdpr-content-blocker' ); ?>
</p>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<?php wp_nonce_field( 'cb_fonts', 'cb_fonts_nonce' ); ?>
<input type="hidden" name="action" value="cb_save_fonts">
<table class="form-table" role="presentation"><tbody>
<tr>
<th scope="row"><?php esc_html_e( 'Lokales Hosting', 'gdpr-content-blocker' ); ?></th>
<td>
<label class="cb-switch" title="<?php esc_attr_e( 'Lokale Schriften ein/aus', 'gdpr-content-blocker' ); ?>">
<input type="checkbox" name="cb_fonts_enabled" value="1" <?php checked( ! empty( $s['enabled'] ) ); ?>>
<span class="cb-switch__slider"></span>
</label>
<p class="description"><?php esc_html_e( 'Wenn aktiv, werden auf der Webseite die heruntergeladenen lokalen Schriften statt der Google-Pfade ausgeliefert.', 'gdpr-content-blocker' ); ?></p>
</td>
</tr>
</tbody></table>
<?php submit_button( __( 'Speichern', 'gdpr-content-blocker' ) ); ?>
</form>
<hr>
<h2><?php esc_html_e( 'Schriften herunterladen', 'gdpr-content-blocker' ); ?></h2>
<p>
<button type="button" id="cb-fonts-fetch" class="button button-primary">
<?php esc_html_e( 'Jetzt herunterladen & einbinden', 'gdpr-content-blocker' ); ?>
</button>
<?php if ( $has_cache ) : ?>
<button type="button" id="cb-fonts-clear" class="button" style="margin-left:6px;">
<?php esc_html_e( 'Lokale Schriften löschen', 'gdpr-content-blocker' ); ?>
</button>
<?php endif; ?>
<span id="cb-fonts-status" style="margin-left:10px;"></span>
</p>
<?php if ( $has_cache ) : ?>
<p class="description">
<?php
printf(
/* translators: 1: stylesheet count, 2: font file count, 3: date/time */
esc_html__( 'Aktuell lokal: %1$d Stylesheet(s), %2$d Schriftdatei(en) – zuletzt aktualisiert am %3$s.', 'gdpr-content-blocker' ),
(int) ( $s['css_count'] ?? 0 ),
(int) ( $s['count'] ?? 0 ),
esc_html( $when )
);
?>
</p>
<?php endif; ?>
<p class="description" style="max-width:760px;">
<?php esc_html_e( 'Hinweis: Gescannt werden die Startseite und einige Unterseiten. Falls auf einzelnen Seiten weitere Google Fonts vorkommen, führen Sie die Aktion nach Inhaltsänderungen erneut aus.', 'gdpr-content-blocker' ); ?>
</p>
<?php
}
}

View File

@@ -358,6 +358,18 @@ class CB_License {
</tr> </tr>
</tbody></table> </tbody></table>
<?php if ( $status === 'limit' ) : ?>
<!-- Not enough seats to activate this site: offer an upgrade. -->
<p style="margin:8px 0 16px;">
<a href="https://lucas-orth.de/wp-plugins/gdpr-content-blocker/" target="_blank" rel="noopener noreferrer" class="button button-primary">
<?php esc_html_e( 'Lizenz upgraden', 'gdpr-content-blocker' ); ?>
</a>
<span class="description" style="margin-left:8px;">
<?php esc_html_e( 'Schalten Sie weitere Domains frei, um diese Seite zusätzlich zu aktivieren – oder geben Sie unten eine bestehende Domain frei.', 'gdpr-content-blocker' ); ?>
</span>
</p>
<?php endif; ?>
<?php if ( $status === 'limit' && ! empty( $lic['pending_domains'] ) ) : ?> <?php if ( $status === 'limit' && ! empty( $lic['pending_domains'] ) ) : ?>
<!-- No free slots: list every bound domain with a trash button to free it. --> <!-- No free slots: list every bound domain with a trash button to free it. -->
<h2><?php esc_html_e( 'Belegte Domains', 'gdpr-content-blocker' ); ?></h2> <h2><?php esc_html_e( 'Belegte Domains', 'gdpr-content-blocker' ); ?></h2>

View File

@@ -170,6 +170,7 @@ class CB_Settings {
'newServiceLbl' => __( 'Neuer Dienst', 'gdpr-content-blocker' ), 'newServiceLbl' => __( 'Neuer Dienst', 'gdpr-content-blocker' ),
'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'scanNonce' => wp_create_nonce( 'cb_scan' ), 'scanNonce' => wp_create_nonce( 'cb_scan' ),
'fontsNonce' => wp_create_nonce( 'cb_fonts' ),
'i18n' => [ 'i18n' => [
'scanning' => __( 'Scanne Webseite …', 'gdpr-content-blocker' ), 'scanning' => __( 'Scanne Webseite …', 'gdpr-content-blocker' ),
'scanError' => __( 'Scan fehlgeschlagen:', 'gdpr-content-blocker' ), 'scanError' => __( 'Scan fehlgeschlagen:', 'gdpr-content-blocker' ),
@@ -188,6 +189,9 @@ class CB_Settings {
'templateAvail' => __( 'Vorlage verfügbar', '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' ),
'fontsFetching' => __( 'Schriften werden heruntergeladen …', 'gdpr-content-blocker' ),
'fontsError' => __( 'Fehler beim Herunterladen:', 'gdpr-content-blocker' ),
'fontsClearConfirm'=> __( 'Lokale Schriften wirklich löschen?', 'gdpr-content-blocker' ),
], ],
] ); ] );
} }
@@ -273,6 +277,9 @@ class CB_Settings {
<a href="#cb-tab-style" class="nav-tab" data-cb-tab="style"> <a href="#cb-tab-style" class="nav-tab" data-cb-tab="style">
<?php esc_html_e( 'Darstellung', 'gdpr-content-blocker' ); ?> <?php esc_html_e( 'Darstellung', 'gdpr-content-blocker' ); ?>
</a> </a>
<a href="#cb-tab-fonts" class="nav-tab" data-cb-tab="fonts">
<?php esc_html_e( 'Schriften', 'gdpr-content-blocker' ); ?>
</a>
<a href="#cb-tab-shortcodes" class="nav-tab" data-cb-tab="shortcodes"> <a href="#cb-tab-shortcodes" class="nav-tab" data-cb-tab="shortcodes">
<?php esc_html_e( 'Shortcodes', 'gdpr-content-blocker' ); ?> <?php esc_html_e( 'Shortcodes', 'gdpr-content-blocker' ); ?>
</a> </a>
@@ -345,6 +352,11 @@ class CB_Settings {
</form> </form>
</div> </div>
<!-- Fonts Tab -->
<div id="cb-tab-fonts" class="cb-tab-content" style="display:none;">
<?php CB_Fonts::render_tab(); ?>
</div>
<!-- Shortcodes Tab --> <!-- Shortcodes Tab -->
<div id="cb-tab-shortcodes" class="cb-tab-content" style="display:none;"> <div id="cb-tab-shortcodes" class="cb-tab-content" style="display:none;">
<?php self::render_shortcodes_help(); ?> <?php self::render_shortcodes_help(); ?>
@@ -735,7 +747,7 @@ class CB_Settings {
* Build the list of URLs to scan: the home page plus a few published * Build the list of URLs to scan: the home page plus a few published
* pages/posts, so the scan covers more than just the front page. * pages/posts, so the scan covers more than just the front page.
*/ */
private static function scan_urls(): array { public static function scan_urls(): array {
$urls = [ home_url( '/' ) ]; $urls = [ home_url( '/' ) ];
$posts = get_posts( [ $posts = get_posts( [

View File

@@ -6,12 +6,51 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: en_US\n" "Language: en_US\n"
msgid "Ungültige Antwort vom Lizenzserver."
msgstr "Invalid response from the license server."
msgid "Keine Berechtigung." msgid "Keine Berechtigung."
msgstr "Insufficient permissions." msgstr "Insufficient permissions."
msgid "Lokale Schriften gelöscht."
msgstr "Local fonts deleted."
msgid "Upload-Verzeichnis konnte nicht erstellt werden."
msgstr "The upload directory could not be created."
msgid "%1$d Schrift-Stylesheet(s) und %2$d Schriftdatei(en) lokal gespeichert."
msgstr "%1$d font stylesheet(s) and %2$d font file(s) stored locally."
msgid "Bindet Google Fonts lokal ein: Das Plugin lädt die Schrift-Stylesheets und Schriftdateien einmalig herunter, speichert sie in Ihrem Uploads-Ordner und ersetzt die Pfade im Seitenquelltext. So wird beim Seitenaufruf keine Verbindung mehr zu Google aufgebaut (kein IP-Transfer) und die Schriften laden schneller."
msgstr "Hosts Google Fonts locally: the plugin downloads the font stylesheets and font files once, stores them in your uploads folder and replaces the paths in the page source. As a result no connection to Google is made on page load (no IP transfer) and fonts load faster."
msgid "Lokales Hosting"
msgstr "Local hosting"
msgid "Lokale Schriften ein/aus"
msgstr "Local fonts on/off"
msgid "Wenn aktiv, werden auf der Webseite die heruntergeladenen lokalen Schriften statt der Google-Pfade ausgeliefert."
msgstr "When enabled, the website serves the downloaded local fonts instead of the Google paths."
msgid "Speichern"
msgstr "Save"
msgid "Schriften herunterladen"
msgstr "Download fonts"
msgid "Jetzt herunterladen & einbinden"
msgstr "Download & embed now"
msgid "Lokale Schriften löschen"
msgstr "Delete local fonts"
msgid "Aktuell lokal: %1$d Stylesheet(s), %2$d Schriftdatei(en) – zuletzt aktualisiert am %3$s."
msgstr "Currently local: %1$d stylesheet(s), %2$d font file(s) — last updated on %3$s."
msgid "Hinweis: Gescannt werden die Startseite und einige Unterseiten. Falls auf einzelnen Seiten weitere Google Fonts vorkommen, führen Sie die Aktion nach Inhaltsänderungen erneut aus."
msgstr "Note: the home page and a few sub-pages are scanned. If individual pages contain additional Google Fonts, run the action again after content changes."
msgid "Ungültige Antwort vom Lizenzserver."
msgstr "Invalid response from the license server."
msgid "Bitte einen Lizenzschlüssel eingeben." msgid "Bitte einen Lizenzschlüssel eingeben."
msgstr "Please enter a license key." msgstr "Please enter a license key."
@@ -57,6 +96,12 @@ msgstr "Status"
msgid "Domain" msgid "Domain"
msgstr "Domain" msgstr "Domain"
msgid "Lizenz upgraden"
msgstr "Upgrade license"
msgid "Schalten Sie weitere Domains frei, um diese Seite zusätzlich zu aktivieren – oder geben Sie unten eine bestehende Domain frei."
msgstr "Unlock more domains to additionally activate this site — or release an existing domain below."
msgid "Belegte Domains" msgid "Belegte Domains"
msgstr "Bound domains" msgstr "Bound domains"
@@ -186,6 +231,15 @@ msgstr "Scanned pages:"
msgid "Gefunden auf" msgid "Gefunden auf"
msgstr "Found on" msgstr "Found on"
msgid "Schriften werden heruntergeladen …"
msgstr "Downloading fonts …"
msgid "Fehler beim Herunterladen:"
msgstr "Download error:"
msgid "Lokale Schriften wirklich löschen?"
msgstr "Really delete the local fonts?"
msgid "Google Ireland Ltd., Irland / Google LLC, USA" msgid "Google Ireland Ltd., Irland / Google LLC, USA"
msgstr "Google Ireland Ltd., Ireland / Google LLC, USA" msgstr "Google Ireland Ltd., Ireland / Google LLC, USA"
@@ -216,6 +270,9 @@ msgstr "Scan"
msgid "Darstellung" msgid "Darstellung"
msgstr "Appearance" msgstr "Appearance"
msgid "Schriften"
msgstr "Fonts"
msgid "Shortcodes" msgid "Shortcodes"
msgstr "Shortcodes" msgstr "Shortcodes"

View File

@@ -1,133 +1,27 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Build gdpr-content-blocker-en_US.po + .mo from strings.txt (German keys in """Build gdpr-content-blocker-en_US.po + .mo.
file order) + the EN list below (same order)."""
import struct, sys, os
EN = [ Robust and order-independent: every German source string (extracted into
"Invalid response from the license server.", hilfsdaten/strings.txt) is looked up in hilfsdaten/translations-en.json.
"Insufficient permissions.", Adding a new __() string to the plugin only requires adding one key/value to
"Please enter a license key.", that JSON file — order does not matter. The build FAILS (and lists them) if any
"License activated successfully.", extracted string has no translation.
"All seats for this license are in use. Release a domain to activate this site.",
"Activation failed.", Usage: python hilfsdaten/extract-strings.py gdpr-content-blocker hilfsdaten/strings.txt
"Please select a valid domain to release.", python hilfsdaten/build-en-mo.py .
"Released %s and activated this site.", """
"License is no longer valid.", import json
"GDPR Content Blocker is not licensed. Protection stays active, but please add a %s for updates and support.", import os
"License key", import struct
"Active", import sys
"Invalid",
"All seats in use", base = sys.argv[1] if len(sys.argv) > 1 else '.'
"Not activated", here = os.path.dirname(os.path.abspath(__file__))
"Status",
"Domain", with open(os.path.join(here, 'translations-en.json'), encoding='utf-8') as f:
"Bound domains", TRANSLATIONS = json.load(f)
"This license is already active on the following domains. Remove one to free a seat — this site will then be activated automatically.",
"Really remove this domain from the license?",
"Remove domain",
"Hidden for security. To change it, deactivate first.",
"Deactivate license",
"Try again",
"Activate license",
"Recipient:",
"Purpose:",
"⚠ Data transfer to a third country outside the EU/EEA",
"Provider's privacy policy",
"Load %s now",
"Always load this service from now on",
"Loading this content from %s requires your consent. Personal data (e.g. your IP address) will be transmitted to the provider.",
"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",
"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",
"Shortcodes",
"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",
"Show/hide details",
"Blocker on/off",
"Remove service",
"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 { … }",
"CSS classes:",
"Withdrawal (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\".",
"Block content manually",
"Wraps an iframe and replaces it with the placeholder of the given service. The ID is the provider name in lowercase with hyphens (e.g. \"Google Maps\" → google-maps).",
"Services overview (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).",
"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') strings_file = os.path.join(base, 'hilfsdaten', 'strings.txt')
de = [] de = []
with open(strings_file, encoding='utf-8') as f: with open(strings_file, encoding='utf-8') as f:
@@ -135,48 +29,65 @@ with open(strings_file, encoding='utf-8') as f:
if line.startswith('MSGID\t'): if line.startswith('MSGID\t'):
de.append(line[len('MSGID\t'):].rstrip('\n')) de.append(line[len('MSGID\t'):].rstrip('\n'))
if len(de) != len(EN): missing = [s for s in de if s not in TRANSLATIONS]
print(f"MISMATCH: {len(de)} German vs {len(EN)} English"); sys.exit(1) if missing:
print('MISSING %d translation(s) — add them to hilfsdaten/translations-en.json:' % len(missing))
for m in missing:
print(' ' + json.dumps(m, ensure_ascii=False))
sys.exit(1)
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'
)
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} entries = {'': header}
for d, e in zip(de, EN): for s in de:
entries[d] = e entries[s] = TRANSLATIONS[s]
def po_escape(s): def po_escape(s):
return s.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n') return s.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n')
po_path = os.path.join(base, 'gdpr-content-blocker', 'languages', 'gdpr-content-blocker-en_US.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: with open(po_path, 'w', encoding='utf-8') as f:
f.write('msgid ""\nmsgstr ""\n') f.write('msgid ""\nmsgstr ""\n')
for line in header.rstrip('\n').split('\n'): for line in header.rstrip('\n').split('\n'):
f.write('"%s\\n"\n' % po_escape(line)) f.write('"%s\\n"\n' % po_escape(line))
f.write('\n') f.write('\n')
for d, e in zip(de, EN): for s in de:
f.write('msgid "%s"\n' % po_escape(d)) f.write('msgid "%s"\n' % po_escape(s))
f.write('msgstr "%s"\n\n' % po_escape(e)) f.write('msgstr "%s"\n\n' % po_escape(TRANSLATIONS[s]))
def write_mo(entries, out): def write_mo(entries, out):
keys = sorted(entries.keys()) keys = sorted(entries.keys())
ids = b''; strs = b''; offsets = [] ids = b''
strs = b''
offsets = []
for k in keys: for k in keys:
kb = k.encode('utf-8'); vb = entries[k].encode('utf-8') kb = k.encode('utf-8')
vb = entries[k].encode('utf-8')
offsets.append((len(ids), len(kb), len(strs), len(vb))) offsets.append((len(ids), len(kb), len(strs), len(vb)))
ids += kb + b'\x00'; strs += vb + b'\x00' ids += kb + b'\x00'
strs += vb + b'\x00'
keystart = 7 * 4 + 16 * len(keys) keystart = 7 * 4 + 16 * len(keys)
valuestart = keystart + len(ids) valuestart = keystart + len(ids)
ko = []; vo = [] ko = []
vo = []
for o1, l1, o2, l2 in offsets: for o1, l1, o2, l2 in offsets:
ko += [l1, o1 + keystart]; vo += [l2, o2 + valuestart] ko += [l1, o1 + keystart]
out_b = struct.pack("Iiiiiii", 0x950412de, 0, len(keys), 7 * 4, 7 * 4 + len(keys) * 8, 0, 0) vo += [l2, o2 + valuestart]
out_b += struct.pack("i" * len(ko), *ko) + struct.pack("i" * len(vo), *vo) + ids + strs 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: with open(out, 'wb') as f:
f.write(out_b) f.write(out_b)
mo_path = os.path.join(base, 'gdpr-content-blocker', 'languages', 'gdpr-content-blocker-en_US.mo') mo_path = os.path.join(base, 'gdpr-content-blocker', 'languages', 'gdpr-content-blocker-en_US.mo')
write_mo(entries, mo_path) write_mo(entries, mo_path)
print(f"OK: {len(de)} strings -> {os.path.basename(po_path)} + .mo") print('OK: %d strings -> %s + .mo' % (len(de), os.path.basename(po_path)))

140
hilfsdaten/strings.txt Normal file
View File

@@ -0,0 +1,140 @@
# 139 unique strings
MSGID Keine Berechtigung.
MSGID Lokale Schriften gelöscht.
MSGID Upload-Verzeichnis konnte nicht erstellt werden.
MSGID %1$d Schrift-Stylesheet(s) und %2$d Schriftdatei(en) lokal gespeichert.
MSGID Bindet Google Fonts lokal ein: Das Plugin lädt die Schrift-Stylesheets und Schriftdateien einmalig herunter, speichert sie in Ihrem Uploads-Ordner und ersetzt die Pfade im Seitenquelltext. So wird beim Seitenaufruf keine Verbindung mehr zu Google aufgebaut (kein IP-Transfer) und die Schriften laden schneller.
MSGID Lokales Hosting
MSGID Lokale Schriften ein/aus
MSGID Wenn aktiv, werden auf der Webseite die heruntergeladenen lokalen Schriften statt der Google-Pfade ausgeliefert.
MSGID Speichern
MSGID Schriften herunterladen
MSGID Jetzt herunterladen & einbinden
MSGID Lokale Schriften löschen
MSGID Aktuell lokal: %1$d Stylesheet(s), %2$d Schriftdatei(en) – zuletzt aktualisiert am %3$s.
MSGID Hinweis: Gescannt werden die Startseite und einige Unterseiten. Falls auf einzelnen Seiten weitere Google Fonts vorkommen, führen Sie die Aktion nach Inhaltsänderungen erneut aus.
MSGID Ungültige Antwort vom Lizenzserver.
MSGID Bitte einen Lizenzschlüssel eingeben.
MSGID Lizenz erfolgreich aktiviert.
MSGID Alle Plätze dieser Lizenz sind belegt. Geben Sie eine Domain frei, um diese Seite zu aktivieren.
MSGID Aktivierung fehlgeschlagen.
MSGID Bitte eine gültige Domain zum Freigeben auswählen.
MSGID Domain %s freigegeben und diese Seite aktiviert.
MSGID Lizenz nicht mehr gültig.
MSGID GDPR Content Blocker ist nicht lizenziert. Der Schutz bleibt aktiv, aber für Updates und Support bitte %s hinterlegen.
MSGID Lizenzschlüssel
MSGID Aktiv
MSGID Ungültig
MSGID Alle Plätze belegt
MSGID Nicht aktiviert
MSGID Status
MSGID Domain
MSGID Lizenz upgraden
MSGID Schalten Sie weitere Domains frei, um diese Seite zusätzlich zu aktivieren – oder geben Sie unten eine bestehende Domain frei.
MSGID Belegte Domains
MSGID Diese Lizenz ist bereits auf folgenden Domains aktiv. Entfernen Sie eine, um den Platz freizugeben – diese Seite wird anschließend automatisch aktiviert.
MSGID Diese Domain wirklich aus der Lizenz entfernen?
MSGID Domain entfernen
MSGID Aus Sicherheitsgründen verdeckt. Zum Ändern bitte zuerst deaktivieren.
MSGID Lizenz deaktivieren
MSGID Erneut versuchen
MSGID Lizenz aktivieren
MSGID Empfänger:
MSGID Zweck:
MSGID ⚠ Datenübermittlung in ein Drittland außerhalb der EU/des EWR
MSGID Datenschutzerklärung des Anbieters
MSGID %s jetzt laden
MSGID Diesen Dienst künftig immer laden
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.
MSGID Einwilligung für externe Inhalte widerrufen
MSGID Betrifft nur die Freigabe externer Einbettungen (z. B. Karten, Videos). Cookie-Einstellungen werden separat verwaltet.
MSGID Übermittlung in ein Drittland außerhalb der EU/des EWR
MSGID Empfänger
MSGID Zweck
MSGID Setzt Cookies
MSGID Ja
MSGID Nein
MSGID Datenschutz
MSGID GDPR Content Blocker
MSGID Dienst wirklich entfernen?
MSGID Neuer Dienst
MSGID Scanne Webseite …
MSGID Scan fehlgeschlagen:
MSGID Keine externen Einbindungen gefunden.
MSGID Anbieter / Host
MSGID Typ
MSGID Anzahl
MSGID Beispiel-URL
MSGID Aktion
MSGID abgedeckt
MSGID Drittanbieter
MSGID eigene Domain
MSGID Als Dienst übernehmen
MSGID Vorlage übernehmen
MSGID Vorlage verfügbar
MSGID Gescannte Seiten:
MSGID Gefunden auf
MSGID Schriften werden heruntergeladen …
MSGID Fehler beim Herunterladen:
MSGID Lokale Schriften wirklich löschen?
MSGID Google Ireland Ltd., Irland / Google LLC, USA
MSGID Darstellung interaktiver Karten und Standortinformationen.
MSGID Einbettung und Wiedergabe von Videos.
MSGID OpenStreetMap Foundation, Großbritannien
MSGID Darstellung interaktiver Karten.
MSGID Vimeo LLC, USA
MSGID Einstellungen gespeichert.
MSGID Dienste
MSGID Scan
MSGID Darstellung
MSGID Schriften
MSGID Shortcodes
MSGID Lizenz
MSGID Über das Plugin
MSGID + Leeren Dienst hinzufügen
MSGID — Vorlage einfügen —
MSGID Dienste speichern
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.
MSGID Webseite scannen
MSGID Darstellung speichern
MSGID Details anzeigen/ausblenden
MSGID Blocker aktiv/inaktiv
MSGID Dienst entfernen
MSGID Anbietername
MSGID Erkennungsmuster (Domain/Pfad)
MSGID Empfänger (inkl. Land)
MSGID Datenschutz-URL des Anbieters
MSGID Verarbeitungszweck
MSGID Individueller Platzhaltertext (leer = Standard)
MSGID Datenübermittlung in Drittland (außerhalb EU/EWR)
MSGID Lädt externe Skripte
MSGID Textfarbe Platzhalter
MSGID Hintergrundfarbe Platzhalter
MSGID Button: Hintergrundfarbe
MSGID Button: Textfarbe
MSGID Button Hover: Hintergrundfarbe
MSGID Button Hover: Textfarbe
MSGID 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 { … }
MSGID CSS-Klassen:
MSGID Widerruf (Datenschutzerklärung)
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".
MSGID Inhalt manuell blockieren
MSGID Umschließt ein iframe und ersetzt es durch den Platzhalter des angegebenen Dienstes. Die ID ist der Anbietername in Kleinbuchstaben mit Bindestrichen (z. B. „Google Maps" → google-maps).
MSGID Dienste-Übersicht (Datenschutzerklärung)
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).
MSGID 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.
MSGID Funktionen
MSGID Echtes Click-to-Load (kein vorab geladenes iframe)
MSGID Granulare Einwilligung pro Dienst
MSGID Art.-13-konformer Platzhalter mit Empfänger, Zweck, Drittland-Hinweis und Datenschutz-Link
MSGID Einfacher Widerruf per Shortcode (Art. 7 Abs. 3 DSGVO)
MSGID Automatische Erkennung gängiger Anbieter + manueller Shortcode
MSGID Rechtliches
MSGID Impressum
MSGID Datenschutzerklärung
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.
MSGID Entwickelt von %s
MSGID Bitte zuerst eine Lizenz aktivieren (Tab „Lizenz").
MSGID Unerwartete Antwort vom Lizenzserver.

View File

@@ -0,0 +1,141 @@
{
"Ungültige Antwort vom Lizenzserver.": "Invalid response from the license server.",
"Keine Berechtigung.": "Insufficient permissions.",
"Bitte einen Lizenzschlüssel eingeben.": "Please enter a license key.",
"Lizenz erfolgreich aktiviert.": "License activated successfully.",
"Alle Plätze dieser Lizenz sind belegt. Geben Sie eine Domain frei, um diese Seite zu aktivieren.": "All seats for this license are in use. Release a domain to activate this site.",
"Aktivierung fehlgeschlagen.": "Activation failed.",
"Bitte eine gültige Domain zum Freigeben auswählen.": "Please select a valid domain to release.",
"Domain %s freigegeben und diese Seite aktiviert.": "Released %s and activated this site.",
"Lizenz nicht mehr gültig.": "License is no longer valid.",
"GDPR Content Blocker ist nicht lizenziert. Der Schutz bleibt aktiv, aber für Updates und Support bitte %s hinterlegen.": "GDPR Content Blocker is not licensed. Protection stays active, but please add a %s for updates and support.",
"Lizenzschlüssel": "License key",
"Aktiv": "Active",
"Ungültig": "Invalid",
"Alle Plätze belegt": "All seats in use",
"Nicht aktiviert": "Not activated",
"Status": "Status",
"Domain": "Domain",
"Belegte Domains": "Bound domains",
"Diese Lizenz ist bereits auf folgenden Domains aktiv. Entfernen Sie eine, um den Platz freizugeben – diese Seite wird anschließend automatisch aktiviert.": "This license is already active on the following domains. Remove one to free a seat — this site will then be activated automatically.",
"Diese Domain wirklich aus der Lizenz entfernen?": "Really remove this domain from the license?",
"Domain entfernen": "Remove domain",
"Aus Sicherheitsgründen verdeckt. Zum Ändern bitte zuerst deaktivieren.": "Hidden for security. To change it, deactivate first.",
"Lizenz deaktivieren": "Deactivate license",
"Erneut versuchen": "Try again",
"Lizenz aktivieren": "Activate license",
"Empfänger:": "Recipient:",
"Zweck:": "Purpose:",
"⚠ Datenübermittlung in ein Drittland außerhalb der EU/des EWR": "⚠ Data transfer to a third country outside the EU/EEA",
"Datenschutzerklärung des Anbieters": "Provider's privacy policy",
"%s jetzt laden": "Load %s now",
"Diesen Dienst künftig immer laden": "Always load this service from now on",
"Um diesen Inhalt von %s zu laden, ist Ihre Einwilligung erforderlich. Dabei werden personenbezogene Daten (z. B. Ihre IP-Adresse) an den Anbieter übertragen.": "Loading this content from %s requires your consent. Personal data (e.g. your IP address) will be transmitted to the provider.",
"Einwilligung für externe Inhalte widerrufen": "Withdraw consent for external content",
"Betrifft nur die Freigabe externer Einbettungen (z. B. Karten, Videos). Cookie-Einstellungen werden separat verwaltet.": "Affects only the release of external embeds (e.g. maps, videos). Cookie settings are managed separately.",
"Übermittlung in ein Drittland außerhalb der EU/des EWR": "Transfer to a third country outside the EU/EEA",
"Empfänger": "Recipient",
"Zweck": "Purpose",
"Setzt Cookies": "Sets cookies",
"Ja": "Yes",
"Nein": "No",
"Datenschutz": "Privacy policy",
"GDPR Content Blocker": "GDPR Content Blocker",
"Dienst wirklich entfernen?": "Really remove this service?",
"Neuer Dienst": "New service",
"Scanne Webseite …": "Scanning website …",
"Scan fehlgeschlagen:": "Scan failed:",
"Keine externen Einbindungen gefunden.": "No external resources found.",
"Anbieter / Host": "Provider / Host",
"Typ": "Type",
"Anzahl": "Count",
"Beispiel-URL": "Example URL",
"Aktion": "Action",
"abgedeckt": "covered",
"Drittanbieter": "third party",
"eigene Domain": "own domain",
"Als Dienst übernehmen": "Add as service",
"Vorlage übernehmen": "Use template",
"Vorlage verfügbar": "Template available",
"Gescannte Seiten:": "Scanned pages:",
"Gefunden auf": "Found on",
"Google Ireland Ltd., Irland / Google LLC, USA": "Google Ireland Ltd., Ireland / Google LLC, USA",
"Darstellung interaktiver Karten und Standortinformationen.": "Display of interactive maps and location information.",
"Einbettung und Wiedergabe von Videos.": "Embedding and playback of videos.",
"OpenStreetMap Foundation, Großbritannien": "OpenStreetMap Foundation, United Kingdom",
"Darstellung interaktiver Karten.": "Display of interactive maps.",
"Vimeo LLC, USA": "Vimeo LLC, USA",
"Einstellungen gespeichert.": "Settings saved.",
"Dienste": "Services",
"Scan": "Scan",
"Darstellung": "Appearance",
"Shortcodes": "Shortcodes",
"Lizenz": "License",
"Über das Plugin": "About the plugin",
"+ Leeren Dienst hinzufügen": "+ Add empty service",
"— Vorlage einfügen —": "— Insert template —",
"Dienste speichern": "Save services",
"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.": "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.",
"Webseite scannen": "Scan website",
"Darstellung speichern": "Save appearance",
"Details anzeigen/ausblenden": "Show/hide details",
"Blocker aktiv/inaktiv": "Blocker on/off",
"Dienst entfernen": "Remove service",
"Anbietername": "Provider name",
"Erkennungsmuster (Domain/Pfad)": "Detection pattern (domain/path)",
"Empfänger (inkl. Land)": "Recipient (incl. country)",
"Datenschutz-URL des Anbieters": "Provider's privacy policy URL",
"Verarbeitungszweck": "Processing purpose",
"Individueller Platzhaltertext (leer = Standard)": "Custom placeholder text (empty = default)",
"Datenübermittlung in Drittland (außerhalb EU/EWR)": "Data transfer to a third country (outside EU/EEA)",
"Lädt externe Skripte": "Loads external scripts",
"Textfarbe Platzhalter": "Placeholder text color",
"Hintergrundfarbe Platzhalter": "Placeholder background color",
"Button: Hintergrundfarbe": "Button: background color",
"Button: Textfarbe": "Button: text color",
"Button Hover: Hintergrundfarbe": "Button hover: background color",
"Button Hover: Textfarbe": "Button hover: text color",
"Custom CSS": "Custom CSS",
"Wird nach den CSS-Variablen eingebunden und kann diese überschreiben. Tipp: denselben Präfix verwenden, z. B. .cb-blocker .cb-blocker__button { … }": "Loaded after the CSS variables and can override them. Tip: use the same prefix, e.g. .cb-blocker .cb-blocker__button { … }",
"CSS-Klassen:": "CSS classes:",
"Widerruf (Datenschutzerklärung)": "Withdrawal (privacy policy)",
"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\".": "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\".",
"Inhalt manuell blockieren": "Block content manually",
"Umschließt ein iframe und ersetzt es durch den Platzhalter des angegebenen Dienstes. Die ID ist der Anbietername in Kleinbuchstaben mit Bindestrichen (z. B. „Google Maps\" → google-maps).": "Wraps an iframe and replaces it with the placeholder of the given service. The ID is the provider name in lowercase with hyphens (e.g. \"Google Maps\" → google-maps).",
"Dienste-Übersicht (Datenschutzerklärung)": "Services overview (privacy policy)",
"Listet alle konfigurierten Dienste mit Empfänger, Drittland-Hinweis, Zweck und Datenschutz-Link auf. Ideal zum Einbinden in die Datenschutzerklärung (Art. 13 DSGVO).": "Lists all configured services with recipient, third-country note, purpose and privacy link. Ideal for inclusion in your privacy policy (Art. 13 GDPR).",
"Version %s": "Version %s",
"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.": "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.",
"Funktionen": "Features",
"Echtes Click-to-Load (kein vorab geladenes iframe)": "True click-to-load (no preloaded iframe)",
"Granulare Einwilligung pro Dienst": "Granular consent per service",
"Art.-13-konformer Platzhalter mit Empfänger, Zweck, Drittland-Hinweis und Datenschutz-Link": "Art.-13-compliant placeholder with recipient, purpose, third-country note and privacy link",
"Einfacher Widerruf per Shortcode (Art. 7 Abs. 3 DSGVO)": "Easy withdrawal via shortcode (Art. 7(3) GDPR)",
"Automatische Erkennung gängiger Anbieter + manueller Shortcode": "Automatic detection of common providers + manual shortcode",
"Rechtliches": "Legal",
"Impressum": "Imprint",
"Datenschutzerklärung": "Privacy policy",
"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.": "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).",
"Entwickelt von %s": "Developed by %s",
"Bitte zuerst eine Lizenz aktivieren (Tab „Lizenz\").": "Please activate a license first (\"License\" tab).",
"Unerwartete Antwort vom Lizenzserver.": "Unexpected response from the license server.",
"Lizenz upgraden": "Upgrade license",
"Schalten Sie weitere Domains frei, um diese Seite zusätzlich zu aktivieren – oder geben Sie unten eine bestehende Domain frei.": "Unlock more domains to additionally activate this site — or release an existing domain below.",
"Schriften": "Fonts",
"Schriften werden heruntergeladen …": "Downloading fonts …",
"Fehler beim Herunterladen:": "Download error:",
"Lokale Schriften wirklich löschen?": "Really delete the local fonts?",
"Upload-Verzeichnis konnte nicht erstellt werden.": "The upload directory could not be created.",
"Lokale Schriften gelöscht.": "Local fonts deleted.",
"%1$d Schrift-Stylesheet(s) und %2$d Schriftdatei(en) lokal gespeichert.": "%1$d font stylesheet(s) and %2$d font file(s) stored locally.",
"Bindet Google Fonts lokal ein: Das Plugin lädt die Schrift-Stylesheets und Schriftdateien einmalig herunter, speichert sie in Ihrem Uploads-Ordner und ersetzt die Pfade im Seitenquelltext. So wird beim Seitenaufruf keine Verbindung mehr zu Google aufgebaut (kein IP-Transfer) und die Schriften laden schneller.": "Hosts Google Fonts locally: the plugin downloads the font stylesheets and font files once, stores them in your uploads folder and replaces the paths in the page source. As a result no connection to Google is made on page load (no IP transfer) and fonts load faster.",
"Lokales Hosting": "Local hosting",
"Lokale Schriften ein/aus": "Local fonts on/off",
"Wenn aktiv, werden auf der Webseite die heruntergeladenen lokalen Schriften statt der Google-Pfade ausgeliefert.": "When enabled, the website serves the downloaded local fonts instead of the Google paths.",
"Speichern": "Save",
"Schriften herunterladen": "Download fonts",
"Jetzt herunterladen & einbinden": "Download & embed now",
"Lokale Schriften löschen": "Delete local fonts",
"Aktuell lokal: %1$d Stylesheet(s), %2$d Schriftdatei(en) – zuletzt aktualisiert am %3$s.": "Currently local: %1$d stylesheet(s), %2$d font file(s) — last updated on %3$s.",
"Hinweis: Gescannt werden die Startseite und einige Unterseiten. Falls auf einzelnen Seiten weitere Google Fonts vorkommen, führen Sie die Aktion nach Inhaltsänderungen erneut aus.": "Note: the home page and a few sub-pages are scanned. If individual pages contain additional Google Fonts, run the action again after content changes."
}