From 97e8a9f29f6353c55fa3961adef07e5d9edf47c0 Mon Sep 17 00:00:00 2001 From: Wehrwolfmann Date: Sat, 8 Aug 2026 00:26:42 +0200 Subject: [PATCH] Use AppStream metadata for localized package descriptions Descriptions currently come from the pacman database (pkgdesc via alpm_manager), which holds one English string per package and has no translations. As a result the description column stays English even when the user's language is fully supported by the application being listed. The distribution already ships translated summaries: archlinux-appstream-data installs /usr/share/swcatalog/xml/{core,extra,multilib}.xml.gz, where each component carries plus for every language it has been translated into. libappstream picks the right language from the current locale on its own. This is where Discover and pamac get their descriptions from. Build a package name -> summary map once at start-up and prefer it over pkgdesc in both the popular apps tree and the repository tree. When AppStream is missing, the summary is empty, or the package has no component at all (CLI tools mostly), the pkgdesc is used exactly as before. Only the OS catalogue is loaded, since it is the only source that carries package names; on a Plasma install that is 1572 components and one pass over them costs about 75 ms, measured. Looking components up one by one via componentsByBundleId() was measured slower for this workload (256 ms for the 147 packages of pkglist.yaml), because each lookup is a separate query. AppStreamQt is an optional dependency: without it the code compiles out and behaviour is unchanged. --- CMakeLists.txt | 17 ++++++++++++++ src/mainwindow.cpp | 56 +++++++++++++++++++++++++++++++++++++++++++--- src/mainwindow.hpp | 6 +++++ 3 files changed, 76 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b8131c3..dc1afb1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,18 @@ CPMAddPackage( find_package(PolkitQt6-1 REQUIRED) +# Optional: localized application summaries from the distribution's AppStream +# catalogue. Without it we fall back to the (English-only) pacman pkgdesc. +option(ENABLE_APPSTREAM "Use AppStream metadata for localized descriptions" ON) +if(ENABLE_APPSTREAM) + find_package(AppStreamQt 1.0 QUIET) + if(AppStreamQt_FOUND) + message(STATUS "AppStreamQt found: descriptions will be localized") + else() + message(STATUS "AppStreamQt not found: descriptions will fall back to pkgdesc") + endif() +endif() + ## ## CONFIGURATION ## @@ -134,6 +146,11 @@ include_directories(${CMAKE_SOURCE_DIR}/src ${CMAKE_BINARY_DIR}) target_link_libraries(${PROJECT_NAME} PRIVATE project_warnings project_options Qt6::Widgets Threads::Threads spdlog::spdlog fmt::fmt ryml::ryml cpr::cpr backend-rustlib-cxxbridge PkgConfig::LIBALPM) +if(AppStreamQt_FOUND) + target_link_libraries(${PROJECT_NAME} PRIVATE AppStreamQt) + target_compile_definitions(${PROJECT_NAME} PRIVATE HAVE_APPSTREAM) +endif() + option(ENABLE_UNITY "Enable Unity builds of projects" OFF) if(ENABLE_UNITY) # Add for any project you want to apply unity builds for diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index cc5039d..e29209a 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -51,6 +51,12 @@ #include // for array #include // for ranges::* +#ifdef HAVE_APPSTREAM +#include +#include +#include +#endif + #include #include #include @@ -133,6 +139,7 @@ void MainWindow::setup() noexcept { const QString icon = "software-update-available-symbolic"; const QIcon backup_icon = QIcon(":/icons/software-update-available.png"); m_ui->icon->setIcon(QIcon::fromTheme(icon, backup_icon)); + loadAppStreamSummaries(); fetch_net_pkglist(); refreshPopularApps(); @@ -399,6 +406,41 @@ void processMap(MainWindow& window, const std::string& parent_category, ryml::No } } +// Collect translated application summaries from the distribution's AppStream +// catalogue, keyed by package name. Does nothing when AppStream is unavailable, +// in which case callers fall back to the English pkgdesc. +void MainWindow::loadAppStreamSummaries() noexcept { +#ifdef HAVE_APPSTREAM + spdlog::debug("+++ {} +++", __PRETTY_FUNCTION__); + + AppStream::Pool pool; + // Only the distro catalogue carries ; skipping the other sources + // keeps the pass short without losing any package mapping. + pool.setFlags(AppStream::Pool::FlagLoadOsCatalog); + if (!pool.load()) { + spdlog::warn("AppStream metadata unavailable, falling back to pkgdesc: {}", pool.lastError().toStdString()); + return; + } + + // A single pass over the catalogue is cheaper than one lookup per package. + for (const auto& component : pool.components()) { + const auto& summary = component.summary(); + if (summary.isEmpty()) { + continue; + } + const bool is_app = (component.kind() == AppStream::Component::KindDesktopApp); + for (const auto& pkgname : component.packageNames()) { + // A package may be claimed by several components; a desktop + // application describes it better than an addon or a codec. + if (is_app || !m_appstream_summaries.contains(pkgname)) { + m_appstream_summaries.insert(pkgname, summary); + } + } + } + spdlog::debug("AppStream summaries loaded: {}", m_appstream_summaries.size()); +#endif +} + // Load data from Github repo void MainWindow::fetch_net_pkglist() noexcept { spdlog::debug("+++ {} +++", __PRETTY_FUNCTION__); @@ -456,8 +498,13 @@ void MainWindow::processFile(const std::string& group, const std::string& catego QString install_names; QString uninstall_names; - if (auto pkg = m_alpm_manager->get_package_view(names[0])) { - description = QString(pkg->desc.data()); + // Prefer the AppStream summary, which is translated into the user's + // language; pkgdesc from the pacman database is English only. + description = m_appstream_summaries.value(QString::fromStdString(names[0])); + if (description.isEmpty()) { + if (auto pkg = m_alpm_manager->get_package_view(names[0])) { + description = QString(pkg->desc.data()); + } } install_names = QString::fromStdString(fmt::format("{} {}", names[0], utils::join_range(names.begin() + 1, names.end(), ' '))); @@ -702,7 +749,10 @@ void MainWindow::displayPackages() noexcept { widget_item->setCheckState(TreeCol::Check, Qt::Unchecked); widget_item->setText(TreeCol::Name, key); widget_item->setText(TreeCol::Version, value.at(0)); - widget_item->setText(TreeCol::Description, value.at(1)); + // Same as for the popular apps: a translated AppStream summary if we + // have one, otherwise the English pkgdesc. + const auto& as_summary = m_appstream_summaries.value(key); + widget_item->setText(TreeCol::Description, as_summary.isEmpty() ? value.at(1) : as_summary); widget_item->setText(TreeCol::Displayed, QStringLiteral("true")); // all items are displayed till filtered // update tree diff --git a/src/mainwindow.hpp b/src/mainwindow.hpp index 9935520..abec897 100644 --- a/src/mainwindow.hpp +++ b/src/mainwindow.hpp @@ -49,6 +49,7 @@ #include #include +#include #include #include #include @@ -137,6 +138,7 @@ class MainWindow : public QDialog { void ifDownloadFailed() noexcept; void listFlatpakRemotes() noexcept; void listSizeInstalledFP() noexcept; + void loadAppStreamSummaries() noexcept; void fetch_net_pkglist() noexcept; void processFile(const std::string& group, const std::string& category, const std::vector& names) noexcept; void refreshPopularApps() noexcept; @@ -214,6 +216,10 @@ class MainWindow : public QDialog { alpm::AlpmManagerPtr m_alpm_manager; + /// Maps a package name to its AppStream summary, translated into the + /// user's language. Empty when no AppStream metadata is available. + QHash m_appstream_summaries{}; + QString m_indexFilterFP{}; bool m_warning_flatpaks{}; bool m_setup_assistant_mode{};