Randa Meetings 2016 Part III: Translation System Troubles

[[Sic, 2016. This post about the results of studying the situation with the translation systems in software by KDE during the Randa event in 2016 had been a draft all the time due to being complicated matter and partly only lamenting about broken things instead of reporting world improvements. Still the lamenting might provide information unknown to some, so time to simply dump the still valid bits (quite some bits happily have been fixed since and are omitted, same for those notes I could no longer make sense off), even without a proper story thread. So if you ever wondered about the magic happening behind the scenes to get translations to appear in software based on Qt & KF, sit down and read on.]]

IMHO new technology should adapt by default to our cultures. Not our cultures to technology, especially if the technology is enforced on us by law or other pressures. Surely technology should instead allow to enhance cultures, extending options at best. But never limiting or disabling.

One motivation to create technology like Free/Libre Software is the idea that every human should be able to take part in the (world) society. A challenge here is to enrich life for people, not to just make it more complex. Also should it not force them into giving up parts of culture for the access to new technology. Code is becoming the new law maker: if your existing cultural artifacts are not mapped in the ontology of the computer systems, it does not exist by their definition. One has to be happy already then if there is at least any “Other” option to file away with.

Sure, e.g. for producers targeting more than their local home culture it would be so nice-because-simple if everyone would use the same language and other cultural artifacts (think measurement units). But who is to decide what should be the norm and how should they know what is best?

When it comes to software, the so called “Internationalization” technologies are here to help us in the humanity being. Adding variability to the user interface to allow adaption to the respective user’s culture (so called “Localization”).
Just, for some little irony, there is also more than one “culture” with Internationalization technologies. And getting those into synchronized cooperation is another challenge, sadly one which is currently not completely mastered when it comes to the software by the KDE community.

Multiple translation systems in one application


On Linux (and surely other *nixoid systems) traditionally gettext is used. Whose translation lookup code is either part of glibc or in a separate LibIntl. For a running executable the localization to choose is controlled by the environment variables “LANGUAGE” (GNU gettext specific), “LC_*” and “LANG” (see GNU gettext utilities documentation). Strings to be translated are grouped in so-called domains. There is one file per domain and per language, a so called “catalog”. A catalog appears in two variants, in the “Portable Object” (PO) format intended for direct editing by humans and in the “Machine Object” (MO) format intended for processing by software. For each domain optionally a separate directory can be set below which all the language catalogs belonging to that domain can be found.
On the call to the gettext API like dgettext("domain", msgid) when “LANG” is set to the id “locale” (and the other vars are not set), the translation will be taken from the file in the sub-path locale/LC_MESSAGES/domain.mo (or some less specific variant of the id “locale” until there is such a file) in the given directory for the domain.
So a library using gettext for translations has to install the catalog files in a certain directory at deploy time and, unless using the default, at execution start have that one registered for their domain (using bindtextdomain(...)). For locating and using the catalogs at run-time, an executable linking to such a library has nothing else to do to assist the library with that. And the same for the setup of translations in the code of the program itself with gettext.

Qt’s QTranslator

Qt uses another approach: one registers a set of handlers of type QTranslator which are queried one after the other if they can resolve the string to be translated. This is done by the central equivalent to the gettext API, QCoreApplication::translate(const char *context, const char *sourceText, const char *disambiguation, int n), which also if no handler could resolve the string simply returns the same string as passed in. That method is invoked indirectly from the tr(...) calls, which are class methods added to QObject subclasses via the Q_OBJECT macro, using the class name as the context.
With the Qt way, usually the caller of the translation invocation has to know which locale should be used and make sure the proper catalog handlers are registered, before doing that invocation. The Qt libraries themselves do not do those registration, it is the duty of the application linking to the Qt libraries to do that.

The Qt translation approach is not only used by all the Qt modules, but also many tier 1 modules of the KDE Frameworks. Because the KDE Frameworks module KI18n, which provides a convenience & utilitiy wrapper around gettext, is not available to them, being a tier 1 module itself.

Automagic setup of QTranslator-based translations

The classical application from the KDE spheres is traditionally developed with gettext and KI18n in mind, and thus not used to care for that registration of Qt translation handlers. To allow them staying that innocent, all libraries done by KDE using the Qt translation approach will trigger the creation and registration of the handler with their catalog themselves during loading of the library, picking a catalog matching current QLocale::system(). They are using the hook Q_COREAPP_STARTUP_FUNCTION, which evaluates to code for the definition of a global static instance of a custom structure whose constructor, invoked then after library load due to being global static instance, registers that function as startup function for the QCoreApplication (or subclass) instance or, if such instance already exists, directly calls the function. To spare the libraries’ authors writing the respective automatic loading code, KDE’s Extra CMake Modules provides the module ECMPoQmTools to have that code generated and added to the library build, by the CMake macro ecm_create_qm_loader(...).

One issue: currently the documentation of ECMPoQmTools misses to hint that generation of the handler is ensured to be done only in the main thread. In case the library is loaded in another thread, the generation code is triggered (and thus delayed) in the main thread via a timer event. This can result in race condition if other code run after loading the library in the other thread already relies on the translation handler present.

KI18n: doing automagic setup for Qt libraries translations even

The thoughtful reader may now wonder, given that KDE Frameworks modules using the Qt translation system are doing that by help of automatic loading of catalogs, whether something similar is valid for the Qt libraries itself when it comes to programs from KDE. The answer is: it depends 🙂
If the program links to the KDE Frameworks module KI18n, directly or indirectly and thus loads it, that library has code using Q_COREAPP_STARTUP_FUNCTION as well to automatically trigger the creation and deploy of the handler of the translations for the Qt libraries (see src/main.cpp). For which Qt libraries that is, see below. Otherwise, as explained before, the program has to do it explicitly.

So this is why the developers of the typical application done in the KDE community do not have to write explicit code to initiate any loading of translation catalogs.

Does it blend?

Just, the above also means that still there are two separate translation systems with different principles and rules in the same application process (if not more from 3rd-party libraries, which though usually use gettext). And that brings a set of issues, like potentially resulting in inconsistently localized UI due to different libraries having different set of localizations available or following different environment variables or internal flags to decide which localization to use (also for things like number formatting). And add to that having different teams with different guidelines doing the translations for the different libraries and programs from different organizations.

KI18n: too helpful with the Qt libraries translations sometimes

The automatic generation and deployment of the handler for the translations of Qt libraries when the KI18n library is linked to and thus loaded (as described above) is not really expected in Qt-only, not-KF-using programs. Yet, when such programs are loading plugins linking directly or indirectly KI18n, these programs will be confronted of getting the KI18n-generated handler deployed on top (and thus overriding any previously installed handler from the program itself). At best this means only duplicated handlers, but it can also mean changing the locale, as the KI18n code picks the catalog locale to use from what is QLocale::system() at the time of being run.

And such plugin can simply be the Qt platform integration plugin, which in the case of the Plasma platform integration has KI18n in the set of linked and thus loaded libraries. This issue is currently reported indirectly via Bug 215837 – Nativ KDE QFileDialog changes translation

When it comes to the Qt platform integration plugin, the use of Q_COREAPP_STARTUP_FUNCTION when being invoked via such a plugin also shows some issue in the design of the Qt startup phase, resulting in the registered function being 2x called, in this case resulting in duplicated creation of translation handlers (reported as QTBUG-54479)

Qt5: no longer one single catalog for all Qt modules

Seems in Qt4 times there was one single catalog per language for all that made up the Qt framework. In Qt5 this no longer is true though, for each language now a separate catalog file is used per Qt module. There is some backward compatibility though which has hidden this for most eyes so far, so called meta catalogs (see Linguist docs). The meta catalog qt_ does not have translations itself, but links to the catalogs qtbase_, qtscript_, qtquick1_, qtmultimedia_ and qtxmlpatterns_ (see yourself and open /usr/share/qt5/translations/qt_ll.qm, with ll your language code, e.g. de, in your favorite hex editor).

So applications which use further Qt modules, directly or indirectly, need to make sure themselves to get the respective catalogs loaded and used. Which gets complicated for those used indirectly (via plugins or indirectly linked as implementation detail of another non-Qt lib). There seems no way to know what catalogs are loaded already.

This might be important for programs from KDE using QtQuick Controls, given there exist qtquickcontrols2_*.qm files with some strings. Yet to be investigated if those catalogs are loaded via some QML mechanism perhaps, or if some handling is needed?

Juggling with catalogs on release time

The catalogs with the string translations for KDE software are maintained and developed by the translators in a database separate from the actual sources, a subversion system, partially for historic reasons.
When doing a release of KDE software, the scripts used to generate the source tarballs then do both a checkout of the sources to package as well as iterating over the translation database to download and add the matching catalogs.
KDE Frameworks extends this scheme by adding the snapshot of the translations in a commit to the source repository, using a tagged git commit off the main branch.

Issues seen:

  • which catalogs to fetch exactly based on fragile system, not exactly defined
  • which version of the database the fetched catalogs are from is not noted, tarball reproducibility from VCS not easily possible (solved for KF)
  • script accidentally added catalog files used internally by KDE translation system for in-source translations (fixed meanwhile)
  • script accidentally added by-product of translation statistic (fixed meanwhile)

Lifting some of the mystery around QT_MOC_COMPAT

((Dumping here the info collected as reminder to self, but also everyone who might wonder and search the internet. If you know a proper place to put it, please copy it there.))

When working on adding macros to control warnings by & visibility to the compiler for deprecated API in the KDE Frameworks modules, a certain C++ preprocessor macro has been found in some places in the code: QT_MOC_COMPAT. This macro is found as annotation to signals or slots which are otherwise tagged as deprecated.

Yet, searching in the Qt documentation (both website & local docs) has not yield any hits. More, grepping the headers of Qt libraries itself does not yield any hits, besides a blank definition of the macro in qglobal.h:

/* moc compats (signals/slots) */
#  define QT_MOC_COMPAT
#  undef QT_MOC_COMPAT
#  define QT_MOC_COMPAT

So, what has it been there for?

Looking at the code generated by moc, one can see that QT_MOC_COMPAT gets reflected in some flags being set on the metadata about the signals and slots methods. See by the example of KActionCollection (as found in the build directory src/KF5XmlGui_autogen/include/moc_kactioncollection.cpp, note the MethodCompatibility comments):

// ...
static const uint qt_meta_data_KActionCollection[] = {

 // content:
       8,       // revision
       0,       // classname
       0,    0, // classinfo
      12,   14, // methods
       2,  108, // properties
       0,    0, // enums/sets
       0,    0, // constructors
       0,       // flags
       5,       // signalCount

 // signals: name, argc, parameters, tag, flags
       1,    1,   74,    2, 0x06 /* Public */,
       5,    1,   77,    2, 0x16 /* Public | MethodCompatibility */,
       6,    1,   80,    2, 0x16 /* Public | MethodCompatibility */,
       7,    1,   83,    2, 0x06 /* Public */,
       8,    1,   86,    2, 0x06 /* Public */,

 // slots: name, argc, parameters, tag, flags
       9,    0,   89,    2, 0x09 /* Protected */,
      10,    0,   90,    2, 0x19 /* Protected | MethodCompatibility */,
// ...

Those flags reflect enums defined in qmetaobject_p.h:

enum MethodFlags  {
    // ...
    MethodCompatibility = 0x10,
    MethodCloned = 0x20,
    MethodScriptable = 0x40,
    MethodRevisioned = 0x80

So, what makes use of this flag?

At first it seems nothing does. Looking some more, one can though discover that the flag is being brought back into the game via some bitshifting and being mapped onto another of set of flags (qmetaobject.h & qmetaobject.cpp):

class Q_CORE_EXPORT QMetaMethod
    // ...
    enum Attributes { Compatibility = 0x1, Cloned = 0x2, Scriptable = 0x4 };
    int attributes() const;
    // ...

int QMetaMethod::attributes() const
    if (!mobj)
        return false;
    return ((mobj->d.data[handle + 4])>>4);

The very flag QMetaMethod::Compatibility is then checked for in debug builds of Qt during QObject::connect(...) calls, which invokes in such builds the following code to generate runtime warnings in the log:

#ifndef QT_NO_DEBUG
static inline void check_and_warn_compat(const QMetaObject *sender, const QMetaMethod &signal,
                                         const QMetaObject *receiver, const QMetaMethod &method)
    if (signal.attributes() & QMetaMethod::Compatibility) {
        if (!(method.attributes() & QMetaMethod::Compatibility))
            qWarning("QObject::connect: Connecting from COMPAT signal (%s::%s)",
                     sender->className(), signal.methodSignature().constData());
    } else if ((method.attributes() & QMetaMethod::Compatibility) &&
               method.methodType() == QMetaMethod::Signal) {
        qWarning("QObject::connect: Connecting from %s::%s to COMPAT slot (%s::%s)",
                 sender->className(), signal.methodSignature().constData(),
                 receiver->className(), method.methodSignature().constData());

Chance is that QT_MOC_COMPAT is a left-over from string-based signal/slot connection times. Where now using the method-function-pointer-based signal/slot connects catches connections with deprecated signals or slots at build time, as opposed to the runtime-only approach of the earlier, which also comes at runtime costs and thus is only available in debug builds of Qt.

Perhaps some Qt contributor reading this can shed more light on this and about its future, presence & past 🙂

How to break your code porting from Q_PRIVATE_SLOT to context & function object based connection

TL;DR Beware of connections to function objects accessing class members which could be triggered during execution of subclass destructor methods.

Oh, those name-based signal/slot connections feel outdated!

So you are a happy embracer of Qt’s new QObject signal/slot connection way of coding based on function-pointer or functor objects, like myself. This compile-time check of signals and slots feels just so much better. And thus you also port any existing code to it. Including some which uses the pimpl approach for some public classes, borrowing Qt’s macros Q_PRIVATE_SLOT, Q_D & Co.:

class MyWidgetPrivate;

class MyWidget : public QWidget
    explicit MyWidget(QWidget *parent);
    // [...]
    // setting a QWidget-subclass to be used as custom form in this widget
    void setForm(Form *form);
    const QScopedPointer d_ptr;
    Q_PRIVATE_SLOT(d_func(), void handleFormDeleted())

// implementation side:

class MyWidgetPrivate
// [...]
    void handleFormDeleted() { /*...*/ }

MyWidget::MyWidget(QWidget *parent)
    : QWidget(parent)
    , d_ptr(new MyWidgetPrivate)
    // [...]

void MyWidget::setForm(Form *form)
    // [...]
    connect(form, SIGNAL(destroyed()), this, SLOT(handleFormDeleted()));

Got some time, let’s modernize the code

The old code calls to be changed into using a connection from the destroyed signal to a lambda expression calling handleFormDeleted() directly on the private object, with MyWidget instance as context object, thus removing the need for that Q_PRIVATE_SLOT:

class MyWidgetPrivate;

class MyWidget : public QWidget
    explicit MyWidget(QWidget *parent);
    // [...]
    // setting a QWidget-subclass to be used as custom form in this widget
    void setForm(Form *form);
    const QScopedPointer d_ptr;

// implementation side:

class MyWidgetPrivate
// [...]
    void handleFormDeleted() { /*...*/ }

MyWidget::MyWidget(QWidget *parent)
    : QWidget(parent)
    , d_ptr(new MyWidgetPrivate)
    // [...]

void MyWidget::setForm(Form *form)
    // [...]
    connect(form, &QObject::destroyed,
            this, [this] { Q_D(MyWidget); d->handleFormDeleted(); });

Looks fine & compiles. Code feels more future-proof with the compiler now warning if some signal or slots got changed/removed.

Ooops, crashing now?

Just… nooos, it sometimes crashes now, in the destructor of MyWidget. How that on this innocent looking change?

Reading once more closely the documentation of QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal, const QObject *context, Functor functor, Qt::ConnectionType type = Qt::AutoConnection) we notice the remark:

The connection will automatically disconnect if the sender or the context is destroyed. However, you should take care that any objects used within the function object are still alive when the signal is emitted.

Which subtly hints to the problem we now have: if the form instance is set as child widget of the MyWidget instance, it will be deleted when ~QWidget() is run as part of the MyWidget destructor. And then emit the destroyed signal. At that point in time this as seen by the function object no longer is a proper MyWidget instance. And things go *boom*.

The old string-based connection as well as the member-function-pointer-based one handle that case for us, by some QObject magic using virtual methods which catch that the receiver no longer is a MyWidget and somehow then just drop the slot call (got lost in the code details, but it is something like this).
While with the new function-object-based connection that one will only become automatically inactive by being destroyed if the ~QObject destructor of either sender or receiver is reached. So having a longer lifetime, which can come a bit unexpected to some.

Fixing the modern times, unsure how

Lesson learned: do not blindly port code to the context & function object based connection. Instead beware of the additional traps which there are given that the function object is an independent complex object and not just a member function pointer. I will have to revisit quite some code where I might have missed this trap with the subclass destructor methods :/
As I seemed not the only one hit by this, I filed QTBUG-71432: “API dox of context & function object using QObject::connect should hint about destructor issues” so other people like me might be saved from this from the start.

Curious to learn about best practices for private slots and non-string-based connections. Thus happy to hear about proposals/hints in the comments.

(Update: text now using C++ standard lingo term “function object” instead of “functor”)

Adding API dox QCH files generation to KDE Frameworks builds

Or: Tying loose ends where some are slightly too short yet.


  • you favour offline documentation (not only due to nice integration with IDEs like KDevelop),
  • develop code using KDE Frameworks or other Qt-based libraries,
  • you know all the KF5 libraries have seen many people taking care for API documentation in the code over all the years,
  • and you had read about doxygen’s capability to create API dox in QCH format,
  • and you want your Linux distribution package management to automatically deliver the latest version of the documentation (resp. QCH files) together with the KDE Frameworks libraries and headers (and ideally same for other Qt-based libraries),

the idea is easy derived to just extend the libraries’ buildsystem to also spit out QCH files during the package builds.

It’s all prepared, can ship next week, latest!!1

Which would just be a simple additional target and command, invoking doxygen with some proper configuration file. Right? So simple, you wonder why no-one had done it yet 🙂

Some initial challenge seems quickly handled, which is even more encouraging:
for proper documentation one also wants cross-linking to documentation of things used in the API which are from other libraries, e.g. base classes and types. Which requires to pass to doxygen the list of those other documentations together with a set of parameters, to generate proper qthelp:// urls or to copy over documentation for things like inherited methods.
Such listing gets very long especially for KDE Frameworks libraries in tier 3. And with indirect dependencies pulled into the API, on changes the list might get incomplete. Same with any other changes of the parameters for those other documentations.
So basically a similar situation to linking code libraries, which proposes to also give it a similar handling: placing the needed information with CMake config files of the targeted library, so whoever cross-links to the QCH file of that library can fetch the up-to-date information from there.

Things seemed to work okay on first tests, so last September a pull request was made to add some respective macro module to Extra-CMake-Modules to get things going and a blog post “Adding API dox generation to the build by CMake macros” was written.

This… works. You just need to prepare this. And ignore that.

Just, looking closer, lots of glitches popped up on the scene. Worse, even show stoppers made their introduction, at both ends of the process pipe:
At generation side doxygen turned out to have bitrotted for QCH creation, possibly due to lack of use? Time to sacrifice to the Powers of FLOSS, and git clone the sources and poke around to see what is broken and how to fix it. Some time and an accepted pull request later the biggest issue (some content missed to be added to the QCH file) was initially handled, just yet needed to also get out as released version (which it now is since some months).
At consumption side Qt Assistant and Qt Creator turned to be no longer able to properly show QCH files with JavaScript and other HTML5 content, due to QWebKit having been deprecated/dropped and both apps in many distributions now only using QTextBrowser for rendering the documentation pages. And not everyone is using KDevelop and its documentation browser, which uses QWebKit or, in master branch, favours QWebEngine if present.
Which means, an investment into QCH files from doxygen would only be interesting to a small audience. Myself currently without resources and interest to mess around with Qt help engine sources, looks with hope on the resurrection of QWebKit as well as the patch for a QtWebEngine based help engine (if you are Qt-involved, please help and push that patch some more!)

Finally kicking off the production cycle

Not properly working tools, nothing trying to use the tools on bigger scale… classical self-blocking state. So time to break this up and get some momentum into, by tying first things together where possible and enabling the generation of QCH files during builds of the KDE Frameworks libraries.

And thus to current master branches (which will become v5.36 in July) there has been now added for one to Extra-CMake-Modules the new module ECMAddQch and then to all of the KDE Frameworks libraries with public C++ API the option to generate QCH files with the API documentation, on passing -DBUILD_QCH=ON to cmake. If also having passed -DKDE_INSTALL_USE_QT_SYS_PATHS=ON (or installing to same prefix as Qt), the generated QCH files will be installed to places where Qt Assistant and Qt Creator even automatically pick them up and include them as expected:

Qt Assistant with lots of KF5 API dox

KDevelop picks them up as well, but needs some manual reconfiguration to do so.

(And of course ECMAddQch is designed to be useful for non-KF5 libraries as well, give it a try once you got hold of it!)

You and getting rid of the remaining obstacles

So while for some setups the generated QCH file of the KDE Frameworks already are useful (I use them since some weeks for e.g. KDevelop development, in KDevelop), for many they still have to become that. Which will take some more time and ideally contributions also from others, including Doxygen and Qt Help engine maintainers.

Here a list of related reported Doxygen bugs:

  • 773693 – Generated QCH files are missing dynsections.js & jquery.js, result in broken display (fixed for v1.8.13 by patch)
  • 773715 – Enabling QCH files without any JavaScript, for viewers without such support
  • 783759 – PERL_PATH config option: when is this needed? Still used?
  • 783762 – QCH files: “Namespaces” or “Files” in the navigation tree get “The page could not be found” (proposed patch)
  • 783768 – QCH files: classes & their constructors get conflicting keyword handling< (proposed patch)
  • YETTOFILE – doxygen tag files contain origin paths for “file”, leaking info and perhaps is an issue with reproducible builds

And some related reported Qt issues:

  • QTBUG-60933 – No qtdbus.tags file generated
  • QTBUG-61790 – Doxygen tag files have invalid data for enum types and values

There is also one related reported CMake issue:

  • 16990 – Wanted: Import support for custom targets (extra bonus: also export support)

And again, it would be also good to see the patch for a QtWebEngine based help engine getting more feedback and pushing by qualified people. And have distributions doing efforts to provide Qt Assistant and Qt Creator with *Web*-based documentation engines (see e.g. bug filed with openSUSE).

May the future be bright^Wdocumented

I am happy to see that Gentoo & FreeBSD packagers have already started to look into extending their KDE Frameworks packaging with generated API dox QCH files for the upcoming 5.36.0 release in July, with other packagers planning to do so soon as well.

So perhaps one not too distant day it will be just normal business to have QCH files with API documentation provided by your distribution not just for the Qt libraries itself, but also for every library based on them. After all documentation has been one of the things making Qt so attractive. As developer of Qt-based software, I very much look forward to that day 🙂

Next stop then: QML API documentation :/

Marble in your CMake or qmake based project, now more easy

Marble, the virtual globe and world atlas, at its core is a library intended for reuse, libmarblewidget. This library is done with minimal dependencies to ease usage (e.g. just needs a few Qt5 modules).

The new version of Marble, coming with libmarblewidget v0.26 and released as part of KDE Applications 16.12, is now making it more easy to integrate libmarblewidget in your own software project, when using CMake or qmake as buildsystem.

To link your project to libmarblewidget and find its headers, you can have that in 1-2 lines:

For qmake a qt_Marble.pri file is installed. So in the pro file of your project just add:

QT += Marble

For this to work you have to do one out of these:

  • install Marble into the same prefix as your Qt5 installation (e.g. /usr)
  • pass -DMARBLE_PRI_INSTALL_USE_QT_SYS_PATHS=ON to cmake when building Marble
  • use the environment variable QMAKEPATH to point to the installation prefix of Marble when calling qmake on your project

For CMake some CMake config files are installed. So in the CMakeLists.txt of your project just add:

find_package(Marble REQUIRED)
target_link_libraries(my_project  Marble)

No more need for some own FindMarble.cmake file.

The CMake variant is already possible since libmarblewidget v0.25 (part of KDE Applications 16.08).

Learn more about how to use Marble in your project, e.g. by simple examples, at marble.kde.org.

Kate, Gwenview & Co. unwantedly trading loss of file meta data against loss of content data, due to QSaveFile

tl;dr Code using QSaveFile (so Kate, KWrite, KDevelop, Gwenview & others) can result in loss of file meta data with extended file attributes, a change of file ownership and loss of hard links on existing files. Cmp. QTBUG-56366.

Wanted: Atomic changes to file content

When you are looking for a way to do transactional writes of files, QSaveFile looks like a simple and clear solution. But, this is 2016, so support for transactional updates of file content with mainstream file systems seems to be still rocket science? At least QSaveFile does a dirty hack as work-around: it creates a new temporary file to which all data is written into, and once all the content has arrived on the permanent storage, on the call of commit() it tries to replace the original file with that temporary file, by registering the new file with the name of the old in the file entries as replacement, which is an atomic system call.

Content data safely arrived, but meta data lost on the way

Just, and this is where things get dirty, this registration as replacement also will result in a replacement of all meta data for the old file with that of the new. And if the old file was registered as a hard link, that link will be dropped as well, the new file registered for the given name is now a file on its own decoupled from the formerly linked ones which also never will learn about the new content just written.
So the hack with the separate temporary file needs some more efforts to simulate a real transactional update of the content of that file identified by its name. Sadly some things are impossible and some things seem not yet implemented. Impossible is e.g. to set the ownership in most cases because for obvious reasons this needs special rights (cmp. API documentation of chown()). And e.g. not yet implemented seems to be copying of extended file attributes.

You who makes a difference?

There is no big warning sign yet on the API documentation of QSaveFile, so some developers might not be aware of these traps when using QSaveFile, like e.g. all over code in KDE projects. Or they are, but the users of their products are not (cmp. related bugs for Kate 333577, 354405, 358457).

So if you desire transactional updates of file contents in your code, consider if there are cases where the loss of the mentioned file meta data can be an issue, e.g. when those files are candidates to be shared on multi-user systems or when extended file attributes are being made use of with those files.

If you are a developer experienced with file system API, please consider writing a fix or improvement for QSaveFile and adopting QTBUG-56366: “QSaveFile results in loss of extended file attributes and change of file ownership”.

If you are a developer of file systems, please consider writing some system API which covers the use cases of transactional access to files. So there could be a proper implementation variant of QSaveFile and also any helpless hacks like .lock files would also no longer be needed.

And if you found wrong claims in this text, please shout out in the comments, as I only explored this issue quick and superficially for now, so chances are I missed something.