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 = () => { -
+