diff --git a/cmd/admin.go b/cmd/admin.go index 3631cd225..b8dc314d8 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -10,6 +10,7 @@ import ( "github.com/ArtalkJS/Artalk/internal/core" "github.com/ArtalkJS/Artalk/internal/entity" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/internal/utils" "github.com/sirupsen/logrus" @@ -19,14 +20,13 @@ import ( var adminCmd = &cobra.Command{ Use: "admin", - Short: "创建管理员账号", - Long: "根据提示创建管理员账号", + Short: "Create or edit an administrator account", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { core.LoadCore(cfgFile, workDir) fmt.Println("--------------------------------") - fmt.Println(" 管理员账户创建 ") + fmt.Println(" " + i18n.T("Create admin account")) fmt.Println("--------------------------------") username, email, password, err := credentials() @@ -41,7 +41,8 @@ var adminCmd = &cobra.Command{ logrus.Fatal(err) } - logrus.Info("该账户已存在,密码修改成功") + logrus.Info(i18n.T("{{name}} already exists", map[string]interface{}{"name": i18n.T("Account")}) + + ", " + i18n.T("Password updated")) return } @@ -49,7 +50,7 @@ var adminCmd = &cobra.Command{ Name: username, Email: email, IsAdmin: true, - BadgeName: "管理员", + BadgeName: i18n.T("Admin"), BadgeColor: "#FF6C00", } user.SetPasswordEncrypt(password) @@ -72,29 +73,29 @@ func init() { func credentials() (string, string, string, error) { reader := bufio.NewReader(os.Stdin) - fmt.Print("Enter Username: ") + fmt.Print(i18n.T("Enter {{name}}", map[string]interface{}{"name": i18n.T("Username")}) + ": ") username, err := reader.ReadString('\n') if err != nil { return "", "", "", err } - fmt.Print("Enter Email: ") + fmt.Print(i18n.T("Enter {{name}}", map[string]interface{}{"name": i18n.T("Email")}) + ": ") email, err := reader.ReadString('\n') if err != nil { return "", "", "", err } if !utils.ValidateEmail(strings.TrimSpace(email)) { - return "", "", "", errors.New("邮箱格式不合法") + return "", "", "", errors.New("invalid email format") } - fmt.Print("Enter Password: ") + fmt.Print(i18n.T("Enter {{name}}", map[string]interface{}{"name": i18n.T("Password")}) + ": ") bytePassword, err := term.ReadPassword(int(syscall.Stdin)) if err != nil { return "", "", "", err } fmt.Println() - fmt.Print("Retype Password: ") + fmt.Print(i18n.T("Retype {{name}}", map[string]interface{}{"name": i18n.T("Password")}) + ": " + ": ") byteRePassword, err := term.ReadPassword(int(syscall.Stdin)) if err != nil { return "", "", "", err @@ -106,7 +107,7 @@ func credentials() (string, string, string, error) { rePassword := strings.TrimSpace(string(byteRePassword)) if rePassword != password { - return "", "", "", errors.New("输入的密码不一致") + return "", "", "", errors.New("inconsistent password input") } return strings.TrimSpace(username), strings.TrimSpace(email), password, nil diff --git a/cmd/export.go b/cmd/export.go index 456d38184..06a781ecc 100644 --- a/cmd/export.go +++ b/cmd/export.go @@ -9,6 +9,7 @@ import ( "github.com/ArtalkJS/Artalk/internal/artransfer" "github.com/ArtalkJS/Artalk/internal/core" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/utils" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -17,13 +18,10 @@ import ( var exportCmd = &cobra.Command{ Use: "export", Aliases: []string{}, - Short: "数据迁移 - 迁出", - Long: "\n# 数据迁移 - 迁出\n\n 将所有数据从 Artalk 导出,用作备份,或迁移至其他地方\n 打包所有数据并导出成 “Artalk 数据行囊 (Artrans)”,为数据迁移做准备\n" + ` -- 重新导入 Artalk,可执行: artalk import <数据行囊文件路径> -- 文档:https://artalk.js.org/guide/transfer.html -`, + Short: "Artransfer - Export", + Long: "\n# Artransfer - Export\n\n See the documentation to learn more: https://artalk.js.org/guide/transfer.html", Run: func(cmd *cobra.Command, args []string) { - core.LoadCore(cfgFile, workDir) // 装载核心 + core.LoadCore(cfgFile, workDir) jsonStr, err := artransfer.ExportArtransString() if err != nil { @@ -67,7 +65,7 @@ var exportCmd = &cobra.Command{ logrus.Fatal(err2) } - logrus.Info("已导出 Artrans 文件:" + filename) + logrus.Info(i18n.T("Export complete") + ": " + filename) } }, } diff --git a/cmd/gen.go b/cmd/gen.go index 8a3a8a4cc..6cc72d6cf 100644 --- a/cmd/gen.go +++ b/cmd/gen.go @@ -9,15 +9,15 @@ import ( ) var genCmd = &cobra.Command{ - Use: "gen <类型> <目标路径>", - Short: "生成一些内容", - Long: "生成一些内容\n例如:artalk gen config ./artalk.yml", + Use: "gen ", + Short: "A collection of several useful generators", + Long: "Generate some content\ne.g. `artalk gen config ./artalk.yml`", Args: cobra.RangeArgs(1, 2), Run: func(cmd *cobra.Command, args []string) { - // 工作目录 + // change working directory if workDir != "" { if err := os.Chdir(workDir); err != nil { - logrus.Fatal("工作目录切换错误 ", err) + logrus.Fatal("change working directory error ", err) } } diff --git a/cmd/import.go b/cmd/import.go index fc470e06a..8267d834f 100644 --- a/cmd/import.go +++ b/cmd/import.go @@ -6,34 +6,32 @@ import ( "github.com/ArtalkJS/Artalk/internal/artransfer" "github.com/ArtalkJS/Artalk/internal/core" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) var importCmd = &cobra.Command{ - Use: "import <数据行囊文件路径>", + Use: "import ", Aliases: []string{}, - Short: "数据迁移 - 迁入", - Long: "\n# 数据迁移 - 迁入\n\n 从其他评论系统迁移数据到 Artalk\n" + ` -- 导入前需要使用转换工具 Artransfer 将其他评论数据转为 Artrans 格式 -- 文档:https://artalk.js.org/guide/transfer.html -`, - Args: cobra.MinimumNArgs(1), + Short: "Artransfer - Import", + Long: "\n# Artransfer - Import\n\n See the documentation to learn more: https://artalk.js.org/guide/transfer.html", + Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - core.LoadCore(cfgFile, workDir) // 装载核心 + core.LoadCore(cfgFile, workDir) // load core parcelFile := args[0] if _, err := os.Stat(parcelFile); errors.Is(err, os.ErrNotExist) { - logrus.Fatal("`数据行囊` 文件不存在,请检查路径是否正确") + logrus.Fatal(i18n.T("{{name}} not found", map[string]interface{}{"name": i18n.T("File")})) } payload := args[1:] payload = append(payload, "json_file:"+parcelFile) - // 导入 Artrans + // import Artrans artransfer.RunImportArtrans(payload) - logrus.Info("导入结束") + logrus.Info(i18n.T("Import complete")) }, } diff --git a/cmd/root.go b/cmd/root.go index 14d90bd30..244c2937e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,14 +12,14 @@ import ( var Version = config.Version + `/` + config.CommitHash var Banner = ` - ________ ________ _________ ________ ___ ___ __ -|\ __ \|\ __ \|\___ ___\\ __ \|\ \ |\ \|\ \ -\ \ \|\ \ \ \|\ \|___ \ \_\ \ \|\ \ \ \ \ \ \/ /|_ - \ \ __ \ \ _ _\ \ \ \ \ \ __ \ \ \ \ \ ___ \ - \ \ \ \ \ \ \\ \| \ \ \ \ \ \ \ \ \ \____\ \ \\ \ \ + ________ ________ _________ ________ ___ ___ __ +|\ __ \|\ __ \|\___ ___\\ __ \|\ \ |\ \|\ \ +\ \ \|\ \ \ \|\ \|___ \ \_\ \ \|\ \ \ \ \ \ \/ /|_ + \ \ __ \ \ _ _\ \ \ \ \ \ __ \ \ \ \ \ ___ \ + \ \ \ \ \ \ \\ \| \ \ \ \ \ \ \ \ \ \____\ \ \\ \ \ \ \__\ \__\ \__\\ _\ \ \__\ \ \__\ \__\ \_______\ \__\\ \__\ \|__|\|__|\|__|\|__| \|__| \|__|\|__|\|_______|\|__| \|__| - + Artalk (` + Version + `) -> A Selfhosted Comment System. @@ -49,13 +49,13 @@ func Execute() { func init() { rootCmd.SetVersionTemplate("Artalk ({{printf \"%s\" .Version}})\n") - rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "配置文件路径 (defaults are './artalk.yml')") - rootCmd.PersistentFlags().StringVarP(&workDir, "workdir", "w", "", "程序工作目录 (defaults are './')") + rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file path (defaults are './artalk.yml')") + rootCmd.PersistentFlags().StringVarP(&workDir, "workdir", "w", "", "program working directory (defaults are './')") // Version Command versionCmd := &cobra.Command{ Use: "version", - Short: "输出版本信息", + Short: "Output Version Information", Run: func(cmd *cobra.Command, args []string) { fmt.Println("Artalk (" + Version + ")") }, @@ -65,7 +65,7 @@ func init() { // Config Command configCmd := &cobra.Command{ Use: "config", - Short: "输出配置信息", + Short: "Output Config Information", Run: func(cmd *cobra.Command, args []string) { core.LoadConfOnly(cfgFile, workDir) buf, _ := json.MarshalIndent(config.Instance, "", " ") diff --git a/cmd/server.go b/cmd/server.go index 5c2088a2a..0f0675988 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -16,11 +16,11 @@ import ( var serverCmd = &cobra.Command{ Use: "server", Aliases: []string{"serve"}, - Short: "启动服务器", + Short: "Start the server", Long: Banner, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - core.LoadCore(cfgFile, workDir) // 装载核心 + core.LoadCore(cfgFile, workDir) fmt.Println(Banner) fmt.Print("-------------------------------\n\n") @@ -62,6 +62,6 @@ var serverCmd = &cobra.Command{ func init() { rootCmd.AddCommand(serverCmd) - flagPV(serverCmd, "host", "", "0.0.0.0", "监听 IP") - flagPV(serverCmd, "port", "", 23366, "监听端口") + flagPV(serverCmd, "host", "", "0.0.0.0", "Listening IP") + flagPV(serverCmd, "port", "", 23366, "Listening port") } diff --git a/cmd/upgrade.go b/cmd/upgrade.go index 934d3dd0a..c27e5cd2f 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -7,6 +7,7 @@ import ( "github.com/ArtalkJS/Artalk/internal/config" "github.com/ArtalkJS/Artalk/internal/core" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/blang/semver" "github.com/rhysd/go-github-selfupdate/selfupdate" "github.com/sirupsen/logrus" @@ -16,14 +17,14 @@ import ( var upgradeCmd = &cobra.Command{ Use: "upgrade", Aliases: []string{"update"}, - Short: "升级到最新版", - Long: "将 Artalk 升级到最新版本,\n更新源为 GitHub Releases,\n更新需要重启 Artalk 才能生效。", + Short: "Upgrade to the latest version", + Long: "Upgrade Artalk to the latest version, \n update source is GitHub Releases, \n update need to restart Artalk to take effect.", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - // loadCore() // 装载核心 + // loadCore() core.LoadConfOnly(cfgFile, workDir) - logrus.Info("从 GitHub Release 检索更新中...") + logrus.Info(i18n.T("Checking for updates") + "...") latest, found, err := selfupdate.DetectLatest("ArtalkJS/Artalk") if err != nil { @@ -34,13 +35,13 @@ var upgradeCmd = &cobra.Command{ if !ignoreVersionCheck { v := semver.MustParse(strings.TrimPrefix(config.Version, "v")) if !found || latest.Version.LTE(v) { - logrus.Println("当前已是最新版本 v" + v.String() + " 无需升级") + logrus.Println(i18n.T("Current version is the latest") + " (v" + v.String() + ")") return } } - logrus.Info("发现新版本: v" + latest.Version.String()) - logrus.Info("正在下载更新中...") + logrus.Info(i18n.T("New version available") + ": v" + latest.Version.String()) + logrus.Info(i18n.T("Downloading") + "...") exe, err := os.Executable() if err != nil { @@ -48,10 +49,10 @@ var upgradeCmd = &cobra.Command{ } if err := selfupdate.UpdateTo(latest.AssetURL, exe); err != nil { - logrus.Fatal("更新失败 ", err) + logrus.Fatal(i18n.T("Update failed")+" ", err) } - logrus.Println("更新完毕") + logrus.Println(i18n.T("Update complete")) fmt.Println("\n-------------------------------\n v" + latest.Version.String() + " Release Note\n" + diff --git a/cmd/utils.go b/cmd/utils.go index 2f19df90b..9a84b663d 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -4,7 +4,7 @@ import ( "github.com/spf13/cobra" ) -//// 捷径函数 //// +//// Shortcut Functions //// func flag(cmd *cobra.Command, name string, defaultVal interface{}, usage string) { f := cmd.PersistentFlags() diff --git a/i18n/en.yml b/i18n/en.yml index 4873cd4d2..383c27632 100644 --- a/i18n/en.yml +++ b/i18n/en.yml @@ -1,3 +1,75 @@ +"Access denied": +"Account": +"Admin access required": +"Admin": +"Cannot reply to this comment": +"Captcha required": +"Checking for updates": +"Comment count": +"Comment failed": +"Comment": +"Config file read failed": +"Confirm to continue?": +"Contains invalid URL": +"Create admin account": +"Current version is the latest": +"Downloading": +"Email": +"Enter {{name}}": +"Export complete": +"Export error": +"File": +"First comment": +"Image exceeds {{file_size}} limit": +"Image upload forbidden": +"Import complete": "Invalid request": -"Please check trusted_domains config": -"Unable to get Origin": +"Invalid request. Please check your `trusted_domains` config.": +"Invalid {{name}}": +"Link": +"Login failed": +"Name": +"New version available": +"Nickname": +"No comment": +"Notify": +"Page fetch failed": +"Page": +"Parameter": +"Parent comment": +"Password update failed": +"Password updated": +"Password": +"Please review": +"Retype {{name}}": +"Save failed": +"Saving": +"Services restart complete": +"Site `{{name}}` not found. Please create it in control center.": +"Site": +"Sub-comment": +"Target Site": +"Task executing in background, please wait...": +"Task in progress, please wait a moment": +"Type": +"URL Resolver": +"Unable to get `{{name}}`": +"Unspecified": +"Unsupported formats": +"Update complete": +"Update failed": +"Upload image via {{method}} failed": +"User": +"Username": +"Verification failed": +"Working directory retrieval failed": +"Wrong captcha": +"{{count}} items imported": +"{{done}} of {{total}} done": +"{{name}} already exists": +"{{name}} cannot be empty": +"{{name}} creation failed": +"{{name}} deletion failed": +"{{name}} is required": +"{{name}} not found": +"{{name}} save failed": diff --git a/i18n/zh-CN.yml b/i18n/zh-CN.yml index 16a0fd130..5156de06a 100644 --- a/i18n/zh-CN.yml +++ b/i18n/zh-CN.yml @@ -1,3 +1,75 @@ -"Invalid request": 无效请求 -"Please check trusted_domains config": 请检查可信域名配置 -"Unable to get Origin": 无法获取 Origin +"Access denied": 无权限 +"Account": 账户 +"Admin access required": 需要管理员权限 +"Admin": 管理员 +"Cannot reply to this comment": 无法回复此评论 +"Captcha required": 需要验证码 +"Checking for updates": 正在检查更新 +"Comment count": 评论数 +"Comment failed": 评论失败 +"Comment": 评论 +"Config file read failed": 配置文件读取失败 +"Confirm to continue?": 确认继续? +"Contains invalid URL": 包含无效的 URL +"Create admin account": 创建管理员账户 +"Current version is the latest": 当前版本已是最新的 +"Downloading": 下载中 +"Email": 邮箱 +"Enter {{name}}": 输入{{name}} +"Export complete": 导出完毕 +"Export error": 导出失败 +"File": 文件 +"First comment": 第一条评论 +"Image exceeds {{file_size}} limit": 图片超过大小限制 {{file_size}} +"Image upload forbidden": 禁止上传图片 +"Import complete": 导入完毕 +"Invalid request": 无效的请求 +"Invalid request. Please check your `trusted_domains` config.": 请求无效, 请检查 `trusted_domains` 配置项 +"Invalid {{name}}": 无效的{{name}} +"Link": 链接 +"Login failed": 登陆失败 +"Name": 名称 +"New version available": 有更新可用 +"Nickname": 昵称 +"No comment": 无评论 +"Notify": 通知 +"Page fetch failed": 页面获取失败 +"Page": 页面 +"Parameter": 参数 +"Parent comment": 父评论 +"Password update failed": 密码修改失败 +"Password updated": 密码已修改 +"Password": 密码 +"Please review": 请过目 +"Retype {{name}}": 重新输入{{name}} +"Save failed": 保存失败 +"Saving": 保存中 +"Services restart complete": 服务重启完毕 +"Site `{{name}}` not found. Please create it in control center.": 未找到站点:`{{name}}`,请在控制台创建站点 +"Site": 站点 +"Sub-comment": 子评论 +"Target Site": 目标站点 +"Task executing in background, please wait...": 任务已开始在后台执行,请稍后... +"Task in progress, please wait a moment": 任务执行中,请稍后 +"Type": 类型 +"URL Resolver": URL 解析器 +"Unable to get `{{name}}`": 无法获取 `{{name}}` +"Unspecified": 未指定 +"Unsupported formats": 不支持的格式 +"Update complete": 更新完毕 +"Update failed": 更新失败 +"Upload image via {{method}} failed": 通过 {{method}} 上传图片失败 +"User": 用户 +"Username": 用户名 +"Verification failed": 验证失败 +"Working directory retrieval failed": 工作目录获取失败 +"Wrong captcha": 验证码错误 +"{{count}} items imported": 已导入 {{count}} 个项目 +"{{done}} of {{total}} done": 已完成 {{done}} 共 {{total}} 个 +"{{name}} already exists": '{{name}}已存在' +"{{name}} cannot be empty": '{{name}}不能为空' +"{{name}} creation failed": '{{name}}创建失败' +"{{name}} deletion failed": '{{name}}删除失败' +"{{name}} is required": '{{name}}必须填写' +"{{name}} not found": '{{name}}未找到' +"{{name}} save failed": '{{name}}保存失败' diff --git a/internal/anti_spam/anti_spam.go b/internal/anti_spam/anti_spam.go index c41cd294d..4446c5188 100644 --- a/internal/anti_spam/anti_spam.go +++ b/internal/anti_spam/anti_spam.go @@ -17,10 +17,12 @@ import ( var AntiSpamReplaceKeywords *[]string +const LOG_TAG = "[Spam Interception] " + func SyncSpamCheck(comment *entity.Comment, fiberCtx *fiber.Ctx) { // 拦截评论 BlockCommentBy := func(blocker string) { - logrus.Info(fmt.Sprintf("[垃圾拦截] %s 成功拦截评论 ID=%d 内容=%s", blocker, comment.ID, strconv.Quote(comment.Content))) + logrus.Info(fmt.Sprintf(LOG_TAG+"%s Successful blocking of comments ID=%d CONT=%s", blocker, comment.ID, strconv.Quote(comment.Content))) if comment.IsPending { return } @@ -30,7 +32,7 @@ func SyncSpamCheck(comment *entity.Comment, fiberCtx *fiber.Ctx) { // 拦截失败处理 BlockFailBy := func(blocker string, err error) { - logrus.Error(fmt.Sprintf("[垃圾拦截] %s 拦截发生错误 ID=%d 错误信息: %s", blocker, comment.ID, strconv.Quote(comment.Content)), err) + logrus.Error(fmt.Sprintf(LOG_TAG+"%s Interception error occurred ID=%d Err: %s", blocker, comment.ID, strconv.Quote(comment.Content)), err) } // 统一拦截处理 @@ -124,7 +126,7 @@ func SyncSpamCheck(comment *entity.Comment, fiberCtx *fiber.Ctx) { for _, f := range keywordsConf.Files { buf, err := ioutil.ReadFile(f) if err != nil { - logrus.Error("关键词词库文件 " + f + " 加载失败") + logrus.Error("Failed to load Keyword Dictionary file:" + f) } else { fileContent := string(buf) *AntiSpamReplaceKeywords = append(*AntiSpamReplaceKeywords, utils.SplitAndTrimSpace(fileContent, keywordsConf.FileSep)...) @@ -138,7 +140,7 @@ func SyncSpamCheck(comment *entity.Comment, fiberCtx *fiber.Ctx) { for _, keyword := range *AntiSpamReplaceKeywords { if strings.Contains(handleContent, keyword) { if keywordsConf.Pending { - BlockCommentBy("关键词") + BlockCommentBy("Keyword") break } @@ -150,7 +152,7 @@ func SyncSpamCheck(comment *entity.Comment, fiberCtx *fiber.Ctx) { } if !keywordsConf.Pending && replaced && keywordsConf.ReplacTo != "" { - logrus.Info(fmt.Sprintf("[垃圾拦截] 关键词替换评论 ID=%d 原始内容=%s 替换内容=%s", comment.ID, strconv.Quote(comment.Content), strconv.Quote(handleContent))) + logrus.Info(fmt.Sprintf(LOG_TAG+"Keyword Replacement Comments ID=%d Original=%s Processed=%s", comment.ID, strconv.Quote(comment.Content), strconv.Quote(handleContent))) // 保存评论 comment.Content = handleContent diff --git a/internal/anti_spam/anti_spam_akismet.go b/internal/anti_spam/anti_spam_akismet.go index 809aeac0e..ff45c4d77 100644 --- a/internal/anti_spam/anti_spam_akismet.go +++ b/internal/anti_spam/anti_spam_akismet.go @@ -68,16 +68,16 @@ func Akismet(params *AkismetParams, key string) (isPass bool, err error) { respStr := string(respBody) if config.Instance.Debug { - logrus.Info("akismet 垃圾检测响应 ", respStr) + logrus.Info("akismet Spam Detection Response ", respStr) } switch respStr { case "true": - // 是垃圾评论 + // is a spam comment isPass = false return isPass, nil case "false": - // 不是垃圾评论 + // not a spam comment isPass = true return isPass, nil } diff --git a/internal/anti_spam/anti_spam_aliyun.go b/internal/anti_spam/anti_spam_aliyun.go index 9ef3e39be..fafaf501f 100644 --- a/internal/anti_spam/anti_spam_aliyun.go +++ b/internal/anti_spam/anti_spam_aliyun.go @@ -45,7 +45,7 @@ func Aliyun(p AliyunParams) (isPass bool, err error) { } if textScanResp.GetHttpStatus() != 200 { - return false, errors.New("Respone got: " + strconv.Itoa(textScanResp.GetHttpStatus())) + return false, errors.New("response got: " + strconv.Itoa(textScanResp.GetHttpStatus())) } // Handle Respone @@ -53,7 +53,7 @@ func Aliyun(p AliyunParams) (isPass bool, err error) { respRaw := textScanResp.GetHttpContentString() dataRaw := gjson.Get(respRaw, "data.0.results.0.suggestion") if !dataRaw.Exists() { - return false, errors.New("Unexpected JSON: " + respRaw) + return false, errors.New("unexpected JSON: " + respRaw) } // Get Result diff --git a/internal/anti_spam/anti_spam_tencent.go b/internal/anti_spam/anti_spam_tencent.go index 39a4a503b..d4ed3ddbf 100644 --- a/internal/anti_spam/anti_spam_tencent.go +++ b/internal/anti_spam/anti_spam_tencent.go @@ -43,7 +43,7 @@ func Tencent(p TencentParams) (isPass bool, err error) { cpf.HttpProfile.Endpoint = "tms.tencentcloudapi.com" client, err := tms.NewClient(credential, p.Region, cpf) if err != nil { - return false, errors.New("NewClient calls error: " + err.Error()) + return false, errors.New("func NewClient calls error: " + err.Error()) } // Prepare Request Data @@ -66,7 +66,7 @@ func Tencent(p TencentParams) (isPass bool, err error) { // Send Request response, err := client.TextModeration(request) if _, hasErr := err.(*tErr.TencentCloudSDKError); hasErr { - return false, errors.New("An API error has returned: " + err.Error()) + return false, errors.New("an API error has returned: " + err.Error()) } if err != nil { return false, err diff --git a/internal/artransfer/artrans.go b/internal/artransfer/artrans.go index 38a38d591..d6bc8d10e 100644 --- a/internal/artransfer/artrans.go +++ b/internal/artransfer/artrans.go @@ -8,6 +8,7 @@ import ( "github.com/ArtalkJS/Artalk/internal/db" "github.com/ArtalkJS/Artalk/internal/entity" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/internal/utils" "github.com/cheggaaa/pb/v3" @@ -16,7 +17,7 @@ import ( var ArtransImporter = &_ArtransImporter{ ImporterInfo: ImporterInfo{ Name: "artrans", - Desc: "从 Artrans 导入数据", + Desc: "Import from Artrans", Note: "", }, } @@ -50,29 +51,29 @@ func ImportArtransByStr(basic *BasicParams, str string) { func ImportArtrans(basic *BasicParams, srcComments []entity.Artran) { if len(srcComments) == 0 { - logFatal("未读取到任何一条评论") + logFatal(i18n.T("No comment")) return } if basic.TargetSiteUrl != "" && !utils.ValidateURL(basic.TargetSiteUrl) { - logFatal("目标站点 URL 无效") + logFatal(i18n.T("Invalid {{name}}", map[string]interface{}{"name": i18n.T("Target Site") + " " + "URL"})) return } // 汇总 - print("# 请过目:\n\n") + print("# " + i18n.T("Please review") + ":\n\n") // 第一条评论 - PrintEncodeData("第一条评论", srcComments[0]) + PrintEncodeData(i18n.T("First comment"), srcComments[0]) showTSiteName := basic.TargetSiteName showTSiteUrl := basic.TargetSiteUrl if showTSiteName == "" { - showTSiteName = "未指定" + showTSiteName = i18n.T("Unspecified") } if showTSiteUrl == "" { - showTSiteUrl = "未指定" + showTSiteUrl = i18n.T("Unspecified") } // 目标站点名和目标站点URL都不为空,才开启 URL 解析器 @@ -86,16 +87,16 @@ func ImportArtrans(basic *BasicParams, srcComments []entity.Artran) { // } PrintTable([][]interface{}{ - {"目标站点名", showTSiteName}, - {"目标站点 URL", showTSiteUrl}, - {"评论数量", fmt.Sprintf("%d", len(srcComments))}, - {"URL 解析器", showUrlResolver}, + {i18n.T("Target Site") + " " + i18n.T("Name"), showTSiteName}, + {i18n.T("Target Site") + " URL", showTSiteUrl}, + {i18n.T("Comment count"), fmt.Sprintf("%d", len(srcComments))}, + {i18n.T("URL Resolver"), showUrlResolver}, }) print("\n") // 确认开始 - if !Confirm("确认开始导入吗?") { + if !Confirm(i18n.T("Confirm to continue?")) { os.Exit(0) } @@ -197,7 +198,7 @@ func ImportArtrans(basic *BasicParams, srcComments []entity.Artran) { importComments = append(importComments, nComment) } - println("数据保存中...") + println(i18n.T("Saving") + "...") // Batch Insert // @link https://gorm.io/docs/create.html#Batch-Insert @@ -264,5 +265,5 @@ func ImportArtrans(basic *BasicParams, srcComments []entity.Artran) { println() } - logInfo(fmt.Sprintf("完成导入 %d 条数据", len(srcComments))) + logInfo(i18n.T("{{count}} items imported", map[string]interface{}{"count": len(srcComments)})) } diff --git a/internal/artransfer/example.go b/internal/artransfer/example.go index 4a04e1419..21cde622e 100644 --- a/internal/artransfer/example.go +++ b/internal/artransfer/example.go @@ -3,7 +3,7 @@ package artransfer var ExampleImporter = &_ExampleImporter{ ImporterInfo: ImporterInfo{ Name: "example", - Desc: "从 Example 导入数据", + Desc: "Import data from ", Note: "", }, } diff --git a/internal/artransfer/importer.go b/internal/artransfer/importer.go index 1fafe3aaf..0af778a51 100644 --- a/internal/artransfer/importer.go +++ b/internal/artransfer/importer.go @@ -17,6 +17,7 @@ import ( "github.com/ArtalkJS/Artalk/internal/config" "github.com/ArtalkJS/Artalk/internal/entity" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/internal/utils" "github.com/araddon/dateparse" @@ -31,7 +32,7 @@ func RunImportArtrans(payload []string) { print("\n") tableData := [][]interface{}{ - {"数据迁移 - 导入"}, + {"Artransfer - Import"}, {strings.ToUpper(name)}, {desc}, } @@ -46,7 +47,7 @@ func RunImportArtrans(payload []string) { //elapsed := time.Since(t1) print("\n") - logInfo("导入执行结束") //,耗时: ", elapsed) + logInfo(i18n.T("Import complete")) //,耗时: ", elapsed) } type ImporterInfo struct { @@ -81,7 +82,7 @@ func GetBasicParamsFrom(payload []string) *BasicParams { }) if !basic.UrlResolver { - logWarn("目标站点 URL 解析器已关闭") + logWarn("Target site URL resolver disabled") } return &basic @@ -89,13 +90,13 @@ func GetBasicParamsFrom(payload []string) *BasicParams { func RequiredBasicTargetSite(basic *BasicParams) error { if basic.TargetSiteName == "" { - return errors.New("请附带参数 `t_name:<目标站点名称>`") + return errors.New(i18n.T("{{name}} is required", map[string]interface{}{"name": "t_name:"})) } if basic.TargetSiteUrl == "" { - return errors.New("请附带参数 `t_url:<目标站点根目录 URL>`") + return errors.New(i18n.T("{{name}} is required", map[string]interface{}{"name": "t_url:"})) } if !utils.ValidateURL(basic.TargetSiteUrl) { - return errors.New("参数 `t_url:<目标站点根目录 URL>` 必须为 URL 格式") + return errors.New("invalid URL for parameter `t_url:`") } return nil @@ -111,7 +112,7 @@ func SiteReady(tSiteName string, tSiteUrls string) (entity.Site, error) { site.Urls = tSiteUrls err := query.CreateSite(&site) if err != nil { - return entity.Site{}, errors.New("站点创建失败") + return entity.Site{}, errors.New("failed to create site") } } else { // 追加 URL @@ -141,7 +142,7 @@ func SiteReady(tSiteName string, tSiteUrls string) (entity.Site, error) { site.Urls = strings.Join(rUrls, ",") err := query.UpdateSite(&site) if err != nil { - return entity.Site{}, errors.New("站点数据更新失败") + return entity.Site{}, errors.New("update site data failed") } } } @@ -162,15 +163,16 @@ func JsonFileReady(payload []string) (string, error) { } if jsonFile == "" { - return "", errors.New("请附带参数 `json_file:`") + + return "", errors.New(i18n.T("{{name}} is required", map[string]interface{}{"name": "json_file:"})) } if _, err := os.Stat(jsonFile); errors.Is(err, os.ErrNotExist) { - return "", errors.New("文件不存在,请检查参数 `json_file` 传入路径是否正确") + return "", errors.New(i18n.T("{{name}} not found", map[string]interface{}{"name": i18n.T("File")})) } buf, err := ioutil.ReadFile(jsonFile) if err != nil { - return "", errors.New("json 文件打开失败:" + err.Error()) + return "", errors.New("file open failed" + ": " + err.Error()) } return string(buf), nil @@ -297,13 +299,13 @@ func JsonDecodeFAS(str string, fasStructure interface{}) error { var err error str, err = TryConvertLineJsonToArr(str) if err != nil { - return errors.New("JSON 不是 Array 类型," + err.Error()) + return errors.New("JSON of array type is required: " + err.Error()) } } err := json.Unmarshal([]byte(utils.JsonObjInArrAnyStr(str)), fasStructure) // lib.ToString() if err != nil { - return errors.New("JSON 解析失败 " + err.Error()) + return errors.New("failed to parse JSON: " + err.Error()) } return nil diff --git a/internal/cache/actions.go b/internal/cache/actions.go index c8c06f8f9..565322694 100644 --- a/internal/cache/actions.go +++ b/internal/cache/actions.go @@ -49,7 +49,7 @@ func FindAndStoreCache(name string, dest interface{}, queryDBResult func() inter func FindCache(name string, dest interface{}) error { if !config.Instance.Cache.Enabled { - return errors.New("缓存功能禁用") + return errors.New("cache disabled") } // `Get()` is Thread Safe, so no need to add Mutex @@ -59,7 +59,7 @@ func FindCache(name string, dest interface{}) error { return err } - logrus.Debug("[缓存命中] " + name) + logrus.Debug("[Cache Hit] " + name) return nil } diff --git a/internal/cache/cache.go b/internal/cache/cache.go index eaf4285c0..99d0530ee 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -66,7 +66,7 @@ func OpenCache() (err error) { ) default: - logrus.Fatal("请检查配置文件 `cache.type` 无效缓存类型:", cacheType) + logrus.Fatal("Invalid cache type `" + cacheType + "`, please check config option `cache.type`") } diff --git a/internal/captcha/geetest.go b/internal/captcha/geetest.go index 815ec0622..6e8184d95 100644 --- a/internal/captcha/geetest.go +++ b/internal/captcha/geetest.go @@ -39,7 +39,7 @@ func GeetestCheck(paramsJSON string) (isPass bool, reason string, err error) { var p GeetestParams err = json.Unmarshal([]byte(paramsJSON), &p) if err != nil { - return false, "", errors.New("Reqest params json parse err: " + err.Error()) + return false, "", errors.New("request params json parse err: " + err.Error()) } // 生成签名 @@ -58,7 +58,7 @@ func GeetestCheck(paramsJSON string) (isPass bool, reason string, err error) { cli := http.Client{Timeout: time.Second * 5} // 5s 超时 resp, err := cli.PostForm(url, form_data) if err != nil || resp.StatusCode != 200 { - return false, "", errors.New("服务接口异常: " + err.Error()) + return false, "", errors.New("service interface exception: " + err.Error()) } // 处理响应结果 @@ -67,7 +67,7 @@ func GeetestCheck(paramsJSON string) (isPass bool, reason string, err error) { gResult := gjson.Get(resJson, "result") if !gResult.Exists() { - return false, "", errors.New("响应结果不符合预期: " + resJson) + return false, "", errors.New("response results are not as expected: " + resJson) } result := gResult.String() diff --git a/internal/config/init.go b/internal/config/init.go index d423a0bd5..a348ad706 100644 --- a/internal/config/init.go +++ b/internal/config/init.go @@ -31,14 +31,14 @@ func Init(cfgFile string) { // load yaml config if err := kf.Load(file.Provider(cfgFile), parser); err != nil { logrus.Errorln(err) - logrus.Fatal("配置文件读取错误") + logrus.Fatal("Config file read error") } Instance = &Config{} if err := kf.Unmarshal("", Instance); err != nil { logrus.Errorln(err) - logrus.Fatal("配置文件解析错误") + logrus.Fatal("Config file parse error") } cfgFileLoaded = cfgFile @@ -50,12 +50,12 @@ func Init(cfgFile string) { func postInit() { // 检查 app_key 是否设置 if strings.TrimSpace(Instance.AppKey) == "" { - logrus.Fatal("请检查配置文件,并设置一个 app_key (任意字符串) 用于数据加密") + logrus.Fatal("Please check config file and set an `app_key` for data encryption") } // 设置时区 if strings.TrimSpace(Instance.TimeZone) == "" { - logrus.Fatal("请检查配置文件,并设置 timezone") + logrus.Fatal("Please check config file and set `timezone`") } denverLoc, _ := time.LoadLocation(Instance.TimeZone) time.Local = denverLoc @@ -63,7 +63,7 @@ func postInit() { // 默认站点配置 Instance.SiteDefault = strings.TrimSpace(Instance.SiteDefault) if Instance.SiteDefault == "" { - logrus.Fatal("请设置 SiteDefault 默认站点,不能为空") + logrus.Fatal("Please check config file and set `site_default`") } // 缓存配置 @@ -83,13 +83,13 @@ func postInit() { /* 检查废弃需更新配置 */ if Instance.Captcha.ActionTimeout != 0 { - logrus.Warn("captcha.action_timeout 配置项已废弃,请使用 captcha.action_reset 代替") + logrus.Warn("The config option `captcha.action_timeout` is deprecated, please use `captcha.action_reset` instead") if Instance.Captcha.ActionReset == 0 { Instance.Captcha.ActionReset = Instance.Captcha.ActionTimeout } } if len(Instance.AllowOrigins) != 0 { - logrus.Warn("allow_origins 配置项已废弃,请使用 trusted_domains 代替") + logrus.Warn("The config option `allow_origins` is deprecated, please use `trusted_domains` instead") if len(Instance.TrustedDomains) == 0 { Instance.TrustedDomains = Instance.AllowOrigins } @@ -97,7 +97,7 @@ func postInit() { // @version < 2.2.0 if Instance.Notify != nil { - logrus.Warn("notify 配置项已废弃,请使用 admin_notify 代替") + logrus.Warn("The config option `notify` is deprecated, please use `admin_notify` instead") Instance.AdminNotify = *Instance.Notify } if Instance.AdminNotify.Email == nil { @@ -106,7 +106,7 @@ func postInit() { } } if Instance.Email.MailSubjectToAdmin != "" { - logrus.Warn("email.mail_subject_to_admin 配置项已废弃,请使用 admin_notify.email.mail_subject 代替") + logrus.Warn("The config option `email.mail_subject_to_admin` is deprecated, please use `admin_notify.email.mail_subject` instead") Instance.AdminNotify.Email.MailSubject = Instance.Email.MailSubjectToAdmin } diff --git a/internal/core/core.go b/internal/core/core.go index 130920179..0a021a5d3 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -56,7 +56,7 @@ func initConfig(cfgFile string, workDir string) { // 切换工作目录 if workDir != "" { if err := os.Chdir(workDir); err != nil { - logrus.Fatal("工作目录切换错误 ", err) + logrus.Fatal("Working directory change error: ", err) } } @@ -137,7 +137,7 @@ func initLog() { func initCache() { err := cache.OpenCache() if err != nil { - logrus.Error("缓存初始化发生错误 ", err) + logrus.Error("[Cache] ", "Init cache error: ", err) os.Exit(1) } } diff --git a/internal/core/gen.go b/internal/core/gen.go index 35773466f..aff82f793 100644 --- a/internal/core/gen.go +++ b/internal/core/gen.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/pkged" "github.com/sirupsen/logrus" ) @@ -25,12 +26,12 @@ func Gen(genType string, specificPath string, overwrite bool) { file, err := pkged.FS().Open(strings.TrimPrefix(genType, "/")) if err != nil { - logrus.Fatal("无效的内置资源: "+genType+" ", err) + logrus.Fatal("Invalid built-in resource `"+genType+"`: ", err) } buf, err := ioutil.ReadAll(file) if err != nil { - logrus.Fatal("读取内置资源: "+genType+" 失败 ", err) + logrus.Fatal("Read built-in resources `"+genType+"` error: ", err) } // 自动生成 app_key @@ -50,20 +51,20 @@ func Gen(genType string, specificPath string, overwrite bool) { } if CheckFileExist(absPath) && !overwrite { - logrus.Fatal("已存在文件: " + absPath) + logrus.Fatal(i18n.T("{{name}} already exists", map[string]interface{}{"name": i18n.T("File")}) + ": " + absPath) } dst, err := os.Create(absPath) if err != nil { - logrus.Fatal("创建目标文件失败 ", err) + logrus.Fatal("Failed to create target file: ", err) } defer dst.Close() if _, err = dst.Write(buf); err != nil { - logrus.Fatal("写入目标文件失败 ", err) + logrus.Fatal("Failed to write target file: ", err) } - logrus.Info("生成文件: " + absPath) + logrus.Info("File Generated: " + absPath) } var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*") diff --git a/internal/db/db.go b/internal/db/db.go index e60343979..0f48df859 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -33,7 +33,7 @@ func InitDB() { var err error db, err := OpenDB(config.Instance.DB.Type, config.Instance.DB.Dsn) if err != nil { - logrus.Error("数据库初始化发生错误 ", err) + logrus.Error("[DB] ", "Init database error: ", err) os.Exit(1) } SetDB(db) @@ -53,7 +53,7 @@ func OpenDB(dbType config.DBType, dsn string) (*gorm.DB, error) { switch dbType { case config.TypeSQLite: if dbConf.File == "" { - logrus.Fatal("请配置 db.file 指定一个 sqlite 数据库路径") + logrus.Fatal("Please set `db.file` option in config file to specify a sqlite database path") } dsn = dbConf.File case config.TypePostgreSQL: @@ -86,7 +86,7 @@ func OpenDB(dbType config.DBType, dsn string) (*gorm.DB, error) { return OpenSqlServer(dsn) } - return nil, errors.New(`不支持的数据库类型 "` + string(dbType) + `"`) + return nil, errors.New(`unsupported database type "` + string(dbType) + `"`) } func OpenSQLite(filename string) (*gorm.DB, error) { diff --git a/internal/email/queue.go b/internal/email/queue.go index 8b6dd187f..56407a0d9 100644 --- a/internal/email/queue.go +++ b/internal/email/queue.go @@ -24,7 +24,7 @@ func InitQueue() { if email.LinkedNotify != nil { // 标记关联评论邮件发送状态 if err := query.NotifySetEmailed(email.LinkedNotify); err != nil { - logrus.Errorf("[EMAIL] 标记关联评论邮件发送状态失败: %s", err) + logrus.Errorf("[Email] ", "Flag associated comment email delivery status failed: %s", err) continue } } diff --git a/internal/email/render.go b/internal/email/render.go index 4fb06040d..ce53f0af9 100644 --- a/internal/email/render.go +++ b/internal/email/render.go @@ -1,211 +1,205 @@ -package email - -import ( - "bytes" - "embed" - "errors" - "fmt" - "html" - "os" - "regexp" - "strings" - - "github.com/ArtalkJS/Artalk/internal/config" - "github.com/ArtalkJS/Artalk/internal/entity" - "github.com/ArtalkJS/Artalk/internal/query" - "github.com/ArtalkJS/Artalk/internal/utils" -) - -//go:embed email_tpl/* -//go:embed notify_tpl/* -var internalTpl embed.FS - -func RenderCommon(str string, notify *entity.Notify, _renderType ...string) string { - // 渲染类型 - renderType := "email" // 默认为邮件发送渲染 - if len(_renderType) > 0 { - renderType = _renderType[0] - } - - fromComment := query.FetchCommentForNotify(notify) - from := query.CookCommentForEmail(&fromComment) - toComment := query.FindNotifyParentComment(notify) - to := query.CookCommentForEmail(&toComment) - - toUser := query.FetchUserForNotify(notify) // 发送目标用户 - - content := to.Content - replyContent := from.Content - if renderType == "notify" { // 多元推送内容 - content = HandleEmoticonsImgTagsForNotify(to.ContentRaw) - replyContent = HandleEmoticonsImgTagsForNotify(from.ContentRaw) - } - - cf := CommonFields{ - From: from, - To: to, - Comment: from, - ParentComment: to, - - Nick: toUser.Name, - Content: content, - ReplyNick: from.Nick, - ReplyContent: replyContent, - PageTitle: from.Page.Title, - PageURL: from.Page.URL, - SiteName: from.SiteName, - SiteURL: from.Site.FirstUrl, - - LinkToReply: query.GetReadLinkByNotify(notify), - } - - flat := utils.StructToFlatDotMap(&cf) - - return ReplaceAllMustache(str, flat) -} - -type CommonFields struct { - From entity.CookedCommentForEmail `json:"from"` - To entity.CookedCommentForEmail `json:"to"` - Comment entity.CookedCommentForEmail `json:"comment"` - ParentComment entity.CookedCommentForEmail `json:"parent_comment"` - - Nick string `json:"nick"` - Content string `json:"content"` - ReplyNick string `json:"reply_nick"` - ReplyContent string `json:"reply_content"` - - PageTitle string `json:"page_title"` - PageURL string `json:"page_url"` - SiteName string `json:"site_name"` - SiteURL string `json:"site_url"` - - LinkToReply string `json:"link_to_reply"` -} - -// 替换 {{ key }} 为 val -func ReplaceAllMustache(data string, dict map[string]interface{}) string { - r := regexp.MustCompile(`{{\s*(.*?)\s*}}`) - return r.ReplaceAllStringFunc(data, func(m string) string { - key := r.FindStringSubmatch(m)[1] - if val, isExist := dict[key]; isExist { - return GetPurifiedValue(key, val) - } - - return m - }) -} - -// 净化文本,防止 XSS -func GetPurifiedValue(k string, v interface{}) string { - val := fmt.Sprintf("%v", v) - - // 白名单 - ignoreEscapeKeys := []string{"reply_content", "content", "link_to_reply"} - if utils.ContainsStr(ignoreEscapeKeys, k) || - strings.HasSuffix(k, ".content") || // 排除 entity.CookedComment.content - strings.HasSuffix(k, ".content_raw") { - return val - } - - val = html.EscapeString(val) - return val -} - -func HandleEmoticonsImgTagsForNotify(str string) string { - r := regexp.MustCompile(`]*?atk-emoticon=["]([^"]*?)["][^>]*?>`) - return r.ReplaceAllStringFunc(str, func(m string) string { - ms := r.FindStringSubmatch(m) - if len(ms) < 2 { - return m - } - if ms[1] == "" { - return "[表情]" - } - return "[" + ms[1] + "]" - }) -} - -// 渲染邮件 Body 内容 -func RenderEmailBody(notify *entity.Notify, isSendToAdmin bool) string { - tplName := config.Instance.Email.MailTpl - - // 发送给管理员的邮件单独使用管理员邮件模板 - if isSendToAdmin { - tplName = config.Instance.AdminNotify.Email.MailTpl - } - - // 配置文件未指定邮件模板路径,使用内置默认模板 - if tplName == "" { - tplName = "default" - } - - tpl := "" - if _, err := os.Stat(tplName); errors.Is(err, os.ErrNotExist) { - tpl = GetInternalEmailTpl(tplName) - } else { - // TODO 反复文件 IO 操作会导致性能下降, - // 之后优化可以改成程序启动时加载模板文件到内存中 - tpl = GetExternalTpl(tplName) - } - - tpl = RenderCommon(tpl, notify) - - return tpl -} - -// 渲染管理员推送 Body 内容 -func RenderNotifyBody(notify *entity.Notify) string { - tplName := config.Instance.AdminNotify.NotifyTpl - if tplName == "" { - tplName = "default" - } - - tpl := "" - if _, err := os.Stat(tplName); errors.Is(err, os.ErrNotExist) { - tpl = GetInternalNotifyTpl(tplName) - } else { - tpl = GetExternalTpl(tplName) - } - - tpl = RenderCommon(tpl, notify, "notify") - - return tpl -} - -// 获取内建邮件模版 -func GetInternalEmailTpl(tplName string) string { - return GetInternalTpl("email_tpl", tplName) -} - -// 获取内建通知模版 -func GetInternalNotifyTpl(tplName string) string { - return GetInternalTpl("notify_tpl", tplName) -} - -// 获取内建模版 -func GetInternalTpl(basePath string, tplName string) string { - filename := fmt.Sprintf("%s/%s.html", basePath, tplName) - f, err := internalTpl.Open(filename) - if err != nil { - return "" - } - - buf := new(bytes.Buffer) - if _, err := buf.ReadFrom(f); err != nil { - return "" - } - contents := buf.String() - - return contents -} - -// 获取外置模版 -func GetExternalTpl(filename string) string { - buf, err := os.ReadFile(filename) - if err != nil { - return "" - } - - return string(buf) -} +package email + +import ( + "bytes" + "embed" + "errors" + "fmt" + "html" + "os" + "regexp" + "strings" + + "github.com/ArtalkJS/Artalk/internal/config" + "github.com/ArtalkJS/Artalk/internal/entity" + "github.com/ArtalkJS/Artalk/internal/query" + "github.com/ArtalkJS/Artalk/internal/utils" +) + +//go:embed email_tpl/* +//go:embed notify_tpl/* +var internalTpl embed.FS + +func RenderCommon(str string, notify *entity.Notify, _renderType ...string) string { + // 渲染类型 + renderType := "email" // 默认为邮件发送渲染 + if len(_renderType) > 0 { + renderType = _renderType[0] + } + + fromComment := query.FetchCommentForNotify(notify) + from := query.CookCommentForEmail(&fromComment) + toComment := query.FindNotifyParentComment(notify) + to := query.CookCommentForEmail(&toComment) + + toUser := query.FetchUserForNotify(notify) // 发送目标用户 + + content := to.Content + replyContent := from.Content + if renderType == "notify" { // 多元推送内容 + content = HandleEmoticonsImgTagsForNotify(to.ContentRaw) + replyContent = HandleEmoticonsImgTagsForNotify(from.ContentRaw) + } + + cf := CommonFields{ + From: from, + To: to, + Comment: from, + ParentComment: to, + + Nick: toUser.Name, + Content: content, + ReplyNick: from.Nick, + ReplyContent: replyContent, + PageTitle: from.Page.Title, + PageURL: from.Page.URL, + SiteName: from.SiteName, + SiteURL: from.Site.FirstUrl, + + LinkToReply: query.GetReadLinkByNotify(notify), + } + + flat := utils.StructToFlatDotMap(&cf) + + return ReplaceAllMustache(str, flat) +} + +type CommonFields struct { + From entity.CookedCommentForEmail `json:"from"` + To entity.CookedCommentForEmail `json:"to"` + Comment entity.CookedCommentForEmail `json:"comment"` + ParentComment entity.CookedCommentForEmail `json:"parent_comment"` + + Nick string `json:"nick"` + Content string `json:"content"` + ReplyNick string `json:"reply_nick"` + ReplyContent string `json:"reply_content"` + + PageTitle string `json:"page_title"` + PageURL string `json:"page_url"` + SiteName string `json:"site_name"` + SiteURL string `json:"site_url"` + + LinkToReply string `json:"link_to_reply"` +} + +// 替换 {{ key }} 为 val +func ReplaceAllMustache(data string, dict map[string]interface{}) string { + return utils.RenderMustaches(data, dict, func(k string, v interface{}) string { + return GetPurifiedValue(k, v) + }) +} + +// 净化文本,防止 XSS +func GetPurifiedValue(k string, v interface{}) string { + val := fmt.Sprintf("%v", v) + + // 白名单 + ignoreEscapeKeys := []string{"reply_content", "content", "link_to_reply"} + if utils.ContainsStr(ignoreEscapeKeys, k) || + strings.HasSuffix(k, ".content") || // 排除 entity.CookedComment.content + strings.HasSuffix(k, ".content_raw") { + return val + } + + val = html.EscapeString(val) + return val +} + +func HandleEmoticonsImgTagsForNotify(str string) string { + r := regexp.MustCompile(`]*?atk-emoticon=["]([^"]*?)["][^>]*?>`) + return r.ReplaceAllStringFunc(str, func(m string) string { + ms := r.FindStringSubmatch(m) + if len(ms) < 2 { + return m + } + if ms[1] == "" { + return "[表情]" + } + return "[" + ms[1] + "]" + }) +} + +// 渲染邮件 Body 内容 +func RenderEmailBody(notify *entity.Notify, isSendToAdmin bool) string { + tplName := config.Instance.Email.MailTpl + + // 发送给管理员的邮件单独使用管理员邮件模板 + if isSendToAdmin { + tplName = config.Instance.AdminNotify.Email.MailTpl + } + + // 配置文件未指定邮件模板路径,使用内置默认模板 + if tplName == "" { + tplName = "default" + } + + tpl := "" + if _, err := os.Stat(tplName); errors.Is(err, os.ErrNotExist) { + tpl = GetInternalEmailTpl(tplName) + } else { + // TODO 反复文件 IO 操作会导致性能下降, + // 之后优化可以改成程序启动时加载模板文件到内存中 + tpl = GetExternalTpl(tplName) + } + + tpl = RenderCommon(tpl, notify) + + return tpl +} + +// 渲染管理员推送 Body 内容 +func RenderNotifyBody(notify *entity.Notify) string { + tplName := config.Instance.AdminNotify.NotifyTpl + if tplName == "" { + tplName = "default" + } + + tpl := "" + if _, err := os.Stat(tplName); errors.Is(err, os.ErrNotExist) { + tpl = GetInternalNotifyTpl(tplName) + } else { + tpl = GetExternalTpl(tplName) + } + + tpl = RenderCommon(tpl, notify, "notify") + + return tpl +} + +// 获取内建邮件模版 +func GetInternalEmailTpl(tplName string) string { + return GetInternalTpl("email_tpl", tplName) +} + +// 获取内建通知模版 +func GetInternalNotifyTpl(tplName string) string { + return GetInternalTpl("notify_tpl", tplName) +} + +// 获取内建模版 +func GetInternalTpl(basePath string, tplName string) string { + filename := fmt.Sprintf("%s/%s.html", basePath, tplName) + f, err := internalTpl.Open(filename) + if err != nil { + return "" + } + + buf := new(bytes.Buffer) + if _, err := buf.ReadFrom(f); err != nil { + return "" + } + contents := buf.String() + + return contents +} + +// 获取外置模版 +func GetExternalTpl(filename string) string { + buf, err := os.ReadFile(filename) + if err != nil { + return "" + } + + return string(buf) +} diff --git a/internal/email/sender_alidm.go b/internal/email/sender_alidm.go index 8f046db34..2453906de 100644 --- a/internal/email/sender_alidm.go +++ b/internal/email/sender_alidm.go @@ -40,7 +40,7 @@ func (s *AliDMSender) Send(email Email) bool { resp, err := client.SingleRequest(req) if err != nil { - logrus.Error("[EMAIL] Aliyun DM 邮件发送失败 ", err) + logrus.Error("[Email] ", "Email sending failed via Aliyun DM", err) return false } diff --git a/internal/email/sender_smtp.go b/internal/email/sender_smtp.go index 6dbab7efe..8aadf04de 100644 --- a/internal/email/sender_smtp.go +++ b/internal/email/sender_smtp.go @@ -27,7 +27,7 @@ func (s *SmtpSender) Send(email Email) bool { // 发送邮件 if err := s.dialer.DialAndSend(m); err != nil { - logrus.Error("[EMAIL] SMTP 邮件发送失败 ", err) + logrus.Error("[Email] ", "Email sending failed via SMTP ", err) return false } diff --git a/internal/i18n/gen/main.go b/internal/i18n/gen/main.go index 2f63910d4..9bdc81aa4 100644 --- a/internal/i18n/gen/main.go +++ b/internal/i18n/gen/main.go @@ -89,7 +89,7 @@ func scan(dir string) []string { return nil } - extKeys := extractFormCode(codeStr) + extKeys := extractFromCode(codeStr) if len(extKeys) > 0 { result = append(result, extKeys...) @@ -108,7 +108,7 @@ func scan(dir string) []string { return result } -func extractFormCode(src string) []string { +func extractFromCode(src string) []string { fileSet := token.NewFileSet() f, err := parser.ParseFile(fileSet, "", src, 0) if err != nil { diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go index 41c29041f..4c306d2ac 100644 --- a/internal/i18n/i18n.go +++ b/internal/i18n/i18n.go @@ -2,9 +2,9 @@ package i18n import ( "fmt" - "strings" "github.com/ArtalkJS/Artalk/internal/pkged" + "github.com/ArtalkJS/Artalk/internal/utils" "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" ) @@ -28,7 +28,7 @@ func Init(locale string) { yaml.Unmarshal(yamlStr, &Locales) } -func T(msg string, params ...interface{}) string { +func T(msg string, params ...map[string]interface{}) string { v, ok := Locales[msg] if !ok || v == "" { v = msg @@ -36,9 +36,9 @@ func T(msg string, params ...interface{}) string { return msgParams(v, params...) } -func msgParams(msg string, params ...interface{}) string { - if strings.Contains(msg, "%") { - msg = fmt.Sprintf(msg, params...) +func msgParams(msg string, params ...map[string]interface{}) string { + if len(params) > 0 { + return utils.RenderMustaches(msg, params[0]) } return msg diff --git a/internal/notify_launcher/notify_launcher.go b/internal/notify_launcher/notify_launcher.go index 2999c4afe..6ac34e1e6 100644 --- a/internal/notify_launcher/notify_launcher.go +++ b/internal/notify_launcher/notify_launcher.go @@ -192,7 +192,7 @@ func SendLark(title string, msg string) { sendData := fmt.Sprintf(`{"msg_type":"text","content":{"text":%s}}`, strconv.Quote(msg)) result, err := http.Post(larkConf.WebhookURL, "application/json", strings.NewReader(sendData)) if err != nil { - logrus.Error("[飞书]", " 消息发送失败:", err) + logrus.Error("[飞书] ", "Failed to send msg:", err) return } @@ -212,7 +212,7 @@ func SendBark(title string, msg string) { result, err := http.Get(fmt.Sprintf("%s/%s/%s", strings.TrimSuffix(barkConf.Server, "/"), url.QueryEscape(title), url.QueryEscape(msg))) if err != nil { - logrus.Error("[Bark]", " 消息发送失败:", err) + logrus.Error("[Bark] ", "Failed to send msg:", err) return } @@ -247,7 +247,7 @@ func SendWebHook(subject string, body string, comment *entity.Comment, pComment jsonByte, _ := json.Marshal(reqData) result, err := http.Post(webhookConf.URL, "application/json", bytes.NewReader(jsonByte)) if err != nil { - logrus.Error("[WebHook 推送]", " 消息发送失败:", err) + logrus.Error("[WebHook Push] ", "Failed to send msg:", err) return } diff --git a/internal/query/service.go b/internal/query/service.go index dcf37287e..e94510c45 100644 --- a/internal/query/service.go +++ b/internal/query/service.go @@ -66,7 +66,7 @@ func FetchPageFromURL(p *entity.Page) error { url := cookedPage.URL if url == "" { - return errors.New("URL is null") + return errors.New("URL cannot be null") } // 获取 URL 页面 title @@ -77,7 +77,7 @@ func FetchPageFromURL(p *entity.Page) error { } if err := UpdatePage(p); err != nil { - logrus.Error("FetchURL 保存失败") + logrus.Error("Failed to save in FetchPage") return err } @@ -86,8 +86,8 @@ func FetchPageFromURL(p *entity.Page) error { func GetTitleByURL(url string) (string, error) { if !utils.ValidateURL(url) { - logrus.Error("URL " + url + " is invalid") - return "", errors.New("URL is invalid") + logrus.Error("Invalid URL: " + url) + return "", errors.New("invalid URL") } // Request the HTML page. diff --git a/internal/utils/template.go b/internal/utils/template.go new file mode 100644 index 000000000..759bc8739 --- /dev/null +++ b/internal/utils/template.go @@ -0,0 +1,25 @@ +package utils + +import ( + "fmt" + "regexp" +) + +// 解析 Mustaches 语法 +// 替换 {{ key }} 为 val +func RenderMustaches(data string, dict map[string]interface{}, valueGetter ...func(k string, v interface{}) string) string { + r := regexp.MustCompile(`{{\s*(.*?)\s*}}`) + + return r.ReplaceAllStringFunc(data, func(m string) string { + key := r.FindStringSubmatch(m)[1] + if val, isExist := dict[key]; isExist { + if len(valueGetter) > 0 { + return valueGetter[0](key, val) + } else { + return fmt.Sprintf("%v", val) + } + } + + return m + }) +} diff --git a/server/common/check.go b/server/common/check.go index 32544cafd..a7c6235f2 100644 --- a/server/common/check.go +++ b/server/common/check.go @@ -2,6 +2,7 @@ package common import ( "github.com/ArtalkJS/Artalk/internal/entity" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/internal/utils" "github.com/gofiber/fiber/v2" @@ -13,7 +14,7 @@ func CheckIsAllowed(c *fiber.Ctx, name string, email string, page entity.Page, s // 如果用户是管理员,或者当前页只能管理员评论 if isAdminUser || page.AdminOnly { if !CheckIsAdminReq(c) { - return false, RespError(c, "需要验证管理员身份", Map{"need_login": true}) + return false, RespError(c, i18n.T("Admin access required"), Map{"need_login": true}) } } diff --git a/server/handler/admin_cache.go b/server/handler/admin_cache.go index 0ec9ae4ed..7a258c078 100644 --- a/server/handler/admin_cache.go +++ b/server/handler/admin_cache.go @@ -2,6 +2,7 @@ package handler import ( "github.com/ArtalkJS/Artalk/internal/cache" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/server/common" "github.com/gofiber/fiber/v2" ) @@ -20,7 +21,7 @@ func AdminCacheWarm(router fiber.Router) { } if !common.GetIsSuperAdmin(c) { - return common.RespError(c, "无权访问") + return common.RespError(c, i18n.T("Access denied")) } go func() { @@ -28,7 +29,7 @@ func AdminCacheWarm(router fiber.Router) { }() return common.RespData(c, common.Map{ - "msg": "缓存预热任务已在后台开始执行,稍等片刻完成...", + "msg": i18n.T("Task executing in background, please wait..."), }) }) } @@ -48,7 +49,7 @@ func AdminCacheFlush(router fiber.Router) { } if !common.GetIsSuperAdmin(c) { - return common.RespError(c, "无权访问") + return common.RespError(c, i18n.T("Access denied")) } if p.FlushAll { @@ -57,10 +58,10 @@ func AdminCacheFlush(router fiber.Router) { }() return common.RespData(c, common.Map{ - "msg": "缓存清理任务已在后台开始执行,稍等片刻完成...", + "msg": i18n.T("Task executing in background, please wait..."), }) } - return common.RespError(c, "参数错误") + return common.RespError(c, i18n.T("Invalid {{name}}", Map{"name": i18n.T("Parameter")})) }) } diff --git a/server/handler/admin_comment_del.go b/server/handler/admin_comment_del.go index 68cd24b7f..db1965d08 100644 --- a/server/handler/admin_comment_del.go +++ b/server/handler/admin_comment_del.go @@ -1,6 +1,7 @@ package handler import ( + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/server/common" "github.com/gofiber/fiber/v2" @@ -28,21 +29,21 @@ func AdminCommentDel(router fiber.Router) { // find comment comment := query.FindComment(p.ID) if comment.IsEmpty() { - return common.RespError(c, "comment not found") + return common.RespError(c, i18n.T("{{name}} not found", Map{"name": i18n.T("Comment")})) } if !common.IsAdminHasSiteAccess(c, comment.SiteName) { - return common.RespError(c, "无权操作") + return common.RespError(c, i18n.T("Access denied")) } // 删除主评论 if err := query.DelComment(&comment); err != nil { - return common.RespError(c, "评论删除失败") + return common.RespError(c, i18n.T("{{name}} deletion failed", Map{"name": i18n.T("Comment")})) } // 删除子评论 if err := query.DelCommentChildren(comment.ID); err != nil { - return common.RespError(c, "子评论删除失败") + return common.RespError(c, i18n.T("{{name}} deletion failed", Map{"name": i18n.T("Sub-comment")})) } return common.RespSuccess(c) diff --git a/server/handler/admin_comment_edit.go b/server/handler/admin_comment_edit.go index 3cc3180fd..030647f53 100644 --- a/server/handler/admin_comment_edit.go +++ b/server/handler/admin_comment_edit.go @@ -5,6 +5,7 @@ import ( "github.com/ArtalkJS/Artalk/internal/email" "github.com/ArtalkJS/Artalk/internal/entity" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/internal/utils" "github.com/ArtalkJS/Artalk/server/common" @@ -46,19 +47,19 @@ func AdminCommentEdit(router fiber.Router) { // find comment comment := query.FindComment(p.ID) if comment.IsEmpty() { - return common.RespError(c, "comment not found") + return common.RespError(c, i18n.T("{{name}} not found", Map{"name": i18n.T("Comment")})) } if !common.IsAdminHasSiteAccess(c, comment.SiteName) { - return common.RespError(c, "无权操作") + return common.RespError(c, i18n.T("Access denied")) } // check params if p.Email != "" && !utils.ValidateEmail(p.Email) { - return common.RespError(c, "Invalid email") + return common.RespError(c, i18n.T("Invalid {{name}}", Map{"name": i18n.T("Email")})) } if p.Link != "" && !utils.ValidateURL(p.Link) { - return common.RespError(c, "Invalid link") + return common.RespError(c, i18n.T("Invalid {{name}}", Map{"name": i18n.T("Link")})) } // content @@ -116,7 +117,7 @@ func AdminCommentEdit(router fiber.Router) { } if err := query.UpdateComment(&comment); err != nil { - return common.RespError(c, "comment save error") + return common.RespError(c, i18n.T("{{name}} save failed", Map{"name": i18n.T("Comment")})) } return common.RespData(c, common.Map{ diff --git a/server/handler/admin_page_del.go b/server/handler/admin_page_del.go index 049b4b8fd..041a3aee2 100644 --- a/server/handler/admin_page_del.go +++ b/server/handler/admin_page_del.go @@ -1,6 +1,7 @@ package handler import ( + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/server/common" "github.com/gofiber/fiber/v2" @@ -25,16 +26,16 @@ func AdminPageDel(router fiber.Router) { page := query.FindPage(p.Key, p.SiteName) if page.IsEmpty() { - return common.RespError(c, "page not found") + return common.RespError(c, i18n.T("{{name}} not found", Map{"name": i18n.T("Page")})) } if !common.IsAdminHasSiteAccess(c, page.SiteName) { - return common.RespError(c, "无权操作") + return common.RespError(c, i18n.T("Access denied")) } err := query.DelPage(&page) if err != nil { - return common.RespError(c, "Page 删除失败") + return common.RespError(c, i18n.T("{{name}} deletion failed", Map{"name": i18n.T("Page")})) } return common.RespSuccess(c) diff --git a/server/handler/admin_page_edit.go b/server/handler/admin_page_edit.go index ff77d9d9b..3dd35e624 100644 --- a/server/handler/admin_page_edit.go +++ b/server/handler/admin_page_edit.go @@ -6,6 +6,7 @@ import ( "github.com/ArtalkJS/Artalk/internal/cache" "github.com/ArtalkJS/Artalk/internal/db" "github.com/ArtalkJS/Artalk/internal/entity" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/server/common" "github.com/gofiber/fiber/v2" @@ -32,7 +33,7 @@ func AdminPageEdit(router fiber.Router) { } if strings.TrimSpace(p.Key) == "" { - return common.RespError(c, "page key 不能为空白字符") + return common.RespError(c, i18n.T("{{name}} cannot be empty", Map{"name": "key"})) } // use site @@ -41,17 +42,17 @@ func AdminPageEdit(router fiber.Router) { // find page var page = query.FindPageByID(p.ID) if page.IsEmpty() { - return common.RespError(c, "page not found") + return common.RespError(c, i18n.T("{{name}} not found", Map{"name": i18n.T("Page")})) } if !common.IsAdminHasSiteAccess(c, page.SiteName) { - return common.RespError(c, "无权操作") + return common.RespError(c, i18n.T("Access denied")) } // 重命名合法性检测 modifyKey := p.Key != page.Key if modifyKey && !query.FindPage(p.Key, p.SiteName).IsEmpty() { - return common.RespError(c, "page 已存在,请换个 key") + return common.RespError(c, i18n.T("{{name}} already exists", Map{"name": i18n.T("Page")})) } // 预先删除缓存,防止修改主键原有 page_key 占用问题 @@ -73,7 +74,7 @@ func AdminPageEdit(router fiber.Router) { } if err := query.UpdatePage(&page); err != nil { - return common.RespError(c, "page save error") + return common.RespError(c, i18n.T("{{name}} save failed", Map{"name": i18n.T("Page")})) } return common.RespData(c, common.Map{ diff --git a/server/handler/admin_page_fetch.go b/server/handler/admin_page_fetch.go index f25e8c387..91f209bb6 100644 --- a/server/handler/admin_page_fetch.go +++ b/server/handler/admin_page_fetch.go @@ -1,11 +1,10 @@ package handler import ( - "fmt" - "github.com/ArtalkJS/Artalk/internal/config" "github.com/ArtalkJS/Artalk/internal/db" "github.com/ArtalkJS/Artalk/internal/entity" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/server/common" "github.com/gofiber/fiber/v2" @@ -37,7 +36,7 @@ func AdminPageFetch(router fiber.Router) { if p.GetStatus { if allPageFetching { return common.RespData(c, common.Map{ - "msg": fmt.Sprintf("已完成 %d 共 %d 个", allPageFetchDone, allPageFetchTotal), + "msg": i18n.T("{{done}} of {{total}} done", Map{"done": allPageFetchDone, "total": allPageFetchTotal}), "is_progress": true, }) } else { @@ -51,7 +50,7 @@ func AdminPageFetch(router fiber.Router) { // 更新全部站点 if p.SiteName != "" { if allPageFetching { - return common.RespError(c, "任务正在进行中,请稍等片刻") + return common.RespError(c, i18n.T("Task in progress, please wait a moment")) } // 异步执行 @@ -82,15 +81,15 @@ func AdminPageFetch(router fiber.Router) { page := query.FindPageByID(p.ID) if page.IsEmpty() { - return common.RespError(c, "page not found") + return common.RespError(c, i18n.T("{{name}} not found", Map{"name": i18n.T("Page")})) } if !common.IsAdminHasSiteAccess(c, page.SiteName) { - return common.RespError(c, "无权操作") + return common.RespError(c, i18n.T("Access denied")) } if err := query.FetchPageFromURL(&page); err != nil { - return common.RespError(c, "page fetch error: "+err.Error()) + return common.RespError(c, i18n.T("Page fetch failed")+": "+err.Error()) } return common.RespData(c, common.Map{ diff --git a/server/handler/admin_page_get.go b/server/handler/admin_page_get.go index 0cd800461..a79b0d169 100644 --- a/server/handler/admin_page_get.go +++ b/server/handler/admin_page_get.go @@ -3,6 +3,7 @@ package handler import ( "github.com/ArtalkJS/Artalk/internal/db" "github.com/ArtalkJS/Artalk/internal/entity" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/server/common" "github.com/gofiber/fiber/v2" @@ -33,7 +34,7 @@ func AdminPageGet(router fiber.Router) { common.UseSite(c, &p.SiteName, &p.SiteID, &p.SiteAll) if !common.IsAdminHasSiteAccess(c, p.SiteName) { - return common.RespError(c, "无权操作") + return common.RespError(c, i18n.T("Access denied")) } // 准备 query diff --git a/server/handler/admin_send_mail.go b/server/handler/admin_send_mail.go index 2f241fed5..202ae8919 100644 --- a/server/handler/admin_send_mail.go +++ b/server/handler/admin_send_mail.go @@ -2,6 +2,7 @@ package handler import ( "github.com/ArtalkJS/Artalk/internal/email" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/server/common" "github.com/gofiber/fiber/v2" ) @@ -21,7 +22,7 @@ func AdminSendMail(router fiber.Router) { } if !common.GetIsSuperAdmin(c) { - return common.RespError(c, "无权访问") + return common.RespError(c, i18n.T("Access denied")) } email.AsyncSendTo(p.Subject, p.Body, p.ToAddr) diff --git a/server/handler/admin_setting.go b/server/handler/admin_setting.go index b6b76f20c..42d96004f 100644 --- a/server/handler/admin_setting.go +++ b/server/handler/admin_setting.go @@ -5,6 +5,7 @@ import ( "github.com/ArtalkJS/Artalk/internal/config" "github.com/ArtalkJS/Artalk/internal/core" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/server/common" "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" @@ -14,12 +15,12 @@ import ( func AdminSettingGet(router fiber.Router) { router.Post("/setting-get", func(c *fiber.Ctx) error { if !common.GetIsSuperAdmin(c) { - return common.RespError(c, "无权访问") + return common.RespError(c, i18n.T("Access denied")) } dat, err := os.ReadFile(config.GetCfgFileLoaded()) if err != nil { - return common.RespError(c, "配置文件读取失败") + return common.RespError(c, i18n.T("Config file read failed")) } return common.RespData(c, string(dat)) @@ -34,7 +35,7 @@ type ParamsAdminSettingSave struct { func AdminSettingSave(router fiber.Router) { router.Post("/setting-save", func(c *fiber.Ctx) error { if !common.GetIsSuperAdmin(c) { - return common.RespError(c, "无权访问") + return common.RespError(c, i18n.T("Access denied")) } var p ParamsAdminSettingSave @@ -45,24 +46,24 @@ func AdminSettingSave(router fiber.Router) { configFile := config.GetCfgFileLoaded() f, err := os.Create(configFile) if err != nil { - return common.RespError(c, "配置文件读取失败,"+err.Error()) + return common.RespError(c, i18n.T("Config file read failed")+": "+err.Error()) } defer f.Close() _, err2 := f.WriteString(p.Data) if err2 != nil { - return common.RespError(c, "保存失败,"+err2.Error()) + return common.RespError(c, i18n.T("Save failed")+": "+err2.Error()) } // 重启服务 workDir, err3 := os.Getwd() if err3 != nil { - return common.RespError(c, "工作路径获取失败,"+err3.Error()) + return common.RespError(c, i18n.T("Working directory retrieval failed")+": "+err3.Error()) } core.LoadCore(configFile, workDir) common.ReloadCorsAllowOrigins() // 刷新 CORS 可信域名 - logrus.Info("服务已重启完毕") + logrus.Info(i18n.T("Services restart complete")) return common.RespSuccess(c) }) diff --git a/server/handler/admin_site_add.go b/server/handler/admin_site_add.go index 27f9dd8c8..107e9feb6 100644 --- a/server/handler/admin_site_add.go +++ b/server/handler/admin_site_add.go @@ -3,6 +3,7 @@ package handler import ( "github.com/ArtalkJS/Artalk/internal/config" "github.com/ArtalkJS/Artalk/internal/entity" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/internal/utils" "github.com/ArtalkJS/Artalk/server/common" @@ -23,24 +24,24 @@ func AdminSiteAdd(router fiber.Router) { } if !common.GetIsSuperAdmin(c) { - return common.RespError(c, "禁止创建站点") + return common.RespError(c, i18n.T("Access denied")) } if p.Urls != "" { urls := utils.SplitAndTrimSpace(p.Urls, ",") for _, url := range urls { if !utils.ValidateURL(url) { - return common.RespError(c, "Invalid url exist") + return common.RespError(c, i18n.T("Contains invalid URL")) } } } if p.Name == config.ATK_SITE_ALL { - return common.RespError(c, "禁止使用保留关键字作为名称") + return common.RespError(c, "Prohibit the use of reserved keywords as names") } if !query.FindSite(p.Name).IsEmpty() { - return common.RespError(c, "site 已存在") + return common.RespError(c, i18n.T("{{name}} already exists", Map{"name": i18n.T("Site")})) } site := entity.Site{} @@ -48,7 +49,7 @@ func AdminSiteAdd(router fiber.Router) { site.Urls = p.Urls err := query.CreateSite(&site) if err != nil { - return common.RespError(c, "site 创建失败") + return common.RespError(c, i18n.T("{{name}} creation failed", Map{"name": i18n.T("Site")})) } // 刷新 CORS 可信域名 diff --git a/server/handler/admin_site_del.go b/server/handler/admin_site_del.go index b0dbd5d90..ed41fd96a 100644 --- a/server/handler/admin_site_del.go +++ b/server/handler/admin_site_del.go @@ -1,6 +1,7 @@ package handler import ( + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/server/common" "github.com/gofiber/fiber/v2" @@ -19,17 +20,17 @@ func AdminSiteDel(router fiber.Router) { } if !common.GetIsSuperAdmin(c) { - return common.RespError(c, "禁止删除站点") + return common.RespError(c, i18n.T("Access denied")) } site := query.FindSiteByID(p.ID) if site.IsEmpty() { - return common.RespError(c, "site 不存在") + return common.RespError(c, i18n.T("{{name}} not found", Map{"name": i18n.T("Site")})) } err := query.DelSite(&site) if err != nil { - return common.RespError(c, "site 删除失败") + return common.RespError(c, i18n.T("{{name}} deletion failed", Map{"name": i18n.T("Site")})) } // 刷新 CORS 可信域名 diff --git a/server/handler/admin_site_edit.go b/server/handler/admin_site_edit.go index bec4ef595..55c33adfb 100644 --- a/server/handler/admin_site_edit.go +++ b/server/handler/admin_site_edit.go @@ -6,6 +6,7 @@ import ( "github.com/ArtalkJS/Artalk/internal/cache" "github.com/ArtalkJS/Artalk/internal/db" "github.com/ArtalkJS/Artalk/internal/entity" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/internal/utils" "github.com/ArtalkJS/Artalk/server/common" @@ -31,29 +32,29 @@ func AdminSiteEdit(router fiber.Router) { site := query.FindSiteByID(p.ID) if site.IsEmpty() { - return common.RespError(c, "site 不存在") + return common.RespError(c, i18n.T("{{name}} not found", Map{"name": i18n.T("Site")})) } // 站点操作权限检查 if !common.IsAdminHasSiteAccess(c, site.Name) { - return common.RespError(c, "无权操作") + return common.RespError(c, i18n.T("Access denied")) } if strings.TrimSpace(p.Name) == "" { - return common.RespError(c, "site 名称不能为空白字符") + return common.RespError(c, i18n.T("{{name}} cannot be empty", Map{"name": "name"})) } // 重命名合法性检测 modifyName := p.Name != site.Name if modifyName && !query.FindSite(p.Name).IsEmpty() { - return common.RespError(c, "site 已存在,请换个名称") + return common.RespError(c, i18n.T("{{name}} already exists", Map{"name": i18n.T("Site")})) } // urls 合法性检测 if p.Urls != "" { for _, url := range utils.SplitAndTrimSpace(p.Urls, ",") { if !utils.ValidateURL(url) { - return common.RespError(c, "Invalid url exist") + return common.RespError(c, i18n.T("Contains invalid URL")) } } } @@ -85,7 +86,7 @@ func AdminSiteEdit(router fiber.Router) { err := query.UpdateSite(&site) if err != nil { - return common.RespError(c, "site 保存失败") + return common.RespError(c, i18n.T("{{name}} save failed", Map{"name": i18n.T("Site")})) } // 刷新 CORS 可信域名 diff --git a/server/handler/admin_transfer.go b/server/handler/admin_transfer.go index 722978220..16c46e372 100644 --- a/server/handler/admin_transfer.go +++ b/server/handler/admin_transfer.go @@ -7,6 +7,7 @@ import ( "os" "github.com/ArtalkJS/Artalk/internal/artransfer" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/internal/utils" "github.com/ArtalkJS/Artalk/server/common" @@ -34,7 +35,7 @@ func adminImport(c *fiber.Ctx) error { var payloadMapRaw map[string]interface{} err := json.Unmarshal([]byte(p.Payload), &payloadMapRaw) if err != nil { - return common.RespError(c, "payload 解析错误", common.Map{ + return common.RespError(c, "Payload parsing error", common.Map{ "error": err, }) } @@ -53,10 +54,10 @@ func adminImport(c *fiber.Ctx) error { user := common.GetUserByReq(c) if sitName, isExist := payloadMap["t_name"]; isExist { if !utils.ContainsStr(query.CookUser(&user).SiteNames, sitName) { - return common.RespError(c, "禁止导入的目标站点名") + return common.RespError(c, "Destination site name of prohibited import") } } else { - return common.RespError(c, "请填写目标站点名") + return common.RespError(c, "Please fill in the target site name") } } @@ -92,14 +93,14 @@ func adminImportUpload(c *fiber.Ctx) error { file, err := c.FormFile("file") if err != nil { logrus.Error(err) - return common.RespError(c, "文件获取失败") + return common.RespError(c, "File read failed") } // 打开文件 src, err := file.Open() if err != nil { logrus.Error(err) - return common.RespError(c, "文件打开失败") + return common.RespError(c, "File open failed") } defer src.Close() @@ -107,13 +108,13 @@ func adminImportUpload(c *fiber.Ctx) error { buf, err := io.ReadAll(src) if err != nil { logrus.Error(err) - return common.RespError(c, "文件读取失败") + return common.RespError(c, "File read failed") } tmpFile, err := os.CreateTemp("", "artalk-import-file-") if err != nil { logrus.Error(err) - return common.RespError(c, "临时文件创建失败") + return common.RespError(c, "tmp file creation failed") } tmpFile.Write(buf) @@ -134,7 +135,7 @@ func adminExport(c *fiber.Ctx) error { return db }) if err != nil { - common.RespError(c, "导出错误", common.Map{ + common.RespError(c, i18n.T("Export error"), common.Map{ "err": err, }) } diff --git a/server/handler/admin_user_add.go b/server/handler/admin_user_add.go index e64b122ee..ca4765644 100644 --- a/server/handler/admin_user_add.go +++ b/server/handler/admin_user_add.go @@ -2,6 +2,7 @@ package handler import ( "github.com/ArtalkJS/Artalk/internal/entity" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/internal/utils" "github.com/ArtalkJS/Artalk/server/common" @@ -25,7 +26,7 @@ type ParamsAdminUserAdd struct { func AdminUserAdd(router fiber.Router) { router.Post("/user-add", func(c *fiber.Ctx) error { if !common.GetIsSuperAdmin(c) { - return common.RespError(c, "无权操作") + return common.RespError(c, i18n.T("Access denied")) } var p ParamsAdminUserAdd @@ -34,14 +35,14 @@ func AdminUserAdd(router fiber.Router) { } if !query.FindUser(p.Name, p.Email).IsEmpty() { - return common.RespError(c, "用户已存在") + return common.RespError(c, i18n.T("{{name}} already exists", Map{"name": i18n.T("User")})) } if !utils.ValidateEmail(p.Email) { - return common.RespError(c, "Invalid email") + return common.RespError(c, i18n.T("Invalid {{name}}", Map{"name": i18n.T("Email")})) } if p.Link != "" && !utils.ValidateURL(p.Link) { - return common.RespError(c, "Invalid link") + return common.RespError(c, i18n.T("Invalid {{name}}", Map{"name": i18n.T("Link")})) } user := entity.User{} @@ -58,13 +59,13 @@ func AdminUserAdd(router fiber.Router) { err := user.SetPasswordEncrypt(p.Password) if err != nil { logrus.Errorln(err) - return common.RespError(c, "密码设置失败") + return common.RespError(c, i18n.T("Password update failed")) } } err := query.CreateUser(&user) if err != nil { - return common.RespError(c, "user 创建失败") + return common.RespError(c, i18n.T("{{name}} creation failed", Map{"name": i18n.T("User")})) } return common.RespData(c, common.Map{ diff --git a/server/handler/admin_user_del.go b/server/handler/admin_user_del.go index 6fb202773..2a0f292b1 100644 --- a/server/handler/admin_user_del.go +++ b/server/handler/admin_user_del.go @@ -1,6 +1,7 @@ package handler import ( + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/server/common" "github.com/gofiber/fiber/v2" @@ -14,7 +15,7 @@ type ParamsAdminUserDel struct { func AdminUserDel(router fiber.Router) { router.Post("/user-del", func(c *fiber.Ctx) error { if !common.GetIsSuperAdmin(c) { - return common.RespError(c, "无权操作") + return common.RespError(c, i18n.T("Access denied")) } var p ParamsAdminUserDel @@ -24,12 +25,12 @@ func AdminUserDel(router fiber.Router) { user := query.FindUserByID(p.ID) if user.IsEmpty() { - return common.RespError(c, "user 不存在") + return common.RespError(c, i18n.T("{{name}} not found", Map{"name": i18n.T("User")})) } err := query.DelUser(&user) if err != nil { - return common.RespError(c, "user 删除失败") + return common.RespError(c, i18n.T("{{name}} deletion failed", Map{"name": i18n.T("User")})) } return common.RespSuccess(c) }) diff --git a/server/handler/admin_user_edit.go b/server/handler/admin_user_edit.go index 44a2bd89c..4fdebca04 100644 --- a/server/handler/admin_user_edit.go +++ b/server/handler/admin_user_edit.go @@ -2,6 +2,7 @@ package handler import ( "github.com/ArtalkJS/Artalk/internal/cache" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/internal/utils" "github.com/ArtalkJS/Artalk/server/common" @@ -28,7 +29,7 @@ type ParamsAdminUserEdit struct { func AdminUserEdit(router fiber.Router) { router.Post("/user-edit", func(c *fiber.Ctx) error { if !common.GetIsSuperAdmin(c) { - return common.RespError(c, "无权操作") + return common.RespError(c, i18n.T("Access denied")) } var p ParamsAdminUserEdit @@ -38,7 +39,7 @@ func AdminUserEdit(router fiber.Router) { user := query.FindUserByID(p.ID) if user.IsEmpty() { - return common.RespError(c, "user 不存在") + return common.RespError(c, i18n.T("{{name}} not found", Map{"name": i18n.T("User")})) } // 改名名合法性检测 @@ -46,14 +47,14 @@ func AdminUserEdit(router fiber.Router) { modifyEmail := p.Email != user.Email if modifyName && modifyEmail && !query.FindUser(p.Name, p.Email).IsEmpty() { - return common.RespError(c, "user 已存在,请更换用户名和邮箱") + return common.RespError(c, i18n.T("{{name}} already exists", Map{"name": i18n.T("User")})) } if !utils.ValidateEmail(p.Email) { - return common.RespError(c, "Invalid email") + return common.RespError(c, i18n.T("Invalid {{name}}", Map{"name": i18n.T("Email")})) } if p.Link != "" && !utils.ValidateURL(p.Link) { - return common.RespError(c, "Invalid link") + return common.RespError(c, i18n.T("Invalid {{name}}", Map{"name": i18n.T("Link")})) } // 删除原有缓存 @@ -74,7 +75,7 @@ func AdminUserEdit(router fiber.Router) { err := query.UpdateUser(&user) if err != nil { - return common.RespError(c, "user 保存失败") + return common.RespError(c, i18n.T("{{name}} save failed", Map{"name": i18n.T("User")})) } return common.RespData(c, common.Map{ diff --git a/server/handler/admin_user_get.go b/server/handler/admin_user_get.go index 6a292c174..9f60093f8 100644 --- a/server/handler/admin_user_get.go +++ b/server/handler/admin_user_get.go @@ -3,6 +3,7 @@ package handler import ( "github.com/ArtalkJS/Artalk/internal/db" "github.com/ArtalkJS/Artalk/internal/entity" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/server/common" "github.com/gofiber/fiber/v2" @@ -23,7 +24,7 @@ type ResponseAdminUserGet struct { func AdminUserGet(router fiber.Router) { router.Post("/user-get", func(c *fiber.Ctx) error { if !common.GetIsSuperAdmin(c) { - return common.RespError(c, "无权操作") + return common.RespError(c, i18n.T("Access denied")) } var p ParamsAdminUserGet diff --git a/server/handler/admin_vote_sync.go b/server/handler/admin_vote_sync.go index 43eae10b4..d347d9766 100644 --- a/server/handler/admin_vote_sync.go +++ b/server/handler/admin_vote_sync.go @@ -1,6 +1,7 @@ package handler import ( + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/server/common" "github.com/gofiber/fiber/v2" @@ -18,7 +19,7 @@ func AdminVoteSync(router fiber.Router) { } if !common.GetIsSuperAdmin(c) { - return common.RespError(c, "无权访问") + return common.RespError(c, i18n.T("Access denied")) } query.VoteSync() diff --git a/server/handler/captcha.go b/server/handler/captcha.go index 536e74c02..b61b95c72 100644 --- a/server/handler/captcha.go +++ b/server/handler/captcha.go @@ -8,6 +8,7 @@ import ( "github.com/ArtalkJS/Artalk/internal/captcha" "github.com/ArtalkJS/Artalk/internal/config" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/server/common" "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" @@ -16,7 +17,7 @@ import ( func Captcha(router fiber.Router) { ca := router.Group("/captcha/", func(c *fiber.Ctx) error { if !config.Instance.Captcha.Enabled { - return common.RespError(c, "验证码功能已关闭") + return common.RespError(c, "Captcha disabled") } return c.Next() }) @@ -87,8 +88,8 @@ func captchaCheck(c *fiber.Ctx) error { if config.Instance.Captcha.Geetest.Enabled { isPass, reason, err := captcha.GeetestCheck(inputVal) if err != nil { - logrus.Error("[Geetest] 验证发生错误 ", err) - return common.RespError(c, "Geetest API 错误") + logrus.Error("[Geetest] Failed to verify: ", err) + return common.RespError(c, "Geetest API error") } if isPass { @@ -98,7 +99,7 @@ func captchaCheck(c *fiber.Ctx) error { } else { // 验证失败 common.OnCaptchaFail(c) - return common.RespError(c, "验证失败", common.Map{ + return common.RespError(c, i18n.T("Verification failed"), common.Map{ "reason": reason, }) } @@ -117,7 +118,7 @@ func captchaCheck(c *fiber.Ctx) error { // 验证码错误 common.DisposeImageCaptcha(ip) common.OnCaptchaFail(c) - return common.RespError(c, "验证码错误", common.Map{ + return common.RespError(c, i18n.T("Wrong captcha"), common.Map{ "img_data": common.GetNewImageCaptchaBase64(ip), }) } diff --git a/server/handler/comment_add.go b/server/handler/comment_add.go index d2ce6f086..9822a19ef 100644 --- a/server/handler/comment_add.go +++ b/server/handler/comment_add.go @@ -6,6 +6,7 @@ import ( "github.com/ArtalkJS/Artalk/internal/anti_spam" "github.com/ArtalkJS/Artalk/internal/config" "github.com/ArtalkJS/Artalk/internal/entity" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/notify_launcher" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/internal/utils" @@ -43,17 +44,17 @@ func CommentAdd(router fiber.Router) { } if strings.TrimSpace(p.Name) == "" { - return common.RespError(c, "昵称不能为空") + return common.RespError(c, i18n.T("{{name}} cannot be empty", Map{"name": i18n.T("Nickname")})) } if strings.TrimSpace(p.Email) == "" { - return common.RespError(c, "邮箱不能为空") + return common.RespError(c, i18n.T("{{name}} cannot be empty", Map{"name": i18n.T("Email")})) } if !utils.ValidateEmail(p.Email) { - return common.RespError(c, "Invalid email") + return common.RespError(c, i18n.T("Invalid {{name}}", Map{"name": i18n.T("Email")})) } if p.Link != "" && !utils.ValidateURL(p.Link) { - return common.RespError(c, "Invalid link") + return common.RespError(c, i18n.T("Invalid {{name}}", Map{"name": i18n.T("Link")})) } ip := c.IP() @@ -83,13 +84,13 @@ func CommentAdd(router fiber.Router) { if p.Rid != 0 { parentComment = query.FindComment(p.Rid) if parentComment.IsEmpty() { - return common.RespError(c, "找不到父评论") + return common.RespError(c, i18n.T("{{name}} not found", Map{"name": i18n.T("Parent comment")})) } if parentComment.PageKey != p.PageKey { - return common.RespError(c, "与父评论的 pageKey 不一致") + return common.RespError(c, "Inconsistent with the page_key of the parent comment") } if !parentComment.IsAllowReply() { - return common.RespError(c, "不允许回复该评论") + return common.RespError(c, i18n.T("Cannot reply to this comment")) } } @@ -97,7 +98,7 @@ func CommentAdd(router fiber.Router) { user := query.FindCreateUser(p.Name, p.Email, p.Link) if user.ID == 0 || page.Key == "" { logrus.Error("Cannot get user or page") - return common.RespError(c, "评论失败") + return common.RespError(c, i18n.T("Comment failed")) } // update user @@ -134,7 +135,7 @@ func CommentAdd(router fiber.Router) { err := query.CreateComment(&comment) if err != nil { logrus.Error("Save Comment error: ", err) - return common.RespError(c, "评论失败") + return common.RespError(c, i18n.T("Comment failed")) } // 异步执行 diff --git a/server/handler/img_upload.go b/server/handler/img_upload.go index 2b000e398..ae8bb7eb7 100644 --- a/server/handler/img_upload.go +++ b/server/handler/img_upload.go @@ -12,6 +12,7 @@ import ( "time" "github.com/ArtalkJS/Artalk/internal/config" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/utils" "github.com/ArtalkJS/Artalk/server/common" "github.com/gofiber/fiber/v2" @@ -36,7 +37,7 @@ func ImgUpload(router fiber.Router) { router.Post("/img-upload", func(c *fiber.Ctx) error { // 功能开关 (管理员始终开启) if !config.Instance.ImgUpload.Enabled && !common.CheckIsAdminReq(c) { - return common.RespError(c, "禁止图片上传", common.Map{ + return common.RespError(c, i18n.T("Image upload forbidden"), common.Map{ "img_upload_enabled": false, }) } @@ -48,7 +49,7 @@ func ImgUpload(router fiber.Router) { } if !utils.ValidateEmail(p.Email) { - return common.RespError(c, "Invalid email") + return common.RespError(c, i18n.T("Invalid {{name}}", Map{"name": i18n.T("Email")})) } // use site @@ -65,7 +66,9 @@ func ImgUpload(router fiber.Router) { // 图片大小限制 (Based on content length) if config.Instance.ImgUpload.MaxSize != 0 { if int64(c.Request().Header.ContentLength()) > config.Instance.ImgUpload.MaxSize*1024*1024 { - return common.RespError(c, fmt.Sprintf("图片大小超过限制 %dMB", config.Instance.ImgUpload.MaxSize)) + return common.RespError(c, i18n.T("Image exceeds {{file_size}} limit", Map{ + "file_size": fmt.Sprintf("%dMB", config.Instance.ImgUpload.MaxSize), + })) } } @@ -73,14 +76,14 @@ func ImgUpload(router fiber.Router) { file, err := c.FormFile("file") if err != nil { logrus.Error(err) - return common.RespError(c, "文件获取失败") + return common.RespError(c, "File read failed") } // 打开文件 src, err := file.Open() if err != nil { logrus.Error(err) - return common.RespError(c, "文件打开失败") + return common.RespError(c, "File open failed") } defer src.Close() @@ -88,13 +91,15 @@ func ImgUpload(router fiber.Router) { buf, err := io.ReadAll(src) if err != nil { logrus.Error(err) - return common.RespError(c, "文件读取失败") + return common.RespError(c, "File read failed") } // 大小限制 (Based on content read) if config.Instance.ImgUpload.MaxSize != 0 { if int64(len(buf)) > config.Instance.ImgUpload.MaxSize*1024*1024 { - return common.RespError(c, fmt.Sprintf("图片大小超过限制 %dMB", config.Instance.ImgUpload.MaxSize)) + return common.RespError(c, i18n.T("Image exceeds {{file_size}} limit", Map{ + "file_size": fmt.Sprintf("%dMB", config.Instance.ImgUpload.MaxSize), + })) } } @@ -107,7 +112,7 @@ func ImgUpload(router fiber.Router) { // "image/svg+xml", } if !utils.ContainsStr(allowMines, fileMine) { - return common.RespError(c, "不支持的格式") + return common.RespError(c, i18n.T("Unsupported formats")) } // 图片文件名 @@ -126,21 +131,21 @@ func ImgUpload(router fiber.Router) { // 创建图片目标文件 if err := utils.EnsureDir(config.Instance.ImgUpload.Path); err != nil { logrus.Error(err) - return common.RespError(c, "创建图片存放文件夹失败") + return common.RespError(c, "Folder creation failed") } fileFullPath := strings.TrimSuffix(config.Instance.ImgUpload.Path, "/") + "/" + filename dst, err := os.Create(fileFullPath) if err != nil { logrus.Error(err) - return common.RespError(c, "图片文件创建失败") + return common.RespError(c, "File creation failed") } defer dst.Close() // 写入图片文件 if _, err = dst.Write(buf); err != nil { logrus.Error(err) - return common.RespError(c, "图片文件写入失败") + return common.RespError(c, "File write failed") } // 生成外部可访问链接 @@ -161,7 +166,7 @@ func ImgUpload(router fiber.Router) { } logrus.Error("[IMG_UPLOAD] [upgit] upgit output: ", upgitURL) - return common.RespError(c, "图片通过 upgit 上传失败") + return common.RespError(c, i18n.T("Upload image via {{method}} failed", Map{"method": "upgit"})) } // 上传成功,删除本地文件 diff --git a/server/handler/mark_read.go b/server/handler/mark_read.go index 81e273bf7..5ac2587ce 100644 --- a/server/handler/mark_read.go +++ b/server/handler/mark_read.go @@ -1,6 +1,7 @@ package handler import ( + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/server/common" "github.com/gofiber/fiber/v2" @@ -32,7 +33,7 @@ func MarkRead(router fiber.Router) { // all read if p.AllRead { if p.Name == "" || p.Email == "" { - return common.RespError(c, "need username and email") + return common.RespError(c, "username or email cannot be empty") } user := query.FindUser(p.Name, p.Email) @@ -47,7 +48,7 @@ func MarkRead(router fiber.Router) { // find notify notify := query.FindNotifyByKey(p.NotifyKey) if notify.IsEmpty() { - return common.RespError(c, "notify key is wrong") + return common.RespError(c, i18n.T("{{name}} not found", Map{"name": i18n.T("Notify")})) } if notify.IsRead { @@ -57,7 +58,7 @@ func MarkRead(router fiber.Router) { // update notify err := query.NotifySetRead(¬ify) if err != nil { - return common.RespError(c, "notify save error") + return common.RespError(c, i18n.T("{{name}} save failed", Map{"name": i18n.T("Notify")})) } return common.RespSuccess(c) diff --git a/server/handler/stat.go b/server/handler/stat.go index c6f13560b..d042d790f 100644 --- a/server/handler/stat.go +++ b/server/handler/stat.go @@ -4,6 +4,7 @@ import ( "github.com/ArtalkJS/Artalk/internal/config" "github.com/ArtalkJS/Artalk/internal/db" "github.com/ArtalkJS/Artalk/internal/entity" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/internal/utils" "github.com/ArtalkJS/Artalk/server/common" @@ -159,6 +160,6 @@ func Stat(router fiber.Router) { return common.RespData(c, query.CookAllPages(pages)) } - return common.RespError(c, "invalid type") + return common.RespError(c, i18n.T("Invalid {{name}}", Map{"name": i18n.T("Type")})) }) } diff --git a/server/handler/user_login.go b/server/handler/user_login.go index 3259ecf34..7f370f97d 100644 --- a/server/handler/user_login.go +++ b/server/handler/user_login.go @@ -8,6 +8,7 @@ import ( "github.com/ArtalkJS/Artalk/internal/config" "github.com/ArtalkJS/Artalk/internal/entity" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/internal/utils" "github.com/ArtalkJS/Artalk/server/common" @@ -34,7 +35,7 @@ func UserLogin(router fiber.Router) { if p.Name == "" { // 仅 Email 的查询 if !utils.ValidateEmail(p.Email) { - return common.RespError(c, "请输入正确的邮箱") + return common.RespError(c, i18n.T("Invalid {{name}}", Map{"name": i18n.T("Email")})) } users := query.FindUsersByEmail(p.Email) if len(users) == 1 { @@ -46,7 +47,7 @@ func UserLogin(router fiber.Router) { for _, u := range users { userNames = append(userNames, u.Name) } - return common.RespError(c, "需要选择用户名", common.Map{ + return common.RespError(c, "Need to select username", common.Map{ // 前端需做处理让用户选择用户名, // 之后再发起带 name 参数的请求 "need_name_select": userNames, @@ -61,7 +62,7 @@ func UserLogin(router fiber.Router) { common.RecordAction(c) if user.IsEmpty() { - return common.RespError(c, "验证失败") + return common.RespError(c, i18n.T("Login failed")) } // 密码验证 @@ -90,7 +91,7 @@ func UserLogin(router fiber.Router) { } if !passwordOK { - return common.RespError(c, "验证失败") + return common.RespError(c, i18n.T("Login failed")) } jwtToken := common.LoginGetUserToken(user) diff --git a/server/handler/user_logout.go b/server/handler/user_logout.go index 6a0979eab..3fdb18da8 100644 --- a/server/handler/user_logout.go +++ b/server/handler/user_logout.go @@ -13,11 +13,11 @@ func UserLogout(router fiber.Router) { router.Post("/logout", func(c *fiber.Ctx) error { if !config.Instance.Cookie.Enabled { - return common.RespError(c, "API 未启用 Cookie") + return common.RespError(c, "API cookie disabled") } if common.GetJwtStrByReqCookie(c) == "" { - return common.RespError(c, "未登录,无需注销") + return common.RespError(c, "Not logged in yet, no need to log out") } // same as login, remove cookie diff --git a/server/handler/utils.go b/server/handler/utils.go new file mode 100644 index 000000000..bf47a5221 --- /dev/null +++ b/server/handler/utils.go @@ -0,0 +1,3 @@ +package handler + +type Map = map[string]interface{} diff --git a/server/handler/vote.go b/server/handler/vote.go index 3709ab9e4..5be1d4a66 100644 --- a/server/handler/vote.go +++ b/server/handler/vote.go @@ -5,6 +5,7 @@ import ( "github.com/ArtalkJS/Artalk/internal/db" "github.com/ArtalkJS/Artalk/internal/entity" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/internal/query" "github.com/ArtalkJS/Artalk/server/common" "github.com/gofiber/fiber/v2" @@ -60,12 +61,12 @@ func Vote(router fiber.Router) { case isVoteComment: comment = query.FindComment(p.TargetID) if comment.IsEmpty() { - return common.RespError(c, "comment not found") + return common.RespError(c, i18n.T("{{name}} not found", Map{"name": i18n.T("Comment")})) } case isVotePage: page = query.FindPageByID(p.TargetID) if page.IsEmpty() { - return common.RespError(c, "page not found") + return common.RespError(c, i18n.T("{{name}} not found", Map{"name": i18n.T("Page")})) } default: return common.RespError(c, "unknown type") diff --git a/server/middleware/admin.go b/server/middleware/admin.go index ca90d127b..798e7f76d 100644 --- a/server/middleware/admin.go +++ b/server/middleware/admin.go @@ -1,6 +1,7 @@ package middleware import ( + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/server/common" "github.com/gofiber/fiber/v2" ) @@ -8,7 +9,7 @@ import ( func AdminOnlyMiddleware() fiber.Handler { return func(c *fiber.Ctx) error { if !common.CheckIsAdminReq(c) { - return common.RespError(c, "需要验证管理员身份", common.Map{"need_login": true}) + return common.RespError(c, i18n.T("Admin access required"), common.Map{"need_login": true}) } return c.Next() diff --git a/server/middleware/limit.go b/server/middleware/limit.go index 7e90c0e37..d16d3b35c 100644 --- a/server/middleware/limit.go +++ b/server/middleware/limit.go @@ -4,6 +4,7 @@ import ( "path" "github.com/ArtalkJS/Artalk/internal/config" + "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/server/common" "github.com/gofiber/fiber/v2" ) @@ -74,7 +75,7 @@ func ActionLimitMiddleware(conf ActionLimitConf) fiber.Handler { respData["img_data"] = common.GetNewImageCaptchaBase64(c.IP()) } - return common.RespError(c, "需要验证码", respData) + return common.RespError(c, i18n.T("Captcha required"), respData) } // 放行 diff --git a/server/middleware/site_origin.go b/server/middleware/site_origin.go index 29fc491d5..644ea7f95 100644 --- a/server/middleware/site_origin.go +++ b/server/middleware/site_origin.go @@ -1,7 +1,6 @@ package middleware import ( - "fmt" "net/url" "path" "strings" @@ -41,7 +40,7 @@ func SiteOriginMiddleware() fiber.Handler { // 请求站点名 == "__ATK_SITE_ALL" 时取消站点隔离 if siteName == config.ATK_SITE_ALL { if !isSuperAdmin { - return common.RespError(c, "仅管理员查询允许取消站点隔离") + return common.RespError(c, "Only admin can query sites with disable isolation") } siteAll = true @@ -56,9 +55,12 @@ func SiteOriginMiddleware() fiber.Handler { findSite := query.FindSite(siteName) if findSite.IsEmpty() { - return common.RespError(c, fmt.Sprintf("未找到站点:`%s`,请在控制台创建站点", siteName), common.Map{ - "err_no_site": true, - }) + return common.RespError(c, + i18n.T("Site `{{name}}` not found. Please create it in control center.", map[string]interface{}{"name": siteName}), + common.Map{ + "err_no_site": true, + }, + ) } site = &findSite siteID = findSite.ID @@ -105,7 +107,7 @@ func CheckOrigin(c *fiber.Ctx, allowSite *entity.Site) (bool, error) { // 从 Referer 获取 Origin referer := string(c.Request().Header.Referer()) if referer == "" { - return false, common.RespError(c, i18n.T("Invalid request")+", "+i18n.T("Unable to get Origin")) + return false, common.RespError(c, i18n.T("Invalid request")+", "+i18n.T("Unable to get `{{name}}`", map[string]interface{}{"name": "origin"})) } origin = referer } @@ -123,7 +125,7 @@ func CheckOrigin(c *fiber.Ctx, allowSite *entity.Site) (bool, error) { return true, nil } - return false, common.RespError(c, i18n.T("Invalid request")+", "+i18n.T("Please check trusted_domains config")) + return false, common.RespError(c, i18n.T("Invalid request. Please check your `trusted_domains` config.")) } // 判断 Origin 是否被允许 diff --git a/server/server.go b/server/server.go index 1af93b6c1..d691c8d43 100644 --- a/server/server.go +++ b/server/server.go @@ -125,7 +125,7 @@ func index(f fiber.Router) { func uploadedStatic(f fiber.Router) { if config.Instance.ImgUpload.Path == "" { config.Instance.ImgUpload.Path = "./data/artalk-img/" - logrus.Warn("图片上传功能 img_upload.path 未配置,使用默认值:" + config.Instance.ImgUpload.Path) + logrus.Warn("[Image Upload] img_upload.path is not configured, using the default value: " + config.Instance.ImgUpload.Path) } // 图片上传静态资源可访问路径