ImaginativeThinking.ca


A developers blog

How the heck can I use qsTr() with variables?

By: Brad

Hi Brad, I have a Qt application where I am using an internal API and when an API call fails I get back an error string. The only problem is that I want to do internationalization and am having trouble trying to figure out how to get the strings I get back from the API translated. I have tried using qsTr() but I’m not able to figure out how to make it work when the string I want translated is a variable.

ThinkingPig

I ran into this exact same issue when working on a project recently but I was writing a front end for a product which talked to the backend over JSON formatted API calls. Some of the user facing strings were coming from the backend via the JSON calls.

This issue occurred on two projects, in one we just updated the API to allow me to request which language I wanted the strings in and left it up to the back-end to properly translate the strings they returned; however, in the other project this was not an option so instead we choose to do something that is a bit hokey but it works.

Before I get into it I would recommend a safer approach might be to have the API return ids (like message codes) and leave the actual strings in your UI code vs. getting them from the API. It will be easier to maintain if you end up modifying strings later on (which I’ll explain below). If that isn’t an option for you, if you can only get pre-formatted strings instead of IDs then try the following.

The qsTr() function ( or the tr() function in C++ ) takes a string and performs a look up in a translation resource (*.qm) file using the string provided as the key in order to find the translated version. This can work with string literals or string variables.

Both of the following will work at run time assuming translations exists in the *.qm file:

Text { text: qsTr("Hello World"); }
Text {
    property string unTranslatedString: "Hello World"
    text: qsTr( unTranslatedString )
}

The issue is that the tools lupdate and lrelease used to generate the *.ts and *.qm files used by Qt for internationalization won’t understand the string variable. lupdate statically combs over your code file line by line and looks for any strings encased in qsTr() or tr() methods and extracts them into the *.ts file, which is a XML file that can be used by translators to generate a list of translations for each translatable string found in the application. This is done on uncompiled code, so it can’t understand variables as it won’t be able to know what string is stored in the variable at run time. This is why the documentation says that you can only use string literals with qsTr() and tr() and why lupdate will give you warnings when it hits a qsTr()/tr() statement which is using a variable vs. a string literal; however, qsTr()/tr() will use strings stored in a variable to look up translations at run time.

Row {
    spacing: 4

    Text {
        text: qsTr( "Device Type" ) + ":"
    }
    Text {
        text: qsTr( myModel.deviceType )
    }
}

In the above example the deviceType property of the myModel model returns a QString which it populates via a JSON call to the back-end. Both qsTr() statements will correctly look for a translated version of the provided strings at runtime; lets say for example that the myModel.deviceType property returns the string Speaker, the qsTr() statement will then look for a translation for the string Speaker within the currently loaded translation resource file.

The issue is that because lupdate can’t make heads or tails of it the returned string Speaker won’t be in your translation resource file so qsTr() will not be able to find a translated version of it; when this happens qsTr() will default to using the untranslated version. If however you simply made sure that your *.qm file contained the translation for the string in question with the correct context it will work.

To achieve this you simply need to add the following anywhere in your application (either in your QML or C++ code):

QT_TR_NOOP("Speaker");

QT_TR_NOOP is a macro which takes a string but does not do a translation look up, it will return the untranslated string. If you simply put it somewhere in your project lupdate will pick up the string, it scans for qsTr(), tr(), QT_TR_NOOP() and other similar functions/macros, and put it in the *.ts file and all strings in the *.ts file will be added to your *.qm files when you run lrelease. The QT_TR_NOOP() macro does not even need to be executed by your application at run time as its only being used to ensure that your dynamic strings are added to your translation files at pre-compile time.

One thing to keep in mind is that Qt’s translation system uses contexts to make sure that if you have the same word on two different views but on view one it has a different meaning then on view two you can provide different translation for the two words in some languages based on their context. When you use qsTr()/tr() a context is defined automatically using the name of the class the call is made within. So if your QML code is in one file class and you put the QT_TR_NOOP() macro in another class they will have different contexts which will result in your code not working, they are the same strings but with different contexts. To explicitly set the context for a given string use QT_TRANSLATE_NOOP() and qsTranslate().

Example:

C++ file
QT_TRANSLATE_NOOP("MyDynamicStringsContext", "Speaker")
QML file
Row {
    spacing: 4

    Text {
        text: qsTr( "Device Type" ) + ":"
    }
    Text {
        text: qsTranslate( "MyDynamicStringsContext", myModel.deviceType )
    }
}

Now because this is all done statically sadly you can’t use a variable for the context string either, it must be a string literal, but what this does is make sure that the string “Speaker” shows up in your *.ts file, that your translators can provide translations for it and you can generate a *.qm file with lrelease which will contain the translated string for “Speaker” with the key “Speaker” and the context “MyDynamicStringsContext” in order for qsTranslate() to find it at run time.

The draw backs is that this is a bit hokey as I said before. Its vulnerable to changes in the string done on the other side of the call breaking the translations; that is if the other end changes the capitalization (yes this is case sensitive) or adds a punctuation or even a trailing space the strings won’t match and qsTranslate() will fail to find the translated string. It also means you need to know all the possible strings ahead of time so you can make sure you have translations for them. So the example would look more like this:

C++ file
QT_TRANSLATE_NOOP("MyDynamicStringsContext", "Speaker")
QT_TRANSLATE_NOOP("MyDynamicStringsContext", "Speaker ") // Notice trailing space.
QT_TRANSLATE_NOOP("MyDynamicStringsContext", "speaker")
QT_TRANSLATE_NOOP("MyDynamicStringsContext", "Other Possible Speaker Type")
QML file
Row {
    spacing: 4

    Text {
        text: qsTr( "Device Type" ) + ":"
    }
    Text {
        text: qsTranslate( "MyDynamicStringsContext", myModel.deviceType )
    }
}

Oh and lastly lupdate will scream at you each time it hits a qsTranlate() statement which wraps a variable as it won’t know what to do with it and will ignore it. Which is ok as you know you have it covered but you could end up drowning in warning messages and may miss some legit issues.

So its fragile but it is an option if you have a reasonable amount of control over both ends but can’t push the burned of translating the strings over to the back-end.

So that is how the heck you can use qsTr() function with variables. I hope that helps you and answers your question

Until next time think imaginatively and design creatively

Brad

My interest in computer programming started back in high school and Software Development has remained a hobby of mine ever since. I graduated as a Computer Engineering Technologist and have been working as a Software Developer for many years. I believe that software is crafted; understanding that how it is done is as important as getting it done. I enjoy the aesthetics in crafting elegant solutions to complex problems and revel in the knowledge that my code is maintainable and thus, will have longevity. I hold the designation Certified Technician (C.Tech.) with the Ontario Association of Computer Engineering Technicians and Technologists (OACETT), have been certified as a Professional Scrum Master level 1 (PSM I) and as a Professional Scrum Developer level 1 (PSD I) by Scrum.org as well as designated as an Officially Certified Qt Developer by the Qt Company. For more on my story check out the about page here

Feel free to write a reply or comment.