Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Access to Qt Translation Infrastructure #206

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/native/QmlNet/QmlNet.pro
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ include(QmlNet.pri)

target.path = $$(PREFIX)/
INSTALLS += target

# Needed for CoTaskMemAlloc
win32:LIBS += ole32.lib
33 changes: 33 additions & 0 deletions src/native/QmlNet/QmlNet/qml/NetTranslator.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@

#include <QmlNet/qml/NetTranslator.h>
#include <QmlNet/types/Callbacks.h>
#include <QmlNetUtilities.h>

NetTranslator::NetTranslator(NetGCHandle *callbackDelegate, TranslateCallback callback, QObject *parent) : _callbackDelegate(callbackDelegate), _callback(callback), QTranslator(parent) {
}

bool NetTranslator::isEmpty() const {
// We only install this translator if C# has callbacks, so
// it is never considered to be empty
return false;
}

QString NetTranslator::translate(const char *context, const char *sourceText, const char *disambiguation, int n) const {
auto contextLength = context ? static_cast<int>(strlen(context)) : -1;
auto sourceTextLength = sourceText ? static_cast<int>(strlen(sourceText)) : -1;
auto disambiguationLength = disambiguation ? static_cast<int>(strlen(disambiguation)) : -1;

auto str = _callback(context, contextLength, sourceText, sourceTextLength, disambiguation, disambiguationLength, n);
return takeStringFromDotNet(str);
}

bool NetDataTranslator::load(QByteArray data, const QString &directory) {
_data = std::move(data);

if (!QTranslator::load(reinterpret_cast<const uchar*>(_data.constData()), _data.size(), directory)) {
_data = QByteArray(); // Do not keep the data copy alive unnecessarily
return false;
}

return true;
}
50 changes: 50 additions & 0 deletions src/native/QmlNet/QmlNet/qml/NetTranslator.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#ifndef NETTRANSLATOR_H
#define NETTRANSLATOR_H

#include <QTranslator>
#include <QString>
#include <QmlNet.h>
#include <QmlNet/types/NetDelegate.h>

/**
* Signature of the managed delegate when converted to an unmanaged function pointer.
* Please note that the returned pointer must be allocated with CoTaskMemAlloc or the
* cross-platform equivalent (usually malloc). I.e. by using Marshal.StringToCoTaskMemUni.
*/
using TranslateCallback = QChar* (__cdecl *)(const char *context, int contextLength, const char *sourceText, int sourceTextLength, const char *disambiguation, int disambiguationLength, int n);

class NetTranslator : public QTranslator
{
Q_OBJECT
public:
NetTranslator(NetGCHandle *callbackDelegate, TranslateCallback callback, QObject *parent = nullptr);

bool isEmpty() const override;
QString translate(const char *context, const char *sourceText, const char *disambiguation, int n) const override;

private:
NetDelegate const _callbackDelegate;
TranslateCallback const _callback;
};

/**
* Holds a reference to managed memory and frees it, once the translator is destroyed.
*/
class NetDataTranslator : public QTranslator
{
Q_OBJECT
public:
NetDataTranslator(QObject *parent = nullptr) : QTranslator(parent) {
}

/**
* Loads the translation data from the given data and keeps
* a copy around to keep the data alive.
*/
bool load(QByteArray data, const QString &directory = QString());

private:
QByteArray _data;
};

#endif // NETTRANSLATOR_H
87 changes: 87 additions & 0 deletions src/native/QmlNet/QmlNet/qml/QCoreApplication.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <QmlNet/qml/QCoreApplication.h>
#include <QmlNet/qml/NetVariantList.h>
#include <QmlNet/qml/NetQObject.h>
#include <QmlNet/qml/NetTranslator.h>
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QmlNetUtilities.h>
Expand Down Expand Up @@ -182,6 +183,92 @@ Q_DECL_EXPORT void qapp_exit(int returnCode)
QGuiApplication::exit(returnCode);
}

Q_DECL_EXPORT NetTranslator* qapp_installTranslator(NetGCHandle *callbackHandle, TranslateCallback callback)
{
auto app = QCoreApplication::instance();
if (!app) {
qWarning() << "Cannot install a translator without a QCoreApplication";
return nullptr;
}

auto translator = new NetTranslator(callbackHandle, callback, app);
if (!QCoreApplication::installTranslator(translator)) {
qWarning() << "Failed to install translator";
delete translator;
return nullptr;
}

return translator;
}

Q_DECL_EXPORT bool qapp_removeTranslator(NetTranslator *translator)
{
auto result = QCoreApplication::removeTranslator(translator);
delete translator;
return result;
}

Q_DECL_EXPORT bool qapp_loadTranslationData(const char *data, int dataLength, const QChar *directory)
{
auto app = QCoreApplication::instance();
if (!app) {
qWarning() << "Cannot install a translator without a QCoreApplication";
return false;
}

QByteArray dataBuffer(data, dataLength);
auto translator = new NetDataTranslator(app);
if (!translator->load(std::move(dataBuffer), QString(directory))) {
qWarning() << "Failed to load translation data";
delete translator;
return false;
}

if (!QCoreApplication::installTranslator(translator)) {
qWarning() << "Failed to install translator";
delete translator;
return false;
}

return true;
}

Q_DECL_EXPORT bool qapp_loadTranslationFile(const QChar *locale, const QChar *filename, const QChar *prefix, const QChar *directory, const QChar *suffix)
{
auto app = QCoreApplication::instance();
if (!app) {
qWarning() << "Cannot install a translator without a QCoreApplication";
return false;
}

auto translator = new QTranslator(app);
bool success;
if (locale) {
success = translator->load(QLocale(QString(locale)), QString(filename), QString(prefix), QString(directory), QString(suffix));
} else {
success = translator->load(QString(filename), QString(prefix), QString(directory), QString(suffix));
}

if (!success) {
qWarning() << "Failed to load translation data";
delete translator;
return false;
}

if (!QCoreApplication::installTranslator(translator)) {
qWarning() << "Failed to install translator";
delete translator;
return false;
}

return true;
}

Q_DECL_EXPORT QChar* qapp_translate(const char *context, const char *sourceText, const char *disambiguation, int n)
{
return returnStringToDotNet(QCoreApplication::translate(context, sourceText, disambiguation, n));
}

Q_DECL_EXPORT QCoreApplication* qapp_internalPointer(QGuiApplicationContainer* container)
{
return container->app;
Expand Down
6 changes: 4 additions & 2 deletions src/native/QmlNet/QmlNet/qml/qml.pri
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ HEADERS += \
$$PWD/NetQObject.h \
$$PWD/NetQObjectSignalConnection.h \
$$PWD/NetQObjectArg.h \
$$PWD/QLocaleInterop.h
$$PWD/QLocaleInterop.h \
$$PWD/NetTranslator.h

SOURCES += \
$$PWD/QQmlApplicationEngine.cpp \
Expand All @@ -42,4 +43,5 @@ SOURCES += \
$$PWD/NetQObject.cpp \
$$PWD/NetQObjectSignalConnection.cpp \
$$PWD/NetQObjectArg.cpp \
$$PWD/QLocaleInterop.cpp
$$PWD/QLocaleInterop.cpp \
$$PWD/NetTranslator.cpp
41 changes: 41 additions & 0 deletions src/native/QmlNet/QmlNetUtilities.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,44 @@ void freeString(QmlNetStringContainer* container)

}

// According to the Microsoft documentation, the string returned from unmanaged
// must be allocated using CoTaskMemAlloc, which doesn't exist on Linux ofcourse.
// This issue says that malloc should be used instead: https://github.com/dotnet/runtime/issues/10748
#ifdef Q_OS_WIN
#include <combaseapi.h>
#define interop_malloc CoTaskMemAlloc
#define interop_free CoTaskMemFree
#else
#define interop_malloc malloc
#define interop_free free
#endif

QChar *returnStringToDotNet(const QString &str)
{
static_assert(sizeof(QChar) == 2, "QChar must be 2-byte UTF-16");

if(str.isNull()) {
return nullptr;
}

auto len = str.length();
auto result = reinterpret_cast<QChar *>(interop_malloc((len + 1) * sizeof(QChar)));

memcpy(result, str.utf16(), len * sizeof(QChar));
result[len] = 0;

return result;
}

QString takeStringFromDotNet(QChar *str)
{
if (!str) {
return QString::null;
}

QString result(reinterpret_cast<QChar*>(str));

interop_free(str);

return result;
}
12 changes: 12 additions & 0 deletions src/native/QmlNet/QmlNetUtilities.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,16 @@ Q_DECL_EXPORT void freeString(QmlNetStringContainer* container);

}

/**
* Creates a copy of the string suitable for returning to managed code.
* Requires a [return:MarshalAs(LPWSTR)] on the managed side, and is
* only usable for *return values*.
*/
QChar *returnStringToDotNet(const QString &str);

/**
* Takes ownership of a string returned by .NET via Marshal.StringToCoTaskMemUni.
*/
QString takeStringFromDotNet(QChar *str);

#endif // QMLNETUTILITIES_H
70 changes: 70 additions & 0 deletions src/net/Qml.Net.Tests/QCoreApplicationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.IO;
using Xunit;
using FluentAssertions;

namespace Qml.Net.Tests
{
public class QCoreApplicationTests
{
#if NETCOREAPP3_1
[Fact]
public void CanInstallTranslator()
{
string Translator(ReadOnlySpan<char> context, ReadOnlySpan<char> sourceText, ReadOnlySpan<char> disambiguation, int n)
{
return $"translated:{new string(context)}:{new string(sourceText)}:{new string(disambiguation)}:{n}";
}

using (var app = new QCoreApplication())
{
app.InstallTranslator(Translator);

var result = app.Translate("ctx", "src", "disambig", 2);
result.Should().Be("translated:ctx:src:disambig:2");
}
}

[Fact]
public void CanRemoveTranslator()
{
string Translator(ReadOnlySpan<char> context, ReadOnlySpan<char> sourceText, ReadOnlySpan<char> disambiguation, int n)
{
return "should not be called";
}

using (var app = new QCoreApplication())
{
Translator translatorDel = Translator;
app.InstallTranslator(translatorDel);
app.RemoveTranslator(translatorDel);

var result = app.Translate("ctx", "src");
result.Should().Be("src");
}
}

[Fact]
public void CanLoadTranslationFromMemory()
{
var resourcesFolder = Path.Join(Path.GetDirectoryName(typeof(QCoreApplicationTests).Assembly.Location),
"Resources");
var qmFile = Path.Join(resourcesFolder, "example.de.qm");
var qmData = File.ReadAllBytes(qmFile);

using (var app = new QCoreApplication())
{
// With no translations installed, the result should be the source text
var untranslated = app.Translate("QPushButton", "Hello world!");
untranslated.Should().Be("Hello world!");

app.LoadTranslationData(qmData);

// After installing our translations, it should be the text from example.de.ts
var translated = app.Translate("QPushButton", "Hello world!");
translated.Should().Be("Hallo from Deutschland!");
}
}
#endif
}
}
6 changes: 6 additions & 0 deletions src/net/Qml.Net.Tests/Qml.Net.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;netcoreapp3.1</TargetFrameworks>
<IsPackable>false</IsPackable>
<EmbeddedResourceUseDependentUponConvention>false</EmbeddedResourceUseDependentUponConvention>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.6.0" />
Expand All @@ -16,4 +17,9 @@
<ItemGroup>
<ProjectReference Include="..\Qml.Net\Qml.Net.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Resources\*.qm">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
Binary file added src/net/Qml.Net.Tests/Resources/example.de.qm
Binary file not shown.
11 changes: 11 additions & 0 deletions src/net/Qml.Net.Tests/Resources/example.de.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="de_DE" sourcelanguage="de">
<context>
<name>QPushButton</name>
<message>
<source>Hello world!</source>
<translation>Hallo from Deutschland!</translation>
</message>
</context>
</TS>
Binary file added src/net/Qml.Net.Tests/Resources/example.de_CH.qm
Binary file not shown.
11 changes: 11 additions & 0 deletions src/net/Qml.Net.Tests/Resources/example.de_CH.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="de_DE" sourcelanguage="de">
<context>
<name>QPushButton</name>
<message>
<source>Hello world!</source>
<translation>Grüezi aus der Schweiz!</translation>
</message>
</context>
</TS>
11 changes: 11 additions & 0 deletions src/net/Qml.Net.Tests/Resources/example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
<context>
<name>QPushButton</name>
<message>
<source>Hello world!</source>
<translation type="unfinished"></translation>
</message>
</context>
</TS>