Files
felhom-controller/controller/scripts/emoji_gate.py
T
admin 612c417024
gates / gates (push) Successful in 19s
v0.247.0: i18n spike — the dashboard can speak English, Hungarian byte-identical
Message bundles (internal/i18n) expanded into templates before parsing, one
template set per language. Launcher, /backups, /apps/<slug> and the layout
converted; household language setting, POST /settings/language, ?lang= override,
report field. Parity test against fixtures captured from unconverted templates;
copy gates read templates expanded; new i18n_missing_gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-17 14:50:57 +02:00

96 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""D1 §10 Python emoji gate — Windows grep silently fails to match multibyte emoji (the D0
grep-gate zero was a false negative). This scans templates by Unicode codepoint.
Run from controller/: python scripts/emoji_gate.py
Exit 1 if any emoji/pictograph remains in web+setup templates.
"""
import io, os, sys, unicodedata
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import i18n_bundle # noqa: E402
ROOTS = [
os.path.join("internal", "web", "templates"),
os.path.join("internal", "setup", "templates"),
]
# Codepoint ranges that count as "emoji / pictographs / dingbats" for UI-copy purposes.
# Deliberately does NOT flag Hungarian letters, typographic quotes/dashes, the middle dot (·),
# arrows used as affordances (→ ↗ ↻ ↑ ↓), the multiplication sign (×), or box-drawing.
def is_emoji(ch):
o = ord(ch)
ranges = [
(0x1F300, 0x1FAFF), # Misc symbols & pictographs, emoticons, transport, supplemental, symbols-ext
(0x2600, 0x26FF), # Misc symbols (☀ ⚙ ⚠ ☁ …)
(0x2700, 0x27BF), # Dingbats (✅ ✂ ✈ ✏ ✓? no — see allow)
(0x1F000, 0x1F0FF), # Mahjong/dominoes/cards
(0xFE00, 0xFE0F), # Variation selectors (emoji presentation)
(0x1F1E6, 0x1F1FF), # Regional indicators
]
if any(a <= o <= b for a, b in ranges):
return True
return False
# Dingbat codepoints that are legitimate UI glyphs (checkmarks/crosses used as plain text marks,
# not emoji). We keep these OUT of the ban — they render as monochrome text, not color emoji.
ALLOW = set("✓✗✔✘•●○■▶") # ✓ ✗ ✔ ✘ • ● ○ ■ ▶
def scan(path):
# v0.247.0: the copy of a converted template lives in the i18n bundle -- scan every language's
# expansion, so an emoji in a translation is caught where it would render.
hits = []
for lang in i18n_bundle.LANGS:
for h in _scan_text(i18n_bundle.read_template(path, lang)):
if h not in hits:
hits.append(h)
return hits
def _scan_text(text):
hits = []
for lineno, line in enumerate(text.split("\n"), 1):
for ch in line:
if ch in ALLOW:
continue
if is_emoji(ch):
try:
name = unicodedata.name(ch)
except ValueError:
name = "U+%04X" % ord(ch)
hits.append((lineno, ch, name))
return hits
def _html_files(root):
# R-421 (2026-09-01): AT ANY DEPTH. This was `os.listdir`, one level only. There are no
# template subdirectories today, so the gate was green and correct — and would have stayed
# green the moment anyone added `templates/partials/`, which is an ordinary thing to do.
# Measured: a planted template in a new partials/ directory passed every listdir-based gate
# and was caught by the two that already used os.walk. See AUDIT-gate-decoys-2026-09-01.md.
out = []
for dirpath, _dirs, names in os.walk(root):
for fn in sorted(names):
if fn.endswith('.html'):
out.append(os.path.join(dirpath, fn))
return sorted(out)
def main():
total = 0
for root in ROOTS:
for path in _html_files(root):
fn = os.path.relpath(path, root)
for lineno, ch, name in scan(path):
total += 1
print("%s:%d %s %s" % (fn, lineno, ch, name))
if total:
print("EMOJI GATE FAILED: %d emoji found" % total)
sys.exit(1)
print("emoji gate OK — no emoji in web/setup templates")
if __name__ == "__main__":
main()