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
71 lines
2.5 KiB
Python
71 lines
2.5 KiB
Python
#!/usr/bin/env python3
|
|
"""The backend never renders UI-language strings (CLAUDE.md).
|
|
|
|
API errors are `{detail, code}` and the frontend translates by `code`; SSE
|
|
`state` events carry counts and markers, and the frontend writes the
|
|
sentence. That contract is only worth anything if it is checked: the moment
|
|
one German error message ships from the backend, an English interface has a
|
|
German sentence in it that no message file can reach.
|
|
|
|
This looks for German text in the places that reach a user: `ApiError`
|
|
messages and `detail` fields. Prompts, template content, the fixture corpus
|
|
and comments are deliberately out of scope, because those are CONTENT and
|
|
German is correct there.
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
APP = Path(__file__).resolve().parents[1] / "app"
|
|
|
|
# Words that do not appear in English but are common in German UI copy.
|
|
# Umlauts alone are too weak (proper nouns), and a full language detector
|
|
# would be a dependency for a rule this narrow.
|
|
GERMAN_MARKERS = re.compile(
|
|
r"\b("
|
|
r"nicht|nichts|kein|keine|keinen|wurde|wurden|werden|wird|"
|
|
r"bitte|erneut|konnte|könnte|müssen|muss|darfst|kannst|"
|
|
r"deine|deinem|deiner|dein|Ihre|Ihrem|"
|
|
r"Fehler|Anfrage|Dokument|Abteilung|Benutzer|Gespräch|Vorlage"
|
|
r")\b",
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
# Only the strings that can reach a user, not every literal in the file.
|
|
USER_FACING = re.compile(
|
|
r"""ApiError\s*\(\s*\d+\s*,\s*(?P<q>["'])(?P<text>.*?)(?P=q)"""
|
|
r"""|detail\s*=\s*(?P<q2>["'])(?P<text2>.*?)(?P=q2)""",
|
|
re.DOTALL,
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
problems: list[str] = []
|
|
for path in sorted(APP.rglob("*.py")):
|
|
source = path.read_text(encoding="utf-8")
|
|
for match in USER_FACING.finditer(source):
|
|
text = match.group("text") or match.group("text2") or ""
|
|
if not GERMAN_MARKERS.search(text):
|
|
continue
|
|
line = source[: match.start()].count("\n") + 1
|
|
relative = path.relative_to(APP.parent)
|
|
problems.append(
|
|
f"{relative}:{line}: user-facing string looks German: "
|
|
f"{text[:70]!r}. The backend returns codes, the frontend "
|
|
f"writes the sentence (docs/i18n.md)."
|
|
)
|
|
|
|
if problems:
|
|
print("check-no-ui-strings: FAILED", file=sys.stderr)
|
|
for problem in problems:
|
|
print(f" {problem}", file=sys.stderr)
|
|
return 1
|
|
|
|
print("check-no-ui-strings: no UI-language strings in backend responses")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|