Since decades KDE’s translation and localization framework KI18n provides a mechanism for marking strings for message extraction and deferred translation, the I18N_NOOP prepprocessor macros. Those can be very error prone though, so for KDE Frameworks 5.89 there is now a proposed replacement.

Translation macros

The I18N_NOOP macro differs from the more widely used i18n() function calls in that it only causes a message to be extracted for translation, but it doesn’t actually perform the translation. This is useful when the translation isn’t possible yet at this point or when there are many possible messages of which only very few are actually needed at runtime. Therefore those macros often appear in static message tables.

This isn’t unique to KI18n, Qt’s translation system has similar macros for example (QT_TR_NOOP etc).

And while this isn’t too bad for a single message, there are many more cases to consider, such as any possible combinations of:

  • Quantified messages, ie. singular/plural support.
  • Messages needing a translation context for disambiguation.
  • Messages containing Kuit markup.

That’s where the macros hit their limits. There we have more than one argument to pass to the runtime translation call, so what would a macro map to? And do we trust the developer to manually carry the context string? (see e.g. I18NC_NOOP vs. I18N_NOOP2)

It gets harder and harder to use this correctly the more variants we need to consider, so Albert rightfully wasn’t too happy when I recently proposed plural variants of those macros.

So what can we do instead?

KLocalizedString

KI18n has another mechanism for deferred translation, KLocalizedString and the various ki18n() construction functions for it. Unlike the macro approach you cannot accidentally disassociate the various strings making up a message with this, it’s all tied together by a single object. Still you can control the time when the actual translation happens, so creating KLocalizedStringinstances can happen very early in the program flow.

There’s a downside though, creating a KLocalizedString isn’t exactly cheap, it involves memory allocations and deep copies of the strings. That isn’t a big problem for individual messages, but it does hurt if you are dealing with larger message tables, with many entries that might never actually be needed during a program run. The macros avoided those costs.

KLazyLocalizedString

We can have the best of both worlds though! The macro solution goes back to the C++98 days, and might have been the best we could do at the time, but things have changed.

And that’s where the new KLazyLocalizedString comes in. It’s a simple constexpr container for string literals needed for translations, and as such can be stored in static data tables. At runtime there’s only one meaningful thing to do with it, converting it to a KLocalizedString when needed.

Similar to KLocalizedString, KLazyLocalizedString has its own set of construction functions, kli18n(). Unlike their runtime counter-parts those enforce the use of string literals. That is necessary anyway for message extraction to work, and it avoids having to deal with runtime memory issues at all here.

Migrating away from I18N_NOOP

Let’s look at the following example which illustrates a typical use-case for the I18N_NOOP macro, a static table containing among other things a message that should be translated at runtime.

struct {
    MyClass::VehicleType type;
    const char *name;
} static constexpr const vehicle_msg_table[] = {
    { MyClass::Train, I18N_NOOP("Train") },
    { MyClass::Bus,   I18N_NOOP("Bus") },
    ...
};

...

const auto it = std::find_if(std::begin(vehicle_msg_table), std::end(vehicle_msg_table), [vehicleType](const auto &m) {
    return m.type == vehicleType;
});
QString translatedMessage = i18n(*it.name);

Ported to KLazyLocalizedString this looks very similar:

struct {
    MyClass::VehicleType type;
    KLazyLocalizedString name;
} static constexpr const vehicle_msg_table[] = {
    { MyClass::Train, kli18n("Train") },
    { MyClass::Bus,   kli18n("Bus") },
    ...
};

...

const auto it = std::find_if(std::begin(vehicle_msg_table), std::end(vehicle_msg_table), [vehicleType](const auto &m) {
    return m.type == vehicleType;
});
QString translatedMessage = KLocalizedString(*it.name).toString();

This is now no longer constrained to using the same kli18n() variant in every entry though, we could change the second entry to include a context for example, without having to worry about this when consuming the table entries later:

    { MyClass::Bus,   kli18nc("the vehicle, not the USB one", "Bus") },

It’s also possible to have plural texts in a static message table, the API docs in the merge request contains an example illustrating that.

Outlook

At this point this is still in review and scheduled for KDE Frameworks 5.89 which is due to be released early January. The old macros would be deprecated at the same time, so we’ll have a bit of porting ahead of us there.