Skip to content

Commit bbe0e8c

Browse files
Lukas Geigerclaude
andcommitted
feat: visible language switcher (Welle-1 U1)
Add a discoverable "Sprache / Language" menu (Deutsch/English) to the Tkinter GUI. The user test reported the DE/EN switch could not be found even though translator.py + locales/translations.json existed but were never wired into the UI. - Menubar cascade with Deutsch/English radiobuttons; selection applies live to menu, buttons, shortcut hint and welcome text. - Language persisted per-user in %APPDATA%/MethodenAnalyser/config.json (XDG fallback elsewhere), restored on next start. - translations.json rebuilt with stable keys + full DE/EN strings. - translator.py: isinstance-guard against corrupt entries. - tests/test_language_switch.py: 8 headless regression tests (switching, persistence, restart, corrupt-config tolerance, guard). - test_cli.py info-dialog version check updated for the i18n mechanism. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014PYYrAKkuxD8Z4CkEP17nn
1 parent b1dc5a4 commit bbe0e8c

6 files changed

Lines changed: 306 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/).
55

66
## [Unreleased]
77

8+
### Neue Funktionen / Features
9+
- **UX-002 / Welle-1 U1** (`MethodenAnalyser3.py`, `locales/translations.json`): Sichtbarer Sprachschalter in der Menüleiste („Sprache / Language" → „Deutsch" / „English"). Menü, Schaltflächen, Tastaturhinweis und Willkommenstext stellen sich sofort um; die Auswahl wird pro Benutzer in `%APPDATA%\MethodenAnalyser\config.json` (bzw. `~/.config/MethodenAnalyser/config.json`) persistiert und beim nächsten Start wiederhergestellt. Das bisher ungenutzte `translator.py` / `locales/translations.json` ist damit im UI erreichbar. Regressionstests in `tests/test_language_switch.py`.
10+
811
### Build / Packaging
912
- `build_exe.bat` ergänzt einen reproduzierbaren PyInstaller-Build mit lokalem Workpath (lokales Build-Verzeichnis), zentralem Build-Exclude-Scanner und Kopie der fertigen EXE nach `dist\MethodenAnalyser.exe` sowie `MethodenAnalyser.exe`.
1013
- `START.bat` startet unter Windows bevorzugt die gebaute EXE und fällt erst danach auf den Python-Start zurück.

MethodenAnalyser3.py

Lines changed: 134 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
from dataclasses import dataclass, field
2020
from functools import lru_cache
2121

22+
try:
23+
from translator import TranslationSystem
24+
except Exception: # pragma: no cover - Uebersetzung ist optional
25+
TranslationSystem = None
26+
2227
# ============================================================================
2328
# KONSTANTEN
2429
# ============================================================================
@@ -43,6 +48,88 @@
4348
EXIT_FINDINGS = 2
4449
EXIT_PARTIAL_ERROR = 3
4550

51+
# ============================================================================
52+
# SPRACHE / KONFIGURATION (Welle-1 U1: sichtbarer DE/EN-Sprachschalter)
53+
# ============================================================================
54+
55+
SUPPORTED_LANGUAGES = ("de", "en")
56+
DEFAULT_LANGUAGE = "de"
57+
58+
# Erste Zeile der Willkommensnachricht je Sprache (fuer Live-Neurendern beim Sprachwechsel)
59+
_WELCOME_HEADS = (
60+
"Willkommen beim Python Code Analyzer!",
61+
"Welcome to Python Code Analyzer!",
62+
)
63+
64+
65+
def _config_dir() -> pathlib.Path:
66+
"""Per-User-Konfigverzeichnis (auch bei read-only Store-Installation schreibbar)."""
67+
if sys.platform.startswith("win"):
68+
base = os.environ.get("APPDATA") or os.path.expanduser("~")
69+
else:
70+
base = os.environ.get("XDG_CONFIG_HOME") or os.path.join(
71+
os.path.expanduser("~"), ".config"
72+
)
73+
return pathlib.Path(base) / "MethodenAnalyser"
74+
75+
76+
def _config_path() -> pathlib.Path:
77+
return _config_dir() / "config.json"
78+
79+
80+
def load_app_config() -> Dict[str, Any]:
81+
"""Liest die persistente App-Konfiguration (leer bei Fehlen/Korruption)."""
82+
try:
83+
with open(_config_path(), "r", encoding="utf-8") as handle:
84+
data = json.load(handle)
85+
return data if isinstance(data, dict) else {}
86+
except (OSError, ValueError):
87+
return {}
88+
89+
90+
def save_app_config(config: Dict[str, Any]) -> bool:
91+
"""Speichert die App-Konfiguration; True bei Erfolg."""
92+
try:
93+
_config_dir().mkdir(parents=True, exist_ok=True)
94+
with open(_config_path(), "w", encoding="utf-8") as handle:
95+
json.dump(config, handle, indent=2, ensure_ascii=False)
96+
return True
97+
except OSError:
98+
return False
99+
100+
101+
def get_saved_language() -> str:
102+
"""Gespeicherte Sprache oder Default, validiert gegen SUPPORTED_LANGUAGES."""
103+
lang = load_app_config().get("language", DEFAULT_LANGUAGE)
104+
return lang if lang in SUPPORTED_LANGUAGES else DEFAULT_LANGUAGE
105+
106+
107+
def set_saved_language(lang: str) -> bool:
108+
"""Persistiert die gewaehlte Sprache in der App-Konfiguration."""
109+
if lang not in SUPPORTED_LANGUAGES:
110+
return False
111+
config = load_app_config()
112+
config["language"] = lang
113+
return save_app_config(config)
114+
115+
116+
_TRANSLATOR = None
117+
118+
119+
def get_translator():
120+
"""Lazy-Singleton des TranslationSystem, mit gespeicherter Sprache initialisiert."""
121+
global _TRANSLATOR
122+
if _TRANSLATOR is None and TranslationSystem is not None:
123+
_TRANSLATOR = TranslationSystem(get_saved_language())
124+
return _TRANSLATOR
125+
126+
127+
def _t(key: str) -> str:
128+
"""Uebersetzt key in die aktuelle Sprache (Fallback: key selbst)."""
129+
translator = get_translator()
130+
return translator.t(key) if translator is not None else key
131+
132+
46133
# Analyse Konfiguration
47134
SIMILARITY_THRESHOLD = 0.8
48135

@@ -1537,6 +1624,11 @@ def progress_cb(cur, tot, fp):
15371624
messagebox.showerror("Fehler", str(e))
15381625

15391626

1627+
def _build_welcome_text() -> str:
1628+
"""Baut die uebersetzte Willkommensnachricht inkl. eingesetztem Shortcut-Hinweis."""
1629+
return _t("welcome_body").replace("{shortcut}", _get_keyboard_shortcut_hint())
1630+
1631+
15401632
def create_gui() -> None:
15411633
"""Erstellt und startet die GUI-Anwendung."""
15421634
root = tk.Tk()
@@ -1555,20 +1647,13 @@ def create_gui() -> None:
15551647
def show_info_dialog() -> None:
15561648
messagebox.showinfo(
15571649
"Python Code Analyzer",
1558-
f"Python Code Analyzer v{TOOL_VERSION}\n\n"
1559-
"Analysiert Python-Dateien auf:\n"
1560-
"• Fehlende Definitionen\n"
1561-
"• Ungenutzte Definitionen\n"
1562-
"• Ungenutzte Imports\n"
1563-
"• Dynamische Aufrufe\n"
1564-
"• Import-Scope-Probleme\n\n"
1565-
"© 2026 - Optimierte Version"
1650+
_t("info_body").replace("{version}", TOOL_VERSION),
15661651
)
15671652

15681653
# Analyse-Button
15691654
btn = tk.Button(
15701655
button_frame,
1571-
text="📂 Datei analysieren",
1656+
text=_t("btn_analyze_file"),
15721657
command=lambda: run_analysis(output),
15731658
bg="#4CAF50",
15741659
fg="white",
@@ -1582,7 +1667,7 @@ def show_info_dialog() -> None:
15821667
# Info-Button
15831668
info_btn = tk.Button(
15841669
button_frame,
1585-
text="ℹ️ Info",
1670+
text=_t("btn_info"),
15861671
command=show_info_dialog,
15871672
bg="#2196F3",
15881673
fg="white",
@@ -1596,7 +1681,7 @@ def show_info_dialog() -> None:
15961681
# Auto-Fix Button
15971682
fix_btn = tk.Button(
15981683
button_frame,
1599-
text="🔧 Auto-Fix Imports",
1684+
text=_t("btn_autofix"),
16001685
command=lambda: auto_fix_unused_imports(output),
16011686
bg="#FF9800",
16021687
fg="white",
@@ -1610,7 +1695,7 @@ def show_info_dialog() -> None:
16101695
# NEU: Projekt-Analyse Button
16111696
project_btn = tk.Button(
16121697
button_frame,
1613-
text="Projekt analysieren",
1698+
text=_t("btn_analyze_project"),
16141699
command=lambda: run_project_analysis(output),
16151700
bg="#9C27B0",
16161701
fg="white",
@@ -1643,17 +1728,43 @@ def show_info_dialog() -> None:
16431728
output.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
16441729

16451730
# Willkommensnachricht
1646-
welcome_text = (
1647-
"Willkommen beim Python Code Analyzer!\n\n"
1648-
f"{_get_keyboard_shortcut_hint()}\n\n"
1649-
"Klicken Sie auf 'Datei analysieren', um eine Python-Datei zu untersuchen.\n\n"
1650-
"Die Analyse umfasst:\n"
1651-
"• Fehlende und ungenutzte Definitionen\n"
1652-
"• Import-Analyse und -Optimierung\n"
1653-
"• Dynamische Methodenaufrufe\n"
1654-
"• Mögliche Tippfehler\n"
1731+
output.insert(tk.END, _build_welcome_text())
1732+
1733+
# --- Menue "Sprache / Language" (Welle-1 U1: sichtbarer DE/EN-Schalter) ---
1734+
translator = get_translator()
1735+
current_lang = translator.get_language() if translator is not None else get_saved_language()
1736+
lang_var = tk.StringVar(value=current_lang)
1737+
1738+
def apply_language(lang: str) -> None:
1739+
"""Wechselt Sprache, persistiert sie und stellt die Oberflaeche live um."""
1740+
if translator is not None:
1741+
translator.set_language(lang)
1742+
set_saved_language(lang)
1743+
lang_var.set(lang)
1744+
btn.config(text=_t("btn_analyze_file"))
1745+
info_btn.config(text=_t("btn_info"))
1746+
fix_btn.config(text=_t("btn_autofix"))
1747+
project_btn.config(text=_t("btn_analyze_project"))
1748+
shortcut_hint.config(text=_get_keyboard_shortcut_hint())
1749+
# Willkommenstext nur neu rendern, solange keine Analyse-Ausgabe angezeigt wird.
1750+
if output.get("1.0", "1.end").strip() in _WELCOME_HEADS:
1751+
output.delete("1.0", tk.END)
1752+
output.insert(tk.END, _build_welcome_text())
1753+
messagebox.showinfo("Sprache / Language", _t("lang_switched_msg"))
1754+
1755+
menubar = tk.Menu(root)
1756+
lang_menu = tk.Menu(menubar, tearoff=0)
1757+
lang_menu.add_radiobutton(
1758+
label="Deutsch", value="de", variable=lang_var,
1759+
command=lambda: apply_language("de"),
16551760
)
1656-
output.insert(tk.END, welcome_text)
1761+
lang_menu.add_radiobutton(
1762+
label="English", value="en", variable=lang_var,
1763+
command=lambda: apply_language("en"),
1764+
)
1765+
menubar.add_cascade(label=_t("menu_language"), menu=lang_menu)
1766+
root.config(menu=menubar)
1767+
16571768
_register_gui_shortcuts(
16581769
root,
16591770
analyze_file_cb=lambda: run_analysis(output),
@@ -1668,7 +1779,7 @@ def show_info_dialog() -> None:
16681779

16691780
def _get_keyboard_shortcut_hint() -> str:
16701781
"""Liefert den sichtbaren Kurzhinweis für die Hauptaktionen der GUI."""
1671-
return "Tastatur: Alt+D Datei | Alt+P Projekt | Alt+F Auto-Fix | F1 Info"
1782+
return _t("shortcut_hint")
16721783

16731784

16741785
def _register_gui_shortcuts(

locales/translations.json

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,38 @@
11
{
2-
"📂 Datei analysieren": {
2+
"btn_analyze_file": {
33
"de": "📂 Datei analysieren",
44
"en": "📂 Analyze File"
55
},
6-
"🔧 Auto-Fix Imports": {
6+
"btn_info": {
7+
"de": "ℹ️ Info",
8+
"en": "ℹ️ Info"
9+
},
10+
"btn_autofix": {
711
"de": "🔧 Auto-Fix Imports",
812
"en": "🔧 Auto-Fix Imports"
13+
},
14+
"btn_analyze_project": {
15+
"de": "Projekt analysieren",
16+
"en": "Analyze Project"
17+
},
18+
"shortcut_hint": {
19+
"de": "Tastatur: Alt+D Datei | Alt+P Projekt | Alt+F Auto-Fix | F1 Info",
20+
"en": "Keyboard: Alt+D File | Alt+P Project | Alt+F Auto-Fix | F1 Info"
21+
},
22+
"menu_language": {
23+
"de": "Sprache / Language",
24+
"en": "Sprache / Language"
25+
},
26+
"welcome_body": {
27+
"de": "Willkommen beim Python Code Analyzer!\n\n{shortcut}\n\nKlicken Sie auf 'Datei analysieren', um eine Python-Datei zu untersuchen.\n\nDie Analyse umfasst:\n• Fehlende und ungenutzte Definitionen\n• Import-Analyse und -Optimierung\n• Dynamische Methodenaufrufe\n• Mögliche Tippfehler\n",
28+
"en": "Welcome to Python Code Analyzer!\n\n{shortcut}\n\nClick 'Analyze File' to inspect a Python file.\n\nThe analysis covers:\n• Missing and unused definitions\n• Import analysis and optimization\n• Dynamic method calls\n• Possible typos\n"
29+
},
30+
"info_body": {
31+
"de": "Python Code Analyzer v{version}\n\nAnalysiert Python-Dateien auf:\n• Fehlende Definitionen\n• Ungenutzte Definitionen\n• Ungenutzte Imports\n• Dynamische Aufrufe\n• Import-Scope-Probleme\n\n© 2026 - Optimierte Version",
32+
"en": "Python Code Analyzer v{version}\n\nAnalyzes Python files for:\n• Missing definitions\n• Unused definitions\n• Unused imports\n• Dynamic calls\n• Import scope issues\n\n© 2026 - Optimized version"
33+
},
34+
"lang_switched_msg": {
35+
"de": "Sprache auf Deutsch umgestellt.",
36+
"en": "Language switched to English."
937
}
10-
}
38+
}

tests/test_cli.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,12 @@ def test_missing_file_returns_analysis_error(self) -> None:
173173
self.assertIn("[FEHLER]", result.stderr)
174174

175175
def test_info_dialog_version_matches_tool_version(self) -> None:
176-
"""TOOL_VERSION muss mit der Versionsangabe im Info-Dialog übereinstimmen."""
176+
"""Der Info-Dialog muss seine Versionsangabe aus TOOL_VERSION ableiten."""
177177
source = SCRIPT_PATH.read_text(encoding="utf-8")
178178
tree = ast.parse(source)
179179

180180
tool_version = None
181-
info_dialog_version = None
181+
version_from_tool_version = False
182182

183183
for node in ast.walk(tree):
184184
if isinstance(node, ast.Assign):
@@ -187,18 +187,43 @@ def test_info_dialog_version_matches_tool_version(self) -> None:
187187
if isinstance(node.value, ast.Constant):
188188
tool_version = node.value.value
189189

190-
# f-string: f"Python Code Analyzer v{TOOL_VERSION}\n\n"
190+
# Variante A (f-string): f"Python Code Analyzer v{TOOL_VERSION}\n\n"
191191
if isinstance(node, ast.JoinedStr):
192192
for part in node.values:
193193
if isinstance(part, ast.FormattedValue):
194194
if isinstance(part.value, ast.Name) and part.value.id == "TOOL_VERSION":
195-
info_dialog_version = "TOOL_VERSION_ref"
195+
version_from_tool_version = True
196+
197+
# Variante B (i18n): _t("info_body").replace("{version}", TOOL_VERSION)
198+
if (
199+
isinstance(node, ast.Call)
200+
and isinstance(node.func, ast.Attribute)
201+
and node.func.attr == "replace"
202+
and any(
203+
isinstance(arg, ast.Name) and arg.id == "TOOL_VERSION"
204+
for arg in node.args
205+
)
206+
):
207+
version_from_tool_version = True
196208

197209
self.assertIsNotNone(tool_version, "TOOL_VERSION nicht gefunden")
198-
self.assertIsNotNone(
199-
info_dialog_version,
200-
"Info-Dialog referenziert TOOL_VERSION nicht als f-string — Versions-Mismatch möglich",
210+
self.assertTrue(
211+
version_from_tool_version,
212+
"Info-Dialog leitet die Version nicht aus TOOL_VERSION ab — Versions-Mismatch möglich",
213+
)
214+
215+
# i18n-Absicherung: die info_body-Uebersetzungen muessen den {version}-Platzhalter
216+
# tragen, sonst liefe die TOOL_VERSION-Ersetzung ins Leere.
217+
translations = json.loads(
218+
(PROJECT_ROOT / "locales" / "translations.json").read_text(encoding="utf-8")
201219
)
220+
info_body = translations.get("info_body", {})
221+
for lang in ("de", "en"):
222+
self.assertIn(
223+
"{version}",
224+
info_body.get(lang, ""),
225+
f"info_body[{lang}] muss den Platzhalter {{version}} enthalten",
226+
)
202227

203228

204229
class RegressionTests(unittest.TestCase):

0 commit comments

Comments
 (0)