mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-03 20:47:43 +08:00
[feat] add system language support across desktop and mobile
This commit is contained in:
@@ -1,116 +1,91 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2024-05-29
|
||||
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
#ifndef _LOCALIZATION_H_
|
||||
#define _LOCALIZATION_H_
|
||||
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2024-05-29
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
#ifndef _LOCALIZATION_H_
|
||||
#define _LOCALIZATION_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "localization_data.h"
|
||||
#include "system_language.h"
|
||||
|
||||
#if _WIN32
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
namespace localization {
|
||||
|
||||
|
||||
namespace crossdesk {
|
||||
namespace localization {
|
||||
|
||||
struct LanguageOption {
|
||||
std::string code;
|
||||
std::string display_name;
|
||||
};
|
||||
|
||||
class LocalizedString {
|
||||
public:
|
||||
constexpr explicit LocalizedString(const char* key) : key_(key) {}
|
||||
const std::string& operator[](int language_index) const;
|
||||
|
||||
private:
|
||||
const char* key_;
|
||||
};
|
||||
|
||||
inline const std::vector<LanguageOption>& GetSupportedLanguages() {
|
||||
static const std::vector<LanguageOption> kSupportedLanguages = {
|
||||
{"zh-CN", reinterpret_cast<const char*>(u8"中文")},
|
||||
{"en-US", "English"},
|
||||
{"ru-RU", reinterpret_cast<const char*>(u8"Русский")}};
|
||||
return kSupportedLanguages;
|
||||
}
|
||||
|
||||
public:
|
||||
constexpr explicit LocalizedString(const char* key) : key_(key) {}
|
||||
const std::string& operator[](int language_index) const;
|
||||
|
||||
private:
|
||||
const char* key_;
|
||||
};
|
||||
|
||||
inline const std::vector<LanguageOption>& GetSupportedLanguages() {
|
||||
static const std::vector<LanguageOption> kSupportedLanguages = {
|
||||
{"zh-CN", "简体中文"}, {"en-US", "English"}, {"ru-RU", "Русский"},
|
||||
{"zh-Hant", "繁體中文"}, {"ja", "日本語"}, {"ko", "한국어"},
|
||||
{"de", "Deutsch"}, {"fr", "Français"}, {"es", "Español"},
|
||||
{"pt-BR", "Português (Brasil)"}};
|
||||
return kSupportedLanguages;
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
|
||||
inline int ClampLanguageIndex(int language_index) {
|
||||
if (language_index >= 0 &&
|
||||
language_index < static_cast<int>(GetSupportedLanguages().size())) {
|
||||
return language_index;
|
||||
}
|
||||
return 0;
|
||||
if (language_index == kSystemLanguage) return SystemLanguage();
|
||||
return language_index >= 0 && language_index < kSystemLanguage
|
||||
? language_index : kEnglishLanguage;
|
||||
}
|
||||
|
||||
using TranslationTable =
|
||||
std::unordered_map<std::string,
|
||||
std::unordered_map<std::string, std::string>>;
|
||||
|
||||
inline std::unordered_map<std::string, std::string> MakeLocalizedValues(
|
||||
const TranslationRow& row) {
|
||||
return {{"zh-CN", reinterpret_cast<const char*>(row.zh)},
|
||||
{"en-US", reinterpret_cast<const char*>(row.en)},
|
||||
{"ru-RU", reinterpret_cast<const char*>(row.ru)}};
|
||||
}
|
||||
|
||||
inline TranslationTable BuildTranslationTable() {
|
||||
TranslationTable table;
|
||||
for (const auto& row : kTranslationRows) {
|
||||
table[row.key] = MakeLocalizedValues(row);
|
||||
}
|
||||
|
||||
using TranslationTable = std::unordered_map<std::string, std::vector<std::string>>;
|
||||
inline const TranslationTable& LiteralTable() {
|
||||
static const TranslationTable table = [] {
|
||||
TranslationTable result;
|
||||
for (const auto& row : kTranslationRows)
|
||||
result.emplace(row.key, std::vector<std::string>(std::begin(row.text), std::end(row.text)));
|
||||
return result;
|
||||
}();
|
||||
return table;
|
||||
}
|
||||
|
||||
inline const TranslationTable& GetTranslationTable() {
|
||||
static const TranslationTable table = BuildTranslationTable();
|
||||
return table;
|
||||
}
|
||||
|
||||
inline const std::string& GetTranslatedText(const std::string& key,
|
||||
int language_index) {
|
||||
static const std::string kEmptyText = "";
|
||||
|
||||
const auto& table = GetTranslationTable();
|
||||
const auto key_it = table.find(key);
|
||||
if (key_it == table.end()) {
|
||||
return kEmptyText;
|
||||
}
|
||||
|
||||
const auto& localized_values = key_it->second;
|
||||
const std::string& language_code =
|
||||
GetSupportedLanguages()[ClampLanguageIndex(language_index)].code;
|
||||
|
||||
const auto exact_it = localized_values.find(language_code);
|
||||
if (exact_it != localized_values.end()) {
|
||||
return exact_it->second;
|
||||
}
|
||||
|
||||
const auto english_it = localized_values.find("en-US");
|
||||
if (english_it != localized_values.end()) {
|
||||
return english_it->second;
|
||||
}
|
||||
|
||||
const auto chinese_it = localized_values.find("zh-CN");
|
||||
if (chinese_it != localized_values.end()) {
|
||||
return chinese_it->second;
|
||||
}
|
||||
|
||||
return kEmptyText;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
inline const std::string& GetLiteralText(const std::string& key, int language) {
|
||||
static const std::string empty;
|
||||
const auto it = LiteralTable().find(key);
|
||||
if (it == LiteralTable().end()) return empty;
|
||||
const auto& values = it->second;
|
||||
return values[ClampLanguageIndex(language)];
|
||||
}
|
||||
inline const std::string& GetTranslatedText(const std::string& key, int language) {
|
||||
static const std::unordered_map<std::string, std::string> names = [] {
|
||||
std::unordered_map<std::string, std::string> result;
|
||||
for (const auto& row : kNamedTranslations) result.emplace(row.name, row.literal);
|
||||
return result;
|
||||
}();
|
||||
static const std::string empty;
|
||||
const auto it = names.find(key);
|
||||
return it == names.end() ? empty : GetLiteralText(it->second, language);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
inline const std::string& Literal(const std::string& key, int language) {
|
||||
return detail::GetLiteralText(key, language);
|
||||
}
|
||||
|
||||
inline const std::string& LocalizedString::operator[](
|
||||
int language_index) const {
|
||||
return detail::GetTranslatedText(key_, language_index);
|
||||
@@ -130,11 +105,11 @@ inline std::string FormatDisplayLabel(size_t display_index,
|
||||
return label;
|
||||
}
|
||||
|
||||
#define CROSSDESK_DECLARE_LOCALIZED_STRING(name, zh, en, ru) \
|
||||
inline const LocalizedString name(#name);
|
||||
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING)
|
||||
#undef CROSSDESK_DECLARE_LOCALIZED_STRING
|
||||
|
||||
#define CROSSDESK_DECLARE_LOCALIZED_STRING(name) \
|
||||
inline const LocalizedString name(#name);
|
||||
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING)
|
||||
#undef CROSSDESK_DECLARE_LOCALIZED_STRING
|
||||
|
||||
#if _WIN32
|
||||
inline const wchar_t* GetWindowsLocalizedLabel(
|
||||
int language_index, const char* key, const wchar_t* fallback,
|
||||
@@ -187,8 +162,8 @@ inline const wchar_t* GetExitProgramLabel(int language_index) {
|
||||
cache);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace localization
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace localization
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-27
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SYSTEM_LANGUAGE_H_
|
||||
#define _SYSTEM_LANGUAGE_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <Windows.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
namespace localization {
|
||||
|
||||
inline constexpr int kSystemLanguage = 10;
|
||||
inline constexpr int kEnglishLanguage = 1;
|
||||
|
||||
// Keep 0/1/2 compatible with existing Chinese/English/Russian config files.
|
||||
inline int MatchLanguage(std::string tag) {
|
||||
for (char& c : tag) {
|
||||
if (c == '_') c = '-';
|
||||
else if (c >= 'A' && c <= 'Z') c += 'a' - 'A';
|
||||
}
|
||||
tag = tag.substr(0, tag.find_first_of(".@"));
|
||||
const auto base = tag.substr(0, tag.find('-'));
|
||||
if (base == "zh") {
|
||||
const auto parts = "-" + tag + "-";
|
||||
if (parts.find("-hans-") != std::string::npos) return 0;
|
||||
return parts.find("-hant-") != std::string::npos ||
|
||||
parts.find("-tw-") != std::string::npos ||
|
||||
parts.find("-hk-") != std::string::npos ||
|
||||
parts.find("-mo-") != std::string::npos ? 3 : 0;
|
||||
}
|
||||
const char* codes[] = {"zh", "en", "ru", "zh", "ja", "ko", "de", "fr", "es", "pt"};
|
||||
for (int i = 1; i < kSystemLanguage; ++i)
|
||||
if (i != 3 && base == codes[i]) return i;
|
||||
if (base == "c" || base == "posix") return kEnglishLanguage;
|
||||
return -1;
|
||||
}
|
||||
|
||||
inline int MatchPreferredLanguages(const std::vector<std::string>& tags) {
|
||||
for (const auto& tag : tags) {
|
||||
const int match = MatchLanguage(tag);
|
||||
if (match >= 0) return match;
|
||||
}
|
||||
return kEnglishLanguage;
|
||||
}
|
||||
|
||||
inline int DetectSystemLanguage() {
|
||||
std::vector<std::string> tags;
|
||||
#if defined(_WIN32)
|
||||
ULONG count = 0, length = 0;
|
||||
GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &count, nullptr, &length);
|
||||
std::vector<wchar_t> names(length);
|
||||
if (length && GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &count, names.data(), &length)) {
|
||||
for (const wchar_t* name = names.data(); *name; name += wcslen(name) + 1) {
|
||||
std::string tag;
|
||||
for (const wchar_t* c = name; *c; ++c) tag += static_cast<char>(*c);
|
||||
tags.push_back(tag);
|
||||
}
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
if (CFArrayRef names = CFLocaleCopyPreferredLanguages()) {
|
||||
for (CFIndex i = 0; i < CFArrayGetCount(names); ++i) {
|
||||
char tag[128] = {};
|
||||
auto name = static_cast<CFStringRef>(CFArrayGetValueAtIndex(names, i));
|
||||
if (CFStringGetCString(name, tag, sizeof(tag), kCFStringEncodingUTF8)) tags.emplace_back(tag);
|
||||
}
|
||||
CFRelease(names);
|
||||
}
|
||||
#else
|
||||
// The Harmony desktop host supplies LANGUAGE from the system i18n API.
|
||||
const char* locale = std::getenv("LC_ALL");
|
||||
if (!locale || !*locale) locale = std::getenv("LC_MESSAGES");
|
||||
if (!locale || !*locale) locale = std::getenv("LANG");
|
||||
if (locale && (std::string(locale) == "C" || std::string(locale) == "POSIX")) return kEnglishLanguage;
|
||||
if (const char* preferred = std::getenv("LANGUAGE")) {
|
||||
std::string list(preferred);
|
||||
size_t start = 0;
|
||||
do {
|
||||
const auto end = list.find(':', start);
|
||||
tags.push_back(list.substr(start, end - start));
|
||||
if (end == std::string::npos) break;
|
||||
start = end + 1;
|
||||
} while (start < list.size());
|
||||
}
|
||||
if (locale) tags.emplace_back(locale);
|
||||
#endif
|
||||
return MatchPreferredLanguages(tags);
|
||||
}
|
||||
|
||||
inline int SystemLanguage() {
|
||||
// Language changes take effect on the next application launch; explicit
|
||||
// application preferences always win over this process-wide default.
|
||||
static const int language = DetectSystemLanguage();
|
||||
return language;
|
||||
}
|
||||
|
||||
} // namespace localization
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user