From 30aee7ab494e7b9a4d0a342a83a5aabfa1f7435b Mon Sep 17 00:00:00 2001 From: h465855hgg <3382198490@qq.com> Date: Thu, 12 Nov 2026 19:47:52 +0800 Subject: [PATCH 1/3] i18n: make the GUI translatable and add a Simplified Chinese interface English remains the source language, so a build without a catalogue behaves as before. Every user-visible GUI string now goes through tr() in member functions, or QCoreApplication::translate() in free functions and classes without Q_OBJECT, with QT_TRANSLATE_NOOP for static display arrays: the menu bar and menus, dialogs, labels, buttons, table headers, tooltips, placeholders, file dialog filters and status texts are covered. gui/i18n.cpp loads the catalogue from the translations directory beside the executable or from the built-in resource. The interface language comes from the new Settings > Language page (ui/language: follow the system, English, Simplified Chinese) and falls back to the desktop locale; a change applies on the next start. translations/cheatengine_zh_CN.ts is compiled with lrelease and embedded through qt_add_translations when the Qt6 Linguist tools are available, so the .deb and the AppImage carry Chinese, and a .qm dropped next to the binary still overrides the built-in copy. Building without the Linguist tools keeps working, in English, with a status line. The desktop file and the AppStream metadata carry Chinese text. CI installs qt6-tools-dev and qt6-l10n-tools, and tests the catalogues with the new tools/check-translations.py, which fails on an unfinished message or a dropped %1/%n placeholder; tools/ci-check.sh runs the same check. docs/I18N.md documents the workflow for translators and how to add a language. The screenshot review renders another interface language with CE_LANG=zh_CN. Not translated on purpose: cescan terminal output, messages from the Qt-free cecore library, the tutorial games (their steps quote the code in the game), Lua and agent tool names, and text stored in cheat tables. --- .github/workflows/build.yml | 5 + .github/workflows/release.yml | 2 + CHANGELOG.md | 8 + CMakeLists.txt | 23 + README.md | 1 + docs/I18N.md | 90 + docs/PROGRESS.md | 19 + gui/advancedoptions.cpp | 24 +- gui/agent_panel.cpp | 112 +- gui/branchmapper.cpp | 33 +- gui/breakpointlist.cpp | 29 +- gui/changeaddressdialog.cpp | 47 +- gui/codefinder.cpp | 81 +- gui/codereferences.cpp | 72 +- gui/debuggerwindow.cpp | 202 +- gui/elfinspector.cpp | 40 +- gui/filepatcher.cpp | 70 +- gui/findstaticswindow.cpp | 16 +- gui/formdesigner.cpp | 88 +- gui/gdbconnectiondialog.cpp | 33 +- gui/globalhotkeys.cpp | 2 +- gui/graphicalmemoryview.cpp | 28 +- gui/guest_scan_dialog.cpp | 67 +- gui/heapregions.cpp | 15 +- gui/i18n.cpp | 86 + gui/i18n.hpp | 20 + gui/luaconsole.cpp | 20 +- gui/main.cpp | 49 +- gui/mainwindow.cpp | 1229 +-- gui/memorybrowser.cpp | 342 +- gui/memoryfill.cpp | 28 +- gui/memoryregions.cpp | 12 +- gui/memviewpreferences.cpp | 38 +- gui/modulelist.cpp | 12 +- gui/monodissector.cpp | 48 +- gui/overlay.cpp | 2 +- gui/pointerscan_dialog.cpp | 86 +- gui/processlistdialog.cpp | 30 +- gui/registereditor.cpp | 62 +- gui/scripteditor.cpp | 96 +- gui/secret_store.cpp | 12 +- gui/settingsdialog.cpp | 243 +- gui/settingsdialog.hpp | 4 + gui/stackview.cpp | 18 +- gui/structuredissector.cpp | 156 +- gui/theme.cpp | 5 +- gui/threadlist.cpp | 6 +- gui/tracerwindow.cpp | 79 +- packaging/cheatengine.desktop | 1 + ...ithub.wleeaf.CheatEngineLinux.metainfo.xml | 9 + test/gui_lifecycle_smoke.cpp | 55 + test/gui_visual_review.hpp | 38 +- tools/check-translations.py | 69 + tools/ci-check.sh | 4 + translations/cheatengine_zh_CN.ts | 8737 +++++++++++++++++ 55 files changed, 10990 insertions(+), 1713 deletions(-) create mode 100644 docs/I18N.md create mode 100644 gui/i18n.cpp create mode 100644 gui/i18n.hpp create mode 100644 tools/check-translations.py create mode 100644 translations/cheatengine_zh_CN.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a4648262..f2a1914a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,6 +28,9 @@ jobs: - name: Check coverage matrix is in sync run: python3 tools/coverage_matrix.py --check + - name: Check translation catalogues are complete + run: python3 tools/check-translations.py + # Lua 5.3 is vendored under third_party/lua and built by CMake, so no # upstream checkout is needed — the repo is self-contained. - name: Install apt dependencies @@ -51,6 +54,8 @@ jobs: qemu-system-arm \ mono-runtime-sgen mono-mcs libmonosgen-2.0-1 \ qt6-base-dev \ + qt6-tools-dev \ + qt6-l10n-tools \ libcapstone-dev \ zlib1g-dev \ libdw-dev \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e3435e35..6d3f0e4a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,6 +59,8 @@ jobs: qemu-system-arm \ mono-runtime-sgen mono-mcs libmonosgen-2.0-1 \ qt6-base-dev \ + qt6-tools-dev \ + qt6-l10n-tools \ libcapstone-dev \ zlib1g-dev \ libdw-dev \ diff --git a/CHANGELOG.md b/CHANGELOG.md index f348921f..df6cad14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,14 @@ reimplementation of Cheat Engine). jump range of the hook. The Auto Assembler now says so instead of reporting an out-of-range operand or a bare allocation failure. Wine 11.0, legacy Wine and Proton are unaffected. +- The GUI is translated with Qt's translation tools and ships with a Simplified + Chinese (zh_CN) catalogue: every menu, dialog, label and message is marked for + translation, Settings > Language offers follow the system / English / 简体中文, + and the catalogue is embedded in the binaries. A `.qm` file beside the + executable or under `translations/` overrides the built-in copy. English + remains the source language, so nothing changes without a catalogue. The + terminal (`cescan`), messages from the Qt-free core library, and the tutorial + games stay in English. See `docs/I18N.md`. --- diff --git a/CMakeLists.txt b/CMakeLists.txt index c4a8f21c..6ec930d8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -368,6 +368,7 @@ if(Qt6_FOUND) gui/resources.qrc gui/main.cpp gui/theme.cpp + gui/i18n.cpp gui/mainwindow.cpp gui/processlistdialog.cpp gui/gdbconnectiondialog.cpp @@ -633,6 +634,28 @@ if(Qt6_FOUND) else() message(STATUS "Global hotkeys: Wayland portal backend disabled (Qt6::DBus not found)") endif() + + # ── Translations ── + # English is the source language; the catalogue below is embedded in the + # binary. A .qm dropped next to the executable or under translations/ + # overrides the built-in copy, so a translation can be tried without a + # rebuild. LinguistTools comes from qt6-tools-dev and qt6-l10n-tools on + # Debian/Ubuntu; without them the build still works, in English. + find_package(Qt6 COMPONENTS LinguistTools QUIET) + if(Qt6LinguistTools_FOUND) + # The include path lets lupdate resolve the headers, so member tr() + # calls get the namespaced context moc uses at runtime + # (ce::gui::MainWindow, not MainWindow). + qt_add_translations(cheatengine TS_FILES translations/cheatengine_zh_CN.ts + INCLUDE_DIRECTORIES ${CMAKE_SOURCE_DIR}) + if(TARGET gui_lifecycle_smoke) + # The screenshots run honours CE_LANG, so the .qm has to exist. + add_dependencies(gui_lifecycle_smoke cheatengine_lrelease) + endif() + message(STATUS "Translations: enabled (zh_CN)") + else() + message(STATUS "Translations: disabled (Qt6 LinguistTools not found)") + endif() else() message(STATUS "Qt6 not found — skipping GUI build") endif() diff --git a/README.md b/README.md index c7c58b0e..fe351205 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ See [AI agents](docs/MCP.md). - [Lua scripting reference](docs/SCRIPTING.md) and [examples](examples) - [Supported targets and features](docs/COMPATIBILITY.md) +- [Translating the interface](docs/I18N.md) - [Changelog](CHANGELOG.md) - [Contributing](CONTRIBUTING.md) diff --git a/docs/I18N.md b/docs/I18N.md new file mode 100644 index 00000000..3f1c78da --- /dev/null +++ b/docs/I18N.md @@ -0,0 +1,90 @@ +# Translating the interface + +The GUI is built with Qt6, so its interface can be translated with Qt's own +translation tools. English is the source language: the strings in the code are +the English text, and a catalogue adds other languages. Without a catalogue the +program looks exactly as it always has. + +Simplified Chinese (zh_CN) ships in `translations/cheatengine_zh_CN.ts` and is +compiled into the binary. The catalogue is embedded as a resource, so the `.deb` +and the AppImage carry it without extra files. + +## Choosing the language + +- With no setting, the interface follows the desktop locale (`LANG`, e.g. + `zh_CN.UTF-8`). +- **Settings > Language** overrides it: follow the system, English, or 简体中文. + The choice is stored in `ui/language` and takes effect on the next start. +- A `.qm` file dropped beside the executable, or under `translations/`, wins over + the built-in catalogue. That lets a translator try a file without a rebuild. + +The Qt standard strings (stock dialog buttons, native file dialogs) come from +Qt's own translations. Linux distributions package those separately +(`qt6-translations-l10n` on Debian/Ubuntu, `qttranslations` on Fedora); install +it for translated standard buttons. The application's own strings work either +way. + +## What is translated + +Everything the GUI shows: menus, actions, window and tab titles, labels, +buttons, check boxes, tooltips, placeholders, table headers, dialogs, message +boxes, file dialog filters, and the empty-state hints. Numbers, addresses and +values are formatted as before. + +Not covered, deliberately: + +- `cescan` and other terminal output, which has no interface language. +- Messages produced by the Qt-free `cecore` library (scanner, debugger and + process errors) are shown as they are, in English. +- The tutorial games (`cetutorial`, `cegtutorial`) keep their English text: the + tutorial steps describe exact code and instructions in the game. +- Text stored in cheat tables and Lua scripts is user data, never translated. +- Lua and agent tool names are part of the scripting API, never translated. + +## For translators + +Install the Qt translation tools: `qt6-tools-dev` and `qt6-l10n-tools` on +Debian/Ubuntu. CMake then also builds the catalogues; without them the GUI is +built in English and a status line says so. + +Refresh the catalogue from the sources: + +```bash +cmake --build build --target update_translations +``` + +That runs `lupdate` over the GUI sources and merges new strings into the `.ts` +files, keeping existing translations. Translate with Qt Linguist: + +```bash +linguist translations/cheatengine_zh_CN.ts +``` + +or with any editor: the file is plain XML. `cmake --build build` compiles the +catalogue with `lrelease`. + +Add a language: + +1. Create `translations/cheatengine_.ts`, e.g. `cheatengine_de.ts` + (`lupdate` knows the language from the file name). +2. Add it to the `qt_add_translations` call in `CMakeLists.txt`. +3. Add the language to `SettingsDialog::buildLanguagePage` so it can be picked. +4. Update this file and the desktop file / AppStream metadata if the release + notes should mention it. + +Notes for a good translation: + +- Keep `%1`, `%2` and `%n` placeholders exactly as they are. +- Keep HTML tags, URLs, and product names (`Cheat Engine`, `Lua`, `ceserver`, + `GDB`, `QEMU`, `Mono`) as they are. +- Menu items that open a window end in `...`; keep the three dots. +- Top-level menus carry a `&` mnemonic; use `文件(&F)` style. +- The screenshot review can render another language; it reads `CE_LANG`: + + ```bash + CE_LANG=zh_CN QT_QPA_PLATFORM=offscreen ./build/gui_lifecycle_smoke \ + --screenshots build/i18n-screenshots + ``` + + The review finds windows and menu items by their English source text, so it + still drives the UI, while every capture is rendered in the chosen language. diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 01371b5e..7c3f390a 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -59,6 +59,25 @@ example owned by root after a `sudo -E` run), scans fell back to `/tmp`, which is RAM-backed on many desktops, so a large scan really did fill it. The fallback is now the disk-backed `/var/tmp`. +## Simplified Chinese interface and a translation pipeline: 12 November 2026 + +The GUI is fully translatable and ships with a Simplified Chinese (zh_CN) +catalogue. Every user-visible string in `gui/` is wrapped in `tr()` (or +`QCoreApplication::translate` where there is no class context), Settings has a +new Language page (follow the system, English, 简体中文), and the catalogue is +embedded in the binary so the `.deb` and the AppImage carry it. A `.qm` dropped +beside the executable or under `translations/` overrides the built-in copy, so a +translation can be tested without rebuilding; the loader moved into +`gui/i18n.cpp` so the screenshot smoke can use it too. + +The catalogue is generated with Qt's `lupdate`/`lrelease`; CI installs +`qt6-tools-dev` and `qt6-l10n-tools` so the release build compiles it. English +stays the source language, so with no catalogue the interface is unchanged. +Deliberately out of scope: `cescan` terminal output, messages from the Qt-free +`cecore` library, and the tutorial games, which quote exact code and step text. +`docs/I18N.md` documents the workflow for translators; the screenshot review can +render another language with `CE_LANG=zh_CN`. + ## GUI polish from a full screenshot review: 13 October 2026 `gui_lifecycle_smoke --screenshots DIR` now renders about 180 PNGs: every diff --git a/gui/advancedoptions.cpp b/gui/advancedoptions.cpp index 3abd071b..58efbe81 100644 --- a/gui/advancedoptions.cpp +++ b/gui/advancedoptions.cpp @@ -16,22 +16,22 @@ namespace ce::gui { AdvancedOptionsWindow::AdvancedOptionsWindow(ce::ProcessHandle* proc, QWidget* parent) : QMainWindow(parent), proc_(proc) { - setWindowTitle("Code list/Pause"); // CE AdvancedOptionsUnit caption + setWindowTitle(tr("Code list/Pause")); // CE AdvancedOptionsUnit caption auto* central = new QWidget; setCentralWidget(central); auto* v = new QVBoxLayout(central); - v->addWidget(new QLabel("Code list:")); // CE Label + v->addWidget(new QLabel(tr("Code list:"))); // CE Label table_ = new QTableWidget(0, 2); // CE lvCodelist (Address, Name) - table_->setHorizontalHeaderLabels({"Address", "Name"}); + table_->setHorizontalHeaderLabels({tr("Address"), tr("Name")}); table_->horizontalHeader()->setStretchLastSection(true); table_->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); table_->setSelectionBehavior(QAbstractItemView::SelectRows); table_->setSelectionMode(QAbstractItemView::SingleSelection); styleDataTable(table_); - setEmptyHint(table_, "Code found with \"Find out what writes\" can be added here to keep it, disassemble it or replace it with NOPs."); + setEmptyHint(table_, tr("Code found with \"Find out what writes\" can be added here to keep it, disassemble it or replace it with NOPs.")); table_->setContextMenuPolicy(Qt::CustomContextMenu); connect(table_, &QWidget::customContextMenuRequested, this, &AdvancedOptionsWindow::showContextMenu); @@ -42,7 +42,7 @@ AdvancedOptionsWindow::AdvancedOptionsWindow(ce::ProcessHandle* proc, QWidget* p auto* row = new QHBoxLayout; // CE Panel1 row->addStretch(); - auto* ok = new QPushButton("OK"); // CE Button1 + auto* ok = new QPushButton(tr("OK")); // CE Button1 connect(ok, &QPushButton::clicked, this, &QWidget::close); row->addWidget(ok); v->addLayout(row); @@ -76,13 +76,13 @@ void AdvancedOptionsWindow::showContextMenu(const QPoint& pos) { const uintptr_t addr = addressAtRow(row); QMenu m(this); - auto* openAct = m.addAction("Open the disassembler at this location"); + auto* openAct = m.addAction(tr("Open the disassembler at this location")); m.addSeparator(); - auto* nopAct = m.addAction("Replace with code that does nothing"); - auto* restoreAct = m.addAction("Restore with original code"); + auto* nopAct = m.addAction(tr("Replace with code that does nothing")); + auto* restoreAct = m.addAction(tr("Restore with original code")); m.addSeparator(); - auto* renameAct = m.addAction("Rename"); - auto* removeAct = m.addAction("Remove from list"); + auto* renameAct = m.addAction(tr("Rename")); + auto* removeAct = m.addAction(tr("Remove from list")); const bool have = (addr != 0); for (auto* a : {openAct, nopAct, renameAct, removeAct}) a->setEnabled(have); @@ -99,7 +99,7 @@ void AdvancedOptionsWindow::showContextMenu(const QPoint& pos) { } else if (chosen == nopAct && proc_ && !nopOriginals_.count(addr)) { auto orig = ce::nopInstruction(*proc_, addr); if (!orig.empty()) nopOriginals_[addr] = orig; - else QMessageBox::warning(this, "NOP", "Could not read/patch the instruction."); + else QMessageBox::warning(this, "NOP", tr("Could not read/patch the instruction.")); } else if (chosen == restoreAct && proc_) { auto it = nopOriginals_.find(addr); if (it != nopOriginals_.end() && ce::restoreBytes(*proc_, addr, it->second)) @@ -107,7 +107,7 @@ void AdvancedOptionsWindow::showContextMenu(const QPoint& pos) { } else if (chosen == renameAct && row >= 0) { bool ok = false; QString cur = table_->item(row, 1) ? table_->item(row, 1)->text() : QString(); - auto name = QInputDialog::getText(this, "Rename", "Name:", QLineEdit::Normal, cur, &ok); + auto name = QInputDialog::getText(this, tr("Rename"), tr("Name:"), QLineEdit::Normal, cur, &ok); if (ok) table_->setItem(row, 1, new QTableWidgetItem(name)); } else if (chosen == removeAct && row >= 0) { table_->removeRow(row); diff --git a/gui/agent_panel.cpp b/gui/agent_panel.cpp index dd8b857e..2753f120 100644 --- a/gui/agent_panel.cpp +++ b/gui/agent_panel.cpp @@ -3,6 +3,7 @@ #include "gui/secret_store.hpp" #include +#include #include #include #include @@ -67,7 +68,7 @@ QString describeCall(const std::string& tool, const Value& arguments) { for (const auto& key : arguments.objectOrder) { const Value& v = arguments.objectValue.at(key); QString shown; - if (key == "script") shown = QString("(%1 lines)").arg(text(v.stringValue).count('\n') + 1); + if (key == "script") shown = QCoreApplication::translate("AgentPanel", "(%1 lines)").arg(text(v.stringValue).count('\n') + 1); else if (v.isArray()) shown = QString("[%1]").arg(v.arrayValue.size()); else if (key == "address") shown = addressArgument(&v); else shown = scalar(v); @@ -78,17 +79,17 @@ QString describeCall(const std::string& tool, const Value& arguments) { } QString describeResult(const Value& result, bool isError) { - if (isError) return "refused: " + text(result.getString("error").value_or("error")); - if (auto count = result.getInt("count")) return QString("%1 found").arg(*count); - if (auto attached = result.getString("attached")) return "attached to " + text(*attached); - if (const Value* list = result.get("instructions"); list && list->isArray()) return QString("%1 instruction(s)").arg(list->arrayValue.size()); - if (const Value* list = result.get("addresses"); list && list->isArray()) return QString("%1 address(es)").arg(list->arrayValue.size()); - if (auto saved = result.getString("saved")) return "saved to " + text(*saved); - if (auto value = result.getString("value_now")) return "now " + text(*value); - if (auto value = result.getString("value")) return "value " + text(*value); - if (result.get("disabled")) return "disabled"; - if (result.get("id")) return "enabled"; - return "done"; + if (isError) return QCoreApplication::translate("AgentPanel", "refused: %1").arg(text(result.getString("error").value_or("error"))); + if (auto count = result.getInt("count")) return QCoreApplication::translate("AgentPanel", "%1 found").arg(*count); + if (auto attached = result.getString("attached")) return QCoreApplication::translate("AgentPanel", "attached to %1").arg(text(*attached)); + if (const Value* list = result.get("instructions"); list && list->isArray()) return QCoreApplication::translate("AgentPanel", "%1 instruction(s)").arg(list->arrayValue.size()); + if (const Value* list = result.get("addresses"); list && list->isArray()) return QCoreApplication::translate("AgentPanel", "%1 address(es)").arg(list->arrayValue.size()); + if (auto saved = result.getString("saved")) return QCoreApplication::translate("AgentPanel", "saved to %1").arg(text(*saved)); + if (auto value = result.getString("value_now")) return QCoreApplication::translate("AgentPanel", "now %1").arg(text(*value)); + if (auto value = result.getString("value")) return QCoreApplication::translate("AgentPanel", "value %1").arg(text(*value)); + if (result.get("disabled")) return QCoreApplication::translate("AgentPanel", "disabled"); + if (result.get("id")) return QCoreApplication::translate("AgentPanel", "enabled"); + return QCoreApplication::translate("AgentPanel", "done"); } } // namespace @@ -100,7 +101,7 @@ AgentConfig loadAgentConfig() { config.provider.kind = openAi ? ChatProviderKind::OpenAiCompatible : ChatProviderKind::Anthropic; config.account = openAi ? "openai-compatible" : "anthropic"; config.envVar = openAi ? "OPENAI_API_KEY" : "ANTHROPIC_API_KEY"; - config.name = openAi ? "OpenAI-compatible server" : "Anthropic"; + config.name = openAi ? QCoreApplication::translate("AgentPanel", "OpenAI-compatible server") : "Anthropic"; config.provider.model = s.value("agent/" + config.account + "/model").toString().trimmed().toStdString(); config.provider.baseUrl = s.value("agent/" + config.account + "/baseUrl").toString().trimmed().toStdString(); config.keyOrigin = s.value("agent/" + config.account + "/keyOrigin").toString(); @@ -137,11 +138,11 @@ QString agentKeyRefusal(bool openAi, const QString& baseUrl, const QString& keyO const QString origin = agentOrigin(openAi, baseUrl); const QUrl url(origin); if (url.scheme() != "https" && !(url.scheme() == "http" && loopbackHost(url.host()))) - return QString("The API key is not sent to %1: use an https address (plain http is only for a server on " + return QCoreApplication::translate("AgentPanel", "The API key is not sent to %1: use an https address (plain http is only for a server on " "this computer).").arg(origin); const QString saved = keyOrigin.isEmpty() ? agentOrigin(openAi, {}) : keyOrigin; if (saved != origin) - return QString("The saved API key is for %1, so it is not sent to %2. Enter the key again in Settings, " + return QCoreApplication::translate("AgentPanel", "The saved API key is for %1, so it is not sent to %2. Enter the key again in Settings, " "AI Assistant to use it there.").arg(saved, origin); return {}; } @@ -182,8 +183,8 @@ AgentPanel::AgentPanel(QWidget* parent) : QWidget(parent) { provider_->setWordWrap(true); transcript_ = new QTextBrowser; transcript_->setOpenLinks(false); - transcript_->setPlaceholderText("Describe what to change in the game, for example:\n\n" - "My health shows 100. Find it and keep it at 100."); + transcript_->setPlaceholderText(tr("Describe what to change in the game, for example:\n\n" + "My health shows 100. Find it and keep it at 100.")); approvalBar_ = new QFrame; approvalBar_->setFrameShape(QFrame::StyledPanel); @@ -191,11 +192,11 @@ AgentPanel::AgentPanel(QWidget* parent) : QWidget(parent) { approvalText_ = new QLabel; approvalText_->setWordWrap(true); approvalText_->setTextInteractionFlags(Qt::TextSelectableByMouse); - auto* allow = new QPushButton("Allow"); + auto* allow = new QPushButton(tr("Allow")); allow->setProperty("primary", true); - allowForChatButton_ = new QPushButton("Allow for this chat"); - allowForChatButton_->setToolTip("Allow later changes in this process without asking, until the chat or the process changes"); - auto* decline = new QPushButton("Decline"); + allowForChatButton_ = new QPushButton(tr("Allow for this chat")); + allowForChatButton_->setToolTip(tr("Allow later changes in this process without asking, until the chat or the process changes")); + auto* decline = new QPushButton(tr("Decline")); connect(allow, &QPushButton::clicked, this, [this] { answerApproval(true); }); connect(allowForChatButton_, &QPushButton::clicked, this, [this] { answerApproval(true, true); }); connect(decline, &QPushButton::clicked, this, [this] { answerApproval(false); }); @@ -217,17 +218,17 @@ AgentPanel::AgentPanel(QWidget* parent) : QWidget(parent) { approvalBar_->hide(); input_ = new QPlainTextEdit; - input_->setPlaceholderText("Message the assistant (Enter sends, Shift+Enter adds a line)"); + input_->setPlaceholderText(tr("Message the assistant (Enter sends, Shift+Enter adds a line)")); input_->setMaximumHeight(input_->fontMetrics().lineSpacing() * 5); input_->installEventFilter(this); - send_ = new QPushButton("Send"); + send_ = new QPushButton(tr("Send")); send_->setProperty("primary", true); - stop_ = new QPushButton("Stop"); - newChat_ = new QPushButton("New chat"); - addToTable_ = new QPushButton("Add to address list"); - addToTable_->setToolTip("Copy the values and scripts the assistant saved into the address list"); - auto* settings = new QPushButton("Settings..."); + stop_ = new QPushButton(tr("Stop")); + newChat_ = new QPushButton(tr("New chat")); + addToTable_ = new QPushButton(tr("Add to address list")); + addToTable_->setToolTip(tr("Copy the values and scripts the assistant saved into the address list")); + auto* settings = new QPushButton(tr("Settings...")); connect(send_, &QPushButton::clicked, this, [this] { send(input_->toPlainText()); }); connect(stop_, &QPushButton::clicked, this, &AgentPanel::stop); connect(newChat_, &QPushButton::clicked, this, [this] { @@ -240,7 +241,9 @@ AgentPanel::AgentPanel(QWidget* parent) : QWidget(parent) { if (!tableSink_) return; const auto entries = session_->tableEntries(); tableSink_(entries); - status_->setText(QString("Added %1 entr%2 to the address list.").arg(entries.size()).arg(entries.size() == 1 ? "y" : "ies")); + status_->setText(entries.size() == 1 + ? tr("Added %1 entry to the address list.").arg(entries.size()) + : tr("Added %1 entries to the address list.").arg(entries.size())); }); connect(settings, &QPushButton::clicked, this, &AgentPanel::settingsRequested); @@ -304,7 +307,7 @@ bool AgentPanel::eventFilter(QObject* watched, QEvent* event) { void AgentPanel::reloadSettings() { const AgentConfig config = loadAgentConfig(); const QString model = text(config.provider.modelName()); - provider_->setText(QString("%1%2. Your messages and what the tools find in the game are sent there.") + provider_->setText(tr("%1%2. Your messages and what the tools find in the game are sent there.") .arg(config.name, model.isEmpty() ? QString() : ", " + model)); // send() compares the settings with the ones the chat was made with, so a // change applies to the next message, even one sent while a turn runs. @@ -322,7 +325,7 @@ void AgentPanel::stop() { } if (chat_) chat_->cancel(); approvalChanged_.notify_all(); - if (busy_) status_->setText("Stopping..."); + if (busy_) status_->setText(tr("Stopping...")); } void AgentPanel::setTarget(pid_t pid, const QString& name) { @@ -398,7 +401,7 @@ void AgentPanel::send(const QString& message) { chat_.reset(); allowForChatPid_ = 0; if (!transcript_->document()->isEmpty()) - addLine(QString("The AI Assistant settings changed, so this message starts a new " + addLine(tr("The AI Assistant settings changed, so this message starts a new " "conversation.").arg(palette().color(QPalette::PlaceholderText).name()), 16); } if (!chat_) { @@ -409,7 +412,7 @@ void AgentPanel::send(const QString& message) { auto found = agentApiKey(config, &source, &error, &refused); if (!found && config.provider.kind == ChatProviderKind::Anthropic) { addLine(QString("%1").arg(!refused.isEmpty() ? refused.toHtmlEscaped() - : QString("Add your Anthropic API key in Settings, AI Assistant%1.") + : tr("Add your Anthropic API key in Settings, AI Assistant%1.") .arg(error.isEmpty() ? QString() : " (" + error.toHtmlEscaped() + ")")), 8); busy_ = false; updateState(); @@ -418,7 +421,7 @@ void AgentPanel::send(const QString& message) { } // A local server usually needs no key, so the chat goes on without it. if (!refused.isEmpty()) - addLine(QString("%2 Continuing without a key.") + addLine(tr("%2 Continuing without a key.") .arg(palette().color(QPalette::PlaceholderText).name(), refused.toHtmlEscaped()), 8); key = found.value_or(QString()).toStdString(); transport = [this](const ChatHttpRequest& request) { return post(request); }; @@ -431,14 +434,14 @@ void AgentPanel::send(const QString& message) { chat_->setApprover([this](const std::string& tool, const Value& arguments) { return approve(tool, arguments); }); } - addLine(QString("You
%1").arg(trimmed.toHtmlEscaped().replace("\n", "
")), transcript_->document()->isEmpty() ? 0 : 16); + addLine(tr("You
%1").arg(trimmed.toHtmlEscaped().replace("\n", "
")), transcript_->document()->isEmpty() ? 0 : 16); input_->clear(); { std::lock_guard lock(approvalMutex_); stopping_ = false; } chat_->clearCancel(); // before the worker starts, so an early Stop counts - status_->setText("Working..."); + status_->setText(tr("Working...")); turnPid_ = targetPid_; turnName_ = targetName_; const pid_t pid = targetPid_; @@ -499,7 +502,7 @@ void AgentPanel::finishTurn(bool ok) { busy_ = false; approvalBar_->hide(); // A failure is explained in the transcript; the status only marks a Stop. - status_->setText(!ok && stopping_ ? QString("Stopped.") : QString()); + status_->setText(!ok && stopping_ ? tr("Stopped.") : QString()); updateState(); syncSession(); input_->setFocus(); @@ -531,7 +534,7 @@ bool AgentPanel::approve(const std::string& tool, const Value& arguments) { QFile comm(QString("/proc/%1/comm").arg(attached)); if (comm.open(QIODevice::ReadOnly)) name = QString::fromUtf8(comm.readAll()).trimmed(); } - in = name.isEmpty() ? QString(" in PID %1").arg(attached) : QString(" in %1 (PID %2)").arg(name).arg(attached); + in = name.isEmpty() ? tr(" in PID %1").arg(attached) : tr(" in %1 (PID %2)").arg(name).arg(attached); } // Values may come as JSON numbers as well as strings. auto field = [&](const char* key) { @@ -555,21 +558,21 @@ bool AgentPanel::approve(const std::string& tool, const Value& arguments) { }; QString description, detail; if (tool == "write") { - description = QString("Write %1 (%2) to %3%4?").arg(field("value"), field("type"), address(), in); + description = tr("Write %1 (%2) to %3%4?").arg(field("value"), field("type"), address(), in); } else if (tool == "save_table") { // The tool makes a relative path absolute the same way. QString path = field("path"); std::error_code error; const auto absolute = std::filesystem::absolute(path.toStdString(), error); if (!error && !path.isEmpty() && path != "?") path = QString::fromStdString(absolute.string()); - description = overwrite ? QString("Save the table to %1, replacing that file if it exists?").arg(path) - : QString("Save the table to %1?").arg(path); + description = overwrite ? tr("Save the table to %1, replacing that file if it exists?").arg(path) + : tr("Save the table to %1?").arg(path); } else if (tool == "attach") { - description = QString("Let the assistant open \"%1\"? It can then read that program's memory, and what it reads " + description = tr("Let the assistant open \"%1\"? It can then read that program's memory, and what it reads " "is sent to the model's provider.").arg(field("target")); } else if (tool == "set_frozen") { const QString value = scalar(arguments.get("value") ? *arguments.get("value") : Value()); - QString record = "record " + field("index"); + QString record = tr("record %1").arg(field("index")); if (auto index = arguments.getInt("index"); index && *index >= 0) { const auto listed = session_->call("list_records", Value::object()); const Value* records = listed.content.get("records"); @@ -581,20 +584,21 @@ bool AgentPanel::approve(const std::string& tool, const Value& arguments) { if (at.isEmpty()) break; // a script record; the tool refuses it QString where; if (auto pointsTo = r.getString("points_to")) { - where = *pointsTo == "unresolved" ? at + ", which leads nowhere now" : at + ", now " + text(*pointsTo); + where = *pointsTo == "unresolved" ? at + tr(", which leads nowhere now") + : at + tr(", now %1").arg(text(*pointsTo)); } else { const auto entries = session_->tableEntries(); const QString resolved = i < entries.size() ? hexText(entries[i].address) : QString(); where = resolved.isEmpty() || resolved == at ? at : at + " = " + resolved; } - record += QString(" (%1 at %2)").arg(text(r.getString("type").value_or("?")), where); + record += tr(" (%1 at %2)").arg(text(r.getString("type").value_or("?")), where); break; } } - description = QString("Freeze %1 at %2%3?").arg(record, value.isEmpty() ? QString("its current value") : value, in); + description = tr("Freeze %1 at %2%3?").arg(record, value.isEmpty() ? tr("its current value") : value, in); } else if (tool == "find_accessed_addresses") { const auto seconds = std::clamp(arguments.getInt("seconds").value_or(4), 1, 30); - description = QString("Put a breakpoint at %1%2 for %3 s to see which addresses that instruction accesses? " + description = tr("Put a breakpoint at %1%2 for %3 s to see which addresses that instruction accesses? " "The game pauses briefly each time it runs.").arg(address(), in, QString::number(seconds)); } else if (tool == "aa_disable") { const QString id = field("id"); @@ -603,9 +607,9 @@ bool AgentPanel::approve(const std::string& tool, const Value& arguments) { if (const Value* enabled = listed.content.get("enabled"); enabled && enabled->isArray()) for (const auto& script : enabled->arrayValue) if (script.getInt("id") == arguments.getInt("id")) name = " \"" + text(script.getString("description").value_or("")) + "\""; - description = QString("Disable script #%1%2%3? Its [DISABLE] section runs and restores the game's code.").arg(id, name, in); + description = tr("Disable script #%1%2%3? Its [DISABLE] section runs and restores the game's code.").arg(id, name, in); } else { - description = QString("Enable the script \"%1\"%2?").arg(text(arguments.getString("description").value_or("")), in); + description = tr("Enable the script \"%1\"%2?").arg(text(arguments.getString("description").value_or("")), in); detail = text(arguments.getString("script").value_or("")); } { @@ -634,7 +638,7 @@ void AgentPanel::showApproval(const QString& description, const QString& detail, approvalDetail_->setVisible(!detail.isEmpty()); allowForChatButton_->setVisible(forChat); approvalBar_->show(); - status_->setText("Waiting for your answer..."); + status_->setText(tr("Waiting for your answer...")); } bool AgentPanel::approvalPending() const { @@ -656,13 +660,13 @@ void AgentPanel::answerApproval(bool allow, bool forChat) { } approvalChanged_.notify_all(); approvalBar_->hide(); - status_->setText("Working..."); + status_->setText(tr("Working...")); } // Runs on the worker thread. ChatHttpResponse AgentPanel::post(const ChatHttpRequest& request) { ChatHttpResponse response; - if (!network_) { response.error = "closing"; return response; } + if (!network_) { response.error = tr("closing").toStdString(); return response; } QNetworkRequest http(QUrl(QString::fromStdString(request.url))); // Never follow a redirect to another host: it would carry the API key there. http.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::SameOriginRedirectPolicy); @@ -692,13 +696,13 @@ ChatHttpResponse AgentPanel::post(const ChatHttpRequest& request) { checkSize(); if (tooLarge) { response.truncated = true; - response.error = "the reply is too large"; + response.error = tr("the reply is too large").toStdString(); delete reply; return response; } response.status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); response.body = reply->readAll().toStdString(); - if (response.status == 0) response.error = stopping_ ? "stopped" : reply->errorString().toStdString(); + if (response.status == 0) response.error = stopping_ ? tr("stopped").toStdString() : reply->errorString().toStdString(); delete reply; return response; } diff --git a/gui/branchmapper.cpp b/gui/branchmapper.cpp index 8d7056d5..d8acf349 100644 --- a/gui/branchmapper.cpp +++ b/gui/branchmapper.cpp @@ -16,7 +16,7 @@ namespace ce::gui { BranchMapper::BranchMapper(ProcessHandle* proc, QWidget* parent) : QMainWindow(parent), proc_(proc) { - setWindowTitle("Branch Mapper (LBR)"); + setWindowTitle(tr("Branch Mapper (LBR)")); resize(900, 600); auto* central = new QWidget; @@ -33,18 +33,18 @@ BranchMapper::BranchMapper(ProcessHandle* proc, QWidget* parent) } tidSpin_->setMaximumWidth(160); tidSpin_->setGroupSeparatorShown(false); - form->addRow("Thread ID:", tidSpin_); + form->addRow(tr("Thread ID:"), tidSpin_); auto* btnRow = new QHBoxLayout; - startBtn_ = new QPushButton("Start sampling"); + startBtn_ = new QPushButton(tr("Start sampling")); startBtn_->setObjectName("branchMapperStart"); - stopBtn_ = new QPushButton("Stop"); + stopBtn_ = new QPushButton(tr("Stop")); stopBtn_->setEnabled(false); - clearBtn_ = new QPushButton("Clear"); + clearBtn_ = new QPushButton(tr("Clear")); const bool local=ce::localTargetPid(proc_).has_value(); - statusLabel_ = new QLabel(!local ? "Branch sampling requires a live local process target" : - LbrTracer::available() ? "Idle" : - "Branch-stack sampling unavailable; requires kernel perf permission and a supported CPU/PMU."); + statusLabel_ = new QLabel(!local ? tr("Branch sampling requires a live local process target") : + LbrTracer::available() ? tr("Idle") : + tr("Branch-stack sampling unavailable; requires kernel perf permission and a supported CPU/PMU.")); statusLabel_->setObjectName("branchMapperStatus");statusLabel_->setWordWrap(true); if (!local) {startBtn_->setEnabled(false);tidSpin_->setEnabled(false);} btnRow->addWidget(startBtn_); @@ -58,12 +58,13 @@ BranchMapper::BranchMapper(ProcessHandle* proc, QWidget* parent) table_ = new QTableWidget; table_->setColumnCount(6); - table_->setHorizontalHeaderLabels({"From", "From symbol", "To", "To symbol", "Hits", "% of total"}); + table_->setHorizontalHeaderLabels( + {tr("From"), tr("From symbol"), tr("To"), tr("To symbol"), tr("Hits"), tr("% of total")}); table_->horizontalHeader()->setStretchLastSection(true); table_->setSelectionBehavior(QAbstractItemView::SelectRows); table_->setProperty("monospace", true); // font from the theme sheet styleDataTable(table_); - setEmptyHint(table_, "Click Start sampling to record the branches this thread takes."); + setEmptyHint(table_, tr("Click Start sampling to record the branches this thread takes.")); layout->addWidget(table_); setCentralWidget(central); @@ -82,12 +83,12 @@ BranchMapper::~BranchMapper() { void BranchMapper::onStart() { if (!ce::localTargetPid(proc_)) { - statusLabel_->setText("Branch sampling requires a live local process target"); + statusLabel_->setText(tr("Branch sampling requires a live local process target")); startBtn_->setEnabled(false);tidSpin_->setEnabled(false);return; } pid_t tid = (pid_t)tidSpin_->value(); if (!tracer_.start(tid)) { - QMessageBox::warning(this, "Start failed", + QMessageBox::warning(this, tr("Start failed"), QString::fromStdString(tracer_.lastError())); return; } @@ -99,7 +100,7 @@ void BranchMapper::onStart() { startBtn_->setEnabled(false); stopBtn_->setEnabled(true); tidSpin_->setEnabled(false); - statusLabel_->setText("Sampling…"); + statusLabel_->setText(tr("Sampling…")); pollTimer_->start(250); } @@ -110,14 +111,14 @@ void BranchMapper::onStop() { startBtn_->setEnabled(local); stopBtn_->setEnabled(false); tidSpin_->setEnabled(local); - statusLabel_->setText(QString("Stopped. %1 samples total.").arg(totalSamples_)); + statusLabel_->setText(tr("Stopped. %1 samples total.").arg(totalSamples_)); } void BranchMapper::onClear() { counts_.clear(); totalSamples_ = 0; table_->setRowCount(0); - statusLabel_->setText(ce::localTargetPid(proc_) ? "Cleared." : "Branch sampling requires a live local process target"); + statusLabel_->setText(ce::localTargetPid(proc_) ? tr("Cleared.") : tr("Branch sampling requires a live local process target")); } void BranchMapper::onPoll() { @@ -156,7 +157,7 @@ void BranchMapper::onPoll() { double pct = totalSamples_ ? (100.0 * r.hits / totalSamples_) : 0.0; table_->setItem((int)i, 5, new QTableWidgetItem(QString::number(pct, 'f', 2) + "%")); } - statusLabel_->setText(QString("Sampling… %1 total / %2 unique pairs") + statusLabel_->setText(tr("Sampling… %1 total / %2 unique pairs") .arg(totalSamples_).arg(flat.size())); } diff --git a/gui/breakpointlist.cpp b/gui/breakpointlist.cpp index 2ad592ba..50f91842 100644 --- a/gui/breakpointlist.cpp +++ b/gui/breakpointlist.cpp @@ -9,7 +9,7 @@ namespace ce::gui { BreakpointListWindow::BreakpointListWindow(BreakpointManager* mgr, QWidget* parent) : QMainWindow(parent), mgr_(mgr) { - setWindowTitle("Breakpoint List"); + setWindowTitle(tr("Breakpoint List")); resize(700, 300); auto* central = new QWidget; @@ -17,18 +17,19 @@ BreakpointListWindow::BreakpointListWindow(BreakpointManager* mgr, QWidget* pare table_ = new QTableWidget; table_->setColumnCount(6); - table_->setHorizontalHeaderLabels({"ID", "Address", "Type", "Action", "Hits", "Enabled"}); + table_->setHorizontalHeaderLabels( + {"ID", tr("Address"), tr("Type"), tr("Action"), tr("Hits"), tr("Enabled")}); table_->horizontalHeader()->setStretchLastSection(true); table_->setSelectionBehavior(QAbstractItemView::SelectRows); table_->setProperty("monospace", true); // font from the theme sheet styleDataTable(table_); - setEmptyHint(table_, "No breakpoints. Set one in the Memory Viewer (F5) or the Debugger."); + setEmptyHint(table_, tr("No breakpoints. Set one in the Memory Viewer (F5) or the Debugger.")); layout->addWidget(table_); auto* btnRow = new QHBoxLayout; - auto* removeBtn = new QPushButton("Remove"); + auto* removeBtn = new QPushButton(tr("Remove")); connect(removeBtn, &QPushButton::clicked, this, &BreakpointListWindow::onRemove); - auto* toggleBtn = new QPushButton("Enable/Disable"); + auto* toggleBtn = new QPushButton(tr("Enable/Disable")); connect(toggleBtn, &QPushButton::clicked, this, &BreakpointListWindow::onToggle); btnRow->addWidget(removeBtn); btnRow->addWidget(toggleBtn); @@ -53,24 +54,24 @@ void BreakpointListWindow::refresh() { QString type; switch (bp.type) { - case BpType::Execute: type = "Execute"; break; - case BpType::Write: type = "Write"; break; - case BpType::Read: type = "Read"; break; - case BpType::Access: type = "Access"; break; + case BpType::Execute: type = tr("Execute"); break; + case BpType::Write: type = tr("Write"); break; + case BpType::Read: type = tr("Read"); break; + case BpType::Access: type = tr("Access"); break; } table_->setItem(i, 2, new QTableWidgetItem(type)); QString action; switch (bp.action) { - case BpAction::Break: action = "Break"; break; - case BpAction::FindCode: action = "Find Code"; break; - case BpAction::FindAccess: action = "Find Access"; break; - case BpAction::Trace: action = "Trace"; break; + case BpAction::Break: action = tr("Break"); break; + case BpAction::FindCode: action = tr("Find Code"); break; + case BpAction::FindAccess: action = tr("Find Access"); break; + case BpAction::Trace: action = tr("Trace"); break; default: action = "?"; break; } table_->setItem(i, 3, new QTableWidgetItem(action)); table_->setItem(i, 4, new QTableWidgetItem(QString::number(bp.hitCount))); - table_->setItem(i, 5, new QTableWidgetItem(bp.enabled ? "Yes" : "No")); + table_->setItem(i, 5, new QTableWidgetItem(bp.enabled ? tr("Yes") : tr("No"))); } } diff --git a/gui/changeaddressdialog.cpp b/gui/changeaddressdialog.cpp index 1d89c4df..afccc127 100644 --- a/gui/changeaddressdialog.cpp +++ b/gui/changeaddressdialog.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -21,22 +22,22 @@ namespace ce::gui { // CE cbvarType order (formAddressChangeUnit). static const struct { const char* label; ce::ValueType type; } kTypes[] = { - {"Byte", ce::ValueType::Byte}, - {"2 Bytes", ce::ValueType::Int16}, - {"4 Bytes", ce::ValueType::Int32}, - {"8 Bytes", ce::ValueType::Int64}, - {"Float", ce::ValueType::Float}, - {"Double", ce::ValueType::Double}, - {"String", ce::ValueType::String}, - {"Array of byte", ce::ValueType::ByteArray}, - {"Pointer", ce::ValueType::Pointer}, + {QT_TRANSLATE_NOOP("ce::gui::ChangeAddressDialog", "Byte"), ce::ValueType::Byte}, + {QT_TRANSLATE_NOOP("ce::gui::ChangeAddressDialog", "2 Bytes"), ce::ValueType::Int16}, + {QT_TRANSLATE_NOOP("ce::gui::ChangeAddressDialog", "4 Bytes"), ce::ValueType::Int32}, + {QT_TRANSLATE_NOOP("ce::gui::ChangeAddressDialog", "8 Bytes"), ce::ValueType::Int64}, + {QT_TRANSLATE_NOOP("ce::gui::ChangeAddressDialog", "Float"), ce::ValueType::Float}, + {QT_TRANSLATE_NOOP("ce::gui::ChangeAddressDialog", "Double"), ce::ValueType::Double}, + {QT_TRANSLATE_NOOP("ce::gui::ChangeAddressDialog", "String"), ce::ValueType::String}, + {QT_TRANSLATE_NOOP("ce::gui::ChangeAddressDialog", "Array of byte"), ce::ValueType::ByteArray}, + {QT_TRANSLATE_NOOP("ce::gui::ChangeAddressDialog", "Pointer"), ce::ValueType::Pointer}, }; ChangeAddressDialog::ChangeAddressDialog(const QString& address, ce::ValueType type, bool showHex, int length, QWidget* parent, bool showSigned, ce::ProcessHandle* proc) : QDialog(parent), proc_(proc) { - setWindowTitle("Change address"); + setWindowTitle(tr("Change address")); auto* v = new QVBoxLayout(this); auto* form = new QFormLayout; @@ -55,12 +56,12 @@ ChangeAddressDialog::ChangeAddressDialog(const QString& address, ce::ValueType t }; addrEdit_ = new QLineEdit(address); // CE editAddress addrEdit_->setMinimumWidth(240); - form->addRow(label("Address:"), addrEdit_); + form->addRow(label(tr("Address:")), addrEdit_); typeCombo_ = new QComboBox; // CE cbvarType int sel = 2; // default 4 Bytes for (int i = 0; i < (int)(sizeof(kTypes) / sizeof(kTypes[0])); ++i) { - typeCombo_->addItem(kTypes[i].label); + typeCombo_->addItem(tr(kTypes[i].label)); if (kTypes[i].type == type) sel = i; } // CE has no separate "Unicode String" type: a Unicode string is the String type @@ -70,21 +71,21 @@ ChangeAddressDialog::ChangeAddressDialog(const QString& address, ce::ValueType t for (int i = 0; i < (int)(sizeof(kTypes) / sizeof(kTypes[0])); ++i) if (kTypes[i].type == ce::ValueType::String) { sel = i; break; } typeCombo_->setCurrentIndex(sel); - form->addRow(label("Type:"), typeCombo_); + form->addRow(label(tr("Type:")), typeCombo_); lengthEdit_ = new QLineEdit(QString::number(length > 0 ? length : 1)); // CE edtSize - lengthLabel_ = label("Length:"); + lengthLabel_ = label(tr("Length:")); lengthLabel_->setBuddy(lengthEdit_); form->addRow(lengthLabel_, lengthEdit_); v->addLayout(form); auto* flags = new QHBoxLayout; - pointerCheck_ = new QCheckBox("Pointer"); // CE cbPointer - hexCheck_ = new QCheckBox("Hexadecimal"); // CE cbHex + pointerCheck_ = new QCheckBox(tr("Pointer")); // CE cbPointer + hexCheck_ = new QCheckBox(tr("Hexadecimal")); // CE cbHex hexCheck_->setChecked(showHex); - signedCheck_ = new QCheckBox("Signed"); // CE cbSigned + signedCheck_ = new QCheckBox(tr("Signed")); // CE cbSigned signedCheck_->setChecked(showSigned); - unicodeCheck_ = new QCheckBox("Unicode"); // CE cbunicode + unicodeCheck_ = new QCheckBox(tr("Unicode")); // CE cbunicode unicodeCheck_->setChecked(startUnicode); flags->addWidget(pointerCheck_); flags->addWidget(hexCheck_); @@ -108,13 +109,13 @@ ChangeAddressDialog::ChangeAddressDialog(const QString& address, ce::ValueType t auto* pv = new QVBoxLayout(pointerBox_); pv->setContentsMargins(0, 0, 0, 0); auto* baseRow = new QHBoxLayout; - baseRow->addWidget(label("Base:")); + baseRow->addWidget(label(tr("Base:"))); pointerBaseEdit_ = new QLineEdit; baseRow->addWidget(pointerBaseEdit_); pv->addLayout(baseRow); offsetsLayout_ = new QVBoxLayout; pv->addLayout(offsetsLayout_); - auto* addOffBtn = new QPushButton("Add offset"); + auto* addOffBtn = new QPushButton(tr("Add offset")); auto* addRow = new QHBoxLayout; addRow->addSpacing(labelWidth_ + addRow->spacing()); addRow->addWidget(addOffBtn); @@ -197,7 +198,7 @@ void ChangeAddressDialog::updatePreview() { ce::ExpressionParser parser(proc_, nullptr); auto addr = parser.parse(expr); if (!addr || !*addr) { - previewLabel_->setText(QStringLiteral("→ ?? (does not resolve)")); + previewLabel_->setText(tr("→ ?? (does not resolve)")); return; } QString txt = QStringLiteral("→ 0x%1").arg(*addr, 0, 16); @@ -248,14 +249,14 @@ void ChangeAddressDialog::addOffsetRow(long long value) { auto* row = new QWidget; auto* h = new QHBoxLayout(row); h->setContentsMargins(0, 0, 0, 0); - auto* label = new QLabel("Offset:"); + auto* label = new QLabel(tr("Offset:")); label->setFixedWidth(labelWidth_); h->addWidget(label); auto* edit = new QLineEdit; edit->setText(value < 0 ? "-" + QString::number(-value, 16) : QString::number(value, 16)); h->addWidget(edit); auto* rm = new QPushButton("−"); - rm->setToolTip("Remove this offset"); + rm->setToolTip(tr("Remove this offset")); rm->setStyleSheet("QPushButton { padding: 0; }"); rm->setFixedSize(30, edit->sizeHint().height()); h->addWidget(rm); diff --git a/gui/codefinder.cpp b/gui/codefinder.cpp index e452671d..07b6a826 100644 --- a/gui/codefinder.cpp +++ b/gui/codefinder.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -28,7 +29,7 @@ static QString hexQ(uint64_t v) { return QString("0x%1").arg(v, 0, 16); } static QString pointerHint(const ce::CpuContext& c, uintptr_t target) { if (!target) return {}; if (c.architecture==ce::CpuArchitecture::Arm64) { - for (size_t i=0;iaddWidget(statusLabel_); table_ = new QTableWidget; table_->setColumnCount(9); - table_->setHorizontalHeaderLabels({"Address", "Instruction", "Hits", "Pointer path", + table_->setHorizontalHeaderLabels({tr("Address"), tr("Instruction"), tr("Hits"), tr("Pointer path"), "RAX", "RBX", "RCX", "RDX", "RIP"}); table_->horizontalHeader()->setStretchLastSection(false); // Fit the address, hit count and 64-bit register columns to content (they @@ -107,7 +108,7 @@ CodeFinderWindow::CodeFinderWindow(CodeFinder* finder, const QString& title, table_->setSelectionBehavior(QAbstractItemView::SelectRows); table_->setProperty("monospace", true); // font from the theme sheet styleDataTable(table_); - setEmptyHint(table_, "Waiting for an instruction to touch the address. Let the game run."); + setEmptyHint(table_, tr("Waiting for an instruction to touch the address. Let the game run.")); connect(table_, &QTableWidget::itemDoubleClicked, this, [this](QTableWidgetItem* item) { if (!item) return; auto results = finder_->results(); @@ -115,22 +116,22 @@ CodeFinderWindow::CodeFinderWindow(CodeFinder* finder, const QString& title, if (row < 0) return; auto* dlg = new QDialog(this); - dlg->setWindowTitle(QString("Hit context @ 0x%1").arg(results[row].instructionAddress, 0, 16)); + dlg->setWindowTitle(tr("Hit context @ 0x%1").arg(results[row].instructionAddress, 0, 16)); dlg->resize(520, 360); auto* dlgLayout = new QVBoxLayout(dlg); - auto* hdr = new QLabel(QString("%1 hits=%2") + auto* hdr = new QLabel(tr("%1 hits=%2") .arg(QString::fromStdString(results[row].instructionText)) .arg(results[row].hitCount)); dlgLayout->addWidget(hdr); - auto* firstLabel = new QLabel("First hit:"); + auto* firstLabel = new QLabel(tr("First hit:")); dlgLayout->addWidget(firstLabel); auto* firstDump = new QPlainTextEdit(fullRegisterDump(results[row].firstContext)); firstDump->setReadOnly(true); firstDump->setFont(monospaceFont(-1)); dlgLayout->addWidget(firstDump); - auto* lastLabel = new QLabel("Last hit:"); + auto* lastLabel = new QLabel(tr("Last hit:")); dlgLayout->addWidget(lastLabel); auto* lastDump = new QPlainTextEdit(fullRegisterDump(results[row].lastContext)); lastDump->setReadOnly(true); @@ -149,16 +150,16 @@ CodeFinderWindow::CodeFinderWindow(CodeFinder* finder, const QString& title, layout->addWidget(table_); auto* btnRow = new QHBoxLayout; - stopBtn_ = new QPushButton("Stop"); + stopBtn_ = new QPushButton(tr("Stop")); connect(stopBtn_, &QPushButton::clicked, this, &CodeFinderWindow::onStop); btnRow->addWidget(stopBtn_); btnRow->addStretch(); - auto* addBtn = new QPushButton("Add to Address List"); - addBtn->setToolTip("Add the selected instructions (or all, if none selected) to the address list."); + auto* addBtn = new QPushButton(tr("Add to Address List")); + addBtn->setToolTip(tr("Add the selected instructions (or all, if none selected) to the address list.")); connect(addBtn, &QPushButton::clicked, this, &CodeFinderWindow::onAddToList); btnRow->addWidget(addBtn); - saveBtn_ = new QPushButton("Save to File..."); - saveBtn_->setToolTip("Export the found instructions, hit counts, and registers to a text file."); + saveBtn_ = new QPushButton(tr("Save to File...")); + saveBtn_->setToolTip(tr("Export the found instructions, hit counts, and registers to a text file.")); connect(saveBtn_, &QPushButton::clicked, this, &CodeFinderWindow::onExportToFile); btnRow->addWidget(saveBtn_); layout->addLayout(btnRow); @@ -173,18 +174,18 @@ CodeFinderWindow::CodeFinderWindow(CodeFinder* finder, const QString& title, void CodeFinderWindow::refresh() { auto results = finder_->results(); statusLabel_->setText(finder_->running() - ? QString("Monitoring... %1 unique instructions found (double-click a row for full register state)").arg(results.size()) - : QString("Stopped. %1 unique instructions found").arg(results.size())); + ? tr("Monitoring... %1 unique instructions found (double-click a row for full register state)").arg(results.size()) + : tr("Stopped. %1 unique instructions found").arg(results.size())); if (auto error=finder_->lastError()) statusLabel_->setText(QString("%1: %2") - .arg(finder_->hasPendingRecovery() ? "Recovering" : finder_->running() ? "Monitoring" : "Stopped") + .arg(finder_->hasPendingRecovery() ? tr("Recovering") : finder_->running() ? tr("Monitoring") : tr("Stopped")) .arg(QString::fromStdString(error.message()))); auto architecture=results.empty() ? ce::CpuArchitecture::Unknown : results.front().lastContext.architecture; table_->setHorizontalHeaderLabels(architecture==ce::CpuArchitecture::Arm64 ? - QStringList{"Address","Instruction","Hits","Pointer path","X0","X1","X2","X3","PC"} : + QStringList{tr("Address"),tr("Instruction"),tr("Hits"),tr("Pointer path"),"X0","X1","X2","X3","PC"} : architecture==ce::CpuArchitecture::X86_32 ? - QStringList{"Address","Instruction","Hits","Pointer path","EAX","EBX","ECX","EDX","EIP"} : - QStringList{"Address","Instruction","Hits","Pointer path","RAX","RBX","RCX","RDX","RIP"}); + QStringList{tr("Address"),tr("Instruction"),tr("Hits"),tr("Pointer path"),"EAX","EBX","ECX","EDX","EIP"} : + QStringList{tr("Address"),tr("Instruction"),tr("Hits"),tr("Pointer path"),"RAX","RBX","RCX","RDX","RIP"}); // Rows re-sort by hit count on every refresh: remember the selection by result // key and select the same results again afterwards, not the same row numbers. @@ -201,8 +202,8 @@ void CodeFinderWindow::refresh() { QString ptr = pointerHint(c, finder_->targetAddress()); auto* ptrItem = new QTableWidgetItem(ptr); if (!ptr.isEmpty()) - ptrItem->setToolTip("This instruction addresses the value via this register; " - "pointer-scan the base for a stable address."); + ptrItem->setToolTip(tr("This instruction addresses the value via this register; " + "pointer-scan the base for a stable address.")); // A hardware watchpoint traps one instruction past the store, so recover the // exact writer from the trap rip (software page-guard already stops on it). uintptr_t insAddr = r.instructionAddress; @@ -269,31 +270,31 @@ void CodeFinderWindow::onAddToList() { addToList_(addr, text); ++added; } - statusLabel_->setText(QString("Added %1 instruction(s) to the address list.").arg(added)); + statusLabel_->setText(tr("Added %1 instruction(s) to the address list.").arg(added)); } void CodeFinderWindow::onExportToFile() { auto results = finder_->results(); - QString path = QFileDialog::getSaveFileName(this, "Save findings", - "code-finder.txt", "Text files (*.txt);;All files (*)"); + QString path = QFileDialog::getSaveFileName(this, tr("Save findings"), + "code-finder.txt", tr("Text files (*.txt);;All files (*)")); if (path.isEmpty()) return; QFile f(path); if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { - QMessageBox::warning(this, "Save failed", f.errorString()); + QMessageBox::warning(this, tr("Save failed"), f.errorString()); return; } QTextStream out(&f); out << windowTitle() << "\n"; - out << QString("%1 unique instruction(s)\n\n").arg(results.size()); + out << tr("%1 unique instruction(s)\n\n").arg(results.size()); for (const auto& r : results) { - out << QString("0x%1 hits=%2 %3\n") + out << tr("0x%1 hits=%2 %3\n") .arg(r.instructionAddress, 0, 16) .arg(r.hitCount) .arg(QString::fromStdString(r.instructionText)); - out << "First hit:\n" << fullRegisterDump(r.firstContext) - << "Last hit:\n" << fullRegisterDump(r.lastContext) << '\n'; + out << tr("First hit:\n") << fullRegisterDump(r.firstContext) + << tr("Last hit:\n") << fullRegisterDump(r.lastContext) << '\n'; } - statusLabel_->setText(QString("Saved %1 instruction(s) to %2").arg(results.size()).arg(path)); + statusLabel_->setText(tr("Saved %1 instruction(s) to %2").arg(results.size()).arg(path)); } int CodeFinderWindow::resultIndexOfRow(int row, const std::vector& results) const { @@ -353,11 +354,11 @@ void CodeFinderWindow::onContextMenu(const QPoint& pos) { const bool isNopped = noppedOriginals_.count(addr) > 0; QMenu menu(this); - QAction* showAct = showInDisasm_ ? menu.addAction("Show in the disassembler") : nullptr; + QAction* showAct = showInDisasm_ ? menu.addAction(tr("Show in the disassembler")) : nullptr; QAction* nopAct = proc_ && !isNopped - ? menu.addAction("Replace with code that does nothing (NOP)") : nullptr; - QAction* restoreAct = proc_ && isNopped ? menu.addAction("Restore with original code") : nullptr; - QAction* addAct = addToList_ ? menu.addAction("Add to the address list") : nullptr; + ? menu.addAction(tr("Replace with code that does nothing (NOP)")) : nullptr; + QAction* restoreAct = proc_ && isNopped ? menu.addAction(tr("Restore with original code")) : nullptr; + QAction* addAct = addToList_ ? menu.addAction(tr("Add to the address list")) : nullptr; if (menu.isEmpty()) return; QAction* picked = menu.exec(table_->viewport()->mapToGlobal(pos)); @@ -367,22 +368,22 @@ void CodeFinderWindow::onContextMenu(const QPoint& pos) { } else if (picked == nopAct) { if (nopInstructionAt(addr)) { refresh(); // the menu's event loop may have re-sorted the rows; relabel by address - statusLabel_->setText(QString("Patched 0x%1 to NOPs.").arg(addr, 0, 16)); + statusLabel_->setText(tr("Patched 0x%1 to NOPs.").arg(addr, 0, 16)); } else { - statusLabel_->setText(QString("Could not patch 0x%1.").arg(addr, 0, 16)); + statusLabel_->setText(tr("Could not patch 0x%1.").arg(addr, 0, 16)); } } else if (picked == restoreAct) { if (restoreInstructionAt(addr)) { refresh(); - statusLabel_->setText(QString("Restored the original code at 0x%1.").arg(addr, 0, 16)); + statusLabel_->setText(tr("Restored the original code at 0x%1.").arg(addr, 0, 16)); } else { - statusLabel_->setText(QString("Could not restore 0x%1.").arg(addr, 0, 16)); + statusLabel_->setText(tr("Could not restore 0x%1.").arg(addr, 0, 16)); } } else if (picked == addAct) { int added = 0; for (const auto& [entryAddr, text] : entries) if (entryAddr) { addToList_(entryAddr, text); ++added; } - statusLabel_->setText(QString("Added %1 instruction(s) to the address list.").arg(added)); + statusLabel_->setText(tr("Added %1 instruction(s) to the address list.").arg(added)); } } diff --git a/gui/codereferences.cpp b/gui/codereferences.cpp index 3e7a0f7c..b388b311 100644 --- a/gui/codereferences.cpp +++ b/gui/codereferences.cpp @@ -16,7 +16,7 @@ namespace ce::gui { CodeReferencesWindow::CodeReferencesWindow(ProcessHandle* proc, QWidget* parent) : QMainWindow(parent), proc_(proc) { - setWindowTitle("Code References"); + setWindowTitle(tr("Code References")); resize(900, 560); auto* central = new QWidget; @@ -28,10 +28,10 @@ CodeReferencesWindow::CodeReferencesWindow(ProcessHandle* proc, QWidget* parent) minCaveSizeSpin_->setRange(4, 4096); minCaveSizeSpin_->setValue(16); assemblyPatternEdit_ = new QLineEdit; - assemblyPatternEdit_->setPlaceholderText("Instruction to find, e.g. mov eax,1"); - auto* analyzeBtn = new QPushButton("Analyze"); + assemblyPatternEdit_->setPlaceholderText(tr("Instruction to find, e.g. mov eax,1")); + auto* analyzeBtn = new QPushButton(tr("Analyze")); top->addWidget(moduleCombo_, 1); - top->addWidget(new QLabel("Min cave bytes:")); + top->addWidget(new QLabel(tr("Min cave bytes:"))); top->addWidget(minCaveSizeSpin_); top->addWidget(assemblyPatternEdit_, 1); top->addWidget(analyzeBtn); @@ -48,7 +48,7 @@ CodeReferencesWindow::CodeReferencesWindow(ProcessHandle* proc, QWidget* parent) cavesTable_ = new QTableWidget; for (auto* table : {stringsTable_, functionsTable_, jumpsTable_, ripRelativeTable_, assemblyTable_}) { table->setColumnCount(3); - table->setHorizontalHeaderLabels({"Instruction", "Target", "Text"}); + table->setHorizontalHeaderLabels({tr("Instruction"), tr("Target"), tr("Text")}); table->horizontalHeader()->setStretchLastSection(true); // Instruction and Target are 16-digit hex addresses; fit them to content // so they aren't clipped, and let the Text column take the slack. @@ -57,8 +57,8 @@ CodeReferencesWindow::CodeReferencesWindow(ProcessHandle* proc, QWidget* parent) table->setSelectionBehavior(QAbstractItemView::SelectRows); styleDataTable(table); setEmptyHint(table, table == assemblyTable_ - ? "Type an instruction above (for example \"mov eax,1\") and click Analyze to find every place the module contains exactly it." - : "Pick a module and click Analyze."); + ? tr("Type an instruction above (for example \"mov eax,1\") and click Analyze to find every place the module contains exactly it.") + : tr("Pick a module and click Analyze.")); table->verticalHeader()->setVisible(false); connect(table, &QTableWidget::cellDoubleClicked, this, [this, table](int row, int column) { bool ok = false; @@ -68,12 +68,12 @@ CodeReferencesWindow::CodeReferencesWindow(ProcessHandle* proc, QWidget* parent) }); } functionSummaryTable_->setColumnCount(2); - functionSummaryTable_->setHorizontalHeaderLabels({"Function", "References"}); + functionSummaryTable_->setHorizontalHeaderLabels({tr("Function"), tr("References")}); functionSummaryTable_->horizontalHeader()->setStretchLastSection(true); functionSummaryTable_->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); functionSummaryTable_->setSelectionBehavior(QAbstractItemView::SelectRows); styleDataTable(functionSummaryTable_); - setEmptyHint(functionSummaryTable_, "Pick a module and click Analyze."); + setEmptyHint(functionSummaryTable_, tr("Pick a module and click Analyze.")); functionSummaryTable_->verticalHeader()->setVisible(false); connect(functionSummaryTable_, &QTableWidget::cellDoubleClicked, this, [this](int row, int) { bool ok = false; @@ -81,13 +81,13 @@ CodeReferencesWindow::CodeReferencesWindow(ProcessHandle* proc, QWidget* parent) if (ok) emit navigateTo(addr); }); callGraphTable_->setColumnCount(3); - callGraphTable_->setHorizontalHeaderLabels({"Caller", "Callee", "Call Site"}); + callGraphTable_->setHorizontalHeaderLabels({tr("Caller"), tr("Callee"), tr("Call Site")}); callGraphTable_->horizontalHeader()->setStretchLastSection(true); callGraphTable_->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); callGraphTable_->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); callGraphTable_->setSelectionBehavior(QAbstractItemView::SelectRows); styleDataTable(callGraphTable_); - setEmptyHint(callGraphTable_, "Pick a module and click Analyze."); + setEmptyHint(callGraphTable_, tr("Pick a module and click Analyze.")); callGraphTable_->verticalHeader()->setVisible(false); connect(callGraphTable_, &QTableWidget::cellDoubleClicked, this, [this](int row, int column) { bool ok = false; @@ -96,26 +96,26 @@ CodeReferencesWindow::CodeReferencesWindow(ProcessHandle* proc, QWidget* parent) if (ok) emit navigateTo(addr); }); cavesTable_->setColumnCount(2); - cavesTable_->setHorizontalHeaderLabels({"Address", "Size"}); + cavesTable_->setHorizontalHeaderLabels({tr("Address"), tr("Size")}); cavesTable_->horizontalHeader()->setStretchLastSection(true); cavesTable_->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); cavesTable_->setSelectionBehavior(QAbstractItemView::SelectRows); styleDataTable(cavesTable_); - setEmptyHint(cavesTable_, "Pick a module and click Analyze to list unused code space."); + setEmptyHint(cavesTable_, tr("Pick a module and click Analyze to list unused code space.")); cavesTable_->verticalHeader()->setVisible(false); connect(cavesTable_, &QTableWidget::cellDoubleClicked, this, [this](int row, int) { bool ok = false; auto addr = cavesTable_->item(row, 0)->text().toULongLong(&ok, 16); if (ok) emit navigateTo(addr); }); - tabs->addTab(stringsTable_, "Referenced Strings"); - tabs->addTab(functionsTable_, "Referenced Functions"); - tabs->addTab(functionSummaryTable_, "Functions"); - tabs->addTab(callGraphTable_, "Call Graph"); - tabs->addTab(jumpsTable_, "Jumps"); - tabs->addTab(ripRelativeTable_, "RIP-relative"); - tabs->addTab(assemblyTable_, "Assembly Scan"); - tabs->addTab(cavesTable_, "Code Caves"); + tabs->addTab(stringsTable_, tr("Referenced Strings")); + tabs->addTab(functionsTable_, tr("Referenced Functions")); + tabs->addTab(functionSummaryTable_, tr("Functions")); + tabs->addTab(callGraphTable_, tr("Call Graph")); + tabs->addTab(jumpsTable_, tr("Jumps")); + tabs->addTab(ripRelativeTable_, tr("RIP-relative")); + tabs->addTab(assemblyTable_, tr("Assembly Scan")); + tabs->addTab(cavesTable_, tr("Code Caves")); layout->addWidget(tabs, 1); statusLabel_ = new QLabel; @@ -134,7 +134,7 @@ CodeReferencesWindow::CodeReferencesWindow(ProcessHandle* proc, QWidget* parent) } } - statusLabel_->setText(QString("%1 modules available").arg(modules_.size())); + statusLabel_->setText(tr("%1 modules available").arg(modules_.size())); } ModuleInfo CodeReferencesWindow::selectedModule() const { @@ -151,7 +151,7 @@ void CodeReferencesWindow::analyzeSelectedModule() { // escaping exception when it cannot be disassembled. auto architecture = disassemblerArchFor(*proc_, module.base); if (!architecture) { - QMessageBox::warning(this, "Code references", QString::fromStdString(architecture.error())); + QMessageBox::warning(this, tr("Code references"), QString::fromStdString(architecture.error())); return; } CodeAnalyzer analyzer(*architecture); @@ -161,14 +161,14 @@ void CodeReferencesWindow::analyzeSelectedModule() { // keeps the window painting and lets the user bail instead of it looking hung. // (The scan only reads memory via process_vm_readv, so this stays on the UI // thread safely; no ptrace-thread affinity to worry about.) - QProgressDialog progress(QString("Analyzing %1…") + QProgressDialog progress(tr("Analyzing %1…") .arg(QString::fromStdString(module.name.empty() ? module.path : module.name)), - "Cancel", 0, 8, this); + tr("Cancel"), 0, 8, this); progress.setWindowModality(Qt::WindowModal); progress.setMinimumDuration(0); int step = 0; - auto tick = [&](const char* what) { - progress.setLabelText(QString("Analyzing %1: %2…") + auto tick = [&](const QString& what) { + progress.setLabelText(tr("Analyzing %1: %2…") .arg(QString::fromStdString(module.name.empty() ? module.path : module.name)) .arg(what)); progress.setValue(step++); @@ -176,23 +176,23 @@ void CodeReferencesWindow::analyzeSelectedModule() { return !progress.wasCanceled(); }; - if (!tick("referenced strings")) return; + if (!tick(tr("referenced strings"))) return; auto strings = analyzer.findReferencedStrings(*proc_, module); - if (!tick("referenced functions")) return; + if (!tick(tr("referenced functions"))) return; auto functions = analyzer.findReferencedFunctions(*proc_, module); - if (!tick("functions")) return; + if (!tick(tr("functions"))) return; auto functionSummary = analyzer.enumerateFunctions(*proc_, module); - if (!tick("call graph")) return; + if (!tick(tr("call graph"))) return; auto callGraph = analyzer.buildCallGraph(*proc_, module); - if (!tick("jumps")) return; + if (!tick(tr("jumps"))) return; auto jumps = analyzer.findJumps(*proc_, module); - if (!tick("RIP-relative")) return; + if (!tick(tr("RIP-relative"))) return; auto ripRelative = analyzer.findRipRelativeInstructions(*proc_, module); - if (!tick("assembly pattern")) return; + if (!tick(tr("assembly pattern"))) return; auto assembly = assemblyPatternEdit_->text().trimmed().isEmpty() ? std::vector{} : analyzer.findAssemblyPattern(*proc_, module, assemblyPatternEdit_->text().toStdString()); - if (!tick("code caves")) return; + if (!tick(tr("code caves"))) return; auto caves = analyzer.findCodeCaves(*proc_, module, minCaveSizeSpin_->value()); progress.setValue(8); @@ -204,7 +204,7 @@ void CodeReferencesWindow::analyzeSelectedModule() { fillTable(ripRelativeTable_, ripRelative); fillTable(assemblyTable_, assembly); fillCavesTable(caves); - statusLabel_->setText(QString("%1: %2 strings, %3 calls, %4 functions, %5 jumps, %6 RIP-relative, %7 assembly, %8 caves") + statusLabel_->setText(tr("%1: %2 strings, %3 calls, %4 functions, %5 jumps, %6 RIP-relative, %7 assembly, %8 caves") .arg(QString::fromStdString(module.name)) .arg(strings.size()) .arg(functions.size()) diff --git a/gui/debuggerwindow.cpp b/gui/debuggerwindow.cpp index bb54ef18..9d936551 100644 --- a/gui/debuggerwindow.cpp +++ b/gui/debuggerwindow.cpp @@ -1,6 +1,7 @@ #include "gui/debuggerwindow.hpp" #include "core/local_target.hpp" #include +#include #include #include #include "core/target_capabilities.hpp" @@ -45,7 +46,7 @@ static QString hex(uintptr_t v) { return QStringLiteral("0x%1").arg(v, 0, 16); } DebuggerWindow::DebuggerWindow(ce::ProcessHandle* proc, QWidget* parent) : QMainWindow(parent), proc_(proc), session_(std::make_unique()) { - setWindowTitle("Debugger"); + setWindowTitle(tr("Debugger")); resize(1080, 720); auto* central = new QWidget(this); @@ -53,29 +54,32 @@ DebuggerWindow::DebuggerWindow(ce::ProcessHandle* proc, QWidget* parent) // ── control row ── auto* controls = new QHBoxLayout(); - contBtn_ = new QPushButton("Continue"); contBtn_->setShortcut(Qt::Key_F9); - intoBtn_ = new QPushButton("Step Into"); intoBtn_->setShortcut(Qt::Key_F7); - overBtn_ = new QPushButton("Step Over"); overBtn_->setShortcut(Qt::Key_F8); - outBtn_ = new QPushButton("Step Out"); - rtcBtn_ = new QPushButton("Run to Cursor"); rtcBtn_->setShortcut(Qt::Key_F4); - detachBtn_ = new QPushButton("Detach"); + contBtn_ = new QPushButton(tr("Continue")); contBtn_->setShortcut(Qt::Key_F9); + intoBtn_ = new QPushButton(tr("Step Into")); intoBtn_->setShortcut(Qt::Key_F7); + overBtn_ = new QPushButton(tr("Step Over")); overBtn_->setShortcut(Qt::Key_F8); + outBtn_ = new QPushButton(tr("Step Out")); + rtcBtn_ = new QPushButton(tr("Run to Cursor")); rtcBtn_->setShortcut(Qt::Key_F4); + detachBtn_ = new QPushButton(tr("Detach")); for (auto* b : {contBtn_, intoBtn_, overBtn_, outBtn_, rtcBtn_, detachBtn_}) controls->addWidget(b); // Break on exceptions (CE's exception breakpoints): a checkable menu of the // common CPU-trap signals. Toggling one arms/disarms addExceptionBreakpoint. auto* excBtn = new QToolButton(); - excBtn->setText("Break on exceptions"); + excBtn->setText(tr("Break on exceptions")); excBtn->setPopupMode(QToolButton::InstantPopup); auto* excMenu = new QMenu(excBtn); struct SigEntry { const char* name; int sig; }; static const SigEntry kSigs[] = { - {"SIGSEGV (segfault)", SIGSEGV}, {"SIGILL (illegal instruction)", SIGILL}, - {"SIGFPE (FP / divide-by-zero)", SIGFPE}, {"SIGBUS (bus error)", SIGBUS}, - {"SIGABRT (abort)", SIGABRT}, {"SIGTRAP", SIGTRAP}, + {QT_TRANSLATE_NOOP("ce::gui::DebuggerWindow", "SIGSEGV (segfault)"), SIGSEGV}, + {QT_TRANSLATE_NOOP("ce::gui::DebuggerWindow", "SIGILL (illegal instruction)"), SIGILL}, + {QT_TRANSLATE_NOOP("ce::gui::DebuggerWindow", "SIGFPE (FP / divide-by-zero)"), SIGFPE}, + {QT_TRANSLATE_NOOP("ce::gui::DebuggerWindow", "SIGBUS (bus error)"), SIGBUS}, + {QT_TRANSLATE_NOOP("ce::gui::DebuggerWindow", "SIGABRT (abort)"), SIGABRT}, + {"SIGTRAP", SIGTRAP}, // bare signal name; nothing to translate }; for (const auto& s : kSigs) { - auto* a = excMenu->addAction(s.name); + auto* a = excMenu->addAction(tr(s.name)); a->setCheckable(true); int sig = s.sig; connect(a, &QAction::toggled, this, [this, sig](bool on) { @@ -90,7 +94,7 @@ DebuggerWindow::DebuggerWindow(ce::ProcessHandle* proc, QWidget* parent) controls->addStretch(); root->addLayout(controls); - statusLabel_ = new QLabel("Not attached"); + statusLabel_ = new QLabel(tr("Not attached")); root->addWidget(statusLabel_); const QFont mono = monospaceFont(); @@ -103,7 +107,7 @@ DebuggerWindow::DebuggerWindow(ce::ProcessHandle* proc, QWidget* parent) disasmView_->setReadOnly(true); disasmView_->setFont(mono); disasmView_->setLineWrapMode(QPlainTextEdit::NoWrap); - disasmView_->setPlaceholderText("The disassembly around the stopped thread's instruction pointer appears here."); + disasmView_->setPlaceholderText(tr("The disassembly around the stopped thread's instruction pointer appears here.")); disasmView_->setContextMenuPolicy(Qt::CustomContextMenu); connect(disasmView_, &QPlainTextEdit::customContextMenuRequested, this, &DebuggerWindow::onDisasmContextMenu); @@ -112,14 +116,14 @@ DebuggerWindow::DebuggerWindow(ce::ProcessHandle* proc, QWidget* parent) stackView_->setReadOnly(true); stackView_->setFont(mono); stackView_->setLineWrapMode(QPlainTextEdit::NoWrap); - stackView_->setPlaceholderText("Stack of the stopped thread"); + stackView_->setPlaceholderText(tr("Stack of the stopped thread")); leftSplit->addWidget(stackView_); auto* memWidget = new QWidget(); auto* memLayout = new QVBoxLayout(memWidget); memLayout->setContentsMargins(0, 0, 0, 0); memAddrInput_ = new QLineEdit(); - memAddrInput_->setPlaceholderText("memory address (hex), then Enter"); + memAddrInput_->setPlaceholderText(tr("memory address (hex), then Enter")); memLayout->addWidget(memAddrInput_); memView_ = new QTextEdit(); memView_->setReadOnly(true); @@ -127,8 +131,8 @@ DebuggerWindow::DebuggerWindow(ce::ProcessHandle* proc, QWidget* parent) memView_->setLineWrapMode(QTextEdit::NoWrap); // A read-only QTextEdit does not paint its placeholder, so start with a dim hint // that the first dump replaces. - memView_->setHtml(QString("Type an address above and press Enter " - "to dump memory here.").arg(ce::gui::editorPalette().dim.name())); + memView_->setHtml(tr("Type an address above and press Enter " + "to dump memory here.").arg(ce::gui::editorPalette().dim.name())); memLayout->addWidget(memView_); leftSplit->addWidget(memWidget); @@ -140,7 +144,7 @@ DebuggerWindow::DebuggerWindow(ce::ProcessHandle* proc, QWidget* parent) auto* right = new QWidget(); auto* rl = new QVBoxLayout(right); auto* threadRow = new QHBoxLayout(); - threadRow->addWidget(new QLabel("Thread:")); + threadRow->addWidget(new QLabel(tr("Thread:"))); threadCombo_ = new QComboBox(); threadRow->addWidget(threadCombo_, 1); rl->addLayout(threadRow); @@ -153,29 +157,29 @@ DebuggerWindow::DebuggerWindow(ce::ProcessHandle* proc, QWidget* parent) regTable_->verticalHeader()->setDefaultSectionSize(QFontMetrics(mono).height() + 8); regTable_->setShowGrid(false); regTable_->setAlternatingRowColors(true); - regTable_->setToolTip("Double-click a value to edit the register while the thread is stopped"); + regTable_->setToolTip(tr("Double-click a value to edit the register while the thread is stopped")); rl->addWidget(regTable_, 3); // Decoded CPU flags under the register table (RFLAGS shows only the raw hex). - flagsLabel_ = new QLabel(QStringLiteral("Flags:")); + flagsLabel_ = new QLabel(tr("Flags:")); flagsLabel_->setFont(mono); - flagsLabel_->setToolTip("Status flags of the currently stopped thread"); + flagsLabel_->setToolTip(tr("Status flags of the currently stopped thread")); rl->addWidget(flagsLabel_); - auto* bpGroup = new QGroupBox("Breakpoints"); + auto* bpGroup = new QGroupBox(tr("Breakpoints")); auto* bl = new QVBoxLayout(bpGroup); auto* addRow = new QHBoxLayout(); bpInput_ = new QLineEdit(); - bpInput_->setPlaceholderText("address (hex), then Add"); - auto* addBtn = new QPushButton("Add"); - auto* dataBpBtn = new QPushButton("Data BP..."); - dataBpBtn->setToolTip("Hardware watchpoint: break when an address is written or accessed"); + bpInput_->setPlaceholderText(tr("address (hex), then Add")); + auto* addBtn = new QPushButton(tr("Add")); + auto* dataBpBtn = new QPushButton(tr("Data BP...")); + dataBpBtn->setToolTip(tr("Hardware watchpoint: break when an address is written or accessed")); addRow->addWidget(bpInput_); addRow->addWidget(addBtn); addRow->addWidget(dataBpBtn); bl->addLayout(addRow); bpList_ = new QListWidget(); - bpList_->setToolTip("Check to enable/disable · double-click to set/edit its condition"); + bpList_->setToolTip(tr("Check to enable/disable · double-click to set/edit its condition")); connect(bpList_, &QListWidget::itemDoubleClicked, this, [this](QListWidgetItem*) { editBreakpointCondition(); }); // Checkbox toggles enable/disable: unchecking removes the planted breakpoint @@ -188,7 +192,7 @@ DebuggerWindow::DebuggerWindow(ce::ProcessHandle* proc, QWidget* parent) if (!session_->isAttached() || !paused()) { QSignalBlocker block(bpList_); it->setCheckState(bps_[row].enabled ? Qt::Checked : Qt::Unchecked); - statusLabel_->setText("Stop the target to enable/disable breakpoints"); + statusLabel_->setText(tr("Stop the target to enable/disable breakpoints")); return; } if (wantEnabled) { @@ -198,7 +202,7 @@ DebuggerWindow::DebuggerWindow(ce::ProcessHandle* proc, QWidget* parent) if (id <= 0) { QSignalBlocker block(bpList_); it->setCheckState(Qt::Unchecked); - statusLabel_->setText("Could not enable breakpoint"); + statusLabel_->setText(tr("Could not enable breakpoint")); return; } bps_[row].id = id; @@ -209,19 +213,20 @@ DebuggerWindow::DebuggerWindow(ce::ProcessHandle* proc, QWidget* parent) if (!removed) { QSignalBlocker block(bpList_); it->setCheckState(Qt::Checked); - statusLabel_->setText("Breakpoint cleanup pending: "+QString::fromStdString(session_->lastError().message())); + statusLabel_->setText(tr("Breakpoint cleanup pending: %1") + .arg(QString::fromStdString(session_->lastError().message()))); return; } bps_[row].enabled = false; } if (paused()) updateDisassembly(session_->getStopContext()); }); - setEmptyHint(bpList_, "No breakpoints yet.\nAdd one above, or press F5\non a line of code."); + setEmptyHint(bpList_, tr("No breakpoints yet.\nAdd one above, or press F5\non a line of code.")); bl->addWidget(bpList_); auto* rmRow = new QHBoxLayout(); rmRow->addStretch(); - rmBtn_ = new QPushButton("Remove"); - rmBtn_->setToolTip("Remove the selected breakpoint"); + rmBtn_ = new QPushButton(tr("Remove")); + rmBtn_->setToolTip(tr("Remove the selected breakpoint")); rmBtn_->setEnabled(false); connect(bpList_, &QListWidget::currentRowChanged, rmBtn_, [this](int row) { rmBtn_->setEnabled(row >= 0 && !stepping_); }); rmRow->addWidget(rmBtn_); @@ -269,8 +274,10 @@ DebuggerWindow::DebuggerWindow(ce::ProcessHandle* proc, QWidget* parent) prevGp_.clear(); prevVectors_ = {}; shownGp_.clear(); shownVectors_ = {}; // fresh session: don't flag the first stop's registers refreshStopped(); // target is all-stopped right after attach } else { - statusLabel_->setText(proc_ ? "Attach failed: " + QString::fromStdString( - ce::targetOperationError(*proc_, ce::TargetFeature::Debugger, session_->lastError())) : "No process attached"); + statusLabel_->setText(proc_ + ? tr("Attach failed: %1").arg(QString::fromStdString( + ce::targetOperationError(*proc_, ce::TargetFeature::Debugger, session_->lastError()))) + : tr("No process attached")); setRunningUi(false, true); } } @@ -298,13 +305,14 @@ void DebuggerWindow::setRunningUi(bool running, bool exited) { void DebuggerWindow::onContinue() { if (!session_->isAttached() || stepping_) return; - statusLabel_->setText("Running..."); + statusLabel_->setText(tr("Running...")); setRunningUi(true); // Continue can leave the target stopped without reporting a stop: show // it as stopped, with the reason, instead of "Running...". if (!session_->continueExecution() && session_->isAttached() && session_->isStopped()) { refreshStopped(); - statusLabel_->setText("Continue failed, still stopped: "+QString::fromStdString(session_->lastError().message())); + statusLabel_->setText(tr("Continue failed, still stopped: %1") + .arg(QString::fromStdString(session_->lastError().message()))); } } @@ -317,7 +325,7 @@ void DebuggerWindow::runStep(ce::StepMode mode) { if (stepThread_.joinable()) stepThread_.join(); // a finished step not yet collected stepping_ = true; setRunningUi(true); - statusLabel_->setText("Stepping... (Detach ends a step that does not return)"); + statusLabel_->setText(tr("Stepping... (Detach ends a step that does not return)")); stepThread_ = std::thread([this, mode] { try { session_->step(mode); } catch (...) {} // a detach can abandon the command QMetaObject::invokeMethod(this, [this] { finishStep(); }, Qt::QueuedConnection); @@ -340,7 +348,8 @@ void DebuggerWindow::runToAddress(uintptr_t addr) { if (!addr) return; // One run-to-cursor at a time: a previous one that stopped short is dropped. if (!clearRunToCursor()) { - statusLabel_->setText("Run-to-cursor cleanup pending: "+QString::fromStdString(session_->lastError().message())); + statusLabel_->setText(tr("Run-to-cursor cleanup pending: %1") + .arg(QString::fromStdString(session_->lastError().message()))); return; } // If an armed user breakpoint already sits there, just continue to it. @@ -355,7 +364,7 @@ void DebuggerWindow::runToAddress(uintptr_t addr) { void DebuggerWindow::onDetach() { if (session_->isAttached()) session_->detach(); - statusLabel_->setText("Detached"); + statusLabel_->setText(tr("Detached")); setRunningUi(false, true); } @@ -364,14 +373,14 @@ void DebuggerWindow::onAddBreakpoint() { if (text.startsWith("0x") || text.startsWith("0X")) text = text.mid(2); bool ok = false; uintptr_t addr = text.toULongLong(&ok, 16); - if (!ok || !addr) { statusLabel_->setText("Invalid breakpoint address"); return; } + if (!ok || !addr) { statusLabel_->setText(tr("Invalid breakpoint address")); return; } addBreakpointAt(addr); bpInput_->clear(); } void DebuggerWindow::addBreakpointAt(uintptr_t addr, const QString& condition) { if (!session_->isAttached() || !paused()) { - statusLabel_->setText("Stop the target before setting a breakpoint"); + statusLabel_->setText(tr("Stop the target before setting a breakpoint")); return; } for (size_t i = 0; i < bps_.size(); ++i) if (bps_[i].addr == addr) { // already set — update condition @@ -384,7 +393,7 @@ void DebuggerWindow::addBreakpointAt(uintptr_t addr, const QString& condition) { auto read = proc_ ? proc_->read(addr, original.data(), original.size()) : ce::Result{std::unexpected(std::make_error_code(std::errc::bad_address))}; if (!read || *read != original.size()) original.clear(); int id = session_->setSoftwareBreakpoint(addr); - if (id <= 0) { statusLabel_->setText("Failed to set breakpoint at " + hex(addr)); return; } + if (id <= 0) { statusLabel_->setText(tr("Failed to set breakpoint at %1").arg(hex(addr))); return; } Bp bp; bp.id = id; bp.addr = addr; bp.condition = condition.toStdString(); bp.original = std::move(original); bps_.push_back(bp); @@ -394,13 +403,16 @@ void DebuggerWindow::addBreakpointAt(uintptr_t addr, const QString& condition) { } QString DebuggerWindow::bpLabel(uintptr_t addr, const QString& condition) { - return condition.isEmpty() ? hex(addr) : hex(addr) + " if " + condition; + // static member function: name the context explicitly for lupdate/runtime. + return condition.isEmpty() ? hex(addr) + : hex(addr) + " " + + QCoreApplication::translate("DebuggerWindow", "if %1").arg(condition); } QString DebuggerWindow::bpRowText(const Bp& b) const { QString base = b.hardware ? bpDataLabel(b.addr, b.hwType, b.hwSize) : bpLabel(b.addr, QString::fromStdString(b.condition)); - if (b.hitCount > 0) base += QStringLiteral(" (hits: %1)").arg(b.hitCount); + if (b.hitCount > 0) base += " " + tr("(hits: %1)").arg(b.hitCount); return base; } @@ -422,9 +434,9 @@ void DebuggerWindow::setConditionalBreakpointAtCursor() { const auto architecture = session_->getStopContext().architecture; const QString example = architecture == ce::CpuArchitecture::Arm64 ? "X0 == 5" : architecture == ce::CpuArchitecture::X86_32 ? "EAX == 5" : "RAX == 5"; - QString cond = QInputDialog::getText(this, "Conditional breakpoint", - "Break only when this expression is true\n" - "(Lua; register names accept either case, also available in ctx, e.g. \"" + example + "\"):", + QString cond = QInputDialog::getText(this, tr("Conditional breakpoint"), + tr("Break only when this expression is true\n" + "(Lua; register names accept either case, also available in ctx, e.g. \"%1\"):").arg(example), QLineEdit::Normal, QString(), &ok); if (!ok) return; addBreakpointAt(addr, cond.trimmed()); @@ -439,13 +451,13 @@ void DebuggerWindow::editBreakpointCondition() { const bool hardware = bps_[row].hardware; bool ok = false; QString cur = QString::fromStdString(bps_[row].condition); - QString cond = QInputDialog::getText(this, "Edit breakpoint condition", - "Break only when this expression is true (empty = unconditional):", + QString cond = QInputDialog::getText(this, tr("Edit breakpoint condition"), + tr("Break only when this expression is true (empty = unconditional):"), QLineEdit::Normal, cur, &ok); if (!ok) return; auto it = std::find_if(bps_.begin(), bps_.end(), [&](const Bp& b) { return b.addr == addr && b.hardware == hardware; }); - if (it == bps_.end()) { statusLabel_->setText("The breakpoint was removed while editing"); return; } + if (it == bps_.end()) { statusLabel_->setText(tr("The breakpoint was removed while editing")); return; } it->condition = cond.trimmed().toStdString(); refreshBpRow(static_cast(it - bps_.begin())); } @@ -461,14 +473,15 @@ bool DebuggerWindow::removeBreakpointRow(int row) { // never returns). Removal while running is fine: the session stops the // target for it. if (stepping_) { - statusLabel_->setText("Wait for the step to finish (or Detach) before removing a breakpoint"); + statusLabel_->setText(tr("Wait for the step to finish (or Detach) before removing a breakpoint")); return false; } if (bps_[row].enabled) { bool removed=bps_[row].hardware ? session_->removeHardwareBreakpoint(bps_[row].id) : session_->removeSoftwareBreakpoint(bps_[row].id); if (!removed) { - statusLabel_->setText("Breakpoint cleanup pending: "+QString::fromStdString(session_->lastError().message())); + statusLabel_->setText(tr("Breakpoint cleanup pending: %1") + .arg(QString::fromStdString(session_->lastError().message()))); return false; } } @@ -496,27 +509,28 @@ QString DebuggerWindow::bpDataLabel(uintptr_t addr, int type, int size) { void DebuggerWindow::onAddDataBreakpoint() { if (!session_->isAttached() || !paused()) { - statusLabel_->setText("Stop the target before setting a breakpoint"); + statusLabel_->setText(tr("Stop the target before setting a breakpoint")); return; } // One dialog for the whole watchpoint: address, trigger and size. QDialog dialog(this); - dialog.setWindowTitle("Data breakpoint"); + dialog.setWindowTitle(tr("Data breakpoint")); auto* form = new QFormLayout(&dialog); auto* addrEdit = new QLineEdit(bpInput_->text().trimmed(), &dialog); - addrEdit->setPlaceholderText("address (hex)"); + addrEdit->setPlaceholderText(tr("address (hex)")); addrEdit->setMinimumWidth(220); auto* typeCombo = new QComboBox(&dialog); - typeCombo->addItem("Writes", 1); - typeCombo->addItem("Reads or writes", 3); + typeCombo->addItem(tr("Writes"), 1); + typeCombo->addItem(tr("Reads or writes"), 3); auto* sizeCombo = new QComboBox(&dialog); - for (int size : {1, 2, 4, 8}) sizeCombo->addItem(QString("%1 %2").arg(size).arg(size == 1 ? "byte" : "bytes"), size); + for (int size : {1, 2, 4, 8}) + sizeCombo->addItem(size == 1 ? tr("%1 byte").arg(size) : tr("%1 bytes").arg(size), size); sizeCombo->setCurrentIndex(std::max(0, sizeCombo->findData(QSettings().value("debug/bpSize", "4").toInt()))); - form->addRow("Address:", addrEdit); - form->addRow("Break on:", typeCombo); - form->addRow("Size:", sizeCombo); + form->addRow(tr("Address:"), addrEdit); + form->addRow(tr("Break on:"), typeCombo); + form->addRow(tr("Size:"), sizeCombo); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); - buttons->button(QDialogButtonBox::Ok)->setText("Set"); + buttons->button(QDialogButtonBox::Ok)->setText(tr("Set")); form->addRow(buttons); connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); @@ -527,14 +541,14 @@ void DebuggerWindow::onAddDataBreakpoint() { uintptr_t addr = addrStr.startsWith("0x", Qt::CaseInsensitive) ? addrStr.mid(2).toULongLong(&ok, 16) : addrStr.toULongLong(&ok, 16); - if (!ok || !addr) { statusLabel_->setText("Invalid address"); return; } + if (!ok || !addr) { statusLabel_->setText(tr("Invalid address")); return; } const int type = typeCombo->currentData().toInt(); // 1=write, 3=access const int size = sizeCombo->currentData().toInt(); for (auto& b : bps_) if (b.hardware && b.addr == addr) return; // already watched int id = session_->setHardwareBreakpoint(addr, type, size); if (id <= 0) { - statusLabel_->setText("No free debug register (max 4 hardware breakpoints)"); + statusLabel_->setText(tr("No free debug register (max 4 hardware breakpoints)")); return; } Bp bp; bp.id = id; bp.addr = addr; bp.hardware = true; bp.hwType = type; bp.hwSize = size; @@ -546,7 +560,7 @@ void DebuggerWindow::onAddDataBreakpoint() { void DebuggerWindow::onDebugEvent(int type) { if (type == static_cast(ce::DebugEventType::ThreadExiting)) { refreshStopped(); - statusLabel_->setText(QStringLiteral("Thread is exiting; stopped on tid %1") + statusLabel_->setText(tr("Thread is exiting; stopped on tid %1") .arg(static_cast(session_->activeThread()))); return; } @@ -556,11 +570,11 @@ void DebuggerWindow::onDebugEvent(int type) { pendingRtcId_=-1; pendingRtcAddr_=0; prevGp_.clear(); prevVectors_={}; shownGp_.clear(); shownVectors_={}; refreshStopped(); - statusLabel_->setText("Process executed a new program; previous breakpoints cleared"); + statusLabel_->setText(tr("Process executed a new program; previous breakpoints cleared")); return; } if (type == static_cast(ce::DebugEventType::ProcessExited)) { - statusLabel_->setText("Process exited"); + statusLabel_->setText(tr("Process exited")); setRunningUi(false, true); return; } @@ -580,8 +594,8 @@ void DebuggerWindow::onDebugEvent(int type) { setRunningUi(true); if (!session_->continueExecution() && session_->isAttached() && session_->isStopped()) { refreshStopped(); - statusLabel_->setText("Continue failed, still stopped: "+ - QString::fromStdString(session_->lastError().message())); + statusLabel_->setText(tr("Continue failed, still stopped: %1") + .arg(QString::fromStdString(session_->lastError().message()))); } return; } @@ -594,7 +608,8 @@ void DebuggerWindow::onDebugEvent(int type) { // target, and also on a stop anywhere else, so it never fires later. if (!clearRunToCursor()) { refreshStopped(); - statusLabel_->setText("Run-to-cursor cleanup pending: "+QString::fromStdString(session_->lastError().message())); + statusLabel_->setText(tr("Run-to-cursor cleanup pending: %1") + .arg(QString::fromStdString(session_->lastError().message()))); return; } refreshStopped(); @@ -618,7 +633,7 @@ void DebuggerWindow::refreshStopped() { updateDisassembly(ctx); updateStack(ctx); if (lastMemAddr_) updateMemoryView(lastMemAddr_); // keep the hex pane current - statusLabel_->setText(QStringLiteral("Stopped at %1 (tid %2)") + statusLabel_->setText(tr("Stopped at %1 (tid %2)") .arg(hex(ctx.instructionPointer())).arg(static_cast(session_->activeThread()))); setRunningUi(false); emit stopped(ctx.instructionPointer()); @@ -676,7 +691,7 @@ void DebuggerWindow::updateMemoryView(uintptr_t addr) { uint8_t buf[128]; auto r = proc_->read(addr, buf, sizeof(buf)); size_t n = (r && *r) ? *r : 0; - if (n == 0) { memView_->setPlainText(" "); memChanged_.clear(); return; } + if (n == 0) { memView_->setPlainText(" " + tr("").arg(hex(addr))); memChanged_.clear(); return; } // Flag bytes that changed since the previous dump at the SAME address, so stepping // shows what the code just wrote (like the standalone hex pane). A new address resets. @@ -819,8 +834,8 @@ void DebuggerWindow::updateRegisters(const ce::CpuContext& c) { if (flagsLabel_) { // Show every status flag's 0/1 state (CE-style), not just the set ones, so a // clear flag next to a conditional jump is still readable. - flagsLabel_->setText(QStringLiteral("Flags: ") + - (kGp ? QString::fromStdString(ce::describeCpuStatusFlags(c)) : QStringLiteral("Unavailable"))); + flagsLabel_->setText(tr("Flags: %1") + + (kGp ? QString::fromStdString(ce::describeCpuStatusFlags(c)) : tr("Unavailable"))); } // Show all architectural SIMD registers, preserving unavailable reads. for (int r = 0; r < vectorCount; ++r) { @@ -831,7 +846,7 @@ void DebuggerWindow::updateRegisters(const ce::CpuContext& c) { int byte = std::endian::native == std::endian::little ? 15 - b : b; s += QString::asprintf("%02x", vectors.registers[r][byte]); } - } else s = "Unavailable"; + } else s = tr("Unavailable"); it->setText(s); it->setForeground(hasPrev && r < static_cast(vectors.count) && r < static_cast(prevVectors_.count) && vectors.registers[r] != prevVectors_.registers[r] ? changedFg : normalFg); @@ -840,7 +855,7 @@ void DebuggerWindow::updateRegisters(const ce::CpuContext& c) { auto* it = cell(kGp + vectorCount + i, false); uint32_t value = i ? vectors.status : vectors.control; uint32_t previous = i ? prevVectors_.status : prevVectors_.control; - it->setText(vectors.count ? QStringLiteral("0x%1").arg(value, 8, 16, QLatin1Char('0')) : QStringLiteral("Unavailable")); + it->setText(vectors.count ? QStringLiteral("0x%1").arg(value, 8, 16, QLatin1Char('0')) : tr("Unavailable")); it->setForeground(hasPrev && vectors.count && prevVectors_.count && value != previous ? changedFg : normalFg); } registerArchitecture_ = c.architecture; @@ -875,15 +890,16 @@ void DebuggerWindow::onRegisterEdited(QTableWidgetItem* item) { auto registers = ce::cpuRegisterValues(ctx); if (item->row() < 0 || item->row() >= static_cast(registers.size())) return; if (!ce::setCpuRegisterValue(ctx, item->row(), v)) { - statusLabel_->setText(QStringLiteral("%1 needs a %2-bit hexadecimal value") + statusLabel_->setText(tr("%1 needs a %2-bit hexadecimal value") .arg(QString::fromStdString(registers[item->row()].name)).arg(registers[item->row()].bits)); } else if (session_->setStopContext(ctx)) { refreshStopped(); return; } else { - statusLabel_->setText("Register edit failed: " + QString::fromStdString(session_->lastError().message())); + statusLabel_->setText(tr("Register edit failed: %1") + .arg(QString::fromStdString(session_->lastError().message()))); } - } else statusLabel_->setText("Invalid hexadecimal register value"); + } else statusLabel_->setText(tr("Invalid hexadecimal register value")); updateRegisters(session_->getStopContext()); } @@ -918,7 +934,7 @@ void DebuggerWindow::updateDisassembly(const ce::CpuContext& c) { else if (c.architecture == ce::CpuArchitecture::X86_32) disasm_.setArch(ce::Arch::X86_32); else if (c.architecture == ce::CpuArchitecture::X86_64) disasm_.setArch(ce::Arch::X86_64); else { - disasmView_->setPlainText(" "); + disasmView_->setPlainText(" " + tr("")); disasmView_->setExtraSelections({}); return; } @@ -1019,7 +1035,7 @@ void DebuggerWindow::updateDisassembly(const ce::CpuContext& c) { ce::conditionalArm64JumpTaken(in.mnemonic, c.pstate) : ce::conditionalJumpTaken(in.mnemonic, c.rflags); if (jt) - anno += *jt ? QStringLiteral(" (will jump)") : QStringLiteral(" (no jump)"); + anno += " " + (*jt ? tr("(will jump)") : tr("(no jump)")); } // User comment (set in the Memory Viewer): show it inline too, so annotated // code reads the same while debugging. @@ -1032,7 +1048,7 @@ void DebuggerWindow::updateDisassembly(const ce::CpuContext& c) { disasmLineAddrs_.push_back(in.address); } } else { - out = " "; + out = " " + tr("").arg(hex(ip)); } disasmView_->setPlainText(out); @@ -1073,7 +1089,7 @@ void DebuggerWindow::updateStack(const ce::CpuContext& c) { const auto order = c.architecture == ce::CpuArchitecture::Arm64 ? proc_->targetDescription().host.byteOrder : ce::ByteOrder::Little; if (!width || order == ce::ByteOrder::Unknown) { - stackView_->setPlainText(" "); + stackView_->setPlainText(" " + tr("")); return; } for (int i = 0; i < 16; ++i) { @@ -1165,21 +1181,21 @@ void DebuggerWindow::nopInstructionAtCursor() { // sits inside it. uint8_t buf[16]; auto rr = proc_->read(addr, buf, sizeof(buf)); - if (!rr || *rr == 0) { statusLabel_->setText("Cannot read the instruction at " + hex(addr)); return; } + if (!rr || *rr == 0) { statusLabel_->setText(tr("Cannot read the instruction at %1").arg(hex(addr))); return; } for (const auto& b : bps_) { if (b.hardware || b.addr < addr || b.addr - addr >= *rr) continue; const size_t offset = b.addr - addr; std::copy_n(b.original.begin(), std::min(b.original.size(), *rr - offset), buf + offset); } auto insns = disasm_.disassemble(addr, {buf, *rr}, 1); - if (insns.empty() || insns[0].size == 0) { statusLabel_->setText("No instruction at " + hex(addr)); return; } + if (insns.empty() || insns[0].size == 0) { statusLabel_->setText(tr("No instruction at %1").arg(hex(addr))); return; } for (const auto& b : bps_) if (!b.hardware && b.enabled && b.addr >= addr && b.addr - addr < insns[0].size) { - statusLabel_->setText("Remove the breakpoint on this instruction before replacing it with NOPs"); + statusLabel_->setText(tr("Remove the breakpoint on this instruction before replacing it with NOPs")); return; } if (ce::nopInstruction(*proc_, addr).empty()) - statusLabel_->setText("Could not replace the instruction at " + hex(addr) + " with NOPs"); + statusLabel_->setText(tr("Could not replace the instruction at %1 with NOPs").arg(hex(addr))); updateDisassembly(session_->getStopContext()); // show the NOPs } @@ -1191,13 +1207,13 @@ void DebuggerWindow::onDisasmContextMenu(const QPoint& pos) { const int block = disasmView_->textCursor().blockNumber(); const bool onLine = block >= 0 && block < static_cast(disasmLineAddrs_.size()); QMenu menu(this); - for (QAction* a : {menu.addAction("Set breakpoint here", this, &DebuggerWindow::setBreakpointAtCursor), - menu.addAction("Set conditional breakpoint...", this, &DebuggerWindow::setConditionalBreakpointAtCursor), - menu.addAction("Replace with NOPs", this, &DebuggerWindow::nopInstructionAtCursor)}) + for (QAction* a : {menu.addAction(tr("Set breakpoint here"), this, &DebuggerWindow::setBreakpointAtCursor), + menu.addAction(tr("Set conditional breakpoint..."), this, &DebuggerWindow::setConditionalBreakpointAtCursor), + menu.addAction(tr("Replace with NOPs"), this, &DebuggerWindow::nopInstructionAtCursor)}) a->setEnabled(onLine); menu.addSeparator(); // Run to this line (CE's F4): only meaningful while the target is paused. - QAction* rtc = menu.addAction("Run to cursor", this, &DebuggerWindow::onRunToCursor); + QAction* rtc = menu.addAction(tr("Run to cursor"), this, &DebuggerWindow::onRunToCursor); rtc->setEnabled(onLine && paused()); menu.exec(disasmView_->viewport()->mapToGlobal(pos)); } diff --git a/gui/elfinspector.cpp b/gui/elfinspector.cpp index 21d1cb51..247af433 100644 --- a/gui/elfinspector.cpp +++ b/gui/elfinspector.cpp @@ -18,18 +18,18 @@ namespace ce::gui { ElfInspector::ElfInspector(const QString& initialPath, QWidget* parent) : QDialog(parent) { - setWindowTitle("ELF Inspector"); + setWindowTitle(tr("ELF Inspector")); resize(900, 640); auto* root = new QVBoxLayout(this); auto* row = new QHBoxLayout; - row->addWidget(new QLabel("ELF file:")); + row->addWidget(new QLabel(tr("ELF file:"))); pathEdit_ = new QLineEdit; pathEdit_->setText(initialPath); row->addWidget(pathEdit_, /*stretch=*/1); - auto* browseBtn = new QPushButton("Browse…"); - auto* loadBtn = new QPushButton("Load"); + auto* browseBtn = new QPushButton(tr("Browse…")); + auto* loadBtn = new QPushButton(tr("Load")); row->addWidget(browseBtn); row->addWidget(loadBtn); root->addLayout(row); @@ -38,7 +38,7 @@ ElfInspector::ElfInspector(const QString& initialPath, QWidget* parent) : QDialo connect(loadBtn, &QPushButton::clicked, this, &ElfInspector::onLoad); tabs_ = new QTabWidget; - auto makePane = [&](const char* title) { + auto makePane = [&](const QString& title) { auto* edit = new QPlainTextEdit; edit->setReadOnly(true); edit->setFont(monospaceFont(-1)); @@ -46,18 +46,18 @@ ElfInspector::ElfInspector(const QString& initialPath, QWidget* parent) : QDialo tabs_->addTab(edit, title); return edit; }; - headerView_ = makePane("Header"); - sectionView_ = makePane("Sections"); - programView_ = makePane("Segments"); - dynamicView_ = makePane("Dynamic"); - symbolView_ = makePane("Symbols"); - notesView_ = makePane("Notes"); + headerView_ = makePane(tr("Header")); + sectionView_ = makePane(tr("Sections")); + programView_ = makePane(tr("Segments")); + dynamicView_ = makePane(tr("Dynamic")); + symbolView_ = makePane(tr("Symbols")); + notesView_ = makePane(tr("Notes")); root->addWidget(tabs_, 1); auto* btnRow = new QHBoxLayout; btnRow->addStretch(); - auto* closeBtn = new QPushButton("Close"); + auto* closeBtn = new QPushButton(tr("Close")); connect(closeBtn, &QPushButton::clicked, this, &QDialog::accept); btnRow->addWidget(closeBtn); root->addLayout(btnRow); @@ -66,8 +66,8 @@ ElfInspector::ElfInspector(const QString& initialPath, QWidget* parent) : QDialo } void ElfInspector::onBrowse() { - auto path = QFileDialog::getOpenFileName(this, "Choose ELF file", - pathEdit_->text(), "All Files (*)"); + auto path = QFileDialog::getOpenFileName(this, tr("Choose ELF file"), + pathEdit_->text(), tr("All Files (*)")); if (!path.isEmpty()) pathEdit_->setText(path); } @@ -76,12 +76,12 @@ void ElfInspector::runReadelf(QPlainTextEdit* sink, const QStringList& args) { // (cecore commonly runs as root) cannot substitute a malicious binary. static const QString readelfPath = QStandardPaths::findExecutable("readelf"); if (readelfPath.isEmpty()) { - sink->setPlainText("readelf not found in PATH (install binutils)."); + sink->setPlainText(tr("readelf not found in PATH (install binutils).")); return; } // Asynchronous: six runs on a large library took up to a minute on the GUI // thread. Output of a load that a newer one replaced is dropped. - sink->setPlainText("Running readelf..."); + sink->setPlainText(tr("Running readelf...")); const int generation = loadGeneration_; auto* process = new QProcess(this); QPointer out(sink); @@ -89,16 +89,16 @@ void ElfInspector::runReadelf(QPlainTextEdit* sink, const QStringList& args) { connect(process, &QProcess::finished, this, [this, process, out, generation, timedOut]() { process->deleteLater(); if (!out || generation != loadGeneration_) return; - if (*timedOut) { out->setPlainText("readelf took longer than 60 seconds and was stopped."); return; } + if (*timedOut) { out->setPlainText(tr("readelf took longer than 60 seconds and was stopped.")); return; } QString text = QString::fromUtf8(process->readAllStandardOutput()); const QByteArray err = process->readAllStandardError(); - if (!err.isEmpty()) text += "\n--- stderr ---\n" + QString::fromUtf8(err); - out->setPlainText(text.isEmpty() ? QStringLiteral("(readelf produced no output)") : text); + if (!err.isEmpty()) text += QStringLiteral("\n") + tr("--- stderr ---") + QStringLiteral("\n") + QString::fromUtf8(err); + out->setPlainText(text.isEmpty() ? tr("(readelf produced no output)") : text); }); connect(process, &QProcess::errorOccurred, this, [this, process, out, generation](QProcess::ProcessError error) { if (error != QProcess::FailedToStart) return; // a crash or kill still emits finished process->deleteLater(); - if (out && generation == loadGeneration_) out->setPlainText("readelf could not be started."); + if (out && generation == loadGeneration_) out->setPlainText(tr("readelf could not be started.")); }); QTimer::singleShot(60'000, process, [process, timedOut]() { *timedOut = true; process->kill(); }); process->start(readelfPath, args); diff --git a/gui/filepatcher.cpp b/gui/filepatcher.cpp index 2d16c992..73635864 100644 --- a/gui/filepatcher.cpp +++ b/gui/filepatcher.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -19,7 +20,7 @@ namespace ce::gui { FilePatcher::FilePatcher(QWidget* parent) : QDialog(parent) { - setWindowTitle("File Patcher"); + setWindowTitle(tr("File Patcher")); resize(540, 420); auto* root = new QVBoxLayout(this); @@ -28,31 +29,31 @@ FilePatcher::FilePatcher(QWidget* parent) : QDialog(parent) { auto* pathRow = new QHBoxLayout; pathEdit_ = new QLineEdit; - pathEdit_->setPlaceholderText("/path/to/file"); - auto* browseBtn = new QPushButton("Browse…"); + pathEdit_->setPlaceholderText(tr("/path/to/file")); + auto* browseBtn = new QPushButton(tr("Browse…")); connect(browseBtn, &QPushButton::clicked, this, &FilePatcher::onChooseFile); pathRow->addWidget(pathEdit_); pathRow->addWidget(browseBtn); - form->addRow("File:", pathRow); + form->addRow(tr("File:"), pathRow); offsetEdit_ = new QLineEdit; - offsetEdit_->setPlaceholderText("0x0 (hex with 0x prefix, decimal otherwise)"); - form->addRow("Offset:", offsetEdit_); + offsetEdit_->setPlaceholderText(tr("0x0 (hex with 0x prefix, decimal otherwise)")); + form->addRow(tr("Offset:"), offsetEdit_); auto* readRow = new QHBoxLayout; readSizeSpin_ = new QSpinBox; readSizeSpin_->setRange(1, 1024); readSizeSpin_->setValue(16); - auto* readBtn = new QPushButton("Read current bytes"); + auto* readBtn = new QPushButton(tr("Read current bytes")); connect(readBtn, &QPushButton::clicked, this, &FilePatcher::onReadBytes); readRow->addWidget(readSizeSpin_); readRow->addWidget(readBtn); readRow->addStretch(); - form->addRow("Inspect:", readRow); + form->addRow(tr("Inspect:"), readRow); root->addLayout(form); - root->addWidget(new QLabel("Hex bytes to write (whitespace ignored):")); + root->addWidget(new QLabel(tr("Hex bytes to write (whitespace ignored):"))); hexEdit_ = new QPlainTextEdit; hexEdit_->setFont(monospaceFont()); hexEdit_->setPlaceholderText("DE AD BE EF\n90 90 90"); @@ -60,9 +61,9 @@ FilePatcher::FilePatcher(QWidget* parent) : QDialog(parent) { auto* btnRow = new QHBoxLayout; btnRow->addStretch(); - auto* applyBtn = new QPushButton("Apply Patch"); + auto* applyBtn = new QPushButton(tr("Apply Patch")); applyBtn->setProperty("primary", true); - auto* closeBtn = new QPushButton("Close"); + auto* closeBtn = new QPushButton(tr("Close")); connect(applyBtn, &QPushButton::clicked, this, &FilePatcher::onApply); connect(closeBtn, &QPushButton::clicked, this, &QDialog::accept); btnRow->addWidget(applyBtn); @@ -71,8 +72,8 @@ FilePatcher::FilePatcher(QWidget* parent) : QDialog(parent) { } void FilePatcher::onChooseFile() { - auto path = QFileDialog::getOpenFileName(this, "Choose file to patch", - pathEdit_->text(), "All Files (*)"); + auto path = QFileDialog::getOpenFileName(this, tr("Choose file to patch"), + pathEdit_->text(), tr("All Files (*)")); if (!path.isEmpty()) pathEdit_->setText(path); } @@ -80,7 +81,7 @@ static QByteArray parseHexBytes(const QString& text, QString* err) { QString clean = text; clean.replace(QRegularExpression(R"(\s+)"), ""); if (clean.size() % 2 != 0) { - if (err) *err = "Hex byte sequence has odd length."; + if (err) *err = QCoreApplication::translate("FilePatcher", "Hex byte sequence has odd length."); return {}; } QByteArray out; @@ -89,7 +90,7 @@ static QByteArray parseHexBytes(const QString& text, QString* err) { bool ok = false; uint8_t b = (uint8_t)QStringView(clean).mid(i, 2).toUInt(&ok, 16); if (!ok) { - if (err) *err = QString("Not a hex byte: '%1'").arg(clean.mid(i, 2)); + if (err) *err = QCoreApplication::translate("FilePatcher", "Not a hex byte: '%1'").arg(clean.mid(i, 2)); return {}; } out.append((char)b); @@ -109,21 +110,21 @@ static bool parseFileOffset(const QString& text, qint64& offset) { void FilePatcher::onReadBytes() { if (pathEdit_->text().isEmpty()) { - QMessageBox::warning(this, "Pick a file", "Choose a file first."); + QMessageBox::warning(this, tr("Pick a file"), tr("Choose a file first.")); return; } qint64 offset = 0; if (!parseFileOffset(offsetEdit_->text(), offset)) { - QMessageBox::warning(this, "Bad offset", "Enter a non-negative offset: 0x-prefixed hex or decimal."); + QMessageBox::warning(this, tr("Bad offset"), tr("Enter a non-negative offset: 0x-prefixed hex or decimal.")); return; } QFile f(pathEdit_->text()); if (!f.open(QIODevice::ReadOnly)) { - QMessageBox::warning(this, "Open failed", f.errorString()); + QMessageBox::warning(this, tr("Open failed"), f.errorString()); return; } if (!f.seek(offset)) { - QMessageBox::warning(this, "Seek failed", "Offset is past end of file."); + QMessageBox::warning(this, tr("Seek failed"), tr("Offset is past end of file.")); return; } QByteArray buf = f.read(readSizeSpin_->value()); @@ -137,51 +138,52 @@ void FilePatcher::onReadBytes() { void FilePatcher::onApply() { if (pathEdit_->text().isEmpty()) { - QMessageBox::warning(this, "Pick a file", "Choose a file first."); + QMessageBox::warning(this, tr("Pick a file"), tr("Choose a file first.")); return; } qint64 offset = 0; if (!parseFileOffset(offsetEdit_->text(), offset)) { - QMessageBox::warning(this, "Bad offset", "Enter a non-negative offset: 0x-prefixed hex or decimal."); + QMessageBox::warning(this, tr("Bad offset"), tr("Enter a non-negative offset: 0x-prefixed hex or decimal.")); return; } QString err; auto bytes = parseHexBytes(hexEdit_->toPlainText(), &err); if (bytes.isEmpty()) { - QMessageBox::warning(this, "Bad hex", err.isEmpty() ? "Provide at least one hex byte." : err); + QMessageBox::warning(this, tr("Bad hex"), err.isEmpty() ? tr("Provide at least one hex byte.") : err); return; } - auto answer = QMessageBox::question(this, "Confirm patch", - QString("Write %1 byte%2 to %3 at offset 0x%4?\nThis modifies the file in place.") - .arg(bytes.size()).arg(bytes.size() == 1 ? "" : "s") - .arg(pathEdit_->text()).arg(offset, 0, 16)); + const QString question = bytes.size() == 1 + ? tr("Write %1 byte to %2 at offset 0x%3?\nThis modifies the file in place.") + : tr("Write %1 bytes to %2 at offset 0x%3?\nThis modifies the file in place."); + auto answer = QMessageBox::question(this, tr("Confirm patch"), + question.arg(bytes.size()).arg(pathEdit_->text()).arg(offset, 0, 16)); if (answer != QMessageBox::Yes) return; QFile f(pathEdit_->text()); if (!f.open(QIODevice::ReadWrite)) { - QMessageBox::warning(this, "Open failed", f.errorString()); + QMessageBox::warning(this, tr("Open failed"), f.errorString()); return; } // ReadWrite seek-past-EOF succeeds and would grow the file (sparse hole) // rather than patch in place; confirm before extending. if (offset + (qint64)bytes.size() > f.size()) { - auto extend = QMessageBox::question(this, "Extend file?", - QString("The patch ends at offset 0x%1 but the file is only 0x%2 bytes.\n" - "Applying it will grow the file (zero-filling the gap). Continue?") + auto extend = QMessageBox::question(this, tr("Extend file?"), + tr("The patch ends at offset 0x%1 but the file is only 0x%2 bytes.\n" + "Applying it will grow the file (zero-filling the gap). Continue?") .arg(offset + (qint64)bytes.size(), 0, 16).arg(f.size(), 0, 16)); if (extend != QMessageBox::Yes) return; } if (!f.seek(offset)) { - QMessageBox::warning(this, "Seek failed", "Offset is past end of file."); + QMessageBox::warning(this, tr("Seek failed"), tr("Offset is past end of file.")); return; } qint64 written = f.write(bytes); if (written != bytes.size()) { - QMessageBox::warning(this, "Write failed", f.errorString()); + QMessageBox::warning(this, tr("Write failed"), f.errorString()); return; } - QMessageBox::information(this, "Patched", - QString("Wrote %1 bytes at 0x%2.").arg(written).arg(offset, 0, 16)); + QMessageBox::information(this, tr("Patched"), + tr("Wrote %1 bytes at 0x%2.").arg(written).arg(offset, 0, 16)); } } // namespace ce::gui diff --git a/gui/findstaticswindow.cpp b/gui/findstaticswindow.cpp index 8ea13b9c..0c543d47 100644 --- a/gui/findstaticswindow.cpp +++ b/gui/findstaticswindow.cpp @@ -14,14 +14,14 @@ namespace ce::gui { FindStaticsWindow::FindStaticsWindow(ProcessHandle* proc, QWidget* parent) : QMainWindow(parent), proc_(proc) { - setWindowTitle("Find Statics"); + setWindowTitle(tr("Find Statics")); resize(720, 540); auto* central = new QWidget; auto* layout = new QVBoxLayout(central); auto* row = new QHBoxLayout; - row->addWidget(new QLabel("Module:")); + row->addWidget(new QLabel(tr("Module:"))); moduleCombo_ = new QComboBox; if (proc_) { modules_ = proc_->modules(); @@ -31,17 +31,17 @@ FindStaticsWindow::FindStaticsWindow(ProcessHandle* proc, QWidget* parent) } } row->addWidget(moduleCombo_, 1); - scanBtn_ = new QPushButton("Scan"); + scanBtn_ = new QPushButton(tr("Scan")); connect(scanBtn_, &QPushButton::clicked, this, &FindStaticsWindow::onScan); row->addWidget(scanBtn_); layout->addLayout(row); - statusLabel_ = new QLabel("Pick a module and click Scan."); + statusLabel_ = new QLabel(tr("Pick a module and click Scan.")); layout->addWidget(statusLabel_); table_ = new QTableWidget; table_->setColumnCount(3); - table_->setHorizontalHeaderLabels({"Static address", "Symbol", "References"}); + table_->setHorizontalHeaderLabels({tr("Static address"), tr("Symbol"), tr("References")}); // Fit the address and symbol columns to content; References (last) takes the rest. auto* hh = table_->horizontalHeader(); hh->setStretchLastSection(true); @@ -49,7 +49,7 @@ FindStaticsWindow::FindStaticsWindow(ProcessHandle* proc, QWidget* parent) hh->setSectionResizeMode(1, QHeaderView::ResizeToContents); table_->setSelectionBehavior(QAbstractItemView::SelectRows); styleDataTable(table_); - setEmptyHint(table_, "Static addresses in the module's data, and the code that references them, appear here."); + setEmptyHint(table_, tr("Static addresses in the module's data, and the code that references them, appear here.")); table_->setProperty("monospace", true); // font from the theme sheet layout->addWidget(table_, 1); @@ -64,7 +64,7 @@ void FindStaticsWindow::onScan() { const auto& mods = modules_; if (idx < 0 || idx >= (int)mods.size()) return; - statusLabel_->setText("Scanning…"); + statusLabel_->setText(tr("Scanning…")); scanBtn_->setEnabled(false); QApplication::processEvents(); @@ -95,7 +95,7 @@ void FindStaticsWindow::onScan() { table_->setItem((int)i, 1, new QTableWidgetItem(sym)); table_->setItem((int)i, 2, new QTableWidgetItem(QString::number(s.references))); } - statusLabel_->setText(QString("Done: %1 static addresses found.").arg(statics.size())); + statusLabel_->setText(tr("Done: %1 static addresses found.").arg(statics.size())); scanBtn_->setEnabled(true); } diff --git a/gui/formdesigner.cpp b/gui/formdesigner.cpp index d9e08e95..6278f339 100644 --- a/gui/formdesigner.cpp +++ b/gui/formdesigner.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -32,8 +33,22 @@ namespace ce::gui { +// A palette entry's widget type is stored, serialized and switched on in English +// (a saved layout must load in any language); only the label shown to the user is +// translated. The item list shows the same label. +static QString formTypeLabel(const QString& type) { + if (type == QStringLiteral("Button")) return QCoreApplication::translate("FormDesigner", "Button"); + if (type == QStringLiteral("Label")) return QCoreApplication::translate("FormDesigner", "Label"); + if (type == QStringLiteral("Edit")) return QCoreApplication::translate("FormDesigner", "Edit"); + if (type == QStringLiteral("CheckBox")) return QCoreApplication::translate("FormDesigner", "CheckBox"); + if (type == QStringLiteral("Memo")) return QCoreApplication::translate("FormDesigner", "Memo"); + if (type == QStringLiteral("GroupBox")) return QCoreApplication::translate("FormDesigner", "GroupBox"); + if (type == QStringLiteral("Panel")) return QCoreApplication::translate("FormDesigner", "Panel"); + return type; +} + FormDesigner::FormDesigner(QWidget* parent) : QMainWindow(parent) { - setWindowTitle("Form Designer"); + setWindowTitle(tr("Form Designer")); resize(1100, 720); auto* central = new QWidget; @@ -41,19 +56,30 @@ FormDesigner::FormDesigner(QWidget* parent) : QMainWindow(parent) { // ── Palette pane (left) ── auto* paletteCol = new QVBoxLayout; - paletteCol->addWidget(new QLabel("Palette")); + paletteCol->addWidget(new QLabel(tr("Palette"))); paletteList_ = new QListWidget; - paletteList_->addItems({"Button", "Label", "Edit", "CheckBox", "Memo", "GroupBox", "Panel"}); + // The type travels in the item data so the label can be translated. + auto addPaletteItem = [&](const QString& label, const char* type) { + auto* item = new QListWidgetItem(label, paletteList_); + item->setData(Qt::UserRole, QString::fromLatin1(type)); + }; + addPaletteItem(tr("Button"), "Button"); + addPaletteItem(tr("Label"), "Label"); + addPaletteItem(tr("Edit"), "Edit"); + addPaletteItem(tr("CheckBox"), "CheckBox"); + addPaletteItem(tr("Memo"), "Memo"); + addPaletteItem(tr("GroupBox"), "GroupBox"); + addPaletteItem(tr("Panel"), "Panel"); paletteList_->setCurrentRow(0); paletteCol->addWidget(paletteList_); - auto* addBtn = new QPushButton("Add to form"); + auto* addBtn = new QPushButton(tr("Add to form")); connect(addBtn, &QPushButton::clicked, this, &FormDesigner::onAddWidget); paletteCol->addWidget(addBtn); - paletteCol->addWidget(new QLabel("Items")); + paletteCol->addWidget(new QLabel(tr("Items"))); itemList_ = new QListWidget; paletteCol->addWidget(itemList_, 1); - auto* delBtn = new QPushButton("Delete"); + auto* delBtn = new QPushButton(tr("Delete")); connect(delBtn, &QPushButton::clicked, this, &FormDesigner::onDelete); paletteCol->addWidget(delBtn); connect(itemList_, &QListWidget::currentRowChanged, this, &FormDesigner::onItemSelected); @@ -81,7 +107,7 @@ FormDesigner::FormDesigner(QWidget* parent) : QMainWindow(parent) { canvas_->setFixedSize(formWidth_, formHeight_); auto* canvasWrap = new QWidget; auto* canvasLayout = new QVBoxLayout(canvasWrap); - auto* canvasHint = new QLabel("Canvas: click a widget on the canvas or in the Items list to select it"); + auto* canvasHint = new QLabel(tr("Canvas: click a widget on the canvas or in the Items list to select it")); canvasHint->setWordWrap(true); canvasLayout->addWidget(canvasHint); auto* canvasScroll = new QScrollArea; @@ -95,8 +121,8 @@ FormDesigner::FormDesigner(QWidget* parent) : QMainWindow(parent) { auto* luaWrap = new QWidget; auto* luaLayout = new QVBoxLayout(luaWrap); auto* luaHeader = new QHBoxLayout; - luaHeader->addWidget(new QLabel("Generated Lua")); - auto* regenBtn = new QPushButton("Refresh"); + luaHeader->addWidget(new QLabel(tr("Generated Lua"))); + auto* regenBtn = new QPushButton(tr("Refresh")); connect(regenBtn, &QPushButton::clicked, this, &FormDesigner::onGenerateLua); luaHeader->addStretch(); luaHeader->addWidget(regenBtn); @@ -112,7 +138,7 @@ FormDesigner::FormDesigner(QWidget* parent) : QMainWindow(parent) { // ── Properties pane (right) ── auto* propsCol = new QVBoxLayout; - propsCol->addWidget(new QLabel("Form")); + propsCol->addWidget(new QLabel(tr("Form"))); { auto* form = new QFormLayout; auto* fw = new QSpinBox; fw->setObjectName("formWidth"); fw->setRange(1, QWIDGETSIZE_MAX); fw->setValue(formWidth_); @@ -123,12 +149,12 @@ FormDesigner::FormDesigner(QWidget* parent) : QMainWindow(parent) { connect(fh, qOverload(&QSpinBox::valueChanged), this, [this, fh](int v) { formHeight_ = v; canvas_->setFixedSize(formWidth_, formHeight_); redrawCanvas(); onGenerateLua(); }); - form->addRow("Width:", fw); - form->addRow("Height:", fh); + form->addRow(tr("Width:"), fw); + form->addRow(tr("Height:"), fh); propsCol->addLayout(form); } - propsCol->addWidget(new QLabel("Selected widget")); + propsCol->addWidget(new QLabel(tr("Selected widget"))); auto* propForm = new QFormLayout; nameEdit_ = new QLineEdit; captionEdit_ = new QLineEdit; @@ -139,12 +165,12 @@ FormDesigner::FormDesigner(QWidget* parent) : QMainWindow(parent) { ySpin_ = new QSpinBox; ySpin_->setRange(0, QWIDGETSIZE_MAX); wSpin_ = new QSpinBox; wSpin_->setRange(1, QWIDGETSIZE_MAX); wSpin_->setValue(100); hSpin_ = new QSpinBox; hSpin_->setRange(1, QWIDGETSIZE_MAX); hSpin_->setValue(24); - propForm->addRow("Name:", nameEdit_); - propForm->addRow("Caption:", captionEdit_); - propForm->addRow("X:", xSpin_); - propForm->addRow("Y:", ySpin_); - propForm->addRow("Width:", wSpin_); - propForm->addRow("Height:", hSpin_); + propForm->addRow(tr("Name:"), nameEdit_); + propForm->addRow(tr("Caption:"), captionEdit_); + propForm->addRow(tr("X:"), xSpin_); + propForm->addRow(tr("Y:"), ySpin_); + propForm->addRow(tr("Width:"), wSpin_); + propForm->addRow(tr("Height:"), hSpin_); propsCol->addLayout(propForm); for (auto* w : std::vector{nameEdit_, captionEdit_, xSpin_, ySpin_, wSpin_, hSpin_}) { @@ -156,9 +182,9 @@ FormDesigner::FormDesigner(QWidget* parent) : QMainWindow(parent) { } propsCol->addStretch(); - auto* saveBtn = new QPushButton("Save layout (.json)…"); - auto* loadBtn = new QPushButton("Load layout (.json)…"); - auto* exportBtn = new QPushButton("Copy Lua to clipboard"); + auto* saveBtn = new QPushButton(tr("Save layout (.json)…")); + auto* loadBtn = new QPushButton(tr("Load layout (.json)…")); + auto* exportBtn = new QPushButton(tr("Copy Lua to clipboard")); connect(saveBtn, &QPushButton::clicked, this, &FormDesigner::onSave); connect(loadBtn, &QPushButton::clicked, this, &FormDesigner::onLoad); connect(exportBtn, &QPushButton::clicked, this, [this]() { @@ -325,7 +351,7 @@ void FormDesigner::onAddWidget() { auto* sel = paletteList_->currentItem(); if (!sel) return; FormItem it; - it.type = sel->text(); + it.type = sel->data(Qt::UserRole).toString(); // Unique even after deletes (button2 must not appear twice). for (size_t n = items_.size() + 1;; ++n) { it.name = QString("%1%2").arg(it.type.toLower()).arg(n); @@ -336,7 +362,7 @@ void FormDesigner::onAddWidget() { if (it.type == "GroupBox") { it.w = 200; it.h = 100; } if (it.type == "Panel") { it.w = 200; it.h = 100; } items_.push_back(it); - itemList_->addItem(QString("%1: %2").arg(it.type).arg(it.name)); + itemList_->addItem(QString("%1: %2").arg(formTypeLabel(it.type)).arg(it.name)); itemList_->setCurrentRow(itemList_->count() - 1); redrawCanvas(); onGenerateLua(); @@ -384,13 +410,13 @@ void FormDesigner::onPropertyChanged() { it.caption = captionEdit_->text(); it.x = xSpin_->value(); it.y = ySpin_->value(); it.w = wSpin_->value(); it.h = hSpin_->value(); - itemList_->item(selected_)->setText(QString("%1: %2").arg(it.type).arg(it.name)); + itemList_->item(selected_)->setText(QString("%1: %2").arg(formTypeLabel(it.type)).arg(it.name)); redrawCanvas(); onGenerateLua(); } void FormDesigner::onSave() { - auto path = QFileDialog::getSaveFileName(this, "Save form layout", "form.json", "JSON (*.json)"); + auto path = QFileDialog::getSaveFileName(this, tr("Save form layout"), "form.json", tr("JSON (*.json)")); if (path.isEmpty()) return; QJsonObject root; root["formWidth"] = formWidth_; @@ -408,23 +434,23 @@ void FormDesigner::onSave() { root["items"] = arr; QFile f(path); if (!f.open(QIODevice::WriteOnly)) { - QMessageBox::warning(this, "Save failed", f.errorString()); + QMessageBox::warning(this, tr("Save failed"), f.errorString()); return; } f.write(QJsonDocument(root).toJson()); } void FormDesigner::onLoad() { - auto path = QFileDialog::getOpenFileName(this, "Load form layout", "", "JSON (*.json)"); + auto path = QFileDialog::getOpenFileName(this, tr("Load form layout"), "", tr("JSON (*.json)")); if (path.isEmpty()) return; QFile f(path); if (!f.open(QIODevice::ReadOnly)) { - QMessageBox::warning(this, "Load failed", f.errorString()); + QMessageBox::warning(this, tr("Load failed"), f.errorString()); return; } auto doc = QJsonDocument::fromJson(f.readAll()); if (!doc.isObject()) { - QMessageBox::warning(this, "Load failed", "Not a valid form layout file."); + QMessageBox::warning(this, tr("Load failed"), tr("Not a valid form layout file.")); return; } auto root = doc.object(); @@ -451,7 +477,7 @@ void FormDesigner::onLoad() { it.w = std::clamp(o.value("w").toInt(100), 1, QWIDGETSIZE_MAX); it.h = std::clamp(o.value("h").toInt(24), 1, QWIDGETSIZE_MAX); items_.push_back(it); - itemList_->addItem(QString("%1: %2").arg(it.type).arg(it.name)); + itemList_->addItem(QString("%1: %2").arg(formTypeLabel(it.type)).arg(it.name)); } redrawCanvas(); onGenerateLua(); diff --git a/gui/gdbconnectiondialog.cpp b/gui/gdbconnectiondialog.cpp index 64da1c91..ec3413b3 100644 --- a/gui/gdbconnectiondialog.cpp +++ b/gui/gdbconnectiondialog.cpp @@ -1,5 +1,6 @@ #include "gui/gdbconnectiondialog.hpp" #include +#include #include #include #include @@ -13,7 +14,7 @@ namespace ce::gui { GdbConnectionDialog::GdbConnectionDialog(QWidget* parent):QDialog(parent) { - setWindowTitle("Connect to GDB / QEMU"); + setWindowTitle(QCoreApplication::translate("GdbConnectionDialog", "Connect to GDB / QEMU")); setObjectName("gdbConnectionDialog"); resize(530,330); auto* form=new QFormLayout(this); @@ -22,22 +23,22 @@ GdbConnectionDialog::GdbConnectionDialog(QWidget* parent):QDialog(parent) { host_->setObjectName("gdbHost"); port_=new QSpinBox;port_->setRange(1,65535); port_->setValue(settings.value("network/gdbPort",1234).toInt());port_->setObjectName("gdbPort"); - form->addRow("Host:",host_);form->addRow("Port:",port_); + form->addRow(QCoreApplication::translate("GdbConnectionDialog", "Host:"),host_);form->addRow(QCoreApplication::translate("GdbConnectionDialog", "Port:"),port_); order_=new QComboBox;order_->setObjectName("gdbByteOrder"); - order_->addItem("From target (may be unknown)",static_cast(ByteOrder::Unknown)); - order_->addItem("Little-endian",static_cast(ByteOrder::Little)); - order_->addItem("Big-endian",static_cast(ByteOrder::Big)); + order_->addItem(QCoreApplication::translate("GdbConnectionDialog", "From target (may be unknown)"),static_cast(ByteOrder::Unknown)); + order_->addItem(QCoreApplication::translate("GdbConnectionDialog", "Little-endian"),static_cast(ByteOrder::Little)); + order_->addItem(QCoreApplication::translate("GdbConnectionDialog", "Big-endian"),static_cast(ByteOrder::Big)); width_=new QComboBox;width_->setObjectName("gdbPointerWidth"); - width_->addItem("From target",0);width_->addItem("4 bytes (32-bit pointers)",4);width_->addItem("8 bytes (64-bit pointers)",8); - form->addRow("Data byte order:",order_);form->addRow("Program pointer width:",width_); - start_=new QLineEdit;start_->setObjectName("gdbRangeStart");start_->setPlaceholderText("e.g. 0x40000000"); - size_=new QLineEdit;size_->setObjectName("gdbRangeSize");size_->setPlaceholderText("e.g. 0x1000"); - form->addRow("Scan range start:",start_);form->addRow("Scan range size:",size_); - auto* explanation=new QLabel("The target must be stopped. Leave both range fields blank to use the stub's memory map. Addresses and sizes accept decimal or 0x-prefixed hex."); + width_->addItem(QCoreApplication::translate("GdbConnectionDialog", "From target"),0);width_->addItem(QCoreApplication::translate("GdbConnectionDialog", "4 bytes (32-bit pointers)"),4);width_->addItem(QCoreApplication::translate("GdbConnectionDialog", "8 bytes (64-bit pointers)"),8); + form->addRow(QCoreApplication::translate("GdbConnectionDialog", "Data byte order:"),order_);form->addRow(QCoreApplication::translate("GdbConnectionDialog", "Program pointer width:"),width_); + start_=new QLineEdit;start_->setObjectName("gdbRangeStart");start_->setPlaceholderText(QCoreApplication::translate("GdbConnectionDialog", "e.g. 0x40000000")); + size_=new QLineEdit;size_->setObjectName("gdbRangeSize");size_->setPlaceholderText(QCoreApplication::translate("GdbConnectionDialog", "e.g. 0x1000")); + form->addRow(QCoreApplication::translate("GdbConnectionDialog", "Scan range start:"),start_);form->addRow(QCoreApplication::translate("GdbConnectionDialog", "Scan range size:"),size_); + auto* explanation=new QLabel(QCoreApplication::translate("GdbConnectionDialog", "The target must be stopped. Leave both range fields blank to use the stub's memory map. Addresses and sizes accept decimal or 0x-prefixed hex.")); explanation->setWordWrap(true);form->addRow(explanation); status_=new QLabel;status_->setWordWrap(true);status_->setObjectName("gdbConnectionStatus");form->addRow(status_); auto* buttons=new QDialogButtonBox; - connect_=buttons->addButton("Connect",QDialogButtonBox::AcceptRole);connect_->setObjectName("gdbConnect");connect_->setProperty("primary",true);connect_->setDefault(true); + connect_=buttons->addButton(QCoreApplication::translate("GdbConnectionDialog", "Connect"),QDialogButtonBox::AcceptRole);connect_->setObjectName("gdbConnect");connect_->setProperty("primary",true);connect_->setDefault(true); cancel_=buttons->addButton(QDialogButtonBox::Cancel);form->addRow(buttons); connect(connect_,&QPushButton::clicked,this,&GdbConnectionDialog::connectTarget); connect(cancel_,&QPushButton::clicked,this,&GdbConnectionDialog::reject); @@ -56,13 +57,13 @@ void GdbConnectionDialog::connectTarget() { options.byteOrder=static_cast(order_->currentData().toInt()); options.pointerWidth=static_cast(width_->currentData().toInt()); const auto host=host_->text().trimmed(); - if (host.isEmpty()) {status_->setText("Enter a host.");return;} + if (host.isEmpty()) {status_->setText(QCoreApplication::translate("GdbConnectionDialog", "Enter a host."));return;} if (!start_->text().trimmed().isEmpty() || !size_->text().trimmed().isEmpty()) { bool startOk=false,sizeOk=false; const auto start=start_->text().trimmed().toULongLong(&startOk,0); const auto size=size_->text().trimmed().toULongLong(&sizeOk,0); if (!startOk || !sizeOk || !size || start>UINTPTR_MAX || size>UINTPTR_MAX-start) { - status_->setText("Enter a valid numeric range start and positive size.");return; + status_->setText(QCoreApplication::translate("GdbConnectionDialog", "Enter a valid numeric range start and positive size."));return; } options.regions.push_back({static_cast(start),static_cast(size),MemProt::ReadWrite,MemType::Private,MemState::Committed,{}}); } @@ -71,7 +72,7 @@ void GdbConnectionDialog::connectTarget() { try { pending_=std::async(std::launch::async,[host=host.toStdString(),port,options]{return GdbProcessHandle::connect(host,port,options);}); } catch (const std::exception& error) {status_->setText(QString::fromUtf8(error.what()));return;} - setBusy(true);status_->setText("Connecting and reading the target description...");poll_->start(); + setBusy(true);status_->setText(QCoreApplication::translate("GdbConnectionDialog", "Connecting and reading the target description..."));poll_->start(); } void GdbConnectionDialog::poll() { if (!pending_.valid() || pending_.wait_for(std::chrono::milliseconds(0))!=std::future_status::ready) return; @@ -90,7 +91,7 @@ void GdbConnectionDialog::poll() { } void GdbConnectionDialog::reject() { if (pending_.valid()) { - canceled_=true;cancellation_.request_stop();status_->setText("Canceling the connection..."); + canceled_=true;cancellation_.request_stop();status_->setText(QCoreApplication::translate("GdbConnectionDialog", "Canceling the connection...")); return; } QDialog::reject(); diff --git a/gui/globalhotkeys.cpp b/gui/globalhotkeys.cpp index c46d540e..eee8173e 100644 --- a/gui/globalhotkeys.cpp +++ b/gui/globalhotkeys.cpp @@ -358,7 +358,7 @@ void GlobalHotkeyManager::rebindWayland() { s.id = waylandShortcutId(it.key()); const QString description = waylandNames_.value(it.key()).description; s.meta[QStringLiteral("description")] = description.isEmpty() - ? QStringLiteral("Cheat Engine hotkey %1").arg(s.id) : QStringLiteral("Cheat Engine: %1").arg(description); + ? tr("Cheat Engine hotkey %1").arg(s.id) : tr("Cheat Engine: %1").arg(description); s.meta[QStringLiteral("preferred_trigger")] = it.value(); shortcuts.push_back(s); } diff --git a/gui/graphicalmemoryview.cpp b/gui/graphicalmemoryview.cpp index acda3411..cd3cfce2 100644 --- a/gui/graphicalmemoryview.cpp +++ b/gui/graphicalmemoryview.cpp @@ -1,4 +1,5 @@ #include "gui/graphicalmemoryview.hpp" +#include #include #include @@ -27,7 +28,8 @@ void MemPixelView::paintEvent(QPaintEvent* event) { p.fillRect(rect(), palette().color(QPalette::Window)); p.setPen(palette().color(QPalette::Text)); p.drawText(rect().adjusted(16, 16, -16, -16), Qt::AlignCenter | Qt::TextWordWrap, - "Enter an address and press Fetch to visualize memory."); + QCoreApplication::translate("ce::gui::MemPixelView", + "Enter an address and press Fetch to visualize memory.")); return; } p.fillRect(event->rect(), Qt::black); @@ -54,7 +56,7 @@ void MemPixelView::paintEvent(QPaintEvent* event) { GraphicalMemoryView::GraphicalMemoryView(ce::ProcessHandle* proc, QWidget* parent) : QMainWindow(parent), proc_(proc) { - setWindowTitle("Graphical Memory View"); + setWindowTitle(tr("Graphical Memory View")); resize(680, 620); auto* central = new QWidget; @@ -62,30 +64,30 @@ GraphicalMemoryView::GraphicalMemoryView(ce::ProcessHandle* proc, QWidget* paren auto* v = new QVBoxLayout(central); auto* controls = new QHBoxLayout; - controls->addWidget(new QLabel("Address:")); + controls->addWidget(new QLabel(tr("Address:"))); addrEdit_ = new QLineEdit; - addrEdit_->setPlaceholderText("address (hex)"); + addrEdit_->setPlaceholderText(tr("address (hex)")); addrEdit_->setMinimumWidth(220); controls->addWidget(addrEdit_, 1); - auto* fetchBtn = new QPushButton("Fetch"); + auto* fetchBtn = new QPushButton(tr("Fetch")); fetchBtn->setObjectName("primaryButton"); - fetchBtn->setToolTip("Read and visualize memory at this address"); + fetchBtn->setToolTip(tr("Read and visualize memory at this address")); controls->addWidget(fetchBtn); v->addLayout(controls); auto* dimensions = new QHBoxLayout; - dimensions->addWidget(new QLabel("Pixels per line:")); + dimensions->addWidget(new QLabel(tr("Pixels per line:"))); perLineSpin_ = new QSpinBox; perLineSpin_->setRange(1, 4096); perLineSpin_->setValue(256); dimensions->addWidget(perLineSpin_); - dimensions->addWidget(new QLabel("Rows:")); + dimensions->addWidget(new QLabel(tr("Rows:"))); rowsSpin_ = new QSpinBox; rowsSpin_->setRange(1, 4096); rowsSpin_->setValue(256); dimensions->addWidget(rowsSpin_); dimensions->addStretch(); v->addLayout(dimensions); - statusLabel_ = new QLabel("No memory loaded."); + statusLabel_ = new QLabel(tr("No memory loaded.")); statusLabel_->setProperty("secondary", true); statusLabel_->setWordWrap(true); v->addWidget(statusLabel_); @@ -120,7 +122,7 @@ void GraphicalMemoryView::fetch() { bool valid = false; uintptr_t addr = addrEdit_->text().trimmed().toULongLong(&valid, 16); if (!valid) { - statusLabel_->setText("Enter a valid hexadecimal address."); + statusLabel_->setText(tr("Enter a valid hexadecimal address.")); addrEdit_->setFocus(); addrEdit_->selectAll(); return; } @@ -132,12 +134,12 @@ void GraphicalMemoryView::fetch() { // One byte per pixel, darker = smaller value. Say when the readable memory // ended early, so a short image isn't mistaken for a rendering fault. if (!got) - statusLabel_->setText(QString("Could not read memory at 0x%1.").arg(addr, 0, 16)); + statusLabel_->setText(tr("Could not read memory at 0x%1.").arg(addr, 0, 16)); else if (got < count) - statusLabel_->setText(QString("Showing %1 bytes from 0x%2; the memory after 0x%3 is not readable.") + statusLabel_->setText(tr("Showing %1 bytes from 0x%2; the memory after 0x%3 is not readable.") .arg(got).arg(addr, 0, 16).arg(addr + got, 0, 16)); else - statusLabel_->setText(QString("Showing %1 bytes from 0x%2, one byte per pixel (black = 00, white = FF).") + statusLabel_->setText(tr("Showing %1 bytes from 0x%2, one byte per pixel (black = 00, white = FF).") .arg(got).arg(addr, 0, 16)); buf.resize(got); view_->setPerLine(perLine); diff --git a/gui/guest_scan_dialog.cpp b/gui/guest_scan_dialog.cpp index d0564d5e..1606e755 100644 --- a/gui/guest_scan_dialog.cpp +++ b/gui/guest_scan_dialog.cpp @@ -84,7 +84,7 @@ std::optional parseValueT(const QString& text) { GuestScanDialog::GuestScanDialog(ce::ProcessHandle* proc, QWidget* parent) : QDialog(parent), proc_(proc) { - setWindowTitle("Emulator guest scan"); + setWindowTitle(tr("Emulator guest scan")); resize(600, 520); if (proc_) if (auto localPid = ce::localTargetPid(proc_)) profile_ = ce::probeTarget(*localPid); // local /proc only @@ -92,11 +92,11 @@ GuestScanDialog::GuestScanDialog(ce::ProcessHandle* proc, QWidget* parent) QString emu = QString::fromStdString(profile_.emulator); auto* header = new QLabel(profile_.guestCandidates.empty() - ? QString("No emulator guest RAM detected. Attach to a recognized " - "emulator (Dolphin, PCSX2, RPCS3, DuckStation, …) first.") - : QString("%1 guest memory. Scan by value, or Unknown Scan then narrow by " - "how the value changed. Enable big-endian for PS3 / Wii / GameCube.") - .arg(emu.isEmpty() ? "Emulator" : emu)); + ? tr("No emulator guest RAM detected. Attach to a recognized " + "emulator (Dolphin, PCSX2, RPCS3, DuckStation, …) first.") + : tr("%1 guest memory. Scan by value, or Unknown Scan then narrow by " + "how the value changed. Enable big-endian for PS3 / Wii / GameCube.") + .arg(emu.isEmpty() ? tr("Emulator") : emu)); header->setWordWrap(true); layout->addWidget(header); @@ -104,50 +104,50 @@ GuestScanDialog::GuestScanDialog(ce::ProcessHandle* proc, QWidget* parent) regionCombo_ = new QComboBox; for (const auto& g : profile_.guestCandidates) regionCombo_->addItem(QString("0x%1 (%2 MB)").arg(g.base, 0, 16).arg(g.size >> 20)); - form->addRow("Guest RAM region:", regionCombo_); + form->addRow(tr("Guest RAM region:"), regionCombo_); typeCombo_ = new QComboBox; - typeCombo_->addItems({"Byte", "2 Bytes", "4 Bytes", "8 Bytes", "Float", "Double"}); + typeCombo_->addItems({tr("Byte"), tr("2 Bytes"), tr("4 Bytes"), tr("8 Bytes"), tr("Float"), tr("Double")}); typeCombo_->setCurrentIndex(2); - form->addRow("Value type:", typeCombo_); + form->addRow(tr("Value type:"), typeCombo_); valueEdit_ = new QLineEdit; - valueEdit_->setPlaceholderText("value to find (decimal or 0x…)"); - form->addRow("Value:", valueEdit_); + valueEdit_->setPlaceholderText(tr("value to find (decimal or 0x…)")); + form->addRow(tr("Value:"), valueEdit_); - bigEndianCheck_ = new QCheckBox("Big-endian (PS3 / Wii / GameCube)"); + bigEndianCheck_ = new QCheckBox(tr("Big-endian (PS3 / Wii / GameCube)")); form->addRow("", bigEndianCheck_); layout->addLayout(form); auto* btnRow = new QHBoxLayout; - firstBtn_ = new QPushButton("First Scan"); - unknownBtn_ = new QPushButton("Unknown Scan"); - nextBtn_ = new QPushButton("Next Scan"); - newBtn_ = new QPushButton("New Scan"); + firstBtn_ = new QPushButton(tr("First Scan")); + unknownBtn_ = new QPushButton(tr("Unknown Scan")); + nextBtn_ = new QPushButton(tr("Next Scan")); + newBtn_ = new QPushButton(tr("New Scan")); for (auto* b : {firstBtn_, unknownBtn_, nextBtn_, newBtn_}) btnRow->addWidget(b); layout->addLayout(btnRow); auto* cmpRow = new QHBoxLayout; - changedBtn_ = new QPushButton("Changed"); - unchangedBtn_ = new QPushButton("Unchanged"); - increasedBtn_ = new QPushButton("Increased"); - decreasedBtn_ = new QPushButton("Decreased"); + changedBtn_ = new QPushButton(tr("Changed")); + unchangedBtn_ = new QPushButton(tr("Unchanged")); + increasedBtn_ = new QPushButton(tr("Increased")); + decreasedBtn_ = new QPushButton(tr("Decreased")); for (auto* b : {changedBtn_, unchangedBtn_, increasedBtn_, decreasedBtn_}) cmpRow->addWidget(b); cmpRow->addStretch(); - auto* addBtn = new QPushButton("Add to Address List"); + auto* addBtn = new QPushButton(tr("Add to Address List")); cmpRow->addWidget(addBtn); layout->addLayout(cmpRow); - statusLabel_ = new QLabel("Ready."); + statusLabel_ = new QLabel(tr("Ready.")); layout->addWidget(statusLabel_); resultsTable_ = new QTableWidget(0, 3); - resultsTable_->setHorizontalHeaderLabels({"Guest address", "Host address", "Value"}); + resultsTable_->setHorizontalHeaderLabels({tr("Guest address"), tr("Host address"), tr("Value")}); resultsTable_->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); resultsTable_->horizontalHeader()->setStretchLastSection(true); resultsTable_->setSelectionBehavior(QAbstractItemView::SelectRows); styleDataTable(resultsTable_); - setEmptyHint(resultsTable_, "Guest RAM scan results appear here."); + setEmptyHint(resultsTable_, tr("Guest RAM scan results appear here.")); layout->addWidget(resultsTable_); connect(firstBtn_, &QPushButton::clicked, this, &GuestScanDialog::onFirstScan); @@ -200,7 +200,7 @@ void GuestScanDialog::onFirstScan() { if (profile_.guestCandidates.empty()) return; const QString valStr = valueEdit_->text(); if (!dispatchType(selectedType(), [&]() { return parseValueT(valStr).has_value(); })) { - statusLabel_->setText("Enter a value that fits the selected type."); + statusLabel_->setText(tr("Enter a value that fits the selected type.")); return; } beginScan(); @@ -232,7 +232,7 @@ void GuestScanDialog::onNextScan() { if (!haveScan_ || unknownMode_) return; const QString valStr = valueEdit_->text(); if (!dispatchType(scanType_, [&]() { return parseValueT(valStr).has_value(); })) { - statusLabel_->setText("Enter a value that fits the selected type."); // keep the results + statusLabel_->setText(tr("Enter a value that fits the selected type.")); // keep the results return; } ce::GuestView gv{ proc_, regionBase_, regionSize_, scanBigEndian_ }; @@ -282,7 +282,7 @@ void GuestScanDialog::onNewScan() { haveScan_ = false; unknownMode_ = false; resultsTable_->setRowCount(0); - statusLabel_->setText("Ready."); + statusLabel_->setText(tr("Ready.")); refreshResults(); } @@ -301,19 +301,20 @@ void GuestScanDialog::refreshResults() { resultsTable_->setItem((int)i, 2, new QTableWidgetItem(fmtBits(candidates_[i].second))); } }); - QString status = QString("%1 result(s)%2").arg(candidates_.size()) - .arg(candidates_.size() > shown ? QString(" (showing first %1)").arg(shown) : QString()); + QString status = candidates_.size() > shown + ? tr("%1 result(s) (showing first %2)").arg(candidates_.size()).arg(shown) + : tr("%1 result(s)").arg(candidates_.size()); if (resultsTruncated_) - status += QString(". The scan stopped at %1 results; try a less common value.") + status += tr(". The scan stopped at %1 results; try a less common value.") .arg(ce::kGuestMaxHits); statusLabel_->setText(status); } else if (unknownMode_) { resultsTable_->setRowCount(0); - QString status = QString("Snapshot taken (%1 MB). Change the value in-game, then " + QString status = tr("Snapshot taken (%1 MB). Change the value in-game, then " "narrow with Changed / Increased / Decreased / Unchanged.") .arg(snapshot_.readableBytes() >> 20); if (snapshot_.truncated) - status += QString(" Only the first %1 MB of the %2 MB region is covered.") + status += " " + tr("Only the first %1 MB of the %2 MB region is covered.") .arg(snapshot_.bytes.size() >> 20).arg(regionSize_ >> 20); statusLabel_->setText(status); } @@ -346,7 +347,7 @@ void GuestScanDialog::onAddToList() { if (row < 0 || row >= (int)candidates_.size()) continue; const uint64_t g = candidates_[row].first; emit addressSelected(gv.toHost(g), scanType_, scanBigEndian_, - QString("guest 0x%1").arg(regionGuestBase_ + g, 0, 16)); + tr("guest 0x%1").arg(regionGuestBase_ + g, 0, 16)); } } diff --git a/gui/heapregions.cpp b/gui/heapregions.cpp index 0a63c4af..6eeedf10 100644 --- a/gui/heapregions.cpp +++ b/gui/heapregions.cpp @@ -1,6 +1,7 @@ #include "gui/heapregions.hpp" #include "gui/theme.hpp" +#include #include #include #include @@ -29,15 +30,15 @@ bool isHeapLike(const MemoryRegion& region) { QString heapKind(const MemoryRegion& region) { if (region.path.find("[heap]") != std::string::npos) - return "brk heap"; - return "anonymous private"; + return QCoreApplication::translate("HeapRegionsWindow", "brk heap"); + return QCoreApplication::translate("HeapRegionsWindow", "anonymous private"); } } // namespace HeapRegionsWindow::HeapRegionsWindow(ProcessHandle* proc, QWidget* parent) : QMainWindow(parent), proc_(proc) { - setWindowTitle("Heap Regions"); + setWindowTitle(tr("Heap Regions")); resize(760, 480); auto* central = new QWidget; @@ -45,7 +46,7 @@ HeapRegionsWindow::HeapRegionsWindow(ProcessHandle* proc, QWidget* parent) table_ = new QTableWidget; table_->setColumnCount(6); - table_->setHorizontalHeaderLabels({"Start", "End", "Size", "Perm", "Kind", "Path"}); + table_->setHorizontalHeaderLabels({tr("Start"), tr("End"), tr("Size"), tr("Perm"), tr("Kind"), tr("Path")}); // Fit the fixed-width columns to content (16-digit hex addresses were clipped) // and let Path (variable) take the remaining width. auto* hh = table_->horizontalHeader(); @@ -53,7 +54,7 @@ HeapRegionsWindow::HeapRegionsWindow(ProcessHandle* proc, QWidget* parent) for (int c = 0; c < 5; ++c) hh->setSectionResizeMode(c, QHeaderView::ResizeToContents); styleDataTable(table_); - table_->setToolTip("Double-click a region to browse it"); + table_->setToolTip(tr("Double-click a region to browse it")); connect(table_, &QTableWidget::cellDoubleClicked, this, [this](int row, int) { bool ok = false; uintptr_t addr = table_->item(row, 0)->text().toULongLong(&ok, 16); @@ -81,7 +82,7 @@ void HeapRegionsWindow::populate() { table_->setItem(row, 0, new QTableWidgetItem(QString("%1").arg(region.base, 16, 16, QChar('0')))); table_->setItem(row, 1, new QTableWidgetItem(QString("%1").arg(region.base + region.size, 16, 16, QChar('0')))); table_->setItem(row, 2, new QTableWidgetItem(formatByteSize(region.size))); - table_->item(row, 2)->setToolTip(QString("%1 bytes (0x%2)").arg(region.size).arg(region.size, 0, 16)); + table_->item(row, 2)->setToolTip(tr("%1 bytes (0x%2)").arg(region.size).arg(region.size, 0, 16)); table_->item(row, 2)->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); table_->setItem(row, 3, new QTableWidgetItem(permsFor(region))); table_->setItem(row, 4, new QTableWidgetItem(heapKind(region))); @@ -89,7 +90,7 @@ void HeapRegionsWindow::populate() { total += region.size; } - setWindowTitle(QString("Heap Regions: %1 regions, %2") + setWindowTitle(tr("Heap Regions: %1 regions, %2") .arg(heaps.size()) .arg(formatByteSize(total))); } diff --git a/gui/i18n.cpp b/gui/i18n.cpp new file mode 100644 index 00000000..05a5ee89 --- /dev/null +++ b/gui/i18n.cpp @@ -0,0 +1,86 @@ +#include "gui/i18n.hpp" + +#include "core/log.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ce::gui { +namespace { + +/// Qt wants "zh_CN" from a desktop locale name such as "zh_CN.UTF-8". +QString desktopLocaleName() { + QString name = qEnvironmentVariable("LANG"); + if (name.isEmpty()) name = QLocale().name(); + return name.section('.', 0, 0); +} + +/// Where a translator may live: beside the executable (installed or build +/// directory), its translations/ subdirectory, or a translations/ directory +/// relative to the working directory. +QStringList translationSearchDirs() { + QStringList dirs; + char exeBuf[4096]; + const ssize_t len = readlink("/proc/self/exe", exeBuf, sizeof(exeBuf) - 1); + if (len > 0) { + exeBuf[len] = '\0'; + const QString exeDir = QFileInfo(QString::fromLocal8Bit(exeBuf)).absolutePath(); + dirs << exeDir + "/translations" << exeDir + "/../translations" << exeDir; + } + dirs << "translations"; + return dirs; +} + +} // namespace + +QString configuredUiLanguage() { + return QSettings().value(QStringLiteral("ui/language")).toString(); +} + +QString installTranslators(QApplication& app, const QString& explicitLocale) { + // Number and date formatting keeps following the desktop locale, as before; + // only the interface language changes when Settings picks one. + const QLocale desktopLocale(desktopLocaleName()); + QLocale::setDefault(desktopLocale); + + QString wanted = explicitLocale; + if (wanted.isEmpty()) wanted = configuredUiLanguage(); + if (wanted.isEmpty()) wanted = desktopLocale.name(); + const QLocale uiLocale(wanted); + + auto* qtTranslator = new QTranslator(&app); + if (qtTranslator->load(uiLocale, QStringLiteral("qt"), QStringLiteral("_"), + QLibraryInfo::path(QLibraryInfo::TranslationsPath))) { + app.installTranslator(qtTranslator); + } + + // A catalogue dropped next to the executable wins over the built-in one, so + // translators can test a .qm without rebuilding. + auto* ceTranslator = new QTranslator(&app); + QString loadedFrom; + const QStringList dirs = translationSearchDirs(); + for (const QString& dir : dirs) { + if (ceTranslator->load(uiLocale, QStringLiteral("cheatengine"), QStringLiteral("_"), dir)) { + loadedFrom = dir; + break; + } + } + if (loadedFrom.isEmpty()) { + // Installed builds embed the catalogue in a resource. + if (ceTranslator->load(QStringLiteral(":/i18n/cheatengine_%1.qm").arg(uiLocale.name()))) + loadedFrom = QStringLiteral("built-in"); + } + if (!loadedFrom.isEmpty()) app.installTranslator(ceTranslator); + + ce::log::info(ce::log::Cat::Gui, "interface language {} (catalogue: {})", + uiLocale.name().toStdString(), + loadedFrom.isEmpty() ? std::string("none") : loadedFrom.toStdString()); + return uiLocale.name(); +} + +} // namespace ce::gui diff --git a/gui/i18n.hpp b/gui/i18n.hpp new file mode 100644 index 00000000..ba9c058f --- /dev/null +++ b/gui/i18n.hpp @@ -0,0 +1,20 @@ +#pragma once +/// Interface language: translator setup shared by the application and the GUI +/// smoke test. + +#include + +class QApplication; + +namespace ce::gui { + +/// The interface language stored in Settings ("ui/language"), or empty when the +/// interface follows the desktop locale. +QString configuredUiLanguage(); + +/// Installs the Qt and Cheat Engine translators and returns the locale that was +/// chosen. explicitLocale, when non-empty, wins over Settings and the desktop +/// locale (used by tests and screenshot runs). +QString installTranslators(QApplication& app, const QString& explicitLocale = {}); + +} // namespace ce::gui diff --git a/gui/luaconsole.cpp b/gui/luaconsole.cpp index 3b9335f6..235f0668 100644 --- a/gui/luaconsole.cpp +++ b/gui/luaconsole.cpp @@ -19,7 +19,7 @@ static void appendColored(QPlainTextEdit* out, const QString& text, const QColor LuaConsole::LuaConsole(LuaEngine* engine, QWidget* parent) : QMainWindow(parent), engine_(engine) { - setWindowTitle("Lua Console"); + setWindowTitle(tr("Lua Console")); resize(600, 400); auto* central = new QWidget; @@ -30,8 +30,8 @@ LuaConsole::LuaConsole(LuaEngine* engine, QWidget* parent) output_->setFont(monospaceFont()); output_->document()->setDocumentMargin(6); // The banner reads as a comment (dim), like the echoed commands. - appendColored(output_, "-- Cheat Engine Lua Console --", editorPalette().dim); - appendColored(output_, "-- Functions: readInteger(addr), writeInteger(addr, val), getModuleBase(name), ...", + appendColored(output_, tr("-- Cheat Engine Lua Console --"), editorPalette().dim); + appendColored(output_, tr("-- Functions: readInteger(addr), writeInteger(addr, val), getModuleBase(name), ..."), editorPalette().dim); layout->addWidget(output_); @@ -40,22 +40,22 @@ LuaConsole::LuaConsole(LuaEngine* engine, QWidget* parent) auto* inputRow = new QHBoxLayout; input_ = new QLineEdit; input_->setFont(monospaceFont()); - input_->setPlaceholderText("Enter Lua code, then press Run or Enter…"); + input_->setPlaceholderText(tr("Enter Lua code, then press Run or Enter…")); connect(input_, &QLineEdit::returnPressed, this, &LuaConsole::onExecute); input_->installEventFilter(this); // Up/Down recall previous commands (REPL history) inputRow->addWidget(input_, 1); - auto* runBtn = new QPushButton("Run"); + auto* runBtn = new QPushButton(tr("Run")); runBtn->setProperty("primary", true); - runBtn->setToolTip("Execute the Lua code (Enter)"); + runBtn->setToolTip(tr("Execute the Lua code (Enter)")); connect(runBtn, &QPushButton::clicked, this, &LuaConsole::onExecute); inputRow->addWidget(runBtn); - auto* clearBtn = new QPushButton("Clear"); - clearBtn->setToolTip("Clear the console output"); + auto* clearBtn = new QPushButton(tr("Clear")); + clearBtn->setToolTip(tr("Clear the console output")); connect(clearBtn, &QPushButton::clicked, this, [this]() { output_->clear(); - appendColored(output_, "-- Cheat Engine Lua Console --", editorPalette().dim); + appendColored(output_, tr("-- Cheat Engine Lua Console --"), editorPalette().dim); }); inputRow->addWidget(clearBtn); layout->addLayout(inputRow); @@ -110,7 +110,7 @@ void LuaConsole::onExecute() { auto err = engine_->execute(code.toStdString()); if (!err.empty()) // Errors in red (legible on both the light and dark themes). - appendColored(output_, "ERROR: " + QString::fromStdString(err), editorPalette().error); + appendColored(output_, tr("ERROR: %1").arg(QString::fromStdString(err)), editorPalette().error); } void LuaConsole::abandonLine() { diff --git a/gui/main.cpp b/gui/main.cpp index 87aaa7ff..32fd883d 100644 --- a/gui/main.cpp +++ b/gui/main.cpp @@ -3,6 +3,7 @@ #include "gui/memorybrowser.hpp" #include "core/log.hpp" #include "gui/theme.hpp" +#include "gui/i18n.hpp" #include #include #include @@ -19,9 +20,6 @@ #include #include #include -#include -#include -#include #include #include #include @@ -100,47 +98,10 @@ int main(int argc, char* argv[]) { ce::gui::applyStoredTheme(); // ── i18n ── - // Two translators: one for Qt's own strings (so dialogs etc. localise), - // and one for cecore's translations. The cecore .qm files live next to - // the binary under translations/cheatengine_.qm. Translations - // aren't shipped yet, but the load is best-effort and silently skips - // when no file matches — the wiring is ready for translators to drop - // .qm files in. - QLocale locale; - { - QString locName = qEnvironmentVariable("LANG", locale.name()); - // Strip ".UTF-8" etc. — Qt only wants "en_US" or "tr_TR". - locName = locName.section('.', 0, 0); - if (!locName.isEmpty()) locale = QLocale(locName); - } - QLocale::setDefault(locale); - - auto* qtTranslator = new QTranslator(&app); - if (qtTranslator->load(locale, "qt", "_", - QLibraryInfo::path(QLibraryInfo::TranslationsPath))) { - app.installTranslator(qtTranslator); - } - auto* ceTranslator = new QTranslator(&app); - { - // Search a few likely locations for cheatengine_.qm: - // /translations/, /../translations/, current dir. - QStringList searchDirs; - char exeBufLocal[4096]; - ssize_t exeLenLocal = readlink("/proc/self/exe", exeBufLocal, sizeof(exeBufLocal) - 1); - if (exeLenLocal > 0) { - exeBufLocal[exeLenLocal] = 0; - auto exeDir = QFileInfo(exeBufLocal).absolutePath(); - searchDirs << (exeDir + "/translations") - << (exeDir + "/../translations"); - } - searchDirs << "translations"; - for (const auto& dir : searchDirs) { - if (ceTranslator->load(locale, "cheatengine", "_", dir)) { - app.installTranslator(ceTranslator); - break; - } - } - } + // The interface language comes from Settings ("ui/language"), otherwise from + // the desktop locale. The catalogue is looked up beside the executable and in + // the built-in resources; see gui/i18n.cpp. + ce::gui::installTranslators(app); // Set application icon — resolve exe path via /proc/self/exe QIcon appIcon; diff --git a/gui/mainwindow.cpp b/gui/mainwindow.cpp index 77223c84..8cfeead8 100644 --- a/gui/mainwindow.cpp +++ b/gui/mainwindow.cpp @@ -150,7 +150,7 @@ constexpr int kValueRefreshMs = 500; static constexpr int kResultBatchSize = 1000; static QString foundLabelText(size_t count) { - return QString("Found: %1").arg(QLocale().toString((qulonglong)count)); + return QString(QCoreApplication::translate("MainWindow", "Found: %1")).arg(QLocale().toString((qulonglong)count)); } // The warning for a scan whose result files are unreliable. Name what failed: @@ -159,12 +159,14 @@ static QString foundLabelText(size_t count) { static QString scanWriteErrorText(const ScanResult& result) { const auto& reason = result.writeErrorReason(); QString text = reason.empty() - ? QString("A scan-result file could not be fully written or read.") - : QString("A scan-result file could not be fully written or read: %1.").arg(QString::fromStdString(reason)); + ? QCoreApplication::translate("MainWindow", "A scan-result file could not be fully written or read.") + : QCoreApplication::translate("MainWindow", + "A scan-result file could not be fully written or read: %1.").arg(QString::fromStdString(reason)); if (reason.find(std::strerror(ENOSPC)) != std::string::npos || reason.find(std::strerror(EDQUOT)) != std::string::npos) - text += QString(" The disk holding %1 is full; free space there or set CE_SCAN_DIR to a larger disk.") + text += QCoreApplication::translate("MainWindow", + " The disk holding %1 is full; free space there or set CE_SCAN_DIR to a larger disk.") .arg(QString::fromStdString(result.directory().parent_path().parent_path().string())); - return text + " Some results are unreliable; scan again."; + return text + QCoreApplication::translate("MainWindow", " Some results are unreliable; scan again."); } // Settings > Display "Uppercase hexadecimal" and "Address width", read like @@ -240,8 +242,8 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { // selectFilePath(sender, setting) in a table's Lua opens a real file chooser. luaEngine_.setFilePicker([this](const std::string& setting) -> std::string { - QString title = setting.empty() ? QString("Select file") - : QString("Select file for %1").arg(QString::fromStdString(setting)); + QString title = setting.empty() ? QString(tr("Select file")) + : QString(tr("Select file for %1")).arg(QString::fromStdString(setting)); return QFileDialog::getOpenFileName(this, title).toStdString(); }); @@ -264,9 +266,9 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { if (dynamic_cast(process_.get()) && !process_->targetDescription().live) { if (nestedLoop) return; onDisconnectProcess(); - processLabel_->setText("GDB target disconnected or exited"); + processLabel_->setText(tr("GDB target disconnected or exited")); processDetail_->clear(); - statusBar()->showMessage("GDB target disconnected or exited",5000); + statusBar()->showMessage(tr("GDB target disconnected or exited"),5000); return; } // A ceserver connection closes on any error or timeout and never comes @@ -276,9 +278,9 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { if (ceserverClient_ && !ceserverClient_->isConnected()) { if (nestedLoop) return; onDisconnectProcess(); - processLabel_->setText("ceserver connection lost"); + processLabel_->setText(tr("ceserver connection lost")); processDetail_->setText(tr("Connect again with File > Connect to ceserver.")); - statusBar()->showMessage("ceserver connection lost",5000); + statusBar()->showMessage(tr("ceserver connection lost"),5000); return; } // Detect target death: once the pid is gone, stop polling and clear state @@ -292,9 +294,9 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { exitInfo.si_pid == currentPid_; if (exitedChild || (!ceserverClient_ && currentPid_ > 0 && ::kill(currentPid_, 0) != 0 && errno == ESRCH)) { if (nestedLoop) return; - processLabel_->setText(QString("Process %1 has exited").arg(currentPid_)); + processLabel_->setText(QString(tr("Process %1 has exited")).arg(currentPid_)); processDetail_->setText(tr("Open another process with Ctrl+O.")); - statusBar()->showMessage("Target process exited", 5000); + statusBar()->showMessage(tr("Target process exited"), 5000); if (!releaseTargetUsers()) return; if (exitedChild) (void)::waitpid(currentPid_, nullptr, WNOHANG); // reap it now that nothing traces it process_.reset(); @@ -368,52 +370,52 @@ void MainWindow::setupMenus() { // Tools, which CE leaves empty in the form and fills at runtime. // ── File ── - auto* file = menuBar()->addMenu("&File"); - file->addAction("Clear scan results", this, [this]() { + auto* file = menuBar()->addMenu(tr("&File")); + file->addAction(tr("Clear scan results"), this, [this]() { resultsModel_->clear(); lastResult_.reset(); undoResult_.reset(); updateScanButtons(); }); - file->addAction("Open Process...", this, &MainWindow::onOpenProcess, QKeySequence("Ctrl+O")); + file->addAction(tr("Open Process..."), this, &MainWindow::onOpenProcess, QKeySequence("Ctrl+O")); file->addSeparator(); - file->addAction("Save", this, &MainWindow::onSaveTable, QKeySequence("Ctrl+S")); - file->addAction("Save As...", this, &MainWindow::onSaveTableAs, QKeySequence("Ctrl+Shift+S")); - file->addAction("Load...", this, &MainWindow::onLoadTable, QKeySequence("Ctrl+L")); - recentMenu_ = file->addMenu("Load Recent"); // populated from QSettings below + file->addAction(tr("Save"), this, &MainWindow::onSaveTable, QKeySequence("Ctrl+S")); + file->addAction(tr("Save As..."), this, &MainWindow::onSaveTableAs, QKeySequence("Ctrl+Shift+S")); + file->addAction(tr("Load..."), this, &MainWindow::onLoadTable, QKeySequence("Ctrl+L")); + recentMenu_ = file->addMenu(tr("Load Recent")); // populated from QSettings below recentMenu_->setToolTipsVisible(true); // show the full path on hover rebuildRecentMenu(); file->addSeparator(); - file->addAction("Save scan results...", this, &MainWindow::onSaveScanResults, + file->addAction(tr("Save scan results..."), this, &MainWindow::onSaveScanResults, QKeySequence("Alt+Shift+S")); file->addSeparator(); - file->addAction("Generate Trainer Lua Script...", this, &MainWindow::onCreateTrainer); + file->addAction(tr("Generate Trainer Lua Script..."), this, &MainWindow::onCreateTrainer); file->addSeparator(); - file->addAction("Connect to ceserver...", this, &MainWindow::onConnectCeserver); // Linux extra - file->addAction("Connect to GDB / QEMU...", this, &MainWindow::onConnectGdb); - file->addAction("Disconnect target", this, &MainWindow::onDisconnectProcess); + file->addAction(tr("Connect to ceserver..."), this, &MainWindow::onConnectCeserver); // Linux extra + file->addAction(tr("Connect to GDB / QEMU..."), this, &MainWindow::onConnectGdb); + file->addAction(tr("Disconnect target"), this, &MainWindow::onDisconnectProcess); file->addSeparator(); - file->addAction("Quit", this, &QWidget::close, QKeySequence("Ctrl+Q")); + file->addAction(tr("Quit"), this, &QWidget::close, QKeySequence("Ctrl+Q")); // ── Top-level menus in CE's order (D3D dropped — Windows-only, see below) ── // File · Edit · Process · Table · Tools · .Net · Network · Plugins · // Languages · Help. - auto* edit = menuBar()->addMenu("&Edit"); - auto* process = menuBar()->addMenu("&Process"); - auto* table = menuBar()->addMenu("Table"); + auto* edit = menuBar()->addMenu(tr("&Edit")); + auto* process = menuBar()->addMenu(tr("&Process")); + auto* table = menuBar()->addMenu(tr("Table")); // CE's "D3D" menu (Direct3D overlay/hook) is Windows-only and every item is // permanently disabled on Linux, so it's dropped here rather than shown as a // dead top-level menu. Graphics hooking is out of scope (see docs/DEVELOPMENT). - auto* tools = menuBar()->addMenu("&Tools"); - auto* dotnet = menuBar()->addMenu(".Net"); + auto* tools = menuBar()->addMenu(tr("&Tools")); + auto* dotnet = menuBar()->addMenu(tr(".Net")); // CE's .Net/Mono menu: dissect a Mono/IL2CPP runtime's classes and fields. - dotnet->addAction("Mono dissector...", this, &MainWindow::openMonoDissector); + dotnet->addAction(tr("Mono dissector..."), this, &MainWindow::openMonoDissector); // CE also has Network, Plugins and Languages menus. Their ceserver transfer // options, plugin loader and translations don't exist here, so the menus are // left out rather than shown full of disabled items (same call as D3D above). // ── Table (CE: Lua script + forms) ── - table->addAction("Show Cheat Table Lua Script", this, &MainWindow::showTableLuaScript, + table->addAction(tr("Show Cheat Table Lua Script"), this, &MainWindow::showTableLuaScript, QKeySequence("Ctrl+Alt+L")); table->addSeparator(); - table->addAction("Create Form...", this, [this]() { + table->addAction(tr("Create Form..."), this, [this]() { auto* fd = new FormDesigner(this); fd->setAttribute(Qt::WA_DeleteOnClose); fd->show(); }); @@ -424,8 +426,8 @@ void MainWindow::setupMenus() { // Regions, Heap list, Module list, Referenced strings, Thread list, Stacktrace, // Registers, Breakpoint list, Debugger, Branch mapper, Break-and-trace, …) live // in ITS View/Debug menus — matching CE — populated by populateBrowserMenus(). - tools->addAction("Memory Viewer...", this, &MainWindow::onMemoryView, QKeySequence("Ctrl+M")); - tools->addAction("AI Assistant", this, [this]() { showAgentPanel(); }, QKeySequence("Ctrl+Shift+I")); + tools->addAction(tr("Memory Viewer..."), this, &MainWindow::onMemoryView, QKeySequence("Ctrl+M")); + tools->addAction(tr("AI Assistant"), this, [this]() { showAgentPanel(); }, QKeySequence("Ctrl+Shift+I")); tools->addSeparator(); // Analysis panels (also in the Memory Viewer's Tools menu) — surfaced here so // they're findable from the main window without opening the viewer first. @@ -433,26 +435,26 @@ void MainWindow::setupMenus() { // Table > Auto Assemble... is the fuller version (it picks up the viewer's // selected instruction), so the Tools copy would only duplicate it. for (QAction* action : tools->actions()) - if (action->text() == "Auto Assemble...") tools->removeAction(action); + if (action->text() == tr("Auto Assemble...")) tools->removeAction(action); tools->addSeparator(); // Table-wide edits (the entry context menu keeps to the entry under the cursor). for (auto* button : findChildren()) - if (button->text() == "Add Address") - edit->addAction("Add Address...", button, &QPushButton::click); - edit->addAction("Add Group", this, [this]() { addressListModel_->addGroup(); }); + if (button->text() == tr("Add Address")) + edit->addAction(tr("Add Address..."), button, &QPushButton::click); + edit->addAction(tr("Add Group"), this, [this]() { addressListModel_->addGroup(); }); edit->addSeparator(); - edit->addAction("Enable all entries", this, [this]() { addressListModel_->setAllActive(true); }); - edit->addAction("Disable all entries", this, [this]() { addressListModel_->setAllActive(false); }); + edit->addAction(tr("Enable all entries"), this, [this]() { addressListModel_->setAllActive(true); }); + edit->addAction(tr("Disable all entries"), this, [this]() { addressListModel_->setAllActive(false); }); edit->addSeparator(); - edit->addAction("Settings...", this, [this]() { openSettingsDialog(); }); + edit->addAction(tr("Settings..."), this, [this]() { openSettingsDialog(); }); // ── Process menu ── - process->addAction("Open Process...", this, &MainWindow::onOpenProcess); + process->addAction(tr("Open Process..."), this, &MainWindow::onOpenProcess); // CE formProcessInfo (Process/System Info) — Linux equivalents. // CE "Create Process" / frmopenfileasprocessdialogunit: launch an executable // and attach. As our own child it is ptrace-accessible under yama scope=1. - process->addAction("Create Process...", this, [this]() { - auto path = QFileDialog::getOpenFileName(this, "Open file as process", "", "Executables (*)"); + process->addAction(tr("Create Process..."), this, [this]() { + auto path = QFileDialog::getOpenFileName(this, tr("Open file as process"), "", tr("Executables (*)")); if (path.isEmpty()) return; std::string p = path.toStdString(); pid_t pid = fork(); @@ -461,15 +463,15 @@ void MainWindow::setupMenus() { execl(p.c_str(), p.c_str(), (char*)nullptr); _exit(127); } - if (pid < 0) { QMessageBox::warning(this, "Create Process", "fork() failed."); return; } + if (pid < 0) { QMessageBox::warning(this, tr("Create Process"), tr("fork() failed.")); return; } usleep(120000); // let it map its image // Through attachToPid so the old target's Debugger/views are torn down // before process_ is replaced (they hold a raw handle). attachToPid(pid, QFileInfo(path).fileName()); }); - process->addAction("Process/System Info", this, [this]() { + process->addAction(tr("Process/System Info"), this, [this]() { QDialog dialog(this); - dialog.setWindowTitle("Process and System Info"); + dialog.setWindowTitle(tr("Process and System Info")); auto* layout = new QVBoxLayout(&dialog); // One form for both sections so their label columns line up. auto* form = new QFormLayout; @@ -489,37 +491,41 @@ void MainWindow::setupMenus() { label->setWordWrap(true); return label; }; - auto* target = section("Process"); + auto* target = section(tr("Process")); if (!process_) { - target->addRow(value("No process is open. Use File > Open Process.")); + target->addRow(value(tr("No process is open. Use File > Open Process."))); } else { const auto modules = process_->modules(); const auto description = process_->targetDescription(); - target->addRow("Name:", value(currentProcessName_)); - target->addRow("PID:", value(QString::number(currentPid_))); - target->addRow("Executable:", value(modules.empty() ? QString("(unknown)") : QString::fromStdString(modules.front().path))); + target->addRow(tr("Name:"), value(currentProcessName_)); + target->addRow(tr("PID:"), value(QString::number(currentPid_))); + target->addRow(tr("Executable:"), value(modules.empty() ? QString(tr("(unknown)")) : QString::fromStdString(modules.front().path))); if (auto localPid = ce::localTargetPid(process_.get())) - target->addRow("Kind:", value(QString::fromStdString(ce::probeTarget(*localPid).summary()))); - target->addRow("Program:", value(QString("%1, %2-byte pointers, %3") + target->addRow(tr("Kind:"), value(QString::fromStdString(ce::probeTarget(*localPid).summary()))); + target->addRow(tr("Program:"), value(QString(tr("%1, %2-byte pointers, %3")) .arg(ce::cpuArchitectureName(description.program.architecture)).arg(description.program.pointerWidth) .arg(ce::byteOrderName(description.program.byteOrder)))); - target->addRow("Modules:", value(QString::number(modules.size()))); - target->addRow("Threads:", value(QString::number(process_->threads().size()))); + target->addRow(tr("Modules:"), value(QString::number(modules.size()))); + target->addRow(tr("Threads:"), value(QString::number(process_->threads().size()))); } - auto* system = section("System"); + auto* system = section(tr("System")); struct utsname uts{}; - if (::uname(&uts) == 0) system->addRow("Kernel:", value(QString("%1 %2 (%3)").arg(uts.sysname, uts.release, uts.machine))); - system->addRow("CPUs:", value(QString::number(std::thread::hardware_concurrency()))); - QString scope = "not present (Yama disabled)"; + if (::uname(&uts) == 0) system->addRow(tr("Kernel:"), value(QString("%1 %2 (%3)").arg(uts.sysname, uts.release, uts.machine))); + system->addRow(tr("CPUs:"), value(QString::number(std::thread::hardware_concurrency()))); + QString scope = tr("not present (Yama disabled)"); if (QFile f("/proc/sys/kernel/yama/ptrace_scope"); f.open(QIODevice::ReadOnly)) { const QString level = QString::fromUtf8(f.readAll()).trimmed(); - static const char* meaning[] = {"any process of the same user", "only child processes", - "administrators only", "no attaching"}; + static const char* const meaning[] = { + QT_TRANSLATE_NOOP("MainWindow", "any process of the same user"), + QT_TRANSLATE_NOOP("MainWindow", "only child processes"), + QT_TRANSLATE_NOOP("MainWindow", "administrators only"), + QT_TRANSLATE_NOOP("MainWindow", "no attaching")}; const int n = level.toInt(); - scope = n >= 0 && n <= 3 ? QString("%1: %2").arg(level, meaning[n]) : level; + scope = n >= 0 && n <= 3 ? QString("%1: %2").arg(level, + QCoreApplication::translate("MainWindow", meaning[n])) : level; } - system->addRow("ptrace scope:", value(scope)); - system->addRow("Qt:", value(qVersion())); + system->addRow(tr("ptrace scope:"), value(scope)); + system->addRow(tr("Qt:"), value(qVersion())); layout->addStretch(); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); @@ -528,27 +534,27 @@ void MainWindow::setupMenus() { dialog.exec(); }); // "Pause the process" toggle (CE's pause-the-game) — SIGSTOP/SIGCONT the target. - auto* pauseAct = process->addAction("Pause the process"); + auto* pauseAct = process->addAction(tr("Pause the process")); pauseAct->setCheckable(true); pauseAction_ = pauseAct; connect(pauseAct, &QAction::toggled, this, [this, pauseAct](bool checked) { auto pid=ce::localTargetPid(process_.get()); if (!pid) { pauseAct->setChecked(false); - statusBar()->showMessage("Pause requires a live local process target",5000);return; + statusBar()->showMessage(tr("Pause requires a live local process target"),5000);return; } if (::kill(*pid, checked ? SIGSTOP : SIGCONT) != 0) pauseAct->setChecked(false); }); // Optionally SIGSTOP the target for the duration of each scan (CE's "pause // while scanning") so values can't change mid-scan, then resume. - auto* pauseScanAct = process->addAction("Pause target while scanning"); + auto* pauseScanAct = process->addAction(tr("Pause target while scanning")); pauseScanAct->setCheckable(true); - pauseScanAct->setToolTip("Suspend the target during each First/Next Scan so its " - "memory is a consistent snapshot, then resume it."); + pauseScanAct->setToolTip(tr("Suspend the target during each First/Next Scan so its " + "memory is a consistent snapshot, then resume it.")); connect(pauseScanAct, &QAction::toggled, this, [this](bool on) { pauseWhileScanning_ = on; }); // ── Table menu ── - table->addAction("Auto Assemble...", this, [this]() { + table->addAction(tr("Auto Assemble..."), this, [this]() { auto* editor = new ScriptEditor(process_.get(), &autoAsm_, this); for (auto it = memoryViewers_.rbegin(); it != memoryViewers_.rend(); ++it) { if (*it && (*it)->isVisible()) { @@ -568,20 +574,20 @@ void MainWindow::setupMenus() { editor->setBeforeExecute([this]() { stopCodeFindersForInjection(); }); editor->show(); }, QKeySequence("Ctrl+A")); - tools->addAction("File Patcher...", this, [this]() { + tools->addAction(tr("File Patcher..."), this, [this]() { auto* dlg = new FilePatcher(this); dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->show(); }); // CE frmSaveMemoryRegionUnit (Save memoryregion): dump a From..To range to a file. - tools->addAction("Save Memory Region...", this, [this]() { - if (!process_) { QMessageBox::warning(this, "Save Memory Region", "Open a process first."); return; } + tools->addAction(tr("Save Memory Region..."), this, [this]() { + if (!process_) { QMessageBox::warning(this, tr("Save Memory Region"), tr("Open a process first.")); return; } auto* dlg = new QDialog(this); - dlg->setWindowTitle("Save Memory Region"); + dlg->setWindowTitle(tr("Save Memory Region")); trackTargetWindow(dlg); // closed when the target changes auto* v = new QVBoxLayout(dlg); - auto* intro = new QLabel("Save a range of the target's memory to a file (up to 64 MiB). " - "Addresses are hexadecimal; To is exclusive."); + auto* intro = new QLabel(tr("Save a range of the target's memory to a file (up to 64 MiB). " + "Addresses are hexadecimal; To is exclusive.")); intro->setWordWrap(true); intro->setProperty("secondary", true); v->addWidget(intro); @@ -592,10 +598,10 @@ void MainWindow::setupMenus() { edit->setFont(monospaceFont()); edit->setMinimumWidth(240); } - fromEdit->setPlaceholderText("e.g. 7f3a00000000"); - toEdit->setPlaceholderText("e.g. 7f3a00001000"); - form->addRow("From:", fromEdit); - form->addRow("To:", toEdit); + fromEdit->setPlaceholderText(tr("e.g. 7f3a00000000")); + toEdit->setPlaceholderText(tr("e.g. 7f3a00001000")); + form->addRow(tr("From:"), fromEdit); + form->addRow(tr("To:"), toEdit); v->addLayout(form); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel); v->addWidget(buttons); @@ -603,46 +609,46 @@ void MainWindow::setupMenus() { connect(buttons, &QDialogButtonBox::accepted, this, [this, dlg, fromEdit, toEdit]() { uintptr_t from = fromEdit->text().trimmed().toULongLong(nullptr, 16); uintptr_t to = toEdit->text().trimmed().toULongLong(nullptr, 16); - if (to <= from) { QMessageBox::warning(dlg, "Save", "\"To\" must be greater than \"From\"."); return; } + if (to <= from) { QMessageBox::warning(dlg, tr("Save"), tr("\"To\" must be greater than \"From\".")); return; } // Cap like the Save Disassembly path: an unbounded range would throw // std::bad_alloc out of a Qt slot (terminate) and OOM. constexpr uintptr_t kMaxSave = 64ull * 1024 * 1024; // 64 MiB if (to - from > kMaxSave) { - QMessageBox::warning(dlg, "Save", "Region too large (max 64 MiB)."); + QMessageBox::warning(dlg, tr("Save"), tr("Region too large (max 64 MiB).")); return; } - auto path = QFileDialog::getSaveFileName(dlg, "Save memory region", "region.bin"); + auto path = QFileDialog::getSaveFileName(dlg, tr("Save memory region"), "region.bin"); if (path.isEmpty()) return; // The process can have exited or been closed while this was open. - if (!process_) { QMessageBox::warning(dlg, "Save", "The process is no longer open."); return; } + if (!process_) { QMessageBox::warning(dlg, tr("Save"), tr("The process is no longer open.")); return; } std::vector buf(to - from); auto r = process_->read(from, buf.data(), buf.size()); - if (!r || *r == 0) { QMessageBox::warning(dlg, "Save", "Could not read that region."); return; } + if (!r || *r == 0) { QMessageBox::warning(dlg, tr("Save"), tr("Could not read that region.")); return; } std::ofstream f(path.toStdString(), std::ios::binary); f.write(reinterpret_cast(buf.data()), (std::streamsize)*r); - if (!f) { QMessageBox::warning(dlg, "Save", QString("Could not write %1.").arg(path)); return; } - QMessageBox::information(dlg, "Saved", QString("Wrote %1 to %2.").arg(formatByteSize(*r), path)); + if (!f) { QMessageBox::warning(dlg, tr("Save"), QString(tr("Could not write %1.")).arg(path)); return; } + QMessageBox::information(dlg, tr("Saved"), QString(tr("Wrote %1 to %2.")).arg(formatByteSize(*r), path)); dlg->accept(); }); dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->show(); }); // CE frmMemoryAllocHandlerUnit (Memory Allocations): allocate + track blocks. - tools->addAction("Memory Allocations...", this, [this]() { - if (!process_) { QMessageBox::warning(this, "Memory Allocations", "Open a process first."); return; } + tools->addAction(tr("Memory Allocations..."), this, [this]() { + if (!process_) { QMessageBox::warning(this, tr("Memory Allocations"), tr("Open a process first.")); return; } auto* dlg = new QDialog(this); - dlg->setWindowTitle("Memory Allocations"); + dlg->setWindowTitle(tr("Memory Allocations")); trackTargetWindow(dlg); dlg->resize(420, 320); auto* v = new QVBoxLayout(dlg); - auto* intro = new QLabel("Memory reserved in the target from here (readable, writable and " - "executable). Double-click a block to browse it."); + auto* intro = new QLabel(tr("Memory reserved in the target from here (readable, writable and " + "executable). Double-click a block to browse it.")); intro->setWordWrap(true); intro->setProperty("secondary", true); v->addWidget(intro); auto* list = new QListWidget; list->setProperty("monospace", true); // font from the theme sheet - setEmptyHint(list, "No blocks allocated yet.\nClick Allocate to reserve memory in the target."); + setEmptyHint(list, tr("No blocks allocated yet.\nClick Allocate to reserve memory in the target.")); auto refill = [this, list]() { list->clear(); for (const auto& a : allocations_) @@ -651,22 +657,22 @@ void MainWindow::setupMenus() { refill(); v->addWidget(list); auto* row = new QHBoxLayout; - auto* allocBtn = new QPushButton("Allocate"); - auto* freeBtn = new QPushButton("Free"); - freeBtn->setToolTip("Free the selected block"); + auto* allocBtn = new QPushButton(tr("Allocate")); + auto* freeBtn = new QPushButton(tr("Free")); + freeBtn->setToolTip(tr("Free the selected block")); freeBtn->setEnabled(false); // needs a selected block connect(list, &QListWidget::currentRowChanged, freeBtn, [freeBtn](int row) { freeBtn->setEnabled(row >= 0); }); - auto* closeBtn = new QPushButton("Close"); + auto* closeBtn = new QPushButton(tr("Close")); row->addWidget(allocBtn); row->addWidget(freeBtn); row->addStretch(); row->addWidget(closeBtn); v->addLayout(row); connect(closeBtn, &QPushButton::clicked, dlg, &QDialog::accept); connect(allocBtn, &QPushButton::clicked, this, [this, dlg, refill]() { bool ok = false; - int size = QInputDialog::getInt(dlg, "Allocate", "Size (bytes):", 4096, 1, 1 << 30, 1, &ok); + int size = QInputDialog::getInt(dlg, tr("Allocate"), tr("Size (bytes):"), 4096, 1, 1 << 30, 1, &ok); if (!ok) return; auto r = process_->allocate(size, ce::MemProt::All, 0); if (!r) { - QMessageBox::warning(dlg, "Allocate", QString("Could not allocate %1 in the target: %2") + QMessageBox::warning(dlg, tr("Allocate"), QString(tr("Could not allocate %1 in the target: %2")) .arg(formatByteSize(size), QString::fromStdString(r.error().message()))); return; } @@ -677,7 +683,7 @@ void MainWindow::setupMenus() { int i = list->currentRow(); if (i < 0 || i >= (int)allocations_.size()) return; if (auto freed = process_->free(allocations_[i].first, allocations_[i].second); !freed) { - QMessageBox::warning(dlg, "Free", QString("Could not free the block: %1") + QMessageBox::warning(dlg, tr("Free"), QString(tr("Could not free the block: %1")) .arg(QString::fromStdString(freed.error().message()))); return; } @@ -692,25 +698,25 @@ void MainWindow::setupMenus() { dlg->show(); }); // CE frmExeTrainerGeneratorUnit: compile a standalone trainer executable. - tools->addAction("Generate Trainer (executable)...", this, [this]() { - auto path = QFileDialog::getSaveFileName(this, "Generate Trainer executable", "trainer"); + tools->addAction(tr("Generate Trainer (executable)..."), this, [this]() { + auto path = QFileDialog::getSaveFileName(this, tr("Generate Trainer executable"), "trainer"); if (path.isEmpty()) return; try { ce::CheatTable table = buildCheatTable(); ce::TrainerGenerator gen; std::string err = gen.generateBinary(table, path.toStdString()); if (err.empty()) - statusBar()->showMessage("Trainer executable written to " + path, 6000); + statusBar()->showMessage(tr("Trainer executable written to %1").arg(path), 6000); else - QMessageBox::critical(this, "Generate Trainer", QString::fromStdString(err)); + QMessageBox::critical(this, tr("Generate Trainer"), QString::fromStdString(err)); } catch (const std::exception& ex) { - QMessageBox::critical(this, "Generate Trainer", ex.what()); + QMessageBox::critical(this, tr("Generate Trainer"), ex.what()); } }); // CE frmStringPointerScanUnit (Structure spider): find pointer paths to a target // and dissect the structure there. - tools->addAction("Structure Spider...", this, [this]() { - if (!process_) { QMessageBox::warning(this, "Structure Spider", "Open a process first."); return; } + tools->addAction(tr("Structure Spider..."), this, [this]() { + if (!process_) { QMessageBox::warning(this, tr("Structure Spider"), tr("Open a process first.")); return; } bool ok = false; // Start from the selected cheat-table entry, the usual thing to spider from. QString initial; @@ -719,17 +725,17 @@ void MainWindow::setupMenus() { if (int r = rows.first().row(); r >= 0 && r < (int)entries.size() && !entries[r].isGroup) initial = QString("0x%1").arg((qulonglong)entries[r].address, 0, 16); } - QString ts = QInputDialog::getText(this, "Structure Spider", - "Address of the value to find pointer paths to (hex):", + QString ts = QInputDialog::getText(this, tr("Structure Spider"), + tr("Address of the value to find pointer paths to (hex):"), QLineEdit::Normal, initial, &ok); if (!ok || ts.trimmed().isEmpty()) return; bool valid = false; uintptr_t target = ts.trimmed().toULongLong(&valid, 16); if (!valid || !target) { - QMessageBox::warning(this, "Structure Spider", QString("\"%1\" is not a hexadecimal address.").arg(ts.trimmed())); + QMessageBox::warning(this, tr("Structure Spider"), QString(tr("\"%1\" is not a hexadecimal address.")).arg(ts.trimmed())); return; } - if (!process_) { QMessageBox::warning(this, "Structure Spider", "The process is no longer open."); return; } + if (!process_) { QMessageBox::warning(this, tr("Structure Spider"), tr("The process is no longer open.")); return; } // The scan runs on a worker so the GUI keeps running; the window shows // its progress. Closing the window, or a target change (which deletes // target windows), cancels the scan and joins the worker. @@ -743,13 +749,13 @@ void MainWindow::setupMenus() { std::string error; }; auto* dlg = new QDialog(this); - dlg->setWindowTitle("Structure spider"); + dlg->setWindowTitle(tr("Structure spider")); dlg->resize(520, 420); dlg->setAttribute(Qt::WA_DeleteOnClose); trackTargetWindow(dlg); auto* v = new QVBoxLayout(dlg); const QString targetText = QString("0x%1").arg((qulonglong)target, 0, 16); - auto* status = new QLabel(QString("Searching for pointer paths to %1...").arg(targetText)); + auto* status = new QLabel(QString(tr("Searching for pointer paths to %1...")).arg(targetText)); status->setWordWrap(true); v->addWidget(status); auto* list = new QListWidget; @@ -762,12 +768,12 @@ void MainWindow::setupMenus() { // a huge target can run out of memory: report either in the window. try { job->paths = job->scanner.scan(*proc, cfg); } catch (const std::exception& e) { job->error = e.what(); } - catch (...) { job->error = "unexpected error"; } + catch (...) { job->error = tr("unexpected error").toStdString(); } job->done = true; }); connect(dlg, &QDialog::finished, job, [job]() { job->scanner.cancel(); }); auto* row = new QHBoxLayout; - auto* dissectBtn = new QPushButton("Dissect structure at target"); + auto* dissectBtn = new QPushButton(tr("Dissect structure at target")); connect(dissectBtn, &QPushButton::clicked, this, [this, target]() { if (!process_) return; auto* sd = new StructureDissector(process_.get(), target, this); @@ -779,11 +785,11 @@ void MainWindow::setupMenus() { sd->show(); }); // Cancel while the scan runs, Close afterwards. - auto* closeBtn = new QPushButton("Cancel"); + auto* closeBtn = new QPushButton(tr("Cancel")); connect(closeBtn, &QPushButton::clicked, dlg, [dlg, job, status]() { if (job->done) { dlg->accept(); return; } job->scanner.cancel(); - status->setText("Cancelling..."); + status->setText(tr("Cancelling...")); }); row->addWidget(dissectBtn); row->addStretch(); row->addWidget(closeBtn); v->addLayout(row); @@ -791,20 +797,20 @@ void MainWindow::setupMenus() { connect(poll, &QTimer::timeout, dlg, [poll, job, status, list, closeBtn, targetText]() { if (!job->done) { if (!job->scanner.wasCancelled()) - status->setText(QString("Searching for pointer paths to %1... %2%") + status->setText(QString(tr("Searching for pointer paths to %1... %2%")) .arg(targetText).arg(int(job->scanner.progress() * 100))); return; } poll->stop(); job->worker.join(); - closeBtn->setText("Close"); + closeBtn->setText(tr("Close")); if (!job->error.empty()) { - status->setText(QString("The pointer scan failed: %1").arg(QString::fromStdString(job->error))); + status->setText(QString(tr("The pointer scan failed: %1")).arg(QString::fromStdString(job->error))); return; } - if (job->scanner.wasCancelled()) { status->setText("Cancelled."); return; } - QString text = QString("%1 pointer path(s) to %2:").arg(job->paths.size()).arg(targetText); - if (job->scanner.truncated()) text += " (the scan stopped at a limit, so some paths may be missing)"; + if (job->scanner.wasCancelled()) { status->setText(tr("Cancelled.")); return; } + QString text = QString(tr("%1 pointer path(s) to %2:")).arg(job->paths.size()).arg(targetText); + if (job->scanner.truncated()) text += " " + tr("(the scan stopped at a limit, so some paths may be missing)"); status->setText(text); for (const auto& p : job->paths) list->addItem(QString::fromStdString(p.toString())); }); @@ -814,10 +820,10 @@ void MainWindow::setupMenus() { // CE frmgroupscanalgoritmgeneratorunit: build a grouped-scan pattern from the // selected scan results (offsets from the first, with each value), for finding // other instances of the same structure. - tools->addAction("Group Scan Generator...", this, [this]() { - if (!process_ || !lastResult_) { QMessageBox::warning(this, "Group Scan Generator", "Run a scan and select results first."); return; } + tools->addAction(tr("Group Scan Generator..."), this, [this]() { + if (!process_ || !lastResult_) { QMessageBox::warning(this, tr("Group Scan Generator"), tr("Run a scan and select results first.")); return; } auto sel = resultsView_->selectionModel()->selectedRows(); - if (sel.size() < 2) { QMessageBox::information(this, "Group Scan Generator", "Select two or more addresses in the scan results first (Ctrl+click or Shift+click). The generator builds a pattern from their offsets and values."); return; } + if (sel.size() < 2) { QMessageBox::information(this, tr("Group Scan Generator"), tr("Select two or more addresses in the scan results first (Ctrl+click or Shift+click). The generator builds a pattern from their offsets and values.")); return; } std::vector addrs; for (const auto& idx : sel) addrs.push_back(resultsModel_->addressAt(idx.row())); std::sort(addrs.begin(), addrs.end()); @@ -830,15 +836,15 @@ void MainWindow::setupMenus() { } QString pattern = parts.join(' '); auto* dlg = new QDialog(this); - dlg->setWindowTitle("Group Scan Generator"); + dlg->setWindowTitle(tr("Group Scan Generator")); auto* v = new QVBoxLayout(dlg); - v->addWidget(new QLabel("Grouped-scan pattern (offset:value, 4-byte):")); + v->addWidget(new QLabel(tr("Grouped-scan pattern (offset:value, 4-byte):"))); auto* edit = new QPlainTextEdit(pattern); edit->setReadOnly(true); v->addWidget(edit); auto* row = new QHBoxLayout; - auto* copyBtn = new QPushButton("Copy"); + auto* copyBtn = new QPushButton(tr("Copy")); connect(copyBtn, &QPushButton::clicked, this, [pattern]() { QApplication::clipboard()->setText(pattern); }); - auto* closeBtn = new QPushButton("Close"); + auto* closeBtn = new QPushButton(tr("Close")); connect(closeBtn, &QPushButton::clicked, dlg, &QDialog::accept); row->addStretch(); row->addWidget(copyBtn); row->addWidget(closeBtn); v->addLayout(row); @@ -846,50 +852,50 @@ void MainWindow::setupMenus() { dlg->show(); }); // CE savedisassemblyfrm: disassemble a From..To range to a text file. - tools->addAction("Save Disassembly...", this, [this]() { - if (!process_) { QMessageBox::warning(this, "Save Disassembly", "Open a process first."); return; } + tools->addAction(tr("Save Disassembly..."), this, [this]() { + if (!process_) { QMessageBox::warning(this, tr("Save Disassembly"), tr("Open a process first.")); return; } bool ok = false; - QString fromS = QInputDialog::getText(this, "Save Disassembly", "From (hex):", + QString fromS = QInputDialog::getText(this, tr("Save Disassembly"), tr("From (hex):"), QLineEdit::Normal, "0", &ok); if (!ok) return; - QString toS = QInputDialog::getText(this, "Save Disassembly", "To (hex):", + QString toS = QInputDialog::getText(this, tr("Save Disassembly"), tr("To (hex):"), QLineEdit::Normal, "0", &ok); if (!ok) return; uintptr_t from = fromS.toULongLong(nullptr, 16), to = toS.toULongLong(nullptr, 16); if (to <= from || to - from > (64u << 20)) { - QMessageBox::warning(this, "Save Disassembly", "Invalid or too-large range."); return; + QMessageBox::warning(this, tr("Save Disassembly"), tr("Invalid or too-large range.")); return; } - if (!process_) { QMessageBox::warning(this, "Save Disassembly", "The process is no longer open."); return; } + if (!process_) { QMessageBox::warning(this, tr("Save Disassembly"), tr("The process is no longer open.")); return; } std::vector buf(to - from); auto r = process_->read(from, buf.data(), buf.size()); - if (!r || *r == 0) { QMessageBox::warning(this, "Save Disassembly", "Could not read that range."); return; } + if (!r || *r == 0) { QMessageBox::warning(this, tr("Save Disassembly"), tr("Could not read that range.")); return; } auto architecture = ce::disassemblerArchFor(*process_, from); if (!architecture) { - QMessageBox::warning(this, "Save Disassembly", QString::fromStdString(architecture.error())); + QMessageBox::warning(this, tr("Save Disassembly"), QString::fromStdString(architecture.error())); return; } ce::Disassembler dis(*architecture); auto insns = dis.disassemble(from, {buf.data(), *r}, 1000000, /*emitDataBytes=*/true); - auto path = QFileDialog::getSaveFileName(this, "Save disassembly", "disasm.asm"); + auto path = QFileDialog::getSaveFileName(this, tr("Save disassembly"), "disasm.asm"); if (path.isEmpty()) return; std::ofstream f(path.toStdString()); for (const auto& i : insns) f << std::hex << i.address << ": " << i.mnemonic << (i.operands.empty() ? "" : " ") << i.operands << "\n"; - QMessageBox::information(this, "Saved", QString("Wrote %1 instructions to %2.") + QMessageBox::information(this, tr("Saved"), QString(tr("Wrote %1 instructions to %2.")) .arg(insns.size()).arg(path)); }); // CE frmMemoryViewExUnit (Graphical Memory View): bytes as pixels. - process->addAction("Wait for process...", this, [this]() { + process->addAction(tr("Wait for process..."), this, [this]() { bool ok = false; - auto name = QInputDialog::getText(this, "Wait for process", - "Name (or part of the name) of the program to wait for.\nCheat Engine opens it as soon as it starts:", + auto name = QInputDialog::getText(this, tr("Wait for process"), + tr("Name (or part of the name) of the program to wait for.\nCheat Engine opens it as soon as it starts:"), QLineEdit::Normal, "", &ok); if (!ok || name.isEmpty()) return; stopProcessWatch(); processWatcher_ = std::make_unique(); auto* status = statusBar(); - status->showMessage(QString("Watching for '%1'…").arg(name)); + status->showMessage(QString(tr("Watching for '%1'…")).arg(name)); const quint64 watch = processWatch_; processWatcher_->start(name.toStdString(), [this, watch](pid_t pid, const std::string& procName) { @@ -898,13 +904,13 @@ void MainWindow::setupMenus() { }, Qt::QueuedConnection); }); }); - auto* snapshotMenu = tools->addMenu("Snapshot"); + auto* snapshotMenu = tools->addMenu(tr("Snapshot")); // Bounded so a large game cannot exhaust memory; a diff reads at most as // much again. static constexpr uint64_t kSnapshotBudget = uint64_t(1) << 30; - snapshotMenu->addAction("Capture writable regions", this, [this]() { + snapshotMenu->addAction(tr("Capture writable regions"), this, [this]() { if (!process_) { - QMessageBox::warning(this, "No process", "Open a process before capturing a snapshot."); + QMessageBox::warning(this, tr("No process"), tr("Open a process before capturing a snapshot.")); return; } ProcessHandle* proc = process_.get(); @@ -914,20 +920,20 @@ void MainWindow::setupMenus() { captured = std::make_shared(Snapshot::capture(*proc, kSnapshotBudget)); }); } catch (const std::exception& e) { - QMessageBox::warning(this, "Snapshot failed", QString::fromStdString(e.what())); + QMessageBox::warning(this, tr("Snapshot failed"), QString::fromStdString(e.what())); return; } snapshot_ = std::move(captured); - QString text = QString("Captured %1 region(s), %2 total.") + QString text = QString(tr("Captured %1 region(s), %2 total.")) .arg(snapshot_->regionCount()).arg(formatByteSize(snapshot_->byteCount())); if (snapshot_->truncated()) - text += QString("\n\nThe snapshot stopped at its %1 limit: writable memory " - "after that point is not included.").arg(formatByteSize(kSnapshotBudget)); - QMessageBox::information(this, "Snapshot captured", text); + text += QString(tr("\n\nThe snapshot stopped at its %1 limit: writable memory " + "after that point is not included.")).arg(formatByteSize(kSnapshotBudget)); + QMessageBox::information(this, tr("Snapshot captured"), text); }); - snapshotMenu->addAction("Diff against current", this, [this]() { + snapshotMenu->addAction(tr("Diff against current"), this, [this]() { if (!process_ || !snapshot_) { - QMessageBox::warning(this, "Need both", "Capture a baseline snapshot first, then open a process."); + QMessageBox::warning(this, tr("Need both"), tr("Capture a baseline snapshot first, then open a process.")); return; } constexpr size_t kShown = 200; @@ -941,17 +947,17 @@ void MainWindow::setupMenus() { diffs = baseline->diff(now, kShown, &changed); }); } catch (const std::exception& e) { - QMessageBox::warning(this, "Diff failed", QString::fromStdString(e.what())); + QMessageBox::warning(this, tr("Diff failed"), QString::fromStdString(e.what())); return; } if (changed == 0) { - QMessageBox::information(this, "Diff", "No byte-level differences detected."); + QMessageBox::information(this, tr("Diff"), tr("No byte-level differences detected.")); return; } // Only the first kShown differences are kept; the count covers all. QString text; - text += QString("%1 byte%2 changed since the baseline.\n\n") - .arg(changed).arg(changed == 1 ? "" : "s"); + text += changed == 1 ? tr("1 byte changed since the baseline.\n\n") + : tr("%1 bytes changed since the baseline.\n\n").arg(changed); for (const auto& d : diffs) { text += QString("0x%1: 0x%2 -> 0x%3\n") .arg(d.address, 16, 16, QChar('0')) @@ -959,10 +965,10 @@ void MainWindow::setupMenus() { .arg(d.after, 2, 16, QChar('0')); } if (changed > diffs.size()) - text += QString("\n(%1 more not shown.)").arg(changed - diffs.size()); + text += QString(tr("\n(%1 more not shown.)")).arg(changed - diffs.size()); auto* dlg = new QDialog(this); - dlg->setWindowTitle("Snapshot Diff"); + dlg->setWindowTitle(tr("Snapshot Diff")); dlg->resize(640, 480); auto* l = new QVBoxLayout(dlg); auto* edit = new QPlainTextEdit(text); @@ -975,73 +981,73 @@ void MainWindow::setupMenus() { dlg->exec(); dlg->deleteLater(); }); - snapshotMenu->addAction("Restore baseline", this, [this]() { + snapshotMenu->addAction(tr("Restore baseline"), this, [this]() { if (!process_ || !snapshot_) { - QMessageBox::warning(this, "Need both", "Capture a baseline snapshot first."); + QMessageBox::warning(this, tr("Need both"), tr("Capture a baseline snapshot first.")); return; } - auto answer = QMessageBox::question(this, "Restore?", - QString("Write the baseline back into the process? This overwrites the " - "captured memory that is still mapped writable (%1 regions, %2).") + auto answer = QMessageBox::question(this, tr("Restore?"), + QString(tr("Write the baseline back into the process? This overwrites the " + "captured memory that is still mapped writable (%1 regions, %2).")) .arg(snapshot_->regionCount()).arg(formatByteSize(snapshot_->byteCount()))); if (answer != QMessageBox::Yes) return; // Both go away if the process exits while the question is open. - if (!process_ || !snapshot_) { QMessageBox::warning(this, "Restore", "The process is no longer open."); return; } + if (!process_ || !snapshot_) { QMessageBox::warning(this, tr("Restore"), tr("The process is no longer open.")); return; } ProcessHandle* proc = process_.get(); std::shared_ptr baseline = snapshot_; uint64_t written = 0; try { runSnapshotTask([&] { written = baseline->restore(*proc); }); } catch (const std::exception& e) { - QMessageBox::warning(this, "Restore failed", QString::fromStdString(e.what())); + QMessageBox::warning(this, tr("Restore failed"), QString::fromStdString(e.what())); return; } - QString text = QString("Wrote %1 bytes back into the process.").arg(written); + QString text = QString(tr("Wrote %1 bytes back into the process.")).arg(written); if (written < baseline->byteCount()) - text += QString("\n\n%1 bytes were skipped: that memory is no longer mapped, " - "or is now read-only or executable.").arg(baseline->byteCount() - written); - QMessageBox::information(this, "Restored", text); + text += QString(tr("\n\n%1 bytes were skipped: that memory is no longer mapped, " + "or is now read-only or executable.")).arg(baseline->byteCount() - written); + QMessageBox::information(this, tr("Restored"), text); }); - snapshotMenu->addAction("Save to file...", this, [this]() { + snapshotMenu->addAction(tr("Save to file..."), this, [this]() { if (!snapshot_) { - QMessageBox::warning(this, "No snapshot", "Capture a snapshot first."); + QMessageBox::warning(this, tr("No snapshot"), tr("Capture a snapshot first.")); return; } - auto path = QFileDialog::getSaveFileName(this, "Save snapshot", "snapshot.cesnap", - "Snapshot (*.cesnap);;All (*)"); + auto path = QFileDialog::getSaveFileName(this, tr("Save snapshot"), "snapshot.cesnap", + tr("Snapshot (*.cesnap);;All (*)")); if (path.isEmpty()) return; if (!snapshot_->save(path.toStdString())) { - QMessageBox::warning(this, "Save failed", "Could not write " + path); + QMessageBox::warning(this, tr("Save failed"), tr("Could not write %1").arg(path)); } }); - snapshotMenu->addAction("Load from file...", this, [this]() { - auto path = QFileDialog::getOpenFileName(this, "Load snapshot", "", - "Snapshot (*.cesnap);;All (*)"); + snapshotMenu->addAction(tr("Load from file..."), this, [this]() { + auto path = QFileDialog::getOpenFileName(this, tr("Load snapshot"), "", + tr("Snapshot (*.cesnap);;All (*)")); if (path.isEmpty()) return; auto loaded = std::make_shared(); std::string err; if (!loaded->load(path.toStdString(), &err)) { - QMessageBox::warning(this, "Load failed", QString::fromStdString(err)); + QMessageBox::warning(this, tr("Load failed"), QString::fromStdString(err)); return; } snapshot_ = std::move(loaded); - QMessageBox::information(this, "Snapshot loaded", - QString("Loaded %1 region(s), %2 bytes.") + QMessageBox::information(this, tr("Snapshot loaded"), + QString(tr("Loaded %1 region(s), %2 bytes.")) .arg(snapshot_->regionCount()).arg(snapshot_->byteCount())); }); tools->addSeparator(); - tools->addAction("Overlay...", this, &MainWindow::showOverlayDialog); - tools->addAction("Speedhack...", this, [this]() { + tools->addAction(tr("Overlay..."), this, &MainWindow::showOverlayDialog); + tools->addAction(tr("Speedhack..."), this, [this]() { auto* dlg = new QDialog(this); - dlg->setWindowTitle("Speedhack"); + dlg->setWindowTitle(tr("Speedhack")); auto* layout = new QVBoxLayout(dlg); layout->setSpacing(10); - auto* intro = new QLabel("Scales the time the game sees. Apply loads the speedhack " - "library into the process the first time."); + auto* intro = new QLabel(tr("Scales the time the game sees. Apply loads the speedhack " + "library into the process the first time.")); intro->setWordWrap(true); intro->setProperty("secondary", true); auto* speedRow = new QHBoxLayout; - speedRow->addWidget(new QLabel("Speed")); + speedRow->addWidget(new QLabel(tr("Speed"))); speedRow->addStretch(); auto* speedValue = new QLabel("1.0×"); speedValue->setStyleSheet(QString("font-size: %1pt; font-weight: 600;") @@ -1060,10 +1066,10 @@ void MainWindow::setupMenus() { presets->addWidget(b); } // Status of the last Apply: injection errors or the speed now in effect. - auto* label = new QLabel(process_ ? "Not applied yet." : "Open a process first."); + auto* label = new QLabel(process_ ? tr("Not applied yet.") : tr("Open a process first.")); label->setProperty("secondary", true); label->setWordWrap(true); - auto* applyBtn = new QPushButton("Apply"); + auto* applyBtn = new QPushButton(tr("Apply")); applyBtn->setProperty("primary", true); applyBtn->setEnabled(process_ != nullptr); connect(applyBtn, &QPushButton::clicked, [this, slider, label]() { @@ -1074,7 +1080,7 @@ void MainWindow::setupMenus() { // The speedhack is injected locally and speaks through this // machine's /dev/shm: a remote PID names some other process here. const auto localPid = ce::localTargetPid(process_.get()); - if (!localPid) { label->setText("The speedhack works on local processes only."); return; } + if (!localPid) { label->setText(tr("The speedhack works on local processes only.")); return; } pid_t pid = *localPid; bool loaded = false; std::ifstream maps("/proc/" + std::to_string(pid) + "/maps"); @@ -1089,7 +1095,7 @@ void MainWindow::setupMenus() { // directory), or next to the binary in a build tree. const std::string so = ce::findCompanionLibrary("libspeedhack.so"); if (so.empty()) { - label->setText("Inject failed: libspeedhack.so is not installed next to Cheat Engine."); + label->setText(tr("Inject failed: libspeedhack.so is not installed next to Cheat Engine.")); return; } // A containerised game (Flatpak, Steam runtime) may see @@ -1098,12 +1104,12 @@ void MainWindow::setupMenus() { if (auto task = ce::os::processMemoryTask(pid)) targetSo = ce::os::targetVisiblePath(*task, so); auto r = ce::os::injectLibrary(*process_, luaResolver_, targetSo); if (!r) { - label->setText(QString("Inject failed: %1") + label->setText(QString(tr("Inject failed: %1")) .arg(QString::fromStdString(r.error()))); return; // don't set a speed the plugin can't read } } - label->setText(QString("Active: the game runs at %1×.").arg(speed, 0, 'f', 1)); + label->setText(QString(tr("Active: the game runs at %1×.")).arg(speed, 0, 'f', 1)); } // Match the plugin side (plugins/speedhack.c): O_NOFOLLOW refuses a // symlink-swapped /dev/shm entry and mode 0600 keeps the channel @@ -1137,13 +1143,13 @@ void MainWindow::setupMenus() { }); // ── Help (CE verbatim) ── - auto* help = menuBar()->addMenu("&Help"); - help->addAction("Cheat Engine Help", this, [this]() { - openHelpDoc("README.md", "Cheat Engine for Linux: Help", + auto* help = menuBar()->addMenu(tr("&Help")); + help->addAction(tr("Cheat Engine Help"), this, [this]() { + openHelpDoc("README.md", tr("Cheat Engine for Linux: Help"), "https://github.com/wleeaf/cheat-engine-linux#readme"); }); - help->addAction("Lua documentation", this, [this]() { - openHelpDoc("docs/SCRIPTING.md", "Lua / Scripting documentation", + help->addAction(tr("Lua documentation"), this, [this]() { + openHelpDoc("docs/SCRIPTING.md", tr("Lua / Scripting documentation"), "https://github.com/wleeaf/cheat-engine-linux/blob/main/docs/SCRIPTING.md"); }); help->addSeparator(); @@ -1151,22 +1157,22 @@ void MainWindow::setupMenus() { auto launchTutorial = [this](const QString& program) { const QString path = QCoreApplication::applicationDirPath() + "/" + program; if (!QProcess::startDetached(path, {})) - QMessageBox::warning(this, "Tutorial not found", - QString("Could not start %1. It is installed next to cheatengine.").arg(path)); + QMessageBox::warning(this, tr("Tutorial not found"), + QString(tr("Could not start %1. It is installed next to cheatengine.")).arg(path)); }; - help->addAction("Cheat Engine Tutorial...", this, [launchTutorial]() { launchTutorial("cetutorial"); }); - help->addAction("Graphical Tutorial...", this, [launchTutorial]() { launchTutorial("cegtutorial"); }); + help->addAction(tr("Cheat Engine Tutorial..."), this, [launchTutorial]() { launchTutorial("cetutorial"); }); + help->addAction(tr("Graphical Tutorial..."), this, [launchTutorial]() { launchTutorial("cegtutorial"); }); help->addSeparator(); - help->addAction("About", this, [this]() { - QMessageBox::about(this, "About Cheat Engine for Linux", - QString("

Cheat Engine for Linux

" + help->addAction(tr("About"), this, [this]() { + QMessageBox::about(this, tr("About Cheat Engine for Linux"), + QString(tr("

Cheat Engine for Linux

" "

Version %1

" "

A memory scanner, debugger and code injection tool for native Linux, " "Wine and Proton programs.

" "

Built with Qt %2, Capstone, Keystone and Lua 5.3.
" "Free and open source under the MIT license.

" "

" - "github.com/wleeaf/cheat-engine-linux

") + "github.com/wleeaf/cheat-engine-linux

")) .arg(CECORE_VERSION, qVersion())); }); } @@ -1207,11 +1213,11 @@ void MainWindow::openHelpDoc(const QString& relPath, const QString& title, const view->document()->setDocumentMargin(12); v->addWidget(view); auto* row = new QHBoxLayout; - auto* online = new QPushButton("Open on GitHub"); + auto* online = new QPushButton(tr("Open on GitHub")); connect(online, &QPushButton::clicked, this, [url]() { QDesktopServices::openUrl(QUrl(url)); }); row->addWidget(online); row->addStretch(); - auto* closeBtn = new QPushButton("Close"); + auto* closeBtn = new QPushButton(tr("Close")); row->addWidget(closeBtn); v->addLayout(row); connect(closeBtn, &QPushButton::clicked, dlg, &QDialog::accept); @@ -1223,18 +1229,18 @@ void MainWindow::openHelpDoc(const QString& relPath, const QString& title, const void MainWindow::showComments() { auto* dlg = new QDialog(this); tableTextWindows_.push_back(dlg); // closed when another table loads - dlg->setWindowTitle("Table Comments"); + dlg->setWindowTitle(tr("Table Comments")); dlg->resize(520, 400); auto* v = new QVBoxLayout(dlg); auto* memo = new QPlainTextEdit; // CE Memo1 memo->setPlainText(tableComment_); - memo->setPlaceholderText("Notes about this cheat table: how to use it, what each entry does, " - "which game version it was made for. They are saved with the table."); + memo->setPlaceholderText(tr("Notes about this cheat table: how to use it, what each entry does, " + "which game version it was made for. They are saved with the table.")); memo->document()->setDocumentMargin(6); v->addWidget(memo); auto* row = new QHBoxLayout; // CE Panel1 row->addStretch(); - auto* closeBtn = new QPushButton("Close"); // CE Button1 + auto* closeBtn = new QPushButton(tr("Close")); // CE Button1 row->addWidget(closeBtn); v->addLayout(row); connect(memo, &QPlainTextEdit::textChanged, this, @@ -1250,22 +1256,22 @@ void MainWindow::showComments() { void MainWindow::showTableLuaScript() { auto* dlg = new QDialog(this); tableTextWindows_.push_back(dlg); // closed when another table loads - dlg->setWindowTitle("Cheat Table Lua Script"); + dlg->setWindowTitle(tr("Cheat Table Lua Script")); dlg->resize(640, 460); auto* v = new QVBoxLayout(dlg); auto* memo = new CodeEditor; memo->setPlainText(tableLuaScript_); - memo->setPlaceholderText("-- Lua that runs when this table is opened; saved with the table."); + memo->setPlaceholderText(tr("-- Lua that runs when this table is opened; saved with the table.")); v->addWidget(memo); auto* row = new QHBoxLayout; - auto* execBtn = new QPushButton("Execute script"); + auto* execBtn = new QPushButton(tr("Execute script")); execBtn->setProperty("primary", true); row->addWidget(execBtn); auto* result = new QLabel; result->setWordWrap(true); row->addWidget(result, 1); - auto* closeBtn = new QPushButton("Close"); + auto* closeBtn = new QPushButton(tr("Close")); row->addWidget(closeBtn); v->addLayout(row); @@ -1280,8 +1286,8 @@ void MainWindow::showTableLuaScript() { auto res = luaEngine_.evalToString(memo->toPlainText().toStdString()); const auto pal = editorPalette(); result->setStyleSheet(QString("color: %1;").arg((res.has_value() ? pal.success : pal.error).name())); - result->setText(res.has_value() ? "Script executed." - : QString("Error: %1").arg(QString::fromStdString(res.error()))); + result->setText(res.has_value() ? tr("Script executed.") + : QString(tr("Error: %1")).arg(QString::fromStdString(res.error()))); }); connect(closeBtn, &QPushButton::clicked, dlg, &QDialog::accept); dlg->setAttribute(Qt::WA_DeleteOnClose); @@ -1305,20 +1311,20 @@ void MainWindow::showAdvancedOptions() { void MainWindow::showOverlayDialog() { QDialog dialog(this); - dialog.setWindowTitle("Overlay"); + dialog.setWindowTitle(tr("Overlay")); auto* layout = new QVBoxLayout(&dialog); layout->setSpacing(10); - auto* intro = new QLabel("A transparent, click-through layer drawn over the whole screen, " - "for use with a game in borderless or windowed mode."); + auto* intro = new QLabel(tr("A transparent, click-through layer drawn over the whole screen, " + "for use with a game in borderless or windowed mode.")); intro->setWordWrap(true); intro->setProperty("secondary", true); layout->addWidget(intro); - auto* osdCheck = new QCheckBox("Status text (process, active cheats, frame rate)"); - auto* crosshairCheck = new QCheckBox("Crosshair in the centre of the screen"); - auto* showButton = new QPushButton("Show Overlay"); + auto* osdCheck = new QCheckBox(tr("Status text (process, active cheats, frame rate)")); + auto* crosshairCheck = new QCheckBox(tr("Crosshair in the centre of the screen")); + auto* showButton = new QPushButton(tr("Show Overlay")); showButton->setProperty("primary", true); - auto* hideButton = new QPushButton("Hide Overlay"); - auto* closeButton = new QPushButton("Close"); + auto* hideButton = new QPushButton(tr("Hide Overlay")); + auto* closeButton = new QPushButton(tr("Close")); osdCheck->setChecked(!overlayWindow_ || overlayWindow_->osdEnabled()); crosshairCheck->setChecked(!overlayWindow_ || overlayWindow_->crosshairEnabled()); @@ -1387,7 +1393,7 @@ void MainWindow::updateOverlayStatus() { ++active; } - overlayWindow_->setStatusText(QString("%1 | Records %2/%3 active") + overlayWindow_->setStatusText(QString(tr("%1 | Records %2/%3 active")) .arg(processLabel_->text()) .arg(active) .arg(total)); @@ -1411,7 +1417,7 @@ void MainWindow::setupUi() { auto* processBar = new QHBoxLayout; auto* openBtn = new QPushButton; openBtn->setFixedSize(38, 38); - openBtn->setToolTip("Open process"); + openBtn->setToolTip(tr("Open process")); // Remove the default button padding so the icon fills the button instead of // being clipped by the frame. openBtn->setStyleSheet("QPushButton { padding: 0px; }"); @@ -1437,7 +1443,7 @@ void MainWindow::setupUi() { openBtn->setIcon(QIcon(btnIcon.scaled(24, 24, Qt::KeepAspectRatio, Qt::SmoothTransformation))); openBtn->setIconSize(QSize(24, 24)); connect(openBtn, &QPushButton::clicked, this, &MainWindow::onOpenProcess); - processLabel_ = new QLabel("No process selected"); + processLabel_ = new QLabel(tr("No process selected")); processLabel_->setObjectName("processLabel"); processLabel_->setStyleSheet(QString("font-weight: 600; font-size: %1pt;") .arg(QApplication::font().pointSizeF() * 1.1, 0, 'f', 1)); @@ -1520,19 +1526,19 @@ void MainWindow::setupUi() { QMenu menu(this); // CE foundlistpopup order. auto* addAct = menu.addAction(sel.size() > 1 - ? QString("Add %1 selected addresses to the address list").arg(sel.size()) - : QString("Add selected address to the address list")); - auto* browseAct = menu.addAction("Browse this memory region"); - auto* disasmAct = menu.addAction("Disassemble this memory region"); - auto* copyAct = menu.addAction(sel.size() > 1 ? QString("Copy %1 addresses").arg(sel.size()) - : QString("Copy address")); + ? QString(tr("Add %1 selected addresses to the address list")).arg(sel.size()) + : QString(tr("Add selected address to the address list"))); + auto* browseAct = menu.addAction(tr("Browse this memory region")); + auto* disasmAct = menu.addAction(tr("Disassemble this memory region")); + auto* copyAct = menu.addAction(sel.size() > 1 ? QString(tr("Copy %1 addresses")).arg(sel.size()) + : QString(tr("Copy address"))); copyAct->setShortcut(QKeySequence::Copy); auto* removeAct = menu.addAction(sel.size() > 1 - ? QString("Remove %1 selected addresses from the results").arg(sel.size()) - : QString("Remove selected address from the results")); + ? QString(tr("Remove %1 selected addresses from the results")).arg(sel.size()) + : QString(tr("Remove selected address from the results"))); menu.addSeparator(); - auto* accAct = menu.addAction("Find out what accesses this address"); - auto* wrAct = menu.addAction("Find out what writes to this address"); + auto* accAct = menu.addAction(tr("Find out what accesses this address")); + auto* wrAct = menu.addAction(tr("Find out what writes to this address")); QAction* picked = menu.exec(resultsView_->viewport()->mapToGlobal(pos)); if (!picked) return; const uintptr_t firstAddr = resultsModel_->addressAt(sel.first().row()); @@ -1562,7 +1568,7 @@ void MainWindow::setupUi() { std::unique_ptr pruned; try { pruned = ce::pruneScanResult(*lastResult_, lastResultValueSize_, mask); } catch (const std::exception& error) { - QMessageBox::warning(this, "Remove from results", QString::fromStdString(error.what())); + QMessageBox::warning(this, tr("Remove from results"), QString::fromStdString(error.what())); return; } resultsModel_->setResult(pruned.get(), lastResultType_, lastResultValueSize_, resultsModel_->stringEncoding()); @@ -1595,27 +1601,27 @@ void MainWindow::setupUi() { leftLayout->addWidget(resultsView_); auto* foundRow = new QHBoxLayout; - foundLabel_ = new QLabel("Found: 0"); + foundLabel_ = new QLabel(tr("Found: 0")); foundRow->addWidget(foundLabel_); foundRow->addStretch(); - auto* resHexCheck = new QCheckBox("Hex"); - resHexCheck->setToolTip("Show result values in hexadecimal"); + auto* resHexCheck = new QCheckBox(tr("Hex")); + resHexCheck->setToolTip(tr("Show result values in hexadecimal")); connect(resHexCheck, &QCheckBox::toggled, this, [this](bool on) { resultsModel_->setDisplayHex(on); }); foundRow->addWidget(resHexCheck); leftLayout->addLayout(foundRow); auto* leftBtns = new QHBoxLayout; - auto* memViewBtn = new QPushButton("Memory View"); + auto* memViewBtn = new QPushButton(tr("Memory View")); memViewBtn->setObjectName("memoryViewButton"); - memViewBtn->setToolTip("Browse the selected process's memory"); + memViewBtn->setToolTip(tr("Browse the selected process's memory")); connect(memViewBtn, &QPushButton::clicked, this, &MainWindow::onMemoryView); - auto* addAddrBtn = new QPushButton("Add Address"); + auto* addAddrBtn = new QPushButton(tr("Add Address")); connect(addAddrBtn, &QPushButton::clicked, this, [this]() { // CE uses formAddressChange for both add and edit. ce::gui::ChangeAddressDialog dlg("", mapValueType(valueTypeCombo_->currentIndex()), false, 1, this, /*showSigned=*/true, process_.get()); openAddressDialog_ = &dlg; - dlg.setWindowTitle("Add address"); + dlg.setWindowTitle(tr("Add address")); if (dlg.exec() != QDialog::Accepted) return; auto text = dlg.address(); if (text.isEmpty()) return; @@ -1633,11 +1639,11 @@ void MainWindow::setupUi() { [](QChar c){ return std::isxdigit(c.toLatin1()); }); QString expr = plainHex ? QString() : trimmed; // Every field of the dialog, as Add Address Manually applies them. - const int id = addressListModel_->addEntry(*addr, dlg.valueType(), "Manual entry", expr, (size_t)dlg.length()); + const int id = addressListModel_->addEntry(*addr, dlg.valueType(), tr("Manual entry"), expr, (size_t)dlg.length()); addressListModel_->setHexView(id, dlg.showHex()); addressListModel_->setSigned(id, dlg.isSigned()); } else - QMessageBox::warning(this, "Add Address", QString("Could not resolve \"%1\".").arg(text.trimmed())); + QMessageBox::warning(this, tr("Add Address"), QString(tr("Could not resolve \"%1\".")).arg(text.trimmed())); }); leftBtns->addWidget(memViewBtn); leftBtns->addWidget(addAddrBtn); @@ -1651,30 +1657,30 @@ void MainWindow::setupUi() { // Value input auto* valueLayout = new QHBoxLayout; - valueLayout->addWidget(new QLabel("Value:")); + valueLayout->addWidget(new QLabel(tr("Value:"))); scanValueEdit_ = new QLineEdit; scanValueEdit_->setObjectName("scanValue"); valueLayout->addWidget(scanValueEdit_); // Second bound, only shown for "Value between..." scans. - betweenAndLabel_ = new QLabel("and"); + betweenAndLabel_ = new QLabel(tr("and")); betweenAndLabel_->setVisible(false); valueLayout->addWidget(betweenAndLabel_); scanValue2Edit_ = new QLineEdit; scanValue2Edit_->setObjectName("scanValueUpper"); scanValue2Edit_->setVisible(false); valueLayout->addWidget(scanValue2Edit_); - hexCheck_ = new QCheckBox("Hex"); - hexCheck_->setToolTip("Interpret integer scan values as hexadecimal"); + hexCheck_ = new QCheckBox(tr("Hex")); + hexCheck_->setToolTip(tr("Interpret integer scan values as hexadecimal")); valueLayout->addWidget(hexCheck_); rightLayout->addLayout(valueLayout); // Scan type scanTypeCombo_ = new QComboBox; scanTypeCombo_->setObjectName("scanComparison"); - scanTypeCombo_->addItems({"Exact Value", "Bigger than...", "Smaller than...", - "Value between...", "Unknown initial value", "Increased value", - "Decreased value", "Changed value", "Unchanged value", "Same as first scan", - "Increased value by...", "Decreased value by..."}); + scanTypeCombo_->addItems({tr("Exact Value"), tr("Bigger than..."), tr("Smaller than..."), + tr("Value between..."), tr("Unknown initial value"), tr("Increased value"), + tr("Decreased value"), tr("Changed value"), tr("Unchanged value"), tr("Same as first scan"), + tr("Increased value by..."), tr("Decreased value by...")}); rightLayout->addWidget(scanTypeCombo_); // Reveal the second value box only for a "Value between..." scan, and grey out @@ -1710,20 +1716,20 @@ void MainWindow::setupUi() { // Names match the cheat-table Type column / Change-address dialog (ce::valueTypeName) // and CE, so a type reads the same in the scanner and the address list. mapValueType // keys on the index, not the text, so the labels are free to be canonical. - valueTypeCombo_->addItems({"Byte", "2 Bytes", "4 Bytes", "8 Bytes", "Float", "Double", "String", "Unicode String", "Array of byte", "Binary", "All", "Pointer", "Grouped", "Custom"}); + valueTypeCombo_->addItems({tr("Byte"), tr("2 Bytes"), tr("4 Bytes"), tr("8 Bytes"), tr("Float"), tr("Double"), tr("String"), tr("Unicode String"), tr("Array of byte"), tr("Binary"), tr("All"), tr("Pointer"), tr("Grouped"), tr("Custom")}); valueTypeCombo_->setCurrentIndex(2); // 4 Bytes default rightLayout->addWidget(valueTypeCombo_); auto* floatLayout = new QHBoxLayout; floatRoundingCombo_ = new QComboBox; floatRoundingCombo_->setObjectName("scanRounding"); - floatRoundingCombo_->setToolTip("Exact uses stored precision. Rounded uses the precision of your input. Truncated compares integer parts. Extreme uses a tolerance."); - floatRoundingCombo_->addItems({"Exact", "Rounded", "Truncated", "Extreme"}); + floatRoundingCombo_->setToolTip(tr("Exact uses stored precision. Rounded uses the precision of your input. Truncated compares integer parts. Extreme uses a tolerance.")); + floatRoundingCombo_->addItems({tr("Exact"), tr("Rounded"), tr("Truncated"), tr("Extreme")}); floatRoundingCombo_->setCurrentIndex(1); // CE defaults float scans to Rounded floatToleranceEdit_ = new QLineEdit; floatToleranceEdit_->setObjectName("scanTolerance"); - floatToleranceEdit_->setToolTip("Absolute difference allowed in Extreme mode. Leave blank or use 0 for automatic tolerance."); - floatToleranceEdit_->setPlaceholderText("Tolerance"); + floatToleranceEdit_->setToolTip(tr("Absolute difference allowed in Extreme mode. Leave blank or use 0 for automatic tolerance.")); + floatToleranceEdit_->setPlaceholderText(tr("Tolerance")); floatToleranceEdit_->setValidator(new QDoubleValidator(0.0, std::numeric_limits::max(), 4096, floatToleranceEdit_)); if (double tolerance = QSettings().value("scan/floatTolerance", 0.0).toDouble(); tolerance > 0) floatToleranceEdit_->setText(QString::number(tolerance, 'g', 12)); @@ -1739,16 +1745,16 @@ void MainWindow::setupUi() { floatRoundingCombo_->setVisible(isFloat); floatToleranceEdit_->setVisible(isFloat && floatRoundingCombo_->currentIndex() == 3); // Type-aware placeholder to guide what to type (AOB wildcards, etc.). - const char* ph = "e.g. 100"; + QString ph = tr("e.g. 100"); switch (vt) { case ValueType::Float: - case ValueType::Double: ph = "e.g. 3.14"; break; + case ValueType::Double: ph = tr("e.g. 3.14"); break; case ValueType::String: - case ValueType::UnicodeString: ph = "text to find"; break; - case ValueType::ByteArray: ph = "e.g. 48 8B ?? 05"; break; - case ValueType::Binary: ph = "e.g. 0110??01"; break; - case ValueType::Pointer: ph = "e.g. 0x7fabc000"; break; - default: ph = "e.g. 100"; break; + case ValueType::UnicodeString: ph = tr("text to find"); break; + case ValueType::ByteArray: ph = tr("e.g. 48 8B ?? 05"); break; + case ValueType::Binary: ph = tr("e.g. 0110??01"); break; + case ValueType::Pointer: ph = tr("e.g. 0x7fabc000"); break; + default: ph = tr("e.g. 100"); break; } scanValueEdit_->setPlaceholderText(ph); }; @@ -1760,13 +1766,13 @@ void MainWindow::setupUi() { // Buttons auto* btnLayout = new QHBoxLayout; - firstScanBtn_ = new QPushButton("First Scan"); + firstScanBtn_ = new QPushButton(tr("First Scan")); // Accent "primary action" styling comes from the theme (QPushButton#primaryButton). firstScanBtn_->setObjectName("primaryButton"); - nextScanBtn_ = new QPushButton("Next Scan"); + nextScanBtn_ = new QPushButton(tr("Next Scan")); nextScanBtn_->setObjectName("nextScanButton"); nextScanBtn_->setEnabled(false); - undoScanBtn_ = new QPushButton("Undo Scan"); + undoScanBtn_ = new QPushButton(tr("Undo Scan")); undoScanBtn_->setEnabled(false); connect(firstScanBtn_, &QPushButton::clicked, this, &MainWindow::onFirstScan); connect(nextScanBtn_, &QPushButton::clicked, this, &MainWindow::onNextScan); @@ -1785,14 +1791,14 @@ void MainWindow::setupUi() { rightLayout->addWidget(undoScanBtn_); // Scan options group - auto* optGroup = new QGroupBox("Memory Scan Options"); + auto* optGroup = new QGroupBox(tr("Memory Scan Options")); auto* optLayout = new QGridLayout(optGroup); - optLayout->addWidget(new QLabel("From:"), 0, 0); + optLayout->addWidget(new QLabel(tr("From:")), 0, 0); fromAddressEdit_ = new QLineEdit("0000000000"); fromAddressEdit_->setObjectName("scanFrom"); fromAddressEdit_->setFont(monospaceFont()); optLayout->addWidget(fromAddressEdit_, 0, 1); - optLayout->addWidget(new QLabel("To:"), 1, 0); + optLayout->addWidget(new QLabel(tr("To:")), 1, 0); toAddressEdit_ = new QLineEdit(QString::number(UINTPTR_MAX,16)); toAddressEdit_->setObjectName("scanTo"); toAddressEdit_->setFont(monospaceFont()); @@ -1800,19 +1806,19 @@ void MainWindow::setupUi() { // Tri-state like CE: checked = must have the protection, unchecked = must NOT, // partial (grey) = don't care. Writable defaults to "must" (values live in // writable memory); Executable defaults to "don't care". - writableCheck_ = new QCheckBox("Writable"); + writableCheck_ = new QCheckBox(tr("Writable")); writableCheck_->setTristate(true); writableCheck_->setCheckState(Qt::Checked); - writableCheck_->setToolTip("Checked: only writable regions. Unchecked: only " - "non-writable. Grey: any (don't care)."); + writableCheck_->setToolTip(tr("Checked: only writable regions. Unchecked: only " + "non-writable. Grey: any (don't care).")); optLayout->addWidget(writableCheck_, 2, 0, 1, 2); - executableCheck_ = new QCheckBox("Executable"); + executableCheck_ = new QCheckBox(tr("Executable")); executableCheck_->setTristate(true); executableCheck_->setCheckState(Qt::PartiallyChecked); - executableCheck_->setToolTip("Checked: only executable regions. Unchecked: only " - "non-executable. Grey: any (don't care)."); + executableCheck_->setToolTip(tr("Checked: only executable regions. Unchecked: only " + "non-executable. Grey: any (don't care).")); optLayout->addWidget(executableCheck_, 3, 0, 1, 2); - fastScanCheck_ = new QCheckBox("Fast Scan"); + fastScanCheck_ = new QCheckBox(tr("Fast Scan")); fastScanCheck_->setChecked(true); optLayout->addWidget(fastScanCheck_, 4, 0); alignEdit_ = new QLineEdit("4"); @@ -1845,7 +1851,7 @@ void MainWindow::setupUi() { valueTypeCombo_->setCurrentIndex(kSettingsToScanner[dvt]); } - percentCheck_ = new QCheckBox("Compare by %"); + percentCheck_ = new QCheckBox(tr("Compare by %")); optLayout->addWidget(percentCheck_, 5, 0); percentValueEdit_ = new QLineEdit("10"); percentValueEdit_->setFixedWidth(68); @@ -1853,7 +1859,7 @@ void MainWindow::setupUi() { percentValueEdit_->setValidator(new QDoubleValidator(0.0, 1000000.0, 4, percentValueEdit_)); optLayout->addWidget(percentValueEdit_, 5, 1); - auto* percent2Label = new QLabel("Percent max:"); + auto* percent2Label = new QLabel(tr("Percent max:")); optLayout->addWidget(percent2Label, 6, 0); percentValue2Edit_ = new QLineEdit("20"); percentValue2Edit_->setFixedWidth(68); @@ -1861,7 +1867,7 @@ void MainWindow::setupUi() { percentValue2Edit_->setValidator(new QDoubleValidator(0.0, 1000000.0, 4, percentValue2Edit_)); optLayout->addWidget(percentValue2Edit_, 6, 1); - caseSensitiveCheck_ = new QCheckBox("Case sensitive (text)"); + caseSensitiveCheck_ = new QCheckBox(tr("Case sensitive (text)")); caseSensitiveCheck_->setChecked(true); // string scans default to case-sensitive optLayout->addWidget(caseSensitiveCheck_, 8, 0, 1, 2); @@ -1874,14 +1880,14 @@ void MainWindow::setupUi() { auto* memRow = new QWidget; auto* memLayout = new QHBoxLayout(memRow); memLayout->setContentsMargins(0, 0, 0, 0); - memPrivateCheck_ = new QCheckBox("Private"); - memPrivateCheck_->setToolTip("Scan MEM_PRIVATE regions (anonymous heap, stack, private maps)"); + memPrivateCheck_ = new QCheckBox(tr("Private")); + memPrivateCheck_->setToolTip(tr("Scan MEM_PRIVATE regions (anonymous heap, stack, private maps)")); memPrivateCheck_->setChecked(s.value("scan/memPrivate", true).toBool()); - memImageCheck_ = new QCheckBox("Image"); - memImageCheck_->setToolTip("Scan MEM_IMAGE regions (loaded executable modules)"); + memImageCheck_ = new QCheckBox(tr("Image")); + memImageCheck_->setToolTip(tr("Scan MEM_IMAGE regions (loaded executable modules)")); memImageCheck_->setChecked(s.value("scan/memImage", true).toBool()); - memMappedCheck_ = new QCheckBox("Mapped"); - memMappedCheck_->setToolTip("Scan MEM_MAPPED regions (other file-backed maps)"); + memMappedCheck_ = new QCheckBox(tr("Mapped")); + memMappedCheck_->setToolTip(tr("Scan MEM_MAPPED regions (other file-backed maps)")); memMappedCheck_->setChecked(s.value("scan/memMapped", true).toBool()); memLayout->addWidget(memPrivateCheck_); memLayout->addWidget(memImageCheck_); @@ -1899,29 +1905,29 @@ void MainWindow::setupUi() { persist("scan/memMapped", memMappedCheck_); } - auto* textEncodingLabel = new QLabel("Text encoding:"); + auto* textEncodingLabel = new QLabel(tr("Text encoding:")); optLayout->addWidget(textEncodingLabel, 7, 0); stringEncodingCombo_ = new QComboBox; stringEncodingCombo_->addItems({"UTF-8", "ISO-8859-1", "CP1252"}); optLayout->addWidget(stringEncodingCombo_, 7, 1); - auto* orderLabel = new QLabel("Data order:"); + auto* orderLabel = new QLabel(tr("Data order:")); scanByteOrderCombo_ = new QComboBox; scanByteOrderCombo_->setObjectName("scanByteOrder"); - scanByteOrderCombo_->addItem("Auto (target)", static_cast(ce::ByteOrder::Unknown)); - scanByteOrderCombo_->addItem("Little-endian", static_cast(ce::ByteOrder::Little)); - scanByteOrderCombo_->addItem("Big-endian", static_cast(ce::ByteOrder::Big)); - scanByteOrderCombo_->setToolTip("Choose the encoding of the data being scanned. Auto uses the selected target's data order."); + scanByteOrderCombo_->addItem(tr("Auto (target)"), static_cast(ce::ByteOrder::Unknown)); + scanByteOrderCombo_->addItem(tr("Little-endian"), static_cast(ce::ByteOrder::Little)); + scanByteOrderCombo_->addItem(tr("Big-endian"), static_cast(ce::ByteOrder::Big)); + scanByteOrderCombo_->setToolTip(tr("Choose the encoding of the data being scanned. Auto uses the selected target's data order.")); orderLabel->setBuddy(scanByteOrderCombo_); optLayout->addWidget(orderLabel, 10, 0); optLayout->addWidget(scanByteOrderCombo_, 10, 1); - auto* widthLabel = new QLabel("Pointer size:"); + auto* widthLabel = new QLabel(tr("Pointer size:")); scanPointerWidthCombo_ = new QComboBox; scanPointerWidthCombo_->setObjectName("scanPointerWidth"); - scanPointerWidthCombo_->addItem("Auto (target)", 0); - scanPointerWidthCombo_->addItem("4 bytes", 4); - scanPointerWidthCombo_->addItem("8 bytes", 8); - scanPointerWidthCombo_->setToolTip("Size of encoded pointers for Pointer, Grouped and Custom scans. A different saved record size requires a New Scan."); + scanPointerWidthCombo_->addItem(tr("Auto (target)"), 0); + scanPointerWidthCombo_->addItem(tr("4 bytes"), 4); + scanPointerWidthCombo_->addItem(tr("8 bytes"), 8); + scanPointerWidthCombo_->setToolTip(tr("Size of encoded pointers for Pointer, Grouped and Custom scans. A different saved record size requires a New Scan.")); widthLabel->setBuddy(scanPointerWidthCombo_); optLayout->addWidget(widthLabel, 11, 0); optLayout->addWidget(scanPointerWidthCombo_, 11, 1); @@ -2016,9 +2022,9 @@ void MainWindow::setupUi() { connect(addressListModel_, &AddressListModel::valueReverted, this, [this](uintptr_t addr, const QString& wrote, const QString& now) { statusBar()->showMessage( - QString("Value at 0x%1 was reverted (wrote %2, now %3). It looks protected: " + QString(tr("Value at 0x%1 was reverted (wrote %2, now %3). It looks protected: " "right-click the entry and choose \"Find what writes\" to locate the " - "code that overwrites it.").arg(addr, 0, 16).arg(wrote).arg(now), 9000); + "code that overwrites it.")).arg(addr, 0, 16).arg(wrote).arg(now), 9000); }); updateTableHint(); // System-wide hotkeys (X11): fire while the game is focused, like CE. The @@ -2115,12 +2121,12 @@ void MainWindow::setupUi() { // "Edit script..." for a single auto-assembler entry — it has no // address to edit, so this opens the script in the AA editor. if (selected.size() == 1 && addressListModel_->isScriptEntry(firstRow)) { - menu.addAction("Edit script...", [this, first]() { if (first.row() >= 0) editScriptEntry(first.row()); }); + menu.addAction(tr("Edit script..."), [this, first]() { if (first.row() >= 0) editScriptEntry(first.row()); }); menu.addSeparator(); } else if (selected.size() == 1 && firstRow < (int)addressListModel_->entries().size() && !addressListModel_->entries()[firstRow].isGroup) { // CE "Change address" (formAddressChangeUnit): edit address/type/flags. - menu.addAction("Change address...", [this, first]() { + menu.addAction(tr("Change address..."), [this, first]() { const int firstRow = first.row(); if (firstRow < 0 || firstRow >= (int)addressListModel_->entries().size()) return; // The dialog's event loop runs table scripts that can add or @@ -2155,71 +2161,71 @@ void MainWindow::setupUi() { } // CE PopupMenu2: Cut / Copy / Paste (PasteTableentryFRM). - menu.addAction("Cut", this, [this]() { onCopyAddresses(); onDeleteAddresses(); }, QKeySequence::Cut); - menu.addAction("Copy", this, &MainWindow::onCopyAddresses, QKeySequence::Copy); + menu.addAction(tr("Cut"), this, [this]() { onCopyAddresses(); onDeleteAddresses(); }, QKeySequence::Cut); + menu.addAction(tr("Copy"), this, &MainWindow::onCopyAddresses, QKeySequence::Copy); // The same paste as Ctrl+V: every field, groups, scripts and CE XML. - menu.addAction("Paste", this, &MainWindow::onPasteAddresses, QKeySequence::Paste); + menu.addAction(tr("Paste"), this, &MainWindow::onPasteAddresses, QKeySequence::Paste); // Copy an individual field of the first selected entry to the clipboard. if (firstRow >= 0 && firstRow < (int)ents0.size()) { const auto& fe = ents0[firstRow]; - auto* copyMenu = menu.addMenu("Copy field"); - copyMenu->addAction("Description", [fe]() { + auto* copyMenu = menu.addMenu(tr("Copy field")); + copyMenu->addAction(tr("Description"), [fe]() { QApplication::clipboard()->setText(fe.description); }); if (!fe.isGroup && fe.autoAsmScript.isEmpty()) { - copyMenu->addAction("Address", [fe]() { + copyMenu->addAction(tr("Address"), [fe]() { QApplication::clipboard()->setText( fe.addressExpr.isEmpty() ? QString("0x%1").arg(fe.address, 0, 16) : fe.addressExpr); }); - copyMenu->addAction("Value", [fe]() { + copyMenu->addAction(tr("Value"), [fe]() { QApplication::clipboard()->setText(fe.currentValue); }); } if (!fe.autoAsmScript.isEmpty()) - copyMenu->addAction("Script", [fe]() { + copyMenu->addAction(tr("Script"), [fe]() { QApplication::clipboard()->setText(fe.autoAsmScript); }); } - menu.addAction("Delete", this, &MainWindow::onDeleteAddresses, QKeySequence::Delete); + menu.addAction(tr("Delete"), this, &MainWindow::onDeleteAddresses, QKeySequence::Delete); menu.addSeparator(); // Rarely used items sit in submenus so the menu stays one column at // large text sizes (it wrapped into two at 130 DPI). - QMenu* arrangeMenu = menu.addMenu("Arrange"); - arrangeMenu->addAction("Indent", [this, selected]() { + QMenu* arrangeMenu = menu.addMenu(tr("Arrange")); + arrangeMenu->addAction(tr("Indent"), [this, selected]() { QList rows; for (const auto& idx : selected) if (idx.row() >= 0) rows.append(idx.row()); addressListModel_->indentRows(rows); }); - arrangeMenu->addAction("Outdent", [this, selected]() { + arrangeMenu->addAction(tr("Outdent"), [this, selected]() { QList rows; for (const auto& idx : selected) if (idx.row() >= 0) rows.append(idx.row()); addressListModel_->outdentRows(rows); }); // CE "Add the selected records to a new group": wrap the selection under a // fresh group header (prompts for its name, like CE's group description). - arrangeMenu->addAction("Add to new group...", [this, selected]() { + arrangeMenu->addAction(tr("Add to new group..."), [this, selected]() { QList rows; for (const auto& idx : selected) if (idx.row() >= 0) rows.append(idx.row()); if (rows.isEmpty()) return; bool ok = false; - QString name = QInputDialog::getText(this, "New group", - "Group name:", QLineEdit::Normal, "New group", &ok); + QString name = QInputDialog::getText(this, tr("New group"), + tr("Group name:"), QLineEdit::Normal, tr("New group"), &ok); if (!ok) return; - if (name.isEmpty()) name = "New group"; + if (name.isEmpty()) name = tr("New group"); addressListModel_->groupSelectedRows(rows, name); }); if (selected.size() == 1) { - auto* up = arrangeMenu->addAction("Move Up", [this, first]() { if (first.row() >= 0) moveSelectedEntry(first.row(), -1); }); + auto* up = arrangeMenu->addAction(tr("Move Up"), [this, first]() { if (first.row() >= 0) moveSelectedEntry(first.row(), -1); }); up->setShortcut(QKeySequence("Ctrl+Up")); - auto* down = arrangeMenu->addAction("Move Down", [this, first]() { if (first.row() >= 0) moveSelectedEntry(first.row(), +1); }); + auto* down = arrangeMenu->addAction(tr("Move Down"), [this, first]() { if (first.row() >= 0) moveSelectedEntry(first.row(), +1); }); down->setShortcut(QKeySequence("Ctrl+Down")); } menu.addSeparator(); // Set the value of ALL selected entries at once (CE's batch set value). - menu.addAction("Set value...", [this, selected]() { + menu.addAction(tr("Set value..."), [this, selected]() { // Pre-fill with the first selected entry's current value (in its display // format), like CE, so the user edits from the existing value not a blank. QString initial; @@ -2233,9 +2239,10 @@ void MainWindow::setupUi() { } } bool ok = false; - QString v = QInputDialog::getText(this, "Set value", - QString("New value for %1 selected entr%2:") - .arg(selected.size()).arg(selected.size() == 1 ? "y" : "ies"), + const QString valuePrompt = selected.size() == 1 + ? tr("New value for 1 selected entry:") + : tr("New value for %1 selected entries:").arg(selected.size()); + QString v = QInputDialog::getText(this, tr("Set value"), valuePrompt, QLineEdit::Normal, initial, &ok); if (!ok) return; // A group header has no value of its own, so setting a group applies the @@ -2261,36 +2268,36 @@ void MainWindow::setupUi() { }); menu.addSeparator(); - auto* freezeMenu = menu.addMenu("Freeze Mode"); - freezeMenu->addAction("Normal", [this, selected]() { + auto* freezeMenu = menu.addMenu(tr("Freeze Mode")); + freezeMenu->addAction(tr("Normal"), [this, selected]() { for (auto& idx : selected) addressListModel_->setFreezeMode(idx.row(), FreezeMode::Normal); }); - freezeMenu->addAction("Increase Only", [this, selected]() { + freezeMenu->addAction(tr("Increase Only"), [this, selected]() { for (auto& idx : selected) addressListModel_->setFreezeMode(idx.row(), FreezeMode::IncreaseOnly); }); - freezeMenu->addAction("Decrease Only", [this, selected]() { + freezeMenu->addAction(tr("Decrease Only"), [this, selected]() { for (auto& idx : selected) addressListModel_->setFreezeMode(idx.row(), FreezeMode::DecreaseOnly); }); - freezeMenu->addAction("Never Increase", [this, selected]() { + freezeMenu->addAction(tr("Never Increase"), [this, selected]() { for (auto& idx : selected) addressListModel_->setFreezeMode(idx.row(), FreezeMode::NeverIncrease); }); - freezeMenu->addAction("Never Decrease", [this, selected]() { + freezeMenu->addAction(tr("Never Decrease"), [this, selected]() { for (auto& idx : selected) addressListModel_->setFreezeMode(idx.row(), FreezeMode::NeverDecrease); }); // Obfuscated values: declare a codec so the value is displayed decoded and // edited/frozen by its logical value (block 6). Reuses ce::ValueCodec::parse. // How the entry is shown and decoded: format, colour, dropdown, codec. - QMenu* encodingMenu = menu.addMenu("Display"); - encodingMenu->addAction("Set value codec...", [this, selected]() { + QMenu* encodingMenu = menu.addMenu(tr("Display")); + encodingMenu->addAction(tr("Set value codec..."), [this, selected]() { if (selected.isEmpty() || selected.first().row() < 0) return; QString cur = QString::fromStdString( addressListModel_->entryCodecSpec(selected.first().row())); bool ok = false; - QString spec = QInputDialog::getText(this, "Value codec", - "How the game obfuscates this value in memory:\n" + QString spec = QInputDialog::getText(this, tr("Value codec"), + tr("How the game obfuscates this value in memory:\n" "none, xor:0xKEY, add:N, rol:N or ror:N (e.g. xor:0x5A).\n\n" - "The list shows the decoded value; edits and freezes are encoded back.", + "The list shows the decoded value; edits and freezes are encoded back."), QLineEdit::Normal, cur, &ok); if (!ok) return; spec = spec.trimmed(); @@ -2298,8 +2305,8 @@ void MainWindow::setupUi() { if (!spec.isEmpty() && spec.compare("none", Qt::CaseInsensitive) != 0) { auto c = ce::ValueCodec::parse(spec.toStdString()); if (!c) { - QMessageBox::warning(this, "Invalid codec", - "Use none, xor:0xKEY, add:N, rol:N, or ror:N."); + QMessageBox::warning(this, tr("Invalid codec"), + tr("Use none, xor:0xKEY, add:N, rol:N, or ror:N.")); return; } codec = *c; @@ -2307,10 +2314,10 @@ void MainWindow::setupUi() { for (auto& idx : selected) addressListModel_->setEntryCodec(idx.row(), codec); }); - auto* orderMenu = encodingMenu->addMenu("Data order"); - for (auto choice : {std::pair{"Auto (target)", ByteOrder::Unknown}, - std::pair{"Little-endian", ByteOrder::Little}, - std::pair{"Big-endian", ByteOrder::Big}}) { + auto* orderMenu = encodingMenu->addMenu(tr("Data order")); + for (auto choice : {std::pair{tr("Auto (target)"), ByteOrder::Unknown}, + std::pair{tr("Little-endian"), ByteOrder::Little}, + std::pair{tr("Big-endian"), ByteOrder::Big}}) { auto* action = orderMenu->addAction(choice.first); action->setCheckable(true); const auto& entry = addressListModel_->entries()[selected.first().row()]; @@ -2323,8 +2330,8 @@ void MainWindow::setupUi() { } }); } - auto* widthMenu = encodingMenu->addMenu("Pointer size"); - for (auto choice : {std::pair{"Auto (target)", 0}, std::pair{"4 bytes", 4}, std::pair{"8 bytes", 8}}) { + auto* widthMenu = encodingMenu->addMenu(tr("Pointer size")); + for (auto choice : {std::pair{tr("Auto (target)"), 0}, std::pair{tr("4 bytes"), 4}, std::pair{tr("8 bytes"), 8}}) { auto* action = widthMenu->addAction(choice.first); action->setCheckable(true); action->setChecked(addressListModel_->entries()[selected.first().row()].pointerWidth == choice.second); connect(action, &QAction::triggered, this, [this, selected, width=choice.second] { @@ -2337,12 +2344,12 @@ void MainWindow::setupUi() { } menu.addSeparator(); - auto* typeMenu = menu.addMenu("Change type"); - const std::pair typeChoices[] = { - {"Byte", ValueType::Byte}, {"2 Bytes", ValueType::Int16}, - {"4 Bytes", ValueType::Int32}, {"8 Bytes", ValueType::Int64}, - {"Float", ValueType::Float}, {"Double", ValueType::Double}, - {"Pointer", ValueType::Pointer}, + auto* typeMenu = menu.addMenu(tr("Change type")); + const std::pair typeChoices[] = { + {tr("Byte"), ValueType::Byte}, {tr("2 Bytes"), ValueType::Int16}, + {tr("4 Bytes"), ValueType::Int32}, {tr("8 Bytes"), ValueType::Int64}, + {tr("Float"), ValueType::Float}, {tr("Double"), ValueType::Double}, + {tr("Pointer"), ValueType::Pointer}, }; for (const auto& [label, vt] : typeChoices) { typeMenu->addAction(label, [this, selected, vt]() { @@ -2356,7 +2363,7 @@ void MainWindow::setupUi() { bool firstHex = !selected.isEmpty() && selected.first().row() < (int)ents.size() && ents[selected.first().row()].showAsHex; encodingMenu->addSeparator(); - auto* hexAct = encodingMenu->addAction("Show as hexadecimal"); + auto* hexAct = encodingMenu->addAction(tr("Show as hexadecimal")); hexAct->setCheckable(true); hexAct->setChecked(firstHex); connect(hexAct, &QAction::triggered, this, [this, selected, firstHex]() { @@ -2368,7 +2375,7 @@ void MainWindow::setupUi() { // (PIE/ASLR) on the next launch. Only meaningful with a process attached // (module bases known); leaves pointer/expression records untouched. if (process_) { - menu.addAction("Convert to module+offset", [this, selected]() { + menu.addAction(tr("Convert to module+offset"), [this, selected]() { if (!process_) return; const auto modules = process_->modules(); // by value: hold it, don't dangle const auto& entries = addressListModel_->entries(); @@ -2384,10 +2391,11 @@ void MainWindow::setupUi() { addressListModel_->setAddressExpression(e.id, expr); ++converted; } - statusBar()->showMessage( - QString("Converted %1 address%2 to module+offset%3") - .arg(converted).arg(converted == 1 ? "" : "es") - .arg(skipped ? QString(" (%1 skipped)").arg(skipped) : QString()), 4000); + const QString skippedText = skipped ? QString(" ") + tr("(%1 skipped)").arg(skipped) : QString(); + const QString convertedText = converted == 1 + ? tr("Converted 1 address to module+offset%1").arg(skippedText) + : tr("Converted %1 addresses to module+offset%2").arg(converted).arg(skippedText); + statusBar()->showMessage(convertedText, 4000); }); } @@ -2396,7 +2404,7 @@ void MainWindow::setupUi() { // have no address. if (firstRow >= 0 && firstRow < (int)ents0.size() && !ents0[firstRow].isGroup && ents0[firstRow].autoAsmScript.isEmpty()) { - menu.addAction("Browse this memory region", [this, selected]() { + menu.addAction(tr("Browse this memory region"), [this, selected]() { if (!process_ || selected.isEmpty()) return; auto& entries = addressListModel_->entries(); int row = selected.first().row(); @@ -2404,18 +2412,18 @@ void MainWindow::setupUi() { openMemoryView(entries[row].address); // full-featured viewer }); } - menu.addAction("Find what accesses this address", [this, selected]() { + menu.addAction(tr("Find what accesses this address"), [this, selected]() { if (!selected.isEmpty()) startCodeFinder(selected.first().row(), false); }); - menu.addAction("Find what writes to this address", [this, selected]() { + menu.addAction(tr("Find what writes to this address"), [this, selected]() { if (!selected.isEmpty()) startCodeFinder(selected.first().row(), true); }); // CE "Pointer scan for this address". if (firstRow >= 0 && firstRow < (int)ents0.size() && !ents0[firstRow].isGroup && ents0[firstRow].autoAsmScript.isEmpty()) { - menu.addAction("Pointer scan for this address", [this, selected]() { + menu.addAction(tr("Pointer scan for this address"), [this, selected]() { if (!process_ || selected.isEmpty()) return; const int row = selected.first().row(); const auto& entries = addressListModel_->entries(); @@ -2437,7 +2445,7 @@ void MainWindow::setupUi() { }); } // CE "Change Color" — per-record display color. - encodingMenu->addAction("Change Color...", [this, selected]() { + encodingMenu->addAction(tr("Change Color..."), [this, selected]() { if (selected.isEmpty()) return; // The dialog runs an event loop in which table scripts may add or // remove records: keep IDs, not rows or a reference into the list. @@ -2450,20 +2458,20 @@ void MainWindow::setupUi() { for (const auto& idx : selected) if (idx.row() >= 0 && idx.row() < (int)entries.size()) ids.push_back(entries[idx.row()].id); } - QColor c = QColorDialog::getColor(initial, this, "Change record color"); + QColor c = QColorDialog::getColor(initial, this, tr("Change record color")); if (!c.isValid()) return; for (int id : ids) addressListModel_->setColor(id, c.name().toStdString()); }); // CE "Set/Change dropdown selection options". if (selected.size() == 1) { - encodingMenu->addAction("Dropdown options...", [this, first]() { + encodingMenu->addAction(tr("Dropdown options..."), [this, first]() { const int firstRow = first.row(); if (firstRow < 0 || firstRow >= (int)addressListModel_->entries().size()) return; const int id = addressListModel_->entries()[firstRow].id; // rows can move during the dialog bool ok = false; - QString v = QInputDialog::getMultiLineText(this, "Dropdown options", - "The values this entry can be set to from a dropdown, one per line,\n" - "as value:label or just value. For example:\n0:Off\n1:On", + QString v = QInputDialog::getMultiLineText(this, tr("Dropdown options"), + tr("The values this entry can be set to from a dropdown, one per line,\n" + "as value:label or just value. For example:\n0:Off\n1:On"), addressListModel_->dropdownList(firstRow), &ok); if (ok) addressListModel_->setDropdownList(id, v.remove('\r').split('\n', Qt::SkipEmptyParts).join('\n')); @@ -2471,23 +2479,23 @@ void MainWindow::setupUi() { } if (selected.size() == 1) { menu.addSeparator(); - QMenu* hotkeyMenu = menu.addMenu("Hotkeys"); - hotkeyMenu->addAction("Toggle hotkey...", [this, selected]() { + QMenu* hotkeyMenu = menu.addMenu(tr("Hotkeys")); + hotkeyMenu->addAction(tr("Toggle hotkey..."), [this, selected]() { int row = selected.first().row(); const auto& entries = addressListModel_->entries(); if (row < 0 || row >= (int)entries.size()) return; QDialog dialog(this); - dialog.setWindowTitle("Configure Hotkey"); + dialog.setWindowTitle(tr("Configure Hotkey")); auto* layout = new QVBoxLayout(&dialog); auto* label = new QLabel(entries[row].description, &dialog); label->setObjectName("pageTitle"); auto* editor = new QKeySequenceEdit(QKeySequence(entries[row].hotkeyKeys), &dialog); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); - auto* clearButton = buttons->addButton("Clear", QDialogButtonBox::ResetRole); + auto* clearButton = buttons->addButton(tr("Clear"), QDialogButtonBox::ResetRole); - auto* note = new QLabel("Press the key combination that turns this entry on and off. " - "It works while the game has focus.", &dialog); + auto* note = new QLabel(tr("Press the key combination that turns this entry on and off. " + "It works while the game has focus."), &dialog); note->setProperty("secondary", true); note->setWordWrap(true); editor->setMinimumWidth(280); @@ -2508,13 +2516,13 @@ void MainWindow::setupUi() { rebuildValueHotkeys(); // grab the new key (and release a cleared one) now } }); - hotkeyMenu->addAction("Value hotkeys...", [this, selected]() { + hotkeyMenu->addAction(tr("Value hotkeys..."), [this, selected]() { int row = selected.first().row(); const auto& entries = addressListModel_->entries(); if (row < 0 || row >= (int)entries.size()) return; QDialog dialog(this); - dialog.setWindowTitle("Configure Value Hotkeys"); + dialog.setWindowTitle(tr("Configure Value Hotkeys")); auto* layout = new QGridLayout(&dialog); auto* label = new QLabel(entries[row].description, &dialog); label->setObjectName("pageTitle"); @@ -2527,21 +2535,21 @@ void MainWindow::setupUi() { stepEdit->setValidator(new QDoubleValidator(stepEdit)); auto* setEditor = new QKeySequenceEdit(QKeySequence(entries[row].setValueHotkeyKeys), &dialog); auto* setValueEdit = new QLineEdit(entries[row].setValueHotkeyValue, &dialog); - setValueEdit->setPlaceholderText("value to set"); + setValueEdit->setPlaceholderText(tr("value to set")); for (QWidget* field : std::initializer_list{increaseEditor, decreaseEditor, stepEdit, setEditor, setValueEdit}) field->setMinimumWidth(220); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); layout->addWidget(label, 0, 0, 1, 2); - layout->addWidget(new QLabel("Increase", &dialog), 1, 0); + layout->addWidget(new QLabel(tr("Increase"), &dialog), 1, 0); layout->addWidget(increaseEditor, 1, 1); - layout->addWidget(new QLabel("Decrease", &dialog), 2, 0); + layout->addWidget(new QLabel(tr("Decrease"), &dialog), 2, 0); layout->addWidget(decreaseEditor, 2, 1); - layout->addWidget(new QLabel("Step", &dialog), 3, 0); + layout->addWidget(new QLabel(tr("Step"), &dialog), 3, 0); layout->addWidget(stepEdit, 3, 1); - layout->addWidget(new QLabel("Set value hotkey", &dialog), 4, 0); + layout->addWidget(new QLabel(tr("Set value hotkey"), &dialog), 4, 0); layout->addWidget(setEditor, 4, 1); - layout->addWidget(new QLabel("Set to value", &dialog), 5, 0); + layout->addWidget(new QLabel(tr("Set to value"), &dialog), 5, 0); layout->addWidget(setValueEdit, 5, 1); layout->addWidget(buttons, 6, 0, 1, 2); connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); @@ -2564,7 +2572,7 @@ void MainWindow::setupUi() { } menu.addSeparator(); - menu.addAction("Add Address Manually...", [this]() { + menu.addAction(tr("Add Address Manually..."), [this]() { // CE's "Add Address Manually" opens the same formAddressChangeUnit as // "Change address", so a new entry gets its type, flags, length, and an // optional structured pointer in one dialog. @@ -2579,30 +2587,30 @@ void MainWindow::setupUi() { int id; if (numeric && !a.contains('[') && !a.contains('+') && !a.contains(' ')) { id = addressListModel_->addEntry((uintptr_t)v, dlg.valueType(), - "No description", QString(), (size_t)dlg.length()); + tr("No description"), QString(), (size_t)dlg.length()); } else { // Symbolic / module+offset / pointer chain: store the expression so it // re-resolves live, seeding the initial numeric address if resolvable. uintptr_t addr0 = 0; if (auto r = parseAddressExpr(a)) addr0 = *r; id = addressListModel_->addEntry(addr0, dlg.valueType(), - "No description", a, (size_t)dlg.length()); + tr("No description"), a, (size_t)dlg.length()); } addressListModel_->setHexView(id, dlg.showHex()); addressListModel_->setSigned(id, dlg.isSigned()); }); - menu.addAction("Add Group", [this]() { + menu.addAction(tr("Add Group"), [this]() { addressListModel_->addGroup(); }); // With entries selected, Paste is already in the edit group above. - if (selected.isEmpty()) menu.addAction("Paste", this, &MainWindow::onPasteAddresses, QKeySequence::Paste); + if (selected.isEmpty()) menu.addAction(tr("Paste"), this, &MainWindow::onPasteAddresses, QKeySequence::Paste); menu.addSeparator(); // Whole-table actions only on empty space: with an entry under the cursor the // menu is about that entry, and it has to fit one column on a 1080p screen. if (selected.isEmpty()) { - menu.addAction("Enable all", [this]() { addressListModel_->setAllActive(true); }); - menu.addAction("Disable all", [this]() { addressListModel_->setAllActive(false); }); + menu.addAction(tr("Enable all"), [this]() { addressListModel_->setAllActive(true); }); + menu.addAction(tr("Disable all"), [this]() { addressListModel_->setAllActive(false); }); } menu.exec(addressListView_->viewport()->mapToGlobal(pos)); @@ -2722,7 +2730,7 @@ void MainWindow::setupUi() { scanForm->setHorizontalSpacing(6); scanForm->setVerticalSpacing(6); scanForm->setColumnStretch(1, 1); - scanForm->addWidget(new QLabel("Scan Value"), 0, 0, Qt::AlignRight | Qt::AlignVCenter); + scanForm->addWidget(new QLabel(tr("Scan Value")), 0, 0, Qt::AlignRight | Qt::AlignVCenter); auto* valRow = new QHBoxLayout; valRow->setSpacing(4); valRow->addWidget(scanValueEdit_, 1); @@ -2730,16 +2738,16 @@ void MainWindow::setupUi() { valRow->addWidget(scanValue2Edit_, 1); valRow->addWidget(hexCheck_); scanForm->addLayout(valRow, 0, 1); - scanForm->addWidget(new QLabel("Scan Type"), 1, 0, Qt::AlignRight | Qt::AlignVCenter); + scanForm->addWidget(new QLabel(tr("Scan Type")), 1, 0, Qt::AlignRight | Qt::AlignVCenter); scanForm->addWidget(scanTypeCombo_, 1, 1); - scanForm->addWidget(new QLabel("Value Type"), 2, 0, Qt::AlignRight | Qt::AlignVCenter); + scanForm->addWidget(new QLabel(tr("Value Type")), 2, 0, Qt::AlignRight | Qt::AlignVCenter); scanForm->addWidget(valueTypeCombo_, 2, 1); auto* floatRow2 = new QHBoxLayout; floatRow2->setSpacing(4); floatRow2->addWidget(floatRoundingCombo_); floatRow2->addWidget(floatToleranceEdit_); floatRow2->addStretch(); - auto* roundingLabel = new QLabel("Rounding"); + auto* roundingLabel = new QLabel(tr("Rounding")); roundingLabel->setBuddy(floatRoundingCombo_); scanForm->addWidget(roundingLabel, 3, 0, Qt::AlignRight | Qt::AlignVCenter); scanForm->addLayout(floatRow2, 3, 1); @@ -2802,11 +2810,11 @@ void MainWindow::setupUi() { addressListView_->setMinimumHeight(140); auto* bottomBar = new QHBoxLayout; - auto* advBtn = new QPushButton("Advanced Options"); + auto* advBtn = new QPushButton(tr("Advanced Options")); // CE "Advanced Options" = the Code list (AdvancedOptionsUnit): a persistent // list of code addresses from find-what-writes, with disassemble/NOP/restore. connect(advBtn, &QPushButton::clicked, this, &MainWindow::showAdvancedOptions); - auto* extrasBtn = new QPushButton("Table Extras"); + auto* extrasBtn = new QPushButton(tr("Table Extras")); // CE "Table Extras" opens the table notes (Comments) — a free-text memo saved // with the table. (Save/Load now live in the File menu.) connect(extrasBtn, &QPushButton::clicked, this, &MainWindow::showComments); @@ -2857,10 +2865,10 @@ static void grantPtraceAccess(QWidget* parent) { args = {QStringLiteral("/bin/sh"), QStringLiteral("-c"), QStringLiteral("echo 0 > /proc/sys/kernel/yama/ptrace_scope")}; followup = isAppImage - ? QObject::tr("Access granted for this session; re-open the process.\n" + ? QCoreApplication::translate("MainWindow", "Access granted for this session; re-open the process.\n" "(This resets on reboot. Install the .deb, or re-run the " "command, to make it permanent.)") - : QObject::tr("Access granted for this session; re-open the process.\n" + : QCoreApplication::translate("MainWindow", "Access granted for this session; re-open the process.\n" "(This resets on reboot. A capability on this install would " "stop it from finding its libraries, because it is outside the " "system library directories.)"); @@ -2872,15 +2880,15 @@ static void grantPtraceAccess(QWidget* parent) { QStringLiteral("setcap cap_sys_ptrace+ep \"$1\" || exit 1; " "if [ -x \"$2\" ]; then setcap cap_sys_ptrace+ep \"$2\" || exit 1; fi"), QStringLiteral("sh"), bin, cescan}; - followup = QObject::tr("Access granted. Restart Cheat Engine for it to take effect."); + followup = QCoreApplication::translate("MainWindow", "Access granted. Restart Cheat Engine for it to take effect."); } int rc = QProcess::execute(QStringLiteral("pkexec"), args); if (rc == 0) - QMessageBox::information(parent, QObject::tr("Access granted"), followup); + QMessageBox::information(parent, QCoreApplication::translate("MainWindow", "Access granted"), followup); else - QMessageBox::warning(parent, QObject::tr("Could not grant access"), - QObject::tr("The privileged command was cancelled or failed (pkexec exit %1). " + QMessageBox::warning(parent, QCoreApplication::translate("MainWindow", "Could not grant access"), + QCoreApplication::translate("MainWindow", "The privileged command was cancelled or failed (pkexec exit %1). " "You can still grant access manually from a terminal.").arg(rc)); } @@ -2910,15 +2918,15 @@ static void warnIfMemoryUnreadable(QWidget* parent, ce::ProcessHandle* p, QMessageBox box(parent); box.setIcon(QMessageBox::Warning); - box.setWindowTitle(QObject::tr("Process memory not readable")); - box.setText(QObject::tr( + box.setWindowTitle(QCoreApplication::translate("MainWindow", "Process memory not readable")); + box.setText(QCoreApplication::translate("MainWindow", "Cannot read the memory of PID %1 (%2).\n\n" "Linux only lets a program read another process's memory if it is allowed " "to ptrace it. With kernel.yama.ptrace_scope = %3, that means the target " "must be a child of Cheat Engine, or Cheat Engine must run with elevated " "rights. Scanning, memory browsing and debugging this process will not work " "until that is granted.").arg(pid).arg(name).arg(scope)); - box.setInformativeText(QObject::tr( + box.setInformativeText(QCoreApplication::translate("MainWindow", "\"Grant access\" will ask for your password (via pkexec) and set it up for " "you. Or do it manually:\n" " • Persistent, per-binary: sudo setcap cap_sys_ptrace+ep %1\n" @@ -2929,7 +2937,7 @@ static void warnIfMemoryUnreadable(QWidget* parent, ce::ProcessHandle* p, QPushButton* grantBtn = nullptr; const bool havePkexec = !QStandardPaths::findExecutable(QStringLiteral("pkexec")).isEmpty(); if (havePkexec) - grantBtn = box.addButton(QObject::tr("Grant access…"), QMessageBox::AcceptRole); + grantBtn = box.addButton(QCoreApplication::translate("MainWindow", "Grant access…"), QMessageBox::AcceptRole); box.addButton(QMessageBox::Close); box.exec(); @@ -2955,10 +2963,10 @@ void MainWindow::autoAttach(pid_t pid, const QString& name, quint64 watch) { // before process_ is replaced (they hold a raw handle). attachToPid(pid, name); if (process_ && currentPid_ == pid) { - statusBar()->showMessage(QString("Auto-attached to %1 (pid %2)").arg(name).arg(pid), 4000); + statusBar()->showMessage(QString(tr("Auto-attached to %1 (pid %2)")).arg(name).arg(pid), 4000); stopProcessWatch(); } else { - statusBar()->showMessage(QString("Found %1 (pid %2) but could not open it").arg(name).arg(pid), 8000); + statusBar()->showMessage(QString(tr("Found %1 (pid %2) but could not open it")).arg(name).arg(pid), 8000); } } @@ -3020,13 +3028,13 @@ void MainWindow::attachToPid(pid_t pid, const QString& name) { ce::TargetProfile prof = ce::probeTarget(pid); processDetail_->setText(QString("PID %1 · %2").arg(pid).arg(QString::fromStdString(prof.summary()))); statusBar()->showMessage( - QString("Attached to %1: %2").arg(label, QString::fromStdString(prof.summary())), 8000); + QString(tr("Attached to %1: %2")).arg(label, QString::fromStdString(prof.summary())), 8000); // The summary + every note live on the process label's tooltip (persistent, // non-intrusive; hover to read). QString tip = QString::fromStdString(prof.summary()); for (const auto& n : prof.notes) tip += "\n\n• " + QString::fromStdString(n); auto target = process_->targetDescription(); - tip += QString("\n\nProgram: %1, %2-byte pointers, %3\nHost ABI: %4") + tip += QString(tr("\n\nProgram: %1, %2-byte pointers, %3\nHost ABI: %4")) .arg(ce::cpuArchitectureName(target.program.architecture)).arg(target.program.pointerWidth) .arg(ce::byteOrderName(target.program.byteOrder)).arg(ce::targetAbiName(target.host.abi)); for (const auto& c : ce::targetCapabilities(target)) @@ -3037,7 +3045,7 @@ void MainWindow::attachToPid(pid_t pid, const QString& name) { // The one note that means nothing will work at all (attach/watch/inject all // fail) is worth an explicit dialog: the target is already being traced. if (prof.tracerPid != 0 && !prof.notes.empty()) - QMessageBox::warning(this, "Target already being traced", + QMessageBox::warning(this, tr("Target already being traced"), QString::fromStdString(prof.notes.front())); } } @@ -3061,14 +3069,14 @@ bool MainWindow::attachToGdb(std::unique_ptr process,const addressListModel_->setProcess(process_.get());resultsModel_->setProcess(process_.get()); resultsModel_->clear();lastResult_.reset();undoResult_.reset(); lastResultType_=undoResultType_=ValueType::Int32;lastResultValueSize_=undoResultValueSize_=0; - foundLabel_->setText("Found: 0"); + foundLabel_->setText(tr("Found: 0")); const QString dataOrder=description.program.byteOrder==ce::ByteOrder::Unknown - ? "data order unknown" : QString("%1 data").arg(ce::byteOrderName(description.program.byteOrder)); + ? tr("data order unknown") : QString(tr("%1 data")).arg(ce::byteOrderName(description.program.byteOrder)); const QString pointers=description.program.pointerWidth - ? QString("%1-bit pointers").arg(description.program.pointerWidth*8) : "pointer width unknown"; - processLabel_->setText(QString("GDB: %1").arg(endpoint)); + ? QString(tr("%1-bit pointers")).arg(description.program.pointerWidth*8) : tr("pointer width unknown"); + processLabel_->setText(QString(tr("GDB: %1")).arg(endpoint)); processDetail_->setText(QString("%1 · %2 · %3").arg(architecture,dataOrder,pointers)); - QString tip=QString("Guest CPU: %1\nData order: %2\nProgram pointers: %3\nTransport: GDB") + QString tip=QString(tr("Guest CPU: %1\nData order: %2\nProgram pointers: %3\nTransport: GDB")) .arg(architecture).arg(ce::byteOrderName(description.program.byteOrder)).arg(pointers); for (const auto& capability:ce::targetCapabilities(description)) if (capability.state!=ce::CapabilityState::Available) @@ -3083,7 +3091,7 @@ bool MainWindow::attachToGdb(std::unique_ptr process,const valueTypeCombo_->setCurrentIndex(8); // ByteArray, see mapValueType. guestScanSetup_ = true; updateScanButtons(); - statusBar()->showMessage(regions.empty() ? "Connected to GDB; no memory map. Reconnect with a known scan range." : "Connected to GDB target",8000); + statusBar()->showMessage(regions.empty() ? tr("Connected to GDB; no memory map. Reconnect with a known scan range.") : tr("Connected to GDB target"),8000); return true; } void MainWindow::onConnectGdb() { @@ -3094,7 +3102,7 @@ void MainWindow::onConnectGdb() { // target must not inherit that (First Scan on a native process searched only // the old guest range and found nothing). void MainWindow::resetGuestScanSetup() { - if (foundLabel_) foundLabel_->setText("Found: 0"); + if (foundLabel_) foundLabel_->setText(tr("Found: 0")); if (!guestScanSetup_) return; guestScanSetup_ = false; fromAddressEdit_->setText("0000000000"); @@ -3112,8 +3120,8 @@ void MainWindow::onDisconnectProcess() { process_.reset();ceserverClient_.reset();currentPid_=0; addressListModel_->setProcess(nullptr);resultsModel_->setProcess(nullptr);resultsModel_->clear(); lastResult_.reset();undoResult_.reset();lastResultType_=undoResultType_=ValueType::Int32; - lastResultValueSize_=undoResultValueSize_=0;foundLabel_->setText("Found: 0"); - processLabel_->setText("No process selected");processLabel_->setToolTip({});processDetail_->setText(tr("Press Ctrl+O or click the magnifier to choose a process."));currentProcessName_.clear();updateWindowTitle();updateScanButtons(); + lastResultValueSize_=undoResultValueSize_=0;foundLabel_->setText(tr("Found: 0")); + processLabel_->setText(tr("No process selected"));processLabel_->setToolTip({});processDetail_->setText(tr("Press Ctrl+O or click the magnifier to choose a process."));currentProcessName_.clear();updateWindowTitle();updateScanButtons(); } // The network half of attaching to a ceserver target: no GUI state, so the @@ -3124,17 +3132,17 @@ MainWindow::CeserverConnection MainWindow::connectCeserver(const QString& host, c.client = std::make_unique(); std::string err; if (!c.client->connectTcp(host.toStdString(), port, err)) { - c.error = QString("Connect failed: %1").arg(QString::fromStdString(err)); + c.error = QString(tr("Connect failed: %1")).arg(QString::fromStdString(err)); return c; } auto version = c.client->getVersion(); if (!version) { - c.error = QString("Handshake failed: %1").arg(QString::fromStdString(version.error())); + c.error = QString(tr("Handshake failed: %1")).arg(QString::fromStdString(version.error())); return c; } c.version = QString::fromStdString(version->versionString); c.handle = ce::os::RemoteProcessHandle::open(*c.client, pid); - if (!c.handle) c.error = QString("Server refused PID %1").arg((int)pid); + if (!c.handle) c.error = QString(tr("Server refused PID %1")).arg((int)pid); return c; } @@ -3156,7 +3164,7 @@ bool MainWindow::adoptCeserver(CeserverConnection connection, const QString& hos stopProcessWatch(); // see attachToPid luaEngine_.setProcess(process_.get()); currentPid_ = pid; - processLabel_->setText(QString("Remote process %1").arg((int)pid)); + processLabel_->setText(QString(tr("Remote process %1")).arg((int)pid)); processDetail_->setText(QString("ceserver %1:%2 · %3").arg(host).arg(port).arg(connection.version)); currentProcessName_ = QString("ceserver %1:%2").arg(host).arg(port); updateWindowTitle(); @@ -3178,38 +3186,38 @@ bool MainWindow::adoptCeserver(CeserverConnection connection, const QString& hos void MainWindow::onConnectCeserver() { QSettings settings; auto* dlg = new QDialog(this); - dlg->setWindowTitle("Connect to ceserver"); + dlg->setWindowTitle(tr("Connect to ceserver")); auto* form = new QFormLayout(dlg); - auto* intro = new QLabel("Attach to a process on another machine (or in a container) " - "through a running ceserver."); + auto* intro = new QLabel(tr("Attach to a process on another machine (or in a container) " + "through a running ceserver.")); intro->setWordWrap(true); intro->setProperty("secondary", true); form->addRow(intro); auto* hostEdit = new QLineEdit(settings.value("network/ceserverHost", "127.0.0.1").toString()); - form->addRow("Host:", hostEdit); + form->addRow(tr("Host:"), hostEdit); auto* portSpin = new QSpinBox; portSpin->setRange(1, 65535); portSpin->setValue(settings.value("network/ceserverPort", 52736).toInt()); - form->addRow("Port:", portSpin); + form->addRow(tr("Port:"), portSpin); auto* pidEdit = new QLineEdit; - pidEdit->setPlaceholderText("process ID on the remote machine"); + pidEdit->setPlaceholderText(tr("process ID on the remote machine")); pidEdit->setValidator(new QIntValidator(1, std::numeric_limits::max(), pidEdit)); portSpin->setGroupSeparatorShown(false); hostEdit->setMinimumWidth(240); - form->addRow("PID:", pidEdit); + form->addRow(tr("PID:"), pidEdit); auto* statusLabel = new QLabel; statusLabel->setWordWrap(true); form->addRow(statusLabel); auto* btnRow = new QHBoxLayout; - auto* connectBtn = new QPushButton("Connect"); + auto* connectBtn = new QPushButton(tr("Connect")); connectBtn->setProperty("primary", true); connectBtn->setDefault(true); - auto* cancelBtn = new QPushButton("Cancel"); + auto* cancelBtn = new QPushButton(tr("Cancel")); btnRow->addStretch(); btnRow->addWidget(connectBtn); btnRow->addWidget(cancelBtn); @@ -3221,12 +3229,12 @@ void MainWindow::onConnectCeserver() { bool ok = false; pid_t pid = pidEdit->text().toInt(&ok); if (!ok || pid <= 0) { - statusLabel->setText("Enter a valid remote PID."); + statusLabel->setText(tr("Enter a valid remote PID.")); return; } const QString host = hostEdit->text(); const auto port = static_cast(portSpin->value()); - statusLabel->setText(QString("Connecting to %1:%2...").arg(host).arg(port)); + statusLabel->setText(QString(tr("Connecting to %1:%2...")).arg(host).arg(port)); connectBtn->setEnabled(false); // Off the GUI thread; Cancel closes the dialog and the late result is // dropped (its connection closes with it). @@ -3500,21 +3508,21 @@ void MainWindow::onFirstScan() { resultsModel_->clear(); lastResult_.reset(); undoResult_.reset(); - foundLabel_->setText("Found: 0"); + foundLabel_->setText(tr("Found: 0")); // Changed, Increased and the like compare with a previous scan, which // a first scan does not have: start again from Exact Value, as CE does. if (scanNeedsPreviousResult(mapScanType(scanTypeCombo_->currentIndex()))) scanTypeCombo_->setCurrentIndex(0); - statusBar()->showMessage("New scan: pick a value type and value, then First Scan.", 4000); + statusBar()->showMessage(tr("New scan: pick a value type and value, then First Scan."), 4000); updateScanButtons(); return; } if (!process_) { - statusBar()->showMessage("No process attached; open one first (Ctrl+O) to scan.", 4000); + statusBar()->showMessage(tr("No process attached; open one first (Ctrl+O) to scan."), 4000); return; } if (auto problem = ce::memoryAccessProblem(*process_)) { - QMessageBox::warning(this, "Cannot read the process", QString::fromStdString(*problem)); + QMessageBox::warning(this, tr("Cannot read the process"), QString::fromStdString(*problem)); return; } @@ -3530,15 +3538,15 @@ void MainWindow::onFirstScan() { auto fromExpr = parseAddressExpr(fromAddressEdit_->text()); auto toExpr = parseAddressExpr(toAddressEdit_->text()); if (!fromExpr || !toExpr) { - QMessageBox::warning(this, "Scan range", - "From/To must be a hex address, symbol, or module+offset (e.g. target2, target2+0x5000)."); + QMessageBox::warning(this, tr("Scan range"), + tr("From/To must be a hex address, symbol, or module+offset (e.g. target2, target2+0x5000).")); return; } config.startAddress = *fromExpr; config.stopAddress = *toExpr; if (config.startAddress > config.stopAddress) { - QMessageBox::warning(this, "Scan range", - "The From address must not be greater than the To address."); + QMessageBox::warning(this, tr("Scan range"), + tr("The From address must not be greater than the To address.")); return; } auto toMatch = [](Qt::CheckState s) { @@ -3566,7 +3574,7 @@ void MainWindow::onFirstScan() { // parseAOB returns false for an empty pattern OR a token that isn't hex // or a wildcard (a typo like "8Z") — reject both instead of scanning wrong. if (!config.parseAOB(text.toStdString())) { - QMessageBox::warning(this, "Array of byte scan", "Enter a valid AOB pattern (e.g. \"7F 45 ?? 46\")."); + QMessageBox::warning(this, tr("Array of byte scan"), tr("Enter a valid AOB pattern (e.g. \"7F 45 ?? 46\").")); return; } config.alignment = 1; @@ -3574,14 +3582,14 @@ void MainWindow::onFirstScan() { config.parseBinary(text.toStdString()); // parseBinary returns void; an empty pattern means nothing was parsed. if (config.byteArray.empty()) { - QMessageBox::warning(this, "Binary scan", "Enter a valid binary pattern (e.g. \"0110??01\")."); + QMessageBox::warning(this, tr("Binary scan"), tr("Enter a valid binary pattern (e.g. \"0110??01\").")); return; } config.alignment = 1; } else if (config.valueType == ValueType::Grouped) { std::string error; if (!config.parseGrouped(text.toStdString(), &error)) { - QMessageBox::warning(this, "Grouped scan", QString::fromStdString(error)); + QMessageBox::warning(this, tr("Grouped scan"), QString::fromStdString(error)); return; } config.alignment = 1; @@ -3591,7 +3599,7 @@ void MainWindow::onFirstScan() { config.alignment = 1; } else if (config.valueType == ValueType::Float || config.valueType == ValueType::Double) { if (!config.parseFloatingValues(text.toStdString(), scanValue2Edit_->text().toStdString())) { - QMessageBox::warning(this, "Floating scan", "Enter valid numeric values for the selected comparison.");return; + QMessageBox::warning(this, tr("Floating scan"), tr("Enter valid numeric values for the selected comparison."));return; } } else if (config.valueType == ValueType::Pointer) { auto value = scanIntegerInput(text, intBase == 16, /*pointer=*/true); @@ -3599,7 +3607,7 @@ void MainWindow::onFirstScan() { config.intValue = value.value_or(0); } else if (config.valueType == ValueType::All) { if (!config.parseAllValues(text.toStdString(), scanValue2Edit_->text().toStdString(), intBase == 16)) { - QMessageBox::warning(this, "All scan", "Enter valid numeric values for the selected comparison.");return; + QMessageBox::warning(this, tr("All scan"), tr("Enter valid numeric values for the selected comparison."));return; } } else { auto value = scanIntegerInput(text, intBase == 16); @@ -3619,7 +3627,7 @@ void MainWindow::onFirstScan() { } } if (!applyFloatOptions(config, floatRoundingCombo_, floatToleranceEdit_)) { - QMessageBox::warning(this, "Floating tolerance", "Enter a finite nonnegative tolerance.");return; + QMessageBox::warning(this, tr("Floating tolerance"), tr("Enter a finite nonnegative tolerance."));return; } size_t resultValueSize = resultValueSizeForConfig(config); @@ -3632,24 +3640,24 @@ void MainWindow::onFirstScan() { // The scanner refused these settings and says why; the advice below // is about running out of memory and does not apply. firstScanBtn_->setEnabled(true); - QMessageBox::warning(this, "Scan failed", QString("The scan could not start: %1").arg(e.what())); + QMessageBox::warning(this, tr("Scan failed"), QString(tr("The scan could not start: %1")).arg(e.what())); return; } catch (const std::exception& e) { // A huge target (e.g. an emulator mapping many GB of guest RAM) can // exhaust memory during the scan/merge. Report it instead of letting // the exception escape the Qt event handler and std::terminate. firstScanBtn_->setEnabled(true); - QMessageBox::warning(this, "Scan failed", - QString("The scan could not complete: %1\n\n" + QMessageBox::warning(this, tr("Scan failed"), + QString(tr("The scan could not complete: %1\n\n" "Try narrowing the From/To range, enabling Fast Scan, or " - "choosing a more specific value type.").arg(e.what())); + "choosing a more specific value type.")).arg(e.what())); return; } firstScanBtn_->setEnabled(true); foundLabel_->setText(foundLabelText(result->count())); if (result->hasWriteError()) - QMessageBox::warning(this, "Scan results truncated", scanWriteErrorText(*result)); + QMessageBox::warning(this, tr("Scan results truncated"), scanWriteErrorText(*result)); if (result->valueSize()) resultValueSize = result->valueSize(); resultsModel_->setResult(result.get(), config.valueType, resultValueSize, config.stringEncoding); @@ -3660,17 +3668,17 @@ void MainWindow::onFirstScan() { lastResultType_ = config.valueType; lastResultValueSize_ = resultValueSize; updateScanButtons(); - statusBar()->showMessage(lastResult_->hasWriteError() ? QStringLiteral("Scan results are incomplete.") : - QStringLiteral("First scan complete. %1").arg(foundLabelText(lastResult_->count())), 4000); + statusBar()->showMessage(lastResult_->hasWriteError() ? tr("Scan results are incomplete.") : + tr("First scan complete. %1").arg(foundLabelText(lastResult_->count())), 4000); } void MainWindow::onNextScan() { if (!process_) { - statusBar()->showMessage("No process attached; open one first (Ctrl+O) to scan.", 4000); + statusBar()->showMessage(tr("No process attached; open one first (Ctrl+O) to scan."), 4000); return; } if (!lastResult_) { - statusBar()->showMessage("Run a First Scan before a Next Scan.", 4000); + statusBar()->showMessage(tr("Run a First Scan before a Next Scan."), 4000); return; } @@ -3695,7 +3703,7 @@ void MainWindow::onNextScan() { // parseAOB returns false for an empty pattern OR a token that isn't hex // or a wildcard (a typo like "8Z") — reject both instead of scanning wrong. if (!config.parseAOB(text.toStdString())) { - QMessageBox::warning(this, "Array of byte scan", "Enter a valid AOB pattern (e.g. \"7F 45 ?? 46\")."); + QMessageBox::warning(this, tr("Array of byte scan"), tr("Enter a valid AOB pattern (e.g. \"7F 45 ?? 46\").")); return; } config.alignment = 1; @@ -3703,14 +3711,14 @@ void MainWindow::onNextScan() { config.parseBinary(text.toStdString()); // parseBinary returns void; an empty pattern means nothing was parsed. if (config.byteArray.empty()) { - QMessageBox::warning(this, "Binary scan", "Enter a valid binary pattern (e.g. \"0110??01\")."); + QMessageBox::warning(this, tr("Binary scan"), tr("Enter a valid binary pattern (e.g. \"0110??01\").")); return; } config.alignment = 1; } else if (config.valueType == ValueType::Grouped) { std::string error; if (!config.parseGrouped(text.toStdString(), &error)) { - QMessageBox::warning(this, "Grouped scan", QString::fromStdString(error)); + QMessageBox::warning(this, tr("Grouped scan"), QString::fromStdString(error)); return; } config.alignment = 1; @@ -3721,7 +3729,7 @@ void MainWindow::onNextScan() { config.alignment = 1; } else if (config.valueType == ValueType::Float || config.valueType == ValueType::Double) { if (!config.parseFloatingValues(text.toStdString(), scanValue2Edit_->text().toStdString())) { - QMessageBox::warning(this, "Floating scan", "Enter valid numeric values for the selected comparison.");return; + QMessageBox::warning(this, tr("Floating scan"), tr("Enter valid numeric values for the selected comparison."));return; } } else if (config.valueType == ValueType::Pointer) { auto value = scanIntegerInput(text, intBase == 16, /*pointer=*/true); @@ -3729,7 +3737,7 @@ void MainWindow::onNextScan() { config.intValue = value.value_or(0); } else if (config.valueType == ValueType::All) { if (!config.parseAllValues(text.toStdString(), scanValue2Edit_->text().toStdString(), intBase == 16)) { - QMessageBox::warning(this, "All scan", "Enter valid numeric values for the selected comparison.");return; + QMessageBox::warning(this, tr("All scan"), tr("Enter valid numeric values for the selected comparison."));return; } } else { auto value = scanIntegerInput(text, intBase == 16); @@ -3748,7 +3756,7 @@ void MainWindow::onNextScan() { } } if (!applyFloatOptions(config, floatRoundingCombo_, floatToleranceEdit_)) { - QMessageBox::warning(this, "Floating tolerance", "Enter a finite nonnegative tolerance.");return; + QMessageBox::warning(this, tr("Floating tolerance"), tr("Enter a finite nonnegative tolerance."));return; } if (percentCheck_->isChecked()) { @@ -3777,15 +3785,15 @@ void MainWindow::onNextScan() { // Guard the same way as onFirstScan: a bad_alloc (or a rejected // next-scan, e.g. a changed value-size stride) must not terminate. nextScanBtn_->setEnabled(true); - QMessageBox::warning(this, "Scan failed", - QString("The next scan could not complete: %1").arg(e.what())); + QMessageBox::warning(this, tr("Scan failed"), + QString(tr("The next scan could not complete: %1")).arg(e.what())); return; } nextScanBtn_->setEnabled(true); foundLabel_->setText(foundLabelText(result->count())); if (result->hasWriteError()) - QMessageBox::warning(this, "Scan results truncated", scanWriteErrorText(*result)); + QMessageBox::warning(this, tr("Scan results truncated"), scanWriteErrorText(*result)); if (result->valueSize()) resultValueSize = result->valueSize(); resultsModel_->setResult(result.get(), config.valueType, resultValueSize, config.stringEncoding); @@ -3796,8 +3804,8 @@ void MainWindow::onNextScan() { lastResultType_ = config.valueType; lastResultValueSize_ = resultValueSize; updateScanButtons(); - statusBar()->showMessage(lastResult_->hasWriteError() ? QStringLiteral("Scan results are incomplete.") : - QStringLiteral("Next scan complete. %1").arg(foundLabelText(lastResult_->count())), 4000); + statusBar()->showMessage(lastResult_->hasWriteError() ? tr("Scan results are incomplete.") : + tr("Next scan complete. %1").arg(foundLabelText(lastResult_->count())), 4000); } void MainWindow::onUndoScan() { @@ -3819,7 +3827,7 @@ void MainWindow::onResultDoubleClicked(const QModelIndex& index) { size_t size = type == ValueType::All ? lastResult_->allTypeValueSize(index.row()) : lastResultValueSize_; if (type == ValueType::All || type == ValueType::Grouped || type == ValueType::Custom || type == ValueType::Binary) type = ValueType::ByteArray; - int id = addressListModel_->addEntry(addr, type, "No description", "", size); + int id = addressListModel_->addEntry(addr, type, tr("No description"), "", size); if (lastResult_->hasDataFormat()) addressListModel_->setEntryDataFormat(addressListModel_->rowOfId(id), lastResult_->byteOrder(), lastResult_->pointerWidth()); } @@ -3938,8 +3946,8 @@ bool MainWindow::maybeSaveTable() { tableLuaScript_.isEmpty() && disasmAnnotations_.empty()) return true; if (tableFingerprint() == savedTableFingerprint_) return true; - const auto answer = QMessageBox::question(this, "Unsaved changes", - "The cheat table has changes that are not saved. Save them?", + const auto answer = QMessageBox::question(this, tr("Unsaved changes"), + tr("The cheat table has changes that are not saved. Save them?"), QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Save); if (answer == QMessageBox::Discard) return true; if (answer != QMessageBox::Save) return false; @@ -4146,9 +4154,9 @@ void MainWindow::editScriptEntry(int row) { auto* editor = new ScriptEditor(process_.get(), &autoAsm_, this); trackTargetWindow(editor); editor->setAttribute(Qt::WA_DeleteOnClose); - editor->setWindowTitle("Auto Assembler: " + desc); + editor->setWindowTitle(tr("Auto Assembler: %1").arg(desc)); editor->setDefaultDescription(desc); - editor->setTableButtonText("Save to Table"); + editor->setTableButtonText(tr("Save to Table")); editor->setScript(script.toStdString()); // Save back to THIS entry (found by stable id, so it survives reordering) // instead of appending a duplicate row. @@ -4247,8 +4255,8 @@ ce::CheatTable MainWindow::buildCheatTable() const { } void MainWindow::onCreateTrainer() { - auto path = QFileDialog::getSaveFileName(this, "Create Trainer (C source)", - "trainer.c", "C source (*.c);;All Files (*)"); + auto path = QFileDialog::getSaveFileName(this, tr("Create Trainer (C source)"), + "trainer.c", tr("C source (*.c);;All Files (*)")); if (path.isEmpty()) return; try { ce::CheatTable table = buildCheatTable(); @@ -4256,15 +4264,15 @@ void MainWindow::onCreateTrainer() { std::string src = gen.generateSource(table); QFile f(path); if (!f.open(QIODevice::WriteOnly)) { - QMessageBox::warning(this, "Create Trainer", "Could not write the trainer source file."); + QMessageBox::warning(this, tr("Create Trainer"), tr("Could not write the trainer source file.")); return; } f.write(src.data(), (qint64)src.size()); statusBar()->showMessage( - QString("Trainer C source written to %1 (compile with gcc)").arg(path), 6000); + QString(tr("Trainer C source written to %1 (compile with gcc)")).arg(path), 6000); } catch (const std::exception& ex) { - QMessageBox::critical(this, "Create Trainer", - QString("Trainer generation failed:\n%1").arg(ex.what())); + QMessageBox::critical(this, tr("Create Trainer"), + QString(tr("Trainer generation failed:\n%1")).arg(ex.what())); } } @@ -4355,12 +4363,12 @@ void MainWindow::rebuildValueHotkeys() { } void MainWindow::onSaveScanResults() { - if (!resultsModel_ || !resultsModel_->resultCount()) { statusBar()->showMessage("No scan results to save.", 4000); return; } - const QString path = QFileDialog::getSaveFileName(this, "Save all current scan results", - "scanresults.txt", "Text (*.txt);;CSV (*.csv);;All files (*)"); + if (!resultsModel_ || !resultsModel_->resultCount()) { statusBar()->showMessage(tr("No scan results to save."), 4000); return; } + const QString path = QFileDialog::getSaveFileName(this, tr("Save all current scan results"), + "scanresults.txt", tr("Text (*.txt);;CSV (*.csv);;All files (*)")); if (path.isEmpty()) return; const size_t rows = resultsModel_->resultCount(); - if (!rows) { statusBar()->showMessage("No scan results to save.", 4000); return; } + if (!rows) { statusBar()->showMessage(tr("No scan results to save."), 4000); return; } QSaveFile file(path); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { statusBar()->showMessage(file.errorString(), 4000); return; @@ -4371,7 +4379,7 @@ void MainWindow::onSaveScanResults() { if (csv) return QString('"') + text.replace("\"", "\"\"") + '"'; return text.replace("\t", "\\t").replace("\r", "\\r").replace("\n", "\\n"); }; - QProgressDialog progress("Saving all scan results...", "Cancel", 0, 1000, this); + QProgressDialog progress(tr("Saving all scan results..."), tr("Cancel"), 0, 1000, this); progress.setWindowModality(Qt::WindowModal); bool changed = false; connect(resultsModel_, &QAbstractItemModel::modelReset, &progress, [&] { changed = true; }); @@ -4383,7 +4391,7 @@ void MainWindow::onSaveScanResults() { QCoreApplication::processEvents(); if (changed || progress.wasCanceled()) { file.cancelWriting(); - statusBar()->showMessage(changed ? "Scan results changed; export canceled." : "Export canceled.", 4000); + statusBar()->showMessage(changed ? tr("Scan results changed; export canceled.") : tr("Export canceled."), 4000); return; } } @@ -4392,9 +4400,9 @@ void MainWindow::onSaveScanResults() { } output.flush(); if (output.status() != QTextStream::Ok || !file.flush() || !file.commit()) { - statusBar()->showMessage("Could not save scan results: " + file.errorString(), 6000); return; + statusBar()->showMessage(tr("Could not save scan results: %1").arg(file.errorString()), 6000); return; } - statusBar()->showMessage(QString("Saved %1 results to %2").arg(qulonglong(rows)).arg(QFileInfo(path).fileName()), 6000); + statusBar()->showMessage(QString(tr("Saved %1 results to %2")).arg(qulonglong(rows)).arg(QFileInfo(path).fileName()), 6000); } namespace { @@ -4434,23 +4442,25 @@ void MainWindow::onSaveTable() { // Cheat Engine writes ".CT"; GTK's file chooser matches patterns case-sensitively, // so list both spellings or a user's existing tables would be hidden. -static const char* kTableFileFilter = - "Cheat Tables (*.ct *.CT);;JSON Tables (*.json *.JSON);;All Files (*)"; +static QString tableFileFilter() { + return QCoreApplication::translate("MainWindow", + "Cheat Tables (*.ct *.CT);;JSON Tables (*.json *.JSON);;All Files (*)"); +} void MainWindow::onSaveTableAs() { QString filter; - auto path=QFileDialog::getSaveFileName(this,"Save Cheat Table",currentTablePath_,kTableFileFilter,&filter); + auto path=QFileDialog::getSaveFileName(this,tr("Save Cheat Table"),currentTablePath_,tableFileFilter(),&filter); if (path.isEmpty()) return; // Native choosers don't add an extension; take it from the chosen file type, // since saveTableToPath picks the format from the name. if (QFileInfo(path).suffix().isEmpty()) - path += filter.startsWith("JSON") ? ".json" : ".CT"; + path += filter.contains("*.json") ? ".json" : ".CT"; saveTableToPath(path); } bool MainWindow::saveTableToPath(const QString& path) { auto fail=[&](const QString& reason) { - QMessageBox::critical(this,"Save Cheat Table",QString("Could not save %1:\n%2").arg(path,reason)); + QMessageBox::critical(this,tr("Save Cheat Table"),QString(tr("Could not save %1:\n%2")).arg(path,reason)); return false; }; try { @@ -4461,7 +4471,7 @@ bool MainWindow::saveTableToPath(const QString& path) { if (path.endsWith(".ct",Qt::CaseInsensitive)) { auto table=buildCheatTable(); TableDeviceBuffer buffer(file);std::ostream output(&buffer); - if (!ce::writeTableXml(table,output)) {file.cancelWriting();return fail("Invalid table data or incomplete write.");} + if (!ce::writeTableXml(table,output)) {file.cancelWriting();return fail(tr("Invalid table data or incomplete write."));} } else { // Keep the GUI's richer native records (including codecs), while // serializing shared table metadata through the core JSON writer. @@ -4470,7 +4480,7 @@ bool MainWindow::saveTableToPath(const QString& path) { metadata.comment=tableComment_.toStdString();metadata.luaScript=tableLuaScript_.toStdString(); metadata.disassemblerComments=disasmAnnotations_; std::ostringstream output; - if (!ce::writeTableJson(metadata,output)) {file.cancelWriting();return fail("Invalid table metadata.");} + if (!ce::writeTableJson(metadata,output)) {file.cancelWriting();return fail(tr("Invalid table metadata."));} auto root=QJsonDocument::fromJson(QByteArray::fromStdString(output.str())).object(); root["process"]=processLabel_->text();root["entries"]=addressListModel_->toJson(); const auto bytes=QJsonDocument(root).toJson(); @@ -4481,16 +4491,16 @@ bool MainWindow::saveTableToPath(const QString& path) { if (!file.flush()) {const auto error=file.errorString();file.cancelWriting();return fail(error);} if (!file.commit()) return fail(file.errorString()); addRecentTable(path); - statusBar()->showMessage(QString("Saved table to %1").arg(QFileInfo(path).fileName()),4000); + statusBar()->showMessage(QString(tr("Saved table to %1")).arg(QFileInfo(path).fileName()),4000); return true; } catch (const std::exception& ex) {return fail(QString::fromUtf8(ex.what()));} - catch (...) {return fail("Unknown save error.");} + catch (...) {return fail(tr("Unknown save error."));} } void MainWindow::onLoadTable() { if (!maybeSaveTable()) return; - auto path = QFileDialog::getOpenFileName(this, "Load Cheat Table", - currentTablePath_.isEmpty() ? QString() : QFileInfo(currentTablePath_).absolutePath(), kTableFileFilter); + auto path = QFileDialog::getOpenFileName(this, tr("Load Cheat Table"), + currentTablePath_.isEmpty() ? QString() : QFileInfo(currentTablePath_).absolutePath(), tableFileFilter()); if (path.isEmpty()) return; loadTableFromPath(path); } @@ -4609,10 +4619,10 @@ bool MainWindow::loadCheatTableModel(const ce::CheatTable& table,const QJsonArra // loaded, so it can define trainer functions/hooks and reference records. if (!table.luaScript.empty()) { // CE asktorunluascript: never auto-run an untrusted table's Lua, ask first. - auto answer = QMessageBox::question(this, "Execute this lua script?", - "This cheat table contains a Lua script.\n\n" + auto answer = QMessageBox::question(this, tr("Execute this lua script?"), + tr("This cheat table contains a Lua script.\n\n" "Only execute scripts from tables you trust, a table's Lua can hook " - "and manipulate the target. Execute it?", + "and manipulate the target. Execute it?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (answer == QMessageBox::Yes) { luaEngine_.setProcess(process_.get()); @@ -4620,7 +4630,7 @@ bool MainWindow::loadCheatTableModel(const ce::CheatTable& table,const QJsonArra auto res = luaEngine_.evalToString(table.luaScript); if (!res.has_value()) statusBar()->showMessage( - QString("Table Lua error: %1").arg(QString::fromStdString(res.error())), 8000); + QString(tr("Table Lua error: %1")).arg(QString::fromStdString(res.error())), 8000); } } // Open Memory Viewers hold the previous table's comments and labels; their @@ -4678,7 +4688,7 @@ void MainWindow::rebuildRecentMenu() { recentMenu_->clear(); const QStringList recent = QSettings().value("recentTables").toStringList(); if (recent.isEmpty()) { - recentMenu_->addAction("(no recent tables)")->setEnabled(false); + recentMenu_->addAction(tr("(no recent tables)"))->setEnabled(false); return; } for (const QString& p : recent) { @@ -4686,12 +4696,12 @@ void MainWindow::rebuildRecentMenu() { // (it stays listed so the user can see what went away, like CE). const bool exists = QFileInfo::exists(p); auto* a = recentMenu_->addAction(QFileInfo(p).fileName()); - a->setToolTip(exists ? p : p + " (missing)"); + a->setToolTip(exists ? p : p + " " + tr("(missing)")); a->setEnabled(exists); if (exists) connect(a, &QAction::triggered, this, [this, p]() { if (maybeSaveTable()) loadTableFromPath(p); }); } recentMenu_->addSeparator(); - connect(recentMenu_->addAction("Clear list"), &QAction::triggered, this, [this]() { + connect(recentMenu_->addAction(tr("Clear list")), &QAction::triggered, this, [this]() { QSettings().remove("recentTables"); rebuildRecentMenu(); }); @@ -4705,14 +4715,14 @@ void MainWindow::loadTableFromPath(const QString& path) { const ce::TableFormat fmt = ce::detectTableFormat(path.toStdString()); if (fmt == ce::TableFormat::Protected) { bool ok = false; - QString pw = QInputDialog::getText(this, "Protected Cheat Table", - "This table is password-protected (.CETRAINER).\nEnter its password:", + QString pw = QInputDialog::getText(this, tr("Protected Cheat Table"), + tr("This table is password-protected (.CETRAINER).\nEnter its password:"), QLineEdit::Password, QString(), &ok); if (!ok) return; CheatTable table; if (!table.loadProtected(path.toStdString(), pw.toStdString())) { - QMessageBox::warning(this, "Load Cheat Table", - "Could not decrypt the table (wrong password or corrupt file)."); + QMessageBox::warning(this, tr("Load Cheat Table"), + tr("Could not decrypt the table (wrong password or corrupt file).")); return; } if (!loadCheatTableModel(table)) return; @@ -4722,23 +4732,23 @@ void MainWindow::loadTableFromPath(const QString& path) { if (fmt != ce::TableFormat::Json) { // CE XML .CT format (Xml, or Unknown which load() self-validates). CheatTable table; - if (!table.load(path.toStdString())) {QMessageBox::warning(this,"Load Cheat Table","Could not read a valid cheat table.");return;} + if (!table.load(path.toStdString())) {QMessageBox::warning(this,tr("Load Cheat Table"),tr("Could not read a valid cheat table."));return;} if (!loadCheatTableModel(table)) return; addRecentTable(path); } else { QFile f(path); - if (!f.open(QIODevice::ReadOnly)) {QMessageBox::warning(this,"Load Cheat Table",f.errorString());return;} + if (!f.open(QIODevice::ReadOnly)) {QMessageBox::warning(this,tr("Load Cheat Table"),f.errorString());return;} const auto bytes=f.readAll(); - if (f.error()!=QFileDevice::NoError) {QMessageBox::warning(this,"Load Cheat Table",f.errorString());return;} + if (f.error()!=QFileDevice::NoError) {QMessageBox::warning(this,tr("Load Cheat Table"),f.errorString());return;} QJsonParseError parseError;auto doc=QJsonDocument::fromJson(bytes,&parseError); if (!doc.isObject() || !doc.object()["entries"].isArray()) { - QMessageBox::warning(this,"Load Cheat Table","Invalid JSON cheat table.");return; + QMessageBox::warning(this,tr("Load Cheat Table"),tr("Invalid JSON cheat table."));return; } for (const auto& value:doc.object()["entries"].toArray()) if (!value.isObject()) { - QMessageBox::warning(this,"Load Cheat Table","Invalid JSON record.");return; + QMessageBox::warning(this,tr("Load Cheat Table"),tr("Invalid JSON record."));return; } CheatTable parsed; - if (!parsed.loadJsonFromString(bytes.toStdString())) {QMessageBox::warning(this,"Load Cheat Table","Invalid JSON record data.");return;} + if (!parsed.loadJsonFromString(bytes.toStdString())) {QMessageBox::warning(this,tr("Load Cheat Table"),tr("Invalid JSON record data."));return;} // Use the exact validated common fields for either JSON schema. Only // GUI codec/presentation settings come from Qt's parsed representation. const auto original=doc.object()["entries"].toArray(); @@ -4748,10 +4758,10 @@ void MainWindow::loadTableFromPath(const QString& path) { } catch (const std::exception& ex) { // A parse/load path (e.g. ct_file) may throw; never let it escape the slot // into std::terminate. - QMessageBox::critical(this, "Load Cheat Table", - QString("Failed to load table:\n%1").arg(ex.what())); + QMessageBox::critical(this, tr("Load Cheat Table"), + QString(tr("Failed to load table:\n%1")).arg(ex.what())); } catch (...) { - QMessageBox::critical(this, "Load Cheat Table", "Failed to load table (unknown error)."); + QMessageBox::critical(this, tr("Load Cheat Table"), tr("Failed to load table (unknown error).")); } } @@ -4832,7 +4842,7 @@ void MainWindow::showInstructionAccesses(uintptr_t instructionAddr) { // Runs synchronously: plant an execute breakpoint on the instruction, resolve the // data address of its memory operand from the live registers on each hit, until // 32 hits or a 4s timeout. Usually fast for a hot instruction; brief wait otherwise. - statusBar()->showMessage(QString("Monitoring the instruction at 0x%1…").arg(instructionAddr, 0, 16)); + statusBar()->showMessage(QString(tr("Monitoring the instruction at 0x%1…")).arg(instructionAddr, 0, 16)); QApplication::setOverrideCursor(Qt::WaitCursor); QApplication::processEvents(); auto results = ce::findInstructionAccesses(*process_, instructionAddr); @@ -4840,21 +4850,21 @@ void MainWindow::showInstructionAccesses(uintptr_t instructionAddr) { if (results.empty()) { statusBar()->clearMessage(); - QMessageBox::information(this, "What this instruction accesses", - QString("No data accesses were recorded for the instruction at 0x%1.\n\nIt may not have " - "run during the sample window, or it has no memory operand.").arg(instructionAddr, 0, 16)); + QMessageBox::information(this, tr("What this instruction accesses"), + QString(tr("No data accesses were recorded for the instruction at 0x%1.\n\nIt may not have " + "run during the sample window, or it has no memory operand.")).arg(instructionAddr, 0, 16)); return; } auto* dlg = new QDialog(this); dlg->setAttribute(Qt::WA_DeleteOnClose); - dlg->setWindowTitle(QString("Addresses accessed by 0x%1").arg(instructionAddr, 0, 16)); + dlg->setWindowTitle(QString(tr("Addresses accessed by 0x%1")).arg(instructionAddr, 0, 16)); dlg->resize(440, 360); auto* lay = new QVBoxLayout(dlg); - lay->addWidget(new QLabel(QString("%1 distinct address(es) accessed (double-click to add to the list):") + lay->addWidget(new QLabel(QString(tr("%1 distinct address(es) accessed (double-click to add to the list):")) .arg(results.size()))); auto* table = new QTableWidget((int)results.size(), 2, dlg); - table->setHorizontalHeaderLabels({"Address", "Hits"}); + table->setHorizontalHeaderLabels({tr("Address"), tr("Hits")}); table->setEditTriggers(QAbstractItemView::NoEditTriggers); table->setSelectionBehavior(QAbstractItemView::SelectRows); for (int i = 0; i < (int)results.size(); ++i) { @@ -4870,14 +4880,14 @@ void MainWindow::showInstructionAccesses(uintptr_t instructionAddr) { connect(table, &QTableWidget::cellDoubleClicked, this, [this, table](int row, int) { if (auto* it = table->item(row, 0)) addressListModel_->addEntry((uintptr_t)it->data(Qt::UserRole).toULongLong(), - ce::ValueType::Int32, "Accessed address"); + ce::ValueType::Int32, tr("Accessed address")); }); lay->addWidget(table); - auto* close = new QPushButton("Close"); + auto* close = new QPushButton(tr("Close")); connect(close, &QPushButton::clicked, dlg, &QDialog::accept); lay->addWidget(close, 0, Qt::AlignRight); dlg->show(); - statusBar()->showMessage(QString("%1 address(es) accessed by 0x%2") + statusBar()->showMessage(QString(tr("%1 address(es) accessed by 0x%2")) .arg(results.size()).arg(instructionAddr, 0, 16), 5000); } @@ -4899,11 +4909,11 @@ int MainWindow::runAutorunScripts() { ++ran; if (!err.empty()) { ++failed; - statusBar()->showMessage(QString("autorun %1: %2").arg(f, QString::fromStdString(err)), 8000); + statusBar()->showMessage(QString(tr("autorun %1: %2")).arg(f, QString::fromStdString(err)), 8000); } } if (ran > 0 && failed == 0) - statusBar()->showMessage(QString("Ran %1 autorun script(s).").arg(ran), 4000); + statusBar()->showMessage(QString(tr("Ran %1 autorun script(s).")).arg(ran), 4000); return ran; } @@ -4934,12 +4944,12 @@ void MainWindow::startCodeFinderForAddress(uintptr_t addr, bool writesOnly, int else if (modeOverride == "st") { software = false; singleThread = true; } if (wine && !codeFinderNoPrompt_ && !software) { - auto r = QMessageBox::information(this, "Wine / Proton game", - "This is a Wine/Proton (Windows) game. Cheat Engine will watch the game's " + auto r = QMessageBox::information(this, tr("Wine / Proton game"), + tr("This is a Wine/Proton (Windows) game. Cheat Engine will watch the game's " "main thread for writes, which is where gameplay values (money, HP, …) are " "usually changed.\n\n" "If nothing shows up, the value may be written by a background thread; " - "tell me and it can be widened. Start monitoring?", + "tell me and it can be widened. Start monitoring?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); if (r != QMessageBox::Yes) return; } @@ -4953,7 +4963,7 @@ void MainWindow::startCodeFinderForAddress(uintptr_t addr, bool writesOnly, int watchSize = QSettings().value("codefinder/watchSize", "4").toInt(); if (watchSize != 1 && watchSize != 2 && watchSize != 4 && watchSize != 8) watchSize = 4; if (!finder->start(*process_, *debugger, addr, writesOnly, watchSize, software, singleThread)) { - QMessageBox::warning(this, "Code finder unavailable", QString::fromStdString(ce::targetOperationError(*process_, + QMessageBox::warning(this, tr("Code finder unavailable"), QString::fromStdString(ce::targetOperationError(*process_, software ? ce::TargetFeature::SoftwareWatchpoint : ce::TargetFeature::HardwareWatchpoint, finder->lastError()))); return; } @@ -4962,7 +4972,7 @@ void MainWindow::startCodeFinderForAddress(uintptr_t addr, bool writesOnly, int codeFinderDebuggers_.push_back(std::move(debugger)); codeFinders_.push_back(std::move(finder)); - auto title = writesOnly ? "Find what writes" : "Find what accesses"; + auto title = writesOnly ? tr("Find what writes") : tr("Find what accesses"); auto* window = new CodeFinderWindow(finderPtr, QString("%1 0x%2").arg(title).arg(addr, 0, 16), process_.get(), this); trackTargetWindow(window); @@ -4975,7 +4985,7 @@ void MainWindow::startCodeFinderForAddress(uintptr_t addr, bool writesOnly, int // (the Code list), where they can be disassembled / NOP'd / restored. window->setAddToList([this](uintptr_t a, const QString& desc) { showAdvancedOptions(); - advancedOptions_->addCode(a, desc.isEmpty() ? QString("code 0x%1").arg(a, 0, 16) : desc); + advancedOptions_->addCode(a, desc.isEmpty() ? QString(tr("code 0x%1")).arg(a, 0, 16) : desc); }); // CE: "Show this address in the disassembler" jumps the Memory Viewer to the // writing/accessing instruction (the found row's recovered address). @@ -4985,7 +4995,7 @@ void MainWindow::startCodeFinderForAddress(uintptr_t addr, bool writesOnly, int void MainWindow::onMemoryView() { if (!process_) { - statusBar()->showMessage("No process attached; open one first (Ctrl+O) to view memory.", 4000); + statusBar()->showMessage(tr("No process attached; open one first (Ctrl+O) to view memory."), 4000); return; } openMemoryView(0); @@ -4993,7 +5003,7 @@ void MainWindow::onMemoryView() { void MainWindow::showDebugger() { if (AutoAsmUiOperation::retiring(&autoAsm_)) return; - if (!process_) { QMessageBox::warning(this, "No process", "Open a process first."); return; } + if (!process_) { QMessageBox::warning(this, tr("No process"), tr("Open a process first.")); return; } // Reuse the existing window: it holds the ptrace attachment, so a second one // would fail to attach. Just raise it if it is already open. if (debuggerWindow_) { @@ -5027,7 +5037,7 @@ void MainWindow::trackStructDissector(StructureDissector* sd) { } void MainWindow::openMonoDissector() { - if (!process_) { QMessageBox::warning(this, "No process", "Open a process first."); return; } + if (!process_) { QMessageBox::warning(this, tr("No process"), tr("Open a process first.")); return; } auto* w = new ce::gui::MonoDissectorWindow(process_.get(), this); trackTargetWindow(w); w->setAttribute(Qt::WA_DeleteOnClose); @@ -5037,9 +5047,9 @@ void MainWindow::openMonoDissector() { [this](quint32 offset, int valueType, const QString& label, bool isStatic) { bool ok = false; // A static field's offset is into the class's static data, not an object. - QString base = QInputDialog::getText(this, "Add Mono field", - QString("%1 for '%2' (field at +0x%3):") - .arg(isStatic ? "Static data address of the class" : "Object base address") + QString base = QInputDialog::getText(this, tr("Add Mono field"), + QString(tr("%1 for '%2' (field at +0x%3):")) + .arg(isStatic ? tr("Static data address of the class") : tr("Object base address")) .arg(label).arg(offset, 0, 16), QLineEdit::Normal, "", &ok); if (!ok || base.trimmed().isEmpty()) return; @@ -5094,13 +5104,21 @@ void MainWindow::rebuildDebuggerComments() { QWidget* MainWindow::openPanelByName(const QString& name) { auto tryIn = [this, &name](QWidget* host) -> QWidget* { + // The --panel argument is an English menu name; with a translation + // catalogue installed the actions carry the translated text. + QStringList candidates{name}; + const QString translated = QCoreApplication::translate("ce::gui::MainWindow", name.toUtf8().constData()); + if (!translated.isEmpty() && translated != name) candidates << translated; QSet before; for (auto* w : QApplication::topLevelWidgets()) before.insert(w); bool matched = false; for (QAction* a : host->findChildren()) { if (a->menu() || a->isSeparator()) continue; QString t = a->text(); t.remove('&'); - if (t.contains(name, Qt::CaseInsensitive)) { a->trigger(); matched = true; break; } + bool hit = false; + for (const QString& candidate : candidates) + if (t.contains(candidate, Qt::CaseInsensitive)) { hit = true; break; } + if (hit) { a->trigger(); matched = true; break; } } if (!matched) return nullptr; QApplication::processEvents(); @@ -5117,7 +5135,7 @@ QWidget* MainWindow::openPanelByName(const QString& name) { AgentPanel* MainWindow::showAgentPanel() { if (!agentPanel_) { - agentDock_ = new QDockWidget("AI Assistant", this); + agentDock_ = new QDockWidget(tr("AI Assistant"), this); agentDock_->setObjectName("aiAssistantDock"); agentPanel_ = new AgentPanel(agentDock_); agentDock_->setWidget(agentPanel_); @@ -5152,6 +5170,11 @@ QDialog* MainWindow::openSettingsDialog(const QString& page) { if (name.isEmpty()) return; if (auto* nav = dialog->findChild("settingsNav")) { auto matches = nav->findItems(name, Qt::MatchContains); + // Nav labels are translated; page names stay English identifiers, so + // fall back to the translated label when the English one does not match. + if (matches.isEmpty()) + matches = nav->findItems(QCoreApplication::translate("ce::gui::SettingsDialog", + name.toUtf8().constData()), Qt::MatchContains); if (!matches.isEmpty()) nav->setCurrentItem(matches.first()); } }; @@ -5201,20 +5224,20 @@ MemoryBrowser* MainWindow::openMemoryView(uintptr_t addr) { browser->setBreakpointSetter([this](uintptr_t addr, bool hardware) { if (!debuggerWindow_) showDebugger(); if (!debuggerWindow_ || !debuggerWindow_->debugAttached()) { - statusBar()->showMessage("The debugger could not attach; no breakpoint was set.", 5000); + statusBar()->showMessage(tr("The debugger could not attach; no breakpoint was set."), 5000); return; } if (!debuggerWindow_->debugStopped()) { - statusBar()->showMessage("Pause the target in the debugger to set a breakpoint.", 5000); + statusBar()->showMessage(tr("Pause the target in the debugger to set a breakpoint."), 5000); return; } debuggerWindow_->addBreakpointAt(addr); if (hardware) - statusBar()->showMessage("Execute breakpoints are software breakpoints in this debugger.", 5000); + statusBar()->showMessage(tr("Execute breakpoints are software breakpoints in this debugger."), 5000); }); browser->setBreakpointRemover([this](uintptr_t addr) { if (debuggerWindow_ && !debuggerWindow_->removeBreakpointAt(addr)) - statusBar()->showMessage("Could not remove the breakpoint; see the debugger window.", 5000); + statusBar()->showMessage(tr("Could not remove the breakpoint; see the debugger window."), 5000); }); browser->setBreakpointQuery([this]() { return debuggerWindow_ ? debuggerWindow_->breakpointAddresses() : std::set{}; @@ -5227,7 +5250,7 @@ MemoryBrowser* MainWindow::openMemoryView(uintptr_t addr) { }); browser->setAddToList([this](uintptr_t addr, ce::ValueType type) { addressListModel_->addEntry(addr, type); - statusBar()->showMessage(QString("Added 0x%1 to the cheat table").arg(addr, 0, 16), 3000); + statusBar()->showMessage(QString(tr("Added 0x%1 to the cheat table")).arg(addr, 0, 16), 3000); }); // Global user-defined symbols: a label set in one Memory Viewer must resolve // everywhere (records, Lua, other views). Seed this view with the app-wide labels, @@ -5280,7 +5303,7 @@ void MainWindow::populateBrowserMenus(MemoryBrowser* b) { auto needProc = [this]() { return process_ != nullptr; }; // ── View: navigable info windows ── - view->addAction("Memory Regions", this, [this, openAt]() { + view->addAction(tr("Memory Regions"), this, [this, openAt]() { if (!process_) return; auto* w = new MemoryRegionsWindow(process_.get(), this); trackTargetWindow(w); @@ -5288,7 +5311,7 @@ void MainWindow::populateBrowserMenus(MemoryBrowser* b) { connect(w, &MemoryRegionsWindow::navigateTo, this, [openAt](uintptr_t a){ openAt(a); }); w->show(); }); - view->addAction("Heap list", this, [this, openAt]() { + view->addAction(tr("Heap list"), this, [this, openAt]() { if (!process_) return; auto* w = new HeapRegionsWindow(process_.get(), this); trackTargetWindow(w); @@ -5296,7 +5319,7 @@ void MainWindow::populateBrowserMenus(MemoryBrowser* b) { connect(w, &HeapRegionsWindow::navigateTo, this, [openAt](uintptr_t a){ openAt(a); }); w->show(); }); - view->addAction("Module list", this, [this, openAt]() { + view->addAction(tr("Module list"), this, [this, openAt]() { if (!process_) return; auto* w = new ModuleListWindow(process_.get(), this); trackTargetWindow(w); @@ -5304,7 +5327,7 @@ void MainWindow::populateBrowserMenus(MemoryBrowser* b) { connect(w, &ModuleListWindow::navigateTo, this, [openAt](uintptr_t a){ openAt(a); }); w->show(); }); - view->addAction("Referenced strings / functions", this, [this, openAt]() { + view->addAction(tr("Referenced strings / functions"), this, [this, openAt]() { if (!process_) return; auto* w = new CodeReferencesWindow(process_.get(), this); trackTargetWindow(w); @@ -5312,19 +5335,19 @@ void MainWindow::populateBrowserMenus(MemoryBrowser* b) { connect(w, &CodeReferencesWindow::navigateTo, this, [openAt](uintptr_t a){ openAt(a); }); w->show(); }); - view->addAction("Thread list", this, [this]() { + view->addAction(tr("Thread list"), this, [this]() { if (!process_) return; auto* w = new ThreadListWindow(process_.get(), this); trackTargetWindow(w); w->setAttribute(Qt::WA_DeleteOnClose); w->show(); }); - view->addAction("Stacktrace", this, [this]() { + view->addAction(tr("Stacktrace"), this, [this]() { if (!process_) return; auto* w = new StackViewWindow(process_.get(), this); trackTargetWindow(w); w->setAttribute(Qt::WA_DeleteOnClose); w->show(); }); - view->addAction("Graphical memory view", this, [this]() { + view->addAction(tr("Graphical memory view"), this, [this]() { if (!process_) return; auto* w = new ce::gui::GraphicalMemoryView(process_.get(), this); trackTargetWindow(w); @@ -5335,7 +5358,7 @@ void MainWindow::populateBrowserMenus(MemoryBrowser* b) { QMenu* dbg = b->debugMenu(); if (dbg) { dbg->addSeparator(); - dbg->addAction("Registers", this, [this]() { + dbg->addAction(tr("Registers"), this, [this]() { if (!process_) return; auto* w = new RegisterEditorWindow(process_.get(), this); trackTargetWindow(w); @@ -5343,16 +5366,16 @@ void MainWindow::populateBrowserMenus(MemoryBrowser* b) { }); // The debugger window holds the breakpoints that are actually armed // (set there, or from the Memory Viewer); its list is the real one. - dbg->addAction("Breakpoint list", this, [this]() { showDebugger(); }); - dbg->addAction("Full debugger", this, [this]() { showDebugger(); }); - dbg->addAction("Break and trace", this, [this]() { + dbg->addAction(tr("Breakpoint list"), this, [this]() { showDebugger(); }); + dbg->addAction(tr("Full debugger"), this, [this]() { showDebugger(); }); + dbg->addAction(tr("Break and trace"), this, [this]() { if (!process_) return; auto* tw = new TracerWindow(process_.get(), [this]() { return createDebuggerForCurrentProcess(); }, this); trackTargetWindow(tw); tw->setAttribute(Qt::WA_DeleteOnClose); tw->show(); }); - dbg->addAction("Branch mapper (LBR)", this, [this]() { + dbg->addAction(tr("Branch mapper (LBR)"), this, [this]() { if (!process_) return; auto* bm = new BranchMapper(process_.get(), this); trackTargetWindow(bm); @@ -5367,13 +5390,13 @@ void MainWindow::populateBrowserMenus(MemoryBrowser* b) { addAnalysisToolsMenu(tools); for (QAction* action : tools->actions()) if (!own.contains(action) && - (action->text() == "Auto Assemble..." || action->text() == "Dissect data/structures...")) + (action->text() == tr("Auto Assemble...") || action->text() == tr("Dissect data/structures..."))) tools->removeAction(action); (void)needProc; } void MainWindow::addAnalysisToolsMenu(QMenu* tools) { - tools->addAction("Auto Assemble...", this, [this]() { + tools->addAction(tr("Auto Assemble..."), this, [this]() { auto* editor = new ScriptEditor(process_.get(), &autoAsm_, this); trackTargetWindow(editor); editor->setAttribute(Qt::WA_DeleteOnClose); @@ -5381,7 +5404,7 @@ void MainWindow::addAnalysisToolsMenu(QMenu* tools) { editor->setBeforeExecute([this]() { stopCodeFindersForInjection(); }); editor->show(); }); - tools->addAction("Pointer scan...", this, [this]() { + tools->addAction(tr("Pointer scan..."), this, [this]() { if (!process_) return; auto* dlg = new PointerScanDialog(process_.get(), this); trackTargetWindow(dlg); @@ -5392,8 +5415,8 @@ void MainWindow::addAnalysisToolsMenu(QMenu* tools) { }); dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->show(); }); - tools->addAction("Emulator guest scan...", this, [this]() { - if (!process_) { QMessageBox::warning(this, "No process", "Open a process first."); return; } + tools->addAction(tr("Emulator guest scan..."), this, [this]() { + if (!process_) { QMessageBox::warning(this, tr("No process"), tr("Open a process first.")); return; } auto* dlg = new GuestScanDialog(process_.get(), this); trackTargetWindow(dlg); connect(dlg, &GuestScanDialog::addressSelected, this, @@ -5403,7 +5426,7 @@ void MainWindow::addAnalysisToolsMenu(QMenu* tools) { }); dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->show(); }); - tools->addAction("Dissect data/structures...", this, [this]() { + tools->addAction(tr("Dissect data/structures..."), this, [this]() { if (!process_) return; auto* sd = new StructureDissector(process_.get(), 0, this); sd->setAttribute(Qt::WA_DeleteOnClose); @@ -5411,14 +5434,14 @@ void MainWindow::addAnalysisToolsMenu(QMenu* tools) { trackStructDissector(sd); sd->show(); }); - tools->addAction("Find static addresses...", this, [this]() { - if (!process_) { QMessageBox::warning(this, "No process", "Open a process first."); return; } + tools->addAction(tr("Find static addresses..."), this, [this]() { + if (!process_) { QMessageBox::warning(this, tr("No process"), tr("Open a process first.")); return; } auto* w = new FindStaticsWindow(process_.get(), this); trackTargetWindow(w); w->setAttribute(Qt::WA_DeleteOnClose); w->show(); }); - tools->addAction("Mono dissector...", this, &MainWindow::openMonoDissector); - tools->addAction("Lua Engine...", this, [this]() { + tools->addAction(tr("Mono dissector..."), this, &MainWindow::openMonoDissector); + tools->addAction(tr("Lua Engine..."), this, [this]() { luaEngine_.setProcess(process_.get()); luaEngine_.setAddressList(addressListModel_); // so getMemoryRecord etc. work if (process_) { luaResolver_.loadProcess(*process_); luaEngine_.setResolver(&luaResolver_); } @@ -5429,7 +5452,7 @@ void MainWindow::addAnalysisToolsMenu(QMenu* tools) { luaConsole_ = console; console->setAttribute(Qt::WA_DeleteOnClose); console->show(); }); - tools->addAction("ELF Inspector...", this, [this]() { + tools->addAction(tr("ELF Inspector..."), this, [this]() { QString initial; // Only a local target's module path names a file readelf can open here // (modules() already maps a sandboxed target's paths through /proc). @@ -5456,7 +5479,7 @@ void MainWindow::updateScanButtons() { // CE-style: once a scan session is open, "First Scan" becomes "New Scan" (a // reset). Ordinary typed scans lock the value type; All scans permit // selection of a surviving numeric type. A fresh state allows any type. - firstScanBtn_->setText(scanActive ? "New Scan" : "First Scan"); + firstScanBtn_->setText(scanActive ? tr("New Scan") : tr("First Scan")); nextScanBtn_->setEnabled(hasResults); undoScanBtn_->setEnabled(undoResult_ != nullptr); if (valueTypeCombo_) valueTypeCombo_->setEnabled(!scanActive || lastResultType_ == ValueType::All); @@ -5572,9 +5595,9 @@ int ScanResultsModel::columnCount(const QModelIndex& parent) const { return pare QVariant ScanResultsModel::headerData(int section, Qt::Orientation o, int role) const { if (role != Qt::DisplayRole || o != Qt::Horizontal) return {}; switch (section) { - case 0: return "Address"; - case 1: return "Value"; // current (live) value - default: return "Previous"; // value captured at scan time + case 0: return tr("Address"); + case 1: return tr("Value"); // current (live) value + default: return tr("Previous"); // value captured at scan time } } @@ -5686,10 +5709,10 @@ void AddressListModel::addScriptEntry(const QString& desc, const QString& script beginInsertRows({}, entries_.size(), entries_.size()); AddressEntry entry; entry.id = allocId(); - entry.description = desc.isEmpty() ? "Auto Assembler script" : desc; + entry.description = desc.isEmpty() ? tr("Auto Assembler script") : desc; entry.autoAsmScript = script; // toggling the checkbox runs [ENABLE]/[DISABLE] entry.type = ValueType::Int32; - entry.currentValue = "(Auto Assembler script)"; + entry.currentValue = tr("(Auto Assembler script)"); entries_.push_back(std::move(entry)); endInsertRows(); } @@ -5875,9 +5898,9 @@ static bool scanTakesValue(ScanCompare compare) { } static void warnBadScanValue(QWidget* parent, const QString& text) { - QMessageBox::warning(parent, "Scan value", text.trimmed().isEmpty() - ? QString("Enter a value for this scan.") - : QString("\"%1\" is not a number (or does not fit in 64 bits).").arg(text.trimmed())); + QMessageBox::warning(parent, QCoreApplication::translate("MainWindow", "Scan value"), text.trimmed().isEmpty() + ? QString(QCoreApplication::translate("MainWindow", "Enter a value for this scan.")) + : QString(QCoreApplication::translate("MainWindow", "\"%1\" is not a number (or does not fit in 64 bits).")).arg(text.trimmed())); } // "40000" -> int16 fits, matching how CE accepts either sign for a fixed width. @@ -6080,7 +6103,7 @@ bool AddressListModel::fromJson(const QJsonArray& arr, bool preserveExisting) { if (scriptBusy_ || mutationBusy_ || AutoAsmUiOperation::retiring(autoAsm_)) return false; QScopedValueRollback mutation(mutationBusy_, true); if (!validRecordFormats(arr)) { - reportActivationError("Table format", "Invalid record data order or pointer size."); return false; + reportActivationError(tr("Table format"), tr("Invalid record data order or pointer size.")); return false; } std::unordered_set reserved,assigned; int largest=0; @@ -6354,7 +6377,7 @@ bool AddressListModel::adjustEntryValue(int row, double delta) { const QString nextText = QString::fromStdString( ce::formatIntegerScalar(bits, ce::scalarWidth(e.type), e.showAsSigned, e.showAsHex)); auto written = writeValueToProcess(proc_, e.address, e.type, nextText, e.codec, e.bigEndian, e.showAsHex, 0, e.dataByteOrder, e.pointerWidth); - if (!written) { reportActivationError("Value edit failed", QString::fromStdString(written.error())); return false; } + if (!written) { reportActivationError(tr("Value edit failed"), QString::fromStdString(written.error())); return false; } e.currentValue = nextText; if (e.active) e.frozenValue = nextText; emit dataChanged(index(row, 4), index(row, 4), {Qt::DisplayRole, Qt::EditRole}); @@ -6399,7 +6422,7 @@ bool AddressListModel::adjustEntryValue(int row, double delta) { } auto written = writeValueToProcess(proc_, e.address, e.type, nextText, e.codec, e.bigEndian, e.showAsHex, 0, e.dataByteOrder, e.pointerWidth); - if (!written) { reportActivationError("Value edit failed", QString::fromStdString(written.error())); return false; } + if (!written) { reportActivationError(tr("Value edit failed"), QString::fromStdString(written.error())); return false; } e.currentValue = nextText; if (e.active) e.frozenValue = nextText; emit dataChanged(index(row, 4), index(row, 4), {Qt::DisplayRole, Qt::EditRole}); @@ -6490,8 +6513,8 @@ bool AddressListModel::setEntryActive(int row, bool active) { const QString script = entries_[row].autoAsmScript; if (!script.isEmpty() || pending) { if (!proc_ || !autoAsm_) { - reportActivationError("Process required", - "Open a process before activating this auto-assembler record."); + reportActivationError(tr("Process required"), + tr("Open a process before activating this auto-assembler record.")); return false; } @@ -6516,7 +6539,7 @@ bool AddressListModel::setEntryActive(int row, bool active) { if (!result.success) { if (hasSavedUndo(entries_[row].autoAsmDisableInfo)) entries_[row].active = true; emit dataChanged(index(row, 0), index(row, columnCount() - 1)); - reportActivationError(active ? "Auto-assembler activation failed" : "Auto-assembler deactivation failed", + reportActivationError(active ? tr("Auto-assembler activation failed") : tr("Auto-assembler deactivation failed"), QString::fromStdString(result.error)); return false; } @@ -6538,7 +6561,7 @@ bool AddressListModel::setEntryActive(int row, bool active) { const bool text = entries_[row].type == ValueType::String || entries_[row].type == ValueType::UnicodeString; // An empty string is a value a String record can hold (and freeze). if (current == QStringLiteral("??") || current == QStringLiteral("?") || (current.isEmpty() && !text)) { - reportActivationError("Freeze failed", "The value cannot be read right now, so there is nothing to freeze."); + reportActivationError(tr("Freeze failed"), tr("The value cannot be read right now, so there is nothing to freeze.")); return false; } } @@ -6702,11 +6725,11 @@ int AddressListModel::columnCount(const QModelIndex&) const { return 5; } QVariant AddressListModel::headerData(int section, Qt::Orientation o, int role) const { if (role != Qt::DisplayRole || o != Qt::Horizontal) return {}; switch (section) { - case 0: return "Active"; - case 1: return "Description"; - case 2: return "Address"; - case 3: return "Type"; - case 4: return "Value"; + case 0: return tr("Active"); + case 1: return tr("Description"); + case 2: return tr("Address"); + case 3: return tr("Type"); + case 4: return tr("Value"); default: return {}; } } @@ -7051,15 +7074,15 @@ bool AddressListModel::setData(const QModelIndex& index, const QVariant& value, options.pointerWidth = e.pointerWidth ? e.pointerWidth : proc_ ? proc_->pointerWidth(e.address) : sizeof(uintptr_t); if (proc_) { if (!reresolveAddress(e)) { - reportActivationError("Value edit failed", "The pointer path does not resolve right now."); + reportActivationError(tr("Value edit failed"), tr("The pointer path does not resolve right now.")); return false; } auto resolved = ce::valueOptionsForTarget(*proc_, e.address, e.type, options); - if (!resolved) { reportActivationError("Value edit failed", QString::fromStdString(resolved.error())); return false; } + if (!resolved) { reportActivationError(tr("Value edit failed"), QString::fromStdString(resolved.error())); return false; } options = *resolved; } auto bytes = ce::encodeTypedValue(e.type, rawValue.toStdString(), options); - if (!bytes) { reportActivationError("Value edit failed", QString::fromStdString(bytes.error())); return false; } + if (!bytes) { reportActivationError(tr("Value edit failed"), QString::fromStdString(bytes.error())); return false; } size_t nextByteCount = bytes->size(); if (e.type == ValueType::String || e.type == ValueType::UnicodeString) { size_t capacity = e.byteCount; @@ -7072,11 +7095,11 @@ bool AddressListModel::setData(const QModelIndex& index, const QVariant& value, } if (proc_) { if (!reresolveAddress(e)) { - reportActivationError("Value edit failed", "The pointer path does not resolve right now."); + reportActivationError(tr("Value edit failed"), tr("The pointer path does not resolve right now.")); return false; } auto written = ce::writeTypedValue(*proc_, e.address, e.type, rawValue.toStdString(), options); - if (!written) { reportActivationError("Value edit failed", QString::fromStdString(written.error())); return false; } + if (!written) { reportActivationError(tr("Value edit failed"), QString::fromStdString(written.error())); return false; } if (!e.active) scheduleEditVerify(e.address, e.type, rawValue, e.codec, e.bigEndian, e.showAsSigned, e.dataByteOrder, e.pointerWidth); } if (e.type == ValueType::String || e.type == ValueType::UnicodeString || e.type == ValueType::ByteArray) diff --git a/gui/memorybrowser.cpp b/gui/memorybrowser.cpp index 94aa7513..fb89980d 100644 --- a/gui/memorybrowser.cpp +++ b/gui/memorybrowser.cpp @@ -259,38 +259,38 @@ void HexView::contextMenuEvent(QContextMenuEvent* e) { uintptr_t selAddr = selCount > 0 ? address_ + (uintptr_t)selLo : addr; QMenu menu(this); - menu.addAction(QString("Address: 0x%1").arg(addr, 16, 16, QChar('0')))->setEnabled(false); + menu.addAction(tr("Address: 0x%1").arg(addr, 16, 16, QChar('0')))->setEnabled(false); menu.addSeparator(); - auto* copyAddr = menu.addAction("Copy address"); + auto* copyAddr = menu.addAction(tr("Copy address")); auto* copyByte = haveByte - ? menu.addAction(QString("Copy byte (0x%1)").arg(selByte, 2, 16, QChar('0'))) + ? menu.addAction(tr("Copy byte (0x%1)").arg(selByte, 2, 16, QChar('0'))) : nullptr; // Selection-based copy appears only for a real multi-byte range (a drag or // shift-click); a single byte keeps the fixed "Copy 16 bytes" shortcut. QAction* copySelAob = selCount > 1 - ? menu.addAction(QString("Copy selection as AOB (%1 bytes)").arg(selCount)) : nullptr; + ? menu.addAction(tr("Copy selection as AOB (%1 bytes)").arg(selCount)) : nullptr; QAction* copySelHex = selCount > 1 - ? menu.addAction("Copy selection (hex, no spaces)") : nullptr; + ? menu.addAction(tr("Copy selection (hex, no spaces)")) : nullptr; QAction* fillSel = selCount > 1 - ? menu.addAction(QString("Fill selection (%1 bytes) with…").arg(selCount)) : nullptr; - auto* copyAob = menu.addAction("Copy 16 bytes as AOB"); - auto* gotoAct = menu.addAction("Goto…"); - auto* followPtr = menu.addAction("Follow pointer here (qword)"); - auto* addToList = menu.addAction("Add address to the list"); + ? menu.addAction(tr("Fill selection (%1 bytes) with…").arg(selCount)) : nullptr; + auto* copyAob = menu.addAction(tr("Copy 16 bytes as AOB")); + auto* gotoAct = menu.addAction(tr("Goto…")); + auto* followPtr = menu.addAction(tr("Follow pointer here (qword)")); + auto* addToList = menu.addAction(tr("Add address to the list")); // Paste an AOB from the clipboard into memory at the cursor (patch), CE-style. const auto clipAob = parseAob(QApplication::clipboard()->text()); QAction* pasteAct = clipAob.empty() ? nullptr - : menu.addAction(QString("Paste %1 bytes here").arg(clipAob.size())); + : menu.addAction(tr("Paste %1 bytes here").arg(clipAob.size())); menu.addSeparator(); - auto* findWrites = menu.addAction("Find what writes this address"); - auto* findAccesses = menu.addAction("Find what accesses this address"); + auto* findWrites = menu.addAction(tr("Find what writes this address")); + auto* findAccesses = menu.addAction(tr("Find what accesses this address")); // Bytes-per-row display option (CE's memory-view width). bytesPerRow_ // parameterizes paint/hit-testing/refresh, so changing it adapts everything. menu.addSeparator(); - auto* bprMenu = menu.addMenu("Bytes per row"); + auto* bprMenu = menu.addMenu(tr("Bytes per row")); // 64 divides by every display-type group size (1/2/4/8), so grouping stays valid. for (int n : {8, 16, 32, 64}) { auto* a = bprMenu->addAction(QString::number(n)); @@ -306,14 +306,17 @@ void HexView::contextMenuEvent(QContextMenuEvent* e) { } // Display type (CE's "Display as"): interpret the grid as byte/word/dword/etc. - auto* dtMenu = menu.addMenu("Display type"); + auto* dtMenu = menu.addMenu(tr("Display type")); const std::pair dtChoices[] = { - {"Byte", DisplayType::Byte}, {"Word (2)", DisplayType::Word}, - {"Dword (4)", DisplayType::Dword}, {"Qword (8)", DisplayType::Qword}, - {"Float", DisplayType::Float}, {"Double", DisplayType::Double}, + {QT_TRANSLATE_NOOP("ce::gui::HexView", "Byte"), DisplayType::Byte}, + {QT_TRANSLATE_NOOP("ce::gui::HexView", "Word (2)"), DisplayType::Word}, + {QT_TRANSLATE_NOOP("ce::gui::HexView", "Dword (4)"), DisplayType::Dword}, + {QT_TRANSLATE_NOOP("ce::gui::HexView", "Qword (8)"), DisplayType::Qword}, + {QT_TRANSLATE_NOOP("ce::gui::HexView", "Float"), DisplayType::Float}, + {QT_TRANSLATE_NOOP("ce::gui::HexView", "Double"), DisplayType::Double}, }; for (auto& [name, dt] : dtChoices) { - auto* a = dtMenu->addAction(name); + auto* a = dtMenu->addAction(tr(name)); a->setCheckable(true); a->setChecked(displayType_ == dt); connect(a, &QAction::triggered, this, [this, dt]() { @@ -359,7 +362,7 @@ void HexView::contextMenuEvent(QContextMenuEvent* e) { } else if (fillSel && picked == fillSel) { // Fill the selected range with one byte value (patch), CE-style. bool ok = false; - QString v = QInputDialog::getText(this, "Fill selection", "Fill byte (hex):", + QString v = QInputDialog::getText(this, tr("Fill selection"), tr("Fill byte (hex):"), QLineEdit::Normal, "00", &ok); bool hok = false; int b = v.trimmed().toInt(&hok, 16); @@ -1070,49 +1073,50 @@ void DisasmView::contextMenuEvent(QContextMenuEvent* e) { for (const auto& in : selInsts) selTotalBytes += (int)in.size; QMenu menu(this); - menu.addAction(QString("Address: 0x%1").arg(inst.address, 16, 16, QChar('0')))->setEnabled(false); + menu.addAction(tr("Address: 0x%1").arg(inst.address, 16, 16, QChar('0')))->setEnabled(false); menu.addSeparator(); - auto* copyAddr = menu.addAction("Copy address"); - auto* copyBytes = menu.addAction(multi ? QString("Copy bytes (%1 instructions)").arg(selInsts.size()) - : QString("Copy bytes")); - auto* copyLine = menu.addAction(multi ? QString("Copy lines (%1)").arg(selInsts.size()) - : QString("Copy line")); + auto* copyAddr = menu.addAction(tr("Copy address")); + auto* copyBytes = menu.addAction(multi ? tr("Copy bytes (%1 instructions)").arg(selInsts.size()) + : tr("Copy bytes")); + auto* copyLine = menu.addAction(multi ? tr("Copy lines (%1)").arg(selInsts.size()) + : tr("Copy line")); menu.addSeparator(); QAction* followAct = nullptr; bool isBranch = inst.mnemonic == "call" || inst.mnemonic == "jmp" || (inst.mnemonic.size() > 1 && inst.mnemonic[0] == 'j'); if ((isBranch && parseImmediate(inst.operands)) || ripEffectiveAddress(inst)) - followAct = menu.addAction("Follow operand"); + followAct = menu.addAction(tr("Follow operand")); bool haveBp = breakpoints_.count(inst.address) > 0; QAction* setSwBp = nullptr; QAction* setHwBp = nullptr; QAction* removeBp = nullptr; if (haveBp) { - removeBp = menu.addAction("Remove breakpoint"); + removeBp = menu.addAction(tr("Remove breakpoint")); } else { - setSwBp = menu.addAction("Set software breakpoint"); - setHwBp = menu.addAction("Set hardware exec breakpoint"); + setSwBp = menu.addAction(tr("Set software breakpoint")); + setHwBp = menu.addAction(tr("Set hardware exec breakpoint")); } menu.addSeparator(); - auto* labelAct = menu.addAction("Label this address…"); - auto* commentAct = menu.addAction("Set comment…"); - auto* addListAct = menu.addAction("Add this address to the list"); - auto* xrefAct = menu.addAction("Find references to this address"); - auto* accessAct = menu.addAction("Find out what addresses this instruction accesses"); - auto* asmAct = menu.addAction("Assemble instruction…"); + auto* labelAct = menu.addAction(tr("Label this address…")); + auto* commentAct = menu.addAction(tr("Set comment…")); + auto* addListAct = menu.addAction(tr("Add this address to the list")); + auto* xrefAct = menu.addAction(tr("Find references to this address")); + auto* accessAct = menu.addAction(tr("Find out what addresses this instruction accesses")); + auto* asmAct = menu.addAction(tr("Assemble instruction…")); auto* nopAct = menu.addAction(multi - ? QString("NOP %1 instructions (%2 bytes)").arg(selInsts.size()).arg(selTotalBytes) - : QString("NOP this instruction (%1 byte%2)").arg(inst.size).arg(inst.size == 1 ? "" : "s")); + ? tr("NOP %1 instructions (%2 bytes)").arg(selInsts.size()).arg(selTotalBytes) + : inst.size == 1 ? tr("NOP this instruction (%1 byte)").arg(inst.size) + : tr("NOP this instruction (%1 bytes)").arg(inst.size)); // Auto Assemble injection templates, pre-filled for the pointed-at instruction. - auto* aaMenu = menu.addMenu("Auto Assemble"); - auto* codeInjAct = aaMenu->addAction("Create code injection here"); - auto* aobInjAct = aaMenu->addAction("Create AOB injection here"); - auto* saveAct = menu.addAction("Save region to file…"); - auto* loadAct = menu.addAction("Load region from file…"); + auto* aaMenu = menu.addMenu(tr("Auto Assemble")); + auto* codeInjAct = aaMenu->addAction(tr("Create code injection here")); + auto* aobInjAct = aaMenu->addAction(tr("Create AOB injection here")); + auto* saveAct = menu.addAction(tr("Save region to file…")); + auto* loadAct = menu.addAction(tr("Load region from file…")); QAction* picked = menu.exec(e->globalPos()); if (!picked) return; @@ -1218,11 +1222,11 @@ void DisasmView::refresh() { const bool denied = !r && (r.error() == std::errc::operation_not_permitted || r.error() == std::errc::permission_denied); emptyReason_ = denied - ? QStringLiteral("Cannot read this process's memory (permission denied).\n" - "Run Cheat Engine with ptrace rights: sudo setcap " - "cap_sys_ptrace+ep , run as root, or set " - "kernel.yama.ptrace_scope=0.") - : QStringLiteral("No readable memory at this address."); + ? tr("Cannot read this process's memory (permission denied).\n" + "Run Cheat Engine with ptrace rights: sudo setcap " + "cap_sys_ptrace+ep , run as root, or set " + "kernel.yama.ptrace_scope=0.") + : tr("No readable memory at this address."); viewport()->update(); return; } @@ -1321,7 +1325,7 @@ void DisasmView::paintEvent(QPaintEvent*) { p.setPen(mv.ascii); p.drawText(viewport()->rect().adjusted(24, 24, -24, -24), Qt::AlignCenter | Qt::TextWordWrap, - emptyReason_.isEmpty() ? QStringLiteral("No data at this address.") + emptyReason_.isEmpty() ? tr("No data at this address.") : emptyReason_); return; } @@ -1564,7 +1568,7 @@ void DisasmView::paintEvent(QPaintEvent*) { // Conditional branch: will it be taken given the flags? if (inst.mnemonic != "jmp") { if (auto jt = ce::conditionalJumpTaken(inst.mnemonic, currentCtx_.rflags)) - annotation += *jt ? QStringLiteral(" (will jump)") : QStringLiteral(" (no jump)"); + annotation += QStringLiteral(" ") + (*jt ? tr("(will jump)") : tr("(no jump)")); } } @@ -1753,8 +1757,8 @@ void DisasmView::wheelEvent(QWheelEvent* e) { void MemoryBrowser::buildMenuBar() { auto* mb = menuBar(); - auto* file = mb->addMenu("&File"); - file->addAction("New window", this, [this]() { + auto* file = mb->addMenu(tr("&File")); + file->addAction(tr("New window"), this, [this]() { // Prefer the owner's opener so the new viewer is tracked (and frozen on // target exit); the raw fallback is only for a browser opened standalone. if (newWindowOpener_) { newWindowOpener_(currentAddr_); return; } @@ -1767,20 +1771,20 @@ void MemoryBrowser::buildMenuBar() { file->addSeparator(); // CE keeps Save/Load memory region in the File menu too (also on the right-click); // these act at the address currently in view. - file->addAction("Save region to file…", this, [this]() { saveRegionToFile(currentAddr_); }); - file->addAction("Load region from file…", this, [this]() { loadRegionFromFile(currentAddr_); }); + file->addAction(tr("Save region to file…"), this, [this]() { saveRegionToFile(currentAddr_); }); + file->addAction(tr("Load region from file…"), this, [this]() { loadRegionFromFile(currentAddr_); }); - auto* search = mb->addMenu("&Search"); + auto* search = mb->addMenu(tr("&Search")); searchMenu_ = search; - { auto* a = search->addAction("Find memory...", this, [this]() { findInMemory(false); }); + { auto* a = search->addAction(tr("Find memory..."), this, [this]() { findInMemory(false); }); a->setShortcut(QKeySequence::Find); } - { auto* a = search->addAction("Find next", this, [this]() { findInMemory(true); }); + { auto* a = search->addAction(tr("Find next"), this, [this]() { findInMemory(true); }); a->setShortcut(QKeySequence("F3")); } - { auto* a = search->addAction("Find previous", this, [this]() { findInMemory(true, /*backward=*/true); }); + { auto* a = search->addAction(tr("Find previous"), this, [this]() { findInMemory(true, /*backward=*/true); }); a->setShortcut(QKeySequence("Shift+F3")); } - viewMenu_ = mb->addMenu("&View"); - ownViewActions_.push_back(viewMenu_->addAction("Disassembler preferences...", this, [this]() { + viewMenu_ = mb->addMenu(tr("&View")); + ownViewActions_.push_back(viewMenu_->addAction(tr("Disassembler preferences..."), this, [this]() { auto* dlg = new ce::gui::MemviewPreferences(this); connect(dlg, &ce::gui::MemviewPreferences::applied, this, [this]() { disasmView_->reloadPreferences(); }); @@ -1789,35 +1793,35 @@ void MemoryBrowser::buildMenuBar() { })); viewMenu_->addSeparator(); - debugMenu_ = mb->addMenu("&Debug"); + debugMenu_ = mb->addMenu(tr("&Debug")); // Triggers the toolbar's Toggle BP (below), which owns F5: two actions with // the same key in one window make Qt run neither. - toggleBreakpointMenu_ = debugMenu_->addAction("Toggle breakpoint\tF5"); // the hint only + toggleBreakpointMenu_ = debugMenu_->addAction(tr("Toggle breakpoint\tF5")); // the hint only debugMenu_->addSeparator(); // Step / run controls (CE parity: the Memory Viewer drives the debug session). // Each delegates to the Debugger window that owns the session; a no-op when the // target is not paused. The current line re-highlights via MainWindow on stop. - { auto* a = debugMenu_->addAction("Run", this, [this]() { if (runFn_) runFn_(); }); + { auto* a = debugMenu_->addAction(tr("Run"), this, [this]() { if (runFn_) runFn_(); }); a->setShortcut(QKeySequence("F9")); } - { auto* a = debugMenu_->addAction("Step into", this, [this]() { if (stepIntoFn_) stepIntoFn_(); }); + { auto* a = debugMenu_->addAction(tr("Step into"), this, [this]() { if (stepIntoFn_) stepIntoFn_(); }); a->setShortcut(QKeySequence("F7")); } - { auto* a = debugMenu_->addAction("Step over", this, [this]() { if (stepOverFn_) stepOverFn_(); }); + { auto* a = debugMenu_->addAction(tr("Step over"), this, [this]() { if (stepOverFn_) stepOverFn_(); }); a->setShortcut(QKeySequence("F8")); } - { auto* a = debugMenu_->addAction("Run to cursor", this, [this]() { + { auto* a = debugMenu_->addAction(tr("Run to cursor"), this, [this]() { uintptr_t addr = disasmView_->selectedAddress(); if (!addr) addr = currentAddr_; if (addr && runToCursorFn_) runToCursorFn_(addr); }); a->setShortcut(QKeySequence("F4")); } - toolsMenu_ = mb->addMenu("&Tools"); - toolsMenu_->addAction("Auto Assemble...", this, [this]() { + toolsMenu_ = mb->addMenu(tr("&Tools")); + toolsMenu_->addAction(tr("Auto Assemble..."), this, [this]() { if (autoAssembleOpener_) { auto [address, size] = injectionSelection(); autoAssembleOpener_(QString(), address, size); } }); - toolsMenu_->addAction("Dissect data/structures...", this, [this]() { + toolsMenu_->addAction(tr("Dissect data/structures..."), this, [this]() { if (dissectOpener_) dissectOpener_(currentAddr_); // at the current address }); } @@ -1848,7 +1852,7 @@ MemoryBrowser::MemoryBrowser(ProcessHandle* proc, QWidget* parent) : QMainWindow(parent), proc_(proc) { if (proc_) modules_ = proc_->modules(); // for module+offset in the status bar - setWindowTitle("Memory Viewer"); + setWindowTitle(tr("Memory Viewer")); resize(900, 600); buildMenuBar(); // CE keeps the memory/debug tools in this window's menu bar @@ -1856,15 +1860,15 @@ MemoryBrowser::MemoryBrowser(ProcessHandle* proc, QWidget* parent) auto* toolbar = new QToolBar; toolbar->setMovable(false); backAct_ = toolbar->addAction("◀"); - backAct_->setToolTip("Back (Alt+Left)"); + backAct_->setToolTip(tr("Back (Alt+Left)")); backAct_->setEnabled(false); connect(backAct_, &QAction::triggered, this, &MemoryBrowser::goBack); fwdAct_ = toolbar->addAction("▶"); - fwdAct_->setToolTip("Forward (Alt+Right)"); + fwdAct_->setToolTip(tr("Forward (Alt+Right)")); fwdAct_->setEnabled(false); connect(fwdAct_, &QAction::triggered, this, &MemoryBrowser::goForward); toolbar->addSeparator(); - toolbar->addWidget(new QLabel(" Address: ")); + toolbar->addWidget(new QLabel(tr(" Address: "))); addressEdit_ = new QLineEdit; addressEdit_->setFont(monospaceFont()); addressEdit_->setMinimumWidth(220); @@ -1873,22 +1877,22 @@ MemoryBrowser::MemoryBrowser(ProcessHandle* proc, QWidget* parent) connect(addressEdit_, &QLineEdit::returnPressed, this, &MemoryBrowser::onGotoAddress); toolbar->addWidget(addressEdit_); - auto* goBtn = new QPushButton("Go"); + auto* goBtn = new QPushButton(tr("Go")); connect(goBtn, &QPushButton::clicked, this, &MemoryBrowser::onGotoAddress); toolbar->addWidget(goBtn); toolbar->addSeparator(); - auto* refreshBtn = new QPushButton("Refresh"); + auto* refreshBtn = new QPushButton(tr("Refresh")); connect(refreshBtn, &QPushButton::clicked, this, &MemoryBrowser::onRefresh); toolbar->addWidget(refreshBtn); toolbar->addSeparator(); auto* bmToggleBtn = new QPushButton("★"); - bmToggleBtn->setToolTip("Toggle bookmark at current address (Ctrl+B)"); + bmToggleBtn->setToolTip(tr("Toggle bookmark at current address (Ctrl+B)")); connect(bmToggleBtn, &QPushButton::clicked, this, &MemoryBrowser::toggleBookmark); toolbar->addWidget(bmToggleBtn); auto* bmBtn = new QToolButton; - bmBtn->setText("Bookmarks"); + bmBtn->setText(tr("Bookmarks")); bmBtn->setPopupMode(QToolButton::InstantPopup); bookmarksMenu_ = new QMenu(bmBtn); bmBtn->setMenu(bookmarksMenu_); @@ -1902,11 +1906,11 @@ MemoryBrowser::MemoryBrowser(ProcessHandle* proc, QWidget* parent) // window owns (CE parity: the Memory Viewer controls stepping); each is a no-op // until the target is paused, and the "Debugger" button opens/raises that window. addToolBarBreak(); - auto* dbgBar = new QToolBar("Debug"); + auto* dbgBar = new QToolBar(tr("Debug")); debugBar_ = dbgBar; dbgBar->setMovable(false); - auto* bpAct = dbgBar->addAction("Toggle BP"); - bpAct->setToolTip("Toggle breakpoint at the selected instruction (F5)"); + auto* bpAct = dbgBar->addAction(tr("Toggle BP")); + bpAct->setToolTip(tr("Toggle breakpoint at the selected instruction (F5)")); bpAct->setShortcut(QKeySequence("F5")); if (toggleBreakpointMenu_) connect(toggleBreakpointMenu_, &QAction::triggered, bpAct, &QAction::trigger); connect(bpAct, &QAction::triggered, this, [this]() { @@ -1926,17 +1930,17 @@ MemoryBrowser::MemoryBrowser(ProcessHandle* proc, QWidget* parent) // Step / run the paused target from the Memory Viewer (F9/F7/F8). These delegate // to the debug session in the Debugger window; the current line re-highlights // here on each stop. Harmless no-ops when nothing is paused. - { auto* a = dbgBar->addAction("Run"); - a->setToolTip("Continue the paused target (F9)"); + { auto* a = dbgBar->addAction(tr("Run")); + a->setToolTip(tr("Continue the paused target (F9)")); connect(a, &QAction::triggered, this, [this]() { if (runFn_) runFn_(); }); } - { auto* a = dbgBar->addAction("Step"); - a->setToolTip("Step into one instruction (F7)"); + { auto* a = dbgBar->addAction(tr("Step")); + a->setToolTip(tr("Step into one instruction (F7)")); connect(a, &QAction::triggered, this, [this]() { if (stepIntoFn_) stepIntoFn_(); }); } - { auto* a = dbgBar->addAction("Step over"); - a->setToolTip("Step over one instruction (F8)"); + { auto* a = dbgBar->addAction(tr("Step over")); + a->setToolTip(tr("Step over one instruction (F8)")); connect(a, &QAction::triggered, this, [this]() { if (stepOverFn_) stepOverFn_(); }); } - { auto* a = dbgBar->addAction("Run to cursor"); - a->setToolTip("Continue until the selected instruction (F4)"); + { auto* a = dbgBar->addAction(tr("Run to cursor")); + a->setToolTip(tr("Continue until the selected instruction (F4)")); connect(a, &QAction::triggered, this, [this]() { uintptr_t addr = disasmView_->selectedAddress(); if (!addr) addr = currentAddr_; @@ -1945,8 +1949,8 @@ MemoryBrowser::MemoryBrowser(ProcessHandle* proc, QWidget* parent) dbgBar->addSeparator(); // The full Debugger window owns the debug session; this opens/raises it at the // selected address so breakpoints, watches, and the register view are available. - auto* dbgAct = dbgBar->addAction("Debugger"); - dbgAct->setToolTip("Open the Debugger here to run, step, and set breakpoints"); + auto* dbgAct = dbgBar->addAction(tr("Debugger")); + dbgAct->setToolTip(tr("Open the Debugger here to run, step, and set breakpoints")); connect(dbgAct, &QAction::triggered, this, [this]() { uintptr_t addr = disasmView_->selectedAddress(); if (!addr) addr = currentAddr_; @@ -1954,9 +1958,9 @@ MemoryBrowser::MemoryBrowser(ProcessHandle* proc, QWidget* parent) }); dbgBar->addSeparator(); // Disassembler Preferences (CE frmMemviewPreferencesUnit): font + colors. - auto* prefsAct = dbgBar->addAction("Preferences"); + auto* prefsAct = dbgBar->addAction(tr("Preferences")); ownViewActions_.push_back(prefsAct); - prefsAct->setToolTip("Disassembler Preferences: font and colors"); + prefsAct->setToolTip(tr("Disassembler Preferences: font and colors")); connect(prefsAct, &QAction::triggered, this, [this]() { auto* dlg = new ce::gui::MemviewPreferences(this); connect(dlg, &ce::gui::MemviewPreferences::applied, this, @@ -2019,7 +2023,7 @@ MemoryBrowser::MemoryBrowser(ProcessHandle* proc, QWidget* parent) registerPanel_->setItem(i, 1, new QTableWidgetItem(QStringLiteral("-"))); } registerPanel_->resizeColumnsToContents(); - registerPanel_->setToolTip("Registers (populated at a debug breakpoint)"); + registerPanel_->setToolTip(tr("Registers (populated at a debug breakpoint)")); auto* topSplit = new QSplitter(Qt::Horizontal); topSplit->setObjectName("memTopSplit"); // persisted across runs @@ -2038,13 +2042,13 @@ MemoryBrowser::MemoryBrowser(ProcessHandle* proc, QWidget* parent) stacktracePanel_ = new QTableWidget; stacktracePanel_->setColumnCount(3); stacktracePanel_->setHorizontalHeaderLabels( - {"Address", is32 ? "DWORD" : "QWORD", "Value"}); + {tr("Address"), is32 ? "DWORD" : "QWORD", tr("Value")}); stacktracePanel_->verticalHeader()->setVisible(false); stacktracePanel_->setEditTriggers(QAbstractItemView::NoEditTriggers); stacktracePanel_->setSelectionBehavior(QAbstractItemView::SelectRows); stacktracePanel_->setProperty("monospace", true); // font from the theme sheet stacktracePanel_->horizontalHeader()->setStretchLastSection(true); - stacktracePanel_->setToolTip("Call stack (populated at a debug breakpoint)"); + stacktracePanel_->setToolTip(tr("Call stack (populated at a debug breakpoint)")); // Double-clicking a stack entry follows its return address in the disassembler. connect(stacktracePanel_, &QTableWidget::cellDoubleClicked, this, [this](int row, int) { @@ -2079,7 +2083,7 @@ MemoryBrowser::MemoryBrowser(ProcessHandle* proc, QWidget* parent) bool showCpu = QSettings().value("memview/cpuPanels", false).toBool(); registerPanel_->setVisible(showCpu); stacktracePanel_->setVisible(showCpu); - auto* cpuAct = viewMenu_->addAction("CPU registers && stack panels"); + auto* cpuAct = viewMenu_->addAction(tr("CPU registers && stack panels")); ownViewActions_.push_back(cpuAct); cpuAct->setCheckable(true); cpuAct->setChecked(showCpu); @@ -2095,8 +2099,8 @@ MemoryBrowser::MemoryBrowser(ProcessHandle* proc, QWidget* parent) connect(hexView_, &HexView::requestFindWhatAccesses, this, [this](uintptr_t addr, bool writesOnly) { if (!proc_) return; // detached: the launcher is gone with the target if (cfLauncher_) cfLauncher_(addr, writesOnly); - else QMessageBox::information(this, "Code finder unavailable", - "No code-finder launcher is wired to this memory browser instance."); + else QMessageBox::information(this, tr("Code finder unavailable"), + tr("No code-finder launcher is wired to this memory browser instance.")); }); connect(hexView_, &HexView::requestAddToList, this, [this](uintptr_t addr, ce::ValueType type) { if (addToList_) addToList_(addr, type); @@ -2119,7 +2123,7 @@ MemoryBrowser::MemoryBrowser(ProcessHandle* proc, QWidget* parent) QString head = modOff.empty() ? QString("0x%1 ").arg(addr, 0, 16) : QString("%1 (0x%2) ").arg(QString::fromStdString(modOff)).arg(addr, 0, 16); - if (got < 1) { statusBar()->showMessage(head + ""); return; } + if (got < 1) { statusBar()->showMessage(head + tr("")); return; } QString msg = head; int8_t i8; std::memcpy(&i8, b, 1); msg += QString("i8=%1 u8=%2").arg(i8).arg((unsigned)b[0]); @@ -2161,8 +2165,8 @@ MemoryBrowser::MemoryBrowser(ProcessHandle* proc, QWidget* parent) connect(disasmView_, &DisasmView::requestSetComment, this, [this](uintptr_t addr) { bool ok = false; QString cur = QString::fromStdString(disasmView_->comment(addr)); - QString text = QInputDialog::getText(this, "Comment", - QString("Comment for 0x%1 (blank to clear):").arg(addr, 0, 16), + QString text = QInputDialog::getText(this, tr("Comment"), + tr("Comment for 0x%1 (blank to clear):").arg(addr, 0, 16), QLineEdit::Normal, cur, &ok); if (ok) editComment(addr, text.trimmed().toStdString()); }); @@ -2175,8 +2179,8 @@ MemoryBrowser::MemoryBrowser(ProcessHandle* proc, QWidget* parent) const auto label = labels.find(addr); std::string existing = label != labels.end() ? label->second : std::string(); bool ok = false; - QString name = QInputDialog::getText(this, "Label address", - QString("Name for 0x%1 (blank to clear):").arg(addr, 0, 16), + QString name = QInputDialog::getText(this, tr("Label address"), + tr("Name for 0x%1 (blank to clear):").arg(addr, 0, 16), QLineEdit::Normal, QString::fromStdString(existing), &ok); if (!ok) return; const std::string trimmed = name.trimmed().toStdString(); @@ -2198,8 +2202,9 @@ MemoryBrowser::MemoryBrowser(ProcessHandle* proc, QWidget* parent) std::string err; std::string script = ce::generateInjectionScript(*proc_, addr, aob, err, size); if (script.empty()) { - QMessageBox::warning(this, "Auto Assemble", - QString::fromStdString(err.empty() ? "Could not generate a template here." : err)); + QMessageBox::warning(this, tr("Auto Assemble"), + err.empty() ? tr("Could not generate a template here.") + : QString::fromStdString(err)); return; } // Hand the pre-filled script to MainWindow, which opens a script editor @@ -2224,8 +2229,8 @@ MemoryBrowser::MemoryBrowser(ProcessHandle* proc, QWidget* parent) auto* gotoShortcut = new QShortcut(QKeySequence("Ctrl+G"), this); connect(gotoShortcut, &QShortcut::activated, this, [this]() { bool ok; - auto text = QInputDialog::getText(this, "Goto Address", - "Address, symbol, or expression (e.g. worker, target2+0x100):", + auto text = QInputDialog::getText(this, tr("Goto Address"), + tr("Address, symbol, or expression (e.g. worker, target2+0x100):"), QLineEdit::Normal, addressEdit_->text(), &ok); if (ok && !text.isEmpty()) { addressEdit_->setText(text); @@ -2409,8 +2414,8 @@ void MemoryBrowser::detachFromTarget() { }; for (QWidget* host : {static_cast(viewMenu_), static_cast(debugBar_)}) if (host) for (QAction* a : host->actions()) if (needsTarget(a)) a->setEnabled(false); - setWindowTitle("Memory Viewer (target exited)"); - statusBar()->showMessage("Target process exited; this view is frozen.", 0); + setWindowTitle(tr("Memory Viewer (target exited)")); + statusBar()->showMessage(tr("Target process exited; this view is frozen."), 0); } void MemoryBrowser::addUserSymbols(const std::map& syms) { @@ -2437,7 +2442,7 @@ void MemoryBrowser::toggleBookmark() { void MemoryBrowser::rebuildBookmarksMenu() { bookmarksMenu_->clear(); if (bookmarks_.empty()) { - bookmarksMenu_->addAction("(no bookmarks; Ctrl+B to add)")->setEnabled(false); + bookmarksMenu_->addAction(tr("(no bookmarks; Ctrl+B to add)"))->setEnabled(false); return; } for (uintptr_t a : bookmarks_) { @@ -2447,7 +2452,7 @@ void MemoryBrowser::rebuildBookmarksMenu() { bookmarksMenu_->addAction(label, this, [this, a]() { navigateTo(a); }); } bookmarksMenu_->addSeparator(); - bookmarksMenu_->addAction("Clear all bookmarks", this, [this]() { + bookmarksMenu_->addAction(tr("Clear all bookmarks"), this, [this]() { bookmarks_.clear(); rebuildBookmarksMenu(); }); @@ -2466,7 +2471,7 @@ void MemoryBrowser::onGotoAddress() { bool ok; uintptr_t addr = text.toULongLong(&ok, 16); if (ok) gotoAddress(addr); - else QMessageBox::warning(this, "Goto", QString("Could not resolve \"%1\".").arg(text)); + else QMessageBox::warning(this, tr("Goto"), tr("Could not resolve \"%1\".").arg(text)); } void MemoryBrowser::onRefresh() { @@ -2487,7 +2492,7 @@ QString MemoryBrowser::breakpointConflict(uintptr_t addr, size_t len) const { if (bpQuery_) for (uintptr_t bp : bpQuery_()) if (bp >= addr && bp - addr < len) - return QString("A breakpoint is set at 0x%1: remove it before changing these bytes").arg(bp, 0, 16); + return tr("A breakpoint is set at 0x%1: remove it before changing these bytes").arg(bp, 0, 16); return {}; } @@ -2508,9 +2513,10 @@ void MemoryBrowser::writeNop(uintptr_t addr, int size) { QString error; if (!nops) error=QString::fromStdString(nops.error()); if (!nops || !patchBytes(addr, *nops, &error)) { - QMessageBox::warning(this, "NOP failed", - QString("Could not write %1 NOP byte%2 to 0x%3.\n%4") - .arg(size).arg(size == 1 ? "" : "s").arg(addr, 0, 16).arg(error)); + QMessageBox::warning(this, tr("NOP failed"), + size == 1 + ? tr("Could not write %1 NOP byte to 0x%2.\n%3").arg(size).arg(addr, 0, 16).arg(error) + : tr("Could not write %1 NOP bytes to 0x%2.\n%3").arg(size).arg(addr, 0, 16).arg(error)); return; } onRefresh(); @@ -2519,39 +2525,39 @@ void MemoryBrowser::writeNop(uintptr_t addr, int size) { void MemoryBrowser::assembleAt(uintptr_t addr, int origSize, const QString& current) { if (!proc_) return; bool ok = false; - QString text = QInputDialog::getText(this, "Assemble", - QString("Instruction to assemble at 0x%1:").arg(addr, 0, 16), + QString text = QInputDialog::getText(this, tr("Assemble"), + tr("Instruction to assemble at 0x%1:").arg(addr, 0, 16), QLineEdit::Normal, current, &ok); if (!ok || text.trimmed().isEmpty()) return; if (!proc_) return; // detached while the dialog was open auto architecture = ce::disassemblerArchFor(*proc_, addr); if (!architecture) { - QMessageBox::warning(this, "Assemble failed", QString::fromStdString(architecture.error())); + QMessageBox::warning(this, tr("Assemble failed"), QString::fromStdString(architecture.error())); return; } auto assemblerArch = ce::assemblerArchFor(*architecture); if (!assemblerArch) { - QMessageBox::warning(this, "Assemble failed", QString::fromStdString(assemblerArch.error())); + QMessageBox::warning(this, tr("Assemble failed"), QString::fromStdString(assemblerArch.error())); return; } ce::Assembler asmr(*assemblerArch); auto res = asmr.assemble(text.toStdString(), addr); if (!res) { - QMessageBox::warning(this, "Assemble failed", - QString("Could not assemble \"%1\":\n%2").arg(text, QString::fromStdString(res.error()))); + QMessageBox::warning(this, tr("Assemble failed"), + tr("Could not assemble \"%1\":\n%2").arg(text, QString::fromStdString(res.error()))); return; } std::vector bytes = *res; if (bytes.empty()) { - QMessageBox::warning(this, "Assemble failed", - QString("\"%1\" assembled to no bytes; not writing.").arg(text)); + QMessageBox::warning(this, tr("Assemble failed"), + tr("\"%1\" assembled to no bytes; not writing.").arg(text)); return; } if ((int)bytes.size() > origSize) { - auto ans = QMessageBox::question(this, "Longer instruction", - QString("The new instruction is %1 bytes but the original was %2. Writing it will " - "overwrite the following instruction(s). Continue?") + auto ans = QMessageBox::question(this, tr("Longer instruction"), + tr("The new instruction is %1 bytes but the original was %2. Writing it will " + "overwrite the following instruction(s). Continue?") .arg(bytes.size()).arg(origSize), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (ans != QMessageBox::Yes) return; @@ -2569,7 +2575,7 @@ void MemoryBrowser::assembleAt(uintptr_t addr, int origSize, const QString& curr } if (boundary > bytes.size()) { auto nops = ce::nopBytesFor(*proc_, addr + bytes.size(), boundary - bytes.size()); - if (!nops) { QMessageBox::warning(this, "Assemble failed", QString::fromStdString(nops.error())); return; } + if (!nops) { QMessageBox::warning(this, tr("Assemble failed"), QString::fromStdString(nops.error())); return; } bytes.insert(bytes.end(), nops->begin(), nops->end()); } } else { @@ -2578,15 +2584,15 @@ void MemoryBrowser::assembleAt(uintptr_t addr, int origSize, const QString& curr if ((int)bytes.size()(origSize)-bytes.size()); if (!nops) { - QMessageBox::warning(this,"Assemble failed",QString::fromStdString(nops.error()));return; + QMessageBox::warning(this,tr("Assemble failed"),QString::fromStdString(nops.error()));return; } bytes.insert(bytes.end(),nops->begin(),nops->end()); } } QString error; if (!patchBytes(addr, bytes, &error)) { - QMessageBox::warning(this, "Write failed", - QString("Assembled %1 bytes but could not write to 0x%2.\n%3").arg(bytes.size()).arg(addr, 0, 16).arg(error)); + QMessageBox::warning(this, tr("Write failed"), + tr("Assembled %1 bytes but could not write to 0x%2.\n%3").arg(bytes.size()).arg(addr, 0, 16).arg(error)); return; } onRefresh(); @@ -2652,8 +2658,8 @@ void MemoryBrowser::findInMemory(bool findNext, bool backward) { mask = lastSearchMask_; } else { bool ok = false; - QString s = QInputDialog::getText(this, "Find in memory", - "Bytes (e.g. 48 8B ?? 05) or \"text\":", QLineEdit::Normal, "", &ok); + QString s = QInputDialog::getText(this, tr("Find in memory"), + tr("Bytes (e.g. 48 8B ?? 05) or \"text\":"), QLineEdit::Normal, "", &ok); if (!ok || s.trimmed().isEmpty()) return; s = s.trimmed(); if (s.startsWith('"') && s.endsWith('"') && s.size() >= 2) { @@ -2663,8 +2669,8 @@ void MemoryBrowser::findInMemory(bool findNext, bool backward) { // AOB with optional "??" wildcards (48 8B ?? 05 / 488b??05). auto bytes = parseAob(s); if (bytes.empty()) { - QMessageBox::warning(this, "Find", - "Enter hex bytes (48 8B 05), a wildcard AOB (48 8B ?? 05), or a quoted \"string\"."); + QMessageBox::warning(this, tr("Find"), + tr("Enter hex bytes (48 8B 05), a wildcard AOB (48 8B ?? 05), or a quoted \"string\".")); return; } for (int b : bytes) { @@ -2682,9 +2688,9 @@ void MemoryBrowser::findInMemory(bool findNext, bool backward) { navigateTo(hit); if (hexView_) hexView_->selectBytes(hit, static_cast(pat.size())); // highlight the match } else { - QMessageBox::information(this, "Find", - backward ? "Not found searching backward from the current address." - : "Not found searching forward from the current address."); + QMessageBox::information(this, tr("Find"), + backward ? tr("Not found searching backward from the current address.") + : tr("Not found searching forward from the current address.")); } } @@ -2697,14 +2703,14 @@ void MemoryBrowser::showXrefs(uintptr_t addr) { if (addr >= m.base && addr < m.base + m.size) { mod = m; found = true; break; } } if (!found) { - QMessageBox::information(this, "References", - "This address is not inside a known module, so its references can't be scanned."); + QMessageBox::information(this, tr("References"), + tr("This address is not inside a known module, so its references can't be scanned.")); return; } auto architecture = ce::disassemblerArchFor(*proc_, mod.base); if (!architecture) { - QMessageBox::warning(this, "References", QString::fromStdString(architecture.error())); + QMessageBox::warning(this, tr("References"), QString::fromStdString(architecture.error())); return; } QApplication::setOverrideCursor(Qt::WaitCursor); @@ -2714,23 +2720,23 @@ void MemoryBrowser::showXrefs(uintptr_t addr) { auto* dlg = new QDialog(this); dlg->setAttribute(Qt::WA_DeleteOnClose); - dlg->setWindowTitle(QString("References to 0x%1: %2 found").arg(addr, 0, 16).arg(refs.size())); + dlg->setWindowTitle(tr("References to 0x%1: %2 found").arg(addr, 0, 16).arg(refs.size())); dlg->resize(640, 360); auto* lay = new QVBoxLayout(dlg); auto* table = new QTableWidget(dlg); table->setColumnCount(3); - table->setHorizontalHeaderLabels({"From", "Type", "Instruction"}); + table->setHorizontalHeaderLabels({tr("From"), tr("Type"), tr("Instruction")}); table->horizontalHeader()->setStretchLastSection(true); table->setEditTriggers(QAbstractItemView::NoEditTriggers); table->setSelectionBehavior(QAbstractItemView::SelectRows); table->setRowCount((int)refs.size()); - auto typeName = [](ce::RefType t) -> QString { + auto typeName = [this](ce::RefType t) -> QString { switch (t) { - case ce::RefType::Call: return "call"; - case ce::RefType::Jump: return "jump"; - case ce::RefType::String: return "string"; - case ce::RefType::RipRelative: return "data"; - default: return "ref"; + case ce::RefType::Call: return tr("call"); + case ce::RefType::Jump: return tr("jump"); + case ce::RefType::String: return tr("string"); + case ce::RefType::RipRelative: return tr("data"); + default: return tr("ref"); } }; for (int i = 0; i < (int)refs.size(); ++i) { @@ -2757,54 +2763,54 @@ void MemoryBrowser::showXrefs(uintptr_t addr) { void MemoryBrowser::saveRegionToFile(uintptr_t addr) { if (!proc_) return; bool ok = false; - int size = QInputDialog::getInt(this, "Save region", "Bytes to save:", 4096, 1, 1 << 28, 1, &ok); + int size = QInputDialog::getInt(this, tr("Save region"), tr("Bytes to save:"), 4096, 1, 1 << 28, 1, &ok); if (!ok) return; - auto path = QFileDialog::getSaveFileName(this, "Save region", - QString("region_%1_%2.bin").arg(addr, 0, 16).arg(size), "Binary (*.bin);;All (*)"); + auto path = QFileDialog::getSaveFileName(this, tr("Save region"), + QString("region_%1_%2.bin").arg(addr, 0, 16).arg(size), tr("Binary (*.bin);;All (*)")); if (path.isEmpty()) return; if (!proc_) return; // detached while the dialog was open std::vector buf(size); auto r = proc_->read(addr, buf.data(), size); if (!r) { - QMessageBox::warning(this, "Read failed", - QString("Could not read %1 bytes at 0x%2.").arg(size).arg(addr, 0, 16)); + QMessageBox::warning(this, tr("Read failed"), + tr("Could not read %1 bytes at 0x%2.").arg(size).arg(addr, 0, 16)); return; } const qint64 got = (qint64)std::min(*r, buf.size()); QFile f(path); if (!f.open(QIODevice::WriteOnly)) { - QMessageBox::warning(this, "Save failed", f.errorString()); + QMessageBox::warning(this, tr("Save failed"), f.errorString()); return; } if (f.write(reinterpret_cast(buf.data()), got) != got || !f.flush()) { - QMessageBox::warning(this, "Save failed", f.errorString()); + QMessageBox::warning(this, tr("Save failed"), f.errorString()); return; } // The region can end inside the request (an unmapped page): say the file // is shorter than asked rather than leave the user to find out. if (got < size) - QMessageBox::information(this, "Save region", - QString("Only %1 of %2 bytes at 0x%3 were readable; the file holds those %1 bytes.") + QMessageBox::information(this, tr("Save region"), + tr("Only %1 of %2 bytes at 0x%3 were readable; the file holds those %1 bytes.") .arg(got).arg(size).arg(addr, 0, 16)); } void MemoryBrowser::loadRegionFromFile(uintptr_t addr) { if (!proc_) return; - auto path = QFileDialog::getOpenFileName(this, "Load region into memory", QString(), - "Binary (*.bin);;All (*)"); + auto path = QFileDialog::getOpenFileName(this, tr("Load region into memory"), QString(), + tr("Binary (*.bin);;All (*)")); if (path.isEmpty()) return; QFile f(path); if (!f.open(QIODevice::ReadOnly)) { - QMessageBox::warning(this, "Load failed", f.errorString()); + QMessageBox::warning(this, tr("Load failed"), f.errorString()); return; } // Cap the load like the save side (256 MB) so a huge/wrong file can't exhaust memory // or blanket-overwrite the target before the confirmation. constexpr qint64 kMaxLoad = 1 << 28; if (f.size() > kMaxLoad) { - QMessageBox::warning(this, "File too large", - QString("The file is %1 bytes; the maximum region load is %2 bytes.") + QMessageBox::warning(this, tr("File too large"), + tr("The file is %1 bytes; the maximum region load is %2 bytes.") .arg(f.size()).arg((qint64)kMaxLoad)); return; } @@ -2813,21 +2819,21 @@ void MemoryBrowser::loadRegionFromFile(uintptr_t addr) { // Writing to the target's memory is destructive, so confirm the byte count and // destination before patching (CE-style "are you sure"). - if (QMessageBox::question(this, "Load region", - QString("Write %1 bytes from\n%2\nto 0x%3?") + if (QMessageBox::question(this, tr("Load region"), + tr("Write %1 bytes from\n%2\nto 0x%3?") .arg(data.size()).arg(path).arg(addr, 0, 16), QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) return; if (!proc_) return; // detached while the dialog was open if (QString conflict = breakpointConflict(addr, static_cast(data.size())); !conflict.isEmpty()) { - QMessageBox::warning(this, "Load region", conflict); + QMessageBox::warning(this, tr("Load region"), conflict); return; } auto w = proc_->write(addr, data.constData(), static_cast(data.size())); if (!w || *w < static_cast(data.size())) { - QMessageBox::warning(this, "Write failed", - QString("Wrote %1 of %2 bytes at 0x%3 (memory may be protected).") + QMessageBox::warning(this, tr("Write failed"), + tr("Wrote %1 of %2 bytes at 0x%3 (memory may be protected).") .arg(w ? (qulonglong)*w : 0).arg(data.size()).arg(addr, 0, 16)); } if (hexView_) hexView_->refresh(); // re-read so the patched bytes show now diff --git a/gui/memoryfill.cpp b/gui/memoryfill.cpp index 9f197733..a3de3fde 100644 --- a/gui/memoryfill.cpp +++ b/gui/memoryfill.cpp @@ -13,7 +13,7 @@ namespace ce::gui { MemoryFillDialog::MemoryFillDialog(ProcessHandle* proc, uintptr_t startAddr, QWidget* parent) : QDialog(parent), proc_(proc) { - setWindowTitle("Fill Memory"); + setWindowTitle(tr("Fill Memory")); resize(350, 150); auto* layout = new QVBoxLayout(this); @@ -24,21 +24,21 @@ MemoryFillDialog::MemoryFillDialog(ProcessHandle* proc, uintptr_t startAddr, QWi form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); addrEdit_ = new QLineEdit(QString("0x%1").arg(startAddr, 0, 16)); addrEdit_->setFont(monospaceFont()); - form->addRow("Start address:", addrEdit_); + form->addRow(tr("Start address:"), addrEdit_); sizeEdit_ = new QLineEdit("256"); - form->addRow("Size (bytes):", sizeEdit_); + form->addRow(tr("Size (bytes):"), sizeEdit_); valueEdit_ = new QLineEdit("00"); valueEdit_->setFont(monospaceFont()); - form->addRow("Fill byte (hex):", valueEdit_); + form->addRow(tr("Fill byte (hex):"), valueEdit_); layout->addLayout(form); auto* btnRow = new QHBoxLayout; - auto* fillBtn = new QPushButton("Fill"); + auto* fillBtn = new QPushButton(tr("Fill")); fillBtn->setObjectName("primaryButton"); connect(fillBtn, &QPushButton::clicked, this, &MemoryFillDialog::onFill); - auto* cancelBtn = new QPushButton("Cancel"); + auto* cancelBtn = new QPushButton(tr("Cancel")); connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject); btnRow->addStretch(); btnRow->addWidget(fillBtn); @@ -50,18 +50,18 @@ void MemoryFillDialog::onFill() { if (!proc_) return; bool ok; uintptr_t addr = addrEdit_->text().toULongLong(&ok, 16); - if (!ok) { QMessageBox::warning(this, "Fill Memory", "Enter a valid hexadecimal start address."); return; } + if (!ok) { QMessageBox::warning(this, tr("Fill Memory"), tr("Enter a valid hexadecimal start address.")); return; } size_t size = sizeEdit_->text().toULongLong(&ok, 0); - if (!ok || size == 0) { QMessageBox::warning(this, "Fill Memory", "Enter a positive size in bytes."); return; } + if (!ok || size == 0) { QMessageBox::warning(this, tr("Fill Memory"), tr("Enter a positive size in bytes.")); return; } constexpr size_t kMaxFillSize = 1u << 28; // 256 MiB, matches saveRegionToFile cap if (size > kMaxFillSize) { - QMessageBox::warning(this, "Fill Memory", - QString("Fill size too large (max %1 bytes).").arg(kMaxFillSize)); + QMessageBox::warning(this, tr("Fill Memory"), + tr("Fill size too large (max %1 bytes).").arg(kMaxFillSize)); return; } unsigned int parsedByte = valueEdit_->text().toUInt(&ok, 16); if (!ok || parsedByte > 0xff) { - QMessageBox::warning(this, "Fill Memory", "Enter a hexadecimal byte from 00 to FF."); + QMessageBox::warning(this, tr("Fill Memory"), tr("Enter a hexadecimal byte from 00 to FF.")); valueEdit_->setFocus(); valueEdit_->selectAll(); return; } @@ -70,12 +70,12 @@ void MemoryFillDialog::onFill() { std::vector buf(size, fillByte); auto r = proc_->write(addr, buf.data(), buf.size()); if (r && *r == buf.size()) { - QMessageBox::information(this, "Fill Memory", QString("Filled %1 bytes at 0x%2 with 0x%3") + QMessageBox::information(this, tr("Fill Memory"), tr("Filled %1 bytes at 0x%2 with 0x%3") .arg(size).arg(addr, 0, 16).arg(fillByte, 2, 16, QChar('0'))); accept(); } else - QMessageBox::warning(this, "Fill Memory", r ? QString("Only %1 of %2 bytes were written.").arg(*r).arg(size) - : QString("Write failed.")); + QMessageBox::warning(this, tr("Fill Memory"), r ? tr("Only %1 of %2 bytes were written.").arg(*r).arg(size) + : tr("Write failed.")); } } // namespace ce::gui diff --git a/gui/memoryregions.cpp b/gui/memoryregions.cpp index 5effb29d..f7c2b4af 100644 --- a/gui/memoryregions.cpp +++ b/gui/memoryregions.cpp @@ -9,7 +9,7 @@ namespace ce::gui { MemoryRegionsWindow::MemoryRegionsWindow(ProcessHandle* proc, QWidget* parent) : QMainWindow(parent), proc_(proc) { - setWindowTitle("Memory Regions"); + setWindowTitle(tr("Memory Regions")); resize(800, 500); auto* central = new QWidget; @@ -17,7 +17,7 @@ MemoryRegionsWindow::MemoryRegionsWindow(ProcessHandle* proc, QWidget* parent) table_ = new QTableWidget; table_->setColumnCount(5); - table_->setHorizontalHeaderLabels({"Start", "End", "Size", "Perm", "Path"}); + table_->setHorizontalHeaderLabels({tr("Start"), tr("End"), tr("Size"), tr("Perm"), tr("Path")}); // Fit the fixed-width columns (16-digit hex addresses, size, perms) to their // content so the addresses aren't clipped, and let Path (variable) take the rest. auto* hh = table_->horizontalHeader(); @@ -25,13 +25,13 @@ MemoryRegionsWindow::MemoryRegionsWindow(ProcessHandle* proc, QWidget* parent) for (int c = 0; c < 4; ++c) hh->setSectionResizeMode(c, QHeaderView::ResizeToContents); styleDataTable(table_); - table_->setToolTip("Double-click a region to browse it"); + table_->setToolTip(tr("Double-click a region to browse it")); connect(table_, &QTableWidget::cellDoubleClicked, this, [this](int row, int) { bool ok; uintptr_t addr = table_->item(row, 0)->text().toULongLong(&ok, 16); if (ok) emit navigateTo(addr); }); - layout->addWidget(makeTableFilter(table_, "Filter by address, permissions or path")); + layout->addWidget(makeTableFilter(table_, tr("Filter by address, permissions or path"))); layout->addWidget(table_); setCentralWidget(central); populate(); @@ -47,7 +47,7 @@ void MemoryRegionsWindow::populate() { table_->setItem(i, 0, new QTableWidgetItem(QString("%1").arg(r.base, 16, 16, QChar('0')))); table_->setItem(i, 1, new QTableWidgetItem(QString("%1").arg(r.base + r.size, 16, 16, QChar('0')))); table_->setItem(i, 2, new QTableWidgetItem(formatByteSize(r.size))); - table_->item(i, 2)->setToolTip(QString("%1 bytes (0x%2)").arg(r.size).arg(r.size, 0, 16)); + table_->item(i, 2)->setToolTip(tr("%1 bytes (0x%2)").arg(r.size).arg(r.size, 0, 16)); table_->item(i, 2)->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); char perms[4] = "---"; if (r.protection & MemProt::Read) perms[0] = 'r'; @@ -58,7 +58,7 @@ void MemoryRegionsWindow::populate() { table_->item(i, 4)->setToolTip(table_->item(i, 4)->text()); if (r.protection & MemProt::Read) totalReadable += r.size; } - setWindowTitle(QString("Memory Regions: %1 regions, %2 readable") + setWindowTitle(tr("Memory Regions: %1 regions, %2 readable") .arg(regions.size()).arg(formatByteSize(totalReadable))); } diff --git a/gui/memviewpreferences.cpp b/gui/memviewpreferences.cpp index 1dd7ffbb..1485801c 100644 --- a/gui/memviewpreferences.cpp +++ b/gui/memviewpreferences.cpp @@ -44,7 +44,7 @@ QPushButton* MemviewPreferences::colorButton(const QString& key, const QColor& d } MemviewPreferences::MemviewPreferences(QWidget* parent) : QDialog(parent) { - setWindowTitle("Disassembler Preferences"); + setWindowTitle(tr("Disassembler Preferences")); resize(500, 620); QSettings s; font_.fromString(s.value("disasm/font", QFont("Monospace", 10).toString()).toString()); @@ -59,9 +59,9 @@ MemviewPreferences::MemviewPreferences(QWidget* parent) : QDialog(parent) { root->addWidget(scroll, 1); // GroupBox2 "Disassembler": font + color group selector. - auto* gbDisasm = new QGroupBox("Disassembler"); + auto* gbDisasm = new QGroupBox(tr("Disassembler")); auto* dl = new QFormLayout(gbDisasm); - fontBtn_ = new QPushButton("Change disassembler font"); + fontBtn_ = new QPushButton(tr("Change disassembler font")); connect(fontBtn_, &QPushButton::clicked, this, [this]() { bool ok = false; QFont f = QFontDialog::getFont(&ok, font_, this); @@ -69,8 +69,8 @@ MemviewPreferences::MemviewPreferences(QWidget* parent) : QDialog(parent) { }); dl->addRow(fontBtn_); colorGroup_ = new QComboBox; - colorGroup_->addItems({"Normal"}); - dl->addRow("Color group:", colorGroup_); + colorGroup_->addItems({tr("Normal")}); + dl->addRow(tr("Color group:"), colorGroup_); v->addWidget(gbDisasm); // Default swatches follow the theme so they match the actual disassembler @@ -80,34 +80,34 @@ MemviewPreferences::MemviewPreferences(QWidget* parent) : QDialog(parent) { auto def = [dark](QColor d, QColor l) { return dark ? d : l; }; // GroupBox1 "Normal": text colors. - auto* gbNormal = new QGroupBox("Normal"); + auto* gbNormal = new QGroupBox(tr("Normal")); auto* nl = new QFormLayout(gbNormal); - nl->addRow("Default color", colorButton("colorDefault", def(QColor(0xcd, 0xd6, 0xf4), QColor(0x00, 0x00, 0x00)))); - nl->addRow("Register color", colorButton("colorRegister", def(QColor(0xcd, 0xd6, 0xf4), QColor(0x3a, 0x42, 0x52)))); - nl->addRow("Symbol color", colorButton("colorSymbol", def(QColor(0xf9, 0xe2, 0xaf), QColor(0x80, 0x60, 0x00)))); - nl->addRow("Hexadecimal color", colorButton("colorHex", def(QColor(0x89, 0xb4, 0xfa), QColor(0x00, 0x00, 0xc0)))); + nl->addRow(tr("Default color"), colorButton("colorDefault", def(QColor(0xcd, 0xd6, 0xf4), QColor(0x00, 0x00, 0x00)))); + nl->addRow(tr("Register color"), colorButton("colorRegister", def(QColor(0xcd, 0xd6, 0xf4), QColor(0x3a, 0x42, 0x52)))); + nl->addRow(tr("Symbol color"), colorButton("colorSymbol", def(QColor(0xf9, 0xe2, 0xaf), QColor(0x80, 0x60, 0x00)))); + nl->addRow(tr("Hexadecimal color"), colorButton("colorHex", def(QColor(0x89, 0xb4, 0xfa), QColor(0x00, 0x00, 0xc0)))); v->addWidget(gbNormal); // GroupBox4 "Jumplines". - auto* gbJump = new QGroupBox("Jumplines"); + auto* gbJump = new QGroupBox(tr("Jumplines")); auto* jl = new QFormLayout(gbJump); - jl->addRow("Conditional jump color", colorButton("colorCondJump", def(QColor(0xfa, 0xb3, 0x87), QColor(0xc0, 0x40, 0x00)))); - jl->addRow("Unconditional jump color", colorButton("colorJump", def(QColor(0x89, 0xb4, 0xfa), QColor(0x00, 0x00, 0xc0)))); - jl->addRow("Call color", colorButton("colorCall", def(QColor(0xcb, 0xa6, 0xf7), QColor(0x88, 0x39, 0xef)))); + jl->addRow(tr("Conditional jump color"), colorButton("colorCondJump", def(QColor(0xfa, 0xb3, 0x87), QColor(0xc0, 0x40, 0x00)))); + jl->addRow(tr("Unconditional jump color"), colorButton("colorJump", def(QColor(0x89, 0xb4, 0xfa), QColor(0x00, 0x00, 0xc0)))); + jl->addRow(tr("Call color"), colorButton("colorCall", def(QColor(0xcb, 0xa6, 0xf7), QColor(0x88, 0x39, 0xef)))); jlThickness_ = new QLineEdit(s.value("disasm/jlThickness", 1).toString()); - jl->addRow("Thickness", jlThickness_); + jl->addRow(tr("Thickness"), jlThickness_); // Lanes default to one character apart, as the disassembler draws them. jlSpacing_ = new QLineEdit(s.value("disasm/jlSpacing", QFontMetrics(font_).horizontalAdvance('0')).toString()); - jl->addRow("Spacing", jlSpacing_); + jl->addRow(tr("Spacing"), jlSpacing_); v->addWidget(gbJump); // GroupBox5 "Space between lines". - auto* gbSpace = new QGroupBox("Space between lines"); + auto* gbSpace = new QGroupBox(tr("Space between lines")); auto* sl = new QFormLayout(gbSpace); spaceAbove_ = new QLineEdit(s.value("disasm/spaceAbove", 0).toString()); - sl->addRow("Above", spaceAbove_); + sl->addRow(tr("Above"), spaceAbove_); spaceBelow_ = new QLineEdit(s.value("disasm/spaceBelow", 0).toString()); - sl->addRow("Below", spaceBelow_); + sl->addRow(tr("Below"), spaceBelow_); v->addWidget(gbSpace); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Apply | QDialogButtonBox::Cancel); diff --git a/gui/modulelist.cpp b/gui/modulelist.cpp index 06a22608..168af0d6 100644 --- a/gui/modulelist.cpp +++ b/gui/modulelist.cpp @@ -10,7 +10,7 @@ namespace ce::gui { ModuleListWindow::ModuleListWindow(ProcessHandle* proc, QWidget* parent) : QMainWindow(parent), proc_(proc) { - setWindowTitle("Module List"); + setWindowTitle(tr("Module List")); resize(850, 500); auto* central = new QWidget; @@ -18,7 +18,7 @@ ModuleListWindow::ModuleListWindow(ProcessHandle* proc, QWidget* parent) table_ = new QTableWidget; table_->setColumnCount(4); - table_->setHorizontalHeaderLabels({"Base", "Size", "Name", "Path"}); + table_->setHorizontalHeaderLabels({tr("Base"), tr("Size"), tr("Name"), tr("Path")}); // Fit Base (hex), Size and Name to content so the base address isn't clipped; // Path (last) takes the remaining width. auto* hh = table_->horizontalHeader(); @@ -26,14 +26,14 @@ ModuleListWindow::ModuleListWindow(ProcessHandle* proc, QWidget* parent) for (int c = 0; c < 3; ++c) hh->setSectionResizeMode(c, QHeaderView::ResizeToContents); styleDataTable(table_); - table_->setToolTip("Double-click a module to browse its base address"); + table_->setToolTip(tr("Double-click a module to browse its base address")); connect(table_, &QTableWidget::cellDoubleClicked, this, [this](int row, int) { bool ok = false; auto addr = table_->item(row, 0)->text().toULongLong(&ok, 16); if (ok) emit navigateTo(addr); }); - layout->addWidget(makeTableFilter(table_, "Filter by name or path")); + layout->addWidget(makeTableFilter(table_, tr("Filter by name or path"))); layout->addWidget(table_); setCentralWidget(central); populate(); @@ -48,13 +48,13 @@ void ModuleListWindow::populate() { const auto& module = modules[i]; table_->setItem(i, 0, new QTableWidgetItem(QString("%1").arg(module.base, 16, 16, QChar('0')))); table_->setItem(i, 1, new QTableWidgetItem(formatByteSize(module.size))); - table_->item(i, 1)->setToolTip(QString("%1 bytes (0x%2)").arg(module.size).arg(module.size, 0, 16)); + table_->item(i, 1)->setToolTip(tr("%1 bytes (0x%2)").arg(module.size).arg(module.size, 0, 16)); table_->item(i, 1)->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); table_->setItem(i, 2, new QTableWidgetItem(QString::fromStdString(module.name))); table_->setItem(i, 3, new QTableWidgetItem(QString::fromStdString(module.path))); for (int column : {2, 3}) table_->item(i, column)->setToolTip(table_->item(i, column)->text()); } - setWindowTitle(QString("Module List: %1 modules").arg(modules.size())); + setWindowTitle(tr("Module List: %1 modules").arg(modules.size())); } } // namespace ce::gui diff --git a/gui/monodissector.cpp b/gui/monodissector.cpp index 684f3428..3af6966f 100644 --- a/gui/monodissector.cpp +++ b/gui/monodissector.cpp @@ -4,6 +4,7 @@ #include "core/module_files.hpp" #include +#include #include #include #include @@ -33,7 +34,9 @@ static ce::MonoDissection il2cppToMonoDissection(const ce::Il2CppBinaryLayout& l idx = d.images.size(); imageIndex[c.image] = idx; ce::MonoImageInfo mi; - mi.name = c.image.empty() ? "" : c.image; + mi.name = c.image.empty() + ? QCoreApplication::translate("MonoDissectorWindow", "").toStdString() + : c.image; d.images.push_back(std::move(mi)); } else { idx = it->second; @@ -93,18 +96,18 @@ static constexpr int kStaticRole = Qt::UserRole + 5; // the field lives in the MonoDissectorWindow::MonoDissectorWindow(ce::ProcessHandle* proc, QWidget* parent) : QMainWindow(parent), proc_(proc) { - setWindowTitle("Mono Dissector"); + setWindowTitle(tr("Mono Dissector")); resize(720, 640); auto* bar = new QToolBar; bar->setMovable(false); - auto* dissectAct = bar->addAction("Dissect (inject agent)"); - dissectAct->setToolTip("Inject the Mono agent and read the target's class/field layout"); + auto* dissectAct = bar->addAction(tr("Dissect (inject agent)")); + dissectAct->setToolTip(tr("Inject the Mono agent and read the target's class/field layout")); connect(dissectAct, &QAction::triggered, this, &MonoDissectorWindow::runDissect); bar->addSeparator(); - bar->addWidget(new QLabel(" Filter: ")); + bar->addWidget(new QLabel(QStringLiteral(" ") + tr("Filter:") + QStringLiteral(" "))); filter_ = new QLineEdit; - filter_->setPlaceholderText("class or field name…"); + filter_->setPlaceholderText(tr("class or field name…")); filter_->setClearButtonEnabled(true); connect(filter_, &QLineEdit::textChanged, this, &MonoDissectorWindow::applyFilter); bar->addWidget(filter_); @@ -114,10 +117,10 @@ MonoDissectorWindow::MonoDissectorWindow(ce::ProcessHandle* proc, QWidget* paren auto* v = new QVBoxLayout(central); tree_ = new QTreeWidget; tree_->setColumnCount(3); - tree_->setHeaderLabels({"Name", "Offset", "Type"}); + tree_->setHeaderLabels({tr("Name"), tr("Offset"), tr("Type")}); tree_->header()->setSectionResizeMode(0, QHeaderView::Stretch); tree_->setAlternatingRowColors(true); - setEmptyHint(tree_, "Click Dissect to load the target's Mono assemblies, classes and fields."); + setEmptyHint(tree_, tr("Click Dissect to load the target's Mono assemblies, classes and fields.")); // Double-click a field row -> request it be added to the address list. connect(tree_, &QTreeWidget::itemDoubleClicked, this, [this](QTreeWidgetItem* it, int) { if (!it || it->data(0, kKindRole).toString() != "fld") return; @@ -127,7 +130,7 @@ MonoDissectorWindow::MonoDissectorWindow(ce::ProcessHandle* proc, QWidget* paren it->data(0, kStaticRole).toBool()); }); v->addWidget(tree_); - status_ = new QLabel("Not dissected yet."); + status_ = new QLabel(tr("Not dissected yet.")); v->addWidget(status_); setCentralWidget(central); } @@ -147,7 +150,8 @@ void MonoDissectorWindow::setDissection(const ce::MonoDissection& d) { auto* clsItem = new QTreeWidgetItem(imgItem, {clsLabel}); clsItem->setData(0, kKindRole, "cls"); for (const auto& f : c.fields) { - QString name = QString::fromStdString(f.name) + (f.isStatic ? " (static)" : ""); + QString name = QString::fromStdString(f.name); + if (f.isStatic) name += QStringLiteral(" ") + tr("(static)"); auto* fldItem = new QTreeWidgetItem(clsItem, { name, QString("0x%1").arg(f.offset, 0, 16), @@ -168,22 +172,22 @@ void MonoDissectorWindow::setDissection(const ce::MonoDissection& d) { auto* mItem = new QTreeWidgetItem(clsItem, { QString::fromStdString(m.name) + "()", m.address ? QString("0x%1").arg(m.address, 0, 16) : QString(), - QStringLiteral("method"), + tr("method"), }); mItem->setData(0, kKindRole, "method"); } } } - status_->setText(QString("%1 %2 · %3 classes · %4 fields%5") - .arg(d.images.size()).arg(d.images.size() == 1 ? "assembly" : "assemblies") + status_->setText(tr("%1 %2 · %3 classes · %4 fields%5") + .arg(d.images.size()).arg(d.images.size() == 1 ? tr("assembly") : tr("assemblies")) .arg(d.classCount()).arg(fieldCount) - .arg(d.ready ? QString() : QString(" (incomplete%1)") - .arg(d.error.empty() ? QString() : ": " + QString::fromStdString(d.error)))); + .arg(d.ready ? QString() : QStringLiteral(" ") + tr("(incomplete%1)") + .arg(d.error.empty() ? QString() : QStringLiteral(": ") + QString::fromStdString(d.error)))); applyFilter(filter_->text()); } void MonoDissectorWindow::runDissect() { - if (!proc_) { status_->setText("No target process."); return; } + if (!proc_) { status_->setText(tr("No target process.")); return; } // Both paths read this machine's files: the IL2CPP metadata and // GameAssembly, or the Mono agent loaded into the target. if (!ce::moduleFilesAreLocal(*proc_)) { @@ -195,7 +199,7 @@ void MonoDissectorWindow::runDissect() { case ce::ManagedKind::Il2Cpp: { // IL2CPP has no live runtime to query; resolve the class layout // statically from global-metadata.dat + the GameAssembly binary. - status_->setText("IL2CPP target: locating metadata + GameAssembly…"); + status_->setText(tr("IL2CPP target: locating metadata + GameAssembly…")); QApplication::setOverrideCursor(Qt::WaitCursor); // Paint the status only: processing events here would let a second // click, or the target's exit freeing proc_, run inside this call. @@ -203,7 +207,7 @@ void MonoDissectorWindow::runDissect() { auto layout = ce::resolveIl2CppForProcess(*proc_); QApplication::restoreOverrideCursor(); if (!layout.ok) { - status_->setText("IL2CPP: " + QString::fromStdString(layout.error)); + status_->setText(tr("IL2CPP: %1").arg(QString::fromStdString(layout.error))); return; } // GameAssembly runtime base, so method RVAs become live addresses. @@ -219,17 +223,17 @@ void MonoDissectorWindow::runDissect() { return; } case ce::ManagedKind::None: - status_->setText("No Mono runtime detected in this process."); + status_->setText(tr("No Mono runtime detected in this process.")); return; case ce::ManagedKind::Mono: break; } std::string agent = ce::findMonoAgentPath(); if (agent.empty()) { - status_->setText("libcecore_mono_agent.so not found next to the binary."); + status_->setText(tr("libcecore_mono_agent.so not found next to the binary.")); return; } - status_->setText("Injecting agent and reading Mono layout…"); + status_->setText(tr("Injecting agent and reading Mono layout…")); QApplication::setOverrideCursor(Qt::WaitCursor); status_->repaint(); // no processEvents: see the IL2CPP case // A loaded resolver is needed so injection can find dlopen in the target. @@ -237,7 +241,7 @@ void MonoDissectorWindow::runDissect() { resolver.loadProcess(*proc_); auto d = ce::dissectMono(*proc_, resolver, agent); QApplication::restoreOverrideCursor(); - if (!d) { status_->setText("Agent injection failed (see CE_LOG=general:debug)."); return; } + if (!d) { status_->setText(tr("Agent injection failed (see CE_LOG=general:debug).")); return; } setDissection(*d); } diff --git a/gui/overlay.cpp b/gui/overlay.cpp index 6a326fb3..df629431 100644 --- a/gui/overlay.cpp +++ b/gui/overlay.cpp @@ -9,7 +9,7 @@ namespace ce::gui { OverlayWindow::OverlayWindow(QWidget* parent) : QWidget(parent) { - setWindowTitle("CE Overlay"); + setWindowTitle(tr("CE Overlay")); setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::BypassWindowManagerHint | Qt::WindowTransparentForInput); diff --git a/gui/pointerscan_dialog.cpp b/gui/pointerscan_dialog.cpp index 0d312458..1a2863b4 100644 --- a/gui/pointerscan_dialog.cpp +++ b/gui/pointerscan_dialog.cpp @@ -14,14 +14,14 @@ namespace ce::gui { PointerScanDialog::PointerScanDialog(ProcessHandle* proc, QWidget* parent) : QDialog(parent), proc_(proc) { - setWindowTitle("Pointer Scanner"); + setWindowTitle(tr("Pointer Scanner")); resize(820, 500); auto* layout = new QVBoxLayout(this); // Give address expressions a full row; keep options and actions together below. auto* configRow = new QHBoxLayout; - configRow->addWidget(new QLabel("Target Address:")); + configRow->addWidget(new QLabel(tr("Target Address:"))); targetEdit_ = new QLineEdit; targetEdit_->setFont(monospaceFont()); targetEdit_->setPlaceholderText("0x7f1234"); @@ -29,13 +29,13 @@ PointerScanDialog::PointerScanDialog(ProcessHandle* proc, QWidget* parent) layout->addLayout(configRow); auto* optionsRow = new QHBoxLayout; - optionsRow->addWidget(new QLabel("Depth:")); + optionsRow->addWidget(new QLabel(tr("Depth:"))); depthSpin_ = new QSpinBox; depthSpin_->setRange(1, 7); depthSpin_->setValue(4); optionsRow->addWidget(depthSpin_); - optionsRow->addWidget(new QLabel("Max Offset:")); + optionsRow->addWidget(new QLabel(tr("Max Offset:"))); offsetSpin_ = new QSpinBox; offsetSpin_->setRange(64, 65536); offsetSpin_->setValue(2048); @@ -43,36 +43,36 @@ PointerScanDialog::PointerScanDialog(ProcessHandle* proc, QWidget* parent) optionsRow->addWidget(offsetSpin_); optionsRow->addStretch(); - scanBtn_ = new QPushButton("Scan"); + scanBtn_ = new QPushButton(tr("Scan")); scanBtn_->setObjectName("primaryButton"); connect(scanBtn_, &QPushButton::clicked, this, &PointerScanDialog::onScan); optionsRow->addWidget(scanBtn_); // Rescan filters the current paths against a new target address (after the // dynamic value has moved) — the key to finding a stable pointer path. - rescanBtn_ = new QPushButton("Rescan…"); - rescanBtn_->setToolTip("Filter current paths to those that still point at a new target address"); + rescanBtn_ = new QPushButton(tr("Rescan…")); + rescanBtn_->setToolTip(tr("Filter current paths to those that still point at a new target address")); rescanBtn_->setEnabled(false); connect(rescanBtn_, &QPushButton::clicked, this, &PointerScanDialog::onRescan); optionsRow->addWidget(rescanBtn_); - saveBtn_ = new QPushButton("Save…"); + saveBtn_ = new QPushButton(tr("Save…")); saveBtn_->setEnabled(false); connect(saveBtn_, &QPushButton::clicked, this, &PointerScanDialog::onSave); optionsRow->addWidget(saveBtn_); - loadBtn_ = new QPushButton("Load…"); + loadBtn_ = new QPushButton(tr("Load…")); connect(loadBtn_, &QPushButton::clicked, this, &PointerScanDialog::onLoad); optionsRow->addWidget(loadBtn_); layout->addLayout(optionsRow); - statusLabel_ = new QLabel("Ready"); + statusLabel_ = new QLabel(tr("Ready")); layout->addWidget(statusLabel_); // Results table resultsTable_ = new QTableWidget; resultsTable_->setColumnCount(3); - resultsTable_->setHorizontalHeaderLabels({"Path", "Current Address", "Value"}); + resultsTable_->setHorizontalHeaderLabels({tr("Path"), tr("Current Address"), tr("Value")}); // The pointer path is the long, variable-length column, so let it absorb the // slack. Size the address/value columns to their contents (and never narrower // than the header text, so "Current Address" isn't clipped to "Current Addres"). @@ -85,7 +85,7 @@ PointerScanDialog::PointerScanDialog(ProcessHandle* proc, QWidget* parent) resultsTable_->setSelectionBehavior(QAbstractItemView::SelectRows); resultsTable_->setProperty("monospace", true); // font from the theme sheet styleDataTable(resultsTable_); - setEmptyHint(resultsTable_, "Enter the address of a value and click Scan to find pointer paths that lead to it."); + setEmptyHint(resultsTable_, tr("Enter the address of a value and click Scan to find pointer paths that lead to it.")); connect(resultsTable_, &QTableWidget::cellDoubleClicked, this, &PointerScanDialog::onResultDoubleClicked); layout->addWidget(resultsTable_); } @@ -102,16 +102,16 @@ void PointerScanDialog::onScan() { if (worker_.joinable()) { // the button reads Cancel while a scan or rescan runs if (job_ == Job::Scan) scanner_->cancel(); else rescanCancel_ = true; - statusLabel_->setText("Cancelling..."); + statusLabel_->setText(tr("Cancelling...")); return; } if (!proc_) return; bool ok; uintptr_t target = targetEdit_->text().toULongLong(&ok, 16); - if (!ok) { statusLabel_->setText("Invalid address"); return; } + if (!ok) { statusLabel_->setText(tr("Invalid address")); return; } - statusLabel_->setText("Scanning..."); + statusLabel_->setText(tr("Scanning...")); resultsTable_->setRowCount(0); PointerScanConfig config; @@ -131,13 +131,13 @@ void PointerScanDialog::startJob(Job job, std::function work) { rescanBtn_->setEnabled(false); saveBtn_->setEnabled(false); loadBtn_->setEnabled(false); - scanBtn_->setText("Cancel"); + scanBtn_->setText(tr("Cancel")); worker_ = std::thread([this, work = std::move(work)] { // A scan throws for a target of unknown pointer width, or when memory // runs out: report it instead of terminating the application. try { work(); } catch (const std::exception& e) { jobError_ = e.what(); } - catch (...) { jobError_ = "unexpected error"; } + catch (...) { jobError_ = tr("unexpected error").toStdString(); } scanDone_ = true; }); if (!scanPoll_) { @@ -145,9 +145,9 @@ void PointerScanDialog::startJob(Job job, std::function work) { connect(scanPoll_, &QTimer::timeout, this, [this] { if (!scanDone_) { if (job_ == Job::Scan) - statusLabel_->setText(QString("Scanning... %1%").arg(int(scanner_->progress() * 100))); + statusLabel_->setText(tr("Scanning... %1%").arg(int(scanner_->progress() * 100))); else - statusLabel_->setText(QString("Rescanning... %1%").arg(int(rescanProgress_ * 100))); + statusLabel_->setText(tr("Rescanning... %1%").arg(int(rescanProgress_ * 100))); return; } scanPoll_->stop(); @@ -163,7 +163,7 @@ void PointerScanDialog::finishJob() { job_ = Job::None; if (job == Job::Scan) finishScan(); else finishRescan(); - scanBtn_->setText("Scan"); + scanBtn_->setText(tr("Scan")); loadBtn_->setEnabled(true); } @@ -171,21 +171,21 @@ void PointerScanDialog::finishScan() { const bool cancelled = scanner_->wasCancelled(); results_ = std::move(scanned_); populateResults(); - QString status = results_.size() > 1000 ? QString("Found %1 paths (showing first 1000)").arg(results_.size()) - : QString("Found %1 paths").arg(results_.size()); + QString status = results_.size() > 1000 ? tr("Found %1 paths (showing first 1000)").arg(results_.size()) + : tr("Found %1 paths").arg(results_.size()); if (!jobError_.empty()) - status = QString("Scan failed: %1").arg(QString::fromStdString(jobError_)); + status = tr("Scan failed: %1").arg(QString::fromStdString(jobError_)); else if (cancelled) - status = QString("Cancelled; %1 paths found so far").arg(results_.size()); + status = tr("Cancelled; %1 paths found so far").arg(results_.size()); else switch (scanner_->limitReached()) { case ce::PointerScanLimit::Results: - status += ". The scan stopped at its path limit, so longer paths may be missing: lower Max offset and scan again."; + status += tr(". The scan stopped at its path limit, so longer paths may be missing: lower Max offset and scan again."); break; case ce::PointerScanLimit::Work: - status += ". The scan stopped at its work limit, so longer paths may be missing: lower Depth or Max offset and scan again."; + status += tr(". The scan stopped at its work limit, so longer paths may be missing: lower Depth or Max offset and scan again."); break; case ce::PointerScanLimit::MapEntries: - status += ". Memory held more pointer-like values than the scan keeps, so some paths may be missing."; + status += tr(". Memory held more pointer-like values than the scan keeps, so some paths may be missing."); break; case ce::PointerScanLimit::None: break; @@ -193,7 +193,7 @@ void PointerScanDialog::finishScan() { // The map can be cut as well as the search. if (jobError_.empty() && !cancelled && scanner_->mapTruncated() && scanner_->limitReached() != ce::PointerScanLimit::MapEntries) - status += " Memory also held more pointer-like values than the scan keeps."; + status += " " + tr("Memory also held more pointer-like values than the scan keeps."); statusLabel_->setText(status); } @@ -203,14 +203,14 @@ void PointerScanDialog::finishRescan() { // Keep the paths the rescan started from. rescanBtn_->setEnabled(!results_.empty()); saveBtn_->setEnabled(!results_.empty()); - statusLabel_->setText(jobError_.empty() ? QString("Rescan cancelled; the paths are unchanged") - : QString("Rescan failed: %1").arg(QString::fromStdString(jobError_))); + statusLabel_->setText(jobError_.empty() ? tr("Rescan cancelled; the paths are unchanged") + : tr("Rescan failed: %1").arg(QString::fromStdString(jobError_))); return; } results_ = std::move(rescanned_); targetEdit_->setText(QString("0x%1").arg(rescanTarget_, 0, 16)); populateResults(); - statusLabel_->setText(QString("Rescan: %1 → %2 paths still reach 0x%3") + statusLabel_->setText(tr("Rescan: %1 → %2 paths still reach 0x%3") .arg(before).arg(results_.size()).arg(rescanTarget_, 0, 16)); } @@ -249,12 +249,12 @@ void PointerScanDialog::populateResults() { void PointerScanDialog::onRescan() { if (!proc_ || results_.empty() || worker_.joinable()) return; bool ok = false; - QString text = QInputDialog::getText(this, "Rescan pointer paths", - "New target address (hex) the paths should now resolve to:", + QString text = QInputDialog::getText(this, tr("Rescan pointer paths"), + tr("New target address (hex) the paths should now resolve to:"), QLineEdit::Normal, targetEdit_->text(), &ok); if (!ok) return; uintptr_t newTarget = text.toULongLong(&ok, 16); - if (!ok || !newTarget) { statusLabel_->setText("Invalid address"); return; } + if (!ok || !newTarget) { statusLabel_->setText(tr("Invalid address")); return; } rescanTo(newTarget); } @@ -264,7 +264,7 @@ void PointerScanDialog::rescanTo(uintptr_t newTarget) { rescanCancel_ = false; rescanProgress_ = 0; rescanned_.clear(); - statusLabel_->setText("Rescanning..."); + statusLabel_->setText(tr("Rescanning...")); // The worker walks its own copy: the table keeps showing (and double-click // keeps reading) results_ meanwhile. startJob(Job::Rescan, [this, paths = results_, newTarget] { @@ -274,28 +274,28 @@ void PointerScanDialog::rescanTo(uintptr_t newTarget) { void PointerScanDialog::onSave() { if (results_.empty()) return; - auto path = QFileDialog::getSaveFileName(this, "Save pointer paths", "pointers.ptr", - "Pointer scan (*.ptr);;All (*)"); + auto path = QFileDialog::getSaveFileName(this, tr("Save pointer paths"), "pointers.ptr", + tr("Pointer scan (*.ptr);;All (*)")); if (path.isEmpty()) return; if (!ce::savePointerPaths(path.toStdString(), results_)) - QMessageBox::warning(this, "Save failed", "Could not write the pointer paths."); + QMessageBox::warning(this, tr("Save failed"), tr("Could not write the pointer paths.")); else - statusLabel_->setText(QString("Saved %1 paths").arg(results_.size())); + statusLabel_->setText(tr("Saved %1 paths").arg(results_.size())); } void PointerScanDialog::onLoad() { - auto path = QFileDialog::getOpenFileName(this, "Load pointer paths", "", - "Pointer scan (*.ptr);;All (*)"); + auto path = QFileDialog::getOpenFileName(this, tr("Load pointer paths"), "", + tr("Pointer scan (*.ptr);;All (*)")); if (path.isEmpty()) return; std::string err; auto loaded = ce::loadPointerPaths(path.toStdString(), &err); if (loaded.empty() && !err.empty()) { - QMessageBox::warning(this, "Load failed", QString::fromStdString(err)); + QMessageBox::warning(this, tr("Load failed"), QString::fromStdString(err)); return; } results_ = std::move(loaded); populateResults(); - statusLabel_->setText(QString("Loaded %1 paths").arg(results_.size())); + statusLabel_->setText(tr("Loaded %1 paths").arg(results_.size())); } void PointerScanDialog::onResultDoubleClicked(int row, int) { diff --git a/gui/processlistdialog.cpp b/gui/processlistdialog.cpp index 757cad3c..01dfaa85 100644 --- a/gui/processlistdialog.cpp +++ b/gui/processlistdialog.cpp @@ -39,7 +39,7 @@ class ProcessItem : public QTreeWidgetItem { } // namespace ProcessListDialog::ProcessListDialog(QWidget* parent) : QDialog(parent) { - setWindowTitle("Open Process"); + setWindowTitle(tr("Open Process")); resize(720, 520); setMinimumSize(560, 360); // narrower clips the columns behind a horizontal scrollbar @@ -50,13 +50,13 @@ ProcessListDialog::ProcessListDialog(QWidget* parent) : QDialog(parent) { // cannot be opened without elevated rights, so they are hidden by default. auto* filterRow = new QHBoxLayout; filterEdit_ = new QLineEdit; - filterEdit_->setPlaceholderText("Filter by process name, PID, or command"); - filterEdit_->setAccessibleName("Filter processes"); + filterEdit_->setPlaceholderText(tr("Filter by process name, PID, or command")); + filterEdit_->setAccessibleName(tr("Filter processes")); filterEdit_->setClearButtonEnabled(true); connect(filterEdit_, &QLineEdit::textChanged, this, &ProcessListDialog::onFilter); filterRow->addWidget(filterEdit_, 1); - mineOnly_ = new QCheckBox("Only my processes"); - mineOnly_->setToolTip("Hide kernel threads and processes owned by other users"); + mineOnly_ = new QCheckBox(tr("Only my processes")); + mineOnly_->setToolTip(tr("Hide kernel threads and processes owned by other users")); mineOnly_->setChecked(geteuid() != 0); connect(mineOnly_, &QCheckBox::toggled, this, [this]() { onFilter(filterEdit_->text()); }); filterRow->addWidget(mineOnly_); @@ -65,7 +65,7 @@ ProcessListDialog::ProcessListDialog(QWidget* parent) : QDialog(parent) { processList_ = new QTreeWidget; processList_->setColumnCount(4); - processList_->setHeaderLabels({"Name", "PID", "Memory", "Command"}); + processList_->setHeaderLabels({tr("Name"), tr("PID"), tr("Memory"), tr("Command")}); processList_->setRootIsDecorated(false); processList_->setUniformRowHeights(true); processList_->setAlternatingRowColors(true); @@ -89,12 +89,12 @@ ProcessListDialog::ProcessListDialog(QWidget* parent) : QDialog(parent) { // Buttons: Refresh is a list action (left); Open/Cancel are the dialog actions (right). auto* btnLayout = new QHBoxLayout; - auto* openBtn = new QPushButton("Open"); + auto* openBtn = new QPushButton(tr("Open")); openBtn_ = openBtn; openBtn_->setObjectName("primaryButton"); openBtn->setDefault(true); - auto* cancelBtn = new QPushButton("Cancel"); - auto* refreshBtn = new QPushButton("Refresh"); + auto* cancelBtn = new QPushButton(tr("Cancel")); + auto* refreshBtn = new QPushButton(tr("Refresh")); refreshBtn->setShortcut(QKeySequence::Refresh); connect(openBtn, &QPushButton::clicked, this, &ProcessListDialog::onAccept); connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject); @@ -154,7 +154,7 @@ void ProcessListDialog::refreshList() { const QString name = QString::fromStdString(p.name); const QString command = QString::fromStdString(p.path); auto* item = new ProcessItem; - item->setText(NameColumn, p.sandboxed ? name + QStringLiteral(" [sandboxed]") : name); + item->setText(NameColumn, p.sandboxed ? name + " " + tr("[sandboxed]") : name); item->setText(PidColumn, QString::number(p.pid)); item->setText(MemoryColumn, rss ? formatByteSize(rss) : QStringLiteral("-")); item->setText(CommandColumn, command); @@ -171,8 +171,8 @@ void ProcessListDialog::refreshList() { p.sandboxed ? QStringLiteral("sandboxed") : QString()}.join('\n')); QString tip = command.isEmpty() ? name : command; if (p.sandboxed) - tip += QStringLiteral("\nSandboxed (Flatpak/Snap/container): attaching reaches into " - "its namespace; scan, edit and symbols work."); + tip += "\n" + tr("Sandboxed (Flatpak/Snap/container): attaching reaches into " + "its namespace; scan, edit and symbols work."); for (int column = 0; column < processList_->columnCount(); ++column) item->setToolTip(column, tip); if (kernelThread || !known) for (int column = 0; column < processList_->columnCount(); ++column) @@ -193,7 +193,7 @@ void ProcessListDialog::onAccept() { const pid_t pid = static_cast(item->data(NameColumn, PidRole).toLongLong()); if (processStartTime(pid) != item->data(NameColumn, StartRole).toString()) { refreshList(); - statusLabel_->setText("That process has exited; the list was refreshed."); + statusLabel_->setText(tr("That process has exited; the list was refreshed.")); return; } selectedPid_ = item->data(NameColumn, PidRole).toLongLong(); @@ -217,8 +217,8 @@ void ProcessListDialog::onFilter(const QString& text) { auto* current = processList_->currentItem(); if (!current || current->isHidden()) processList_->setCurrentItem(first); openBtn_->setEnabled(visible > 0 && processList_->currentItem()); - statusLabel_->setText(visible ? QString("%1 %2").arg(visible).arg(visible == 1 ? "process" : "processes") - : "No processes match this filter."); + statusLabel_->setText(visible ? tr("%1 %2").arg(visible).arg(visible == 1 ? tr("process") : tr("processes")) + : tr("No processes match this filter.")); if (auto* item = processList_->currentItem()) processList_->scrollToItem(item); } diff --git a/gui/registereditor.cpp b/gui/registereditor.cpp index d5c4c92d..4275cbf0 100644 --- a/gui/registereditor.cpp +++ b/gui/registereditor.cpp @@ -34,7 +34,7 @@ QString bytesToHex(const uint8_t* data, size_t size) { RegisterEditorWindow::RegisterEditorWindow(ProcessHandle* proc, QWidget* parent) : QMainWindow(parent), proc_(proc) { - setWindowTitle("Register Editor"); + setWindowTitle(tr("Register Editor")); resize(820, 680); auto* central = new QWidget; @@ -42,16 +42,16 @@ RegisterEditorWindow::RegisterEditorWindow(ProcessHandle* proc, QWidget* parent) // Thread picker and the two actions on one row. auto* buttons = new QHBoxLayout; - buttons->addWidget(new QLabel("Thread:")); + buttons->addWidget(new QLabel(tr("Thread:"))); threadCombo_ = new QComboBox; threadCombo_->setObjectName("registerEditorThreads"); threadCombo_->setMinimumWidth(220); buttons->addWidget(threadCombo_, 1); - auto* refreshButton = new QPushButton("Refresh"); + auto* refreshButton = new QPushButton(tr("Refresh")); refreshButton->setObjectName("registerEditorRefresh"); - applyButton_ = new QPushButton("Apply changes"); + applyButton_ = new QPushButton(tr("Apply changes")); applyButton_->setObjectName("registerEditorApply"); - applyButton_->setToolTip("Apply only edited values to the thread’s current register state"); + applyButton_->setToolTip(tr("Apply only edited values to the thread’s current register state")); applyButton_->setProperty("primary", true); buttons->addSpacing(8); buttons->addWidget(refreshButton); @@ -67,24 +67,24 @@ RegisterEditorWindow::RegisterEditorWindow(ProcessHandle* proc, QWidget* parent) table_ = new QTableWidget; table_->setColumnCount(2); - table_->setHorizontalHeaderLabels({"Register", "Value"}); + table_->setHorizontalHeaderLabels({tr("Register"), tr("Value")}); table_->horizontalHeader()->setStretchLastSection(true); styleDataTable(table_); // Values are edited in place; Apply writes the edited ones back. table_->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed | QAbstractItemView::AnyKeyPressed); - table_->setToolTip("Double-click a value to edit it, then press Apply changes"); + table_->setToolTip(tr("Double-click a value to edit it, then press Apply changes")); table_->setObjectName("registerEditorGeneral"); table_->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); layout->addWidget(table_); - auto* fpLabel = new QLabel("Floating point / SIMD registers"); + auto* fpLabel = new QLabel(tr("Floating point / SIMD registers")); fpLabel->setObjectName("registerEditorVectorLabel"); layout->addWidget(fpLabel); fpTable_ = new QTableWidget; fpTable_->setColumnCount(3); - fpTable_->setHorizontalHeaderLabels({"Register", "XMM low 128", "YMM high 128"}); + fpTable_->setHorizontalHeaderLabels({tr("Register"), tr("XMM low 128"), tr("YMM high 128")}); // Fit both 128-bit value columns so YMM values remain accessible when narrowed. fpTable_->horizontalHeader()->setStretchLastSection(false); fpTable_->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); @@ -96,22 +96,22 @@ RegisterEditorWindow::RegisterEditorWindow(ProcessHandle* proc, QWidget* parent) setCentralWidget(central); if (auto* guest=dynamic_cast(proc_)) { - setWindowTitle("GDB Register Editor"); - threadCombo_->addItem("CPU selected by the GDB stub");threadCombo_->setEnabled(false); + setWindowTitle(tr("GDB Register Editor")); + threadCombo_->addItem(tr("CPU selected by the GDB stub"));threadCombo_->setEnabled(false); fpLabel->hide();fpTable_->hide();table_->setSelectionBehavior(QAbstractItemView::SelectRows); table_->setSelectionMode(QAbstractItemView::ExtendedSelection); - table_->setHorizontalHeaderLabels({"XML register", "Target-order hexadecimal bytes"}); + table_->setHorizontalHeaderLabels({tr("XML register"), tr("Target-order hexadecimal bytes")}); table_->setRowCount(static_cast(guest->registerDescription().registers.size())); for (size_t row=0;rowregisterDescription().registers.size();++row) { const auto& reg=guest->registerDescription().registers[row]; auto* name=new QTableWidgetItem(QString::fromStdString(reg.name)); name->setFlags(name->flags() & ~Qt::ItemIsEditable); - name->setToolTip(QString("%1 bits; register %2; %3").arg(reg.bits).arg(reg.number).arg(QString::fromStdString(reg.type))); - auto* value=new QTableWidgetItem("Select this row and press Refresh"); + name->setToolTip(tr("%1 bits; register %2; %3").arg(reg.bits).arg(reg.number).arg(QString::fromStdString(reg.type))); + auto* value=new QTableWidgetItem(tr("Select this row and press Refresh")); value->setFlags(value->flags() & ~Qt::ItemIsEditable); table_->setItem(static_cast(row),0,name);table_->setItem(static_cast(row),1,value); } - applyButton_->setToolTip("Write only edited XML registers; values are raw bytes in the target's register order"); + applyButton_->setToolTip(tr("Write only edited XML registers; values are raw bytes in the target's register order")); if (table_->rowCount()) table_->selectRow(0); refreshGdbRegisters();return; } @@ -142,14 +142,14 @@ void RegisterEditorWindow::refreshRegisters() { table_->setEnabled(false); applyButton_->setEnabled(false); if (!proc_ || threadCombo_->currentIndex() < 0) { - statusLabel_->setText("No thread selected"); + statusLabel_->setText(tr("No thread selected")); return; } const auto tid = static_cast(threadCombo_->currentData().toLongLong()); auto snapshot = ce::inspectThread(*proc_, tid); if (!snapshot) { statusLabel_->setText(QString::fromStdString(snapshot.error())); return; } displaySnapshot(std::move(*snapshot)); - statusLabel_->setText(QString("Registers of thread %1. Double-click a value to edit it, then Apply changes.").arg(tid)); + statusLabel_->setText(tr("Registers of thread %1. Double-click a value to edit it, then Apply changes.").arg(tid)); } void RegisterEditorWindow::displaySnapshot(ThreadSnapshot snapshot) { @@ -167,16 +167,16 @@ void RegisterEditorWindow::displaySnapshot(ThreadSnapshot snapshot) { const bool arm = snapshot_->context.architecture == CpuArchitecture::Arm64; const unsigned count = arm ? 32 : snapshot_->context.architecture == CpuArchitecture::X86_32 ? 8 : 16; fpTable_->setColumnHidden(2, arm); - fpTable_->setHorizontalHeaderLabels({"Register", arm ? "128-bit value" : "XMM low 128", "YMM high 128"}); + fpTable_->setHorizontalHeaderLabels({tr("Register"), arm ? tr("128-bit value") : tr("XMM low 128"), tr("YMM high 128")}); fpTable_->setRowCount(count + (arm ? 2 : 1)); const QString unavailable = snapshot_->vectorError ? - QString("unavailable: %1").arg(QString::fromStdString(snapshot_->vectorError.message())) : "unavailable"; + tr("unavailable: %1").arg(QString::fromStdString(snapshot_->vectorError.message())) : tr("unavailable"); for (unsigned row = 0; row < count; ++row) { fpTable_->setItem(row, 0, new QTableWidgetItem(QString(arm ? "V%1" : "XMM%1/YMM%1").arg(row))); fpTable_->setItem(row, 1, new QTableWidgetItem(row < snapshot_->vectors.count ? bytesToHex(snapshot_->vectors.registers[row].data(), 16) : unavailable)); fpTable_->setItem(row, 2, new QTableWidgetItem(row < snapshot_->ymmCount ? - bytesToHex(snapshot_->ymmHigh[row].data(), 16) : "unavailable")); + bytesToHex(snapshot_->ymmHigh[row].data(), 16) : tr("unavailable"))); } for (unsigned row = count; row < static_cast(fpTable_->rowCount()); ++row) { const bool control = !arm || row == count; @@ -198,13 +198,13 @@ void RegisterEditorWindow::applyRegisters() { for (size_t row = 0; row < registers.size(); ++row) { bool ok = false; const auto value = table_->item(row, 1)->text().toULongLong(&ok, 16); - if (!ok) { statusLabel_->setText(QString("Invalid %1 value").arg(QString::fromStdString(registers[row].name))); return; } + if (!ok) { statusLabel_->setText(tr("Invalid %1 value").arg(QString::fromStdString(registers[row].name))); return; } values.push_back(value); changed |= value != registers[row].value; } auto valid = mergeCpuRegisterEdits(snapshot_->context, snapshot_->context, values); if (!valid) { statusLabel_->setText(QString::fromStdString(valid.error())); return; } - if (!changed) { statusLabel_->setText("No register values changed"); return; } + if (!changed) { statusLabel_->setText(tr("No register values changed")); return; } auto applied = ce::inspectThread(*proc_, tid, false, &*snapshot_, values); if (!applied) { statusLabel_->setText(QString::fromStdString(applied.error())); @@ -212,13 +212,13 @@ void RegisterEditorWindow::applyRegisters() { return; } displaySnapshot(std::move(*applied)); - statusLabel_->setText(QString("Applied changes to TID %1").arg(tid)); + statusLabel_->setText(tr("Applied changes to TID %1").arg(tid)); } void RegisterEditorWindow::refreshGdbRegisters() { auto* guest=dynamic_cast(proc_);if (!guest) return; if (!guest->targetDescription().live) { - statusLabel_->setText("GDB target is disconnected or exited");table_->setEnabled(false);applyButton_->setEnabled(false);return; + statusLabel_->setText(tr("GDB target is disconnected or exited"));table_->setEnabled(false);applyButton_->setEnabled(false);return; } table_->setEnabled(true); auto rows=table_->selectionModel()->selectedRows(); @@ -228,7 +228,7 @@ void RegisterEditorWindow::refreshGdbRegisters() { auto* value=table_->item(row.row(),1); auto bytes=guest->readRegister(guest->registerDescription().registers[row.row()].name); if (!bytes) { - lastError=QString::fromStdString(bytes.error());value->setText("Unavailable: "+lastError); + lastError=QString::fromStdString(bytes.error());value->setText(tr("Unavailable: %1").arg(lastError)); value->setData(Qt::UserRole,{});value->setFlags(value->flags() & ~Qt::ItemIsEditable); if (!guest->targetDescription().live) {table_->setEnabled(false);break;} } else { @@ -238,7 +238,7 @@ void RegisterEditorWindow::refreshGdbRegisters() { } } applyButton_->setEnabled(table_->isEnabled()); - statusLabel_->setText(lastError.isEmpty() ? QString("Read %1 selected register(s). Select other rows and Refresh to read them.").arg(count) : lastError); + statusLabel_->setText(lastError.isEmpty() ? tr("Read %1 selected register(s). Select other rows and Refresh to read them.").arg(count) : lastError); } void RegisterEditorWindow::applyGdbRegisters() { auto* guest=dynamic_cast(proc_);if (!guest) return; @@ -247,23 +247,23 @@ void RegisterEditorWindow::applyGdbRegisters() { auto* item=table_->item(row,1);if (!item->data(Qt::UserRole).isValid()) continue; const auto text=item->text().trimmed().toLatin1();const auto original=item->data(Qt::UserRole).toByteArray(); if (text.size()!=original.size()*2 || !std::all_of(text.begin(),text.end(),[](unsigned char c){return (c>='0' && c<='9') || (c>='a' && c<='f') || (c>='A' && c<='F');})) { - statusLabel_->setText(QString("%1 requires %2 hexadecimal byte pairs").arg(table_->item(row,0)->text()).arg(original.size()));return; + statusLabel_->setText(tr("%1 requires %2 hexadecimal byte pairs").arg(table_->item(row,0)->text()).arg(original.size()));return; } auto bytes=QByteArray::fromHex(text);if (bytes!=original) edits.push_back({row,std::move(bytes)}); } - if (edits.empty()) {statusLabel_->setText("No register values changed");return;} - if (!guest->targetDescription().live) {table_->setEnabled(false);applyButton_->setEnabled(false);statusLabel_->setText("GDB target is disconnected or exited");return;} + if (edits.empty()) {statusLabel_->setText(tr("No register values changed"));return;} + if (!guest->targetDescription().live) {table_->setEnabled(false);applyButton_->setEnabled(false);statusLabel_->setText(tr("GDB target is disconnected or exited"));return;} size_t completed=0; for (const auto& edit:edits) { const auto& reg=guest->registerDescription().registers[edit.row]; auto result=guest->writeRegister(reg.name,{reinterpret_cast(edit.bytes.constData()),static_cast(edit.bytes.size())}); if (!result) { - statusLabel_->setText(QString("Applied %1 of %2 register edits. %3: %4. Refresh before retrying.").arg(completed).arg(edits.size()).arg(QString::fromStdString(reg.name),QString::fromStdString(result.error()))); + statusLabel_->setText(tr("Applied %1 of %2 register edits. %3: %4. Refresh before retrying.").arg(completed).arg(edits.size()).arg(QString::fromStdString(reg.name),QString::fromStdString(result.error()))); applyButton_->setEnabled(false);return; } table_->item(edit.row,1)->setData(Qt::UserRole,edit.bytes);++completed; } - statusLabel_->setText(QString("Applied %1 register edit(s)").arg(completed)); + statusLabel_->setText(tr("Applied %1 register edit(s)").arg(completed)); } } // namespace ce::gui diff --git a/gui/scripteditor.cpp b/gui/scripteditor.cpp index 8bd8c309..4a967df5 100644 --- a/gui/scripteditor.cpp +++ b/gui/scripteditor.cpp @@ -146,52 +146,52 @@ class AaHighlighter : public QSyntaxHighlighter { ScriptEditor::ScriptEditor(ProcessHandle* proc, AutoAssembler* autoAsm, QWidget* parent) : QMainWindow(parent), proc_(proc), autoAsm_(autoAsm) { - setWindowTitle("Auto Assembler"); + setWindowTitle(tr("Auto Assembler")); resize(860, 600); // wide enough for the whole toolbar, Templates included // Toolbar auto* toolbar = new QToolBar; toolbar->setMovable(false); - executeBtn_ = new QAction("Execute", this); + executeBtn_ = new QAction(tr("Execute"), this); executeBtn_->setEnabled(proc_ && autoAsm_); executeBtn_->setObjectName("primaryButton"); - executeBtn_->setToolTip("Enable the script in the selected process"); + executeBtn_->setToolTip(tr("Enable the script in the selected process")); connect(executeBtn_, &QAction::triggered, this, &ScriptEditor::onExecute); - disableBtn_ = new QAction("Disable", this); + disableBtn_ = new QAction(tr("Disable"), this); disableBtn_->setEnabled(false); connect(disableBtn_, &QAction::triggered, this, &ScriptEditor::onDisable); - auto* checkBtn = new QAction("Syntax Check", this); + auto* checkBtn = new QAction(tr("Syntax Check"), this); connect(checkBtn, &QAction::triggered, this, &ScriptEditor::onCheck); - auto* addTableBtn = new QAction("Add to Cheat Table", this); + auto* addTableBtn = new QAction(tr("Add to Cheat Table"), this); addTableBtn_ = addTableBtn; addTableBtn_->setEnabled(false); - addTableBtn->setToolTip("Save this script as a cheat-table entry whose checkbox " - "enables/disables it."); + addTableBtn->setToolTip(tr("Save this script as a cheat-table entry whose checkbox " + "enables/disables it.")); connect(addTableBtn, &QAction::triggered, this, &ScriptEditor::onAddToTable); - auto* loadBtn = new QAction("Load", this); + auto* loadBtn = new QAction(tr("Load"), this); connect(loadBtn, &QAction::triggered, this, [this]() { QPointer self(this); - auto path = QFileDialog::getOpenFileName(this, "Load Script", "", "CE Scripts (*.cea *.CEA *.asm);;All Files (*)"); + auto path = QFileDialog::getOpenFileName(this, tr("Load Script"), "", tr("CE Scripts (*.cea *.CEA *.asm);;All Files (*)")); if (!self || path.isEmpty()) return; QFile f(path); if (f.open(QIODevice::ReadOnly)) { editor_->setPlainText(QTextStream(&f).readAll()); - } else QMessageBox::warning(this, "Load failed", f.errorString()); + } else QMessageBox::warning(this, tr("Load failed"), f.errorString()); }); - auto* saveBtn = new QAction("Save", this); + auto* saveBtn = new QAction(tr("Save"), this); connect(saveBtn, &QAction::triggered, this, [this]() { QPointer self(this); - auto path = QFileDialog::getSaveFileName(this, "Save Script", "", "CE Scripts (*.cea *.CEA);;All Files (*)"); + auto path = QFileDialog::getSaveFileName(this, tr("Save Script"), "", tr("CE Scripts (*.cea *.CEA);;All Files (*)")); if (!self || path.isEmpty()) return; QSaveFile file(path); const QByteArray bytes = editor_->toPlainText().toUtf8(); if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.flush() || !file.commit()) - QMessageBox::warning(this, "Save failed", file.errorString()); + QMessageBox::warning(this, tr("Save failed"), file.errorString()); }); toolbar->setMovable(false); @@ -234,8 +234,8 @@ ScriptEditor::ScriptEditor(ProcessHandle* proc, AutoAssembler* autoAsm, QWidget* QString name = label; connect(action, &QAction::triggered, this, [this, body, name]() { if (!editor_->toPlainText().trimmed().isEmpty()) { - auto answer = QMessageBox::question(this, "Insert template?", - QString("Replace the current script with the '%1' template?\n" + auto answer = QMessageBox::question(this, tr("Insert template?"), + tr("Replace the current script with the '%1' template?\n" "Click No to insert at the cursor instead.").arg(name), QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::Yes); @@ -248,9 +248,9 @@ ScriptEditor::ScriptEditor(ProcessHandle* proc, AutoAssembler* autoAsm, QWidget* editor_->insertPlainText(body); }); } - auto* templateAction = toolbar->addAction("Templates"); + auto* templateAction = toolbar->addAction(tr("Templates")); templateAction->setMenu(templateMenu); - templateAction->setToolTip("Generate a template for the selected instruction (Ctrl+I)"); + templateAction->setToolTip(tr("Generate a template for the selected instruction (Ctrl+I)")); if (auto* button = qobject_cast(toolbar->widgetForAction(templateAction))) button->setPopupMode(QToolButton::InstantPopup); @@ -262,15 +262,15 @@ ScriptEditor::ScriptEditor(ProcessHandle* proc, AutoAssembler* autoAsm, QWidget* editor_ = new CodeEditor; editor_->setTabStopDistance(QFontMetricsF(editor_->font()).horizontalAdvance(' ') * 4); new AaHighlighter(editor_->document()); // syntax coloring - editor_->setPlaceholderText("Choose Templates to generate an injection at the selected instruction,\n" - "or write an Auto Assembler script here."); + editor_->setPlaceholderText(tr("Choose Templates to generate an injection at the selected instruction,\n" + "or write an Auto Assembler script here.")); splitter->addWidget(editor_); output_ = new QTextEdit; output_->setReadOnly(true); output_->setFont(monospaceFont(-1)); output_->document()->setDocumentMargin(6); - output_->setPlaceholderText("Messages from Execute and Syntax Check appear here."); + output_->setPlaceholderText(tr("Messages from Execute and Syntax Check appear here.")); output_->setMaximumHeight(150); // The console paints its own line colors, so it can't inherit the app // stylesheet; give it the theme's editor background/text (was hardcoded dark, @@ -313,7 +313,7 @@ void ScriptEditor::onExecute() { if (busy_ || enabled_) return; if (!proc_ || !autoAsm_) { output_->setTextColor(ce::gui::editorPalette().error); - output_->append("No process selected!"); + output_->append(tr("No process selected!")); return; } @@ -331,7 +331,7 @@ void ScriptEditor::onExecute() { if (!self) return; output_->clear(); output_->setTextColor(ce::gui::editorPalette().text); - output_->append("Executing..."); + output_->append(tr("Executing...")); auto result = assembler->execute(*process, script); if (!self) { @@ -346,21 +346,21 @@ void ScriptEditor::onExecute() { if (result.success) { output_->setTextColor(ce::gui::editorPalette().success); - output_->append("Script executed successfully."); + output_->append(tr("Script executed successfully.")); lastDisableInfo_ = result.disableInfo; enabledScript_ = script; enabled_ = true; enableOrder_ = AutoAsmUiOperation::nextOrder(); } else { output_->setTextColor(ce::gui::editorPalette().error); - output_->append("FAILED: " + QString::fromStdString(result.error)); + output_->append(tr("FAILED: %1").arg(QString::fromStdString(result.error))); if (result.disableInfo.ownership || result.disableInfo.image || !result.disableInfo.symbols.empty() || !result.disableInfo.originals.empty() || !result.disableInfo.allocs.empty() || !result.disableInfo.protections.empty()) { lastDisableInfo_ = std::move(result.disableInfo); enabledScript_ = script; enabled_ = true; enableOrder_ = AutoAsmUiOperation::nextOrder(); - output_->append("Cleanup is still pending. Use Disable to retry restoration."); + output_->append(tr("Cleanup is still pending. Use Disable to retry restoration.")); } } } @@ -389,7 +389,7 @@ bool ScriptEditor::cleanupInjection(bool report) { if (self && report) { output_->clear(); output_->setTextColor(ce::gui::editorPalette().text); - output_->append("Disabling..."); + output_->append(tr("Disabling...")); } auto result = assembler->disable(*process, script, undo); @@ -401,7 +401,7 @@ bool ScriptEditor::cleanupInjection(bool report) { if (result.success) { if (report) { output_->setTextColor(ce::gui::editorPalette().success); - output_->append("Script disabled."); + output_->append(tr("Script disabled.")); } enabled_ = false; enabledScript_.clear(); @@ -409,8 +409,8 @@ bool ScriptEditor::cleanupInjection(bool report) { } else { if (report) { output_->setTextColor(ce::gui::editorPalette().error); - output_->append("Cleanup incomplete: " + QString::fromStdString(result.error)); - output_->append("Recovery state retained. Use Disable to retry."); + output_->append(tr("Cleanup incomplete: %1").arg(QString::fromStdString(result.error))); + output_->append(tr("Recovery state retained. Use Disable to retry.")); } } return result.success; @@ -423,11 +423,11 @@ void ScriptEditor::onGenerateCodeInjection() { void ScriptEditor::onGenerateInjection(ce::InjectionKind kind) { QPointer self(this); if (!proc_) { - QMessageBox::warning(this, "No process", "Attach to a process first."); + QMessageBox::warning(this, tr("No process"), tr("Attach to a process first.")); return; } if (enabled_) { - QMessageBox::information(this, "Injection template", "Disable the current script before generating another injection."); + QMessageBox::information(this, tr("Injection template"), tr("Disable the current script before generating another injection.")); return; } QString initial; @@ -441,19 +441,19 @@ void ScriptEditor::onGenerateInjection(ce::InjectionKind kind) { } } bool ok = false; - QString text = QInputDialog::getText(this, "Injection template", - "Address or expression to inject at:", QLineEdit::Normal, initial, &ok); + QString text = QInputDialog::getText(this, tr("Injection template"), + tr("Address or expression to inject at:"), QLineEdit::Normal, initial, &ok); if (!self || !ok || text.trimmed().isEmpty()) return; ce::SymbolResolver symbols; symbols.loadProcess(*proc_); auto address = ce::ExpressionParser(proc_, &symbols).parse(text.trimmed().toStdString()); if (!address || !*address) { - QMessageBox::warning(this, "Bad address", "Enter a valid address, module offset, or symbol expression."); + QMessageBox::warning(this, tr("Bad address"), tr("Enter a valid address, module offset, or symbol expression.")); return; } std::string pointerRegister; if (kind == ce::InjectionKind::Pointer) { - QString reg = QInputDialog::getText(this, "Pointer injection", "Register holding the pointer:", + QString reg = QInputDialog::getText(this, tr("Pointer injection"), tr("Register holding the pointer:"), QLineEdit::Normal, proc_->runs32BitCode() ? "eax" : "rax", &ok); if (!self || !ok || reg.trimmed().isEmpty()) return; pointerRegister = reg.trimmed().toStdString(); @@ -462,12 +462,12 @@ void ScriptEditor::onGenerateInjection(ce::InjectionKind kind) { size_t size = *address == injectionAddress_ ? injectionSize_ : 5; std::string script = ce::generateInjectionScript(*proc_, *address, kind, genErr, pointerRegister, size); if (script.empty()) { - QMessageBox::warning(this, "Injection template", QString::fromStdString(genErr)); + QMessageBox::warning(this, tr("Injection template"), QString::fromStdString(genErr)); return; } if (!editor_->toPlainText().trimmed().isEmpty()) { - auto answer = QMessageBox::question(this, "Replace script?", - "Replace the current script with the generated injection?", + auto answer = QMessageBox::question(this, tr("Replace script?"), + tr("Replace the current script with the generated injection?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); if (!self || answer != QMessageBox::Yes) return; } @@ -476,29 +476,29 @@ void ScriptEditor::onGenerateInjection(ce::InjectionKind kind) { editor_->setPlainText(QString::fromStdString(script)); output_->clear(); output_->setTextColor(ce::gui::editorPalette().success); - output_->append(QString("Generated injection at 0x%1. Edit the code in newmem.").arg(QString::number(*address, 16))); + output_->append(tr("Generated injection at 0x%1. Edit the code in newmem.").arg(QString::number(*address, 16))); } void ScriptEditor::onAddToTable() { QPointer self(this); auto script = editor_->toPlainText(); if (script.trimmed().isEmpty()) { - QMessageBox::information(this, "Add to Cheat Table", "The script is empty."); + QMessageBox::information(this, tr("Add to Cheat Table"), tr("The script is empty.")); return; } if (!addToTable_) { - QMessageBox::warning(this, "Add to Cheat Table", - "This editor isn't connected to a cheat table."); + QMessageBox::warning(this, tr("Add to Cheat Table"), + tr("This editor isn't connected to a cheat table.")); return; } bool ok = false; - QString desc = QInputDialog::getText(this, "Add to Cheat Table", - "Description for the table entry:", QLineEdit::Normal, + QString desc = QInputDialog::getText(this, tr("Add to Cheat Table"), + tr("Description for the table entry:"), QLineEdit::Normal, defaultDescription_, &ok); if (!self || !ok) return; addToTable_(desc, script); output_->setTextColor(ce::gui::editorPalette().success); - output_->append("Saved to the cheat table. Toggle its checkbox to enable/disable."); + output_->append(tr("Saved to the cheat table. Toggle its checkbox to enable/disable.")); } void ScriptEditor::onCheck() { @@ -518,10 +518,10 @@ void ScriptEditor::onCheck() { if (result.success) { output_->setTextColor(ce::gui::editorPalette().success); - output_->append("Syntax check passed."); + output_->append(tr("Syntax check passed.")); } else { output_->setTextColor(ce::gui::editorPalette().error); - output_->append("Syntax check failed: " + QString::fromStdString(result.error)); + output_->append(tr("Syntax check failed: %1").arg(QString::fromStdString(result.error))); } } diff --git a/gui/secret_store.cpp b/gui/secret_store.cpp index bfdf45ff..ec5081da 100644 --- a/gui/secret_store.cpp +++ b/gui/secret_store.cpp @@ -1,5 +1,6 @@ #include "gui/secret_store.hpp" +#include #include #include #include @@ -125,8 +126,8 @@ bool runPrompt(const QDBusObjectPath& prompt, QString* error) { if (failed(call(prompt.path(), kPromptInterface, "Prompt", {QString()}, error), 0)) return false; if (!waiter.done) loop.exec(); bus.disconnect(kService, prompt.path(), kPromptInterface, "Completed", &waiter, SLOT(completed(bool,QDBusVariant))); - if (!waiter.done) { if (error) *error = "the keyring did not answer"; return false; } - if (waiter.dismissed) { if (error) *error = "the keyring prompt was dismissed"; return false; } + if (!waiter.done) { if (error) *error = QCoreApplication::translate("SecretStore", "the keyring did not answer"); return false; } + if (waiter.dismissed) { if (error) *error = QCoreApplication::translate("SecretStore", "the keyring prompt was dismissed"); return false; } return true; } @@ -193,7 +194,7 @@ bool SecretStore::store(const QString& value, QString* error) { } ++changes; persisted_ = false; - if (!available()) { if (error) *error = "no keyring service is running"; return false; } + if (!available()) { if (error) *error = QCoreApplication::translate("SecretStore", "no keyring service is running"); return false; } auto session = openSession(error); if (!session) return false; const SessionCloser closer{*session}; @@ -201,12 +202,13 @@ bool SecretStore::store(const QString& value, QString* error) { if (failed(alias, 1)) return false; const auto collection = alias.arguments().at(0).value(); if (collection.path().isEmpty() || collection.path() == "/") { - if (error) *error = "the keyring has no default collection"; + if (error) *error = QCoreApplication::translate("SecretStore", "the keyring has no default collection"); return false; } if (!unlock({collection}, error)) return false; QVariantMap properties; - properties["org.freedesktop.Secret.Item.Label"] = QString("Cheat Engine for Linux: %1 API key").arg(account_); + properties["org.freedesktop.Secret.Item.Label"] = + QCoreApplication::translate("SecretStore", "Cheat Engine for Linux: %1 API key").arg(account_); properties["org.freedesktop.Secret.Item.Attributes"] = QVariant::fromValue(attributesFor(account_)); const Secret secret{*session, {}, value.toUtf8(), "text/plain"}; auto created = call(collection.path(), "org.freedesktop.Secret.Collection", "CreateItem", diff --git a/gui/settingsdialog.cpp b/gui/settingsdialog.cpp index 886241ef..44b21eec 100644 --- a/gui/settingsdialog.cpp +++ b/gui/settingsdialog.cpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include "core/agent_chat.hpp" #include "gui/agent_panel.hpp" @@ -51,6 +53,8 @@ constexpr const char* HK_VALUE_STEP_KEY = "hotkeys/valueStep"; constexpr const char* NET_CESERVER_HOST_KEY = "network/ceserverHost"; constexpr const char* NET_CESERVER_PORT_KEY = "network/ceserverPort"; +constexpr const char* UI_LANGUAGE_KEY = "ui/language"; + // Fields sized for their content instead of stretching across the page. constexpr int kFieldWidth = 260; @@ -98,15 +102,22 @@ QLabel* hint(const QString& text) { QComboBox* sizeCombo(const QString& current) { auto* combo = new QComboBox; - for (const char* size : {"1", "2", "4", "8"}) - combo->addItem(QString("%1 %2").arg(size).arg(QString(size) == "1" ? "byte" : "bytes"), QString(size)); + for (const char* size : {"1", "2", "4", "8"}) { + const QString unit = QString(size) == "1" + ? QCoreApplication::translate("SettingsDialog", "byte") + : QCoreApplication::translate("SettingsDialog", "bytes"); + combo->addItem(QCoreApplication::translate("SettingsDialog", "%1 %2") + .arg(size) + .arg(unit), + QString(size)); + } combo->setCurrentIndex(std::max(0, combo->findData(current))); return sized(combo); } } // namespace SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent) { - setWindowTitle("Settings"); + setWindowTitle(tr("Settings")); resize(760, 520); auto* root = new QVBoxLayout(this); @@ -115,7 +126,15 @@ SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent) { // Vertical category sidebar: every page name is visible at once. auto* nav = new QListWidget; nav->setObjectName("settingsNav"); - nav->setFixedWidth(std::max(160, QFontMetrics(nav->font()).horizontalAdvance("Memory View") + 48)); + const QStringList pageNames = { + tr("Scan"), tr("Display"), tr("Language"), tr("Memory View"), tr("Debugger"), + tr("Hotkeys"), tr("Network"), tr("Lua"), tr("AI Assistant"), + }; + int navTextWidth = 0; + const QFontMetrics navMetrics(nav->font()); + for (const QString& name : pageNames) + navTextWidth = std::max(navTextWidth, navMetrics.horizontalAdvance(name)); + nav->setFixedWidth(std::max(160, navTextWidth + 48)); nav->setUniformItemSizes(true); nav->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); nav->setStyleSheet("QListWidget#settingsNav { padding: 4px; }" @@ -130,14 +149,15 @@ SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent) { nav->addItem(name); pages->addWidget(scroll); }; - addPage(buildScanPage(), "Scan"); - addPage(buildDisplayPage(), "Display"); - addPage(buildMemoryViewPage(), "Memory View"); - addPage(buildDebuggerPage(), "Debugger"); - addPage(buildHotkeysPage(), "Hotkeys"); - addPage(buildNetworkPage(), "Network"); - addPage(buildLuaPage(), "Lua"); - addPage(buildAgentPage(), "AI Assistant"); + addPage(buildScanPage(), pageNames[0]); + addPage(buildDisplayPage(), pageNames[1]); + addPage(buildLanguagePage(), pageNames[2]); + addPage(buildMemoryViewPage(), pageNames[3]); + addPage(buildDebuggerPage(), pageNames[4]); + addPage(buildHotkeysPage(), pageNames[5]); + addPage(buildNetworkPage(), pageNames[6]); + addPage(buildLuaPage(), pageNames[7]); + addPage(buildAgentPage(), pageNames[8]); connect(nav, &QListWidget::currentRowChanged, pages, &QStackedWidget::setCurrentIndex); nav->setCurrentRow(0); @@ -150,13 +170,13 @@ SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent) { auto* btnRow = new QHBoxLayout; btnRow->addStretch(); - auto* okBtn = new QPushButton("OK"); + auto* okBtn = new QPushButton(tr("OK")); okBtn->setObjectName("primaryButton"); okBtn->setDefault(true); connect(okBtn, &QPushButton::clicked, this, [this]() { onApply(); accept(); }); - auto* cancelBtn = new QPushButton("Cancel"); + auto* cancelBtn = new QPushButton(tr("Cancel")); connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject); - auto* applyBtn = new QPushButton("Apply"); + auto* applyBtn = new QPushButton(tr("Apply")); connect(applyBtn, &QPushButton::clicked, this, &SettingsDialog::onApply); for (auto* button : {okBtn, cancelBtn, applyBtn}) { button->setMinimumWidth(84); @@ -167,159 +187,182 @@ SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent) { QWidget* SettingsDialog::buildScanPage() { auto* w = new QWidget; - auto* form = makePage(w, "Scan", "Defaults for a new scan. The main window's scan options start from these."); + auto* form = makePage(w, tr("Scan"), + tr("Defaults for a new scan. The main window's scan options start from these.")); QSettings s; defaultValueTypeCombo_ = sized(new QComboBox); - defaultValueTypeCombo_->addItems({"Byte", "2 Bytes", "4 Bytes", "8 Bytes", - "Float", "Double", "String", "Array of byte", "Pointer"}); + defaultValueTypeCombo_->addItems({tr("Byte"), tr("2 Bytes"), tr("4 Bytes"), tr("8 Bytes"), + tr("Float"), tr("Double"), tr("String"), + tr("Array of byte"), tr("Pointer")}); defaultValueTypeCombo_->setCurrentIndex(s.value(SCAN_DEFAULT_VT_KEY, 2).toInt()); - form->addRow("Value type:", defaultValueTypeCombo_); + form->addRow(tr("Value type:"), defaultValueTypeCombo_); alignSpin_ = sized(new QSpinBox); alignSpin_->setRange(1, 16); - alignSpin_->setSuffix(" bytes"); + alignSpin_->setSuffix(tr(" bytes")); alignSpin_->setValue(s.value(SCAN_ALIGN_KEY, 4).toInt()); - form->addRow("Fast scan alignment:", alignSpin_); + form->addRow(tr("Fast scan alignment:"), alignSpin_); - fastScanCheck_ = new QCheckBox("Fast scan (only aligned addresses)"); + fastScanCheck_ = new QCheckBox(tr("Fast scan (only aligned addresses)")); fastScanCheck_->setChecked(s.value(SCAN_FAST_KEY, true).toBool()); form->addRow("", fastScanCheck_); - writableCheck_ = new QCheckBox("Only writable memory"); + writableCheck_ = new QCheckBox(tr("Only writable memory")); writableCheck_->setChecked(s.value(SCAN_WRITABLE_KEY, true).toBool()); form->addRow("", writableCheck_); - executableCheck_ = new QCheckBox("Only executable memory"); + executableCheck_ = new QCheckBox(tr("Only executable memory")); executableCheck_->setChecked(s.value(SCAN_EXEC_KEY, false).toBool()); form->addRow("", executableCheck_); threadsSpin_ = sized(new QSpinBox); threadsSpin_->setRange(0, 256); - threadsSpin_->setSpecialValueText("Automatic (one per CPU)"); + threadsSpin_->setSpecialValueText(tr("Automatic (one per CPU)")); threadsSpin_->setValue(s.value(SCAN_THREADS_KEY, 0).toInt()); - form->addRow("Worker threads:", threadsSpin_); + form->addRow(tr("Worker threads:"), threadsSpin_); defaultFloatToleranceSpin_ = sized(new QDoubleSpinBox); defaultFloatToleranceSpin_->setRange(0.0, 1e9); defaultFloatToleranceSpin_->setDecimals(6); - defaultFloatToleranceSpin_->setSpecialValueText("Automatic"); + defaultFloatToleranceSpin_->setSpecialValueText(tr("Automatic")); defaultFloatToleranceSpin_->setValue(s.value(SCAN_FLOAT_TOL_KEY, 0.0).toDouble()); - form->addRow("Float tolerance:", defaultFloatToleranceSpin_); - form->addRow("", hint("Used by the \"Extreme\" float rounding mode.")); + form->addRow(tr("Float tolerance:"), defaultFloatToleranceSpin_); + form->addRow("", hint(tr("Used by the \"Extreme\" float rounding mode."))); return w; } QWidget* SettingsDialog::buildDisplayPage() { auto* w = new QWidget; - auto* form = makePage(w, "Display", "Theme and the font used by tables, the disassembler and hex views."); + auto* form = makePage(w, tr("Display"), + tr("Theme and the font used by tables, the disassembler and hex views.")); QSettings s; themeCombo_ = sized(new QComboBox); themeCombo_->setObjectName("displayTheme"); - themeCombo_->addItems({"Light", "Dark"}); + themeCombo_->addItems({tr("Light"), tr("Dark")}); themeCombo_->setCurrentIndex(isDarkTheme() ? 1 : 0); - form->addRow("Theme:", themeCombo_); + form->addRow(tr("Theme:"), themeCombo_); fontFamilyCombo_ = sized(new QFontComboBox); fontFamilyCombo_->setFontFilters(QFontComboBox::MonospacedFonts); fontFamilyCombo_->setCurrentFont(QFont(s.value(DISP_FONT_FAMILY_KEY, defaultMonospaceFamily()).toString())); - form->addRow("Monospace font:", fontFamilyCombo_); + form->addRow(tr("Monospace font:"), fontFamilyCombo_); fontSizeSpin_ = sized(new QSpinBox); fontSizeSpin_->setObjectName("displayFontSize"); fontSizeSpin_->setRange(7, 24); - fontSizeSpin_->setSuffix(" pt"); + fontSizeSpin_->setSuffix(tr(" pt")); fontSizeSpin_->setValue(s.value(DISP_FONT_SIZE_KEY, defaultMonospaceSize()).toInt()); - form->addRow("Font size:", fontSizeSpin_); + form->addRow(tr("Font size:"), fontSizeSpin_); addressWidthCombo_ = sized(new QComboBox); - addressWidthCombo_->addItems({"32-bit (8 hex digits)", "64-bit (16 hex digits)"}); + addressWidthCombo_->addItems({tr("32-bit (8 hex digits)"), tr("64-bit (16 hex digits)")}); addressWidthCombo_->setCurrentIndex(s.value(DISP_ADDR_WIDTH_KEY, 1).toInt()); - form->addRow("Address width:", addressWidthCombo_); + form->addRow(tr("Address width:"), addressWidthCombo_); - hexUpperCheck_ = new QCheckBox("Uppercase hexadecimal (0x7F3A, not 0x7f3a)"); + hexUpperCheck_ = new QCheckBox(tr("Uppercase hexadecimal (0x7F3A, not 0x7f3a)")); hexUpperCheck_->setChecked(s.value(DISP_HEX_UPPER_KEY, false).toBool()); form->addRow("", hexUpperCheck_); return w; } +QWidget* SettingsDialog::buildLanguagePage() { + auto* w = new QWidget; + auto* form = makePage(w, tr("Language"), + tr("Language of the interface. It applies after the next start.")); + + languageCombo_ = sized(new QComboBox); + languageCombo_->addItem(tr("Follow system"), QString()); + // Language names stay in their own language. + languageCombo_->addItem(QStringLiteral("English"), QStringLiteral("en")); + languageCombo_->addItem(QStringLiteral("简体中文"), QStringLiteral("zh_CN")); + languageCombo_->setCurrentIndex(std::max(0, languageCombo_->findData( + QSettings().value(UI_LANGUAGE_KEY).toString()))); + form->addRow(tr("Interface language:"), languageCombo_); + form->addRow("", hint(tr("English and Simplified Chinese are available for now."))); + return w; +} + QWidget* SettingsDialog::buildMemoryViewPage() { auto* w = new QWidget; - auto* form = makePage(w, "Memory View", "Layout and refresh of the hex dump in the Memory Viewer."); + auto* form = makePage(w, tr("Memory View"), + tr("Layout and refresh of the hex dump in the Memory Viewer.")); QSettings s; bytesPerRowSpin_ = sized(new QSpinBox); bytesPerRowSpin_->setRange(8, 64); // the view uses 8, 16, 32 or 64 bytesPerRowSpin_->setSingleStep(8); bytesPerRowSpin_->setValue(s.value(MV_BYTES_PER_ROW_KEY, 16).toInt()); - form->addRow("Bytes per row:", bytesPerRowSpin_); + form->addRow(tr("Bytes per row:"), bytesPerRowSpin_); - autoRefreshCheck_ = new QCheckBox("Refresh values while the view is open"); + autoRefreshCheck_ = new QCheckBox(tr("Refresh values while the view is open")); autoRefreshCheck_->setChecked(s.value(MV_AUTO_REFRESH_KEY, true).toBool()); form->addRow("", autoRefreshCheck_); refreshIntervalSpin_ = sized(new QSpinBox); refreshIntervalSpin_->setRange(100, 30'000); refreshIntervalSpin_->setSingleStep(100); - refreshIntervalSpin_->setSuffix(" ms"); + refreshIntervalSpin_->setSuffix(tr(" ms")); refreshIntervalSpin_->setValue(s.value(MV_REFRESH_MS_KEY, 2000).toInt()); // the Memory Viewer default refreshIntervalSpin_->setEnabled(autoRefreshCheck_->isChecked()); connect(autoRefreshCheck_, &QCheckBox::toggled, refreshIntervalSpin_, &QWidget::setEnabled); - form->addRow("Refresh interval:", refreshIntervalSpin_); - form->addRow("", hint("Applies to Memory Viewer windows opened from now on.")); + form->addRow(tr("Refresh interval:"), refreshIntervalSpin_); + form->addRow("", hint(tr("Applies to Memory Viewer windows opened from now on."))); return w; } QWidget* SettingsDialog::buildDebuggerPage() { auto* w = new QWidget; - auto* form = makePage(w, "Debugger", "Defaults for hardware watchpoints."); + auto* form = makePage(w, tr("Debugger"), tr("Defaults for hardware watchpoints.")); QSettings s; dataBpSizeCombo_ = sizeCombo(s.value(DBG_BP_SIZE_KEY, "4").toString()); - form->addRow("Data breakpoint size:", dataBpSizeCombo_); + form->addRow(tr("Data breakpoint size:"), dataBpSizeCombo_); watchSizeCombo_ = sizeCombo(s.value(CF_WATCH_SIZE_KEY, "4").toString()); - form->addRow("Find-what-writes size:", watchSizeCombo_); - form->addRow("", hint("How many bytes from the address a watchpoint covers. " - "x86 watchpoints must be aligned to their size.")); + form->addRow(tr("Find-what-writes size:"), watchSizeCombo_); + form->addRow("", hint(tr("How many bytes from the address a watchpoint covers. " + "x86 watchpoints must be aligned to their size."))); return w; } QWidget* SettingsDialog::buildHotkeysPage() { auto* w = new QWidget; - auto* form = makePage(w, "Hotkeys", "Hotkeys belong to cheat table entries: right-click an entry and " - "choose Hotkeys."); + auto* form = makePage(w, tr("Hotkeys"), tr("Hotkeys belong to cheat table entries: right-click an entry and " + "choose Hotkeys.")); QSettings s; valueStepEdit_ = sized(new QLineEdit(s.value(HK_VALUE_STEP_KEY, "1").toString())); valueStepEdit_->setValidator(new QDoubleValidator(valueStepEdit_)); - form->addRow("Default value step:", valueStepEdit_); - form->addRow("", hint("How much an increase or decrease hotkey changes a value, " - "until the entry sets its own step.")); + form->addRow(tr("Default value step:"), valueStepEdit_); + form->addRow("", hint(tr("How much an increase or decrease hotkey changes a value, " + "until the entry sets its own step."))); return w; } QWidget* SettingsDialog::buildNetworkPage() { auto* w = new QWidget; - auto* form = makePage(w, "Network", "The ceserver that File > Connect to ceserver offers by default."); + auto* form = makePage(w, tr("Network"), + tr("The ceserver that File > Connect to ceserver offers by default.")); QSettings s; ceserverHostEdit_ = sized(new QLineEdit(s.value(NET_CESERVER_HOST_KEY, "127.0.0.1").toString())); - ceserverHostEdit_->setPlaceholderText("host name or IP address"); - form->addRow("ceserver host:", ceserverHostEdit_); + ceserverHostEdit_->setPlaceholderText(tr("host name or IP address")); + form->addRow(tr("ceserver host:"), ceserverHostEdit_); ceserverPortSpin_ = sized(new QSpinBox); ceserverPortSpin_->setRange(1, 65535); ceserverPortSpin_->setGroupSeparatorShown(false); ceserverPortSpin_->setValue(s.value(NET_CESERVER_PORT_KEY, 52736).toInt()); - form->addRow("ceserver port:", ceserverPortSpin_); + form->addRow(tr("ceserver port:"), ceserverPortSpin_); return w; } QWidget* SettingsDialog::buildLuaPage() { auto* w = new QWidget; - auto* form = makePage(w, "Lua", "Scripts in the autorun folder run once at startup, like Cheat Engine's autorun."); + auto* form = makePage(w, tr("Lua"), + tr("Scripts in the autorun folder run once at startup, like Cheat Engine's autorun.")); const QString folder = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) + "/autorun"; auto* path = new QLineEdit(QDir::toNativeSeparators(folder)); path->setReadOnly(true); path->setToolTip(path->text()); path->setMinimumWidth(kFieldWidth); - auto* open = new QPushButton("Open folder"); + auto* open = new QPushButton(tr("Open folder")); connect(open, &QPushButton::clicked, this, [folder]() { QDir().mkpath(folder); QDesktopServices::openUrl(QUrl::fromLocalFile(folder)); @@ -327,10 +370,10 @@ QWidget* SettingsDialog::buildLuaPage() { auto* row = new QHBoxLayout; row->addWidget(path, 1); row->addWidget(open); - form->addRow("Autorun folder:", row); - form->addRow("", hint("Lua cannot run shell commands or write local files unless Cheat Engine " - "was started with CECORE_LUA_ALLOW_UNSAFE=1 in its environment. A cheat " - "table cannot turn this on by itself.")); + form->addRow(tr("Autorun folder:"), row); + form->addRow("", hint(tr("Lua cannot run shell commands or write local files unless Cheat Engine " + "was started with CECORE_LUA_ALLOW_UNSAFE=1 in its environment. A cheat " + "table cannot turn this on by itself."))); form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); return w; } @@ -341,7 +384,8 @@ const char* const kAgentEnv[2] = {"ANTHROPIC_API_KEY", "OPENAI_API_KEY"}; QString agentKeyState(int provider, const QString& baseUrl) { if (!qEnvironmentVariableIsEmpty(kAgentEnv[provider])) { if (agentEnvKeyApplies(provider == 1, baseUrl)) - return QString("Using %1 from the environment.").arg(kAgentEnv[provider]); + return QCoreApplication::translate("SettingsDialog", "Using %1 from the environment.") + .arg(kAgentEnv[provider]); // Not sent to another server, so a key for it is set here. } QSettings s; @@ -349,46 +393,53 @@ QString agentKeyState(int provider, const QString& baseUrl) { // A key is only sent to the address it was saved for. QString refusal = agentKeyRefusal(provider == 1, baseUrl, s.value(QString("agent/%1/keyOrigin").arg(kAgentAccounts[provider])).toString()); if (!refusal.isEmpty()) refusal.prepend(' '); - if (saved == "keyring") return "A key is saved in the system keyring." + refusal; + if (saved == "keyring") + return QCoreApplication::translate("SettingsDialog", "A key is saved in the system keyring.") + refusal; if (SecretStore(kAgentAccounts[provider]).inMemory()) - return "A key is set until Cheat Engine closes (no keyring was available)." + refusal; - return provider == 0 ? "No key yet. Create one at console.anthropic.com." : "No key set; local servers usually need none."; + return QCoreApplication::translate("SettingsDialog", + "A key is set until Cheat Engine closes (no keyring was available).") + refusal; + return provider == 0 + ? QCoreApplication::translate("SettingsDialog", + "No key yet. Create one at console.anthropic.com.") + : QCoreApplication::translate("SettingsDialog", + "No key set; local servers usually need none."); } } // namespace QWidget* SettingsDialog::buildAgentPage() { auto* w = new QWidget; - auto* form = makePage(w, "AI Assistant", "The model behind Tools > AI Assistant. You bring the API key, " - "or run a model on your own computer."); + auto* form = makePage(w, tr("AI Assistant"), + tr("The model behind Tools > AI Assistant. You bring the API key, " + "or run a model on your own computer.")); QSettings s; agentProviderCombo_ = sized(new QComboBox); - agentProviderCombo_->addItem("Anthropic (Claude)"); - agentProviderCombo_->addItem("OpenAI-compatible (OpenAI, Ollama, LM Studio)"); + agentProviderCombo_->addItem(tr("Anthropic (Claude)")); + agentProviderCombo_->addItem(tr("OpenAI-compatible (OpenAI, Ollama, LM Studio)")); for (int i = 0; i < 2; ++i) { agentModels_[i] = s.value(QString("agent/%1/model").arg(kAgentAccounts[i])).toString(); agentUrls_[i] = s.value(QString("agent/%1/baseUrl").arg(kAgentAccounts[i])).toString(); } - form->addRow("Provider:", agentProviderCombo_); + form->addRow(tr("Provider:"), agentProviderCombo_); agentModelEdit_ = sized(new QLineEdit); - form->addRow("Model:", agentModelEdit_); + form->addRow(tr("Model:"), agentModelEdit_); agentUrlEdit_ = sized(new QLineEdit); - form->addRow("API address:", agentUrlEdit_); + form->addRow(tr("API address:"), agentUrlEdit_); agentKeyEdit_ = sized(new QLineEdit); agentKeyEdit_->setEchoMode(QLineEdit::Password); - agentKeyEdit_->setPlaceholderText("Paste a new key to save it"); - auto* forget = new QPushButton("Forget key"); + agentKeyEdit_->setPlaceholderText(tr("Paste a new key to save it")); + auto* forget = new QPushButton(tr("Forget key")); auto* keyRow = new QHBoxLayout; keyRow->addWidget(agentKeyEdit_); keyRow->addWidget(forget); keyRow->addStretch(1); - form->addRow("API key:", keyRow); + form->addRow(tr("API key:"), keyRow); agentKeyStatus_ = hint(QString()); form->addRow("", agentKeyStatus_); - form->addRow("", hint("Keys are kept in the system keyring, never in Cheat Engine's settings file; " - "ANTHROPIC_API_KEY or OPENAI_API_KEY in the environment take precedence. A saved " - "key is only sent to the API address it was entered with. Your " - "messages and what the tools find in the game are sent to this provider, and " - "every change to the game waits for your Allow.")); + form->addRow("", hint(tr("Keys are kept in the system keyring, never in Cheat Engine's settings file; " + "ANTHROPIC_API_KEY or OPENAI_API_KEY in the environment take precedence. A saved " + "key is only sent to the API address it was entered with. Your " + "messages and what the tools find in the game are sent to this provider, and " + "every change to the game waits for your Allow."))); connect(agentProviderCombo_, &QComboBox::currentIndexChanged, this, &SettingsDialog::showAgentProvider); connect(forget, &QPushButton::clicked, this, [this]() { const int provider = agentProviderCombo_->currentIndex(); @@ -396,7 +447,9 @@ QWidget* SettingsDialog::buildAgentPage() { SecretStore(kAgentAccounts[provider]).remove(&error); QSettings().remove(QString("agent/%1/keyStored").arg(kAgentAccounts[provider])); QSettings().remove(QString("agent/%1/keyOrigin").arg(kAgentAccounts[provider])); - agentKeyStatus_->setText(error.isEmpty() ? agentKeyState(provider, agentUrlEdit_->text()) : "Could not remove the key: " + error); + agentKeyStatus_->setText(error.isEmpty() + ? agentKeyState(provider, agentUrlEdit_->text()) + : tr("Could not remove the key: %1").arg(error)); }); agentProviderCombo_->setCurrentIndex(s.value("agent/provider", 0).toInt() == 1 ? 1 : 0); showAgentProvider(agentProviderCombo_->currentIndex()); @@ -410,9 +463,13 @@ void SettingsDialog::showAgentProvider(int index) { } agentShown_ = index; agentModelEdit_->setText(agentModels_[index]); - agentModelEdit_->setPlaceholderText(index == 0 ? QString(ChatProvider::defaultAnthropicModel()) : "for example gpt-5 or qwen3"); + agentModelEdit_->setPlaceholderText(index == 0 + ? QString(ChatProvider::defaultAnthropicModel()) + : tr("for example gpt-5 or qwen3")); agentUrlEdit_->setText(agentUrls_[index]); - agentUrlEdit_->setPlaceholderText(index == 0 ? "https://api.anthropic.com" : "https://api.openai.com/v1 or http://localhost:11434/v1"); + agentUrlEdit_->setPlaceholderText(index == 0 + ? QStringLiteral("https://api.anthropic.com") + : tr("https://api.openai.com/v1 or http://localhost:11434/v1")); agentKeyEdit_->clear(); agentKeyStatus_->setText(agentKeyState(index, agentUrls_[index])); } @@ -440,6 +497,12 @@ void SettingsDialog::onApply() { // sheet also carries the data tables' monospace font. applyTheme(dark); + // A translator can only be installed before the windows are built, so the new + // language starts with the next run. + const QString language = languageCombo_->currentData().toString(); + const bool languageChanged = language != s.value(UI_LANGUAGE_KEY).toString(); + s.setValue(UI_LANGUAGE_KEY, language); + s.setValue(MV_BYTES_PER_ROW_KEY, bytesPerRowSpin_->value()); s.setValue(MV_AUTO_REFRESH_KEY, autoRefreshCheck_->isChecked()); s.setValue(MV_REFRESH_MS_KEY, refreshIntervalSpin_->value()); @@ -471,10 +534,14 @@ void SettingsDialog::onApply() { if (store.persisted()) s.setValue(QString("agent/%1/keyStored").arg(kAgentAccounts[provider]), "keyring"); else s.remove(QString("agent/%1/keyStored").arg(kAgentAccounts[provider])); agentKeyEdit_->clear(); - agentKeyStatus_->setText(store.persisted() ? agentKeyState(provider, agentUrls_[provider]) - : "Kept until Cheat Engine closes: " + error + "."); + agentKeyStatus_->setText(store.persisted() + ? agentKeyState(provider, agentUrls_[provider]) + : tr("Kept until Cheat Engine closes: %1").arg(error)); } emit settingsApplied(); + if (languageChanged) + QMessageBox::information(this, tr("Language"), + tr("Restart Cheat Engine to apply the new interface language.")); } } // namespace ce::gui diff --git a/gui/settingsdialog.hpp b/gui/settingsdialog.hpp index c185c426..aac6589a 100644 --- a/gui/settingsdialog.hpp +++ b/gui/settingsdialog.hpp @@ -27,6 +27,7 @@ private slots: private: QWidget* buildScanPage(); QWidget* buildDisplayPage(); + QWidget* buildLanguagePage(); QWidget* buildMemoryViewPage(); QWidget* buildDebuggerPage(); QWidget* buildHotkeysPage(); @@ -51,6 +52,9 @@ private slots: QCheckBox* hexUpperCheck_; QComboBox* addressWidthCombo_; + // Language + QComboBox* languageCombo_; + // Memory View QSpinBox* bytesPerRowSpin_; QCheckBox* autoRefreshCheck_; diff --git a/gui/stackview.cpp b/gui/stackview.cpp index 5e1a8abd..87e4ed07 100644 --- a/gui/stackview.cpp +++ b/gui/stackview.cpp @@ -18,19 +18,19 @@ namespace ce::gui { StackViewWindow::StackViewWindow(ProcessHandle* proc, QWidget* parent) : QMainWindow(parent), proc_(proc) { - setWindowTitle("Stack Trace"); + setWindowTitle(tr("Stack Trace")); resize(850, 560); auto* central = new QWidget; auto* layout = new QVBoxLayout(central); auto* threadRow = new QHBoxLayout; - threadRow->addWidget(new QLabel("Thread:")); + threadRow->addWidget(new QLabel(tr("Thread:"))); threadCombo_ = new QComboBox; threadCombo_->setObjectName("stackViewThreads"); threadCombo_->setMinimumWidth(220); threadRow->addWidget(threadCombo_, 1); - auto* refreshButton = new QPushButton("Refresh"); + auto* refreshButton = new QPushButton(tr("Refresh")); connect(refreshButton, &QPushButton::clicked, this, &StackViewWindow::refreshStack); threadRow->addSpacing(8); threadRow->addWidget(refreshButton); @@ -46,25 +46,25 @@ StackViewWindow::StackViewWindow(ProcessHandle* proc, QWidget* parent) stackTable_ = new QTableWidget; stackTable_->setObjectName("stackViewRaw"); stackTable_->setColumnCount(3); - stackTable_->setHorizontalHeaderLabels({"Address", "Value", "Offset"}); + stackTable_->setHorizontalHeaderLabels({tr("Address"), tr("Value"), tr("Offset")}); // Address and Value are 16-digit hex; fit them so they aren't clipped, let // the Offset column take the slack. stackTable_->horizontalHeader()->setStretchLastSection(true); stackTable_->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); stackTable_->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); styleDataTable(stackTable_); - tabs_->addTab(stackTable_, "Raw Stack"); + tabs_->addTab(stackTable_, tr("Raw Stack")); traceTable_ = new QTableWidget; traceTable_->setObjectName("stackViewFrames"); traceTable_->setColumnCount(5); - traceTable_->setHorizontalHeaderLabels({"Frame", "Instruction", "Return", "Frame Pointer", "Symbol"}); + traceTable_->setHorizontalHeaderLabels({tr("Frame"), tr("Instruction"), tr("Return"), tr("Frame Pointer"), tr("Symbol")}); // Fit the frame/address columns to content; Symbol (last) takes the slack. traceTable_->horizontalHeader()->setStretchLastSection(true); for (int c = 0; c < 4; ++c) traceTable_->horizontalHeader()->setSectionResizeMode(c, QHeaderView::ResizeToContents); styleDataTable(traceTable_); - tabs_->addTab(traceTable_, "Stack Trace"); + tabs_->addTab(traceTable_, tr("Stack Trace")); layout->addWidget(tabs_); @@ -92,7 +92,7 @@ void StackViewWindow::refreshStack() { stackTable_->setRowCount(0); traceTable_->setRowCount(0); if (!proc_ || threadCombo_->currentIndex() < 0) { - statusLabel_->setText("No thread selected"); + statusLabel_->setText(tr("No thread selected")); return; } @@ -129,7 +129,7 @@ void StackViewWindow::refreshStack() { traceTable_->setItem(row, 4, new QTableWidgetItem(QString::fromStdString(frame.symbol))); } - statusLabel_->setText(QString("Thread %1 %2 0x%3 %4 0x%5 %6 frames") + statusLabel_->setText(tr("Thread %1 %2 0x%3 %4 0x%5 %6 frames") .arg(tid) .arg(context.architecture == CpuArchitecture::Arm64 ? "SP" : digits == 8 ? "ESP" : "RSP") .arg(context.stackPointer(), 0, 16) diff --git a/gui/structuredissector.cpp b/gui/structuredissector.cpp index a1097287..7bd742ce 100644 --- a/gui/structuredissector.cpp +++ b/gui/structuredissector.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "analysis/structure_tools.hpp" #include "core/expression.hpp" @@ -35,7 +36,7 @@ StructureDissector::StructureDissector(ProcessHandle* proc, uintptr_t baseAddr, if (proc_) moduleCache_ = proc_->modules(); // for "-> module+offset" on pointer fields updatePointerWidth(); - setWindowTitle("Structure Dissector"); + setWindowTitle(tr("Structure Dissector")); resize(750, 600); auto* central = new QWidget; @@ -43,17 +44,17 @@ StructureDissector::StructureDissector(ProcessHandle* proc, uintptr_t baseAddr, // Address bar auto* addrRow = new QHBoxLayout; - addrRow->addWidget(new QLabel("Base Address:")); + addrRow->addWidget(new QLabel(tr("Base Address:"))); addressEdit_ = new QLineEdit(QString("0x%1").arg(baseAddr, 0, 16)); addressEdit_->setFont(monospaceFont()); addressEdit_->setMinimumWidth(150); connect(addressEdit_, &QLineEdit::returnPressed, this, &StructureDissector::onGotoAddress); addrRow->addWidget(addressEdit_); - auto* goBtn = new QPushButton("Go"); + auto* goBtn = new QPushButton(tr("Go")); connect(goBtn, &QPushButton::clicked, this, &StructureDissector::onGotoAddress); addrRow->addWidget(goBtn); - addrRow->addWidget(new QLabel("Bytes:")); + addrRow->addWidget(new QLabel(tr("Bytes:"))); sizeSpin_ = new QSpinBox; sizeSpin_->setRange(8, 8192); sizeSpin_->setSingleStep(64); @@ -68,15 +69,15 @@ StructureDissector::StructureDissector(ProcessHandle* proc, uintptr_t baseAddr, // base in ANY instance are highlighted. With one address this is CE's // "compare two structures"; with several it is the N-instance dissector, // surfacing the fields that discriminate between instances (e.g. team id). - addrRow->addWidget(new QLabel("Compare:")); + addrRow->addWidget(new QLabel(tr("Compare:"))); compareEdit_ = new QLineEdit; compareEdit_->setFont(monospaceFont()); compareEdit_->setMinimumWidth(180); - compareEdit_->setPlaceholderText("0x…, 0x… (optional, comma/space separated)"); + compareEdit_->setPlaceholderText(tr("0x…, 0x… (optional, comma/space separated)")); compareEdit_->setToolTip( - "Enter one or more struct addresses to compare against the base. Rows whose " - "bytes differ in any instance are highlighted, so the fields that vary " - "between instances stand out. Press Enter to apply."); + tr("Enter one or more struct addresses to compare against the base. Rows whose " + "bytes differ in any instance are highlighted, so the fields that vary " + "between instances stand out. Press Enter to apply.")); connect(compareEdit_, &QLineEdit::returnPressed, this, [this]() { applyCompareAddresses(); }); addrRow->addWidget(compareEdit_, 1); layout->addLayout(addrRow); @@ -84,35 +85,36 @@ StructureDissector::StructureDissector(ProcessHandle* proc, uintptr_t baseAddr, // Second row: definition / export actions, so the address row above stays // uncrowded (the single-row layout squeezed the Base/Compare edits to nothing). auto* actionRow = new QHBoxLayout; - auto* saveBtn = new QPushButton("Save Definition…"); - saveBtn->setToolTip("Save this structure definition (named fields + size)"); + auto* saveBtn = new QPushButton(tr("Save Definition…")); + saveBtn->setToolTip(tr("Save this structure definition (named fields + size)")); connect(saveBtn, &QPushButton::clicked, this, &StructureDissector::onSaveDefinition); actionRow->addWidget(saveBtn); - auto* loadBtn = new QPushButton("Load Definition…"); - loadBtn->setToolTip("Load a previously saved structure definition"); + auto* loadBtn = new QPushButton(tr("Load Definition…")); + loadBtn->setToolTip(tr("Load a previously saved structure definition")); connect(loadBtn, &QPushButton::clicked, this, &StructureDissector::onLoadDefinition); actionRow->addWidget(loadBtn); - auto* cppBtn = new QPushButton("Copy as C++"); - cppBtn->setToolTip("Copy the current layout to the clipboard as a C++ struct"); + auto* cppBtn = new QPushButton(tr("Copy as C++")); + cppBtn->setToolTip(tr("Copy the current layout to the clipboard as a C++ struct")); connect(cppBtn, &QPushButton::clicked, this, &StructureDissector::onCopyAsCpp); actionRow->addWidget(cppBtn); - auto* il2cppBtn = new QPushButton("Type as IL2CPP…"); - il2cppBtn->setToolTip("Label these bytes with a Unity IL2CPP class's fields " - "(names + offsets from global-metadata.dat + GameAssembly)"); + auto* il2cppBtn = new QPushButton(tr("Type as IL2CPP…")); + il2cppBtn->setToolTip(tr("Label these bytes with a Unity IL2CPP class's fields " + "(names + offsets from global-metadata.dat + GameAssembly)")); connect(il2cppBtn, &QPushButton::clicked, this, &StructureDissector::onTypeAsIl2Cpp); actionRow->addWidget(il2cppBtn); - auto* cstructBtn = new QPushButton("Type as C struct…"); - cstructBtn->setToolTip("Label these bytes with a native struct from the target's DWARF debug info"); + auto* cstructBtn = new QPushButton(tr("Type as C struct…")); + cstructBtn->setToolTip(tr("Label these bytes with a native struct from the target's DWARF debug info")); connect(cstructBtn, &QPushButton::clicked, this, &StructureDissector::onTypeAsCStruct); actionRow->addWidget(cstructBtn); - auto* addAllBtn = new QPushButton("Add All to List"); - addAllBtn->setToolTip("Add every named field (base+offset, name, guessed type) to the cheat table"); + auto* addAllBtn = new QPushButton(tr("Add All to List")); + addAllBtn->setToolTip(tr("Add every named field (base+offset, name, guessed type) to the cheat table")); connect(addAllBtn, &QPushButton::clicked, this, [this]() { int n = addAllFieldsToList(); - statusBar()->showMessage(n > 0 ? QString("Added %1 field%2 to the cheat table") - .arg(n).arg(n == 1 ? "" : "s") - : QStringLiteral("No named fields to add (double-click a row, or Type as…)"), - 4000); + statusBar()->showMessage( + n > 0 ? (n == 1 ? tr("Added %1 field to the cheat table").arg(n) + : tr("Added %1 fields to the cheat table").arg(n)) + : tr("No named fields to add (double-click a row, or Type as…)"), + 4000); }); actionRow->addWidget(addAllBtn); actionRow->addStretch(1); @@ -121,7 +123,7 @@ StructureDissector::StructureDissector(ProcessHandle* proc, uintptr_t baseAddr, // Table table_ = new QTableWidget; table_->setColumnCount(7); - table_->setHorizontalHeaderLabels({"Offset", "Name", "Hex", "Int8", "Int32", "Float", "Pointer?"}); + table_->setHorizontalHeaderLabels({tr("Offset"), tr("Name"), tr("Hex"), tr("Int8"), tr("Int32"), tr("Float"), tr("Pointer?")}); // Fit the numeric/offset columns to content, give field names a fixed, // resizable column, and let the pointer column (often a long symbol) take // the slack, so an unnamed structure doesn't open as a wide empty column. @@ -135,7 +137,7 @@ StructureDissector::StructureDissector(ProcessHandle* proc, uintptr_t baseAddr, table_->setProperty("monospace", true); // font from the theme sheet table_->setShowGrid(false); table_->setWordWrap(false); - setEmptyHint(table_, "Enter the address of a structure above and press Go to see its fields."); + setEmptyHint(table_, tr("Enter the address of a structure above and press Go to see its fields.")); table_->setSelectionBehavior(QAbstractItemView::SelectRows); table_->verticalHeader()->setVisible(false); table_->setAlternatingRowColors(true); @@ -149,8 +151,8 @@ StructureDissector::StructureDissector(ProcessHandle* proc, uintptr_t baseAddr, int off = row * 8; bool ok = false; QString cur = fieldNames_.count(off) ? fieldNames_[off] : QString(); - QString name = QInputDialog::getText(this, "Name field", - QString("Name for offset +0x%1:").arg(off, 0, 16), QLineEdit::Normal, cur, &ok); + QString name = QInputDialog::getText(this, tr("Name field"), + tr("Name for offset +0x%1:").arg(off, 0, 16), QLineEdit::Normal, cur, &ok); if (!ok) return; if (name.trimmed().isEmpty()) fieldNames_.erase(off); else fieldNames_[off] = name.trimmed(); @@ -224,7 +226,7 @@ void StructureDissector::onRefresh() { void StructureDissector::detachFromTarget() { if (refreshTimer_) refreshTimer_->stop(); proc_ = nullptr; // populateTable()/onRefresh() both bail on a null proc_ - setWindowTitle("Structure Dissector (target exited)"); + setWindowTitle(tr("Structure Dissector (target exited)")); } QString StructureDissector::formatValue(const uint8_t* data, int offset, const QString& type) const { @@ -306,46 +308,50 @@ void StructureDissector::onContextMenu(const QPoint& pos) { // Prefer the field's declared type (set via naming / "Type as C struct") over the // byte-pattern guess, matching what "Add all fields" uses. ce::ValueType guessed = resolveFieldType(off); - auto typeName = [](ce::ValueType t) { + auto typeName = [](ce::ValueType t) -> QString { switch (t) { - case ce::ValueType::Pointer: return "Pointer"; - case ce::ValueType::Float: return "Float"; - case ce::ValueType::Int64: return "8 Bytes"; - default: return "4 Bytes"; + case ce::ValueType::Pointer: return QCoreApplication::translate("StructureDissector", "Pointer"); + case ce::ValueType::Float: return QCoreApplication::translate("StructureDissector", "Float"); + case ce::ValueType::Int64: return QCoreApplication::translate("StructureDissector", "8 Bytes"); + default: return QCoreApplication::translate("StructureDissector", "4 Bytes"); } }; QMenu menu(this); - menu.addAction(QString("Offset +0x%1 (0x%2)").arg(off, 2, 16, QChar('0')).arg(addr, 0, 16)) + menu.addAction(tr("Offset +0x%1 (0x%2)").arg(off, 2, 16, QChar('0')).arg(addr, 0, 16)) ->setEnabled(false); menu.addSeparator(); - auto* addGuess = menu.addAction(QString("Add to address list as %1 (auto)").arg(typeName(guessed))); - auto* add4 = menu.addAction("Add as 4 Bytes"); - auto* add8 = menu.addAction("Add as 8 Bytes"); - auto* addF = menu.addAction("Add as Float"); - auto* addP = menu.addAction("Add as Pointer"); + auto* addGuess = menu.addAction(tr("Add to address list as %1 (auto)").arg(typeName(guessed))); + auto* add4 = menu.addAction(tr("Add as 4 Bytes")); + auto* add8 = menu.addAction(tr("Add as 8 Bytes")); + auto* addF = menu.addAction(tr("Add as Float")); + auto* addP = menu.addAction(tr("Add as Pointer")); // Build the structure in place (CE's "Change element"): name a field and pin its // type, so the value column reinterprets it and save/load persists the definition. menu.addSeparator(); - auto* nameAct = menu.addAction("Name field..."); - auto* typeMenu = menu.addMenu("Set field type"); + auto* nameAct = menu.addAction(tr("Name field...")); + auto* typeMenu = menu.addMenu(tr("Set field type")); const std::pair typeOpts[] = { - {"Byte", ce::ValueType::Byte}, {"2 Bytes", ce::ValueType::Int16}, - {"4 Bytes", ce::ValueType::Int32}, {"8 Bytes", ce::ValueType::Int64}, - {"Float", ce::ValueType::Float}, {"Double", ce::ValueType::Double}, - {"Pointer", ce::ValueType::Pointer}, {"String", ce::ValueType::String}, + {QT_TRANSLATE_NOOP("StructureDissector", "Byte"), ce::ValueType::Byte}, + {QT_TRANSLATE_NOOP("StructureDissector", "2 Bytes"), ce::ValueType::Int16}, + {QT_TRANSLATE_NOOP("StructureDissector", "4 Bytes"), ce::ValueType::Int32}, + {QT_TRANSLATE_NOOP("StructureDissector", "8 Bytes"), ce::ValueType::Int64}, + {QT_TRANSLATE_NOOP("StructureDissector", "Float"), ce::ValueType::Float}, + {QT_TRANSLATE_NOOP("StructureDissector", "Double"), ce::ValueType::Double}, + {QT_TRANSLATE_NOOP("StructureDissector", "Pointer"), ce::ValueType::Pointer}, + {QT_TRANSLATE_NOOP("StructureDissector", "String"), ce::ValueType::String}, }; QList typeActs; for (const auto& [label, vt] : typeOpts) { - auto* a = typeMenu->addAction(label); + auto* a = typeMenu->addAction(QCoreApplication::translate("StructureDissector", label)); a->setData(static_cast(vt)); a->setCheckable(true); if (fieldTypes_.count(off) && fieldTypes_[off] == vt) a->setChecked(true); typeActs.append(a); } typeMenu->addSeparator(); - auto* autoTypeAct = typeMenu->addAction("Auto (guess)"); + auto* autoTypeAct = typeMenu->addAction(tr("Auto (guess)")); QAction* picked = menu.exec(table_->viewport()->mapToGlobal(pos)); if (!picked) return; @@ -354,8 +360,8 @@ void StructureDissector::onContextMenu(const QPoint& pos) { if (picked == nameAct) { bool ok = false; QString cur = fieldNames_.count(off) ? fieldNames_[off] : QString(); - QString name = QInputDialog::getText(this, "Name field", - QString("Name for offset +0x%1:").arg(off, 0, 16), QLineEdit::Normal, cur, &ok); + QString name = QInputDialog::getText(this, tr("Name field"), + tr("Name for offset +0x%1:").arg(off, 0, 16), QLineEdit::Normal, cur, &ok); if (!ok) return; if (name.trimmed().isEmpty()) fieldNames_.erase(off); else fieldNames_[off] = name.trimmed(); @@ -393,7 +399,7 @@ void StructureDissector::addFieldToList(int offset, ce::ValueType type) { // Carry the field's name (if it has one) to the cheat-table description. auto nit = fieldNames_.find(offset); QString desc = (nit != fieldNames_.end() && !nit->second.isEmpty()) - ? nit->second : QString("Dissect +0x%1").arg(offset, 0, 16); + ? nit->second : tr("Dissect +0x%1").arg(offset, 0, 16); addToListCb_(baseAddr_ + offset, type, desc); } @@ -436,27 +442,27 @@ bool StructureDissector::loadDefinition(const QString& path) { } void StructureDissector::onSaveDefinition() { - auto path = QFileDialog::getSaveFileName(this, "Save structure definition", - "structure.json", "Structure def (*.json);;All (*)"); + auto path = QFileDialog::getSaveFileName(this, tr("Save structure definition"), + "structure.json", tr("Structure def (*.json);;All (*)")); if (path.isEmpty()) return; if (!saveDefinition(path)) - QMessageBox::warning(this, "Save failed", "Could not write the definition file."); + QMessageBox::warning(this, tr("Save failed"), tr("Could not write the definition file.")); } void StructureDissector::onLoadDefinition() { - auto path = QFileDialog::getOpenFileName(this, "Load structure definition", - "", "Structure def (*.json);;All (*)"); + auto path = QFileDialog::getOpenFileName(this, tr("Load structure definition"), + "", tr("Structure def (*.json);;All (*)")); if (path.isEmpty()) return; if (!loadDefinition(path)) - QMessageBox::warning(this, "Load failed", - "Could not read the file or it is not a valid structure definition."); + QMessageBox::warning(this, tr("Load failed"), + tr("Could not read the file or it is not a valid structure definition.")); } void StructureDissector::onTypeAsIl2Cpp() { - if (!proc_) { QMessageBox::information(this, "IL2CPP", "No target process."); return; } + if (!proc_) { QMessageBox::information(this, "IL2CPP", tr("No target process.")); return; } bool ok = false; - QString cls = QInputDialog::getText(this, "Type as IL2CPP class", - "Class name (e.g. UnityEngine.Vector3 or Player):", QLineEdit::Normal, "", &ok); + QString cls = QInputDialog::getText(this, tr("Type as IL2CPP class"), + tr("Class name (e.g. UnityEngine.Vector3 or Player):"), QLineEdit::Normal, "", &ok); if (!ok || cls.trimmed().isEmpty()) return; QApplication::setOverrideCursor(Qt::WaitCursor); @@ -472,7 +478,7 @@ void StructureDissector::onTypeAsIl2Cpp() { if (!found) for (const auto& c : layout.classes) if (c.name == want) { found = &c; break; } if (!found) { - QMessageBox::warning(this, "IL2CPP", "Class not found: " + cls); + QMessageBox::warning(this, "IL2CPP", tr("Class not found: %1").arg(cls)); return; } @@ -496,14 +502,14 @@ void StructureDissector::onTypeAsIl2Cpp() { } void StructureDissector::onTypeAsCStruct() { - if (!proc_) { QMessageBox::information(this, "DWARF", "No target process."); return; } + if (!proc_) { QMessageBox::information(this, "DWARF", tr("No target process.")); return; } if (!ce::DwarfInfo::available()) { - QMessageBox::information(this, "DWARF", "This build has no libdw (DWARF) support."); + QMessageBox::information(this, "DWARF", tr("This build has no libdw (DWARF) support.")); return; } bool ok = false; - QString name = QInputDialog::getText(this, "Type as C struct", - "Struct name (from the target's debug info):", QLineEdit::Normal, "", &ok); + QString name = QInputDialog::getText(this, tr("Type as C struct"), + tr("Struct name (from the target's debug info):"), QLineEdit::Normal, "", &ok); if (!ok || name.trimmed().isEmpty()) return; QApplication::setOverrideCursor(Qt::WaitCursor); @@ -512,7 +518,7 @@ void StructureDissector::onTypeAsCStruct() { auto st = reg.structByName(name.trimmed().toStdString()); QApplication::restoreOverrideCursor(); if (!st) { - QMessageBox::warning(this, "DWARF", "Struct not found (needs debug info): " + name); + QMessageBox::warning(this, "DWARF", tr("Struct not found (needs debug info): %1").arg(name)); return; } ce::StructureDefinition def = ce::dwarfStructToStructure(*st); @@ -591,7 +597,7 @@ void StructureDissector::populateTable() { // Single-struct dissection: one row per 8 bytes, all display types shown. // Compare mode with four instances also has seven columns: check the // headers, not just the count, when switching back. - const QStringList headers{"Offset", "Name", "Hex", "Int8", "Int32", "Float", "Pointer?"}; + const QStringList headers{tr("Offset"), tr("Name"), tr("Hex"), tr("Int8"), tr("Int32"), tr("Float"), tr("Pointer?")}; if (table_->columnCount() != 7 || headerLabels() != headers) { table_->setColumnCount(7); table_->setHorizontalHeaderLabels(headers); @@ -633,7 +639,7 @@ void StructureDissector::populateTable() { // offset is coloured (same = default, different = red), so the fields that // discriminate between the instances stand out. const int cols = 3 + (int)compareAddrs_.size(); - QStringList headers; headers << "Offset" << "Name" << "Base"; + QStringList headers; headers << tr("Offset") << tr("Name") << tr("Base"); for (auto a : compareAddrs_) headers << QString("0x%1").arg(a, 0, 16); if (table_->columnCount() != cols || headerLabels() != headers) { // new addresses, same count table_->setColumnCount(cols); @@ -725,8 +731,8 @@ bool StructureDissector::pointerColoredForTest(int row) const { void StructureDissector::onCopyAsCpp() { if (fieldNames_.empty()) { - QMessageBox::information(this, "Copy as C++", - "Name some fields first (double-click a row to name the offset)."); + QMessageBox::information(this, tr("Copy as C++"), + tr("Name some fields first (double-click a row to name the offset).")); return; } ce::StructureDefinition def; @@ -752,8 +758,8 @@ void StructureDissector::onCopyAsCpp() { } QString cpp = QString::fromStdString(ce::generateCppStruct(def)); QApplication::clipboard()->setText(cpp); - QMessageBox::information(this, "Copy as C++", - "C++ struct copied to clipboard:\n\n" + cpp); + QMessageBox::information(this, tr("Copy as C++"), + tr("C++ struct copied to clipboard:\n\n%1").arg(cpp)); } } // namespace ce::gui diff --git a/gui/theme.cpp b/gui/theme.cpp index ddd6e799..261ced85 100644 --- a/gui/theme.cpp +++ b/gui/theme.cpp @@ -1,6 +1,7 @@ #include "gui/theme.hpp" #include +#include #include #include #include @@ -510,7 +511,9 @@ QString threadName(int pid, int tid) { QString threadLabel(int pid, int tid) { if (pid <= 0) return QString::number(tid); const QString name = threadName(pid, tid); - if (tid == pid) return name.isEmpty() ? QString("%1 (main)").arg(tid) : QString("%1 %2 (main)").arg(tid).arg(name); + if (tid == pid) + return name.isEmpty() ? QCoreApplication::translate("Theme", "%1 (main)").arg(tid) + : QCoreApplication::translate("Theme", "%1 %2 (main)").arg(tid).arg(name); return name.isEmpty() ? QString::number(tid) : QString("%1 %2").arg(tid).arg(name); } diff --git a/gui/threadlist.cpp b/gui/threadlist.cpp index 4de47f61..d0769ca0 100644 --- a/gui/threadlist.cpp +++ b/gui/threadlist.cpp @@ -11,7 +11,7 @@ namespace ce::gui { ThreadListWindow::ThreadListWindow(ProcessHandle* proc, QWidget* parent) : QMainWindow(parent), proc_(proc) { - setWindowTitle("Thread List"); + setWindowTitle(tr("Thread List")); resize(520, 360); auto* central = new QWidget; @@ -19,7 +19,7 @@ ThreadListWindow::ThreadListWindow(ProcessHandle* proc, QWidget* parent) table_ = new QTableWidget; table_->setColumnCount(3); - table_->setHorizontalHeaderLabels({"TID", "Name", "State"}); + table_->setHorizontalHeaderLabels({tr("TID"), tr("Name"), tr("State")}); // TID and State are short and fixed; let the thread Name column take the slack. auto* hh = table_->horizontalHeader(); hh->setStretchLastSection(false); @@ -69,7 +69,7 @@ void ThreadListWindow::populate() { } table_->setItem(i, 2, new QTableWidgetItem(QString::fromStdString(state))); } - setWindowTitle(QString("Thread List: %1 threads").arg(threads.size())); + setWindowTitle(tr("Thread List: %1 threads").arg(threads.size())); } } // namespace ce::gui diff --git a/gui/tracerwindow.cpp b/gui/tracerwindow.cpp index 288821dd..94ebf0bd 100644 --- a/gui/tracerwindow.cpp +++ b/gui/tracerwindow.cpp @@ -3,6 +3,7 @@ #include "gui/tracerwindow.hpp" #include "gui/theme.hpp" #include +#include #include "core/target_capabilities.hpp" #include @@ -42,12 +43,20 @@ QVariant TraceTableModel::data(const QModelIndex& index, int role) const { QVariant TraceTableModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation != Qt::Horizontal || role != Qt::DisplayRole || section < 0 || section >= 8) return {}; const auto architecture = entries_->empty() ? ce::CpuArchitecture::Unknown : entries_->front().context.architecture; - static const char* x64[] = {"#", "Address", "Instruction", "RAX", "RBX", "RCX", "RDX", "RIP"}; - static const char* x86[] = {"#", "Address", "Instruction", "EAX", "EBX", "ECX", "EDX", "EIP"}; - static const char* a64[] = {"#", "Address", "Instruction", "X0", "X1", "X2", "X3", "PC"}; + // "Address" and "Instruction" are column headers; the per-architecture + // register mnemonics are technical names and stay untranslated. + static const char* x64[] = {"#", QT_TRANSLATE_NOOP("TraceTableModel", "Address"), + QT_TRANSLATE_NOOP("TraceTableModel", "Instruction"), + "RAX", "RBX", "RCX", "RDX", "RIP"}; + static const char* x86[] = {"#", QT_TRANSLATE_NOOP("TraceTableModel", "Address"), + QT_TRANSLATE_NOOP("TraceTableModel", "Instruction"), + "EAX", "EBX", "ECX", "EDX", "EIP"}; + static const char* a64[] = {"#", QT_TRANSLATE_NOOP("TraceTableModel", "Address"), + QT_TRANSLATE_NOOP("TraceTableModel", "Instruction"), + "X0", "X1", "X2", "X3", "PC"}; const char** names = architecture == ce::CpuArchitecture::Arm64 ? a64 : architecture == ce::CpuArchitecture::X86_32 ? x86 : x64; - return QString::fromLatin1(names[section]); + return QCoreApplication::translate("TraceTableModel", names[section]); } static QString fullRegisterDump(const ce::CpuContext& c) { @@ -82,7 +91,7 @@ static QString fullRegisterDump(const ce::CpuContext& c) { TracerWindow::TracerWindow(ProcessHandle* proc, DebuggerFactory factory, QWidget* parent) : QMainWindow(parent), proc_(proc), debuggerFactory_(std::move(factory)) { - setWindowTitle("Break and Trace"); + setWindowTitle(tr("Break and Trace")); resize(1100, 600); buildUi(); } @@ -102,37 +111,37 @@ void TracerWindow::buildUi() { auto* form = new QGridLayout; int row = 0; - form->addWidget(new QLabel("Start address (hex):"), row, 0); + form->addWidget(new QLabel(tr("Start address (hex):")), row, 0); startAddressEdit_ = new QLineEdit; - startAddressEdit_->setPlaceholderText("0x0000000000000000, leave blank to start at the instruction pointer"); + startAddressEdit_->setPlaceholderText(tr("0x0000000000000000, leave blank to start at the instruction pointer")); form->addWidget(startAddressEdit_, row, 1, 1, 3); ++row; - form->addWidget(new QLabel("Max steps:"), row, 0); + form->addWidget(new QLabel(tr("Max steps:")), row, 0); maxStepsSpin_ = new QSpinBox; maxStepsSpin_->setRange(1, 1'000'000); maxStepsSpin_->setValue(1000); form->addWidget(maxStepsSpin_, row, 1); - stepOverCheck_ = new QCheckBox("Step over calls"); + stepOverCheck_ = new QCheckBox(tr("Step over calls")); form->addWidget(stepOverCheck_, row, 2); - stayInModuleCheck_ = new QCheckBox("Stay in module"); + stayInModuleCheck_ = new QCheckBox(tr("Stay in module")); form->addWidget(stayInModuleCheck_, row, 3); ++row; - form->addWidget(new QLabel("Module start (hex):"), row, 0); + form->addWidget(new QLabel(tr("Module start (hex):")), row, 0); moduleStartEdit_ = new QLineEdit; - moduleStartEdit_->setPlaceholderText("0x... (only if Stay in module)"); + moduleStartEdit_->setPlaceholderText(tr("0x... (only if Stay in module)")); form->addWidget(moduleStartEdit_, row, 1); - form->addWidget(new QLabel("end:"), row, 2); + form->addWidget(new QLabel(tr("end:")), row, 2); moduleEndEdit_ = new QLineEdit; form->addWidget(moduleEndEdit_, row, 3); ++row; - form->addWidget(new QLabel("Stop address (hex, optional):"), row, 0); + form->addWidget(new QLabel(tr("Stop address (hex, optional):")), row, 0); stopAddressEdit_ = new QLineEdit; - stopAddressEdit_->setPlaceholderText("Trace stops when this address is reached"); + stopAddressEdit_->setPlaceholderText(tr("Trace stops when this address is reached")); form->addWidget(stopAddressEdit_, row, 1, 1, 3); ++row; @@ -140,14 +149,14 @@ void TracerWindow::buildUi() { // Buttons + progress auto* btnRow = new QHBoxLayout; - startBtn_ = new QPushButton("Start Trace"); - cancelBtn_ = new QPushButton("Cancel"); + startBtn_ = new QPushButton(tr("Start Trace")); + cancelBtn_ = new QPushButton(tr("Cancel")); cancelBtn_->setEnabled(false); - saveBtn_ = new QPushButton("Save…"); + saveBtn_ = new QPushButton(tr("Save…")); saveBtn_->setEnabled(false); progressBar_ = new QProgressBar; progressBar_->setRange(0, 100); - statusLabel_ = new QLabel("Idle"); + statusLabel_ = new QLabel(tr("Idle")); btnRow->addWidget(startBtn_); btnRow->addWidget(cancelBtn_); btnRow->addWidget(saveBtn_); @@ -167,12 +176,12 @@ void TracerWindow::buildUi() { table_->setSelectionBehavior(QAbstractItemView::SelectRows); table_->setProperty("monospace", true); // font from the theme sheet styleDataTable(table_); - setEmptyHint(table_, "Traced instructions appear here."); + setEmptyHint(table_, tr("Traced instructions appear here.")); connect(table_, &QTableView::doubleClicked, this, [this](const QModelIndex& index) { if (!index.isValid() || index.row() >= (int)entries_.size()) return; const auto& e = entries_[index.row()]; auto* dlg = new QDialog(this); - dlg->setWindowTitle(QString("Step %1 @ 0x%2").arg(index.row()).arg(e.address, 0, 16)); + dlg->setWindowTitle(tr("Step %1 @ 0x%2").arg(index.row()).arg(e.address, 0, 16)); dlg->resize(500, 320); auto* dl = new QVBoxLayout(dlg); auto* hdr = new QLabel(QString("%1").arg(QString::fromStdString(e.instruction))); @@ -206,7 +215,7 @@ void TracerWindow::buildUi() { void TracerWindow::onStart() { if (!proc_) { - QMessageBox::warning(this, "No process", "Open a process before tracing."); + QMessageBox::warning(this, tr("No process"), tr("Open a process before tracing.")); return; } if (worker_.joinable()) return; @@ -218,7 +227,7 @@ void TracerWindow::onStart() { bool ok=false; auto value=text.toULongLong(&ok,16); if (!ok || value>UINTPTR_MAX) { - statusLabel_->setText("Invalid hexadecimal address"); + statusLabel_->setText(tr("Invalid hexadecimal address")); edit->setFocus(); edit->selectAll(); return false; } result=static_cast(value); return true; @@ -230,20 +239,20 @@ void TracerWindow::onStart() { if (cfg.stayInModule) { if (!address(moduleStartEdit_,cfg.moduleBase) || !address(moduleEndEdit_,cfg.moduleEnd)) return; if (!cfg.moduleEnd || cfg.moduleEnd<=cfg.moduleBase) { - statusLabel_->setText("Module end must be greater than its start"); return; + statusLabel_->setText(tr("Module end must be greater than its start")); return; } } debugger_ = debuggerFactory_ ? debuggerFactory_() : std::make_unique(); if (!debugger_) { - QMessageBox::warning(this, "No debugger", - "No debugger could be constructed for the attached process."); + QMessageBox::warning(this, tr("No debugger"), + tr("No debugger could be constructed for the attached process.")); return; } entries_.clear(); model_->reload(); progressBar_->setValue(0); - statusLabel_->setText("Tracing…"); + statusLabel_->setText(tr("Tracing…")); startBtn_->setEnabled(false); cancelBtn_->setEnabled(true); saveBtn_->setEnabled(false); @@ -260,7 +269,7 @@ void TracerWindow::onStart() { void TracerWindow::onCancel() { tracer_.cancel(); - statusLabel_->setText("Cancelling…"); + statusLabel_->setText(tr("Cancelling…")); } void TracerWindow::onTraceFinished() { @@ -268,11 +277,13 @@ void TracerWindow::onTraceFinished() { model_->reload(); // headers follow the trace's architecture progressBar_->setValue(100); - statusLabel_->setText(QString("%1 steps recorded%2") - .arg(entries_.size()) - .arg(entries_.size() == 0 ? " (check start address / module range)" : "")); + if (entries_.empty()) + statusLabel_->setText(tr("%1 steps recorded (check start address / module range)") + .arg(entries_.size())); + else + statusLabel_->setText(tr("%1 steps recorded").arg(entries_.size())); if (auto error=tracer_.lastError()) - statusLabel_->setText(QString("%1 steps recorded: %2").arg(entries_.size()).arg(QString::fromStdString( + statusLabel_->setText(tr("%1 steps recorded: %2").arg(entries_.size()).arg(QString::fromStdString( proc_ ? ce::targetOperationError(*proc_, ce::TargetFeature::Trace, error) : error.message()))); startBtn_->setEnabled(true); cancelBtn_->setEnabled(false); @@ -280,11 +291,11 @@ void TracerWindow::onTraceFinished() { } void TracerWindow::onSave() { - auto path = QFileDialog::getSaveFileName(this, "Save trace", "trace.txt", "Text (*.txt)"); + auto path = QFileDialog::getSaveFileName(this, tr("Save trace"), "trace.txt", tr("Text (*.txt)")); if (path.isEmpty()) return; QFile f(path); if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { - QMessageBox::warning(this, "Save failed", f.errorString()); + QMessageBox::warning(this, tr("Save failed"), f.errorString()); return; } QTextStream out(&f); diff --git a/packaging/cheatengine.desktop b/packaging/cheatengine.desktop index 72100dcb..27b75cd1 100644 --- a/packaging/cheatengine.desktop +++ b/packaging/cheatengine.desktop @@ -2,6 +2,7 @@ Type=Application Name=Cheat Engine Comment=Memory scanner and debugger for Linux +Comment[zh_CN]=Linux 内存扫描器与调试器 Exec=cheatengine %f Icon=cheatengine Categories=Development;Debugger; diff --git a/packaging/io.github.wleeaf.CheatEngineLinux.metainfo.xml b/packaging/io.github.wleeaf.CheatEngineLinux.metainfo.xml index 34fcfb3b..95445aaa 100644 --- a/packaging/io.github.wleeaf.CheatEngineLinux.metainfo.xml +++ b/packaging/io.github.wleeaf.CheatEngineLinux.metainfo.xml @@ -4,7 +4,9 @@ MIT MIT Cheat Engine for Linux + Cheat Engine for Linux Memory scanner, debugger, and injector for modding games and apps + 用于修改游戏和应用的内存扫描器、调试器与注入器 wleeaf @@ -18,6 +20,13 @@ Focused on single-player game modding and reverse engineering. It does not target multiplayer anti-cheat.

+

+ Cheat Engine 的 Linux 原生重写版本,使用 C++ 和 Qt6 构建:内存扫描、指针扫描、 + 自动汇编器、交互式多线程调试器、结构分析、作弊表以及 Lua 脚本。 +

+

+ 面向单机游戏修改和逆向工程,不针对多人在线游戏的反作弊。 +

cheatengine.desktop diff --git a/test/gui_lifecycle_smoke.cpp b/test/gui_lifecycle_smoke.cpp index cdc01a4f..4315aac4 100644 --- a/test/gui_lifecycle_smoke.cpp +++ b/test/gui_lifecycle_smoke.cpp @@ -5,6 +5,7 @@ #include "gui/tracerwindow.hpp" #include "gui/debuggerwindow.hpp" #include "gui/scripteditor.hpp" +#include "gui/i18n.hpp" #include "core/expression.hpp" #include #include @@ -353,6 +354,57 @@ static bool injectionTemplateChecks() { } #include +#include + +// ── Interface-language helpers for the screenshot review ── +// The review drives windows by their English source text. When a catalogue is +// loaded (CE_LANG) the widgets show the translation, so match the English source +// against the translated string too. kUiContexts lists every context that +// carries user-visible strings (see translations/cheatengine_zh_CN.ts). +static const char* const kUiContexts[] = { + // Member tr() calls use the namespaced context moc records at runtime. + "ce::gui::AddressListModel", "ce::gui::AdvancedOptionsWindow", "ce::gui::AgentPanel", + "ce::gui::BranchMapper", "ce::gui::BreakpointListWindow", "ce::gui::ChangeAddressDialog", + "ce::gui::CodeFinderWindow", "ce::gui::CodeReferencesWindow", "ce::gui::DebuggerWindow", + "ce::gui::DisasmView", "ce::gui::ElfInspector", "ce::gui::FilePatcher", + "ce::gui::FindStaticsWindow", "ce::gui::FormDesigner", "ce::gui::GraphicalMemoryView", + "ce::gui::GuestScanDialog", "ce::gui::HeapRegionsWindow", "ce::gui::HexView", + "ce::gui::LuaConsole", "ce::gui::MainWindow", "ce::gui::MemPixelView", + "ce::gui::MemoryBrowser", "ce::gui::MemoryFillDialog", "ce::gui::MemoryRegionsWindow", + "ce::gui::MemviewPreferences", "ce::gui::ModuleListWindow", "ce::gui::MonoDissectorWindow", + "ce::gui::OverlayWindow", "ce::gui::PointerScanDialog", "ce::gui::ProcessListDialog", + "ce::gui::RegisterEditorWindow", "ce::gui::ScanResultsModel", "ce::gui::ScriptEditor", + "ce::gui::SettingsDialog", "ce::gui::StackViewWindow", "ce::gui::StructureDissector", + "ce::gui::ThreadListWindow", "ce::gui::TracerWindow", + // Free functions and classes without Q_OBJECT pass their context explicitly. + "AgentPanel", "CodeFinderWindow", "DebuggerWindow", "FilePatcher", "FormDesigner", + "GdbConnectionDialog", "GlobalHotkeyManager", "HeapRegionsWindow", "MainWindow", + "MonoDissectorWindow", "QObject", "SecretStore", "SettingsDialog", "StructureDissector", + "Theme", "TraceTableModel", "TracerWindow", +}; + +static bool uiTextIs(const QString& actual, const char* source) { + if (actual == QLatin1String(source)) return true; + for (const char* context : kUiContexts) { + const QString translated = QCoreApplication::translate(context, source); + if (translated != QLatin1String(source) && actual == translated) return true; + } + return false; +} + +static bool uiTextIs(const QString& actual, const QString& source) { + return uiTextIs(actual, source.toUtf8().constData()); +} + +// Select a combo item by its English source text, falling back to its translation. +static void uiSelect(QComboBox* combo, const char* source, const char* context) { + if (!combo) return; + const QString english = QString::fromLatin1(source); + if (combo->findText(english) >= 0) { combo->setCurrentText(english); return; } + const QString translated = QCoreApplication::translate(context, source); + if (combo->findText(translated) >= 0) combo->setCurrentText(translated); +} + #include "test/gui_visual_review.hpp" static bool visualInteractionChecks() { @@ -693,6 +745,9 @@ int main(int argc, char** argv) { QApplication app(argc, argv); app.setOrganizationName("cecore-test"); app.setApplicationName("gui-lifecycle"); + // The screenshot review can render another interface language: CE_LANG=zh_CN. + if (const QString language = qEnvironmentVariable("CE_LANG"); !language.isEmpty()) + ce::gui::installTranslators(app, language); if (argc == 3 && QString::fromLocal8Bit(argv[1]) == "--screenshots") { alarm(300); int result = renderGuiReview(QString::fromLocal8Bit(argv[2]), first, reinterpret_cast(&watched)); diff --git a/test/gui_visual_review.hpp b/test/gui_visual_review.hpp index 82ba20bd..69b6115e 100644 --- a/test/gui_visual_review.hpp +++ b/test/gui_visual_review.hpp @@ -108,7 +108,7 @@ static int renderGuiReview(const QString& directory, pid_t pid, uintptr_t valueA QAction* chosen = nullptr; std::function find = [&](QMenu* m) { for (auto* a : m->actions()) { - if (a->text() == actionText) chosen = a; + if (uiTextIs(a->text(), actionText)) chosen = a; else if (a->menu()) find(a->menu()); } }; @@ -134,7 +134,7 @@ static int renderGuiReview(const QString& directory, pid_t pid, uintptr_t valueA pump(); }; auto clickText = [](QWidget& w, const QString& text) { - for (auto* b : w.findChildren()) if (b->text() == text && b->isEnabled()) { b->click(); return true; } + for (auto* b : w.findChildren()) if (uiTextIs(b->text(), text) && b->isEnabled()) { b->click(); return true; } return false; }; // Wait (pumping events) until `done` holds or the timeout passes. @@ -177,7 +177,11 @@ static int renderGuiReview(const QString& directory, pid_t pid, uintptr_t valueA model->addGroup("Player settings"); main.findChild()->setResult(&results, ce::ValueType::Int32, 4); model->updateValues(&process); - for (auto* label : main.findChildren()) if (label->text() == "Found: 0") label->setText("Found: 24"); + const QString foundZero = QCoreApplication::translate("MainWindow", "Found: %1").arg(0); + const QString foundTwentyFour = QCoreApplication::translate("MainWindow", "Found: %1").arg(24); + for (auto* label : main.findChildren()) + if (uiTextIs(label->text(), "Found: 0") || label->text() == foundZero) + label->setText(foundTwentyFour); capture(main, prefix + "main-populated", {1000, 780}); // The README picture (docs/screenshot.png): CE_README_GAME names a build of // test/readme_game.c, which prints the addresses of its health, ammo, gold @@ -197,7 +201,7 @@ static int renderGuiReview(const QString& directory, pid_t pid, uintptr_t valueA table->groupSelectedRows({0, 1, 2, 3}, "Player"); readme.findChild("scanValue")->setText("100"); for (auto* button : readme.findChildren()) - if (button->text() == "First Scan") button->click(); + if (uiTextIs(button->text(), "First Scan")) button->click(); readme.resize(1100, 760); readme.show(); // Let the scan finish and the address list's live values refresh. @@ -227,14 +231,14 @@ static int renderGuiReview(const QString& directory, pid_t pid, uintptr_t valueA // Scan-option states: float rounding + tolerance, a range scan, text. auto* valueType = main.findChild("scanValueType"); auto* comparison = main.findChild("scanComparison"); - valueType->setCurrentText("Float"); - main.findChild("scanRounding")->setCurrentText("Extreme"); - comparison->setCurrentText("Value between..."); + uiSelect(valueType, "Float", "MainWindow"); + uiSelect(main.findChild("scanRounding"), "Extreme", "MainWindow"); + uiSelect(comparison, "Value between...", "MainWindow"); capture(main, prefix + "main-float-between", {1000, 780}); - valueType->setCurrentText("String"); - comparison->setCurrentText("Exact Value"); + uiSelect(valueType, "String", "MainWindow"); + uiSelect(comparison, "Exact Value", "MainWindow"); capture(main, prefix + "main-string", {1000, 780}); - valueType->setCurrentText("4 Bytes"); + uiSelect(valueType, "4 Bytes", "MainWindow"); auto* progress = main.findChild(); progress->setValue(42); progress->setVisible(true); capture(main, prefix + "main-scanning", {1000, 780}); @@ -258,11 +262,11 @@ static int renderGuiReview(const QString& directory, pid_t pid, uintptr_t valueA {"Pointer scan for this address", "entry-pointer-scan"}}) captureMenuAction(entryMenu, action, prefix + "dialog-" + name); // A real First Scan, so the results list and its menu show live data. - main.findChild("scanValueType")->setCurrentText("4 Bytes"); - main.findChild("scanComparison")->setCurrentText("Exact Value"); + uiSelect(main.findChild("scanValueType"), "4 Bytes", "MainWindow"); + uiSelect(main.findChild("scanComparison"), "Exact Value", "MainWindow"); main.findChild("scanValue")->setText("0"); for (auto* button : main.findChildren()) - if (button->text() == "First Scan") button->click(); + if (uiTextIs(button->text(), "First Scan")) button->click(); pump(40); capture(main, prefix + "main-after-scan", {1000, 780}); main.show(); @@ -276,7 +280,7 @@ static int renderGuiReview(const QString& directory, pid_t pid, uintptr_t valueA auto openAction = [&](const QString& text) { return [&main, text] { for (auto* action : main.findChildren()) - if (action->text() == text) { action->trigger(); return; } + if (uiTextIs(action->text(), text)) { action->trigger(); return; } std::printf("visual review: no action %s\n", qPrintable(text)); }; }; @@ -302,9 +306,9 @@ static int renderGuiReview(const QString& directory, pid_t pid, uintptr_t valueA pump(); } for (auto* button : main.findChildren()) { - if (button->text() == "Add Address") captureNew([button] { button->click(); }, prefix + "dialog-add-address"); - if (button->text() == "Table Extras") captureNew([button] { button->click(); }, prefix + "dialog-comments"); - if (button->text() == "Advanced Options") captureNew([button] { button->click(); }, prefix + "dialog-advanced-options"); + if (uiTextIs(button->text(), "Add Address")) captureNew([button] { button->click(); }, prefix + "dialog-add-address"); + if (uiTextIs(button->text(), "Table Extras")) captureNew([button] { button->click(); }, prefix + "dialog-comments"); + if (uiTextIs(button->text(), "Advanced Options")) captureNew([button] { button->click(); }, prefix + "dialog-advanced-options"); } } { diff --git a/tools/check-translations.py b/tools/check-translations.py new file mode 100644 index 00000000..d8fd29f0 --- /dev/null +++ b/tools/check-translations.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Check that the Qt translation catalogues in translations/ are complete. + +Every must have a finished translation, and the %n / %1..%9 +placeholders of the source must also appear in the translation. Used by CI so +a new tr() string cannot land without its translation. + +Usage: python3 tools/check-translations.py +Exits non-zero (prints FAILED) when a catalogue is incomplete. +""" +import glob +import os +import re +import sys +import xml.etree.ElementTree as ET + +PLACEHOLDER = re.compile(r'%(?:n|\d{1,3})') + +def placeholders(text: str) -> set[str]: + return set(PLACEHOLDER.findall(text or '')) + +def check(path: str) -> list[str]: + errors = [] + tree = ET.parse(path) + language = tree.getroot().get('language') or os.path.basename(path).split('_')[-1].split('.')[0] + for context in tree.getroot().findall('context'): + name = context.findtext('name') or '?' + for message in context.findall('message'): + source = message.findtext('source') or '' + translation = message.find('translation') + if translation is None or translation.get('type') == 'unfinished': + errors.append(f"{path}: {name}: unfinished: {source!r}") + continue + if not (translation.text or '').strip(): + errors.append(f"{path}: {name}: empty translation: {source!r}") + continue + if message.get('numerus') == 'yes': + forms = [form.text or '' for form in translation.findall('numerusform')] + if not forms or any(not form.strip() for form in forms): + errors.append(f"{path}: {name}: unfinished plural: {source!r}") + continue + missing = placeholders(source) - placeholders(translation.text) + if missing: + errors.append(f"{path}: {name}: translation drops {sorted(missing)}: {source!r}") + print(f"{path}: language={language} OK") + return errors + +def main() -> int: + root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + catalogues = sorted(glob.glob(os.path.join(root, 'translations', '*.ts'))) + if not catalogues: + print("FAILED: no translation catalogues under translations/") + return 1 + errors = [] + for path in catalogues: + try: + errors.extend(check(os.path.relpath(path, root))) + except ET.ParseError as error: + errors.append(f"{path}: invalid XML: {error}") + if errors: + for error in errors: + print(f" {error}") + print(f"FAILED: {len(errors)} translation problem(s)") + return 1 + print("Translations complete") + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/ci-check.sh b/tools/ci-check.sh index 9a35da66..bd0b0daa 100755 --- a/tools/ci-check.sh +++ b/tools/ci-check.sh @@ -56,6 +56,10 @@ step "coverage matrix (docs/compatibility-matrix.json) in sync" python3 tools/coverage_matrix.py --check ok "coverage table current" +step "translation catalogues complete (translations/*.ts)" +python3 tools/check-translations.py +ok "translations complete" + # ── Job 1: sanitizers (ASan+UBSan, NO Qt) — the one that catches portability gaps ── step "sanitizers job (ASan, no-GUI): configure" # Reconfigure the existing tree so repeated checks can reuse compiled dependencies. diff --git a/translations/cheatengine_zh_CN.ts b/translations/cheatengine_zh_CN.ts new file mode 100644 index 00000000..1952a4aa --- /dev/null +++ b/translations/cheatengine_zh_CN.ts @@ -0,0 +1,8737 @@ + + + + + AgentPanel + + + (%1 lines) + (%1 行) + + + + refused: %1 + 已拒绝:%1 + + + + %1 found + 找到 %1 个 + + + + attached to %1 + 已附加到 %1 + + + + %1 instruction(s) + %1 条指令 + + + + %1 address(es) + %1 个地址 + + + + saved to %1 + 已保存到 %1 + + + + now %1 + 现为 %1 + + + + value %1 + 值 %1 + + + + disabled + 已禁用 + + + + enabled + 已启用 + + + + done + 完成 + + + + OpenAI-compatible server + OpenAI 兼容服务器 + + + + The API key is not sent to %1: use an https address (plain http is only for a server on this computer). + API 密钥不会发送到 %1:请使用 https 地址(明文 http 仅用于本机上的服务器)。 + + + + The saved API key is for %1, so it is not sent to %2. Enter the key again in Settings, AI Assistant to use it there. + 已保存的 API 密钥属于 %1,因此不会发送到 %2。请在“设置 > AI 助手”中重新输入该密钥,以便在那里使用。 + + + + CodeFinderWindow + + + x%1 (direct) + x%1(直接) + + + + %1 (direct) + %1(直接) + + + + DebuggerWindow + + + if %1 + 如果 %1 + + + + FilePatcher + + + Hex byte sequence has odd length. + 十六进制字节序列长度为奇数。 + + + + Not a hex byte: '%1' + 不是十六进制字节:'%1' + + + + FormDesigner + + + Button + 按钮 + + + + Label + 标签 + + + + Edit + 编辑框 + + + + CheckBox + 复选框 + + + + Memo + 多行文本 + + + + GroupBox + 分组框 + + + + Panel + 面板 + + + + GdbConnectionDialog + + + Connect to GDB / QEMU + 连接到 GDB / QEMU + + + + Host: + 主机: + + + + Port: + 端口: + + + + From target (may be unknown) + 来自目标(可能未知) + + + + Little-endian + 小端序 + + + + Big-endian + 大端序 + + + + From target + 来自目标 + + + + 4 bytes (32-bit pointers) + 4 字节(32 位指针) + + + + 8 bytes (64-bit pointers) + 8 字节(64 位指针) + + + + Data byte order: + 数据字节序: + + + + Program pointer width: + 程序指针宽度: + + + + e.g. 0x40000000 + 例如 0x40000000 + + + + e.g. 0x1000 + 例如 0x1000 + + + + Scan range start: + 扫描范围起始: + + + + Scan range size: + 扫描范围大小: + + + + The target must be stopped. Leave both range fields blank to use the stub's memory map. Addresses and sizes accept decimal or 0x-prefixed hex. + 目标必须处于停止状态。两个范围字段均留空则使用 stub 的内存映射。地址和大小接受十进制或以 0x 为前缀的十六进制。 + + + + Connect + 连接 + + + + Enter a host. + 请输入主机。 + + + + Enter a valid numeric range start and positive size. + 请输入有效的数值范围起始地址和正的尺寸。 + + + + Connecting and reading the target description... + 正在连接并读取目标描述... + + + + Canceling the connection... + 正在取消连接... + + + + GlobalHotkeyManager + + + Cheat Engine hotkey %1 + Cheat Engine 快捷键 %1 + + + + Cheat Engine: %1 + Cheat Engine:%1 + + + + HeapRegionsWindow + + + brk heap + brk 堆 + + + + anonymous private + 匿名私有 + + + + MainWindow + + + Found: %1 + 已找到:%1 + + + + any process of the same user + 同一用户的任意进程 + + + + only child processes + 仅子进程 + + + + administrators only + 仅管理员 + + + + no attaching + 禁止附加 + + + + Access granted for this session; re-open the process. +(This resets on reboot. Install the .deb, or re-run the command, to make it permanent.) + 本次会话已授予访问权限;请重新打开进程。 +(重启后失效。安装 .deb,或重新运行该命令,使其永久生效。) + + + + Access granted for this session; re-open the process. +(This resets on reboot. A capability on this install would stop it from finding its libraries, because it is outside the system library directories.) + 本次会话已授予访问权限;请重新打开进程。 +(重启后失效。此安装上的能力会导致它无法找到自己的库,因为库位于系统库目录之外。) + + + + Access granted. Restart Cheat Engine for it to take effect. + 已授予访问权限。请重启 Cheat Engine 使其生效。 + + + + Access granted + 已授予访问权限 + + + + Could not grant access + 无法授予访问权限 + + + + The privileged command was cancelled or failed (pkexec exit %1). You can still grant access manually from a terminal. + 特权命令已取消或失败(pkexec 退出码 %1)。你仍可以在终端中手动授予访问权限。 + + + + Process memory not readable + 无法读取进程内存 + + + + Cannot read the memory of PID %1 (%2). + +Linux only lets a program read another process's memory if it is allowed to ptrace it. With kernel.yama.ptrace_scope = %3, that means the target must be a child of Cheat Engine, or Cheat Engine must run with elevated rights. Scanning, memory browsing and debugging this process will not work until that is granted. + 无法读取 PID %1(%2)的内存。 + +Linux 只允许程序在获准 ptrace 另一个进程时读取其内存。当 kernel.yama.ptrace_scope = %3 时,目标必须是 Cheat Engine 的子进程,或者 Cheat Engine 必须以提升的权限运行。在获得该权限之前,扫描、内存浏览和调试此进程都将无法工作。 + + + + "Grant access" will ask for your password (via pkexec) and set it up for you. Or do it manually: + • Persistent, per-binary: sudo setcap cap_sys_ptrace+ep %1 + • Whole system, until reboot: sudo sysctl kernel.yama.ptrace_scope=0 + “授予访问权限”会通过 pkexec 要求你输入密码并自动完成设置。也可以手动操作: + • 永久、按二进制文件: sudo setcap cap_sys_ptrace+ep %1 + • 整个系统,直至重启: sudo sysctl kernel.yama.ptrace_scope=0 + + + + Grant access… + 授予访问权限… + + + + Cheat Tables (*.ct *.CT);;JSON Tables (*.json *.JSON);;All Files (*) + 作弊表格 (*.ct *.CT);;JSON 表格 (*.json *.JSON);;所有文件 (*) + + + + Scan value + 扫描数值 + + + + Enter a value for this scan. + 请输入用于此扫描的数值。 + + + + "%1" is not a number (or does not fit in 64 bits). + “%1”不是数字(或超出 64 位范围)。 + + + + MonoDissectorWindow + + + <unknown> + <未知> + + + + SecretStore + + + the keyring did not answer + 密钥环没有响应 + + + + the keyring prompt was dismissed + 密钥环提示已被取消 + + + + no keyring service is running + 没有正在运行的密钥环服务 + + + + the keyring has no default collection + 密钥环没有默认集合 + + + + Cheat Engine for Linux: %1 API key + Cheat Engine for Linux:%1 API 密钥 + + + + SettingsDialog + + + byte + 字节 + + + + bytes + 字节 + + + + %1 %2 + %1 %2 + + + + Using %1 from the environment. + 使用环境变量中的 %1。 + + + + A key is saved in the system keyring. + 密钥已保存在系统密钥环中。 + + + + A key is set until Cheat Engine closes (no keyring was available). + 密钥仅在 Cheat Engine 关闭前有效(没有可用的密钥环)。 + + + + No key yet. Create one at console.anthropic.com. + 尚无密钥。请在 console.anthropic.com 创建。 + + + + No key set; local servers usually need none. + 未设置密钥;本地服务器通常不需要。 + + + + StructureDissector + + + + Pointer + 指针 + + + + + Float + 浮点数 + + + + + 8 Bytes + 8 字节 + + + + + 4 Bytes + 4 字节 + + + + Byte + 字节 + + + + 2 Bytes + 2 字节 + + + + Double + 双精度浮点数 + + + + String + 字符串 + + + + Theme + + + %1 (main) + %1 (主线程) + + + + %1 %2 (main) + %1 %2 (主线程) + + + + TraceTableModel + + + + + Address + 地址 + + + + + + Instruction + 指令 + + + + ce::gui::AddressListModel + + + Auto Assembler script + 自动汇编脚本 + + + + (Auto Assembler script) + (自动汇编脚本) + + + + Table format + 表格格式 + + + + Invalid record data order or pointer size. + 记录数据字节序或指针大小无效。 + + + + + + + + + + Value edit failed + 数值编辑失败 + + + + Process required + 需要进程 + + + + Open a process before activating this auto-assembler record. + 请先打开进程再激活此自动汇编记录。 + + + + Auto-assembler activation failed + 自动汇编激活失败 + + + + Auto-assembler deactivation failed + 自动汇编停用失败 + + + + Freeze failed + 冻结失败 + + + + The value cannot be read right now, so there is nothing to freeze. + 当前无法读取数值,因此没有可冻结的内容。 + + + + Active + 启用 + + + + Description + 描述 + + + + Address + 地址 + + + + Type + 类型 + + + + Value + 数值 + + + + + The pointer path does not resolve right now. + 指针路径当前无法解析。 + + + + ce::gui::AdvancedOptionsWindow + + + Code list/Pause + 代码列表/暂停 + + + + Code list: + 代码列表: + + + + Address + 地址 + + + + Name + 名称 + + + + Code found with "Find out what writes" can be added here to keep it, disassemble it or replace it with NOPs. + 通过“找出是什么改写了它”找到的代码可以添加到这里,以便保留、反汇编或替换为 NOP。 + + + + OK + 确定 + + + + Open the disassembler at this location + 在此位置打开反汇编器 + + + + Replace with code that does nothing + 替换为空操作代码 + + + + Restore with original code + 恢复为原始代码 + + + + + Rename + 重命名 + + + + Remove from list + 从列表中移除 + + + + Could not read/patch the instruction. + 无法读取/修改该指令。 + + + + Name: + 名称: + + + + ce::gui::AgentPanel + + + Describe what to change in the game, for example: + +My health shows 100. Find it and keep it at 100. + 描述你想在游戏中修改的内容,例如: + +我的生命值显示为 100。找到它并保持为 100。 + + + + Allow + 允许 + + + + Allow for this chat + 本次对话内允许 + + + + Allow later changes in this process without asking, until the chat or the process changes + 在本次对话或该进程发生变化之前,允许之后对此进程的修改而不再询问 + + + + Decline + 拒绝 + + + + Message the assistant (Enter sends, Shift+Enter adds a line) + 给助手发消息(Enter 发送,Shift+Enter 换行) + + + + Send + 发送 + + + + Stop + 停止 + + + + New chat + 新对话 + + + + Add to address list + 添加到地址列表 + + + + Copy the values and scripts the assistant saved into the address list + 将助手保存的数值和脚本复制到地址列表 + + + + Settings... + 设置... + + + + Added %1 entry to the address list. + 已向地址列表添加 %1 个条目。 + + + + Added %1 entries to the address list. + 已向地址列表添加 %1 个条目。 + + + + %1%2. Your messages and what the tools find in the game are sent there. + %1%2。你的消息以及这些工具在游戏中发现的内容都会发送到那里。 + + + + Stopping... + 正在停止... + + + + <span style='color:%1'>The AI Assistant settings changed, so this message starts a new conversation.</span> + <span style='color:%1'>AI 助手设置已更改,因此这条消息将开始新的对话。</span> + + + + Add your Anthropic API key in Settings, AI Assistant%1. + 请在“设置 > AI 助手”中添加你的 Anthropic API 密钥%1。 + + + + <span style='color:%1'>%2 Continuing without a key.</span> + <span style='color:%1'>%2 将不使用密钥继续。</span> + + + + <b>You</b><br>%1 + <b>你</b><br>%1 + + + + + Working... + 处理中... + + + + Stopped. + 已停止。 + + + + in PID %1 + (PID %1) + + + + in %1 (PID %2) + (%1,PID %2) + + + + Write %1 (%2) to %3%4? + 将 %1(%2)写入 %3%4? + + + + Save the table to %1, replacing that file if it exists? + 将表格保存到 %1,如果该文件存在则覆盖? + + + + Save the table to %1? + 将表格保存到 %1? + + + + Let the assistant open "%1"? It can then read that program's memory, and what it reads is sent to the model's provider. + 允许助手打开“%1”吗?它随后可以读取该程序的内存,读取到的内容会发送给模型提供商。 + + + + record %1 + 记录 %1 + + + + , which leads nowhere now + ,它现在指向无效地址 + + + + , now %1 + ,现为 %1 + + + + (%1 at %2) + (%1,位于 %2) + + + + Freeze %1 at %2%3? + 冻结 %1,将其设为 %2%3? + + + + its current value + 其当前值 + + + + Put a breakpoint at %1%2 for %3 s to see which addresses that instruction accesses? The game pauses briefly each time it runs. + 在 %1%2 设置断点并持续 %3 秒,以查看该指令访问了哪些地址?每次执行时游戏会短暂暂停。 + + + + Disable script #%1%2%3? Its [DISABLE] section runs and restores the game's code. + 禁用脚本 #%1%2%3 吗?它的 [DISABLE] 段会运行并恢复游戏代码。 + + + + Enable the script "%1"%2? + 启用脚本“%1”%2? + + + + Waiting for your answer... + 正在等待你的答复... + + + + closing + 正在关闭 + + + + the reply is too large + 回复内容过大 + + + + stopped + 已停止 + + + + ce::gui::BranchMapper + + + Branch Mapper (LBR) + 分支映射器 (LBR) + + + + Thread ID: + 线程 ID: + + + + Start sampling + 开始采样 + + + + Stop + 停止 + + + + Clear + 清除 + + + + + + Branch sampling requires a live local process target + 分支采样需要一个活动的本地进程目标 + + + + Idle + 空闲 + + + + Branch-stack sampling unavailable; requires kernel perf permission and a supported CPU/PMU. + 分支栈采样不可用;需要内核 perf 权限以及受支持的 CPU/PMU。 + + + + From + 来源 + + + + From symbol + 来源符号 + + + + To + 目标 + + + + To symbol + 目标符号 + + + + Hits + 命中次数 + + + + % of total + 占比 + + + + Click Start sampling to record the branches this thread takes. + 点击“开始采样”记录此线程执行的分支。 + + + + Start failed + 启动失败 + + + + Sampling… + 采样中… + + + + Stopped. %1 samples total. + 已停止。共 %1 个样本。 + + + + Cleared. + 已清除。 + + + + Sampling… %1 total / %2 unique pairs + 采样中… 共 %1 个 / %2 个唯一样本对 + + + + ce::gui::BreakpointListWindow + + + Breakpoint List + 断点列表 + + + + Address + 地址 + + + + Type + 类型 + + + + Action + 操作 + + + + Hits + 命中次数 + + + + Enabled + 已启用 + + + + No breakpoints. Set one in the Memory Viewer (F5) or the Debugger. + 还没有断点。请在内存查看器 (F5) 或调试器中设置断点。 + + + + Remove + 移除 + + + + Enable/Disable + 启用/禁用 + + + + Execute + 执行 + + + + Write + 写入 + + + + Read + 读取 + + + + Access + 访问 + + + + Break + 中断 + + + + Find Code + 查找代码 + + + + Find Access + 查找访问 + + + + Trace + 跟踪 + + + + Yes + 是 + + + + No + 否 + + + + ce::gui::ChangeAddressDialog + + + Byte + 字节 + + + + 2 Bytes + 2 字节 + + + + 4 Bytes + 4 字节 + + + + 8 Bytes + 8 字节 + + + + Float + 浮点数 + + + + Double + 双精度浮点数 + + + + String + 字符串 + + + + Array of byte + 字节数组 + + + + + Pointer + 指针 + + + + Change address + 更改地址 + + + + Address: + 地址: + + + + Type: + 类型: + + + + Length: + 长度: + + + + Hexadecimal + 十六进制 + + + + Signed + 有符号 + + + + Unicode + Unicode + + + + Base: + 基址: + + + + Add offset + 添加偏移 + + + + → ?? (does not resolve) + → ??(无法解析) + + + + Offset: + 偏移: + + + + Remove this offset + 移除此偏移 + + + + ce::gui::CodeFinderWindow + + + Monitoring... + 监视中... + + + + + + + Address + 地址 + + + + + + + Instruction + 指令 + + + + + + + Hits + 命中次数 + + + + + + + Pointer path + 指针路径 + + + + Waiting for an instruction to touch the address. Let the game run. + 正在等待访问该地址的指令。请让游戏继续运行。 + + + + Hit context @ 0x%1 + 命中上下文 @ 0x%1 + + + + <b>%1</b> hits=%2 + <b>%1</b> 命中=%2 + + + + <b>First hit:</b> + <b>首次命中:</b> + + + + <b>Last hit:</b> + <b>最后命中:</b> + + + + Stop + 停止 + + + + Add to Address List + 添加到地址列表 + + + + Add the selected instructions (or all, if none selected) to the address list. + 将选中的指令(未选中任何指令时则为全部)添加到地址列表。 + + + + Save to File... + 保存到文件... + + + + Export the found instructions, hit counts, and registers to a text file. + 将找到的指令、命中次数和寄存器导出到文本文件。 + + + + Monitoring... %1 unique instructions found (double-click a row for full register state) + 监视中... 已找到 %1 条唯一指令(双击某行可查看完整寄存器状态) + + + + Stopped. %1 unique instructions found + 已停止。已找到 %1 条唯一指令 + + + + Recovering + 正在恢复 + + + + Monitoring + 监视中 + + + + Stopped + 已停止 + + + + This instruction addresses the value via this register; pointer-scan the base for a stable address. + 该指令通过此寄存器访问该数值;请对该基址做指针扫描以获得稳定地址。 + + + + + Added %1 instruction(s) to the address list. + 已向地址列表添加 %1 条指令。 + + + + Save findings + 保存查找结果 + + + + Text files (*.txt);;All files (*) + 文本文件 (*.txt);;所有文件 (*) + + + + Save failed + 保存失败 + + + + %1 unique instruction(s) + + + %1 条唯一指令 + + + + + + 0x%1 hits=%2 %3 + + 0x%1 命中=%2 %3 + + + + + First hit: + + 首次命中: + + + + + Last hit: + + 最后命中: + + + + + Saved %1 instruction(s) to %2 + 已将 %1 条指令保存到 %2 + + + + Show in the disassembler + 在反汇编器中显示 + + + + Replace with code that does nothing (NOP) + 替换为不执行任何操作的代码(NOP) + + + + Restore with original code + 恢复为原始代码 + + + + Add to the address list + 添加到地址列表 + + + + Patched 0x%1 to NOPs. + 已将 0x%1 替换为 NOP。 + + + + Could not patch 0x%1. + 无法替换 0x%1。 + + + + Restored the original code at 0x%1. + 已恢复 0x%1 处的原始代码。 + + + + Could not restore 0x%1. + 无法恢复 0x%1。 + + + + ce::gui::CodeReferencesWindow + + + Code References + 代码引用 + + + + Instruction to find, e.g. mov eax,1 + 要查找的指令,例如 mov eax,1 + + + + Analyze + 分析 + + + + Min cave bytes: + 最小代码洞字节数: + + + + Instruction + 指令 + + + + Target + 目标 + + + + Text + 文本 + + + + Type an instruction above (for example "mov eax,1") and click Analyze to find every place the module contains exactly it. + 在上方输入一条指令(例如“mov eax,1”)并点击“分析”,找出模块中所有完全包含该指令的位置。 + + + + + + Pick a module and click Analyze. + 选择一个模块并点击“分析”。 + + + + Function + 函数 + + + + References + 引用次数 + + + + Caller + 调用者 + + + + Callee + 被调用者 + + + + Call Site + 调用位置 + + + + Address + 地址 + + + + Size + 大小 + + + + Pick a module and click Analyze to list unused code space. + 选择一个模块并点击“分析”,列出未使用的代码空间。 + + + + Referenced Strings + 引用的字符串 + + + + Referenced Functions + 引用的函数 + + + + Functions + 函数 + + + + Call Graph + 调用图 + + + + Jumps + 跳转 + + + + + RIP-relative + RIP 相对 + + + + Assembly Scan + 汇编扫描 + + + + Code Caves + 代码洞 + + + + %1 modules available + %1 个模块可用 + + + + Code references + 代码引用 + + + + Analyzing %1… + 正在分析 %1… + + + + Cancel + 取消 + + + + Analyzing %1: %2… + 正在分析 %1:%2… + + + + referenced strings + 引用的字符串 + + + + referenced functions + 引用的函数 + + + + functions + 函数 + + + + call graph + 调用图 + + + + jumps + 跳转 + + + + assembly pattern + 汇编模式 + + + + code caves + 代码洞 + + + + %1: %2 strings, %3 calls, %4 functions, %5 jumps, %6 RIP-relative, %7 assembly, %8 caves + %1:%2 个字符串,%3 个调用,%4 个函数,%5 个跳转,%6 个 RIP 相对引用,%7 个汇编匹配,%8 个代码洞 + + + + ce::gui::DebuggerWindow + + + Debugger + 调试器 + + + + Continue + 继续 + + + + Step Into + 单步进入 + + + + Step Over + 单步跳过 + + + + Step Out + 单步跳出 + + + + Run to Cursor + 运行到光标 + + + + Detach + 分离 + + + + Break on exceptions + 异常时中断 + + + + SIGSEGV (segfault) + SIGSEGV(段错误) + + + + SIGILL (illegal instruction) + SIGILL(非法指令) + + + + SIGFPE (FP / divide-by-zero) + SIGFPE(浮点 / 除零) + + + + SIGBUS (bus error) + SIGBUS(总线错误) + + + + SIGABRT (abort) + SIGABRT(中止) + + + + Not attached + 未附加 + + + + The disassembly around the stopped thread's instruction pointer appears here. + 停止线程指令指针附近的反汇编将显示在这里。 + + + + Stack of the stopped thread + 已停止线程的栈 + + + + memory address (hex), then Enter + 内存地址(十六进制),然后按回车 + + + + <span style="color:%1">Type an address above and press Enter to dump memory here.</span> + <span style="color:%1">在上方输入地址并按回车,即可在此转储内存。</span> + + + + Thread: + 线程: + + + + Double-click a value to edit the register while the thread is stopped + 线程停止时双击数值即可编辑寄存器 + + + + Flags: + 标志位: + + + + Status flags of the currently stopped thread + 当前停止线程的状态标志位 + + + + Breakpoints + 断点 + + + + address (hex), then Add + 地址(十六进制),然后点击“添加” + + + + Add + 添加 + + + + Data BP... + 数据断点... + + + + Hardware watchpoint: break when an address is written or accessed + 硬件监视点:当地址被写入或访问时中断 + + + + Check to enable/disable · double-click to set/edit its condition + 勾选以启用/禁用 · 双击设置/编辑条件 + + + + Stop the target to enable/disable breakpoints + 停止目标后才能启用/禁用断点 + + + + Could not enable breakpoint + 无法启用断点 + + + + + Breakpoint cleanup pending: %1 + 断点清理未完成:%1 + + + + No breakpoints yet. +Add one above, or press F5 +on a line of code. + 还没有断点。 +请在上方添加一个, +或在代码行上按 F5。 + + + + Remove + 移除 + + + + Remove the selected breakpoint + 移除选中的断点 + + + + Attach failed: %1 + 附加失败:%1 + + + + No process attached + 未附加进程 + + + + Running... + 运行中... + + + + + Continue failed, still stopped: %1 + 继续失败,仍处于停止状态:%1 + + + + Stepping... (Detach ends a step that does not return) + 单步执行中...(分离会结束无法返回的单步) + + + + + Run-to-cursor cleanup pending: %1 + 运行到光标清理未完成:%1 + + + + Detached + 已分离 + + + + Invalid breakpoint address + 无效的断点地址 + + + + + Stop the target before setting a breakpoint + 设置断点前请先停止目标 + + + + Failed to set breakpoint at %1 + 在 %1 处设置断点失败 + + + + (hits: %1) + (命中:%1) + + + + Conditional breakpoint + 条件断点 + + + + Break only when this expression is true +(Lua; register names accept either case, also available in ctx, e.g. "%1"): + 仅在以下表达式为真时中断 +(Lua;寄存器名不区分大小写,也可用于 ctx,例如“%1”): + + + + Edit breakpoint condition + 编辑断点条件 + + + + Break only when this expression is true (empty = unconditional): + 仅在以下表达式为真时中断(留空表示无条件): + + + + The breakpoint was removed while editing + 编辑期间该断点已被移除 + + + + Wait for the step to finish (or Detach) before removing a breakpoint + 请等待单步结束(或分离)后再移除断点 + + + + Data breakpoint + 数据断点 + + + + address (hex) + 地址(十六进制) + + + + Writes + 写入 + + + + Reads or writes + 读取或写入 + + + + %1 byte + %1 字节 + + + + %1 bytes + %1 字节 + + + + Address: + 地址: + + + + Break on: + 中断条件: + + + + Size: + 大小: + + + + Set + 设置 + + + + Invalid address + 无效地址 + + + + No free debug register (max 4 hardware breakpoints) + 没有空闲调试寄存器(最多 4 个硬件断点) + + + + Thread is exiting; stopped on tid %1 + 线程正在退出;停止在 tid %1 + + + + Process executed a new program; previous breakpoints cleared + 进程已执行新程序;之前的断点已清除 + + + + Process exited + 进程已退出 + + + + Stopped at %1 (tid %2) + 停止于 %1(tid %2) + + + + <unreadable at %1> + <无法读取 %1> + + + + Flags: %1 + 标志位:%1 + + + + + + Unavailable + 不可用 + + + + %1 needs a %2-bit hexadecimal value + %1 需要 %2 位的十六进制数值 + + + + Register edit failed: %1 + 寄存器编辑失败:%1 + + + + Invalid hexadecimal register value + 无效的十六进制寄存器数值 + + + + <instruction architecture unavailable> + <指令架构不可用> + + + + (will jump) + (将跳转) + + + + (no jump) + (不跳转) + + + + <unable to read code at %1> + <无法读取 %1 处的代码> + + + + <stack register format unavailable> + <栈寄存器格式不可用> + + + + Cannot read the instruction at %1 + 无法读取 %1 处的指令 + + + + No instruction at %1 + 在 %1 处没有指令 + + + + Remove the breakpoint on this instruction before replacing it with NOPs + 替换为 NOP 前请先移除此指令上的断点 + + + + Could not replace the instruction at %1 with NOPs + 无法将 %1 处的指令替换为 NOP + + + + Set breakpoint here + 在此处设置断点 + + + + Set conditional breakpoint... + 设置条件断点... + + + + Replace with NOPs + 替换为 NOP + + + + Run to cursor + 运行到光标处 + + + + ce::gui::DisasmView + + + Address: 0x%1 + 地址:0x%1 + + + + Copy address + 复制地址 + + + + Copy bytes (%1 instructions) + 复制字节(%1 条指令) + + + + Copy bytes + 复制字节 + + + + Copy lines (%1) + 复制行(%1) + + + + Copy line + 复制行 + + + + Follow operand + 跟随操作数 + + + + Remove breakpoint + 移除断点 + + + + Set software breakpoint + 设置软件断点 + + + + Set hardware exec breakpoint + 设置硬件执行断点 + + + + Label this address… + 为此地址添加标签… + + + + Set comment… + 设置注释… + + + + Add this address to the list + 将此地址添加到列表 + + + + Find references to this address + 查找对此地址的引用 + + + + Find out what addresses this instruction accesses + 查找此指令访问的地址 + + + + Assemble instruction… + 汇编指令… + + + + NOP %1 instructions (%2 bytes) + NOP %1 条指令(%2 字节) + + + + NOP this instruction (%1 byte) + NOP 此指令(%1 字节) + + + + NOP this instruction (%1 bytes) + NOP 此指令(%1 字节) + + + + Auto Assemble + 自动汇编 + + + + Create code injection here + 在此创建代码注入 + + + + Create AOB injection here + 在此创建 AOB 注入 + + + + Save region to file… + 将区域保存到文件… + + + + Load region from file… + 从文件加载区域… + + + + Cannot read this process's memory (permission denied). +Run Cheat Engine with ptrace rights: sudo setcap cap_sys_ptrace+ep <cheatengine>, run as root, or set kernel.yama.ptrace_scope=0. + 无法读取此进程的内存(权限不足)。 +请以 ptrace 权限运行 Cheat Engine:sudo setcap cap_sys_ptrace+ep <cheatengine>、以 root 身份运行,或设置 kernel.yama.ptrace_scope=0。 + + + + No readable memory at this address. + 此地址没有可读内存。 + + + + No data at this address. + 此地址没有数据。 + + + + (will jump) + (将跳转) + + + + (no jump) + (不跳转) + + + + ce::gui::ElfInspector + + + ELF Inspector + ELF 检查器 + + + + ELF file: + ELF 文件: + + + + Browse… + 浏览… + + + + Load + 加载 + + + + Header + 文件头 + + + + Sections + 节区 + + + + Segments + 段 + + + + Dynamic + 动态 + + + + Symbols + 符号 + + + + Notes + 注释 + + + + Close + 关闭 + + + + Choose ELF file + 选择 ELF 文件 + + + + All Files (*) + 所有文件 (*) + + + + readelf not found in PATH (install binutils). + 未在 PATH 中找到 readelf(请安装 binutils)。 + + + + Running readelf... + 正在运行 readelf... + + + + readelf took longer than 60 seconds and was stopped. + readelf 运行超过 60 秒,已被停止。 + + + + --- stderr --- + --- 标准错误 --- + + + + (readelf produced no output) + (readelf 没有输出) + + + + readelf could not be started. + 无法启动 readelf。 + + + + ce::gui::FilePatcher + + + File Patcher + 文件修补器 + + + + /path/to/file + 文件路径 + + + + Browse… + 浏览… + + + + File: + 文件: + + + + 0x0 (hex with 0x prefix, decimal otherwise) + 0x0(带 0x 前缀为十六进制,否则为十进制) + + + + Offset: + 偏移: + + + + Read current bytes + 读取当前字节 + + + + Inspect: + 查看: + + + + Hex bytes to write (whitespace ignored): + 要写入的十六进制字节(忽略空白): + + + + Apply Patch + 应用补丁 + + + + Close + 关闭 + + + + Choose file to patch + 选择要修补的文件 + + + + All Files (*) + 所有文件 (*) + + + + + Pick a file + 选择文件 + + + + + Choose a file first. + 请先选择一个文件。 + + + + + Bad offset + 偏移无效 + + + + + Enter a non-negative offset: 0x-prefixed hex or decimal. + 请输入非负偏移:带 0x 前缀的十六进制或十进制。 + + + + + Open failed + 打开失败 + + + + + Seek failed + 定位失败 + + + + + Offset is past end of file. + 偏移超出文件末尾。 + + + + Bad hex + 十六进制无效 + + + + Provide at least one hex byte. + 请至少提供一个十六进制字节。 + + + + Write %1 byte to %2 at offset 0x%3? +This modifies the file in place. + 在偏移 0x%3 处向 %2 写入 %1 个字节? +这会直接修改该文件。 + + + + Write %1 bytes to %2 at offset 0x%3? +This modifies the file in place. + 在偏移 0x%3 处向 %2 写入 %1 个字节? +这会直接修改该文件。 + + + + Confirm patch + 确认补丁 + + + + Extend file? + 扩展文件? + + + + The patch ends at offset 0x%1 but the file is only 0x%2 bytes. +Applying it will grow the file (zero-filling the gap). Continue? + 补丁结束于偏移 0x%1,但文件只有 0x%2 个字节。 +应用后文件会变长(空隙以零填充)。是否继续? + + + + Write failed + 写入失败 + + + + Patched + 已修补 + + + + Wrote %1 bytes at 0x%2. + 已在 0x%2 处写入 %1 个字节。 + + + + ce::gui::FindStaticsWindow + + + Find Statics + 查找静态地址 + + + + Module: + 模块: + + + + Scan + 扫描 + + + + Pick a module and click Scan. + 选择一个模块并点击“扫描”。 + + + + Static address + 静态地址 + + + + Symbol + 符号 + + + + References + 引用次数 + + + + Static addresses in the module's data, and the code that references them, appear here. + 模块数据中的静态地址,以及引用它们的代码,会显示在这里。 + + + + Scanning… + 正在扫描… + + + + Done: %1 static addresses found. + 完成:找到 %1 个静态地址。 + + + + ce::gui::FormDesigner + + + Form Designer + 窗体设计器 + + + + <b>Palette</b> + <b>控件面板</b> + + + + Button + 按钮 + + + + Label + 标签 + + + + Edit + 编辑框 + + + + CheckBox + 复选框 + + + + Memo + 多行文本 + + + + GroupBox + 分组框 + + + + Panel + 面板 + + + + Add to form + 添加到窗体 + + + + <b>Items</b> + <b>项目</b> + + + + Delete + 删除 + + + + <b>Canvas</b>: click a widget on the canvas or in the Items list to select it + <b>画布</b>:单击画布上的控件,或在“项目”列表中单击,以选中它 + + + + <b>Generated Lua</b> + <b>生成的 Lua</b> + + + + Refresh + 刷新 + + + + <b>Form</b> + <b>窗体</b> + + + + + Width: + 宽度: + + + + + Height: + 高度: + + + + <b>Selected widget</b> + <b>选中的控件</b> + + + + Name: + 名称: + + + + Caption: + 标题: + + + + X: + X: + + + + Y: + Y: + + + + Save layout (.json)… + 保存布局 (.json)… + + + + Load layout (.json)… + 加载布局 (.json)… + + + + Copy Lua to clipboard + 复制 Lua 到剪贴板 + + + + Save form layout + 保存窗体布局 + + + + + JSON (*.json) + JSON 文件 (*.json) + + + + Save failed + 保存失败 + + + + Load form layout + 加载窗体布局 + + + + + Load failed + 加载失败 + + + + Not a valid form layout file. + 不是有效的窗体布局文件。 + + + + ce::gui::GraphicalMemoryView + + + Graphical Memory View + 图形化内存视图 + + + + Address: + 地址: + + + + address (hex) + 地址(十六进制) + + + + Fetch + 读取 + + + + Read and visualize memory at this address + 读取并可视化此地址的内存 + + + + Pixels per line: + 每行像素数: + + + + Rows: + 行数: + + + + No memory loaded. + 未加载内存。 + + + + Enter a valid hexadecimal address. + 请输入有效的十六进制地址。 + + + + Could not read memory at 0x%1. + 无法读取 0x%1 处的内存。 + + + + Showing %1 bytes from 0x%2; the memory after 0x%3 is not readable. + 显示从 0x%2 开始的 %1 字节;0x%3 之后的内存不可读。 + + + + Showing %1 bytes from 0x%2, one byte per pixel (black = 00, white = FF). + 显示从 0x%2 开始的 %1 字节,每个字节对应一个像素(黑色 = 00,白色 = FF)。 + + + + ce::gui::GuestScanDialog + + + Emulator guest scan + 模拟器客户机扫描 + + + + <b>No emulator guest RAM detected.</b> Attach to a recognized emulator (Dolphin, PCSX2, RPCS3, DuckStation, …) first. + <b>未检测到模拟器的客户机内存。</b>请先附加到受支持的模拟器(Dolphin、PCSX2、RPCS3、DuckStation 等)。 + + + + <b>%1</b> guest memory. Scan by value, or Unknown Scan then narrow by how the value changed. Enable big-endian for PS3 / Wii / GameCube. + <b>%1</b> 客户机内存。按数值扫描,或先执行未知扫描,再根据数值的变化方式缩小范围。PS3 / Wii / GameCube 请启用大端序。 + + + + Emulator + 模拟器 + + + + Guest RAM region: + 客户机内存区域: + + + + Byte + 字节 + + + + 2 Bytes + 2 字节 + + + + 4 Bytes + 4 字节 + + + + 8 Bytes + 8 字节 + + + + Float + 浮点数 + + + + Double + 双精度浮点数 + + + + Value type: + 数值类型: + + + + value to find (decimal or 0x…) + 要查找的数值(十进制或 0x…) + + + + Value: + 数值: + + + + Big-endian (PS3 / Wii / GameCube) + 大端序(PS3 / Wii / GameCube) + + + + First Scan + 首次扫描 + + + + Unknown Scan + 未知扫描 + + + + Next Scan + 再次扫描 + + + + New Scan + 新扫描 + + + + Changed + 有变化 + + + + Unchanged + 无变化 + + + + Increased + 增大 + + + + Decreased + 减小 + + + + Add to Address List + 添加到地址列表 + + + + + Ready. + 就绪。 + + + + Guest address + 客户机地址 + + + + Host address + 主机地址 + + + + Value + 数值 + + + + Guest RAM scan results appear here. + 客户机内存扫描结果将显示在这里。 + + + + + Enter a value that fits the selected type. + 请输入符合所选类型的数值。 + + + + %1 result(s) (showing first %2) + %1 个结果(仅显示前 %2 个) + + + + %1 result(s) + %1 个结果 + + + + . The scan stopped at %1 results; try a less common value. + 。扫描在 %1 个结果处停止;请尝试更常见的数值。 + + + + Snapshot taken (%1 MB). Change the value in-game, then narrow with Changed / Increased / Decreased / Unchanged. + 已生成快照(%1 MB)。请在游戏中改变数值,然后用“有变化 / 增大 / 减小 / 无变化”缩小范围。 + + + + Only the first %1 MB of the %2 MB region is covered. + 仅覆盖 %2 MB 区域中的前 %1 MB。 + + + + guest 0x%1 + 客户机 0x%1 + + + + ce::gui::HeapRegionsWindow + + + Heap Regions + 堆区域 + + + + Start + 起始 + + + + End + 结束 + + + + Size + 大小 + + + + Perm + 权限 + + + + Kind + 类型 + + + + Path + 路径 + + + + Double-click a region to browse it + 双击区域即可浏览 + + + + %1 bytes (0x%2) + %1 字节 (0x%2) + + + + Heap Regions: %1 regions, %2 + 堆区域:%1 个区域,%2 + + + + ce::gui::HexView + + + Address: 0x%1 + 地址:0x%1 + + + + Copy address + 复制地址 + + + + Copy byte (0x%1) + 复制字节 (0x%1) + + + + Copy selection as AOB (%1 bytes) + 将所选内容复制为 AOB(%1 字节) + + + + Copy selection (hex, no spaces) + 复制所选内容(十六进制,无空格) + + + + Fill selection (%1 bytes) with… + 将所选内容(%1 字节)填充为… + + + + Copy 16 bytes as AOB + 复制 16 字节为 AOB + + + + Goto… + 转到… + + + + Follow pointer here (qword) + 跟随此处的指针(qword) + + + + Add address to the list + 将地址添加到列表 + + + + Paste %1 bytes here + 在此粘贴 %1 字节 + + + + Find what writes this address + 查找写入此地址的内容 + + + + Find what accesses this address + 查找访问此地址的内容 + + + + Bytes per row + 每行字节数 + + + + Display type + 显示类型 + + + + Byte + 字节 + + + + Word (2) + 字(2 字节) + + + + Dword (4) + 双字(4 字节) + + + + Qword (8) + 四字(8 字节) + + + + Float + 浮点数 + + + + Double + 双精度浮点数 + + + + Fill selection + 填充所选内容 + + + + Fill byte (hex): + 填充字节(十六进制): + + + + ce::gui::LuaConsole + + + Lua Console + Lua 控制台 + + + + + -- Cheat Engine Lua Console -- + -- Cheat Engine Lua 控制台 -- + + + + -- Functions: readInteger(addr), writeInteger(addr, val), getModuleBase(name), ... + -- 函数:readInteger(addr), writeInteger(addr, val), getModuleBase(name), ... + + + + Enter Lua code, then press Run or Enter… + 输入 Lua 代码,然后按“运行”或回车… + + + + Run + 运行 + + + + Execute the Lua code (Enter) + 执行 Lua 代码(回车) + + + + Clear + 清除 + + + + Clear the console output + 清除控制台输出 + + + + ERROR: %1 + 错误:%1 + + + + ce::gui::MainWindow + + + Select file + 选择文件 + + + + Select file for %1 + 选择 %1 的文件 + + + + + GDB target disconnected or exited + GDB 目标已断开或已退出 + + + + + ceserver connection lost + ceserver 连接已丢失 + + + + Connect again with File > Connect to ceserver. + 请使用“文件 > 连接到 ceserver”重新连接。 + + + + Process %1 has exited + 进程 %1 已退出 + + + + Open another process with Ctrl+O. + 使用 Ctrl+O 打开另一个进程。 + + + + Target process exited + 目标进程已退出 + + + + &File + 文件(&F) + + + + Clear scan results + 清除扫描结果 + + + + + Open Process... + 打开进程... + + + + + + + + + Save + 保存 + + + + Save As... + 另存为... + + + + Load... + 加载... + + + + Load Recent + 最近加载 + + + + Save scan results... + 保存扫描结果... + + + + Generate Trainer Lua Script... + 生成修改器 Lua 脚本... + + + + Connect to ceserver... + 连接到 ceserver... + + + + Connect to GDB / QEMU... + 连接到 GDB / QEMU... + + + + Disconnect target + 断开目标连接 + + + + Quit + 退出 + + + + &Edit + 编辑(&E) + + + + &Process + 进程(&P) + + + + Table + 表格 + + + + &Tools + 工具(&T) + + + + .Net + .Net + + + + + Mono dissector... + Mono 反汇编器... + + + + Show Cheat Table Lua Script + 显示表格 Lua 脚本 + + + + Create Form... + 创建窗体... + + + + Memory Viewer... + 内存查看器... + + + + + AI Assistant + AI 助手 + + + + + + + Auto Assemble... + 自动汇编... + + + + + + Add Address + 添加地址 + + + + Add Address... + 添加地址... + + + + + Add Group + 添加分组 + + + + Enable all entries + 启用所有条目 + + + + Disable all entries + 禁用所有条目 + + + + Settings... + 设置... + + + + Create Process... + 创建进程... + + + + Open file as process + 作为进程打开文件 + + + + Executables (*) + 可执行文件 (*) + + + + Create Process + 创建进程 + + + + fork() failed. + fork() 失败。 + + + + Process/System Info + 进程/系统信息 + + + + Process and System Info + 进程与系统信息 + + + + Process + 进程 + + + + No process is open. Use File > Open Process. + 当前没有打开的进程。请使用“文件 > 打开进程”。 + + + + Name: + 名称: + + + + + PID: + PID: + + + + Executable: + 可执行文件: + + + + (unknown) + (未知) + + + + Kind: + 类型: + + + + Program: + 程序: + + + + %1, %2-byte pointers, %3 + %1,%2 字节指针,%3 + + + + Modules: + 模块: + + + + Threads: + 线程: + + + + System + 系统 + + + + Kernel: + 内核: + + + + CPUs: + CPU: + + + + not present (Yama disabled) + 不存在(Yama 已禁用) + + + + ptrace scope: + ptrace 范围: + + + + Qt: + Qt: + + + + Pause the process + 暂停进程 + + + + Pause requires a live local process target + 暂停需要活动的本地进程目标 + + + + Pause target while scanning + 扫描时暂停目标 + + + + Suspend the target during each First/Next Scan so its memory is a consistent snapshot, then resume it. + 在每次首次/再次扫描期间挂起目标,使其内存保持一致快照,然后恢复。 + + + + File Patcher... + 文件修补器... + + + + Save Memory Region... + 保存内存区域... + + + + + Save Memory Region + 保存内存区域 + + + + + + + + + + + + Open a process first. + 请先打开一个进程。 + + + + Save a range of the target's memory to a file (up to 64 MiB). Addresses are hexadecimal; To is exclusive. + 将目标内存的一段保存到文件(最多 64 MiB)。地址为十六进制,结束地址不包含在内。 + + + + e.g. 7f3a00000000 + 例如 7f3a00000000 + + + + e.g. 7f3a00001000 + 例如 7f3a00001000 + + + + + From: + 起始地址: + + + + + To: + 结束地址: + + + + "To" must be greater than "From". + “To”必须大于“From”。 + + + + Region too large (max 64 MiB). + 区域过大(最大 64 MiB)。 + + + + Save memory region + 保存内存区域 + + + + + + + The process is no longer open. + 该进程已不再处于打开状态。 + + + + Could not read that region. + 无法读取该区域。 + + + + Could not write %1. + 无法写入 %1。 + + + + + Saved + 已保存 + + + + Wrote %1 to %2. + 已将 %1 写入 %2。 + + + + Memory Allocations... + 内存分配... + + + + + Memory Allocations + 内存分配 + + + + Memory reserved in the target from here (readable, writable and executable). Double-click a block to browse it. + 从这里开始在目标中保留的内存(可读、可写、可执行)。双击某个块即可浏览它。 + + + + No blocks allocated yet. +Click Allocate to reserve memory in the target. + 尚未分配任何块。 +点击“分配”以在目标中保留内存。 + + + + + + Allocate + 分配 + + + + + Free + 释放 + + + + Free the selected block + 释放选中的块 + + + + + + + + + + + Close + 关闭 + + + + Size (bytes): + 大小(字节): + + + + Could not allocate %1 in the target: %2 + 无法在目标中分配 %1:%2 + + + + Could not free the block: %1 + 无法释放该块:%1 + + + + Generate Trainer (executable)... + 生成修改器(可执行文件)... + + + + Generate Trainer executable + 生成修改器可执行文件 + + + + Trainer executable written to %1 + 修改器可执行文件已写入 %1 + + + + + Generate Trainer + 生成修改器 + + + + Structure Spider... + 结构搜索器... + + + + + + + Structure Spider + 结构搜索器 + + + + Address of the value to find pointer paths to (hex): + 要查找指针路径的数值地址(十六进制): + + + + "%1" is not a hexadecimal address. + “%1”不是十六进制地址。 + + + + Structure spider + 结构搜索器 + + + + Searching for pointer paths to %1... + 正在搜索指向 %1 的指针路径... + + + + unexpected error + 意外错误 + + + + Dissect structure at target + 在目标处解析结构 + + + + + + Cancel + 取消 + + + + Cancelling... + 正在取消... + + + + Searching for pointer paths to %1... %2% + 正在搜索指向 %1 的指针路径... %2% + + + + The pointer scan failed: %1 + 指针扫描失败:%1 + + + + Cancelled. + 已取消。 + + + + %1 pointer path(s) to %2: + %1 条指向 %2 的指针路径: + + + + (the scan stopped at a limit, so some paths may be missing) + (扫描因达到上限而停止,因此可能缺少部分路径) + + + + Group Scan Generator... + 分组扫描生成器... + + + + + + Group Scan Generator + 分组扫描生成器 + + + + Run a scan and select results first. + 请先运行扫描并选择结果。 + + + + Select two or more addresses in the scan results first (Ctrl+click or Shift+click). The generator builds a pattern from their offsets and values. + 请先在扫描结果中选择两个或更多地址(Ctrl+单击或 Shift+单击)。生成器会根据它们的偏移和数值构建模式。 + + + + Grouped-scan pattern (offset:value, 4-byte): + 分组扫描模式(偏移:数值,4 字节): + + + + + Copy + 复制 + + + + Save Disassembly... + 保存反汇编... + + + + + + + + + + Save Disassembly + 保存反汇编 + + + + From (hex): + 起始地址(十六进制): + + + + To (hex): + 结束地址(十六进制): + + + + Invalid or too-large range. + 范围无效或过大。 + + + + Could not read that range. + 无法读取该范围。 + + + + Save disassembly + 保存反汇编 + + + + Wrote %1 instructions to %2. + 已将 %1 条指令写入 %2。 + + + + Wait for process... + 等待进程... + + + + Wait for process + 等待进程 + + + + Name (or part of the name) of the program to wait for. +Cheat Engine opens it as soon as it starts: + 要等待的程序名称(或名称的一部分)。 +Cheat Engine 会在它启动时立即打开它: + + + + Watching for '%1'… + 正在监视“%1”… + + + + Snapshot + 快照 + + + + Capture writable regions + 捕获可写区域 + + + + + + + + No process + 没有进程 + + + + Open a process before capturing a snapshot. + 请先打开进程再捕获快照。 + + + + Snapshot failed + 快照失败 + + + + Captured %1 region(s), %2 total. + 已捕获 %1 个区域,共 %2。 + + + + + +The snapshot stopped at its %1 limit: writable memory after that point is not included. + + +快照在达到 %1 上限时停止:该点之后的可写内存不包含在内。 + + + + Snapshot captured + 已捕获快照 + + + + Diff against current + 与当前比较 + + + + + Need both + 需要两者 + + + + Capture a baseline snapshot first, then open a process. + 请先捕获基线快照,然后打开进程。 + + + + Diff failed + 比较失败 + + + + Diff + 比较 + + + + No byte-level differences detected. + 未检测到字节级差异。 + + + + 1 byte changed since the baseline. + + + 1 个字节自基线以来发生了变化。 + + + + + + %1 bytes changed since the baseline. + + + %1 个字节自基线以来发生了变化。 + + + + + + +(%1 more not shown.) + +(另有 %1 条未显示。) + + + + Snapshot Diff + 快照比较 + + + + Restore baseline + 恢复基线 + + + + Capture a baseline snapshot first. + 请先捕获基线快照。 + + + + Restore? + 恢复? + + + + Write the baseline back into the process? This overwrites the captured memory that is still mapped writable (%1 regions, %2). + 要将基线写回进程吗?这会覆盖仍映射为可写的已捕获内存(%1 个区域,%2)。 + + + + Restore + 恢复 + + + + Restore failed + 恢复失败 + + + + Wrote %1 bytes back into the process. + 已将 %1 字节写回进程。 + + + + + +%1 bytes were skipped: that memory is no longer mapped, or is now read-only or executable. + + +跳过了 %1 字节:该内存已不再映射,或现在为只读或可执行。 + + + + Restored + 已恢复 + + + + Save to file... + 保存到文件... + + + + No snapshot + 没有快照 + + + + Capture a snapshot first. + 请先捕获快照。 + + + + Save snapshot + 保存快照 + + + + + Snapshot (*.cesnap);;All (*) + 快照 (*.cesnap);;所有文件 (*) + + + + Save failed + 保存失败 + + + + Could not write %1 + 无法写入 %1 + + + + Load from file... + 从文件加载... + + + + Load snapshot + 加载快照 + + + + Load failed + 加载失败 + + + + Snapshot loaded + 快照已加载 + + + + Loaded %1 region(s), %2 bytes. + 已加载 %1 个区域,%2 字节。 + + + + Overlay... + 覆盖层... + + + + Speedhack... + 变速器... + + + + Speedhack + 变速器 + + + + Scales the time the game sees. Apply loads the speedhack library into the process the first time. + 缩放游戏所感受到的时间。首次应用时会把变速库加载到进程中。 + + + + Speed + 速度 + + + + Not applied yet. + 尚未应用。 + + + + Apply + 应用 + + + + The speedhack works on local processes only. + 变速器仅适用于本地进程。 + + + + Inject failed: libspeedhack.so is not installed next to Cheat Engine. + 注入失败:libspeedhack.so 未安装在 Cheat Engine 旁边。 + + + + Inject failed: %1 + 注入失败:%1 + + + + Active: the game runs at %1×. + 已激活:游戏以 %1× 速度运行。 + + + + &Help + 帮助(&H) + + + + Cheat Engine Help + Cheat Engine 帮助 + + + + Cheat Engine for Linux: Help + Cheat Engine for Linux:帮助 + + + + Lua documentation + Lua 文档 + + + + Lua / Scripting documentation + Lua / 脚本文档 + + + + Tutorial not found + 未找到教程 + + + + Could not start %1. It is installed next to cheatengine. + 无法启动 %1。它安装在 cheatengine 旁边。 + + + + Cheat Engine Tutorial... + Cheat Engine 教程... + + + + Graphical Tutorial... + 图形教程... + + + + About + 关于 + + + + About Cheat Engine for Linux + 关于 Cheat Engine for Linux + + + + <h2 style='margin-bottom:2px'>Cheat Engine for Linux</h2><p style='margin-top:0'>Version %1</p><p>A memory scanner, debugger and code injection tool for native Linux, Wine and Proton programs.</p><p>Built with Qt %2, Capstone, Keystone and Lua 5.3.<br>Free and open source under the MIT license.</p><p><a href='https://github.com/wleeaf/cheat-engine-linux'>github.com/wleeaf/cheat-engine-linux</a></p> + <h2 style='margin-bottom:2px'>Cheat Engine for Linux</h2><p style='margin-top:0'>版本 %1</p><p>适用于原生 Linux、Wine 和 Proton 程序的内存扫描、调试与代码注入工具。</p><p>基于 Qt %2、Capstone、Keystone 和 Lua 5.3 构建。<br>以 MIT 许可证免费开源。</p><p><a href='https://github.com/wleeaf/cheat-engine-linux'>github.com/wleeaf/cheat-engine-linux</a></p> + + + + Open on GitHub + 在 GitHub 上打开 + + + + Table Comments + 表格注释 + + + + Notes about this cheat table: how to use it, what each entry does, which game version it was made for. They are saved with the table. + 关于此作弊表格的备注:如何使用它、每个条目的作用,以及它是为哪个游戏版本制作的。这些备注会随表格一起保存。 + + + + Cheat Table Lua Script + 表格 Lua 脚本 + + + + -- Lua that runs when this table is opened; saved with the table. + -- 打开此表格时运行的 Lua;随表格一起保存。 + + + + Execute script + 执行脚本 + + + + Script executed. + 脚本已执行。 + + + + Error: %1 + 错误:%1 + + + + Overlay + 覆盖层 + + + + A transparent, click-through layer drawn over the whole screen, for use with a game in borderless or windowed mode. + 绘制在整个屏幕上的透明、可穿透点击的图层,用于无边框或窗口模式下的游戏。 + + + + Status text (process, active cheats, frame rate) + 状态文字(进程、已启用的作弊、帧率) + + + + Crosshair in the centre of the screen + 屏幕中央的准星 + + + + Show Overlay + 显示覆盖层 + + + + Hide Overlay + 隐藏覆盖层 + + + + %1 | Records %2/%3 active + %1 | 记录 %2/%3 已启用 + + + + Open process + 打开进程 + + + + + No process selected + 未选择进程 + + + + + Press Ctrl+O or click the magnifier to choose a process. + 按 Ctrl+O 或点击放大镜选择进程。 + + + + No results yet. + +Enter a value and press First Scan, +or open a process with Ctrl+O. + 还没有结果。 + +输入数值后按“首次扫描”, +或用 Ctrl+O 打开进程。 + + + + No results yet. + +Enter a value and press First Scan. + 还没有结果。 + +输入数值后按“首次扫描”。 + + + + No results yet. + +Open a process with Ctrl+O, +then enter a value and press First Scan. + 还没有结果。 + +用 Ctrl+O 打开进程, +然后输入数值并按“首次扫描”。 + + + + Add %1 selected addresses to the address list + 将选中的 %1 个地址添加到地址列表 + + + + Add selected address to the address list + 将选中的地址添加到地址列表 + + + + + Browse this memory region + 浏览此内存区域 + + + + Disassemble this memory region + 反汇编此内存区域 + + + + Copy %1 addresses + 复制 %1 个地址 + + + + Copy address + 复制地址 + + + + Remove %1 selected addresses from the results + 从结果中移除选中的 %1 个地址 + + + + Remove selected address from the results + 从结果中移除选中的地址 + + + + Find out what accesses this address + 查找访问此地址的内容 + + + + Find out what writes to this address + 查找写入此地址的内容 + + + + Remove from results + 从结果中移除 + + + + + + + + Found: 0 + 已找到:0 + + + + + Hex + 十六进制 + + + + Show result values in hexadecimal + 以十六进制显示结果数值 + + + + Memory View + 内存视图 + + + + Browse the selected process's memory + 浏览所选进程的内存 + + + + Add address + 添加地址 + + + + Manual entry + 手动输入 + + + + Could not resolve "%1". + 无法解析“%1”。 + + + + Value: + 数值: + + + + and + 和 + + + + Interpret integer scan values as hexadecimal + 将整数扫描数值按十六进制解释 + + + + Exact Value + 精确数值 + + + + Bigger than... + 大于... + + + + Smaller than... + 小于... + + + + Value between... + 数值介于... + + + + Unknown initial value + 未知初始值 + + + + Increased value + 增加的数值 + + + + Decreased value + 减少的数值 + + + + Changed value + 变化的数值 + + + + Unchanged value + 未变化的数值 + + + + Same as first scan + 与首次扫描相同 + + + + Increased value by... + 数值增加了... + + + + Decreased value by... + 数值减少了... + + + + + Byte + 字节 + + + + + 2 Bytes + 2 字节 + + + + + 4 Bytes + 4 字节 + + + + + 8 Bytes + 8 字节 + + + + + Float + 浮点数 + + + + + Double + 双精度浮点数 + + + + String + 字符串 + + + + Unicode String + Unicode 字符串 + + + + Array of byte + 字节数组 + + + + Binary + 二进制 + + + + All + 全部 + + + + + Pointer + 指针 + + + + Grouped + 分组 + + + + Custom + 自定义 + + + + Exact uses stored precision. Rounded uses the precision of your input. Truncated compares integer parts. Extreme uses a tolerance. + 精确使用存储的精度。取整使用输入值的精度。截断比较整数部分。极值使用容差。 + + + + Exact + 精确 + + + + Rounded + 取整 + + + + Truncated + 截断 + + + + Extreme + 极值 + + + + Absolute difference allowed in Extreme mode. Leave blank or use 0 for automatic tolerance. + 极值模式下允许的绝对差值。留空或使用 0 表示自动容差。 + + + + Tolerance + 容差 + + + + + e.g. 100 + 例如 100 + + + + e.g. 3.14 + 例如 3.14 + + + + text to find + 要查找的文本 + + + + e.g. 48 8B ?? 05 + 例如 48 8B ?? 05 + + + + e.g. 0110??01 + 例如 0110??01 + + + + e.g. 0x7fabc000 + 例如 0x7fabc000 + + + + + First Scan + 首次扫描 + + + + Next Scan + 再次扫描 + + + + Undo Scan + 撤销扫描 + + + + Memory Scan Options + 内存扫描选项 + + + + Writable + 可写 + + + + Checked: only writable regions. Unchecked: only non-writable. Grey: any (don't care). + 选中:仅可写区域。未选中:仅不可写区域。灰色:任意(不关心)。 + + + + Executable + 可执行 + + + + Checked: only executable regions. Unchecked: only non-executable. Grey: any (don't care). + 选中:仅可执行区域。未选中:仅不可执行区域。灰色:任意(不关心)。 + + + + Fast Scan + 快速扫描 + + + + Compare by % + 按百分比比较 + + + + Percent max: + 最大百分比: + + + + Case sensitive (text) + 区分大小写(文本) + + + + Private + 私有 + + + + Scan MEM_PRIVATE regions (anonymous heap, stack, private maps) + 扫描 MEM_PRIVATE 区域(匿名堆、栈、私有映射) + + + + Image + 映像 + + + + Scan MEM_IMAGE regions (loaded executable modules) + 扫描 MEM_IMAGE 区域(已加载的可执行模块) + + + + Mapped + 映射 + + + + Scan MEM_MAPPED regions (other file-backed maps) + 扫描 MEM_MAPPED 区域(其他文件映射) + + + + Text encoding: + 文本编码: + + + + Data order: + 数据字节序: + + + + + + + Auto (target) + 自动(目标) + + + + + Little-endian + 小端序 + + + + + Big-endian + 大端序 + + + + Choose the encoding of the data being scanned. Auto uses the selected target's data order. + 选择被扫描数据的字节序。自动使用所选目标的数据字节序。 + + + + Pointer size: + 指针大小: + + + + + 4 bytes + 4 字节 + + + + + 8 bytes + 8 字节 + + + + Size of encoded pointers for Pointer, Grouped and Custom scans. A different saved record size requires a New Scan. + 指针、分组和自定义扫描所用的指针大小。不同的已保存记录大小需要新扫描。 + + + + No saved addresses. +Double-click a result or use Add Address. + 没有已保存的地址。 +双击结果或使用“添加地址”。 + + + + Value at 0x%1 was reverted (wrote %2, now %3). It looks protected: right-click the entry and choose "Find what writes" to locate the code that overwrites it. + 0x%1 处的数值已被回退(写入 %2,现在为 %3)。它似乎受到了保护:右键单击该条目并选择“查找什么写入”来定位覆盖它的代码。 + + + + Edit script... + 编辑脚本... + + + + Change address... + 更改地址... + + + + Cut + 剪切 + + + + + Paste + 粘贴 + + + + Copy field + 复制字段 + + + + Description + 描述 + + + + + Address + 地址 + + + + Value + 数值 + + + + Script + 脚本 + + + + Delete + 删除 + + + + Arrange + 排列 + + + + Indent + 增加缩进 + + + + Outdent + 减少缩进 + + + + Add to new group... + 添加到新分组... + + + + + + New group + 新建分组 + + + + Group name: + 分组名称: + + + + Move Up + 上移 + + + + Move Down + 下移 + + + + Set value... + 设置数值... + + + + New value for 1 selected entry: + 为 1 个选中条目设置新数值: + + + + New value for %1 selected entries: + 为 %1 个选中条目设置新数值: + + + + Set value + 设置数值 + + + + Freeze Mode + 冻结模式 + + + + Normal + 常规 + + + + Increase Only + 仅增加 + + + + Decrease Only + 仅减少 + + + + Never Increase + 不增加 + + + + Never Decrease + 不减少 + + + + Display + 显示 + + + + Set value codec... + 设置数值编解码器... + + + + Value codec + 数值编解码器 + + + + How the game obfuscates this value in memory: +none, xor:0xKEY, add:N, rol:N or ror:N (e.g. xor:0x5A). + +The list shows the decoded value; edits and freezes are encoded back. + 游戏如何在内存中混淆此数值: +无、xor:0xKEY、add:N、rol:N 或 ror:N(例如 xor:0x5A)。 + +列表显示解码后的数值;编辑和冻结会重新编码。 + + + + Invalid codec + 编解码器无效 + + + + Use none, xor:0xKEY, add:N, rol:N, or ror:N. + 请使用 none、xor:0xKEY、add:N、rol:N 或 ror:N。 + + + + Data order + 数据字节序 + + + + Pointer size + 指针大小 + + + + Change type + 更改类型 + + + + Show as hexadecimal + 以十六进制显示 + + + + Convert to module+offset + 转换为模块+偏移 + + + + (%1 skipped) + (跳过 %1 个) + + + + Converted 1 address to module+offset%1 + 已将 1 个地址转换为模块+偏移%1 + + + + Converted %1 addresses to module+offset%2 + 已将 %1 个地址转换为模块+偏移%2 + + + + Find what accesses this address + 查找访问此地址的内容 + + + + Find what writes to this address + 查找写入此地址的内容 + + + + Pointer scan for this address + 对此地址进行指针扫描 + + + + Change Color... + 更改颜色... + + + + Change record color + 更改记录颜色 + + + + Dropdown options... + 下拉选项... + + + + Dropdown options + 下拉选项 + + + + The values this entry can be set to from a dropdown, one per line, +as value:label or just value. For example: +0:Off +1:On + 此条目可从下拉列表设置的数值,每行一个, +格式为 数值:标签 或仅 数值。例如: +0:关 +1:开 + + + + Hotkeys + 快捷键 + + + + Toggle hotkey... + 切换快捷键... + + + + Configure Hotkey + 配置快捷键 + + + + Clear + 清除 + + + + Press the key combination that turns this entry on and off. It works while the game has focus. + 按下可开启和关闭此条目的按键组合。它在游戏获得焦点时也有效。 + + + + Value hotkeys... + 数值快捷键... + + + + Configure Value Hotkeys + 配置数值快捷键 + + + + value to set + 要设置的数值 + + + + Increase + 增加 + + + + Decrease + 减少 + + + + Step + 步长 + + + + Set value hotkey + 设置数值快捷键 + + + + Set to value + 设置为数值 + + + + Add Address Manually... + 手动添加地址... + + + + + + No description + 无描述 + + + + Enable all + 全部启用 + + + + Disable all + 全部禁用 + + + + Scan Value + 扫描数值 + + + + Scan Type + 扫描类型 + + + + Value Type + 数值类型 + + + + Rounding + 取整方式 + + + + Advanced Options + 高级选项 + + + + Table Extras + 表格附加项 + + + + Auto-attached to %1 (pid %2) + 已自动附加到 %1(PID %2) + + + + Found %1 (pid %2) but could not open it + 找到 %1(PID %2),但无法打开 + + + + Attached to %1: %2 + 已附加到 %1:%2 + + + + + +Program: %1, %2-byte pointers, %3 +Host ABI: %4 + + +程序:%1,%2 字节指针,%3 +主机 ABI:%4 + + + + Target already being traced + 目标已被跟踪 + + + + data order unknown + 数据字节序未知 + + + + %1 data + %1 数据 + + + + %1-bit pointers + %1 位指针 + + + + pointer width unknown + 指针宽度未知 + + + + GDB: %1 + GDB:%1 + + + + Guest CPU: %1 +Data order: %2 +Program pointers: %3 +Transport: GDB + 客户机 CPU:%1 +数据字节序:%2 +程序指针:%3 +传输方式:GDB + + + + Connected to GDB; no memory map. Reconnect with a known scan range. + 已连接到 GDB;没有内存映射。请使用已知的扫描范围重新连接。 + + + + Connected to GDB target + 已连接到 GDB 目标 + + + + Connect failed: %1 + 连接失败:%1 + + + + Handshake failed: %1 + 握手失败:%1 + + + + Server refused PID %1 + 服务器拒绝了 PID %1 + + + + The current target is still in use: wait for the scan to finish or disable its scripts, then connect again. + 当前目标仍在使用中:请等待扫描结束或禁用其脚本,然后再次连接。 + + + + Remote process %1 + 远程进程 %1 + + + + Connect to ceserver + 连接到 ceserver + + + + Attach to a process on another machine (or in a container) through a running ceserver. + 通过运行中的 ceserver 附加到另一台机器(或容器)上的进程。 + + + + Host: + 主机: + + + + Port: + 端口: + + + + process ID on the remote machine + 远程机器上的进程 ID + + + + Connect + 连接 + + + + Enter a valid remote PID. + 请输入有效的远程 PID。 + + + + Connecting to %1:%2... + 正在连接到 %1:%2... + + + + New scan: pick a value type and value, then First Scan. + 新扫描:选择数值类型和数值,然后执行首次扫描。 + + + + + No process attached; open one first (Ctrl+O) to scan. + 未附加进程;请先打开一个进程(Ctrl+O)再扫描。 + + + + Cannot read the process + 无法读取进程 + + + + + Scan range + 扫描范围 + + + + From/To must be a hex address, symbol, or module+offset (e.g. target2, target2+0x5000). + 起始/结束地址必须是十六进制地址、符号或模块+偏移(例如 target2、target2+0x5000)。 + + + + The From address must not be greater than the To address. + 起始地址不得大于结束地址。 + + + + + Array of byte scan + 字节数组扫描 + + + + + Enter a valid AOB pattern (e.g. "7F 45 ?? 46"). + 请输入有效的 AOB 模式(例如“7F 45 ?? 46”)。 + + + + + Binary scan + 二进制扫描 + + + + + Enter a valid binary pattern (e.g. "0110??01"). + 请输入有效的二进制模式(例如“0110??01”)。 + + + + + Grouped scan + 分组扫描 + + + + + Floating scan + 浮点扫描 + + + + + + + Enter valid numeric values for the selected comparison. + 请为所选比较输入有效的数值。 + + + + + All scan + 全部扫描 + + + + + Floating tolerance + 浮点容差 + + + + + Enter a finite nonnegative tolerance. + 请输入有限的非负容差。 + + + + + + Scan failed + 扫描失败 + + + + The scan could not start: %1 + 扫描无法开始:%1 + + + + The scan could not complete: %1 + +Try narrowing the From/To range, enabling Fast Scan, or choosing a more specific value type. + 扫描无法完成:%1 + +请尝试缩小起始/结束范围、启用快速扫描,或选择更具体的数值类型。 + + + + + Scan results truncated + 扫描结果被截断 + + + + + A scan-result file could not be fully written (the disk may be full). Some results are unreliable; free space and scan again. + 扫描结果文件无法完整写入(磁盘可能已满)。部分结果不可靠;请释放空间后重新扫描。 + + + + + Scan results are incomplete. + 扫描结果不完整。 + + + + First scan complete. %1 + 首次扫描完成。%1 + + + + Run a First Scan before a Next Scan. + 请先运行首次扫描,再运行再次扫描。 + + + + The next scan could not complete: %1 + 再次扫描无法完成:%1 + + + + Next scan complete. %1 + 再次扫描完成。%1 + + + + Unsaved changes + 未保存的更改 + + + + The cheat table has changes that are not saved. Save them? + 作弊表格有尚未保存的更改。要保存吗? + + + + Auto Assembler: %1 + 自动汇编:%1 + + + + Save to Table + 保存到表格 + + + + Create Trainer (C source) + 创建修改器(C 源码) + + + + C source (*.c);;All Files (*) + C 源码 (*.c);;所有文件 (*) + + + + + Create Trainer + 创建修改器 + + + + Could not write the trainer source file. + 无法写入修改器源码文件。 + + + + Trainer C source written to %1 (compile with gcc) + 修改器 C 源码已写入 %1(使用 gcc 编译) + + + + Trainer generation failed: +%1 + 修改器生成失败: +%1 + + + + + No scan results to save. + 没有可保存的扫描结果。 + + + + Save all current scan results + 保存所有当前扫描结果 + + + + Text (*.txt);;CSV (*.csv);;All files (*) + 文本 (*.txt);;CSV (*.csv);;所有文件 (*) + + + + Saving all scan results... + 正在保存所有扫描结果... + + + + Scan results changed; export canceled. + 扫描结果已变化;导出已取消。 + + + + Export canceled. + 导出已取消。 + + + + Could not save scan results: %1 + 无法保存扫描结果:%1 + + + + Saved %1 results to %2 + 已将 %1 条结果保存到 %2 + + + + + Save Cheat Table + 保存作弊表格 + + + + Could not save %1: +%2 + 无法保存 %1: +%2 + + + + Invalid table data or incomplete write. + 表格数据无效或写入不完整。 + + + + Invalid table metadata. + 表格元数据无效。 + + + + Saved table to %1 + 已将表格保存到 %1 + + + + Unknown save error. + 未知的保存错误。 + + + + + + + + + + + + + Load Cheat Table + 加载作弊表格 + + + + Execute this lua script? + 执行此 Lua 脚本? + + + + This cheat table contains a Lua script. + +Only execute scripts from tables you trust, a table's Lua can hook and manipulate the target. Execute it? + 此作弊表格包含 Lua 脚本。 + +仅执行来自你信任的表格的脚本,表格中的 Lua 可以挂钩和操控目标。要执行吗? + + + + Table Lua error: %1 + 表格 Lua 错误:%1 + + + + (no recent tables) + (无最近表格) + + + + (missing) + (缺失) + + + + Clear list + 清除列表 + + + + Protected Cheat Table + 受保护的作弊表格 + + + + This table is password-protected (.CETRAINER). +Enter its password: + 此表格受密码保护(.CETRAINER)。 +请输入密码: + + + + Could not decrypt the table (wrong password or corrupt file). + 无法解密表格(密码错误或文件损坏)。 + + + + Could not read a valid cheat table. + 无法读取有效的作弊表格。 + + + + Invalid JSON cheat table. + 无效的 JSON 作弊表格。 + + + + Invalid JSON record. + 无效的 JSON 记录。 + + + + Invalid JSON record data. + 无效的 JSON 记录数据。 + + + + Failed to load table: +%1 + 加载表格失败: +%1 + + + + Failed to load table (unknown error). + 加载表格失败(未知错误)。 + + + + Monitoring the instruction at 0x%1… + 正在监视 0x%1 处的指令… + + + + What this instruction accesses + 此指令访问的内容 + + + + No data accesses were recorded for the instruction at 0x%1. + +It may not have run during the sample window, or it has no memory operand. + 未记录到 0x%1 处指令的数据访问。 + +它可能在采样窗口期间未运行,或者没有内存操作数。 + + + + Addresses accessed by 0x%1 + 被 0x%1 访问的地址 + + + + %1 distinct address(es) accessed (double-click to add to the list): + 访问了 %1 个不同地址(双击可添加到列表): + + + + Hits + 命中次数 + + + + Accessed address + 访问的地址 + + + + %1 address(es) accessed by 0x%2 + %1 个地址被 0x%2 访问 + + + + autorun %1: %2 + 自动运行 %1:%2 + + + + Ran %1 autorun script(s). + 已运行 %1 个自动运行脚本。 + + + + Wine / Proton game + Wine / Proton 游戏 + + + + This is a Wine/Proton (Windows) game. Cheat Engine will watch the game's main thread for writes, which is where gameplay values (money, HP, …) are usually changed. + +If nothing shows up, the value may be written by a background thread; tell me and it can be widened. Start monitoring? + 这是一个 Wine/Proton(Windows)游戏。Cheat Engine 将监视游戏主线程的写入,游戏数值(金钱、生命值等)通常是在那里被修改的。 + +如果没有出现任何结果,该数值可能是由后台线程写入的;告诉我即可扩大监视范围。要开始监视吗? + + + + Code finder unavailable + 代码查找器不可用 + + + + Find what writes + 查找写入 + + + + Find what accesses + 查找访问 + + + + code 0x%1 + 代码 0x%1 + + + + No process attached; open one first (Ctrl+O) to view memory. + 未附加进程;请先打开一个进程(Ctrl+O)再查看内存。 + + + + Add Mono field + 添加 Mono 字段 + + + + %1 for '%2' (field at +0x%3): + %1:%2(字段位于 +0x%3): + + + + Static data address of the class + 类的静态数据地址 + + + + Object base address + 对象基地址 + + + + The debugger could not attach; no breakpoint was set. + 调试器无法附加;未设置断点。 + + + + Pause the target in the debugger to set a breakpoint. + 在调试器中暂停目标以设置断点。 + + + + Execute breakpoints are software breakpoints in this debugger. + 在此调试器中,执行断点是软件断点。 + + + + Could not remove the breakpoint; see the debugger window. + 无法移除断点;请查看调试器窗口。 + + + + Added 0x%1 to the cheat table + 已将 0x%1 添加到作弊表格 + + + + Memory Regions + 内存区域 + + + + Heap list + 堆列表 + + + + Module list + 模块列表 + + + + Referenced strings / functions + 引用的字符串 / 函数 + + + + Thread list + 线程列表 + + + + Stacktrace + 调用栈 + + + + Graphical memory view + 图形内存视图 + + + + Registers + 寄存器 + + + + Breakpoint list + 断点列表 + + + + Full debugger + 完整调试器 + + + + Break and trace + 中断并跟踪 + + + + Branch mapper (LBR) + 分支映射器(LBR) + + + + + Dissect data/structures... + 剖析数据/结构... + + + + Pointer scan... + 指针扫描... + + + + Emulator guest scan... + 模拟器客户机扫描... + + + + Find static addresses... + 查找静态地址... + + + + Lua Engine... + Lua 引擎... + + + + ELF Inspector... + ELF 检查器... + + + + New Scan + 新扫描 + + + + ce::gui::MemPixelView + + + Enter an address and press Fetch to visualize memory. + 输入地址并点击“读取”以可视化内存。 + + + + ce::gui::MemoryBrowser + + + &File + 文件(&F) + + + + New window + 新建窗口 + + + + Save region to file… + 将区域保存到文件… + + + + Load region from file… + 从文件加载区域… + + + + &Search + 搜索(&S) + + + + Find memory... + 查找内存... + + + + Find next + 查找下一个 + + + + Find previous + 查找上一个 + + + + &View + 视图(&V) + + + + Disassembler preferences... + 反汇编器首选项... + + + + &Debug + 调试(&D) + + + + Toggle breakpoint F5 + 切换断点 F5 + + + + + Run + 运行 + + + + Step into + 单步进入 + + + + + Step over + 单步跳过 + + + + + Run to cursor + 运行到光标处 + + + + &Tools + 工具(&T) + + + + Auto Assemble... + 自动汇编... + + + + Dissect data/structures... + 剖析数据/结构... + + + + Memory Viewer + 内存查看器 + + + + Back (Alt+Left) + 后退 (Alt+Left) + + + + Forward (Alt+Right) + 前进 (Alt+Right) + + + + Address: + 地址: + + + + Go + 转到 + + + + Refresh + 刷新 + + + + Toggle bookmark at current address (Ctrl+B) + 在当前地址切换书签 (Ctrl+B) + + + + Bookmarks + 书签 + + + + Debug + 调试 + + + + Toggle BP + 切换断点 + + + + Toggle breakpoint at the selected instruction (F5) + 切换所选指令处的断点 (F5) + + + + Continue the paused target (F9) + 继续已暂停的目标 (F9) + + + + Step + 单步 + + + + Step into one instruction (F7) + 单步进入一条指令 (F7) + + + + Step over one instruction (F8) + 单步跳过一条指令 (F8) + + + + Continue until the selected instruction (F4) + 继续直到所选指令 (F4) + + + + Debugger + 调试器 + + + + Open the Debugger here to run, step, and set breakpoints + 在此打开调试器以运行、单步和设置断点 + + + + Preferences + 首选项 + + + + Disassembler Preferences: font and colors + 反汇编器首选项:字体和颜色 + + + + Registers (populated at a debug breakpoint) + 寄存器(在调试断点处填充) + + + + Address + 地址 + + + + Value + 数值 + + + + Call stack (populated at a debug breakpoint) + 调用栈(在调试断点处填充) + + + + CPU registers && stack panels + CPU 寄存器 && 栈面板 + + + + Code finder unavailable + 代码查找器不可用 + + + + No code-finder launcher is wired to this memory browser instance. + 此内存查看器实例未连接代码查找启动器。 + + + + <unreadable> + <无法读取> + + + + Comment + 注释 + + + + Comment for 0x%1 (blank to clear): + 0x%1 的注释(留空则清除): + + + + Label address + 标签地址 + + + + Name for 0x%1 (blank to clear): + 0x%1 的名称(留空则清除): + + + + Auto Assemble + 自动汇编 + + + + Could not generate a template here. + 无法在此生成模板。 + + + + Goto Address + 转到地址 + + + + Address, symbol, or expression (e.g. worker, target2+0x100): + 地址、符号或表达式(例如 worker、target2+0x100): + + + + Memory Viewer (target exited) + 内存查看器(目标已退出) + + + + Target process exited; this view is frozen. + 目标进程已退出;此视图已冻结。 + + + + (no bookmarks; Ctrl+B to add) + (没有书签;按 Ctrl+B 添加) + + + + Clear all bookmarks + 清除所有书签 + + + + Goto + 转到 + + + + Could not resolve "%1". + 无法解析“%1”。 + + + + A breakpoint is set at 0x%1: remove it before changing these bytes + 0x%1 处设置了断点:更改这些字节前请先移除该断点 + + + + NOP failed + NOP 失败 + + + + Could not write %1 NOP byte to 0x%2. +%3 + 无法将 %1 个 NOP 字节写入 0x%2。 +%3 + + + + Could not write %1 NOP bytes to 0x%2. +%3 + 无法将 %1 个 NOP 字节写入 0x%2。 +%3 + + + + Assemble + 汇编 + + + + Instruction to assemble at 0x%1: + 要在 0x%1 处汇编的指令: + + + + + + + + + Assemble failed + 汇编失败 + + + + Could not assemble "%1": +%2 + 无法汇编“%1”: +%2 + + + + "%1" assembled to no bytes; not writing. + “%1”汇编结果为空,未写入。 + + + + Longer instruction + 指令更长 + + + + The new instruction is %1 bytes but the original was %2. Writing it will overwrite the following instruction(s). Continue? + 新指令为 %1 字节,而原指令为 %2 字节。写入将覆盖后续指令。是否继续? + + + + + Write failed + 写入失败 + + + + Assembled %1 bytes but could not write to 0x%2. +%3 + 已汇编 %1 字节,但无法写入 0x%2。 +%3 + + + + Find in memory + 在内存中查找 + + + + Bytes (e.g. 48 8B ?? 05) or "text": + 字节(例如 48 8B ?? 05)或“文本”: + + + + + Find + 查找 + + + + Enter hex bytes (48 8B 05), a wildcard AOB (48 8B ?? 05), or a quoted "string". + 输入十六进制字节(48 8B 05)、带通配符的 AOB(48 8B ?? 05),或用引号括起的“字符串”。 + + + + Not found searching backward from the current address. + 从当前地址向后查找未找到。 + + + + Not found searching forward from the current address. + 从当前地址向前查找未找到。 + + + + + References + 引用 + + + + This address is not inside a known module, so its references can't be scanned. + 此地址不在已知模块内,因此无法扫描其引用。 + + + + References to 0x%1: %2 found + 对 0x%1 的引用:找到 %2 处 + + + + From + 来源 + + + + Type + 类型 + + + + Instruction + 指令 + + + + call + 调用 + + + + jump + 跳转 + + + + string + 字符串 + + + + data + 数据 + + + + ref + 引用 + + + + + + Save region + 保存区域 + + + + Bytes to save: + 要保存的字节数: + + + + + Binary (*.bin);;All (*) + 二进制文件 (*.bin);;所有文件 (*) + + + + Read failed + 读取失败 + + + + Could not read %1 bytes at 0x%2. + 无法读取 0x%2 处的 %1 字节。 + + + + + Save failed + 保存失败 + + + + Only %1 of %2 bytes at 0x%3 were readable; the file holds those %1 bytes. + 0x%3 处的 %2 字节中只有 %1 字节可读;文件仅包含这 %1 字节。 + + + + Load region into memory + 将区域加载到内存 + + + + Load failed + 加载失败 + + + + File too large + 文件过大 + + + + The file is %1 bytes; the maximum region load is %2 bytes. + 文件为 %1 字节;区域加载上限为 %2 字节。 + + + + + Load region + 加载区域 + + + + Write %1 bytes from +%2 +to 0x%3? + 将 %1 字节从 +%2 +写入 0x%3? + + + + Wrote %1 of %2 bytes at 0x%3 (memory may be protected). + 已在 0x%3 处写入 %2 字节中的 %1 字节(内存可能受保护)。 + + + + ce::gui::MemoryFillDialog + + + + + + + + + Fill Memory + 填充内存 + + + + Start address: + 起始地址: + + + + Size (bytes): + 大小(字节): + + + + Fill byte (hex): + 填充字节(十六进制): + + + + Fill + 填充 + + + + Cancel + 取消 + + + + Enter a valid hexadecimal start address. + 请输入有效的十六进制起始地址。 + + + + Enter a positive size in bytes. + 请输入正的字节大小。 + + + + Fill size too large (max %1 bytes). + 填充大小过大(最多 %1 字节)。 + + + + Enter a hexadecimal byte from 00 to FF. + 请输入 00 到 FF 之间的十六进制字节。 + + + + Filled %1 bytes at 0x%2 with 0x%3 + 已在 0x%2 处用 0x%3 填充 %1 字节 + + + + Only %1 of %2 bytes were written. + 仅写入了 %2 字节中的 %1 字节。 + + + + Write failed. + 写入失败。 + + + + ce::gui::MemoryRegionsWindow + + + Memory Regions + 内存区域 + + + + Start + 起始 + + + + End + 结束 + + + + Size + 大小 + + + + Perm + 权限 + + + + Path + 路径 + + + + Double-click a region to browse it + 双击区域即可浏览 + + + + Filter by address, permissions or path + 按地址、权限或路径筛选 + + + + %1 bytes (0x%2) + %1 字节 (0x%2) + + + + Memory Regions: %1 regions, %2 readable + 内存区域:%1 个区域,%2 可读 + + + + ce::gui::MemviewPreferences + + + Disassembler Preferences + 反汇编器首选项 + + + + Disassembler + 反汇编器 + + + + Change disassembler font + 更改反汇编器字体 + + + + + Normal + 常规 + + + + Color group: + 颜色组: + + + + Default color + 默认颜色 + + + + Register color + 寄存器颜色 + + + + Symbol color + 符号颜色 + + + + Hexadecimal color + 十六进制颜色 + + + + Jumplines + 跳转线 + + + + Conditional jump color + 条件跳转颜色 + + + + Unconditional jump color + 无条件跳转颜色 + + + + Call color + 调用颜色 + + + + Thickness + 粗细 + + + + Spacing + 间距 + + + + Space between lines + 行间距 + + + + Above + 上方 + + + + Below + 下方 + + + + ce::gui::ModuleListWindow + + + Module List + 模块列表 + + + + Base + 基址 + + + + Size + 大小 + + + + Name + 名称 + + + + Path + 路径 + + + + Double-click a module to browse its base address + 双击模块即可浏览其基址 + + + + Filter by name or path + 按名称或路径筛选 + + + + %1 bytes (0x%2) + %1 字节 (0x%2) + + + + Module List: %1 modules + 模块列表:%1 个模块 + + + + ce::gui::MonoDissectorWindow + + + Mono Dissector + Mono 反汇编器 + + + + Dissect (inject agent) + 剖析(注入代理) + + + + Inject the Mono agent and read the target's class/field layout + 注入 Mono 代理并读取目标进程的类/字段布局 + + + + Filter: + 过滤: + + + + class or field name… + 类名或字段名… + + + + Name + 名称 + + + + Offset + 偏移 + + + + Type + 类型 + + + + Click Dissect to load the target's Mono assemblies, classes and fields. + 点击“剖析”以加载目标进程的 Mono 程序集、类和字段。 + + + + Not dissected yet. + 尚未剖析。 + + + + (static) + (静态) + + + + method + 方法 + + + + %1 %2 · %3 classes · %4 fields%5 + %1 个%2 · %3 个类 · %4 个字段%5 + + + + assembly + 程序集 + + + + assemblies + 程序集 + + + + (incomplete%1) + (不完整%1) + + + + No target process. + 没有目标进程。 + + + + IL2CPP target: locating metadata + GameAssembly… + IL2CPP 目标:正在定位元数据 + GameAssembly… + + + + IL2CPP: %1 + IL2CPP:%1 + + + + No Mono runtime detected in this process. + 此进程中未检测到 Mono 运行时。 + + + + libcecore_mono_agent.so not found next to the binary. + 未在二进制文件旁找到 libcecore_mono_agent.so。 + + + + Injecting agent and reading Mono layout… + 正在注入代理并读取 Mono 布局… + + + + Agent injection failed (see CE_LOG=general:debug). + 代理注入失败(请设置 CE_LOG=general:debug 查看详情)。 + + + + ce::gui::OverlayWindow + + + CE Overlay + CE 覆盖层 + + + + ce::gui::PointerScanDialog + + + Pointer Scanner + 指针扫描器 + + + + Target Address: + 目标地址: + + + + Depth: + 深度: + + + + Max Offset: + 最大偏移: + + + + + Scan + 扫描 + + + + Rescan… + 重新扫描… + + + + Filter current paths to those that still point at a new target address + 将当前路径筛选为仍指向新目标地址的路径 + + + + Save… + 保存… + + + + Load… + 加载… + + + + Ready + 就绪 + + + + Path + 路径 + + + + Current Address + 当前地址 + + + + Value + 数值 + + + + Enter the address of a value and click Scan to find pointer paths that lead to it. + 输入数值的地址并点击“扫描”,查找指向该值的指针路径。 + + + + Cancelling... + 正在取消... + + + + + Invalid address + 无效地址 + + + + Scanning... + 正在扫描... + + + + Cancel + 取消 + + + + unexpected error + 意外错误 + + + + Scanning... %1% + 正在扫描... %1% + + + + Rescanning... %1% + 正在重新扫描... %1% + + + + Found %1 paths (showing first 1000) + 找到 %1 条路径(仅显示前 1000 条) + + + + Found %1 paths + 找到 %1 条路径 + + + + Scan failed: %1 + 扫描失败:%1 + + + + Cancelled; %1 paths found so far + 已取消;目前找到 %1 条路径 + + + + . The scan stopped at its path limit, so longer paths may be missing: lower Max offset and scan again. + 。扫描在到达路径上限时停止,因此更长的路径可能缺失:请降低最大偏移后重新扫描。 + + + + . The scan stopped at its work limit, so longer paths may be missing: lower Depth or Max offset and scan again. + 。扫描在到达工作量上限时停止,因此更长的路径可能缺失:请降低深度或最大偏移后重新扫描。 + + + + . Memory held more pointer-like values than the scan keeps, so some paths may be missing. + 。内存中保存的类指针数值多于扫描所保留的数量,因此部分路径可能缺失。 + + + + Memory also held more pointer-like values than the scan keeps. + 内存中保存的类指针数值也多于扫描所保留的数量。 + + + + Rescan cancelled; the paths are unchanged + 重新扫描已取消;路径未更改 + + + + Rescan failed: %1 + 重新扫描失败:%1 + + + + Rescan: %1 → %2 paths still reach 0x%3 + 重新扫描:%1 → %2 条路径仍可到达 0x%3 + + + + Rescan pointer paths + 重新扫描指针路径 + + + + New target address (hex) the paths should now resolve to: + 路径现在应解析到的新目标地址(十六进制): + + + + Rescanning... + 正在重新扫描... + + + + Save pointer paths + 保存指针路径 + + + + + Pointer scan (*.ptr);;All (*) + 指针扫描 (*.ptr);;所有文件 (*) + + + + Save failed + 保存失败 + + + + Could not write the pointer paths. + 无法写入指针路径。 + + + + Saved %1 paths + 已保存 %1 条路径 + + + + Load pointer paths + 加载指针路径 + + + + Load failed + 加载失败 + + + + Loaded %1 paths + 已加载 %1 条路径 + + + + ce::gui::ProcessListDialog + + + Open Process + 打开进程 + + + + Filter by process name, PID, or command + 按进程名、PID 或命令行筛选 + + + + Filter processes + 筛选进程 + + + + Only my processes + 仅显示我的进程 + + + + Hide kernel threads and processes owned by other users + 隐藏内核线程和其他用户的进程 + + + + Name + 名称 + + + + PID + PID + + + + Memory + 内存 + + + + Command + 命令行 + + + + Open + 打开 + + + + Cancel + 取消 + + + + Refresh + 刷新 + + + + [sandboxed] + [沙箱] + + + + Sandboxed (Flatpak/Snap/container): attaching reaches into its namespace; scan, edit and symbols work. + 沙箱(Flatpak/Snap/容器):附加操作会进入其命名空间;扫描、编辑和符号均可正常工作。 + + + + That process has exited; the list was refreshed. + 该进程已退出;列表已刷新。 + + + + %1 %2 + %1 %2 + + + + process + 个进程 + + + + processes + 个进程 + + + + No processes match this filter. + 没有进程匹配此筛选条件。 + + + + ce::gui::RegisterEditorWindow + + + Register Editor + 寄存器编辑器 + + + + Thread: + 线程: + + + + Refresh + 刷新 + + + + Apply changes + 应用更改 + + + + Apply only edited values to the thread’s current register state + 仅将已编辑的数值应用到该线程的当前寄存器状态 + + + + + + Register + 寄存器 + + + + Value + 数值 + + + + Double-click a value to edit it, then press Apply changes + 双击数值进行编辑,然后按“应用更改” + + + + Floating point / SIMD registers + 浮点 / SIMD 寄存器 + + + + + XMM low 128 + XMM 低 128 位 + + + + + YMM high 128 + YMM 高 128 位 + + + + GDB Register Editor + GDB 寄存器编辑器 + + + + CPU selected by the GDB stub + 由 GDB stub 选择的 CPU + + + + XML register + XML 寄存器 + + + + Target-order hexadecimal bytes + 目标顺序的十六进制字节 + + + + %1 bits; register %2; %3 + %1 位;寄存器 %2;%3 + + + + Select this row and press Refresh + 选择此行并按“刷新” + + + + Write only edited XML registers; values are raw bytes in the target's register order + 仅写入已编辑的 XML 寄存器;数值是按目标寄存器顺序排列的原始字节 + + + + No thread selected + 未选择线程 + + + + Registers of thread %1. Double-click a value to edit it, then Apply changes. + 线程 %1 的寄存器。双击数值进行编辑,然后按“应用更改”。 + + + + 128-bit value + 128 位值 + + + + unavailable: %1 + 不可用:%1 + + + + + unavailable + 不可用 + + + + Invalid %1 value + %1 值无效 + + + + + No register values changed + 没有寄存器数值发生变化 + + + + Applied changes to TID %1 + 已将更改应用到 TID %1 + + + + + GDB target is disconnected or exited + GDB 目标已断开或已退出 + + + + Unavailable: %1 + 不可用:%1 + + + + Read %1 selected register(s). Select other rows and Refresh to read them. + 已读取 %1 个选中的寄存器。选择其他行并刷新即可读取它们。 + + + + %1 requires %2 hexadecimal byte pairs + %1 需要 %2 组十六进制字节对 + + + + Applied %1 of %2 register edits. %3: %4. Refresh before retrying. + 已应用 %2 个寄存器编辑中的 %1 个。%3:%4。重试前请刷新。 + + + + Applied %1 register edit(s) + 已应用 %1 个寄存器编辑 + + + + ce::gui::ScanResultsModel + + + Address + 地址 + + + + Value + 数值 + + + + Previous + 先前值 + + + + ce::gui::ScriptEditor + + + Auto Assembler + 自动汇编器 + + + + Execute + 执行 + + + + Enable the script in the selected process + 在选中的进程中启用该脚本 + + + + Disable + 禁用 + + + + Syntax Check + 语法检查 + + + + + + + Add to Cheat Table + 添加到作弊表 + + + + Save this script as a cheat-table entry whose checkbox enables/disables it. + 将此脚本保存为作弊表条目,其复选框用于启用/禁用该脚本。 + + + + Load + 加载 + + + + Load Script + 加载脚本 + + + + CE Scripts (*.cea *.CEA *.asm);;All Files (*) + CE 脚本 (*.cea *.CEA *.asm);;所有文件 (*) + + + + Load failed + 加载失败 + + + + Save + 保存 + + + + Save Script + 保存脚本 + + + + CE Scripts (*.cea *.CEA);;All Files (*) + CE 脚本 (*.cea *.CEA);;所有文件 (*) + + + + Save failed + 保存失败 + + + + Insert template? + 插入模板? + + + + Replace the current script with the '%1' template? +Click No to insert at the cursor instead. + 是否用“%1”模板替换当前脚本? +点击“否”将在光标处插入。 + + + + Templates + 模板 + + + + Generate a template for the selected instruction (Ctrl+I) + 为选中的指令生成模板 (Ctrl+I) + + + + Choose Templates to generate an injection at the selected instruction, +or write an Auto Assembler script here. + 选择“模板”以在选中的指令处生成注入, +或在此编写自动汇编脚本。 + + + + Messages from Execute and Syntax Check appear here. + 执行和语法检查的消息会显示在这里。 + + + + No process selected! + 未选择进程! + + + + Executing... + 正在执行... + + + + Script executed successfully. + 脚本执行成功。 + + + + FAILED: %1 + 失败:%1 + + + + Cleanup is still pending. Use Disable to retry restoration. + 清理仍未完成。请使用“禁用”重试恢复。 + + + + Disabling... + 正在禁用... + + + + Script disabled. + 脚本已禁用。 + + + + Cleanup incomplete: %1 + 清理未完成:%1 + + + + Recovery state retained. Use Disable to retry. + 已保留恢复状态。请使用“禁用”重试。 + + + + No process + 没有进程 + + + + Attach to a process first. + 请先附加到一个进程。 + + + + + + Injection template + 注入模板 + + + + Disable the current script before generating another injection. + 生成另一个注入前请先禁用当前脚本。 + + + + Address or expression to inject at: + 注入地址或表达式: + + + + Bad address + 地址无效 + + + + Enter a valid address, module offset, or symbol expression. + 请输入有效的地址、模块偏移或符号表达式。 + + + + Pointer injection + 指针注入 + + + + Register holding the pointer: + 保存指针的寄存器: + + + + Replace script? + 替换脚本? + + + + Replace the current script with the generated injection? + 是否用生成的注入替换当前脚本? + + + + Generated injection at 0x%1. Edit the code in newmem. + 已在 0x%1 生成注入。请在 newmem 中编辑代码。 + + + + The script is empty. + 脚本为空。 + + + + This editor isn't connected to a cheat table. + 此编辑器未连接到作弊表。 + + + + Description for the table entry: + 表格条目的描述: + + + + Saved to the cheat table. Toggle its checkbox to enable/disable. + 已保存到作弊表。切换其复选框即可启用/禁用。 + + + + Syntax check passed. + 语法检查通过。 + + + + Syntax check failed: %1 + 语法检查失败:%1 + + + + ce::gui::SettingsDialog + + + Settings + 设置 + + + + + Scan + 扫描 + + + + + Display + 显示 + + + + + + Language + 语言 + + + + + Memory View + 内存查看 + + + + + Debugger + 调试器 + + + + + Hotkeys + 快捷键 + + + + + Network + 网络 + + + + + Lua + Lua + + + + + AI Assistant + AI 助手 + + + + OK + 确定 + + + + Cancel + 取消 + + + + Apply + 应用 + + + + Defaults for a new scan. The main window's scan options start from these. + 新扫描的默认设置。主窗口的扫描选项初始值来自这里。 + + + + Byte + 字节 + + + + 2 Bytes + 2 字节 + + + + 4 Bytes + 4 字节 + + + + 8 Bytes + 8 字节 + + + + Float + 浮点数 + + + + Double + 双精度浮点数 + + + + String + 字符串 + + + + Array of byte + 字节数组 + + + + Pointer + 指针 + + + + Value type: + 数值类型: + + + + bytes + 字节 + + + + Fast scan alignment: + 快速扫描对齐: + + + + Fast scan (only aligned addresses) + 快速扫描(仅对齐地址) + + + + Only writable memory + 仅可写内存 + + + + Only executable memory + 仅可执行内存 + + + + Automatic (one per CPU) + 自动(每个 CPU 一个) + + + + Worker threads: + 工作线程: + + + + Automatic + 自动 + + + + Float tolerance: + 浮点数容差: + + + + Used by the "Extreme" float rounding mode. + 用于“极限”浮点舍入模式。 + + + + Theme and the font used by tables, the disassembler and hex views. + 主题,以及表格、反汇编器和十六进制视图使用的字体。 + + + + Light + 浅色 + + + + Dark + 深色 + + + + Theme: + 主题: + + + + Monospace font: + 等宽字体: + + + + pt + pt + + + + Font size: + 字号: + + + + 32-bit (8 hex digits) + 32 位(8 位十六进制) + + + + 64-bit (16 hex digits) + 64 位(16 位十六进制) + + + + Address width: + 地址宽度: + + + + Uppercase hexadecimal (0x7F3A, not 0x7f3a) + 大写十六进制(0x7F3A,而非 0x7f3a) + + + + Language of the interface. It applies after the next start. + 界面语言。下次启动后生效。 + + + + Follow system + 跟随系统 + + + + Interface language: + 界面语言: + + + + English and Simplified Chinese are available for now. + 目前提供英文和简体中文。 + + + + Layout and refresh of the hex dump in the Memory Viewer. + 内存查看器中十六进制转储的布局与刷新。 + + + + Bytes per row: + 每行字节数: + + + + Refresh values while the view is open + 视图打开时刷新数值 + + + + ms + ms + + + + Refresh interval: + 刷新间隔: + + + + Applies to Memory Viewer windows opened from now on. + 对之后打开的内存查看器窗口生效。 + + + + Defaults for hardware watchpoints. + 硬件监视点的默认设置。 + + + + Data breakpoint size: + 数据断点大小: + + + + Find-what-writes size: + “查找写入”大小: + + + + How many bytes from the address a watchpoint covers. x86 watchpoints must be aligned to their size. + 监视点覆盖地址起的多少字节。x86 监视点必须按其大小对齐。 + + + + Hotkeys belong to cheat table entries: right-click an entry and choose Hotkeys. + 快捷键属于表格条目:右键单击条目并选择“快捷键”。 + + + + Default value step: + 默认数值步长: + + + + How much an increase or decrease hotkey changes a value, until the entry sets its own step. + 增加或减少快捷键每次改变多少数值,直到条目设置自己的步长。 + + + + The ceserver that File > Connect to ceserver offers by default. + “文件 > 连接到 ceserver”默认提供的 ceserver。 + + + + host name or IP address + 主机名或 IP 地址 + + + + ceserver host: + ceserver 主机: + + + + ceserver port: + ceserver 端口: + + + + Scripts in the autorun folder run once at startup, like Cheat Engine's autorun. + autorun 文件夹中的脚本在启动时运行一次,与 Cheat Engine 的 autorun 相同。 + + + + Open folder + 打开文件夹 + + + + Autorun folder: + autorun 文件夹: + + + + Lua cannot run shell commands or write local files unless Cheat Engine was started with CECORE_LUA_ALLOW_UNSAFE=1 in its environment. A cheat table cannot turn this on by itself. + 除非启动 Cheat Engine 时在环境中设置了 CECORE_LUA_ALLOW_UNSAFE=1,否则 Lua 无法执行 shell 命令或写入本地文件。作弊表本身无法开启此功能。 + + + + The model behind Tools > AI Assistant. You bring the API key, or run a model on your own computer. + “工具 > AI 助手”使用的模型。你可以自带 API 密钥,也可以在本机运行模型。 + + + + Anthropic (Claude) + Anthropic (Claude) + + + + OpenAI-compatible (OpenAI, Ollama, LM Studio) + 兼容 OpenAI(OpenAI、Ollama、LM Studio) + + + + Provider: + 提供商: + + + + Model: + 模型: + + + + API address: + API 地址: + + + + Paste a new key to save it + 粘贴新密钥以保存 + + + + Forget key + 忘记密钥 + + + + API key: + API 密钥: + + + + Keys are kept in the system keyring, never in Cheat Engine's settings file; ANTHROPIC_API_KEY or OPENAI_API_KEY in the environment take precedence. A saved key is only sent to the API address it was entered with. Your messages and what the tools find in the game are sent to this provider, and every change to the game waits for your Allow. + 密钥保存在系统密钥环中,而不是 Cheat Engine 的设置文件里;环境变量 ANTHROPIC_API_KEY 或 OPENAI_API_KEY 优先。已保存的密钥只会发送到输入它时使用的 API 地址。你的消息以及工具在游戏中发现的内容会发送给该提供商,而对游戏的每次更改都会等待你的允许。 + + + + Could not remove the key: %1 + 无法删除密钥:%1 + + + + for example gpt-5 or qwen3 + 例如 gpt-5 或 qwen3 + + + + https://api.openai.com/v1 or http://localhost:11434/v1 + https://api.openai.com/v1 或 http://localhost:11434/v1 + + + + Kept until Cheat Engine closes: %1 + 仅在 Cheat Engine 关闭前有效:%1 + + + + Restart Cheat Engine to apply the new interface language. + 重启 Cheat Engine 以应用新的界面语言。 + + + + ce::gui::StackViewWindow + + + + Stack Trace + 栈回溯 + + + + Thread: + 线程: + + + + Refresh + 刷新 + + + + Address + 地址 + + + + Value + 数值 + + + + Offset + 偏移 + + + + Raw Stack + 原始栈 + + + + Frame + 帧 + + + + Instruction + 指令 + + + + Return + 返回地址 + + + + Frame Pointer + 帧指针 + + + + Symbol + 符号 + + + + No thread selected + 未选择线程 + + + + Thread %1 %2 0x%3 %4 0x%5 %6 frames + 线程 %1 %2 0x%3 %4 0x%5 %6 帧 + + + + ce::gui::StructureDissector + + + Structure Dissector + 结构剖析器 + + + + Base Address: + 基址: + + + + Go + 转到 + + + + Bytes: + 字节数: + + + + Compare: + 比较: + + + + 0x…, 0x… (optional, comma/space separated) + 0x…, 0x…(可选,用逗号或空格分隔) + + + + Enter one or more struct addresses to compare against the base. Rows whose bytes differ in any instance are highlighted, so the fields that vary between instances stand out. Press Enter to apply. + 输入一个或多个结构体地址,与基址进行比较。只要在任一实例中字节不同,该行就会高亮,从而突出显示各实例之间存在差异的字段。按 Enter 应用。 + + + + Save Definition… + 保存定义… + + + + Save this structure definition (named fields + size) + 保存此结构体定义(已命名字段 + 大小) + + + + Load Definition… + 加载定义… + + + + Load a previously saved structure definition + 加载之前保存的结构体定义 + + + + + + Copy as C++ + 复制为 C++ + + + + Copy the current layout to the clipboard as a C++ struct + 将当前布局作为 C++ 结构体复制到剪贴板 + + + + Type as IL2CPP… + 按 IL2CPP 类型… + + + + Label these bytes with a Unity IL2CPP class's fields (names + offsets from global-metadata.dat + GameAssembly) + 用 Unity IL2CPP 类的字段标注这些字节(名称 + 来自 global-metadata.dat 和 GameAssembly 的偏移) + + + + Type as C struct… + 按 C 结构体类型… + + + + Label these bytes with a native struct from the target's DWARF debug info + 用目标进程 DWARF 调试信息中的原生结构体标注这些字节 + + + + Add All to List + 全部添加到列表 + + + + Add every named field (base+offset, name, guessed type) to the cheat table + 将所有已命名字段(基址+偏移、名称、推测类型)添加到作弊表 + + + + Added %1 field to the cheat table + 已向作弊表添加 %1 个字段 + + + + Added %1 fields to the cheat table + 已向作弊表添加 %1 个字段 + + + + No named fields to add (double-click a row, or Type as…) + 没有可添加的已命名字段(双击某行,或使用“按类型…”) + + + + + + Offset + 偏移 + + + + + + Name + 名称 + + + + + Hex + 十六进制 + + + + + Int8 + 8 位整数 + + + + + Int32 + 32 位整数 + + + + + Float + 浮点数 + + + + + Pointer? + 指针? + + + + Enter the address of a structure above and press Go to see its fields. + 在上方输入结构体地址,然后点击“转到”查看其字段。 + + + + + Name field + 命名字段 + + + + + Name for offset +0x%1: + 偏移 +0x%1 的字段名: + + + + Structure Dissector (target exited) + 结构剖析器(目标已退出) + + + + Offset +0x%1 (0x%2) + 偏移 +0x%1 (0x%2) + + + + Add to address list as %1 (auto) + 以 %1(自动)添加到地址列表 + + + + Add as 4 Bytes + 添加为 4 字节 + + + + Add as 8 Bytes + 添加为 8 字节 + + + + Add as Float + 添加为浮点数 + + + + Add as Pointer + 添加为指针 + + + + Name field... + 命名字段... + + + + Set field type + 设置字段类型 + + + + Auto (guess) + 自动(推测) + + + + Dissect +0x%1 + 剖析 +0x%1 + + + + Save structure definition + 保存结构体定义 + + + + + Structure def (*.json);;All (*) + 结构体定义 (*.json);;所有文件 (*) + + + + Save failed + 保存失败 + + + + Could not write the definition file. + 无法写入定义文件。 + + + + Load structure definition + 加载结构体定义 + + + + Load failed + 加载失败 + + + + Could not read the file or it is not a valid structure definition. + 无法读取该文件,或它不是有效的结构体定义。 + + + + + No target process. + 没有目标进程。 + + + + Type as IL2CPP class + 按 IL2CPP 类类型 + + + + Class name (e.g. UnityEngine.Vector3 or Player): + 类名(例如 UnityEngine.Vector3 或 Player): + + + + Class not found: %1 + 未找到类:%1 + + + + This build has no libdw (DWARF) support. + 此构建不支持 libdw (DWARF)。 + + + + Type as C struct + 按 C 结构体类型 + + + + Struct name (from the target's debug info): + 结构体名(来自目标进程的调试信息): + + + + Struct not found (needs debug info): %1 + 未找到结构体(需要调试信息):%1 + + + + Base + 基准 + + + + Name some fields first (double-click a row to name the offset). + 请先命名一些字段(双击某行可为偏移命名)。 + + + + C++ struct copied to clipboard: + +%1 + 已复制 C++ 结构体到剪贴板: + +%1 + + + + ce::gui::ThreadListWindow + + + Thread List + 线程列表 + + + + TID + 线程 ID + + + + Name + 名称 + + + + State + 状态 + + + + Thread List: %1 threads + 线程列表:%1 个线程 + + + + ce::gui::TracerWindow + + + Break and Trace + 断点与跟踪 + + + + Start address (hex): + 起始地址(十六进制): + + + + 0x0000000000000000, leave blank to start at the instruction pointer + 0x0000000000000000,留空则从指令指针处开始 + + + + Max steps: + 最大步数: + + + + Step over calls + 单步跳过调用 + + + + Stay in module + 保持在模块内 + + + + Module start (hex): + 模块起始地址(十六进制): + + + + 0x... (only if Stay in module) + 0x...(仅在“保持在模块内”时) + + + + end: + 结束: + + + + Stop address (hex, optional): + 停止地址(十六进制,可选): + + + + Trace stops when this address is reached + 跟踪到达此地址时停止 + + + + Start Trace + 开始跟踪 + + + + Cancel + 取消 + + + + Save… + 保存… + + + + Idle + 空闲 + + + + Traced instructions appear here. + 跟踪的指令将显示在这里。 + + + + Step %1 @ 0x%2 + 第 %1 步 @ 0x%2 + + + + No process + 没有进程 + + + + Open a process before tracing. + 跟踪前请先打开一个进程。 + + + + Invalid hexadecimal address + 无效的十六进制地址 + + + + Module end must be greater than its start + 模块结束地址必须大于起始地址 + + + + No debugger + 没有调试器 + + + + No debugger could be constructed for the attached process. + 无法为已附加的进程创建调试器。 + + + + Tracing… + 跟踪中… + + + + Cancelling… + 正在取消… + + + + %1 steps recorded (check start address / module range) + 已记录 %1 步(请检查起始地址 / 模块范围) + + + + %1 steps recorded + 已记录 %1 步 + + + + %1 steps recorded: %2 + 已记录 %1 步:%2 + + + + Save trace + 保存跟踪记录 + + + + Text (*.txt) + 文本 (*.txt) + + + + Save failed + 保存失败 + + + From 9c270e46cedeee4153d938b1f3c73f573d44bd80 Mon Sep 17 00:00:00 2001 From: h465855hgg <3382198490@qq.com> Date: Thu, 12 Nov 2026 20:36:16 +0800 Subject: [PATCH 2/3] i18n: offer an immediate restart when the interface language changes Every window builds its texts in its constructor, so a new language needs a new start. Instead of leaving that to the user, applying a different language in Settings now asks to restart, and relaunches the same command line (the AppImage file itself when running from a bundled build). Choosing Later keeps the behaviour of applying the change on the next manual start. --- docs/I18N.md | 3 +- gui/settingsdialog.cpp | 37 ++++- translations/cheatengine_zh_CN.ts | 235 +++++++++++++++++------------- 3 files changed, 165 insertions(+), 110 deletions(-) diff --git a/docs/I18N.md b/docs/I18N.md index 3f1c78da..ec90dce6 100644 --- a/docs/I18N.md +++ b/docs/I18N.md @@ -14,7 +14,8 @@ and the AppImage carry it without extra files. - With no setting, the interface follows the desktop locale (`LANG`, e.g. `zh_CN.UTF-8`). - **Settings > Language** overrides it: follow the system, English, or 简体中文. - The choice is stored in `ui/language` and takes effect on the next start. + The choice is stored in `ui/language`; when it changes, Cheat Engine offers to + restart itself so the new language takes effect right away. - A `.qm` file dropped beside the executable, or under `translations/`, wins over the built-in catalogue. That lets a translator try a file without a rebuild. diff --git a/gui/settingsdialog.cpp b/gui/settingsdialog.cpp index 44b21eec..6a6c47a2 100644 --- a/gui/settingsdialog.cpp +++ b/gui/settingsdialog.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include "core/agent_chat.hpp" #include "gui/agent_panel.hpp" @@ -114,6 +115,23 @@ QComboBox* sizeCombo(const QString& current) { combo->setCurrentIndex(std::max(0, combo->findData(current))); return sized(combo); } + +// Relaunch the program so a new interface language takes effect without the user +// restarting it by hand. Under an AppImage, the mount that applicationFilePath() +// points into disappears when this process exits, so the AppImage file itself is +// started again. +void restartApplication(QWidget* parent) { + QStringList arguments = QCoreApplication::arguments(); + if (!arguments.isEmpty()) arguments.removeFirst(); + const QString appImage = qEnvironmentVariable("APPIMAGE"); + const QString program = appImage.isEmpty() ? QCoreApplication::applicationFilePath() : appImage; + if (QProcess::startDetached(program, arguments, QDir::currentPath())) + QCoreApplication::quit(); + else + QMessageBox::warning(parent, QCoreApplication::translate("SettingsDialog", "Language"), + QCoreApplication::translate("SettingsDialog", + "Cheat Engine could not restart itself. Start it again to apply the language.")); +} } // namespace SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent) { @@ -268,7 +286,7 @@ QWidget* SettingsDialog::buildDisplayPage() { QWidget* SettingsDialog::buildLanguagePage() { auto* w = new QWidget; auto* form = makePage(w, tr("Language"), - tr("Language of the interface. It applies after the next start.")); + tr("Language of the interface. A restart is offered when it changes.")); languageCombo_ = sized(new QComboBox); languageCombo_->addItem(tr("Follow system"), QString()); @@ -539,9 +557,20 @@ void SettingsDialog::onApply() { : tr("Kept until Cheat Engine closes: %1").arg(error)); } emit settingsApplied(); - if (languageChanged) - QMessageBox::information(this, tr("Language"), - tr("Restart Cheat Engine to apply the new interface language.")); + if (languageChanged) { + // A new language needs a fresh start: every window builds its texts in its + // constructor, so offer the restart instead of leaving the choice to the + // next manual start. Unsaved table state is the user's call. + QMessageBox box(this); + box.setIcon(QMessageBox::Question); + box.setWindowTitle(tr("Language")); + box.setText(tr("Restart Cheat Engine to apply the new interface language.")); + box.setInformativeText(tr("Save the table first if you need the current addresses and scan results.")); + auto* restart = box.addButton(tr("Restart now"), QMessageBox::AcceptRole); + box.addButton(tr("Later"), QMessageBox::RejectRole); + box.exec(); + if (box.clickedButton() == restart) restartApplication(this); + } } } // namespace ce::gui diff --git a/translations/cheatengine_zh_CN.ts b/translations/cheatengine_zh_CN.ts index 1952a4aa..c5aad29f 100644 --- a/translations/cheatengine_zh_CN.ts +++ b/translations/cheatengine_zh_CN.ts @@ -434,42 +434,52 @@ Linux 只允许程序在获准 ptrace 另一个进程时读取其内存。当 ke SettingsDialog - + byte 字节 - + bytes 字节 - + %1 %2 %1 %2 - + + Language + 语言 + + + + Cheat Engine could not restart itself. Start it again to apply the language. + Cheat Engine 无法自行重启,请手动启动以应用新的语言。 + + + Using %1 from the environment. 使用环境变量中的 %1。 - + A key is saved in the system keyring. 密钥已保存在系统密钥环中。 - + A key is set until Cheat Engine closes (no keyring was available). 密钥仅在 Cheat Engine 关闭前有效(没有可用的密钥环)。 - + No key yet. Create one at console.anthropic.com. 尚无密钥。请在 console.anthropic.com 创建。 - + No key set; local servers usually need none. 未设置密钥;本地服务器通常不需要。 @@ -7712,440 +7722,455 @@ or write an Auto Assembler script here. ce::gui::SettingsDialog - + Settings 设置 - - + + Scan 扫描 - - + + Display 显示 - - - + + + Language 语言 - - + + Memory View 内存查看 - - + + Debugger 调试器 - - + + Hotkeys 快捷键 - - + + Network 网络 - - + + Lua Lua - - + + AI Assistant AI 助手 - + OK 确定 - + Cancel 取消 - + Apply 应用 - + Defaults for a new scan. The main window's scan options start from these. 新扫描的默认设置。主窗口的扫描选项初始值来自这里。 - + Byte 字节 - + 2 Bytes 2 字节 - + 4 Bytes 4 字节 - + 8 Bytes 8 字节 - + Float 浮点数 - + Double 双精度浮点数 - + String 字符串 - + Array of byte 字节数组 - + Pointer 指针 - + Value type: 数值类型: - + bytes 字节 - + Fast scan alignment: 快速扫描对齐: - + Fast scan (only aligned addresses) 快速扫描(仅对齐地址) - + Only writable memory 仅可写内存 - + Only executable memory 仅可执行内存 - + Automatic (one per CPU) 自动(每个 CPU 一个) - + Worker threads: 工作线程: - + Automatic 自动 - + Float tolerance: 浮点数容差: - + Used by the "Extreme" float rounding mode. 用于“极限”浮点舍入模式。 - + Theme and the font used by tables, the disassembler and hex views. 主题,以及表格、反汇编器和十六进制视图使用的字体。 - + Light 浅色 - + Dark 深色 - + Theme: 主题: - + Monospace font: 等宽字体: - + pt pt - + Font size: 字号: - + 32-bit (8 hex digits) 32 位(8 位十六进制) - + 64-bit (16 hex digits) 64 位(16 位十六进制) - + Address width: 地址宽度: - + Uppercase hexadecimal (0x7F3A, not 0x7f3a) 大写十六进制(0x7F3A,而非 0x7f3a) - - Language of the interface. It applies after the next start. - 界面语言。下次启动后生效。 + + Language of the interface. A restart is offered when it changes. + 界面语言。更改时会提示重启。 - + Follow system 跟随系统 - + Interface language: 界面语言: - + English and Simplified Chinese are available for now. 目前提供英文和简体中文。 - + Layout and refresh of the hex dump in the Memory Viewer. 内存查看器中十六进制转储的布局与刷新。 - + Bytes per row: 每行字节数: - + Refresh values while the view is open 视图打开时刷新数值 - + ms ms - + Refresh interval: 刷新间隔: - + Applies to Memory Viewer windows opened from now on. 对之后打开的内存查看器窗口生效。 - + Defaults for hardware watchpoints. 硬件监视点的默认设置。 - + Data breakpoint size: 数据断点大小: - + Find-what-writes size: “查找写入”大小: - + How many bytes from the address a watchpoint covers. x86 watchpoints must be aligned to their size. 监视点覆盖地址起的多少字节。x86 监视点必须按其大小对齐。 - + Hotkeys belong to cheat table entries: right-click an entry and choose Hotkeys. 快捷键属于表格条目:右键单击条目并选择“快捷键”。 - + Default value step: 默认数值步长: - + How much an increase or decrease hotkey changes a value, until the entry sets its own step. 增加或减少快捷键每次改变多少数值,直到条目设置自己的步长。 - + The ceserver that File > Connect to ceserver offers by default. “文件 > 连接到 ceserver”默认提供的 ceserver。 - + host name or IP address 主机名或 IP 地址 - + ceserver host: ceserver 主机: - + ceserver port: ceserver 端口: - + Scripts in the autorun folder run once at startup, like Cheat Engine's autorun. autorun 文件夹中的脚本在启动时运行一次,与 Cheat Engine 的 autorun 相同。 - + Open folder 打开文件夹 - + Autorun folder: autorun 文件夹: - + Lua cannot run shell commands or write local files unless Cheat Engine was started with CECORE_LUA_ALLOW_UNSAFE=1 in its environment. A cheat table cannot turn this on by itself. 除非启动 Cheat Engine 时在环境中设置了 CECORE_LUA_ALLOW_UNSAFE=1,否则 Lua 无法执行 shell 命令或写入本地文件。作弊表本身无法开启此功能。 - + The model behind Tools > AI Assistant. You bring the API key, or run a model on your own computer. “工具 > AI 助手”使用的模型。你可以自带 API 密钥,也可以在本机运行模型。 - + Anthropic (Claude) Anthropic (Claude) - + OpenAI-compatible (OpenAI, Ollama, LM Studio) 兼容 OpenAI(OpenAI、Ollama、LM Studio) - + Provider: 提供商: - + Model: 模型: - + API address: API 地址: - + Paste a new key to save it 粘贴新密钥以保存 - + Forget key 忘记密钥 - + API key: API 密钥: - + Keys are kept in the system keyring, never in Cheat Engine's settings file; ANTHROPIC_API_KEY or OPENAI_API_KEY in the environment take precedence. A saved key is only sent to the API address it was entered with. Your messages and what the tools find in the game are sent to this provider, and every change to the game waits for your Allow. 密钥保存在系统密钥环中,而不是 Cheat Engine 的设置文件里;环境变量 ANTHROPIC_API_KEY 或 OPENAI_API_KEY 优先。已保存的密钥只会发送到输入它时使用的 API 地址。你的消息以及工具在游戏中发现的内容会发送给该提供商,而对游戏的每次更改都会等待你的允许。 - + Could not remove the key: %1 无法删除密钥:%1 - + for example gpt-5 or qwen3 例如 gpt-5 或 qwen3 - + https://api.openai.com/v1 or http://localhost:11434/v1 https://api.openai.com/v1 或 http://localhost:11434/v1 - + Kept until Cheat Engine closes: %1 仅在 Cheat Engine 关闭前有效:%1 - + Restart Cheat Engine to apply the new interface language. 重启 Cheat Engine 以应用新的界面语言。 + + + Save the table first if you need the current addresses and scan results. + 如需保留当前的地址列表和扫描结果,请先保存表格。 + + + + Restart now + 立即重启 + + + + Later + 稍后 + ce::gui::StackViewWindow From 337d0995540625a9aa93e5829088862fd02be1ef Mon Sep 17 00:00:00 2001 From: h465855hgg <3382198490@qq.com> Date: Sat, 14 Nov 2026 08:35:27 +0800 Subject: [PATCH 3/3] i18n: address the review comments - Close the main window before starting the replacement instance. The Settings dialog now only emits restartRequested; MainWindow closes itself through the normal closeEvent path (save prompt, releaseTargetUsers) and calls launchRestart() only when that close was accepted, so a cancelled save prompt cannot leave two instances behind and the release step is never skipped. - A binary holding a file capability runs as a secure-exec process: gui/i18n.cpp now loads only the built-in catalogue then, so a .qm beside the executable or in the working directory can no longer replace the interface strings of its confirmation dialogs. The startup log line says when this applies. - Rebase onto main. The new scanWriteErrorText() strings and the scan-warning title are translatable too, the CHANGELOG entry is folded into the existing Unreleased list, and the PROGRESS log entry keeps its date. - Document that CE_LANG is a screenshot-mode hook and that a privileged install ignores .qm overrides. --- docs/I18N.md | 12 + gui/i18n.cpp | 34 +- gui/i18n.hpp | 6 + gui/mainwindow.cpp | 14 + gui/mainwindow.hpp | 3 + gui/settingsdialog.cpp | 20 +- gui/settingsdialog.hpp | 3 + translations/cheatengine_zh_CN.ts | 1626 +++++++++++++++-------------- 8 files changed, 885 insertions(+), 833 deletions(-) diff --git a/docs/I18N.md b/docs/I18N.md index ec90dce6..ec7082a9 100644 --- a/docs/I18N.md +++ b/docs/I18N.md @@ -18,6 +18,11 @@ and the AppImage carry it without extra files. restart itself so the new language takes effect right away. - A `.qm` file dropped beside the executable, or under `translations/`, wins over the built-in catalogue. That lets a translator try a file without a rebuild. + This does not apply to a privileged install: the `.deb` gives `cheatengine` + `cap_sys_ptrace`, which makes it a secure-exec process, and such a binary only + uses the built-in catalogue so nobody can replace the strings of its + confirmation dialogs. The Qt standard translations are still read from the + system location. The Qt standard strings (stock dialog buttons, native file dialogs) come from Qt's own translations. Linux distributions package those separately @@ -89,3 +94,10 @@ Notes for a good translation: The review finds windows and menu items by their English source text, so it still drives the UI, while every capture is rendered in the chosen language. + `CE_LANG` is a hook of the screenshot mode only: the other smoke tests + compare English status text, so they are not meant to run with a catalogue + loaded. + + The Qt standard translations are read from the system location + (`QLibraryInfo::TranslationsPath`), so install the desktop's Qt translation + package when the stock dialog buttons should be translated too. diff --git a/gui/i18n.cpp b/gui/i18n.cpp index 05a5ee89..07adcba4 100644 --- a/gui/i18n.cpp +++ b/gui/i18n.cpp @@ -1,10 +1,13 @@ #include "gui/i18n.hpp" #include "core/log.hpp" +#include "core/secure_exec.hpp" #include +#include #include #include #include +#include #include #include #include @@ -60,14 +63,20 @@ QString installTranslators(QApplication& app, const QString& explicitLocale) { } // A catalogue dropped next to the executable wins over the built-in one, so - // translators can test a .qm without rebuilding. + // translators can test a .qm without rebuilding. A binary holding a file + // capability (the .deb grants cap_sys_ptrace) is a secure-exec process: the + // person who starts it must not be able to replace the interface strings of + // its confirmation dialogs, so only the built-in catalogue is used then. auto* ceTranslator = new QTranslator(&app); QString loadedFrom; - const QStringList dirs = translationSearchDirs(); - for (const QString& dir : dirs) { - if (ceTranslator->load(uiLocale, QStringLiteral("cheatengine"), QStringLiteral("_"), dir)) { - loadedFrom = dir; - break; + const bool secure = ce::secureExecution(); + if (!secure) { + const QStringList dirs = translationSearchDirs(); + for (const QString& dir : dirs) { + if (ceTranslator->load(uiLocale, QStringLiteral("cheatengine"), QStringLiteral("_"), dir)) { + loadedFrom = dir; + break; + } } } if (loadedFrom.isEmpty()) { @@ -77,10 +86,19 @@ QString installTranslators(QApplication& app, const QString& explicitLocale) { } if (!loadedFrom.isEmpty()) app.installTranslator(ceTranslator); - ce::log::info(ce::log::Cat::Gui, "interface language {} (catalogue: {})", + ce::log::info(ce::log::Cat::Gui, "interface language {} (catalogue: {}){}", uiLocale.name().toStdString(), - loadedFrom.isEmpty() ? std::string("none") : loadedFrom.toStdString()); + loadedFrom.isEmpty() ? std::string("none") : loadedFrom.toStdString(), + secure ? " (secure-exec: built-in only)" : ""); return uiLocale.name(); } +bool launchRestart() { + QStringList arguments = QCoreApplication::arguments(); + if (!arguments.isEmpty()) arguments.removeFirst(); + const QString appImage = qEnvironmentVariable("APPIMAGE"); + const QString program = appImage.isEmpty() ? QCoreApplication::applicationFilePath() : appImage; + return QProcess::startDetached(program, arguments, QDir::currentPath()); +} + } // namespace ce::gui diff --git a/gui/i18n.hpp b/gui/i18n.hpp index ba9c058f..ccf6c4ba 100644 --- a/gui/i18n.hpp +++ b/gui/i18n.hpp @@ -17,4 +17,10 @@ QString configuredUiLanguage(); /// locale (used by tests and screenshot runs). QString installTranslators(QApplication& app, const QString& explicitLocale = {}); +/// Starts a new instance of the program with the same arguments, for the restart +/// offered after an interface-language change. Under an AppImage the AppImage +/// file itself is started, not the mount path that disappears with this process. +/// Returns false when the new process could not be started. +bool launchRestart(); + } // namespace ce::gui diff --git a/gui/mainwindow.cpp b/gui/mainwindow.cpp index 8cfeead8..e3531a5b 100644 --- a/gui/mainwindow.cpp +++ b/gui/mainwindow.cpp @@ -7,6 +7,7 @@ #include "core/local_target.hpp" #include "gui/gdbconnectiondialog.hpp" #include "gui/theme.hpp" +#include "gui/i18n.hpp" #include "core/expression.hpp" #include "core/value_io.hpp" #include "core/memview_nav.hpp" @@ -3937,6 +3938,18 @@ void MainWindow::markTableSaved() { savedTableFingerprint_ = tableFingerprint(); // Before the table is replaced or the window closes: Save, Discard or Cancel // when it has changes no file holds. Nothing tracked this, so edits were // dropped without a word. +void MainWindow::restartAfterLanguageChange() { + // closeEvent() offers to save the table and releases the target users + // (breakpoints, injected code) before the process goes away. Start the new + // instance only after that close was accepted, so a cancelled save prompt + // does not leave two instances behind and the release is never skipped. + if (!close()) return; + if (!ce::gui::launchRestart()) + QMessageBox::warning(nullptr, tr("Language"), + tr("Cheat Engine could not restart itself. Start it again to apply the language.")); + QCoreApplication::quit(); +} + bool MainWindow::maybeSaveTable() { // Headless (the offscreen test platform): nobody can answer a prompt. const auto platform = QGuiApplication::platformName(); @@ -5188,6 +5201,7 @@ QDialog* MainWindow::openSettingsDialog(const QString& page) { auto* dlg = new SettingsDialog(this); settingsDialog_ = dlg; dlg->setAttribute(Qt::WA_DeleteOnClose); + connect(dlg, &SettingsDialog::restartRequested, this, &MainWindow::restartAfterLanguageChange); connect(dlg, &SettingsDialog::settingsApplied, this, [this]() { if (agentPanel_) agentPanel_->reloadSettings(); // A scan blocks user input, so Apply can't land mid-scan; the scanner is idle. diff --git a/gui/mainwindow.hpp b/gui/mainwindow.hpp index 3c335ea5..6bf1edc1 100644 --- a/gui/mainwindow.hpp +++ b/gui/mainwindow.hpp @@ -133,6 +133,9 @@ private slots: void onSaveScanResults(); // export the current found-address list to txt/csv void onCreateTrainer(); void onFreezeTimer(); + /// Settings accepted a restart after an interface-language change: close the + /// window (save prompt, release target users), then start the new instance. + void restartAfterLanguageChange(); protected: // Keeps the empty-results hint sized to the results viewport. diff --git a/gui/settingsdialog.cpp b/gui/settingsdialog.cpp index 6a6c47a2..0862047b 100644 --- a/gui/settingsdialog.cpp +++ b/gui/settingsdialog.cpp @@ -13,7 +13,6 @@ #include #include #include -#include #include "core/agent_chat.hpp" #include "gui/agent_panel.hpp" @@ -115,23 +114,6 @@ QComboBox* sizeCombo(const QString& current) { combo->setCurrentIndex(std::max(0, combo->findData(current))); return sized(combo); } - -// Relaunch the program so a new interface language takes effect without the user -// restarting it by hand. Under an AppImage, the mount that applicationFilePath() -// points into disappears when this process exits, so the AppImage file itself is -// started again. -void restartApplication(QWidget* parent) { - QStringList arguments = QCoreApplication::arguments(); - if (!arguments.isEmpty()) arguments.removeFirst(); - const QString appImage = qEnvironmentVariable("APPIMAGE"); - const QString program = appImage.isEmpty() ? QCoreApplication::applicationFilePath() : appImage; - if (QProcess::startDetached(program, arguments, QDir::currentPath())) - QCoreApplication::quit(); - else - QMessageBox::warning(parent, QCoreApplication::translate("SettingsDialog", "Language"), - QCoreApplication::translate("SettingsDialog", - "Cheat Engine could not restart itself. Start it again to apply the language.")); -} } // namespace SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent) { @@ -569,7 +551,7 @@ void SettingsDialog::onApply() { auto* restart = box.addButton(tr("Restart now"), QMessageBox::AcceptRole); box.addButton(tr("Later"), QMessageBox::RejectRole); box.exec(); - if (box.clickedButton() == restart) restartApplication(this); + if (box.clickedButton() == restart) emit restartRequested(); } } diff --git a/gui/settingsdialog.hpp b/gui/settingsdialog.hpp index aac6589a..5aadd97f 100644 --- a/gui/settingsdialog.hpp +++ b/gui/settingsdialog.hpp @@ -22,6 +22,9 @@ class SettingsDialog : public QDialog { explicit SettingsDialog(QWidget* parent = nullptr); signals: void settingsApplied(); + /// The user accepted a restart after changing the interface language. The + /// main window closes itself (saving and releasing the target) first. + void restartRequested(); private slots: void onApply(); private: diff --git a/translations/cheatengine_zh_CN.ts b/translations/cheatengine_zh_CN.ts index c5aad29f..c019e89a 100644 --- a/translations/cheatengine_zh_CN.ts +++ b/translations/cheatengine_zh_CN.ts @@ -288,71 +288,91 @@ MainWindow - + Found: %1 已找到:%1 - + + A scan-result file could not be fully written or read. + 扫描结果文件未能完整写入或读取。 + + + + A scan-result file could not be fully written or read: %1. + 扫描结果文件未能完整写入或读取:%1。 + + + + The disk holding %1 is full; free space there or set CE_SCAN_DIR to a larger disk. + 存放 %1 的磁盘已满;请清理空间,或将 CE_SCAN_DIR 指向更大的磁盘。 + + + + Some results are unreliable; scan again. + 部分结果不可靠;请重新扫描。 + + + any process of the same user 同一用户的任意进程 - + only child processes 仅子进程 - + administrators only 仅管理员 - + no attaching 禁止附加 - + Access granted for this session; re-open the process. (This resets on reboot. Install the .deb, or re-run the command, to make it permanent.) 本次会话已授予访问权限;请重新打开进程。 (重启后失效。安装 .deb,或重新运行该命令,使其永久生效。) - + Access granted for this session; re-open the process. (This resets on reboot. A capability on this install would stop it from finding its libraries, because it is outside the system library directories.) 本次会话已授予访问权限;请重新打开进程。 (重启后失效。此安装上的能力会导致它无法找到自己的库,因为库位于系统库目录之外。) - + Access granted. Restart Cheat Engine for it to take effect. 已授予访问权限。请重启 Cheat Engine 使其生效。 - + Access granted 已授予访问权限 - + Could not grant access 无法授予访问权限 - + The privileged command was cancelled or failed (pkexec exit %1). You can still grant access manually from a terminal. 特权命令已取消或失败(pkexec 退出码 %1)。你仍可以在终端中手动授予访问权限。 - + Process memory not readable 无法读取进程内存 - + Cannot read the memory of PID %1 (%2). Linux only lets a program read another process's memory if it is allowed to ptrace it. With kernel.yama.ptrace_scope = %3, that means the target must be a child of Cheat Engine, or Cheat Engine must run with elevated rights. Scanning, memory browsing and debugging this process will not work until that is granted. @@ -361,7 +381,7 @@ Linux only lets a program read another process's memory if it is allowed to Linux 只允许程序在获准 ptrace 另一个进程时读取其内存。当 kernel.yama.ptrace_scope = %3 时,目标必须是 Cheat Engine 的子进程,或者 Cheat Engine 必须以提升的权限运行。在获得该权限之前,扫描、内存浏览和调试此进程都将无法工作。 - + "Grant access" will ask for your password (via pkexec) and set it up for you. Or do it manually: • Persistent, per-binary: sudo setcap cap_sys_ptrace+ep %1 • Whole system, until reboot: sudo sysctl kernel.yama.ptrace_scope=0 @@ -370,27 +390,27 @@ Linux 只允许程序在获准 ptrace 另一个进程时读取其内存。当 ke • 整个系统,直至重启: sudo sysctl kernel.yama.ptrace_scope=0 - + Grant access… 授予访问权限… - + Cheat Tables (*.ct *.CT);;JSON Tables (*.json *.JSON);;All Files (*) 作弊表格 (*.ct *.CT);;JSON 表格 (*.json *.JSON);;所有文件 (*) - + Scan value 扫描数值 - + Enter a value for this scan. 请输入用于此扫描的数值。 - + "%1" is not a number (or does not fit in 64 bits). “%1”不是数字(或超出 64 位范围)。 @@ -434,52 +454,42 @@ Linux 只允许程序在获准 ptrace 另一个进程时读取其内存。当 ke SettingsDialog - + byte 字节 - + bytes 字节 - + %1 %2 %1 %2 - - Language - 语言 - - - - Cheat Engine could not restart itself. Start it again to apply the language. - Cheat Engine 无法自行重启,请手动启动以应用新的语言。 - - - + Using %1 from the environment. 使用环境变量中的 %1。 - + A key is saved in the system keyring. 密钥已保存在系统密钥环中。 - + A key is set until Cheat Engine closes (no keyring was available). 密钥仅在 Cheat Engine 关闭前有效(没有可用的密钥环)。 - + No key yet. Create one at console.anthropic.com. 尚无密钥。请在 console.anthropic.com 创建。 - + No key set; local servers usually need none. 未设置密钥;本地服务器通常不需要。 @@ -564,94 +574,94 @@ Linux 只允许程序在获准 ptrace 另一个进程时读取其内存。当 ke ce::gui::AddressListModel - + Auto Assembler script 自动汇编脚本 - + (Auto Assembler script) (自动汇编脚本) - + Table format 表格格式 - + Invalid record data order or pointer size. 记录数据字节序或指针大小无效。 - - - - - - - + + + + + + + Value edit failed 数值编辑失败 - + Process required 需要进程 - + Open a process before activating this auto-assembler record. 请先打开进程再激活此自动汇编记录。 - + Auto-assembler activation failed 自动汇编激活失败 - + Auto-assembler deactivation failed 自动汇编停用失败 - + Freeze failed 冻结失败 - + The value cannot be read right now, so there is nothing to freeze. 当前无法读取数值,因此没有可冻结的内容。 - + Active 启用 - + Description 描述 - + Address 地址 - + Type 类型 - + Value 数值 - - + + The pointer path does not resolve right now. 指针路径当前无法解析。 @@ -3212,758 +3222,758 @@ Applying it will grow the file (zero-filling the gap). Continue? ce::gui::MainWindow - + Select file 选择文件 - + Select file for %1 选择 %1 的文件 - - + + GDB target disconnected or exited GDB 目标已断开或已退出 - - + + ceserver connection lost ceserver 连接已丢失 - + Connect again with File > Connect to ceserver. 请使用“文件 > 连接到 ceserver”重新连接。 - + Process %1 has exited 进程 %1 已退出 - + Open another process with Ctrl+O. 使用 Ctrl+O 打开另一个进程。 - + Target process exited 目标进程已退出 - + &File 文件(&F) - + Clear scan results 清除扫描结果 - - + + Open Process... 打开进程... - - - - - + + + + + Save 保存 - + Save As... 另存为... - + Load... 加载... - + Load Recent 最近加载 - + Save scan results... 保存扫描结果... - + Generate Trainer Lua Script... 生成修改器 Lua 脚本... - + Connect to ceserver... 连接到 ceserver... - + Connect to GDB / QEMU... 连接到 GDB / QEMU... - + Disconnect target 断开目标连接 - + Quit 退出 - + &Edit 编辑(&E) - + &Process 进程(&P) - + Table 表格 - + &Tools 工具(&T) - + .Net .Net - - + + Mono dissector... Mono 反汇编器... - + Show Cheat Table Lua Script 显示表格 Lua 脚本 - + Create Form... 创建窗体... - + Memory Viewer... 内存查看器... - - + + AI Assistant AI 助手 - - - - + + + + Auto Assemble... 自动汇编... - - - + + + Add Address 添加地址 - + Add Address... 添加地址... - - + + Add Group 添加分组 - + Enable all entries 启用所有条目 - + Disable all entries 禁用所有条目 - + Settings... 设置... - + Create Process... 创建进程... - + Open file as process 作为进程打开文件 - + Executables (*) 可执行文件 (*) - + Create Process 创建进程 - + fork() failed. fork() 失败。 - + Process/System Info 进程/系统信息 - + Process and System Info 进程与系统信息 - + Process 进程 - + No process is open. Use File > Open Process. 当前没有打开的进程。请使用“文件 > 打开进程”。 - + Name: 名称: - - + + PID: PID: - + Executable: 可执行文件: - + (unknown) (未知) - + Kind: 类型: - + Program: 程序: - + %1, %2-byte pointers, %3 %1,%2 字节指针,%3 - + Modules: 模块: - + Threads: 线程: - + System 系统 - + Kernel: 内核: - + CPUs: CPU: - + not present (Yama disabled) 不存在(Yama 已禁用) - + ptrace scope: ptrace 范围: - + Qt: Qt: - + Pause the process 暂停进程 - + Pause requires a live local process target 暂停需要活动的本地进程目标 - + Pause target while scanning 扫描时暂停目标 - + Suspend the target during each First/Next Scan so its memory is a consistent snapshot, then resume it. 在每次首次/再次扫描期间挂起目标,使其内存保持一致快照,然后恢复。 - + File Patcher... 文件修补器... - + Save Memory Region... 保存内存区域... - - + + Save Memory Region 保存内存区域 - - - - - - - - - + + + + + + + + + Open a process first. 请先打开一个进程。 - + Save a range of the target's memory to a file (up to 64 MiB). Addresses are hexadecimal; To is exclusive. 将目标内存的一段保存到文件(最多 64 MiB)。地址为十六进制,结束地址不包含在内。 - + e.g. 7f3a00000000 例如 7f3a00000000 - + e.g. 7f3a00001000 例如 7f3a00001000 - - + + From: 起始地址: - - + + To: 结束地址: - + "To" must be greater than "From". “To”必须大于“From”。 - + Region too large (max 64 MiB). 区域过大(最大 64 MiB)。 - + Save memory region 保存内存区域 - - - - + + + + The process is no longer open. 该进程已不再处于打开状态。 - + Could not read that region. 无法读取该区域。 - + Could not write %1. 无法写入 %1。 - - + + Saved 已保存 - + Wrote %1 to %2. 已将 %1 写入 %2。 - + Memory Allocations... 内存分配... - - + + Memory Allocations 内存分配 - + Memory reserved in the target from here (readable, writable and executable). Double-click a block to browse it. 从这里开始在目标中保留的内存(可读、可写、可执行)。双击某个块即可浏览它。 - + No blocks allocated yet. Click Allocate to reserve memory in the target. 尚未分配任何块。 点击“分配”以在目标中保留内存。 - - - + + + Allocate 分配 - - + + Free 释放 - + Free the selected block 释放选中的块 - - - - - - - - + + + + + + + + Close 关闭 - + Size (bytes): 大小(字节): - + Could not allocate %1 in the target: %2 无法在目标中分配 %1:%2 - + Could not free the block: %1 无法释放该块:%1 - + Generate Trainer (executable)... 生成修改器(可执行文件)... - + Generate Trainer executable 生成修改器可执行文件 - + Trainer executable written to %1 修改器可执行文件已写入 %1 - - + + Generate Trainer 生成修改器 - + Structure Spider... 结构搜索器... - - - - + + + + Structure Spider 结构搜索器 - + Address of the value to find pointer paths to (hex): 要查找指针路径的数值地址(十六进制): - + "%1" is not a hexadecimal address. “%1”不是十六进制地址。 - + Structure spider 结构搜索器 - + Searching for pointer paths to %1... 正在搜索指向 %1 的指针路径... - + unexpected error 意外错误 - + Dissect structure at target 在目标处解析结构 - - - + + + Cancel 取消 - + Cancelling... 正在取消... - + Searching for pointer paths to %1... %2% 正在搜索指向 %1 的指针路径... %2% - + The pointer scan failed: %1 指针扫描失败:%1 - + Cancelled. 已取消。 - + %1 pointer path(s) to %2: %1 条指向 %2 的指针路径: - + (the scan stopped at a limit, so some paths may be missing) (扫描因达到上限而停止,因此可能缺少部分路径) - + Group Scan Generator... 分组扫描生成器... - - - + + + Group Scan Generator 分组扫描生成器 - + Run a scan and select results first. 请先运行扫描并选择结果。 - + Select two or more addresses in the scan results first (Ctrl+click or Shift+click). The generator builds a pattern from their offsets and values. 请先在扫描结果中选择两个或更多地址(Ctrl+单击或 Shift+单击)。生成器会根据它们的偏移和数值构建模式。 - + Grouped-scan pattern (offset:value, 4-byte): 分组扫描模式(偏移:数值,4 字节): - - + + Copy 复制 - + Save Disassembly... 保存反汇编... - - - - - - - + + + + + + + Save Disassembly 保存反汇编 - + From (hex): 起始地址(十六进制): - + To (hex): 结束地址(十六进制): - + Invalid or too-large range. 范围无效或过大。 - + Could not read that range. 无法读取该范围。 - + Save disassembly 保存反汇编 - + Wrote %1 instructions to %2. 已将 %1 条指令写入 %2。 - + Wait for process... 等待进程... - + Wait for process 等待进程 - + Name (or part of the name) of the program to wait for. Cheat Engine opens it as soon as it starts: 要等待的程序名称(或名称的一部分)。 Cheat Engine 会在它启动时立即打开它: - + Watching for '%1'… 正在监视“%1”… - + Snapshot 快照 - + Capture writable regions 捕获可写区域 - - - - - + + + + + No process 没有进程 - + Open a process before capturing a snapshot. 请先打开进程再捕获快照。 - + Snapshot failed 快照失败 - + Captured %1 region(s), %2 total. 已捕获 %1 个区域,共 %2。 - + The snapshot stopped at its %1 limit: writable memory after that point is not included. @@ -3972,43 +3982,43 @@ The snapshot stopped at its %1 limit: writable memory after that point is not in 快照在达到 %1 上限时停止:该点之后的可写内存不包含在内。 - + Snapshot captured 已捕获快照 - + Diff against current 与当前比较 - - + + Need both 需要两者 - + Capture a baseline snapshot first, then open a process. 请先捕获基线快照,然后打开进程。 - + Diff failed 比较失败 - + Diff 比较 - + No byte-level differences detected. 未检测到字节级差异。 - + 1 byte changed since the baseline. @@ -4017,7 +4027,7 @@ The snapshot stopped at its %1 limit: writable memory after that point is not in - + %1 bytes changed since the baseline. @@ -4026,54 +4036,54 @@ The snapshot stopped at its %1 limit: writable memory after that point is not in - + (%1 more not shown.) (另有 %1 条未显示。) - + Snapshot Diff 快照比较 - + Restore baseline 恢复基线 - + Capture a baseline snapshot first. 请先捕获基线快照。 - + Restore? 恢复? - + Write the baseline back into the process? This overwrites the captured memory that is still mapped writable (%1 regions, %2). 要将基线写回进程吗?这会覆盖仍映射为可写的已捕获内存(%1 个区域,%2)。 - + Restore 恢复 - + Restore failed 恢复失败 - + Wrote %1 bytes back into the process. 已将 %1 字节写回进程。 - + %1 bytes were skipped: that memory is no longer mapped, or is now read-only or executable. @@ -4082,280 +4092,280 @@ The snapshot stopped at its %1 limit: writable memory after that point is not in 跳过了 %1 字节:该内存已不再映射,或现在为只读或可执行。 - + Restored 已恢复 - + Save to file... 保存到文件... - + No snapshot 没有快照 - + Capture a snapshot first. 请先捕获快照。 - + Save snapshot 保存快照 - - + + Snapshot (*.cesnap);;All (*) 快照 (*.cesnap);;所有文件 (*) - + Save failed 保存失败 - + Could not write %1 无法写入 %1 - + Load from file... 从文件加载... - + Load snapshot 加载快照 - + Load failed 加载失败 - + Snapshot loaded 快照已加载 - + Loaded %1 region(s), %2 bytes. 已加载 %1 个区域,%2 字节。 - + Overlay... 覆盖层... - + Speedhack... 变速器... - + Speedhack 变速器 - + Scales the time the game sees. Apply loads the speedhack library into the process the first time. 缩放游戏所感受到的时间。首次应用时会把变速库加载到进程中。 - + Speed 速度 - + Not applied yet. 尚未应用。 - + Apply 应用 - + The speedhack works on local processes only. 变速器仅适用于本地进程。 - + Inject failed: libspeedhack.so is not installed next to Cheat Engine. 注入失败:libspeedhack.so 未安装在 Cheat Engine 旁边。 - + Inject failed: %1 注入失败:%1 - + Active: the game runs at %1×. 已激活:游戏以 %1× 速度运行。 - + &Help 帮助(&H) - + Cheat Engine Help Cheat Engine 帮助 - + Cheat Engine for Linux: Help Cheat Engine for Linux:帮助 - + Lua documentation Lua 文档 - + Lua / Scripting documentation Lua / 脚本文档 - + Tutorial not found 未找到教程 - + Could not start %1. It is installed next to cheatengine. 无法启动 %1。它安装在 cheatengine 旁边。 - + Cheat Engine Tutorial... Cheat Engine 教程... - + Graphical Tutorial... 图形教程... - + About 关于 - + About Cheat Engine for Linux 关于 Cheat Engine for Linux - + <h2 style='margin-bottom:2px'>Cheat Engine for Linux</h2><p style='margin-top:0'>Version %1</p><p>A memory scanner, debugger and code injection tool for native Linux, Wine and Proton programs.</p><p>Built with Qt %2, Capstone, Keystone and Lua 5.3.<br>Free and open source under the MIT license.</p><p><a href='https://github.com/wleeaf/cheat-engine-linux'>github.com/wleeaf/cheat-engine-linux</a></p> <h2 style='margin-bottom:2px'>Cheat Engine for Linux</h2><p style='margin-top:0'>版本 %1</p><p>适用于原生 Linux、Wine 和 Proton 程序的内存扫描、调试与代码注入工具。</p><p>基于 Qt %2、Capstone、Keystone 和 Lua 5.3 构建。<br>以 MIT 许可证免费开源。</p><p><a href='https://github.com/wleeaf/cheat-engine-linux'>github.com/wleeaf/cheat-engine-linux</a></p> - + Open on GitHub 在 GitHub 上打开 - + Table Comments 表格注释 - + Notes about this cheat table: how to use it, what each entry does, which game version it was made for. They are saved with the table. 关于此作弊表格的备注:如何使用它、每个条目的作用,以及它是为哪个游戏版本制作的。这些备注会随表格一起保存。 - + Cheat Table Lua Script 表格 Lua 脚本 - + -- Lua that runs when this table is opened; saved with the table. -- 打开此表格时运行的 Lua;随表格一起保存。 - + Execute script 执行脚本 - + Script executed. 脚本已执行。 - + Error: %1 错误:%1 - + Overlay 覆盖层 - + A transparent, click-through layer drawn over the whole screen, for use with a game in borderless or windowed mode. 绘制在整个屏幕上的透明、可穿透点击的图层,用于无边框或窗口模式下的游戏。 - + Status text (process, active cheats, frame rate) 状态文字(进程、已启用的作弊、帧率) - + Crosshair in the centre of the screen 屏幕中央的准星 - + Show Overlay 显示覆盖层 - + Hide Overlay 隐藏覆盖层 - + %1 | Records %2/%3 active %1 | 记录 %2/%3 已启用 - + Open process 打开进程 - - + + No process selected 未选择进程 - - + + Press Ctrl+O or click the magnifier to choose a process. 按 Ctrl+O 或点击放大镜选择进程。 - + No results yet. Enter a value and press First Scan, @@ -4366,7 +4376,7 @@ or open a process with Ctrl+O. 或用 Ctrl+O 打开进程。 - + No results yet. Enter a value and press First Scan. @@ -4375,7 +4385,7 @@ Enter a value and press First Scan. 输入数值后按“首次扫描”。 - + No results yet. Open a process with Ctrl+O, @@ -4386,645 +4396,645 @@ then enter a value and press First Scan. 然后输入数值并按“首次扫描”。 - + Add %1 selected addresses to the address list 将选中的 %1 个地址添加到地址列表 - + Add selected address to the address list 将选中的地址添加到地址列表 - - + + Browse this memory region 浏览此内存区域 - + Disassemble this memory region 反汇编此内存区域 - + Copy %1 addresses 复制 %1 个地址 - + Copy address 复制地址 - + Remove %1 selected addresses from the results 从结果中移除选中的 %1 个地址 - + Remove selected address from the results 从结果中移除选中的地址 - + Find out what accesses this address 查找访问此地址的内容 - + Find out what writes to this address 查找写入此地址的内容 - + Remove from results 从结果中移除 - - - - - + + + + + Found: 0 已找到:0 - - + + Hex 十六进制 - + Show result values in hexadecimal 以十六进制显示结果数值 - + Memory View 内存视图 - + Browse the selected process's memory 浏览所选进程的内存 - + Add address 添加地址 - + Manual entry 手动输入 - + Could not resolve "%1". 无法解析“%1”。 - + Value: 数值: - + and 和 - + Interpret integer scan values as hexadecimal 将整数扫描数值按十六进制解释 - + Exact Value 精确数值 - + Bigger than... 大于... - + Smaller than... 小于... - + Value between... 数值介于... - + Unknown initial value 未知初始值 - + Increased value 增加的数值 - + Decreased value 减少的数值 - + Changed value 变化的数值 - + Unchanged value 未变化的数值 - + Same as first scan 与首次扫描相同 - + Increased value by... 数值增加了... - + Decreased value by... 数值减少了... - - + + Byte 字节 - - + + 2 Bytes 2 字节 - - + + 4 Bytes 4 字节 - - + + 8 Bytes 8 字节 - - + + Float 浮点数 - - + + Double 双精度浮点数 - + String 字符串 - + Unicode String Unicode 字符串 - + Array of byte 字节数组 - + Binary 二进制 - + All 全部 - - + + Pointer 指针 - + Grouped 分组 - + Custom 自定义 - + Exact uses stored precision. Rounded uses the precision of your input. Truncated compares integer parts. Extreme uses a tolerance. 精确使用存储的精度。取整使用输入值的精度。截断比较整数部分。极值使用容差。 - + Exact 精确 - + Rounded 取整 - + Truncated 截断 - + Extreme 极值 - + Absolute difference allowed in Extreme mode. Leave blank or use 0 for automatic tolerance. 极值模式下允许的绝对差值。留空或使用 0 表示自动容差。 - + Tolerance 容差 - - + + e.g. 100 例如 100 - + e.g. 3.14 例如 3.14 - + text to find 要查找的文本 - + e.g. 48 8B ?? 05 例如 48 8B ?? 05 - + e.g. 0110??01 例如 0110??01 - + e.g. 0x7fabc000 例如 0x7fabc000 - - + + First Scan 首次扫描 - + Next Scan 再次扫描 - + Undo Scan 撤销扫描 - + Memory Scan Options 内存扫描选项 - + Writable 可写 - + Checked: only writable regions. Unchecked: only non-writable. Grey: any (don't care). 选中:仅可写区域。未选中:仅不可写区域。灰色:任意(不关心)。 - + Executable 可执行 - + Checked: only executable regions. Unchecked: only non-executable. Grey: any (don't care). 选中:仅可执行区域。未选中:仅不可执行区域。灰色:任意(不关心)。 - + Fast Scan 快速扫描 - + Compare by % 按百分比比较 - + Percent max: 最大百分比: - + Case sensitive (text) 区分大小写(文本) - + Private 私有 - + Scan MEM_PRIVATE regions (anonymous heap, stack, private maps) 扫描 MEM_PRIVATE 区域(匿名堆、栈、私有映射) - + Image 映像 - + Scan MEM_IMAGE regions (loaded executable modules) 扫描 MEM_IMAGE 区域(已加载的可执行模块) - + Mapped 映射 - + Scan MEM_MAPPED regions (other file-backed maps) 扫描 MEM_MAPPED 区域(其他文件映射) - + Text encoding: 文本编码: - + Data order: 数据字节序: - - - - + + + + Auto (target) 自动(目标) - - + + Little-endian 小端序 - - + + Big-endian 大端序 - + Choose the encoding of the data being scanned. Auto uses the selected target's data order. 选择被扫描数据的字节序。自动使用所选目标的数据字节序。 - + Pointer size: 指针大小: - - + + 4 bytes 4 字节 - - + + 8 bytes 8 字节 - + Size of encoded pointers for Pointer, Grouped and Custom scans. A different saved record size requires a New Scan. 指针、分组和自定义扫描所用的指针大小。不同的已保存记录大小需要新扫描。 - + No saved addresses. Double-click a result or use Add Address. 没有已保存的地址。 双击结果或使用“添加地址”。 - + Value at 0x%1 was reverted (wrote %2, now %3). It looks protected: right-click the entry and choose "Find what writes" to locate the code that overwrites it. 0x%1 处的数值已被回退(写入 %2,现在为 %3)。它似乎受到了保护:右键单击该条目并选择“查找什么写入”来定位覆盖它的代码。 - + Edit script... 编辑脚本... - + Change address... 更改地址... - + Cut 剪切 - - + + Paste 粘贴 - + Copy field 复制字段 - + Description 描述 - - + + Address 地址 - + Value 数值 - + Script 脚本 - + Delete 删除 - + Arrange 排列 - + Indent 增加缩进 - + Outdent 减少缩进 - + Add to new group... 添加到新分组... - - - + + + New group 新建分组 - + Group name: 分组名称: - + Move Up 上移 - + Move Down 下移 - + Set value... 设置数值... - + New value for 1 selected entry: 为 1 个选中条目设置新数值: - + New value for %1 selected entries: 为 %1 个选中条目设置新数值: - + Set value 设置数值 - + Freeze Mode 冻结模式 - + Normal 常规 - + Increase Only 仅增加 - + Decrease Only 仅减少 - + Never Increase 不增加 - + Never Decrease 不减少 - + Display 显示 - + Set value codec... 设置数值编解码器... - + Value codec 数值编解码器 - + How the game obfuscates this value in memory: none, xor:0xKEY, add:N, rol:N or ror:N (e.g. xor:0x5A). @@ -5035,92 +5045,92 @@ The list shows the decoded value; edits and freezes are encoded back. 列表显示解码后的数值;编辑和冻结会重新编码。 - + Invalid codec 编解码器无效 - + Use none, xor:0xKEY, add:N, rol:N, or ror:N. 请使用 none、xor:0xKEY、add:N、rol:N 或 ror:N。 - + Data order 数据字节序 - + Pointer size 指针大小 - + Change type 更改类型 - + Show as hexadecimal 以十六进制显示 - + Convert to module+offset 转换为模块+偏移 - + (%1 skipped) (跳过 %1 个) - + Converted 1 address to module+offset%1 已将 1 个地址转换为模块+偏移%1 - + Converted %1 addresses to module+offset%2 已将 %1 个地址转换为模块+偏移%2 - + Find what accesses this address 查找访问此地址的内容 - + Find what writes to this address 查找写入此地址的内容 - + Pointer scan for this address 对此地址进行指针扫描 - + Change Color... 更改颜色... - + Change record color 更改记录颜色 - + Dropdown options... 下拉选项... - + Dropdown options 下拉选项 - + The values this entry can be set to from a dropdown, one per line, as value:label or just value. For example: 0:Off @@ -5131,139 +5141,139 @@ as value:label or just value. For example: 1:开 - + Hotkeys 快捷键 - + Toggle hotkey... 切换快捷键... - + Configure Hotkey 配置快捷键 - + Clear 清除 - + Press the key combination that turns this entry on and off. It works while the game has focus. 按下可开启和关闭此条目的按键组合。它在游戏获得焦点时也有效。 - + Value hotkeys... 数值快捷键... - + Configure Value Hotkeys 配置数值快捷键 - + value to set 要设置的数值 - + Increase 增加 - + Decrease 减少 - + Step 步长 - + Set value hotkey 设置数值快捷键 - + Set to value 设置为数值 - + Add Address Manually... 手动添加地址... - - - + + + No description 无描述 - + Enable all 全部启用 - + Disable all 全部禁用 - + Scan Value 扫描数值 - + Scan Type 扫描类型 - + Value Type 数值类型 - + Rounding 取整方式 - + Advanced Options 高级选项 - + Table Extras 表格附加项 - + Auto-attached to %1 (pid %2) 已自动附加到 %1(PID %2) - + Found %1 (pid %2) but could not open it 找到 %1(PID %2),但无法打开 - + Attached to %1: %2 已附加到 %1:%2 - + Program: %1, %2-byte pointers, %3 @@ -5274,37 +5284,37 @@ Host ABI: %4 主机 ABI:%4 - + Target already being traced 目标已被跟踪 - + data order unknown 数据字节序未知 - + %1 data %1 数据 - + %1-bit pointers %1 位指针 - + pointer width unknown 指针宽度未知 - + GDB: %1 GDB:%1 - + Guest CPU: %1 Data order: %2 Program pointers: %3 @@ -5315,188 +5325,188 @@ Transport: GDB 传输方式:GDB - + Connected to GDB; no memory map. Reconnect with a known scan range. 已连接到 GDB;没有内存映射。请使用已知的扫描范围重新连接。 - + Connected to GDB target 已连接到 GDB 目标 - + Connect failed: %1 连接失败:%1 - + Handshake failed: %1 握手失败:%1 - + Server refused PID %1 服务器拒绝了 PID %1 - + The current target is still in use: wait for the scan to finish or disable its scripts, then connect again. 当前目标仍在使用中:请等待扫描结束或禁用其脚本,然后再次连接。 - + Remote process %1 远程进程 %1 - + Connect to ceserver 连接到 ceserver - + Attach to a process on another machine (or in a container) through a running ceserver. 通过运行中的 ceserver 附加到另一台机器(或容器)上的进程。 - + Host: 主机: - + Port: 端口: - + process ID on the remote machine 远程机器上的进程 ID - + Connect 连接 - + Enter a valid remote PID. 请输入有效的远程 PID。 - + Connecting to %1:%2... 正在连接到 %1:%2... - + New scan: pick a value type and value, then First Scan. 新扫描:选择数值类型和数值,然后执行首次扫描。 - - + + No process attached; open one first (Ctrl+O) to scan. 未附加进程;请先打开一个进程(Ctrl+O)再扫描。 - + Cannot read the process 无法读取进程 - - + + Scan range 扫描范围 - + From/To must be a hex address, symbol, or module+offset (e.g. target2, target2+0x5000). 起始/结束地址必须是十六进制地址、符号或模块+偏移(例如 target2、target2+0x5000)。 - + The From address must not be greater than the To address. 起始地址不得大于结束地址。 - - + + Array of byte scan 字节数组扫描 - - + + Enter a valid AOB pattern (e.g. "7F 45 ?? 46"). 请输入有效的 AOB 模式(例如“7F 45 ?? 46”)。 - - + + Binary scan 二进制扫描 - - + + Enter a valid binary pattern (e.g. "0110??01"). 请输入有效的二进制模式(例如“0110??01”)。 - - + + Grouped scan 分组扫描 - - + + Floating scan 浮点扫描 - - - - + + + + Enter valid numeric values for the selected comparison. 请为所选比较输入有效的数值。 - - + + All scan 全部扫描 - - + + Floating tolerance 浮点容差 - - + + Enter a finite nonnegative tolerance. 请输入有限的非负容差。 - - - + + + Scan failed 扫描失败 - + The scan could not start: %1 扫描无法开始:%1 - + The scan could not complete: %1 Try narrowing the From/To range, enabling Fast Scan, or choosing a more specific value type. @@ -5505,191 +5515,195 @@ Try narrowing the From/To range, enabling Fast Scan, or choosing a more specific 请尝试缩小起始/结束范围、启用快速扫描,或选择更具体的数值类型。 - - + + Scan results truncated 扫描结果被截断 - - - A scan-result file could not be fully written (the disk may be full). Some results are unreliable; free space and scan again. - 扫描结果文件无法完整写入(磁盘可能已满)。部分结果不可靠;请释放空间后重新扫描。 - - - - + + Scan results are incomplete. 扫描结果不完整。 - + First scan complete. %1 首次扫描完成。%1 - + Run a First Scan before a Next Scan. 请先运行首次扫描,再运行再次扫描。 - + The next scan could not complete: %1 再次扫描无法完成:%1 - + Next scan complete. %1 再次扫描完成。%1 - + + Language + 语言 + + + + Cheat Engine could not restart itself. Start it again to apply the language. + Cheat Engine 无法自行重启,请手动启动以应用语言。 + + + Unsaved changes 未保存的更改 - + The cheat table has changes that are not saved. Save them? 作弊表格有尚未保存的更改。要保存吗? - + Auto Assembler: %1 自动汇编:%1 - + Save to Table 保存到表格 - + Create Trainer (C source) 创建修改器(C 源码) - + C source (*.c);;All Files (*) C 源码 (*.c);;所有文件 (*) - - + + Create Trainer 创建修改器 - + Could not write the trainer source file. 无法写入修改器源码文件。 - + Trainer C source written to %1 (compile with gcc) 修改器 C 源码已写入 %1(使用 gcc 编译) - + Trainer generation failed: %1 修改器生成失败: %1 - - + + No scan results to save. 没有可保存的扫描结果。 - + Save all current scan results 保存所有当前扫描结果 - + Text (*.txt);;CSV (*.csv);;All files (*) 文本 (*.txt);;CSV (*.csv);;所有文件 (*) - + Saving all scan results... 正在保存所有扫描结果... - + Scan results changed; export canceled. 扫描结果已变化;导出已取消。 - + Export canceled. 导出已取消。 - + Could not save scan results: %1 无法保存扫描结果:%1 - + Saved %1 results to %2 已将 %1 条结果保存到 %2 - - + + Save Cheat Table 保存作弊表格 - + Could not save %1: %2 无法保存 %1: %2 - + Invalid table data or incomplete write. 表格数据无效或写入不完整。 - + Invalid table metadata. 表格元数据无效。 - + Saved table to %1 已将表格保存到 %1 - + Unknown save error. 未知的保存错误。 - - - - - - - - - - + + + + + + + + + + Load Cheat Table 加载作弊表格 - + Execute this lua script? 执行此 Lua 脚本? - + This cheat table contains a Lua script. Only execute scripts from tables you trust, a table's Lua can hook and manipulate the target. Execute it? @@ -5698,86 +5712,86 @@ Only execute scripts from tables you trust, a table's Lua can hook and mani 仅执行来自你信任的表格的脚本,表格中的 Lua 可以挂钩和操控目标。要执行吗? - + Table Lua error: %1 表格 Lua 错误:%1 - + (no recent tables) (无最近表格) - + (missing) (缺失) - + Clear list 清除列表 - + Protected Cheat Table 受保护的作弊表格 - + This table is password-protected (.CETRAINER). Enter its password: 此表格受密码保护(.CETRAINER)。 请输入密码: - + Could not decrypt the table (wrong password or corrupt file). 无法解密表格(密码错误或文件损坏)。 - + Could not read a valid cheat table. 无法读取有效的作弊表格。 - + Invalid JSON cheat table. 无效的 JSON 作弊表格。 - + Invalid JSON record. 无效的 JSON 记录。 - + Invalid JSON record data. 无效的 JSON 记录数据。 - + Failed to load table: %1 加载表格失败: %1 - + Failed to load table (unknown error). 加载表格失败(未知错误)。 - + Monitoring the instruction at 0x%1… 正在监视 0x%1 处的指令… - + What this instruction accesses 此指令访问的内容 - + No data accesses were recorded for the instruction at 0x%1. It may not have run during the sample window, or it has no memory operand. @@ -5786,47 +5800,47 @@ It may not have run during the sample window, or it has no memory operand. - + Addresses accessed by 0x%1 被 0x%1 访问的地址 - + %1 distinct address(es) accessed (double-click to add to the list): 访问了 %1 个不同地址(双击可添加到列表): - + Hits 命中次数 - + Accessed address 访问的地址 - + %1 address(es) accessed by 0x%2 %1 个地址被 0x%2 访问 - + autorun %1: %2 自动运行 %1:%2 - + Ran %1 autorun script(s). 已运行 %1 个自动运行脚本。 - + Wine / Proton game Wine / Proton 游戏 - + This is a Wine/Proton (Windows) game. Cheat Engine will watch the game's main thread for writes, which is where gameplay values (money, HP, …) are usually changed. If nothing shows up, the value may be written by a background thread; tell me and it can be widened. Start monitoring? @@ -5835,168 +5849,168 @@ If nothing shows up, the value may be written by a background thread; tell me an 如果没有出现任何结果,该数值可能是由后台线程写入的;告诉我即可扩大监视范围。要开始监视吗? - + Code finder unavailable 代码查找器不可用 - + Find what writes 查找写入 - + Find what accesses 查找访问 - + code 0x%1 代码 0x%1 - + No process attached; open one first (Ctrl+O) to view memory. 未附加进程;请先打开一个进程(Ctrl+O)再查看内存。 - + Add Mono field 添加 Mono 字段 - + %1 for '%2' (field at +0x%3): %1:%2(字段位于 +0x%3): - + Static data address of the class 类的静态数据地址 - + Object base address 对象基地址 - + The debugger could not attach; no breakpoint was set. 调试器无法附加;未设置断点。 - + Pause the target in the debugger to set a breakpoint. 在调试器中暂停目标以设置断点。 - + Execute breakpoints are software breakpoints in this debugger. 在此调试器中,执行断点是软件断点。 - + Could not remove the breakpoint; see the debugger window. 无法移除断点;请查看调试器窗口。 - + Added 0x%1 to the cheat table 已将 0x%1 添加到作弊表格 - + Memory Regions 内存区域 - + Heap list 堆列表 - + Module list 模块列表 - + Referenced strings / functions 引用的字符串 / 函数 - + Thread list 线程列表 - + Stacktrace 调用栈 - + Graphical memory view 图形内存视图 - + Registers 寄存器 - + Breakpoint list 断点列表 - + Full debugger 完整调试器 - + Break and trace 中断并跟踪 - + Branch mapper (LBR) 分支映射器(LBR) - - + + Dissect data/structures... 剖析数据/结构... - + Pointer scan... 指针扫描... - + Emulator guest scan... 模拟器客户机扫描... - + Find static addresses... 查找静态地址... - + Lua Engine... Lua 引擎... - + ELF Inspector... ELF 检查器... - + New Scan 新扫描 @@ -7452,17 +7466,17 @@ to 0x%3? ce::gui::ScanResultsModel - + Address 地址 - + Value 数值 - + Previous 先前值 @@ -7722,452 +7736,452 @@ or write an Auto Assembler script here. ce::gui::SettingsDialog - + Settings 设置 - - + + Scan 扫描 - - + + Display 显示 - - - + + + Language 语言 - - + + Memory View 内存查看 - - + + Debugger 调试器 - - + + Hotkeys 快捷键 - - + + Network 网络 - - + + Lua Lua - - + + AI Assistant AI 助手 - + OK 确定 - + Cancel 取消 - + Apply 应用 - + Defaults for a new scan. The main window's scan options start from these. 新扫描的默认设置。主窗口的扫描选项初始值来自这里。 - + Byte 字节 - + 2 Bytes 2 字节 - + 4 Bytes 4 字节 - + 8 Bytes 8 字节 - + Float 浮点数 - + Double 双精度浮点数 - + String 字符串 - + Array of byte 字节数组 - + Pointer 指针 - + Value type: 数值类型: - + bytes 字节 - + Fast scan alignment: 快速扫描对齐: - + Fast scan (only aligned addresses) 快速扫描(仅对齐地址) - + Only writable memory 仅可写内存 - + Only executable memory 仅可执行内存 - + Automatic (one per CPU) 自动(每个 CPU 一个) - + Worker threads: 工作线程: - + Automatic 自动 - + Float tolerance: 浮点数容差: - + Used by the "Extreme" float rounding mode. 用于“极限”浮点舍入模式。 - + Theme and the font used by tables, the disassembler and hex views. 主题,以及表格、反汇编器和十六进制视图使用的字体。 - + Light 浅色 - + Dark 深色 - + Theme: 主题: - + Monospace font: 等宽字体: - + pt pt - + Font size: 字号: - + 32-bit (8 hex digits) 32 位(8 位十六进制) - + 64-bit (16 hex digits) 64 位(16 位十六进制) - + Address width: 地址宽度: - + Uppercase hexadecimal (0x7F3A, not 0x7f3a) 大写十六进制(0x7F3A,而非 0x7f3a) - + Language of the interface. A restart is offered when it changes. 界面语言。更改时会提示重启。 - + Follow system 跟随系统 - + Interface language: 界面语言: - + English and Simplified Chinese are available for now. 目前提供英文和简体中文。 - + Layout and refresh of the hex dump in the Memory Viewer. 内存查看器中十六进制转储的布局与刷新。 - + Bytes per row: 每行字节数: - + Refresh values while the view is open 视图打开时刷新数值 - + ms ms - + Refresh interval: 刷新间隔: - + Applies to Memory Viewer windows opened from now on. 对之后打开的内存查看器窗口生效。 - + Defaults for hardware watchpoints. 硬件监视点的默认设置。 - + Data breakpoint size: 数据断点大小: - + Find-what-writes size: “查找写入”大小: - + How many bytes from the address a watchpoint covers. x86 watchpoints must be aligned to their size. 监视点覆盖地址起的多少字节。x86 监视点必须按其大小对齐。 - + Hotkeys belong to cheat table entries: right-click an entry and choose Hotkeys. 快捷键属于表格条目:右键单击条目并选择“快捷键”。 - + Default value step: 默认数值步长: - + How much an increase or decrease hotkey changes a value, until the entry sets its own step. 增加或减少快捷键每次改变多少数值,直到条目设置自己的步长。 - + The ceserver that File > Connect to ceserver offers by default. “文件 > 连接到 ceserver”默认提供的 ceserver。 - + host name or IP address 主机名或 IP 地址 - + ceserver host: ceserver 主机: - + ceserver port: ceserver 端口: - + Scripts in the autorun folder run once at startup, like Cheat Engine's autorun. autorun 文件夹中的脚本在启动时运行一次,与 Cheat Engine 的 autorun 相同。 - + Open folder 打开文件夹 - + Autorun folder: autorun 文件夹: - + Lua cannot run shell commands or write local files unless Cheat Engine was started with CECORE_LUA_ALLOW_UNSAFE=1 in its environment. A cheat table cannot turn this on by itself. 除非启动 Cheat Engine 时在环境中设置了 CECORE_LUA_ALLOW_UNSAFE=1,否则 Lua 无法执行 shell 命令或写入本地文件。作弊表本身无法开启此功能。 - + The model behind Tools > AI Assistant. You bring the API key, or run a model on your own computer. “工具 > AI 助手”使用的模型。你可以自带 API 密钥,也可以在本机运行模型。 - + Anthropic (Claude) Anthropic (Claude) - + OpenAI-compatible (OpenAI, Ollama, LM Studio) 兼容 OpenAI(OpenAI、Ollama、LM Studio) - + Provider: 提供商: - + Model: 模型: - + API address: API 地址: - + Paste a new key to save it 粘贴新密钥以保存 - + Forget key 忘记密钥 - + API key: API 密钥: - + Keys are kept in the system keyring, never in Cheat Engine's settings file; ANTHROPIC_API_KEY or OPENAI_API_KEY in the environment take precedence. A saved key is only sent to the API address it was entered with. Your messages and what the tools find in the game are sent to this provider, and every change to the game waits for your Allow. 密钥保存在系统密钥环中,而不是 Cheat Engine 的设置文件里;环境变量 ANTHROPIC_API_KEY 或 OPENAI_API_KEY 优先。已保存的密钥只会发送到输入它时使用的 API 地址。你的消息以及工具在游戏中发现的内容会发送给该提供商,而对游戏的每次更改都会等待你的允许。 - + Could not remove the key: %1 无法删除密钥:%1 - + for example gpt-5 or qwen3 例如 gpt-5 或 qwen3 - + https://api.openai.com/v1 or http://localhost:11434/v1 https://api.openai.com/v1 或 http://localhost:11434/v1 - + Kept until Cheat Engine closes: %1 仅在 Cheat Engine 关闭前有效:%1 - + Restart Cheat Engine to apply the new interface language. 重启 Cheat Engine 以应用新的界面语言。 - + Save the table first if you need the current addresses and scan results. 如需保留当前的地址列表和扫描结果,请先保存表格。 - + Restart now 立即重启 - + Later 稍后