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