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:
@@ -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();
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
529
gdpr-content-blocker/includes/class-fonts.php
Normal file
529
gdpr-content-blocker/includes/class-fonts.php
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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( [
|
||||||
|
|||||||
Binary file not shown.
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
140
hilfsdaten/strings.txt
Normal 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.
|
||||||
141
hilfsdaten/translations-en.json
Normal file
141
hilfsdaten/translations-en.json
Normal 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."
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user