Skip to content

Commit

Permalink
support setting decimal separator and digit grouping symbol
Browse files Browse the repository at this point in the history
  • Loading branch information
mayswind committed Jun 29, 2024
1 parent d9c8142 commit 399413a
Show file tree
Hide file tree
Showing 51 changed files with 1,278 additions and 580 deletions.
3 changes: 3 additions & 0 deletions cmd/user_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,9 @@ func printUserInfo(user *models.User) {
fmt.Printf("[ShortDateFormat] %s (%d)\n", user.ShortDateFormat, user.ShortDateFormat)
fmt.Printf("[LongTimeFormat] %s (%d)\n", user.LongTimeFormat, user.LongTimeFormat)
fmt.Printf("[ShortTimeFormat] %s (%d)\n", user.ShortTimeFormat, user.ShortTimeFormat)
fmt.Printf("[DecimalSeparator] %s (%d)\n", user.DecimalSeparator, user.DecimalSeparator)
fmt.Printf("[DigitGroupingSymbol] %s (%d)\n", user.DigitGroupingSymbol, user.DigitGroupingSymbol)
fmt.Printf("[DigitGrouping] %s (%d)\n", user.DigitGrouping, user.DigitGrouping)
fmt.Printf("[Deleted] %t\n", user.Deleted)
fmt.Printf("[EmailVerified] %t\n", user.EmailVerified)
fmt.Printf("[CreatedAt] %s (%d)\n", utils.FormatUnixTimeToLongDateTimeInServerTimezone(user.CreatedUnixTime), user.CreatedUnixTime)
Expand Down
52 changes: 52 additions & 0 deletions pkg/api/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/locales"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
Expand Down Expand Up @@ -336,6 +337,57 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
userNew.ShortTimeFormat = models.SHORT_TIME_FORMAT_INVALID
}

if userUpdateReq.DecimalSeparator != nil && *userUpdateReq.DecimalSeparator != user.DecimalSeparator {
user.DecimalSeparator = *userUpdateReq.DecimalSeparator
userNew.DecimalSeparator = *userUpdateReq.DecimalSeparator
anythingUpdate = true
} else {
userNew.DecimalSeparator = models.DECIMAL_SEPARATOR_INVALID
}

if userUpdateReq.DigitGroupingSymbol != nil && *userUpdateReq.DigitGroupingSymbol != user.DigitGroupingSymbol {
user.DigitGroupingSymbol = *userUpdateReq.DigitGroupingSymbol
userNew.DigitGroupingSymbol = *userUpdateReq.DigitGroupingSymbol
anythingUpdate = true
} else {
userNew.DigitGroupingSymbol = models.DIGIT_GROUPING_SYMBOL_INVALID
}

if userUpdateReq.DigitGrouping != nil && *userUpdateReq.DigitGrouping != user.DigitGrouping {
user.DigitGrouping = *userUpdateReq.DigitGrouping
userNew.DigitGrouping = *userUpdateReq.DigitGrouping
anythingUpdate = true
} else {
userNew.DigitGrouping = models.DIGIT_GROUPING_TYPE_INVALID
}

if modifyUserLanguage || userNew.DecimalSeparator != models.DECIMAL_SEPARATOR_INVALID || userNew.DigitGroupingSymbol != models.DIGIT_GROUPING_SYMBOL_INVALID {
decimalSeparator := userNew.DecimalSeparator
digitGroupingSymbol := userNew.DigitGroupingSymbol

if userNew.DecimalSeparator == models.DECIMAL_SEPARATOR_INVALID {
decimalSeparator = user.DecimalSeparator
}

if userNew.DigitGroupingSymbol == models.DIGIT_GROUPING_SYMBOL_INVALID {
digitGroupingSymbol = user.DigitGroupingSymbol
}

locale := user.Language

if modifyUserLanguage {
locale = userNew.Language
}

if locale == "" {
locale = c.GetClientLocale()
}

if locales.IsDecimalSeparatorEqualsDigitGroupingSymbol(decimalSeparator, digitGroupingSymbol, locale) {
return nil, errs.ErrDecimalSeparatorAndDigitGroupingSymbolCannotBeEqual
}
}

if !anythingUpdate {
return nil, errs.ErrNothingWillBeUpdated
}
Expand Down
47 changes: 24 additions & 23 deletions pkg/errs/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,28 @@ import (

// Error codes related to users
var (
ErrLoginNameInvalid = NewNormalError(NormalSubcategoryUser, 0, http.StatusUnauthorized, "login name is invalid")
ErrLoginNameOrPasswordInvalid = NewNormalError(NormalSubcategoryUser, 1, http.StatusUnauthorized, "login name or password is invalid")
ErrLoginNameOrPasswordWrong = NewNormalError(NormalSubcategoryUser, 2, http.StatusUnauthorized, "login name or password is wrong")
ErrUserIdInvalid = NewNormalError(NormalSubcategoryUser, 3, http.StatusBadRequest, "user id is invalid")
ErrUsernameIsEmpty = NewNormalError(NormalSubcategoryUser, 4, http.StatusBadRequest, "username is empty")
ErrEmailIsEmpty = NewNormalError(NormalSubcategoryUser, 5, http.StatusBadRequest, "email is empty")
ErrNicknameIsEmpty = NewNormalError(NormalSubcategoryUser, 6, http.StatusBadRequest, "nickname is empty")
ErrPasswordIsEmpty = NewNormalError(NormalSubcategoryUser, 7, http.StatusBadRequest, "password is empty")
ErrUserDefaultCurrencyIsEmpty = NewNormalError(NormalSubcategoryUser, 8, http.StatusBadRequest, "user default currency is empty")
ErrUserDefaultCurrencyIsInvalid = NewNormalError(NormalSubcategoryUser, 9, http.StatusBadRequest, "user default currency is invalid")
ErrUserNotFound = NewNormalError(NormalSubcategoryUser, 10, http.StatusBadRequest, "user not found")
ErrUserPasswordWrong = NewNormalError(NormalSubcategoryUser, 11, http.StatusBadRequest, "password is wrong")
ErrUsernameAlreadyExists = NewNormalError(NormalSubcategoryUser, 12, http.StatusBadRequest, "username already exists")
ErrUserEmailAlreadyExists = NewNormalError(NormalSubcategoryUser, 13, http.StatusBadRequest, "email already exists")
ErrUserRegistrationNotAllowed = NewNormalError(NormalSubcategoryUser, 14, http.StatusBadRequest, "user registration not allowed")
ErrUserDefaultAccountIsInvalid = NewNormalError(NormalSubcategoryUser, 15, http.StatusBadRequest, "user default account is invalid")
ErrUserIsDisabled = NewNormalError(NormalSubcategoryUser, 16, http.StatusBadRequest, "user is disabled")
ErrEmptyIsInvalid = NewNormalError(NormalSubcategoryUser, 17, http.StatusBadRequest, "email is invalid")
ErrEmailIsEmptyOrInvalid = NewNormalError(NormalSubcategoryUser, 18, http.StatusBadRequest, "email is empty or invalid")
ErrNewPasswordEqualsOldInvalid = NewNormalError(NormalSubcategoryUser, 19, http.StatusBadRequest, "new password equals old password")
ErrEmailIsNotVerified = NewNormalError(NormalSubcategoryUser, 20, http.StatusBadRequest, "email is not verified")
ErrEmailIsVerified = NewNormalError(NormalSubcategoryUser, 21, http.StatusBadRequest, "email is verified")
ErrEmailValidationNotAllowed = NewNormalError(NormalSubcategoryUser, 22, http.StatusBadRequest, "email validation not allowed")
ErrLoginNameInvalid = NewNormalError(NormalSubcategoryUser, 0, http.StatusUnauthorized, "login name is invalid")
ErrLoginNameOrPasswordInvalid = NewNormalError(NormalSubcategoryUser, 1, http.StatusUnauthorized, "login name or password is invalid")
ErrLoginNameOrPasswordWrong = NewNormalError(NormalSubcategoryUser, 2, http.StatusUnauthorized, "login name or password is wrong")
ErrUserIdInvalid = NewNormalError(NormalSubcategoryUser, 3, http.StatusBadRequest, "user id is invalid")
ErrUsernameIsEmpty = NewNormalError(NormalSubcategoryUser, 4, http.StatusBadRequest, "username is empty")
ErrEmailIsEmpty = NewNormalError(NormalSubcategoryUser, 5, http.StatusBadRequest, "email is empty")
ErrNicknameIsEmpty = NewNormalError(NormalSubcategoryUser, 6, http.StatusBadRequest, "nickname is empty")
ErrPasswordIsEmpty = NewNormalError(NormalSubcategoryUser, 7, http.StatusBadRequest, "password is empty")
ErrUserDefaultCurrencyIsEmpty = NewNormalError(NormalSubcategoryUser, 8, http.StatusBadRequest, "user default currency is empty")
ErrUserDefaultCurrencyIsInvalid = NewNormalError(NormalSubcategoryUser, 9, http.StatusBadRequest, "user default currency is invalid")
ErrUserNotFound = NewNormalError(NormalSubcategoryUser, 10, http.StatusBadRequest, "user not found")
ErrUserPasswordWrong = NewNormalError(NormalSubcategoryUser, 11, http.StatusBadRequest, "password is wrong")
ErrUsernameAlreadyExists = NewNormalError(NormalSubcategoryUser, 12, http.StatusBadRequest, "username already exists")
ErrUserEmailAlreadyExists = NewNormalError(NormalSubcategoryUser, 13, http.StatusBadRequest, "email already exists")
ErrUserRegistrationNotAllowed = NewNormalError(NormalSubcategoryUser, 14, http.StatusBadRequest, "user registration not allowed")
ErrUserDefaultAccountIsInvalid = NewNormalError(NormalSubcategoryUser, 15, http.StatusBadRequest, "user default account is invalid")
ErrUserIsDisabled = NewNormalError(NormalSubcategoryUser, 16, http.StatusBadRequest, "user is disabled")
ErrEmptyIsInvalid = NewNormalError(NormalSubcategoryUser, 17, http.StatusBadRequest, "email is invalid")
ErrEmailIsEmptyOrInvalid = NewNormalError(NormalSubcategoryUser, 18, http.StatusBadRequest, "email is empty or invalid")
ErrNewPasswordEqualsOldInvalid = NewNormalError(NormalSubcategoryUser, 19, http.StatusBadRequest, "new password equals old password")
ErrEmailIsNotVerified = NewNormalError(NormalSubcategoryUser, 20, http.StatusBadRequest, "email is not verified")
ErrEmailIsVerified = NewNormalError(NormalSubcategoryUser, 21, http.StatusBadRequest, "email is verified")
ErrEmailValidationNotAllowed = NewNormalError(NormalSubcategoryUser, 22, http.StatusBadRequest, "email validation not allowed")
ErrDecimalSeparatorAndDigitGroupingSymbolCannotBeEqual = NewNormalError(NormalSubcategoryUser, 23, http.StatusBadRequest, "decimal separator and digit grouping symbol cannot be equal")
)
24 changes: 24 additions & 0 deletions pkg/locales/all_locales.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package locales

import "github.com/mayswind/ezbookkeeping/pkg/models"

// DefaultLanguage represents the default language
var DefaultLanguage = en

Expand All @@ -22,3 +24,25 @@ func GetLocaleTextItems(locale string) *LocaleTextItems {

return DefaultLanguage
}

func IsDecimalSeparatorEqualsDigitGroupingSymbol(decimalSeparator models.DecimalSeparator, digitGroupingSymbol models.DigitGroupingSymbol, locale string) bool {
if decimalSeparator == models.DECIMAL_SEPARATOR_DEFAULT && digitGroupingSymbol == models.DIGIT_GROUPING_SYMBOL_DEFAULT {
return false
}

if byte(decimalSeparator) == byte(digitGroupingSymbol) {
return true
}

localeTextItems := GetLocaleTextItems(locale)

if decimalSeparator == models.DECIMAL_SEPARATOR_DEFAULT {
decimalSeparator = localeTextItems.DefaultTypes.DecimalSeparator
}

if digitGroupingSymbol == models.DIGIT_GROUPING_SYMBOL_DEFAULT {
digitGroupingSymbol = localeTextItems.DefaultTypes.DigitGroupingSymbol
}

return byte(decimalSeparator) == byte(digitGroupingSymbol)
}
8 changes: 8 additions & 0 deletions pkg/locales/base.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
package locales

import "github.com/mayswind/ezbookkeeping/pkg/models"

// LocaleTextItems represents all text items need to be translated
type LocaleTextItems struct {
DefaultTypes *DefaultTypes
VerifyEmailTextItems *VerifyEmailTextItems
ForgetPasswordMailTextItems *ForgetPasswordMailTextItems
}

type DefaultTypes struct {
DecimalSeparator models.DecimalSeparator
DigitGroupingSymbol models.DigitGroupingSymbol
}

// VerifyEmailTextItems represents text items need to be translated in verify mail
type VerifyEmailTextItems struct {
Title string
Expand Down
6 changes: 6 additions & 0 deletions pkg/locales/en.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package locales

import "github.com/mayswind/ezbookkeeping/pkg/models"

var en = &LocaleTextItems{
DefaultTypes: &DefaultTypes{
DecimalSeparator: models.DECIMAL_SEPARATOR_DOT,
DigitGroupingSymbol: models.DIGIT_GROUPING_SYMBOL_COMMA,
},
VerifyEmailTextItems: &VerifyEmailTextItems{
Title: "Verify Email",
SalutationFormat: "Hi %s,",
Expand Down
6 changes: 6 additions & 0 deletions pkg/locales/zh_hans.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package locales

import "github.com/mayswind/ezbookkeeping/pkg/models"

var zhHans = &LocaleTextItems{
DefaultTypes: &DefaultTypes{
DecimalSeparator: models.DECIMAL_SEPARATOR_DOT,
DigitGroupingSymbol: models.DIGIT_GROUPING_SYMBOL_COMMA,
},
VerifyEmailTextItems: &VerifyEmailTextItems{
Title: "验证邮箱",
SalutationFormat: "%s 您好,",
Expand Down
95 changes: 95 additions & 0 deletions pkg/models/numeral.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package models

import (
"fmt"
)

// DecimalSeparator represents the type of decimal separator
type DecimalSeparator byte

// Decimal Separator
const (
DECIMAL_SEPARATOR_DEFAULT DecimalSeparator = 0
DECIMAL_SEPARATOR_DOT DecimalSeparator = 1
DECIMAL_SEPARATOR_COMMA DecimalSeparator = 2
DECIMAL_SEPARATOR_SPACE DecimalSeparator = 3
DECIMAL_SEPARATOR_INVALID DecimalSeparator = 255
)

// String returns a textual representation of the decimal separator enum
func (f DecimalSeparator) String() string {
switch f {
case DECIMAL_SEPARATOR_DEFAULT:
return "Default"
case DECIMAL_SEPARATOR_DOT:
return "Dot"
case DECIMAL_SEPARATOR_COMMA:
return "Comma"
case DECIMAL_SEPARATOR_SPACE:
return "Space"
case DECIMAL_SEPARATOR_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(f))
}
}

// DigitGroupingSymbol represents the digit grouping symbol
type DigitGroupingSymbol byte

// Digit Grouping Symbol
const (
DIGIT_GROUPING_SYMBOL_DEFAULT DigitGroupingSymbol = 0
DIGIT_GROUPING_SYMBOL_DOT DigitGroupingSymbol = 1
DIGIT_GROUPING_SYMBOL_COMMA DigitGroupingSymbol = 2
DIGIT_GROUPING_SYMBOL_SPACE DigitGroupingSymbol = 3
DIGIT_GROUPING_SYMBOL_APOSTROPHE DigitGroupingSymbol = 4
DIGIT_GROUPING_SYMBOL_INVALID DigitGroupingSymbol = 255
)

// String returns a textual representation of the digit grouping symbol enum
func (f DigitGroupingSymbol) String() string {
switch f {
case DIGIT_GROUPING_SYMBOL_DEFAULT:
return "Default"
case DIGIT_GROUPING_SYMBOL_DOT:
return "Dot"
case DIGIT_GROUPING_SYMBOL_COMMA:
return "Comma"
case DIGIT_GROUPING_SYMBOL_SPACE:
return "Space"
case DIGIT_GROUPING_SYMBOL_APOSTROPHE:
return "Apostrophe"
case DIGIT_GROUPING_SYMBOL_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(f))
}
}

// DigitGroupingType represents digit grouping type
type DigitGroupingType byte

// Digit Grouping Type
const (
DIGIT_GROUPING_TYPE_DEFAULT DigitGroupingType = 0
DIGIT_GROUPING_TYPE_NONE DigitGroupingType = 1
DIGIT_GROUPING_TYPE_THOUSANDS_SEPARATOR DigitGroupingType = 2
DIGIT_GROUPING_TYPE_INVALID DigitGroupingType = 255
)

// String returns a textual representation of the digit grouping type enum
func (d DigitGroupingType) String() string {
switch d {
case DIGIT_GROUPING_TYPE_DEFAULT:
return "Default"
case DIGIT_GROUPING_TYPE_NONE:
return "None"
case DIGIT_GROUPING_TYPE_THOUSANDS_SEPARATOR:
return "Thousands Separator"
case DIGIT_GROUPING_TYPE_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(d))
}
}
18 changes: 18 additions & 0 deletions pkg/models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ type User struct {
ShortDateFormat ShortDateFormat `xorm:"TINYINT"`
LongTimeFormat LongTimeFormat `xorm:"TINYINT"`
ShortTimeFormat ShortTimeFormat `xorm:"TINYINT"`
DecimalSeparator DecimalSeparator `xorm:"TINYINT"`
DigitGroupingSymbol DigitGroupingSymbol `xorm:"TINYINT"`
DigitGrouping DigitGroupingType `xorm:"TINYINT"`
Disabled bool
Deleted bool `xorm:"NOT NULL"`
EmailVerified bool `xorm:"NOT NULL"`
Expand All @@ -89,6 +92,9 @@ type UserBasicInfo struct {
ShortDateFormat ShortDateFormat `json:"shortDateFormat"`
LongTimeFormat LongTimeFormat `json:"longTimeFormat"`
ShortTimeFormat ShortTimeFormat `json:"shortTimeFormat"`
DecimalSeparator DecimalSeparator `json:"decimalSeparator"`
DigitGroupingSymbol DigitGroupingSymbol `json:"digitGroupingSymbol"`
DigitGrouping DigitGroupingType `json:"digitGrouping"`
EmailVerified bool `json:"emailVerified"`
}

Expand Down Expand Up @@ -142,6 +148,9 @@ type UserProfileUpdateRequest struct {
ShortDateFormat *ShortDateFormat `json:"shortDateFormat" binding:"omitempty,min=0,max=3"`
LongTimeFormat *LongTimeFormat `json:"longTimeFormat" binding:"omitempty,min=0,max=3"`
ShortTimeFormat *ShortTimeFormat `json:"shortTimeFormat" binding:"omitempty,min=0,max=3"`
DecimalSeparator *DecimalSeparator `json:"decimalSeparator" binding:"omitempty,min=0,max=3"`
DigitGroupingSymbol *DigitGroupingSymbol `json:"digitGroupingSymbol" binding:"omitempty,min=0,max=4"`
DigitGrouping *DigitGroupingType `json:"digitGrouping" binding:"omitempty,min=0,max=2"`
}

// UserProfileUpdateResponse represents the data returns to frontend after updating profile
Expand All @@ -166,6 +175,9 @@ type UserProfileResponse struct {
ShortDateFormat ShortDateFormat `json:"shortDateFormat"`
LongTimeFormat LongTimeFormat `json:"longTimeFormat"`
ShortTimeFormat ShortTimeFormat `json:"shortTimeFormat"`
DecimalSeparator DecimalSeparator `json:"decimalSeparator"`
DigitGroupingSymbol DigitGroupingSymbol `json:"digitGroupingSymbol"`
DigitGrouping DigitGroupingType `json:"digitGrouping"`
EmailVerified bool `json:"emailVerified"`
LastLoginAt int64 `json:"lastLoginAt"`
}
Expand Down Expand Up @@ -229,6 +241,9 @@ func (u *User) ToUserBasicInfo() *UserBasicInfo {
ShortDateFormat: u.ShortDateFormat,
LongTimeFormat: u.LongTimeFormat,
ShortTimeFormat: u.ShortTimeFormat,
DecimalSeparator: u.DecimalSeparator,
DigitGroupingSymbol: u.DigitGroupingSymbol,
DigitGrouping: u.DigitGrouping,
EmailVerified: u.EmailVerified,
}
}
Expand All @@ -250,6 +265,9 @@ func (u *User) ToUserProfileResponse() *UserProfileResponse {
ShortDateFormat: u.ShortDateFormat,
LongTimeFormat: u.LongTimeFormat,
ShortTimeFormat: u.ShortTimeFormat,
DecimalSeparator: u.DecimalSeparator,
DigitGroupingSymbol: u.DigitGroupingSymbol,
DigitGrouping: u.DigitGrouping,
EmailVerified: u.EmailVerified,
LastLoginAt: u.LastLoginUnixTime,
}
Expand Down
12 changes: 12 additions & 0 deletions pkg/services/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,18 @@ func (s *UserService) UpdateUser(c *core.Context, user *models.User, modifyUserL
updateCols = append(updateCols, "short_time_format")
}

if models.DECIMAL_SEPARATOR_DEFAULT <= user.DecimalSeparator && user.DecimalSeparator <= models.DECIMAL_SEPARATOR_SPACE {
updateCols = append(updateCols, "decimal_separator")
}

if models.DIGIT_GROUPING_SYMBOL_DEFAULT <= user.DigitGroupingSymbol && user.DigitGroupingSymbol <= models.DIGIT_GROUPING_SYMBOL_APOSTROPHE {
updateCols = append(updateCols, "digit_grouping_symbol")
}

if models.DIGIT_GROUPING_TYPE_DEFAULT <= user.DigitGrouping && user.DigitGrouping <= models.DIGIT_GROUPING_TYPE_THOUSANDS_SEPARATOR {
updateCols = append(updateCols, "digit_grouping")
}

user.UpdatedUnixTime = now
updateCols = append(updateCols, "updated_unix_time")

Expand Down
Loading

0 comments on commit 399413a

Please sign in to comment.