Files
pablan/frontend/scripts/check-messages.py
T
ProfessorNovaandClaude Opus 5 97dbff309c Pablan, as it stands
Self-hosted knowledge management for SMEs: a split-screen Markdown editor
whose sections an LLM refines while you write, and RAG question answering
over the documents that result. FastAPI + Postgres/pgvector on the back,
SvelteKit on the front, everything OpenAI-compatible and self-hostable.

Squashed into a single commit; the development history stays local.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CA43ZJda8Rbp2hKXNy8f6b
2026-09-04 08:36:17 +02:00

106 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.
#!/usr/bin/env python3
"""Gate on the two message rules that cannot be caught at runtime.
1. **Every locale carries every key.** Paraglide falls back to the base
locale for a missing message, which means an untranslated string ships
silently as German inside an English interface. A missing key is a build
failure here instead.
2. **No em or en dashes in UI copy.** They are hard to type, inconsistent
across the app when hand-written, and in German they collide with the
Gedankenstrich convention. Commas, colons or a second sentence do the
job. Prose in docs/ and comments is unaffected: this only reads the
message files.
Run by `make lint`, so both rules hold for every route the migration
touches rather than only where someone remembered.
"""
import json
import sys
from pathlib import Path
MESSAGES = Path(__file__).resolve().parents[1] / "messages"
BASE_LOCALE = "de"
DASHES = {"—": "em dash", "": "en dash"}
def load(path: Path) -> dict[str, object]:
data = json.loads(path.read_text(encoding="utf-8"))
return {key: value for key, value in data.items() if not key.startswith("$")}
def strings_in(value: object) -> list[str]:
"""Every translatable string inside a message.
A message is either a plain string or a list of variants, each with a
`match` object mapping a selector to a string (see docs/i18n.md,
pluralization). Declarations and selectors are machinery, not copy, so
only the match values are checked for dashes.
"""
if isinstance(value, str):
return [value]
if isinstance(value, list):
return [
text
for variant in value
if isinstance(variant, dict)
for text in variant.get("match", {}).values()
if isinstance(text, str)
]
return []
def main() -> int:
files = sorted(MESSAGES.glob("*.json"))
if not files:
print(f"check-messages: no message files in {MESSAGES}", file=sys.stderr)
return 1
catalogs = {path.stem: load(path) for path in files}
if BASE_LOCALE not in catalogs:
print(f"check-messages: missing base locale {BASE_LOCALE}.json", file=sys.stderr)
return 1
problems: list[str] = []
base_keys = set(catalogs[BASE_LOCALE])
for locale, catalog in sorted(catalogs.items()):
if locale == BASE_LOCALE:
continue
for key in sorted(base_keys - set(catalog)):
problems.append(
f"{locale}.json: missing message '{key}' "
f"(present in {BASE_LOCALE}.json): a missing translation "
f"would silently ship as {BASE_LOCALE}"
)
for key in sorted(set(catalog) - base_keys):
problems.append(
f"{locale}.json: message '{key}' has no counterpart in "
f"{BASE_LOCALE}.json: the source language defines the set"
)
for locale, catalog in sorted(catalogs.items()):
for key, value in sorted(catalog.items()):
for char, name in DASHES.items():
if any(char in text for text in strings_in(value)):
problems.append(
f"{locale}.json: message '{key}' contains an {name} "
f"({char}): use a comma, a colon, or two sentences"
)
if problems:
print("check-messages: FAILED", file=sys.stderr)
for problem in problems:
print(f" {problem}", file=sys.stderr)
return 1
total = len(base_keys)
locales = ", ".join(sorted(catalogs))
print(f"check-messages: {total} messages complete in {locales}, no dashes")
return 0
if __name__ == "__main__":
sys.exit(main())