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 );
|
||||
}
|
||||
|
||||
/* ── 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 ── */
|
||||
function activateTab( tab ) {
|
||||
$( '.cb-tab-content' ).hide();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Plugin Name: GDPR Content Blocker
|
||||
* Plugin URI: https://lucas-orth.de
|
||||
* 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 URI: https://lucas-orth.de
|
||||
* Text Domain: gdpr-content-blocker
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
define( 'CB_VERSION', '1.2.0' );
|
||||
define( 'CB_VERSION', '1.3.0' );
|
||||
define( 'CB_FILE', __FILE__ );
|
||||
define( 'CB_DIR', plugin_dir_path( __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-license.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
|
||||
// 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_License::init();
|
||||
CB_Updater::init();
|
||||
CB_Fonts::init();
|
||||
}
|
||||
|
||||
// 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>
|
||||
</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'] ) ) : ?>
|
||||
<!-- 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>
|
||||
|
||||
@@ -170,6 +170,7 @@ class CB_Settings {
|
||||
'newServiceLbl' => __( 'Neuer Dienst', 'gdpr-content-blocker' ),
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'scanNonce' => wp_create_nonce( 'cb_scan' ),
|
||||
'fontsNonce' => wp_create_nonce( 'cb_fonts' ),
|
||||
'i18n' => [
|
||||
'scanning' => __( 'Scanne Webseite …', 'gdpr-content-blocker' ),
|
||||
'scanError' => __( 'Scan fehlgeschlagen:', 'gdpr-content-blocker' ),
|
||||
@@ -188,6 +189,9 @@ class CB_Settings {
|
||||
'templateAvail' => __( 'Vorlage verfügbar', 'gdpr-content-blocker' ),
|
||||
'scannedPages'=> __( 'Gescannte Seiten:', '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">
|
||||
<?php esc_html_e( 'Darstellung', 'gdpr-content-blocker' ); ?>
|
||||
</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">
|
||||
<?php esc_html_e( 'Shortcodes', 'gdpr-content-blocker' ); ?>
|
||||
</a>
|
||||
@@ -345,6 +352,11 @@ class CB_Settings {
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Fonts Tab -->
|
||||
<div id="cb-tab-fonts" class="cb-tab-content" style="display:none;">
|
||||
<?php CB_Fonts::render_tab(); ?>
|
||||
</div>
|
||||
|
||||
<!-- Shortcodes Tab -->
|
||||
<div id="cb-tab-shortcodes" class="cb-tab-content" style="display:none;">
|
||||
<?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
|
||||
* 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( '/' ) ];
|
||||
|
||||
$posts = get_posts( [
|
||||
|
||||
Binary file not shown.
@@ -6,12 +6,51 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: en_US\n"
|
||||
|
||||
msgid "Ungültige Antwort vom Lizenzserver."
|
||||
msgstr "Invalid response from the license server."
|
||||
|
||||
msgid "Keine Berechtigung."
|
||||
msgstr "Insufficient permissions."
|
||||
|
||||
msgid "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."
|
||||
msgstr "Please enter a license key."
|
||||
|
||||
@@ -57,6 +96,12 @@ msgstr "Status"
|
||||
msgid "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"
|
||||
msgstr "Bound domains"
|
||||
|
||||
@@ -186,6 +231,15 @@ msgstr "Scanned pages:"
|
||||
msgid "Gefunden auf"
|
||||
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"
|
||||
msgstr "Google Ireland Ltd., Ireland / Google LLC, USA"
|
||||
|
||||
@@ -216,6 +270,9 @@ msgstr "Scan"
|
||||
msgid "Darstellung"
|
||||
msgstr "Appearance"
|
||||
|
||||
msgid "Schriften"
|
||||
msgstr "Fonts"
|
||||
|
||||
msgid "Shortcodes"
|
||||
msgstr "Shortcodes"
|
||||
|
||||
|
||||
@@ -1,133 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Build gdpr-content-blocker-en_US.po + .mo from strings.txt (German keys in
|
||||
file order) + the EN list below (same order)."""
|
||||
import struct, sys, os
|
||||
"""Build gdpr-content-blocker-en_US.po + .mo.
|
||||
|
||||
EN = [
|
||||
"Invalid response from the license server.",
|
||||
"Insufficient permissions.",
|
||||
"Please enter a license key.",
|
||||
"License activated successfully.",
|
||||
"All seats for this license are in use. Release a domain to activate this site.",
|
||||
"Activation failed.",
|
||||
"Please select a valid domain to release.",
|
||||
"Released %s and activated this site.",
|
||||
"License is no longer valid.",
|
||||
"GDPR Content Blocker is not licensed. Protection stays active, but please add a %s for updates and support.",
|
||||
"License key",
|
||||
"Active",
|
||||
"Invalid",
|
||||
"All seats in use",
|
||||
"Not activated",
|
||||
"Status",
|
||||
"Domain",
|
||||
"Bound domains",
|
||||
"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.",
|
||||
]
|
||||
Robust and order-independent: every German source string (extracted into
|
||||
hilfsdaten/strings.txt) is looked up in hilfsdaten/translations-en.json.
|
||||
Adding a new __() string to the plugin only requires adding one key/value to
|
||||
that JSON file — order does not matter. The build FAILS (and lists them) if any
|
||||
extracted string has no translation.
|
||||
|
||||
Usage: python hilfsdaten/extract-strings.py gdpr-content-blocker hilfsdaten/strings.txt
|
||||
python hilfsdaten/build-en-mo.py .
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
base = sys.argv[1] if len(sys.argv) > 1 else '.'
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
with open(os.path.join(here, 'translations-en.json'), encoding='utf-8') as f:
|
||||
TRANSLATIONS = json.load(f)
|
||||
|
||||
base = sys.argv[1]
|
||||
strings_file = os.path.join(base, 'hilfsdaten', 'strings.txt')
|
||||
de = []
|
||||
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'):
|
||||
de.append(line[len('MSGID\t'):].rstrip('\n'))
|
||||
|
||||
if len(de) != len(EN):
|
||||
print(f"MISMATCH: {len(de)} German vs {len(EN)} English"); sys.exit(1)
|
||||
missing = [s for s in de if s not in TRANSLATIONS]
|
||||
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}
|
||||
for d, e in zip(de, EN):
|
||||
entries[d] = e
|
||||
for s in de:
|
||||
entries[s] = TRANSLATIONS[s]
|
||||
|
||||
|
||||
def po_escape(s):
|
||||
return s.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n')
|
||||
|
||||
|
||||
po_path = os.path.join(base, 'gdpr-content-blocker', 'languages', 'gdpr-content-blocker-en_US.po')
|
||||
with open(po_path, 'w', encoding='utf-8') as f:
|
||||
f.write('msgid ""\nmsgstr ""\n')
|
||||
for line in header.rstrip('\n').split('\n'):
|
||||
f.write('"%s\\n"\n' % po_escape(line))
|
||||
f.write('\n')
|
||||
for d, e in zip(de, EN):
|
||||
f.write('msgid "%s"\n' % po_escape(d))
|
||||
f.write('msgstr "%s"\n\n' % po_escape(e))
|
||||
for s in de:
|
||||
f.write('msgid "%s"\n' % po_escape(s))
|
||||
f.write('msgstr "%s"\n\n' % po_escape(TRANSLATIONS[s]))
|
||||
|
||||
|
||||
def write_mo(entries, out):
|
||||
keys = sorted(entries.keys())
|
||||
ids = b''; strs = b''; offsets = []
|
||||
ids = b''
|
||||
strs = b''
|
||||
offsets = []
|
||||
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)))
|
||||
ids += kb + b'\x00'; strs += vb + b'\x00'
|
||||
ids += kb + b'\x00'
|
||||
strs += vb + b'\x00'
|
||||
keystart = 7 * 4 + 16 * len(keys)
|
||||
valuestart = keystart + len(ids)
|
||||
ko = []; vo = []
|
||||
ko = []
|
||||
vo = []
|
||||
for o1, l1, o2, l2 in offsets:
|
||||
ko += [l1, o1 + keystart]; vo += [l2, o2 + valuestart]
|
||||
out_b = struct.pack("Iiiiiii", 0x950412de, 0, len(keys), 7 * 4, 7 * 4 + len(keys) * 8, 0, 0)
|
||||
out_b += struct.pack("i" * len(ko), *ko) + struct.pack("i" * len(vo), *vo) + ids + strs
|
||||
ko += [l1, o1 + keystart]
|
||||
vo += [l2, o2 + valuestart]
|
||||
out_b = struct.pack('Iiiiiii', 0x950412de, 0, len(keys), 7 * 4, 7 * 4 + len(keys) * 8, 0, 0)
|
||||
out_b += struct.pack('i' * len(ko), *ko) + struct.pack('i' * len(vo), *vo) + ids + strs
|
||||
with open(out, 'wb') as f:
|
||||
f.write(out_b)
|
||||
|
||||
|
||||
mo_path = os.path.join(base, 'gdpr-content-blocker', 'languages', 'gdpr-content-blocker-en_US.mo')
|
||||
write_mo(entries, mo_path)
|
||||
print(f"OK: {len(de)} strings -> {os.path.basename(po_path)} + .mo")
|
||||
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