6#include "langselect_p.h"
8#include <Cutelyst/Application>
9#include <Cutelyst/Context>
10#include <Cutelyst/Plugins/Session/Session>
11#include <Cutelyst/Response>
17#include <QLoggingCategory>
18#include <QNetworkCookie>
22Q_LOGGING_CATEGORY(C_LANGSELECT,
"cutelyst.plugin.langselect", QtWarningMsg)
28#define SELECTION_TRIED QStringLiteral("_c_langselect_tried")
32 , d_ptr(new LangSelectPrivate)
41 , d_ptr(new LangSelectPrivate)
45 d->autoDetect =
false;
56 if (d->fallbackLocale.language() ==
QLocale::C) {
57 qCCritical(C_LANGSELECT,
"We need a valid fallback locale.");
62 if (d->source ==
URLQuery && d->queryKey.isEmpty()) {
63 qCCritical(C_LANGSELECT,
"Can not use url query as source with empty key name.");
65 }
else if (d->source ==
Session && d->sessionKey.isEmpty()) {
66 qCCritical(C_LANGSELECT,
"Can not use session as source with empty key name.");
68 }
else if (d->source ==
Cookie && d->cookieName.isEmpty()) {
69 qCCritical(C_LANGSELECT,
"Can not use cookie as source with empty cookie name.");
73 qCCritical(C_LANGSELECT,
"Invalid source.");
77 d->beforePrepareAction(c, skipMethod);
80 if (!d->locales.contains(d->fallbackLocale)) {
81 d->locales.append(d->fallbackLocale);
85 qCDebug(C_LANGSELECT) <<
"Initialized LangSelect plugin with the following settings:";
86 qCDebug(C_LANGSELECT) <<
"Supported locales:" << d->locales;
87 qCDebug(C_LANGSELECT) <<
"Fallback locale:" << d->fallbackLocale;
88 qCDebug(C_LANGSELECT) <<
"Auto detection source:" << d->source;
89 qCDebug(C_LANGSELECT) <<
"Detect from header:" << d->detectFromHeader;
98 d->locales.reserve(locales.
size());
99 for (
const QLocale &l : locales) {
101 d->locales.push_back(l);
103 qCWarning(C_LANGSELECT)
104 <<
"Can not add invalid locale" << l <<
"to the list of supported locales.";
113 d->locales.reserve(locales.
size());
114 for (
const QString &l : locales) {
117 d->locales.push_back(locale);
119 qCWarning(C_LANGSELECT,
120 "Can not add invalid locale \"%s\" to the list of supported locales.",
130 d->locales.push_back(locale);
132 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << locale
133 <<
"to the list of supported locales.";
142 d->locales.push_back(l);
144 qCWarning(C_LANGSELECT,
145 "Can not add invalid locale \"%s\" to the list of supported locales.",
146 qUtf8Printable(locale));
158 const QDir dir(path);
159 if (Q_LIKELY(dir.
exists())) {
160 const auto _pref = prefix.
isEmpty() ? QStringLiteral(
".") : prefix;
161 const auto _suff = suffix.
isEmpty() ? QStringLiteral(
".qm") : suffix;
162 const QString filter = name + _pref + u
'*' + _suff;
164 if (Q_LIKELY(!files.empty())) {
165 d->locales.reserve(files.size());
166 bool shrinkToFit =
false;
168 const auto fn = fi.fileName();
169 const auto prefIdx = fn.indexOf(_pref);
171 fn.mid(prefIdx + _pref.length(),
172 fn.length() - prefIdx - _suff.length() - _pref.length());
175 d->locales.push_back(l);
176 qCDebug(C_LANGSELECT,
177 "Added locale \"%s\" to the list of supported locales.",
178 qUtf8Printable(locPart));
183 "Can not add invalid locale \"%s\" to the list of supported locales.",
184 qUtf8Printable(locPart));
188 d->locales.squeeze();
191 qCWarning(C_LANGSELECT,
192 "Can not find translation files for \"%s\" in \"%s\".",
193 qUtf8Printable(filter),
194 qUtf8Printable(path));
197 qCWarning(C_LANGSELECT,
198 "Can not set locales from not existing directory \"%s\".",
199 qUtf8Printable(path));
202 qCWarning(C_LANGSELECT,
"Can not set locales from dir with empty path or name.");
211 const QDir dir(path);
212 if (Q_LIKELY(dir.
exists())) {
214 if (Q_LIKELY(!dirs.empty())) {
215 d->locales.reserve(dirs.size());
216 bool shrinkToFit =
false;
217 for (
const QString &subDir : dirs) {
218 const QString relFn = subDir + u
'/' + name;
222 d->locales.push_back(l);
223 qCDebug(C_LANGSELECT,
224 "Added locale \"%s\" to the list of supported locales.",
225 qUtf8Printable(subDir));
228 qCWarning(C_LANGSELECT,
229 "Can not add invalid locale \"%s\" to the list of supported "
231 qUtf8Printable(subDir));
238 d->locales.squeeze();
242 qCWarning(C_LANGSELECT,
243 "Can not set locales from not existing directory \"%s\".",
244 qUtf8Printable(path));
247 qCWarning(C_LANGSELECT,
"Can not set locales from dirs with empty path or names.");
272 d->cookieName = name;
278 d->subDomainMap.clear();
280 d->locales.reserve(map.
size());
284 d->subDomainMap.insert(i.key(), i.value());
285 d->locales.append(i.value());
287 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << i.value() <<
"for subdomain"
288 << i.key() <<
"to the subdomain map.";
292 d->locales.squeeze();
298 d->domainMap.clear();
300 d->locales.reserve(map.
size());
303 if (Q_LIKELY(i.value().language() !=
QLocale::C)) {
304 d->domainMap.insert(i.key(), i.value());
305 d->locales.append(i.value());
307 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << i.value() <<
"for domain"
308 << i.key() <<
"to the domain map.";
312 d->locales.squeeze();
318 d->fallbackLocale = fallback;
324 d->detectFromHeader = enabled;
330 if (Q_LIKELY(!key.
isEmpty())) {
331 d->langStashKey = key;
333 qCWarning(C_LANGSELECT) <<
"Can not set an empty key name for the language code stash key. "
334 "Using current key name"
342 if (Q_LIKELY(!key.
isEmpty())) {
343 d->dirStashKey = key;
345 qCWarning(C_LANGSELECT) <<
"Can not set an empty key name for the language direction stash "
346 "key. Using current key name"
354 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
358 return lsp->supportedLocales();
364 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
368 const auto d = lsp->d_ptr;
369 const auto _key = !key.
isEmpty() ? key : d->queryKey;
370 if (!d->getFromQuery(c, _key)) {
371 if (!d->getFromHeader(c)) {
374 d->setToQuery(c, _key);
378 d->setContentLanguage(c);
385 bool foundInSession =
false;
388 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
389 return foundInSession;
392 const auto d = lsp->d_ptr;
393 const auto _key = !key.
isEmpty() ? key : d->sessionKey;
394 foundInSession = d->getFromSession(c, _key);
395 if (!foundInSession) {
396 if (!d->getFromHeader(c)) {
399 d->setToSession(c, _key);
401 d->setContentLanguage(c);
403 return foundInSession;
408 bool foundInCookie =
false;
411 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
412 return foundInCookie;
415 const auto d = lsp->d_ptr;
416 const auto _name = !name.
isEmpty() ? name : d->cookieName;
417 foundInCookie = d->getFromCookie(c, _name);
418 if (!foundInCookie) {
419 if (!d->getFromHeader(c)) {
422 d->setToCookie(c, _name);
424 d->setContentLanguage(c);
426 return foundInCookie;
431 bool foundInSubDomain =
false;
434 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
435 return foundInSubDomain;
438 const auto d = lsp->d_ptr;
439 const auto _map = !subDomainMap.
empty() ? subDomainMap : d->subDomainMap;
440 foundInSubDomain = d->getFromSubdomain(c, _map);
441 if (!foundInSubDomain) {
442 if (!d->getFromHeader(c)) {
447 d->setContentLanguage(c);
449 return foundInSubDomain;
454 bool foundInDomain =
false;
457 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
458 return foundInDomain;
461 const auto d = lsp->d_ptr;
462 const auto _map = !domainMap.
empty() ? domainMap : d->domainMap;
463 foundInDomain = d->getFromDomain(c, _map);
464 if (!foundInDomain) {
465 if (!d->getFromHeader(c)) {
470 d->setContentLanguage(c);
472 return foundInDomain;
478 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
482 const auto d = lsp->d_ptr;
485 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in path";
487 d->setContentLanguage(c);
490 if (!d->getFromHeader(c)) {
493 auto uri = c->req()->uri();
495 const auto localeIdx = pathParts.
indexOf(locale);
497 uri.setPath(pathParts.join(u
'/'));
498 qCDebug(C_LANGSELECT) <<
"Storing selected locale by redirecting to" << uri;
507 bool redirect =
false;
512 if (getFromSession(c, sessionKey)) {
516 if (getFromCookie(c, cookieName)) {
520 if (getFromQuery(c, queryKey)) {
524 if (getFromSubdomain(c, subDomainMap)) {
528 if (getFromDomain(c, domainMap)) {
543 if (foundIn != _source) {
545 setToSession(c, sessionKey);
547 setToCookie(c, cookieName);
549 setToQuery(c, queryKey);
558 setContentLanguage(c);
564bool LangSelectPrivate::getFromQuery(
Context *c,
const QString &key)
const
567 if (l.language() !=
QLocale::C && locales.contains(l)) {
568 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in url query key" << key;
572 qCDebug(C_LANGSELECT) <<
"Can not find supported locale in url query key" << key;
577bool LangSelectPrivate::getFromCookie(
Context *c,
const QString &cookie)
const
579 const QLocale l(c->req()->
cookie(cookie));
580 if (l.language() !=
QLocale::C && locales.contains(l)) {
581 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in cookie name" << cookie;
585 qCDebug(C_LANGSELECT) <<
"Can no find supported locale in cookie value with name" << cookie;
590bool LangSelectPrivate::getFromSession(
Context *c,
const QString &key)
const
594 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in session key" << key;
598 qCDebug(C_LANGSELECT) <<
"Can not find supported locale in session value with key" << key;
603bool LangSelectPrivate::getFromSubdomain(
Context *c,
const QMap<QString, QLocale> &map)
const
605 const auto domain = c->req()->uri().
host();
608 if (domain.startsWith(i.key())) {
609 qCDebug(C_LANGSELECT) <<
"Found valid locale" << i.value()
610 <<
"in subdomain map for domain" << domain;
618 if (domainParts.size() > 2) {
619 const QLocale l(domainParts.at(0));
621 qCDebug(C_LANGSELECT) <<
"Found supported locale" << l <<
"in subdomain of domain"
627 qCDebug(C_LANGSELECT) <<
"Can not find supported locale for subdomain" << domain;
631bool LangSelectPrivate::getFromDomain(
Context *c,
const QMap<QString, QLocale> &map)
const
633 const auto domain = c->req()->uri().
host();
636 if (domain.endsWith(i.key())) {
637 qCDebug(C_LANGSELECT) <<
"Found valid locale" << i.value() <<
"in domain map for domain"
646 if (domainParts.size() > 1) {
647 const QLocale l(domainParts.at(domainParts.size() - 1));
649 qCDebug(C_LANGSELECT) <<
"Found supported locale" << l <<
"in domain" << domain;
654 qCDebug(C_LANGSELECT) <<
"Can not find supported locale for domain" << domain;
658bool LangSelectPrivate::getFromHeader(
Context *c,
const QString &name)
const
660 if (detectFromHeader) {
662 if (Q_LIKELY(!accpetedLangs.empty())) {
663 std::map<float, QLocale> langMap;
664 for (
const QString &al : accpetedLangs) {
665 const auto idx = al.indexOf(u
';');
666 float priority = 1.0f;
670 langPart = al.
left(idx);
671 const auto ref = QStringView(al).
mid(idx + 1);
672 priority = ref.
mid(ref.indexOf(u
'=') + 1).
toFloat(&ok);
676 QLocale locale(langPart);
678 const auto search = langMap.find(priority);
679 if (search == langMap.cend()) {
680 langMap.insert({priority, locale});
684 if (!langMap.empty()) {
685 auto i = langMap.crbegin();
686 while (i != langMap.crend()) {
687 if (locales.contains(i->second)) {
689 qCDebug(C_LANGSELECT)
690 <<
"Selected locale" << c->
locale() <<
"from" << name <<
"header";
697 i = langMap.crbegin();
698 const auto constLocales = locales;
699 while (i != langMap.crend()) {
700 for (
const QLocale &l : constLocales) {
701 if (l.
language() == i->second.language()) {
703 qCDebug(C_LANGSELECT)
704 <<
"Selected locale" << c->
locale() <<
"from" << name <<
"header";
717void LangSelectPrivate::setToQuery(
Context *c,
const QString &key)
const
719 auto uri = c->req()->uri();
720 QUrlQuery query(uri);
721 if (query.hasQueryItem(key)) {
722 query.removeQueryItem(key);
726 qCDebug(C_LANGSELECT) <<
"Storing selected locale in URL query by redirecting to" << uri;
730void LangSelectPrivate::setToCookie(
Context *c,
const QString &name)
const
732 qCDebug(C_LANGSELECT) <<
"Storing selected locale in cookie with name" << name;
734#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0)
735 cookie.setSameSitePolicy(QNetworkCookie::SameSite::Lax);
740void LangSelectPrivate::setToSession(
Context *c,
const QString &key)
const
742 qCDebug(C_LANGSELECT) <<
"Storing selected locale in session key" << key;
746void LangSelectPrivate::setFallback(
Context *c)
const
748 qCDebug(C_LANGSELECT) <<
"Can not find fitting locale, using fallback locale" << fallbackLocale;
752void LangSelectPrivate::setContentLanguage(
Context *c)
const
754 if (addContentLanguageHeader) {
760 : QStringLiteral(
"rtl"))}});
763void LangSelectPrivate::beforePrepareAction(
Context *c,
bool *skipMethod)
const
769 if (!c->
stash(SELECTION_TRIED).isNull()) {
773 detectLocale(c, source, skipMethod);
778void LangSelectPrivate::_q_postFork(
Application *app)
783#include "moc_langselect.cpp"
The Cutelyst Application.
void beforePrepareAction(Cutelyst::Context *c, bool *skipMethod)
T plugin()
Returns the registered plugin that casts to the template type T.
void postForked(Cutelyst::Application *app)
void stash(const QVariantHash &unite)
void detach(Action *action=nullptr)
QLocale locale() const noexcept
Response * res() const noexcept
void setStash(const QString &key, const QVariant &value)
void setLocale(const QLocale &locale)
Language selection plugin.
void setLocalesFromDir(const QString &path, const QString &name, const QString &prefix=QStringLiteral("."), const QString &suffix=QStringLiteral(".qm"))
void setDetectFromHeader(bool enabled)
void setLanguageDirStashKey(const QString &key=QStringLiteral("c_langselect_dir"))
static bool fromPath(Context *c, const QString &locale)
static QVector< QLocale > getSupportedLocales()
void setFallbackLocale(const QLocale &fallback)
void setQueryKey(const QString &key)
void setSubDomainMap(const QMap< QString, QLocale > &map)
static bool fromCookie(Context *c, const QString &name=QString())
static bool fromDomain(Context *c, const QMap< QString, QLocale > &domainMap=QMap< QString, QLocale >())
void setDomainMap(const QMap< QString, QLocale > &map)
void setLanguageCodeStashKey(const QString &key=QStringLiteral("c_langselect_lang"))
static bool fromUrlQuery(Context *c, const QString &key=QString())
void setCookieName(const QString &name)
static bool fromSession(Context *c, const QString &key=QString())
void setLocalesFromDirs(const QString &path, const QString &name)
static bool fromSubDomain(Context *c, const QMap< QString, QLocale > &subDomainMap=QMap< QString, QLocale >())
QVector< QLocale > supportedLocales() const
virtual ~LangSelect() override
void setSessionKey(const QString &key)
void addSupportedLocale(const QLocale &locale)
virtual bool setup(Application *app) override
void setSupportedLocales(const QVector< QLocale > &locales)
LangSelect(Application *parent, Source source)
Plugin(Application *parent)
QString header(const QString &key) const
QString queryParam(const QString &key, const QString &defaultValue={}) const
QString cookie(const QString &name) const
void redirect(const QUrl &url, quint16 status=Found)
void setHeader(const QString &field, const QString &value)
void setCookie(const QNetworkCookie &cookie)
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
static void setValue(Context *c, const QString &key, const QVariant &value)
The Cutelyst namespace holds all public Cutelyst API.
QFileInfoList entryInfoList(Filters filters, SortFlags sort) const const
QStringList entryList(Filters filters, SortFlags sort) const const
bool exists() const const
QString bcp47Name() const const
Language language() const const
const_iterator constBegin() const const
const_iterator constEnd() const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QObject * parent() const const
QStringList split(const QString &sep, SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString left(int n) const const
QString mid(int position, int n) const const
float toFloat(bool *ok) const const
QByteArray toLatin1() const const
QString toLower() const const
int indexOf(QStringView str, int from) const const
QString host(ComponentFormattingOptions options) const const
QString path(ComponentFormattingOptions options) const const
QLocale toLocale() const const