Skip to content

Commit 1dbb852

Browse files
committed
feat(user): persists prefer language in db #1155
1 parent 06f7597 commit 1dbb852

File tree

8 files changed

+132
-61
lines changed

8 files changed

+132
-61
lines changed

api/user/current_user.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ func GetCurrentUser(c *gin.Context) {
1818
func UpdateCurrentUser(c *gin.Context) {
1919
cosy.Core[model.User](c).
2020
SetValidRules(gin.H{
21-
"name": "required",
21+
"name": "omitempty",
22+
"language": "omitempty",
2223
}).
2324
Custom(func(c *cosy.Ctx[model.User]) {
2425
user := api.CurrentUser(c.Context)
2526
user.Name = c.Model.Name
27+
user.Language = c.Model.Language
2628

2729
db := cosy.UseDB()
2830
err := db.Where("id = ?", user.ID).Updates(user).Error
@@ -72,3 +74,29 @@ func UpdateCurrentUserPassword(c *gin.Context) {
7274
"message": "ok",
7375
})
7476
}
77+
78+
func UpdateCurrentUserLanguage(c *gin.Context) {
79+
var json struct {
80+
Language string `json:"language" binding:"required"`
81+
}
82+
83+
if !cosy.BindAndValid(c, &json) {
84+
return
85+
}
86+
87+
user := api.CurrentUser(c)
88+
user.Language = json.Language
89+
90+
db := cosy.UseDB()
91+
err := db.Where("id = ?", user.ID).Updates(&model.User{
92+
Language: json.Language,
93+
}).Error
94+
if err != nil {
95+
cosy.ErrHandler(c, err)
96+
return
97+
}
98+
99+
c.JSON(http.StatusOK, gin.H{
100+
"language": json.Language,
101+
})
102+
}

api/user/router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,5 @@ func InitUserRouter(r *gin.RouterGroup) {
4646
r.GET("/user", GetCurrentUser)
4747
r.POST("/user", middleware.RequireSecureSession(), UpdateCurrentUser)
4848
r.POST("/user/password", middleware.RequireSecureSession(), UpdateCurrentUserPassword)
49+
r.POST("/user/language", UpdateCurrentUserLanguage)
4950
}

app/src/api/user.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,22 @@ export interface User extends ModelBase {
66
password: string
77
enabled_2fa: boolean
88
status: boolean
9+
language: string
910
}
1011

1112
const user = extendCurdApi(useCurdApi<User>('/users'), {
1213
getCurrentUser: () => {
1314
return http.get('/user')
1415
},
15-
updateCurrentUser: (data: User) => {
16+
updateCurrentUser: (data: Partial<User>) => {
1617
return http.post('/user', data)
1718
},
1819
updateCurrentUserPassword: (data: { old_password: string, new_password: string }) => {
1920
return http.post('/user/password', data)
2021
},
22+
updateCurrentUserLanguage: (data: { language: string }) => {
23+
return http.post('/user/language', data)
24+
},
2125
})
2226

2327
export default user

app/src/components/SetLanguage/SetLanguage.vue

Lines changed: 73 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,11 @@
22
import dayjs from 'dayjs'
33
import loadTranslations from '@/api/translations'
44
import gettext from '@/gettext'
5-
import { useSettingsStore } from '@/pinia'
6-
7-
import 'dayjs/locale/fr'
8-
import 'dayjs/locale/ja'
9-
import 'dayjs/locale/ko'
10-
import 'dayjs/locale/de'
11-
import 'dayjs/locale/zh-cn'
12-
import 'dayjs/locale/zh-tw'
13-
import 'dayjs/locale/pt'
14-
import 'dayjs/locale/es'
15-
import 'dayjs/locale/it'
16-
import 'dayjs/locale/ar'
17-
import 'dayjs/locale/ru'
18-
import 'dayjs/locale/tr'
19-
import 'dayjs/locale/vi'
5+
import { useSettingsStore, useUserStore } from '@/pinia'
206
217
const settings = useSettingsStore()
8+
const userStore = useUserStore()
9+
const { info } = storeToRefs(userStore)
2210
2311
const route = useRoute()
2412
@@ -43,6 +31,7 @@ watch(current, v => {
4331
loadTranslations(route)
4432
settings.set_language(v)
4533
gettext.current = v
34+
userStore.updateCurrentUserLanguage(v)
4635
4736
updateTitle()
4837
})
@@ -51,54 +40,80 @@ onMounted(() => {
5140
updateTitle()
5241
})
5342
54-
function init() {
55-
switch (current.value) {
56-
case 'fr':
57-
dayjs.locale('fr')
58-
break
59-
case 'ja':
60-
dayjs.locale('ja')
61-
break
62-
case 'ko':
63-
dayjs.locale('ko')
64-
break
65-
case 'de':
66-
dayjs.locale('de')
67-
break
68-
case 'zh_CN':
69-
dayjs.locale('zh-cn')
70-
break
71-
case 'zh_TW':
72-
dayjs.locale('zh-tw')
73-
break
74-
case 'pt':
75-
dayjs.locale('pt')
76-
break
77-
case 'es':
78-
dayjs.locale('es')
79-
break
80-
case 'it':
81-
dayjs.locale('it')
82-
break
83-
case 'ar':
84-
dayjs.locale('ar')
85-
break
86-
case 'ru':
87-
dayjs.locale('ru')
88-
break
89-
case 'tr':
90-
dayjs.locale('tr')
91-
break
92-
case 'vi':
93-
dayjs.locale('vi')
94-
break
95-
default:
43+
// Language mapping configuration
44+
const localeMap: Record<string, string> = {
45+
fr: 'fr',
46+
ja: 'ja',
47+
ko: 'ko',
48+
de: 'de',
49+
zh_CN: 'zh-cn',
50+
zh_TW: 'zh-tw',
51+
pt: 'pt',
52+
es: 'es',
53+
it: 'it',
54+
ar: 'ar',
55+
ru: 'ru',
56+
tr: 'tr',
57+
vi: 'vi',
58+
}
59+
60+
// Predefined locale importers for dynamic loading
61+
// This approach works with Vite's static analysis requirements
62+
const localeImporters = {
63+
'fr': () => import('dayjs/locale/fr'),
64+
'ja': () => import('dayjs/locale/ja'),
65+
'ko': () => import('dayjs/locale/ko'),
66+
'de': () => import('dayjs/locale/de'),
67+
'zh-cn': () => import('dayjs/locale/zh-cn'),
68+
'zh-tw': () => import('dayjs/locale/zh-tw'),
69+
'pt': () => import('dayjs/locale/pt'),
70+
'es': () => import('dayjs/locale/es'),
71+
'it': () => import('dayjs/locale/it'),
72+
'ar': () => import('dayjs/locale/ar'),
73+
'ru': () => import('dayjs/locale/ru'),
74+
'tr': () => import('dayjs/locale/tr'),
75+
'vi': () => import('dayjs/locale/vi'),
76+
}
77+
78+
// Dynamically load dayjs locale files
79+
async function loadDayjsLocale(locale: string) {
80+
const dayjsLocale = localeMap[locale]
81+
82+
if (!dayjsLocale) {
83+
dayjs.locale('en')
84+
return
85+
}
86+
87+
try {
88+
// Use predefined importer function
89+
const importer = localeImporters[dayjsLocale]
90+
if (importer) {
91+
await importer()
92+
dayjs.locale(dayjsLocale)
93+
}
94+
else {
95+
// Fallback to English if locale not found
9696
dayjs.locale('en')
97+
}
98+
}
99+
catch (error) {
100+
console.warn(`Failed to load dayjs locale: ${dayjsLocale}`, error)
101+
// Graceful fallback to English
102+
dayjs.locale('en')
97103
}
98104
}
99105
100-
init()
106+
// Initialize current language
107+
async function init() {
108+
await loadDayjsLocale(current.value)
109+
}
101110
111+
// Reactive initialization and watch
112+
onMounted(async () => {
113+
current.value = info.value.language || 'en'
114+
await nextTick()
115+
await init()
116+
})
102117
watch(current, init)
103118
</script>
104119

app/src/pinia/moudule/user.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,17 @@ export const useUserStore = defineStore('user', () => {
8787
}
8888
}
8989

90+
async function updateCurrentUserLanguage(language: string) {
91+
try {
92+
await user.updateCurrentUserLanguage({ language })
93+
info.value.language = language
94+
}
95+
catch (error) {
96+
console.error('Failed to update language:', error)
97+
throw error
98+
}
99+
}
100+
90101
return {
91102
token,
92103
unreadCount,
@@ -101,6 +112,7 @@ export const useUserStore = defineStore('user', () => {
101112
getCurrentUser,
102113
updateCurrentUser,
103114
updateCurrentUserPassword,
115+
updateCurrentUserLanguage,
104116
}
105117
}, {
106118
persist: true,

app/src/views/other/Login.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ function onSubmit() {
6666
login(r.token)
6767
await nextTick()
6868
secureSessionId.value = r.secure_session_id
69+
await userStore.getCurrentUser()
70+
await nextTick()
6971
await router.push(next)
7072
break
7173
case 199:
@@ -115,6 +117,8 @@ if (route.query?.code !== undefined && route.query?.state !== undefined) {
115117
116118
const next = (route.query?.next || '').toString() || '/'
117119
120+
await userStore.getCurrentUser()
121+
await nextTick()
118122
await router.push(next)
119123
})
120124
loading.value = false
@@ -150,6 +154,8 @@ async function handlePasskeyLogin() {
150154
151155
passkeyLogin(asseResp.rawId, r.token)
152156
secureSessionId.value = r.secure_session_id
157+
await userStore.getCurrentUser()
158+
await nextTick()
153159
await router.push(next)
154160
}
155161

model/user.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type User struct {
3232
OTPSecret []byte `json:"-" gorm:"type:blob"`
3333
RecoveryCodes RecoveryCodes `json:"-" gorm:"serializer:json[aes]"`
3434
EnabledTwoFA bool `json:"enabled_2fa" gorm:"-"`
35+
Language string `json:"language" gorm:"default:en"`
3536
}
3637

3738
type AuthToken struct {

query/users.gen.go

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)