6 #include "langselect_p.h"
8 #include <Cutelyst/Application>
9 #include <Cutelyst/Context>
10 #include <Cutelyst/Plugins/Session/Session>
11 #include <Cutelyst/Response>
15 #include <QLoggingCategory>
17 #include <QNetworkCookie>
23 Q_LOGGING_CATEGORY(C_LANGSELECT,
"cutelyst.plugin.langselect", QtWarningMsg)
29 #define SELECTION_TRIED QStringLiteral("_c_langselect_tried")
32 , d_ptr(new LangSelectPrivate)
40 , d_ptr(new LangSelectPrivate)
44 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) {
100 if (Q_LIKELY(l.language() != QLocale::C)) {
101 d->locales.push_back(l);
103 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << l <<
"to the list of suppored locales.";
112 d->locales.reserve(locales.size());
113 for (
const QString &l : locales) {
115 if (Q_LIKELY(locale.language() != QLocale::C)) {
116 d->locales.push_back(locale);
118 qCWarning(C_LANGSELECT,
"Can not add invalid locale \"%s\" to the list of supported locales.", qUtf8Printable(l));
125 if (Q_LIKELY(locale.language() != QLocale::C)) {
127 d->locales.push_back(locale);
129 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << locale <<
"to the list of supported locales.";
136 if (Q_LIKELY(l.language() != QLocale::C)) {
138 d->locales.push_back(l);
140 qCWarning(C_LANGSELECT,
"Can not add invalid locale \"%s\" to the list of supported locales.", qUtf8Printable(locale));
148 if (Q_LIKELY(!path.isEmpty() && !name.isEmpty())) {
149 const QDir dir(path);
150 if (Q_LIKELY(dir.exists())) {
151 const auto _pref = prefix.isEmpty() ? QStringLiteral(
".") : prefix;
152 const auto _suff = suffix.isEmpty() ? QStringLiteral(
".qm") : suffix;
153 const QString filter = name + _pref + u
'*' + _suff;
154 const auto files = dir.entryInfoList({name}, QDir::Files);
155 if (Q_LIKELY(!files.empty())) {
156 d->locales.reserve(files.size());
157 bool shrinkToFit =
false;
158 for (
const QFileInfo &fi : files) {
159 const auto fn = fi.fileName();
160 const auto prefIdx = fn.indexOf(_pref);
161 const auto locPart = fn.mid(prefIdx + _pref.length(), fn.length() - prefIdx - _suff.length() - _pref.length());
163 if (Q_LIKELY(l.language() != QLocale::C)) {
164 d->locales.push_back(l);
165 qCDebug(C_LANGSELECT,
"Added locale \"%s\" to the list of supported locales.", qUtf8Printable(locPart));
168 qCWarning(C_LANGSELECT,
"Can not add invalid locale \"%s\" to the list of supported locales.", qUtf8Printable(locPart));
172 d->locales.squeeze();
175 qCWarning(C_LANGSELECT,
"Can not find translation files for \"%s\" in \"%s\".", qUtf8Printable(filter), qUtf8Printable(path));
178 qCWarning(C_LANGSELECT,
"Can not set locales from not existing directory \"%s\".", qUtf8Printable(path));
181 qCWarning(C_LANGSELECT,
"Can not set locales from dir with emtpy path or name.");
189 if (Q_LIKELY(!path.isEmpty() && !name.isEmpty())) {
190 const QDir dir(path);
191 if (Q_LIKELY(dir.exists())) {
192 const auto dirs = dir.entryList(QDir::AllDirs);
193 if (Q_LIKELY(!dirs.empty())) {
194 d->locales.reserve(dirs.size());
195 bool shrinkToFit =
false;
196 for (
const QString &subDir : dirs) {
197 const QString relFn = subDir + u
'/' + name;
198 if (dir.exists(relFn)) {
200 if (Q_LIKELY(l.language() != QLocale::C)) {
201 d->locales.push_back(l);
202 qCDebug(C_LANGSELECT,
"Added locale \"%s\" to the list of supported locales.", qUtf8Printable(subDir));
205 qCWarning(C_LANGSELECT,
"Can not add invalid locale \"%s\" to the list of supported locales.", qUtf8Printable(subDir));
212 d->locales.squeeze();
216 qCWarning(C_LANGSELECT,
"Can not set locales from not existing directory \"%s\".", qUtf8Printable(path));
219 qCWarning(C_LANGSELECT,
"Can not set locales from dirs with empty path or names.");
244 d->cookieName = name;
250 d->subDomainMap.clear();
252 d->locales.reserve(map.size());
253 auto i = map.constBegin();
254 while (i != map.constEnd()) {
255 if (i.value().language() != QLocale::C) {
256 d->subDomainMap.insert(i.key(), i.value());
257 d->locales.append(i.value());
259 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << i.value() <<
"for subdomain" << i.key() <<
"to the subdomain map.";
263 d->locales.squeeze();
269 d->domainMap.clear();
271 d->locales.reserve(map.size());
272 auto i = map.constBegin();
273 while (i != map.constEnd()) {
274 if (Q_LIKELY(i.value().language() != QLocale::C)) {
275 d->domainMap.insert(i.key(), i.value());
276 d->locales.append(i.value());
278 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << i.value() <<
"for domain" << i.key() <<
"to the domain map.";
282 d->locales.squeeze();
288 d->fallbackLocale = fallback;
294 d->detectFromHeader = enabled;
300 if (Q_LIKELY(!key.isEmpty())) {
301 d->langStashKey = key;
303 qCWarning(C_LANGSELECT) <<
"Can not set an empty key name for the language code stash key. Using current key name" << d->langStashKey;
310 if (Q_LIKELY(!key.isEmpty())) {
311 d->dirStashKey = key;
313 qCWarning(C_LANGSELECT) <<
"Can not set an empty key name for the language direction stash key. Using current key name" << d->dirStashKey;
320 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
321 return QVector<QLocale>();
330 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
334 const auto d = lsp->d_ptr;
335 const auto _key = !key.isEmpty() ? key : d->queryKey;
336 if (!d->getFromQuery(c, _key)) {
337 if (!d->getFromHeader(c)) {
340 d->setToQuery(c, _key);
344 d->setContentLanguage(c);
351 bool foundInSession =
false;
354 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
355 return foundInSession;
358 const auto d = lsp->d_ptr;
359 const auto _key = !key.isEmpty() ? key : d->sessionKey;
360 foundInSession = d->getFromSession(c, _key);
361 if (!foundInSession) {
362 if (!d->getFromHeader(c)) {
365 d->setToSession(c, _key);
367 d->setContentLanguage(c);
369 return foundInSession;
374 bool foundInCookie =
false;
377 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
378 return foundInCookie;
381 const auto d = lsp->d_ptr;
382 const auto _name = !name.isEmpty() ? name : d->cookieName;
383 foundInCookie = d->getFromCookie(c, _name);
384 if (!foundInCookie) {
385 if (!d->getFromHeader(c)) {
388 d->setToCookie(c, _name);
390 d->setContentLanguage(c);
392 return foundInCookie;
397 bool foundInSubDomain =
false;
400 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
401 return foundInSubDomain;
404 const auto d = lsp->d_ptr;
405 const auto _map = !subDomainMap.empty() ? subDomainMap : d->subDomainMap;
406 foundInSubDomain = d->getFromSubdomain(c, _map);
407 if (!foundInSubDomain) {
408 if (!d->getFromHeader(c)) {
413 d->setContentLanguage(c);
415 return foundInSubDomain;
420 bool foundInDomain =
false;
423 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
424 return foundInDomain;
427 const auto d = lsp->d_ptr;
428 const auto _map = !domainMap.empty() ? domainMap : d->domainMap;
429 foundInDomain = d->getFromDomain(c, _map);
430 if (!foundInDomain) {
431 if (!d->getFromHeader(c)) {
436 d->setContentLanguage(c);
438 return foundInDomain;
444 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
448 const auto d = lsp->d_ptr;
449 const QLocale l(locale);
450 if (l.language() != QLocale::C && d->locales.contains(l)) {
451 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in path";
453 d->setContentLanguage(c);
456 if (!d->getFromHeader(c)) {
459 auto uri = c->req()->uri();
460 auto pathParts = uri.path().split(u
'/');
461 const auto localeIdx = pathParts.indexOf(locale);
462 pathParts[localeIdx] = c->
locale().bcp47Name().toLower();
463 uri.setPath(pathParts.join(u
'/'));
464 qCDebug(C_LANGSELECT) <<
"Storing selected locale by redirecting to" << uri;
473 bool redirect =
false;
478 if (getFromSession(c, sessionKey)) {
482 if (getFromCookie(c, cookieName)) {
486 if (getFromQuery(c, queryKey)) {
490 if (getFromSubdomain(c, subDomainMap)) {
494 if (getFromDomain(c, domainMap)) {
510 if (foundIn != _source) {
512 setToSession(c, sessionKey);
514 setToCookie(c, cookieName);
516 setToQuery(c, queryKey);
525 setContentLanguage(c);
531 bool LangSelectPrivate::getFromQuery(
Context *c,
const QString &key)
const
534 if (l.language() != QLocale::C && locales.contains(l)) {
535 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in url query key" << key;
539 qCDebug(C_LANGSELECT) <<
"Can not find supported locale in url query key" << key;
544 bool LangSelectPrivate::getFromCookie(
Context *c,
const QString &cookie)
const
546 const QLocale l(c->req()->
cookie(cookie));
547 if (l.language() != QLocale::C && locales.contains(l)) {
548 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in cookie name" << cookie;
552 qCDebug(C_LANGSELECT) <<
"Can no find supported locale in cookie value with name" << cookie;
557 bool LangSelectPrivate::getFromSession(
Context *c,
const QString &key)
const
560 if (l.language() != QLocale::C && locales.contains(l)) {
561 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in session key" << key;
565 qCDebug(C_LANGSELECT) <<
"Can not find supported locale in session value with key" << key;
570 bool LangSelectPrivate::getFromSubdomain(
Context *c,
const QMap<QString, QLocale> &map)
const
572 const auto domain = c->req()->uri().host();
573 auto i = map.constBegin();
574 while (i != map.constEnd()) {
575 if (domain.startsWith(i.key())) {
576 qCDebug(C_LANGSELECT) <<
"Found valid locale" << i.value() <<
"in subdomain map for domain" << domain;
583 const auto domainParts = domain.split(u
'.', Qt::SkipEmptyParts);
584 if (domainParts.size() > 2) {
585 const QLocale l(domainParts.at(0));
586 if (l.language() != QLocale::C && locales.contains(l)) {
587 qCDebug(C_LANGSELECT) <<
"Found supported locale" << l <<
"in subdomain of domain" << domain;
592 qCDebug(C_LANGSELECT) <<
"Can not find supported locale for subdomain" << domain;
596 bool LangSelectPrivate::getFromDomain(
Context *c,
const QMap<QString, QLocale> &map)
const
598 const auto domain = c->req()->uri().host();
599 auto i = map.constBegin();
600 while (i != map.constEnd()) {
601 if (domain.endsWith(i.key())) {
602 qCDebug(C_LANGSELECT) <<
"Found valid locale" << i.value() <<
"in domain map for domain" << domain;
609 const auto domainParts = domain.split(u
'.', Qt::SkipEmptyParts);
610 if (domainParts.size() > 1) {
611 const QLocale l(domainParts.at(domainParts.size() - 1));
612 if (l.language() != QLocale::C && locales.contains(l)) {
613 qCDebug(C_LANGSELECT) <<
"Found supported locale" << l <<
"in domain" << domain;
618 qCDebug(C_LANGSELECT) <<
"Can not find supported locale for domain" << domain;
622 bool LangSelectPrivate::getFromHeader(
Context *c,
const QString &name)
const
624 if (detectFromHeader) {
625 const auto accpetedLangs = c->req()->
header(name).split(u
',', Qt::SkipEmptyParts);
626 if (Q_LIKELY(!accpetedLangs.empty())) {
627 std::map<float,QLocale> langMap;
628 for (
const QString &al : accpetedLangs) {
629 const auto idx = al.indexOf(u
';');
630 float priority = 1.0f;
634 langPart = al.left(idx);
635 const auto ref = QStringView(al).mid(idx + 1);
636 priority = ref.mid(ref.indexOf(u
'=') + 1).toFloat(&ok);
640 QLocale locale(langPart);
641 if (ok && locale.language() != QLocale::C) {
642 const auto search = langMap.find(priority);
643 if (search == langMap.cend()) {
644 langMap.insert({priority, locale});
648 if (!langMap.empty()) {
649 auto i = langMap.crbegin();
650 while (i != langMap.crend()) {
651 if (locales.contains(i->second)) {
653 qCDebug(C_LANGSELECT) <<
"Selected locale" << c->
locale() <<
"from" << name <<
"header";
660 i = langMap.crbegin();
661 const auto constLocales = locales;
662 while (i != langMap.crend()) {
663 for (
const QLocale &l : constLocales) {
664 if (l.language() == i->second.language()) {
666 qCDebug(C_LANGSELECT) <<
"Selected locale" << c->
locale() <<
"from" << name <<
"header";
679 void LangSelectPrivate::setToQuery(
Context *c,
const QString &key)
const
681 auto uri = c->req()->uri();
682 QUrlQuery query(uri);
683 if (query.hasQueryItem(key)) {
684 query.removeQueryItem(key);
686 query.addQueryItem(key, c->
locale().bcp47Name().toLower());
688 qCDebug(C_LANGSELECT) <<
"Storing selected locale in URL query by redirecting to" << uri;
692 void LangSelectPrivate::setToCookie(
Context *c,
const QString &name)
const
694 qCDebug(C_LANGSELECT) <<
"Storing selected locale in cookie with name" << name;
695 c->
res()->
setCookie(QNetworkCookie(name.toLatin1(), c->
locale().bcp47Name().toLatin1()));
698 void LangSelectPrivate::setToSession(
Context *c,
const QString &key)
const
700 qCDebug(C_LANGSELECT) <<
"Storing selected locale in session key" << key;
704 void LangSelectPrivate::setFallback(
Context *c)
const
706 qCDebug(C_LANGSELECT) <<
"Can not find fitting locale, using fallback locale" << fallbackLocale;
710 void LangSelectPrivate::setContentLanguage(
Context *c)
const
712 if (addContentLanguageHeader) {
716 {langStashKey, c->
locale().bcp47Name()},
717 {dirStashKey, (c->
locale().textDirection() == Qt::LeftToRight ? QStringLiteral(
"ltr") : QStringLiteral(
"rtl"))}
721 void LangSelectPrivate::beforePrepareAction(
Context *c,
bool *skipMethod)
const
727 if (!c->
stash(SELECTION_TRIED).isNull()) {
731 detectLocale(c, source, skipMethod);
736 void LangSelectPrivate::_q_postFork(
Application *app)
741 #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)
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.