この記事では、Qtの翻訳システムで作業しているときに遭遇した不便な点についてお話しし、これらの不便な点に対処する方法を共有したいと思います。
まず、Qtの翻訳システムの仕組みを簡単に思い出してください。
まず第一に、開発者はコードを記述するときに、特別な関数の1つでさまざまな言語に翻訳する必要がある文字列をラップします。
tr("Push me", "button text");
次に、プロジェクトファイルは、翻訳者が実際に翻訳自体を実行するファイルを示します。
TRANSLATIONS += translations/myapp_ru.ts
次に、
lupdateユーティリティが
起動し 、ソース翻訳ファイル(通常のXMLファイル)を作成(または更新)した後、翻訳者が特別なツール-Qt Linguistを使用してそれらを操作できます。
trおよび
変換関数でラップされた行は、ユーティリティによって処理され、.tsファイルに追加されます。
最後に、すべての行が翻訳されると、
lreleaseユーティリティ
が起動し、ソース翻訳ファイル(.ts)を特別なバイナリ形式の.qmファイルに変換します。 次のコードをアプリケーションに追加するだけです。
QTranslator *t = new QTranslator; t->load("/path/to/translations/myapp_ru.qm"); QApplication::installTranslator(t);
以上で、行は目的の言語で表示されます。
不便1:翻訳ストレージ
そのため、アプリケーションの起動時に翻訳ファイルをダウンロードし、目的の言語のテキストがテキストボックス(または他の場所)に表示されたので、行を翻訳しました。 実際、このような些細なケースでは、それ以上は必要ありません。 ただし、次の例を検討してください。 ユーザーが入力したコマンドの処理が実装されているコンソールアプリケーションがあるとします。 次のように、ハンドラー関数を設定することにより、新しいコマンドを追加できます。
typedef bool (*HandlerFunction)(const QStringList &arguments); QMap<QString, HandlerFunction> handlerMap; void installHandler(const QString &command, HandlerFunction f) { handlerMap.insert(command, f); }
すべて問題ありませんが、「help
command 」と
入力すると、対応するコマンド
commandのヘルプが表示されます。 行います:
QMap<QString, QString> helpMap; void installHelp(const QString &command, const QString &help) { helpMap.insert(command, help); }
キャッチを感じますか? はい、最初はすべて問題ありません。
installHelp("mycommand", tr("Does some cool stuff"));
QTranslatorが事前にインストールされている場合、翻訳された文字列を取得します。 しかし、ユーザーが言語の変更を決定した場合(つまり、別の翻訳ファイルが読み込まれた場合)はどうでしょうか。 文字列は同じままです。
この問題にはいくつかの解決策があります。 私が最も自然で便利だと思うものを含め、いくつかを挙げます。
解決策1:工場
文字列を、文字列を返すファクトリ関数に置き換えることができます。
typedef QString(*HelpFactoryFunction)(void); QMap<QString, HelpFactoryFunction> helpMap; void installHelp(const QString &command, HelpFactoryFunction f) { helpMap.insert(command, f); }
ファクトリー関数とそのアプリケーションは次のようになります。
QString myHelpFactory() { return tr("Does some cool stuff"); } installHelp("mycommand", &myHelpFactory);
これで問題は解決しますか? はい、翻訳はヘルプが呼び出されるたびに実行されるため、言語を変更すると、ヘルプはこの新しい言語に翻訳されて表示されます。 これは美しい決断ですか? 誰もが異なる考え方をしていますが、私はそうは思いません。
解決策2:QT_TRANSLATE_NOOP3
ヘッダーファイル
<QtGlobal>には、そのようなマクロ
QT_TRANSLATE_NOOP3があります。 ラップされた文字列を翻訳用にマークし、この文字列(翻訳されていない形式)とコメントを含む匿名の構造(構造体)を返します。 将来、作成された構造は
trおよび
変換関数で使用できます。
言うまでもなく、コードは面倒でいですか? 私はそうは思いません。 さらに、そのような構造体をパラメーターとして関数に渡す際に問題が発生します。 コード:
typedef struct { const char *source; const char *comment; } TranslateNoop3; QMap<QString, TranslateNoop3> helpMap; void installHelp(const QString &command, const TranslateNoop3 &t) { helpMap.insert(command, t); }
使用法:
installHelp("mycommand", QT_TRANSLATE_NOOP3("context", "Does some cool stuff", "help"));
コメントなしの翻訳では、別のマクロ(および別の構造)が使用されるという事実
-QT_TRANSLATE_NOOP-私は完全に黙っています。 ただし
、installHelpオーバーロードを
ブロックして、ある構造を別の構造に変換する必要があります。 うんざり。 これはQt開発者の良心に任せます。
解決策3:自作のラッパークラス
ある意味では、私のソリューションは
QT_TRANSLATE_NOOP3の改良バージョンです。 コードをすぐに見ることをお勧めします。
translation.h class Translation { private: QString context; QString disambiguation; int n; QString sourceText; public: explicit Translation(); Translation(const Translation &other); public: static Translation translate(const char *context, const char *sourceText, const char *disambiguation = 0, int n = -1); public: QString translate() const; public: Translation &operator =(const Translation &other); operator QString() const; operator QVariant() const; public: friend QDataStream &operator <<(QDataStream &stream, const Translation &t); friend QDataStream &operator >>(QDataStream &stream, Translation &t); }; Q_DECLARE_METATYPE(Translation)
translation.cpp Translation::Translation() { n = -1; } Translation::Translation(const Translation &other) { *this = other; } Translation Translation::translate(const char *context, const char *sourceText, const char *disambiguation, int n) { if (n < 0) n = -1; Translation t; t.context = context; t.sourceText = sourceText; t.disambiguation = disambiguation; tn = n; return t; } QString Translation::translate() const { return QCoreApplication::translate(context.toUtf8().constData(), sourceText.toUtf8().constData(), disambiguation.toUtf8().constData(), n); } Translation &Translation::operator =(const Translation &other) { context = other.context; sourceText = other.sourceText; disambiguation = other.disambiguation; n = other.n; return *this; } Translation::operator QString() const { return translate(); } Translation::operator QVariant() const { return QVariant::fromValue(*this); } QDataStream &operator <<(QDataStream &stream, const Translation &t) { QVariantMap m; m.insert("context", t.context); m.insert("source_text", t.sourceText); m.insert("disambiguation", t.disambiguation); m.insert("n", tn); stream << m; return stream; } QDataStream &operator >>(QDataStream &stream, Translation &t) { QVariantMap m; stream >> m; t.context = m.value("context").toString(); t.sourceText = m.value("source_text").toString(); t.disambiguation = m.value("disambiguation").toString(); tn = m.value("n", -1).toInt(); return stream; }
私は
lupdateの興味深いプロパティを使用しました
。translate関数がどのネームスペースにあるかは関係ありません。主なことは、まったく同じ名前であり、引数の順序とタイプが
QCoreApplication :: translateのようになっていることです。 この場合、
翻訳機能でラップされた行は翻訳対象としてマークされ、.tsファイルに追加されます。
残りは小さいままです。静的
翻訳メソッドを実装して、
Translationクラスのインスタンスを作成します。これは、本質的には、
QT_TRANSLATE_NOOP3が返す匿名構造のより便利な類似物です。 別の
変換メソッドも追加しますが、静的ではありません。
QCoreApplication :: translate内で呼び出し、静的メソッド
Translation :: translateが呼び出されたときに指定されたコンテキスト、ソース行、コメントをパラメーターに渡します。 コピーおよび(デ)シリアル化のためのメソッドを追加し、翻訳を保存するための便利なコンテナを取得します。 クラスの他のメソッドについては説明しません。これらのメソッドは解決される問題に直接関係せず、この記事の対象となるC ++およびQtに精通している開発者にとっては些細なことです。
翻訳を使用したヘルプの例を次に示します。
QMap<QString, Translation> helpMap; void installHelp(const QString &command, const Translation &help) { helpMap.insert(command, help); } installHelp("mycommand", Translation::translate("context", "Do some cool stuff"));
それは工場よりも自然に見え、
QT_TRANSLATE_NOOP3よりきれいですよね?
不便2:継承なしの翻訳
Qtで私が遭遇した2番目の不便は、少なくとも1つのクラスを継承せずにインターフェイスを動的に変換できないことです。 すぐに例を見てみましょう:
int main(int argc, char **argv) { QApplication app(argc, argv); QTranslator *t = new QTranslator; t->load("/path/to/translations/myapp_ru.qm"); QApplication::installTranslator(t); QWidget *w = new QWidget; w->setWindowTitle(QApplication::translate("main", "Cool widget")); w->show(); LanguageSettingsWidget *lw = new LanguageSettingsWidget; lw->show(); int ret = app.exec(); delete w; return ret; }
例からわかるように、翻訳ファイルをロードし、
QWidgetを作成してその名前を設定します。 しかし、突然、ユーザーは
LanguageSettingsWidgetの使用を決定し、別の言語を選択しました。
QWidgetの名前は変更する必要がありますが、このために追加の手順を実行する必要があります。 繰り返しますが、いくつかのオプションがあります。
解決策1:継承
QWidgetから継承して、仮想メソッドのいずれかをオーバーライドできます。
class MyWidget : public QWidget { protected: void changeEvent(QEvent *e) { if (e->type() != QEvent::LanguageChange) return; setWindowTitle(tr("Cool widget")); } };
この場合、新しい
QTranslatorをインストールする
と、changeEventメソッドが呼び出され、この場合は
setWindowTitleが呼び出されます。 ただ? 十分。 便利ですか? 必ずしもそうとは限らないと思います(特に、移動のためだけにそのような庭をフェンスで囲む必要がある場合)。
解決策2:外部からの転送
また、このクラスへのポインターを
QWidgetから既に継承されている別のクラスに渡し、対応するメソッドをそこで呼び出すことができます。 私はコードを提供しません-それは明らかであり、前の例とほとんど変わりません。 これは間違いなく悪い方法であるとしか言えません-クラスがお互いを知っているほど、より良いです。
解決策3: 別の自転車、別のラッパー
アイデアは簡単です。
メタオブジェクトシステムとして便利なQtツールを使用しましょう(シグナルとスロットがここに属していることが理解されます)。 記事の最初の部分-Translatorからの翻訳オブジェクトだけでなく、ターゲットオブジェクトへのポインターを渡すクラスを作成します。 さらに、転送を書き込むプロパティ、または引数として渡す
スロットを指定します。 したがって、言葉を減らして、より多くの作業を行います。
dynamictranslator.h class DynamicTranslator : public QObject { Q_OBJECT private: QByteArray targetPropertyName; QByteArray targetSlotName; Translation translation; public: explicit DynamicTranslator(QObject *parent, const QByteArray &targetPropertyName, const Translation &t); explicit DynamicTranslator(QObject *parent, const Translation &t, const QByteArray &targetSlotName); protected: bool event(QEvent *e); private: Q_DISABLE_COPY(DynamicTranslator) };
dynamictranslator.cpp DynamicTranslator::DynamicTranslator(QObject *parent, const QByteArray &targetPropertyName, const Translation &t) : QObject(parent) { this->targetPropertyName = targetPropertyName; translation = t; } DynamicTranslator::DynamicTranslator(QObject *parent, const Translation &t, const QByteArray &targetSlotName) : QObject(parent) { this->targetSlotName = targetSlotName; translation = t; } bool DynamicTranslator::event(QEvent *e) { if (e->type() != QEvent::LanguageChange) return false; QObject *target = parent(); if (!target) return false; if (!targetPropertyName.isEmpty()) target->setProperty(targetPropertyName.constData(), translation.translate()); else if (!targetSlotName.isEmpty()) QMetaObject::invokeMethod(target, targetSlotName.constData(), Q_ARG(QString, translation.translate())); return false; }
ここで何が起こっていますか?
DynamicTranslatorクラスのインスタンスを作成するときに、ターゲットオブジェクト、翻訳、スロットの名前(
setWindowTitleなど )またはプロパティの名前(
windowTitle )を指定します。
DynamicTranslatorは、言語を変更する
たびに 、
QMetaObjectを使用して対応するスロットを
呼び出すか、
setPropertyを使用して目的のプロパティを設定します。 実際には次のようになります。
int main(int argc, char **argv) { QApplication app(argc, argv); QTranslator *t = new QTranslator; t->load("/path/to/translations/myapp_ru.qm"); QApplication::installTranslator(t); QWidget *w = new QWidget; Translation t = Translation::translate("main", "Cool widget"); w->setWindowTitle(t); new DynamicTranslator(w, "windowTitle", t); w->show(); LanguageSettingsWidget *lw = new LanguageSettingsWidget; lw->show(); int ret = app.exec(); delete w; return ret; }
ウィジェット
wは
DynamicTranslatorの親であるため、削除することを心配する必要はありません
。DynamicTranslatorは
QWidgetとともに削除され
ます 。
結論の代わりに
もちろん、翻訳の不便さに対処する考慮された方法は、唯一のものではありません。 たとえば、十分な大きさのアプリケーションでは、一般にQtが提供するものの代わりにサードパーティの翻訳ツールを使用できます(たとえば、すべてのテキストをファイルに保存し、コードで識別子のみを指定できます)。 繰り返しますが、大規模なアプリケーションでは、数十行の余分な行(継承またはファクトリ関数の作成の場合)で天気が悪くなることはありません。 ただし、ここで紹介するソリューションは、コードの時間と行を少し節約するだけでなく、このコードをより自然にすることができます。
批判と代替ソリューションは常に歓迎されます-より正確で、美しく、明確なコードを一緒に書きましょう。 ご清聴ありがとうございました。