translate.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. // ============================================================================
  2. // translate.go — 工具页翻译
  3. //
  4. // 调用 MyMemory 免费接口做中英互译(自动检测方向),
  5. // 含每日配额限流提示与错误处理。
  6. // ============================================================================
  7. package main
  8. import (
  9. "encoding/json"
  10. "errors"
  11. "fmt"
  12. "net/http"
  13. "net/url"
  14. "regexp"
  15. "strings"
  16. "time"
  17. )
  18. // TranslateResult 一次翻译的结果,供前端展示。
  19. type TranslateResult struct {
  20. Text string `json:"text"` // 翻译结果文本
  21. Detected string `json:"detected"` // 检测到的源语言(zh / en)
  22. Target string `json:"target"` // 目标语言(zh / en)
  23. }
  24. // myMemoryResp MyMemory API 响应(只取需要的字段)。
  25. type myMemoryResp struct {
  26. ResponseData struct {
  27. TranslatedText string `json:"translatedText"`
  28. } `json:"responseData"`
  29. ResponseStatus int `json:"responseStatus"`
  30. ResponseDetail string `json:"responseDetails"`
  31. }
  32. // cjkRe 匹配 CJK(中日韩)字符,用于检测文本是否为中文。
  33. // 注意:Go 的 regexp 不支持 \u 转义,需用 \x{...} 形式。
  34. var cjkRe = regexp.MustCompile(`[\x{4e00}-\x{9fff}]`)
  35. // detectLang 粗略检测文本语言:含 CJK 字符视为中文,否则视为英文。
  36. // 返回 MyMemory 使用的 langpair 源语言代码。
  37. func detectLang(text string) string {
  38. if cjkRe.MatchString(text) {
  39. return "zh-CN"
  40. }
  41. return "en"
  42. }
  43. // quotaHintRe 匹配 MyMemory 限流提示中的恢复时间(如 "00 HOURS 20 MINUTES 16 SECONDS")。
  44. var quotaHintRe = regexp.MustCompile(`NEXT AVAILABLE IN\s+([\d\sA-Z]+)`)
  45. // quotaError 根据 MyMemory 的限流提示生成用户友好的错误信息。
  46. func quotaError(raw string) error {
  47. // 从提示语中提取恢复时间,告知用户多久后可用
  48. msg := "翻译服务今日免费额度已用完"
  49. if m := quotaHintRe.FindStringSubmatch(raw); len(m) > 1 {
  50. parts := strings.Fields(strings.ToUpper(m[1]))
  51. // 期望格式:HH HOURS MM MINUTES SS SECONDS
  52. if len(parts) >= 6 && parts[1] == "HOURS" && parts[3] == "MINUTES" && parts[5] == "SECONDS" {
  53. h, _ := time.ParseDuration(parts[0] + "h")
  54. mn, _ := time.ParseDuration(parts[2] + "m")
  55. s, _ := time.ParseDuration(parts[4] + "s")
  56. wait := h + mn + s
  57. if wait > 0 {
  58. msg = fmt.Sprintf("翻译服务今日免费额度已用完,约 %s 后恢复", wait.Round(time.Minute))
  59. }
  60. }
  61. }
  62. msg += "(每日限额约 5000 词,请稍后再试)"
  63. return errors.New(msg)
  64. }
  65. // translateMyMemory 调用 MyMemory 免费翻译 API,自动检测中英文方向。
  66. // MyMemory 无 API key,但有每日配额(约 5000 词),超限返回 429。
  67. func translateMyMemory(text string) (TranslateResult, error) {
  68. text = strings.TrimSpace(text)
  69. if text == "" {
  70. return TranslateResult{}, errors.New("请输入要翻译的内容")
  71. }
  72. src := detectLang(text)
  73. target := "en"
  74. if src == "en" {
  75. target = "zh-CN"
  76. }
  77. // MyMemory langpair 使用下划线连接:zh-CN|en 或 en|zh-CN
  78. endpoint := fmt.Sprintf("https://api.mymemory.translated.net/get?q=%s&langpair=%s",
  79. url.QueryEscape(text), url.QueryEscape(src+"|"+target))
  80. client := &http.Client{Timeout: 15 * time.Second}
  81. req, err := http.NewRequest(http.MethodGet, endpoint, nil)
  82. if err != nil {
  83. return TranslateResult{}, err
  84. }
  85. req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36")
  86. req.Header.Set("Accept", "application/json")
  87. resp, err := client.Do(req)
  88. if err != nil {
  89. return TranslateResult{}, fmt.Errorf("翻译服务请求失败:%w", err)
  90. }
  91. defer resp.Body.Close()
  92. var mr myMemoryResp
  93. if err := json.NewDecoder(resp.Body).Decode(&mr); err != nil {
  94. return TranslateResult{}, fmt.Errorf("翻译服务响应解析失败:%w", err)
  95. }
  96. // MyMemory 超限时 HTTP 仍返回 200,但 responseStatus 为 429 且提示语在 translatedText 中。
  97. if mr.ResponseStatus == 429 {
  98. return TranslateResult{}, quotaError(mr.ResponseDetail)
  99. }
  100. if mr.ResponseStatus != 200 {
  101. detail := mr.ResponseDetail
  102. if detail == "" {
  103. detail = fmt.Sprintf("服务返回状态码 %d", mr.ResponseStatus)
  104. }
  105. return TranslateResult{}, fmt.Errorf("翻译失败:%s", detail)
  106. }
  107. if strings.Contains(mr.ResponseData.TranslatedText, "MYMEMORY WARNING") {
  108. return TranslateResult{}, quotaError(mr.ResponseData.TranslatedText)
  109. }
  110. translated := strings.TrimSpace(mr.ResponseData.TranslatedText)
  111. if translated == "" {
  112. return TranslateResult{}, errors.New("翻译结果为空,请重试")
  113. }
  114. return TranslateResult{
  115. Text: translated,
  116. Detected: src,
  117. Target: target,
  118. }, nil
  119. }