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:
@@ -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)))
|
||||
|
||||
Reference in New Issue
Block a user