Merge branch 'feat/language' into 'test'

Feat/language

See merge request 
This commit is contained in:
linkinstar 2022-11-02 07:33:57 +00:00
commit ada9daebf6
19 changed files with 346 additions and 75 deletions

View File

@ -76,7 +76,6 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
if err != nil {
return nil, nil, err
}
langController := controller.NewLangController(i18nTranslator)
engine, err := data.NewDB(debug, dbConf)
if err != nil {
return nil, nil, err
@ -90,17 +89,19 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
cleanup()
return nil, nil, err
}
siteInfoRepo := site_info.NewSiteInfo(dataData)
configRepo := config.NewConfigRepo(dataData)
emailRepo := export.NewEmailRepo(dataData)
emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo)
siteInfoService := service.NewSiteInfoService(siteInfoRepo, emailService)
langController := controller.NewLangController(i18nTranslator, siteInfoService)
authRepo := auth.NewAuthRepo(dataData)
authService := auth2.NewAuthService(authRepo)
configRepo := config.NewConfigRepo(dataData)
userRepo := user.NewUserRepo(dataData, configRepo)
uniqueIDRepo := unique.NewUniqueIDRepo(dataData)
activityRepo := activity_common.NewActivityRepo(dataData, uniqueIDRepo, configRepo)
userRankRepo := rank.NewUserRankRepo(dataData, configRepo)
userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configRepo)
emailRepo := export.NewEmailRepo(dataData)
siteInfoRepo := site_info.NewSiteInfo(dataData)
emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo)
userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf)
captchaRepo := captcha.NewCaptchaRepo(dataData)
captchaService := action.NewCaptchaService(captchaRepo)
@ -166,7 +167,6 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
reasonService := reason2.NewReasonService(reasonRepo)
reasonController := controller.NewReasonController(reasonService)
themeController := controller_backyard.NewThemeController()
siteInfoService := service.NewSiteInfoService(siteInfoRepo, emailService)
siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService)
siteinfoController := controller.NewSiteinfoController(siteInfoService)
notificationRepo := notification.NewNotificationRepo(dataData)

View File

@ -121,11 +121,6 @@ const docTemplate = `{
},
"/answer/admin/api/language/options": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Get language options",
"produces": [
"application/json"
@ -1432,11 +1427,6 @@ const docTemplate = `{
},
"/answer/api/v1/language/options": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Get language options",
"produces": [
"application/json"
@ -3391,6 +3381,52 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/user/interface": {
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "UserUpdateInterface update user interface config",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "UserUpdateInterface update user interface config",
"parameters": [
{
"type": "string",
"description": "access-token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"description": "UpdateInfoRequest",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.UpdateUserInterfaceRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/user/login/email": {
"post": {
"description": "UserEmailLogin",
@ -4822,6 +4858,10 @@ const docTemplate = `{
"description": "is admin",
"type": "boolean"
},
"language": {
"description": "language",
"type": "string"
},
"last_login_date": {
"description": "last login date",
"type": "integer"
@ -4918,6 +4958,10 @@ const docTemplate = `{
"description": "is admin",
"type": "boolean"
},
"language": {
"description": "language",
"type": "string"
},
"last_login_date": {
"description": "last login date",
"type": "integer"
@ -5583,6 +5627,19 @@ const docTemplate = `{
}
}
},
"schema.UpdateUserInterfaceRequest": {
"type": "object",
"required": [
"language"
],
"properties": {
"language": {
"description": "language",
"type": "string",
"maxLength": 100
}
}
},
"schema.UpdateUserStatusReq": {
"type": "object",
"required": [

View File

@ -109,11 +109,6 @@
},
"/answer/admin/api/language/options": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Get language options",
"produces": [
"application/json"
@ -1420,11 +1415,6 @@
},
"/answer/api/v1/language/options": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Get language options",
"produces": [
"application/json"
@ -3379,6 +3369,52 @@
}
}
},
"/answer/api/v1/user/interface": {
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "UserUpdateInterface update user interface config",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "UserUpdateInterface update user interface config",
"parameters": [
{
"type": "string",
"description": "access-token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"description": "UpdateInfoRequest",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.UpdateUserInterfaceRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/user/login/email": {
"post": {
"description": "UserEmailLogin",
@ -4810,6 +4846,10 @@
"description": "is admin",
"type": "boolean"
},
"language": {
"description": "language",
"type": "string"
},
"last_login_date": {
"description": "last login date",
"type": "integer"
@ -4906,6 +4946,10 @@
"description": "is admin",
"type": "boolean"
},
"language": {
"description": "language",
"type": "string"
},
"last_login_date": {
"description": "last login date",
"type": "integer"
@ -5571,6 +5615,19 @@
}
}
},
"schema.UpdateUserInterfaceRequest": {
"type": "object",
"required": [
"language"
],
"properties": {
"language": {
"description": "language",
"type": "string",
"maxLength": 100
}
}
},
"schema.UpdateUserStatusReq": {
"type": "object",
"required": [

View File

@ -652,6 +652,9 @@ definitions:
is_admin:
description: is admin
type: boolean
language:
description: language
type: string
last_login_date:
description: last login date
type: integer
@ -723,6 +726,9 @@ definitions:
is_admin:
description: is admin
type: boolean
language:
description: language
type: string
last_login_date:
description: last login date
type: integer
@ -1203,6 +1209,15 @@ definitions:
- synonym_tag_list
- tag_id
type: object
schema.UpdateUserInterfaceRequest:
properties:
language:
description: language
maxLength: 100
type: string
required:
- language
type: object
schema.UpdateUserStatusReq:
properties:
status:
@ -1452,8 +1467,6 @@ paths:
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: Get language options
tags:
- Lang
@ -2245,8 +2258,6 @@ paths:
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: Get language options
tags:
- Lang
@ -3433,6 +3444,35 @@ paths:
summary: UserUpdateInfo update user info
tags:
- User
/answer/api/v1/user/interface:
put:
consumes:
- application/json
description: UserUpdateInterface update user interface config
parameters:
- description: access-token
in: header
name: Authorization
required: true
type: string
- description: UpdateInfoRequest
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.UpdateUserInterfaceRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: UserUpdateInterface update user interface config
tags:
- User
/answer/api/v1/user/login/email:
post:
consumes:

6
i18n/i18n.yaml Normal file
View File

@ -0,0 +1,6 @@
# all support language
language_options:
- label: "简体中文(CN)"
value: "zh_CN"
- label: "English(US)"
value: "en_US"

View File

@ -1,17 +1,58 @@
package translator
import (
"fmt"
"os"
"path/filepath"
"github.com/google/wire"
myTran "github.com/segmentfault/pacman/contrib/i18n"
"github.com/segmentfault/pacman/i18n"
"sigs.k8s.io/yaml"
)
// ProviderSet is providers.
var ProviderSet = wire.NewSet(NewTranslator)
var GlobalTrans i18n.Translator
// LangOption language option
type LangOption struct {
Label string `json:"label"`
Value string `json:"value"`
}
// LanguageOptions language
var LanguageOptions []*LangOption
// NewTranslator new a translator
func NewTranslator(c *I18n) (tr i18n.Translator, err error) {
GlobalTrans, err = myTran.NewTranslator(c.BundleDir)
if err != nil {
return nil, err
}
i18nFile, err := os.ReadFile(filepath.Join(c.BundleDir, "i18n.yaml"))
if err != nil {
return nil, fmt.Errorf("read i18n file failed: %s", err)
}
s := struct {
LangOption []*LangOption `json:"language_options"`
}{}
err = yaml.Unmarshal(i18nFile, &s)
if err != nil {
return nil, fmt.Errorf("i18n file parsing failed: %s", err)
}
LanguageOptions = s.LangOption
return GlobalTrans, err
}
// CheckLanguageIsValid check user input language is valid
func CheckLanguageIsValid(lang string) bool {
for _, option := range LanguageOptions {
if option.Value == lang {
return true
}
}
return false
}

View File

@ -4,18 +4,20 @@ import (
"encoding/json"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/service"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/i18n"
)
type LangController struct {
translator i18n.Translator
translator i18n.Translator
siteInfoService *service.SiteInfoService
}
// NewLangController new language controller.
func NewLangController(tr i18n.Translator) *LangController {
return &LangController{translator: tr}
func NewLangController(tr i18n.Translator, siteInfoService *service.SiteInfoService) *LangController {
return &LangController{translator: tr, siteInfoService: siteInfoService}
}
// GetLangMapping get language config mapping
@ -33,15 +35,38 @@ func (u *LangController) GetLangMapping(ctx *gin.Context) {
handler.HandleResponse(ctx, nil, resp)
}
// GetLangOptions Get language options
// GetAdminLangOptions Get language options
// @Summary Get language options
// @Description Get language options
// @Security ApiKeyAuth
// @Tags Lang
// @Produce json
// @Success 200 {object} handler.RespBody{}
// @Router /answer/api/v1/language/options [get]
// @Router /answer/admin/api/language/options [get]
func (u *LangController) GetLangOptions(ctx *gin.Context) {
handler.HandleResponse(ctx, nil, schema.GetLangOptions)
func (u *LangController) GetAdminLangOptions(ctx *gin.Context) {
handler.HandleResponse(ctx, nil, translator.LanguageOptions)
}
// GetUserLangOptions Get language options
// @Summary Get language options
// @Description Get language options
// @Tags Lang
// @Produce json
// @Success 200 {object} handler.RespBody{}
// @Router /answer/api/v1/language/options [get]
func (u *LangController) GetUserLangOptions(ctx *gin.Context) {
siteInterfaceResp, err := u.siteInfoService.GetSiteInterface(ctx)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
options := translator.LanguageOptions
if len(siteInterfaceResp.Language) > 0 {
defaultOption := []*translator.LangOption{
{Label: "Default", Value: siteInterfaceResp.Language},
}
options = append(defaultOption, options...)
}
handler.HandleResponse(ctx, nil, options)
}

View File

@ -373,6 +373,27 @@ func (uc *UserController) UserUpdateInfo(ctx *gin.Context) {
handler.HandleResponse(ctx, err, nil)
}
// UserUpdateInterface update user interface config
// @Summary UserUpdateInterface update user interface config
// @Description UserUpdateInterface update user interface config
// @Tags User
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param Authorization header string true "access-token"
// @Param data body schema.UpdateUserInterfaceRequest true "UpdateInfoRequest"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/user/interface [put]
func (uc *UserController) UserUpdateInterface(ctx *gin.Context) {
req := &schema.UpdateUserInterfaceRequest{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserId = middleware.GetLoginUserIDFromContext(ctx)
err := uc.userService.UserUpdateInterface(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// UploadUserAvatar godoc
// @Summary UserUpdateInfo
// @Description UserUpdateInfo

View File

@ -45,6 +45,7 @@ type User struct {
Location string `xorm:"not null default '' VARCHAR(100) location"`
IPInfo string `xorm:"not null default '' VARCHAR(255) ip_info"`
IsAdmin bool `xorm:"not null default false BOOL is_admin"`
Language string `xorm:"not null default '' VARCHAR(100) language"`
}
// TableName user table name

View File

@ -5,7 +5,6 @@ import (
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/entity"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
@ -43,6 +42,7 @@ var noopMigration = func(_ *xorm.Engine) error { return nil }
var migrations = []Migration{
// 0->1
NewMigration("this is first version, no operation", noopMigration),
NewMigration("add user language", addUserLanguage),
}
// GetCurrentDBVersion returns the current db version
@ -86,17 +86,17 @@ func Migrate(dataConf *data.Database) error {
expectedVersion := ExpectedVersion()
for currentDBVersion < expectedVersion {
log.Infof("[migrate] current db version is %d, try to migrate version %d, latest version is %d",
fmt.Printf("[migrate] current db version is %d, try to migrate version %d, latest version is %d\n",
currentDBVersion, currentDBVersion+1, expectedVersion)
migrationFunc := migrations[currentDBVersion]
log.Infof("[migrate] try to migrate db version %d, description: %s", currentDBVersion+1, migrationFunc.Description())
fmt.Printf("[migrate] try to migrate db version %d, description: %s\n", currentDBVersion+1, migrationFunc.Description())
if err := migrationFunc.Migrate(engine); err != nil {
log.Errorf("[migrate] migrate to db version %d failed: ", currentDBVersion+1, err.Error())
fmt.Printf("[migrate] migrate to db version %d failed: %s\n", currentDBVersion+1, err.Error())
return err
}
log.Infof("[migrate] migrate to db version %d success", currentDBVersion+1)
fmt.Printf("[migrate] migrate to db version %d success\n", currentDBVersion+1)
if _, err := engine.Update(&entity.Version{ID: 1, VersionNumber: currentDBVersion + 1}); err != nil {
log.Errorf("[migrate] migrate to db version %d, update failed: %s", currentDBVersion+1, err.Error())
fmt.Printf("[migrate] migrate to db version %d, update failed: %s", currentDBVersion+1, err.Error())
return err
}
currentDBVersion++

12
internal/migrations/v1.go Normal file
View File

@ -0,0 +1,12 @@
package migrations
import (
"xorm.io/xorm"
)
func addUserLanguage(x *xorm.Engine) error {
type User struct {
Language string `xorm:"not null default '' VARCHAR(100) language"`
}
return x.Sync(new(User))
}

View File

@ -101,6 +101,14 @@ func (ur *userRepo) UpdateEmail(ctx context.Context, userID, email string) (err
return
}
func (ur *userRepo) UpdateLanguage(ctx context.Context, userID, language string) (err error) {
_, err = ur.data.DB.Where("id = ?", userID).Update(&entity.User{Language: language})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// UpdateInfo update user info
func (ur *userRepo) UpdateInfo(ctx context.Context, userInfo *entity.User) (err error) {
_, err = ur.data.DB.Where("id = ?", userInfo.ID).

View File

@ -79,7 +79,7 @@ func NewAnswerAPIRouter(
func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
// i18n
r.GET("/language/config", a.langController.GetLangMapping)
r.GET("/language/options", a.langController.GetLangOptions)
r.GET("/language/options", a.langController.GetUserLangOptions)
// comment
r.GET("/comment/page", a.commentController.GetCommentWithPage)
@ -177,6 +177,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
// user
r.PUT("/user/password", a.userController.UserModifyPassWord)
r.PUT("/user/info", a.userController.UserUpdateInfo)
r.PUT("/user/interface", a.userController.UserUpdateInterface)
r.POST("/user/avatar/upload", a.userController.UploadUserAvatar)
r.POST("/user/post/file", a.userController.UploadUserPostFile)
r.POST("/user/notice/set", a.userController.UserNoticeSet)
@ -213,7 +214,7 @@ func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) {
r.GET("/reasons", a.reasonController.Reasons)
// language
r.GET("/language/options", a.langController.GetLangOptions)
r.GET("/language/options", a.langController.GetAdminLangOptions)
// theme
r.GET("/theme/options", a.themeController.GetThemeOptions)

View File

@ -7,6 +7,7 @@ import (
"net/http"
"os"
"github.com/answerdev/answer/i18n"
"github.com/answerdev/answer/ui"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/log"
@ -52,6 +53,7 @@ func (a *UIRouter) Register(r *gin.Engine) {
r.LoadHTMLGlob(staticPath + "/*.html")
r.Static("/static", staticPath+"/static")
r.StaticFS("/i18n/", http.FS(i18n.I18n))
r.NoRoute(func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{})
})

View File

@ -1,18 +0,0 @@
package schema
// GetLangOption get label option
type GetLangOption struct {
Label string `json:"label"`
Value string `json:"value"`
}
var GetLangOptions = []*GetLangOption{
{
Label: "English(US)",
Value: "en_US",
},
{
Label: "中文(CN)",
Value: "zh_CN",
},
}

View File

@ -62,6 +62,8 @@ type GetUserResp struct {
Location string `json:"location"`
// ip info
IPInfo string `json:"ip_info"`
// language
Language string `json:"language"`
// access token
AccessToken string `json:"access_token"`
// is admin
@ -305,6 +307,14 @@ func (u *UpdateInfoRequest) Check() (errField *validator.ErrorField, err error)
return nil, nil
}
// UpdateUserInterfaceRequest update user interface request
type UpdateUserInterfaceRequest struct {
// language
Language string `validate:"required,gt=1,lte=100" json:"language"`
// user id
UserId string `json:"-" `
}
type UserRetrievePassWordRequest struct {
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail" ` // e_mail
CaptchaID string `json:"captcha_id" ` // captcha_id

View File

@ -5,6 +5,7 @@ import (
"encoding/json"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/export"
@ -77,10 +78,9 @@ func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGe
func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.SiteInterfaceReq) (err error) {
var (
siteType = "interface"
themeExist,
langExist bool
content []byte
siteType = "interface"
themeExist bool
content []byte
)
// check theme
@ -96,13 +96,7 @@ func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.Site
}
// check language
for _, lang := range schema.GetLangOptions {
if lang.Value == req.Language {
langExist = true
break
}
}
if !langExist {
if !translator.CheckLanguageIsValid(req.Language) {
err = errors.BadRequest(reason.LangNotFound)
return
}

View File

@ -15,6 +15,7 @@ type UserRepo interface {
UpdateEmailStatus(ctx context.Context, userID string, emailStatus int) error
UpdateNoticeStatus(ctx context.Context, userID string, noticeStatus int) error
UpdateEmail(ctx context.Context, userID, email string) error
UpdateLanguage(ctx context.Context, userID, language string) error
UpdatePass(ctx context.Context, userID, pass string) error
UpdateInfo(ctx context.Context, userInfo *entity.User) (err error)
GetByUserID(ctx context.Context, userID string) (userInfo *entity.User, exist bool, err error)

View File

@ -11,6 +11,7 @@ import (
"github.com/Chain-Zhang/pinyin"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity"
@ -283,6 +284,18 @@ func (us *UserService) UserEmailHas(ctx context.Context, email string) (bool, er
return has, nil
}
// UserUpdateInterface update user interface
func (us *UserService) UserUpdateInterface(ctx context.Context, req *schema.UpdateUserInterfaceRequest) (err error) {
if !translator.CheckLanguageIsValid(req.Language) {
return errors.BadRequest(reason.LangNotFound)
}
err = us.userRepo.UpdateLanguage(ctx, req.UserId, req.Language)
if err != nil {
return
}
return nil
}
// UserRegisterByEmail user register
func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo *schema.UserRegisterReq) (
resp *schema.GetUserResp, err error,