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"
version = "0.1.0"
dependencies = [
"i18n",
"library",
"mimalloc",
"server",
@ -821,6 +822,12 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.3.9"
@ -992,6 +999,8 @@ version = "0.1.0"
dependencies = [
"csv",
"lazy_static",
"strum",
"strum_macros",
"tracing",
]
@ -2250,7 +2259,7 @@ checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8"
dependencies = [
"dotenvy",
"either",
"heck",
"heck 0.4.1",
"hex",
"once_cell",
"proc-macro2",
@ -2397,6 +2406,25 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "subtle"
version = "2.6.1"

View File

@ -14,6 +14,7 @@ mimalloc = { workspace = true }
server = { path = "server" }
library = { path = "library" }
i18n = { path = "i18n" }
[workspace.dependencies]
tokio = "1.36"
@ -54,3 +55,5 @@ quote = "1.0.37"
hyper = "1.4.1"
tower = "0.5.1"
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)]
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>,
#[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>,
}
#[derive(Debug, Validate, Deserialize, Serialize)]
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>,
}

View File

@ -3,6 +3,6 @@ use validator::Validate;
#[derive(Debug, Validate, Deserialize, Serialize)]
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>
}

View File

@ -3,9 +3,9 @@ use validator::Validate;
#[derive(Deserialize, Validate)]
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>,
#[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>,
}

View File

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

@ -7,3 +7,5 @@ edition = "2021"
lazy_static = { workspace = true }
tracing = { 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 message_ids::MessageId;
use strum::IntoEnumIterator;
pub mod message_ids;
const I18N_FILE: &str = "i18n.csv";
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_id_map = HashMap::new();
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);
}
}
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
}
fn get_i18n() -> &'static HashMap<&'static str, HashMap<&'static str, &'static str>> {
I18N.get_or_init(init_i18n)
pub fn init_i18n() -> &'static HashMap<&'static str, HashMap<&'static str, &'static str>> {
I18N.get_or_init(init)
}
/// 获取语言模板
pub fn lang(lang_id: &str, message_id: &str) -> &'static str {
get_i18n()
pub fn lang(lang_id: &str, message_id: MessageId) -> &'static str {
init_i18n()
.get(lang_id)
.and_then(|map| map.get(message_id))
.and_then(|map| map.get(message_id.as_ref()))
.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";
pub const ACCOUNT_DISABLED: &str = "ACCOUNT_DISABLED";
pub const ACCOUNT_NO_PERMISSION: &str = "ACCOUNT_NO_PERMISSION";
pub const INCORRECT_USERNAME_OR_PASSWORD: &str = "INCORRECT_USERNAME_OR_PASSWORD";
pub const INVALID_TOKEN: &str = "INVALID_TOKEN";
#[derive(Debug, EnumIter, EnumString, Display, PartialEq, AsRefStr)]
pub enum MessageId {
ServerInternalError,
Hello,
AccountDisabled,
AccountNoPermission,
IncorrectUsernameOrPassword,
InvalidToken,
pub const VALIDATE_FEEDBACK_CONTENT_REQUIRED: &'static str = "VALIDATE_FEEDBACK_CONTENT_REQUIRED";
pub const VALIDATE_ACCOUNT_NAME_REQUIRED: &'static str = "VALIDATE_ACCOUNT_NAME_REQUIRED";
pub const VALIDATE_ACCOUNT_PASSWORD_REQUIRED: &'static str = "VALIDATE_ACCOUNT_PASSWORD_REQUIRED";
pub const VALIDATE_ACCOUNT_ID_TOKEN_REQUIRED: &'static str = "VALIDATE_ACCOUNT_ID_TOKEN_REQUIRED";
pub const VALIDATE_ACCOUNT_LANG_TAG_REQUIRED: &'static str = "VALIDATE_ACCOUNT_LANG_TAG_REQUIRED";
pub const VALIDATE_PAGEABLE_PAGE_REQUIRED: &'static str = "VALIDATE_PAGEABLE_PAGE_REQUIRED";
pub const VALIDATE_PAGEABLE_PAGE_SIZE_REQUIRED: &'static str = "VALIDATE_PAGEABLE_PAGE_SIZE_REQUIRED";
ValidateFeedbackContentRequired,
ValidateAccountNameRequired,
ValidateAccountPasswordRequired,
ValidateAccountIdTokenRequired,
ValidateAccountLangTagRequired,
ValidatePageablePageRequired,
ValidatePageablePageSizeRequired,
}

View File

@ -1,4 +1,6 @@
use i18n::message;
use std::str::FromStr;
use i18n::{message, message_ids::MessageId};
use validator::Validate;
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)| {
errs.iter().for_each(|e| {
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)| {
errs.iter().for_each(|e| {
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) => {
err.iter().for_each(|e| {
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::vo::account::{LoginAccount, RefreshTokenResult};
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::context::{Context, WhiteContext};
use library::model::response::ResErr::ErrPerm;
@ -48,7 +48,7 @@ pub async fn authenticate_google(
if let Some(disable_time) = account.disable_time {
if disable_time > Utc::now() {
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();
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 {

View File

@ -8,7 +8,7 @@ use domain::{
};
use i18n::{
message,
message_ids::{ACCOUNT_DISABLED, ACCOUNT_NO_PERMISSION, INCORRECT_USERNAME_OR_PASSWORD},
message_ids::MessageId,
};
use library::{
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!("登录用户失败,用户查询为空");
return Err(ResErr::params(message!(
context.get_lang_tag(),
INCORRECT_USERNAME_OR_PASSWORD
MessageId::IncorrectUsernameOrPassword
)));
}
let account = account.unwrap();
@ -34,7 +34,7 @@ pub async fn authenticate_with_password(
tracing::error!("账户已禁用");
return Err(ResErr::auth(message!(
context.get_lang_tag(),
ACCOUNT_DISABLED
MessageId::AccountDisabled
)));
}
}
@ -42,7 +42,7 @@ pub async fn authenticate_with_password(
tracing::error!("账户不是管理员,无权限");
return Err(ResErr::perm(message!(
context.get_lang_tag(),
ACCOUNT_NO_PERMISSION
MessageId::AccountNoPermission
)));
}

View File

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