#!/usr/bin/env bash
|
set -euo pipefail
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
python3 - "$ROOT_DIR" <<'PY'
|
import json
|
import re
|
import sys
|
from pathlib import Path
|
|
root = Path(sys.argv[1])
|
app = root / "Apptag"
|
preferences = app / "PreferencesView.swift"
|
pro = app / "ProEntitlement.swift"
|
l10n = app / "L10n.swift"
|
localization_dir = app / "Localization"
|
|
|
def fail(message: str) -> None:
|
raise SystemExit(f"FAIL: {message}")
|
|
|
def read(path: Path) -> str:
|
try:
|
return path.read_text(encoding="utf-8")
|
except Exception as error:
|
fail(f"could not read {path}: {error}")
|
|
|
def function_body(source: str, signature: str) -> str:
|
start = source.find(signature)
|
if start == -1:
|
fail(f"missing function signature: {signature}")
|
brace = source.find("{", start)
|
if brace == -1:
|
fail(f"missing function body for: {signature}")
|
depth = 0
|
for index in range(brace, len(source)):
|
char = source[index]
|
if char == "{":
|
depth += 1
|
elif char == "}":
|
depth -= 1
|
if depth == 0:
|
return source[brace + 1:index]
|
fail(f"unterminated function body for: {signature}")
|
|
|
preferences_text = read(preferences)
|
pro_text = read(pro)
|
l10n_text = read(l10n)
|
|
required_pro = [
|
"func remainingSeconds(at date: Date = Date()) -> Int",
|
"ceil(endsAt.timeIntervalSince(date))",
|
"max(0, Int(",
|
]
|
missing_pro = [needle for needle in required_pro if needle not in pro_text]
|
if missing_pro:
|
fail("ProThemePreviewState countdown helper is incomplete: " + " | ".join(missing_pro))
|
|
required_preferences = [
|
"themePreviewCountdownTimer",
|
"@State private var themePreviewNow = Date()",
|
"pro.theme.status.previewCountdown",
|
".replacingOccurrences(of: \"%time%\", with: countdown)",
|
".countdown(text: themePreviewCountdownText ?? \"00:00\")",
|
"formatThemePreviewCountdown(seconds:",
|
"String(format: \"%02d:%02d\"",
|
"proEntitlement.stopThemePreview()",
|
".font(.system(size: 10, weight: .semibold, design: .monospaced))",
|
]
|
missing_preferences = [needle for needle in required_preferences if needle not in preferences_text]
|
if missing_preferences:
|
fail("Preferences countdown UI is incomplete: " + " | ".join(missing_preferences))
|
|
accessory_body = function_body(preferences_text, "private func themeOptionAccessory(for theme: AppGridTheme) -> ThemeOptionAccessory")
|
if "pro.card.previewBadge" in accessory_body:
|
fail("current preview theme card must show MM:SS, not the old Preview badge")
|
|
codes_match = re.search(
|
r"static\s+let\s+supported\s*:\s*\[\(code:\s*String,\s*name:\s*String\)\]\s*=\s*\[(.*?)\n\s*\]",
|
l10n_text,
|
re.S,
|
)
|
if not codes_match:
|
fail("could not find L10n.supported")
|
codes = re.findall(r'\("([^"]+)",\s*"[^"]+"\)', codes_match.group(1))
|
if len(codes) != 29:
|
fail(f"expected 29 languages, got {len(codes)}")
|
|
for code in codes:
|
path = localization_dir / f"{code}.json"
|
if not path.exists():
|
fail(f"missing localization file: {path}")
|
data = json.loads(path.read_text(encoding="utf-8"))
|
value = data.get("pro.theme.status.previewCountdown", "")
|
if not isinstance(value, str) or not value.strip():
|
fail(f"{path.name} missing pro.theme.status.previewCountdown")
|
if "%time%" not in value:
|
fail(f"{path.name} countdown copy lost %time% placeholder")
|
if "5" in value or "five" in value.lower():
|
fail(f"{path.name} countdown copy must not hardcode preview duration: {value!r}")
|
|
print("PASS Pro theme preview countdown QA: UI timer, MM:SS card accessory, expiry cleanup, and 29-language placeholder coverage are wired")
|
PY
|