Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions i18n/en_US.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2383,6 +2383,7 @@ ui:
user_normal: This user is already normal.
user_suspended: This user has been suspended.
user_deleted: This user has been deleted.
user_added: User has been added successfully.
badge_activated: This badge has been activated.
badge_inactivated: This badge has been inactivated.
users_deleted: These users have been deleted.
Expand Down
11 changes: 11 additions & 0 deletions internal/base/handler/lang.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
165 changes: 165 additions & 0 deletions internal/base/translator/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"

"github.com/google/wire"
myTran "github.com/segmentfault/pacman/contrib/i18n"
Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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()
}
11 changes: 8 additions & 3 deletions ui/src/pages/Admin/Users/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,14 @@ const Users: FC = () => {
return new Promise((resolve, reject) => {
addUsers(userModel)
.then(() => {
if (/all|staff/.test(curFilter) && curPage === 1) {
refreshUsers();
}
toastStore.getState().show({
msg: t('user_added', { keyPrefix: 'messages' }),
variant: 'success',
});
urlSearchParams.set('filter', 'normal');
urlSearchParams.delete('page');
setUrlSearchParams(urlSearchParams);
refreshUsers();
resolve(true);
})
.catch((e) => {
Expand Down
2 changes: 1 addition & 1 deletion ui/src/pages/SideNavLayout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const Index: FC = () => {
<Outlet />
</div>
</div>
<div className="d-flex justify-content-center">
<div className="d-flex justify-content-center px-0 px-md-4">
<div className="main-mx-with">
<Footer />
</div>
Expand Down