i18n增加校验,检查系统用到的多语言字段是否在csv文件中定义

This commit is contained in:
李运家 2024-09-26 16:42:23 +08:00
parent f7dca5ba30
commit ba544174b7
13 changed files with 104 additions and 53 deletions

30
Cargo.lock generated
View File

@ -292,6 +292,7 @@ dependencies = [
name = "chuanyue-service" name = "chuanyue-service"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"i18n",
"library", "library",
"mimalloc", "mimalloc",
"server", "server",
@ -821,6 +822,12 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.3.9" version = "0.3.9"
@ -992,6 +999,8 @@ version = "0.1.0"
dependencies = [ dependencies = [
"csv", "csv",
"lazy_static", "lazy_static",
"strum",
"strum_macros",
"tracing", "tracing",
] ]
@ -2250,7 +2259,7 @@ checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8"
dependencies = [ dependencies = [
"dotenvy", "dotenvy",
"either", "either",
"heck", "heck 0.4.1",
"hex", "hex",
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
@ -2397,6 +2406,25 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
[[package]]
name = "strum_macros"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.77",
]
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"

View File

@ -14,6 +14,7 @@ mimalloc = { workspace = true }
server = { path = "server" } server = { path = "server" }
library = { path = "library" } library = { path = "library" }
i18n = { path = "i18n" }
[workspace.dependencies] [workspace.dependencies]
tokio = "1.36" tokio = "1.36"
@ -53,4 +54,6 @@ syn = "2.0.77"
quote = "1.0.37" quote = "1.0.37"
hyper = "1.4.1" hyper = "1.4.1"
tower = "0.5.1" tower = "0.5.1"
csv = "1.3.0" csv = "1.3.0"
strum = "0.26.3"
strum_macros = "0.26.3"

View File

@ -3,15 +3,15 @@ use validator::Validate;
#[derive(Debug, Validate, Deserialize, Serialize)] #[derive(Debug, Validate, Deserialize, Serialize)]
pub struct AuthenticateWithPassword { pub struct AuthenticateWithPassword {
#[validate(required(message = "VALIDATE_ACCOUNT_NAME_REQUIRED"), length(min = 1, message = "VALIDATE_ACCOUNT_NAME_REQUIRED"))] #[validate(required(message = "ValidateAccountNameRequired"), length(min = 1, message = "ValidateAccountNameRequired"))]
pub username: Option<String>, pub username: Option<String>,
#[validate(required(message = "VALIDATE_ACCOUNT_PASSWORD_REQUIRED"), length(min = 1, message = "VALIDATE_ACCOUNT_PASSWORD_REQUIRED"))] #[validate(required(message = "ValidateAccountPasswordRequired"), length(min = 1, message = "ValidateAccountPasswordRequired"))]
pub password: Option<String>, pub password: Option<String>,
} }
#[derive(Debug, Validate, Deserialize, Serialize)] #[derive(Debug, Validate, Deserialize, Serialize)]
pub struct AuthenticateGooleAccountReq { pub struct AuthenticateGooleAccountReq {
#[validate(required(message = "VALIDATE_ACCOUNT_ID_TOKEN_REQUIRED"), length(min = 1, message = "VALIDATE_ACCOUNT_ID_TOKEN_REQUIRED"))] #[validate(required(message = "ValidateAccountIdTokenRequired"), length(min = 1, message = "ValidateAccountIdTokenRequired"))]
pub id_token: Option<String>, pub id_token: Option<String>,
} }

View File

@ -3,6 +3,6 @@ use validator::Validate;
#[derive(Debug, Validate, Deserialize, Serialize)] #[derive(Debug, Validate, Deserialize, Serialize)]
pub struct FeedbackAdd { pub struct FeedbackAdd {
#[validate(required(message = "VALIDATE_FEEDBACK_CONTENT_REQUIRED"), length(min = 1, message = "VALIDATE_FEEDBACK_CONTENT_REQUIRED"))] #[validate(required(message = "ValidateFeedbackContentRequired"), length(min = 1, message = "ValidateFeedbackContentRequired"))]
pub content: Option<String> pub content: Option<String>
} }

View File

@ -3,9 +3,9 @@ use validator::Validate;
#[derive(Deserialize, Validate)] #[derive(Deserialize, Validate)]
pub struct PageParams { pub struct PageParams {
#[validate(required(message = "VALIDATE_PAGEABLE_PAGE_REQUIRED"), range(min = 1, message = "VALIDATE_PAGEABLE_PAGE_REQUIRED"))] #[validate(required(message = "ValidatePageablePageRequired"), range(min = 1, message = "ValidatePageablePageRequired"))]
pub page: Option<i64>, pub page: Option<i64>,
#[serde(rename = "pageSize")] #[serde(rename = "pageSize")]
#[validate(required(message = "VALIDATE_PAGEABLE_PAGE_SIZE_REQUIRED"), range(min = 1, message = "VALIDATE_PAGEABLE_PAGE_SIZE_REQUIRED"))] #[validate(required(message = "ValidatePageablePageSizeRequired"), range(min = 1, message = "ValidatePageablePageSizeRequired"))]
pub page_size: Option<i64>, pub page_size: Option<i64>,
} }

View File

@ -1,14 +1,14 @@
id,en-US,zh-CN id,en-US,zh-CN
SERVER_INTERNAL_ERROR,Internal server error,系统内部错误 ServerInternalError,Internal server error,系统内部错误
HELLO,hello {},你好 {} Hello,hello {},你好 {}
ACCOUNT_DISABLED,account is disabled,账户已禁用 AccountDisabled,account is disabled,账户已禁用
ACCOUNT_NO_PERMISSION,account has no permission,账户无权限 AccountNoPermission,account has no permission,账户无权限
INCORRECT_USERNAME_OR_PASSWORD,incorrect username or password,用户名或密码错误 IncorrectUsernameOrPassword,incorrect username or password,用户名或密码错误
INVALID_TOKEN,invalid token,无效令牌 InvalidToken,invalid token,无效令牌
VALIDATE_FEEDBACK_CONTENT_REQUIRED,feedback content is required,反馈内容不能为空 ValidateFeedbackContentRequired,feedback content is required,反馈内容不能为空
VALIDATE_ACCOUNT_NAME_REQUIRED,username is required,"用户名称不能为空" ValidateAccountNameRequired,username is required,"用户名称不能为空"
VALIDATE_ACCOUNT_PASSWORD_REQUIRED,password is required,密码不能为空 ValidateAccountPasswordRequired,password is required,密码不能为空
VALIDATE_ACCOUNT_ID_TOKEN_REQUIRED,ID Token is required,用户ID Token不能为空 ValidateAccountIdTokenRequired,ID Token is required,用户ID Token不能为空
VALIDATE_ACCOUNT_LANG_TAG_REQUIRED,lang tag is required,用户语言标识不能为空 ValidateAccountLangTagRequired,lang tag is required,用户语言标识不能为空
VALIDATE_PAGEABLE_PAGE_REQUIRED,invalid page number,页码无效 ValidatePageablePageRequired,invalid page number,页码无效
VALIDATE_PAGEABLE_PAGE_SIZE_REQUIRED,invalid quantity per page,每页数量无效 ValidatePageablePageSizeRequired,invalid quantity per page,每页数量无效
1 id en-US zh-CN
2 SERVER_INTERNAL_ERROR ServerInternalError Internal server error 系统内部错误
3 HELLO Hello hello {} 你好 {}
4 ACCOUNT_DISABLED AccountDisabled account is disabled 账户已禁用
5 ACCOUNT_NO_PERMISSION AccountNoPermission account has no permission 账户无权限
6 INCORRECT_USERNAME_OR_PASSWORD IncorrectUsernameOrPassword incorrect username or password 用户名或密码错误
7 INVALID_TOKEN InvalidToken invalid token 无效令牌
8 VALIDATE_FEEDBACK_CONTENT_REQUIRED ValidateFeedbackContentRequired feedback content is required 反馈内容不能为空
9 VALIDATE_ACCOUNT_NAME_REQUIRED ValidateAccountNameRequired username is required 用户名称不能为空
10 VALIDATE_ACCOUNT_PASSWORD_REQUIRED ValidateAccountPasswordRequired password is required 密码不能为空
11 VALIDATE_ACCOUNT_ID_TOKEN_REQUIRED ValidateAccountIdTokenRequired ID Token is required 用户ID Token不能为空
12 VALIDATE_ACCOUNT_LANG_TAG_REQUIRED ValidateAccountLangTagRequired lang tag is required 用户语言标识不能为空
13 VALIDATE_PAGEABLE_PAGE_REQUIRED ValidatePageablePageRequired invalid page number 页码无效
14 VALIDATE_PAGEABLE_PAGE_SIZE_REQUIRED ValidatePageablePageSizeRequired invalid quantity per page 每页数量无效

View File

@ -6,4 +6,6 @@ edition = "2021"
[dependencies] [dependencies]
lazy_static = { workspace = true } lazy_static = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
csv = { workspace = true } csv = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }

View File

@ -2,12 +2,15 @@ extern crate self as i18n;
use std::{collections::HashMap, sync::OnceLock}; use std::{collections::HashMap, sync::OnceLock};
use message_ids::MessageId;
use strum::IntoEnumIterator;
pub mod message_ids; pub mod message_ids;
const I18N_FILE: &str = "i18n.csv"; const I18N_FILE: &str = "i18n.csv";
static I18N: OnceLock<HashMap<&'static str, HashMap<&'static str, &'static str>>> = OnceLock::new(); static I18N: OnceLock<HashMap<&'static str, HashMap<&'static str, &'static str>>> = OnceLock::new();
fn init_i18n() -> HashMap<&'static str, HashMap<&'static str, &'static str>> { fn init() -> HashMap<&'static str, HashMap<&'static str, &'static str>> {
let mut i18n_map = HashMap::new(); let mut i18n_map = HashMap::new();
let mut i18n_id_map = HashMap::new(); let mut i18n_id_map = HashMap::new();
let mut rdr = csv::Reader::from_path(I18N_FILE).expect("读取多语言文件失败"); let mut rdr = csv::Reader::from_path(I18N_FILE).expect("读取多语言文件失败");
@ -31,19 +34,27 @@ fn init_i18n() -> HashMap<&'static str, HashMap<&'static str, &'static str>> {
.insert(message_id, message); .insert(message_id, message);
} }
} }
tracing::info!("多语言文件解析完成:{:?}", i18n_map);
// 检查多语言文件是否完整
for (lang_id, lang_map) in i18n_map.iter() {
MessageId::iter().for_each(|message_id| {
if !lang_map.contains_key(message_id.as_ref()) {
tracing::error!("多语言文件缺失消息:{},语言:{}", message_id, lang_id);
}
});
}
i18n_map i18n_map
} }
fn get_i18n() -> &'static HashMap<&'static str, HashMap<&'static str, &'static str>> { pub fn init_i18n() -> &'static HashMap<&'static str, HashMap<&'static str, &'static str>> {
I18N.get_or_init(init_i18n) I18N.get_or_init(init)
} }
/// 获取语言模板 /// 获取语言模板
pub fn lang(lang_id: &str, message_id: &str) -> &'static str { pub fn lang(lang_id: &str, message_id: MessageId) -> &'static str {
get_i18n() init_i18n()
.get(lang_id) .get(lang_id)
.and_then(|map| map.get(message_id)) .and_then(|map| map.get(message_id.as_ref()))
.unwrap_or_else(|| &"UNKNOWN") .unwrap_or_else(|| &"UNKNOWN")
} }

View File

@ -1,15 +1,19 @@
pub const SERVER_INTERNAL_ERROR: &str = "SERVER_INTERNAL_ERROR"; use strum_macros::{AsRefStr, Display, EnumIter, EnumString};
pub const HELLO: &str = "HELLO"; #[derive(Debug, EnumIter, EnumString, Display, PartialEq, AsRefStr)]
pub const ACCOUNT_DISABLED: &str = "ACCOUNT_DISABLED"; pub enum MessageId {
pub const ACCOUNT_NO_PERMISSION: &str = "ACCOUNT_NO_PERMISSION"; ServerInternalError,
pub const INCORRECT_USERNAME_OR_PASSWORD: &str = "INCORRECT_USERNAME_OR_PASSWORD"; Hello,
pub const INVALID_TOKEN: &str = "INVALID_TOKEN"; AccountDisabled,
AccountNoPermission,
IncorrectUsernameOrPassword,
InvalidToken,
pub const VALIDATE_FEEDBACK_CONTENT_REQUIRED: &'static str = "VALIDATE_FEEDBACK_CONTENT_REQUIRED"; ValidateFeedbackContentRequired,
pub const VALIDATE_ACCOUNT_NAME_REQUIRED: &'static str = "VALIDATE_ACCOUNT_NAME_REQUIRED"; ValidateAccountNameRequired,
pub const VALIDATE_ACCOUNT_PASSWORD_REQUIRED: &'static str = "VALIDATE_ACCOUNT_PASSWORD_REQUIRED"; ValidateAccountPasswordRequired,
pub const VALIDATE_ACCOUNT_ID_TOKEN_REQUIRED: &'static str = "VALIDATE_ACCOUNT_ID_TOKEN_REQUIRED"; ValidateAccountIdTokenRequired,
pub const VALIDATE_ACCOUNT_LANG_TAG_REQUIRED: &'static str = "VALIDATE_ACCOUNT_LANG_TAG_REQUIRED"; ValidateAccountLangTagRequired,
pub const VALIDATE_PAGEABLE_PAGE_REQUIRED: &'static str = "VALIDATE_PAGEABLE_PAGE_REQUIRED"; ValidatePageablePageRequired,
pub const VALIDATE_PAGEABLE_PAGE_SIZE_REQUIRED: &'static str = "VALIDATE_PAGEABLE_PAGE_SIZE_REQUIRED"; ValidatePageablePageSizeRequired,
}

View File

@ -1,4 +1,6 @@
use i18n::message; use std::str::FromStr;
use i18n::{message, message_ids::MessageId};
use validator::Validate; use validator::Validate;
use super::response::{ResData, ResErr, ResResult}; use super::response::{ResData, ResErr, ResResult};
@ -16,7 +18,7 @@ pub fn validate_params(params: &impl Validate, local: &str) -> ResResult<ResData
err.field_errors().iter().for_each(|(_field, errs)| { err.field_errors().iter().for_each(|(_field, errs)| {
errs.iter().for_each(|e| { errs.iter().for_each(|e| {
let msg = e.message.clone().unwrap_or_default(); let msg = e.message.clone().unwrap_or_default();
errors.push(message!(local, msg.trim())) errors.push(message!(local, MessageId::from_str(msg.trim()).unwrap()))
}); });
}); });
}, },
@ -25,7 +27,7 @@ pub fn validate_params(params: &impl Validate, local: &str) -> ResResult<ResData
err.field_errors().iter().for_each(|(_field, errs)| { err.field_errors().iter().for_each(|(_field, errs)| {
errs.iter().for_each(|e| { errs.iter().for_each(|e| {
let msg = e.message.clone().unwrap_or_default(); let msg = e.message.clone().unwrap_or_default();
errors.push(message!(local, msg.trim())) errors.push(message!(local, MessageId::from_str(msg.trim()).unwrap()))
}); });
}); });
} }
@ -33,7 +35,7 @@ pub fn validate_params(params: &impl Validate, local: &str) -> ResResult<ResData
validator::ValidationErrorsKind::Field(err) => { validator::ValidationErrorsKind::Field(err) => {
err.iter().for_each(|e| { err.iter().for_each(|e| {
let msg = e.message.clone().unwrap_or_default(); let msg = e.message.clone().unwrap_or_default();
errors.push(message!(local, msg.trim())) errors.push(message!(local, MessageId::from_str(msg.trim()).unwrap()))
}); });
}, },
}; };

View File

@ -5,7 +5,7 @@ use domain::dto::account::AuthenticateGooleAccountReq;
use domain::entities::account::Account; use domain::entities::account::Account;
use domain::vo::account::{LoginAccount, RefreshTokenResult}; use domain::vo::account::{LoginAccount, RefreshTokenResult};
use i18n::message; use i18n::message;
use i18n::message_ids::{ACCOUNT_DISABLED, INVALID_TOKEN}; use i18n::message_ids::MessageId;
use library::cache::account_cache::{CacheAccount, LOGIN_CACHE}; use library::cache::account_cache::{CacheAccount, LOGIN_CACHE};
use library::context::{Context, WhiteContext}; use library::context::{Context, WhiteContext};
use library::model::response::ResErr::ErrPerm; use library::model::response::ResErr::ErrPerm;
@ -48,7 +48,7 @@ pub async fn authenticate_google(
if let Some(disable_time) = account.disable_time { if let Some(disable_time) = account.disable_time {
if disable_time > Utc::now() { if disable_time > Utc::now() {
tracing::error!("账户已禁用"); tracing::error!("账户已禁用");
return Err(ResErr::service(message!(context.get_lang_tag(), ACCOUNT_DISABLED))); return Err(ResErr::service(message!(context.get_lang_tag(), MessageId::AccountDisabled)));
} }
} }
@ -89,7 +89,7 @@ pub async fn refresh_token(
let account = context.account.clone(); let account = context.account.clone();
if token::verify_refresh_token(&refresh_token).is_err() { if token::verify_refresh_token(&refresh_token).is_err() {
return Err(ResErr::params(message!(context.get_lang_tag(), INVALID_TOKEN))); return Err(ResErr::params(message!(context.get_lang_tag(), MessageId::InvalidToken)));
} }
let refresh_token = RefreshTokenResult { let refresh_token = RefreshTokenResult {

View File

@ -8,7 +8,7 @@ use domain::{
}; };
use i18n::{ use i18n::{
message, message,
message_ids::{ACCOUNT_DISABLED, ACCOUNT_NO_PERMISSION, INCORRECT_USERNAME_OR_PASSWORD}, message_ids::MessageId,
}; };
use library::{ use library::{
cache::account_cache::{CacheAccount, LOGIN_CACHE}, context::WhiteContext, db, model::response::{ResErr, ResResult}, token::{generate_refresh_token, generate_token} cache::account_cache::{CacheAccount, LOGIN_CACHE}, context::WhiteContext, db, model::response::{ResErr, ResResult}, token::{generate_refresh_token, generate_token}
@ -25,7 +25,7 @@ pub async fn authenticate_with_password(
tracing::info!("登录用户失败,用户查询为空"); tracing::info!("登录用户失败,用户查询为空");
return Err(ResErr::params(message!( return Err(ResErr::params(message!(
context.get_lang_tag(), context.get_lang_tag(),
INCORRECT_USERNAME_OR_PASSWORD MessageId::IncorrectUsernameOrPassword
))); )));
} }
let account = account.unwrap(); let account = account.unwrap();
@ -34,7 +34,7 @@ pub async fn authenticate_with_password(
tracing::error!("账户已禁用"); tracing::error!("账户已禁用");
return Err(ResErr::auth(message!( return Err(ResErr::auth(message!(
context.get_lang_tag(), context.get_lang_tag(),
ACCOUNT_DISABLED MessageId::AccountDisabled
))); )));
} }
} }
@ -42,7 +42,7 @@ pub async fn authenticate_with_password(
tracing::error!("账户不是管理员,无权限"); tracing::error!("账户不是管理员,无权限");
return Err(ResErr::perm(message!( return Err(ResErr::perm(message!(
context.get_lang_tag(), context.get_lang_tag(),
ACCOUNT_NO_PERMISSION MessageId::AccountNoPermission
))); )));
} }

View File

@ -7,6 +7,7 @@ static GLOBAL: MiMalloc = MiMalloc;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let (_std_guard, _file_guard) = library::core::logger::init_log(config!()); let (_std_guard, _file_guard) = library::core::logger::init_log(config!());
let _i18n = i18n::init_i18n();
library::core::db::init_database(config!()).await; library::core::db::init_database(config!()).await;
server::serve().await; server::serve().await;