Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
validator.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include "validator.h"
5#include "translator.h"
6#ifndef LINGUIST_CONSOLE_APPLICATION
7# include "phrase.h"
8#endif
9
10#include <QMap>
11
12QT_USE_NAMESPACE
13
14using namespace Qt::Literals::StringLiterals;
15
16namespace {
17
18static QString leadingWhitespace(const QString &str)
19{
20 int i = 0;
21 for (; i < str.size(); i++) {
22 if (!str[i].isSpace()) {
23 break;
24 }
25 }
26 return str.left(i);
27}
28
29static QString trailingWhitespace(const QString &str)
30{
31 int i = str.size();
32 while (--i >= 0) {
33 if (!str[i].isSpace()) {
34 break;
35 }
36 }
37 return str.mid(i + 1);
38}
39
40static Validator::Ending ending(QString str, QLocale::Language lang)
41{
42 str = str.simplified();
43 if (str.isEmpty())
45
46 switch (str.at(str.size() - 1).unicode()) {
47 case 0x002e: // full stop
48 if (str.endsWith("..."_L1))
50 else
52 case 0x0589: // armenian full stop
53 case 0x06d4: // arabic full stop
54 case 0x3002: // ideographic full stop
56 case 0x0021: // exclamation mark
57 case 0x003f: // question mark
58 case 0x00a1: // inverted exclamation mark
59 case 0x00bf: // inverted question mark
60 case 0x01c3: // latin letter retroflex click
61 case 0x037e: // greek question mark
62 case 0x061f: // arabic question mark
63 case 0x203c: // double exclamation mark
64 case 0x203d: // interrobang
65 case 0x2048: // question exclamation mark
66 case 0x2049: // exclamation question mark
67 case 0x2762: // heavy exclamation mark ornament
68 case 0xff01: // full width exclamation mark
69 case 0xff1f: // full width question mark
71 case 0x003b: // greek 'compatibility' questionmark
72 return lang == QLocale::Greek ? Validator::End_Interrobang : Validator::End_None;
73 case 0x003a: // colon
74 case 0xff1a: // full width colon
76 case 0x2026: // horizontal ellipsis
78 default:
80 }
81}
82
83static bool haveMnemonic(const QString &str)
84{
85 for (const ushort *p = (ushort *)str.constData();;) { // Assume null-termination
86 ushort c = *p++;
87 if (!c)
88 break;
89 if (c == '&') {
90 c = *p++;
91 if (!c)
92 return false;
93 // Matches QKeySequence::mnemonic(), except for
94 // '&#' - most likely the start of an NCR
95 // '& ' - too many false positives
96 if (c != '&' && c != ' ' && c != '#' && QChar(c).isPrint()) {
97 const ushort *pp = p;
98 for (; *p < 256 && isalpha(*p); p++)
99 ;
100 if (pp == p || *p != ';')
101 return true;
102 // This looks like a HTML &entity;, so ignore it. As a HTML string
103 // won't contain accels anyway, we can stop scanning here.
104 break;
105 }
106 }
107 }
108 return false;
109}
110
111static QHash<int, int> countPlaceMarkers(const QString &str)
112{
113 QHash<int, int> counts;
114 const QChar *c = str.unicode();
115 const QChar *cend = c + str.size();
116 while (c < cend) {
117 if (c->unicode() == '%') {
118 const QChar *escape_start = ++c;
119 while (c->isDigit())
120 ++c;
121 const QChar *escape_end = c;
122 bool ok = true;
123 int markerIndex =
124 QString::fromRawData(escape_start, escape_end - escape_start).toInt(&ok);
125 if (ok)
126 counts[markerIndex]++;
127 } else {
128 ++c;
129 }
130 }
131 return counts;
132}
133} // namespace
134
135Validator Validator::fromSource(const QString &source, const Checks &checks,
136 const QLocale::Language &locale,
137 const QHash<QString, QList<Phrase *>> &phrases)
138{
139 Q_UNUSED(phrases)
140 Validator v;
141 if (checks.accelerator)
142 v.m_haveMnemonic.emplace(haveMnemonic(source));
143 if (checks.punctuation)
144 v.m_ending.emplace(ending(source, locale));
145 if (checks.placeMarker)
146 v.m_placeMarkerCounts.emplace(countPlaceMarkers(source));
147 if (checks.surroundingWhiteSpace) {
148 v.m_leadingWhiteSpace.emplace(leadingWhitespace(source));
149 v.m_trailingWhiteSpace.emplace(trailingWhitespace(source));
150 }
151#ifndef LINGUIST_CONSOLE_APPLICATION
152 if (checks.phraseMatch) {
153 v.m_matchingPhraseTargets.emplace();
154 QString fsource = friendlyString(source);
155 QStringList lookupWords = fsource.split(QLatin1Char(' '));
156
157 for (const QString &s : std::as_const(lookupWords))
158 if (auto wordPhrases = phrases.find(s); wordPhrases != phrases.constEnd())
159 for (const Phrase *p : *wordPhrases)
160 if (fsource == friendlyString(p->source()))
161 v.m_matchingPhraseTargets.value()[s].append(friendlyString(p->target()));
162 }
163#endif
164 return v;
165}
166
167QMap<Validator::ErrorType, QString> Validator::validate(QStringList translations,
168 const TranslatorMessage &msg,
169 const QLocale::Language &locale,
170 QList<bool> countRefNeeds)
171{
172 int i = 0;
173 QMap<ErrorType, QString> errors;
174 for (QStringView translation : std::as_const(translations)) {
175 while (!translation.isEmpty()) {
176 auto sep = translation.indexOf(Translator::BinaryVariantSeparator);
177 if (sep < 0)
178 sep = translation.size();
179 const QString trans = translation.first(sep).toString();
180
181 const bool needsRef = msg.isPlural() && countRefNeeds.at(i);
182 errors.insert(validateTranslation(trans, locale, needsRef));
183 translation.slice(std::min(sep + 1, translation.size()));
184 }
185 i++;
186 }
187 return errors;
188}
189
190QMap<Validator::ErrorType, QString> Validator::validateTranslation(const QString &translation,
191 const QLocale::Language &locale,
192 bool needsRef)
193{
194 QMap<ErrorType, QString> errors;
195 if (m_haveMnemonic && *m_haveMnemonic != haveMnemonic(translation))
196 errors.insert(*m_haveMnemonic ? MissingAccelerator : SuperfluousAccelerator, translation);
197 if (m_placeMarkerCounts) {
198 if (*m_placeMarkerCounts != countPlaceMarkers(translation))
199 errors.insert(PlaceMarkersDiffer, translation);
200 if (needsRef && !translation.contains(QLatin1String("%n"))
201 && !translation.contains(QLatin1String("%Ln")))
202 errors.insert(NumerusMarkerMissing, translation);
203 }
204 if (m_ending && *m_ending != ending(translation, locale))
205 errors.insert(PunctuationDiffers, translation);
206
207 if (m_leadingWhiteSpace
208 && (*m_leadingWhiteSpace != leadingWhitespace(translation)
209 || *m_trailingWhiteSpace != trailingWhitespace(translation)))
210 errors.insert(SurroundingWhitespaceDiffers, translation);
211#ifndef LINGUIST_CONSOLE_APPLICATION
212 if (m_matchingPhraseTargets) {
213 const QString ftranslation = friendlyString(translation);
214 for (auto itr = m_matchingPhraseTargets->cbegin(); itr != m_matchingPhraseTargets->cend();
215 itr++) {
216 bool found = false;
217 for (const QString &target : itr.value()) {
218 if (ftranslation.indexOf(target) >= 0) {
219 found = true;
220 break;
221 }
222 }
223 if (!found)
224 errors.insert(IgnoredPhrasebook, itr.key());
225 }
226 }
227#endif
228 return errors;
229}
bool surroundingWhiteSpace
Definition validator.h:33
QMap< ErrorType, QString > validate(QStringList translations, const TranslatorMessage &msg, const QLocale::Language &locale, QList< bool > countRefNeeds)
@ PunctuationDiffers
Definition validator.h:43
@ PlaceMarkersDiffer
Definition validator.h:45
@ NumerusMarkerMissing
Definition validator.h:46
@ IgnoredPhrasebook
Definition validator.h:44
@ SurroundingWhitespaceDiffers
Definition validator.h:42
@ End_Interrobang
Definition validator.h:49
@ End_Ellipsis
Definition validator.h:49
@ End_FullStop
Definition validator.h:49