Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 \
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

---

Expand Down
23 changes: 23 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
103 changes: 103 additions & 0 deletions docs/I18N.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# 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`; 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.
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
(`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_<locale>.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.
`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.
19 changes: 19 additions & 0 deletions docs/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 12 additions & 12 deletions gui/advancedoptions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -99,15 +99,15 @@ 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))
nopOriginals_.erase(it);
} 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);
Expand Down
Loading
Loading