feat: sync langpack and auth updates
Sync telesrv commits 49f9bab and 04d9563 into the public mirror. Exclude private docs and runtime key material per sync rules.
This commit is contained in:
parent
6af61f26ba
commit
1292540350
244 changed files with 1552172 additions and 27668 deletions
|
|
@ -4,18 +4,38 @@ import (
|
|||
"context"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sync/singleflight"
|
||||
"golang.org/x/text/unicode/bidi"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
||||
// Service 提供客户端语言包查询。
|
||||
type Service struct {
|
||||
packs store.LangPackStore
|
||||
packs store.LangPackStore
|
||||
packCache *langPackCache
|
||||
languageCache *languageListCache
|
||||
packLoads singleflight.Group
|
||||
languageLoads singleflight.Group
|
||||
}
|
||||
|
||||
// NewService 创建 langpack 服务。
|
||||
func NewService(packs store.LangPackStore) *Service {
|
||||
return &Service{packs: packs}
|
||||
return newServiceWithCacheLimits(
|
||||
packs,
|
||||
defaultLangPackCacheMaxBytes,
|
||||
defaultLangPackCacheMaxEntries,
|
||||
defaultLanguageListCacheMaxEntries,
|
||||
)
|
||||
}
|
||||
|
||||
func newServiceWithCacheLimits(packs store.LangPackStore, maxBytes int64, maxEntries, languageEntries int) *Service {
|
||||
return &Service{
|
||||
packs: packs,
|
||||
packCache: newLangPackCache(maxBytes, maxEntries),
|
||||
languageCache: newLanguageListCache(languageEntries),
|
||||
}
|
||||
}
|
||||
|
||||
// GetLangPack 返回完整语言包。
|
||||
|
|
@ -30,11 +50,23 @@ func (s *Service) GetDifference(ctx context.Context, langPack, langCode string,
|
|||
if s == nil || s.packs == nil {
|
||||
return domain.LangPack{LangPack: packName, LangCode: code, FromVersion: fromVersion}, nil
|
||||
}
|
||||
pack, err := s.packs.GetPack(ctx, packName, code, fromVersion)
|
||||
var (
|
||||
pack domain.LangPack
|
||||
err error
|
||||
)
|
||||
if fromVersion == 0 {
|
||||
pack, err = s.effectivePack(ctx, packName, code)
|
||||
} else {
|
||||
pack, err = s.rawPack(ctx, packName, code)
|
||||
}
|
||||
if err != nil {
|
||||
return domain.LangPack{}, err
|
||||
}
|
||||
return s.overlayWebAStrings(ctx, pack, packName, code, fromVersion)
|
||||
pack.FromVersion = fromVersion
|
||||
if pack.Version <= fromVersion {
|
||||
pack.Strings = nil
|
||||
}
|
||||
return pack, nil
|
||||
}
|
||||
|
||||
// GetStrings 返回指定 key 的语言包字符串。
|
||||
|
|
@ -44,29 +76,42 @@ func (s *Service) GetStrings(ctx context.Context, langPack, langCode string, key
|
|||
if s == nil || s.packs == nil {
|
||||
return domain.LangPack{LangPack: packName, LangCode: code}, nil
|
||||
}
|
||||
pack, err := s.packs.GetStrings(ctx, packName, code, keys)
|
||||
pack, err := s.effectivePack(ctx, packName, code)
|
||||
if err != nil {
|
||||
return domain.LangPack{}, err
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return s.overlayWebAStrings(ctx, pack, packName, code, 0)
|
||||
}
|
||||
missing := missingLangPackKeys(keys, pack.Strings)
|
||||
if len(missing) == 0 || !shouldOverlayWebA(packName) {
|
||||
return pack, nil
|
||||
}
|
||||
overlay, err := s.packs.GetStrings(ctx, "weba", code, missing)
|
||||
if err != nil {
|
||||
return domain.LangPack{}, err
|
||||
wanted := make(map[string]struct{}, len(keys))
|
||||
for _, key := range keys {
|
||||
wanted[key] = struct{}{}
|
||||
}
|
||||
return mergeMissingLangPackStrings(pack, overlay), nil
|
||||
selected := pack
|
||||
selected.Strings = make([]domain.LangPackString, 0, len(keys))
|
||||
for _, item := range pack.Strings {
|
||||
if _, ok := wanted[item.Key]; ok {
|
||||
selected.Strings = append(selected.Strings, item)
|
||||
}
|
||||
}
|
||||
return selected, nil
|
||||
}
|
||||
|
||||
// ListLanguages 返回已 seed 的语言包语言列表。
|
||||
func (s *Service) ListLanguages(ctx context.Context, langPack string) ([]domain.LangPackLanguage, error) {
|
||||
packName := normalizePack(langPack)
|
||||
if s == nil || s.packs == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return s.cachedLanguages(ctx, packName)
|
||||
}
|
||||
|
||||
func normalizePack(langPack string) string {
|
||||
if langPack == "" {
|
||||
pack := strings.ToLower(strings.TrimSpace(langPack))
|
||||
if pack == "" {
|
||||
return "tdesktop"
|
||||
}
|
||||
return langPack
|
||||
return pack
|
||||
}
|
||||
|
||||
func normalizeCode(langCode string) string {
|
||||
|
|
@ -74,6 +119,7 @@ func normalizeCode(langCode string) string {
|
|||
if code == "" {
|
||||
return "en"
|
||||
}
|
||||
code = strings.ReplaceAll(code, "_", "-")
|
||||
return strings.TrimSuffix(code, "-raw")
|
||||
}
|
||||
|
||||
|
|
@ -86,15 +132,117 @@ func shouldOverlayWebA(langPack string) bool {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Service) overlayWebAStrings(ctx context.Context, pack domain.LangPack, langPack, langCode string, fromVersion int) (domain.LangPack, error) {
|
||||
if fromVersion != 0 || !shouldOverlayWebA(langPack) {
|
||||
return pack, nil
|
||||
func (s *Service) rawPack(ctx context.Context, langPack, langCode string) (domain.LangPack, error) {
|
||||
key := langPackCacheKey{pack: langPack, code: langCode, kind: langPackCacheRaw}
|
||||
return s.cachedPack(ctx, key, func() (domain.LangPack, error) {
|
||||
return s.packs.GetPack(ctx, langPack, langCode, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) effectivePack(ctx context.Context, langPack, langCode string) (domain.LangPack, error) {
|
||||
if !shouldOverlayWebA(langPack) {
|
||||
return s.rawPack(ctx, langPack, langCode)
|
||||
}
|
||||
overlay, err := s.packs.GetPack(ctx, "weba", langCode, fromVersion)
|
||||
if err != nil {
|
||||
return domain.LangPack{}, err
|
||||
key := langPackCacheKey{pack: langPack, code: langCode, kind: langPackCacheEffective}
|
||||
return s.cachedPack(ctx, key, func() (domain.LangPack, error) {
|
||||
pack, err := s.rawPack(ctx, langPack, langCode)
|
||||
if err != nil {
|
||||
return domain.LangPack{}, err
|
||||
}
|
||||
overlay, err := s.rawPack(ctx, "weba", langCode)
|
||||
if err != nil {
|
||||
return domain.LangPack{}, err
|
||||
}
|
||||
return mergeMissingLangPackStrings(pack, overlay), nil
|
||||
})
|
||||
}
|
||||
|
||||
type cachedPackLoadResult struct {
|
||||
pack domain.LangPack
|
||||
stable bool
|
||||
}
|
||||
|
||||
func (s *Service) cachedPack(ctx context.Context, key langPackCacheKey, load func() (domain.LangPack, error)) (domain.LangPack, error) {
|
||||
if s.packCache == nil {
|
||||
return load()
|
||||
}
|
||||
return mergeMissingLangPackStrings(pack, overlay), nil
|
||||
for {
|
||||
if pack, ok := s.packCache.get(key); ok {
|
||||
return pack, nil
|
||||
}
|
||||
value, err, _ := s.packLoads.Do(key.singleflightKey(), func() (any, error) {
|
||||
if pack, ok := s.packCache.get(key); ok {
|
||||
return cachedPackLoadResult{pack: pack, stable: true}, nil
|
||||
}
|
||||
loadEpoch := s.packCache.loadEpoch()
|
||||
pack, err := load()
|
||||
if err != nil {
|
||||
return cachedPackLoadResult{}, err
|
||||
}
|
||||
return cachedPackLoadResult{
|
||||
pack: pack,
|
||||
stable: s.packCache.putIfEpoch(key, pack, loadEpoch),
|
||||
}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return domain.LangPack{}, err
|
||||
}
|
||||
result := value.(cachedPackLoadResult)
|
||||
if result.stable {
|
||||
return cloneLangPack(result.pack), nil
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return domain.LangPack{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type cachedLanguagesLoadResult struct {
|
||||
languages []domain.LangPackLanguage
|
||||
stable bool
|
||||
}
|
||||
|
||||
func (s *Service) cachedLanguages(ctx context.Context, langPack string) ([]domain.LangPackLanguage, error) {
|
||||
if languages, ok := s.languageCache.get(langPack); ok {
|
||||
return languages, nil
|
||||
}
|
||||
for {
|
||||
value, err, _ := s.languageLoads.Do(langPack, func() (any, error) {
|
||||
if languages, ok := s.languageCache.get(langPack); ok {
|
||||
return cachedLanguagesLoadResult{languages: languages, stable: true}, nil
|
||||
}
|
||||
loadEpoch := s.languageCache.loadEpoch()
|
||||
languages, err := s.packs.ListLanguages(ctx, langPack)
|
||||
if err != nil {
|
||||
return cachedLanguagesLoadResult{}, err
|
||||
}
|
||||
for i := range languages {
|
||||
languages[i] = completeLanguageMetadata(langPack, languages[i])
|
||||
}
|
||||
return cachedLanguagesLoadResult{
|
||||
languages: languages,
|
||||
stable: s.languageCache.putIfEpoch(langPack, languages, loadEpoch),
|
||||
}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := value.(cachedLanguagesLoadResult)
|
||||
if result.stable {
|
||||
return cloneLanguages(result.languages), nil
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) flushCaches() {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.packCache.flush()
|
||||
s.languageCache.flush()
|
||||
}
|
||||
|
||||
func mergeMissingLangPackStrings(pack, overlay domain.LangPack) domain.LangPack {
|
||||
|
|
@ -121,6 +269,57 @@ func mergeMissingLangPackStrings(pack, overlay domain.LangPack) domain.LangPack
|
|||
return pack
|
||||
}
|
||||
|
||||
func completeLanguageMetadata(langPack string, lang domain.LangPackLanguage) domain.LangPackLanguage {
|
||||
if lang.LangPack == "" {
|
||||
lang.LangPack = langPack
|
||||
}
|
||||
lang.LangCode = normalizeCode(lang.LangCode)
|
||||
if lang.PluralCode == "" {
|
||||
lang.PluralCode = pluralCode(lang.LangCode)
|
||||
}
|
||||
if lang.NativeName == "" {
|
||||
lang.NativeName = lang.Name
|
||||
}
|
||||
if lang.Name == "" {
|
||||
lang.Name = lang.NativeName
|
||||
}
|
||||
if lang.Name == "" {
|
||||
lang.Name = lang.LangCode
|
||||
}
|
||||
if lang.NativeName == "" {
|
||||
lang.NativeName = lang.Name
|
||||
}
|
||||
if lang.StringsCount == 0 {
|
||||
lang.StringsCount = lang.TranslatedCount
|
||||
}
|
||||
if lang.TranslatedCount == 0 {
|
||||
lang.TranslatedCount = lang.StringsCount
|
||||
}
|
||||
lang.Official = true
|
||||
lang.Rtl = lang.Rtl || isRTLText(lang.NativeName)
|
||||
return lang
|
||||
}
|
||||
|
||||
func pluralCode(langCode string) string {
|
||||
if idx := strings.IndexAny(langCode, "-_"); idx > 0 {
|
||||
return langCode[:idx]
|
||||
}
|
||||
return langCode
|
||||
}
|
||||
|
||||
func isRTLText(value string) bool {
|
||||
for _, r := range value {
|
||||
properties, _ := bidi.LookupRune(r)
|
||||
switch properties.Class() {
|
||||
case bidi.R, bidi.AL:
|
||||
return true
|
||||
case bidi.L:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func missingLangPackKeys(keys []string, strings []domain.LangPackString) []string {
|
||||
if len(keys) == 0 {
|
||||
return nil
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue