// SPDX-FileCopyrightText: 2026 Reion Wong // SPDX-License-Identifier: BSD-3-Clause #include "pages/language/language.h" #include #include #include #include #include #include #include #include "base/file.h" #include "base/i18n.h" #include "base/locale.h" #include "base/paths.h" #include "base/settings.h" using namespace cutefish::base; using cutefish::i18n::tr; namespace settings { static constexpr const char* kService = "org.cutefish.Services"; static constexpr const char* kPath = "/org/cutefish/Services/Language"; static constexpr const char* kInterface = "org.cutefish.Services.Language"; static std::string displayName(const icu::Locale& locale, const icu::Locale& displayLocale) { icu::UnicodeString name; locale.getDisplayName(displayLocale, name); std::string utf8; name.toUTF8String(utf8); return utf8; } static icu::UnicodeString folded(const std::string& text) { icu::UnicodeString string = icu::UnicodeString::fromUTF8(text); string.foldCase(); return string; } static std::string trimmed(const std::string& text) { const size_t start = text.find_first_not_of(" \t\n\r\f\v"); if (start == std::string::npos) return {}; return text.substr(start, text.find_last_not_of(" \t\n\r\f\v") - start + 1); } std::string Language::systemLanguage() { std::string language; if (const auto contents = file::read("/etc/locale.gen")) { std::istringstream lines(*contents); std::string line; while (std::getline(lines, line)) { if (line.starts_with('#') || trimmed(line).empty()) continue; const std::string first = line.substr(0, line.find(' ')); language = first.substr(0, first.find('.')); } } return language.empty() ? "en_US" : language; } std::string Language::defaultConfigPath() { return paths::cutefishConfig("services"); } std::vector Language::preferredLanguageCodes(const std::string& configPath) { Settings settings(configPath); const std::string primary = settings.stringValue("Language/language", {}); if (primary.empty()) return {systemLanguage()}; std::vector codes{primary}; for (const std::string& code : settings.stringListValue("Language/preferredLanguages", {})) { if (!code.empty() && std::find(codes.begin(), codes.end(), code) == codes.end()) codes.push_back(code); } return codes; } std::string Language::displayLanguage() { return cutefish::base::Locale::system().name(); } std::vector Language::catalog(const std::string& displayCode) { static const char* const supportedSystemLanguages[] = { "en_US", // English (US) "en_GB", // English (UK) "ja_JP", // Japanese "ko_KR", // Korean "fr_FR", // French "de_DE", // German "es_ES", // Spanish (Spain) "pt_BR", // Portuguese (Brazil) "ru_RU", // Russian "it_IT", // Italian "id_ID", // Indonesian "vi_VN", // Vietnamese "th_TH", // Thai "ms_MY", // Malay "hi_IN", // Hindi "bn_BD", // Bangla "tr_TR", // Turkish "uk_UA", // Ukrainian "pl_PL", // Polish "nl_NL", // Dutch "fr_CA", // French (Canada) "pt_PT", // Portuguese (Portugal) "es_419", // Spanish (Latin America) "es_MX", // Spanish (Mexico) "cs_CZ", // Czech "da_DK", // Danish "fi_FI", // Finnish "sv_SE", // Swedish "nb_NO", // Norwegian Bokmål "hu_HU", // Hungarian "ro_RO", // Romanian "sk_SK", // Slovak "sl_SI", // Slovenian "hr_HR", // Croatian "bg_BG", // Bulgarian "ca_ES", // Catalan "el_GR", // Greek "ar", // Arabic "he_IL", // Hebrew "zh_Hans_CN", // Chinese, Simplified "zh_Hant_HK", // Chinese, Traditional (Hong Kong) "zh_Hant_TW", // Chinese, Traditional (Taiwan) }; std::vector entries; const icu::Locale displayLocale(displayCode.c_str()); for (const char* code : supportedSystemLanguages) { const icu::Locale locale(code); if (locale.isBogus() || !*locale.getLanguage()) continue; LanguageEntry entry; entry.code = code; entry.name = displayName(locale, locale); entry.description = displayName(locale, displayLocale); entry.searchText = entry.name + ' ' + entry.description + ' ' + entry.code; entries.push_back(std::move(entry)); } return entries; } // Built once per display language and process, so reopening the page // repeats no ICU work, even when the page that asked first closed meanwhile. static std::mutex s_catalogMutex; static std::map>> s_catalogs; Language::Language(std::string configPath) : m_configPath(std::move(configPath)) , m_service(dbus::Connection::session(), kService, kPath, kInterface) { reload(); m_changedMatch = m_service.onSignal("languageChanged", [this](const dbus::Message&) { reload(); }); } std::vector Language::availableLanguages() const { const icu::UnicodeString needle = folded(m_search); std::vector entries; for (const LanguageEntry& entry : m_catalog) { if (std::find(m_preferredCodes.begin(), m_preferredCodes.end(), entry.code) != m_preferredCodes.end()) continue; if (!needle.isEmpty() && folded(entry.searchText).indexOf(needle) < 0) continue; entries.push_back(entry); } return entries; } void Language::loadLanguages() { if (m_loading || m_loaded) return; const std::string displayCode = displayLanguage(); { std::lock_guard lock(s_catalogMutex); if (const auto cached = s_catalogs.find(displayCode); cached != s_catalogs.end()) { m_catalog = *cached->second; m_loaded = true; availableLanguagesChanged.emit(); return; } } m_loading = true; loadingChanged.emit(); m_catalogJob = runInThread( [displayCode] { auto entries = std::make_shared>(catalog(displayCode)); std::lock_guard lock(s_catalogMutex); return s_catalogs.emplace(displayCode, std::move(entries)).first->second; }, [this](std::shared_ptr> entries) { m_catalog = *entries; m_loaded = true; m_loading = false; loadingChanged.emit(); availableLanguagesChanged.emit(); }); } void Language::search(const std::string& text) { const std::string search = trimmed(text); if (search == m_search) return; m_search = search; availableLanguagesChanged.emit(); } void Language::reload() { setPreferredCodes(preferredLanguageCodes(m_configPath)); } void Language::setPreferredCodes(std::vector codes) { if (codes == m_preferredCodes) return; m_preferredCodes = std::move(codes); m_preferred.clear(); const icu::Locale displayLocale(displayLanguage().c_str()); for (const std::string& code : m_preferredCodes) { const icu::Locale locale(code.c_str()); m_preferred.push_back({code, displayName(locale, locale), displayName(locale, displayLocale), {}}); } preferredLanguagesChanged.emit(); availableLanguagesChanged.emit(); } void Language::addLanguage(const std::string& code) { const bool known = std::any_of(m_catalog.begin(), m_catalog.end(), [&code](const LanguageEntry& entry) { return entry.code == code; }); if (!known || std::find(m_preferredCodes.begin(), m_preferredCodes.end(), code) != m_preferredCodes.end()) return; std::vector codes = m_preferredCodes; codes.push_back(code); saveLanguages(codes); } void Language::removeLanguage(int index) { if (m_preferredCodes.size() <= 1 || index < 0 || index >= int(m_preferredCodes.size())) return; std::vector codes = m_preferredCodes; codes.erase(codes.begin() + index); saveLanguages(codes); } void Language::moveLanguage(int from, int to) { const int count = int(m_preferredCodes.size()); if (from < 0 || to < 0 || from >= count || to >= count || from == to) return; std::vector codes = m_preferredCodes; std::string code = codes[from]; codes.erase(codes.begin() + from); codes.insert(codes.begin() + to, std::move(code)); saveLanguages(codes); } void Language::saveLanguages(const std::vector& codes) { if (m_saving) return; m_errorMessage.clear(); errorMessageChanged.emit(); m_saving = true; savingChanged.emit(); if (!m_service.isValid()) { m_errorMessage = tr("Could not save preferred languages. Please try again."); errorMessageChanged.emit(); m_saving = false; savingChanged.emit(); return; } m_saveCall = m_service.callAsync("setPreferredLanguages", [this, codes](const dbus::Reply& reply) { if (reply.isError()) { m_errorMessage = tr("Could not save preferred languages. Please try again."); errorMessageChanged.emit(); } else { setPreferredCodes(codes); } m_saving = false; savingChanged.emit(); }, codes); } } // namespace settings