diff --git a/src/acbf/AcbfBookinfo.h b/src/acbf/AcbfBookinfo.h
--- a/src/acbf/AcbfBookinfo.h
+++ b/src/acbf/AcbfBookinfo.h
@@ -346,6 +346,12 @@
* @param index
*/
Q_INVOKABLE void removeLanguage(int index);
+ /**
+ * @brief language
+ * @param index of language.
+ * @return language at index;
+ */
+ Q_INVOKABLE Language* language(int index);
/**
* @return a list of sequence objects that describe the series and
diff --git a/src/acbf/AcbfBookinfo.cpp b/src/acbf/AcbfBookinfo.cpp
--- a/src/acbf/AcbfBookinfo.cpp
+++ b/src/acbf/AcbfBookinfo.cpp
@@ -586,6 +586,11 @@
removeLanguage(d->languages.at(index));
}
+Language *BookInfo::language(int index)
+{
+ return d->languages.at(index);
+}
+
QList BookInfo::sequence()
{
return d->sequence;
diff --git a/src/acbf/AcbfLanguage.h b/src/acbf/AcbfLanguage.h
--- a/src/acbf/AcbfLanguage.h
+++ b/src/acbf/AcbfLanguage.h
@@ -40,6 +40,8 @@
class ACBF_EXPORT Language : public QObject
{
Q_OBJECT
+ Q_PROPERTY(QString language READ language WRITE setLanguage NOTIFY languageChanged)
+ Q_PROPERTY(bool show READ show WRITE setShow NOTIFY showChanged)
public:
explicit Language(BookInfo* parent = nullptr);
~Language() override;
@@ -64,6 +66,10 @@
* @returns the language of this language entry.
*/
QString language() const;
+ /**
+ * @brief fires when language is set.
+ */
+ Q_SIGNAL void languageChanged();
/**
* \brief set whether the language entry should be overlaid(true) or is the native
@@ -76,6 +82,10 @@
* language(false).
*/
bool show() const;
+ /**
+ * @brief fires when show is set.
+ */
+ Q_SIGNAL void showChanged();
private:
class Private;
std::unique_ptr d;
diff --git a/src/acbf/AcbfLanguage.cpp b/src/acbf/AcbfLanguage.cpp
--- a/src/acbf/AcbfLanguage.cpp
+++ b/src/acbf/AcbfLanguage.cpp
@@ -40,6 +40,7 @@
: QObject(parent)
, d(new Private)
{
+ qRegisterMetaType("Language*");
}
Language::~Language() = default;
@@ -64,6 +65,7 @@
void Language::setLanguage(const QString& language)
{
d->language = language;
+ emit languageChanged();
}
QString Language::language() const
@@ -74,6 +76,7 @@
void Language::setShow(bool show)
{
d->show = show;
+ emit showChanged();
}
bool Language::show() const
diff --git a/src/acbf/AcbfPage.cpp b/src/acbf/AcbfPage.cpp
--- a/src/acbf/AcbfPage.cpp
+++ b/src/acbf/AcbfPage.cpp
@@ -301,7 +301,7 @@
}
}
}
- setTextLayer(to);
+ setTextLayer(to, languageTo);
}
QStringList Page::textLayerLanguages() const
diff --git a/src/creator/qml/BookMetainfoPage.qml b/src/creator/qml/BookMetainfoPage.qml
--- a/src/creator/qml/BookMetainfoPage.qml
+++ b/src/creator/qml/BookMetainfoPage.qml
@@ -21,6 +21,7 @@
import QtQuick 2.2
import QtQuick.Controls 2.2 as QtControls
+import QtQuick.Dialogs 1.2
import org.kde.kirigami 2.1 as Kirigami
@@ -109,6 +110,98 @@
text:root.model.acbfData ? root.model.acbfData.metaData.bookInfo.keywords("").join(", ") : "";
}
+ Kirigami.Heading {
+ width: parent.width;
+ height: paintedHeight + Kirigami.Units.smallSpacing * 2;
+ text: i18nc("label text for the edit field for the language list", "Languages");
+ }
+
+ Repeater {
+ model: root.model.acbfData ? root.model.acbfData.metaData.bookInfo.languageEntryList: 0;
+ delegate: QtControls.Label {
+ width:parent.width - showLanguageCheckbox.width - setDefaultLanguage.width -removeLanguageButton.width - (Kirigami.Units.smallSpacing*3);
+ height: setDefaultLanguage.height;
+ text: modelData !== ""? Qt.locale(modelData).nativeLanguageName + " (%1)".arg(modelData): i18nc("default textlayer", "Default");
+ QtControls.Button {
+ id: setDefaultLanguage;
+ anchors {
+ left: parent.right;
+ leftMargin: Kirigami.Units.smallSpacing;
+ }
+ text: i18nc("Text for copy button", "Copy to default layer");
+ visible: modelData !== "";
+ onClicked: {
+ var text = root.model.acbfData.metaData.bookInfo.title(modelData);
+ root.model.acbfData.metaData.bookInfo.setTitle(text, "");
+ defaultTitle.text = text;;
+ text = root.model.acbfData.metaData.bookInfo.annotation(modelData);
+ root.model.acbfData.metaData.bookInfo.setAnnotation(text, "");
+ defaultAnnotation.text = text.join("\n\n");
+ text = root.model.acbfData.metaData.bookInfo.keywords(modelData);
+ root.model.acbfData.metaData.bookInfo.setKeywords(text, "");
+ defaultKeywords.text = text.join(", ");
+ for (var i = 0; i< root.model.acbfData.body.pageCount; i++) {
+ root.model.acbfData.body.page(i).duplicateTextLayer(modelData, "");
+ }
+ root.model.setDirty();
+ }
+ }
+ QtControls.CheckBox{
+ id: showLanguageCheckbox;
+ anchors {
+ left: setDefaultLanguage.right;
+ leftMargin: Kirigami.Units.smallSpacing;
+ }
+ checked: root.model.acbfData.metaData.bookInfo.language(index).show;
+ text: i18nc("Label of checkbox for the 'show' property.", "Show");
+ height: parent.height;
+ onToggled: root.model.acbfData.metaData.bookInfo.language(index).show = checked;
+ }
+
+ QtControls.Button {
+ id: removeLanguageButton;
+ anchors {
+ left: showLanguageCheckbox.right;
+ leftMargin: Kirigami.Units.smallSpacing;
+ }
+ contentItem: Kirigami.Icon {
+ source: "list-remove";
+ }
+ height: parent.height;
+ width: height;
+ onClicked: {
+ // When removing, set the model dirty first, and then remove the entry to avoid reference errors.
+ for (var i = 0; i< root.model.acbfData.body.pageCount; i++) {
+ root.model.acbfData.body.page(i).removeTextLayer(modelData);
+ }
+ root.model.setDirty();
+ root.model.acbfData.metaData.bookInfo.removeLanguage(index);
+ }
+ }
+ }
+ }
+ Item {
+ width: parent.width;
+ height: childrenRect.height;
+ QtControls.Button {
+ text: i18nc("Label for POT export button.", "Export default language POT");
+ width: (parent.width-Kirigami.Units.smallSpacing)/2;
+ onClicked: exportPOT.open();
+ id: exportPOTButton;
+ }
+ QtControls.Button {
+ anchors {
+ left: exportPOTButton.right;
+ leftMargin: Kirigami.Units.smallSpacing;
+ top: exportPOTButton.top;
+ }
+
+ text: i18nc("Label for PO impot button.", "Import translation PO");
+ width: (parent.width-Kirigami.Units.smallSpacing)/2;
+ onClicked: importPO.open();
+ }
+ }
+
Kirigami.Heading {
width: parent.width;
height: paintedHeight + Kirigami.Units.smallSpacing * 2;
@@ -1051,5 +1144,81 @@
root.model.setDirty();
}
}
+
+ FileDialog {
+ id: exportPOT;
+ title: i18nc("Title of the folder selection fialog for exporting pot","Please choose a location to save the POT file.")
+ folder: mainWindow.homeDir();
+ selectFolder: true;
+ property int splitPos: osIsWindows ? 8 : 7;
+ onAccepted: {
+ if(folder.toString().substring(0, 7) === "file://") {
+ var file = model.filename.split("/").pop();
+ file = file.split(".")[0];
+ root.model.generatePot( folder.toString().substring(splitPos)+"/"+file+".pot", "");
+ }
+ }
+ onRejected: {
+ // Just do nothing, we don't really care...
+ }
+ }
+
+ FileDialog {
+ id: importPO;
+ title: i18nc("Title of the file selection fialog for importing po files","Please choose a PO file to load.")
+ folder: mainWindow.homeDir();
+ nameFilters: ["PO translation files (*.po)"];
+ property int splitPos: osIsWindows ? 8 : 7;
+ onAccepted: {
+ if(fileUrl.toString().substring(0, 7) === "file://") {
+ addLanguageFromPOFile.summary = root.model.readPoFileSummary(fileUrl.toString().substring(splitPos));
+ addLanguageFromPOFile.url = fileUrl.toString().substring(splitPos);
+ addLanguageFromPOFile.open();
+ }
+ }
+ onRejected: {
+ // Just do nothing, we don't really care...
+ }
+ }
+
+ Dialog {
+ id: addLanguageFromPOFile;
+ property var summary: ["lang", "author"];
+ property string url: "";
+ signal save();
+ title: i18nc("Title for adding translation from po file.", "Add translation from PO file");
+ standardButtons: StandardButton.Save | StandardButton.Cancel;
+ width: childrenRect.width;
+ Column {
+ width: parent.width;
+ height: childrenRect.height;
+ spacing: Kirigami.Units.smallSpacing;
+ QtControls.Label{
+ width: parent.width;
+ text: i18nc("Language label for import Po file", "Language: %1 (%2)", Qt.locale(addLanguageFromPOFile.summary[0]).nativeLanguageName, addLanguageFromPOFile.summary[0]);
+ }
+ Item {
+ width: parent.width;
+ height: Kirigami.Units.smallSpacing;
+ }
+ QtControls.Label{
+ width: parent.width;
+ text: i18nc("Author label for import Po file", "Author: %1", addLanguageFromPOFile.summary[1]);
+ }
+ Item {
+ width: parent.width;
+ height: Kirigami.Units.smallSpacing;
+ }
+ QtControls.CheckBox {
+ id: emailCheckBox;
+ width: parent.width;
+ text: i18nc("label for include translator's email checkbox", "Include translator's email");
+ checked: false;
+ }
+ }
+ onAccepted: {
+ root.model.readPoFile(url, emailCheckBox.checked);
+ }
+ }
}
}
diff --git a/src/qtquick/ArchiveBookModel.h b/src/qtquick/ArchiveBookModel.h
--- a/src/qtquick/ArchiveBookModel.h
+++ b/src/qtquick/ArchiveBookModel.h
@@ -172,6 +172,28 @@
*/
Q_INVOKABLE QString createBook(QString folder, QString title, QString coverUrl);
+ /**
+ * @brief Generate a POT file from a given text layer. These can be
+ * used with translation programs to make translation files.
+ * @param fileName the filename to write the pot to.
+ * @param language the language of which to use the textlayer.
+ * @return whether creating the filename was succesful.
+ */
+ Q_INVOKABLE bool generatePot(const QString fileName, const QString language);
+ /**
+ * @brief readPoFile
+ * @param fileName po file to read.
+ * @param addTranslatorEmail whether to include the translator's email address.
+ * @return
+ */
+ Q_INVOKABLE bool readPoFile(const QString fileName, bool addTranslatorEmail);
+ /**
+ * @brief readPoFileSummary
+ * @param fileName po file to read.
+ * @return a qstringlist with the language on the first entry and the translator on the second.
+ */
+ Q_INVOKABLE QStringList readPoFileSummary(const QString fileName);
+
friend class ArchiveImageProvider;
protected:
const KArchiveFile* archiveFile(const QString& filePath);
diff --git a/src/qtquick/ArchiveBookModel.cpp b/src/qtquick/ArchiveBookModel.cpp
--- a/src/qtquick/ArchiveBookModel.cpp
+++ b/src/qtquick/ArchiveBookModel.cpp
@@ -30,6 +30,8 @@
#include
#include
#include
+#include
+#include
#include
#include
@@ -1251,3 +1253,324 @@
}
return false;
}
+
+bool ArchiveBookModel::generatePot(const QString fileName, const QString language)
+{
+ QFile file(fileName);
+ qDebug() << fileName;
+ bool success = false;
+
+ if (file.open(QFile::WriteOnly| QFile::Truncate) && acbfData()) {
+ AdvancedComicBookFormat::Document* acbf = qobject_cast(acbfData());
+ QTextStream pot(&file);
+ QString quote = "\"";
+ QString newLine = "\n";
+
+ pot << "msid " << quote+quote+newLine;
+ pot << "msgstr " << quote+quote+newLine;
+
+ pot << quote << "POT-Creation-Date: " << QDateTime::currentDateTimeUtc().toString() << "\\n" << quote << newLine;
+ pot << quote << "Content-Type: text/plain; charset=UTF-8\\n" << quote << newLine;
+ pot << quote << "Content-Transfer-Encoding: 8bit\\n" << quote << newLine;
+ pot << quote << "X-Generator: Peruse Creator\\n" << quote << newLine;
+
+ pot << newLine;
+ pot << "#. Title of the work" << newLine;
+ pot << "msgctxt \"@meta-title\"" << newLine;
+ pot << "msgid " << quote << acbf->metaData()->bookInfo()->title(language) << quote << newLine;
+ pot << "msgstr " << quote << quote << newLine;
+ pot << newLine;
+
+ pot << "#. The summary" << newLine;
+ pot << "msgctxt \"@meta-summary\"" << newLine;
+ pot << "msgid " << quote << quote << newLine;
+ for (int i =0; i < acbf->metaData()->bookInfo()->annotation(language).size(); i++) {
+ QString paragraph = acbf->metaData()->bookInfo()->annotation(language).at(i);
+ paragraph.replace(quote, "\\\"");
+ paragraph.replace("\'", "\\\'");
+ paragraph.replace("#", "\\#");
+ pot << quote << "" << paragraph << "
" << quote << newLine;
+ }
+ pot << "msgstr " << quote << quote << newLine;
+ pot << newLine;
+
+ pot << "#. The keywords, these need to be comma separated." + newLine;
+ pot << "msgctxt \"@meta-keywords\"" << newLine;
+ pot << "msgid " << quote << acbf->metaData()->bookInfo()->keywords(language).join(", ") << quote << newLine;
+ pot << "msgstr " << quote << quote << newLine;
+ pot << newLine;
+
+ // cover.
+ AdvancedComicBookFormat::Textlayer* textlayer = acbf->metaData()->bookInfo()->coverpage()->textLayer(language);
+
+ for (int t = 0; t < textlayer->textareas().size(); t++) {
+ AdvancedComicBookFormat::Textarea* textarea = textlayer->textarea(t);
+
+ pot << newLine;
+ pot << "msgid " << quote << quote << newLine;
+ for (int i =0; i < textarea->paragraphs().size(); i++) {
+ QString paragraph = textarea->paragraphs().at(i);
+ paragraph.replace(quote, "\\\"");
+ paragraph.replace("\'", "\\\'");
+ paragraph.replace("#", "\\#");
+ pot << quote << "" << paragraph << "
" << quote << newLine;
+ }
+ pot << "msgstr " << quote << quote << newLine;
+ pot << newLine;
+ }
+
+ // pages.
+ for (int p = 0; p < acbf->body()->pageCount(); p++) {
+ textlayer = acbf->body()->page(p)->textLayer(language);
+
+ if (!acbf->body()->page(p)->title("").isEmpty()) {
+ pot << "#. Page title" << newLine;
+ pot << "msgid " << quote << acbf->body()->page(p)->title("") << quote << newLine;
+ pot << "msgstr " << quote << quote << newLine;
+ pot << newLine;
+ }
+
+ for (int t = 0; t < textlayer->textareas().size(); t++) {
+ AdvancedComicBookFormat::Textarea* textarea = textlayer->textarea(t);
+
+ pot << newLine;
+ pot << "msgid " << quote << quote << newLine;
+ for (int i =0; i < textarea->paragraphs().size(); i++) {
+ QString paragraph = textarea->paragraphs().at(i);
+ paragraph.replace(quote, "\\\"");
+ paragraph.replace("\'", "\\\'");
+ paragraph.replace("#", "\\#");
+ pot << quote << "" << paragraph << "
" << quote << newLine;
+ }
+ pot << "msgstr " << quote << quote << newLine;
+ pot << newLine;
+ }
+ }
+ success = true;
+ }
+ file.close();
+ return success;
+}
+
+bool ArchiveBookModel::readPoFile(const QString fileName, bool addTranslatorEmail)
+{
+ QFile file(fileName);
+ bool success = false;
+ QString language;
+ QString translator;
+ QString multiline;
+ QString context;
+ QString id;
+ QString translation;
+ QHash table;
+
+ if (file.open(QFile::ReadOnly|QFile::Text)) {
+ QTextStream po(&file);
+ QString line;
+ while (po.readLineInto(&line)) {
+ if (line.startsWith("\"Last-Translator: "))
+ {
+ line.remove("\"Last-Translator: ");
+ translator = line.replace("\\n\"", "");
+ }
+
+ else if (line.startsWith("\"Language: "))
+ {
+ line.remove("\"Language: ");
+ language = line.replace("\\n\"", "");
+ }
+
+ else if (line.startsWith("msgctxt"))
+ {
+ if (!line.endsWith("\"\"")) {
+ line.remove("msgctxt \"");
+ context = line.replace("\"", "");
+ }
+ }
+
+ else if (line.startsWith("msgid"))
+ {
+ if (!multiline.isEmpty()) {
+ context.append(multiline);
+ multiline.clear();
+ }
+ if (!line.endsWith("\"\"")) {
+ line.remove("msgid \"");
+ id = line.replace("\"", "");
+ }
+ }
+
+ else if (line.startsWith("msgstr"))
+ {
+ if (!multiline.isEmpty()) {
+ id.append(multiline);
+ multiline.clear();
+ }
+ if (!line.endsWith("\"\"")) {
+ line.remove("msgstr \"");
+ translation = line.replace("\"", "");
+ }
+ }
+
+ else if (line.startsWith("\""))
+ {
+ if (!line.endsWith("\"\"")) {
+ line.remove("\"");
+ multiline.append(line.replace("\"", ""));
+ }
+ }
+
+ else if (line.isEmpty()) {
+ translation.append(multiline);
+ multiline.clear();
+ if (!id.isEmpty()) {
+ if (context.isEmpty()) {
+ table.insert(id, translation);
+ } else {
+ table.insert(context, translation);
+ }
+ }
+ context.clear();
+ id.clear();
+ translation.clear();
+ } else {
+ multiline.clear();
+ context.clear();
+ id.clear();
+ translation.clear();
+ }
+
+ }
+
+ AdvancedComicBookFormat::Document* acbf = qobject_cast(acbfData());
+ if (!acbf->metaData()->bookInfo()->languageEntryList().contains(language)) {
+ acbf->metaData()->bookInfo()->addLanguage(language);
+ }
+ if (!table.value("@meta-title").isEmpty()) {
+ acbf->metaData()->bookInfo()->setTitle(table.value("@meta-title"), language);
+ qDebug() << Q_FUNC_INFO << "adding translation" << language << acbf->metaData()->bookInfo()->title(language);
+ }
+ if (!table.value("@meta-summary").isEmpty()) {
+ QStringList paragraphs = table.value("@meta-summary").split("
", QString::SkipEmptyParts);
+ for (int i=0; i < paragraphs.size(); i++) {
+ QString p = paragraphs.at(i);
+ p.replace("\\\"", "\"");
+ p.replace("\\\'", "\'");
+ p.replace("\\#", "#");
+ paragraphs.replace(i, p.replace("", "").trimmed());
+ }
+ acbf->metaData()->bookInfo()->setAnnotation(paragraphs, language);
+ qDebug() << Q_FUNC_INFO << "adding translation" << language << paragraphs;
+ }
+ if (!table.value("@meta-keywords").isEmpty()) {
+ QStringList keys = table.value("@meta-keywords").split(",", QString::SkipEmptyParts);
+ for (int i=0; i < keys.size(); i++) {
+ QString p = keys.at(i);
+ keys.replace(i, p.trimmed());
+ }
+ qDebug() << Q_FUNC_INFO << "adding translation" << language << keys;
+ acbf->metaData()->bookInfo()->setKeywords(keys, language);
+ }
+
+ // cover
+ acbf->metaData()->bookInfo()->coverpage()->duplicateTextLayer("", language);
+ AdvancedComicBookFormat::Textlayer* translation = acbf->metaData()->bookInfo()->coverpage()->textLayer(language);
+ for (int t=0; t < translation->textareaPointStrings().size(); t++) {
+ QString key;
+ for (int i =0; i < translation->textarea(t)->paragraphs().size(); i++) {
+ QString paragraph = translation->textarea(t)->paragraphs().at(i);
+ paragraph.replace("\"", "\\\"");
+ paragraph.replace("\'", "\\\'");
+ paragraph.replace("#", "\\#");
+ key.append(paragraph);
+ }
+ QStringList paragraphs = table.value("
"+key+"
").split("", QString::SkipEmptyParts);
+ for (int i=0; i < paragraphs.size(); i++) {
+ QString p = paragraphs.at(i);
+ p.replace("\\\"", "\"");
+ p.replace("\\\'", "\'");
+ p.replace("\\#", "#");
+ paragraphs.replace(i, p.replace("", "").trimmed());
+ }
+ translation->textarea(t)->setParagraphs(paragraphs);
+ if (!paragraphs.isEmpty()) {
+ qDebug() << Q_FUNC_INFO << "adding translation" << language << translation->textarea(t)->paragraphs();
+ }
+ }
+ // pages
+ for (int p = 0; p < acbf->body()->pageCount(); p++) {
+ acbf->body()->page(p)->duplicateTextLayer("", language);
+ if (!table.value(acbf->body()->page(p)->title("")).isEmpty()) {
+ acbf->body()->page(p)->setTitle(table.value(acbf->body()->page(p)->title("")), language);
+ qDebug() << Q_FUNC_INFO << "adding translation" << language << acbf->body()->page(p)->title(language);
+ }
+ AdvancedComicBookFormat::Textlayer* translation = acbf->body()->page(p)->textLayer(language);
+ for (int t=0; t < translation->textareaPointStrings().size(); t++) {
+ QString key;
+ for (int i =0; i < translation->textarea(t)->paragraphs().size(); i++) {
+ QString paragraph = translation->textarea(t)->paragraphs().at(i);
+ paragraph.replace("\"", "\\\"");
+ paragraph.replace("\'", "\\\'");
+ paragraph.replace("#", "\\#");
+ key.append(paragraph);
+ }
+ QStringList paragraphs = table.value("
"+key+"
").split("", QString::SkipEmptyParts);
+ for (int i=0; i < paragraphs.size(); i++) {
+ QString p = paragraphs.at(i);
+ p.replace("\\\"", "\"");
+ p.replace("\\\'", "\'");
+ p.replace("\\#", "#");
+ paragraphs.replace(i, p.replace("", "").trimmed());
+ }
+ translation->textarea(t)->setParagraphs(paragraphs);
+ if (!paragraphs.isEmpty()) {
+ qDebug() << Q_FUNC_INFO << "adding translation" << language << translation->textarea(t)->paragraphs();
+ }
+ }
+ }
+ // Finally, add translator.
+ QStringList emails;
+ if (addTranslatorEmail) {
+ QString email = translator.split("<").at(1);
+ email.replace(">", "");
+ emails.append(email);
+ }
+ translator = translator.split("<").at(0);
+ qDebug() << Q_FUNC_INFO << "adding translator" << translator << emails;
+
+ if (acbf->metaData()->bookInfo()->authorNames().indexOf(translator) < 0) {
+ acbf->metaData()->bookInfo()->addAuthor("Translator", language, "", "", "", translator, QStringList(), emails);
+ }
+
+ }
+ file.close();
+ return success;
+}
+
+QStringList ArchiveBookModel::readPoFileSummary(const QString fileName)
+{
+ QFile file(fileName);
+ QString translator;
+ QString language;
+ if (file.open(QFile::ReadOnly|QFile::Text)) {
+ QTextStream po(&file);
+ QString line;
+ while (po.readLineInto(&line) && (translator.isEmpty() || language.isEmpty())) {
+ if (line.startsWith("\"Last-Translator: "))
+ {
+ line.remove("\"Last-Translator: ");
+ translator = line.replace("\\n\"", "");
+ }
+
+ else if (line.startsWith("\"Language: "))
+ {
+ line.remove("\"Language: ");
+ language = line.replace("\\n\"", "");
+ }
+ }
+ }
+ QStringList strings;
+ strings << language;
+ strings << translator;
+ return strings;
+}