feat(i18n): complete internationalization for usage query feature

Fully internationalized the usage query feature to support both Chinese and English.

Frontend changes:
- Refactored UsageScriptModal to use i18n translation keys
- Replaced hardcoded Chinese template names with constants
- Implemented dynamic template generation with i18n support
- Internationalized all labels, placeholders, and code comments
- Added template name mapping for translation

Backend changes:
- Replaced all hardcoded Chinese error messages in usage_script.rs
- Converted 33 error instances from AppError::Message to AppError::localized
- Added bilingual error messages for runtime, parsing, HTTP, and validation errors

Translation updates:
- Added 11 new translation key pairs to zh.json and en.json
- Covered template names, field labels, placeholders, and code comments

Impact:
- 100% i18n coverage for usage query functionality
- All user-facing text and error messages now support language switching
- Better user experience for English-speaking users
This commit is contained in:
Jason
2025-11-05 00:07:54 +08:00
parent bafddb8e52
commit 720c4d9774
4 changed files with 91 additions and 59 deletions

View File

@@ -31,28 +31,28 @@ pub async fn execute_usage_script(
// 2. 在独立作用域中提取 request 配置(确保 Runtime/Context 在 await 前释放)
let request_config = {
let runtime =
Runtime::new().map_err(|e| AppError::Message(format!("创建 JS 运行时失败: {}", e)))?;
Runtime::new().map_err(|e| AppError::localized("usage_script.runtime_create_failed", format!("创建 JS 运行时失败: {}", e), format!("Failed to create JS runtime: {}", e)))?;
let context = Context::full(&runtime)
.map_err(|e| AppError::Message(format!("创建 JS 上下文失败: {}", e)))?;
.map_err(|e| AppError::localized("usage_script.context_create_failed", format!("创建 JS 上下文失败: {}", e), format!("Failed to create JS context: {}", e)))?;
context.with(|ctx| {
// 执行用户代码,获取配置对象
let config: rquickjs::Object = ctx
.eval(replaced.clone())
.map_err(|e| AppError::Message(format!("解析配置失败: {}", e)))?;
.map_err(|e| AppError::localized("usage_script.config_parse_failed", format!("解析配置失败: {}", e), format!("Failed to parse config: {}", e)))?;
// 提取 request 配置
let request: rquickjs::Object = config
.get("request")
.map_err(|e| AppError::Message(format!("缺少 request 配置: {}", e)))?;
.map_err(|e| AppError::localized("usage_script.request_missing", format!("缺少 request 配置: {}", e), format!("Missing request config: {}", e)))?;
// 将 request 转换为 JSON 字符串
let request_json: String = ctx
.json_stringify(request)
.map_err(|e| AppError::Message(format!("序列化 request 失败: {}", e)))?
.ok_or_else(|| AppError::Message("序列化返回 None".into()))?
.map_err(|e| AppError::localized("usage_script.request_serialize_failed", format!("序列化 request 失败: {}", e), format!("Failed to serialize request: {}", e)))?
.ok_or_else(|| AppError::localized("usage_script.serialize_none", "序列化返回 None", "Serialization returned None"))?
.get()
.map_err(|e| AppError::Message(format!("获取字符串失败: {}", e)))?;
.map_err(|e| AppError::localized("usage_script.get_string_failed", format!("获取字符串失败: {}", e), format!("Failed to get string: {}", e)))?;
Ok::<_, AppError>(request_json)
})?
@@ -60,7 +60,7 @@ pub async fn execute_usage_script(
// 3. 解析 request 配置
let request: RequestConfig = serde_json::from_str(&request_config)
.map_err(|e| AppError::Message(format!("request 配置格式错误: {}", e)))?;
.map_err(|e| AppError::localized("usage_script.request_format_invalid", format!("request 配置格式错误: {}", e), format!("Invalid request config format: {}", e)))?;
// 4. 发送 HTTP 请求
let response_data = send_http_request(&request, timeout_secs).await?;
@@ -68,42 +68,42 @@ pub async fn execute_usage_script(
// 5. 在独立作用域中执行 extractor确保 Runtime/Context 在函数结束前释放)
let result: Value = {
let runtime =
Runtime::new().map_err(|e| AppError::Message(format!("创建 JS 运行时失败: {}", e)))?;
Runtime::new().map_err(|e| AppError::localized("usage_script.runtime_create_failed", format!("创建 JS 运行时失败: {}", e), format!("Failed to create JS runtime: {}", e)))?;
let context = Context::full(&runtime)
.map_err(|e| AppError::Message(format!("创建 JS 上下文失败: {}", e)))?;
.map_err(|e| AppError::localized("usage_script.context_create_failed", format!("创建 JS 上下文失败: {}", e), format!("Failed to create JS context: {}", e)))?;
context.with(|ctx| {
// 重新 eval 获取配置对象
let config: rquickjs::Object = ctx
.eval(replaced.clone())
.map_err(|e| AppError::Message(format!("重新解析配置失败: {}", e)))?;
.map_err(|e| AppError::localized("usage_script.config_reparse_failed", format!("重新解析配置失败: {}", e), format!("Failed to re-parse config: {}", e)))?;
// 提取 extractor 函数
let extractor: Function = config
.get("extractor")
.map_err(|e| AppError::Message(format!("缺少 extractor 函数: {}", e)))?;
.map_err(|e| AppError::localized("usage_script.extractor_missing", format!("缺少 extractor 函数: {}", e), format!("Missing extractor function: {}", e)))?;
// 将响应数据转换为 JS 值
let response_js: rquickjs::Value = ctx
.json_parse(response_data.as_str())
.map_err(|e| AppError::Message(format!("解析响应 JSON 失败: {}", e)))?;
.map_err(|e| AppError::localized("usage_script.response_parse_failed", format!("解析响应 JSON 失败: {}", e), format!("Failed to parse response JSON: {}", e)))?;
// 调用 extractor(response)
let result_js: rquickjs::Value = extractor
.call((response_js,))
.map_err(|e| AppError::Message(format!("执行 extractor 失败: {}", e)))?;
.map_err(|e| AppError::localized("usage_script.extractor_exec_failed", format!("执行 extractor 失败: {}", e), format!("Failed to execute extractor: {}", e)))?;
// 转换为 JSON 字符串
let result_json: String = ctx
.json_stringify(result_js)
.map_err(|e| AppError::Message(format!("序列化结果失败: {}", e)))?
.ok_or_else(|| AppError::Message("序列化返回 None".into()))?
.map_err(|e| AppError::localized("usage_script.result_serialize_failed", format!("序列化结果失败: {}", e), format!("Failed to serialize result: {}", e)))?
.ok_or_else(|| AppError::localized("usage_script.serialize_none", "序列化返回 None", "Serialization returned None"))?
.get()
.map_err(|e| AppError::Message(format!("获取字符串失败: {}", e)))?;
.map_err(|e| AppError::localized("usage_script.get_string_failed", format!("获取字符串失败: {}", e), format!("Failed to get string: {}", e)))?;
// 解析为 serde_json::Value
serde_json::from_str(&result_json)
.map_err(|e| AppError::Message(format!("JSON 解析失败: {}", e)))
.map_err(|e| AppError::localized("usage_script.json_parse_failed", format!("JSON 解析失败: {}", e), format!("JSON parse failed: {}", e)))
})?
}; // Runtime 和 Context 在这里被 drop
@@ -131,7 +131,7 @@ async fn send_http_request(config: &RequestConfig, timeout_secs: u64) -> Result<
let client = Client::builder()
.timeout(Duration::from_secs(timeout))
.build()
.map_err(|e| AppError::Message(format!("创建客户端失败: {}", e)))?;
.map_err(|e| AppError::localized("usage_script.client_create_failed", format!("创建客户端失败: {}", e), format!("Failed to create client: {}", e)))?;
// 严格校验 HTTP 方法,非法值不回退为 GET
let method: reqwest::Method = config
@@ -155,13 +155,13 @@ async fn send_http_request(config: &RequestConfig, timeout_secs: u64) -> Result<
let resp = req
.send()
.await
.map_err(|e| AppError::Message(format!("请求失败: {}", e)))?;
.map_err(|e| AppError::localized("usage_script.request_failed", format!("请求失败: {}", e), format!("Request failed: {}", e)))?;
let status = resp.status();
let text = resp
.text()
.await
.map_err(|e| AppError::Message(format!("读取响应失败: {}", e)))?;
.map_err(|e| AppError::localized("usage_script.read_response_failed", format!("读取响应失败: {}", e), format!("Failed to read response: {}", e)))?;
if !status.is_success() {
let preview = if text.len() > 200 {
@@ -169,7 +169,7 @@ async fn send_http_request(config: &RequestConfig, timeout_secs: u64) -> Result<
} else {
text.clone()
};
return Err(AppError::Message(format!("HTTP {} : {}", status, preview)));
return Err(AppError::localized("usage_script.http_error", format!("HTTP {} : {}", status, preview), format!("HTTP {} : {}", status, preview)));
}
Ok(text)
@@ -180,11 +180,11 @@ fn validate_result(result: &Value) -> Result<(), AppError> {
// 如果是数组,验证每个元素
if let Some(arr) = result.as_array() {
if arr.is_empty() {
return Err(AppError::InvalidInput("脚本返回的数组不能为空".into()));
return Err(AppError::localized("usage_script.empty_array", "脚本返回的数组不能为空", "Script returned empty array"));
}
for (idx, item) in arr.iter().enumerate() {
validate_single_usage(item)
.map_err(|e| AppError::InvalidInput(format!("数组索引[{}]验证失败: {}", idx, e)))?;
.map_err(|e| AppError::localized("usage_script.array_validation_failed", format!("数组索引[{}]验证失败: {}", idx, e), format!("Validation failed at index [{}]: {}", idx, e)))?;
}
return Ok(());
}
@@ -197,48 +197,44 @@ fn validate_result(result: &Value) -> Result<(), AppError> {
fn validate_single_usage(result: &Value) -> Result<(), AppError> {
let obj = result
.as_object()
.ok_or_else(|| AppError::InvalidInput("脚本必须返回对象或对象数组".into()))?;
.ok_or_else(|| AppError::localized("usage_script.must_return_object", "脚本必须返回对象或对象数组", "Script must return object or array of objects"))?;
// 所有字段均为可选,只进行类型检查
if obj.contains_key("isValid")
&& !result["isValid"].is_null()
&& !result["isValid"].is_boolean()
{
return Err(AppError::InvalidInput("isValid 必须是布尔值或 null".into()));
return Err(AppError::localized("usage_script.isvalid_type_error", "isValid 必须是布尔值或 null", "isValid must be boolean or null"));
}
if obj.contains_key("invalidMessage")
&& !result["invalidMessage"].is_null()
&& !result["invalidMessage"].is_string()
{
return Err(AppError::InvalidInput(
"invalidMessage 必须是字符串或 null".into(),
));
return Err(AppError::localized("usage_script.invalidmessage_type_error", "invalidMessage 必须是字符串或 null", "invalidMessage must be string or null"));
}
if obj.contains_key("remaining")
&& !result["remaining"].is_null()
&& !result["remaining"].is_number()
{
return Err(AppError::InvalidInput("remaining 必须是数字或 null".into()));
return Err(AppError::localized("usage_script.remaining_type_error", "remaining 必须是数字或 null", "remaining must be number or null"));
}
if obj.contains_key("unit") && !result["unit"].is_null() && !result["unit"].is_string() {
return Err(AppError::InvalidInput("unit 必须是字符串或 null".into()));
return Err(AppError::localized("usage_script.unit_type_error", "unit 必须是字符串或 null", "unit must be string or null"));
}
if obj.contains_key("total") && !result["total"].is_null() && !result["total"].is_number() {
return Err(AppError::InvalidInput("total 必须是数字或 null".into()));
return Err(AppError::localized("usage_script.total_type_error", "total 必须是数字或 null", "total must be number or null"));
}
if obj.contains_key("used") && !result["used"].is_null() && !result["used"].is_number() {
return Err(AppError::InvalidInput("used 必须是数字或 null".into()));
return Err(AppError::localized("usage_script.used_type_error", "used 必须是数字或 null", "used must be number or null"));
}
if obj.contains_key("planName")
&& !result["planName"].is_null()
&& !result["planName"].is_string()
{
return Err(AppError::InvalidInput(
"planName 必须是字符串或 null".into(),
));
return Err(AppError::localized("usage_script.planname_type_error", "planName 必须是字符串或 null", "planName must be string or null"));
}
if obj.contains_key("extra") && !result["extra"].is_null() && !result["extra"].is_string() {
return Err(AppError::InvalidInput("extra 必须是字符串或 null".into()));
return Err(AppError::localized("usage_script.extra_type_error", "extra 必须是字符串或 null", "extra must be string or null"));
}
Ok(())