19 #include "langselect_p.h"
21 #include <Cutelyst/Application>
22 #include <Cutelyst/Context>
23 #include <Cutelyst/Plugins/Session/Session>
24 #include <Cutelyst/Response>
28 #include <QLoggingCategory>
30 #include <QNetworkCookie>
36 Q_LOGGING_CATEGORY(C_LANGSELECT,
"cutelyst.plugin.langselect", QtWarningMsg)
42 #define SELECTION_TRIED QStringLiteral("_c_langselect_tried")
45 , d_ptr(new LangSelectPrivate)
53 , d_ptr(new LangSelectPrivate)
57 d->autoDetect =
false;
69 if (d->fallbackLocale.language() == QLocale::C) {
70 qCCritical(C_LANGSELECT,
"We need a valid fallback locale.");
75 if (d->source ==
URLQuery && d->queryKey.isEmpty()) {
76 qCCritical(C_LANGSELECT,
"Can not use url query as source with empty key name.");
78 }
else if (d->source ==
Session && d->sessionKey.isEmpty()) {
79 qCCritical(C_LANGSELECT,
"Can not use session as source with empty key name.");
81 }
else if (d->source ==
Cookie && d->cookieName.isEmpty()) {
82 qCCritical(C_LANGSELECT,
"Can not use cookie as source with empty cookie name.");
86 qCCritical(C_LANGSELECT,
"Invalid source.");
90 d->beforePrepareAction(c, skipMethod);
93 if (!d->locales.contains(d->fallbackLocale)) {
94 d->locales.append(d->fallbackLocale);
98 qCDebug(C_LANGSELECT) <<
"Initialized LangSelect plugin with the following settings:";
99 qCDebug(C_LANGSELECT) <<
"Supported locales:" << d->locales;
100 qCDebug(C_LANGSELECT) <<
"Fallback locale:" << d->fallbackLocale;
101 qCDebug(C_LANGSELECT) <<
"Auto detection source:" << d->source;
102 qCDebug(C_LANGSELECT) <<
"Detect from header:" << d->detectFromHeader;
111 d->locales.reserve(locales.size());
112 for (
const QLocale &l : locales) {
113 if (Q_LIKELY(l.language() != QLocale::C)) {
114 d->locales.push_back(l);
116 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << l <<
"to the list of suppored locales.";
125 d->locales.reserve(locales.size());
126 for (
const QString &l : locales) {
128 if (Q_LIKELY(locale.language() != QLocale::C)) {
129 d->locales.push_back(locale);
131 qCWarning(C_LANGSELECT,
"Can not add invalid locale \"%s\" to the list of supported locales.", qUtf8Printable(l));
138 if (Q_LIKELY(locale.language() != QLocale::C)) {
140 d->locales.push_back(locale);
142 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << locale <<
"to the list of supported locales.";
149 if (Q_LIKELY(l.language() != QLocale::C)) {
151 d->locales.push_back(l);
153 qCWarning(C_LANGSELECT,
"Can not add invalid locale \"%s\" to the list of supported locales.", qUtf8Printable(locale));
161 if (Q_LIKELY(!path.isEmpty() && !name.isEmpty())) {
162 const QDir dir(path);
163 if (Q_LIKELY(dir.exists())) {
164 const auto _pref = prefix.isEmpty() ? QStringLiteral(
".") : prefix;
165 const auto _suff = suffix.isEmpty() ? QStringLiteral(
".qm") : suffix;
166 const QString filter = name + _pref + QLatin1Char(
'*') + _suff;
167 const auto files = dir.entryInfoList({name}, QDir::Files);
168 if (Q_LIKELY(!files.empty())) {
169 d->locales.reserve(files.size());
170 bool shrinkToFit =
false;
171 for (
const QFileInfo &fi : files) {
172 const auto fn = fi.fileName();
173 const auto prefIdx = fn.indexOf(_pref);
174 const auto locPart = fn.mid(prefIdx + _pref.length(), fn.length() - prefIdx - _suff.length() - _pref.length());
176 if (Q_LIKELY(l.language() != QLocale::C)) {
177 d->locales.push_back(l);
178 qCDebug(C_LANGSELECT,
"Added locale \"%s\" to the list of supported locales.", qUtf8Printable(locPart));
181 qCWarning(C_LANGSELECT,
"Can not add invalid locale \"%s\" to the list of supported locales.", qUtf8Printable(locPart));
185 d->locales.squeeze();
188 qCWarning(C_LANGSELECT,
"Can not find translation files for \"%s\" in \"%s\".", qUtf8Printable(filter), qUtf8Printable(path));
191 qCWarning(C_LANGSELECT,
"Can not set locales from not existing directory \"%s\".", qUtf8Printable(path));
194 qCWarning(C_LANGSELECT,
"Can not set locales from dir with emtpy path or name.");
202 if (Q_LIKELY(!path.isEmpty() && !name.isEmpty())) {
203 const QDir dir(path);
204 if (Q_LIKELY(dir.exists())) {
205 const auto dirs = dir.entryList(QDir::AllDirs);
206 if (Q_LIKELY(!dirs.empty())) {
207 d->locales.reserve(dirs.size());
208 bool shrinkToFit =
false;
209 for (
const QString &subDir : dirs) {
210 const QString relFn = subDir + QLatin1Char(
'/') + name;
211 if (dir.exists(relFn)) {
213 if (Q_LIKELY(l.language() != QLocale::C)) {
214 d->locales.push_back(l);
215 qCDebug(C_LANGSELECT,
"Added locale \"%s\" to the list of supported locales.", qUtf8Printable(subDir));
218 qCWarning(C_LANGSELECT,
"Can not add invalid locale \"%s\" to the list of supported locales.", qUtf8Printable(subDir));
225 d->locales.squeeze();
229 qCWarning(C_LANGSELECT,
"Can not set locales from not existing directory \"%s\".", qUtf8Printable(path));
232 qCWarning(C_LANGSELECT,
"Can not set locales from dirs with empty path or names.");
257 d->cookieName = name;
263 d->subDomainMap.clear();
265 d->locales.reserve(map.size());
266 auto i = map.constBegin();
267 while (i != map.constEnd()) {
268 if (i.value().language() != QLocale::C) {
269 d->subDomainMap.insert(i.key(), i.value());
270 d->locales.append(i.value());
272 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << i.value() <<
"for subdomain" << i.key() <<
"to the subdomain map.";
276 d->locales.squeeze();
282 d->domainMap.clear();
284 d->locales.reserve(map.size());
285 auto i = map.constBegin();
286 while (i != map.constEnd()) {
287 if (Q_LIKELY(i.value().language() != QLocale::C)) {
288 d->domainMap.insert(i.key(), i.value());
289 d->locales.append(i.value());
291 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << i.value() <<
"for domain" << i.key() <<
"to the domain map.";
295 d->locales.squeeze();
301 d->fallbackLocale = fallback;
307 d->detectFromHeader = enabled;
313 if (Q_LIKELY(!key.isEmpty())) {
314 d->langStashKey = key;
316 qCWarning(C_LANGSELECT) <<
"Can not set an empty key name for the language code stash key. Using current key name" << d->langStashKey;
323 if (Q_LIKELY(!key.isEmpty())) {
324 d->dirStashKey = key;
326 qCWarning(C_LANGSELECT) <<
"Can not set an empty key name for the language direction stash key. Using current key name" << d->dirStashKey;
333 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
334 return QVector<QLocale>();
343 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
347 const auto d = lsp->d_ptr;
348 const auto _key = !key.isEmpty() ? key : d->queryKey;
349 if (!d->getFromQuery(c, _key)) {
350 if (!d->getFromHeader(c)) {
353 d->setToQuery(c, _key);
357 d->setContentLanguage(c);
364 bool foundInSession =
false;
367 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
368 return foundInSession;
371 const auto d = lsp->d_ptr;
372 const auto _key = !key.isEmpty() ? key : d->sessionKey;
373 foundInSession = d->getFromSession(c, _key);
374 if (!foundInSession) {
375 if (!d->getFromHeader(c)) {
378 d->setToSession(c, _key);
380 d->setContentLanguage(c);
382 return foundInSession;
387 bool foundInCookie =
false;
390 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
391 return foundInCookie;
394 const auto d = lsp->d_ptr;
395 const auto _name = !name.isEmpty() ? name : d->cookieName;
396 foundInCookie = d->getFromCookie(c, _name);
397 if (!foundInCookie) {
398 if (!d->getFromHeader(c)) {
401 d->setToCookie(c, _name);
403 d->setContentLanguage(c);
405 return foundInCookie;
410 bool foundInSubDomain =
false;
413 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
414 return foundInSubDomain;
417 const auto d = lsp->d_ptr;
418 const auto _map = !subDomainMap.empty() ? subDomainMap : d->subDomainMap;
419 foundInSubDomain = d->getFromSubdomain(c, _map);
420 if (!foundInSubDomain) {
421 if (!d->getFromHeader(c)) {
426 d->setContentLanguage(c);
428 return foundInSubDomain;
433 bool foundInDomain =
false;
436 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
437 return foundInDomain;
440 const auto d = lsp->d_ptr;
441 const auto _map = !domainMap.empty() ? domainMap : d->domainMap;
442 foundInDomain = d->getFromDomain(c, _map);
443 if (!foundInDomain) {
444 if (!d->getFromHeader(c)) {
449 d->setContentLanguage(c);
451 return foundInDomain;
457 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
461 const auto d = lsp->d_ptr;
462 const QLocale l(locale);
463 if (l.language() != QLocale::C && d->locales.contains(l)) {
464 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in path";
466 d->setContentLanguage(c);
469 if (!d->getFromHeader(c)) {
472 auto uri = c->req()->uri();
473 auto pathParts = uri.path().split(QLatin1Char(
'/'));
474 const auto localeIdx = pathParts.indexOf(locale);
475 pathParts[localeIdx] = c->
locale().bcp47Name().toLower();
476 uri.setPath(pathParts.join(QLatin1Char(
'/')));
477 qCDebug(C_LANGSELECT) <<
"Storing selected locale by redirecting to" << uri;
486 bool redirect =
false;
491 if (getFromSession(c, sessionKey)) {
495 if (getFromCookie(c, cookieName)) {
499 if (getFromQuery(c, queryKey)) {
503 if (getFromSubdomain(c, subDomainMap)) {
507 if (getFromDomain(c, domainMap)) {
523 if (foundIn != _source) {
525 setToSession(c, sessionKey);
527 setToCookie(c, cookieName);
529 setToQuery(c, queryKey);
538 setContentLanguage(c);
544 bool LangSelectPrivate::getFromQuery(
Context *c,
const QString &key)
const
547 if (l.language() != QLocale::C && locales.contains(l)) {
548 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in url query key" << key;
552 qCDebug(C_LANGSELECT) <<
"Can not find supported locale in url query key" << key;
557 bool LangSelectPrivate::getFromCookie(
Context *c,
const QString &cookie)
const
559 const QLocale l(c->req()->
cookie(cookie));
560 if (l.language() != QLocale::C && locales.contains(l)) {
561 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in cookie name" << cookie;
565 qCDebug(C_LANGSELECT) <<
"Can no find supported locale in cookie value with name" << cookie;
570 bool LangSelectPrivate::getFromSession(
Context *c,
const QString &key)
const
573 if (l.language() != QLocale::C && locales.contains(l)) {
574 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in session key" << key;
578 qCDebug(C_LANGSELECT) <<
"Can not find supported locale in session value with key" << key;
583 bool LangSelectPrivate::getFromSubdomain(
Context *c,
const QMap<QString, QLocale> &map)
const
585 const auto domain = c->req()->uri().host();
586 auto i = map.constBegin();
587 while (i != map.constEnd()) {
588 if (domain.startsWith(i.key())) {
589 qCDebug(C_LANGSELECT) <<
"Found valid locale" << i.value() <<
"in subdomain map for domain" << domain;
595 const auto domainParts = domain.split(QLatin1Char(
'.'), QString::SkipEmptyParts);
596 if (domainParts.size() > 2) {
597 const QLocale l(domainParts.at(0));
598 if (l.language() != QLocale::C && locales.contains(l)) {
599 qCDebug(C_LANGSELECT) <<
"Found supported locale" << l <<
"in subdomain of domain" << domain;
604 qCDebug(C_LANGSELECT) <<
"Can not find supported locale for subdomain" << domain;
608 bool LangSelectPrivate::getFromDomain(
Context *c,
const QMap<QString, QLocale> &map)
const
610 const auto domain = c->req()->uri().host();
611 auto i = map.constBegin();
612 while (i != map.constEnd()) {
613 if (domain.endsWith(i.key())) {
614 qCDebug(C_LANGSELECT) <<
"Found valid locale" << i.value() <<
"in domain map for domain" << domain;
620 const auto domainParts = domain.split(QLatin1Char(
'.'), QString::SkipEmptyParts);
621 if (domainParts.size() > 1) {
622 const QLocale l(domainParts.at(domainParts.size() - 1));
623 if (l.language() != QLocale::C && locales.contains(l)) {
624 qCDebug(C_LANGSELECT) <<
"Found supported locale" << l <<
"in domain" << domain;
629 qCDebug(C_LANGSELECT) <<
"Can not find supported locale for domain" << domain;
633 bool LangSelectPrivate::getFromHeader(
Context *c,
const QString &name)
const
635 if (detectFromHeader) {
636 const auto accpetedLangs = c->req()->
header(name).split(QLatin1Char(
','), QString::SkipEmptyParts);
637 if (Q_LIKELY(!accpetedLangs.empty())) {
638 std::map<float,QLocale> langMap;
639 for (
const QString &al : accpetedLangs) {
640 const auto idx = al.indexOf(QLatin1Char(
';'));
641 float priority = 1.0f;
645 langPart = al.left(idx);
646 const QStringRef ref = al.midRef(idx + 1);
647 priority = ref.mid(ref.indexOf(QLatin1Char(
'=')) +1).toFloat(&ok);
651 QLocale locale(langPart);
652 if (ok && locale.language() != QLocale::C) {
653 const auto search = langMap.find(priority);
654 if (search == langMap.cend()) {
655 langMap.insert({priority, locale});
659 if (!langMap.empty()) {
660 auto i = langMap.crbegin();
661 while (i != langMap.crend()) {
662 if (locales.contains(i->second)) {
664 qCDebug(C_LANGSELECT) <<
"Selected locale" << c->
locale() <<
"from" << name <<
"header";
671 i = langMap.crbegin();
672 const auto constLocales = locales;
673 while (i != langMap.crend()) {
674 for (
const QLocale &l : constLocales) {
675 if (l.language() == i->second.language()) {
677 qCDebug(C_LANGSELECT) <<
"Selected locale" << c->
locale() <<
"from" << name <<
"header";
690 void LangSelectPrivate::setToQuery(
Context *c,
const QString &key)
const
692 auto uri = c->req()->uri();
693 QUrlQuery query(uri);
694 if (query.hasQueryItem(key)) {
695 query.removeQueryItem(key);
697 query.addQueryItem(key, c->
locale().bcp47Name().toLower());
699 qCDebug(C_LANGSELECT) <<
"Storing selected locale in URL query by redirecting to" << uri;
703 void LangSelectPrivate::setToCookie(
Context *c,
const QString &name)
const
705 qCDebug(C_LANGSELECT) <<
"Storing selected locale in cookie with name" << name;
706 c->
res()->
setCookie(QNetworkCookie(name.toLatin1(), c->
locale().bcp47Name().toLatin1()));
709 void LangSelectPrivate::setToSession(
Context *c,
const QString &key)
const
711 qCDebug(C_LANGSELECT) <<
"Storing selected locale in session key" << key;
715 void LangSelectPrivate::setFallback(
Context *c)
const
717 qCDebug(C_LANGSELECT) <<
"Can not find fitting locale, using fallback locale" << fallbackLocale;
721 void LangSelectPrivate::setContentLanguage(
Context *c)
const
723 if (addContentLanguageHeader) {
727 {langStashKey, c->
locale().bcp47Name()},
728 {dirStashKey, (c->
locale().textDirection() == Qt::LeftToRight ? QStringLiteral(
"ltr") : QStringLiteral(
"rtl"))}
732 void LangSelectPrivate::beforePrepareAction(
Context *c,
bool *skipMethod)
const
738 if (!c->
stash(SELECTION_TRIED).isNull()) {
742 detectLocale(c, source, skipMethod);
747 void LangSelectPrivate::_q_postFork(
Application *app)
752 #include "moc_langselect.cpp"