From ccb2334f7f7242d4f6a3a552570f48ac59098122 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Thu, 1 Oct 2026 04:14:40 +0200 Subject: [PATCH 1/2] fix(i18n): persist UI language and translate missing help text --- CHANGELOG.md | 6 + locales/translations.json | 698 +++++++++++++++++++++- manage_translations.py | 17 +- src/gui/batch_rename_dialog.py | 30 +- src/gui/checksum_dialog.py | 9 +- src/gui/diff_dialog.py | 10 +- src/gui/main_window.py | 9 +- src/gui/preview/preview_panel.py | 9 +- src/gui/settings_dialog.py | 47 +- src/gui/sidebar/advanced_search_dialog.py | 29 +- src/main.py | 15 +- src/modules/launcher/apps_panel.py | 13 +- src/modules/prompts/prompts_panel.py | 11 +- tests/test_tooltip_language.py | 85 +++ 14 files changed, 907 insertions(+), 81 deletions(-) create mode 100644 tests/test_tooltip_language.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f9b5d68..dcd186d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/). ## [Unreleased] +### Übersetzte Hilfetexte und gespeicherte Sprache (2026-10-01) +- 86 bislang fehlende Hilfetexte, Beschriftungen und Fehlermeldungen im Katalog ergänzt; Hilfetexte über die Übersetzungsfunktion angebunden (DE, EN, ES, ZH, JA, RU). +- Die Sprachauswahl erklärt in allen sechs Sprachen, dass Sprachänderungen beim nächsten Start übernommen werden. +- Sprachauswahl wird geladen und gespeichert; beim nächsten Start verwenden die angebundenen Texte und Qt-Standarddialoge die gespeicherte Sprache. +- Übersetzungsprüfung erkennt auch bereits angebundene `t(...)`-Aufrufe. Weitere fest deutsche Bedienelemente bleiben separat offen. + ### Repository-Lebenszyklus-Härtung & CI/CD-Parität (2026-09-30, Pfad A) - **CI/CD Lifecycle Workflows & Label-Governance**: - `.github/workflows/auto-assign.yml`: Automatisches Zuweisen von Pull Requests an Maintainer via `actions/github-script@v7`, `timeout-minutes: 5`, least-privilege permissions (`issues: write`, `pull-requests: write`) und Concurrency `cancel-in-progress: true`. diff --git a/locales/translations.json b/locales/translations.json index 64f25eb..7807687 100644 --- a/locales/translations.json +++ b/locales/translations.json @@ -1862,5 +1862,701 @@ "zh": "浏览...", "ja": "参照...", "ru": "Обзор..." + }, + "Alle berechneten Prüfsummen (MD5, SHA-1, SHA-256, SHA-512) in die Zwischenablage kopieren": { + "de": "Alle berechneten Prüfsummen (MD5, SHA-1, SHA-256, SHA-512) in die Zwischenablage kopieren", + "en": "Copy all calculated checksums (MD5, SHA-1, SHA-256, SHA-512) to the clipboard", + "es": "Copiar todas las sumas de comprobación calculadas (MD5, SHA-1, SHA-256, SHA-512) al portapapeles", + "zh": "将所有计算出的校验和(MD5、SHA-1、SHA-256、SHA-512)复制到剪贴板", + "ja": "計算したすべてのチェックサム(MD5、SHA-1、SHA-256、SHA-512)をクリップボードにコピー", + "ru": "Копировать все вычисленные контрольные суммы (MD5, SHA-1, SHA-256, SHA-512) в буфер обмена" + }, + "Apps durchsuchen": { + "de": "Apps durchsuchen", + "en": "Search apps", + "es": "Buscar aplicaciones", + "zh": "搜索应用", + "ja": "アプリを検索", + "ru": "Поиск приложений" + }, + "Arbeitsblatt der Excel-Arbeitsmappe auswählen": { + "de": "Arbeitsblatt der Excel-Arbeitsmappe auswählen", + "en": "Select a worksheet in the Excel workbook", + "es": "Seleccionar una hoja del libro de Excel", + "zh": "选择 Excel 工作簿中的工作表", + "ja": "Excel ブックのワークシートを選択", + "ru": "Выбрать лист книги Excel" + }, + "Auf bestimmte Dateitypen einschränken": { + "de": "Auf bestimmte Dateitypen einschränken", + "en": "Restrict to specific file types", + "es": "Limitar a tipos de archivo específicos", + "zh": "限定特定文件类型", + "ja": "特定のファイル形式に限定", + "ru": "Ограничить определёнными типами файлов" + }, + "Basisschriftgröße in Punkten": { + "de": "Basisschriftgröße in Punkten", + "en": "Base font size in points", + "es": "Tamaño de fuente base en puntos", + "zh": "基本字体大小(磅)", + "ja": "基本フォントサイズ(ポイント)", + "ru": "Основной размер шрифта в пунктах" + }, + "Dateiauswahldialog öffnen": { + "de": "Dateiauswahldialog öffnen", + "en": "Open the file selection dialog", + "es": "Abrir el diálogo de selección de archivos", + "zh": "打开文件选择对话框", + "ja": "ファイル選択ダイアログを開く", + "ru": "Открыть диалог выбора файла" + }, + "Dateidialog zur Auswahl des Startordners öffnen": { + "de": "Dateidialog zur Auswahl des Startordners öffnen", + "en": "Open the dialog to select the startup folder", + "es": "Abrir el diálogo para seleccionar la carpeta inicial", + "zh": "打开启动文件夹选择对话框", + "ja": "起動フォルダーの選択ダイアログを開く", + "ru": "Открыть диалог выбора начальной папки" + }, + "Dateien fortlaufend nummerieren": { + "de": "Dateien fortlaufend nummerieren", + "en": "Number files sequentially", + "es": "Numerar los archivos consecutivamente", + "zh": "为文件连续编号", + "ja": "ファイルに連番を付ける", + "ru": "Последовательно нумеровать файлы" + }, + "Dateien über dieser Größe werden nicht indiziert": { + "de": "Dateien über dieser Größe werden nicht indiziert", + "en": "Files larger than this size are not indexed", + "es": "Los archivos mayores que este tamaño no se indexan", + "zh": "不索引超过此大小的文件", + "ja": "このサイズを超えるファイルはインデックスに登録しません", + "ru": "Файлы больше этого размера не индексируются" + }, + "Dateiendungen kommagetrennt eingeben": { + "de": "Dateiendungen kommagetrennt eingeben", + "en": "Enter file extensions separated by commas", + "es": "Introducir extensiones de archivo separadas por comas", + "zh": "输入文件扩展名,以逗号分隔", + "ja": "ファイル拡張子をカンマ区切りで入力", + "ru": "Введите расширения файлов через запятую" + }, + "Dateinamen durchsuchen": { + "de": "Dateinamen durchsuchen", + "en": "Search file names", + "es": "Buscar nombres de archivo", + "zh": "搜索文件名", + "ja": "ファイル名を検索", + "ru": "Поиск по именам файлов" + }, + "Dateivergleich starten (Strg+Enter)": { + "de": "Dateivergleich starten (Strg+Enter)", + "en": "Start file comparison (Ctrl+Enter)", + "es": "Iniciar comparación de archivos (Ctrl+Enter)", + "zh": "开始比较文件(Ctrl+Enter)", + "ja": "ファイル比較を開始(Ctrl+Enter)", + "ru": "Начать сравнение файлов (Ctrl+Enter)" + }, + "Ergebnistabelle (Doppelklick oder Enter zum Öffnen)": { + "de": "Ergebnistabelle (Doppelklick oder Enter zum Öffnen)", + "en": "Results table (double-click or press Enter to open)", + "es": "Tabla de resultados (doble clic o Intro para abrir)", + "zh": "结果表(双击或按 Enter 打开)", + "ja": "結果一覧(ダブルクリックまたは Enter で開く)", + "ru": "Таблица результатов (двойной щелчок или Enter для открытия)" + }, + "Erste Seite von PDF-Dokumenten im Vorschaufenster darstellen": { + "de": "Erste Seite von PDF-Dokumenten im Vorschaufenster darstellen", + "en": "Show the first page of PDF documents in the preview panel", + "es": "Mostrar la primera página de los documentos PDF en la vista previa", + "zh": "在预览面板中显示 PDF 文档的第一页", + "ja": "プレビューパネルに PDF の最初のページを表示", + "ru": "Показывать первую страницу PDF в панели предпросмотра" + }, + "Exakte Schreibweise beim Suchen berücksichtigen": { + "de": "Exakte Schreibweise beim Suchen berücksichtigen", + "en": "Match letter case when searching", + "es": "Distinguir mayúsculas y minúsculas en la búsqueda", + "zh": "搜索时区分大小写", + "ja": "検索で大文字と小文字を区別", + "ru": "Учитывать регистр при поиске" + }, + "Farbschema der Benutzeroberfläche": { + "de": "Farbschema der Benutzeroberfläche", + "en": "Interface color scheme", + "es": "Esquema de colores de la interfaz", + "zh": "界面配色方案", + "ja": "インターフェースの配色", + "ru": "Цветовая схема интерфейса" + }, + "Fehler: Pfad ist ein Verzeichnis, keine Datei.": { + "de": "Fehler: Pfad ist ein Verzeichnis, keine Datei.", + "en": "Error: The path is a directory, not a file.", + "es": "Error: La ruta es un directorio, no un archivo.", + "zh": "错误:该路径是目录,不是文件。", + "ja": "エラー:パスはファイルではなくディレクトリです。", + "ru": "Ошибка: путь указывает на каталог, а не на файл." + }, + "Fenstergröße und Position beim Beenden speichern": { + "de": "Fenstergröße und Position beim Beenden speichern", + "en": "Save window size and position on exit", + "es": "Guardar tamaño y posición de la ventana al salir", + "zh": "退出时保存窗口大小和位置", + "ja": "終了時にウィンドウのサイズと位置を保存", + "ru": "Сохранять размер и положение окна при выходе" + }, + "Freitextnotizen zur Datei eingeben": { + "de": "Freitextnotizen zur Datei eingeben", + "en": "Enter notes for the file", + "es": "Introducir notas sobre el archivo", + "zh": "输入文件备注", + "ja": "ファイルのメモを入力", + "ru": "Введите заметки к файлу" + }, + "Führt die geplante Umbenennung für alle Dateien aus (Enter)": { + "de": "Führt die geplante Umbenennung für alle Dateien aus (Enter)", + "en": "Apply the planned rename to all files (Enter)", + "es": "Aplicar el cambio de nombre previsto a todos los archivos (Intro)", + "zh": "对所有文件执行计划的重命名(Enter)", + "ja": "すべてのファイルに予定した名前変更を実行(Enter)", + "ru": "Выполнить запланированное переименование всех файлов (Enter)" + }, + "Geben Sie hier einen Hash ein, um die Integrität automatisch zu überprüfen": { + "de": "Geben Sie hier einen Hash ein, um die Integrität automatisch zu überprüfen", + "en": "Enter a hash here to automatically verify integrity", + "es": "Introduzca un hash para comprobar automáticamente la integridad", + "zh": "在此输入哈希值以自动验证完整性", + "ja": "ハッシュ値を入力して整合性を自動検証", + "ru": "Введите хеш для автоматической проверки целостности" + }, + "Groß- und Kleinschreibung des Namens anpassen": { + "de": "Groß- und Kleinschreibung des Namens anpassen", + "en": "Change the letter case of the name", + "es": "Cambiar las mayúsculas y minúsculas del nombre", + "zh": "更改名称的大小写", + "ja": "名前の大文字と小文字を変更", + "ru": "Изменить регистр букв в имени" + }, + "Groß-/Kleinschreibung bei der Mustersuche berücksichtigen": { + "de": "Groß-/Kleinschreibung bei der Mustersuche berücksichtigen", + "en": "Match letter case in pattern searches", + "es": "Distinguir mayúsculas y minúsculas al buscar patrones", + "zh": "模式搜索时区分大小写", + "ja": "パターン検索で大文字と小文字を区別", + "ru": "Учитывать регистр при поиске по шаблону" + }, + "Größe der Dateisymbole in Pixeln": { + "de": "Größe der Dateisymbole in Pixeln", + "en": "File icon size in pixels", + "es": "Tamaño de los iconos de archivo en píxeles", + "zh": "文件图标大小(像素)", + "ja": "ファイルアイコンのサイズ(ピクセル)", + "ru": "Размер значков файлов в пикселях" + }, + "Indexprüfung direkt beim Start von ExplorerPro durchführen": { + "de": "Indexprüfung direkt beim Start von ExplorerPro durchführen", + "en": "Check the index when ExplorerPro starts", + "es": "Comprobar el índice al iniciar ExplorerPro", + "zh": "启动 ExplorerPro 时检查索引", + "ja": "ExplorerPro の起動時にインデックスを確認", + "ru": "Проверять индекс при запуске ExplorerPro" + }, + "Inkrement pro Datei": { + "de": "Inkrement pro Datei", + "en": "Increment per file", + "es": "Incremento por archivo", + "zh": "每个文件的编号增量", + "ja": "ファイルごとの増分", + "ru": "Шаг нумерации для каждого файла" + }, + "Kategorie auswählen oder neu eingeben": { + "de": "Kategorie auswählen oder neu eingeben", + "en": "Select a category or enter a new one", + "es": "Seleccionar una categoría o introducir una nueva", + "zh": "选择类别或输入新类别", + "ja": "カテゴリを選択または新規入力", + "ru": "Выберите категорию или введите новую" + }, + "Kategorie für die Einordnung des Prompts": { + "de": "Kategorie für die Einordnung des Prompts", + "en": "Category for organizing the prompt", + "es": "Categoría para clasificar el prompt", + "zh": "用于归类提示词的类别", + "ja": "プロンプトを分類するカテゴリ", + "ru": "Категория для классификации промпта" + }, + "Kommagetrennte Tags filtern": { + "de": "Kommagetrennte Tags filtern", + "en": "Filter by comma-separated tags", + "es": "Filtrar por etiquetas separadas por comas", + "zh": "按逗号分隔的标签筛选", + "ja": "カンマ区切りのタグで絞り込む", + "ru": "Фильтровать по тегам, разделённым запятыми" + }, + "Kommagetrennte Tags für die Datei eingeben": { + "de": "Kommagetrennte Tags für die Datei eingeben", + "en": "Enter file tags separated by commas", + "es": "Introducir etiquetas del archivo separadas por comas", + "zh": "输入文件标签,以逗号分隔", + "ja": "ファイルのタグをカンマ区切りで入力", + "ru": "Введите теги файла через запятую" + }, + "Kopieren hochsensibler Daten wie Passwörter und API-Keys blockieren": { + "de": "Kopieren hochsensibler Daten wie Passwörter und API-Keys blockieren", + "en": "Block copying highly sensitive data such as passwords and API keys", + "es": "Bloquear la copia de datos muy sensibles, como contraseñas y claves API", + "zh": "阻止复制密码和 API 密钥等高度敏感的数据", + "ja": "パスワードや API キーなど機密性の高いデータのコピーをブロック", + "ru": "Блокировать копирование особо конфиденциальных данных, например паролей и ключей API" + }, + "Macht die vorherige Umbenennungs-Aktion rückgängig": { + "de": "Macht die vorherige Umbenennungs-Aktion rückgängig", + "en": "Undo the previous rename operation", + "es": "Deshacer el cambio de nombre anterior", + "zh": "撤销上一次重命名操作", + "ja": "直前の名前変更を元に戻す", + "ru": "Отменить предыдущее переименование" + }, + "Maximale Dateigröße für generierte Vorschauen": { + "de": "Maximale Dateigröße für generierte Vorschauen", + "en": "Maximum file size for previews", + "es": "Tamaño máximo de archivo para vistas previas", + "zh": "生成预览的最大文件大小", + "ja": "プレビューを生成する最大ファイルサイズ", + "ru": "Максимальный размер файла для предпросмотра" + }, + "Miniaturansichten für Grafikdateien rendern": { + "de": "Miniaturansichten für Grafikdateien rendern", + "en": "Render thumbnails for image files", + "es": "Generar miniaturas de archivos de imagen", + "zh": "生成图像文件的缩略图", + "ja": "画像ファイルのサムネイルを生成", + "ru": "Создавать миниатюры изображений" + }, + "Neuer Text für gefundene Treffer": { + "de": "Neuer Text für gefundene Treffer", + "en": "Replacement text for matches", + "es": "Texto de sustitución para las coincidencias", + "zh": "用于替换匹配项的新文本", + "ja": "一致した部分の置換テキスト", + "ru": "Текст для замены найденных совпадений" + }, + "Nur eigenständige Wörter als Treffer werten": { + "de": "Nur eigenständige Wörter als Treffer werten", + "en": "Match whole words only", + "es": "Buscar solo palabras completas", + "zh": "仅匹配完整单词", + "ja": "単語全体が一致するものだけを検索", + "ru": "Искать только целые слова" + }, + "Pfad zur ausführbaren Datei": { + "de": "Pfad zur ausführbaren Datei", + "en": "Path to the executable file", + "es": "Ruta del archivo ejecutable", + "zh": "可执行文件的路径", + "ja": "実行ファイルのパス", + "ru": "Путь к исполняемому файлу" + }, + "Position der Nummerierung im Dateinamen": { + "de": "Position der Nummerierung im Dateinamen", + "en": "Position of numbering in the file name", + "es": "Posición de la numeración en el nombre del archivo", + "zh": "文件名中编号的位置", + "ja": "ファイル名での番号の位置", + "ru": "Положение номера в имени файла" + }, + "Programmsprache für Menüs und Dialoge": { + "de": "Programmsprache für Menüs und Dialoge", + "en": "Language for menus and dialogs", + "es": "Idioma de los menús y diálogos", + "zh": "菜单和对话框的语言", + "ja": "メニューとダイアログの言語", + "ru": "Язык меню и диалогов" + }, + "Präfix vor den Dateinamen stellen": { + "de": "Präfix vor den Dateinamen stellen", + "en": "Add a prefix before the file name", + "es": "Añadir un prefijo al nombre del archivo", + "zh": "在文件名前添加前缀", + "ja": "ファイル名の前に接頭辞を追加", + "ru": "Добавить префикс перед именем файла" + }, + "Schlagwörter zur Filterung, kommagetrennt": { + "de": "Schlagwörter zur Filterung, kommagetrennt", + "en": "Filter keywords, separated by commas", + "es": "Palabras clave para filtrar, separadas por comas", + "zh": "筛选关键词,以逗号分隔", + "ja": "絞り込み用のキーワードをカンマ区切りで入力", + "ru": "Ключевые слова для фильтрации через запятую" + }, + "Schließt das Dialogfenster (Esc)": { + "de": "Schließt das Dialogfenster (Esc)", + "en": "Close the dialog (Esc)", + "es": "Cerrar el diálogo (Esc)", + "zh": "关闭对话框(Esc)", + "ja": "ダイアログを閉じる(Esc)", + "ru": "Закрыть диалог (Esc)" + }, + "Schließt das Mehrfachumbenennungs-Fenster (Esc)": { + "de": "Schließt das Mehrfachumbenennungs-Fenster (Esc)", + "en": "Close the batch rename window (Esc)", + "es": "Cerrar la ventana de cambio de nombre por lotes (Esc)", + "zh": "关闭批量重命名窗口(Esc)", + "ja": "一括名前変更ウィンドウを閉じる(Esc)", + "ru": "Закрыть окно группового переименования (Esc)" + }, + "Schließt das Prüfsummen-Fenster (Esc)": { + "de": "Schließt das Prüfsummen-Fenster (Esc)", + "en": "Close the checksum window (Esc)", + "es": "Cerrar la ventana de sumas de comprobación (Esc)", + "zh": "关闭校验和窗口(Esc)", + "ja": "チェックサムウィンドウを閉じる(Esc)", + "ru": "Закрыть окно контрольных сумм (Esc)" + }, + "Schließt den Vergleichs-Dialog (Esc)": { + "de": "Schließt den Vergleichs-Dialog (Esc)", + "en": "Close the comparison dialog (Esc)", + "es": "Cerrar el diálogo de comparación (Esc)", + "zh": "关闭比较对话框(Esc)", + "ja": "比較ダイアログを閉じる(Esc)", + "ru": "Закрыть диалог сравнения (Esc)" + }, + "Setzt alle Kriterien und Filter auf Standardwerte zurück": { + "de": "Setzt alle Kriterien und Filter auf Standardwerte zurück", + "en": "Reset all criteria and filters to defaults", + "es": "Restablecer todos los criterios y filtros predeterminados", + "zh": "将所有条件和筛选器重置为默认值", + "ja": "すべての条件とフィルターを既定値に戻す", + "ru": "Сбросить все критерии и фильтры к значениям по умолчанию" + }, + "Sicherheitsabfrage vor dem unwiderruflichen Löschen einblenden": { + "de": "Sicherheitsabfrage vor dem unwiderruflichen Löschen einblenden", + "en": "Ask for confirmation before permanent deletion", + "es": "Pedir confirmación antes de eliminar permanentemente", + "zh": "永久删除前请求确认", + "ja": "完全削除の前に確認する", + "ru": "Запрашивать подтверждение перед безвозвратным удалением" + }, + "Speichert die Anwendungseinstellungen (Enter)": { + "de": "Speichert die Anwendungseinstellungen (Enter)", + "en": "Save application settings (Enter)", + "es": "Guardar ajustes de la aplicación (Intro)", + "zh": "保存应用设置(Enter)", + "ja": "アプリの設定を保存(Enter)", + "ru": "Сохранить настройки приложения (Enter)" + }, + "Speichert die Datenschutz-Einstellungen (Enter)": { + "de": "Speichert die Datenschutz-Einstellungen (Enter)", + "en": "Save privacy settings (Enter)", + "es": "Guardar ajustes de privacidad (Intro)", + "zh": "保存隐私设置(Enter)", + "ja": "プライバシー設定を保存(Enter)", + "ru": "Сохранить настройки конфиденциальности (Enter)" + }, + "Standardverzeichnis beim Programmstart": { + "de": "Standardverzeichnis beim Programmstart", + "en": "Default folder on startup", + "es": "Carpeta predeterminada al iniciar", + "zh": "启动时的默认文件夹", + "ja": "起動時の既定フォルダー", + "ru": "Папка по умолчанию при запуске" + }, + "Startet die erweiterte Suche (Enter)": { + "de": "Startet die erweiterte Suche (Enter)", + "en": "Start advanced search (Enter)", + "es": "Iniciar búsqueda avanzada (Intro)", + "zh": "开始高级搜索(Enter)", + "ja": "詳細検索を開始(Enter)", + "ru": "Начать расширенный поиск (Enter)" + }, + "Startnummer der Zählung": { + "de": "Startnummer der Zählung", + "en": "Starting number", + "es": "Número inicial", + "zh": "起始编号", + "ja": "開始番号", + "ru": "Начальный номер" + }, + "Suchbegriff als regulären Ausdruck auswerten": { + "de": "Suchbegriff als regulären Ausdruck auswerten", + "en": "Treat the search term as a regular expression", + "es": "Interpretar el término de búsqueda como expresión regular", + "zh": "将搜索词作为正则表达式", + "ja": "検索語を正規表現として扱う", + "ru": "Интерпретировать поисковый запрос как регулярное выражение" + }, + "Suchbegriff eingeben (Enter startet die Suche)": { + "de": "Suchbegriff eingeben (Enter startet die Suche)", + "en": "Enter a search term (Enter starts the search)", + "es": "Introducir un término de búsqueda (Intro inicia la búsqueda)", + "zh": "输入搜索词(Enter 开始搜索)", + "ja": "検索語を入力(Enter で検索開始)", + "ru": "Введите поисковый запрос (Enter запускает поиск)" + }, + "Suffix an den Dateinamen anhängen": { + "de": "Suffix an den Dateinamen anhängen", + "en": "Append a suffix to the file name", + "es": "Añadir un sufijo al nombre del archivo", + "zh": "在文件名后添加后缀", + "ja": "ファイル名の後に接尾辞を追加", + "ru": "Добавить суффикс к имени файла" + }, + "Syntaxhervorhebung für Quellcode und Textdateien nutzen": { + "de": "Syntaxhervorhebung für Quellcode und Textdateien nutzen", + "en": "Use syntax highlighting for source code and text files", + "es": "Usar resaltado de sintaxis en archivos de código y texto", + "zh": "对源代码和文本文件启用语法高亮", + "ja": "ソースコードとテキストファイルに構文強調を使用", + "ru": "Использовать подсветку синтаксиса для исходного кода и текстовых файлов" + }, + "Versteckte Dateien und Systemordner im Dateibrowser anzeigen": { + "de": "Versteckte Dateien und Systemordner im Dateibrowser anzeigen", + "en": "Show hidden files and system folders in the file browser", + "es": "Mostrar archivos ocultos y carpetas del sistema en el explorador", + "zh": "在文件浏览器中显示隐藏文件和系统文件夹", + "ja": "ファイルブラウザーに隠しファイルとシステムフォルダーを表示", + "ru": "Показывать скрытые файлы и системные папки в обозревателе файлов" + }, + "Verwirft Änderungen (Esc)": { + "de": "Verwirft Änderungen (Esc)", + "en": "Discard changes (Esc)", + "es": "Descartar cambios (Esc)", + "zh": "放弃更改(Esc)", + "ja": "変更を破棄(Esc)", + "ru": "Отменить изменения (Esc)" + }, + "Verzeichnisse im Hintergrund automatisch für die Schnellsuche indizieren": { + "de": "Verzeichnisse im Hintergrund automatisch für die Schnellsuche indizieren", + "en": "Automatically index folders in the background for quick search", + "es": "Indexar carpetas automáticamente en segundo plano para la búsqueda rápida", + "zh": "在后台自动索引文件夹以便快速搜索", + "ja": "クイック検索用にフォルダーをバックグラウンドで自動登録", + "ru": "Автоматически индексировать папки в фоне для быстрого поиска" + }, + "Vollständigen Dateipfad durchsuchen": { + "de": "Vollständigen Dateipfad durchsuchen", + "en": "Search the full file path", + "es": "Buscar en la ruta completa del archivo", + "zh": "搜索完整文件路径", + "ja": "ファイルのフルパスを検索", + "ru": "Поиск по полному пути файла" + }, + "Vollständiger Prompt-Text": { + "de": "Vollständiger Prompt-Text", + "en": "Full prompt text", + "es": "Texto completo del prompt", + "zh": "完整提示词文本", + "ja": "プロンプトの全文", + "ru": "Полный текст промпта" + }, + "Volltextinhalte durchsuchen": { + "de": "Volltextinhalte durchsuchen", + "en": "Search full-text content", + "es": "Buscar en el contenido completo", + "zh": "搜索全文内容", + "ja": "全文を検索", + "ru": "Поиск по полному тексту" + }, + "Vorschau des ausgewählten Prompts": { + "de": "Vorschau des ausgewählten Prompts", + "en": "Preview of the selected prompt", + "es": "Vista previa del prompt seleccionado", + "zh": "所选提示词的预览", + "ja": "選択したプロンプトのプレビュー", + "ru": "Предпросмотр выбранного промпта" + }, + "Vorschauleiste auf der rechten Seite einblenden": { + "de": "Vorschauleiste auf der rechten Seite einblenden", + "en": "Show the preview panel on the right", + "es": "Mostrar el panel de vista previa a la derecha", + "zh": "显示右侧预览面板", + "ja": "右側にプレビューパネルを表示", + "ru": "Показывать панель предпросмотра справа" + }, + "Vorschautabelle aller zu ändernden Dateinamen": { + "de": "Vorschautabelle aller zu ändernden Dateinamen", + "en": "Preview table of all file names to be changed", + "es": "Tabla de vista previa de los nombres de archivo que se cambiarán", + "zh": "显示所有待更改文件名的预览表", + "ja": "変更するすべてのファイル名のプレビュー一覧", + "ru": "Таблица предпросмотра всех изменяемых имён файлов" + }, + "Zeilenweiser Vergleich der beiden Dateien": { + "de": "Zeilenweiser Vergleich der beiden Dateien", + "en": "Line-by-line comparison of the two files", + "es": "Comparación línea por línea de ambos archivos", + "zh": "逐行比较两个文件", + "ja": "2 つのファイルを行ごとに比較", + "ru": "Построчное сравнение двух файлов" + }, + "Zeitraum der letzten Änderung auswählen": { + "de": "Zeitraum der letzten Änderung auswählen", + "en": "Select the last modification date range", + "es": "Seleccionar el intervalo de fechas de última modificación", + "zh": "选择最后修改时间的范围", + "ja": "最終更新日の範囲を選択", + "ru": "Выбрать диапазон дат последнего изменения" + }, + "Zu ersetzender Teilstring im Dateinamen": { + "de": "Zu ersetzender Teilstring im Dateinamen", + "en": "Substring to replace in the file name", + "es": "Texto que se sustituirá en el nombre del archivo", + "zh": "文件名中要替换的子字符串", + "ja": "ファイル名で置換する文字列", + "ru": "Подстрока в имени файла для замены" + }, + "Zwischenablage kontinuierlich auf schutzwürdige Muster überwachen": { + "de": "Zwischenablage kontinuierlich auf schutzwürdige Muster überwachen", + "en": "Continuously monitor the clipboard for sensitive patterns", + "es": "Supervisar continuamente el portapapeles en busca de patrones sensibles", + "zh": "持续监控剪贴板中的敏感数据模式", + "ja": "クリップボード内の機密データのパターンを継続監視", + "ru": "Постоянно отслеживать в буфере обмена шаблоны конфиденциальных данных" + }, + "Öffnet den Bearbeitungsdialog für den Prompt": { + "de": "Öffnet den Bearbeitungsdialog für den Prompt", + "en": "Open the prompt editor dialog", + "es": "Abrir el diálogo de edición del prompt", + "zh": "打开提示词编辑对话框", + "ja": "プロンプトの編集ダイアログを開く", + "ru": "Открыть диалог редактирования промпта" + }, + "Öffnet den Ordner der ausgewählten Datei": { + "de": "Öffnet den Ordner der ausgewählten Datei", + "en": "Open the folder of the selected file", + "es": "Abrir la carpeta del archivo seleccionado", + "zh": "打开所选文件所在的文件夹", + "ja": "選択したファイルのフォルダーを開く", + "ru": "Открыть папку выбранного файла" + }, + "Öffnet die Dateiauswahl für Datei 1 (Basis)": { + "de": "Öffnet die Dateiauswahl für Datei 1 (Basis)", + "en": "Open file selection for file 1 (base)", + "es": "Abrir la selección del archivo 1 (base)", + "zh": "打开文件 1(基准)的选择对话框", + "ja": "ファイル 1(基準)の選択ダイアログを開く", + "ru": "Открыть выбор файла 1 (базового)" + }, + "Öffnet die Dateiauswahl für Datei 2 (Vergleich)": { + "de": "Öffnet die Dateiauswahl für Datei 2 (Vergleich)", + "en": "Open file selection for file 2 (comparison)", + "es": "Abrir la selección del archivo 2 (comparación)", + "zh": "打开文件 2(比较)的选择对话框", + "ja": "ファイル 2(比較)の選択ダイアログを開く", + "ru": "Открыть выбор файла 2 (сравниваемого)" + }, + "Öffnet die Excel-Datei im Standardprogramm": { + "de": "Öffnet die Excel-Datei im Standardprogramm", + "en": "Open the Excel file in the default application", + "es": "Abrir el archivo Excel en la aplicación predeterminada", + "zh": "在默认应用中打开 Excel 文件", + "ja": "既定のアプリで Excel ファイルを開く", + "ru": "Открыть файл Excel в приложении по умолчанию" + }, + "Öffnet die ausgewählte Datei": { + "de": "Öffnet die ausgewählte Datei", + "en": "Open the selected file", + "es": "Abrir el archivo seleccionado", + "zh": "打开所选文件", + "ja": "選択したファイルを開く", + "ru": "Открыть выбранный файл" + }, + "Datei nicht gefunden": { + "de": "Datei nicht gefunden", + "en": "File not found", + "es": "Archivo no encontrado", + "zh": "找不到文件", + "ja": "ファイルが見つかりません", + "ru": "Файл не найден" + }, + "Dateien umbenannt, aber Fehler aufgetreten:": { + "de": "Dateien umbenannt, aber Fehler aufgetreten:", + "en": "Files renamed, but errors occurred:", + "es": "Archivos renombrados, pero se produjeron errores:", + "zh": "文件已重命名,但发生了错误:", + "ja": "ファイルの名前を変更しましたが、エラーが発生しました:", + "ru": "Файлы переименованы, но возникли ошибки:" + }, + "Dateien zurückgesetzt, Fehler:": { + "de": "Dateien zurückgesetzt, Fehler:", + "en": "Files restored; errors:", + "es": "Archivos restaurados; errores:", + "zh": "文件已还原;错误:", + "ja": "ファイルを復元しました。エラー:", + "ru": "Файлы восстановлены; ошибки:" + }, + "Fehler": { + "de": "Fehler", + "en": "Error", + "es": "Error", + "zh": "错误", + "ja": "エラー", + "ru": "Ошибка" + }, + "Pfad ist keine Datei": { + "de": "Pfad ist keine Datei", + "en": "The path is not a file", + "es": "La ruta no es un archivo", + "zh": "该路径不是文件", + "ja": "パスはファイルではありません", + "ru": "Путь не указывает на файл" + }, + "Ungültiger Pfad": { + "de": "Ungültiger Pfad", + "en": "Invalid path", + "es": "Ruta no válida", + "zh": "无效路径", + "ja": "無効なパス", + "ru": "Недопустимый путь" + }, + "Sprachänderungen werden beim nächsten Start übernommen.": { + "de": "Sprachänderungen werden beim nächsten Start übernommen.", + "en": "Language changes take effect on the next start.", + "es": "Los cambios de idioma se aplican en el próximo inicio.", + "zh": "语言更改将在下次启动时生效。", + "ja": "言語の変更は次回起動時に適用されます。", + "ru": "Изменение языка вступит в силу при следующем запуске." + }, + "Umbenennen": { + "de": "Umbenennen", + "en": "Rename", + "es": "Renombrar", + "zh": "重命名", + "ja": "名前を変更", + "ru": "Переименовать" + }, + "Status": { + "de": "Status", + "en": "Status", + "es": "Estado", + "zh": "状态", + "ja": "状態", + "ru": "Статус" + }, + "Erfolg": { + "de": "Erfolg", + "en": "Success", + "es": "Correcto", + "zh": "成功", + "ja": "成功", + "ru": "Успех" + }, + "Inhalt": { + "de": "Inhalt", + "en": "Content", + "es": "Contenido", + "zh": "内容", + "ja": "内容", + "ru": "Содержимое" + }, + "Diff kopieren": { + "de": "Diff kopieren", + "en": "Copy diff", + "es": "Copiar diferencias", + "zh": "复制差异", + "ja": "差分をコピー", + "ru": "Копировать различия" } -} \ No newline at end of file +} diff --git a/manage_translations.py b/manage_translations.py index 93847c9..49fe395 100644 --- a/manage_translations.py +++ b/manage_translations.py @@ -9,6 +9,7 @@ """ import argparse +import ast import json import os import re @@ -76,6 +77,20 @@ def find_german_strings(source_dir: str) -> Set[str]: for match in pattern.findall(content): if is_german(match): german_strings.add(match.strip()) + # Translated calls must remain visible to the auditor after + # setters are changed from setToolTip("...") to setToolTip(t("...")). + try: + tree = ast.parse(content) + except SyntaxError: + continue + for node in ast.walk(tree): + if (isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "t" and node.args + and isinstance(node.args[0], ast.Constant) + and isinstance(node.args[0].value, str) + and node.args[0].value.strip()): + german_strings.add(node.args[0].value.strip()) return german_strings @@ -123,7 +138,7 @@ def manage_translations(source_dir: str = ".", check_mode: bool = False) -> int: for lang, count in missing_by_lang.items(): print(f" - {lang}: {count} unvollständig") else: - print("\n[ok] Alle Strings haben vollständige Übersetzungen in allen 6 Sprachen (DE, EN, ES, ZH, JA, RU).") + print("\n[ok] Alle Katalogeinträge haben vollständige Übersetzungen in allen 6 Sprachen (DE, EN, ES, ZH, JA, RU).") print(f"\n[i] Gesamt: {len(translations)} Strings in {trans_file}") diff --git a/src/gui/batch_rename_dialog.py b/src/gui/batch_rename_dialog.py index a1ca879..7fd70ed 100644 --- a/src/gui/batch_rename_dialog.py +++ b/src/gui/batch_rename_dialog.py @@ -75,7 +75,7 @@ def _setup_ui(self): self.search_edit = QLineEdit() self.search_edit.setAccessibleName("Suchbegriff für Umbenennung") self.search_edit.setAccessibleDescription("Zu suchender Text im ursprünglichen Dateinamen.") - self.search_edit.setToolTip("Zu ersetzender Teilstring im Dateinamen") + self.search_edit.setToolTip(t("Zu ersetzender Teilstring im Dateinamen")) search_lbl.setBuddy(self.search_edit) self.search_edit.textChanged.connect(self._update_preview) search_grid.addWidget(self.search_edit, 0, 1) @@ -85,7 +85,7 @@ def _setup_ui(self): self.replace_edit = QLineEdit() self.replace_edit.setAccessibleName("Ersetzungstext") self.replace_edit.setAccessibleDescription("Neuer Text, der anstelle des Suchbegriffs eingesetzt wird.") - self.replace_edit.setToolTip("Neuer Text für gefundene Treffer") + self.replace_edit.setToolTip(t("Neuer Text für gefundene Treffer")) replace_lbl.setBuddy(self.replace_edit) self.replace_edit.textChanged.connect(self._update_preview) search_grid.addWidget(self.replace_edit, 1, 1) @@ -93,7 +93,7 @@ def _setup_ui(self): self.regex_cb = QCheckBox(t("Reguläre Ausdrücke (Regex)")) self.regex_cb.setAccessibleName("Reguläre Ausdrücke") self.regex_cb.setAccessibleDescription("Aktiviert die Auswertung regulärer Ausdrücke im Suchbegriff.") - self.regex_cb.setToolTip("Suchbegriff als regulären Ausdruck auswerten") + self.regex_cb.setToolTip(t("Suchbegriff als regulären Ausdruck auswerten")) self.regex_cb.toggled.connect(self._update_preview) search_grid.addWidget(self.regex_cb, 2, 0, 1, 2) @@ -101,7 +101,7 @@ def _setup_ui(self): self.case_sensitive_cb.setChecked(True) self.case_sensitive_cb.setAccessibleName("Groß- und Kleinschreibung beachten") self.case_sensitive_cb.setAccessibleDescription("Unterscheidet zwischen Groß- und Kleinbuchstaben beim Suchen.") - self.case_sensitive_cb.setToolTip("Exakte Schreibweise beim Suchen berücksichtigen") + self.case_sensitive_cb.setToolTip(t("Exakte Schreibweise beim Suchen berücksichtigen")) self.case_sensitive_cb.toggled.connect(self._update_preview) search_grid.addWidget(self.case_sensitive_cb, 3, 0, 1, 2) @@ -116,7 +116,7 @@ def _setup_ui(self): self.prefix_edit = QLineEdit() self.prefix_edit.setAccessibleName("Dateinamen-Präfix") self.prefix_edit.setAccessibleDescription("Text, der vor jeden Dateinamen eingefügt wird.") - self.prefix_edit.setToolTip("Präfix vor den Dateinamen stellen") + self.prefix_edit.setToolTip(t("Präfix vor den Dateinamen stellen")) prefix_lbl.setBuddy(self.prefix_edit) self.prefix_edit.textChanged.connect(self._update_preview) affix_grid.addWidget(self.prefix_edit, 0, 1) @@ -126,7 +126,7 @@ def _setup_ui(self): self.suffix_edit = QLineEdit() self.suffix_edit.setAccessibleName("Dateinamen-Suffix") self.suffix_edit.setAccessibleDescription("Text, der an das Ende des Dateinamens vor die Dateiendung gehängt wird.") - self.suffix_edit.setToolTip("Suffix an den Dateinamen anhängen") + self.suffix_edit.setToolTip(t("Suffix an den Dateinamen anhängen")) suffix_lbl.setBuddy(self.suffix_edit) self.suffix_edit.textChanged.connect(self._update_preview) affix_grid.addWidget(self.suffix_edit, 1, 1) @@ -141,7 +141,7 @@ def _setup_ui(self): self.case_combo.addItem(t("Satzanfang groß"), "sentence") self.case_combo.setAccessibleName("Schreibweisenumwandlung") self.case_combo.setAccessibleDescription("Wandelt die Schreibweise aller Dateinamen einheitlich um.") - self.case_combo.setToolTip("Groß- und Kleinschreibung des Namens anpassen") + self.case_combo.setToolTip(t("Groß- und Kleinschreibung des Namens anpassen")) case_lbl.setBuddy(self.case_combo) self.case_combo.currentIndexChanged.connect(self._update_preview) affix_grid.addWidget(self.case_combo, 2, 1) @@ -166,7 +166,7 @@ def _setup_ui(self): self.num_enabled_cb = QCheckBox(t("Nummerierung aktivieren")) self.num_enabled_cb.setAccessibleName("Nummerierung aktivieren") self.num_enabled_cb.setAccessibleDescription("Schaltet die sequentielle Dateinummerierung ein.") - self.num_enabled_cb.setToolTip("Dateien fortlaufend nummerieren") + self.num_enabled_cb.setToolTip(t("Dateien fortlaufend nummerieren")) self.num_enabled_cb.toggled.connect(self._update_preview) num_grid.addWidget(self.num_enabled_cb, 0, 0, 1, 2) @@ -176,7 +176,7 @@ def _setup_ui(self): self.start_spin.setRange(0, 999999) self.start_spin.setValue(1) self.start_spin.setAccessibleName("Startwert der Nummerierung") - self.start_spin.setToolTip("Startnummer der Zählung") + self.start_spin.setToolTip(t("Startnummer der Zählung")) start_lbl.setBuddy(self.start_spin) self.start_spin.valueChanged.connect(self._update_preview) num_grid.addWidget(self.start_spin, 1, 1) @@ -187,7 +187,7 @@ def _setup_ui(self): self.step_spin.setRange(1, 1000) self.step_spin.setValue(1) self.step_spin.setAccessibleName("Schrittweite der Nummerierung") - self.step_spin.setToolTip("Inkrement pro Datei") + self.step_spin.setToolTip(t("Inkrement pro Datei")) step_lbl.setBuddy(self.step_spin) self.step_spin.valueChanged.connect(self._update_preview) num_grid.addWidget(self.step_spin, 2, 1) @@ -210,7 +210,7 @@ def _setup_ui(self): self.num_pos_combo.addItem(t("Am Anfang (Präfix)"), "prefix") self.num_pos_combo.addItem(t("Ersetzen"), "replace") self.num_pos_combo.setAccessibleName("Position der Nummerierung") - self.num_pos_combo.setToolTip("Position der Nummerierung im Dateinamen") + self.num_pos_combo.setToolTip(t("Position der Nummerierung im Dateinamen")) pos_lbl.setBuddy(self.num_pos_combo) self.num_pos_combo.currentIndexChanged.connect(self._update_preview) num_grid.addWidget(self.num_pos_combo, 4, 1) @@ -234,7 +234,7 @@ def _setup_ui(self): self.table.setAlternatingRowColors(True) self.table.setAccessibleName("Umbenennungs-Vorschautabelle") self.table.setAccessibleDescription("Zeigt Originalnamen, vorgeschlagene neue Namen und eventuelle Konflikte.") - self.table.setToolTip("Vorschautabelle aller zu ändernden Dateinamen") + self.table.setToolTip(t("Vorschautabelle aller zu ändernden Dateinamen")) preview_layout.addWidget(self.table) # Statusleiste / Zusammenfassung @@ -249,7 +249,7 @@ def _setup_ui(self): self.rollback_btn = QPushButton(t("Rückgängig")) self.rollback_btn.setAccessibleName("Letzte Umbenennung rückgängig machen") - self.rollback_btn.setToolTip("Macht die vorherige Umbenennungs-Aktion rückgängig") + self.rollback_btn.setToolTip(t("Macht die vorherige Umbenennungs-Aktion rückgängig")) self.rollback_btn.setEnabled(False) self.rollback_btn.clicked.connect(self._do_rollback) btn_layout.addWidget(self.rollback_btn) @@ -258,14 +258,14 @@ def _setup_ui(self): self.rename_btn = QPushButton(t("Umbenennen")) self.rename_btn.setAccessibleName("Dateien jetzt umbenennen") - self.rename_btn.setToolTip("Führt die geplante Umbenennung für alle Dateien aus (Enter)") + self.rename_btn.setToolTip(t("Führt die geplante Umbenennung für alle Dateien aus (Enter)")) self.rename_btn.setDefault(True) self.rename_btn.clicked.connect(self._do_rename) btn_layout.addWidget(self.rename_btn) self.close_btn = QPushButton(t("Schließen")) self.close_btn.setAccessibleName("Dialog schließen") - self.close_btn.setToolTip("Schließt das Mehrfachumbenennungs-Fenster (Esc)") + self.close_btn.setToolTip(t("Schließt das Mehrfachumbenennungs-Fenster (Esc)")) self.close_btn.setShortcut("Escape") self.close_btn.clicked.connect(self.reject) btn_layout.addWidget(self.close_btn) diff --git a/src/gui/checksum_dialog.py b/src/gui/checksum_dialog.py index 3b9afb6..170fe12 100644 --- a/src/gui/checksum_dialog.py +++ b/src/gui/checksum_dialog.py @@ -14,6 +14,7 @@ from PySide6.QtGui import QFont from core.checksum_service import ChecksumWorker, verify_hash +from translator import t class ChecksumDialog(QDialog): @@ -104,7 +105,7 @@ def _setup_ui(self): self.copy_all_btn = QPushButton("📋 Alle Prüfsummen kopieren") self.copy_all_btn.setAccessibleName("Alle Prüfsummen kopieren") self.copy_all_btn.setAccessibleDescription("Kopiert alle berechneten Hash-Werte strukturiert in die Zwischenablage.") - self.copy_all_btn.setToolTip("Alle berechneten Prüfsummen (MD5, SHA-1, SHA-256, SHA-512) in die Zwischenablage kopieren") + self.copy_all_btn.setToolTip(t("Alle berechneten Prüfsummen (MD5, SHA-1, SHA-256, SHA-512) in die Zwischenablage kopieren")) self.copy_all_btn.clicked.connect(self._copy_all_hashes) self.copy_all_btn.setEnabled(False) hashes_layout.addRow("", self.copy_all_btn) @@ -124,7 +125,7 @@ def _setup_ui(self): self.verify_edit.setClearButtonEnabled(True) self.verify_edit.setAccessibleName("Erwartete Prüfsumme zur Verifikation") self.verify_edit.setAccessibleDescription("Eingabefeld für den Vergleichs-Hash. Erkennt automatisch den passenden Prüfsummen-Algorithmus.") - self.verify_edit.setToolTip("Geben Sie hier einen Hash ein, um die Integrität automatisch zu überprüfen") + self.verify_edit.setToolTip(t("Geben Sie hier einen Hash ein, um die Integrität automatisch zu überprüfen")) self.verify_edit.textChanged.connect(self._on_verify_text_changed) verify_layout.addWidget(self.verify_edit) @@ -141,7 +142,7 @@ def _setup_ui(self): self.close_btn = QPushButton("Schließen") self.close_btn.setAccessibleName("Dialog schließen") - self.close_btn.setToolTip("Schließt das Prüfsummen-Fenster (Esc)") + self.close_btn.setToolTip(t("Schließt das Prüfsummen-Fenster (Esc)")) self.close_btn.setShortcut("Escape") self.close_btn.setDefault(True) self.close_btn.clicked.connect(self.accept) @@ -157,7 +158,7 @@ def _start_calculation(self): return if not os.path.isfile(self.filepath): self.progress_bar.setVisible(False) - self.verify_result_label.setText("Fehler: Pfad ist ein Verzeichnis, keine Datei.") + self.verify_result_label.setText(t("Fehler: Pfad ist ein Verzeichnis, keine Datei.")) self.verify_result_label.setStyleSheet("color: red; font-weight: bold;") return diff --git a/src/gui/diff_dialog.py b/src/gui/diff_dialog.py index 6c88e16..f45e283 100644 --- a/src/gui/diff_dialog.py +++ b/src/gui/diff_dialog.py @@ -67,7 +67,7 @@ def _setup_ui(self): files_grid.addWidget(self.file1_edit, 0, 1) browse1_btn = QPushButton(t("Durchsuchen...")) browse1_btn.setAccessibleName("Datei 1 durchsuchen") - browse1_btn.setToolTip("Öffnet die Dateiauswahl für Datei 1 (Basis)") + browse1_btn.setToolTip(t("Öffnet die Dateiauswahl für Datei 1 (Basis)")) browse1_btn.clicked.connect(self._browse_file1) files_grid.addWidget(browse1_btn, 0, 2) @@ -81,7 +81,7 @@ def _setup_ui(self): files_grid.addWidget(self.file2_edit, 1, 1) browse2_btn = QPushButton(t("Durchsuchen...")) browse2_btn.setAccessibleName("Datei 2 durchsuchen") - browse2_btn.setToolTip("Öffnet die Dateiauswahl für Datei 2 (Vergleich)") + browse2_btn.setToolTip(t("Öffnet die Dateiauswahl für Datei 2 (Vergleich)")) browse2_btn.clicked.connect(self._browse_file2) files_grid.addWidget(browse2_btn, 1, 2) @@ -89,7 +89,7 @@ def _setup_ui(self): self.compare_btn.setStyleSheet("font-weight: bold; padding: 5px;") self.compare_btn.setAccessibleName("Dateien vergleichen") self.compare_btn.setAccessibleDescription("Startet die Differenzanalyse zwischen Datei 1 und Datei 2.") - self.compare_btn.setToolTip("Dateivergleich starten (Strg+Enter)") + self.compare_btn.setToolTip(t("Dateivergleich starten (Strg+Enter)")) self.compare_btn.setShortcut("Ctrl+Return") self.compare_btn.clicked.connect(self._do_compare) files_grid.addWidget(self.compare_btn, 2, 1, 1, 2) @@ -127,7 +127,7 @@ def _setup_ui(self): self.table.verticalHeader().setVisible(False) self.table.setAccessibleName("Diff-Ergebnistabelle") self.table.setAccessibleDescription("Zeilenweiser Vergleich beider Dateien: Grün = hinzugefügt, Rot = entfernt.") - self.table.setToolTip("Zeilenweiser Vergleich der beiden Dateien") + self.table.setToolTip(t("Zeilenweiser Vergleich der beiden Dateien")) main_layout.addWidget(self.table) # 4. Buttons @@ -144,7 +144,7 @@ def _setup_ui(self): self.close_btn = QPushButton(t("Schließen")) self.close_btn.setAccessibleName("Dialog schließen") - self.close_btn.setToolTip("Schließt den Vergleichs-Dialog (Esc)") + self.close_btn.setToolTip(t("Schließt den Vergleichs-Dialog (Esc)")) self.close_btn.setShortcut("Escape") self.close_btn.setDefault(True) self.close_btn.clicked.connect(self.accept) diff --git a/src/gui/main_window.py b/src/gui/main_window.py index 1ac5f80..4a9629a 100644 --- a/src/gui/main_window.py +++ b/src/gui/main_window.py @@ -22,6 +22,7 @@ from .preview.preview_panel import PreviewPanel from .status_bar import StatusBarWidget from version import __version__ +from translator import t class SearchToolBar(QToolBar): @@ -179,12 +180,12 @@ def _setup_ui(self): self.case_sensitive_cb = QCheckBox("Groß-/Kleinschreibung beachten") self.case_sensitive_cb.setAccessibleName("Groß- und Kleinschreibung beachten") - self.case_sensitive_cb.setToolTip("Groß-/Kleinschreibung bei der Mustersuche berücksichtigen") + self.case_sensitive_cb.setToolTip(t("Groß-/Kleinschreibung bei der Mustersuche berücksichtigen")) options_layout.addWidget(self.case_sensitive_cb) self.whole_words_cb = QCheckBox("Nur ganze Wörter") self.whole_words_cb.setAccessibleName("Nur ganze Wörter") - self.whole_words_cb.setToolTip("Nur eigenständige Wörter als Treffer werten") + self.whole_words_cb.setToolTip(t("Nur eigenständige Wörter als Treffer werten")) options_layout.addWidget(self.whole_words_cb) self.auto_clear_cb = QCheckBox("Clipboard bei ROT automatisch leeren") @@ -213,11 +214,11 @@ def _setup_ui(self): ok_btn = buttons.button(QDialogButtonBox.StandardButton.Ok) if ok_btn: ok_btn.setAccessibleName("Einstellungen speichern") - ok_btn.setToolTip("Speichert die Datenschutz-Einstellungen (Enter)") + ok_btn.setToolTip(t("Speichert die Datenschutz-Einstellungen (Enter)")) cancel_btn = buttons.button(QDialogButtonBox.StandardButton.Cancel) if cancel_btn: cancel_btn.setAccessibleName("Abbrechen") - cancel_btn.setToolTip("Verwirft Änderungen (Esc)") + cancel_btn.setToolTip(t("Verwirft Änderungen (Esc)")) buttons.accepted.connect(self._save_and_close) buttons.rejected.connect(self.reject) layout.addWidget(buttons) diff --git a/src/gui/preview/preview_panel.py b/src/gui/preview/preview_panel.py index f0e760d..afcb030 100644 --- a/src/gui/preview/preview_panel.py +++ b/src/gui/preview/preview_panel.py @@ -17,6 +17,7 @@ from pathlib import Path from core.shortcut_utils import build_shortcut_preview_target, is_windows_shortcut +from translator import t # Optionale Imports try: @@ -347,7 +348,7 @@ def _setup_ui(self): self.tags_edit = QLineEdit() self.tags_edit.setPlaceholderText("Tags hinzufügen (kommagetrennt)") self.tags_edit.setAccessibleName("Metadaten-Tags") - self.tags_edit.setToolTip("Kommagetrennte Tags für die Datei eingeben") + self.tags_edit.setToolTip(t("Kommagetrennte Tags für die Datei eingeben")) self.tags_edit.editingFinished.connect(self.save_user_data) tags_layout.addWidget(self.tags_edit) @@ -361,7 +362,7 @@ def _setup_ui(self): self.notes_edit.setMaximumHeight(100) self.notes_edit.setPlaceholderText("Notizen zur Datei...") self.notes_edit.setAccessibleName("Datei-Notizen") - self.notes_edit.setToolTip("Freitextnotizen zur Datei eingeben") + self.notes_edit.setToolTip(t("Freitextnotizen zur Datei eingeben")) notes_layout.addWidget(self.notes_edit) layout.addWidget(notes_group) @@ -472,13 +473,13 @@ def _setup_ui(self): self.sheet_combo = QComboBox() self.sheet_combo.setMinimumWidth(120) self.sheet_combo.setAccessibleName("Excel-Arbeitsblatt") - self.sheet_combo.setToolTip("Arbeitsblatt der Excel-Arbeitsmappe auswählen") + self.sheet_combo.setToolTip(t("Arbeitsblatt der Excel-Arbeitsmappe auswählen")) self.sheet_combo.currentTextChanged.connect(self._on_sheet_changed) header.addWidget(self.sheet_combo, 1) self.open_extern_btn = QPushButton("Extern öffnen") self.open_extern_btn.setAccessibleName("In externer Anwendung öffnen") - self.open_extern_btn.setToolTip("Öffnet die Excel-Datei im Standardprogramm") + self.open_extern_btn.setToolTip(t("Öffnet die Excel-Datei im Standardprogramm")) self.open_extern_btn.setVisible(False) self.open_extern_btn.clicked.connect(self._open_extern) header.addWidget(self.open_extern_btn) diff --git a/src/gui/settings_dialog.py b/src/gui/settings_dialog.py index 3a1a64b..20de489 100644 --- a/src/gui/settings_dialog.py +++ b/src/gui/settings_dialog.py @@ -15,7 +15,7 @@ ) from core.settings_manager import SettingsManager -from translator import TranslationSystem +from translator import TranslationSystem, t class SettingsDialog(QDialog): @@ -80,11 +80,11 @@ def _build_general_tab(self) -> QWidget: self.start_folder_edit.setPlaceholderText("Leer = Benutzerordner") self.start_folder_edit.setAccessibleName("Startordner") self.start_folder_edit.setAccessibleDescription("Standardordner, der beim Start der Anwendung geöffnet wird.") - self.start_folder_edit.setToolTip("Standardverzeichnis beim Programmstart") + self.start_folder_edit.setToolTip(t("Standardverzeichnis beim Programmstart")) path_row.addWidget(self.start_folder_edit) browse_btn = QPushButton("Auswählen...") browse_btn.setAccessibleName("Startordner auswählen") - browse_btn.setToolTip("Dateidialog zur Auswahl des Startordners öffnen") + browse_btn.setToolTip(t("Dateidialog zur Auswahl des Startordners öffnen")) browse_btn.clicked.connect(self._choose_start_folder) path_row.addWidget(browse_btn) form.addRow("Startordner:", path_row) @@ -92,24 +92,27 @@ def _build_general_tab(self) -> QWidget: self.language_cb = QComboBox() self.language_cb.setAccessibleName("Sprachauswahl") self.language_cb.setAccessibleDescription("Auswahl der Benutzeroberflächensprache.") - self.language_cb.setToolTip("Programmsprache für Menüs und Dialoge") + self.language_cb.setToolTip( + t("Programmsprache für Menüs und Dialoge") + "\n" + + t("Sprachänderungen werden beim nächsten Start übernommen.") + ) for code, display in TranslationSystem.get_language_display_names().items(): self.language_cb.addItem(display, code) form.addRow("Sprache:", self.language_cb) self.show_hidden_cb = QCheckBox("Versteckte Dateien anzeigen") self.show_hidden_cb.setAccessibleName("Versteckte Dateien anzeigen") - self.show_hidden_cb.setToolTip("Versteckte Dateien und Systemordner im Dateibrowser anzeigen") + self.show_hidden_cb.setToolTip(t("Versteckte Dateien und Systemordner im Dateibrowser anzeigen")) form.addRow(self.show_hidden_cb) self.confirm_delete_cb = QCheckBox("Vor dem Löschen nachfragen") self.confirm_delete_cb.setAccessibleName("Vor dem Löschen nachfragen") - self.confirm_delete_cb.setToolTip("Sicherheitsabfrage vor dem unwiderruflichen Löschen einblenden") + self.confirm_delete_cb.setToolTip(t("Sicherheitsabfrage vor dem unwiderruflichen Löschen einblenden")) form.addRow(self.confirm_delete_cb) self.remember_size_cb = QCheckBox("Fenstergröße merken") self.remember_size_cb.setAccessibleName("Fenstergröße merken") - self.remember_size_cb.setToolTip("Fenstergröße und Position beim Beenden speichern") + self.remember_size_cb.setToolTip(t("Fenstergröße und Position beim Beenden speichern")) form.addRow(self.remember_size_cb) return page @@ -120,19 +123,19 @@ def _build_index_tab(self) -> QWidget: self.auto_index_cb = QCheckBox("Ordner automatisch indizieren") self.auto_index_cb.setAccessibleName("Ordner automatisch indizieren") - self.auto_index_cb.setToolTip("Verzeichnisse im Hintergrund automatisch für die Schnellsuche indizieren") + self.auto_index_cb.setToolTip(t("Verzeichnisse im Hintergrund automatisch für die Schnellsuche indizieren")) form.addRow(self.auto_index_cb) self.index_startup_cb = QCheckBox("Beim Programmstart indizieren") self.index_startup_cb.setAccessibleName("Beim Programmstart indizieren") - self.index_startup_cb.setToolTip("Indexprüfung direkt beim Start von ExplorerPro durchführen") + self.index_startup_cb.setToolTip(t("Indexprüfung direkt beim Start von ExplorerPro durchführen")) form.addRow(self.index_startup_cb) self.max_file_size_spin = QSpinBox() self.max_file_size_spin.setRange(1, 10000) self.max_file_size_spin.setSuffix(" MB") self.max_file_size_spin.setAccessibleName("Maximale Dateigröße für den Index") - self.max_file_size_spin.setToolTip("Dateien über dieser Größe werden nicht indiziert") + self.max_file_size_spin.setToolTip(t("Dateien über dieser Größe werden nicht indiziert")) form.addRow("Maximale Dateigröße:", self.max_file_size_spin) return page @@ -143,29 +146,29 @@ def _build_preview_tab(self) -> QWidget: self.show_preview_cb = QCheckBox("Vorschaufenster anzeigen") self.show_preview_cb.setAccessibleName("Vorschaufenster anzeigen") - self.show_preview_cb.setToolTip("Vorschauleiste auf der rechten Seite einblenden") + self.show_preview_cb.setToolTip(t("Vorschauleiste auf der rechten Seite einblenden")) form.addRow(self.show_preview_cb) self.preview_images_cb = QCheckBox("Bilder in der Vorschau anzeigen") self.preview_images_cb.setAccessibleName("Bilder in der Vorschau anzeigen") - self.preview_images_cb.setToolTip("Miniaturansichten für Grafikdateien rendern") + self.preview_images_cb.setToolTip(t("Miniaturansichten für Grafikdateien rendern")) form.addRow(self.preview_images_cb) self.preview_pdfs_cb = QCheckBox("PDF-Dateien in der Vorschau anzeigen") self.preview_pdfs_cb.setAccessibleName("PDF-Dateien in der Vorschau anzeigen") - self.preview_pdfs_cb.setToolTip("Erste Seite von PDF-Dokumenten im Vorschaufenster darstellen") + self.preview_pdfs_cb.setToolTip(t("Erste Seite von PDF-Dokumenten im Vorschaufenster darstellen")) form.addRow(self.preview_pdfs_cb) self.preview_code_cb = QCheckBox("Quelltext in der Vorschau anzeigen") self.preview_code_cb.setAccessibleName("Quelltext in der Vorschau anzeigen") - self.preview_code_cb.setToolTip("Syntaxhervorhebung für Quellcode und Textdateien nutzen") + self.preview_code_cb.setToolTip(t("Syntaxhervorhebung für Quellcode und Textdateien nutzen")) form.addRow(self.preview_code_cb) self.max_preview_spin = QSpinBox() self.max_preview_spin.setRange(1, 1000) self.max_preview_spin.setSuffix(" MB") self.max_preview_spin.setAccessibleName("Maximale Dateigröße für die Vorschau") - self.max_preview_spin.setToolTip("Maximale Dateigröße für generierte Vorschauen") + self.max_preview_spin.setToolTip(t("Maximale Dateigröße für generierte Vorschauen")) form.addRow("Maximale Vorschaugröße:", self.max_preview_spin) return page @@ -176,12 +179,12 @@ def _build_privacy_tab(self) -> QWidget: self.clipboard_monitor_cb = QCheckBox("Zwischenablage überwachen") self.clipboard_monitor_cb.setAccessibleName("Zwischenablage überwachen") - self.clipboard_monitor_cb.setToolTip("Zwischenablage kontinuierlich auf schutzwürdige Muster überwachen") + self.clipboard_monitor_cb.setToolTip(t("Zwischenablage kontinuierlich auf schutzwürdige Muster überwachen")) form.addRow(self.clipboard_monitor_cb) self.auto_block_cb = QCheckBox("Sensible Inhalte automatisch blockieren") self.auto_block_cb.setAccessibleName("Sensible Inhalte automatisch blockieren") - self.auto_block_cb.setToolTip("Kopieren hochsensibler Daten wie Passwörter und API-Keys blockieren") + self.auto_block_cb.setToolTip(t("Kopieren hochsensibler Daten wie Passwörter und API-Keys blockieren")) form.addRow(self.auto_block_cb) self.notifications_cb = QCheckBox("Hinweise anzeigen") @@ -204,7 +207,7 @@ def _build_appearance_tab(self) -> QWidget: self.theme_combo = QComboBox() self.theme_combo.setAccessibleName("Farbschema") - self.theme_combo.setToolTip("Farbschema der Benutzeroberfläche") + self.theme_combo.setToolTip(t("Farbschema der Benutzeroberfläche")) for value, label in self.THEMES: self.theme_combo.addItem(label, value) form.addRow("Farbschema:", self.theme_combo) @@ -212,14 +215,14 @@ def _build_appearance_tab(self) -> QWidget: self.font_size_spin = QSpinBox() self.font_size_spin.setRange(6, 32) self.font_size_spin.setAccessibleName("Schriftgröße") - self.font_size_spin.setToolTip("Basisschriftgröße in Punkten") + self.font_size_spin.setToolTip(t("Basisschriftgröße in Punkten")) form.addRow("Schriftgröße:", self.font_size_spin) self.icon_size_spin = QSpinBox() self.icon_size_spin.setRange(12, 64) self.icon_size_spin.setSuffix(" px") self.icon_size_spin.setAccessibleName("Symbolgröße") - self.icon_size_spin.setToolTip("Größe der Dateisymbole in Pixeln") + self.icon_size_spin.setToolTip(t("Größe der Dateisymbole in Pixeln")) form.addRow("Symbolgröße:", self.icon_size_spin) return page @@ -237,6 +240,9 @@ def _load_settings(self): """Überträgt die gespeicherten Werte in die Bedienelemente.""" get = self.settings.get + language_index = self.language_cb.findData(get("general", "language", "de")) + self.language_cb.setCurrentIndex(language_index if language_index >= 0 else 0) + self.start_folder_edit.setText(get("general", "start_folder", "") or "") self.show_hidden_cb.setChecked(bool(get("general", "show_hidden_files", False))) self.confirm_delete_cb.setChecked(bool(get("general", "confirm_delete", True))) @@ -268,6 +274,7 @@ def collect_settings(self) -> dict: """Liest die Bedienelemente aus und gibt die Werte als Dict zurück.""" return { "general": { + "language": self.language_cb.currentData(), "start_folder": self.start_folder_edit.text().strip(), "show_hidden_files": self.show_hidden_cb.isChecked(), "confirm_delete": self.confirm_delete_cb.isChecked(), diff --git a/src/gui/sidebar/advanced_search_dialog.py b/src/gui/sidebar/advanced_search_dialog.py index 5804e74..3eb20b9 100644 --- a/src/gui/sidebar/advanced_search_dialog.py +++ b/src/gui/sidebar/advanced_search_dialog.py @@ -13,6 +13,7 @@ ) from PySide6.QtCore import Qt, Signal, QDate, QThread, Slot from pathlib import Path +from translator import t class AdvancedSearchWorker(QThread): @@ -108,7 +109,7 @@ def _setup_ui(self): self.query_input.setPlaceholderText("Suchbegriff eingeben...") self.query_input.setAccessibleName("Suchbegriff") self.query_input.setAccessibleDescription("Suchbegriff für Dateiname, Inhalt oder Pfad.") - self.query_input.setToolTip("Suchbegriff eingeben (Enter startet die Suche)") + self.query_input.setToolTip(t("Suchbegriff eingeben (Enter startet die Suche)")) self.query_input.returnPressed.connect(self._do_search) search_layout.addWidget(self.query_input) @@ -129,16 +130,16 @@ def _setup_ui(self): self.search_name_cb = QCheckBox("Dateiname") self.search_name_cb.setChecked(True) self.search_name_cb.setAccessibleName("Im Dateinamen suchen") - self.search_name_cb.setToolTip("Dateinamen durchsuchen") + self.search_name_cb.setToolTip(t("Dateinamen durchsuchen")) self.search_content_cb = QCheckBox("Inhalt") self.search_content_cb.setChecked(True) self.search_content_cb.setAccessibleName("Im Dateiinhalt suchen") - self.search_content_cb.setToolTip("Volltextinhalte durchsuchen") + self.search_content_cb.setToolTip(t("Volltextinhalte durchsuchen")) self.search_path_cb = QCheckBox("Pfad") self.search_path_cb.setAccessibleName("Im Pfad suchen") - self.search_path_cb.setToolTip("Vollständigen Dateipfad durchsuchen") + self.search_path_cb.setToolTip(t("Vollständigen Dateipfad durchsuchen")) scope_layout.addWidget(self.search_name_cb) scope_layout.addWidget(self.search_content_cb) @@ -152,14 +153,14 @@ def _setup_ui(self): for name, _ in self.FILE_TYPES: self.type_combo.addItem(name) self.type_combo.setAccessibleName("Dateityp-Filter") - self.type_combo.setToolTip("Auf bestimmte Dateitypen einschränken") + self.type_combo.setToolTip(t("Auf bestimmte Dateitypen einschränken")) self.type_combo.currentIndexChanged.connect(self._on_type_changed) type_layout.addWidget(self.type_combo) self.custom_ext_input = QLineEdit() self.custom_ext_input.setPlaceholderText(".py, .txt, ...") self.custom_ext_input.setAccessibleName("Benutzerdefinierte Dateiendungen") - self.custom_ext_input.setToolTip("Dateiendungen kommagetrennt eingeben") + self.custom_ext_input.setToolTip(t("Dateiendungen kommagetrennt eingeben")) self.custom_ext_input.hide() type_layout.addWidget(self.custom_ext_input) @@ -171,7 +172,7 @@ def _setup_ui(self): for name, _, _ in self.DATE_PRESETS: self.date_preset_combo.addItem(name) self.date_preset_combo.setAccessibleName("Änderungszeitraum") - self.date_preset_combo.setToolTip("Zeitraum der letzten Änderung auswählen") + self.date_preset_combo.setToolTip(t("Zeitraum der letzten Änderung auswählen")) self.date_preset_combo.currentIndexChanged.connect(self._on_date_preset_changed) date_layout.addWidget(self.date_preset_combo) @@ -218,7 +219,7 @@ def _setup_ui(self): self.tags_input = QLineEdit() self.tags_input.setPlaceholderText("tag1, tag2, ... (mit Komma trennen)") self.tags_input.setAccessibleName("Datei-Tags filtern") - self.tags_input.setToolTip("Kommagetrennte Tags filtern") + self.tags_input.setToolTip(t("Kommagetrennte Tags filtern")) criteria_layout.addRow("Tags:", self.tags_input) layout.addWidget(criteria_group) @@ -229,13 +230,13 @@ def _setup_ui(self): self.search_btn = QPushButton("🔍 Suchen") self.search_btn.setDefault(True) self.search_btn.setAccessibleName("Suche ausführen") - self.search_btn.setToolTip("Startet die erweiterte Suche (Enter)") + self.search_btn.setToolTip(t("Startet die erweiterte Suche (Enter)")) self.search_btn.clicked.connect(self._do_search) button_layout.addWidget(self.search_btn) self.clear_btn = QPushButton("Zurücksetzen") self.clear_btn.setAccessibleName("Suchfilter zurücksetzen") - self.clear_btn.setToolTip("Setzt alle Kriterien und Filter auf Standardwerte zurück") + self.clear_btn.setToolTip(t("Setzt alle Kriterien und Filter auf Standardwerte zurück")) self.clear_btn.clicked.connect(self._reset_form) button_layout.addWidget(self.clear_btn) @@ -265,7 +266,7 @@ def _setup_ui(self): self.results_table.setSortingEnabled(True) self.results_table.setAccessibleName("Suchergebnisse") self.results_table.setAccessibleDescription("Liste gefundener Dateien mit Pfad, Typ, Größe und Datum.") - self.results_table.setToolTip("Ergebnistabelle (Doppelklick oder Enter zum Öffnen)") + self.results_table.setToolTip(t("Ergebnistabelle (Doppelklick oder Enter zum Öffnen)")) self.results_table.doubleClicked.connect(self._on_result_double_clicked) # Spaltenbreiten @@ -288,14 +289,14 @@ def _setup_ui(self): self.open_btn = QPushButton("Öffnen") self.open_btn.setAccessibleName("Ausgewählte Datei öffnen") - self.open_btn.setToolTip("Öffnet die ausgewählte Datei") + self.open_btn.setToolTip(t("Öffnet die ausgewählte Datei")) self.open_btn.clicked.connect(self._open_selected) self.open_btn.setEnabled(False) status_layout.addWidget(self.open_btn) self.open_folder_btn = QPushButton("Ordner öffnen") self.open_folder_btn.setAccessibleName("Enthaltenden Ordner öffnen") - self.open_folder_btn.setToolTip("Öffnet den Ordner der ausgewählten Datei") + self.open_folder_btn.setToolTip(t("Öffnet den Ordner der ausgewählten Datei")) self.open_folder_btn.clicked.connect(self._open_folder) self.open_folder_btn.setEnabled(False) status_layout.addWidget(self.open_folder_btn) @@ -309,7 +310,7 @@ def _setup_ui(self): close_btn = dialog_buttons.button(QDialogButtonBox.StandardButton.Close) if close_btn: close_btn.setAccessibleName("Dialog schließen") - close_btn.setToolTip("Schließt das Dialogfenster (Esc)") + close_btn.setToolTip(t("Schließt das Dialogfenster (Esc)")) dialog_buttons.rejected.connect(self.reject) layout.addWidget(dialog_buttons) diff --git a/src/main.py b/src/main.py index 63ea7ab..804e8b0 100644 --- a/src/main.py +++ b/src/main.py @@ -65,6 +65,17 @@ def set_application_version(app: QApplication) -> None: app.setApplicationVersion(__version__) +def configure_application_language(): + """Restore the saved UI language before constructing any translated widgets.""" + from core.settings_manager import SettingsManager + from translator import get_translator, SUPPORTED_LANGUAGES + + language = SettingsManager.instance().get("general", "language", "de") + if language not in SUPPORTED_LANGUAGES: + language = "de" + return get_translator(language) + + def main(): """Haupteinstiegspunkt für ExplorerPro""" # High DPI Support @@ -75,8 +86,8 @@ def main(): app = QApplication(sys.argv) app.setApplicationName("ExplorerPro") app.setOrganizationName("ExplorerPro") - from translator import get_translator - install_qt_translations(app, get_translator().get_language()) + translator = configure_application_language() + install_qt_translations(app, translator.get_language()) set_application_version(app) icon = load_app_icon() if not icon.isNull(): diff --git a/src/modules/launcher/apps_panel.py b/src/modules/launcher/apps_panel.py index a11bb6a..991dd06 100644 --- a/src/modules/launcher/apps_panel.py +++ b/src/modules/launcher/apps_panel.py @@ -22,6 +22,7 @@ import subprocess from core.platform_utils import open_path_with_system +from translator import t @dataclass @@ -149,11 +150,11 @@ def _setup_ui(self): self.path_edit = QLineEdit() self.path_edit.setAccessibleName("Programmpfad") self.path_edit.setAccessibleDescription("Dateipfad zur ausführbaren Datei oder zum Skript.") - self.path_edit.setToolTip("Pfad zur ausführbaren Datei") + self.path_edit.setToolTip(t("Pfad zur ausführbaren Datei")) path_btn = QPushButton("...") path_btn.setFixedWidth(30) path_btn.setAccessibleName("Programmdatei durchsuchen") - path_btn.setToolTip("Dateiauswahldialog öffnen") + path_btn.setToolTip(t("Dateiauswahldialog öffnen")) path_btn.clicked.connect(self._browse_path) path_layout.addWidget(self.path_edit) path_layout.addWidget(path_btn) @@ -163,7 +164,7 @@ def _setup_ui(self): self.category_combo.setEditable(True) self.category_combo.setAccessibleName("Kategorie") self.category_combo.setAccessibleDescription("Kategorie zur Gruppierung im Schnellstarter.") - self.category_combo.setToolTip("Kategorie auswählen oder neu eingeben") + self.category_combo.setToolTip(t("Kategorie auswählen oder neu eingeben")) self.category_combo.addItems([ "Allgemein", "Entwicklung", "Office", "Grafik", "Multimedia", "Internet", "System", "Spiele" @@ -192,7 +193,7 @@ def _setup_ui(self): ok_btn = buttons.button(QDialogButtonBox.StandardButton.Ok) if ok_btn: ok_btn.setAccessibleName("App speichern") - ok_btn.setToolTip("Speichert die Anwendungseinstellungen (Enter)") + ok_btn.setToolTip(t("Speichert die Anwendungseinstellungen (Enter)")) cancel_btn = buttons.button(QDialogButtonBox.StandardButton.Cancel) if cancel_btn: cancel_btn.setAccessibleName("Abbrechen") @@ -259,9 +260,9 @@ def _setup_ui(self): header = QHBoxLayout() self.search_edit = QLineEdit() self.search_edit.setPlaceholderText("🔍 App suchen...") - self.search_edit.setAccessibleName("Apps durchsuchen") + self.search_edit.setAccessibleName(t("Apps durchsuchen")) self.search_edit.setAccessibleDescription("Filtert die Anwendungsliste nach Name.") - self.search_edit.setToolTip("Apps durchsuchen") + self.search_edit.setToolTip(t("Apps durchsuchen")) self.search_edit.textChanged.connect(self._filter_apps) header.addWidget(self.search_edit) diff --git a/src/modules/prompts/prompts_panel.py b/src/modules/prompts/prompts_panel.py index 2097251..3351a47 100644 --- a/src/modules/prompts/prompts_panel.py +++ b/src/modules/prompts/prompts_panel.py @@ -18,6 +18,7 @@ from dataclasses import dataclass, field from datetime import datetime import json +from translator import t @dataclass @@ -76,7 +77,7 @@ def _setup_ui(self): self.category_combo = QComboBox() self.category_combo.setEditable(True) self.category_combo.setAccessibleName("Prompt-Kategorie") - self.category_combo.setToolTip("Kategorie für die Einordnung des Prompts") + self.category_combo.setToolTip(t("Kategorie für die Einordnung des Prompts")) self.category_combo.addItems(self.categories) form.addRow("Kategorie:", self.category_combo) @@ -84,7 +85,7 @@ def _setup_ui(self): self.tags_edit = QLineEdit() self.tags_edit.setPlaceholderText("tag1, tag2, tag3 (mit Komma trennen)") self.tags_edit.setAccessibleName("Prompt-Tags") - self.tags_edit.setToolTip("Schlagwörter zur Filterung, kommagetrennt") + self.tags_edit.setToolTip(t("Schlagwörter zur Filterung, kommagetrennt")) form.addRow("Tags:", self.tags_edit) layout.addLayout(form) @@ -98,7 +99,7 @@ def _setup_ui(self): self.content_edit.setAccessibleDescription( "Text der Prompt-Vorlage. Variablen in doppelten geschweiften Klammern werden beim Verwenden abgefragt." ) - self.content_edit.setToolTip("Vollständiger Prompt-Text") + self.content_edit.setToolTip(t("Vollständiger Prompt-Text")) layout.addWidget(self.content_edit) # Variablen-Hinweis @@ -284,7 +285,7 @@ def _setup_ui(self): self.preview_text.setReadOnly(True) self.preview_text.setMaximumHeight(100) self.preview_text.setAccessibleName("Prompt-Vorschautext") - self.preview_text.setToolTip("Vorschau des ausgewählten Prompts") + self.preview_text.setToolTip(t("Vorschau des ausgewählten Prompts")) preview_layout.addWidget(self.preview_text) # Aktions-Buttons @@ -299,7 +300,7 @@ def _setup_ui(self): self.edit_btn = QPushButton("✏️ Bearbeiten") self.edit_btn.setAccessibleName("Ausgewählten Prompt bearbeiten") - self.edit_btn.setToolTip("Öffnet den Bearbeitungsdialog für den Prompt") + self.edit_btn.setToolTip(t("Öffnet den Bearbeitungsdialog für den Prompt")) self.edit_btn.clicked.connect(self._edit_selected) self.edit_btn.setEnabled(False) btn_layout.addWidget(self.edit_btn) diff --git a/tests/test_tooltip_language.py b/tests/test_tooltip_language.py new file mode 100644 index 0000000..61dc443 --- /dev/null +++ b/tests/test_tooltip_language.py @@ -0,0 +1,85 @@ +"""Language selection survives a restart and changes real Qt help text.""" + +import json +from pathlib import Path + +import pytest +import shiboken6 +from PySide6.QtWidgets import QApplication + +import manage_translations +import translator +from core.settings_manager import SettingsManager +from gui.settings_dialog import SettingsDialog +from src.main import configure_application_language + + +@pytest.fixture +def isolated_settings(monkeypatch, tmp_path): + # Use real JSON persistence without touching the user's settings or singleton. + monkeypatch.setattr(SettingsManager, "_instance", None) + monkeypatch.setattr(SettingsManager, "_get_config_path", lambda self: tmp_path / "settings.json") + monkeypatch.setattr(translator, "_default_translator", translator.TranslationSystem()) + return SettingsManager.instance() + + +@pytest.mark.parametrize("language", translator.SUPPORTED_LANGUAGES) +def test_language_survives_dialog_save_and_restart(isolated_settings, monkeypatch, language): + app = QApplication.instance() or QApplication([]) + settings = isolated_settings + dialog = SettingsDialog(settings=settings) + try: + dialog.language_cb.setCurrentIndex(dialog.language_cb.findData(language)) + dialog.apply_to_settings() + saved = json.loads(settings._config_path.read_text(encoding="utf-8")) + assert saved["general"]["language"] == language + finally: + shiboken6.delete(dialog) + + # Recreate the manager and restore the translator as at application startup. + monkeypatch.setattr(SettingsManager, "_instance", None) + restored = SettingsManager.instance() + tr = configure_application_language() + assert tr.get_language() == language + reopened = SettingsDialog(settings=restored) + try: + assert reopened.language_cb.currentData() == language + assert reopened.start_folder_edit.toolTip() == tr.translations["Standardverzeichnis beim Programmstart"][language] + assert reopened.language_cb.toolTip().splitlines()[1] == tr.translations["Sprachänderungen werden beim nächsten Start übernommen."][language] + assert app is QApplication.instance() + finally: + shiboken6.delete(reopened) + + +@pytest.mark.parametrize("invalid", ["fr", None, ["en"], {"language": "en"}]) +def test_invalid_saved_language_falls_back_to_german(isolated_settings, invalid): + app = QApplication.instance() or QApplication([]) + isolated_settings.set("general", "language", invalid) + assert configure_application_language().get_language() == "de" + dialog = SettingsDialog(settings=isolated_settings) + try: + assert dialog.language_cb.currentData() == "de" + assert dialog.start_folder_edit.toolTip() == "Standardverzeichnis beim Programmstart" + assert app is QApplication.instance() + finally: + shiboken6.delete(dialog) + + +def test_auditor_checks_translated_calls_without_modifying_catalog(tmp_path): + (tmp_path / "sample.py").write_text( + 'from translator import t\nbutton.setToolTip(t("Neue Hilfe"))\n' + 'button.setText(t("Status"))\n' + '# t("Nicht vorhandene Hilfe")\n', encoding="utf-8" + ) + locales = tmp_path / "locales" + locales.mkdir() + catalog = locales / "translations.json" + catalog.write_text("{}\n", encoding="utf-8") + assert manage_translations.find_german_strings(str(tmp_path)) == {"Neue Hilfe", "Status"} + assert manage_translations.manage_translations(str(tmp_path), check_mode=True) == 1 + assert catalog.read_text(encoding="utf-8") == "{}\n" + + +def test_repository_auditor_has_no_untranslated_help_text(): + root = Path(__file__).resolve().parents[1] + assert manage_translations.manage_translations(str(root), check_mode=True) == 0 From 0fb2220002770f993224394e7a541af4cd587d92 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Sun, 4 Oct 2026 14:26:26 +0200 Subject: [PATCH 2/2] fix: configure_application_language nach Merge wiederherstellen, GC-Test-Stub um SUPPORTED_LANGUAGES ergaenzen --- src/main.py | 11 +++++++++++ tests/test_gui_gc.py | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index 0dc852e..86c246d 100644 --- a/src/main.py +++ b/src/main.py @@ -66,6 +66,17 @@ def set_application_version(app: QApplication) -> None: app.setApplicationVersion(__version__) +def configure_application_language(): + """Restore the saved UI language before constructing any translated widgets.""" + from core.settings_manager import SettingsManager + from translator import get_translator, SUPPORTED_LANGUAGES + + language = SettingsManager.instance().get("general", "language", "de") + if language not in SUPPORTED_LANGUAGES: + language = "de" + return get_translator(language) + + def main(): """Haupteinstiegspunkt für ExplorerPro""" # High DPI Support diff --git a/tests/test_gui_gc.py b/tests/test_gui_gc.py index e121632..7dbfb15 100644 --- a/tests/test_gui_gc.py +++ b/tests/test_gui_gc.py @@ -184,7 +184,8 @@ class Translator: def get_language(self): return 'en' module = types.ModuleType('translator') -module.get_translator = lambda: Translator() +module.get_translator = lambda *args: Translator() +module.SUPPORTED_LANGUAGES = {'de': 'Deutsch', 'en': 'English'} sys.modules['translator'] = module def fail_setup(app, language): raise RuntimeError('application-setup-failed')