diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index 2476beeb..d9739f1e 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -4544,7 +4544,7 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) { if (!wall_settings || (wall_changed && !updated_wall.Save(ui_->gallery_path))) { main->set_gallery_settings_error(UiText( localization_language_index_ == 0 ? "保存失败,请重试" - : localization_language_index_ == 2 ? "Не удалось сохранить" + : localization_language_index_ == 2 ? "Не удалось сохранить. Повторите попытку" : "Could not save; try again")); main->set_settings_tab(1); return false; @@ -4564,9 +4564,9 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) { if (wall_changed && !ui_->gallery.Save(ui_->gallery_path)) LOG_ERROR("Failed to restore DeskHub settings after IP direct save failure"); main->set_settings_save_error(UiText( - localization_language_index_ == 0 ? "无法保存 IP 直连设置,请重试。" - : localization_language_index_ == 2 ? "Не удалось сохранить настройки IP. Повторите попытку." - : "Could not save direct IP settings. Try again.")); + localization_language_index_ == 0 ? "无法保存 IP 连接设置,请重试。" + : localization_language_index_ == 2 ? "Не удалось сохранить настройки подключения по IP. Повторите попытку." + : "Could not save IP connection settings. Try again.")); return false; } #if defined(_WIN32) || defined(__APPLE__) || (defined(__linux__) && !defined(__OHOS__)) @@ -4575,8 +4575,8 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) { LOG_ERROR("Failed to restore DeskHub settings after lock preference save failure"); main->set_settings_save_error(UiText( localization_language_index_ == 0 ? "无法保存断开后锁屏设置,请重试。" - : localization_language_index_ == 2 ? "Не удалось сохранить настройку блокировки. Повторите попытку." - : "Could not save lock screen settings. Try again.")); + : localization_language_index_ == 2 ? "Не удалось сохранить настройку блокировки при отключении. Повторите попытку." + : "Could not save lock-on-disconnect settings. Try again.")); return false; } #endif @@ -5311,8 +5311,8 @@ bool GuiApplication::AddGalleryDevice(const std::string& input, password.find_first_not_of( "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") != std::string::npos) - return error("请输入不超过 6 位的字母或数字密码", - "Use a password of 1–6 letters or digits", + return error("请输入 1–6 位英文字母或数字密码", + "Use 1–6 letters (A–Z, a–z) or digits (0–9)", "Пароль: 1–6 латинских букв или цифр"); if (!signal_connected_) return error("尚未连接服务器,请稍后重试", @@ -5330,7 +5330,7 @@ bool GuiApplication::AddGalleryDevice(const std::string& input, updated.Add(id, Trim(group)); if (!updated.Save(ui_->gallery_path)) return error("保存失败,请重试", "Could not save; try again", - "Не удалось сохранить"); + "Не удалось сохранить. Повторите попытку"); ui_->gallery = std::move(updated); ui_->gallery_hidden_active_ids.erase(id); if (auto props = FindRemoteSession(id)) { @@ -5373,7 +5373,7 @@ bool GuiApplication::ImportGalleryRecent(const std::string& input_group) { if (!updated.Save(ui_->gallery_path)) { ui_->wall->set_gallery_notice(UiText( localization_language_index_ == 0 ? "保存失败,请重试" - : localization_language_index_ == 2 ? "Не удалось сохранить" + : localization_language_index_ == 2 ? "Не удалось сохранить. Повторите попытку" : "Could not save; try again")); return false; } @@ -5455,7 +5455,7 @@ bool GuiApplication::SaveDeviceGroups(const std::string& new_group, if (!updated.SetGroups(id, names) || !updated.Save(ui_->gallery_path)) { main->set_device_group_error(UiText( localization_language_index_ == 0 ? "保存失败,请重试" - : localization_language_index_ == 2 ? "Не удалось сохранить" + : localization_language_index_ == 2 ? "Не удалось сохранить. Повторите попытку" : "Could not save; try again")); return false; } @@ -5715,7 +5715,7 @@ void GuiApplication::GalleryAction(const std::string& action, SyncGallery(); }; if (action == "file") { - OpenFileDialog(std::string(tr("选择分发文件", "Select a file", "Выберите файл")), + OpenFileDialog(std::string(tr("选择分发文件", "Select a file to send", "Выберите файл для отправки")), [enqueue](std::string file) { if (!file.empty()) enqueue(file); }); @@ -6036,9 +6036,9 @@ void GuiApplication::SyncGallery() { device.id = UiText(FormatPeerId(id.c_str())); const auto mode = props->traversal_mode_.load(); device.mode = UiText(mode == TraversalMode::P2P - ? tr("直连", "Direct", "Напрямую") + ? tr("直连", "Direct", "Прямой") : mode == TraversalMode::UnknownMode - ? tr("等待统计", "Awaiting stats", "Ждём данные") + ? tr("等待统计", "Awaiting stats", "Ожидание статистики") : tr("中继", "Relay", "Ретрансляция")); device.inbound = device.outbound = device.loss = "—"; device.video_latency = "—"; @@ -6301,7 +6301,7 @@ void GuiApplication::SyncGallery() { }); // A queued retry takes precedence over errors from the previous session. if (queued) - tile.status = UiText(tr("排队中", "Queued", "В очереди")); + tile.status = UiText(tr("队列中", "Queued", "В очереди")); else if (props && props->connection_auth_.Get().password_required) tile.status = UiText(tr("需要密码", "Password required", "Нужен пароль")); else if (props && !tile.connected) diff --git a/apps/desktop/src/gui/assets/localization/localization_data.h b/apps/desktop/src/gui/assets/localization/localization_data.h index 6951aaf9..d9191801 100644 --- a/apps/desktop/src/gui/assets/localization/localization_data.h +++ b/apps/desktop/src/gui/assets/localization/localization_data.h @@ -21,23 +21,30 @@ struct TranslationRow { // Single source of truth for all UI strings. #define CROSSDESK_LOCALIZATION_ALL(X) \ + X(window_minimize, u8"最小化", "Minimize", u8"Свернуть") \ + X(window_maximize, u8"最大化", "Maximize", u8"Развернуть") \ + X(window_restore, u8"还原窗口", "Restore window", u8"Восстановить окно") \ + X(window_close, u8"关闭窗口", "Close window", u8"Закрыть окно") \ + X(save, u8"保存", "Save", u8"Сохранить") \ + X(copyright_notice, u8"© 2026 JUNKUN DI。保留所有权利。", "© 2026 by JUNKUN DI. All rights reserved.", u8"© 2026 JUNKUN DI. Все права защищены.") \ + X(license_notice, u8"依据 GNU GPL v3 许可发布。", "Licensed under GNU GPL v3.", u8"Распространяется по лицензии GNU GPL v3.") \ X(messages, u8"消息", "Messages", u8"Сообщения") \ X(message_unread, u8"未读", "Unread", u8"Не прочитано") \ X(message_read, u8"已读", "Read", u8"Прочитано") \ X(message_read_all, u8"全部已读", "Mark all as read", u8"Отметить всё прочитанным") \ X(message_empty, u8"暂无公告", "No announcements", u8"Нет объявлений") \ - X(message_select, u8"选择公告查看详情", "Select an announcement to read", u8"Выберите объявление") \ + X(message_select, u8"选择公告查看详情", "Select an announcement to read", u8"Выберите объявление для просмотра") \ X(message_loading, u8"正在加载…", "Loading…", u8"Загрузка…") \ - X(message_read_save_error, u8"无法保存本地阅读状态,请重试。", "Unable to save reading status locally. Please try again.", u8"Не удалось сохранить статус чтения. Повторите попытку.") \ + X(message_read_save_error, u8"无法保存本地阅读状态,请重试。", "Unable to save reading status locally. Please try again.", u8"Не удалось сохранить статус чтения локально. Повторите попытку.") \ X(message_close, u8"关闭", "Close", u8"Закрыть") \ X(message_delete, u8"删除", "Delete", u8"Удалить") \ X(message_delete_title, u8"删除公告", "Delete announcement", u8"Удалить объявление") \ X(message_delete_confirmation, u8"删除这条公告?", "Delete this announcement?", u8"Удалить это объявление?") \ X(message_delete_error, u8"无法删除这条公告,请重试。", "Unable to delete this announcement. Please try again.", u8"Не удалось удалить объявление. Повторите попытку.") \ X(console_cli_help, u8"Linux 无头运行选项:\n --headless 自动选择已有桌面并启用控制台\n --headless-display DISPLAY 高级:指定已有显示会话\n --headless-size WIDTHxHEIGHT 新建虚拟屏幕;默认1920x1080,偶数尺寸320..8192\n --headless-session EXECUTABLE 启动桌面环境(startxfce4;none 仅启动裸屏幕)\n --no-headless 禁用自动回退\n正常启动会自动选择已有桌面,无需填写显示编号。控制台输入 settings 管理设置。\n", "Linux headless options:\n --headless Select an existing desktop and enable the console\n --headless-display DISPLAY Advanced: override desktop selection\n --headless-size WIDTHxHEIGHT New virtual display; default 1920x1080, even 320..8192\n --headless-session EXECUTABLE Start a desktop (startxfce4; none for a bare display)\n --no-headless Disable automatic fallback\nNormal startup selects your existing desktop automatically. Enter settings in the console to configure it.\n", u8"Параметры запуска без монитора:\n --headless Выбрать рабочий стол и включить консоль\n --headless-display DISPLAY Дополнительно: указать сеанс дисплея\n --headless-size WIDTHxHEIGHT Новый виртуальный экран; 1920x1080 по умолчанию, чётные размеры 320..8192\n --headless-session EXECUTABLE Запустить рабочий стол (startxfce4; none — только экран)\n --no-headless Отключить автоматический резервный запуск\nРабочий стол выбирается автоматически. Команда settings открывает настройки консоли.\n") \ - X(console_title, u8"CrossDesk 无头控制台", "CrossDesk headless console", u8"Консоль CrossDesk") \ + X(console_title, u8"CrossDesk 无头控制台", "CrossDesk headless console", u8"Консоль CrossDesk без графического интерфейса") \ X(console_log_file, u8"日志文件", "Log file", u8"Файл журнала") \ - X(console_help, u8"命令: status 查看 ID/密码/状态;settings 设置;password 修改密码;random 随机密码;help 帮助;quit 退出", "Commands: status, settings, password, random, help, quit", u8"Команды: status, settings, password, random, help, quit") \ + X(console_help, u8"命令: status 查看 ID/密码/状态;settings 设置;password 修改密码;random 随机密码;help 帮助;quit 退出", "Commands: status — show ID/password/status; settings — configure; password — change password; random — random password; help — help; quit — exit", u8"Команды: status — ID/пароль/состояние; settings — настройки; password — сменить пароль; random — случайный пароль; help — справка; quit — выход") \ X(console_status, u8"状态", "Status", u8"Состояние") \ X(console_online, u8"在线", "Online", u8"В сети") \ X(console_offline, u8"未连接服务器", "Disconnected from server", u8"Нет подключения к серверу") \ @@ -77,7 +84,7 @@ struct TranslationRow { X(console_settings_next_connection, u8"已保存,将用于后续连接。", "Saved; applies to subsequent connections.", u8"Сохранено. Будет применено к следующим подключениям.") \ X(console_settings_next_file, u8"已保存,将用于后续接收的文件。", "Saved; applies to subsequently received files.", u8"Сохранено. Будет применено к следующим получаемым файлам.") \ X(console_settings_autostart, u8"已保存,在下次图形桌面登录时生效。", "Saved; applies at the next graphical desktop login.", u8"Сохранено. Будет применено при следующем входе в графический сеанс.") \ - X(console_settings_daemon, u8"已保存,普通模式重启后生效;无头控制台仍保持前台运行。", "Saved for the next normal-mode start; the headless console stays in the foreground.", u8"Сохранено для следующего запуска в обычном режиме. Консоль остаётся на переднем плане.") \ + X(console_settings_daemon, u8"已保存,普通模式重启后生效;无头控制台仍保持前台运行。", "Saved for the next normal-mode start; the headless console stays in the foreground.", u8"Сохранено для следующего запуска в обычном режиме. Консоль без графического интерфейса остаётся на переднем плане.") \ X(console_settings_server_required, u8"请先设置有效的服务器地址和端口,再启用自托管。", "Set a valid server host and port before enabling self-hosting.", u8"Укажите адрес и порт сервера перед включением собственного сервера.") \ X(console_settings_path_invalid, u8"请选择已存在且可写的绝对目录路径;default 使用默认目录。", "Use an existing writable absolute directory, or default for the default location.", u8"Укажите существующий доступный для записи абсолютный путь к каталогу или default.") \ X(console_settings_readonly, u8"该选项由系统或控制端管理,不可在此修改。", "This option is managed by the system or controller and is read-only here.", u8"Этот параметр управляется системой или клиентом и здесь недоступен для изменения.") \ @@ -91,7 +98,7 @@ struct TranslationRow { X(console_setting_files, u8"文件保存目录", "Received file directory", u8"Каталог получаемых файлов") \ X(console_setting_autostart, u8"登录桌面时自启", "Start at desktop login", u8"Запуск при входе в систему") \ X(console_setting_daemon, u8"守护进程(无头控制台中不生效)", "Daemon (inactive in headless console)", u8"Фоновый режим (не действует в консоли)") \ - X(save_remote_previews, u8"保存远程画面预览:", "Save remote screen previews:", u8"Сохранять превью экрана:") \ + X(save_remote_previews, u8"保存远程画面预览:", "Save remote screen previews:", u8"Сохранять превью удалённого экрана:") \ X(unknown_platform, u8"未知平台", "Unknown platform", u8"Неизвестная платформа") \ X(privacy_screen, u8"隐私屏", "Privacy screen", u8"Приватный экран") \ X(privacy_enable, u8"开启隐私屏", "Enable privacy screen", u8"Включить приватный экран") \ @@ -106,7 +113,7 @@ struct TranslationRow { X(password, u8"密码", "Password", u8"Пароль") \ X(refresh_password, u8"刷新随机密码", "Refresh random password", \ u8"Обновить случайный пароль") \ - X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \ + X(password_format_invalid, u8"密码必须为 6 位英文字母或数字。", "Use exactly 6 letters (A–Z, a–z) or digits (0–9).", u8"Пароль должен содержать ровно 6 латинских букв или цифр.") \ X(remote_desktop, u8"远程桌面", "Remote Desktop", \ u8"Удаленный рабочий стол") \ X(device_name, u8"设备名称", "Device Name", u8"Имя устройства") \ @@ -126,11 +133,9 @@ struct TranslationRow { u8"Развернуть панель управления") \ X(collapse_control_bar, u8"收起控制栏", "Collapse Control Bar", \ 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(fullscreen, u8"全屏", "Fullscreen", u8"На весь экран") \ + X(show_net_traffic_stats, u8"显示网络状态", "Show network status", u8"Показать состояние сети") \ + X(hide_net_traffic_stats, u8"隐藏网络状态", "Hide network status", u8"Скрыть состояние сети") \ X(video, u8"视频", "Video", u8"Видео") \ X(audio, u8"音频", "Audio", u8"Аудио") \ X(data, u8"数据", "Data", u8"Данные") \ @@ -142,18 +147,14 @@ struct TranslationRow { u8"Выйти из полноэкранного режима") \ X(control_mouse, u8"控制鼠标", "Control Mouse", u8"Управление мышью") \ X(release_mouse, u8"释放鼠标", "Release Mouse", u8"Освободить мышь") \ - X(audio_capture, u8"播放声音", "Audio Capture", u8"Воспроизведение звука") \ - X(mute, u8" 静音", " Mute", u8"Без звука") \ - X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Сочетания клавиш") \ + X(audio_capture, u8"播放声音", "Play audio", u8"Воспроизведение звука") \ + X(mute, u8"静音", "Mute", u8"Без звука") \ + X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Отправить сочетание клавиш") \ X(send_sas, u8"发送 SAS", "Send SAS", u8"Отправить SAS") \ - X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать") \ - X(remote_password_box_visible, u8"远端密码框已出现", \ - "Remote password box visible", u8"Окно ввода пароля видно") \ - X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送 SAS", \ - "Remote lock screen visible, send SAS", \ - u8"Видна блокировка, отправьте SAS") \ - X(remote_secure_desktop_active, u8"远端已进入安全桌面", \ - "Remote secure desktop active", u8"Активен защищенный рабочий стол") \ + X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать удалённое устройство") \ + X(remote_password_box_visible, u8"远端密码框已出现", "Remote password box visible", u8"На удалённом устройстве появилось поле пароля") \ + X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送 SAS", "Remote lock screen visible, send SAS", u8"Удалённый экран заблокирован, можно отправить SAS") \ + X(remote_secure_desktop_active, u8"远端已进入安全桌面", "Remote secure desktop active", u8"На удалённом устройстве активен защищённый рабочий стол") \ X(remote_service_unavailable, u8"远端 Windows 服务不可用", \ "Remote Windows service unavailable", \ u8"Служба Windows на удаленной стороне недоступна") \ @@ -173,8 +174,7 @@ struct TranslationRow { u8"нормально; будет недоступно только управление этим компьютером на " \ u8"экране блокировки.") \ X(install_windows_service, u8"安装", "Install", u8"Установить") \ - X(windows_service_settings_label, u8"锁屏控制服务:", \ - "Lock Screen Service:", u8"Служба блокировки экрана:") \ + X(windows_service_settings_label, u8"锁屏控制服务:", "Lock-screen control service:", u8"Служба управления при блокировке:") \ X(windows_service_installed, u8"已安装", "Installed", u8"Установлена") \ X(do_not_remind_again, u8"不再提醒", "Do not remind again", \ u8"Больше не напоминать") \ @@ -210,13 +210,8 @@ struct TranslationRow { u8"Способ захвата:") \ X(screen_capture_method_auto, u8"自动", "Auto", u8"Авто") \ X(video_settings, u8"画面设置", "Video settings", u8"Настройки видео") \ - X(remote_update_title, u8"建议升级被控端", "Remote update recommended", \ - u8"Рекомендуется обновление") \ - X(remote_update, \ - u8"新版本包含功能改进和性能优化,建议将被控端升级至最新版本。", \ - "Update the remote device to the latest version for improved features and performance.", \ - u8"В новой версии улучшены функции и производительность. " \ - u8"Рекомендуем обновить CrossDesk на удалённом устройстве.") \ + X(remote_update_title, u8"建议升级被控端", "Remote update recommended", u8"Рекомендуем обновить удалённое устройство") \ + X(remote_update, u8"新版本包含功能改进和性能优化,建议将被控端升级至最新版本。", "Update the remote device to the latest version for improved features and performance.", u8"В новой версии улучшены функции и производительность. Рекомендуем обновить CrossDesk на удалённом устройстве до последней версии.") \ X(remote_update_dismiss, u8"知道了", "Got it", u8"Понятно") \ X(video_settings_pending, u8"正在应用…", "Applying…", u8"Применение…") \ X(video_settings_failed, u8"调整失败,请重试。", "Could not apply settings. Try again.", u8"Не удалось применить настройки. Повторите попытку.") \ @@ -233,44 +228,34 @@ struct TranslationRow { 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(video_encode_format, u8"视频编码格式:", "Video codec:", u8"Видеокодек:") \ X(av1, u8"AV1", "AV1", "AV1") \ X(h264, u8"H.264", "H.264", "H.264") \ - X(force_relay, u8"强制中继连接:", "Force Relay:", u8"Только через реле:") \ - 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(force_relay, u8"强制中继连接:", "Force relay connection:", u8"Только через ретранслятор:") \ + X(self_hosted_server_config, u8"自托管配置", "Self-hosted server", u8"Собственный сервер") \ + X(self_hosted_server_settings, u8"自托管设置", "Self-hosted server settings", u8"Настройки своего сервера") \ X(self_hosted_server_address, u8"服务器地址:", \ "Server Address:", u8"Адрес сервера:") \ - X(self_hosted_server_port, u8"信令服务端口:", \ - "Signal Service Port:", u8"Порт сигнального сервиса:") \ + X(self_hosted_server_port, u8"信令服务端口:", "Signaling port:", u8"Порт сигнализации:") \ X(self_hosted_address_invalid, u8"请输入有效的主机名、IP 或 HTTPS/WSS 信令地址,可包含路径。", "Enter a valid host, IP, or HTTPS/WSS signaling URL. Paths are supported.", u8"Введите имя хоста, IP или HTTPS/WSS-адрес сигнального сервиса. Путь допустим.") \ X(self_hosted_port_invalid, u8"请输入 1–65535 范围内的信令端口。", "Enter a signaling port between 1 and 65535.", u8"Введите порт сигнального сервиса от 1 до 65535.") \ - X(self_hosted_save_failed, u8"配置保存失败,请检查配置目录是否可写后重试。", "Could not save settings. Check that the configuration directory is writable and retry.", u8"Не удалось сохранить настройки. Проверьте доступ на запись в каталог конфигурации.") \ + X(self_hosted_save_failed, u8"配置保存失败,请检查配置目录是否可写后重试。", "Could not save settings. Check that the configuration directory is writable and retry.", u8"Не удалось сохранить настройки. Проверьте доступ на запись в каталог конфигурации и повторите попытку.") \ X(ok, u8"确认", "OK", u8"ОК") \ X(cancel, u8"取消", "Cancel", u8"Отмена") \ - X(new_password, u8"请输入六位密码:", \ - "Please input a six-char password:", u8"Введите шестизначный пароль:") \ + X(new_password, u8"请输入六位密码:", "Enter a six-character password:", u8"Введите пароль из 6 символов:") \ X(input_password, u8"请输入密码:", \ "Please input password:", u8"Введите пароль:") \ - X(validate_password, u8"验证密码中...", "Validate password ...", \ - u8"Проверка пароля...") \ + X(validate_password, u8"验证密码中...", "Validating 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(signal_connected, u8"已连接服务器", "Connected to server", u8"Подключено к серверу") \ + X(signal_disconnected, u8"未连接服务器", "Disconnected from server", u8"Нет подключения к серверу") \ X(signal_reconnect, u8"重新连接", "Reconnect", u8"Переподключиться") \ X(signal_reconnecting, u8"正在重新连接...", "Reconnecting...", \ u8"Переподключение...") \ - X(signal_tls_cert_error, u8"证书验证失败,请重新安装自托管根证书", \ - "Certificate verification failed. Reinstall the self-hosted root " \ - "certificate.", \ - u8"Ошибка проверки сертификата. Переустановите корневой сертификат.") \ + X(signal_tls_cert_error, u8"证书验证失败,请重新安装自托管根证书", "Certificate verification failed. Reinstall the self-hosted root certificate.", u8"Ошибка проверки сертификата. Переустановите корневой сертификат собственного сервера.") \ X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \ X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \ u8"P2P отключено") \ @@ -281,7 +266,7 @@ struct TranslationRow { X(receiving_screen, u8"画面接收中...", "Receiving screen...", \ u8"Получение изображения...") \ X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \ - X(connection_auth_throttled, u8"验证尝试过于频繁,请稍后重试。", "Too many authentication attempts. Try again later.", u8"Слишком много попыток. Повторите позже.") \ + X(connection_auth_throttled, u8"验证尝试过于频繁,请稍后重试。", "Too many authentication attempts. Try again later.", u8"Слишком много попыток аутентификации. Повторите позже.") \ X(connection_service_busy, u8"认证服务繁忙,请稍后重试。", "Authentication service busy. Try again later.", u8"Сервис проверки занят. Повторите позже.") \ X(connection_not_authenticated, u8"登录状态已失效,请重新连接。", "Signaling login expired. Please reconnect.", u8"Сеанс входа истёк. Подключитесь заново.") \ X(connection_retry_after, u8"请在 %d 秒后重试。", "Retry in %d seconds.", u8"Повторите через %d сек.") \ @@ -317,26 +302,22 @@ struct TranslationRow { "Access Website: ", u8"Официальный сайт: ") \ X(update, u8"更新", "Update", u8"Обновить") \ X(install_update, u8"安装更新", "Install update", u8"Установить обновление") \ - X(manual_download, u8"手动下载", "Download", u8"Скачать") \ + X(manual_download, u8"手动下载", "Download manually", u8"Скачать вручную") \ X(install_update_confirm, u8"下载完成后将关闭 CrossDesk 并安装更新,当前连接和文件传输会中断。安装需要系统授权,完成后重新启动。是否继续?", \ "CrossDesk will close after downloading the update. Connections and file transfers will be interrupted. Installation requires system authorization, then CrossDesk will restart. Continue?", \ u8"После загрузки CrossDesk закроется для обновления. Соединения и передача файлов будут прерваны. Потребуется разрешение системы, затем приложение перезапустится. Продолжить?") \ X(update_preparing, u8"正在准备更新…", "Preparing update…", u8"Подготовка обновления…") \ X(update_install_failed, u8"更新未完成,可重试或手动安装。", "Update did not complete. Retry or install manually.", u8"Обновление не завершено. Повторите или установите вручную.") \ - X(connection_alias, u8"修改名称", "Edit Alias", \ - u8"Изменить имя подключения") \ + X(connection_alias, u8"修改名称", "Rename", u8"Переименовать") \ X(delete_connection, u8"删除连接", "Delete Connection", \ u8"Удалить подключение") \ - X(connect_to_this_connection, u8"发起连接", "Connect to this connection", \ - u8"Подключиться") \ + X(connect_to_this_connection, u8"发起连接", "Connect", u8"Подключиться") \ X(input_connection_alias, u8"请输入连接名称:", \ "Please input connection name:", u8"Введите имя подключения:") \ - X(confirm_delete_connection, u8"确认删除此连接", \ - "Confirm to delete this connection", u8"Удалить это подключение?") \ + X(confirm_delete_connection, u8"确认删除此连接", "Delete this connection?", u8"Удалить это подключение?") \ X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \ X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \ - X(privacy_on_connect, u8"被连接时开启隐私屏:", "Privacy screen on connect:", \ - u8"Приватность при подключении:") \ + X(privacy_on_connect, u8"被连接时开启隐私屏:", "Privacy screen on incoming connection:", u8"Приватный экран при подключении:") \ X(lock_on_disconnect, u8"连接断开后锁屏:", "Lock screen on disconnect:", \ u8"Блокировка при отключении:") \ X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \ @@ -371,19 +352,19 @@ struct TranslationRow { "File Transfer Save Path:", u8"Путь сохранения файлов:") \ X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \ X(resolution, u8"分辨率", "Res", u8"Разрешение") \ - X(video_latency, u8"视频延时", "Video delay", u8"Задержка") \ - X(connection_latency, u8"连接延时", "Connection RTT", u8"Задержка соединения") \ - X(connection_mode, u8"连接模式", "Mode", u8"Режим") \ + X(video_latency, u8"视频延时", "Video latency", u8"Задержка видео") \ + X(connection_latency, u8"连接延时", "Connection latency", u8"Задержка соединения") \ + X(connection_mode, u8"连接模式", "Connection mode", u8"Режим подключения") \ X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \ - X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \ + X(connection_mode_relay, u8"中继", "Relay", u8"Ретрансляция") \ X(transport_encryption, u8"加密传输", "Encryption", u8"Шифрование") \ X(remote_video_encoder, u8"被控端编码器", "Remote encoder", u8"Кодер удалённой стороны") \ X(local_video_decoder, u8"控制端解码器", "Local decoder", u8"Локальный декодер") \ X(transport_encryption_enabled, u8"已开启", "Enabled", u8"Включено") \ X(transport_encryption_disabled, u8"未开启", "Disabled", u8"Выключено") \ - X(online, u8"在线", "Online", u8"Онлайн") \ - X(offline, u8"离线", "Offline", u8"Офлайн") \ - X(device_offline, u8"设备离线", "Device Offline", 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"屏幕录制权限", \ diff --git a/apps/desktop/src/gui/ui/common.slint b/apps/desktop/src/gui/ui/common.slint index 115f468b..12a58423 100644 --- a/apps/desktop/src/gui/ui/common.slint +++ b/apps/desktop/src/gui/ui/common.slint @@ -4,7 +4,7 @@ export global FloatingBarStyle { out property corner-radius: UiRadius.menu; } -// Filled controls remain distinct from white cards without restoring outlines. +// Fields and selection controls share a muted fill. export global ControlStyle { out property surface: #dce6f3; out property hover: #cedef1; @@ -23,6 +23,20 @@ export global ControlStyle { out property input-focus-tint: #235eaa1e; } +// Action buttons use a light surface and a crisp edge, distinct from fields. +export global ButtonStyle { + out property surface: #ffffff; + out property hover: #f0f5ff; + out property pressed: #dfe9fa; + out property border: #b8c7db; + out property hover-border: #7e9fd1; + out property pressed-border: #6488bf; + out property disabled-border: #dce2eb; + out property selected: #3564e3; + out property selected-hover: #4574f0; + out property selected-pressed: #254cc0; +} + // ImGui loads a 32px font atlas and applies per-window font scales. These // logical sizes reproduce those visual tiers on Slint's HiDPI renderer. export global ImGuiFontStyle { @@ -218,43 +232,48 @@ export component CompactComboBox inherits ShadowSurface { width: 73px; height: 24px; background: !root.enabled ? ControlStyle.disabled - : combo-touch.pressed || combo-popup.is-open ? ControlStyle.pressed - : combo-touch.has-hover ? ControlStyle.hover : ControlStyle.surface; - shadow-color: root.enabled && combo-touch.has-hover ? root.hover-shadow-color : root.normal-shadow-color; + : combo-touch.pressed ? ButtonStyle.pressed + : combo-popup.is-open || combo-touch.has-hover ? ButtonStyle.hover : ButtonStyle.surface; + border-width: 1px; + border-color: !root.enabled ? ButtonStyle.disabled-border + : combo-popup.is-open ? UiShadow.focus + : combo-touch.pressed ? ButtonStyle.pressed-border + : combo-touch.has-hover ? ButtonStyle.hover-border : ButtonStyle.border; + shadow-enabled: root.enabled && !combo-touch.pressed; + shadow-focused: root.enabled && combo-popup.is-open; + shadow-color: combo-popup.is-open ? UiShadow.focus + : root.enabled && combo-touch.has-hover ? root.hover-shadow-color : root.normal-shadow-color; border-radius: UiRadius.control; + animate background, border-color, shadow-color { duration: 120ms; } + accessible-role: combobox; + accessible-enabled: root.enabled; + accessible-expandable: true; + accessible-expanded: combo-popup.is-open; + accessible-value: root.model[root.current-index]; Text { - x: root.subtle ? 10px : 5px; - width: parent.width - (root.show-arrow ? (root.subtle ? 36px : 24px) : (root.subtle ? 20px : 10px)); + x: root.subtle ? 10px : 8px; + width: parent.width - self.x - (root.show-arrow ? 28px : self.x); + height: parent.height; text: root.model[root.current-index]; color: root.enabled ? #202124 : #8b8f95; font-size: root.subtle ? 12px : ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; + accessible-role: none; } - if root.show-arrow: Rectangle { - x: parent.width - (root.subtle ? 27px : 21px); - width: 20px; - height: parent.height - 2px; - y: 1px; - background: root.subtle ? transparent : root.enabled ? ControlStyle.selected : #d7d9dc; - border-top-right-radius: UiRadius.control - 1px; - border-bottom-right-radius: UiRadius.control - 1px; - Text { - text: FontAwesomeIcons.angle-down; - color: root.subtle ? ControlStyle.icon : #202124; - font-family: "Font Awesome 6 Free"; - font-weight: 900; - font-size: 10px; - horizontal-alignment: center; - vertical-alignment: center; - transform-rotation: combo-popup.is-open ? 180deg : 0deg; - animate transform-rotation { - duration: 140ms; - easing: ease-out; - enabled: combo-popup.is-open; - } - } + if root.show-arrow: Text { + x: parent.width - 26px; + width: 18px; height: parent.height; + // Switch glyphs so the software renderer also shows the open state. + text: combo-popup.is-open ? FontAwesomeIcons.angle-up : FontAwesomeIcons.angle-down; + color: !root.enabled ? #98a2b3 : combo-popup.is-open ? UiShadow.focus : ControlStyle.icon; + font-family: "Font Awesome 6 Free"; + font-weight: 900; + font-size: 10px; + horizontal-alignment: center; + vertical-alignment: center; + accessible-role: none; } combo-touch := TouchArea { enabled: root.enabled; mouse-cursor: pointer; @@ -282,9 +301,9 @@ export component CompactComboBox inherits ShadowSurface { combo-popup := PopupWindow { x: 0px; - y: root.height; + y: root.height + 4px; width: root.width; - height: root.model.length * 23px; + height: root.model.length * 23px + 8px; close-policy: close-on-click-outside; ShadowSurface { @@ -299,20 +318,29 @@ export component CompactComboBox inherits ShadowSurface { border-radius: UiRadius.menu; clip: true; for choice[index] in root.model: Rectangle { - y: index * 23px; - width: parent.width; + x: 4px; y: 4px + index * 23px; + width: parent.width - 8px; height: 23px; + border-radius: UiRadius.control; background: index == root.current-index - ? (option-touch.pressed ? ControlStyle.selected-pressed : option-touch.has-hover ? ControlStyle.selected-hover : ControlStyle.selected) - : option-touch.pressed ? ControlStyle.pressed : option-touch.has-hover ? ControlStyle.hover : white; + ? (option-touch.pressed ? ButtonStyle.pressed : ButtonStyle.hover) + : option-touch.pressed ? ButtonStyle.pressed : option-touch.has-hover ? ButtonStyle.hover : white; Text { - x: 5px; width: parent.width - 10px; text: choice; + x: 6px; width: parent.width - 28px; height: parent.height; text: choice; color: index == root.current-index ? ControlStyle.selected-text : #202124; font-weight: index == root.current-index ? 600 : 400; font-size: root.subtle ? 12px : ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; } + if index == root.current-index: Text { + x: parent.width - 20px; width: 14px; height: parent.height; + text: FontAwesomeIcons.check; color: UiShadow.focus; + font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: 10px; + horizontal-alignment: center; vertical-alignment: center; + accessible-role: none; + } option-touch := TouchArea { enabled: root.enabled; + mouse-cursor: pointer; clicked => { popup-open-timer.running = false; root.popup-animation-enabled = false; @@ -332,6 +360,8 @@ export component CompactComboBox inherits ShadowSurface { clip: true; shadow-color: root.normal-shadow-color; border-radius: UiRadius.menu; + border-width: 1px; + border-color: ButtonStyle.border; z: 2; } } @@ -401,11 +431,18 @@ export component CompactButton inherits ShadowSurface { width: 34px; height: 24px; background: !root.enabled ? (root.quiet ? transparent : ControlStyle.disabled) - : button-touch.pressed ? (root.primary ? #82b8f2 : ControlStyle.pressed) - : button-touch.has-hover ? (root.primary ? #b8d9fb : ControlStyle.hover) - : root.primary ? #a8d0fa : root.quiet ? transparent : ControlStyle.surface; - shadow-color: root.primary ? UiShadow.focus : root.quiet ? transparent : UiShadow.edge; + : button-touch.pressed ? (root.primary ? #82b8f2 : ButtonStyle.pressed) + : button-touch.has-hover ? (root.primary ? #b8d9fb : ButtonStyle.hover) + : root.primary ? #a8d0fa : root.quiet ? transparent : ButtonStyle.surface; + border-width: root.quiet ? 0px : 1px; + border-color: !root.enabled ? ButtonStyle.disabled-border + : root.primary ? UiShadow.focus + : button-touch.pressed ? ButtonStyle.pressed-border + : button-touch.has-hover ? ButtonStyle.hover-border : ButtonStyle.border; + shadow-enabled: root.enabled && !root.quiet && !button-touch.pressed; + shadow-color: root.primary ? UiShadow.focus : button-touch.has-hover ? UiShadow.hover : UiShadow.edge; border-radius: UiRadius.control; + animate background, border-color { duration: 100ms; } Text { text: root.text; color: root.enabled ? #24272d : #8e9298; @@ -414,7 +451,7 @@ export component CompactButton inherits ShadowSurface { horizontal-alignment: center; vertical-alignment: center; } - button-touch := TouchArea { enabled: root.enabled; clicked => { root.clicked(); } } + button-touch := TouchArea { enabled: root.enabled; mouse-cursor: pointer; clicked => { root.clicked(); } } } export component PrimaryButton inherits ShadowSurface { @@ -459,13 +496,17 @@ export component SecondaryButton inherits ShadowSurface { min-width: 72px; height: 32px; background: !root.enabled ? ControlStyle.disabled - : touch.pressed ? ControlStyle.pressed - : touch.has-hover ? ControlStyle.hover : ControlStyle.surface; - shadow-color: UiShadow.edge; + : touch.pressed ? ButtonStyle.pressed + : touch.has-hover ? ButtonStyle.hover : ButtonStyle.surface; + border-width: 1px; + border-color: !root.enabled ? ButtonStyle.disabled-border + : touch.pressed ? ButtonStyle.pressed-border + : touch.has-hover ? ButtonStyle.hover-border : ButtonStyle.border; + shadow-color: touch.has-hover ? UiShadow.hover : UiShadow.edge; shadow-compact: true; shadow-enabled: root.enabled && !touch.pressed; border-radius: UiRadius.control; - animate background { duration: 100ms; } + animate background, border-color { duration: 100ms; } Text { text: root.text; @@ -478,6 +519,7 @@ export component SecondaryButton inherits ShadowSurface { touch := TouchArea { enabled: root.enabled; + mouse-cursor: pointer; clicked => { root.clicked(); } } } diff --git a/apps/desktop/src/gui/ui/device_strings.slint b/apps/desktop/src/gui/ui/device_strings.slint index 5f5dd794..2c123ff9 100644 --- a/apps/desktop/src/gui/ui/device_strings.slint +++ b/apps/desktop/src/gui/ui/device_strings.slint @@ -3,4 +3,16 @@ export global GalleryStrings { public pure function tr(zh: string, en: string, ru: string) -> string { return self.language == 0 ? zh : self.language == 2 ? ru : en; } + public pure function device-count(count: int) -> string { + if self.language == 0 { return count + " 台设备"; } + if self.language == 2 { + let last-two = mod(count, 100); + let last = mod(count, 10); + if last-two >= 11 && last-two <= 14 { return count + " устройств"; } + if last == 1 { return count + " устройство"; } + if last >= 2 && last <= 4 { return count + " устройства"; } + return count + " устройств"; + } + return count + (count == 1 ? " device" : " devices"); + } } diff --git a/apps/desktop/src/gui/ui/gallery_network_dialog.slint b/apps/desktop/src/gui/ui/gallery_network_dialog.slint index 0392af45..7b1c3182 100644 --- a/apps/desktop/src/gui/ui/gallery_network_dialog.slint +++ b/apps/desktop/src/gui/ui/gallery_network_dialog.slint @@ -67,7 +67,7 @@ component NetworkRow inherits Rectangle { text: !root.device.encryption-known ? GalleryStrings.tr("待确认", "Unknown", "Неизвестно") : root.device.encryption-enabled - ? GalleryStrings.tr("已加密", "Encrypted", "Защищено") + ? GalleryStrings.tr("已加密", "Encrypted", "Зашифровано") : GalleryStrings.tr("未加密", "Unencrypted", "Без шифр."); color: !root.device.encryption-known ? #7a8490 : root.device.encryption-enabled ? #18794e : #8a6428; @@ -208,7 +208,7 @@ export component GalleryNetworkDialog inherits Rectangle { } Text { x: 20px; y: 17px; width: parent.width - 120px; height: 24px; - text: GalleryStrings.tr("设备网络状态", "Device network status", "Сеть устройств"); + text: GalleryStrings.tr("设备网络状态", "Device network status", "Состояние сети устройств"); font-size: 17px; font-weight: 600; color: #293746; } CompactButton { diff --git a/apps/desktop/src/gui/ui/gallery_settings.slint b/apps/desktop/src/gui/ui/gallery_settings.slint index 2c428e7d..42b15ddb 100644 --- a/apps/desktop/src/gui/ui/gallery_settings.slint +++ b/apps/desktop/src/gui/ui/gallery_settings.slint @@ -11,7 +11,7 @@ export component GallerySettings inherits Rectangle { y: 0px; width: parent.width; title: GalleryStrings.tr("画面传输", "Video transmission", "Передача видео"); rows: 3; SettingsRow { - label: GalleryStrings.tr("分辨率上限", "Resolution limit", "Разрешение"); + label: GalleryStrings.tr("分辨率上限", "Resolution limit", "Макс. разрешение"); label-reserve: self.width - 112px; SettingsSelect { border-radius: UiRadius.control; @@ -33,7 +33,7 @@ export component GallerySettings inherits Rectangle { } } SettingsRow { - label: GalleryStrings.tr("视频总带宽", "Video budget", "Бюджет видео"); + label: GalleryStrings.tr("视频总带宽", "Total bandwidth", "Общий битрейт"); label-reserve: self.width - 112px; divider: false; SettingsSelect { border-radius: UiRadius.control; diff --git a/apps/desktop/src/gui/ui/gallery_view.slint b/apps/desktop/src/gui/ui/gallery_view.slint index a5374321..aaaf5574 100644 --- a/apps/desktop/src/gui/ui/gallery_view.slint +++ b/apps/desktop/src/gui/ui/gallery_view.slint @@ -290,7 +290,7 @@ export component GalleryView inherits Rectangle { selected(index) => { root.action("group", index); } } Text { - text: root.count + GalleryStrings.tr(" 台设备", " devices", " устройств"); + text: GalleryStrings.device-count(root.count); min-width: 0px; overflow: elide; font-size: 11px; color: #858c96; vertical-alignment: center; } Rectangle { horizontal-stretch: 1; min-width: 0px; } @@ -336,8 +336,8 @@ export component GalleryView inherits Rectangle { : GalleryStrings.tr("取消全选", "Deselect all", "Снять выбор"); clicked => { root.action("all-select", ""); } } - GalleryToolbarButton { horizontal-padding: root.toolbar-button-padding; primary: true; enabled: root.selected-count > 0; text: GalleryStrings.tr("连接", "Connect", "Связь"); clicked => { root.action("connect", ""); } } - GalleryToolbarButton { horizontal-padding: root.toolbar-button-padding; enabled: root.selected-count > 0; text: GalleryStrings.tr("断开", "Close", "Отключить"); clicked => { root.confirm-action("disconnect", self.text); } } + GalleryToolbarButton { horizontal-padding: root.toolbar-button-padding; primary: true; enabled: root.selected-count > 0; text: GalleryStrings.tr("连接", "Connect", "Подключить"); clicked => { root.action("connect", ""); } } + GalleryToolbarButton { horizontal-padding: root.toolbar-button-padding; enabled: root.selected-count > 0; text: GalleryStrings.tr("断开", "Disconnect", "Отключить"); clicked => { root.confirm-action("disconnect", self.text); } } batch-menu := GalleryMenuButton { horizontal-padding: root.toolbar-button-padding; width: self.preferred-button-width; enabled: root.selected-count > 0; @@ -579,7 +579,7 @@ export component GalleryView inherits Rectangle { background: root.controlling ? #e7f1fd : #f0f2f5; focus-state := Text { x: 8px; width: parent.width - 16px; height: parent.height; - text: root.group-controlling ? GalleryStrings.tr("群控中", "Group control", "Групповое") + text: root.group-controlling ? GalleryStrings.tr("群控中", "Group control", "Групп. управление") : root.controlling ? GalleryStrings.tr("正在操控", "Controlling", "Управление") : root.focused.connected && root.focused.has-frame && !root.focused.stale ? GalleryStrings.tr("只读预览", "View only", "Просмотр") : root.focused.status; @@ -590,8 +590,8 @@ export component GalleryView inherits Rectangle { horizontal-padding: root.toolbar-button-padding; primary: root.group-controlling; enabled: root.group-controlling || (root.focused.connected && root.focused.has-frame && !root.focused.stale); - text: root.group-controlling ? GalleryStrings.tr("关闭群控", "Stop group", "Откл. группу") : GalleryStrings.tr("开启群控", "Group control", "Групп. ввод"); - alternate-text: root.group-controlling ? GalleryStrings.tr("开启群控", "Group control", "Групп. ввод") : GalleryStrings.tr("关闭群控", "Stop group", "Откл. группу"); + text: root.group-controlling ? GalleryStrings.tr("关闭群控", "Stop group control", "Откл. групп. ввод") : GalleryStrings.tr("开启群控", "Group control", "Групп. ввод"); + alternate-text: root.group-controlling ? GalleryStrings.tr("开启群控", "Group control", "Групп. ввод") : GalleryStrings.tr("关闭群控", "Stop group control", "Откл. групп. ввод"); accessible-role: button; accessible-label: self.text + GalleryStrings.tr(":同步键鼠到所有已连接设备", ": mirror input to all connected devices", ": ввод на всех подключённых устройствах"); accessible-action-default => { self.clicked(); } @@ -846,7 +846,7 @@ export component GalleryView inherits Rectangle { if root.dragging: ShadowSurface { x: clamp(grid.x + root.drag-x + 14px, 8px, parent.width - self.width - 8px); y: clamp(grid.y + root.drag-y + 14px, 8px, footer.y - self.height - 12px); - width: 224px; height: 54px; z: 10; + width: 224px; height: max(54px, drag-hint.y + drag-hint.height + 9px); z: 10; background: #fffffff2; shadow-color: UiShadow.focus; border-radius: UiRadius.surface; Rectangle { x: 8px; y: 9px; width: 64px; height: 36px; @@ -857,12 +857,12 @@ export component GalleryView inherits Rectangle { } } Text { x: 80px; y: 8px; width: parent.width - 88px; height: 18px; text: root.dragged-tile.name; font-size: 12px; color: #303943; overflow: elide; } - Text { - x: 80px; y: 29px; width: parent.width - 88px; height: 16px; - text: root.drag-page-direction != 0 ? GalleryStrings.tr("停留翻页,松开取消", "Hold to turn page", "Удерживайте для перелистывания") + drag-hint := Text { + x: 80px; y: 29px; width: parent.width - 88px; height: max(16px, self.preferred-height); + text: root.drag-page-direction != 0 ? GalleryStrings.tr("停留翻页,松开取消", "Hold to turn page; release to cancel", "Удерживайте для перелистывания; отпустите для отмены") : root.drag-target >= 0 ? GalleryStrings.tr("松开鼠标交换位置", "Release to swap", "Отпустите для обмена") - : GalleryStrings.tr("拖到另一台设备上", "Drag onto another device", "На другое устройство"); - font-size: 10px; color: #527ca8; overflow: elide; + : GalleryStrings.tr("拖到另一台设备上", "Drag onto another device", "Перетащите на другое устройство"); + font-size: 10px; color: #527ca8; wrap: word-wrap; } } if root.swap-save-failed: ShadowSurface { @@ -922,10 +922,10 @@ export component GalleryView inherits Rectangle { TouchArea { clicked => { remember.checked = !remember.checked; } } } Text { x: 18px; y: 216px; width: 364px; height: 28px; text: root.notice; color: #8e4740; font-size: 11px; wrap: word-wrap; overflow: elide; } - GalleryButton { x: 172px; y: 260px; width: 72px; height: 28px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { password.text = ""; root.adding = false; } } + GalleryButton { x: 136px; y: 260px; width: 72px; height: 28px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { password.text = ""; root.adding = false; } } GalleryButton { - x: 254px; y: 260px; width: 128px; height: 28px; primary: true; - text: GalleryStrings.tr("添加并连接", "Add and connect", "Подключить"); + x: 218px; y: 260px; width: 164px; height: 28px; primary: true; + text: GalleryStrings.tr("添加并连接", "Add and connect", "Добавить и подключить"); enabled: device-id.text != "" && password.text != ""; clicked => { add-dialog.submit(); } } @@ -937,8 +937,8 @@ export component GalleryView inherits Rectangle { ShadowSurface { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 440px; height: 378px; background: white; border-radius: UiRadius.surface; shadow-color: UiShadow.edge; - Text { x: 18px; y: 16px; height: 24px; text: GalleryStrings.tr("从最近连接添加", "Add from recent connections", "Добавить из недавних"); font-size: 16px; } - Text { x: 18px; y: 44px; width: 404px; height: 18px; text: GalleryStrings.tr("加入桌面墙后,可通过批量选择连接", "Add devices, then use Select to connect them.", "После добавления подключайтесь через выбор."); color: #62666d; font-size: 11px; } + Text { x: 18px; y: 16px; height: 24px; text: GalleryStrings.tr("从近期连接添加", "Add from recent connections", "Добавить из недавних"); font-size: 16px; } + Text { x: 18px; y: 44px; width: 404px; height: 18px; text: GalleryStrings.tr("加入桌面墙后,可通过批量选择连接", "Add to DeskHub, then select devices to connect.", "Добавьте в DeskHub, затем выберите устройства для подключения."); color: #62666d; font-size: 11px; } ImGuiCheckBox { x: 18px; y: 74px; enabled: root.recent-available > 0; checked: root.recent-devices.length > 0 && root.recent-selected == root.recent-available; } Text { x: 45px; y: 74px; height: 21px; text: GalleryStrings.tr("全选", "Select all", "Выбрать все"); color: root.recent-available > 0 ? #24272d : #92969e; font-size: 12px; vertical-alignment: center; } TouchArea { x: 18px; y: 74px; width: 144px; height: 21px; enabled: root.recent-available > 0; clicked => { root.action("recent-all", ""); } } @@ -967,7 +967,7 @@ export component GalleryView inherits Rectangle { row-touch := TouchArea { enabled: !device.added; clicked => { root.action("recent-select", device.id); } } } } - if root.recent-devices.length == 0: Text { text: GalleryStrings.tr("暂无最近连接", "No recent connections", "Нет недавних подключений"); color: #808080; font-size: 12px; horizontal-alignment: center; vertical-alignment: center; } + if root.recent-devices.length == 0: Text { text: GalleryStrings.tr("暂无近期连接", "No recent connections", "Нет недавних подключений"); color: #808080; font-size: 12px; horizontal-alignment: center; vertical-alignment: center; } Rectangle { x: -UiRadius.surface; y: -UiRadius.surface; width: parent.width + 2 * UiRadius.surface; @@ -985,7 +985,7 @@ export component GalleryView inherits Rectangle { import-group := RoundedLineEdit { x: 84px; y: 274px; width: 338px; height: 30px; text: root.import-group; placeholder-text: GalleryStrings.tr("分组名称(可选)", "Group name (optional)", "Группа (необязательно)"); } Text { x: 18px; y: 309px; width: 404px; height: 20px; text: root.notice; font-size: 11px; color: #8e4740; overflow: elide; } GalleryButton { x: 192px; y: 336px; width: 72px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { root.importing = false; } } - GalleryButton { x: 274px; y: 336px; width: 148px; primary: true; text: GalleryStrings.tr("加入桌面墙", "Add selected", "Добавить"); enabled: root.recent-selected > 0; clicked => { if root.import-recent(import-group.text) { root.importing = false; } } } + GalleryButton { x: 274px; y: 336px; width: 148px; primary: true; text: GalleryStrings.tr("加入桌面墙", "Add to DeskHub", "В DeskHub"); enabled: root.recent-selected > 0; clicked => { if root.import-recent(import-group.text) { root.importing = false; } } } } } if root.confirming: Rectangle { @@ -1002,7 +1002,7 @@ export component GalleryView inherits Rectangle { Rectangle { x: parent.width - 108px; y: 20px; width: 88px; height: 22px; background: #eff3f8; border-radius: UiRadius.surface; - Text { text: root.selected-count + GalleryStrings.tr(" 台设备", " devices", " устр."); color: #626e7d; font-size: 11px; horizontal-alignment: center; vertical-alignment: center; } + Text { text: GalleryStrings.device-count(root.selected-count); color: #626e7d; font-size: 11px; horizontal-alignment: center; vertical-alignment: center; } } confirmation-list := ShadowSurface { x: 20px; y: 58px; width: parent.width - 40px; @@ -1021,7 +1021,7 @@ export component GalleryView inherits Rectangle { width: GalleryStrings.language == 0 ? 68px : 104px; height: 20px; border-radius: 10px; background: device.connected ? #eaf6ee : #f0f2f5; Rectangle { x: 9px; y: 7px; width: 6px; height: 6px; border-radius: 3px; background: device.connected ? #27a762 : #a0a6b0; } - Text { x: 21px; width: parent.width - 27px; height: parent.height; text: device.connected ? GalleryStrings.tr("已连接", "Connected", "Подключено") : GalleryStrings.tr("未连接", "Not connected", "Нет связи"); color: device.connected ? #348259 : #858d98; font-size: 10px; vertical-alignment: center; } + Text { x: 21px; width: parent.width - 27px; height: parent.height; text: device.connected ? GalleryStrings.tr("已连接", "Connected", "Подключено") : GalleryStrings.tr("未连接", "Not connected", "Нет соединения"); color: device.connected ? #348259 : #858d98; font-size: 10px; vertical-alignment: center; } } if index + 1 < root.selected-devices.length: ShadowSeparator { x: 12px; y: parent.height - 1px; width: parent.width - 24px; height: 1px; shadow-color: UiShadow.separator; } } diff --git a/apps/desktop/src/gui/ui/lan_strings.slint b/apps/desktop/src/gui/ui/lan_strings.slint index a34bbf6d..bc10363a 100644 --- a/apps/desktop/src/gui/ui/lan_strings.slint +++ b/apps/desktop/src/gui/ui/lan_strings.slint @@ -20,9 +20,9 @@ export global LanStrings { GalleryStrings.tr("原设置已保留。", "Your previous settings are unchanged.", "Прежние настройки сохранены.") ]; } if code == "Selected LAN interface is unavailable" { return [ - GalleryStrings.tr("未找到可用的局域网连接。", "Connect to Wi-Fi or Ethernet,", "Подключитесь к Wi-Fi или Ethernet"), - GalleryStrings.tr("请连接 Wi-Fi 或网线,", "or select a local IP", "либо выберите IP"), - GalleryStrings.tr("或在主界面选择本机 IP。", "on the main screen.", "в главном окне.") + GalleryStrings.tr("未找到可用的局域网连接。", "No local network connection was found.", "Нет доступного подключения к LAN."), + GalleryStrings.tr("请连接 Wi-Fi 或网线,", "Connect to Wi-Fi or Ethernet,", "Подключитесь к Wi-Fi или Ethernet"), + GalleryStrings.tr("或在主界面选择本机 IP。", "or select a local IP on the main screen.", "или выберите локальный IP в главном окне.") ]; } // Keep all branches the same size for Slint's C++ model generator. return ["", "", ""]; @@ -33,15 +33,26 @@ export global LanStrings { let separator = GalleryStrings.tr("", " ", " "); return lines[0] + separator + lines[1] + separator + lines[2]; } - if code == "InvalidCredentials" { return GalleryStrings.tr("密码不正确,或对方密码已更新。请查看对方的局域网访问密码后重试。", "Check the other desktop's current LAN password and try again.", "Проверьте текущий пароль LAN другого компьютера и повторите попытку."); } + if code == "" { return ""; } + if code == "InvalidCredentials" { return GalleryStrings.tr("密码不正确,或对方密码已更新。请查看对方的局域网访问密码后重试。", "The password is incorrect or has changed. Check the other desktop's current LAN password and try again.", "Пароль неверен или изменён. Проверьте текущий пароль LAN другого компьютера и повторите попытку."); } if code == "AuthenticationRateLimited" { return GalleryStrings.tr("尝试次数过多,请等待倒计时结束后重试。", "Too many attempts. Try again when the countdown ends.", "Слишком много попыток. Дождитесь окончания обратного отсчёта."); } if code == "CredentialsChanged" { return GalleryStrings.tr("对方密码已更新,请使用新密码重试。", "The password changed. Try again with the new password.", "Пароль изменился. Повторите с новым паролем."); } if code == "Busy" { return GalleryStrings.tr("设备正忙,请稍后重试。", "The desktop is busy. Try again shortly.", "Компьютер занят. Повторите позже."); } if code == "LAN password must contain exactly 6 ASCII letters or digits" { return GalleryStrings.tr("访问密码必须为 6 位英文字母或数字,区分大小写。", "Use exactly 6 letters (A–Z, a–z) or digits (0–9). Case-sensitive.", "Пароль: ровно 6 латинских букв или цифр. Регистр учитывается."); } - if code == "ConnectionFailed" || code == "ConnectionTimeout" || code == "Timeout" || code == "ConnectionClosed" { return GalleryStrings.tr("无法连接,请检查对方 IP、程序是否运行,以及防火墙是否放行。", "Check the IP, whether the other desktop is running CrossDesk, and its firewall settings.", "Проверьте IP, работу CrossDesk на другом компьютере и брандмауэр."); } - if code == "Enter a numeric IPv4 address or IPv4:port" { return GalleryStrings.tr("请输入有效的 IP,例如 192.168.1.20;指定端口可用 IP:端口,端口范围 1–65535。", "Enter a valid IP such as 192.168.1.20, or IP:port (1–65535).", "Введите IP, например 192.168.1.20, или IP:порт (1–65535)."); } + if code == "ConnectionFailed" || code == "ConnectionTimeout" || code == "IceConnectionTimeout" || code == "Timeout" || code == "ConnectionClosed" || code == "HeartbeatTimeout" || code == "SignalSendFailed" { return GalleryStrings.tr("无法连接,请检查对方 IP、程序是否运行,以及防火墙是否放行。", "Could not connect. Check the IP, whether the other desktop is running CrossDesk, and its firewall settings.", "Не удалось подключиться. Проверьте IP, работу CrossDesk на другом компьютере и брандмауэр."); } + if code == "Enter a numeric IPv4 address or IPv4:port" || code == "Invalid LAN endpoint" { return GalleryStrings.tr("请输入有效的 IPv4,例如 192.168.1.20;指定端口可用 IPv4:端口,端口范围 1–65535。", "Enter a valid IPv4 address such as 192.168.1.20, or IPv4:port (1–65535).", "Введите IPv4, например 192.168.1.20, или IPv4:порт (1–65535)."); } if code == "Invalid LAN port" { return GalleryStrings.tr("请输入 1–65535 之间的整数端口。", "Enter a port number from 1 to 65535.", "Введите целый номер порта от 1 до 65535."); } - if code == "LAN password is not ready" { return GalleryStrings.tr("请点击主界面密码旁的修改按钮设置密码。", "Set a password using Edit next to the password on the main screen.", "Задайте пароль кнопкой редактирования в главном окне."); } - return code; + if code == "LAN password is not ready" { return GalleryStrings.tr("请点击主界面密码旁的修改按钮设置密码。", "Set a password using Edit next to the password on the main screen.", "Задайте пароль кнопкой редактирования рядом с паролем в главном окне."); } + if code == "LAN connection limit reached" { return GalleryStrings.tr("IP 连接数量已达上限,请先断开部分连接后重试。", "The IP connection limit has been reached. Disconnect some sessions and try again.", "Достигнут лимит IP-подключений. Завершите часть сеансов и повторите попытку."); } + if code == "UnsupportedNetworkPath" || code == "NetworkPolicyFailed" { return GalleryStrings.tr("当前网络路径不支持 IP 连接,请检查双方的局域网连接。", "This network path does not support IP connections. Check both devices' LAN connections.", "Этот сетевой маршрут не поддерживает IP-подключения. Проверьте подключения обоих устройств к LAN."); } + if code == "IncompatibleProtocol" || code == "RequiredCryptoUnsupported" || code == "TLS 1.3 is required for LAN connections" { return GalleryStrings.tr("双方的协议或加密能力不兼容,请将双方 CrossDesk 升级至最新版本后重试。", "The protocol or encryption capabilities are incompatible. Update CrossDesk on both devices and try again.", "Протоколы или возможности шифрования несовместимы. Обновите CrossDesk на обоих устройствах и повторите попытку."); } + if code == "InvalidCertificate" || code == "FingerprintMissing" || code == "FingerprintMismatch" { return GalleryStrings.tr("无法验证对方设备的身份,请确认连接地址和对方设备后重试。", "Could not verify the remote device's identity. Check the address and the remote device before retrying.", "Не удалось проверить подлинность удалённого устройства. Проверьте адрес и устройство перед повторной попыткой."); } + if code == "DtlsHandshakeFailed" || code == "SrtpInstallFailed" || code == "SrtpKeyExportFailed" || code == "SecureTransportFailed" { return GalleryStrings.tr("无法建立加密连接,请检查双方网络并重试。", "Could not establish an encrypted connection. Check both devices' networks and try again.", "Не удалось установить зашифрованное соединение. Проверьте сеть на обоих устройствах и повторите попытку."); } + if code == "InvalidSignal" || code == "InvalidLocalSignal" || code == "Invalid LAN protocol message" { return GalleryStrings.tr("连接协商数据无效,请重试并确认双方 CrossDesk 版本兼容。", "Invalid connection negotiation data. Try again and check that both CrossDesk versions are compatible.", "Неверные данные согласования соединения. Повторите попытку и проверьте совместимость версий CrossDesk."); } + if code == "ListenerChanged" { return GalleryStrings.tr("连接地址或监听端口已更改,请确认最新地址后重试。", "The connection address or listening port has changed. Check the current address and try again.", "Адрес подключения или порт прослушивания изменился. Проверьте текущий адрес и повторите попытку."); } + if code == "PeerPreparationFailed" { return GalleryStrings.tr("无法准备远程会话,请稍后重试。", "Could not prepare the remote session. Try again shortly.", "Не удалось подготовить удалённый сеанс. Повторите позже."); } + if code == "Cannot copy text" { return GalleryStrings.tr("复制失败,请重试。", "Could not copy. Try again.", "Не удалось скопировать. Повторите попытку."); } + // Keep diagnostic details for unexpected system errors, with a localized explanation. + return GalleryStrings.tr("IP 连接操作失败,详情:", "IP connection operation failed. Details: ", "Ошибка операции IP-подключения. Подробности: ") + code; } } diff --git a/apps/desktop/src/gui/ui/main_window.slint b/apps/desktop/src/gui/ui/main_window.slint index 40500d6f..47bebad5 100644 --- a/apps/desktop/src/gui/ui/main_window.slint +++ b/apps/desktop/src/gui/ui/main_window.slint @@ -43,6 +43,8 @@ enum CopyFeedback { idle, success, failed } enum SponsorPlatform { afdian, kofi, github } export global UiStrings { + in-out property window-minimize: "Minimize"; + in-out property window-close: "Close window"; in-out property local-desktop: "Local Desktop"; in-out property remote-desktop: "Remote Desktop"; in-out property recent-connections: "Recent Connections"; @@ -65,7 +67,7 @@ export global UiStrings { in-out property cancel: "Cancel"; in-out property save: "Save"; in-out property new-password: "New password (6 characters)"; - in-out property invalid-password: "Password must contain exactly 6 characters"; + in-out property invalid-password: "Use exactly 6 letters (A–Z, a–z) or digits (0–9)."; in-out property edit-alias: "Connection alias"; in-out property delete-connection: "Delete connection"; in-out property confirm-delete: "Delete this recent connection?"; @@ -86,7 +88,7 @@ export global UiStrings { in-out property server-host: "Server host"; in-out property server-port: "Signal port"; in-out property version: "Version"; - in-out property copyright: "© 2026 by JUNKUN DI. All right reserved."; + in-out property copyright: "© 2026 by JUNKUN DI. All rights reserved."; in-out property license: "Licensed under GNU GPL v3."; in-out property signal-connected: "Signal server connected"; in-out property signal-disconnected: "Signal server disconnected"; @@ -122,7 +124,7 @@ export global UiStrings { in-out property wayland-notice-acknowledge: "Got it"; in-out property service-setup-title: "Install CrossDesk Service"; in-out property service-setup-message: "Administrator permission is required to control this device while it is locked."; - in-out property service-settings-label: "Lock Screen Service:"; + in-out property service-settings-label: "Lock-screen control service:"; in-out property install-service: "Install"; in-out property service-installed: "Installed"; in-out property do-not-remind: "Do not remind again"; @@ -898,7 +900,7 @@ export component MainWindow inherits Window { : root.window-active ? #e6e6e6 : transparent; accessible-role: button; - accessible-label: "Minimize"; + accessible-label: UiStrings.window-minimize; accessible-action-default => { wayland-minimize-touch.clicked(); } Rectangle { x: 8px; @@ -924,7 +926,7 @@ export component MainWindow inherits Window { : root.window-active ? #e6e6e6 : transparent; accessible-role: button; - accessible-label: "Close"; + accessible-label: UiStrings.window-close; accessible-action-default => { wayland-close-touch.clicked(); } Path { width: parent.width; @@ -992,7 +994,7 @@ export component MainWindow inherits Window { : minimize-touch.has-hover ? #e5e5e5 : transparent; accessible-role: button; - accessible-label: "Minimize"; + accessible-label: UiStrings.window-minimize; accessible-action-default => { minimize-touch.clicked(); } Text { text: FontAwesomeIcons.minus; @@ -1014,7 +1016,7 @@ export component MainWindow inherits Window { : close-touch.has-hover ? #c42b1c : transparent; accessible-role: button; - accessible-label: "Close"; + accessible-label: UiStrings.window-close; accessible-action-default => { close-touch.clicked(); } Text { text: FontAwesomeIcons.xmark; @@ -1111,9 +1113,9 @@ export component MainWindow inherits Window { width: 136px; height: 28px; background: WorkbenchStyle.field-surface; enabled: !root.connection-pending && !root.lan-connecting; - tooltip: root.lan-mode ? GalleryStrings.tr("切换到 ID 连接模式", "Switch to ID mode", "Переключить в режим ID") : GalleryStrings.tr("切换到 IP 直连模式", "Switch to direct IP mode", "Переключить в режим IP"); + tooltip: root.lan-mode ? GalleryStrings.tr("切换到 ID 连接模式", "Switch to ID connection mode", "Переключить в режим ID") : GalleryStrings.tr("切换到 IP 连接模式", "Switch to IP connection mode", "Переключить в режим IP"); accessible-role: switch; - accessible-label: GalleryStrings.tr("IP 直连模式", "Direct IP mode", "Режим IP"); + accessible-label: GalleryStrings.tr("IP 连接模式", "IP connection mode", "Режим IP"); accessible-checked: root.lan-mode; z: 8; clicked => { @@ -1135,7 +1137,7 @@ export component MainWindow inherits Window { y: 2px; width: (parent.width - 6px) / 2; height: parent.height - 4px; - text: lan ? GalleryStrings.tr("IP 直连", "Direct IP", "Режим IP") : GalleryStrings.tr("ID 连接", "ID mode", "Режим ID"); + text: lan ? GalleryStrings.tr("IP 连接", "IP mode", "Режим IP") : GalleryStrings.tr("ID 连接", "ID mode", "Режим ID"); font-size: 12px; font-weight: 500; color: root.lan-mode == lan ? white : WorkbenchStyle.muted; @@ -1244,7 +1246,7 @@ export component MainWindow inherits Window { x: 0px; y: 0px; width: (parent.width - 16px) / 2; height: parent.height; Text { x: 16px; y: 14px; width: parent.width - 32px; height: 24px; overflow: elide; text: UiStrings.local-desktop; font-size: 16px; font-weight: 600; color: WorkbenchStyle.text; } - Text { x: 16px; y: 38px; width: parent.width - 32px; height: 18px; text: root.lan-mode ? GalleryStrings.tr("将 IP 和密码分享给连接方", "Share your IP and password to get connected", "Передайте IP и пароль для управления") : GalleryStrings.tr("将 ID 和密码分享给连接方", "Share your ID and password to get connected", "Передайте ID и пароль для подключения"); font-size: 11px; color: WorkbenchStyle.muted; overflow: elide; } + Text { x: 16px; y: 38px; width: parent.width - 32px; height: 18px; text: root.lan-mode ? GalleryStrings.tr("将 IP 和密码分享给连接方", "Share your IP and password to get connected", "Передайте IP и пароль для подключения") : GalleryStrings.tr("将 ID 和密码分享给连接方", "Share your ID and password to get connected", "Передайте ID и пароль для подключения"); font-size: 11px; color: WorkbenchStyle.muted; overflow: elide; } local-address-label := Text { x: 16px; y: 68px; height: 18px; text: root.lan-mode ? GalleryStrings.tr("本机 IP / 端口", "Local IP / Port", "IP / Порт") : UiStrings.local-id; font-size: 12px; color: WorkbenchStyle.muted; vertical-alignment: top; } lan-switch-ip-metrics := Text { visible: false; accessible-role: none; @@ -1382,7 +1384,7 @@ export component MainWindow inherits Window { font-size: 18px * local-address-field.lan-font-scale; font-weight: 600; input-type: InputType.number; color: WorkbenchStyle.text; enabled: root.lan-mode && !root.lan-password-pending; - accessible-label: GalleryStrings.tr("局域网端口", "LAN port", "Порт ЛВС"); + accessible-label: GalleryStrings.tr("局域网端口", "LAN port", "Порт LAN"); vertical-alignment: center; horizontal-alignment: left; selection-foreground-color: white; selection-background-color: WorkbenchStyle.accent; changed has-focus => { @@ -1567,7 +1569,7 @@ export component MainWindow inherits Window { Text { x: 84px; y: 36px; width: parent.width - 104px; height: 24px; text: root.lan-mode - ? GalleryStrings.tr("暂无 IP 直连设备", "No recent IP connections", "Нет недавних IP-подключений") + ? GalleryStrings.tr("暂无近期 IP 连接", "No recent IP connections", "Нет недавних IP-подключений") : root.recent-group-index == 0 ? GalleryStrings.tr("暂无近期连接", "No recent connections", "Нет недавних подключений") : GalleryStrings.tr("该分组暂无近期连接", "No recent connections in this group", "В этой группе нет недавних подключений"); @@ -1886,7 +1888,7 @@ export component MainWindow inherits Window { height: parent.height; width: parent.width - 160px; overflow: elide; - text: root.lan-mode ? (root.lan-message != "" ? LanStrings.error-message(root.lan-message) : root.lan-listener-error != "" ? (root.lan-listener-address == "" ? "" : root.lan-listener-address + " · ") + LanStrings.error-message(root.lan-listener-error) : root.lan-listening ? GalleryStrings.tr("局域网模式已启用", "LAN mode enabled", "Режим LAN включён") : root.lan-password-pending ? GalleryStrings.tr("局域网模式准备中…", "Preparing LAN mode…", "Подготовка режима LAN…") : GalleryStrings.tr("局域网模式未就绪", "LAN mode not ready", "Режим LAN не готов")) + text: root.lan-mode ? (root.lan-message != "" ? LanStrings.error-message(root.lan-message) : root.lan-listener-error != "" ? (root.lan-listener-address == "" ? "" : root.lan-listener-address + " · ") + LanStrings.error-message(root.lan-listener-error) : root.lan-listening ? GalleryStrings.tr("IP 连接已启用", "IP connections enabled", "Подключения по IP включены") : root.lan-password-pending ? GalleryStrings.tr("IP 连接准备中…", "Preparing IP connections…", "Подготовка подключений по IP…") : GalleryStrings.tr("IP 连接未就绪", "IP connections not ready", "Подключения по IP не готовы")) : root.signal-tls-error ? UiStrings.tls-error : root.signal-connected ? UiStrings.signal-connected : root.signal-reconnecting ? UiStrings.signal-reconnecting @@ -2412,7 +2414,7 @@ export component MainWindow inherits Window { } } Text { - text: GalleryStrings.tr("当前远程控制会话将结束。", "The current remote session will end.", "Текущий сеанс удалённого управления завершится."); + text: GalleryStrings.tr("当前远程控制会话将结束。", "The current remote session will end.", "Текущий сеанс удалённого управления будет завершён."); color: WorkbenchStyle.muted; font-size: 13px; wrap: word-wrap; @@ -2626,6 +2628,7 @@ export component MainWindow inherits Window { // Center every content section; the scrollbar sits in the right margin. private property content-inset: 26px; private property scrollbar-gutter: 22px; + private property shadow-gutter: 6px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: min(400px, parent.width - 40px); @@ -2638,31 +2641,28 @@ export component MainWindow inherits Window { width: parent.width - 2 * settings-dialog.content-inset; height: 27px; text: UiStrings.settings; font-size: 20px; font-weight: 600; color: #22324a; } + // Match the compact connection-mode switch, centered in the tab row. ShadowSurface { - x: settings-dialog.content-inset; y: 56px; - width: parent.width - 2 * settings-dialog.content-inset; height: 40px; - background: #dfe7f2; border-radius: UiRadius.menu; - shadow-color: UiShadow.edge; - ShadowSurface { - x: 4px + root.settings-tab * (parent.width - 4px) / 2; - y: 4px; width: (parent.width - 12px) / 2; height: parent.height - 8px; - background: WorkbenchStyle.surface; border-radius: UiRadius.control; - shadow-color: UiShadow.edge; - animate x { duration: 140ms; easing: ease-out; } + x: settings-dialog.content-inset; y: 62px; + width: parent.width - 2 * settings-dialog.content-inset; height: 28px; + background: WorkbenchStyle.field-surface; border-radius: UiRadius.control; + border-width: 1px; border-color: WorkbenchStyle.accent; + shadow-compact: true; shadow-color: UiShadow.edge; + Rectangle { + x: root.settings-tab == 1 ? parent.width / 2 + 1px : 2px; + y: 2px; width: (parent.width - 6px) / 2; height: parent.height - 4px; + background: WorkbenchStyle.accent; border-radius: UiRadius.control; + animate x { duration: 180ms; easing: ease-in-out; } } for label[index] in [GalleryStrings.tr("常规", "General", "Общие"), GalleryStrings.tr("桌面墙", "DeskHub", "DeskHub")]: Rectangle { - x: 4px + index * (parent.width - 4px) / 2; - y: 4px; width: (parent.width - 12px) / 2; height: parent.height - 8px; - border-radius: UiRadius.control; - background: root.settings-tab == index ? transparent - : tab-touch.pressed ? WorkbenchStyle.pressed-surface - : tab-touch.has-hover ? WorkbenchStyle.hover-surface : transparent; - animate background { duration: 100ms; } + x: index == 1 ? parent.width / 2 + 1px : 2px; + y: 2px; width: (parent.width - 6px) / 2; height: parent.height - 4px; accessible-role: button; accessible-label: label; accessible-action-default => { tab-touch.clicked(); } Text { - text: label; font-size: 12px; font-weight: root.settings-tab == index ? 600 : 500; - color: root.settings-tab == index ? WorkbenchStyle.accent : WorkbenchStyle.muted; + text: label; font-size: 12px; font-weight: 500; + color: root.settings-tab == index ? white : WorkbenchStyle.muted; + animate color { duration: 180ms; } horizontal-alignment: center; vertical-alignment: center; } tab-touch := TouchArea { @@ -2672,8 +2672,9 @@ export component MainWindow inherits Window { } } if root.settings-tab == 0: settings-scroll := ScrollView { - x: settings-dialog.content-inset; y: 108px; - width: parent.width - 2 * settings-dialog.content-inset + settings-dialog.scrollbar-gutter; + x: settings-dialog.content-inset - settings-dialog.shadow-gutter; y: 108px; + width: parent.width - 2 * settings-dialog.content-inset + + settings-dialog.scrollbar-gutter + settings-dialog.shadow-gutter; height: parent.height - 174px - (root.settings-save-error == "" ? 0px : 28px); viewport-width: self.visible-width; viewport-height: settings-content.preferred-height; @@ -2687,7 +2688,10 @@ export component MainWindow inherits Window { width: settings-scroll.viewport-width; height: settings-scroll.viewport-height; settings-content := VerticalLayout { - padding: 0px; padding-right: settings-dialog.scrollbar-gutter; padding-bottom: 4px; + // Keep the six outer shadow layers inside the scroll clip + // without moving or narrowing the settings cards. + padding: 0px; padding-left: settings-dialog.shadow-gutter; + padding-right: settings-dialog.scrollbar-gutter; padding-bottom: 4px; spacing: 14px; SettingsGroup { title: GalleryStrings.tr("界面", "Interface", "Интерфейс"); rows: 1; @@ -2711,7 +2715,7 @@ export component MainWindow inherits Window { SettingsGroup { title: GalleryStrings.tr("网络连接", "Network", "Сеть"); rows: 3; SettingsToggleRow { - label: GalleryStrings.tr("启用 IP 直连", "Enable direct IP", "Прямое IP-подключение"); + label: GalleryStrings.tr("启用 IP 连接", "Enable IP connections", "Подключение по IP"); checked <=> root.ip-direct-enabled-draft; } SettingsToggleRow { label: UiStrings.force-relay; checked <=> root.force-relay-enabled; enabled: !root.settings-session-active; } @@ -2733,7 +2737,7 @@ export component MainWindow inherits Window { } } Text { - text: GalleryStrings.tr("关闭 IP 直连将隐藏模式按钮,并断开 IP 直连会话。", "Disabling direct IP hides the mode button and disconnects direct IP sessions.", "Отключение скроет кнопку режима и завершит прямые IP-сеансы."); + text: GalleryStrings.tr("关闭 IP 连接将隐藏模式按钮,并断开 IP 连接会话。", "Disabling IP connections hides the mode button and ends IP sessions.", "Отключение подключений по IP скроет кнопку режима и завершит IP-сеансы."); font-size: 11px; color: WorkbenchStyle.muted; wrap: word-wrap; } SettingsGroup { @@ -2829,7 +2833,7 @@ export component MainWindow inherits Window { text: root.autostart-status == AutostartStatus.unsupported ? GalleryStrings.tr( "当前设备不支持开机自启动。", "This device does not support auto start.", "Это устройство не поддерживает автозапуск.") : root.autostart-status == AutostartStatus.failed ? GalleryStrings.tr( - "无法读取系统状态。请刷新重试,或在系统设置中核对。", "Could not read the system status. Retry or check it in Settings.", "Не удалось прочитать состояние. Повторите попытку или проверьте настройки системы.") : + "无法读取系统状态。请刷新重试,或在系统设置中核对。", "Could not read the system status. Refresh to retry, or check System Settings.", "Не удалось прочитать состояние. Повторите попытку или проверьте настройки системы.") : GalleryStrings.tr("请在系统设置中添加或移除本应用,返回后会自动刷新状态。", "Add or remove this app in system Settings. The status refreshes when you return.", "Добавьте или удалите приложение в настройках системы. После возвращения статус обновится."); color: WorkbenchStyle.text; font-size: 13px; wrap: word-wrap; } @@ -2844,7 +2848,7 @@ export component MainWindow inherits Window { } SettingsButton { x: 24px; y: parent.height - 48px; width: 112px; - text: GalleryStrings.tr("刷新状态", "Refresh status", "Обновить"); + text: GalleryStrings.tr("刷新状态", "Refresh status", "Обновить статус"); enabled: root.autostart-status != AutostartStatus.checking; clicked => { root.refresh-autostart-status(); } } diff --git a/apps/desktop/src/gui/ui/settings_controls.slint b/apps/desktop/src/gui/ui/settings_controls.slint index 903ee618..5a053c61 100644 --- a/apps/desktop/src/gui/ui/settings_controls.slint +++ b/apps/desktop/src/gui/ui/settings_controls.slint @@ -1,6 +1,6 @@ import { ShadowSeparator, ShadowSurface, UiShadow } from "ui_shadow.slint"; import { UiRadius } from "ui_radius.slint"; -import { CompactComboBox, ToggleSwitch, DialogSurface, IconButton, FontAwesomeIcons, ControlStyle } from "common.slint"; +import { CompactComboBox, ToggleSwitch, DialogSurface, IconButton, FontAwesomeIcons, ControlStyle, ButtonStyle } from "common.slint"; import { WorkbenchStyle } from "workbench_controls.slint"; // Shared visual treatment for the global settings pages. @@ -18,12 +18,18 @@ export component SettingsButton inherits ShadowSurface { width: 88px; height: 32px; border-radius: UiRadius.control; background: !root.enabled ? ControlStyle.disabled : root.primary ? (touch.pressed ? #1e4fa3 : touch.has-hover ? #2866c9 : #3275dc) - : touch.pressed ? ControlStyle.pressed : touch.has-hover ? ControlStyle.hover - : root.quiet ? transparent : ControlStyle.surface; - shadow-enabled: !root.quiet && !root.primary; - shadow-color: !root.enabled ? UiShadow.edge + : touch.pressed ? ButtonStyle.pressed : touch.has-hover ? ButtonStyle.hover + : root.quiet ? transparent : ButtonStyle.surface; + border-width: root.quiet || root.primary ? 0px : 1px; + border-color: !root.enabled ? ButtonStyle.disabled-border + : touch.pressed ? ButtonStyle.pressed-border + : touch.has-hover ? ButtonStyle.hover-border : ButtonStyle.border; + shadow-enabled: root.enabled && !root.quiet && !touch.pressed; + shadow-color: root.primary ? UiShadow.focus : touch.has-hover ? UiShadow.hover : UiShadow.edge; + animate background, border-color { duration: 120ms; } accessible-role: button; accessible-label: root.text; + accessible-enabled: root.enabled; accessible-action-default => { if root.enabled { root.clicked(); } } Rectangle { x: root.align-left ? root.content-padding : (parent.width - self.width) / 2; @@ -113,7 +119,7 @@ export component SettingsDialog inherits DialogSurface { export component SettingsSelect inherits CompactComboBox { subtle: true; - show-arrow: false; + show-arrow: true; normal-shadow-color: UiShadow.edge; hover-shadow-color: UiShadow.hover; height: 28px; diff --git a/apps/desktop/src/gui/ui/stream_window.slint b/apps/desktop/src/gui/ui/stream_window.slint index 463e1bdf..0ccc416d 100644 --- a/apps/desktop/src/gui/ui/stream_window.slint +++ b/apps/desktop/src/gui/ui/stream_window.slint @@ -2,7 +2,7 @@ import { ShadowSeparator, ShadowSurface, UiShadow } from "ui_shadow.slint"; import { UiRadius } from "ui_radius.slint"; import { ComboBox, ScrollView } from "std-widgets.slint"; import { RemoteCursor } from "remote_cursor.slint"; -import { FontAwesomeIcons, IconButton, PrimaryButton, SecondaryButton, DialogSurface, ImGuiFontStyle, ControlStyle } from "common.slint"; +import { FontAwesomeIcons, IconButton, PrimaryButton, SecondaryButton, DialogSurface, ImGuiFontStyle, ControlStyle, ButtonStyle } from "common.slint"; export struct StreamTab { remote-id: string, @@ -26,6 +26,10 @@ export struct NetworkStatsRow { } export global StreamStrings { + in-out property window-minimize: "Minimize"; + in-out property window-maximize: "Maximize"; + in-out property window-restore: "Restore window"; + in-out property window-close: "Close window"; in-out property remote-update-title: "Remote update recommended"; in-out property remote-update: "Update the remote device to the latest version for improved features and performance."; in-out property remote-update-dismiss: "Got it"; @@ -47,11 +51,11 @@ export global StreamStrings { in-out property privacy-disable: "Turn privacy off / recover"; in-out property control-mouse: "Control Mouse"; in-out property release-mouse: "Release Mouse"; - in-out property audio: "Audio Capture"; + in-out property audio: "Play audio"; in-out property mute: "Mute"; in-out property select-file: "Select File to Send"; - in-out property show-stats: "Show Net Traffic Stats"; - in-out property hide-stats: "Hide Net Traffic Stats"; + in-out property show-stats: "Show network status"; + in-out property hide-stats: "Hide network status"; in-out property fullscreen: "Fullscreen"; in-out property exit-fullscreen: "Exit fullscreen"; in-out property disconnect: "Disconnect"; @@ -65,7 +69,7 @@ export global StreamStrings { in-out property stats-out: "Out"; in-out property stats-loss-rate: "Loss Rate"; in-out property stats-resolution: "Res"; - in-out property stats-video-latency: "Video delay"; + in-out property stats-video-latency: "Video latency"; in-out property stats-connection-latency: "Connection RTT"; in-out property stats-connection-mode: "Mode"; in-out property stats-encoder: "Remote encoder"; @@ -245,13 +249,21 @@ component VideoSettingRow inherits VerticalLayout { for option[index] in root.options: ShadowSurface { height: 27px; background: !root.enabled ? ControlStyle.disabled - : root.selected == index ? (option-touch.pressed ? ControlStyle.selected-pressed : option-touch.has-hover ? ControlStyle.selected-hover : ControlStyle.selected) - : option-touch.pressed ? ControlStyle.pressed - : option-touch.has-hover ? ControlStyle.hover : ControlStyle.surface; + : root.selected == index ? (option-touch.pressed ? ButtonStyle.selected-pressed : option-touch.has-hover ? ButtonStyle.selected-hover : ButtonStyle.selected) + : option-touch.pressed ? ButtonStyle.pressed + : option-touch.has-hover ? ButtonStyle.hover : ButtonStyle.surface; border-radius: UiRadius.control; + border-width: 1px; + border-color: !root.enabled ? ButtonStyle.disabled-border + : root.selected == index ? ButtonStyle.selected-pressed + : option-touch.pressed ? ButtonStyle.pressed-border + : option-touch.has-hover ? ButtonStyle.hover-border : ButtonStyle.border; + shadow-compact: true; + shadow-enabled: root.enabled && !option-touch.pressed; shadow-color: root.enabled && root.selected == index ? UiShadow.focus : UiShadow.edge; + animate background, border-color { duration: 120ms; } Text { text: option; color: !root.enabled ? #8b8f95 - : root.selected == index ? ControlStyle.selected-text : #30343b; + : root.selected == index ? white : #30343b; font-weight: root.selected == index ? 600 : 400; font-size: 11px; horizontal-alignment: center; vertical-alignment: center; } option-touch := TouchArea { @@ -809,7 +821,7 @@ export component StreamWindow inherits Window { : root.window-active ? #e6e6e6 : transparent; accessible-role: button; - accessible-label: "Minimize"; + accessible-label: StreamStrings.window-minimize; accessible-action-default => { stream-minimize-touch.clicked(); } Rectangle { x: 8px; @@ -835,7 +847,7 @@ export component StreamWindow inherits Window { : root.window-active ? #e6e6e6 : transparent; accessible-role: button; - accessible-label: root.window-maximized ? "Restore" : "Maximize"; + accessible-label: root.window-maximized ? StreamStrings.window-restore : StreamStrings.window-maximize; accessible-action-default => { stream-maximize-touch.clicked(); } if !root.window-maximized: Rectangle { x: 8px; @@ -885,7 +897,7 @@ export component StreamWindow inherits Window { : root.window-active ? #e6e6e6 : transparent; accessible-role: button; - accessible-label: "Close"; + accessible-label: StreamStrings.window-close; accessible-action-default => { stream-close-touch.clicked(); } Path { width: parent.width; diff --git a/apps/desktop/src/gui/ui/ui_localization.h b/apps/desktop/src/gui/ui/ui_localization.h index e8bcd3e9..a8796882 100644 --- a/apps/desktop/src/gui/ui/ui_localization.h +++ b/apps/desktop/src/gui/ui/ui_localization.h @@ -42,6 +42,11 @@ inline int ApplyMainWindowStrings( localization::detail::ClampLanguageIndex(language_index); window->global().set_language(language); auto& strings = window->global(); + strings.set_window_minimize(Text(localization::window_minimize[language])); + strings.set_window_close(Text(localization::window_close[language])); + strings.set_save(Text(localization::save[language])); + strings.set_copyright(Text(localization::copyright_notice[language])); + strings.set_license(Text(localization::license_notice[language])); auto& messages = window->global(); messages.set_messages(Text(localization::messages[language])); messages.set_unread(Text(localization::message_unread[language])); @@ -81,7 +86,7 @@ inline int ApplyMainWindowStrings( strings.set_ok(Text(localization::ok[language])); strings.set_cancel(Text(localization::cancel[language])); strings.set_new_password(Text(localization::new_password[language])); - strings.set_invalid_password(Text(localization::max_password_len[language])); + strings.set_invalid_password(Text(localization::password_format_invalid[language])); strings.set_edit_alias( Text(localization::input_connection_alias[language])); strings.set_delete_connection( @@ -192,6 +197,10 @@ inline int ApplyStreamWindowStrings( const int language = localization::detail::ClampLanguageIndex(language_index); auto& strings = window->template global(); + strings.set_window_minimize(Text(localization::window_minimize[language])); + strings.set_window_maximize(Text(localization::window_maximize[language])); + strings.set_window_restore(Text(localization::window_restore[language])); + strings.set_window_close(Text(localization::window_close[language])); strings.set_remote_update_title(Text(localization::remote_update_title[language])); strings.set_remote_update(Text(localization::remote_update[language])); strings.set_remote_update_dismiss(Text(localization::remote_update_dismiss[language])); diff --git a/apps/desktop/src/gui/ui/workbench_controls.slint b/apps/desktop/src/gui/ui/workbench_controls.slint index 6e3c94c9..00d08e8b 100644 --- a/apps/desktop/src/gui/ui/workbench_controls.slint +++ b/apps/desktop/src/gui/ui/workbench_controls.slint @@ -1,6 +1,6 @@ import { ShadowSurface, UiShadow } from "ui_shadow.slint"; import { UiRadius } from "ui_radius.slint"; -import { ControlStyle } from "common.slint"; +import { ControlStyle, ButtonStyle } from "common.slint"; // Colors and spacing for the connection workbench; shapes are shared app-wide. export global WorkbenchStyle { out property canvas: #edf2f8; @@ -21,7 +21,7 @@ export component WorkbenchButton inherits ShadowSurface { in property text-size: 13px; in property text-color: root.destructive || root.primary ? white : WorkbenchStyle.text; in property normal-shadow-color: UiShadow.edge; - in property normal-background: ControlStyle.surface; + in property normal-background: ButtonStyle.surface; in property icon; in property tooltip; in property tooltip-above: false; @@ -46,7 +46,12 @@ export component WorkbenchButton inherits ShadowSurface { width: 112px; height: 36px; border-radius: UiRadius.control; - shadow-enabled: root.show-focus || !root.soft; + border-width: root.show-focus || (!root.quiet && !root.soft && !root.primary && !root.destructive) ? 1px : 0px; + border-color: !root.enabled ? ButtonStyle.disabled-border + : root.show-focus ? WorkbenchStyle.accent + : touch.pressed ? ButtonStyle.pressed-border + : touch.has-hover ? ButtonStyle.hover-border : ButtonStyle.border; + shadow-enabled: root.enabled && (root.show-focus || (!root.soft && !root.quiet && !touch.pressed)); shadow-focused: root.show-focus; shadow-color: root.destructive ? UiShadow.error : root.show-focus || root.primary ? UiShadow.focus @@ -57,12 +62,12 @@ export component WorkbenchButton inherits ShadowSurface { : root.soft ? (root.primary ? (touch.pressed ? #254cc0 : touch.has-hover || root.show-focus ? WorkbenchStyle.accent : #dce7ff) - : (touch.pressed ? ControlStyle.pressed : touch.has-hover ? ControlStyle.hover : ControlStyle.surface)) + : (touch.pressed ? ButtonStyle.pressed : touch.has-hover ? ButtonStyle.hover : ButtonStyle.surface)) : root.primary ? (touch.pressed ? #254cc0 : touch.has-hover ? #4574f0 : WorkbenchStyle.accent) - : (touch.pressed ? ControlStyle.pressed : touch.has-hover ? ControlStyle.hover : root.quiet ? transparent : root.normal-background); + : (touch.pressed ? ButtonStyle.pressed : touch.has-hover ? ButtonStyle.hover : root.quiet ? transparent : root.normal-background); opacity: root.enabled ? 1 : 0.45; - animate background, shadow-color { duration: 120ms; } + animate background, border-color, shadow-color { duration: 120ms; } forward-focus: button-focus; accessible-role: button; accessible-label: root.tooltip != "" ? root.tooltip : root.text;