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