From 3000e3aec0814b710d90bc853115ac36078cfe23 Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Mon, 15 Dec 2025 15:48:45 +0800 Subject: [PATCH 1/2] Fix/translation (#1460) --- internal/base/handler/lang.go | 11 ++ internal/base/translator/provider.go | 165 +++++++++++++++++++++++++++ ui/src/pages/SideNavLayout/index.tsx | 2 +- 3 files changed, 177 insertions(+), 1 deletion(-) diff --git a/internal/base/handler/lang.go b/internal/base/handler/lang.go index 8886f0631..202449e9d 100644 --- a/internal/base/handler/lang.go +++ b/internal/base/handler/lang.go @@ -23,11 +23,22 @@ import ( "context" "github.com/apache/answer/internal/base/constant" + "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/i18n" ) // GetLangByCtx get language from header func GetLangByCtx(ctx context.Context) i18n.Language { + if ginCtx, ok := ctx.(*gin.Context); ok { + acceptLanguage, ok := ginCtx.Get(constant.AcceptLanguageFlag) + if ok { + if acceptLanguage, ok := acceptLanguage.(i18n.Language); ok { + return acceptLanguage + } + return i18n.DefaultLanguage + } + } + acceptLanguage, ok := ctx.Value(constant.AcceptLanguageContextKey).(i18n.Language) if ok { return acceptLanguage diff --git a/internal/base/translator/provider.go b/internal/base/translator/provider.go index 47212e84f..1a465b1e8 100644 --- a/internal/base/translator/provider.go +++ b/internal/base/translator/provider.go @@ -23,6 +23,8 @@ import ( "fmt" "os" "path/filepath" + "sort" + "strings" "github.com/google/wire" myTran "github.com/segmentfault/pacman/contrib/i18n" @@ -100,6 +102,7 @@ func NewTranslator(c *I18n) (tr i18n.Translator, err error) { // add translator use backend translation if err = myTran.AddTranslator(content, file.Name()); err != nil { log.Debugf("add translator failed: %s %s", file.Name(), err) + reportTranslatorFormatError(file.Name(), buf) continue } } @@ -160,3 +163,165 @@ func TrWithData(lang i18n.Language, key string, templateData any) string { } return translation } + +// reportTranslatorFormatError re-parses the YAML file to locate the invalid entry +// when go-i18n fails to add the translator. +func reportTranslatorFormatError(fileName string, content []byte) { + var raw any + if err := yaml.Unmarshal(content, &raw); err != nil { + log.Errorf("parse translator file %s failed when diagnosing format error: %s", fileName, err) + return + } + if err := inspectTranslatorNode(raw, nil, true); err != nil { + log.Errorf("translator file %s invalid: %s", fileName, err) + } +} + +func inspectTranslatorNode(node any, path []string, isRoot bool) error { + switch data := node.(type) { + case nil: + if isRoot { + return fmt.Errorf("root value is empty") + } + return fmt.Errorf("%s contains an empty value", formatTranslationPath(path)) + case string: + if isRoot { + return fmt.Errorf("root value must be an object but found string") + } + return nil + case bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: + if isRoot { + return fmt.Errorf("root value must be an object but found %T", data) + } + return fmt.Errorf("%s expects a string translation but found %T", formatTranslationPath(path), data) + case map[string]any: + if isMessageMap(data) { + return nil + } + keys := make([]string, 0, len(data)) + for key := range data { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + if err := inspectTranslatorNode(data[key], append(path, key), false); err != nil { + return err + } + } + return nil + case map[string]string: + mapped := make(map[string]any, len(data)) + for k, v := range data { + mapped[k] = v + } + return inspectTranslatorNode(mapped, path, isRoot) + case map[any]any: + if isMessageMap(data) { + return nil + } + type kv struct { + key string + val any + } + items := make([]kv, 0, len(data)) + for key, val := range data { + strKey, ok := key.(string) + if !ok { + return fmt.Errorf("%s uses a non-string key %#v", formatTranslationPath(path), key) + } + items = append(items, kv{key: strKey, val: val}) + } + sort.Slice(items, func(i, j int) bool { + return items[i].key < items[j].key + }) + for _, item := range items { + if err := inspectTranslatorNode(item.val, append(path, item.key), false); err != nil { + return err + } + } + return nil + case []any: + for idx, child := range data { + nextPath := append(path, fmt.Sprintf("[%d]", idx)) + if err := inspectTranslatorNode(child, nextPath, false); err != nil { + return err + } + } + return nil + case []map[string]any: + for idx, child := range data { + nextPath := append(path, fmt.Sprintf("[%d]", idx)) + if err := inspectTranslatorNode(child, nextPath, false); err != nil { + return err + } + } + return nil + default: + if isRoot { + return fmt.Errorf("root value must be an object but found %T", data) + } + return fmt.Errorf("%s contains unsupported value type %T", formatTranslationPath(path), data) + } +} + +var translatorReservedKeys = []string{ + "id", "description", "hash", "leftdelim", "rightdelim", + "zero", "one", "two", "few", "many", "other", +} + +func isMessageMap(data any) bool { + switch v := data.(type) { + case map[string]any: + for _, key := range translatorReservedKeys { + val, ok := v[key] + if !ok { + continue + } + if _, ok := val.(string); ok { + return true + } + } + case map[string]string: + for _, key := range translatorReservedKeys { + val, ok := v[key] + if !ok { + continue + } + if val != "" { + return true + } + } + case map[any]any: + for _, key := range translatorReservedKeys { + val, ok := v[key] + if !ok { + continue + } + if _, ok := val.(string); ok { + return true + } + } + } + return false +} + +func formatTranslationPath(path []string) string { + if len(path) == 0 { + return "root" + } + var b strings.Builder + for _, part := range path { + if part == "" { + continue + } + if part[0] == '[' { + b.WriteString(part) + continue + } + if b.Len() > 0 { + b.WriteByte('.') + } + b.WriteString(part) + } + return b.String() +} diff --git a/ui/src/pages/SideNavLayout/index.tsx b/ui/src/pages/SideNavLayout/index.tsx index 907b9b281..b9bc38b9c 100644 --- a/ui/src/pages/SideNavLayout/index.tsx +++ b/ui/src/pages/SideNavLayout/index.tsx @@ -38,7 +38,7 @@ const Index: FC = () => { -
+
From a9ade924701835bcbbf1f9a101a3efc3a16ac7d5 Mon Sep 17 00:00:00 2001 From: Douglas Cortez Date: Wed, 24 Dec 2025 00:24:21 -0300 Subject: [PATCH 2/2] fix(notification): use SSO provider for external_id lookup in notifications --- .../user_external_login_repo.go | 2 +- .../notification/new_question_notification.go | 15 +++++++++------ .../service/notification_common/notification.go | 15 +++++++-------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/internal/repo/user_external_login/user_external_login_repo.go b/internal/repo/user_external_login/user_external_login_repo.go index c797e461d..b5cf85e86 100644 --- a/internal/repo/user_external_login/user_external_login_repo.go +++ b/internal/repo/user_external_login/user_external_login_repo.go @@ -87,7 +87,7 @@ func (ur *userExternalLoginRepo) GetByUserID(ctx context.Context, provider, user func (ur *userExternalLoginRepo) GetUserExternalLoginList(ctx context.Context, userID string) ( resp []*entity.UserExternalLogin, err error) { resp = make([]*entity.UserExternalLogin, 0) - err = ur.data.DB.Context(ctx).Where("user_id = ?", userID).Find(&resp) + err = ur.data.DB.Context(ctx).Where("user_id = ?", userID).OrderBy("updated_at DESC").Find(&resp) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } diff --git a/internal/service/notification/new_question_notification.go b/internal/service/notification/new_question_notification.go index 2f83042b2..085b3ec45 100644 --- a/internal/service/notification/new_question_notification.go +++ b/internal/service/notification/new_question_notification.go @@ -238,14 +238,17 @@ func (ns *ExternalNotificationService) syncNewQuestionNotificationToPlugin(ctx c } } - userInfo, exist, err := ns.userExternalLoginRepo.GetByUserID(ctx, fn.Info().SlugName, subscriberUserID) + externalLogins, err := ns.userExternalLoginRepo.GetUserExternalLoginList(ctx, subscriberUserID) if err != nil { - log.Errorf("get user external login info failed: %v", err) - return nil - } - if exist { - newMsg.ReceiverExternalID = userInfo.ExternalID + log.Errorf("get user external login list failed for user %s: %v", subscriberUserID, err) + } else if len(externalLogins) > 0 { + newMsg.ReceiverExternalID = externalLogins[0].ExternalID + if len(externalLogins) > 1 { + log.Debugf("user %s has %d SSO logins, using most recent: provider=%s", + subscriberUserID, len(externalLogins), externalLogins[0].Provider) + } } + fn.Notify(newMsg) } return nil diff --git a/internal/service/notification_common/notification.go b/internal/service/notification_common/notification.go index 0bbd1865f..5dbadcb96 100644 --- a/internal/service/notification_common/notification.go +++ b/internal/service/notification_common/notification.go @@ -423,15 +423,14 @@ func (ns *NotificationCommon) syncNotificationToPlugin(ctx context.Context, objI } } + externalLogins, err := ns.userExternalLoginRepo.GetUserExternalLoginList(ctx, msg.ReceiverUserID) + if err != nil { + log.Errorf("get user external login list failed for user %s: %v", msg.ReceiverUserID, err) + } else if len(externalLogins) > 0 { + pluginNotificationMsg.ReceiverExternalID = externalLogins[0].ExternalID + } + _ = plugin.CallNotification(func(fn plugin.Notification) error { - userInfo, exist, err := ns.userExternalLoginRepo.GetByUserID(ctx, fn.Info().SlugName, msg.ReceiverUserID) - if err != nil { - log.Errorf("get user external login info failed: %v", err) - return nil - } - if exist { - pluginNotificationMsg.ReceiverExternalID = userInfo.ExternalID - } fn.Notify(pluginNotificationMsg) return nil })