1 /*
2   This file is part of Lokalize
3 
4   SPDX-FileCopyrightText: 2007-2014 Nick Shaforostoff <shafff@ukr.net>
5   SPDX-FileCopyrightText: 2018-2019 Simon Depiets <sdepiets@gmail.com>
6 
7   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
8 */
9 
10 #include "mergecatalog.h"
11 
12 #include "lokalize_debug.h"
13 
14 #include "catalog_private.h"
15 #include "catalogstorage.h"
16 #include "cmd.h"
17 #include <klocalizedstring.h>
18 #include <QMultiHash>
19 #include <QtAlgorithms>
20 
21 
MergeCatalog(QObject * parent,Catalog * baseCatalog,bool saveChanges)22 MergeCatalog::MergeCatalog(QObject* parent, Catalog* baseCatalog, bool saveChanges)
23     : Catalog(parent)
24     , m_baseCatalog(baseCatalog)
25     , m_unmatchedCount(0)
26     , m_modified(false)
27 {
28     setActivePhase(baseCatalog->activePhase(), baseCatalog->activePhaseRole());
29     if (saveChanges) {
30         connect(baseCatalog, &Catalog::signalEntryModified, this, &MergeCatalog::copyFromBaseCatalogIfInDiffIndex);
31         connect(baseCatalog, QOverload<>::of(&Catalog::signalFileSaved), this, &MergeCatalog::save);
32     }
33 }
34 
copyFromBaseCatalog(const DocPosition & pos,int options)35 void MergeCatalog::copyFromBaseCatalog(const DocPosition& pos, int options)
36 {
37     bool a = m_mergeDiffIndex.contains(pos.entry);
38     if (options & EvenIfNotInDiffIndex || !a) {
39         //sync changes
40         DocPosition ourPos = pos;
41         if ((ourPos.entry = m_map.at(ourPos.entry)) == -1)
42             return;
43 
44         //note the explicit use of map...
45         if (m_storage->isApproved(ourPos) != m_baseCatalog->isApproved(pos))
46             //qCWarning(LOKALIZE_LOG)<<ourPos.entry<<"MISMATCH";
47             m_storage->setApproved(ourPos, m_baseCatalog->isApproved(pos));
48         DocPos p(pos);
49         if (!m_originalHashes.contains(p))
50             m_originalHashes[p] = qHash(m_storage->target(ourPos));
51         m_storage->setTarget(ourPos, m_baseCatalog->target(pos));
52         setModified(ourPos, true);
53 
54         if (options & EvenIfNotInDiffIndex && a)
55             m_mergeDiffIndex.removeAll(pos.entry);
56 
57         m_modified = true;
58         Q_EMIT signalEntryModified(pos);
59     }
60 }
61 
msgstr(const DocPosition & pos) const62 QString MergeCatalog::msgstr(const DocPosition& pos) const
63 {
64     DocPosition us = pos;
65     us.entry = m_map.at(pos.entry);
66 
67     return (us.entry == -1) ? QString() : Catalog::msgstr(us);
68 }
69 
isApproved(uint index) const70 bool MergeCatalog::isApproved(uint index) const
71 {
72     return (m_map.at(index) == -1) ? false : Catalog::isApproved(m_map.at(index));
73 }
74 
state(const DocPosition & pos) const75 TargetState MergeCatalog::state(const DocPosition& pos) const
76 {
77     DocPosition us = pos;
78     us.entry = m_map.at(pos.entry);
79 
80     return (us.entry == -1) ? New : Catalog::state(us);
81 }
82 
83 
isPlural(uint index) const84 bool MergeCatalog::isPlural(uint index) const
85 {
86     //sanity
87     if (m_map.at(index) == -1) {
88         qCWarning(LOKALIZE_LOG) << "!!! index" << index << "m_map.at(index)" << m_map.at(index) << "numberOfEntries()" << numberOfEntries();
89         return false;
90     }
91 
92     return Catalog::isPlural(m_map.at(index));
93 }
94 
isPresent(const int & entry) const95 bool MergeCatalog::isPresent(const int& entry) const
96 {
97     return m_map.at(entry) != -1;
98 }
99 
calcMatchItem(const DocPosition & basePos,const DocPosition & mergePos)100 MatchItem MergeCatalog::calcMatchItem(const DocPosition& basePos, const DocPosition& mergePos)
101 {
102     CatalogStorage& baseStorage = *(m_baseCatalog->m_storage);
103     CatalogStorage& mergeStorage = *(m_storage);
104 
105     MatchItem item(mergePos.entry, basePos.entry, true);
106     //TODO make more robust, perhaps after XLIFF?
107     QStringList baseMatchData = baseStorage.matchData(basePos);
108     QStringList mergeMatchData = mergeStorage.matchData(mergePos);
109 
110     //compare ids
111     item.score += 40 * ((baseMatchData.isEmpty() && mergeMatchData.isEmpty()) ? baseStorage.id(basePos) == mergeStorage.id(mergePos)
112                         : baseMatchData == mergeMatchData);
113 
114     //TODO look also for changed/new <note>s
115 
116     //translation isn't changed
117     if (baseStorage.targetAllForms(basePos, true) == mergeStorage.targetAllForms(mergePos, true)) {
118         item.translationIsDifferent = baseStorage.isApproved(basePos) != mergeStorage.isApproved(mergePos);
119         item.score += 29 + 1 * item.translationIsDifferent;
120     }
121 #if 0
122     if (baseStorage.source(basePos) == "%1 (%2)") {
123         qCDebug(LOKALIZE_LOG) << "BASE";
124         qCDebug(LOKALIZE_LOG) << m_baseCatalog->url();
125         qCDebug(LOKALIZE_LOG) << basePos.entry;
126         qCDebug(LOKALIZE_LOG) << baseStorage.source(basePos);
127         qCDebug(LOKALIZE_LOG) << baseMatchData.first();
128         qCDebug(LOKALIZE_LOG) << "MERGE";
129         qCDebug(LOKALIZE_LOG) << url();
130         qCDebug(LOKALIZE_LOG) << mergePos.entry;
131         qCDebug(LOKALIZE_LOG) << mergeStorage.source(mergePos);
132         qCDebug(LOKALIZE_LOG) << mergeStorage.matchData(mergePos).first();
133         qCDebug(LOKALIZE_LOG) << item.score;
134         qCDebug(LOKALIZE_LOG) << "";
135     }
136 #endif
137     return item;
138 }
139 
strip(QString source)140 static QString strip(QString source)
141 {
142     source.remove('\n');
143     return source;
144 }
145 
loadFromUrl(const QString & filePath)146 int MergeCatalog::loadFromUrl(const QString& filePath)
147 {
148     int errorLine = Catalog::loadFromUrl(filePath);
149     if (Q_UNLIKELY(errorLine != 0))
150         return errorLine;
151 
152     //now calc the entry mapping
153 
154     CatalogStorage& baseStorage = *(m_baseCatalog->m_storage);
155     CatalogStorage& mergeStorage = *(m_storage);
156 
157     DocPosition i(0);
158     int size = baseStorage.size();
159     int mergeSize = mergeStorage.size();
160     m_map.fill(-1, size);
161     QMultiMap<int, int> backMap; //will be used to maintain one-to-one relation
162 
163 
164     //precalc for fast lookup
165     QMultiHash<QString, int> mergeMap;
166     while (i.entry < mergeSize) {
167         mergeMap.insert(strip(mergeStorage.source(i)), i.entry);
168         ++(i.entry);
169     }
170 
171     i.entry = 0;
172     while (i.entry < size) {
173         QString key = strip(baseStorage.source(i));
174         const QList<int>& entries = mergeMap.values(key);
175         QList<MatchItem> scores;
176 
177         int k = entries.size();
178         if (k) {
179             while (--k >= 0)
180                 scores << calcMatchItem(i, DocPosition(entries.at(k)));
181 
182             std::sort(scores.begin(), scores.end(), std::greater<MatchItem>());
183 
184             m_map[i.entry] = scores.first().mergeEntry;
185             backMap.insert(scores.first().mergeEntry, i.entry);
186 
187             if (scores.first().translationIsDifferent)
188                 m_mergeDiffIndex.append(i.entry);
189 
190         }
191         ++(i.entry);
192     }
193 
194 
195     //maintain one-to-one relation
196     const QList<int>& mergePositions = backMap.uniqueKeys();
197     for (int mergePosition : mergePositions) {
198         const QList<int>& basePositions = backMap.values(mergePosition);
199         if (basePositions.size() == 1)
200             continue;
201 
202         //qCDebug(LOKALIZE_LOG)<<"kv"<<mergePosition<<basePositions;
203         QList<MatchItem> scores;
204         for (int value : basePositions)
205             scores << calcMatchItem(DocPosition(value), mergePosition);
206 
207         std::sort(scores.begin(), scores.end(), std::greater<MatchItem>());
208         int i = scores.size();
209         while (--i > 0) {
210             //qCDebug(LOKALIZE_LOG)<<"erasing"<<scores.at(i).baseEntry<<m_map[scores.at(i).baseEntry]<<",m_map["<<scores.at(i).baseEntry<<"]=-1";
211             m_map[scores.at(i).baseEntry] = -1;
212         }
213     }
214 
215     //fuzzy match unmatched entries?
216     /*    QMultiHash<QString, int>::iterator it = mergeMap.begin();
217         while (it != mergeMap.end())
218         {
219             //qCWarning(LOKALIZE_LOG)<<it.value()<<it.key();
220             ++it;
221         }*/
222     m_unmatchedCount = numberOfEntries() - mergePositions.count();
223     m_modified = false;
224     m_originalHashes.clear();
225 
226     return 0;
227 }
228 
isModified(DocPos pos) const229 bool MergeCatalog::isModified(DocPos pos) const
230 {
231     return Catalog::isModified(pos) && m_originalHashes.value(pos) != qHash(target(pos.toDocPosition()));
232 }
233 
save()234 bool MergeCatalog::save()
235 {
236     bool ok = !m_modified || Catalog::save();
237     if (ok) m_modified = false;
238     m_originalHashes.clear();
239     return ok;
240 }
241 
copyToBaseCatalog(DocPosition & pos)242 void MergeCatalog::copyToBaseCatalog(DocPosition& pos)
243 {
244     bool changeContents = m_baseCatalog->msgstr(pos) != msgstr(pos);
245 
246     m_baseCatalog->beginMacro(i18nc("@item Undo action item", "Accept change in translation"));
247 
248     if (m_baseCatalog->state(pos) != state(pos))
249         SetStateCmd::instantiateAndPush(m_baseCatalog, pos, state(pos));
250 
251     if (changeContents) {
252         pos.offset = 0;
253         if (!m_baseCatalog->msgstr(pos).isEmpty())
254             m_baseCatalog->push(new DelTextCmd(m_baseCatalog, pos, m_baseCatalog->msgstr(pos)));
255 
256         m_baseCatalog->push(new InsTextCmd(m_baseCatalog, pos, msgstr(pos)));
257     }
258     ////////this is NOT done automatically by BaseCatalogEntryChanged slot
259     bool remove = true;
260     if (isPlural(pos.entry)) {
261         DocPosition p = pos;
262         p.form = qMin(m_baseCatalog->numberOfPluralForms(), numberOfPluralForms()); //just sanity check
263         p.form = qMax((int)p.form, 1); //just sanity check
264         while ((--(p.form)) >= 0 && remove)
265             remove = m_baseCatalog->msgstr(p) == msgstr(p);
266     }
267     if (remove)
268         removeFromDiffIndex(pos.entry);
269 
270     m_baseCatalog->endMacro();
271 }
272 
copyToBaseCatalog(int options)273 void MergeCatalog::copyToBaseCatalog(int options)
274 {
275     DocPosition pos;
276     pos.offset = 0;
277     bool insHappened = false;
278     const QLinkedList<int> changed = differentEntries();
279     for (int entry : changed) {
280         pos.entry = entry;
281         if (options & EmptyOnly && !m_baseCatalog->isEmpty(entry))
282             continue;
283         if (options & HigherOnly && !m_baseCatalog->isEmpty(entry) && m_baseCatalog->state(pos) >= state(pos))
284             continue;
285 
286         int formsCount = (m_baseCatalog->isPlural(entry)) ? m_baseCatalog->numberOfPluralForms() : 1;
287         pos.form = 0;
288         while (pos.form < formsCount) {
289             //m_baseCatalog->push(new DelTextCmd(m_baseCatalog,pos,m_baseCatalog->msgstr(pos.entry,0))); ?
290             //some forms may still contain translation...
291             if (!(options & EmptyOnly && !m_baseCatalog->isEmpty(pos)) /*&&
292                 !(options&HigherOnly && !m_baseCatalog->isEmpty(pos) && m_baseCatalog->state(pos)>=state(pos))*/) {
293                 if (!insHappened) {
294                     //stop basecatalog from sending signalEntryModified to us
295                     //when we are the ones who does the modification
296                     disconnect(m_baseCatalog, &Catalog::signalEntryModified, this, &MergeCatalog::copyFromBaseCatalogIfInDiffIndex);
297                     insHappened = true;
298                     m_baseCatalog->beginMacro(i18nc("@item Undo action item", "Accept all new translations"));
299                 }
300 
301                 copyToBaseCatalog(pos);
302                 /// ///
303                 /// m_baseCatalog->push(new InsTextCmd(m_baseCatalog,pos,mergeCatalog.msgstr(pos)));
304                 /// ///
305             }
306             ++(pos.form);
307         }
308         /// ///
309         /// removeFromDiffIndex(m_pos.entry);
310         /// ///
311     }
312 
313     if (insHappened) {
314         m_baseCatalog->endMacro();
315         //reconnect to catch all modifications coming from outside
316         connect(m_baseCatalog, &Catalog::signalEntryModified, this, &MergeCatalog::copyFromBaseCatalogIfInDiffIndex);
317     }
318 }
319 
320