user1414413
user1414413

Reputation: 403

Qt convert translated text to english

My Qt application supports dynamic translation IE the user can change languages whilst the application is running

Now I have a need to find the English equivalent of a translated string and don't seem to be able to find a way

For example Given QString s = tr("Hello"); I need to be able to get "Hello" from s after translation has taken place.

Has anyone done this before or have any ideas on how (if) it can be achieved

Thanks

Upvotes: 4

Views: 2472

Answers (5)

fassl
fassl

Reputation: 754

You can just translate the English back to the locale you want programmatically with for instance a python script:

translate.py

import sys
import xml.etree.cElementTree as ET

tree = ET.ElementTree(file=sys.argv[1])
root = tree.getroot()
root.attrib["language"] = "c"
for context in root:
    for node in context:
        if node.tag != "message":
            continue

        source = (None, None)
        translation = (None, None)

        for sub in node:
            if sub.tag == "source":
                source = (sub, sub.text)
            elif sub.tag == "translation":
                translation = (sub, sub.text)

        source[0].text = translation[1]
        translation[0].text = source[1]

tree.write(sys.argv[2])

Create the translation before linking:

app.pro

TRANSLATIONS += \
    $$PWD/../en_US.ts \

translate.input = TRANSLATIONS
translate.variable_out = TRANSLATE_OUTPUT
translate.output = $$PWD/../${QMAKE_FILE_BASE}_c.ts
translate.commands = \
    python $$system_path($$PWD/../translate.py) \
        ${QMAKE_FILE_NAME} \
        ${QMAKE_FILE_OUT}
translate.config = no_link target_predeps

QMAKE_EXTRA_COMPILERS += translate

translate.release.input = TRANSLATE_OUTPUT
translate.release.output = $$PWD/../${QMAKE_FILE_BASE}.qm
translate.release.commands = \
    $$system_path($$[QT_INSTALL_BINS]/lrelease) \
        ${QMAKE_FILE_NAME} \
        ${QMAKE_FILE_OUT}
translate.release.config = no_link target_predeps

QMAKE_EXTRA_COMPILERS += translate.release

And finally in your app use a QTranslator to get back your source string:


auto translator{ QTranslator{} };

translator.load(QLocale{ QLocale::C },
                QLocale{}.name(),
                QLatin1String{ "_" },
                QLatin1String{ ":/translations" });

You probably want to replace all occurrences of the C locale I used with whatever locale you need.

Upvotes: 0

Pascal Laferrière
Pascal Laferrière

Reputation: 109

So I ran into the same situation where I also wanted to avoid duplicating some string literals in my code and still be able to print out some translated error messages to user of my application but then log the original English version of the text to a log.

I started using jaba's code and ran into some issues when using some special characters like "\n" and some Unicode tags. It turns out that the way Qt parses the string literals marked by the translation macros involves replacing these special characters. The transcode function is taken directly from Qt's Linguist source code in order to perform the same process at runtime. Without this, the translate function fails to find the string since what was stored in the translation file won't match exactly the compiled string. So what I was able to come up with looks something like this:

#pragma once

#include <QApplication>

#include <QString>
#include <QRegularExpression>
#include <cassert>

class QObject;

//NOTE: enable_if_t is only available in C++14 so we need to define it here
template< bool Condition, typename T = void >
using enable_if_t = typename std::enable_if<Condition, T>::type;

namespace
{
    constexpr char QT_TR_NOOP_MACRO [] = "QT_TR_NOOP(";
    constexpr char QT_TRANSLATE_NOOP_MACRO [] = "QT_TRANSLATE_NOOP(";
    constexpr char QT_TRANSLATE_NOOP_3_MACRO [] = "QT_TRANSLATE_NOOP3(";

    //TODO: Add QT_TR_N_NOOP, QT_TRANSLATE_N_NOOP, QT_TRANSLATE_N_NOOP3
    enum class MacroType { Unknown, QT_TR_NOOP, QT_TRANSLATE_NOOP, QT_TRANSLATE_NOOP3 };

    MacroType getMacroTypeFromString(const QString& str)
    {
        if (str.contains(QT_TR_NOOP_MACRO))
            return MacroType::QT_TR_NOOP;
        else if (str.contains(QT_TRANSLATE_NOOP_3_MACRO))
            return MacroType::QT_TRANSLATE_NOOP3;
        else if (str.contains(QT_TRANSLATE_NOOP_MACRO))
            return MacroType::QT_TRANSLATE_NOOP;
        else
        {
            return MacroType::Unknown;
        }
    }

    QStringList extractArguments(const QString& raw)
    {
        QStringList macroArguments;

        const QRegularExpression findQuotesRegExp("\"([^\"\\\\]|\\\\.)*\"");
        QRegularExpressionMatchIterator regExpIter = findQuotesRegExp.globalMatch(raw);

        while (regExpIter.hasNext())
        {
            QRegularExpressionMatch match = regExpIter.next();
            QString argumentString = match.captured(0);
            argumentString.remove(0, 1); // Remove leading quote
            argumentString.remove(argumentString.length() - 1, 1); // Remove trailing quote
            macroArguments.push_back(argumentString);
        }

        return macroArguments;
    }

    QString transcode(const QString &str)
    {
        static const char tab[] = "abfnrtv";
        static const char backTab[] = "\a\b\f\n\r\t\v";
        // This function has to convert back to bytes, as C's \0* sequences work at that level.
        const QByteArray ba = str.toUtf8();
        std::vector<uchar> in(std::begin(ba), std::end(ba));
        size_t inputLength = in.size();
        QByteArray out;
        out.reserve(static_cast<int>(inputLength));

        for (size_t i = 0; i < inputLength;)
        {
            uchar c = in[i++];
            if (c == '\\')
            {
                if (i >= inputLength)
                    break;

                c = in[i++];

                if (c == '\n')
                    continue;

                if (c == 'x' || c == 'u' || c == 'U')
                {
                    const bool unicode = (c != 'x');
                    QByteArray hex;

                    while (i < inputLength && isxdigit((c = in[i])))
                    {
                        hex += static_cast<char>(c);
                        i++;
                    }

                    if (unicode)
                    {
                        out += QString(QChar(hex.toUInt(nullptr, 16))).toUtf8();
                    }
                    else
                    {
                        out += static_cast<char>(hex.toUInt(nullptr, 16));
                    }
                }
                else if (c >= '0' && c < '8')
                {
                    QByteArray oct;
                    int n = 0;
                    oct += static_cast<char>(c);

                    while (n < 2 && i < inputLength && (c = in[i]) >= '0' && c < '8')
                    {
                        i++;
                        n++;
                        oct += static_cast<char>(c);
                    }

                    out += static_cast<char>(oct.toUInt(nullptr, 8));
                }
                else
                {
                    const char *p = strchr(tab, c);
                    out += !p ? static_cast<char>(c) : backTab[p - tab];
                }
            }
            else
            {
                out += static_cast<char>(c);
            }
        }

        return QString::fromUtf8(out.constData(), out.length());
    }
}

#define TR_STRING_VOID(QT_TRANSLATE_NOOP_STRING) TranslatedString<void>(#QT_TRANSLATE_NOOP_STRING)
#define TR_STRING(QT_TR_NOOP_STRING) TranslatedString<decltype(this)>(#QT_TR_NOOP_STRING)

template <class T> class TranslatedString
{

public:

    explicit TranslatedString(const char* raw)
    {
        init(raw);
    }

    inline const QString& native() const
    {
        return m_Native;
    }

    inline QString translated() const
    {
        return QCoreApplication::translate(qUtf8Printable(m_Scope), qUtf8Printable(m_Native));
    }

private:

    //TODO: The init method should be SFINAE'd such that only the
    //      respective macros for void vs QObject subclass are supported
    //

    inline void init(const QString& raw)
    {
        m_MacroType = getMacroTypeFromString(raw);

        assert(("Unsupported Qt translation Macro Type", (m_MacroType != MacroType::Unknown)));

        QStringList macroArguments = extractArguments(raw);

(macroArguments.size() == 2)));

        if (macroArguments.size() == 1 && m_MacroType == MacroType::QT_TR_NOOP)
        {
            const QRegularExpression findClassNameRegExp("(?<=\\bclass\\s)(\\w+)");
            m_Scope = QRegularExpressionMatch(findClassNameRegExp.match(typeid(T).name())).captured(0);
            m_Native = transcode(macroArguments[0]);
        }
        else if ((macroArguments.size() == 2 && m_MacroType == MacroType::QT_TRANSLATE_NOOP) || (macroArguments.size() == 3 && m_MacroType == MacroType::QT_TRANSLATE_NOOP3))
        {
            m_Scope = macroArguments[0];
            m_Native = transcode(macroArguments[1]);
        }
        else
        {
            //TODO: Figure out what to put here
        }
    }

    MacroType m_MacroType = MacroType::Unknown;
    QString m_Native;
    QString m_Scope;
};

I've also refactored the code to match my own personal coding style and naming conventions but those are just cosmetic changes. As I was refactoring the code, I ended up getting rid of the SFINAE stuff for the constructor and everything still worked fine for my purposes. Albeit, I'm not sure if that's increased the chances of this class being used incorrectly by others so I'd be curious to know if the init function should have two alternate versions where if the class is void, then only some of the translation macros are valid and the number of arguments would be parsed accordingly while if the class is a QObject subclass then an alternate set of macros would be validated. If I were to add this though, I've found that using something like the following would probably work:

std::enable_if_t<std::is_convertible<T, QObject>::value

Please correct me if I'm wrong on this as I'm not sure if this would cover all possible cases of references to types and whatnot.

I also removed all of the "arg" functions since these didn't allow for dynamic translations, meaning that any strings with additional arguments couldn't be re-translated to take into account either new values being passed as arguments or the possible swapping of the order of arguments based on the language. Qt shows an example of this in their documentation.

Using similar macros, some examples of how this could be used are as follows:

namespace
{
    enum class ERROR
    {
        COUNT,
        ARGUMENT_SWAP,
    };

    static const std::map<ERROR, TranslatedString<void>> ERROR_STRINGS =
    {
        std::make_pair(ERROR::COUNT, TR_STRING_VOID(QT_TRANSLATE_NOOP("MainWindow", "%1"))),
        // Translated text could be given as "Second Argument: %2,   First Argument: %1"
        std::make_pair(ERROR::ARGUMENT_SWAP, TR_STRING_VOID(QT_TRANSLATE_NOOP("MainWindow", "First Argument: %1,   Second Argument: %2"))),
    };
}

void MainWindow::retranslateUi()
{
    static int count = 0;
    count++;

    // This has the copyright symbol unicode in it.
    auto ts = TR_STRING(QT_TR_NOOP("Test translation. \xC2\xA9"));

    // Both the translated string and the original text are available
    ui->translatedLabel->setText(ts.translated());
    ui->nativeLabel->setText(ts.native());

    ui->translatedCountLabel->setText(ERROR_STRINGS.at(COUNT).translated().arg(count));
    ui->nativeCountLabel->setText(ERROR_STRINGS.at(COUNT).native().arg(count));

    // Note that the arguments need to be provided to each string
    // so that they can be replaced according to the correct order
    // of the translated text which is only available at runtime.
    ui->translatedSwappedArgumentsLabel->setText(ERROR_STRINGS.at(ARGUMENT_SWAP).translated().arg(50.0).arg("String"));
    ui->swappedArgumentsLabel->setText(ERROR_STRINGS.at(ARGUMENT_SWAP).native().arg(50.0).arg("String"));
}

Upvotes: 0

jaba
jaba

Reputation: 775

Based on Mykhailo Bryzhaks answer I adopted the class a bit so we can have contexts in the .ts file which will make things a lot easier for big solutions:

#ifndef TrString_h__
#define TrString_h__

#include <QString>
#include <QRegularExpression>
#include <cassert>

class QObject;

namespace
{
    static QRegularExpression s_findClassNameRegExp("(?<=\\bclass\\s)(\\w+)");
    static QRegularExpression s_findQuotesRegExp("\"([^\"\\\\]|\\\\.)*\"");
}

#define TRStringVoid(QT_TRANSLATE_NOOP_STRING) TrString<void>(#QT_TRANSLATE_NOOP_STRING)
#define TRString(QT_TR_NOOP_STRING) TrString<decltype(this)>(this, #QT_TR_NOOP_STRING)

// Use      //: MyComment
// one line before the call to add a comment for the translator

namespace
{
    const QString kQT_TR_NOOP_Macro = "QT_TR_NOOP(";
    const QString kQT_TRANSLATE_NOOP_Macro = "QT_TRANSLATE_NOOP(";
    const QString kQT_TRANSLATE_NOOP_3_Macro = "QT_TRANSLATE_NOOP3(";
}

// Use TRString or TRStringVoid macro to get an instance of this class
template <class T> class TrString
{
    enum class MacroType { Unknown, QT_TR_NOOP, QT_TRANSLATE_NOOP, QT_TRANSLATE_NOOP3 };

public:
    // used for global functions where T is void
    template <typename SfinaeT = void, typename SfinaeT2 = std::enable_if_t<std::is_void<T>::value, T>>
    TrString(const char* lineString)
        : m_native(lineString)
    {
        m_macroType = getLineStringType(lineString);

        assert(("You must use TRStringVoid(QT_TRANSLATE_NOOP('Context','StringToTranslate')) here because you are not in a class context.",
            (m_macroType == MacroType::QT_TRANSLATE_NOOP || m_macroType == MacroType::QT_TRANSLATE_NOOP3)));

        initStrings();
    }

    // used for calls from within objects
    template <typename SfinaeT = void, typename SfinaeT2 = std::enable_if_t<!std::is_void<T>::value, T>>
    TrString(SfinaeT2 caller, const char* lineString)
        : m_native(lineString)
    {
        m_macroType = getLineStringType(lineString);
        checkIfTemplateHasCorrectType(caller);
        initStrings();
    }

    TrString& operator=(const TrString& other)
    {
        m_scope = other.m_scope;
        m_native = other.m_native;
        m_translated = other.m_translated;
    }

    TrString& arg(const TrString& arg1)
    {
        this->m_native = this->m_native.arg(arg1.native());
        this->m_translated = this->m_translated.arg(qApp->translate(m_scope.toLocal8Bit().data(), arg1.translated().toLocal8Bit().data()));

        return *this;
    }

    TrString& arg(const QString arg1)
    {
        this->m_native = this->m_native.arg(arg1);
        this->m_translated = this->m_translated.arg(arg1);
        return *this;
    }

    TrString& arg(const double arg1)
    {
        this->m_native = this->m_native.arg(arg1);
        this->m_translated = this->m_translated.arg(arg1);
        return *this;
    }

    inline const QString& native() const
    {
        return this->m_native;
    }

    inline const QString& translated() const
    {
        return this->m_translated;
    }

private:
    template<class SfinaeT = T>
    typename std::enable_if<!std::is_polymorphic< std::remove_pointer_t<SfinaeT>>::value, void>::type
        checkIfTemplateHasCorrectType(SfinaeT caller)
    {
        assert(("You must use TRString(QT_TRANSLATE_NOOP('Context', 'StringToTranslate')) here because your class does not derive from QObject.",
            (m_macroType == MacroType::QT_TRANSLATE_NOOP || m_macroType == MacroType::QT_TRANSLATE_NOOP3)));
    }

    template<class SfinaeT = T>
    typename std::enable_if<std::is_polymorphic< std::remove_pointer_t<SfinaeT>>::value, void>::type
        checkIfTemplateHasCorrectType(SfinaeT caller)
    {
        if (!dynamic_cast<QObject*>(caller))
            assert(("You must use TRString(QT_TRANSLATE_NOOP('Context', 'StringToTranslate')) here because your class does not derive from QObject.",
            (m_macroType == MacroType::QT_TRANSLATE_NOOP || m_macroType == MacroType::QT_TRANSLATE_NOOP3)));
        else
            m_isDerivedFromQObject = true;
    }

    MacroType getLineStringType(QString lineString) const
    {
        if (m_native.contains(kQT_TR_NOOP_Macro))
            return MacroType::QT_TR_NOOP;
        else if (m_native.contains(kQT_TRANSLATE_NOOP_3_Macro))
            return MacroType::QT_TRANSLATE_NOOP3;
        else if (m_native.contains(kQT_TRANSLATE_NOOP_Macro))
            return MacroType::QT_TRANSLATE_NOOP;
        else
        {
            assert(("You must you TRString(QT_TR_NOOP('StringToTranslate') or TRString(QT_TRANSLATE_NOOP('Context', 'StringToTranslate')) if you are not in a QObject derived class for this to work.", false));
            return MacroType::Unknown;
        }
    }

    inline void initStrings()
    {
        QRegularExpressionMatchIterator regExpIter = s_findQuotesRegExp.globalMatch(m_native);
        QStringList macroArguments;

        while (regExpIter.hasNext())
        {
            QRegularExpressionMatch match = regExpIter.next();
            QString argumentString = match.captured(0);
            argumentString.remove(0, 1);
            argumentString.remove(argumentString.length() - 1, 1);
            macroArguments.push_back(argumentString);
        }

        if (macroArguments.size() == 1 && m_macroType == MacroType::QT_TR_NOOP)
        {
            m_scope = QRegularExpressionMatch(s_findClassNameRegExp.match(typeid(T).name())).captured(0);
            m_native = macroArguments[0];
        }
        else if ((macroArguments.size() == 2 && m_macroType == MacroType::QT_TRANSLATE_NOOP) || (macroArguments.size() == 3 && m_macroType == MacroType::QT_TRANSLATE_NOOP3))
        {
            m_scope = macroArguments[0];
            m_native = macroArguments[1];
        }
        else
        {
            assert(("You must you TRString(QT_TR_NOOP('StringToTranslate') or TRString(QT_TRANSLATE_NOOP('Context', 'StringToTranslate')) if you are not in a QObject derived class for this to work.", false));
        }

        m_translated = qApp->translate(m_scope.toLocal8Bit().data(), m_native.toLocal8Bit().data());
    }

    MacroType m_macroType = MacroType::Unknown;
    bool m_isDerivedFromQObject = false;
    QString m_native;
    QString m_translated;
    QString m_scope;
};

#endif // TrString_h__

Usage in a QObject derived class:

auto logString = TRString(QT_TR_NOOP("Indian Pale Ale"));
qDebug() << "Native: " << logString.native(); // "Indian Pale Ale"
qDebug() << "Transl: " << logString.translated(); // "Indisches Blass Ale"

Usage in a class that does not derive from QObject:

auto logString = TRString(QT_TRANSLATE_NOOP("MyClass", "Log message"));

Usage outside of classes:

auto logString = TRStringVoid(QT_TRANSLATE_NOOP("Main", "Log message in main"));

If you inadvertently use the wrong Qt macro or the wrong TRString macro an assert will be created with a message on how to fix the call.

Upvotes: 0

Mykhailo Bryzhak
Mykhailo Bryzhak

Reputation: 601

In my application I needed to write translated messages to UI and original messages to program log file.

My solution was to create wrapper class that could both translated and original data.

class TS {
public:
    TS(const char* str) {
        init(str);
    }

    TS(const QString& str) {
        init(str.toStdString().c_str());
    }

    TS& arg(const QString& arg1, const bool translate = true) {
        this->orig = this->orig.arg(arg1);
        if (translate) {
            this->tran = this->tran.arg(qApp->translate("", arg1.toStdString().c_str()));
        } else {
            this->tran = this->tran.arg(arg1);
        }
        return *this;
    }

    TS& arg(const int arg1) {
        this->orig = this->orig.arg(arg1);
        this->tran = this->tran.arg(arg1);
        return *this;
    }

    inline const QString& getOrig() const {
        return this->orig;
    }

    inline const QString& getTran() const {
        return this->tran;
    }

private:
    inline void init(const char* str) {
        this->orig = str;
        this->tran = qApp->translate("", str);
    }

private:
    QString orig;
    QString tran;
};

Usage:

void info(const TS& rsMsg);

...

m_rLog.info(QT_TRANSLATE_NOOP("", "Plain Text"));
m_rLog.info(TS(QT_TRANSLATE_NOOP("", "Text with argument : %1")).arg( 123 ));

Upvotes: 3

Thomas McGuire
Thomas McGuire

Reputation: 5466

There is no way to get the original string from the translated string in Qt. The actual translation, in the end, is done by the QTranslator class, which doesn't expose a reverse lookup function, not even as private API.

You need to change your code to avoid the need for reverse string lookups. One way is to always store the English string or some other identifier where you need it.

This is actually a common case when using QAction, which is why QAction offers to store arbitrary data alongside the translated string, in QAction::setData().

Upvotes: 1

Related Questions