crate(i18n): add clash-verge-i18n crate and integrate localization support (#5961)

* crate(i18n): add clash-verge-i18n crate and integrate localization support

* refactor(service): remove redundant reinstall_service functions for Windows, Linux, and macOS

* chore(i18n): align i18n key

* feat(i18n): unify scan roots and add backend Rust/YAML support to cleanup script

* chore(i18n): add scripts to package.json

* fix(tray): initialize i18n locale before setup

* refactor(i18n): move locale initialization into Config::init_config

* fix(i18n): refresh systray tooltip on language change and correct docs reference

* fix(tray): remove unnecessary locale synchronization to improve performance

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
This commit is contained in:
Sline
2025-12-27 15:03:19 +08:00
committed by GitHub
co-authored by Tunglies
parent 1b477ed0b2
commit c8aeae3f83
31 changed files with 486 additions and 312 deletions
+19 -60
View File
@@ -30,9 +30,8 @@ pub enum ServiceStatus {
#[derive(Clone)]
pub struct ServiceManager(ServiceStatus);
#[allow(clippy::unused_async)]
#[cfg(target_os = "windows")]
async fn uninstall_service() -> Result<()> {
fn uninstall_service() -> Result<()> {
logging!(info, Type::Service, "uninstall service");
use deelevate::{PrivilegeLevel, Token};
@@ -63,9 +62,8 @@ async fn uninstall_service() -> Result<()> {
Ok(())
}
#[allow(clippy::unused_async)]
#[cfg(target_os = "windows")]
async fn install_service() -> Result<()> {
fn install_service() -> Result<()> {
logging!(info, Type::Service, "install service");
use deelevate::{PrivilegeLevel, Token};
@@ -93,27 +91,8 @@ async fn install_service() -> Result<()> {
Ok(())
}
#[cfg(target_os = "windows")]
async fn reinstall_service() -> Result<()> {
logging!(info, Type::Service, "reinstall service");
// 先卸载服务
if let Err(err) = uninstall_service().await {
logging!(warn, Type::Service, "failed to uninstall service: {}", err);
}
// 再安装服务
match install_service().await {
Ok(_) => Ok(()),
Err(err) => {
bail!(format!("failed to install service: {err}"))
}
}
}
#[allow(clippy::unused_async)]
#[cfg(target_os = "linux")]
async fn uninstall_service() -> Result<()> {
fn uninstall_service() -> Result<()> {
logging!(info, Type::Service, "uninstall service");
let uninstall_path = tauri::utils::platform::current_exe()?.with_file_name("clash-verge-service-uninstall");
@@ -169,8 +148,7 @@ async fn uninstall_service() -> Result<()> {
}
#[cfg(target_os = "linux")]
#[allow(clippy::unused_async)]
async fn install_service() -> Result<()> {
fn install_service() -> Result<()> {
logging!(info, Type::Service, "install service");
let install_path = tauri::utils::platform::current_exe()?.with_file_name("clash-verge-service-install");
@@ -222,24 +200,6 @@ async fn install_service() -> Result<()> {
Ok(())
}
#[cfg(target_os = "linux")]
async fn reinstall_service() -> Result<()> {
logging!(info, Type::Service, "reinstall service");
// 先卸载服务
if let Err(err) = uninstall_service().await {
logging!(warn, Type::Service, "failed to uninstall service: {}", err);
}
// 再安装服务
match install_service().await {
Ok(_) => Ok(()),
Err(err) => {
bail!(format!("failed to install service: {err}"))
}
}
}
#[cfg(target_os = "linux")]
fn linux_running_as_root() -> bool {
use crate::core::handle;
@@ -249,7 +209,7 @@ fn linux_running_as_root() -> bool {
}
#[cfg(target_os = "macos")]
async fn uninstall_service() -> Result<()> {
fn uninstall_service() -> Result<()> {
logging!(info, Type::Service, "uninstall service");
let binary_path = dirs::service_path()?;
@@ -261,9 +221,9 @@ async fn uninstall_service() -> Result<()> {
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
crate::utils::i18n::sync_locale().await;
// clash_verge_i18n::sync_locale(Config::verge().await.latest_arc().language.as_deref());
let prompt = rust_i18n::t!("service.adminUninstallPrompt").to_string();
let prompt = clash_verge_i18n::t!("service.adminUninstallPrompt");
let command =
format!(r#"do shell script "sudo '{uninstall_shell}'" with administrator privileges with prompt "{prompt}""#);
@@ -282,7 +242,7 @@ async fn uninstall_service() -> Result<()> {
}
#[cfg(target_os = "macos")]
async fn install_service() -> Result<()> {
fn install_service() -> Result<()> {
logging!(info, Type::Service, "install service");
let binary_path = dirs::service_path()?;
@@ -294,9 +254,9 @@ async fn install_service() -> Result<()> {
let install_shell: String = install_path.to_string_lossy().into_owned();
crate::utils::i18n::sync_locale().await;
// clash_verge_i18n::sync_locale(Config::verge().await.latest_arc().language.as_deref());
let prompt = rust_i18n::t!("service.adminInstallPrompt").to_string();
let prompt = clash_verge_i18n::t!("service.adminInstallPrompt");
let command =
format!(r#"do shell script "sudo '{install_shell}'" with administrator privileges with prompt "{prompt}""#);
@@ -309,17 +269,16 @@ async fn install_service() -> Result<()> {
Ok(())
}
#[cfg(target_os = "macos")]
async fn reinstall_service() -> Result<()> {
fn reinstall_service() -> Result<()> {
logging!(info, Type::Service, "reinstall service");
// 先卸载服务
if let Err(err) = uninstall_service().await {
if let Err(err) = uninstall_service() {
logging!(warn, Type::Service, "failed to uninstall service: {}", err);
}
// 再安装服务
match install_service().await {
match install_service() {
Ok(_) => Ok(()),
Err(err) => {
bail!(format!("failed to install service: {err}"))
@@ -328,9 +287,9 @@ async fn reinstall_service() -> Result<()> {
}
/// 强制重装服务(UI修复按钮)
async fn force_reinstall_service() -> Result<()> {
fn force_reinstall_service() -> Result<()> {
logging!(info, Type::Service, "用户请求强制重装服务");
reinstall_service().await.map_err(|err| {
reinstall_service().map_err(|err| {
logging!(error, Type::Service, "强制重装服务失败: {}", err);
err
})
@@ -550,22 +509,22 @@ impl ServiceManager {
}
ServiceStatus::NeedsReinstall | ServiceStatus::ReinstallRequired => {
logging!(info, Type::Service, "服务需要重装,执行重装流程");
reinstall_service().await?;
reinstall_service()?;
wait_and_check_service_available(self).await?;
}
ServiceStatus::ForceReinstallRequired => {
logging!(info, Type::Service, "服务需要强制重装,执行强制重装流程");
force_reinstall_service().await?;
force_reinstall_service()?;
wait_and_check_service_available(self).await?;
}
ServiceStatus::InstallRequired => {
logging!(info, Type::Service, "需要安装服务,执行安装流程");
install_service().await?;
install_service()?;
wait_and_check_service_available(self).await?;
}
ServiceStatus::UninstallRequired => {
logging!(info, Type::Service, "服务需要卸载,执行卸载流程");
uninstall_service().await?;
uninstall_service()?;
self.0 = ServiceStatus::Unavailable("Service Uninstalled".into());
}
ServiceStatus::Unavailable(reason) => {
+6 -3
View File
@@ -1,8 +1,11 @@
use rust_i18n::t;
use clash_verge_i18n::t;
use std::{borrow::Cow, sync::Arc};
fn to_arc_str(value: Cow<'static, str>) -> Arc<str> {
match value {
fn to_arc_str<S>(value: S) -> Arc<str>
where
S: Into<Cow<'static, str>>,
{
match value.into() {
Cow::Borrowed(s) => Arc::from(s),
Cow::Owned(s) => Arc::from(s.into_boxed_str()),
}
+8 -15
View File
@@ -12,11 +12,8 @@ use crate::process::AsyncHandler;
use crate::singleton;
use crate::utils::window_manager::WindowManager;
use crate::{
Type, cmd,
config::Config,
feat, logging,
module::lightweight::is_in_lightweight_mode,
utils::{dirs::find_target_icons, i18n},
Type, cmd, config::Config, feat, logging, module::lightweight::is_in_lightweight_mode,
utils::dirs::find_target_icons,
};
use super::handle;
@@ -389,8 +386,6 @@ impl Tray {
let app_handle = handle::Handle::app_handle();
i18n::sync_locale().await;
let verge = Config::verge().await.latest_arc();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
@@ -417,9 +412,9 @@ impl Tray {
}
// Get localized strings before using them
let sys_proxy_text = rust_i18n::t!("tray.tooltip.systemProxy");
let tun_text = rust_i18n::t!("tray.tooltip.tun");
let profile_text = rust_i18n::t!("tray.tooltip.profile");
let sys_proxy_text = clash_verge_i18n::t!("tray.tooltip.systemProxy");
let tun_text = clash_verge_i18n::t!("tray.tooltip.tun");
let profile_text = clash_verge_i18n::t!("tray.tooltip.profile");
let v = env!("CARGO_PKG_VERSION");
let reassembled_version = v.split_once('+').map_or_else(
@@ -724,8 +719,6 @@ async fn create_tray_menu(
) -> Result<tauri::menu::Menu<Wry>> {
let current_proxy_mode = mode.unwrap_or("");
i18n::sync_locale().await;
// TODO: should update tray menu again when it was timeout error
let proxy_nodes_data = tokio::time::timeout(
Duration::from_millis(1000),
@@ -829,9 +822,9 @@ async fn create_tray_menu(
None
} else {
let current_mode_text = match current_proxy_mode {
"global" => rust_i18n::t!("tray.global"),
"direct" => rust_i18n::t!("tray.direct"),
_ => rust_i18n::t!("tray.rule"),
"global" => clash_verge_i18n::t!("tray.global"),
"direct" => clash_verge_i18n::t!("tray.direct"),
_ => clash_verge_i18n::t!("tray.rule"),
};
let outbound_modes_label = format!("{} ({})", texts.outbound_modes, current_mode_text);
Some(Submenu::with_id_and_items(