Warning, file /plasma/plasma-workspace/kcms/region_language/kcmregionandlang.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002     kcmregionandlang.cpp
0003     SPDX-FileCopyrightText: 2014 Sebastian Kügler <sebas@kde.org>
0004     SPDX-FileCopyrightText: 2021 Han Young <hanyoung@protonmail.com>
0005     SPDX-FileCopyrightText: 2023 Serenity Cybersecurity, LLC <license@futurecrew.ru>
0006                                  Author: Gleb Popov <arrowd@FreeBSD.org>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "kcmregionandlang.h"
0012 
0013 #include <unistd.h>
0014 
0015 #include <QDBusConnection>
0016 #include <QDBusMessage>
0017 #include <QDBusPendingCall>
0018 
0019 #include <KLocalizedString>
0020 #include <KPluginFactory>
0021 #include <KSharedConfig>
0022 
0023 #include "languagelistmodel.h"
0024 #include "localegenerator.h"
0025 #include "localegeneratorbase.h"
0026 #include "localelistmodel.h"
0027 #include "optionsmodel.h"
0028 #include "regionandlangsettings.h"
0029 
0030 using namespace KCM_RegionAndLang;
0031 
0032 K_PLUGIN_CLASS_WITH_JSON(KCMRegionAndLang, "kcm_regionandlang.json")
0033 
0034 KCMRegionAndLang::KCMRegionAndLang(QObject *parent, const KPluginMetaData &data, const QVariantList &args)
0035     : KQuickAddons::ManagedConfigModule(parent, data, args)
0036     , m_settings(new RegionAndLangSettings(this))
0037     , m_optionsModel(new OptionsModel(this))
0038     , m_generator(LocaleGenerator::getGenerator())
0039 {
0040     connect(m_generator, &LocaleGeneratorBase::userHasToGenerateManually, this, &KCMRegionAndLang::userHasToGenerateManually);
0041     connect(m_generator, &LocaleGeneratorBase::success, this, &KCMRegionAndLang::generateFinished);
0042     connect(m_generator, &LocaleGeneratorBase::needsFont, this, &KCMRegionAndLang::requireInstallFont);
0043     connect(m_generator, &LocaleGeneratorBase::success, this, &KCMRegionAndLang::saveToConfigFile);
0044     connect(m_generator, &LocaleGeneratorBase::userHasToGenerateManually, this, &KCMRegionAndLang::saveToConfigFile);
0045     connect(m_generator, &LocaleGeneratorBase::needsFont, this, &KCMRegionAndLang::saveToConfigFile);
0046 
0047     // if we don't support auto locale generation for current system (BSD, musl etc.), userHasToGenerateManually regarded as success
0048     if (strcmp(m_generator->metaObject()->className(), "LocaleGeneratorBase") != 0) {
0049         connect(m_generator, &LocaleGeneratorBase::success, this, &KCMRegionAndLang::takeEffectNextTime);
0050     } else {
0051         connect(m_generator, &LocaleGeneratorBase::userHasToGenerateManually, this, &KCMRegionAndLang::takeEffectNextTime);
0052     }
0053 
0054     setQuickHelp(i18n("You can configure the formats used for time, dates, money and other numbers here."));
0055 
0056     qmlRegisterAnonymousType<RegionAndLangSettings>("kcmregionandlang", 1);
0057     qmlRegisterAnonymousType<OptionsModel>("kcmregionandlang", 1);
0058     qmlRegisterAnonymousType<SelectedLanguageModel>("kcmregionandlang", 1);
0059     qmlRegisterType<LocaleListModel>("kcmregionandlang", 1, 0, "LocaleListModel");
0060     qmlRegisterType<LanguageListModel>("kcmregionandlang", 1, 0, "LanguageListModel");
0061     qRegisterMetaType<KCM_RegionAndLang::SettingType>();
0062     qmlRegisterUncreatableMetaObject(KCM_RegionAndLang::staticMetaObject, "kcmregionandlang", 1, 0, "SettingType", "Error: SettingType is an enum");
0063 
0064 #if GLIBC_LOCALE_GENERATED
0065     // fedora pre generate locales, fetch available locales from localectl. /usr/share/i18n/locales is empty in fedora
0066     QDir glibcLocaleDir(localeFileDirPath());
0067     if (glibcLocaleDir.isEmpty()) {
0068         auto localectlPath = QStandardPaths::findExecutable(QStringLiteral("localectl"));
0069         if (!localectlPath.isEmpty()) {
0070             m_localectl = new QProcess(this);
0071             m_localectl->setProgram(localectlPath);
0072             m_localectl->setArguments({QStringLiteral("list-locales")});
0073             connect(m_localectl, &QProcess::finished, this, [this](int exitCode, QProcess::ExitStatus status) {
0074                 m_enabled = true; // set to true even if failed. otherwise our failed notification is also grey out
0075                 if (exitCode != 0 || status != QProcess::NormalExit) {
0076                     Q_EMIT encountedError(failedFindLocalesMessage());
0077                 }
0078                 Q_EMIT enabledChanged();
0079             });
0080             m_localectl->start();
0081         }
0082     } else {
0083         m_enabled = true;
0084     }
0085 #else
0086     m_enabled = true;
0087 #endif
0088 }
0089 
0090 QString KCMRegionAndLang::failedFindLocalesMessage()
0091 {
0092     return xi18nc("@info this will be shown as an error message",
0093                  "Could not find the system's available locales using the <command>localectl</command> tool. Please file a bug report about this at <link>https://bugs.kde.org</link>");
0094 }
0095 
0096 QString KCMRegionAndLang::localeFileDirPath()
0097 {
0098     return QStringLiteral("/usr/share/i18n/locales");
0099 }
0100 
0101 void KCMRegionAndLang::save()
0102 {
0103     // assemble full locales in use
0104     QStringList locales;
0105     if (!settings()->isDefaultSetting(SettingType::Lang)) {
0106         locales.append(settings()->lang());
0107     }
0108     if (!settings()->isDefaultSetting(SettingType::Numeric)) {
0109         locales.append(settings()->numeric());
0110     }
0111     if (!settings()->isDefaultSetting(SettingType::Time)) {
0112         locales.append(settings()->time());
0113     }
0114     if (!settings()->isDefaultSetting(SettingType::Measurement)) {
0115         locales.append(settings()->measurement());
0116     }
0117     if (!settings()->isDefaultSetting(SettingType::Currency)) {
0118         locales.append(settings()->monetary());
0119     }
0120     if (!settings()->isDefaultSetting(SettingType::PaperSize)) {
0121         locales.append(settings()->paperSize());
0122     }
0123     if (!settings()->isDefaultSetting(SettingType::Address)) {
0124         locales.append(settings()->address());
0125     }
0126     if (!settings()->isDefaultSetting(SettingType::NameStyle)) {
0127         locales.append(settings()->nameStyle());
0128     }
0129     if (!settings()->isDefaultSetting(SettingType::PhoneNumbers)) {
0130         locales.append(settings()->phoneNumbers());
0131     }
0132 #ifdef GLIBC_LOCALE
0133     if (!settings()->language().isEmpty()) {
0134         QStringList languages = settings()->language().split(QLatin1Char(':'));
0135         for (const QString &lang : languages) {
0136             auto glibcLocale = toGlibcLocale(lang);
0137             if (glibcLocale.has_value()) {
0138                 locales.append(glibcLocale.value());
0139             }
0140         }
0141     }
0142 #endif
0143 
0144     auto setLangCall = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.Accounts"),
0145                                                       QStringLiteral("/org/freedesktop/Accounts/User%1").arg(getuid()),
0146                                                       QStringLiteral("org.freedesktop.Accounts.User"),
0147                                                       QStringLiteral("SetLanguage"));
0148     setLangCall.setArguments({settings()->lang()});
0149     QDBusConnection::systemBus().asyncCall(setLangCall);
0150 
0151     if (!locales.isEmpty()) {
0152         Q_EMIT startGenerateLocale();
0153         m_generator->localesGenerate(locales);
0154     } else {
0155         // probably after clicking "defaults" so all the setting is default
0156         saveToConfigFile();
0157     }
0158     Q_EMIT saveClicked();
0159 }
0160 
0161 void KCMRegionAndLang::saveToConfigFile()
0162 {
0163     KQuickAddons::ManagedConfigModule::save();
0164 }
0165 
0166 RegionAndLangSettings *KCMRegionAndLang::settings() const
0167 {
0168     return m_settings;
0169 }
0170 
0171 OptionsModel *KCMRegionAndLang::optionsModel() const
0172 {
0173     return m_optionsModel;
0174 }
0175 
0176 void KCMRegionAndLang::unset(SettingType setting)
0177 {
0178     const char *entry = nullptr;
0179     if (setting == SettingType::Lang) {
0180         entry = "LANG";
0181         settings()->setLang(settings()->defaultLangValue());
0182     } else if (setting == SettingType::Numeric) {
0183         entry = "LC_NUMERIC";
0184         settings()->setNumeric(settings()->defaultNumericValue());
0185     } else if (setting == SettingType::Time) {
0186         entry = "LC_TIME";
0187         settings()->setTime(settings()->defaultTimeValue());
0188     } else if (setting == SettingType::Measurement) {
0189         entry = "LC_MEASUREMENT";
0190         settings()->setMeasurement(settings()->defaultMeasurementValue());
0191     } else if (setting == SettingType::Currency) {
0192         entry = "LC_MONETARY";
0193         settings()->setMonetary(settings()->defaultMonetaryValue());
0194     } else if (setting == SettingType::PaperSize) {
0195         entry = "LC_PAPER";
0196         settings()->setPaperSize(settings()->defaultPaperSizeValue());
0197     } else if (setting == SettingType::Address) {
0198         entry = "LC_ADDRESS";
0199         settings()->setAddress(settings()->defaultAddressValue());
0200     } else if (setting == SettingType::NameStyle) {
0201         entry = "LC_NAME";
0202         settings()->setNameStyle(settings()->defaultNameStyleValue());
0203     } else if (setting == SettingType::PhoneNumbers) {
0204         entry = "LC_TELEPHONE";
0205         settings()->setPhoneNumbers(settings()->defaultPhoneNumbersValue());
0206     }
0207 
0208     settings()->config()->group(QStringLiteral("Formats")).deleteEntry(entry);
0209 }
0210 
0211 void KCMRegionAndLang::reboot()
0212 {
0213     auto method = QDBusMessage::createMethodCall(QStringLiteral("org.kde.LogoutPrompt"),
0214                                                  QStringLiteral("/LogoutPrompt"),
0215                                                  QStringLiteral("org.kde.LogoutPrompt"),
0216                                                  QStringLiteral("promptReboot"));
0217     QDBusConnection::sessionBus().asyncCall(method);
0218 }
0219 
0220 bool KCMRegionAndLang::isGlibc()
0221 {
0222 #ifdef OS_UBUNTU
0223     return true;
0224 #elif defined(GLIBC_LOCALE)
0225     return true;
0226 #else
0227     return false;
0228 #endif
0229 }
0230 
0231 bool KCMRegionAndLang::enabled() const
0232 {
0233     return m_enabled;
0234 }
0235 
0236 #ifdef GLIBC_LOCALE
0237 std::optional<QString> KCMRegionAndLang::toGlibcLocale(const QString &lang)
0238 {
0239     static std::unordered_map<QString, QString> map = constructGlibcLocaleMap();
0240 
0241     if (map.count(lang)) {
0242         return map[lang];
0243     }
0244     return std::nullopt;
0245 }
0246 #endif
0247 
0248 QString KCMRegionAndLang::toUTF8Locale(const QString &locale)
0249 {
0250     if (locale.contains(QLatin1String("UTF-8"))) {
0251         return locale;
0252     }
0253 
0254     if (locale.contains(QLatin1Char('@'))) {
0255         // uz_UZ@cyrillic to uz_UZ.UTF-8@cyrillic
0256         auto localeDup = locale;
0257         localeDup.replace(QLatin1Char('@'), QLatin1String(".UTF-8@"));
0258         return localeDup;
0259     }
0260 
0261     return locale + QLatin1String(".UTF-8");
0262 }
0263 
0264 #ifdef GLIBC_LOCALE
0265 std::unordered_map<QString, QString> KCMRegionAndLang::constructGlibcLocaleMap()
0266 {
0267     std::unordered_map<QString, QString> localeMap;
0268 
0269     QDir glibcLocaleDir(localeFileDirPath());
0270     auto availableLocales = glibcLocaleDir.entryList(QDir::Files);
0271     // not glibc system or corrupted system
0272     if (availableLocales.isEmpty()) {
0273         if (m_localectl) {
0274             availableLocales = QString(m_localectl->readAllStandardOutput()).split('\n');
0275         }
0276         if (availableLocales.isEmpty()) {
0277             Q_EMIT encountedError(failedFindLocalesMessage());
0278             return localeMap;
0279         }
0280     }
0281 
0282     // map base locale code to actual glibc locale filename: "en" => ["en_US", "en_GB"]
0283     std::unordered_map<QString, std::vector<QString>> baseLocaleMap(availableLocales.size());
0284     for (const auto &glibcLocale : availableLocales) {
0285         // we want only absolute base locale code, for sr@ijekavian and en_US, we get sr and en
0286         auto baseLocale = glibcLocale.split('_')[0].split('@')[0];
0287         if (baseLocaleMap.count(baseLocale)) {
0288             baseLocaleMap[baseLocale].push_back(glibcLocale);
0289         } else {
0290             baseLocaleMap.insert({baseLocale, {glibcLocale}});
0291         }
0292     }
0293 
0294     auto plasmaLocales = KLocalizedString::availableDomainTranslations(QByteArrayLiteral("plasmashell")).values();
0295     for (const auto &plasmaLocale : plasmaLocales) {
0296         auto baseLocale = plasmaLocale.split('_')[0].split('@')[0];
0297         if (baseLocaleMap.count(baseLocale)) {
0298             const auto &prefixedLocales = baseLocaleMap[baseLocale];
0299 
0300             // if we have one to one match, use that. Eg. en_US to en_US
0301             auto fullMatch = std::find(prefixedLocales.begin(), prefixedLocales.end(), plasmaLocale);
0302             if (fullMatch != prefixedLocales.end()) {
0303                 localeMap.insert({plasmaLocale, toUTF8Locale(*fullMatch)});
0304                 continue;
0305             }
0306 
0307             // language name with same country code has higher priority, eg. es_ES > es_PA, de_DE > de_DE@euro
0308             auto mainLocale = plasmaLocale + "_" + plasmaLocale.toUpper();
0309             fullMatch = std::find(prefixedLocales.begin(), prefixedLocales.end(), mainLocale);
0310             if (fullMatch != prefixedLocales.end()) {
0311                 localeMap.insert({plasmaLocale, toUTF8Locale(*fullMatch)});
0312                 continue;
0313             }
0314 
0315             // we try to match the locale with least characters diff (compare language code with country code, "sv" with "SE".lower() for "sv_SE"),
0316             // so ca@valencia matches with ca_ES@valencia
0317             // bad case: ca matches with ca_AD but not ca_ES
0318             int closestMatchIndex = 0;
0319             float minDiffPercentage = 1.0;
0320             std::array<int, 255> frequencyMap = {0};
0321             for (QChar c : plasmaLocale) {
0322                 // to lower so "sv_SE" has higher priority than "sv_FI" for language "sv"
0323                 frequencyMap[int(c.toLower().toLatin1())]++;
0324             }
0325 
0326             int i = 0;
0327             for (const auto &glibcLocale : prefixedLocales) {
0328                 auto duplicated = frequencyMap;
0329                 int skipBase = baseLocale.size() + 1; // we skip "sv_" part of "sv_SE", eg. compare "SE" part with "sv"
0330                 for (QChar c : glibcLocale) {
0331                     if (skipBase--) {
0332                         continue;
0333                     }
0334                     duplicated[int(c.toLower().toLatin1())]--;
0335                 }
0336                 int diffChars = std::reduce(duplicated.begin(), duplicated.end(), 0, [](int sum, int diff) {
0337                     return sum + std::abs(diff);
0338                 });
0339                 float diffPercentage = float(diffChars) / glibcLocale.size();
0340                 if (diffPercentage < minDiffPercentage) {
0341                     minDiffPercentage = diffPercentage;
0342                     closestMatchIndex = i;
0343                 }
0344                 i++;
0345             }
0346             localeMap.insert({plasmaLocale, toUTF8Locale(prefixedLocales[closestMatchIndex])});
0347         }
0348     }
0349     return localeMap;
0350 }
0351 #endif
0352 
0353 #include "kcmregionandlang.moc"
0354 #include "moc_kcmregionandlang.cpp"