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
This commit is contained in:
co-authored by
Claude Opus 5
parent
68d3a43191
commit
784b76baf7
@@ -0,0 +1,70 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user