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