From a785ae749104c9e56f75fa47c241c84ab7710aff Mon Sep 17 00:00:00 2001 From: iamxvbaba <28732408+iamxvbaba@users.noreply.github.com> Date: Sat, 25 Jul 2026 14:08:43 +0800 Subject: [PATCH] fix(langpack): sync WebK catalog alias support --- internal/app/langpack/parser_test.go | 6 +- internal/app/langpack/service.go | 34 +++++- internal/app/langpack/service_test.go | 101 ++++++++++++++++++ internal/domain/langpack.go | 9 ++ internal/rpc/errors.go | 3 + internal/rpc/langpack.go | 24 ++++- internal/rpc/langpack_compat_test.go | 144 ++++++++++++++++++++++++++ 7 files changed, 313 insertions(+), 8 deletions(-) diff --git a/internal/app/langpack/parser_test.go b/internal/app/langpack/parser_test.go index 76f93fe6..5a69695a 100644 --- a/internal/app/langpack/parser_test.go +++ b/internal/app/langpack/parser_test.go @@ -2,11 +2,13 @@ package langpack import ( "context" + "errors" "os" "path/filepath" "strings" "testing" + "telesrv/internal/domain" "telesrv/internal/store/memory" ) @@ -205,8 +207,8 @@ func TestSeedDirectoryReconcilesManifest(t *testing.T) { t.Fatalf("reconcile removed file = %d, %v", seeded, err) } languages, err = service.ListLanguages(ctx, "tdesktop") - if err != nil || len(languages) != 0 { - t.Fatalf("languages after removal = %+v, err %v", languages, err) + if !errors.Is(err, domain.ErrLangPackInvalid) || len(languages) != 0 { + t.Fatalf("languages after removal = %+v, err %v, want ErrLangPackInvalid", languages, err) } } diff --git a/internal/app/langpack/service.go b/internal/app/langpack/service.go index 8c5c7b27..2514fe88 100644 --- a/internal/app/langpack/service.go +++ b/internal/app/langpack/service.go @@ -70,6 +70,9 @@ 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 } + if err := s.validateLanguage(ctx, packName, code); err != nil { + return domain.LangPack{}, err + } var ( pack domain.LangPack err error @@ -96,6 +99,9 @@ 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 } + if err := s.validateLanguage(ctx, packName, code); err != nil { + return domain.LangPack{}, err + } pack, err := s.effectivePack(ctx, packName, code) if err != nil { return domain.LangPack{}, err @@ -123,7 +129,14 @@ func (s *Service) ListLanguages(ctx context.Context, langPack string) ([]domain. if s == nil || s.packs == nil { return nil, nil } - return s.cachedLanguages(ctx, packName) + languages, err := s.cachedLanguages(ctx, packName) + if err != nil { + return nil, err + } + if len(languages) == 0 { + return nil, domain.ErrLangPackInvalid + } + return languages, nil } func normalizePack(langPack string) string { @@ -131,6 +144,9 @@ func normalizePack(langPack string) string { if pack == "" { return "tdesktop" } + if pack == "web" { + return "webk" + } return pack } @@ -262,6 +278,22 @@ func (s *Service) cachedLanguages(ctx context.Context, langPack string) ([]domai } } +func (s *Service) validateLanguage(ctx context.Context, langPack, langCode string) error { + languages, err := s.cachedLanguages(ctx, langPack) + if err != nil { + return err + } + if len(languages) == 0 { + return domain.ErrLangPackInvalid + } + for _, language := range languages { + if normalizeCode(language.LangCode) == langCode { + return nil + } + } + return domain.ErrLangCodeNotSupported +} + func (s *Service) brandPack(pack domain.LangPack) domain.LangPack { for i := range pack.Strings { item := &pack.Strings[i] diff --git a/internal/app/langpack/service_test.go b/internal/app/langpack/service_test.go index fd4abea0..a7ef11e8 100644 --- a/internal/app/langpack/service_test.go +++ b/internal/app/langpack/service_test.go @@ -2,6 +2,7 @@ package langpack import ( "context" + "errors" "sync" "testing" "time" @@ -11,6 +12,106 @@ import ( "telesrv/internal/store/memory" ) +func TestServiceNormalizesWebAliasAndRejectsUnknownCatalogEntries(t *testing.T) { + ctx := context.Background() + packs := memory.NewLangPackStore() + svc := NewService(packs) + for _, pack := range []domain.LangPack{ + { + LangPack: "webk", + LangCode: "en", + Version: 7, + Strings: []domain.LangPackString{{Key: "lng_settings_language", Value: "Language"}}, + }, + { + LangPack: "webk", + LangCode: "zh-hans", + Version: 9, + Strings: []domain.LangPackString{{Key: "lng_settings_language", Value: "语言"}}, + }, + } { + if err := packs.UpsertPack(ctx, pack); err != nil { + t.Fatalf("seed %s/%s: %v", pack.LangPack, pack.LangCode, err) + } + } + + languages, err := svc.ListLanguages(ctx, " WEB ") + if err != nil { + t.Fatalf("list web languages: %v", err) + } + if len(languages) != 2 || findLanguage(languages, "zh-hans") == nil { + t.Fatalf("web languages = %+v, want canonical webk catalog", languages) + } + + full, err := svc.GetLangPack(ctx, "web", "ZH_HANS") + if err != nil { + t.Fatalf("get web langpack: %v", err) + } + if full.LangPack != "webk" || full.LangCode != "zh-hans" || full.Version != 9 || stringValue(full.Strings, "lng_settings_language") != "语言" { + t.Fatalf("web langpack = %+v, want canonical webk/zh-hans", full) + } + + diff, err := svc.GetDifference(ctx, "web", "zh-hans", 1) + if err != nil { + t.Fatalf("get web difference: %v", err) + } + if diff.LangPack != "webk" || diff.FromVersion != 1 || len(diff.Strings) != 1 { + t.Fatalf("web difference = %+v, want canonical webk delta", diff) + } + + selected, err := svc.GetStrings(ctx, "web", "zh-hans", []string{"lng_settings_language"}) + if err != nil { + t.Fatalf("get web strings: %v", err) + } + if selected.LangPack != "webk" || stringValue(selected.Strings, "lng_settings_language") != "语言" { + t.Fatalf("web strings = %+v, want selected webk string", selected) + } + + invalidPackCalls := map[string]func() error{ + "list": func() error { + _, err := svc.ListLanguages(ctx, "web-invalid") + return err + }, + "full": func() error { + _, err := svc.GetLangPack(ctx, "web-invalid", "en") + return err + }, + "difference": func() error { + _, err := svc.GetDifference(ctx, "web-invalid", "en", 1) + return err + }, + "strings": func() error { + _, err := svc.GetStrings(ctx, "web-invalid", "en", []string{"key"}) + return err + }, + } + for name, call := range invalidPackCalls { + if err := call(); !errors.Is(err, domain.ErrLangPackInvalid) { + t.Fatalf("%s invalid pack error = %v, want ErrLangPackInvalid", name, err) + } + } + + unsupportedCodeCalls := map[string]func() error{ + "full": func() error { + _, err := svc.GetLangPack(ctx, "web", "fr") + return err + }, + "difference": func() error { + _, err := svc.GetDifference(ctx, "web", "fr", 1) + return err + }, + "strings": func() error { + _, err := svc.GetStrings(ctx, "web", "fr", []string{"key"}) + return err + }, + } + for name, call := range unsupportedCodeCalls { + if err := call(); !errors.Is(err, domain.ErrLangCodeNotSupported) { + t.Fatalf("%s unsupported code error = %v, want ErrLangCodeNotSupported", name, err) + } + } +} + func TestServiceNormalizesWebARawLangCode(t *testing.T) { ctx := context.Background() packs := memory.NewLangPackStore() diff --git a/internal/domain/langpack.go b/internal/domain/langpack.go index 078f5da4..b4a3f94b 100644 --- a/internal/domain/langpack.go +++ b/internal/domain/langpack.go @@ -1,5 +1,14 @@ package domain +import "errors" + +var ( + // ErrLangPackInvalid 表示客户端请求的语言包目录不存在。 + ErrLangPackInvalid = errors.New("lang pack invalid") + // ErrLangCodeNotSupported 表示语言包目录存在,但其中没有请求的语言码。 + ErrLangCodeNotSupported = errors.New("lang code not supported") +) + // LangPack 是一份客户端语言包的查询结果。 type LangPack struct { LangPack string diff --git a/internal/rpc/errors.go b/internal/rpc/errors.go index 53a1c2e1..3811eef0 100644 --- a/internal/rpc/errors.go +++ b/internal/rpc/errors.go @@ -29,6 +29,9 @@ func inputConstructorInvalidErr() error { return tgerr.New(400, "INPUT_CONSTRUCT // langCodeNotSupportedErr 表示请求的语言码没有已导入的语言包。 func langCodeNotSupportedErr() error { return tgerr.New(400, "LANG_CODE_NOT_SUPPORTED") } +// langPackInvalidErr 表示请求的语言包目录不存在。 +func langPackInvalidErr() error { return tgerr.New(400, "LANG_PACK_INVALID") } + // folderIDInvalidErr 表示客户端传入多个 folder peer 或非法 folder。 func folderIDInvalidErr() error { return tgerr.New(400, "FOLDER_ID_INVALID") } diff --git a/internal/rpc/langpack.go b/internal/rpc/langpack.go index 7fe7345c..a8d8470e 100644 --- a/internal/rpc/langpack.go +++ b/internal/rpc/langpack.go @@ -2,10 +2,13 @@ package rpc import ( "context" + "errors" "strings" "github.com/iamxvbaba/td/tg" "github.com/iamxvbaba/td/tlprofile" + + "telesrv/internal/domain" ) // registerLangpack 注册 langpack.* RPC handler。 @@ -20,7 +23,7 @@ func (r *Router) registerLangpack(d *tlprofile.Dispatcher) { languages, err := r.langpackLanguages(ctx, langPack) if err != nil { - return nil, internalErr() + return nil, langpackServiceErr(err) } return languages, nil }) @@ -42,7 +45,7 @@ func (r *Router) registerLangpack(d *tlprofile.Dispatcher) { } pack, err := r.deps.LangPack.GetLangPack(ctx, langPack, req.LangCode) if err != nil { - return nil, internalErr() + return nil, langpackServiceErr(err) } return tgLangPackDifference(pack), nil }) @@ -52,7 +55,7 @@ func (r *Router) registerLangpack(d *tlprofile.Dispatcher) { } pack, err := r.deps.LangPack.GetDifference(ctx, langPackOrClient(ctx, req.LangPack), req.LangCode, req.FromVersion) if err != nil { - return nil, internalErr() + return nil, langpackServiceErr(err) } return tgLangPackDifference(pack), nil }) @@ -62,7 +65,7 @@ func (r *Router) registerLangpack(d *tlprofile.Dispatcher) { } pack, err := r.deps.LangPack.GetStrings(ctx, langPackOrClient(ctx, req.LangPack), req.LangCode, req.Keys) if err != nil { - return nil, internalErr() + return nil, langpackServiceErr(err) } return tgLangPackStrings(pack.Strings), nil }) @@ -88,7 +91,7 @@ func (r *Router) langpackLanguage(ctx context.Context, langPack, langCode string langCode = normalizeLangpackCode(langCode) languages, err := r.langpackLanguages(ctx, langPack) if err != nil { - return tg.LangPackLanguage{}, internalErr() + return tg.LangPackLanguage{}, langpackServiceErr(err) } for _, lang := range languages { if strings.ToLower(lang.LangCode) == langCode { @@ -150,3 +153,14 @@ func normalizeLangpackCode(langCode string) string { code = strings.ReplaceAll(code, "_", "-") return strings.TrimSuffix(code, "-raw") } + +func langpackServiceErr(err error) error { + switch { + case errors.Is(err, domain.ErrLangPackInvalid): + return langPackInvalidErr() + case errors.Is(err, domain.ErrLangCodeNotSupported): + return langCodeNotSupportedErr() + default: + return internalErr() + } +} diff --git a/internal/rpc/langpack_compat_test.go b/internal/rpc/langpack_compat_test.go index ae99c815..9951c9e5 100644 --- a/internal/rpc/langpack_compat_test.go +++ b/internal/rpc/langpack_compat_test.go @@ -51,6 +51,121 @@ func TestLangpackGetLanguagesCurrentAndLegacy(t *testing.T) { }) } +func TestLangpackWebAliasCoversEveryRPC(t *testing.T) { + r := newSeededLangpackRouter(t) + ctx := context.Background() + + var languagesIn bin.Buffer + if err := (&tg.LangpackGetLanguagesRequest{LangPack: "web"}).Encode(&languagesIn); err != nil { + t.Fatalf("encode getLanguages: %v", err) + } + languages := dispatchLangpackLanguages(t, r, ctx, &languagesIn) + assertHasLangpackLanguage(t, languages, "zh-hans") + + lang, err := r.langpackLanguage(ctx, "web", "zh-hans") + if err != nil { + t.Fatalf("getLanguage web alias: %v", err) + } + if lang.LangCode != "zh-hans" { + t.Fatalf("getLanguage web alias = %+v, want zh-hans", lang) + } + + var fullIn bin.Buffer + if err := (&tg.LangpackGetLangPackRequest{LangPack: "web", LangCode: "zh-hans"}).Encode(&fullIn); err != nil { + t.Fatalf("encode getLangPack: %v", err) + } + full := dispatchLangpackDifference(t, r, ctx, &fullIn) + if full.LangCode != "zh-hans" || len(full.Strings) != 1 { + t.Fatalf("getLangPack web alias = %+v, want populated zh-hans pack", full) + } + + var differenceIn bin.Buffer + if err := (&tg.LangpackGetDifferenceRequest{LangPack: "web", LangCode: "zh-hans", FromVersion: 1}).Encode(&differenceIn); err != nil { + t.Fatalf("encode getDifference: %v", err) + } + difference := dispatchLangpackDifference(t, r, ctx, &differenceIn) + if difference.LangCode != "zh-hans" || difference.FromVersion != 1 || len(difference.Strings) != 1 { + t.Fatalf("getDifference web alias = %+v, want populated zh-hans delta", difference) + } + + var stringsIn bin.Buffer + if err := (&tg.LangpackGetStringsRequest{LangPack: "web", LangCode: "zh-hans", Keys: []string{"lng_settings_language"}}).Encode(&stringsIn); err != nil { + t.Fatalf("encode getStrings: %v", err) + } + enc, err := r.Dispatch(ctx, [8]byte{}, 0, &stringsIn) + if err != nil { + t.Fatalf("dispatch getStrings web alias: %v", err) + } + var stringsOut bin.Buffer + if err := enc.Encode(&stringsOut); err != nil { + t.Fatalf("encode getStrings response: %v", err) + } + var stringsVector tg.LangPackStringClassVector + if err := stringsVector.Decode(&stringsOut); err != nil { + t.Fatalf("decode getStrings response: %v", err) + } + if len(stringsVector.Elems) != 1 { + t.Fatalf("getStrings web alias = %+v, want one selected string", stringsVector.Elems) + } +} + +func TestLangpackInvalidCatalogErrorsAreMappedForEveryRPC(t *testing.T) { + r := newSeededLangpackRouter(t) + ctx := context.Background() + tests := []struct { + name string + encode func(*bin.Buffer) error + wantErr string + }{ + { + name: "getLanguages invalid pack", + encode: func(buf *bin.Buffer) error { + return (&tg.LangpackGetLanguagesRequest{LangPack: "web-invalid"}).Encode(buf) + }, + wantErr: "LANG_PACK_INVALID", + }, + { + name: "getLanguage invalid pack", + encode: func(buf *bin.Buffer) error { + return (&tg.LangpackGetLanguageRequest{LangPack: "web-invalid", LangCode: "en"}).Encode(buf) + }, + wantErr: "LANG_PACK_INVALID", + }, + { + name: "getLangPack invalid pack", + encode: func(buf *bin.Buffer) error { + return (&tg.LangpackGetLangPackRequest{LangPack: "web-invalid", LangCode: "en"}).Encode(buf) + }, + wantErr: "LANG_PACK_INVALID", + }, + { + name: "getDifference unsupported code", + encode: func(buf *bin.Buffer) error { + return (&tg.LangpackGetDifferenceRequest{LangPack: "web", LangCode: "fr", FromVersion: 1}).Encode(buf) + }, + wantErr: "LANG_CODE_NOT_SUPPORTED", + }, + { + name: "getStrings unsupported code", + encode: func(buf *bin.Buffer) error { + return (&tg.LangpackGetStringsRequest{LangPack: "web", LangCode: "fr", Keys: []string{"key"}}).Encode(buf) + }, + wantErr: "LANG_CODE_NOT_SUPPORTED", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var in bin.Buffer + if err := test.encode(&in); err != nil { + t.Fatalf("encode request: %v", err) + } + if _, err := r.Dispatch(ctx, [8]byte{}, 0, &in); err == nil || !strings.Contains(err.Error(), test.wantErr) { + t.Fatalf("dispatch error = %v, want %s", err, test.wantErr) + } + }) + } +} + func TestLangpackGetLanguage(t *testing.T) { r := newSeededLangpackRouter(t) @@ -247,6 +362,18 @@ func seededLangPackService(t testing.TB) LangPackService { {Key: "TranslateLanguageFA", Value: "فارسی"}, }, }, + { + LangPack: "webk", + LangCode: "en", + Version: 2, + Strings: []domain.LangPackString{{Key: "lng_settings_language", Value: "Language"}}, + }, + { + LangPack: "webk", + LangCode: "zh-hans", + Version: 2, + Strings: []domain.LangPackString{{Key: "lng_settings_language", Value: "语言"}}, + }, } { if err := store.UpsertPack(ctx, pack); err != nil { t.Fatalf("seed %s/%s: %v", pack.LangPack, pack.LangCode, err) @@ -272,6 +399,23 @@ func dispatchLangpackLanguages(t *testing.T, r *Router, ctx context.Context, in return langs.Elems } +func dispatchLangpackDifference(t *testing.T, r *Router, ctx context.Context, in *bin.Buffer) tg.LangPackDifference { + t.Helper() + enc, err := r.Dispatch(ctx, [8]byte{}, 0, in) + if err != nil { + t.Fatalf("dispatch langpack difference: %v", err) + } + var out bin.Buffer + if err := enc.Encode(&out); err != nil { + t.Fatalf("encode langpack difference: %v", err) + } + var diff tg.LangPackDifference + if err := diff.Decode(&out); err != nil { + t.Fatalf("decode langpack difference: %v", err) + } + return diff +} + func assertHasLangpackLanguage(t *testing.T, langs []tg.LangPackLanguage, code string) { t.Helper() for _, lang := range langs {