diff --git a/src/config_center/config_center.cpp b/src/config_center/config_center.cpp index 9b985a1..3a6260e 100644 --- a/src/config_center/config_center.cpp +++ b/src/config_center/config_center.cpp @@ -20,8 +20,14 @@ int ConfigCenter::Load() { return -1; } - language_ = static_cast( - ini_.GetLongValue(section_, "language", static_cast(language_))); + const long language_value = + ini_.GetLongValue(section_, "language", static_cast(language_)); + if (language_value < static_cast(LANGUAGE::CHINESE) || + language_value > static_cast(LANGUAGE::RUSSIAN)) { + language_ = LANGUAGE::ENGLISH; + } else { + language_ = static_cast(language_value); + } video_quality_ = static_cast(ini_.GetLongValue( section_, "video_quality", static_cast(video_quality_))); @@ -385,4 +391,4 @@ int ConfigCenter::SetFileTransferSavePath(const std::string& path) { std::string ConfigCenter::GetFileTransferSavePath() const { return file_transfer_save_path_; } -} // namespace crossdesk \ No newline at end of file +} // namespace crossdesk diff --git a/src/config_center/config_center.h b/src/config_center/config_center.h index 90df2ce..2258f71 100644 --- a/src/config_center/config_center.h +++ b/src/config_center/config_center.h @@ -15,7 +15,7 @@ namespace crossdesk { class ConfigCenter { public: - enum class LANGUAGE { CHINESE = 0, ENGLISH = 1 }; + enum class LANGUAGE { CHINESE = 0, ENGLISH = 1, RUSSIAN = 2 }; enum class VIDEO_QUALITY { LOW = 0, MEDIUM = 1, HIGH = 2 }; enum class VIDEO_FRAME_RATE { FPS_30 = 0, FPS_60 = 1 }; enum class VIDEO_ENCODE_FORMAT { H264 = 0, AV1 = 1 }; @@ -90,4 +90,4 @@ class ConfigCenter { std::string file_transfer_save_path_ = ""; }; } // namespace crossdesk -#endif \ No newline at end of file +#endif diff --git a/src/gui/assets/localization/localization.h b/src/gui/assets/localization/localization.h index e90206e..acb6560 100644 --- a/src/gui/assets/localization/localization.h +++ b/src/gui/assets/localization/localization.h @@ -1,246 +1,156 @@ -/* - * @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) 2024 by DI JUNKUN, All Rights Reserved. + */ +#ifndef _LOCALIZATION_H_ +#define _LOCALIZATION_H_ + #include +#include #include +#include "localization_data.h" + #if _WIN32 #include #endif + +namespace crossdesk { +namespace localization { + +struct LanguageOption { + std::string code; + std::string display_name; +}; -namespace crossdesk { +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& GetSupportedLanguages() { + static const std::vector kSupportedLanguages = { + {"zh-CN", reinterpret_cast(u8"中文")}, + {"en-US", "English"}, + {"ru-RU", reinterpret_cast(u8"Русский")}}; + return kSupportedLanguages; +} + +namespace detail { -namespace localization { +inline int ClampLanguageIndex(int language_index) { + if (language_index >= 0 && + language_index < static_cast(GetSupportedLanguages().size())) { + return language_index; + } + return 0; +} -static std::vector local_desktop = { - reinterpret_cast(u8"本桌面"), "Local Desktop"}; -static std::vector local_id = { - reinterpret_cast(u8"本机ID"), "Local ID"}; -static std::vector local_id_copied_to_clipboard = { - reinterpret_cast(u8"已复制到剪贴板"), "Copied to clipboard"}; -static std::vector password = { - reinterpret_cast(u8"密码"), "Password"}; -static std::vector max_password_len = { - reinterpret_cast(u8"最大6个字符"), "Max 6 chars"}; +using TranslationTable = + std::unordered_map>; -static std::vector remote_desktop = { - reinterpret_cast(u8"控制远程桌面"), "Control Remote Desktop"}; -static std::vector remote_id = { - reinterpret_cast(u8"对端ID"), "Remote ID"}; -static std::vector connect = { - reinterpret_cast(u8"连接"), "Connect"}; -static std::vector recent_connections = { - reinterpret_cast(u8"近期连接"), "Recent Connections"}; -static std::vector disconnect = { - reinterpret_cast(u8"断开连接"), "Disconnect"}; -static std::vector fullscreen = { - reinterpret_cast(u8"全屏"), " Fullscreen"}; -static std::vector show_net_traffic_stats = { - reinterpret_cast(u8"显示流量统计"), "Show Net Traffic Stats"}; -static std::vector hide_net_traffic_stats = { - reinterpret_cast(u8"隐藏流量统计"), "Hide Net Traffic Stats"}; -static std::vector video = { - reinterpret_cast(u8"视频"), "Video"}; -static std::vector audio = { - reinterpret_cast(u8"音频"), "Audio"}; -static std::vector data = {reinterpret_cast(u8"数据"), - "Data"}; -static std::vector total = { - reinterpret_cast(u8"总计"), "Total"}; -static std::vector in = {reinterpret_cast(u8"输入"), - "In"}; -static std::vector out = {reinterpret_cast(u8"输出"), - "Out"}; -static std::vector loss_rate = { - reinterpret_cast(u8"丢包率"), "Loss Rate"}; -static std::vector exit_fullscreen = { - reinterpret_cast(u8"退出全屏"), "Exit fullscreen"}; -static std::vector control_mouse = { - reinterpret_cast(u8"控制"), "Control"}; -static std::vector release_mouse = { - reinterpret_cast(u8"释放"), "Release"}; -static std::vector audio_capture = { - reinterpret_cast(u8"声音"), "Audio"}; -static std::vector mute = { - reinterpret_cast(u8" 静音"), " Mute"}; -static std::vector settings = { - reinterpret_cast(u8"设置"), "Settings"}; -static std::vector language = { - reinterpret_cast(u8"语言:"), "Language:"}; -static std::vector language_zh = { - reinterpret_cast(u8"中文"), "Chinese"}; -static std::vector language_en = { - reinterpret_cast(u8"英文"), "English"}; -static std::vector video_quality = { - reinterpret_cast(u8"视频质量:"), "Video Quality:"}; -static std::vector video_frame_rate = { - reinterpret_cast(u8"画面采集帧率:"), - "Video Capture Frame Rate:"}; -static std::vector video_quality_high = { - reinterpret_cast(u8"高"), "High"}; -static std::vector video_quality_medium = { - reinterpret_cast(u8"中"), "Medium"}; -static std::vector video_quality_low = { - reinterpret_cast(u8"低"), "Low"}; -static std::vector video_encode_format = { - reinterpret_cast(u8"视频编码格式:"), "Video Encode Format:"}; -static std::vector av1 = {reinterpret_cast(u8"AV1"), - "AV1"}; -static std::vector h264 = { - reinterpret_cast(u8"H.264"), "H.264"}; -static std::vector enable_hardware_video_codec = { - reinterpret_cast(u8"启用硬件编解码器:"), - "Enable Hardware Video Codec:"}; -static std::vector enable_turn = { - reinterpret_cast(u8"启用中继服务:"), "Enable TURN Service:"}; -static std::vector enable_srtp = { - reinterpret_cast(u8"启用SRTP:"), "Enable SRTP:"}; -static std::vector self_hosted_server_config = { - reinterpret_cast(u8"自托管服务器配置"), - "Self-Hosted Server Config"}; -static std::vector self_hosted_server_settings = { - reinterpret_cast(u8"自托管服务器设置"), - "Self-Hosted Server Settings"}; -static std::vector self_hosted_server_address = { - reinterpret_cast(u8"服务器地址:"), "Server Address:"}; -static std::vector self_hosted_server_port = { - reinterpret_cast(u8"信令服务端口:"), "Signal Service Port:"}; -static std::vector self_hosted_server_coturn_server_port = { - reinterpret_cast(u8"中继服务端口:"), "Relay Service Port:"}; -static std::vector select_a_file = { - reinterpret_cast(u8"请选择文件"), "Please select a file"}; -static std::vector ok = {reinterpret_cast(u8"确认"), - "OK"}; -static std::vector cancel = { - reinterpret_cast(u8"取消"), "Cancel"}; +inline std::unordered_map MakeLocalizedValues( + const TranslationRow& row) { + return {{"zh-CN", reinterpret_cast(row.zh)}, + {"en-US", row.en}, + {"ru-RU", reinterpret_cast(row.ru)}}; +} -static std::vector new_password = { - reinterpret_cast(u8"请输入六位密码:"), - "Please input a six-char password:"}; +inline TranslationTable BuildTranslationTable() { + TranslationTable table; + for (const auto& row : kTranslationRows) { + table[row.key] = MakeLocalizedValues(row); + } -static std::vector input_password = { - reinterpret_cast(u8"请输入密码:"), "Please input password:"}; -static std::vector validate_password = { - reinterpret_cast(u8"验证密码中..."), "Validate password ..."}; -static std::vector reinput_password = { - reinterpret_cast(u8"请重新输入密码"), - "Please input password again"}; - -static std::vector remember_password = { - reinterpret_cast(u8"记住密码"), "Remember password"}; - -static std::vector signal_connected = { - reinterpret_cast(u8"已连接服务器"), "Connected"}; -static std::vector signal_disconnected = { - reinterpret_cast(u8"未连接服务器"), "Disconnected"}; - -static std::vector p2p_connected = { - reinterpret_cast(u8"对等连接已建立"), "P2P Connected"}; -static std::vector p2p_disconnected = { - reinterpret_cast(u8"对等连接已断开"), "P2P Disconnected"}; -static std::vector p2p_connecting = { - reinterpret_cast(u8"正在建立对等连接..."), - "P2P Connecting ..."}; -static std::vector receiving_screen = { - reinterpret_cast(u8"画面接收中..."), "Receiving screen..."}; -static std::vector p2p_failed = { - reinterpret_cast(u8"对等连接失败"), "P2P Failed"}; -static std::vector p2p_closed = { - reinterpret_cast(u8"对等连接已关闭"), "P2P closed"}; - -static std::vector no_such_id = { - reinterpret_cast(u8"无此ID"), "No such ID"}; - -static std::vector about = { - reinterpret_cast(u8"关于"), "About"}; -static std::vector notification = { - reinterpret_cast(u8"通知"), "Notification"}; -static std::vector new_version_available = { - reinterpret_cast(u8"新版本可用"), "New Version Available"}; -static std::vector version = { - reinterpret_cast(u8"版本"), "Version"}; -static std::vector release_date = { - reinterpret_cast(u8"发布日期: "), "Release Date: "}; -static std::vector access_website = { - reinterpret_cast(u8"访问官网: "), "Access Website: "}; -static std::vector update = { - reinterpret_cast(u8"更新"), "Update"}; - -static std::vector confirm_delete_connection = { - reinterpret_cast(u8"确认删除此连接"), - "Confirm to delete this connection"}; - -static std::vector enable_autostart = { - reinterpret_cast(u8"开机自启:"), "Auto Start:"}; -static std::vector enable_daemon = { - reinterpret_cast(u8"启用守护进程:"), "Enable Daemon:"}; -static std::vector takes_effect_after_restart = { - reinterpret_cast(u8"重启后生效"), - "Takes effect after restart"}; -static std::vector select_file = { - reinterpret_cast(u8"选择文件"), "Select File"}; -static std::vector file_transfer_progress = { - reinterpret_cast(u8"文件传输进度"), "File Transfer Progress"}; -static std::vector queued = { - reinterpret_cast(u8"队列中"), "Queued"}; -static std::vector sending = { - reinterpret_cast(u8"正在传输"), "Sending"}; -static std::vector completed = { - reinterpret_cast(u8"已完成"), "Completed"}; -static std::vector failed = { - reinterpret_cast(u8"失败"), "Failed"}; -static std::vector controller = { - reinterpret_cast(u8"控制端:"), "Controller:"}; -static std::vector file_transfer = { - reinterpret_cast(u8"文件传输:"), "File Transfer:"}; -static std::vector connection_status = { - reinterpret_cast(u8"连接状态:"), "Connection Status:"}; -static std::vector file_transfer_save_path = { - reinterpret_cast(u8"文件接收保存路径:"), - "File Transfer Save Path:"}; -static std::vector browse = { - reinterpret_cast(u8"浏览"), "Browse"}; -static std::vector default_desktop = { - reinterpret_cast(u8"桌面"), "Desktop"}; -static std::vector minimize_to_tray = { - reinterpret_cast(u8"退出时最小化到系统托盘:"), - "Minimize to system tray when exit:"}; -static std::vector resolution = { - reinterpret_cast(u8"分辨率"), "Res"}; -static std::vector connection_mode = { - reinterpret_cast(u8"连接模式"), "Mode"}; -static std::vector connection_mode_direct = { - reinterpret_cast(u8"直连"), "Direct"}; -static std::vector connection_mode_relay = { - reinterpret_cast(u8"中继"), "Relay"}; -static std::vector online = { - reinterpret_cast(u8"在线"), "Online"}; -static std::vector offline = { - reinterpret_cast(u8"离线"), "Offline"}; -static std::vector device_offline = { - reinterpret_cast(u8"设备离线"), "Device Offline"}; - -#if _WIN32 -static std::vector exit_program = {L"退出", L"Exit"}; -#endif -#ifdef __APPLE__ -static std::vector request_permissions = { - reinterpret_cast(u8"权限请求"), "Request Permissions"}; -static std::vector screen_recording_permission = { - reinterpret_cast(u8"屏幕录制权限"), - "Screen Recording Permission"}; -static std::vector accessibility_permission = { - reinterpret_cast(u8"辅助功能权限"), - "Accessibility Permission"}; -static std::vector permission_required_message = { - reinterpret_cast(u8"该应用需要授权以下权限:"), - "The application requires the following permissions:"}; -#endif -} // namespace localization -} // namespace crossdesk -#endif \ No newline at end of file + 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& LocalizedString::operator[]( + int language_index) const { + return detail::GetTranslatedText(key_, language_index); +} + +#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 + +#if _WIN32 +inline const wchar_t* GetExitProgramLabel(int language_index) { + static std::vector cache(GetSupportedLanguages().size()); + const int normalized_index = detail::ClampLanguageIndex(language_index); + std::wstring& cached_text = cache[normalized_index]; + if (!cached_text.empty()) { + return cached_text.c_str(); + } + + const std::string& utf8_text = + detail::GetTranslatedText("exit_program", normalized_index); + if (utf8_text.empty()) { + cached_text = L"Exit"; + return cached_text.c_str(); + } + + int wide_length = + MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, nullptr, 0); + if (wide_length <= 0) { + cached_text = L"Exit"; + return cached_text.c_str(); + } + + cached_text.resize(static_cast(wide_length - 1)); + MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, cached_text.data(), + wide_length); + return cached_text.c_str(); +} +#endif + +} // namespace localization +} // namespace crossdesk + +#endif diff --git a/src/gui/assets/localization/localization_data.h b/src/gui/assets/localization/localization_data.h new file mode 100644 index 0000000..17ddb40 --- /dev/null +++ b/src/gui/assets/localization/localization_data.h @@ -0,0 +1,166 @@ +/* + * @Author: DI JUNKUN + * @Date: 2024-05-29 + * Copyright (c) 2024 by DI JUNKUN, All Rights Reserved. + */ +#ifndef _LOCALIZATION_DATA_H_ +#define _LOCALIZATION_DATA_H_ + +namespace crossdesk { +namespace localization { + +namespace detail { + +struct TranslationRow { + const char* key; + const char* zh; + const char* en; + const char* ru; +}; + +// Single source of truth for all UI strings. +#define CROSSDESK_LOCALIZATION_ALL(X) \ + X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \ + X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \ + X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \ + u8"Скопировано в буфер обмена") \ + X(password, u8"密码", "Password", u8"Пароль") \ + X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \ + X(remote_desktop, u8"远程桌面", "Remote Desktop", \ + u8"Удаленный рабочий стол") \ + X(remote_id, u8"对端ID", "Remote ID", u8"Удаленный ID") \ + X(connect, u8"连接", "Connect", u8"Подключиться") \ + X(recent_connections, u8"近期连接", "Recent Connections", \ + u8"Недавние подключения") \ + X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \ + X(fullscreen, u8"全屏", " Fullscreen", u8"Полный экран") \ + X(show_net_traffic_stats, u8"显示流量统计", "Show Net Traffic Stats", \ + u8"Показать статистику трафика") \ + X(hide_net_traffic_stats, u8"隐藏流量统计", "Hide Net Traffic Stats", \ + u8"Скрыть статистику трафика") \ + X(video, u8"视频", "Video", u8"Видео") \ + X(audio, u8"音频", "Audio", u8"Аудио") \ + X(data, u8"数据", "Data", u8"Данные") \ + X(total, u8"总计", "Total", u8"Итого") \ + X(in, u8"输入", "In", u8"Вход") \ + X(out, u8"输出", "Out", u8"Выход") \ + X(loss_rate, u8"丢包率", "Loss Rate", u8"Потери пакетов") \ + X(exit_fullscreen, u8"退出全屏", "Exit fullscreen", \ + u8"Выйти из полноэкранного режима") \ + X(control_mouse, u8"控制", "Control", u8"Управление") \ + X(release_mouse, u8"释放", "Release", u8"Освободить") \ + X(audio_capture, u8"声音", "Audio", u8"Звук") \ + X(mute, u8" 静音", " Mute", u8"Без звука") \ + X(settings, u8"设置", "Settings", u8"Настройки") \ + X(language, u8"语言:", "Language:", u8"Язык:") \ + X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \ + X(video_frame_rate, u8"画面采集帧率:", \ + "Video Capture Frame Rate:", u8"Частота захвата видео:") \ + X(video_quality_high, u8"高", "High", u8"Высокое") \ + X(video_quality_medium, u8"中", "Medium", u8"Среднее") \ + X(video_quality_low, u8"低", "Low", u8"Низкое") \ + X(video_encode_format, u8"视频编码格式:", \ + "Video Encode Format:", u8"Формат кодека видео:") \ + X(av1, u8"AV1", "AV1", "AV1") \ + X(h264, u8"H.264", "H.264", "H.264") \ + X(enable_hardware_video_codec, u8"启用硬件编解码器:", \ + "Enable Hardware Video Codec:", u8"Использовать аппаратный кодек:") \ + X(enable_turn, u8"启用中继服务:", \ + "Enable TURN Service:", u8"Включить TURN-сервис:") \ + X(enable_srtp, u8"启用SRTP:", "Enable SRTP:", u8"Включить SRTP:") \ + X(self_hosted_server_config, u8"自托管配置", "Self-Hosted Config", \ + u8"Конфигурация self-hosted") \ + X(self_hosted_server_settings, u8"自托管设置", "Self-Hosted Settings", \ + u8"Настройки self-hosted") \ + X(self_hosted_server_address, u8"服务器地址:", \ + "Server Address:", u8"Адрес сервера:") \ + X(self_hosted_server_port, u8"信令服务端口:", \ + "Signal Service Port:", u8"Порт сигнального сервиса:") \ + X(self_hosted_server_coturn_server_port, u8"中继服务端口:", \ + "Relay Service Port:", u8"Порт реле-сервиса:") \ + X(ok, u8"确认", "OK", u8"ОК") \ + X(cancel, u8"取消", "Cancel", u8"Отмена") \ + X(new_password, u8"请输入六位密码:", \ + "Please input a six-char password:", u8"Введите шестизначный пароль:") \ + X(input_password, u8"请输入密码:", \ + "Please input password:", u8"Введите пароль:") \ + X(validate_password, u8"验证密码中...", "Validate password ...", \ + u8"Проверка пароля...") \ + X(reinput_password, u8"请重新输入密码", "Please input password again", \ + u8"Повторно введите пароль") \ + X(remember_password, u8"记住密码", "Remember password", \ + u8"Запомнить пароль") \ + X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \ + X(signal_disconnected, u8"未连接服务器", "Disconnected", \ + u8"Нет подключения к серверу") \ + X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \ + X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \ + u8"P2P отключено") \ + X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \ + u8"Подключение P2P...") \ + X(receiving_screen, u8"画面接收中...", "Receiving screen...", \ + u8"Получение изображения...") \ + X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \ + X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \ + X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \ + X(about, u8"关于", "About", u8"О программе") \ + X(notification, u8"通知", "Notification", u8"Уведомление") \ + X(new_version_available, u8"新版本可用", "New Version Available", \ + u8"Доступна новая версия") \ + X(version, u8"版本", "Version", u8"Версия") \ + X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \ + X(access_website, u8"访问官网: ", \ + "Access Website: ", u8"Официальный сайт: ") \ + X(update, u8"更新", "Update", u8"Обновить") \ + X(confirm_delete_connection, u8"确认删除此连接", \ + "Confirm to delete this connection", u8"Удалить это подключение?") \ + X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \ + X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \ + X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \ + u8"Вступит в силу после перезапуска") \ + X(select_file, u8"选择文件", "Select File", u8"Выбрать файл") \ + X(file_transfer_progress, u8"文件传输进度", "File Transfer Progress", \ + u8"Прогресс передачи файлов") \ + X(queued, u8"队列中", "Queued", u8"В очереди") \ + X(sending, u8"正在传输", "Sending", u8"Передача") \ + X(completed, u8"已完成", "Completed", u8"Завершено") \ + X(failed, u8"失败", "Failed", u8"Ошибка") \ + X(controller, u8"控制端:", "Controller:", u8"Контроллер:") \ + X(file_transfer, u8"文件传输:", "File Transfer:", u8"Передача файлов:") \ + X(connection_status, u8"连接状态:", \ + "Connection Status:", u8"Состояние соединения:") \ + X(file_transfer_save_path, u8"文件接收保存路径:", \ + "File Transfer Save Path:", u8"Путь сохранения файлов:") \ + X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \ + X(minimize_to_tray, u8"退出时最小化到系统托盘:", \ + "Minimize on Exit:", u8"Сворачивать в трей при выходе:") \ + X(resolution, u8"分辨率", "Res", u8"Разрешение") \ + X(connection_mode, u8"连接模式", "Mode", u8"Режим") \ + X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \ + X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \ + X(online, u8"在线", "Online", u8"Онлайн") \ + X(offline, u8"离线", "Offline", u8"Офлайн") \ + X(device_offline, u8"设备离线", "Device Offline", u8"Устройство офлайн") \ + X(request_permissions, u8"权限请求", "Request Permissions", \ + u8"Запрос разрешений") \ + X(screen_recording_permission, u8"屏幕录制权限", \ + "Screen Recording Permission", u8"Разрешение на запись экрана") \ + X(accessibility_permission, u8"辅助功能权限", "Accessibility Permission", \ + u8"Разрешение специальных возможностей") \ + X(permission_required_message, u8"该应用需要授权以下权限:", \ + "The application requires the following permissions:", \ + u8"Для работы приложения требуются следующие разрешения:") \ + X(exit_program, u8"退出", "Exit", u8"Выход") + +inline constexpr TranslationRow kTranslationRows[] = { +#define CROSSDESK_DECLARE_TRANSLATION_ROW(name, zh, en, ru) {#name, zh, en, ru}, + CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_TRANSLATION_ROW) +#undef CROSSDESK_DECLARE_TRANSLATION_ROW +}; + +} // namespace detail + +} // namespace localization +} // namespace crossdesk + +#endif diff --git a/src/gui/render.cpp b/src/gui/render.cpp index 01de424..ae90908 100644 --- a/src/gui/render.cpp +++ b/src/gui/render.cpp @@ -36,6 +36,32 @@ namespace crossdesk { namespace { +const ImWchar* GetMultilingualGlyphRanges() { + static std::vector glyph_ranges; + if (glyph_ranges.empty()) { + ImGuiIO& io = ImGui::GetIO(); + ImFontGlyphRangesBuilder builder; + builder.AddRanges(io.Fonts->GetGlyphRangesDefault()); + builder.AddRanges(io.Fonts->GetGlyphRangesChineseFull()); + builder.AddRanges(io.Fonts->GetGlyphRangesCyrillic()); + + ImVector built_ranges; + builder.BuildRanges(&built_ranges); + glyph_ranges.assign(built_ranges.Data, + built_ranges.Data + built_ranges.Size); + } + return glyph_ranges.empty() ? nullptr : glyph_ranges.data(); +} + +bool CanReadFontFile(const char* font_path) { + if (!font_path) { + return false; + } + + std::ifstream font_file(font_path, std::ios::binary); + return font_file.good(); +} + #if defined(__linux__) && !defined(__APPLE__) inline bool X11GetDisplayAndWindow(SDL_Window* window, Display** display_out, ::Window* x11_window_out) { @@ -479,7 +505,8 @@ int Render::LoadSettingsFromCacheFile() { thumbnail_ = std::make_shared(cache_path_ + "/thumbnails/", aes128_key_, aes128_iv_); - language_button_value_ = (int)config_center_->GetLanguage(); + language_button_value_ = localization::detail::ClampLanguageIndex( + (int)config_center_->GetLanguage()); video_quality_button_value_ = (int)config_center_->GetVideoQuality(); video_frame_rate_button_value_ = (int)config_center_->GetVideoFrameRate(); video_encode_format_button_value_ = @@ -1195,78 +1222,88 @@ int Render::SetupFontAndStyle(ImFont** system_chinese_font_out) { io.IniFilename = NULL; // disable imgui.ini - // Load Fonts + // Build one merged atlas: UI font + icon font + multilingual fallback fonts. ImFontConfig config; config.FontDataOwnedByAtlas = false; - io.Fonts->AddFontFromMemoryTTF(OPPOSans_Regular_ttf, OPPOSans_Regular_ttf_len, - font_size, &config, - io.Fonts->GetGlyphRangesChineseFull()); - config.MergeMode = true; - static const ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_FA, 0}; - io.Fonts->AddFontFromMemoryTTF(fa_solid_900_ttf, fa_solid_900_ttf_len, 30.0f, - &config, icon_ranges); - - // Load system Chinese font as fallback config.MergeMode = false; - config.FontDataOwnedByAtlas = false; + if (system_chinese_font_out) { *system_chinese_font_out = nullptr; } + ImFont* ui_font = io.Fonts->AddFontFromMemoryTTF( + OPPOSans_Regular_ttf, OPPOSans_Regular_ttf_len, font_size, &config, + io.Fonts->GetGlyphRangesDefault()); + if (!ui_font) { + ui_font = io.Fonts->AddFontDefault(&config); + } + + if (!ui_font) { + LOG_WARN("Failed to initialize base UI font"); + ImGui::StyleColorsLight(); + return 0; + } + + ImFontConfig icon_config = config; + icon_config.MergeMode = true; + static const ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_FA, 0}; + io.Fonts->AddFontFromMemoryTTF(fa_solid_900_ttf, fa_solid_900_ttf_len, + font_size, &icon_config, icon_ranges); + #if defined(_WIN32) - // Windows: Try Microsoft YaHei (微软雅黑) first, then SimSun (宋体) - const char* font_paths[] = {"C:/Windows/Fonts/msyh.ttc", - "C:/Windows/Fonts/msyhbd.ttc", - "C:/Windows/Fonts/simsun.ttc", nullptr}; + // Cover CJK + Cyrillic on Windows. + const char* fallback_font_paths[] = { + "C:/Windows/Fonts/msyh.ttc", "C:/Windows/Fonts/msyhbd.ttc", + "C:/Windows/Fonts/simsun.ttc", "C:/Windows/Fonts/arial.ttf", + "C:/Windows/Fonts/segoeui.ttf", nullptr}; #elif defined(__APPLE__) - // macOS: Try PingFang SC first, then STHeiti - const char* font_paths[] = {"/System/Library/Fonts/PingFang.ttc", - "/System/Library/Fonts/STHeiti Light.ttc", - "/System/Library/Fonts/STHeiti Medium.ttc", - nullptr}; + // Cover CJK + Cyrillic on macOS. + const char* fallback_font_paths[] = { + "/System/Library/Fonts/PingFang.ttc", + "/System/Library/Fonts/Hiragino Sans GB.ttc", + "/System/Library/Fonts/Supplemental/Arial Unicode.ttf", + "/System/Library/Fonts/Supplemental/Arial.ttf", nullptr}; #else - // Linux: Try common Chinese fonts - const char* font_paths[] = { + // Cover CJK + Cyrillic on Linux. + const char* fallback_font_paths[] = { "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", - "/usr/share/fonts/truetype/arphic/uming.ttc", - "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc", nullptr}; + "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc", + "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + nullptr}; #endif - for (int i = 0; font_paths[i] != nullptr; i++) { - std::ifstream font_file(font_paths[i], std::ios::binary); - if (font_file.good()) { - font_file.close(); - if (!system_chinese_font_out) { - break; - } + ImFontConfig fallback_config = config; + fallback_config.MergeMode = true; + const ImWchar* multilingual_ranges = GetMultilingualGlyphRanges(); + bool merged_multilingual_font = false; - *system_chinese_font_out = - io.Fonts->AddFontFromFileTTF(font_paths[i], font_size, &config, - io.Fonts->GetGlyphRangesChineseFull()); - if (*system_chinese_font_out != nullptr) { - // Merge FontAwesome icons into the Chinese font - config.MergeMode = true; - static const ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_FA, 0}; - io.Fonts->AddFontFromMemoryTTF(fa_solid_900_ttf, fa_solid_900_ttf_len, - font_size, &config, icon_ranges); - config.MergeMode = false; - LOG_INFO("Loaded system Chinese font with icons: {}", font_paths[i]); - break; + for (int i = 0; fallback_font_paths[i] != nullptr; ++i) { + const char* font_path = fallback_font_paths[i]; + if (!CanReadFontFile(font_path)) { + continue; + } + + ImFont* merged_font = io.Fonts->AddFontFromFileTTF( + font_path, font_size, &fallback_config, multilingual_ranges); + if (merged_font != nullptr) { + merged_multilingual_font = true; + if (system_chinese_font_out && *system_chinese_font_out == nullptr) { + *system_chinese_font_out = merged_font; } + LOG_INFO("Merged multilingual fallback font: {}", font_path); } } - // If no system font found, use default font + if (!merged_multilingual_font) { + LOG_WARN( + "No multilingual fallback fonts found, non-ASCII text may not render"); + } + + io.FontDefault = ui_font; if (system_chinese_font_out && *system_chinese_font_out == nullptr) { - *system_chinese_font_out = io.Fonts->AddFontDefault(&config); - // Merge FontAwesome icons into the default font - config.MergeMode = true; - static const ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_FA, 0}; - io.Fonts->AddFontFromMemoryTTF(fa_solid_900_ttf, fa_solid_900_ttf_len, - font_size, &config, icon_ranges); - config.MergeMode = false; - LOG_WARN("System Chinese font not found, using default font with icons"); + *system_chinese_font_out = ui_font; } ImGui::StyleColorsLight(); @@ -1439,10 +1476,10 @@ int Render::Run() { if (!latest_version_info_.empty() && latest_version_info_.contains("version") && latest_version_info_["version"].is_string()) { - latest_version_ = latest_version_info_["version"]; + latest_version_ = 'v' + latest_version_info_["version"].get(); if (latest_version_info_.contains("releaseNotes") && latest_version_info_["releaseNotes"].is_string()) { - release_notes_ = latest_version_info_["releaseNotes"]; + release_notes_ = latest_version_info_["releaseNotes"].get(); } else { release_notes_ = ""; } @@ -1503,12 +1540,16 @@ void Render::InitializeLogger() { InitLogger(exec_log_path_); } void Render::InitializeSettings() { LoadSettingsFromCacheFile(); - localization_language_ = (ConfigCenter::LANGUAGE)language_button_value_; - localization_language_index_ = language_button_value_; - if (localization_language_index_ != 0 && localization_language_index_ != 1) { - localization_language_index_ = 0; - LOG_ERROR("Invalid language index: [{}], use [0] by default", - localization_language_index_); + localization_language_index_ = + localization::detail::ClampLanguageIndex(language_button_value_); + language_button_value_ = localization_language_index_; + + if (localization_language_index_ == 0) { + localization_language_ = ConfigCenter::LANGUAGE::CHINESE; + } else if (localization_language_index_ == 1) { + localization_language_ = ConfigCenter::LANGUAGE::ENGLISH; + } else { + localization_language_ = ConfigCenter::LANGUAGE::RUSSIAN; } } @@ -2559,4 +2600,4 @@ void Render::ProcessFileDropEvent(const SDL_Event& event) { // Handle the dropped file on server window as needed } } -} // namespace crossdesk \ No newline at end of file +} // namespace crossdesk diff --git a/src/gui/tray/win_tray.cpp b/src/gui/tray/win_tray.cpp index 3119bb6..1f93170 100644 --- a/src/gui/tray/win_tray.cpp +++ b/src/gui/tray/win_tray.cpp @@ -89,7 +89,7 @@ bool WinTray::HandleTrayMessage(MSG* msg) { GetCursorPos(&pt); HMENU menu = CreatePopupMenu(); AppendMenuW(menu, MF_STRING, 1001, - localization::exit_program[language_index_]); + localization::GetExitProgramLabel(language_index_)); SetForegroundWindow(hwnd_message_only_); int cmd = @@ -112,4 +112,4 @@ bool WinTray::HandleTrayMessage(MSG* msg) { } return true; } -} // namespace crossdesk \ No newline at end of file +} // namespace crossdesk diff --git a/src/gui/windows/about_window.cpp b/src/gui/windows/about_window.cpp index a6313f6..ec95646 100644 --- a/src/gui/windows/about_window.cpp +++ b/src/gui/windows/about_window.cpp @@ -49,8 +49,9 @@ bool Render::OpenUrl(const std::string& url) { void Render::Hyperlink(const std::string& label, const std::string& url, const float window_width) { ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 0, 255, 255)); - ImGui::SetCursorPosX(window_width * 0.1f); - ImGui::Text("%s", label.c_str()); + ImGui::SetCursorPosX((window_width - ImGui::CalcTextSize(label.c_str()).x) / + 2.0f); + ImGui::TextUnformatted(label.c_str()); ImGui::PopStyleColor(); if (ImGui::IsItemHovered()) { @@ -71,7 +72,7 @@ int Render::AboutWindow() { float about_window_width = title_bar_button_width_ * 7.5f; float about_window_height = latest_version_.empty() ? title_bar_button_width_ * 4.0f - : title_bar_button_width_ * 4.6f; + : title_bar_button_width_ * 4.9f; const ImGuiViewport* viewport = ImGui::GetMainViewport(); ImGui::SetNextWindowPos(ImVec2( @@ -106,12 +107,14 @@ int Render::AboutWindow() { ImGui::Text("%s", text.c_str()); if (update_available_) { - std::string latest_version = + std::string new_version_available = localization::new_version_available[localization_language_index_] + - ": " + latest_version_; + ": "; + ImGui::SetCursorPosX(about_window_width * 0.1f); + ImGui::Text("%s", new_version_available.c_str()); std::string access_website = localization::access_website[localization_language_index_]; - Hyperlink(latest_version, "https://crossdesk.cn", about_window_width); + Hyperlink(latest_version_, "https://crossdesk.cn", about_window_width); } ImGui::Text(""); @@ -124,7 +127,7 @@ int Render::AboutWindow() { ImGui::Text("%s", license_text.c_str()); ImGui::SetCursorPosX(about_window_width * 0.445f); - ImGui::SetCursorPosY(about_window_height * 0.75f); + ImGui::SetCursorPosY(about_window_height * 0.8f); // OK if (ImGui::Button(localization::ok[localization_language_index_].c_str())) { show_about_window_ = false; diff --git a/src/gui/windows/main_settings_window.cpp b/src/gui/windows/main_settings_window.cpp index 6fa423c..691badc 100644 --- a/src/gui/windows/main_settings_window.cpp +++ b/src/gui/windows/main_settings_window.cpp @@ -60,9 +60,9 @@ int Render::SettingWindow() { ImGui::SetWindowFontScale(0.5f); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f); { - const char* language_items[] = { - localization::language_zh[localization_language_index_].c_str(), - localization::language_en[localization_language_index_].c_str()}; + const auto& supported_languages = localization::GetSupportedLanguages(); + language_button_value_ = + localization::detail::ClampLanguageIndex(language_button_value_); settings_items_offset += settings_items_padding; ImGui::SetCursorPosY(settings_items_offset); @@ -77,13 +77,23 @@ int Render::SettingWindow() { } ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f); - if (ImGui::BeginCombo("##language", - language_items[language_button_value_])) { + if (ImGui::BeginCombo( + "##language", + localization::GetSupportedLanguages() + [localization::detail::ClampLanguageIndex( + language_button_value_)] + .display_name + .c_str())) { ImGui::SetWindowFontScale(0.5f); - for (int i = 0; i < IM_ARRAYSIZE(language_items); i++) { + for (int i = 0; i < static_cast(supported_languages.size()); + ++i) { bool selected = (i == language_button_value_); - if (ImGui::Selectable(language_items[i], selected)) + if (ImGui::Selectable( + supported_languages[i].display_name.c_str(), selected)) language_button_value_ = i; + if (selected) { + ImGui::SetItemDefaultFocus(); + } } ImGui::EndCombo(); @@ -438,16 +448,24 @@ int Render::SettingWindow() { show_self_hosted_server_config_window_ = false; // Language + language_button_value_ = + localization::detail::ClampLanguageIndex(language_button_value_); if (language_button_value_ == 0) { - config_center_->SetLanguage(ConfigCenter::LANGUAGE::CHINESE); + localization_language_ = ConfigCenter::LANGUAGE::CHINESE; + } else if (language_button_value_ == 1) { + localization_language_ = ConfigCenter::LANGUAGE::ENGLISH; } else { - config_center_->SetLanguage(ConfigCenter::LANGUAGE::ENGLISH); + localization_language_ = ConfigCenter::LANGUAGE::RUSSIAN; } + config_center_->SetLanguage(localization_language_); language_button_value_last_ = language_button_value_; - localization_language_ = (ConfigCenter::LANGUAGE)language_button_value_; localization_language_index_ = language_button_value_; LOG_INFO("Set localization language: {}", - localization_language_index_ == 0 ? "zh" : "en"); + localization::GetSupportedLanguages() + [localization::detail::ClampLanguageIndex( + localization_language_index_)] + .code + .c_str()); // Video quality if (video_quality_button_value_ == 0) { @@ -602,4 +620,4 @@ int Render::SettingWindow() { return 0; } -} // namespace crossdesk \ No newline at end of file +} // namespace crossdesk