From 1d4aa56dd662bf37936b4bff44b88a862ce6c25d Mon Sep 17 00:00:00 2001 From: krau <71133316+krau@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:27:03 +0800 Subject: [PATCH] feat: add i18n for import command --- client/bot/handlers/import.go | 45 +++++++++++++++++----------- common/i18n/i18nk/keys.go | 27 +++++++++++++++++ common/i18n/locale/en.yaml | 28 ++++++++++++++++++ common/i18n/locale/zh-Hans.yaml | 28 ++++++++++++++++++ core/tasks/batchimport/progress.go | 47 +++++++++++++++++++----------- 5 files changed, 141 insertions(+), 34 deletions(-) diff --git a/client/bot/handlers/import.go b/client/bot/handlers/import.go index c9482d9..5e4fad2 100644 --- a/client/bot/handlers/import.go +++ b/client/bot/handlers/import.go @@ -9,6 +9,8 @@ import ( "github.com/celestix/gotgproto/ext" "github.com/charmbracelet/log" "github.com/gotd/td/tg" + "github.com/krau/SaveAny-Bot/common/i18n" + "github.com/krau/SaveAny-Bot/common/i18n/i18nk" "github.com/krau/SaveAny-Bot/common/utils/tgutil" "github.com/krau/SaveAny-Bot/config" storconfig "github.com/krau/SaveAny-Bot/config/storage" @@ -24,11 +26,7 @@ func handleImportCmd(ctx *ext.Context, update *ext.Update) error { args := strings.Split(update.EffectiveMessage.Text, " ") if len(args) < 3 { - ctx.Reply(update, ext.ReplyTextString("用法: /import [target_chat_id] [filter]\n\n"+ - "示例:\n"+ - "/import 本机1 /downloads\n"+ - "/import MyAlist /media/photos -1001234567890\n"+ - "/import MyLocal /backup \".*\\\\.mp4$\""), nil) + ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgImportUsage, nil)), nil) return dispatcher.EndGroups } @@ -41,33 +39,42 @@ func handleImportCmd(ctx *ext.Context, update *ext.Update) error { stor, err := storage.GetStorageByUserIDAndName(ctx, userID, storageName) if err != nil { logger.Errorf("Failed to get storage by user ID and name: %s", err) - ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("存储端 '%s' 不存在或您无权访问: %v", storageName, err)), nil) + ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgImportErrorStorageNotFound, map[string]any{ + "StorageName": storageName, + "Error": err, + })), nil) return dispatcher.EndGroups } // 2. 检查是否支持列举 listable, ok := stor.(storage.StorageListable) if !ok { - ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("存储端 '%s' 不支持列举文件功能", storageName)), nil) + ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgImportErrorStorageNotListable, map[string]any{ + "StorageName": storageName, + })), nil) return dispatcher.EndGroups } // 3. 检查是否支持读取 _, ok = stor.(storage.StorageReadable) if !ok { - ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("存储端 '%s' 不支持读取文件功能", storageName)), nil) + ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgImportErrorStorageNotReadable, map[string]any{ + "StorageName": storageName, + })), nil) return dispatcher.EndGroups } // 4. 获取目标 Telegram 存储 telegramStorage, err := storage.GetTelegramStorageByUserID(ctx, userID) if err != nil { - ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("未找到可用的 Telegram 存储: %v", err)), nil) + ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgImportErrorNoTelegramStorage, map[string]any{ + "Error": err, + })), nil) return dispatcher.EndGroups } // 5. 列举目录文件 - replied, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件列表..."), nil) + replied, err := ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgImportInfoFetchingFiles, nil)), nil) if err != nil { logger.Errorf("Failed to reply: %s", err) return dispatcher.EndGroups @@ -77,7 +84,7 @@ func handleImportCmd(ctx *ext.Context, update *ext.Update) error { if err != nil { ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{ ID: replied.ID, - Message: fmt.Sprintf("获取文件列表失败: %v", err), + Message: i18n.T(i18nk.BotMsgImportErrorListFilesFailed, map[string]any{"Error": err}), }) return dispatcher.EndGroups } @@ -89,7 +96,7 @@ func handleImportCmd(ctx *ext.Context, update *ext.Update) error { if err != nil { ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{ ID: replied.ID, - Message: fmt.Sprintf("正则表达式无效: %v", err), + Message: i18n.T(i18nk.BotMsgImportErrorInvalidRegex, map[string]any{"Error": err}), }) return dispatcher.EndGroups } @@ -109,7 +116,7 @@ func handleImportCmd(ctx *ext.Context, update *ext.Update) error { if len(filteredFiles) == 0 { ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{ ID: replied.ID, - Message: "目录中没有可导入的文件", + Message: i18n.T(i18nk.BotMsgImportErrorNoFilesToImport, nil), }) return dispatcher.EndGroups } @@ -128,7 +135,7 @@ func handleImportCmd(ctx *ext.Context, update *ext.Update) error { if err != nil { ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{ ID: replied.ID, - Message: fmt.Sprintf("无效的 Chat ID: %v", err), + Message: i18n.T(i18nk.BotMsgImportErrorInvalidChatId, map[string]any{"Error": err}), }) return dispatcher.EndGroups } @@ -138,7 +145,7 @@ func handleImportCmd(ctx *ext.Context, update *ext.Update) error { if targetChatID == 0 { ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{ ID: replied.ID, - Message: "未指定目标频道 ID,且 Telegram 存储未配置默认 chat_id", + Message: i18n.T(i18nk.BotMsgImportErrorNoTargetChatId, nil), }) return dispatcher.EndGroups } @@ -166,14 +173,18 @@ func handleImportCmd(ctx *ext.Context, update *ext.Update) error { if err := core.AddTask(injectCtx, task); err != nil { ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{ ID: replied.ID, - Message: fmt.Sprintf("添加任务失败: %v", err), + Message: i18n.T(i18nk.BotMsgImportErrorAddTaskFailed, map[string]any{"Error": err}), }) return dispatcher.EndGroups } ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{ ID: replied.ID, - Message: fmt.Sprintf("✅ 已添加 %d 个文件到导入队列\n总大小: %.2f MB\n任务 ID: %s", len(elems), float64(totalSize)/(1024*1024), taskID), + Message: i18n.T(i18nk.BotMsgImportInfoTaskAdded, map[string]any{ + "Count": len(elems), + "SizeMB": fmt.Sprintf("%.2f", float64(totalSize)/(1024*1024)), + "TaskID": taskID, + }), }) return dispatcher.EndGroups diff --git a/common/i18n/i18nk/keys.go b/common/i18n/i18nk/keys.go index 1edfa87..fc1a4ef 100644 --- a/common/i18n/i18nk/keys.go +++ b/common/i18n/i18nk/keys.go @@ -106,6 +106,19 @@ const ( BotMsgDlInfoFilesSelectStorage Key = "bot.msg.dl.info_files_select_storage" BotMsgDlUsage Key = "bot.msg.dl.usage" BotMsgHelpTextFmt Key = "bot.msg.help_text_fmt" + BotMsgImportErrorAddTaskFailed Key = "bot.msg.import.error_add_task_failed" + BotMsgImportErrorInvalidChatId Key = "bot.msg.import.error_invalid_chat_id" + BotMsgImportErrorInvalidRegex Key = "bot.msg.import.error_invalid_regex" + BotMsgImportErrorListFilesFailed Key = "bot.msg.import.error_list_files_failed" + BotMsgImportErrorNoFilesToImport Key = "bot.msg.import.error_no_files_to_import" + BotMsgImportErrorNoTargetChatId Key = "bot.msg.import.error_no_target_chat_id" + BotMsgImportErrorNoTelegramStorage Key = "bot.msg.import.error_no_telegram_storage" + BotMsgImportErrorStorageNotFound Key = "bot.msg.import.error_storage_not_found" + BotMsgImportErrorStorageNotListable Key = "bot.msg.import.error_storage_not_listable" + BotMsgImportErrorStorageNotReadable Key = "bot.msg.import.error_storage_not_readable" + BotMsgImportInfoFetchingFiles Key = "bot.msg.import.info_fetching_files" + BotMsgImportInfoTaskAdded Key = "bot.msg.import.info_task_added" + BotMsgImportUsage Key = "bot.msg.import.usage" BotMsgMediaGroupErrorBuildStorageSelectKeyboardFailed Key = "bot.msg.media_group.error_build_storage_select_keyboard_failed" BotMsgMediaGroupInfoGroupFoundFilesSelectStorage Key = "bot.msg.media_group.info_group_found_files_select_storage" BotMsgMediaGroupInfoSavingFiles Key = "bot.msg.media_group.info_saving_files" @@ -150,6 +163,20 @@ const ( BotMsgProgressFileProcessingPrefix Key = "bot.msg.progress.file_processing_prefix" BotMsgProgressFileSizePrefix Key = "bot.msg.progress.file_size_prefix" BotMsgProgressFileStartPrefix Key = "bot.msg.progress.file_start_prefix" + BotMsgProgressImportAvgSpeedPrefix Key = "bot.msg.progress.import_avg_speed_prefix" + BotMsgProgressImportElapsedTimePrefix Key = "bot.msg.progress.import_elapsed_time_prefix" + BotMsgProgressImportFailedFilesPrefix Key = "bot.msg.progress.import_failed_files_prefix" + BotMsgProgressImportFailedPrefix Key = "bot.msg.progress.import_failed_prefix" + BotMsgProgressImportProcessingMore Key = "bot.msg.progress.import_processing_more" + BotMsgProgressImportProcessingPrefix Key = "bot.msg.progress.import_processing_prefix" + BotMsgProgressImportProgressPrefix Key = "bot.msg.progress.import_progress_prefix" + BotMsgProgressImportRemainingTimePrefix Key = "bot.msg.progress.import_remaining_time_prefix" + BotMsgProgressImportSpeedPrefix Key = "bot.msg.progress.import_speed_prefix" + BotMsgProgressImportStartPrefix Key = "bot.msg.progress.import_start_prefix" + BotMsgProgressImportSuccessPrefix Key = "bot.msg.progress.import_success_prefix" + BotMsgProgressImportTotalFilesPrefix Key = "bot.msg.progress.import_total_files_prefix" + BotMsgProgressImportTotalSizePrefix Key = "bot.msg.progress.import_total_size_prefix" + BotMsgProgressImportUploadedPrefix Key = "bot.msg.progress.import_uploaded_prefix" BotMsgProgressParsedDonePrefix Key = "bot.msg.progress.parsed_done_prefix" BotMsgProgressParsedStartPrefix Key = "bot.msg.progress.parsed_start_prefix" BotMsgProgressProcessingListPrefix Key = "bot.msg.progress.processing_list_prefix" diff --git a/common/i18n/locale/en.yaml b/common/i18n/locale/en.yaml index 81db9b7..758a686 100644 --- a/common/i18n/locale/en.yaml +++ b/common/i18n/locale/en.yaml @@ -296,6 +296,20 @@ bot: info_urls_select_storage: "Found {{.Count}} links, please select storage" info_downloading: "Downloading via yt-dlp..." error_download_failed: "yt-dlp download failed: {{.Error}}" + import: + usage: "Usage: /import [target_chat_id] [filter]\n\nExamples:\n/import local1 /downloads\n/import MyAlist /media/photos -1001234567890\n/import MyLocal /backup \".*\\\\.mp4$\"" + error_storage_not_found: "Storage '{{.StorageName}}' not found or access denied: {{.Error}}" + error_storage_not_listable: "Storage '{{.StorageName}}' does not support listing files" + error_storage_not_readable: "Storage '{{.StorageName}}' does not support reading files" + error_no_telegram_storage: "No Telegram storage found: {{.Error}}" + info_fetching_files: "Fetching file list..." + error_list_files_failed: "Failed to list files: {{.Error}}" + error_invalid_regex: "Invalid regular expression: {{.Error}}" + error_no_files_to_import: "No files to import in directory" + error_invalid_chat_id: "Invalid Chat ID: {{.Error}}" + error_no_target_chat_id: "No target channel ID specified and Telegram storage has no default chat_id configured" + error_add_task_failed: "Failed to add task: {{.Error}}" + info_task_added: "Added {{.Count}} files to import queue\nTotal size: {{.SizeMB}} MB\nTask ID: {{.TaskID}}" cancel: usage: "Usage: /cancel " error_cancel_failed: "Failed to cancel task: {{.Error}}" @@ -344,6 +358,20 @@ bot: ytdlp_done: "yt-dlp download completed and transferred ({{.Count}} files)\n" downloaded_prefix: "\nDownloaded: " current_speed_prefix: "\nCurrent speed: " + import_start_prefix: "Importing: " + import_progress_prefix: "Import progress: " + import_uploaded_prefix: "\nUploaded: " + import_speed_prefix: "\nSpeed: " + import_remaining_time_prefix: "\nRemaining time: " + import_processing_prefix: "\nProcessing:\n" + import_processing_more: "...and {{.Count}} more files\n" + import_failed_prefix: "❌ Import failed\n" + import_success_prefix: "✅ Import completed\n" + import_total_files_prefix: "\nTotal files: " + import_total_size_prefix: "\nTotal size: " + import_elapsed_time_prefix: "\nElapsed time: " + import_avg_speed_prefix: "\nAverage speed: " + import_failed_files_prefix: "\nFailed files: " syncpeers: start: "Starting to sync peers..." done: "Peer sync completed, total {{.Count}} chats synced" diff --git a/common/i18n/locale/zh-Hans.yaml b/common/i18n/locale/zh-Hans.yaml index bc605e9..7e04c1d 100644 --- a/common/i18n/locale/zh-Hans.yaml +++ b/common/i18n/locale/zh-Hans.yaml @@ -297,6 +297,20 @@ bot: info_urls_select_storage: "共 {{.Count}} 个链接, 请选择存储位置" info_downloading: "正在通过 yt-dlp 下载..." error_download_failed: "yt-dlp 下载失败: {{.Error}}" + import: + usage: "用法: /import [target_chat_id] [filter]\n\n示例:\n/import 本机1 /downloads\n/import MyAlist /media/photos -1001234567890\n/import MyLocal /backup \".*\\\\.mp4$\"" + error_storage_not_found: "存储端 '{{.StorageName}}' 不存在或您无权访问: {{.Error}}" + error_storage_not_listable: "存储端 '{{.StorageName}}' 不支持列举文件功能" + error_storage_not_readable: "存储端 '{{.StorageName}}' 不支持读取文件功能" + error_no_telegram_storage: "未找到可用的 Telegram 存储: {{.Error}}" + info_fetching_files: "正在获取文件列表..." + error_list_files_failed: "获取文件列表失败: {{.Error}}" + error_invalid_regex: "正则表达式无效: {{.Error}}" + error_no_files_to_import: "目录中没有可导入的文件" + error_invalid_chat_id: "无效的 Chat ID: {{.Error}}" + error_no_target_chat_id: "未指定目标频道 ID,且 Telegram 存储未配置默认 chat_id" + error_add_task_failed: "添加任务失败: {{.Error}}" + info_task_added: "已添加 {{.Count}} 个文件到导入队列\n总大小: {{.SizeMB}} MB\n任务 ID: {{.TaskID}}" cancel: usage: "用法: /cancel " error_cancel_failed: "取消任务失败: {{.Error}}" @@ -345,6 +359,20 @@ bot: ytdlp_done: "yt-dlp 下载完成并已转存 ({{.Count}} 个文件)\n" downloaded_prefix: "\n已下载: " current_speed_prefix: "\n当前速度: " + import_start_prefix: "正在导入: " + import_progress_prefix: "导入进度: " + import_uploaded_prefix: "\n已上传: " + import_speed_prefix: "\n速度: " + import_remaining_time_prefix: "\n剩余时间: " + import_processing_prefix: "\n正在处理:\n" + import_processing_more: "...和其他 {{.Count}} 个文件\n" + import_failed_prefix: "导入失败\n" + import_success_prefix: "导入完成\n" + import_total_files_prefix: "\n总文件数: " + import_total_size_prefix: "\n总大小: " + import_elapsed_time_prefix: "\n耗时: " + import_avg_speed_prefix: "\n平均速度: " + import_failed_files_prefix: "\n失败文件数: " syncpeers: start: "正在同步对话列表..." success: "对话列表同步完成, 共同步 {{.Count}} 个对话" diff --git a/core/tasks/batchimport/progress.go b/core/tasks/batchimport/progress.go index 1ace812..73d7423 100644 --- a/core/tasks/batchimport/progress.go +++ b/core/tasks/batchimport/progress.go @@ -11,6 +11,8 @@ import ( "github.com/gotd/td/telegram/message/entity" "github.com/gotd/td/telegram/message/styling" "github.com/gotd/td/tg" + "github.com/krau/SaveAny-Bot/common/i18n" + "github.com/krau/SaveAny-Bot/common/i18n/i18nk" "github.com/krau/SaveAny-Bot/common/utils/dlutil" "github.com/krau/SaveAny-Bot/common/utils/tgutil" ) @@ -42,7 +44,7 @@ func (p *Progress) OnStart(ctx context.Context, info TaskInfo) { entityBuilder := entity.Builder{} if err := styling.Perform(&entityBuilder, - styling.Plain("正在导入: "), + styling.Plain(i18n.T(i18nk.BotMsgProgressImportStartPrefix, nil)), styling.Code(fmt.Sprintf("%.2f MB (%d个文件)", float64(info.TotalSize())/(1024*1024), info.Count())), ); err != nil { log.FromContext(ctx).Errorf("Failed to build entities: %s", err) @@ -86,28 +88,32 @@ func (p *Progress) OnProgress(ctx context.Context, info TaskInfo) { entityBuilder := entity.Builder{} var progressText strings.Builder - progressText.WriteString(fmt.Sprintf("导入进度: %d%%\n", percent)) - progressText.WriteString(fmt.Sprintf("已上传: %.2f MB / %.2f MB\n", + progressText.WriteString(i18n.T(i18nk.BotMsgProgressImportProgressPrefix, nil)) + progressText.WriteString(fmt.Sprintf("%d%%", percent)) + progressText.WriteString(i18n.T(i18nk.BotMsgProgressImportUploadedPrefix, nil)) + progressText.WriteString(fmt.Sprintf("%.2f MB / %.2f MB", float64(info.Uploaded())/(1024*1024), float64(info.TotalSize())/(1024*1024))) if p.start.Unix() > 0 { elapsed := time.Since(p.start) speed := float64(info.Uploaded()) / elapsed.Seconds() - progressText.WriteString(fmt.Sprintf("速度: %s/s\n", dlutil.FormatSize(int64(speed)))) + progressText.WriteString(i18n.T(i18nk.BotMsgProgressImportSpeedPrefix, nil)) + progressText.WriteString(dlutil.FormatSize(int64(speed)) + "/s") if info.Uploaded() > 0 { remaining := time.Duration(float64(info.TotalSize()-info.Uploaded()) / speed * float64(time.Second)) - progressText.WriteString(fmt.Sprintf("剩余时间: %s\n", formatDuration(remaining))) + progressText.WriteString(i18n.T(i18nk.BotMsgProgressImportRemainingTimePrefix, nil)) + progressText.WriteString(formatDuration(remaining)) } } processing := info.Processing() if len(processing) > 0 { - progressText.WriteString("\n正在处理:\n") + progressText.WriteString(i18n.T(i18nk.BotMsgProgressImportProcessingPrefix, nil)) for i, elem := range processing { if i >= 3 { - progressText.WriteString(fmt.Sprintf("...和其他 %d 个文件\n", len(processing)-3)) + progressText.WriteString(i18n.T(i18nk.BotMsgProgressImportProcessingMore, map[string]any{"Count": len(processing) - 3})) break } fmt.Fprintf(&progressText, "- %s\n", elem.FileName()) @@ -150,29 +156,36 @@ func (p *Progress) OnDone(ctx context.Context, info TaskInfo, err error) { var resultText strings.Builder if err != nil { - resultText.WriteString("❌ 导入失败\n") - fmt.Fprintf(&resultText, "错误: %v\n", err) + resultText.WriteString(i18n.T(i18nk.BotMsgProgressImportFailedPrefix, nil)) + resultText.WriteString(i18n.T(i18nk.BotMsgProgressErrorPrefix, nil)) + fmt.Fprintf(&resultText, "%v\n", err) } else { - resultText.WriteString("✅ 导入完成\n") + resultText.WriteString(i18n.T(i18nk.BotMsgProgressImportSuccessPrefix, nil)) } elapsed := time.Since(p.start) - resultText.WriteString(fmt.Sprintf("\n总文件数: %d\n", info.Count())) - resultText.WriteString(fmt.Sprintf("总大小: %.2f MB\n", float64(info.TotalSize())/(1024*1024))) - resultText.WriteString(fmt.Sprintf("已上传: %.2f MB\n", float64(info.Uploaded())/(1024*1024))) - resultText.WriteString(fmt.Sprintf("耗时: %s\n", formatDuration(elapsed))) + resultText.WriteString(i18n.T(i18nk.BotMsgProgressImportTotalFilesPrefix, nil)) + fmt.Fprintf(&resultText, "%d\n", info.Count()) + resultText.WriteString(i18n.T(i18nk.BotMsgProgressImportTotalSizePrefix, nil)) + fmt.Fprintf(&resultText, "%.2f MB\n", float64(info.TotalSize())/(1024*1024)) + resultText.WriteString(i18n.T(i18nk.BotMsgProgressImportUploadedPrefix, nil)) + fmt.Fprintf(&resultText, "%.2f MB\n", float64(info.Uploaded())/(1024*1024)) + resultText.WriteString(i18n.T(i18nk.BotMsgProgressImportElapsedTimePrefix, nil)) + fmt.Fprintf(&resultText, "%s\n", formatDuration(elapsed)) if elapsed.Seconds() > 0 { avgSpeed := float64(info.Uploaded()) / elapsed.Seconds() - resultText.WriteString(fmt.Sprintf("平均速度: %s/s\n", dlutil.FormatSize(int64(avgSpeed)))) + resultText.WriteString(i18n.T(i18nk.BotMsgProgressImportAvgSpeedPrefix, nil)) + fmt.Fprintf(&resultText, "%s/s\n", dlutil.FormatSize(int64(avgSpeed))) } failedFiles := info.FailedFiles() if len(failedFiles) > 0 { - fmt.Fprintf(&resultText, "\n失败文件数: %d\n", len(failedFiles)) + resultText.WriteString(i18n.T(i18nk.BotMsgProgressImportFailedFilesPrefix, nil)) + fmt.Fprintf(&resultText, "%d\n", len(failedFiles)) for i, name := range failedFiles { if i >= 5 { - fmt.Fprintf(&resultText, "...和其他 %d 个文件\n", len(failedFiles)-5) + resultText.WriteString(i18n.T(i18nk.BotMsgProgressImportProcessingMore, map[string]any{"Count": len(failedFiles) - 5})) break } fmt.Fprintf(&resultText, "- %s\n", name)