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; +}