Files
sub2api/backend/internal/service/group_i18n.go
T
feeeei 8b74741efe fix(groups): 广场 Vary 保留 Origin,分组译文语言代码统一大小写
- 模型广场以追加方式写入 Vary: Accept-Language,保留 CORS 中间件为具体来源
  写入的 Vary: Origin
- 管理端写入分组译文时语言代码统一为 BCP 47 惯用大小写(en、zh-TW),
  统一后重复的语言代码(如 en 与 EN)返回 400 INVALID_GROUP_I18N
- 用户侧按语言取译文时规范写法的键优先,仅大小写不同的多个键按字节序取最小者,
  同一请求语言的结果固定
2026-10-12 18:09:03 +08:00

65 lines
2.3 KiB
Go

package service
import (
"fmt"
"maps"
"regexp"
"slices"
"strings"
"unicode/utf8"
"github.com/Wei-Shaw/sub2api/internal/domain"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
)
type GroupI18n = domain.GroupI18n
type GroupLocaleText = domain.GroupLocaleText
// groupI18nNameMaxRunes 与 groups.name 的长度上限一致。
const groupI18nNameMaxRunes = 100
// 语言代码只校验 BCP 47 形态(en、zh、zh-TW);具体提供哪些语言由前端语言包决定。
var groupI18nLocalePattern = regexp.MustCompile(`^[A-Za-z]{2,8}(-[A-Za-z0-9]{1,8})*$`)
// normalizeGroupI18n 把语言代码统一为 domain.CanonicalLocale 的写法,去除译文首尾空白,
// 并丢弃名称与描述都为空的语言。统一写法后重复的语言代码(如 en 与 EN)直接拒绝,
// 不在其中任选一份。返回值始终非 nil,落库为 JSON 对象。
func normalizeGroupI18n(in GroupI18n) (GroupI18n, error) {
out := make(GroupI18n, len(in))
seen := make(map[string]struct{}, len(in))
for _, raw := range slices.Sorted(maps.Keys(in)) {
if !groupI18nLocalePattern.MatchString(raw) {
return nil, infraerrors.BadRequest("INVALID_GROUP_I18N", fmt.Sprintf("invalid locale %q in i18n", raw))
}
locale := domain.CanonicalLocale(raw)
if _, dup := seen[locale]; dup {
return nil, infraerrors.BadRequest("INVALID_GROUP_I18N", fmt.Sprintf("duplicate locale %q in i18n", locale))
}
seen[locale] = struct{}{}
text := in[raw]
name := strings.TrimSpace(text.Name)
description := strings.TrimSpace(text.Description)
if utf8.RuneCountInString(name) > groupI18nNameMaxRunes {
return nil, infraerrors.BadRequest("INVALID_GROUP_I18N", fmt.Sprintf("i18n name for locale %q must be at most %d characters", locale, groupI18nNameMaxRunes))
}
if name == "" && description == "" {
continue
}
out[locale] = GroupLocaleText{Name: name, Description: description}
}
return out, nil
}
// cloneGroupI18nForDuplicate 复制译文中的描述。名称译文不随复制带走:
// 副本的 name 已改写,沿用源分组的名称译文会让两个分组在该语言下同名。
func cloneGroupI18nForDuplicate(in GroupI18n) GroupI18n {
out := make(GroupI18n, len(in))
for locale, text := range in {
if text.Description == "" {
continue
}
out[locale] = GroupLocaleText{Description: text.Description}
}
return out
}