feat(i18n): automatically set language during registration

This commit is contained in:
kolaente 2025-03-02 13:21:24 +01:00
parent 4e90c6bb78
commit c6cade3aeb
No known key found for this signature in database
GPG Key ID: F40E70337AB24C9B
7 changed files with 79 additions and 41 deletions

View File

@ -37,6 +37,7 @@ export const SUPPORTED_LOCALES = {
'bg-BG': 'Български',
'ko-KR': '한국어',
// IMPORTANT: Also add new languages to useDayjsLanguageSync
// IMPORTANT: Also add new languages to pkg/i18n/i18n.go
} as const
export type SupportedLocale = keyof typeof SUPPORTED_LOCALES

View File

@ -178,13 +178,25 @@ export const useAuthStore = defineStore('auth', () => {
* Registers a new user and logs them in.
* Not sure if this is the right place to put the logic in, maybe a seperate js component would be better suited.
*/
async function register(credentials) {
async function register(credentials, language: string|null = null) {
const HTTP = HTTPFactory()
setIsLoading(true)
if (!language) {
language = i18n.global.locale.value ?? getBrowserLanguage()
}
try {
await HTTP.post('register', credentials)
await HTTP.post('register', {
...credentials,
language,
})
return login(credentials)
} catch (e) {
if (e.response?.data?.code === 2002 && e.response?.data?.invalid_fields[0]?.startsWith('language:')) {
return register(credentials, 'en')
}
if (e.response?.data?.message) {
throw e.response.data
}
@ -302,23 +314,6 @@ export const useAuthStore = defineStore('auth', () => {
setUser(newUser)
updateLastUserRefresh()
if (
newUser.type === AUTH_TYPES.USER &&
(
typeof newUser.settings.language === 'undefined' ||
newUser.settings.language === ''
)
) {
// save current language
await saveUserSettings({
settings: {
...settings.value,
language: settings.value.language ? settings.value.language : getBrowserLanguage(),
},
showMessage: false,
})
}
return newUser
} catch (e) {
if((e?.response?.status >= 400 && e?.response?.status < 500) ||
@ -327,8 +322,6 @@ export const useAuthStore = defineStore('auth', () => {
return
}
console.log('continuerd')
const cause = {e}
if (typeof e?.response?.data?.message !== 'undefined') {

View File

@ -41,12 +41,40 @@ type Translator struct {
mu sync.RWMutex
}
// singleton instance
var translator = &Translator{
translations: make(map[string]TranslationStore),
fallbackLang: "en",
}
var availableLanguages = map[string]bool{
"en": true,
"de-DE": true,
"de-swiss": true,
"ru-RU": true,
"fr-FR": true,
"vi-VN": true,
"it-IT": true,
"cs-CZ": true,
"pl-PL": true,
"nl-NL": true,
"pt-PT": true,
"zh-CN": true,
"no-NO": true,
"es-ES": true,
"da-DK": true,
"ja-JP": true,
"hu-HU": true,
"ar-SA": true,
"sl-SI": true,
"pt-BR": true,
"hr-HR": true,
"uk-UA": true,
"lt-LT": true,
"bg-BG": true,
"ko-KR": true,
// IMPORTANT: Also add new languages to the frontend
}
// Init initializes the global translator with translation files
func Init() {
dir := "lang"
@ -61,6 +89,11 @@ func Init() {
}
langCode := strings.TrimSuffix(entry.Name(), ".json")
if !availableLanguages[langCode] {
continue
}
filePath := filepath.Join(dir, entry.Name())
err = translator.loadFile(localeFS, langCode, filePath)
@ -166,3 +199,8 @@ func stringSliceToInterfaceSlice(strings []string) []interface{} {
}
return interfaces
}
func HasLanguage(lang string) bool {
_, exists := translator.translations[lang]
return exists
}

View File

@ -20,22 +20,28 @@ import (
"errors"
"net/http"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/web/handler"
"github.com/labstack/echo/v4"
)
type UserRegister struct {
// The language of the new user. Must be a valid IETF BCP 47 language code and exist in Vikunja.
Language string `json:"language" valid:"language"`
user.APIUserPassword
}
// RegisterUser is the register handler
// @Summary Register
// @Description Creates a new user account.
// @tags auth
// @Accept json
// @Produce json
// @Param credentials body user.APIUserPassword true "The user credentials"
// @Param credentials body v1.UserRegister true "The user with credentials to create"
// @Success 200 {object} user.User
// @Failure 400 {object} web.HTTPError "No or invalid user register object provided / User already exists."
// @Failure 500 {object} models.Message "Internal error"
@ -45,7 +51,7 @@ func RegisterUser(c echo.Context) error {
return echo.ErrNotFound
}
// Check for Request Content
var userIn *user.APIUserPassword
var userIn *UserRegister
if err := c.Bind(&userIn); err != nil {
return c.JSON(http.StatusBadRequest, models.Message{Message: "No or invalid user model provided."})
}
@ -65,7 +71,12 @@ func RegisterUser(c echo.Context) error {
defer s.Close()
// Insert the user
newUser, err := user.CreateUser(s, userIn.APIFormat())
newUser, err := user.CreateUser(s, &user.User{
Username: userIn.Username,
Password: userIn.Password,
Email: userIn.Email,
Language: userIn.Language,
})
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err)

View File

@ -99,7 +99,7 @@ type User struct {
OverdueTasksRemindersTime string `xorm:"varchar(5) not null default '09:00'" json:"-"`
DefaultProjectID int64 `xorm:"bigint null index" json:"-"`
WeekStart int `xorm:"null" json:"-"`
Language string `xorm:"varchar(50) null" json:"-"`
Language string `xorm:"varchar(50) null" json:"-" valid:"language"`
Timezone string `xorm:"varchar(255) null" json:"-"`
DeletionScheduledAt time.Time `xorm:"datetime null" json:"-"`
@ -201,8 +201,6 @@ func GetFromAuth(a web.Auth) (*User, error) {
// APIUserPassword represents a user object without timestamps and a json password field.
type APIUserPassword struct {
// The unique, numeric id of this user.
ID int64 `json:"id"`
// The user's username. Cannot contain anything that looks like an url or whitespaces.
Username string `json:"username" valid:"length(3|250),username" minLength:"3" maxLength:"250"`
// The user's password in clear text. Only used when registering the user. The maximum limi is 72 bytes, which may be less than 72 characters. This is due to the limit in the bcrypt hashing algorithm used to store passwords in Vikunja.
@ -211,16 +209,6 @@ type APIUserPassword struct {
Email string `json:"email" valid:"email,length(0|250)" maxLength:"250"`
}
// APIFormat formats an API User into a normal user struct
func (apiUser *APIUserPassword) APIFormat() *User {
return &User{
ID: apiUser.ID,
Username: apiUser.Username,
Password: apiUser.Password,
Email: apiUser.Email,
}
}
// GetUserByID returns user by its ID
func GetUserByID(s *xorm.Session, id int64) (user *User, err error) {
// Apparently xorm does otherwise look for all users but return only one, which leads to returning one even if the ID is 0

View File

@ -69,9 +69,12 @@ func CreateUser(s *xorm.Session, user *User) (newUser *User, err error) {
user.OverdueTasksRemindersTime = config.DefaultSettingsOverdueTaskRemindersTime.GetString()
user.DefaultProjectID = config.DefaultSettingsDefaultProjectID.GetInt64()
user.WeekStart = config.DefaultSettingsWeekStart.GetInt()
user.Language = config.DefaultSettingsLanguage.GetString()
user.Timezone = config.DefaultSettingsTimezone.GetString()
if user.Language == "" {
user.Language = config.DefaultSettingsLanguage.GetString()
}
// Insert it
_, err = s.Insert(user)
if err != nil {

View File

@ -19,6 +19,8 @@ package user
import (
"strings"
"code.vikunja.io/api/pkg/i18n"
"github.com/asaskevich/govalidator"
)
@ -50,4 +52,6 @@ func init() {
return len([]byte(str)) < 72
}
govalidator.TagMap["language"] = i18n.HasLanguage
}