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(QDir::Filters filters, QDir::SortFlags sort) const const
QStringList entryList(QDir::Filters filters, QDir::SortFlags sort) const const
bool exists() const const
qsizetype size() const const
QString bcp47Name(QLocale::TagSeparator separator) const const
QLocale::Language language() const const
QMap< Key, T >::const_iterator constBegin() const const
QMap< Key, T >::const_iterator constEnd() const const
QMap< Key, T >::size_type size() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QObject * parent() const const
bool isEmpty() const const
QString left(qsizetype n) &&
QString mid(qsizetype position, qsizetype n) &&
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
float toFloat(bool *ok) const const
QByteArray toLatin1() const const
QString toLower() const const
qsizetype indexOf(QLatin1StringView str, qsizetype from, Qt::CaseSensitivity cs) const const
QString host(QUrl::ComponentFormattingOptions options) const const
QString path(QUrl::ComponentFormattingOptions options) const const
QLocale toLocale() const const