Files
mailgo/internal/i18n/i18n.go
T
dsh c881146a40 feat(web): 多语言界面(中/英/日)与用户语言偏好字段
- 新增 internal/i18n 包:以原始中文字符串为 key 的翻译目录(zh 恒等,
  en 兜底,ja 缺译回退 en),t/tf 模板函数与 Accept-Language 解析
  (按 q 值排序,兜底英语)
- User 模型新增 language 字段(auto|en|zh|ja,默认 auto),
  AutoMigrate 自动加列;UserStore 新增 UpdateLanguage
- 语言解析:登录用户按偏好(auto 时取浏览器 Accept-Language),
  登录/封禁页按浏览器语言;所有 c.HTML 渲染注入 Lang 模板数据
- 全部页面模板(用户页 + 管理后台 12 页)接入翻译:导航、文件夹、
  邮件列表/阅读/写信、设置、登录、封禁页、表单、表格、JS 确认框等;
  html lang 属性随语言切换;日期函数(shortDate/time12/time12m)
  与 folderLabel 改为语言感知(上午/下午 vs AM/PM vs 午前/午後)
- 设置页新增「界面语言」选择(自动/English/中文/日本語),
  /settings 表单支持单独提交语言偏好;管理后台用户表单可预设语言
- 封禁原因等入库中文文案在展示时经 t 翻译(未知 key 原样回退)
- 测试:i18n 单测(解析/回退/译文)、render 测试补 Lang 数据、
  时间格式化测试适配新签名;全量 go test 通过
2026-08-20 00:21:10 -04:00

140 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package i18n provides lightweight UI localization for the MailGo web
// frontend. Supported languages: zh (Chinese), en (English, fallback) and
// ja (Japanese). Users may also pick "auto" so the effective language is
// derived from their browser's Accept-Language header.
//
// Translation keys are the original Chinese UI strings themselves: the zh
// catalog is the identity map, and the en/ja catalogs map those keys to
// their translations. Missing translations fall back to English, then to
// the key itself.
package i18n
import (
"fmt"
"sort"
"strconv"
"strings"
)
// Language preference values stored on the user record.
const (
// LangAuto 跟随浏览器语言(Accept-Language)。
LangAuto = "auto"
// LangEn 英语:兜底语言。
LangEn = "en"
// LangZh 中文。
LangZh = "zh"
// LangJa 日文。
LangJa = "ja"
)
// Supported 判断 lang 是否为可显式选择的界面语言(不含 auto)。
func Supported(lang string) bool {
switch lang {
case LangEn, LangZh, LangJa:
return true
default:
return false
}
}
// Normalize 把浏览器语言标签规整为受支持的语言代码:
// zh-CN / zh-TW / zh-Hans → zhja-JP → jaen-US / en-GB → en
// 其余返回空串(无法识别)。
func Normalize(tag string) string {
tag = strings.ToLower(strings.TrimSpace(tag))
if i := strings.IndexByte(tag, '-'); i >= 0 {
tag = tag[:i]
}
if i := strings.IndexByte(tag, '_'); i >= 0 {
tag = tag[:i]
}
switch tag {
case LangZh, "zh-hans", "zh-hant":
return LangZh
case LangJa:
return LangJa
case LangEn:
return LangEn
default:
return ""
}
}
// FromAcceptLanguage 从 Accept-Language 请求头解析首选语言。
// 按 q 值(缺省 1.0)降序取第一个受支持的语言;都不支持时回退英语。
func FromAcceptLanguage(header string) string {
type entry struct {
lang string
q float64
}
var entries []entry
for _, part := range strings.Split(header, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
tag := part
q := 1.0
if i := strings.IndexByte(part, ';'); i >= 0 {
tag = strings.TrimSpace(part[:i])
for _, param := range strings.Split(part[i+1:], ";") {
param = strings.TrimSpace(param)
if strings.HasPrefix(param, "q=") {
if v, err := strconv.ParseFloat(param[2:], 64); err == nil {
q = v
}
}
}
}
if lang := Normalize(tag); lang != "" {
entries = append(entries, entry{lang: lang, q: q})
}
}
sort.SliceStable(entries, func(i, j int) bool { return entries[i].q > entries[j].q })
for _, e := range entries {
if e.q > 0 {
return e.lang
}
}
return LangEn
}
// Resolve 计算有效界面语言:用户偏好为显式语言时直接采用;
// auto(或空/未知值)时按浏览器 Accept-Language 解析;最终兜底英语。
func Resolve(pref, acceptLanguage string) string {
if Supported(pref) {
return pref
}
return FromAcceptLanguage(acceptLanguage)
}
// T 返回 key 在 lang 下的译文。zh 直接返回 key;
// ja 优先查日文表,缺译回退英文表;其余语言(含未知)查英文表,
// 仍缺失时原样返回 key(保证界面永不出现空文本)。
func T(lang, key string) string {
switch Normalize(lang) {
case LangZh:
return key
case LangJa:
if s, ok := ja[key]; ok {
return s
}
if s, ok := en[key]; ok {
return s
}
return key
default:
if s, ok := en[key]; ok {
return s
}
return key
}
}
// TF 是 T 的格式化版本:对译文执行 fmt.Sprintf。
// 用于带占位符的消息(如「还剩 %d 次尝试机会」)。
func TF(lang, key string, args ...interface{}) string {
return fmt.Sprintf(T(lang, key), args...)
}