feat(applications): localize desktop entries by primary language and rescan on change

pull/18/head
reionwong 1 month ago
parent 91113c8ae3
commit acd520a5cc

@ -1,6 +1,8 @@
#include "applicationregistry.h"
#include "applicationruntime.h"
#include <QCollator>
#include <QDBusConnection>
#include <QDir>
#include <QDirIterator>
#include <QFile>
@ -19,8 +21,9 @@ namespace {
class DesktopEntryScanner : public QRunnable
{
public:
explicit DesktopEntryScanner(ApplicationRegistry *registry)
DesktopEntryScanner(ApplicationRegistry *registry, const QString &language)
: m_registry(registry)
, m_language(language)
{
setAutoDelete(true);
}
@ -56,7 +59,7 @@ public:
continue;
DesktopEntryData data;
if (DesktopEntry::parse(id, path, file.readAll(), &data)) {
if (DesktopEntry::parse(id, path, file.readAll(), &data, m_language)) {
selectedIds.insert(id);
results.append(std::move(data));
}
@ -75,6 +78,7 @@ public:
private:
ApplicationRegistry *m_registry;
QString m_language;
};
QString normalized(const QString &value)
@ -192,6 +196,11 @@ ApplicationRegistry::ApplicationRegistry(QObject *parent)
&ApplicationRegistry::scheduleScan);
connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &ApplicationRegistry::scheduleScan);
connect(&m_debounceTimer, &QTimer::timeout, this, &ApplicationRegistry::startScan);
QDBusConnection::sessionBus().connect(QStringLiteral("com.cutefish.Services"),
QStringLiteral("/com/cutefish/Services/Language"),
QStringLiteral("com.cutefish.Services.Language"),
QStringLiteral("languageChanged"), this,
SLOT(onLanguageChanged()));
monitorPaths();
startScan();
@ -334,6 +343,12 @@ void ApplicationRegistry::scheduleScan()
m_debounceTimer.start();
}
void ApplicationRegistry::onLanguageChanged()
{
if (DesktopEntry::displayLanguage() != m_language)
scheduleScan();
}
void ApplicationRegistry::startScan()
{
if (m_scanInProgress) {
@ -346,7 +361,8 @@ void ApplicationRegistry::startScan()
m_loading = true;
emit loadingChanged();
}
QThreadPool::globalInstance()->start(new DesktopEntryScanner(this));
m_language = DesktopEntry::displayLanguage();
QThreadPool::globalInstance()->start(new DesktopEntryScanner(this, m_language));
}
void ApplicationRegistry::applyScan(const QList<DesktopEntryData> &results)
@ -376,8 +392,10 @@ void ApplicationRegistry::applyScan(const QList<DesktopEntryData> &results)
visibleEntries.append(entry);
}
std::sort(visibleEntries.begin(), visibleEntries.end(), [](DesktopEntry *a, DesktopEntry *b) {
const int nameCompare = QString::localeAwareCompare(a->name(), b->name());
const QCollator collator{QLocale(m_language)};
std::sort(visibleEntries.begin(), visibleEntries.end(),
[&collator](DesktopEntry *a, DesktopEntry *b) {
const int nameCompare = collator.compare(a->name(), b->name());
return nameCompare == 0 ? a->id() < b->id() : nameCompare < 0;
});

@ -79,6 +79,7 @@ public slots:
void applyScan(const QList<DesktopEntryData> &results);
private:
Q_SLOT void onLanguageChanged();
void monitorPaths();
DesktopEntryModel *m_model;
@ -87,6 +88,7 @@ private:
QHash<QString, DesktopEntry *> m_entriesById;
QHash<QString, DesktopEntry *> m_entriesByPath;
QList<DesktopEntry *> m_entries;
QString m_language;
bool m_loading = false;
bool m_scanInProgress = false;
bool m_scanQueued = false;

@ -2,6 +2,7 @@
#include "applicationlauncher.h"
#include "applicationruntime.h"
#include "languageenvironment.h"
#include <QFile>
#include <QFileInfo>
@ -57,20 +58,32 @@ bool listContainsDesktop(const QStringList &list, const QStringList &desktops)
return false;
}
QString localizedValue(const QMap<QString, QString> &values,
const QMap<QString, QString> &localized,
const QString &key)
// XDG locale matching order: lang_COUNTRY@MODIFIER, lang_COUNTRY, lang@MODIFIER, lang.
QStringList localeCandidates(const QString &language)
{
const QLocale locale = QLocale::system();
const QStringList candidates = {
locale.name(),
locale.bcp47Name(),
locale.name().section(QLatin1Char('_'), 0, 0)
const int modifierStart = language.indexOf(QLatin1Char('@'));
const QString modifier = modifierStart < 0 ? QString() : language.mid(modifierStart);
const QString name = language.left(modifierStart < 0 ? language.size() : modifierStart);
const QString base = name.section(QLatin1Char('_'), 0, 0);
QStringList candidates = {
name + modifier,
name,
base + modifier,
base,
QLocale(name).bcp47Name()
};
candidates.removeAll(QString());
candidates.removeDuplicates();
return candidates;
}
QString localizedValue(const QMap<QString, QString> &values,
const QMap<QString, QString> &localized,
const QString &key,
const QStringList &candidates)
{
for (const QString &candidate : candidates) {
if (candidate.isEmpty())
continue;
const auto it = localized.constFind(key + QLatin1Char('[') + candidate
+ QLatin1Char(']'));
if (it != localized.constEnd())
@ -265,12 +278,24 @@ QStringList DesktopEntry::parseExec(const QString &exec)
return entry.commandForArguments(QStringList());
}
QString DesktopEntry::displayLanguage()
{
// The session language lives in settings; this process keeps the locale it started with.
const QStringList languages = LanguageEnvironment::translationLanguages(
LanguageEnvironment::preferredLanguages().mid(0, 1));
return languages.isEmpty() ? QLocale::system().name() : languages.first();
}
bool DesktopEntry::parse(const QString &id, const QString &path,
const QByteArray &contents, DesktopEntryData *result)
const QByteArray &contents, DesktopEntryData *result,
const QString &language)
{
if (!result)
return false;
const QStringList candidates = localeCandidates(language.isEmpty() ? displayLanguage()
: language);
DesktopEntryData data;
data.id = id;
data.path = path;
@ -283,9 +308,11 @@ bool DesktopEntry::parse(const QString &id, const QString &path,
auto finishGroup = [&]() {
if (group == QStringLiteral("Desktop Entry")) {
data.type = values.value(QStringLiteral("Type"));
data.name = localizedValue(values, localized, QStringLiteral("Name"));
data.genericName = localizedValue(values, localized, QStringLiteral("GenericName"));
data.comment = localizedValue(values, localized, QStringLiteral("Comment"));
data.name = localizedValue(values, localized, QStringLiteral("Name"), candidates);
data.genericName = localizedValue(values, localized, QStringLiteral("GenericName"),
candidates);
data.comment = localizedValue(values, localized, QStringLiteral("Comment"),
candidates);
data.icon = values.value(QStringLiteral("Icon"));
data.exec = values.value(QStringLiteral("Exec"));
data.tryExec = values.value(QStringLiteral("TryExec"));
@ -306,7 +333,7 @@ bool DesktopEntry::parse(const QString &id, const QString &path,
if (!action.isEmpty()) {
DesktopActionData item;
item.id = actionId;
item.name = action.value(QStringLiteral("Name"));
item.name = localizedValue(action, action, QStringLiteral("Name"), candidates);
item.icon = action.value(QStringLiteral("Icon"));
item.exec = action.value(QStringLiteral("Exec"));
data.actions.append(item);

@ -100,8 +100,13 @@ public:
QStringList commandForArguments(const QStringList &arguments) const;
// Primary preferred language as a POSIX name, e.g. "zh_CN" or "sr_RS@latin".
static QString displayLanguage();
// An empty language uses displayLanguage().
static bool parse(const QString &id, const QString &path,
const QByteArray &contents, DesktopEntryData *result);
const QByteArray &contents, DesktopEntryData *result,
const QString &language = QString());
static QStringList parseExec(const QString &exec);
void update(const DesktopEntryData &data);

@ -52,6 +52,23 @@ private slots:
QVERIFY(!registry.resolveWindow({}, {}, "/usr/bin/fcitx5-config-qt", "fcitx5-config-qt"));
QCOMPARE(registry.resolveWindow(launcher.id, {}, {}, {}), registry.byId(launcher.id));
}
void localizesByLanguage()
{
const QByteArray contents = "[Desktop Entry]\nType=Application\nName=Files\n"
"Name[zh_CN]=文件\nName[sr@latin]=Datoteke\nExec=files\n";
DesktopEntryData data;
QVERIFY(DesktopEntry::parse("files", "/applications/files.desktop", contents, &data,
"zh_CN"));
QCOMPARE(data.name, QStringLiteral("文件"));
QVERIFY(DesktopEntry::parse("files", "/applications/files.desktop", contents, &data,
"sr_RS@latin"));
QCOMPARE(data.name, QStringLiteral("Datoteke"));
QVERIFY(DesktopEntry::parse("files", "/applications/files.desktop", contents, &data,
"de_DE"));
QCOMPARE(data.name, QStringLiteral("Files"));
}
};
QTEST_GUILESS_MAIN(ApplicationRegistryTest)

Loading…
Cancel
Save