diff --git a/je_editor/pyside_ui/browser/browser_view.py b/je_editor/pyside_ui/browser/browser_view.py index b8f92d0c..5153c8e9 100644 --- a/je_editor/pyside_ui/browser/browser_view.py +++ b/je_editor/pyside_ui/browser/browser_view.py @@ -9,11 +9,20 @@ from PySide6.QtCore import QObject from PySide6.QtGui import QCloseEvent -from PySide6.QtWebEngineCore import QWebEngineDownloadRequest -from PySide6.QtWebEngineWidgets import QWebEngineView -from je_editor.pyside_ui.browser.browser_download_window import BrowserDownloadWindow -from je_editor.utils.logging.loggin_instance import jeditor_logger +from je_editor.utils.browser.chromium_flags import quiet_chromium_logging + +# 必須在 QtWebEngine 初始化之前設定,因此排在匯入它之前 +# This has to be set before QtWebEngine initialises, so it comes before the import +quiet_chromium_logging() + +from PySide6.QtWebEngineCore import QWebEngineDownloadRequest # noqa: E402 +from PySide6.QtWebEngineWidgets import QWebEngineView # noqa: E402 + +from je_editor.pyside_ui.browser.browser_download_window import ( # noqa: E402 + BrowserDownloadWindow +) +from je_editor.utils.logging.loggin_instance import jeditor_logger # noqa: E402 class BrowserView(QWebEngineView): diff --git a/je_editor/pyside_ui/main_ui/menu/language_menu/build_language_server.py b/je_editor/pyside_ui/main_ui/menu/language_menu/build_language_server.py index 874a6a38..dbd444c4 100644 --- a/je_editor/pyside_ui/main_ui/menu/language_menu/build_language_server.py +++ b/je_editor/pyside_ui/main_ui/menu/language_menu/build_language_server.py @@ -9,7 +9,6 @@ # 匯入 Qt 動作與訊息框 # Import QAction and QMessageBox from PySide6 from PySide6.QtGui import QAction -from PySide6.QtWidgets import QMessageBox # 匯入使用者設定字典,用來保存語言設定 # Import user settings dictionary for saving language preferences @@ -78,6 +77,11 @@ def set_language(language: str, ui_we_want_to_set: EditorMain) -> None: f"language: {language} " f"ui_we_want_to_set: {ui_we_want_to_set}") + # 先留下舊字典:重新標示畫面時要靠它反查哪些字是翻譯來的 + # Keep the old dictionary: relabelling needs it to tell which words on screen + # came from a translation and which are file names + previous_words = dict(language_wrapper.language_word_dict) + # 重設語言 (更新多語言字典) # Reset language (update multi-language dictionary) language_wrapper.reset_language(language) @@ -86,8 +90,7 @@ def set_language(language: str, ui_we_want_to_set: EditorMain) -> None: # Update user settings dictionary to persist language preference user_setting_dict.update({"language": language}) - # 顯示提示訊息,提醒使用者需要重新啟動程式 - # Show a message box to remind user to restart the application - message_box = QMessageBox(ui_we_want_to_set) - message_box.setText(language_wrapper.language_word_dict.get("language_menu_bar_please_restart_messagebox")) - message_box.show() + # 立即重新標示整個介面,不需要重新啟動 + # Relabel the whole interface at once, with no restart + from je_editor.pyside_ui.main_ui.retranslate import retranslate_ui + retranslate_ui(ui_we_want_to_set, previous_words) diff --git a/je_editor/pyside_ui/main_ui/outline_panel/outline_panel_widget.py b/je_editor/pyside_ui/main_ui/outline_panel/outline_panel_widget.py index e7efea4e..54a36598 100644 --- a/je_editor/pyside_ui/main_ui/outline_panel/outline_panel_widget.py +++ b/je_editor/pyside_ui/main_ui/outline_panel/outline_panel_widget.py @@ -66,6 +66,20 @@ def __init__(self, main_window=None) -> None: self.refresh() + def retranslate(self) -> None: + """ + 換語言後重新標示自己 + Relabel after the language changes. + """ + word = language_wrapper.language_word_dict + self.refresh_button.setText(word.get("outline_panel_refresh")) + self.tree.setHeaderLabels([ + word.get("outline_panel_col_symbol"), + word.get("outline_panel_col_kind"), + word.get("outline_panel_col_line"), + ]) + self.refresh() + def current_code_edit(self): """ 取得目前分頁的程式碼編輯器 diff --git a/je_editor/pyside_ui/main_ui/problems_panel/problems_panel_widget.py b/je_editor/pyside_ui/main_ui/problems_panel/problems_panel_widget.py index e2b2acbe..32dcfe71 100644 --- a/je_editor/pyside_ui/main_ui/problems_panel/problems_panel_widget.py +++ b/je_editor/pyside_ui/main_ui/problems_panel/problems_panel_widget.py @@ -116,6 +116,30 @@ def diagnostics(self) -> list[Diagnostic]: """取得面板目前顯示的診斷 / The diagnostics currently listed.""" return list(self._diagnostics) + def retranslate(self) -> None: + """ + 換語言後重新標示自己 + Relabel after the language changes. + + 面板記著目前的診斷,因此不能整個拆掉重建;改字之後重畫清單,狀態列的文字 + 也會跟著換成新語言。 + The panel is holding the current diagnostics and so cannot be rebuilt from + scratch; relabelling and then redrawing the list also moves the status + text to the new language. + """ + word = language_wrapper.language_word_dict + self.refresh_button.setText(word.get("problems_panel_refresh")) + self.project_check.setText(word.get("problems_panel_whole_project")) + self.fix_button.setText(word.get("problems_panel_fix")) + self.severity_filter.setItemText(0, word.get("problems_panel_all_severities")) + self.result_tree.setHeaderLabels([ + word.get("problems_panel_col_code"), + word.get("problems_panel_col_message"), + word.get("problems_panel_col_line"), + word.get("problems_panel_col_file"), + ]) + self._render_items() + def refresh(self) -> None: """ 重新讀取診斷並重畫清單 diff --git a/je_editor/pyside_ui/main_ui/retranslate.py b/je_editor/pyside_ui/main_ui/retranslate.py new file mode 100644 index 00000000..b8086382 --- /dev/null +++ b/je_editor/pyside_ui/main_ui/retranslate.py @@ -0,0 +1,111 @@ +""" +換語言後把整個介面重新標示一次 +Relabel the whole interface after the language changes. + +原本換語言只會跳出「請重新啟動」——設定改了,但畫面上一個字也沒變。選單與工具列 +是啟動時建好的,重建它們最省事也最完整;面板各自有狀態,因此由它們自己重新標示。 +Changing the language used to do nothing but ask for a restart: the setting +moved and not one word on screen did. The menus and the toolbar are built at +startup and rebuilding them is both the simplest and the most complete way to +relabel them; the panels hold state, so each relabels itself. +""" +from __future__ import annotations + +from typing import Dict + +from je_editor.pyside_ui.main_ui.save_settings.shortcut_setting import reload_bound_shortcuts +from je_editor.utils.logging.loggin_instance import jeditor_logger +from je_editor.utils.multi_language.multi_language_wrapper import language_wrapper +from je_editor.utils.multi_language.retranslate_text import TITLE_KEYS, translated_again + + +def retranslate_ui(main_window, previous_words: Dict[str, str]) -> None: + """ + 依目前語言重新標示整個介面 + Relabel the whole interface in the language now in use. + + :param main_window: 主編輯器視窗 / the main editor window + :param previous_words: 換語言之前的字典 / the dictionary from before the change + """ + jeditor_logger.info("retranslate.py retranslate_ui") + words = language_wrapper.language_word_dict + main_window.setWindowTitle(words.get("application_name", "")) + main_window.setToolTip(words.get("application_name", "")) + _rebuild_menu_bar(main_window) + _rebuild_toolbar(main_window) + _retranslate_tabs(main_window, previous_words, words) + _retranslate_docks(main_window, previous_words, words) + refresh = getattr(main_window, "refresh_status_bar", None) + if callable(refresh): + refresh() + # 舊選單與工具列的動作已經消失,把它們從快捷鍵表中清掉 + # The old menu and toolbar actions are gone; drop them from the shortcut table + reload_bound_shortcuts() + + +def _rebuild_menu_bar(main_window) -> None: + """重建選單列 / Build the menu bar again.""" + from je_editor.pyside_ui.main_ui.menu.set_menu_bar import set_menu_bar + # setMenuBar 會接手並刪掉舊的那一個 / setMenuBar takes over and deletes the old one + set_menu_bar(main_window) + + +def _rebuild_toolbar(main_window) -> None: + """ + 重建工具列 + Build the toolbar again. + + 先等舊工具列的背景工作結束:git 分支掃描完成後要去填那個下拉選單,而下拉選單 + 正要被刪掉。不等的話 Qt 會直接讓程序中止。 + The outgoing toolbar's background work is waited for first: the git branch + scan finishes by filling that combo box, and the combo box is about to be + deleted. Without the wait, Qt aborts the process. + """ + from je_editor.pyside_ui.main_ui.toolbar.toolbar_builder import ( + build_toolbar, stop_background_threads + ) + stop_background_threads() + old = getattr(main_window, "main_toolbar", None) + if old is not None: + main_window.removeToolBar(old) + old.deleteLater() + build_toolbar(main_window) + + +def _retranslate_tabs(main_window, previous: Dict[str, str], current: Dict[str, str]) -> None: + """ + 重新標示分頁標題,檔名保持原樣 + Relabel the tab titles, leaving file names alone. + """ + tab_widget = getattr(main_window, "tab_widget", None) + if tab_widget is None: + return + for index in range(tab_widget.count()): + tab_widget.setTabText( + index, translated_again(tab_widget.tabText(index), previous, current, TITLE_KEYS)) + _retranslate_widget(tab_widget.widget(index)) + + +def _retranslate_docks(main_window, previous: Dict[str, str], current: Dict[str, str]) -> None: + """重新標示浮動視窗的標題與內容 / Relabel the docks and what they hold.""" + from PySide6.QtWidgets import QDockWidget + for dock in main_window.findChildren(QDockWidget): + dock.setWindowTitle( + translated_again(dock.windowTitle(), previous, current, TITLE_KEYS)) + _retranslate_widget(dock.widget()) + + +def _retranslate_widget(widget) -> None: + """ + 請一個元件重新標示自己 + Ask a widget to relabel itself. + + 有 ``retranslate`` 就呼叫它。沒有的元件維持原本的字,直到重新開啟為止——這比 + 硬把它拆掉重建安全,因為那會連同它記著的狀態一起丟掉。 + A widget with a ``retranslate`` is asked to. One without keeps its wording + until it is reopened, which is safer than tearing it down and rebuilding it: + that would throw away whatever state it is holding. + """ + retranslate = getattr(widget, "retranslate", None) + if callable(retranslate): + retranslate() diff --git a/je_editor/pyside_ui/main_ui/test_panel/test_panel_widget.py b/je_editor/pyside_ui/main_ui/test_panel/test_panel_widget.py index 9c112bba..9e0a39d9 100644 --- a/je_editor/pyside_ui/main_ui/test_panel/test_panel_widget.py +++ b/je_editor/pyside_ui/main_ui/test_panel/test_panel_widget.py @@ -187,6 +187,31 @@ def results(self) -> list[PytestResult]: """取得目前顯示的結果 / The results currently listed.""" return list(self._results) + def retranslate(self) -> None: + """ + 換語言後重新標示自己 + Relabel after the language changes. + + 面板記著上一次的測試結果,因此只換文字再重畫清單,不重建整個面板。 + The panel is holding the last run's results, so it relabels and redraws + rather than being rebuilt. + """ + word = language_wrapper.language_word_dict + self.run_button.setText(word.get("test_panel_run")) + self.run_selected_button.setText(word.get("test_panel_run_selected")) + self.rerun_failures_button.setText(word.get("test_panel_rerun_failures")) + self.filter_edit.setPlaceholderText(word.get("test_panel_filter_placeholder")) + self.coverage_check.setText(word.get("test_panel_coverage")) + self.traceback_view.setPlaceholderText(word.get("test_panel_traceback_placeholder")) + self.result_tree.setHeaderLabels([ + word.get("test_panel_col_outcome"), + word.get("test_panel_col_test"), + word.get("test_panel_col_file"), + ]) + if not self._results: + self.status_label.setText(word.get("test_panel_ready")) + self._render_items() + def start_run(self, node_ids: list[str] | None = None) -> bool: """ 啟動一次測試執行 diff --git a/je_editor/pyside_ui/main_ui/todo_panel/todo_panel_widget.py b/je_editor/pyside_ui/main_ui/todo_panel/todo_panel_widget.py index 4964d7b5..d3f675c1 100644 --- a/je_editor/pyside_ui/main_ui/todo_panel/todo_panel_widget.py +++ b/je_editor/pyside_ui/main_ui/todo_panel/todo_panel_widget.py @@ -155,6 +155,28 @@ def _on_scanned(self, items: list) -> None: self.refresh_button.setEnabled(True) self._render_items() + def retranslate(self) -> None: + """ + 換語言後重新標示自己 + Relabel after the language changes. + + 標籤篩選的第一項是「所有標籤」,其餘是掃到的標籤本身(TODO、FIXME),那些 + 不是翻譯來的,不能動。 + The first entry in the tag filter is "all tags" and the rest are the tags + as found in the code (TODO, FIXME); those did not come from a translation + and are left alone. + """ + word = language_wrapper.language_word_dict + self.refresh_button.setText(word.get("todo_panel_refresh")) + self.tag_filter.setItemText(0, word.get("todo_panel_all_tags")) + self.result_tree.setHeaderLabels([ + word.get("todo_panel_col_tag"), + word.get("todo_panel_col_message"), + word.get("todo_panel_col_file"), + word.get("todo_panel_col_line"), + ]) + self._render_items() + def _render_items(self) -> None: """依篩選條件重建清單 / Rebuild the tree for the current filter.""" visible = self.visible_items() diff --git a/je_editor/start_editor.py b/je_editor/start_editor.py index c93ec76a..c5911206 100644 --- a/je_editor/start_editor.py +++ b/je_editor/start_editor.py @@ -7,6 +7,7 @@ from je_editor.pyside_ui.main_ui.main_editor import EditorMain from je_editor.plugins.plugin_loader import load_external_plugins +from je_editor.utils.browser.chromium_flags import quiet_chromium_logging def start_editor(debug_mode: bool = False) -> None: @@ -18,6 +19,12 @@ def start_editor(debug_mode: bool = False) -> None: :param debug_mode: 是否啟用除錯模式 / whether to enable debug mode """ + # 壓下內嵌 Chromium 的啟動雜訊;標準錯誤會被導進輸出面板,那些訊息會直接 + # 出現在使用者眼前 + # Quiet the embedded Chromium's startup noise: standard error is piped into + # the output pane, so its messages land in front of the user + quiet_chromium_logging() + # 嘗試取得現有的 QCoreApplication 實例 # Try to get an existing QCoreApplication instance new_editor = QCoreApplication.instance() diff --git a/je_editor/utils/browser/__init__.py b/je_editor/utils/browser/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/je_editor/utils/browser/chromium_flags.py b/je_editor/utils/browser/chromium_flags.py new file mode 100644 index 00000000..9020fb7b --- /dev/null +++ b/je_editor/utils/browser/chromium_flags.py @@ -0,0 +1,64 @@ +""" +壓下內嵌瀏覽器的啟動雜訊 +Quiet the embedded browser's startup noise. + +QtWebEngine 裡包著一整個 Chromium,它會把自己的訊息寫到標準錯誤,例如探測硬體 +視訊編碼器失敗的 ``mf_video_encoder_util.cc: Set output type failed``。那些訊息 +對編輯器的使用者沒有意義——Chromium 自己會退回可用的路徑——但這個編輯器把標準 +錯誤導進輸出面板,於是它們就出現在使用者眼前。 +QtWebEngine embeds a whole Chromium, which writes its own messages to standard +error -- such as ``mf_video_encoder_util.cc: Set output type failed`` when it +probes a hardware video encoder that will not take the settings. None of that +means anything to someone using a text editor, since Chromium falls back on its +own, but this editor pipes standard error into its output pane, so it lands in +front of the user. + +純邏輯:只組出環境變數的值,不啟動任何東西。 +Pure logic: it only assembles an environment variable, starting nothing. +""" +from __future__ import annotations + +import os +from typing import Dict, Optional + +# Chromium 讀取旗標的環境變數 / The variable Chromium reads its flags from +FLAGS_VARIABLE = "QTWEBENGINE_CHROMIUM_FLAGS" +# 只保留致命錯誤:0=INFO、1=WARNING、2=ERROR、3=FATAL +# Keep fatal messages only: 0=INFO, 1=WARNING, 2=ERROR, 3=FATAL +QUIET_FLAG = "--log-level=3" + + +def flags_with_quiet_logging(existing: Optional[str]) -> str: + """ + 把「安靜」旗標加進既有的旗標字串 + Add the quiet flag to whatever flags are already set. + + 使用者自己設過 log level 就不動它:他顯然是想看那些訊息。 + A log level the user set is left alone: they evidently want to see them. + + :param existing: 目前的旗標字串 / the flags as they stand + :return: 加上之後的旗標字串 / the flags afterwards + """ + current = (existing or "").strip() + if "--log-level" in current: + return current + return f"{current} {QUIET_FLAG}".strip() + + +def quiet_chromium_logging(environment: Optional[Dict[str, str]] = None) -> str: + """ + 設定環境變數,讓內嵌的 Chromium 少說話 + Set the environment variable that keeps the embedded Chromium quiet. + + 必須在 QtWebEngine 初始化之前呼叫,也就是第一個瀏覽器分頁被建立之前。 + This has to run before QtWebEngine initialises, which is before the first + browser tab is created. + + :param environment: 要設定的環境,省略時用行程本身的 + / the environment to set, or the process's own when omitted + :return: 設定後的旗標字串 / the flags as they now stand + """ + target = os.environ if environment is None else environment + flags = flags_with_quiet_logging(target.get(FLAGS_VARIABLE)) + target[FLAGS_VARIABLE] = flags + return flags diff --git a/je_editor/utils/multi_language/retranslate_text.py b/je_editor/utils/multi_language/retranslate_text.py new file mode 100644 index 00000000..a8f0cea7 --- /dev/null +++ b/je_editor/utils/multi_language/retranslate_text.py @@ -0,0 +1,97 @@ +""" +把已經顯示出來的文字換成另一個語言的說法 +Move text that is already on screen to another language's wording. + +換語言時,畫面上有些字是翻譯來的(分頁標題「編輯器」、面板標題),有些不是(檔 +名、分支名稱)。全部重設會把檔名蓋掉,全部不動則等於沒換語言,因此要能分辨。 +When the language changes some of what is on screen came from a translation -- +a tab reading "Editor", a panel's title -- and some did not: file names, branch +names. Replacing everything would overwrite the file names and replacing nothing +would not be a language change at all, so the two have to be told apart. + +做法是反查:這段文字剛好等於舊語言的某個字串嗎?是的話就換成新語言的同一個鍵。 +The way to tell is to look it up backwards: is this text exactly one of the old +language's strings? If so it becomes the new language's version of that key. + +純邏輯,不含 Qt。 +Pure logic, with no Qt. +""" +from __future__ import annotations + +from typing import Dict, Iterable, Optional + +# 分頁與浮動視窗的標題可能來自這些鍵;只有這些會被換掉,其餘(檔名、分支名稱)不動 +# The keys a tab or dock title may have come from. Only these are replaced, and +# everything else -- file names, branch names -- is left alone. +TITLE_KEYS = ( + "tab_name_editor", + "tab_name_web_browser", + "tab_name_frontengine", + "tab_menu_editor_tab_name", + "tab_menu_web_tab_name", + "tab_menu_frontengine_tab_name", + "tab_menu_console_widget_tab_name", + "tab_menu_todo_panel_tab_name", + "tab_menu_test_panel_tab_name", + "tab_menu_problems_panel_tab_name", + "tab_menu_outline_panel_tab_name", + "tab_menu_diff_against_head_name", + "tab_menu_diff_against_staged_name", + "search_replace_tab_title", + "plugin_browser_tab_name", + "dock_editor_title", + "dock_browser_title", + "dock_frontengine_title", + "chat_ui_dock_label", + "editor_code_result", + "editor_format_check", + "editor_terminal", + "editor_debugger_input_title_label", +) + + +def key_for_text(text: str, words: Dict[str, str], + candidates: Optional[Iterable[str]] = None) -> Optional[str]: + """ + 反查一段文字是哪個鍵翻譯出來的 + Look up which key a piece of text was translated from. + + 要限定候選的鍵:英文裡「Editor」同時是分頁名稱、浮動視窗標題與一個沒有翻譯的 + 選單代號,盲目反查很容易挑到那個沒翻譯的,結果就是換了語言卻沒有變。 + The candidate keys have to be narrowed: in English "Editor" is a tab name, a + dock title, and an untranslated menu identifier all at once, and an unguided + lookup readily lands on the untranslated one — which looks like the language + change simply not working. + + :param text: 畫面上的文字 / the text on screen + :param words: 當時使用的字典 / the dictionary in use at the time + :param candidates: 只考慮這些鍵;省略時全部都算 / only these keys, or all when omitted + :return: 對應的鍵,找不到時為 ``None`` / the key, or ``None`` + """ + if not text: + return None + keys = list(candidates) if candidates is not None else list(words) + matches = sorted(key for key in keys if words.get(key) == text) + return matches[0] if matches else None + + +def translated_again(text: str, previous: Dict[str, str], current: Dict[str, str], + candidates: Optional[Iterable[str]] = None) -> str: + """ + 把一段文字換成新語言的說法 + Move a piece of text to the new language's wording. + + 認不出來的文字原樣保留——那多半是檔名或分支名稱,不該被翻譯。 + Text that cannot be placed is left exactly as it is: it is most likely a file + or branch name, and translating that would be wrong. + + :param text: 目前的文字 / the text as it stands + :param previous: 換語言之前的字典 / the dictionary from before the change + :param current: 換語言之後的字典 / the dictionary now in use + :param candidates: 只考慮這些鍵 / only these keys are considered + :return: 換過的文字 / the text afterwards + """ + key = key_for_text(text, previous, candidates) + if key is None: + return text + return current.get(key, text) diff --git a/test/test_chromium_flags.py b/test/test_chromium_flags.py new file mode 100644 index 00000000..67089b84 --- /dev/null +++ b/test/test_chromium_flags.py @@ -0,0 +1,48 @@ +"""Tests that the embedded browser's startup noise is quieted.""" +from __future__ import annotations + +from je_editor.utils.browser.chromium_flags import ( + FLAGS_VARIABLE, QUIET_FLAG, flags_with_quiet_logging, quiet_chromium_logging +) + + +class TestQuietingChromium: + """ + QtWebEngine embeds a whole Chromium that writes its own messages to standard + error, and this editor pipes standard error into its output pane -- so a + hardware video encoder it probed and rejected ends up in front of the user. + """ + + def test_the_flag_is_added_when_there_is_nothing(self): + assert flags_with_quiet_logging(None) == QUIET_FLAG + + def test_the_flag_is_added_when_there_is_nothing_but_space(self): + assert flags_with_quiet_logging(" ") == QUIET_FLAG + + def test_existing_flags_are_kept(self): + result = flags_with_quiet_logging("--disable-gpu") + assert "--disable-gpu" in result and QUIET_FLAG in result + + def test_a_log_level_the_user_chose_is_left_alone(self): + # Someone who set a level evidently wants to see those messages. + assert flags_with_quiet_logging("--log-level=0") == "--log-level=0" + + def test_a_log_level_among_other_flags_is_left_alone(self): + flags = "--disable-gpu --log-level=1" + assert flags_with_quiet_logging(flags) == flags + + def test_the_environment_is_set(self): + environment: dict = {} + quiet_chromium_logging(environment) + assert environment[FLAGS_VARIABLE] == QUIET_FLAG + + def test_setting_it_twice_does_not_repeat_the_flag(self): + environment: dict = {} + quiet_chromium_logging(environment) + quiet_chromium_logging(environment) + assert environment[FLAGS_VARIABLE].count("--log-level") == 1 + + def test_what_was_already_there_survives(self): + environment = {FLAGS_VARIABLE: "--single-process"} + quiet_chromium_logging(environment) + assert "--single-process" in environment[FLAGS_VARIABLE] diff --git a/test/test_retranslate.py b/test/test_retranslate.py new file mode 100644 index 00000000..0f0fd3b1 --- /dev/null +++ b/test/test_retranslate.py @@ -0,0 +1,204 @@ +"""Tests that changing the language relabels the interface without a restart.""" +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +import pytest +from PySide6.QtWidgets import QMainWindow, QMenuBar, QTabWidget, QWidget + +from je_editor.utils.multi_language.english import english_word_dict +from je_editor.utils.multi_language.multi_language_wrapper import language_wrapper +from je_editor.utils.multi_language.retranslate_text import ( + TITLE_KEYS, key_for_text, translated_again +) +from je_editor.utils.multi_language.simplified_chinese import simplified_chinese_word_dict + + +class TestLookingUpTranslatedText: + """ + Some words on screen came from a translation and some are file names. + Replacing everything would overwrite the file names. + """ + + def test_a_translated_word_is_found(self): + assert key_for_text("Editor", {"tab_name_editor": "Editor"}) == "tab_name_editor" + + def test_a_file_name_is_not_found(self): + assert key_for_text("main.py", {"tab_name_editor": "Editor"}) is None + + def test_nothing_is_found_for_nothing(self): + assert key_for_text("", {"tab_name_editor": "Editor"}) is None + + def test_a_shared_translation_answers_predictably(self): + words = {"b_key": "Same", "a_key": "Same"} + assert key_for_text("Same", words) == "a_key" + + def test_only_the_offered_keys_are_considered(self): + # "Editor" is a tab name, a dock title and an untranslated menu + # identifier all at once; without narrowing, the untranslated one wins + # and the language change looks like it did nothing. + assert key_for_text( + "Editor", english_word_dict, ["tab_name_editor"]) == "tab_name_editor" + + def test_a_key_outside_the_offered_set_is_not_matched(self): + assert key_for_text("Editor", english_word_dict, ["toolbar_run"]) is None + + def test_the_title_keys_all_exist(self): + missing = [key for key in TITLE_KEYS if key not in english_word_dict] + assert missing == [] + + def test_a_translated_word_moves_language(self): + assert translated_again( + "Editor", {"tab_name_editor": "Editor"}, + {"tab_name_editor": "编辑器"}) == "编辑器" + + def test_a_file_name_is_left_alone(self): + assert translated_again( + "main.py", {"tab_name_editor": "Editor"}, + {"tab_name_editor": "编辑器"}) == "main.py" + + def test_a_key_the_new_language_lacks_stays_put(self): + assert translated_again("Editor", {"tab_name_editor": "Editor"}, {}) == "Editor" + + def test_the_real_dictionaries_move_a_tab_title(self): + assert translated_again( + english_word_dict["tab_name_editor"], + english_word_dict, simplified_chinese_word_dict, TITLE_KEYS + ) == simplified_chinese_word_dict["tab_name_editor"] + + +@pytest.fixture() +def window(qapp, qtbot): + """A window with the pieces retranslating touches.""" + from PySide6.QtGui import QFontDatabase + main_window = QMainWindow() + qtbot.addWidget(main_window) + main_window.menu = QMenuBar() + main_window.font_database = QFontDatabase() + main_window.tab_widget = QTabWidget() + main_window.setCentralWidget(main_window.tab_widget) + main_window.encoding = "utf-8" + yield main_window + # Building a toolbar starts a git scan that fills its combo box. The real + # window waits for those when it closes; a bare one has to do it here, or Qt + # aborts the process when the widget goes away underneath a running scan. + from je_editor.pyside_ui.main_ui.toolbar.toolbar_builder import stop_background_threads + stop_background_threads() + + +@pytest.fixture() +def english(qapp): + """Start each test in English, and leave the wrapper as it was found.""" + before = language_wrapper.language + language_wrapper.reset_language("English") + yield + language_wrapper.reset_language(before) + + +class TestRetranslatingTheWindow: + def test_a_translated_tab_title_changes(self, window, english): + from je_editor.pyside_ui.main_ui.retranslate import retranslate_ui + window.tab_widget.addTab(QWidget(), english_word_dict["tab_name_editor"]) + previous = dict(language_wrapper.language_word_dict) + language_wrapper.reset_language("Simplified_Chinese") + retranslate_ui(window, previous) + assert window.tab_widget.tabText(0) == \ + simplified_chinese_word_dict["tab_name_editor"] + + def test_a_file_name_tab_is_left_alone(self, window, english): + from je_editor.pyside_ui.main_ui.retranslate import retranslate_ui + window.tab_widget.addTab(QWidget(), "main.py") + previous = dict(language_wrapper.language_word_dict) + language_wrapper.reset_language("Japanese") + retranslate_ui(window, previous) + assert window.tab_widget.tabText(0) == "main.py" + + def test_the_window_title_changes(self, window, english): + from je_editor.pyside_ui.main_ui.retranslate import retranslate_ui + previous = dict(language_wrapper.language_word_dict) + language_wrapper.reset_language("Simplified_Chinese") + retranslate_ui(window, previous) + assert window.windowTitle() == \ + simplified_chinese_word_dict["application_name"] + + def test_the_menu_bar_is_rebuilt_in_the_new_language(self, window, english): + from je_editor.pyside_ui.main_ui.retranslate import retranslate_ui + previous = dict(language_wrapper.language_word_dict) + language_wrapper.reset_language("Simplified_Chinese") + retranslate_ui(window, previous) + titles = [action.text() for action in window.menu.actions()] + assert simplified_chinese_word_dict["file_menu_label"] in titles + + def test_the_toolbar_is_rebuilt(self, window, english): + from je_editor.pyside_ui.main_ui.retranslate import retranslate_ui + previous = dict(language_wrapper.language_word_dict) + language_wrapper.reset_language("Japanese") + retranslate_ui(window, previous) + assert window.main_toolbar is not None + + def test_a_panel_is_asked_to_relabel_itself(self, window, english): + from je_editor.pyside_ui.main_ui.retranslate import retranslate_ui + panel = MagicMock(spec=["retranslate"]) + holder = QWidget() + holder.retranslate = panel.retranslate + window.tab_widget.addTab(holder, "Panel") + previous = dict(language_wrapper.language_word_dict) + language_wrapper.reset_language("Japanese") + retranslate_ui(window, previous) + panel.retranslate.assert_called_once() + + def test_a_widget_without_one_is_tolerated(self, window, english): + from je_editor.pyside_ui.main_ui.retranslate import retranslate_ui + window.tab_widget.addTab(QWidget(), "Plain") + previous = dict(language_wrapper.language_word_dict) + language_wrapper.reset_language("Japanese") + retranslate_ui(window, previous) + assert window.tab_widget.count() == 1 + + +class TestChangingLanguageNeedsNoRestart: + def test_choosing_a_language_relabels_at_once(self, window, english): + from je_editor.pyside_ui.main_ui.menu.language_menu.build_language_server import ( + set_language + ) + window.tab_widget.addTab(QWidget(), english_word_dict["tab_name_editor"]) + with patch.dict( + "je_editor.pyside_ui.main_ui.save_settings.user_setting_file.user_setting_dict", + {}, clear=False + ): + set_language("Simplified_Chinese", window) + assert window.tab_widget.tabText(0) == \ + simplified_chinese_word_dict["tab_name_editor"] + + def test_the_choice_is_remembered(self, window, english): + from je_editor.pyside_ui.main_ui.menu.language_menu.build_language_server import ( + set_language + ) + from je_editor.pyside_ui.main_ui.save_settings.user_setting_file import ( + user_setting_dict + ) + saved = user_setting_dict.get("language") + try: + set_language("Japanese", window) + assert user_setting_dict["language"] == "Japanese" + finally: + user_setting_dict["language"] = saved + + +class TestPanelsRelabelThemselves: + @pytest.mark.parametrize("panel_import,builder", [ + ("je_editor.pyside_ui.main_ui.problems_panel.problems_panel_widget:" + "ProblemsPanelWidget", "problems"), + ("je_editor.pyside_ui.main_ui.outline_panel.outline_panel_widget:" + "OutlinePanelWidget", "outline"), + ]) + def test_the_panel_moves_language(self, qapp, qtbot, english, panel_import, builder): + module_name, class_name = panel_import.split(":") + module = __import__(module_name, fromlist=[class_name]) + panel = getattr(module, class_name)(main_window=None) + qtbot.addWidget(panel) + english_label = panel.refresh_button.text() + language_wrapper.reset_language("Simplified_Chinese") + panel.retranslate() + assert panel.refresh_button.text() != english_label + panel.close() diff --git a/test/test_toolbar_actions.py b/test/test_toolbar_actions.py index 12c9654f..613b9083 100644 --- a/test/test_toolbar_actions.py +++ b/test/test_toolbar_actions.py @@ -4,7 +4,9 @@ import pytest from PySide6.QtWidgets import QMainWindow -from je_editor.pyside_ui.main_ui.toolbar.toolbar_builder import build_toolbar +from je_editor.pyside_ui.main_ui.toolbar.toolbar_builder import ( + build_toolbar, stop_background_threads +) from je_editor.utils.shortcuts.shortcut_registry import ( WINDOW_SHORTCUTS, normalise_sequence ) @@ -17,6 +19,8 @@ def toolbar_window(qapp, qtbot): qtbot.addWidget(window) build_toolbar(window) yield window + # The git scan the toolbar starts fills a combo box that is about to go away. + stop_background_threads() @pytest.mark.usefixtures("qapp")