- 新增 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 通过
140 lines
3.6 KiB
Go
140 lines
3.6 KiB
Go
// 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 → zh;ja-JP → ja;en-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...)
|
||
}
|