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
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()
}
2 changes: 1 addition & 1 deletion internal/entity/user_entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ type User struct {
Status int `xorm:"not null default 1 INT(11) status"`
AuthorityGroup int `xorm:"not null default 1 INT(11) authority_group"`
DisplayName string `xorm:"not null default '' VARCHAR(30) display_name"`
Avatar string `xorm:"not null default '' VARCHAR(1024) avatar"`
Avatar string `xorm:"not null default '' VARCHAR(2048) avatar"`
Mobile string `xorm:"not null VARCHAR(20) mobile"`
Bio string `xorm:"not null TEXT bio"`
BioHTML string `xorm:"not null TEXT bio_html"`
Expand Down
1 change: 1 addition & 0 deletions internal/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ var migrations = []Migration{
NewMigration("v1.5.1", "add plugin kv storage", addPluginKVStorage, true),
NewMigration("v1.6.0", "move user config to interface", moveUserConfigToInterface, true),
NewMigration("v1.7.0", "add optional tags", addOptionalTags, true),
NewMigration("v1.7.2", "expand avatar column length", expandAvatarColumnLength, false),
}

func GetMigrations() []Migration {
Expand Down
37 changes: 37 additions & 0 deletions internal/migrations/v29.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package migrations

import (
"context"
"fmt"

"xorm.io/xorm"
)

func expandAvatarColumnLength(ctx context.Context, x *xorm.Engine) error {
type User struct {
Avatar string `xorm:"not null default '' VARCHAR(2048) avatar"`
}
if err := x.Context(ctx).Sync(new(User)); err != nil {
return fmt.Errorf("expand avatar column length failed: %w", err)
}
return nil
}
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