diff --git a/src/catalog/catalog.cpp b/src/catalog/catalog.cpp index 18cea48..69ca480 100644 --- a/src/catalog/catalog.cpp +++ b/src/catalog/catalog.cpp @@ -1,1053 +1,1053 @@ /* **************************************************************************** This file is part of Lokalize This file contains parts of KBabel code Copyright (C) 1999-2000 by Matthias Kiefer 2001-2005 by Stanislav Visnovsky 2006 by Nicolas Goutte 2007-2014 by Nick Shaforostoff This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #include "catalog.h" #include "catalog_private.h" #include "project.h" #include "projectmodel.h" //to notify about modification #include "catalogstorage.h" #include "gettextstorage.h" #include "gettextimport.h" #include "gettextexport.h" #include "xliffstorage.h" #include "tsstorage.h" #include "mergecatalog.h" #include "version.h" #include "prefs_lokalize.h" #include "jobs.h" #include "dbfilesmodel.h" #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #define U QLatin1String #else #define U QStringLiteral #endif //QString Catalog::supportedMimeFilters("text/x-gettext-translation application/x-xliff application/x-linguist"); //" text/x-gettext-translation-template") QString Catalog::supportedFileTypes(bool includeTemplates) { QString sep = QStringLiteral(";;"); QString all = i18n("All supported files (*.po *.pot *.xlf *.xliff *.ts)") + sep; return all + (includeTemplates ? i18n("Gettext (*.po *.pot)") : i18n("Gettext (*.po)")) + sep + i18n("XLIFF (*.xlf *.xliff)") + sep + i18n("Linguist (*.ts)"); } static const QString extensions[] = {U(".po"), U(".pot"), U(".xlf"), U(".xliff"), U(".ts")}; static const char* const xliff_states[] = { I18N_NOOP("New"), I18N_NOOP("Needs translation"), I18N_NOOP("Needs full localization"), I18N_NOOP("Needs adaptation"), I18N_NOOP("Translated"), I18N_NOOP("Needs translation review"), I18N_NOOP("Needs full localization review"), I18N_NOOP("Needs adaptation review"), I18N_NOOP("Final"), I18N_NOOP("Signed-off") }; const char* const* Catalog::states() { return xliff_states; } QStringList Catalog::supportedExtensions() { QStringList result; int i = sizeof(extensions) / sizeof(QString); while (--i >= 0) result.append(extensions[i]); return result; } bool Catalog::extIsSupported(const QString& path) { QStringList ext = supportedExtensions(); int i = ext.size(); while (--i >= 0 && !path.endsWith(ext.at(i))) ; return i != -1; } Catalog::Catalog(QObject *parent) : QUndoStack(parent) , d(this) , m_storage(0) { //cause refresh events for files modified from lokalize itself aint delivered automatically connect(this, QOverload::of(&Catalog::signalFileSaved), Project::instance()->model(), QOverload::of(&ProjectModel::slotFileSaved), Qt::QueuedConnection); QTimer* t = &(d._autoSaveTimer); t->setInterval(2 * 60 * 1000); t->setSingleShot(false); connect(t, &QTimer::timeout, this, &Catalog::doAutoSave); connect(this, QOverload<>::of(&Catalog::signalFileSaved), t, QOverload<>::of(&QTimer::start)); connect(this, QOverload<>::of(&Catalog::signalFileLoaded), t, QOverload<>::of(&QTimer::start)); connect(this, &Catalog::indexChanged, this, &Catalog::setAutoSaveDirty); connect(Project::local(), &Project::configChanged, this, &Catalog::projectConfigChanged); } Catalog::~Catalog() { clear(); //delete m_storage; //deleted in clear(); } void Catalog::clear() { setIndex(cleanIndex());//to keep TM in sync QUndoStack::clear(); d._errorIndex.clear(); d._nonApprovedIndex.clear(); d._emptyIndex.clear(); delete m_storage; m_storage = 0; d._filePath.clear(); d._lastModifiedPos = DocPosition(); d._modifiedEntries.clear(); while (!d._altTransCatalogs.isEmpty()) d._altTransCatalogs.takeFirst()->deleteLater(); d._altTranslations.clear(); /* d.msgidDiffList.clear(); d.msgstr2MsgidDiffList.clear(); d.diffCache.clear(); */ } void Catalog::push(QUndoCommand* cmd) { generatePhaseForCatalogIfNeeded(this); QUndoStack::push(cmd); } //BEGIN STORAGE TRANSLATION int Catalog::capabilities() const { if (Q_UNLIKELY(!m_storage)) return 0; return m_storage->capabilities(); } int Catalog::numberOfEntries() const { if (Q_UNLIKELY(!m_storage)) return 0; return m_storage->size(); } static DocPosition alterForSinglePlural(const Catalog* th, DocPosition pos) { //if source lang is english (implied) and target lang has only 1 plural form (e.g. Chinese) if (Q_UNLIKELY(th->numberOfPluralForms() == 1 && th->isPlural(pos))) pos.form = 1; return pos; } QString Catalog::msgid(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->source(alterForSinglePlural(this, pos)); } -QString Catalog::msgidWithPlurals(const DocPosition& pos) const +QString Catalog::msgidWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { if (Q_UNLIKELY(!m_storage)) return QString(); - return m_storage->sourceWithPlurals(pos); + return m_storage->sourceWithPlurals(pos, truncateFirstLine); } QString Catalog::msgstr(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->target(pos); } -QString Catalog::msgstrWithPlurals(const DocPosition& pos) const +QString Catalog::msgstrWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { if (Q_UNLIKELY(!m_storage)) return QString(); - return m_storage->targetWithPlurals(pos); + return m_storage->targetWithPlurals(pos, truncateFirstLine); } CatalogString Catalog::sourceWithTags(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return CatalogString(); return m_storage->sourceWithTags(alterForSinglePlural(this, pos)); } CatalogString Catalog::targetWithTags(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return CatalogString(); return m_storage->targetWithTags(pos); } CatalogString Catalog::catalogString(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return CatalogString(); return m_storage->catalogString(pos.part == DocPosition::Source ? alterForSinglePlural(this, pos) : pos); } QVector Catalog::notes(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QVector(); return m_storage->notes(pos); } QVector Catalog::developerNotes(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QVector(); return m_storage->developerNotes(pos); } Note Catalog::setNote(const DocPosition& pos, const Note& note) { if (Q_UNLIKELY(!m_storage)) return Note(); return m_storage->setNote(pos, note); } QStringList Catalog::noteAuthors() const { if (Q_UNLIKELY(!m_storage)) return QStringList(); return m_storage->noteAuthors(); } void Catalog::attachAltTransCatalog(Catalog* altCat) { d._altTransCatalogs.append(altCat); if (numberOfEntries() != altCat->numberOfEntries()) qCWarning(LOKALIZE_LOG) << altCat->url() << "has different number of entries"; } void Catalog::attachAltTrans(int entry, const AltTrans& trans) { d._altTranslations.insert(entry, trans); } QVector Catalog::altTrans(const DocPosition& pos) const { QVector result; if (m_storage) result = m_storage->altTrans(pos); foreach (Catalog* altCat, d._altTransCatalogs) { if (pos.entry >= altCat->numberOfEntries()) { qCDebug(LOKALIZE_LOG) << "ignoring" << altCat->url() << "this time because" << pos.entry << "<" << altCat->numberOfEntries(); continue; } if (altCat->source(pos) != source(pos)) { qCDebug(LOKALIZE_LOG) << "ignoring" << altCat->url() << "this time because s don't match"; continue; } QString target = altCat->msgstr(pos); if (!target.isEmpty() && altCat->isApproved(pos)) { result << AltTrans(); result.last().target = target; result.last().type = AltTrans::Reference; result.last().origin = altCat->url(); } } if (d._altTranslations.contains(pos.entry)) result << d._altTranslations.value(pos.entry); return result; } QStringList Catalog::sourceFiles(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QStringList(); return m_storage->sourceFiles(pos); } QString Catalog::id(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->id(pos); } QStringList Catalog::context(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QStringList(); return m_storage->context(pos); } QString Catalog::setPhase(const DocPosition& pos, const QString& phase) { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->setPhase(pos, phase); } void Catalog::setActivePhase(const QString& phase, ProjectLocal::PersonRole role) { //qCDebug(LOKALIZE_LOG)<<"setting active phase"<size(); while (pos.entry < limit) { if (!isApproved(pos)) d._nonApprovedIndex << pos.entry; if (m_storage->isEmpty(pos)) d._emptyIndex << pos.entry; ++(pos.entry); } emit signalNumberOfFuzziesChanged(); emit signalNumberOfEmptyChanged(); } QString Catalog::phase(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->phase(pos); } Phase Catalog::phase(const QString& name) const { return m_storage->phase(name); } QList Catalog::allPhases() const { return m_storage->allPhases(); } QVector Catalog::phaseNotes(const QString& phase) const { return m_storage->phaseNotes(phase); } QVector Catalog::setPhaseNotes(const QString& phase, QVector notes) { return m_storage->setPhaseNotes(phase, notes); } QMap Catalog::allTools() const { return m_storage->allTools(); } bool Catalog::isPlural(uint index) const { return m_storage && m_storage->isPlural(DocPosition(index)); } bool Catalog::isApproved(uint index) const { if (Q_UNLIKELY(!m_storage)) return false; bool extendedStates = m_storage->capabilities()&ExtendedStates; return (extendedStates &&::isApproved(state(DocPosition(index)), activePhaseRole())) || (!extendedStates && m_storage->isApproved(DocPosition(index))); } TargetState Catalog::state(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return NeedsTranslation; if (m_storage->capabilities()&ExtendedStates) return m_storage->state(pos); else return closestState(m_storage->isApproved(pos), activePhaseRole()); } bool Catalog::isEmpty(uint index) const { return m_storage && m_storage->isEmpty(DocPosition(index)); } bool Catalog::isEmpty(const DocPosition& pos) const { return m_storage && m_storage->isEmpty(pos); } bool Catalog::isEquivTrans(const DocPosition& pos) const { return m_storage && m_storage->isEquivTrans(pos); } int Catalog::binUnitsCount() const { return m_storage ? m_storage->binUnitsCount() : 0; } int Catalog::unitById(const QString& id) const { return m_storage ? m_storage->unitById(id) : 0; } QString Catalog::mimetype() { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->mimetype(); } QString Catalog::fileType() { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->fileType(); } CatalogType Catalog::type() { if (Q_UNLIKELY(!m_storage)) return Gettext; return m_storage->type(); } QString Catalog::sourceLangCode() const { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->sourceLangCode(); } QString Catalog::targetLangCode() const { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->targetLangCode(); } void Catalog::setTargetLangCode(const QString& targetLangCode) { if (Q_UNLIKELY(!m_storage)) return; bool notify = m_storage->targetLangCode() != targetLangCode; m_storage->setTargetLangCode(targetLangCode); if (notify) emit signalFileLoaded(); } //END STORAGE TRANSLATION //BEGIN OPEN/SAVE #define DOESNTEXIST -1 #define ISNTREADABLE -2 #define UNKNOWNFORMAT -3 KAutoSaveFile* Catalog::checkAutoSave(const QString& url) { KAutoSaveFile* autoSave = 0; QList staleFiles = KAutoSaveFile::staleFiles(QUrl::fromLocalFile(url)); foreach (KAutoSaveFile *stale, staleFiles) { if (stale->open(QIODevice::ReadOnly) && !autoSave) { autoSave = stale; autoSave->setParent(this); } else stale->deleteLater(); } if (autoSave) qCInfo(LOKALIZE_LOG) << "autoSave" << autoSave->fileName(); return autoSave; } int Catalog::loadFromUrl(const QString& filePath, const QString& saidUrl, int* fileSize, bool fast) { QFileInfo info(filePath); if (Q_UNLIKELY(!info.exists() || info.isDir())) return DOESNTEXIST; if (Q_UNLIKELY(!info.isReadable())) return ISNTREADABLE; bool readOnly = !info.isWritable(); QTime a; a.start(); QFile file(filePath); if (!file.open(QIODevice::ReadOnly)) return ISNTREADABLE;//TODO CatalogStorage* storage = nullptr; if (filePath.endsWith(QLatin1String(".po")) || filePath.endsWith(QLatin1String(".pot"))) storage = new GettextCatalog::GettextStorage; else if (filePath.endsWith(QLatin1String(".xlf")) || filePath.endsWith(QLatin1String(".xliff"))) storage = new XliffStorage; else if (filePath.endsWith(QLatin1String(".ts"))) storage = new TsStorage; else { //try harder QTextStream in(&file); int i = 0; bool gettext = false; while (!in.atEnd() && ++i < 64 && !gettext) gettext = in.readLine().contains(QLatin1String("msgid")); if (gettext) storage = new GettextCatalog::GettextStorage; else return UNKNOWNFORMAT; } int line = storage->load(&file); file.close(); if (Q_UNLIKELY(line != 0 || (!storage->size() && (line == -1)))) { delete storage; return line; } if (a.elapsed() > 100) qCDebug(LOKALIZE_LOG) << filePath << "opened in" << a.elapsed(); //ok... clear(); //commit transaction m_storage = storage; updateApprovedEmptyIndexCache(); d._numberOfPluralForms = storage->numberOfPluralForms(); d._autoSaveDirty = true; d._readOnly = readOnly; d._filePath = saidUrl.isEmpty() ? filePath : saidUrl; //set some sane role, a real phase with a nmae will be created later with the first edit command setActivePhase(QString(), Project::local()->role()); if (!fast) { KAutoSaveFile* autoSave = checkAutoSave(d._filePath); d._autoSaveRecovered = autoSave; if (autoSave) { d._autoSave->deleteLater(); d._autoSave = autoSave; //restore 'modified' status for entries MergeCatalog* mergeCatalog = new MergeCatalog(this, this); int errorLine = mergeCatalog->loadFromUrl(autoSave->fileName()); if (Q_LIKELY(errorLine == 0)) mergeCatalog->copyToBaseCatalog(); mergeCatalog->deleteLater(); d._autoSave->close(); } else d._autoSave->setManagedFile(QUrl::fromLocalFile(d._filePath)); } if (fileSize) *fileSize = file.size(); emit signalFileLoaded(); emit signalFileLoaded(d._filePath); return 0; } bool Catalog::save() { return saveToUrl(d._filePath); } //this function is not called if QUndoStack::isClean() ! bool Catalog::saveToUrl(QString localFilePath) { if (Q_UNLIKELY(!m_storage)) return true; bool nameChanged = localFilePath.length(); if (Q_LIKELY(!nameChanged)) localFilePath = d._filePath; QString localPath = QFileInfo(localFilePath).absolutePath(); if (!QFileInfo::exists(localPath)) if (!QDir::root().mkpath(localPath)) return false; QFile file(localFilePath); if (Q_UNLIKELY(!file.open(QIODevice::WriteOnly))) //i18n("Wasn't able to open file %1",filename.ascii()); return false; bool belongsToProject = localFilePath.contains(Project::instance()->poDir()); if (Q_UNLIKELY(!m_storage->save(&file, belongsToProject))) return false; file.close(); d._autoSave->remove(); d._autoSaveRecovered = false; setClean(); //undo/redo if (nameChanged) { d._filePath = localFilePath; d._autoSave->setManagedFile(QUrl::fromLocalFile(localFilePath)); } //Settings::self()->setCurrentGroup("Bookmarks"); //Settings::self()->addItemIntList(d._filePath.url(),d._bookmarkIndex); emit signalFileSaved(); emit signalFileSaved(localFilePath); return true; /* else if (status==NO_PERMISSIONS) { if (KMessageBox::warningContinueCancel(this, i18n("You do not have permission to write to file:\n%1\n" "Do you want to save to another file or cancel?", _currentURL.prettyUrl()), i18n("Error"),KStandardGuiItem::save())==KMessageBox::Continue) return fileSaveAs(); } */ } void Catalog::doAutoSave() { if (isClean() || !(d._autoSaveDirty)) return; if (Q_UNLIKELY(!m_storage)) return; if (!d._autoSave->open(QIODevice::WriteOnly)) { emit signalFileAutoSaveFailed(d._autoSave->fileName()); return; } qCInfo(LOKALIZE_LOG) << "doAutoSave" << d._autoSave->fileName(); m_storage->save(d._autoSave); d._autoSave->close(); d._autoSaveDirty = false; } void Catalog::projectConfigChanged() { setActivePhase(activePhase(), Project::local()->role()); } QByteArray Catalog::contents() { QBuffer buf; buf.open(QIODevice::WriteOnly); m_storage->save(&buf); buf.close(); return buf.data(); } //END OPEN/SAVE /** * helper method to keep db in a good shape :) * called on * 1) entry switch * 2) automatic editing code like replace or undo/redo operation **/ static void updateDB( const QString& filePath, const QString& ctxt, const CatalogString& english, const CatalogString& newTarget, int form, bool approved, const QString& dbName //const DocPosition&,//for back tracking ) { TM::UpdateJob* j = new TM::UpdateJob(filePath, ctxt, english, newTarget, form, approved, dbName); TM::threadPool()->start(j); } //BEGIN UNDO/REDO const DocPosition& Catalog::undo() { QUndoStack::undo(); return d._lastModifiedPos; } const DocPosition& Catalog::redo() { QUndoStack::redo(); return d._lastModifiedPos; } void Catalog::flushUpdateDBBuffer() { if (!Settings::autoaddTM()) return; DocPosition pos = d._lastModifiedPos; if (pos.entry == -1 || pos.entry >= numberOfEntries()) { //nothing to flush //qCWarning(LOKALIZE_LOG)<<"nothing to flush or new file opened"; return; } QString dbName; if (Project::instance()->targetLangCode() == targetLangCode()) { dbName = Project::instance()->projectID(); } else { dbName = sourceLangCode() % '-' % targetLangCode(); qCInfo(LOKALIZE_LOG) << "updating" << dbName << "because target language of project db does not match" << Project::instance()->targetLangCode() << targetLangCode(); if (!TM::DBFilesModel::instance()->m_configurations.contains(dbName)) { TM::OpenDBJob* openDBJob = new TM::OpenDBJob(dbName, TM::Local, true); connect(openDBJob, &TM::OpenDBJob::done, TM::DBFilesModel::instance(), &TM::DBFilesModel::updateProjectTmIndex); openDBJob->m_setParams = true; openDBJob->m_tmConfig.markup = Project::instance()->markup(); openDBJob->m_tmConfig.accel = Project::instance()->accel(); openDBJob->m_tmConfig.sourceLangCode = sourceLangCode(); openDBJob->m_tmConfig.targetLangCode = targetLangCode(); TM::DBFilesModel::instance()->openDB(openDBJob); } } int form = -1; if (isPlural(pos.entry)) form = pos.form; updateDB(url(), context(pos.entry).first(), sourceWithTags(pos), targetWithTags(pos), form, isApproved(pos.entry), dbName); d._lastModifiedPos = DocPosition(); } void Catalog::setLastModifiedPos(const DocPosition& pos) { if (pos.entry >= numberOfEntries()) //bin-units return; bool entryChanged = DocPos(d._lastModifiedPos) != DocPos(pos); if (entryChanged) flushUpdateDBBuffer(); d._lastModifiedPos = pos; } bool CatalogPrivate::addToEmptyIndexIfAppropriate(CatalogStorage* storage, const DocPosition& pos, bool alreadyEmpty) { if ((!pos.offset) && (storage->target(pos).isEmpty()) && (!alreadyEmpty)) { insertInList(_emptyIndex, pos.entry); return true; } return false; } void Catalog::targetDelete(const DocPosition& pos, int count) { if (Q_UNLIKELY(!m_storage)) return; bool alreadyEmpty = m_storage->isEmpty(pos); m_storage->targetDelete(pos, count); if (d.addToEmptyIndexIfAppropriate(m_storage, pos, alreadyEmpty)) emit signalNumberOfEmptyChanged(); emit signalEntryModified(pos); } bool CatalogPrivate::removeFromUntransIndexIfAppropriate(CatalogStorage* storage, const DocPosition& pos) { if ((!pos.offset) && (storage->isEmpty(pos))) { _emptyIndex.removeAll(pos.entry); return true; } return false; } void Catalog::targetInsert(const DocPosition& pos, const QString& arg) { if (Q_UNLIKELY(!m_storage)) return; if (d.removeFromUntransIndexIfAppropriate(m_storage, pos)) emit signalNumberOfEmptyChanged(); m_storage->targetInsert(pos, arg); emit signalEntryModified(pos); } void Catalog::targetInsertTag(const DocPosition& pos, const InlineTag& tag) { if (Q_UNLIKELY(!m_storage)) return; if (d.removeFromUntransIndexIfAppropriate(m_storage, pos)) emit signalNumberOfEmptyChanged(); m_storage->targetInsertTag(pos, tag); emit signalEntryModified(pos); } InlineTag Catalog::targetDeleteTag(const DocPosition& pos) { if (Q_UNLIKELY(!m_storage)) return InlineTag(); bool alreadyEmpty = m_storage->isEmpty(pos); InlineTag tag = m_storage->targetDeleteTag(pos); if (d.addToEmptyIndexIfAppropriate(m_storage, pos, alreadyEmpty)) emit signalNumberOfEmptyChanged(); emit signalEntryModified(pos); return tag; } void Catalog::setTarget(DocPosition pos, const CatalogString& s) { //TODO for case of markup present m_storage->setTarget(pos, s.string); } TargetState Catalog::setState(const DocPosition& pos, TargetState state) { bool extendedStates = m_storage && m_storage->capabilities()&ExtendedStates; bool approved =::isApproved(state, activePhaseRole()); if (Q_UNLIKELY(!m_storage || (extendedStates && m_storage->state(pos) == state) || (!extendedStates && m_storage->isApproved(pos) == approved))) return this->state(pos); TargetState prevState; if (extendedStates) { prevState = m_storage->setState(pos, state); d._statesIndex[prevState].removeAll(pos.entry); insertInList(d._statesIndex[state], pos.entry); } else { prevState = closestState(!approved, activePhaseRole()); m_storage->setApproved(pos, approved); } if (!approved) insertInList(d._nonApprovedIndex, pos.entry); else d._nonApprovedIndex.removeAll(pos.entry); emit signalNumberOfFuzziesChanged(); emit signalEntryModified(pos); return prevState; } Phase Catalog::updatePhase(const Phase& phase) { return m_storage->updatePhase(phase); } void Catalog::setEquivTrans(const DocPosition& pos, bool equivTrans) { if (m_storage) m_storage->setEquivTrans(pos, equivTrans); } bool Catalog::setModified(DocPos entry, bool modified) { if (modified) { if (d._modifiedEntries.contains(entry)) return false; d._modifiedEntries.insert(entry); } else d._modifiedEntries.remove(entry); return true; } bool Catalog::isModified(DocPos entry) const { return d._modifiedEntries.contains(entry); } bool Catalog::isModified(int entry) const { if (!isPlural(entry)) return isModified(DocPos(entry, 0)); int f = numberOfPluralForms(); while (--f >= 0) if (isModified(DocPos(entry, f))) return true; return false; } //END UNDO/REDO int findNextInList(const QLinkedList& list, int index) { int nextIndex = -1; foreach (int key, list) { if (Q_UNLIKELY(key > index)) { nextIndex = key; break; } } return nextIndex; } int findPrevInList(const QLinkedList& list, int index) { int prevIndex = -1; foreach (int key, list) { if (Q_UNLIKELY(key >= index)) break; prevIndex = key; } return prevIndex; } void insertInList(QLinkedList& list, int index) { QLinkedList::Iterator it = list.begin(); while (it != list.end() && index > *it) ++it; list.insert(it, index); } void Catalog::setBookmark(uint idx, bool set) { if (set) insertInList(d._bookmarkIndex, idx); else d._bookmarkIndex.removeAll(idx); } bool isApproved(TargetState state, ProjectLocal::PersonRole role) { static const TargetState marginStates[] = {Translated, Final, SignedOff}; return state >= marginStates[role]; } bool isApproved(TargetState state) { static const TargetState marginStates[] = {Translated, Final, SignedOff}; return state == marginStates[0] || state == marginStates[1] || state == marginStates[2]; } TargetState closestState(bool approved, ProjectLocal::PersonRole role) { Q_ASSERT(role != ProjectLocal::Undefined); static const TargetState approvementStates[][3] = { {NeedsTranslation, NeedsReviewTranslation, NeedsReviewTranslation}, {Translated, Final, SignedOff} }; return approvementStates[approved][role]; } bool Catalog::isObsolete(int entry) const { if (Q_UNLIKELY(!m_storage)) return false; return m_storage->isObsolete(entry); } QString Catalog::originalOdfFilePath() { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->originalOdfFilePath(); } void Catalog::setOriginalOdfFilePath(const QString& odfFilePath) { if (Q_UNLIKELY(!m_storage)) return; m_storage->setOriginalOdfFilePath(odfFilePath); } diff --git a/src/catalog/catalog.h b/src/catalog/catalog.h index afb5f9a..9837520 100644 --- a/src/catalog/catalog.h +++ b/src/catalog/catalog.h @@ -1,369 +1,369 @@ /***************************************************************************** This file is part of Lokalize This file contains parts of KBabel code Copyright (C) 1999-2000 by Matthias Kiefer 2001-2004 by Stanislav Visnovsky 2007-2014 by Nick Shaforostoff This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #ifndef CATALOG_H #define CATALOG_H #include "pos.h" #include "catalogstring.h" #include "catalogcapabilities.h" #include "note.h" #include "state.h" #include "phase.h" #include "alttrans.h" #include "catalog_private.h" class CatalogStorage; class MassReplaceJob; class KAutoSaveFile; #include namespace GettextCatalog { class CatalogImportPlugin; class CatalogExportPlugin; } bool isApproved(TargetState state, ProjectLocal::PersonRole role); bool isApproved(TargetState state); //disregarding Phase TargetState closestState(bool approved, ProjectLocal::PersonRole role); int findPrevInList(const QLinkedList& list, int index); int findNextInList(const QLinkedList& list, int index); void insertInList(QLinkedList& list, int index); // insert index in the right place in the list /** * This class represents a catalog * It uses CatalogStorage interface to work with catalogs in different formats * Also it defines all necessary functions to set and get the entries * * @short Wrapper class that represents a translation catalog * @author Nick Shaforostoff */ class Catalog: public QUndoStack { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.Lokalize.FileContainer") public: explicit Catalog(QObject* parent); virtual ~Catalog(); QString msgid(const DocPosition&) const; virtual QString msgstr(const DocPosition&) const; - QString msgidWithPlurals(const DocPosition&) const; - QString msgstrWithPlurals(const DocPosition&) const; + QString msgidWithPlurals(const DocPosition&, bool truncateFirstLine) const; + QString msgstrWithPlurals(const DocPosition&, bool truncateFirstLine) const; static QStringList supportedExtensions(); static bool extIsSupported(const QString& path); static const char* const* states(); int capabilities() const; void push(QUndoCommand* cmd); public slots: //DBus interface QString source(const DocPosition& pos) const { return msgid(pos); } QString target(const DocPosition& pos) const { return msgstr(pos); } // used by XLIFF storage) CatalogString sourceWithTags(const DocPosition& pos) const; CatalogString targetWithTags(const DocPosition& pos) const; CatalogString catalogString(const DocPosition& pos) const; /** * @a pos.form is note number * @returns previous note contents, if any */ Note setNote(const DocPosition& pos, const Note& note); QVector notes(const DocPosition& pos) const; QVector developerNotes(const DocPosition& pos) const; QStringList noteAuthors() const; QVector altTrans(const DocPosition& pos) const; QStringList sourceFiles(const DocPosition& pos) const; //QString msgctxt(uint index) const; //the result is guaranteed to have at least 1 string QStringList context(const DocPosition& pos) const; QString id(const DocPosition& pos) const; ///@returns previous phase-name QString setPhase(const DocPosition& pos, const QString& phase); QString phase(const DocPosition& pos) const; QString activePhase() const { return d._phase; } ProjectLocal::PersonRole activePhaseRole() const { return d._phaseRole; } void setActivePhase(const QString& phase, ProjectLocal::PersonRole role = ProjectLocal::Approver); Phase phase(const QString& name) const; QList allPhases() const; QMap allTools() const; QVector phaseNotes(const QString& phase) const; ///@arg pos.entry - number of phase, @arg pos.form - number of note QVector setPhaseNotes(const QString& phase, QVector); bool isPlural(uint index) const; bool isPlural(const DocPosition& pos) const { return isPlural(pos.entry); } bool isApproved(uint index) const; bool isApproved(const DocPosition& pos) const { return isApproved(pos.entry); } TargetState state(const DocPosition& pos) const; bool isEquivTrans(const DocPosition&) const; ///@returns true if at least one form is untranslated bool isEmpty(uint index) const; bool isEmpty(const DocPosition&) const; bool isModified(DocPos entry) const; bool isModified(int entry) const; bool isObsolete(int entry) const; /// so DocPosition::entry may actually be < size()+binUnitsCount() int binUnitsCount() const; int unitById(const QString& id) const; bool isBookmarked(uint index) const { return d._bookmarkIndex.contains(index); } void setBookmark(uint, bool); int numberOfPluralForms() const { return d._numberOfPluralForms; } int numberOfEntries() const; int numberOfNonApproved() const { return d._nonApprovedIndex.size(); } int numberOfUntranslated() const { return d._emptyIndex.size(); } public: QString originalOdfFilePath(); void setOriginalOdfFilePath(const QString&); int firstFuzzyIndex() const { return d._nonApprovedIndex.isEmpty() ? numberOfEntries() : d._nonApprovedIndex.first(); } int lastFuzzyIndex() const { return d._nonApprovedIndex.isEmpty() ? -1 : d._nonApprovedIndex.last(); } int nextFuzzyIndex(uint index) const { return findNextInList(d._nonApprovedIndex, index); } int prevFuzzyIndex(uint index) const { return findPrevInList(d._nonApprovedIndex, index); } int firstUntranslatedIndex() const { return d._emptyIndex.isEmpty() ? numberOfEntries() : d._emptyIndex.first(); } int lastUntranslatedIndex() const { return d._emptyIndex.isEmpty() ? -1 : d._emptyIndex.last(); } int nextUntranslatedIndex(uint index) const { return findNextInList(d._emptyIndex, index); } int prevUntranslatedIndex(uint index) const { return findPrevInList(d._emptyIndex, index); } int firstBookmarkIndex() const { return d._bookmarkIndex.isEmpty() ? numberOfEntries() : d._bookmarkIndex.first(); } int lastBookmarkIndex() const { return d._bookmarkIndex.isEmpty() ? -1 : d._bookmarkIndex.last(); } int nextBookmarkIndex(uint index) const { return findNextInList(d._bookmarkIndex, index); } int prevBookmarkIndex(uint index) const { return findPrevInList(d._bookmarkIndex, index); } bool autoSaveRecovered() { return d._autoSaveRecovered; } public: void clear(); bool isEmpty() { return !m_storage; } bool isReadOnly() { return d._readOnly; } void attachAltTransCatalog(Catalog*); void attachAltTrans(int entry, const AltTrans& trans); virtual const DocPosition& undo(); virtual const DocPosition& redo(); void setTarget(DocPosition pos, const CatalogString& s); //for batch use only! //void setErrorIndex(const QList& errors){d._errorIndex=errors;} void setUrl(const QString& u) { d._filePath = u; //used for template load } public slots: //DBus interface const QString& url() const { return d._filePath; } ///@returns 0 if success, >0 erroneous line (parsing error) int loadFromUrl(const QString& url, const QString& saidUrl = QString(), int* fileSize = nullptr, bool fast = false); bool saveToUrl(QString url); bool save(); QByteArray contents(); QString mimetype(); QString fileType(); CatalogType type(); QString sourceLangCode() const; QString targetLangCode() const; void setTargetLangCode(const QString& targetLangCode); /** * updates DB for _posBuffer and accompanying _originalForLastModified */ void flushUpdateDBBuffer(); protected: virtual KAutoSaveFile* checkAutoSave(const QString& url); protected slots: void doAutoSave(); void setAutoSaveDirty() { d._autoSaveDirty = true; } void projectConfigChanged(); protected: /** * (EDITING) * accessed from undo/redo code * called _BEFORE_ modification */ void setLastModifiedPos(const DocPosition&); /** * (EDITING) * accessed from undo/redo code * accessed from mergeCatalog) * it _does_ check if action should be taken */ void setApproved(const DocPosition& pos, bool approved); void targetDelete(const DocPosition& pos, int count); void targetInsert(const DocPosition& pos, const QString& arg); InlineTag targetDeleteTag(const DocPosition& pos); void targetInsertTag(const DocPosition& pos, const InlineTag& tag); TargetState setState(const DocPosition& pos, TargetState state); Phase updatePhase(const Phase& phase); void setEquivTrans(const DocPosition&, bool equivTrans); /// @returns true if entry wasn't modified before bool setModified(DocPos entry, bool modif); void updateApprovedEmptyIndexCache(); protected: CatalogPrivate d; CatalogStorage *m_storage; friend class GettextCatalog::CatalogImportPlugin; friend class GettextCatalog::CatalogExportPlugin; friend class LokalizeUnitCmd; friend class InsTextCmd; friend class DelTextCmd; friend class InsTagCmd; friend class DelTagCmd; friend class SetStateCmd; friend class SetNoteCmd; friend class UpdatePhaseCmd; friend class MergeCatalog; friend class SetEquivTransCmd; friend class MassReplaceJob; public: //static QString supportedMimeFilters; static QString supportedFileTypes(bool includeTemplates = true); signals: void signalEntryModified(const DocPosition&); void activePhaseChanged(); void signalNumberOfFuzziesChanged(); void signalNumberOfEmptyChanged(); Q_SCRIPTABLE void signalFileLoaded(); void signalFileLoaded(const QString&); Q_SCRIPTABLE void signalFileSaved(); void signalFileSaved(const QString&); void signalFileAutoSaveFailed(const QString&); }; #endif diff --git a/src/catalog/catalogstorage.h b/src/catalog/catalogstorage.h index 1f11a35..c118755 100644 --- a/src/catalog/catalogstorage.h +++ b/src/catalog/catalogstorage.h @@ -1,274 +1,274 @@ /* Copyright 2008-2009 Nick Shaforostoff This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CATALOGSTORAGE_H #define CATALOGSTORAGE_H #include "pos.h" #include "catalogstring.h" #include "note.h" #include "state.h" #include "phase.h" #include "alttrans.h" #include "catalogcapabilities.h" #include #include /** * Abstract interface for storage of translation file * * format-specific elements like \" for gettext PO should be eliminated * * @short Abstract interface for storage of translation file * @author Nick Shaforostoff */ class CatalogStorage { public: CatalogStorage(); virtual ~CatalogStorage(); virtual int capabilities() const = 0; virtual int load(QIODevice* device) = 0; virtual bool save(QIODevice* device, bool belongsToProject = false) = 0; virtual int size() const = 0; int numberOfEntries()const { return size(); } int numberOfPluralForms() const { return m_numberOfPluralForms; } /** * flat-model interface (ignores XLIFF grouping) * * format-specific texts like \" for gettext PO should be eliminated **/ virtual QString source(const DocPosition& pos) const = 0; virtual QString target(const DocPosition& pos) const = 0; - virtual QString sourceWithPlurals(const DocPosition& pos) const = 0; - virtual QString targetWithPlurals(const DocPosition& pos) const = 0; + virtual QString sourceWithPlurals(const DocPosition& pos, bool truncateFirstLine) const = 0; + virtual QString targetWithPlurals(const DocPosition& pos, bool truncateFirstLine) const = 0; virtual CatalogString sourceWithTags(DocPosition pos) const = 0; virtual CatalogString targetWithTags(DocPosition pos) const = 0; virtual CatalogString catalogString(const DocPosition& pos) const = 0; /** * edit operations used by undo/redo system and sync-mode **/ virtual void targetDelete(const DocPosition& pos, int count) = 0; virtual void targetInsert(const DocPosition& pos, const QString& arg) = 0; virtual void setTarget(const DocPosition& pos, const QString& arg) = 0; //called for mergeCatalog TODO switch to CatalogString virtual void targetInsertTag(const DocPosition&, const InlineTag&) {} virtual InlineTag targetDeleteTag(const DocPosition&) { return InlineTag(); } virtual Phase updatePhase(const Phase&) { return Phase(); } virtual QList allPhases() const { return QList(); } virtual QMap allTools() const { return QMap(); } /// all plural forms. pos.form doesn't matter virtual QStringList sourceAllForms(const DocPosition& pos, bool stripNewLines = false) const = 0; virtual QStringList targetAllForms(const DocPosition& pos, bool stripNewLines = false) const = 0; virtual QVector altTrans(const DocPosition& pos) const = 0; virtual QVector notes(const DocPosition& pos) const = 0; virtual Note setNote(DocPosition pos, const Note& note) = 0; virtual QStringList noteAuthors() const { return QStringList(); } virtual QVector developerNotes(const DocPosition& pos) const = 0; virtual QStringList sourceFiles(const DocPosition& pos) const = 0; virtual QString setPhase(const DocPosition& pos, const QString& phase) { Q_UNUSED(pos); Q_UNUSED(phase); return QString(); } virtual QString phase(const DocPosition& pos) const { Q_UNUSED(pos); return QString(); } virtual Phase phase(const QString& name) const { Q_UNUSED(name); return Phase(); } virtual QVector phaseNotes(const QString& phase) const { Q_UNUSED(phase); return QVector(); } virtual QVector setPhaseNotes(const QString& phase, QVector notes) { Q_UNUSED(phase); Q_UNUSED(notes); return QVector(); } //the result must be guaranteed to have at least 1 string virtual QStringList context(const DocPosition&) const = 0; //DocPosition.form - number of //virtual QString context(const DocPosition&) const=0; //virtual int contextCount(const DocPosition&) const=0; /** * user-invisible data for matching, e.g. during TM database lookup * it is comprised of several strings * * database stores them and thus it is possible to * fuzzy-match 'matchData' later * * it is responsibility of CatalogStorage implementations to * separate/assemble the list properly according to the format specifics * * pos.form doesn't matter **/ virtual QStringList matchData(const DocPosition&) const = 0; /** * entry id unique for this file * * pos.form doesn't matter **/ virtual QString id(const DocPosition&) const = 0; virtual bool isPlural(const DocPosition&) const = 0; virtual bool isEmpty(const DocPosition&) const = 0; virtual bool isEquivTrans(const DocPosition&) const { return true; } virtual void setEquivTrans(const DocPosition&, bool equivTrans) { Q_UNUSED(equivTrans) } virtual bool isApproved(const DocPosition&) const { return true; } virtual void setApproved(const DocPosition&, bool approved) { Q_UNUSED(approved) } virtual TargetState state(const DocPosition&) const { return New; } virtual TargetState setState(const DocPosition&, TargetState) { return New; } virtual bool isObsolete(int entry) const { Q_UNUSED(entry) return false; } virtual bool isTranslateable(int entry) const { Q_UNUSED(entry) return true; } virtual int binUnitsCount() const { return 0; } virtual int unitById(const QString& id) const { Q_UNUSED(id); return 0; } const QString& url() const { return m_url; } void setUrl(const QString& u) { m_url = u; //TODO } virtual QString mimetype() const = 0; virtual QString fileType() const = 0; virtual CatalogType type() const = 0; virtual QString originalOdfFilePath() { return QString(); } virtual void setOriginalOdfFilePath(const QString&) {} QString sourceLangCode() const { return m_sourceLangCode; } QString targetLangCode() const { return m_targetLangCode; } virtual void setTargetLangCode(const QString& langCode) { m_targetLangCode = langCode; } protected: QString m_url; QString m_sourceLangCode; QString m_targetLangCode; int m_numberOfPluralForms; }; inline CatalogStorage::CatalogStorage() : m_sourceLangCode(QStringLiteral("en_US")) , m_numberOfPluralForms(0) { } inline CatalogStorage::~CatalogStorage() { } #endif diff --git a/src/catalog/gettext/gettextstorage.cpp b/src/catalog/gettext/gettextstorage.cpp index cc67a08..3647f83 100644 --- a/src/catalog/gettext/gettextstorage.cpp +++ b/src/catalog/gettext/gettextstorage.cpp @@ -1,466 +1,494 @@ /* Copyright 2008-2014 Nick Shaforostoff This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "gettextstorage.h" #include "lokalize_debug.h" #include "gettextheader.h" #include "catalogitem_private.h" #include "gettextimport.h" #include "gettextexport.h" #include "project.h" #include "version.h" #include "prefs_lokalize.h" #include "diff.h" #include #include #include #include #include #include QMutex regExMutex; // static QString GNUPluralForms(const QString& lang); using namespace GettextCatalog; GettextStorage::GettextStorage() : CatalogStorage() , m_codec(0) , m_maxLineLength(80) , m_trailingNewLines(0) , m_generatedFromDocbook(false) { } GettextStorage::~GettextStorage() { } //BEGIN OPEN/SAVE int GettextStorage::load(QIODevice* device/*, bool readonly*/) { //GettextImportPlugin importer=GettextImportPlugin(readonly?(new ExtraDataSaver()):(new ExtraDataListSaver())); GettextImportPlugin importer; ConversionStatus status = OK; int errorLine; { QMutexLocker locker(®ExMutex); status = importer.open(device, this, &errorLine); } //for langs with more than 2 forms //we create any form-entries additionally needed uint i = 0; uint lim = size(); while (i < lim) { CatalogItem& item = m_entries[i]; if (item.isPlural() && item.msgstrPlural().count() < m_numberOfPluralForms ) { QVector msgstr(item.msgstrPlural()); while (msgstr.count() < m_numberOfPluralForms) msgstr.append(QString()); item.setMsgstr(msgstr); } ++i; } //qCompress(m_storage->m_catalogExtraData.join("\n\n").toUtf8(),9); return status == OK ? 0 : (errorLine + 1); } bool GettextStorage::save(QIODevice* device, bool belongsToProject) { QString header = m_header.msgstr(); QString comment = m_header.comment(); QString catalogProjectId;//=m_url.fileName(); //catalogProjectId=catalogProjectId.left(catalogProjectId.lastIndexOf('.')); { QMutexLocker locker(®ExMutex); updateHeader(header, comment, m_targetLangCode, m_numberOfPluralForms, catalogProjectId, m_generatedFromDocbook, belongsToProject, /*forSaving*/true, m_codec); } m_header.setMsgstr(header); m_header.setComment(comment); //GettextExportPlugin exporter(m_maxLineLength>70?m_maxLineLength:-1, m_trailingNewLines);// this is kinda hackish... GettextExportPlugin exporter(Project::instance()->wordWrap(), m_trailingNewLines); ConversionStatus status = OK; status = exporter.save(device/*x-gettext-translation*/, this, m_codec); return status == OK; } //END OPEN/SAVE //BEGIN STORAGE TRANSLATION int GettextStorage::size() const { return m_entries.size(); } static const QChar altSep(156); static InlineTag makeInlineTag(int i) { static const QString altSepText(QStringLiteral(" | ")); static const QString ctype = i18n("separator for different-length string alternatives"); return InlineTag(i, i, InlineTag::x, QString::number(i), QString(), altSepText, ctype); } static CatalogString makeCatalogString(const QString& string) { CatalogString result; result.string = string; int i = 0; while ((i = result.string.indexOf(altSep, i)) != -1) { result.string[i] = TAGRANGE_IMAGE_SYMBOL; result.tags.append(makeInlineTag(i)); ++i; } return result; } //flat-model interface (ignores XLIFF grouping) CatalogString GettextStorage::sourceWithTags(DocPosition pos) const { return makeCatalogString(source(pos)); } CatalogString GettextStorage::targetWithTags(DocPosition pos) const { return makeCatalogString(target(pos)); } QString GettextStorage::source(const DocPosition& pos) const { return m_entries.at(pos.entry).msgid(pos.form); } QString GettextStorage::target(const DocPosition& pos) const { return m_entries.at(pos.entry).msgstr(pos.form); } -QString GettextStorage::sourceWithPlurals(const DocPosition& pos) const +QString GettextStorage::sourceWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { if (m_entries.at(pos.entry).isPlural()) { const QVector plurals = m_entries.at(pos.entry).msgidPlural(); QString pluralString; for (int i = 0; i < plurals.size(); i++) { - pluralString += plurals.at(i); + QString str = plurals.at(i); + if (truncateFirstLine) + { + int truncatePos = str.indexOf("\n"); + if (truncatePos != -1) + str.truncate(truncatePos); + } + pluralString += str; if (i != plurals.size() - 1) { pluralString += '|'; } } return pluralString; } else { - return m_entries.at(pos.entry).msgid(pos.form); + QString str = m_entries.at(pos.entry).msgid(pos.form); + if (truncateFirstLine) + { + int truncatePos = str.indexOf("\n"); + if (truncatePos != -1) + str.truncate(truncatePos); + } + return str; } } -QString GettextStorage::targetWithPlurals(const DocPosition& pos) const +QString GettextStorage::targetWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { if (m_entries.at(pos.entry).isPlural()) { const QVector plurals = m_entries.at(pos.entry).msgstrPlural(); QString pluralString; for (int i = 0; i < plurals.size(); i++) { - pluralString += plurals.at(i); + QString str = plurals.at(i); + if (truncateFirstLine) + { + int truncatePos = str.indexOf("\n"); + if (truncatePos != -1) + str.truncate(truncatePos); + } + pluralString += str; if (i != plurals.size() - 1) { pluralString += '|'; } } return pluralString; } else { - return m_entries.at(pos.entry).msgstr(pos.form); + QString str = m_entries.at(pos.entry).msgstr(pos.form); + if (truncateFirstLine) + { + int truncatePos = str.indexOf("\n"); + if (truncatePos != -1) + str.truncate(truncatePos); + } + return str; } } void GettextStorage::targetDelete(const DocPosition& pos, int count) { m_entries[pos.entry].d._msgstrPlural[pos.form].remove(pos.offset, count); } void GettextStorage::targetInsert(const DocPosition& pos, const QString& arg) { m_entries[pos.entry].d._msgstrPlural[pos.form].insert(pos.offset, arg); } void GettextStorage::setTarget(const DocPosition& pos, const QString& arg) { m_entries[pos.entry].d._msgstrPlural[pos.form] = arg; } void GettextStorage::targetInsertTag(const DocPosition& pos, const InlineTag& tag) { Q_UNUSED(tag); targetInsert(pos, altSep); } InlineTag GettextStorage::targetDeleteTag(const DocPosition& pos) { targetDelete(pos, 1); return makeInlineTag(pos.offset); } QStringList GettextStorage::sourceAllForms(const DocPosition& pos, bool stripNewLines) const { return m_entries.at(pos.entry).allPluralForms(CatalogItem::Source, stripNewLines); } QStringList GettextStorage::targetAllForms(const DocPosition& pos, bool stripNewLines) const { return m_entries.at(pos.entry).allPluralForms(CatalogItem::Target, stripNewLines); } QVector GettextStorage::altTrans(const DocPosition& pos) const { static const QRegExp alt_trans_mark_re(QStringLiteral("^#\\|")); QStringList prev = m_entries.at(pos.entry).comment().split('\n').filter(alt_trans_mark_re); QString oldSingular; QString oldPlural; QString* cur = &oldSingular; QStringList::iterator it = prev.begin(); static const QString msgid_plural_alt = QStringLiteral("#| msgid_plural \""); while (it != prev.end()) { if (it->startsWith(msgid_plural_alt)) cur = &oldPlural; int start = it->indexOf('\"') + 1; int end = it->lastIndexOf('\"'); if (start && end != -1) { if (!cur->isEmpty()) (*cur) += '\n'; if (!(cur->isEmpty() && (end - start) == 0)) //for multiline msgs (*cur) += it->midRef(start, end - start); } ++it; } if (pos.form == 0) cur = &oldSingular; cur->replace(QStringLiteral("\\\""), QStringLiteral("\"")); QVector result; if (!cur->isEmpty()) result << AltTrans(CatalogString(*cur), i18n("Previous source value, saved by Gettext during transition to a newer POT template")); return result; } Note GettextStorage::setNote(DocPosition pos, const Note& note) { //qCWarning(LOKALIZE_LOG)<<"s"< l = notes(pos); if (l.size()) oldNote = l.first(); QStringList comment = m_entries.at(pos.entry).comment().split('\n'); //remove previous comment; QStringList::iterator it = comment.begin(); while (it != comment.end()) { if (it->startsWith(QLatin1String("# "))) it = comment.erase(it); else ++it; } if (note.content.size()) comment.prepend(QStringLiteral("# ") + note.content.split('\n').join(QStringLiteral("\n# "))); m_entries[pos.entry].setComment(comment.join(QStringLiteral("\n"))); //qCWarning(LOKALIZE_LOG)<<"e"< GettextStorage::notes(const DocPosition& docPosition, const QRegExp& re, int preLen) const { QVector result; QString content; QStringList note = m_entries.at(docPosition.entry).comment().split('\n').filter(re); foreach (const QString &s, note) { if (s.size() >= preLen) { content += s.midRef(preLen); content += QLatin1Char('\n'); } } if (!content.isEmpty()) { content.chop(1); result << Note(content); } return result; //i18nc("@info PO comment parsing. contains filename","Place:"); //i18nc("@info PO comment parsing","GUI place:"); } QVector GettextStorage::notes(const DocPosition& docPosition) const { static const QRegExp nre(QStringLiteral("^# ")); return notes(docPosition, nre, 2); } QVector GettextStorage::developerNotes(const DocPosition& docPosition) const { static const QRegExp dnre(QStringLiteral("^#\\. (?!i18n: file:)")); return notes(docPosition, dnre, 3); } QStringList GettextStorage::sourceFiles(const DocPosition& pos) const { QStringList result; QStringList commentLines = m_entries.at(pos.entry).comment().split('\n'); static const QRegExp i18n_file_re(QStringLiteral("^#. i18n: file: ")); foreach (const QString &uiLine, commentLines.filter(i18n_file_re)) { foreach (const QStringRef &fileRef, uiLine.midRef(15).split(' ')) { result << fileRef.toString(); } } bool hasUi = !result.isEmpty(); static const QRegExp cpp_re(QStringLiteral("^#: ")); foreach (const QString &cppLine, commentLines.filter(cpp_re)) { if (hasUi && cppLine.startsWith(QLatin1String("#: rc.cpp"))) continue; foreach (const QStringRef &fileRef, cppLine.midRef(3).split(' ')) { result << fileRef.toString(); } } return result; } QStringList GettextStorage::context(const DocPosition& pos) const { return matchData(pos); } QStringList GettextStorage::matchData(const DocPosition& pos) const { QString ctxt = m_entries.at(pos.entry).msgctxt(); //KDE-specific //Splits @info:whatsthis and actual note /* if (ctxt.startsWith('@') && ctxt.contains(' ')) { QStringList result(ctxt.section(' ',0,0,QString::SectionSkipEmpty)); result<poDir()); updateHeader(values, comment, m_targetLangCode, m_numberOfPluralForms, catalogProjectId, m_generatedFromDocbook, belongsToProject, /*forSaving*/true, m_codec); m_header = newHeader; m_header.setComment(comment); m_header.setMsgstr(values); // setClean(false); //emit signalHeaderChanged(); return true; } qCWarning(LOKALIZE_LOG) << "header Not valid"; return false; } diff --git a/src/catalog/gettext/gettextstorage.h b/src/catalog/gettext/gettextstorage.h index 02c567e..ef01be6 100644 --- a/src/catalog/gettext/gettextstorage.h +++ b/src/catalog/gettext/gettextstorage.h @@ -1,137 +1,137 @@ /* Copyright 2008-2014 Nick Shaforostoff This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef GETTEXTSTORAGE_H #define GETTEXTSTORAGE_H #include #include #include "catalogitem.h" #include "catalogstorage.h" /** * Implementation of Gettext PO format support */ namespace GettextCatalog { /** * @short Implementation of storage for Gettext PO * @author Nick Shaforostoff */ class GettextStorage: public CatalogStorage { public: GettextStorage(); ~GettextStorage() override; int capabilities() const override { return 0; } int load(QIODevice* device/*, bool readonly=false*/) override; bool save(QIODevice* device, bool belongsToProject = false) override; int size() const override; //flat-model interface (ignores XLIFF grouping) QString source(const DocPosition& pos) const override; QString target(const DocPosition& pos) const override; - QString sourceWithPlurals(const DocPosition& pos) const override; - QString targetWithPlurals(const DocPosition& pos) const override; + QString sourceWithPlurals(const DocPosition& pos, bool truncateFirstLine) const override; + QString targetWithPlurals(const DocPosition& pos, bool truncateFirstLine) const override; CatalogString sourceWithTags(DocPosition pos) const override; CatalogString targetWithTags(DocPosition pos) const override; CatalogString catalogString(const DocPosition& pos) const override { return pos.part == DocPosition::Target ? targetWithTags(pos) : sourceWithTags(pos); } void targetDelete(const DocPosition& pos, int count) override; void targetInsert(const DocPosition& pos, const QString& arg) override; void setTarget(const DocPosition& pos, const QString& arg) override;//called for mergeCatalog void targetInsertTag(const DocPosition&, const InlineTag&) override; InlineTag targetDeleteTag(const DocPosition&) override; QStringList sourceAllForms(const DocPosition& pos, bool stripNewLines = false) const override; QStringList targetAllForms(const DocPosition& pos, bool stripNewLines = false) const override; QVector notes(const DocPosition& pos) const override; Note setNote(DocPosition pos, const Note& note) override; QVector altTrans(const DocPosition& pos) const override; QStringList sourceFiles(const DocPosition& pos) const override; QVector developerNotes(const DocPosition& pos) const override; QStringList context(const DocPosition& pos) const override; QStringList matchData(const DocPosition& pos) const override; QString id(const DocPosition& pos) const override; bool isPlural(const DocPosition& pos) const override; bool isApproved(const DocPosition& pos) const override; void setApproved(const DocPosition& pos, bool approved) override; bool isEmpty(const DocPosition& pos) const override; QString mimetype() const override { return QStringLiteral("text/x-gettext-translation"); } QString fileType() const override { return QStringLiteral("Gettext (*.po)"); } CatalogType type() const override { return Gettext; } private: bool setHeader(const CatalogItem& newHeader); void setCodec(QTextCodec* codec) { m_codec = codec; } QVector notes(const DocPosition& pos, const QRegExp& re, int preLen) const; private: QVector m_entries; QVector m_obsoleteEntries; CatalogItem m_header; QTextCodec* m_codec; short m_maxLineLength; short m_trailingNewLines; bool m_generatedFromDocbook; QStringList m_catalogExtraData; QByteArray m_catalogExtraDataCompressed; friend class CatalogImportPlugin; friend class GettextExportPlugin; }; } #endif diff --git a/src/catalog/ts/tsstorage.cpp b/src/catalog/ts/tsstorage.cpp index 7604c96..03a50dd 100644 --- a/src/catalog/ts/tsstorage.cpp +++ b/src/catalog/ts/tsstorage.cpp @@ -1,545 +1,559 @@ /* Copyright 2008-2014 Nick Shaforostoff This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "tsstorage.h" #include "lokalize_debug.h" #include "gettextheader.h" #include "project.h" #include "version.h" #include "prefs_lokalize.h" #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #define U QLatin1String #else #define U QStringLiteral #endif //static const char* const noyes[]={"no","yes"}; static const QString names[] = {U("source"), U("translation"), U("oldsource"), U("translatorcomment"), U("comment"), U("name"), U("numerus")}; enum TagNames {SourceTag, TargetTag, OldSourceTag, NoteTag, DevNoteTag, NameTag, PluralTag}; static const QString attrnames[] = {U("location"), U("type"), U("obsolete")}; enum AttrNames {LocationAttr, TypeAttr, ObsoleteAttr}; static const QString attrvalues[] = {U("obsolete"), U("vanished")}; enum AttValues {ObsoleteVal, VanishedVal}; TsStorage::TsStorage() : CatalogStorage() { } TsStorage::~TsStorage() { } int TsStorage::capabilities() const { return 0;//MultipleNotes; } //BEGIN OPEN/SAVE int TsStorage::load(QIODevice* device) { QTime chrono; chrono.start(); QXmlSimpleReader reader; reader.setFeature(QStringLiteral("http://qt-project.org/xml/features/report-whitespace-only-CharData"), true); reader.setFeature(QStringLiteral("http://xml.org/sax/features/namespaces"), false); QXmlInputSource source(device); QString errorMsg; int errorLine;//+errorColumn; bool success = m_doc.setContent(&source, &reader, &errorMsg, &errorLine/*,errorColumn*/); if (!success) { qCWarning(LOKALIZE_LOG) << "parse error" << errorMsg << errorLine; return errorLine + 1; } QDomElement file = m_doc.elementsByTagName(QStringLiteral("TS")).at(0).toElement(); m_sourceLangCode = file.attribute(QStringLiteral("sourcelanguage")); m_targetLangCode = file.attribute(QStringLiteral("language")); m_numberOfPluralForms = numberOfPluralFormsForLangCode(m_targetLangCode); //Create entry mapping. //Along the way: for langs with more than 2 forms //we create any form-entries additionally needed entries = m_doc.elementsByTagName(QStringLiteral("message")); qCWarning(LOKALIZE_LOG) << chrono.elapsed() << "secs, " << entries.size() << "entries"; return 0; } bool TsStorage::save(QIODevice* device, bool belongsToProject) { Q_UNUSED(belongsToProject) QTextStream stream(device); m_doc.save(stream, 4); return true; } //END OPEN/SAVE //BEGIN STORAGE TRANSLATION int TsStorage::size() const { //return m_map.size(); return entries.size(); } /** * helper structure used during XLIFF XML walk-through */ struct TsContentEditingData { enum ActionType {Get, DeleteText, InsertText, CheckLength}; QString stringToInsert; int pos; int lengthOfStringToRemove; ActionType actionType; ///Get TsContentEditingData(ActionType type = Get) : pos(-1) , lengthOfStringToRemove(-1) , actionType(type) {} ///DeleteText TsContentEditingData(int p, int l) : pos(p) , lengthOfStringToRemove(l) , actionType(DeleteText) {} ///InsertText TsContentEditingData(int p, const QString& s) : stringToInsert(s) , pos(p) , lengthOfStringToRemove(-1) , actionType(InsertText) {} }; static QString doContent(QDomElement elem, int startingPos, TsContentEditingData* data); /** * walks through XLIFF XML and performs actions depending on TsContentEditingData: * - reads content * - deletes content, or * - inserts content */ static QString content(QDomElement elem, TsContentEditingData* data = 0) { return doContent(elem, 0, data); } static QString doContent(QDomElement elem, int startingPos, TsContentEditingData* data) { //actually startingPos is current pos QString result; if (elem.isNull() || (!result.isEmpty() && data && data->actionType == TsContentEditingData::CheckLength)) return QString(); bool seenCharacterDataAfterElement = false; QDomNode n = elem.firstChild(); while (!n.isNull()) { if (n.isCharacterData()) { seenCharacterDataAfterElement = true; QDomCharacterData c = n.toCharacterData(); QString cData = c.data(); if (data && data->pos != -1 && data->pos >= startingPos && data->pos <= startingPos + cData.size()) { // time to do some action! ;) int localStartPos = data->pos - startingPos; //BEGIN DELETE TEXT if (data->actionType == TsContentEditingData::DeleteText) { //(data->lengthOfStringToRemove!=-1) if (localStartPos + data->lengthOfStringToRemove > cData.size()) { //text is fragmented into several QDomCharacterData int localDelLen = cData.size() - localStartPos; //qCWarning(LOKALIZE_LOG)<<"text is fragmented into several QDomCharacterData. localDelLen:"<lengthOfStringToRemove = data->lengthOfStringToRemove - localDelLen; //data->pos=startingPos; //qCWarning(LOKALIZE_LOG)<<"\tsetup:"<pos<lengthOfStringToRemove; } else { //qCWarning(LOKALIZE_LOG)<<"simple delete"<lengthOfStringToRemove; c.deleteData(localStartPos, data->lengthOfStringToRemove); data->actionType = TsContentEditingData::CheckLength; return QString('a');//so it exits 100% } } //END DELETE TEXT //INSERT else if (data->actionType == TsContentEditingData::InsertText) { c.insertData(localStartPos, data->stringToInsert); data->actionType = TsContentEditingData::CheckLength; return QString('a');//so it exits 100% } cData = c.data(); } //else // if (data&&data->pos!=-1/*&& n.nextSibling().isNull()*/) // qCWarning(LOKALIZE_LOG)<<"arg!"<pos"<pos; result += cData; startingPos += cData.size(); } n = n.nextSibling(); } if (!seenCharacterDataAfterElement) { //add empty charData child so that user could add some text elem.appendChild(elem.ownerDocument().createTextNode(QString())); } return result; } //flat-model interface (ignores XLIFF grouping) CatalogString TsStorage::catalogString(QDomElement contentElement) const { CatalogString catalogString; TsContentEditingData data(TsContentEditingData::Get); catalogString.string = content(contentElement, &data); return catalogString; } CatalogString TsStorage::catalogString(const DocPosition& pos) const { return catalogString(pos.part == DocPosition::Target ? targetForPos(pos) : sourceForPos(pos.entry)); } CatalogString TsStorage::targetWithTags(DocPosition pos) const { return catalogString(targetForPos(pos)); } CatalogString TsStorage::sourceWithTags(DocPosition pos) const { return catalogString(sourceForPos(pos.entry)); } QString TsStorage::source(const DocPosition& pos) const { return content(sourceForPos(pos.entry)); } QString TsStorage::target(const DocPosition& pos) const { return content(targetForPos(pos)); } -QString TsStorage::sourceWithPlurals(const DocPosition& pos) const -{ - return source(pos); +QString TsStorage::sourceWithPlurals(const DocPosition& pos, bool truncateFirstLine) const +{ + QString str = source(pos); + if (truncateFirstLine) + { + int truncatePos = str.indexOf("\n"); + if (truncatePos != -1) + str.truncate(truncatePos); + } + return str; } -QString TsStorage::targetWithPlurals(const DocPosition& pos) const +QString TsStorage::targetWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { - return target(pos); + QString str = target(pos); + if (truncateFirstLine) + { + int truncatePos = str.indexOf("\n"); + if (truncatePos != -1) + str.truncate(truncatePos); + } + return str; } void TsStorage::targetDelete(const DocPosition& pos, int count) { TsContentEditingData data(pos.offset, count); content(targetForPos(pos), &data); } void TsStorage::targetInsert(const DocPosition& pos, const QString& arg) { qCWarning(LOKALIZE_LOG) << pos.entry << arg; QDomElement targetEl = targetForPos(pos); //BEGIN add <*target> if (targetEl.isNull()) { QDomNode unitEl = unitForPos(pos.entry); QDomNode refNode = unitEl.firstChildElement(names[SourceTag]); targetEl = unitEl.insertAfter(m_doc.createElement(names[TargetTag]), refNode).toElement(); if (pos.entry < size()) { targetEl.appendChild(m_doc.createTextNode(arg));//i bet that pos.offset is 0 ;) return; } } //END add <*target> if (arg.isEmpty()) return; //means we were called just to add tag TsContentEditingData data(pos.offset, arg); content(targetEl, &data); } void TsStorage::setTarget(const DocPosition& pos, const QString& arg) { Q_UNUSED(pos); Q_UNUSED(arg); //TODO } QVector TsStorage::altTrans(const DocPosition& pos) const { QVector result; QString oldsource = content(unitForPos(pos.entry).firstChildElement(names[OldSourceTag])); if (!oldsource.isEmpty()) result << AltTrans(CatalogString(oldsource), i18n("Previous source value, saved by lupdate tool")); return result; } QStringList TsStorage::sourceFiles(const DocPosition& pos) const { QStringList result; QDomElement elem = unitForPos(pos.entry).firstChildElement(attrnames[LocationAttr]); while (!elem.isNull()) { QString sourcefile = elem.attribute(QStringLiteral("filename")); QString linenumber = elem.attribute(QStringLiteral("line")); if (!(sourcefile.isEmpty() && linenumber.isEmpty())) result.append(sourcefile + ':' + linenumber); elem = elem.nextSiblingElement(attrnames[LocationAttr]); } //qSort(result); return result; } QVector TsStorage::notes(const DocPosition& pos) const { QVector result; QDomElement elem = unitForPos(pos.entry).firstChildElement(names[NoteTag]); while (!elem.isNull()) { Note note; note.content = elem.text(); result.append(note); elem = elem.nextSiblingElement(names[NoteTag]); } return result; } QVector TsStorage::developerNotes(const DocPosition& pos) const { QVector result; QDomElement elem = unitForPos(pos.entry).firstChildElement(names[DevNoteTag]); while (!elem.isNull()) { Note note; note.content = elem.text(); result.append(note); elem = elem.nextSiblingElement(names[DevNoteTag]); } return result; } Note TsStorage::setNote(DocPosition pos, const Note& note) { //qCWarning(LOKALIZE_LOG)< if needed QDomElement target = unitForPos(pos.entry).firstChildElement(names[TargetTag]); //asking directly to bypass plural state detection if (target.attribute(attrnames[TypeAttr]) == attrvalues[ObsoleteVal]) return; if (approved) target.removeAttribute(attrnames[TypeAttr]); else target.setAttribute(attrnames[TypeAttr], QStringLiteral("unfinished")); } bool TsStorage::isApproved(const DocPosition& pos) const { QDomElement target = unitForPos(pos.entry).firstChildElement(names[TargetTag]); return !target.hasAttribute(attrnames[TypeAttr]) || target.attribute(attrnames[TypeAttr]) == attrvalues[VanishedVal]; } bool TsStorage::isObsolete(int entry) const { QDomElement target = unitForPos(entry).firstChildElement(names[TargetTag]); QString v = target.attribute(attrnames[TypeAttr]); return v == attrvalues[ObsoleteVal] || v == attrvalues[VanishedVal]; } bool TsStorage::isEmpty(const DocPosition& pos) const { TsContentEditingData data(TsContentEditingData::CheckLength); return content(targetForPos(pos), &data).isEmpty(); } bool TsStorage::isEquivTrans(const DocPosition& pos) const { Q_UNUSED(pos) return true;//targetForPos(pos.entry).attribute("equiv-trans")!="no"; } void TsStorage::setEquivTrans(const DocPosition& pos, bool equivTrans) { Q_UNUSED(pos) Q_UNUSED(equivTrans) //targetForPos(pos.entry).setAttribute("equiv-trans",noyes[equivTrans]); } QDomElement TsStorage::unitForPos(int pos) const { return entries.at(pos).toElement(); } QDomElement TsStorage::targetForPos(DocPosition pos) const { QDomElement unit = unitForPos(pos.entry); QDomElement translation = unit.firstChildElement(names[TargetTag]); if (!unit.hasAttribute(names[PluralTag])) return translation; if (pos.form == -1) pos.form = 0; QDomNodeList forms = translation.elementsByTagName(QStringLiteral("numerusform")); while (pos.form >= forms.size()) translation.appendChild(unit.ownerDocument().createElement(QStringLiteral("numerusform"))); return forms.at(pos.form).toElement(); } QDomElement TsStorage::sourceForPos(int pos) const { return unitForPos(pos).firstChildElement(names[SourceTag]); } void TsStorage::setTargetLangCode(const QString& langCode) { m_targetLangCode = langCode; QDomElement file = m_doc.elementsByTagName(QStringLiteral("TS")).at(0).toElement(); if (m_targetLangCode != file.attribute(QStringLiteral("language")).replace('-', '_')) { QString l = langCode; file.setAttribute(QStringLiteral("language"), l.replace('_', '-')); } } //END STORAGE TRANSLATION diff --git a/src/catalog/ts/tsstorage.h b/src/catalog/ts/tsstorage.h index 7429ddc..8ad8d1a 100644 --- a/src/catalog/ts/tsstorage.h +++ b/src/catalog/ts/tsstorage.h @@ -1,121 +1,121 @@ /* Copyright 2008-2014 Nick Shaforostoff This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TSSTORAGE_H #define TSSTORAGE_H #include "catalogstorage.h" #include #include #include #include // class QDomDocument; class TsStorage: public CatalogStorage { public: TsStorage(); ~TsStorage() override; int capabilities() const override; int load(QIODevice* device) override; bool save(QIODevice* device, bool belongsToProject = false) override; int size() const override; bool isEmpty() const; //flat-model interface (ignores TS grouping) QString source(const DocPosition& pos) const override; QString target(const DocPosition& pos) const override; - QString sourceWithPlurals(const DocPosition& pos) const override; - QString targetWithPlurals(const DocPosition& pos) const override; + QString sourceWithPlurals(const DocPosition& pos, bool truncateFirstLine) const override; + QString targetWithPlurals(const DocPosition& pos, bool truncateFirstLine) const override; CatalogString targetWithTags(DocPosition pos) const override; CatalogString sourceWithTags(DocPosition pos) const override; CatalogString catalogString(const DocPosition& pos) const override; /// all plural forms. pos.form doesn't matter TODO QStringList sourceAllForms(const DocPosition& pos, bool stripNewLines = false) const override { Q_UNUSED(pos) Q_UNUSED(stripNewLines) return QStringList(); } QStringList targetAllForms(const DocPosition& pos, bool stripNewLines = false) const override { Q_UNUSED(pos) Q_UNUSED(stripNewLines) return QStringList(); } void targetDelete(const DocPosition& pos, int count) override; void targetInsert(const DocPosition& pos, const QString& arg) override; void setTarget(const DocPosition& pos, const QString& arg) override;//called for mergeCatalog QStringList sourceFiles(const DocPosition& pos) const override; QVector altTrans(const DocPosition& pos) const override; ///@a pos.form is note number Note setNote(DocPosition pos, const Note& note) override; QVector notes(const DocPosition& pos) const override; QVector developerNotes(const DocPosition& pos) const override; QStringList context(const DocPosition& pos) const override; QStringList matchData(const DocPosition& pos) const override; QString id(const DocPosition& pos) const override; bool isPlural(const DocPosition& pos) const override; bool isEmpty(const DocPosition& pos) const override; bool isEquivTrans(const DocPosition& pos) const override; void setEquivTrans(const DocPosition& pos, bool equivTrans) override; bool isApproved(const DocPosition& pos) const override; void setApproved(const DocPosition& pos, bool approved) override; bool isObsolete(int entry) const override; QString mimetype() const override { return QStringLiteral("application/x-linguist"); } QString fileType() const override { return QStringLiteral("Qt Linguist (*.ts)"); } CatalogType type() const override { return Ts; } void setTargetLangCode(const QString& langCode) override; private: QDomElement unitForPos(int pos) const; QDomElement targetForPos(DocPosition pos) const; QDomElement sourceForPos(int pos) const; CatalogString catalogString(QDomElement contentElement) const; private: mutable QDomDocument m_doc; QDomNodeList entries; }; #endif diff --git a/src/catalog/xliff/xliffstorage.cpp b/src/catalog/xliff/xliffstorage.cpp index 9ef7b19..5e437b9 100644 --- a/src/catalog/xliff/xliffstorage.cpp +++ b/src/catalog/xliff/xliffstorage.cpp @@ -1,1025 +1,1039 @@ /* Copyright 2008-2009 Nick Shaforostoff This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "xliffstorage.h" #include "lokalize_debug.h" #include "gettextheader.h" #include "project.h" #include "version.h" #include "prefs_lokalize.h" #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #define U QLatin1String #else #define U QStringLiteral #endif static const QString noyes[] = {U("no"), U("yes")}; static const QString bintargettarget[] = {U("bin-target"), U("target")}; static const QString binsourcesource[] = {U("bin-source"), U("source")}; static const QString NOTE = U("note"); XliffStorage::XliffStorage() : CatalogStorage() { } XliffStorage::~XliffStorage() { } int XliffStorage::capabilities() const { return KeepsNoteAuthors | MultipleNotes | Phases | ExtendedStates | Tags; } //BEGIN OPEN/SAVE int XliffStorage::load(QIODevice* device) { QTime chrono; chrono.start(); QXmlSimpleReader reader; reader.setFeature(QStringLiteral("http://qt-project.org/xml/features/report-whitespace-only-CharData"), true); reader.setFeature(QStringLiteral("http://xml.org/sax/features/namespaces"), false); QXmlInputSource source(device); QString errorMsg; int errorLine;//+errorColumn; bool success = m_doc.setContent(&source, &reader, &errorMsg, &errorLine/*,errorColumn*/); QString FILE = QStringLiteral("file"); if (!success || m_doc.elementsByTagName(FILE).isEmpty()) { qCWarning(LOKALIZE_LOG) << errorMsg; return errorLine + 1; } QDomElement file = m_doc.elementsByTagName(FILE).at(0).toElement(); m_sourceLangCode = file.attribute(QStringLiteral("source-language")).replace(u'-', u'_'); m_targetLangCode = file.attribute(QStringLiteral("target-language")).replace(u'-', u'_'); m_numberOfPluralForms = numberOfPluralFormsForLangCode(m_targetLangCode); //Create entry mapping. //Along the way: for langs with more than 2 forms //we create any form-entries additionally needed entries = m_doc.elementsByTagName(QStringLiteral("trans-unit")); int size = entries.size(); m_map.clear(); m_map.reserve(size); for (int i = 0; i < size; ++i) { QDomElement parentElement = entries.at(i).parentNode().toElement(); //if (Q_UNLIKELY( e.isNull() ))//sanity // continue; m_map << i; m_unitsById[entries.at(i).toElement().attribute(QStringLiteral("id"))] = i; if (parentElement.tagName() == QLatin1String("group") && parentElement.attribute(QStringLiteral("restype")) == QLatin1String("x-gettext-plurals")) { m_plurals.insert(i); int localPluralNum = m_numberOfPluralForms; while (--localPluralNum > 0 && (++i) < size) { QDomElement p = entries.at(i).parentNode().toElement(); if (p.tagName() == QLatin1String("group") && p.attribute(QStringLiteral("restype")) == QLatin1String("x-gettext-plurals")) continue; parentElement.appendChild(entries.at(m_map.last()).cloneNode()); } } } binEntries = m_doc.elementsByTagName(QStringLiteral("bin-unit")); size = binEntries.size(); int offset = m_map.size(); for (int i = 0; i < size; ++i) m_unitsById[binEntries.at(i).toElement().attribute(QStringLiteral("id"))] = offset + i; // entries=m_doc.elementsByTagName("body"); // uint i=0; // uint lim=size(); // while (i msgstr(item.msgstrPlural()); // while (msgstr.count() tags; QString stringToInsert; int pos; int lengthOfStringToRemove; ActionType actionType; ///Get ContentEditingData(ActionType type = Get) : pos(-1) , lengthOfStringToRemove(-1) , actionType(type) {} ///DeleteText ContentEditingData(int p, int l) : pos(p) , lengthOfStringToRemove(l) , actionType(DeleteText) {} ///InsertText ContentEditingData(int p, const QString& s) : stringToInsert(s) , pos(p) , lengthOfStringToRemove(-1) , actionType(InsertText) {} ///InsertTag ContentEditingData(int p, const InlineTag& range) : pos(p) , lengthOfStringToRemove(-1) , actionType(InsertTag) { tags.append(range); } ///DeleteTag ContentEditingData(int p) : pos(p) , lengthOfStringToRemove(-1) , actionType(DeleteTag) {} }; static QString doContent(QDomElement elem, int startingPos, ContentEditingData* data); /** * walks through XLIFF XML and performs actions depending on ContentEditingData: * - reads content * - deletes content, or * - inserts content */ static QString content(QDomElement elem, ContentEditingData* data = 0) { return doContent(elem, 0, data); } static QString doContent(QDomElement elem, int startingPos, ContentEditingData* data) { //actually startingPos is current pos QString result; if (elem.isNull() || (!result.isEmpty() && data && data->actionType == ContentEditingData::CheckLength)) return QString(); bool seenCharacterDataAfterElement = false; QDomNode n = elem.firstChild(); while (!n.isNull()) { if (n.isCharacterData()) { seenCharacterDataAfterElement = true; QDomCharacterData c = n.toCharacterData(); QString cData = c.data(); if (data && data->pos != -1 && data->pos >= startingPos && data->pos <= startingPos + cData.size()) { // time to do some action! ;) int localStartPos = data->pos - startingPos; //BEGIN DELETE TEXT if (data->actionType == ContentEditingData::DeleteText) { //(data->lengthOfStringToRemove!=-1) if (localStartPos + data->lengthOfStringToRemove > cData.size()) { //text is fragmented into several QDomCharacterData int localDelLen = cData.size() - localStartPos; //qCWarning(LOKALIZE_LOG)<<"text is fragmented into several QDomCharacterData. localDelLen:"<lengthOfStringToRemove = data->lengthOfStringToRemove - localDelLen; //data->pos=startingPos; //qCWarning(LOKALIZE_LOG)<<"\tsetup:"<pos<lengthOfStringToRemove; } else { //qCWarning(LOKALIZE_LOG)<<"simple delete"<lengthOfStringToRemove; c.deleteData(localStartPos, data->lengthOfStringToRemove); data->actionType = ContentEditingData::CheckLength; return QString('a');//so it exits 100% } } //END DELETE TEXT //INSERT else if (data->actionType == ContentEditingData::InsertText) { c.insertData(localStartPos, data->stringToInsert); data->actionType = ContentEditingData::CheckLength; return QString('a');//so it exits 100% } //BEGIN INSERT TAG else if (data->actionType == ContentEditingData::InsertTag) { const InlineTag& tag = data->tags.first(); QString mid = cData.mid(localStartPos); qCDebug(LOKALIZE_LOG) << "inserting tag" << tag.name() << tag.id << tag.start << tag.end << mid << data->pos << startingPos; if (mid.size()) c.deleteData(localStartPos, mid.size()); QDomElement newNode = elem.insertAfter(elem.ownerDocument().createElement(tag.getElementName()), n).toElement(); newNode.setAttribute(QStringLiteral("id"), tag.id); if (!tag.xid.isEmpty()) newNode.setAttribute(QStringLiteral("xid"), tag.xid); if (tag.isPaired() && tag.end > (tag.start + 1)) { //qCWarning(LOKALIZE_LOG)<<"isPaired"; int len = tag.end - tag.start - 1; //-image symbol int localLen = qMin(len, mid.size()); if (localLen) { //appending text //qCWarning(LOKALIZE_LOG)<<"localLen. appending"<missingLen (or siblings end) int childrenCumulativeLen = 0; QDomNode sibling = newNode.nextSibling(); while (!sibling.isNull()) { //&&(childrenCumulativeLen missingLen) { if (tmp.isCharacterData()) { //divide the last string const QString& endData = tmp.toCharacterData().data(); QString last = endData.left(endData.size() - (childrenCumulativeLen - missingLen)); newNode.appendChild(elem.ownerDocument().createTextNode(last)); tmp.toCharacterData().deleteData(0, last.size()); //qCWarning(LOKALIZE_LOG)<<"end of add"<actionType = ContentEditingData::CheckLength; return QStringLiteral("a");//we're done here } //END INSERT TAG cData = c.data(); } //else // if (data&&data->pos!=-1/*&& n.nextSibling().isNull()*/) // qCWarning(LOKALIZE_LOG)<<"arg!"<pos"<pos; result += cData; startingPos += cData.size(); } else if (n.isElement()) { QDomElement el = n.toElement(); //BEGIN DELETE TAG if (data && data->actionType == ContentEditingData::DeleteTag && data->pos == startingPos) { //qCWarning(LOKALIZE_LOG)<<"start deleting tag"; data->tags.append(InlineTag(startingPos, -1, InlineTag::getElementType(el.tagName().toUtf8()), el.attribute("id"), el.attribute("xid"))); if (data->tags.first().isPaired()) { //get end position ContentEditingData subData(ContentEditingData::Get); QString subContent = doContent(el, startingPos, &subData); data->tags[0].end = 1 + startingPos + subContent.size(); //tagsymbol+text //qCWarning(LOKALIZE_LOG)<<"get end position"<actionType = ContentEditingData::CheckLength; return QStringLiteral("a");//we're done here } //END DELETE TAG if (!seenCharacterDataAfterElement) //add empty charData child so that user could add some text elem.insertBefore(elem.ownerDocument().createTextNode(QString()), n); seenCharacterDataAfterElement = false; if (data) { result += QChar(TAGRANGE_IMAGE_SYMBOL); ++startingPos; } int oldStartingPos = startingPos; //detect type of the tag InlineTag::InlineElement i = InlineTag::getElementType(el.tagName().toUtf8()); //1 or 2 images to represent it? //2 = there may be content inside if (InlineTag::isPaired(i)) { QString recursiveContent = doContent(el, startingPos, data); if (!recursiveContent.isEmpty()) { result += recursiveContent; startingPos += recursiveContent.size(); } if (data) { result += QChar(TAGRANGE_IMAGE_SYMBOL); ++startingPos; } } if (data && data->actionType == ContentEditingData::Get) { QString id = el.attribute(QStringLiteral("id")); if (i == InlineTag::mrk) //TODO attr map id = el.attribute(QStringLiteral("mtype")); //qCWarning(LOKALIZE_LOG)<<"tagName"<tags.append(InlineTag(oldStartingPos - 1, startingPos - 1, i, id, el.attribute(QStringLiteral("xid")))); } } n = n.nextSibling(); } if (!seenCharacterDataAfterElement) { //add empty charData child so that user could add some text elem.appendChild(elem.ownerDocument().createTextNode(QString())); } return result; } //flat-model interface (ignores XLIFF grouping) CatalogString XliffStorage::catalogString(QDomElement unit, DocPosition::Part part) const { static const QString names[] = {U("source"), U("target"), U("seg-source")}; CatalogString catalogString; ContentEditingData data(ContentEditingData::Get); int nameIndex = part == DocPosition::Target; if (nameIndex == 0 && !unit.firstChildElement(names[2]).isNull()) nameIndex = 2; catalogString.string = content(unit.firstChildElement(names[nameIndex]), &data); catalogString.tags = data.tags; return catalogString; } CatalogString XliffStorage::catalogString(const DocPosition& pos) const { return catalogString(unitForPos(pos.entry), pos.part); } CatalogString XliffStorage::targetWithTags(DocPosition pos) const { return catalogString(unitForPos(pos.entry), DocPosition::Target); } CatalogString XliffStorage::sourceWithTags(DocPosition pos) const { return catalogString(unitForPos(pos.entry), DocPosition::Source); } static QString genericContent(QDomElement elem, bool nonbin) { return nonbin ? content(elem) : elem.firstChildElement(QStringLiteral("external-file")).attribute(QStringLiteral("href")); } QString XliffStorage::source(const DocPosition& pos) const { return genericContent(sourceForPos(pos.entry), pos.entry < size()); } QString XliffStorage::target(const DocPosition& pos) const { return genericContent(targetForPos(pos.entry), pos.entry < size()); } -QString XliffStorage::sourceWithPlurals(const DocPosition& pos) const +QString XliffStorage::sourceWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { - return source(pos); + QString str = source(pos); + if (truncateFirstLine) + { + int truncatePos = str.indexOf("\n"); + if (truncatePos != -1) + str.truncate(truncatePos); + } + return str; } -QString XliffStorage::targetWithPlurals(const DocPosition& pos) const +QString XliffStorage::targetWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { - return target(pos); + QString str = target(pos); + if (truncateFirstLine) + { + int truncatePos = str.indexOf("\n"); + if (truncatePos != -1) + str.truncate(truncatePos); + } + return str; } void XliffStorage::targetDelete(const DocPosition& pos, int count) { if (pos.entry < size()) { ContentEditingData data(pos.offset, count); content(targetForPos(pos.entry), &data); } else { //only bulk delete requests are generated targetForPos(pos.entry).firstChildElement(QStringLiteral("external-file")).setAttribute(QStringLiteral("href"), QString()); } } void XliffStorage::targetInsert(const DocPosition& pos, const QString& arg) { //qCWarning(LOKALIZE_LOG)<<"targetinsert"< if (targetEl.isNull()) { QDomNode unitEl = unitForPos(pos.entry); QDomNode refNode = unitEl.firstChildElement(QStringLiteral("seg-source")); //obey standard if (refNode.isNull()) refNode = unitEl.firstChildElement(binsourcesource[pos.entry < size()]); targetEl = unitEl.insertAfter(m_doc.createElement(bintargettarget[pos.entry < size()]), refNode).toElement(); targetEl.setAttribute(QStringLiteral("state"), QStringLiteral("new")); if (pos.entry < size()) { targetEl.appendChild(m_doc.createTextNode(arg));//i bet that pos.offset is 0 ;) return; } } //END add <*target> if (arg.isEmpty()) return; //means we were called just to add tag if (pos.entry >= size()) { QDomElement ef = targetEl.firstChildElement(QStringLiteral("external-file")); if (ef.isNull()) ef = targetEl.appendChild(m_doc.createElement(QStringLiteral("external-file"))).toElement(); ef.setAttribute(QStringLiteral("href"), arg); return; } ContentEditingData data(pos.offset, arg); content(targetEl, &data); } void XliffStorage::targetInsertTag(const DocPosition& pos, const InlineTag& tag) { targetInsert(pos, QString()); //adds if needed ContentEditingData data(tag.start, tag); content(targetForPos(pos.entry), &data); } InlineTag XliffStorage::targetDeleteTag(const DocPosition& pos) { ContentEditingData data(pos.offset); content(targetForPos(pos.entry), &data); if (data.tags[0].end == -1) data.tags[0].end = data.tags[0].start; return data.tags.first(); } void XliffStorage::setTarget(const DocPosition& pos, const QString& arg) { Q_UNUSED(pos); Q_UNUSED(arg); //TODO } QVector XliffStorage::altTrans(const DocPosition& pos) const { QVector result; QDomElement elem = unitForPos(pos.entry).firstChildElement(QStringLiteral("alt-trans")); while (!elem.isNull()) { AltTrans aTrans; aTrans.source = catalogString(elem, DocPosition::Source); aTrans.target = catalogString(elem, DocPosition::Target); aTrans.phase = elem.attribute(QStringLiteral("phase-name")); aTrans.origin = elem.attribute(QStringLiteral("origin")); aTrans.score = elem.attribute(QStringLiteral("match-quality")).toInt(); aTrans.lang = elem.firstChildElement(QStringLiteral("target")).attribute(QStringLiteral("xml:lang")); const char* const types[] = { "proposal", "previous-version", "rejected", "reference", "accepted" }; QString typeStr = elem.attribute(QStringLiteral("alttranstype")); int i = -1; while (++i < int(sizeof(types) / sizeof(char*)) && types[i] != typeStr) ; aTrans.type = AltTrans::Type(i); result << aTrans; elem = elem.nextSiblingElement(QStringLiteral("alt-trans")); } return result; } static QDomElement phaseElement(QDomDocument m_doc, const QString& name, QDomElement& phasegroup) { QDomElement file = m_doc.elementsByTagName(QStringLiteral("file")).at(0).toElement(); QDomElement header = file.firstChildElement(QStringLiteral("header")); phasegroup = header.firstChildElement(QStringLiteral("phase-group")); if (phasegroup.isNull()) { phasegroup = m_doc.createElement(QStringLiteral("phase-group")); //order following XLIFF spec QDomElement skl = header.firstChildElement(QStringLiteral("skl")); if (!skl.isNull()) header.insertAfter(phasegroup, skl); else header.insertBefore(phasegroup, header.firstChildElement()); } QDomElement phaseElem = phasegroup.firstChildElement(QStringLiteral("phase")); while (!phaseElem.isNull() && phaseElem.attribute(QStringLiteral("phase-name")) != name) phaseElem = phaseElem.nextSiblingElement(QStringLiteral("phase")); return phaseElem; } static Phase phaseFromElement(QDomElement phaseElem) { Phase phase; phase.name = phaseElem.attribute(QStringLiteral("phase-name")); phase.process = phaseElem.attribute(QStringLiteral("process-name")); phase.company = phaseElem.attribute(QStringLiteral("company-name")); phase.contact = phaseElem.attribute(QStringLiteral("contact-name")); phase.email = phaseElem.attribute(QStringLiteral("contact-email")); phase.phone = phaseElem.attribute(QStringLiteral("contact-phone")); phase.tool = phaseElem.attribute(QStringLiteral("tool-id")); phase.date = QDate::fromString(phaseElem.attribute(QStringLiteral("date")), Qt::ISODate); return phase; } Phase XliffStorage::updatePhase(const Phase& phase) { QDomElement phasegroup; QDomElement phaseElem = phaseElement(m_doc, phase.name, phasegroup); Phase prev = phaseFromElement(phaseElem); if (phaseElem.isNull() && !phase.name.isEmpty()) { phaseElem = phasegroup.appendChild(m_doc.createElement(QStringLiteral("phase"))).toElement(); phaseElem.setAttribute(QStringLiteral("phase-name"), phase.name); } phaseElem.setAttribute(QStringLiteral("process-name"), phase.process); if (!phase.company.isEmpty()) phaseElem.setAttribute(QStringLiteral("company-name"), phase.company); phaseElem.setAttribute(QStringLiteral("contact-name"), phase.contact); phaseElem.setAttribute(QStringLiteral("contact-email"), phase.email); //Q_ASSERT(phase.contact.length()); //is empty when exiting w/o saving if (!phase.phone.isEmpty()) phaseElem.setAttribute(QLatin1String("contact-phone"), phase.phone); phaseElem.setAttribute(QStringLiteral("tool-id"), phase.tool); if (phase.date.isValid()) phaseElem.setAttribute(QStringLiteral("date"), phase.date.toString(Qt::ISODate)); return prev; } QList XliffStorage::allPhases() const { QList result; QDomElement file = m_doc.elementsByTagName(QStringLiteral("file")).at(0).toElement(); QDomElement header = file.firstChildElement(QStringLiteral("header")); QDomElement phasegroup = header.firstChildElement(QStringLiteral("phase-group")); QDomElement phaseElem = phasegroup.firstChildElement(QStringLiteral("phase")); while (!phaseElem.isNull()) { result.append(phaseFromElement(phaseElem)); phaseElem = phaseElem.nextSiblingElement(QStringLiteral("phase")); } return result; } Phase XliffStorage::phase(const QString& name) const { QDomElement phasegroup; QDomElement phaseElem = phaseElement(m_doc, name, phasegroup); return phaseFromElement(phaseElem); } QMap XliffStorage::allTools() const { QMap result; QDomElement file = m_doc.elementsByTagName(QStringLiteral("file")).at(0).toElement(); QDomElement header = file.firstChildElement(QStringLiteral("header")); QDomElement toolElem = header.firstChildElement(QStringLiteral("tool")); while (!toolElem.isNull()) { Tool tool; tool.tool = toolElem.attribute(QStringLiteral("tool-id")); tool.name = toolElem.attribute(QStringLiteral("tool-name")); tool.version = toolElem.attribute(QStringLiteral("tool-version")); tool.company = toolElem.attribute(QStringLiteral("tool-company")); result.insert(tool.tool, tool); toolElem = toolElem.nextSiblingElement(QStringLiteral("tool")); } return result; } QStringList XliffStorage::sourceFiles(const DocPosition& pos) const { QStringList result; QDomElement elem = unitForPos(pos.entry).firstChildElement(QStringLiteral("context-group")); while (!elem.isNull()) { if (elem.attribute(QStringLiteral("purpose")).contains(QLatin1String("location"))) { QDomElement context = elem.firstChildElement(QStringLiteral("context")); while (!context.isNull()) { QString sourcefile; QString linenumber; const QString contextType = context.attribute(QStringLiteral("context-type")); if (contextType == QLatin1String("sourcefile")) sourcefile = context.text(); else if (contextType == QLatin1String("linenumber")) linenumber = context.text(); if (!(sourcefile.isEmpty() && linenumber.isEmpty())) result.append(sourcefile % ':' % linenumber); context = context.nextSiblingElement(QStringLiteral("context")); } } elem = elem.nextSiblingElement(QStringLiteral("context-group")); } //qSort(result); return result; } static void initNoteFromElement(Note& note, QDomElement elem) { note.content = elem.text(); note.from = elem.attribute(QStringLiteral("from")); note.lang = elem.attribute(QStringLiteral("xml:lang")); if (elem.attribute(QStringLiteral("annotates")) == QLatin1String("source")) note.annotates = Note::Source; else if (elem.attribute(QStringLiteral("annotates")) == QLatin1String("target")) note.annotates = Note::Target; bool ok; note.priority = elem.attribute(QStringLiteral("priority")).toInt(&ok); if (!ok) note.priority = 0; } QVector XliffStorage::notes(const DocPosition& pos) const { QList result; QDomElement elem = entries.at(m_map.at(pos.entry)).firstChildElement(NOTE); while (!elem.isNull()) { Note note; initNoteFromElement(note, elem); result.append(note); elem = elem.nextSiblingElement(NOTE); } qSort(result); return result.toVector(); } QVector XliffStorage::developerNotes(const DocPosition& pos) const { Q_UNUSED(pos); //TODO return QVector(); } Note XliffStorage::setNote(DocPosition pos, const Note& note) { //qCWarning(LOKALIZE_LOG)< result; QDomNodeList notes = m_doc.elementsByTagName(NOTE); int i = notes.size(); while (--i >= 0) { QString from = notes.at(i).toElement().attribute(QStringLiteral("from")); if (!from.isEmpty()) result.insert(from); } return result.toList(); } QVector phaseNotes(QDomDocument m_doc, const QString& phasename, bool remove = false) { QVector result; QDomElement phasegroup; QDomElement phaseElem = phaseElement(m_doc, phasename, phasegroup); QDomElement noteElem = phaseElem.firstChildElement(NOTE); while (!noteElem.isNull()) { Note note; initNoteFromElement(note, noteElem); result.append(note); QDomElement old = noteElem; noteElem = noteElem.nextSiblingElement(NOTE); if (remove) phaseElem.removeChild(old); } return result; } QVector XliffStorage::phaseNotes(const QString& phasename) const { return ::phaseNotes(m_doc, phasename, false); } QVector XliffStorage::setPhaseNotes(const QString& phasename, QVector notes) { QVector result =::phaseNotes(m_doc, phasename, true); QDomElement phasegroup; QDomElement phaseElem = phaseElement(m_doc, phasename, phasegroup); foreach (const Note& note, notes) { QDomElement elem = phaseElem.appendChild(m_doc.createElement(NOTE)).toElement(); elem.appendChild(m_doc.createTextNode(note.content)); if (!note.from.isEmpty()) elem.setAttribute(QStringLiteral("from"), note.from); if (note.priority) elem.setAttribute(QStringLiteral("priority"), note.priority); } return result; } QString XliffStorage::setPhase(const DocPosition& pos, const QString& phase) { QString PHASENAME = QStringLiteral("phase-name"); targetInsert(pos, QString()); //adds if needed QDomElement target = targetForPos(pos.entry); QString result = target.attribute(PHASENAME); if (phase.isEmpty()) target.removeAttribute(PHASENAME); else if (phase != result) target.setAttribute(PHASENAME, phase); return result; } QString XliffStorage::phase(const DocPosition& pos) const { QDomElement target = targetForPos(pos.entry); return target.attribute(QStringLiteral("phase-name")); } QStringList XliffStorage::context(const DocPosition& pos) const { Q_UNUSED(pos); //TODO return QStringList(QString()); } QStringList XliffStorage::matchData(const DocPosition& pos) const { Q_UNUSED(pos); return QStringList(); } QString XliffStorage::id(const DocPosition& pos) const { return unitForPos(pos.entry).attribute(QStringLiteral("id")); } bool XliffStorage::isPlural(const DocPosition& pos) const { return m_plurals.contains(pos.entry); } /* bool XliffStorage::isApproved(const DocPosition& pos) const { return entries.at(m_map.at(pos.entry)).toElement().attribute("approved")=="yes"; } void XliffStorage::setApproved(const DocPosition& pos, bool approved) { static const char* const noyes[]={"no","yes"}; entries.at(m_map.at(pos.entry)).toElement().setAttribute("approved",noyes[approved]); } */ static const QString xliff_states[] = { U("new"), U("needs-translation"), U("needs-l10n"), U("needs-adaptation"), U("translated"), U("needs-review-translation"), U("needs-review-l10n"), U("needs-review-adaptation"), U("final"), U("signed-off") }; TargetState stringToState(const QString& state) { int i = sizeof(xliff_states) / sizeof(QString); while (--i > 0 && state != xliff_states[i]) ; return TargetState(i); } TargetState XliffStorage::setState(const DocPosition& pos, TargetState state) { targetInsert(pos, QString()); //adds if needed QDomElement target = targetForPos(pos.entry); TargetState prev = stringToState(target.attribute(QStringLiteral("state"))); target.setAttribute(QStringLiteral("state"), xliff_states[state]); unitForPos(pos.entry).setAttribute(QStringLiteral("approved"), noyes[state == SignedOff]); return prev; } TargetState XliffStorage::state(const DocPosition& pos) const { QDomElement target = targetForPos(pos.entry); if (!target.hasAttribute(QStringLiteral("state")) && unitForPos(pos.entry).attribute(QStringLiteral("approved")) == QLatin1String("yes")) return SignedOff; return stringToState(target.attribute(QStringLiteral("state"))); } bool XliffStorage::isEmpty(const DocPosition& pos) const { ContentEditingData data(ContentEditingData::CheckLength); return content(targetForPos(pos.entry), &data).isEmpty(); } bool XliffStorage::isEquivTrans(const DocPosition& pos) const { return targetForPos(pos.entry).attribute(QStringLiteral("equiv-trans")) != QLatin1String("no"); } void XliffStorage::setEquivTrans(const DocPosition& pos, bool equivTrans) { targetForPos(pos.entry).setAttribute(QStringLiteral("equiv-trans"), noyes[equivTrans]); } QDomElement XliffStorage::unitForPos(int pos) const { if (pos < size()) return entries.at(m_map.at(pos)).toElement(); return binEntries.at(pos - size()).toElement(); } QDomElement XliffStorage::targetForPos(int pos) const { return unitForPos(pos).firstChildElement(bintargettarget[pos < size()]); } QDomElement XliffStorage::sourceForPos(int pos) const { return unitForPos(pos).firstChildElement(binsourcesource[pos < size()]); } int XliffStorage::binUnitsCount() const { return binEntries.size(); } int XliffStorage::unitById(const QString& id) const { return m_unitsById.contains(id) ? m_unitsById.value(id) : -1; } QString XliffStorage::originalOdfFilePath() { QDomElement file = m_doc.elementsByTagName(QStringLiteral("file")).at(0).toElement(); return file.attribute(QStringLiteral("original")); } void XliffStorage::setOriginalOdfFilePath(const QString& odfFilePath) { QDomElement file = m_doc.elementsByTagName(QStringLiteral("file")).at(0).toElement(); return file.setAttribute(QStringLiteral("original"), odfFilePath); } //END STORAGE TRANSLATION diff --git a/src/catalog/xliff/xliffstorage.h b/src/catalog/xliff/xliffstorage.h index 40e17a1..731e43a 100644 --- a/src/catalog/xliff/xliffstorage.h +++ b/src/catalog/xliff/xliffstorage.h @@ -1,142 +1,142 @@ /* Copyright 2008-2014 Nick Shaforostoff This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef XLIFFSTORAGE_H #define XLIFFSTORAGE_H #include "catalogstorage.h" #include #include #include #include #include // class QDomDocument; class XliffStorage: public CatalogStorage { public: XliffStorage(); ~XliffStorage() override; int capabilities() const override; int load(QIODevice* device) override; bool save(QIODevice* device, bool belongsToProject = false) override; int size() const override; bool isEmpty() const; //flat-model interface (ignores XLIFF grouping) QString source(const DocPosition& pos) const override; QString target(const DocPosition& pos) const override; - QString sourceWithPlurals(const DocPosition& pos) const override; - QString targetWithPlurals(const DocPosition& pos) const override; + QString sourceWithPlurals(const DocPosition& pos, bool truncateFirstLine) const override; + QString targetWithPlurals(const DocPosition& pos, bool truncateFirstLine) const override; CatalogString targetWithTags(DocPosition pos) const override; CatalogString sourceWithTags(DocPosition pos) const override; CatalogString catalogString(const DocPosition& pos) const override; /// all plural forms. pos.form doesn't matter TODO QStringList sourceAllForms(const DocPosition& pos, bool stripNewLines = false) const override { Q_UNUSED(pos) Q_UNUSED(stripNewLines) return QStringList(); } QStringList targetAllForms(const DocPosition& pos, bool stripNewLines = false) const override { Q_UNUSED(pos) Q_UNUSED(stripNewLines) return QStringList(); } void targetDelete(const DocPosition& pos, int count) override; void targetInsert(const DocPosition& pos, const QString& arg) override; void setTarget(const DocPosition& pos, const QString& arg) override;//called for mergeCatalog void targetInsertTag(const DocPosition&, const InlineTag&) override; InlineTag targetDeleteTag(const DocPosition&) override; Phase updatePhase(const Phase& phase) override; QList allPhases() const override; Phase phase(const QString& name) const override; QMap allTools() const override; QVector phaseNotes(const QString& phase) const override; QVector setPhaseNotes(const QString& phase, QVector notes) override; QStringList sourceFiles(const DocPosition& pos) const override; QVector altTrans(const DocPosition& pos) const override; ///@a pos.form is note number Note setNote(DocPosition pos, const Note& note) override; QVector notes(const DocPosition& pos) const override; QStringList noteAuthors() const override; QVector developerNotes(const DocPosition& pos) const override; QString setPhase(const DocPosition& pos, const QString& phase) override; QString phase(const DocPosition& pos) const override; QStringList context(const DocPosition& pos) const override; QStringList matchData(const DocPosition& pos) const override; QString id(const DocPosition& pos) const override; bool isPlural(const DocPosition& pos) const override; bool isEmpty(const DocPosition& pos) const override; bool isEquivTrans(const DocPosition& pos) const override; void setEquivTrans(const DocPosition& pos, bool equivTrans) override; TargetState state(const DocPosition& pos) const override; TargetState setState(const DocPosition& pos, TargetState state) override; int binUnitsCount() const override; int unitById(const QString& id) const override; QString mimetype() const override { return QStringLiteral("application/x-xliff"); } QString fileType() const override { return QStringLiteral("XLIFF (*.xliff *.xlf)"); } CatalogType type() const override { return Xliff; } QString originalOdfFilePath() override; void setOriginalOdfFilePath(const QString&) override; void setTargetLangCode(const QString& langCode) override; private: QDomElement unitForPos(int pos) const; QDomElement targetForPos(int pos) const; QDomElement sourceForPos(int pos) const; CatalogString catalogString(QDomElement unit, DocPosition::Part part) const; private: mutable QDomDocument m_doc; QVector m_map;//need mapping to treat plurals as 1 entry QSet m_plurals; QDomNodeList entries; QDomNodeList binEntries; QMap m_unitsById; }; #endif diff --git a/src/cataloglistview/catalogmodel.cpp b/src/cataloglistview/catalogmodel.cpp index 18b2a47..124760f 100644 --- a/src/cataloglistview/catalogmodel.cpp +++ b/src/cataloglistview/catalogmodel.cpp @@ -1,297 +1,297 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2014 by Nick Shaforostoff This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "catalogmodel.h" #include "lokalize_debug.h" #include "catalog.h" #include "project.h" #include #include #include #include #include #define DYNAMICFILTER_LIMIT 256 QVector CatalogTreeModel::m_fonts; CatalogTreeModel::CatalogTreeModel(QObject* parent, Catalog* catalog) : QAbstractItemModel(parent) , m_catalog(catalog) , m_ignoreAccel(true) { if (m_fonts.isEmpty()) { QVector fonts(4, QApplication::font()); fonts[1].setItalic(true); //fuzzy fonts[2].setBold(true); //modified fonts[3].setItalic(true); //fuzzy fonts[3].setBold(true); //modified m_fonts.reserve(4); for (int i = 0; i < 4; i++) m_fonts << fonts.at(i); } connect(catalog, &Catalog::signalEntryModified, this, &CatalogTreeModel::reflectChanges); connect(catalog, QOverload<>::of(&Catalog::signalFileLoaded), this, &CatalogTreeModel::fileLoaded); } QModelIndex CatalogTreeModel::index(int row, int column, const QModelIndex& /*parent*/) const { return createIndex(row, column); } QModelIndex CatalogTreeModel::parent(const QModelIndex& /*index*/) const { return QModelIndex(); } int CatalogTreeModel::columnCount(const QModelIndex& parent) const { Q_UNUSED(parent); return DisplayedColumnCount; } void CatalogTreeModel::fileLoaded() { beginResetModel(); endResetModel(); } void CatalogTreeModel::reflectChanges(DocPosition pos) { emit dataChanged(index(pos.entry, 0), index(pos.entry, DisplayedColumnCount - 1)); #if 0 I disabled dynamicSortFilter function //lazy sorting/filtering if (rowCount() < DYNAMICFILTER_LIMIT || m_prevChanged != pos) { qCWarning(LOKALIZE_LOG) << "first dataChanged emitment" << pos.entry; emit dataChanged(index(pos.entry, 0), index(pos.entry, DisplayedColumnCount - 1)); if (!(rowCount() < DYNAMICFILTER_LIMIT)) { qCWarning(LOKALIZE_LOG) << "second dataChanged emitment" << m_prevChanged.entry; emit dataChanged(index(m_prevChanged.entry, 0), index(m_prevChanged.entry, DisplayedColumnCount - 1)); } } m_prevChanged = pos; #endif } int CatalogTreeModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return m_catalog->numberOfEntries(); } QVariant CatalogTreeModel::headerData(int section, Qt::Orientation /*orientation*/, int role) const { if (role != Qt::DisplayRole) return QVariant(); switch (section) { case Key: return i18nc("@title:column", "Entry"); case Source: return i18nc("@title:column Original text", "Source"); case Target: return i18nc("@title:column Text in target language", "Target"); case Notes: return i18nc("@title:column", "Notes"); case Context: return i18nc("@title:column", "Context"); case TranslationStatus: return i18nc("@title:column", "Translation Status"); } return QVariant(); } QVariant CatalogTreeModel::data(const QModelIndex& index, int role) const { if (m_catalog->numberOfEntries() <= index.row()) return QVariant(); if (role == Qt::SizeHintRole) { //no need to cache because of uniform row heights return QFontMetrics(QApplication::font()).size(Qt::TextSingleLine, QString::fromLatin1(" ")); } else if (role == Qt::FontRole/* && index.column()==Target*/) { bool fuzzy = !m_catalog->isApproved(index.row()); bool modified = m_catalog->isModified(index.row()); return m_fonts.at(fuzzy * 1 | modified * 2); } else if (role == Qt::ForegroundRole) { if (m_catalog->isBookmarked(index.row())) { static KColorScheme colorScheme(QPalette::Normal); return colorScheme.foreground(KColorScheme::LinkText); } if (m_catalog->isObsolete(index.row())) { static KColorScheme colorScheme(QPalette::Normal); return colorScheme.foreground(KColorScheme::InactiveText); } } else if (role == Qt::UserRole) { switch (index.column()) { case TranslationStatus: return m_catalog->isApproved(index.row()); case IsEmpty: return m_catalog->isEmpty(index.row()); case State: return int(m_catalog->state(index.row())); case IsModified: return m_catalog->isModified(index.row()); case IsPlural: return m_catalog->isPlural(index.row()); default: role = Qt::DisplayRole; } } else if (role == StringFilterRole) { //exclude UI strings if (index.column() >= TranslationStatus) return QVariant(); else if (index.column() == Source || index.column() == Target) { - QString str = index.column() == Source ? m_catalog->msgidWithPlurals(index.row()) : m_catalog->msgstrWithPlurals(index.row()); + QString str = index.column() == Source ? m_catalog->msgidWithPlurals(index.row(), false) : m_catalog->msgstrWithPlurals(index.row(), false); return m_ignoreAccel ? str.remove(Project::instance()->accel()) : str; } role = Qt::DisplayRole; } if (role != Qt::DisplayRole) return QVariant(); switch (index.column()) { case Key: return index.row() + 1; case Source: - return m_catalog->msgidWithPlurals(index.row()); + return m_catalog->msgidWithPlurals(index.row(), true); case Target: - return m_catalog->msgstrWithPlurals(index.row()); + return m_catalog->msgstrWithPlurals(index.row(), true); case Notes: { QString result; foreach (const Note ¬e, m_catalog->notes(index.row())) result += note.content; return result; } case Context: return m_catalog->context(index.row()); case TranslationStatus: static QString statuses[] = {i18nc("@info:status 'non-fuzzy' in gettext terminology", "Ready"), i18nc("@info:status 'fuzzy' in gettext terminology", "Needs review"), i18nc("@info:status", "Untranslated") }; if (m_catalog->isEmpty(index.row())) return statuses[2]; return statuses[!m_catalog->isApproved(index.row())]; } return QVariant(); } CatalogTreeFilterModel::CatalogTreeFilterModel(QObject* parent) : QSortFilterProxyModel(parent) , m_filterOptions(AllStates) , m_individualRejectFilterEnable(false) , m_mergeCatalog(NULL) { setFilterKeyColumn(-1); setFilterCaseSensitivity(Qt::CaseInsensitive); setFilterRole(CatalogTreeModel::StringFilterRole); setDynamicSortFilter(false); } void CatalogTreeFilterModel::setSourceModel(QAbstractItemModel* sourceModel) { QSortFilterProxyModel::setSourceModel(sourceModel); connect(sourceModel, &QAbstractItemModel::modelReset, this, QOverload<>::of(&CatalogTreeFilterModel::setEntriesFilteredOut)); setEntriesFilteredOut(false); } void CatalogTreeFilterModel::setEntriesFilteredOut() { return setEntriesFilteredOut(false); } void CatalogTreeFilterModel::setEntriesFilteredOut(bool filteredOut) { m_individualRejectFilter.fill(filteredOut, sourceModel()->rowCount()); m_individualRejectFilterEnable = filteredOut; invalidateFilter(); } void CatalogTreeFilterModel::setEntryFilteredOut(int entry, bool filteredOut) { // if (entry>=m_individualRejectFilter.size()) // sourceModelReset(); m_individualRejectFilter[entry] = filteredOut; m_individualRejectFilterEnable = true; invalidateFilter(); } void CatalogTreeFilterModel::setFilterOptions(int o) { m_filterOptions = o; setFilterCaseSensitivity(o & CaseInsensitive ? Qt::CaseInsensitive : Qt::CaseSensitive); static_cast(sourceModel())->setIgnoreAccel(o & IgnoreAccel); invalidateFilter(); } bool CatalogTreeFilterModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { int filerOptions = m_filterOptions; bool accepts = true; if (bool(filerOptions & Ready) != bool(filerOptions & NotReady)) { bool ready = sourceModel()->index(source_row, CatalogTreeModel::TranslationStatus, source_parent).data(Qt::UserRole).toBool(); accepts = (ready == bool(filerOptions & Ready) || ready != bool(filerOptions & NotReady)); } if (accepts && bool(filerOptions & NonEmpty) != bool(filerOptions & Empty)) { bool untr = sourceModel()->index(source_row, CatalogTreeModel::IsEmpty, source_parent).data(Qt::UserRole).toBool(); accepts = (untr == bool(filerOptions & Empty) || untr != bool(filerOptions & NonEmpty)); } if (accepts && bool(filerOptions & Modified) != bool(filerOptions & NonModified)) { bool modified = sourceModel()->index(source_row, CatalogTreeModel::IsModified, source_parent).data(Qt::UserRole).toBool(); accepts = (modified == bool(filerOptions & Modified) || modified != bool(filerOptions & NonModified)); } if (accepts && bool(filerOptions & Plural) != bool(filerOptions & NonPlural)) { bool modified = sourceModel()->index(source_row, CatalogTreeModel::IsPlural, source_parent).data(Qt::UserRole).toBool(); accepts = (modified == bool(filerOptions & Plural) || modified != bool(filerOptions & NonPlural)); } // These are the possible sync options of a row: // * SameInSync: The sync file contains a row with the same msgid and the same msgstr. // * DifferentInSync: The sync file contains a row with the same msgid and different msgstr. // * NotInSync: The sync file does not contain any row with the same msgid. // // The code below takes care of filtering rows when any of those options is not checked. // const int mask = (SameInSync | DifferentInSync | NotInSync); if (accepts && m_mergeCatalog && (filerOptions & mask) && (filerOptions & mask) != mask) { bool isPresent = m_mergeCatalog->isPresent(source_row); bool isDifferent = m_mergeCatalog->isDifferent(source_row); accepts = ! ((isPresent && !isDifferent && !bool(filerOptions & SameInSync)) || (isPresent && isDifferent && !bool(filerOptions & DifferentInSync)) || (!isPresent && !bool(filerOptions & NotInSync)) ); } if (accepts && (filerOptions & STATES) != STATES) { int state = sourceModel()->index(source_row, CatalogTreeModel::State, source_parent).data(Qt::UserRole).toInt(); accepts = (filerOptions & (1 << (state + FIRSTSTATEPOSITION))); } accepts = accepts && !(m_individualRejectFilterEnable && source_row < m_individualRejectFilter.size() && m_individualRejectFilter.at(source_row)); return accepts && QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); } void CatalogTreeFilterModel::setMergeCatalogPointer(MergeCatalog* pointer) { m_mergeCatalog = pointer; }