@allbluecn/admin-app 0.2.0 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/package.json +10 -10
  2. package/src/components/admin/admin-user-menu.tsx +12 -6
  3. package/src/components/admin/developer-log.tsx +42 -44
  4. package/src/components/ai-provider/model-list-editor.tsx +5 -4
  5. package/src/components/ai-provider/provider-form.tsx +63 -64
  6. package/src/components/ai-provider/provider-table.tsx +35 -36
  7. package/src/components/auth/admin-login-form.tsx +23 -24
  8. package/src/components/auth/oauth-section.tsx +3 -5
  9. package/src/components/auth/password-input.tsx +2 -3
  10. package/src/components/common/placeholder-page.tsx +2 -4
  11. package/src/components/component/component-form-dialog.tsx +48 -49
  12. package/src/components/component/component-import-dialog.tsx +5 -5
  13. package/src/components/component/component-table.tsx +13 -14
  14. package/src/components/data-table/data-table-pagination.tsx +4 -6
  15. package/src/components/data-table/table-detail-layout.tsx +2 -3
  16. package/src/components/dependency/changelog-dialog.tsx +3 -4
  17. package/src/components/dependency/columns.tsx +32 -24
  18. package/src/components/dependency/dependency-table.tsx +37 -29
  19. package/src/components/dependency/edit-dialog.tsx +24 -25
  20. package/src/components/knowledge-public/bookmark-form-dialog.tsx +7 -7
  21. package/src/components/knowledge-public/compliance-form-dialog.tsx +13 -13
  22. package/src/components/knowledge-public/legal-form-dialog.tsx +51 -46
  23. package/src/components/layout/sidebar-02/app-sidebar.tsx +33 -34
  24. package/src/components/layout/sidebar-02/nav-main.tsx +1 -1
  25. package/src/components/market/columns.tsx +20 -21
  26. package/src/components/market/market-app-form.tsx +45 -38
  27. package/src/components/market/market-app-list.tsx +16 -17
  28. package/src/components/monitoring/error-list.tsx +18 -19
  29. package/src/components/monitoring/health-cards.tsx +11 -13
  30. package/src/components/monitoring/metrics-stats.tsx +11 -12
  31. package/src/components/monitoring/request-log-table.tsx +8 -9
  32. package/src/components/ui/pagination.tsx +0 -2
  33. package/src/components/ui/sidebar.tsx +1 -1
  34. package/src/components/user/columns.tsx +9 -9
  35. package/src/components/user/settings-activity-log.tsx +39 -29
  36. package/src/components/user/settings-profile.tsx +36 -36
  37. package/src/components/user/user-create-dialog.tsx +4 -4
  38. package/src/components/user/user-filters.tsx +16 -18
  39. package/src/lib/i18n/config.ts +0 -32
  40. package/src/providers/index.tsx +6 -16
  41. package/src/routeTree.gen.ts +1199 -0
  42. package/src/routes/__root.tsx +9 -23
  43. package/src/routes/_nav/apps/route.lazy.tsx +2 -3
  44. package/src/routes/_nav/audit/route.lazy.tsx +10 -11
  45. package/src/routes/_nav/billing/invoices/route.lazy.tsx +6 -7
  46. package/src/routes/_nav/billing/route.lazy.tsx +3 -4
  47. package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.lazy.tsx +16 -17
  48. package/src/routes/_nav/billing/subscriptions/route.lazy.tsx +9 -10
  49. package/src/routes/_nav/compliance/route.lazy.tsx +34 -35
  50. package/src/routes/_nav/components/route.lazy.tsx +6 -6
  51. package/src/routes/_nav/dashboard/route.lazy.tsx +3 -5
  52. package/src/routes/_nav/dependencies/route.lazy.tsx +7 -8
  53. package/src/routes/_nav/docs/route.lazy.tsx +2 -3
  54. package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +40 -41
  55. package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +32 -33
  56. package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +32 -33
  57. package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +24 -31
  58. package/src/routes/_nav/licenses/new/route.lazy.tsx +25 -26
  59. package/src/routes/_nav/licenses/route.lazy.tsx +17 -18
  60. package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +26 -27
  61. package/src/routes/_nav/monitoring/errors/route.lazy.tsx +11 -12
  62. package/src/routes/_nav/monitoring/health/route.lazy.tsx +18 -20
  63. package/src/routes/_nav/monitoring/logs/route.lazy.tsx +12 -13
  64. package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +13 -15
  65. package/src/routes/_nav/monitoring/overview/route.lazy.tsx +12 -13
  66. package/src/routes/_nav/monitoring/route.lazy.tsx +8 -9
  67. package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +55 -57
  68. package/src/routes/_nav/permissions/route.lazy.tsx +48 -50
  69. package/src/routes/_nav/settings/route.lazy.tsx +2 -3
  70. package/src/routes/_nav/sso/route.lazy.tsx +20 -18
  71. package/src/routes/_nav/users/$userId/route.lazy.tsx +86 -82
  72. package/src/routes/_nav/users/route.lazy.tsx +4 -5
  73. package/src/routes/setup/route.lazy.tsx +15 -16
  74. package/src/start.ts +2 -2
  75. package/src/lib/i18n/client.ts +0 -32
  76. package/src/lib/i18n/locales/en/admin-ai.json +0 -128
  77. package/src/lib/i18n/locales/en/admin-billing.json +0 -56
  78. package/src/lib/i18n/locales/en/admin-users.json +0 -88
  79. package/src/lib/i18n/locales/en/common.json +0 -877
  80. package/src/lib/i18n/locales/en/enterprise.json +0 -208
  81. package/src/lib/i18n/locales/zh/admin-ai.json +0 -128
  82. package/src/lib/i18n/locales/zh/admin-billing.json +0 -56
  83. package/src/lib/i18n/locales/zh/admin-users.json +0 -88
  84. package/src/lib/i18n/locales/zh/common.json +0 -877
  85. package/src/lib/i18n/locales/zh/enterprise.json +0 -208
  86. package/src/lib/i18n/server.ts +0 -18
  87. package/src/lib/i18n/types.ts +0 -5
@@ -16,10 +16,8 @@
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
18
  import { createLazyFileRoute } from '@tanstack/react-router'
19
+ import { m } from '@/paraglide/messages.js'
19
20
  import { useState, useTransition, useMemo } from 'react'
20
- import { I18nextProvider } from 'react-i18next'
21
- import { useTranslation } from 'react-i18next'
22
- import { createServerI18n } from '@/lib/i18n/server'
23
21
  import { formatTimestamp } from '@allbluecn/web-app/lib/datetime'
24
22
  import { getUserDetailFn } from '@/server/serverFns/user/detail'
25
23
  import { updateUserRoleFn } from '@/server/serverFns/user/update-role'
@@ -31,6 +29,19 @@ import { Badge } from '@/components/ui/badge'
31
29
  import { Separator } from '@/components/ui/separator'
32
30
  import { Button } from '@/components/ui/button'
33
31
  import { UserAvatar } from '@allbluecn/web-app/user-avatar'
32
+
33
+ const STATUS_LABELS: Record<string, () => string> = {
34
+ active: m['admin-users_status_active'],
35
+ banned: m['admin-users_status_banned'],
36
+ inactive: m['admin-users_status_inactive'],
37
+ }
38
+
39
+ const ROLE_LABELS: Record<string, () => string> = {
40
+ admin: m['admin-users_role_admin'],
41
+ guest: m['admin-users_role_guest'],
42
+ superAdmin: m['admin-users_role_superAdmin'],
43
+ user: m['admin-users_role_user'],
44
+ }
34
45
  import {
35
46
  Dialog,
36
47
  DialogContent,
@@ -90,27 +101,20 @@ export const Route = createLazyFileRoute('/_nav/users/$userId')({
90
101
  })
91
102
 
92
103
  function AdminUserErrorBoundary({ error, reset }: { error: Error; reset: () => void }) {
93
- const i18n = useMemo(() => createServerI18n('zh'), [])
94
- return (
95
- <I18nextProvider i18n={i18n}>
96
- <AdminUserErrorBoundaryInner error={error} reset={reset} />
97
- </I18nextProvider>
98
- )
104
+ return <AdminUserErrorBoundaryInner error={error} reset={reset} />
99
105
  }
100
106
 
101
107
  function AdminUserErrorBoundaryInner({ error, reset }: { error: Error; reset: () => void }) {
102
- const { t } = useTranslation('admin-users')
103
108
  return (
104
109
  <div className="flex flex-col items-center justify-center h-64 gap-3">
105
110
  <AlertCircle className="size-8 text-destructive" />
106
- <p className="text-sm text-muted-foreground">{t('detail.errorLoading')}: {error.message}</p>
107
- <Button variant="outline" size="sm" onClick={reset}>{t('detail.retry')}</Button>
111
+ <p className="text-sm text-muted-foreground">{m['admin-users_detail_errorLoading']()}: {error.message}</p>
112
+ <Button variant="outline" size="sm" onClick={reset}>{m['admin-users_detail_retry']()}</Button>
108
113
  </div>
109
114
  )
110
115
  }
111
116
 
112
117
  function UserDetailPage() {
113
- const { t } = useTranslation('admin-users')
114
118
  const statusBadgeVariant: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
115
119
  ACTIVE: 'default',
116
120
  INACTIVE: 'secondary',
@@ -124,7 +128,7 @@ function UserDetailPage() {
124
128
  if (notFound) {
125
129
  return (
126
130
  <div className="flex h-64 items-center justify-center">
127
- <p className="text-muted-foreground">{t('detail.userNotFound')}</p>
131
+ <p className="text-muted-foreground">{m['admin-users_detail_userNotFound']()}</p>
128
132
  </div>
129
133
  )
130
134
  }
@@ -141,8 +145,8 @@ function UserDetailPage() {
141
145
  if (updated) setUser(updated)
142
146
  })
143
147
 
144
- const getRoleLabel = (role: string) => t(`role.${role}`)
145
- const getStatusLabel = (status: string) => t(`status.${status}`)
148
+ const getRoleLabel = (role: string) => (ROLE_LABELS[role] ?? (() => role))()
149
+ const getStatusLabel = (status: string) => (STATUS_LABELS[status] ?? (() => status))()
146
150
 
147
151
  return (
148
152
  <>
@@ -176,21 +180,21 @@ function UserDetailPage() {
176
180
  <SettingsProfile
177
181
  readOnly
178
182
  profile={{ name: user.username, email: user.email }}
179
- title={t('detail.profile.title')}
180
- description={t('detail.profile.description')}
183
+ title={m['admin-users_detail_profile_title']()}
184
+ description={m['admin-users_detail_profile_description']()}
181
185
  actions={
182
186
  <>
183
187
  <Button variant="outline" size="sm" onClick={() => setRoleDialog({ open: true, value: user.role })}>
184
- {t('action.changeRole')}
188
+ {m['admin-users_action_changeRole']()}
185
189
  </Button>
186
190
  <Button variant="outline" size="sm" onClick={() => setStatusDialog({ open: true, value: user.status })}>
187
- {t('action.changeStatus')}
191
+ {m['admin-users_action_changeStatus']()}
188
192
  </Button>
189
193
  <Button variant="outline" size="sm" onClick={() => setPasswordDialog({ open: true, value: '' })}>
190
- {t('action.resetPassword')}
194
+ {m['admin-users_action_resetPassword']()}
191
195
  </Button>
192
196
  <Button variant="destructive" size="sm" onClick={() => setDeleteDialog({ open: true, confirmName: '' })}>
193
- {t('common:button.delete')}
197
+ {m.common_button_delete()}
194
198
  </Button>
195
199
  </>
196
200
  }
@@ -198,22 +202,22 @@ function UserDetailPage() {
198
202
 
199
203
  <Card className="shadow-xs">
200
204
  <CardHeader>
201
- <CardTitle>{t('detail.account.title')}</CardTitle>
202
- <CardDescription>{t('detail.account.description')}</CardDescription>
205
+ <CardTitle>{m['admin-users_detail_account_title']()}</CardTitle>
206
+ <CardDescription>{m['admin-users_detail_account_description']()}</CardDescription>
203
207
  </CardHeader>
204
208
  <CardContent>
205
209
  <div className="space-y-2 text-sm">
206
210
  <div className="flex justify-between rounded-lg border p-3">
207
- <span className="text-muted-foreground">{t('detail.account.createdAt')}</span>
211
+ <span className="text-muted-foreground">{m['admin-users_detail_account_createdAt']()}</span>
208
212
  <span>{formatTimestamp(user.createdAt)}</span>
209
213
  </div>
210
214
  <div className="flex justify-between rounded-lg border p-3">
211
- <span className="text-muted-foreground">{t('detail.account.updatedAt')}</span>
215
+ <span className="text-muted-foreground">{m['admin-users_detail_account_updatedAt']()}</span>
212
216
  <span>{formatTimestamp(user.updatedAt)}</span>
213
217
  </div>
214
218
  <div className="flex justify-between rounded-lg border p-3">
215
- <span className="text-muted-foreground">{t('detail.account.lastLogin')}</span>
216
- <span>{user.lastLoginAt ? formatTimestamp(user.lastLoginAt) : t('detail.account.neverLoggedIn')}</span>
219
+ <span className="text-muted-foreground">{m['admin-users_detail_account_lastLogin']()}</span>
220
+ <span>{user.lastLoginAt ? formatTimestamp(user.lastLoginAt) : m['admin-users_detail_account_neverLoggedIn']()}</span>
217
221
  </div>
218
222
  </div>
219
223
  </CardContent>
@@ -221,8 +225,8 @@ function UserDetailPage() {
221
225
 
222
226
  <Card className="shadow-xs">
223
227
  <CardHeader>
224
- <CardTitle>{t('detail.stats.title')}</CardTitle>
225
- <CardDescription>{t('detail.stats.description')}</CardDescription>
228
+ <CardTitle>{m['admin-users_detail_stats_title']()}</CardTitle>
229
+ <CardDescription>{m['admin-users_detail_stats_description']()}</CardDescription>
226
230
  </CardHeader>
227
231
  <CardContent>
228
232
  <div className="grid grid-cols-2 gap-3">
@@ -230,28 +234,28 @@ function UserDetailPage() {
230
234
  <FileTextIcon className="h-4 w-4 text-muted-foreground" />
231
235
  <div>
232
236
  <div className="text-lg font-semibold">{user._count.prds}</div>
233
- <div className="text-xs text-muted-foreground">{t('detail.stats.prds')}</div>
237
+ <div className="text-xs text-muted-foreground">{m['admin-users_detail_stats_prds']()}</div>
234
238
  </div>
235
239
  </div>
236
240
  <div className="flex items-center gap-2 rounded-lg border p-3">
237
241
  <Package2Icon className="h-4 w-4 text-muted-foreground" />
238
242
  <div>
239
243
  <div className="text-lg font-semibold">{user._count.anyhubApps}</div>
240
- <div className="text-xs text-muted-foreground">{t('detail.stats.apps')}</div>
244
+ <div className="text-xs text-muted-foreground">{m['admin-users_detail_stats_apps']()}</div>
241
245
  </div>
242
246
  </div>
243
247
  <div className="flex items-center gap-2 rounded-lg border p-3">
244
248
  <ShoppingBagIcon className="h-4 w-4 text-muted-foreground" />
245
249
  <div>
246
250
  <div className="text-lg font-semibold">{user._count.anyhubMarketApps}</div>
247
- <div className="text-xs text-muted-foreground">{t('detail.stats.marketApps')}</div>
251
+ <div className="text-xs text-muted-foreground">{m['admin-users_detail_stats_marketApps']()}</div>
248
252
  </div>
249
253
  </div>
250
254
  <div className="flex items-center gap-2 rounded-lg border p-3">
251
255
  <ShieldCheckIcon className="h-4 w-4 text-muted-foreground" />
252
256
  <div>
253
257
  <div className="text-lg font-semibold">{user._count.reviews}</div>
254
- <div className="text-xs text-muted-foreground">{t('detail.stats.reviews')}</div>
258
+ <div className="text-xs text-muted-foreground">{m['admin-users_detail_stats_reviews']()}</div>
255
259
  </div>
256
260
  </div>
257
261
  </div>
@@ -266,23 +270,23 @@ function UserDetailPage() {
266
270
  id: log.id,
267
271
  action: 'login',
268
272
  type: 'login',
269
- description: log.success ? t('detail.loginLogs.success') : t('detail.loginLogs.failure'),
273
+ description: log.success ? m['admin-users_detail_loginLogs_success']() : m['admin-users_detail_loginLogs_failure'](),
270
274
  ipAddress: log.ip || '',
271
275
  location: log.location || '',
272
276
  device: formatParsedUA(parseUserAgent(log.userAgent)),
273
277
  timestamp: new Date(log.createdAt),
274
278
  status: log.success ? 'success' : 'failed',
275
- reason: log.success ? undefined : log.message || t('detail.loginLogs.unknownReason'),
279
+ reason: log.success ? undefined : log.message || m['admin-users_detail_loginLogs_unknownReason'](),
276
280
  }))}
277
- title={t('detail.loginLogs.title')}
278
- description={t('detail.loginLogs.description')}
279
- onExport={async (filters) => {
281
+ title={m['admin-users_detail_loginLogs_title']()}
282
+ description={m['admin-users_detail_loginLogs_description']()}
283
+ onExport={async (filters: any) => {
280
284
  const rows = user.loginLogs
281
285
  .filter((log: any) => {
282
286
  if (filters.type && filters.type !== 'login') return false
283
287
  if (
284
288
  filters.search &&
285
- !`${log.success ? t('detail.loginLogs.success') : t('detail.loginLogs.failure')} ${log.ip || ''}`
289
+ !`${log.success ? m['admin-users_detail_loginLogs_success']() : m['admin-users_detail_loginLogs_failure']()} ${log.ip || ''}`
286
290
  .toLowerCase()
287
291
  .includes(filters.search.toLowerCase())
288
292
  )
@@ -293,8 +297,8 @@ function UserDetailPage() {
293
297
  const parsed = parseUserAgent(log.userAgent)
294
298
  return [
295
299
  new Date(log.createdAt).toISOString(),
296
- log.success ? t('detail.loginLogs.success') : t('detail.loginLogs.failure'),
297
- log.success ? '' : log.message || t('detail.loginLogs.unknownReason'),
300
+ log.success ? m['admin-users_detail_loginLogs_success']() : m['admin-users_detail_loginLogs_failure'](),
301
+ log.success ? '' : log.message || m['admin-users_detail_loginLogs_unknownReason'](),
298
302
  log.ip || '',
299
303
  log.location || '',
300
304
  parsed.browser,
@@ -304,7 +308,7 @@ function UserDetailPage() {
304
308
  .map((v: string) => `"${(v || '').replace(/"/g, '""')}"`)
305
309
  .join(',')
306
310
  })
307
- const csv = [t('detail.loginLogs.csvHeader'), ...rows].join('\n')
311
+ const csv = [m['admin-users_detail_loginLogs_csvHeader'](), ...rows].join('\n')
308
312
  const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
309
313
  const url = URL.createObjectURL(blob)
310
314
  const a = document.createElement('a')
@@ -312,118 +316,118 @@ function UserDetailPage() {
312
316
  a.download = `user-${user.username}-login-logs.csv`
313
317
  a.click()
314
318
  URL.revokeObjectURL(url)
315
- toast.success(t('detail.loginLogs.exportSuccess'))
319
+ toast.success(m['admin-users_detail_loginLogs_exportSuccess']())
316
320
  }}
317
321
  />
318
322
  </div>
319
323
  </div>
320
324
  </TableDetailLayout>
321
325
 
322
- <Dialog open={roleDialog.open} onOpenChange={(o) => setRoleDialog((d) => ({ ...d, open: o }))}>
326
+ <Dialog open={roleDialog.open} onOpenChange={(o: boolean) => setRoleDialog((d) => ({ ...d, open: o }))}>
323
327
  <DialogContent className="sm:max-w-100">
324
328
  <DialogHeader>
325
- <DialogTitle>{t('dialog.role.title', { username: user.username })}</DialogTitle>
329
+ <DialogTitle>{m['admin-users_dialog_role_title']({ username: user.username })}</DialogTitle>
326
330
  </DialogHeader>
327
331
  <div className="py-4">
328
- <Label>{t('dialog.role.newRole')}</Label>
329
- <Select value={roleDialog.value} onValueChange={(v) => setRoleDialog((d) => ({ ...d, value: v }))}>
332
+ <Label>{m['admin-users_dialog_role_newRole']()}</Label>
333
+ <Select value={roleDialog.value} onValueChange={(v: string) => setRoleDialog((d) => ({ ...d, value: v }))}>
330
334
  <SelectTrigger><SelectValue /></SelectTrigger>
331
335
  <SelectContent>
332
- <SelectItem value="USER">{t('role.user')}</SelectItem>
333
- <SelectItem value="ADMIN">{t('role.admin')}</SelectItem>
334
- <SelectItem value="SUPER_ADMIN">{t('role.superAdmin')}</SelectItem>
336
+ <SelectItem value="USER">{m['admin-users_role_user']()}</SelectItem>
337
+ <SelectItem value="ADMIN">{m['admin-users_role_admin']()}</SelectItem>
338
+ <SelectItem value="SUPER_ADMIN">{m['admin-users_role_superAdmin']()}</SelectItem>
335
339
  </SelectContent>
336
340
  </Select>
337
341
  </div>
338
342
  <DialogFooter>
339
- <Button variant="outline" onClick={() => setRoleDialog((d) => ({ ...d, open: false }))}>{t('common:button.cancel')}</Button>
343
+ <Button variant="outline" onClick={() => setRoleDialog((d) => ({ ...d, open: false }))}>{m.common_button_cancel()}</Button>
340
344
  <Button disabled={isPending} onClick={() => startTransition(async () => {
341
345
  try {
342
346
  await updateUserRoleFn({ data: { userId: user.id, role: roleDialog.value as 'USER' | 'ADMIN' | 'SUPER_ADMIN' } })
343
- toast.success(t('toast.roleUpdated'))
347
+ toast.success(m['admin-users_toast_roleUpdated']())
344
348
  setRoleDialog((d) => ({ ...d, open: false }))
345
349
  await refetchUser()
346
- } catch { toast.error(t('toast.updateFailed')) }
347
- })}>{t('common:button.confirm')}</Button>
350
+ } catch { toast.error(m['admin-users_toast_updateFailed']()) }
351
+ })}>{m.common_button_confirm()}</Button>
348
352
  </DialogFooter>
349
353
  </DialogContent>
350
354
  </Dialog>
351
355
 
352
- <Dialog open={statusDialog.open} onOpenChange={(o) => setStatusDialog((d) => ({ ...d, open: o }))}>
356
+ <Dialog open={statusDialog.open} onOpenChange={(o: boolean) => setStatusDialog((d) => ({ ...d, open: o }))}>
353
357
  <DialogContent className="sm:max-w-100">
354
358
  <DialogHeader>
355
- <DialogTitle>{t('dialog.status.title', { username: user.username })}</DialogTitle>
359
+ <DialogTitle>{m['admin-users_dialog_status_title']({ username: user.username })}</DialogTitle>
356
360
  </DialogHeader>
357
361
  <div className="py-4">
358
- <Label>{t('dialog.status.newStatus')}</Label>
359
- <Select value={statusDialog.value} onValueChange={(v) => setStatusDialog((d) => ({ ...d, value: v }))}>
362
+ <Label>{m['admin-users_dialog_status_newStatus']()}</Label>
363
+ <Select value={statusDialog.value} onValueChange={(v: string) => setStatusDialog((d) => ({ ...d, value: v }))}>
360
364
  <SelectTrigger><SelectValue /></SelectTrigger>
361
365
  <SelectContent>
362
- <SelectItem value="ACTIVE">{t('status.active')}</SelectItem>
363
- <SelectItem value="INACTIVE">{t('status.inactive')}</SelectItem>
364
- <SelectItem value="BANNED">{t('status.banned')}</SelectItem>
366
+ <SelectItem value="ACTIVE">{m['admin-users_status_active']()}</SelectItem>
367
+ <SelectItem value="INACTIVE">{m['admin-users_status_inactive']()}</SelectItem>
368
+ <SelectItem value="BANNED">{m['admin-users_status_banned']()}</SelectItem>
365
369
  </SelectContent>
366
370
  </Select>
367
371
  </div>
368
372
  <DialogFooter>
369
- <Button variant="outline" onClick={() => setStatusDialog((d) => ({ ...d, open: false }))}>{t('common:button.cancel')}</Button>
373
+ <Button variant="outline" onClick={() => setStatusDialog((d) => ({ ...d, open: false }))}>{m.common_button_cancel()}</Button>
370
374
  <Button disabled={isPending} onClick={() => startTransition(async () => {
371
375
  try {
372
376
  await updateUserStatusFn({ data: { userId: user.id, status: statusDialog.value as 'ACTIVE' | 'INACTIVE' | 'BANNED' } })
373
- toast.success(t('toast.statusUpdated'))
377
+ toast.success(m['admin-users_toast_statusUpdated']())
374
378
  setStatusDialog((d) => ({ ...d, open: false }))
375
379
  await refetchUser()
376
- } catch { toast.error(t('toast.updateFailed')) }
377
- })}>{t('common:button.confirm')}</Button>
380
+ } catch { toast.error(m['admin-users_toast_updateFailed']()) }
381
+ })}>{m.common_button_confirm()}</Button>
378
382
  </DialogFooter>
379
383
  </DialogContent>
380
384
  </Dialog>
381
385
 
382
- <Dialog open={passwordDialog.open} onOpenChange={(o) => setPasswordDialog((d) => ({ ...d, open: o }))}>
386
+ <Dialog open={passwordDialog.open} onOpenChange={(o: boolean) => setPasswordDialog((d) => ({ ...d, open: o }))}>
383
387
  <DialogContent className="sm:max-w-100">
384
388
  <DialogHeader>
385
- <DialogTitle>{t('dialog.password.title', { username: user.username })}</DialogTitle>
389
+ <DialogTitle>{m['admin-users_dialog_password_title']({ username: user.username })}</DialogTitle>
386
390
  </DialogHeader>
387
391
  <form onSubmit={(e) => { e.preventDefault(); startTransition(async () => {
388
392
  try {
389
393
  await resetUserPasswordFn({ data: { userId: user.id, newPassword: passwordDialog.value } })
390
- toast.success(t('toast.passwordReset'))
394
+ toast.success(m['admin-users_toast_passwordReset']())
391
395
  setPasswordDialog((d) => ({ ...d, open: false, value: '' }))
392
- } catch { toast.error(t('toast.resetFailed')) }
396
+ } catch { toast.error(m['admin-users_toast_resetFailed']()) }
393
397
  })}}>
394
398
  <div className="py-4">
395
- <Label>{t('dialog.password.newPassword')}</Label>
396
- <Input type="password" required value={passwordDialog.value} onChange={(e) => setPasswordDialog((d) => ({ ...d, value: e.target.value }))} />
399
+ <Label>{m['admin-users_dialog_password_newPassword']()}</Label>
400
+ <Input type="password" required value={passwordDialog.value} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPasswordDialog((d) => ({ ...d, value: e.target.value }))} />
397
401
  </div>
398
402
  <DialogFooter>
399
- <Button type="button" variant="outline" onClick={() => setPasswordDialog((d) => ({ ...d, open: false }))}>{t('common:button.cancel')}</Button>
400
- <Button type="submit" disabled={isPending}>{t('dialog.password.confirm')}</Button>
403
+ <Button type="button" variant="outline" onClick={() => setPasswordDialog((d) => ({ ...d, open: false }))}>{m.common_button_cancel()}</Button>
404
+ <Button type="submit" disabled={isPending}>{m['admin-users_dialog_password_confirm']()}</Button>
401
405
  </DialogFooter>
402
406
  </form>
403
407
  </DialogContent>
404
408
  </Dialog>
405
409
 
406
- <AlertDialog open={deleteDialog.open} onOpenChange={(o) => setDeleteDialog((d) => ({ ...d, open: o }))}>
410
+ <AlertDialog open={deleteDialog.open} onOpenChange={(o: boolean) => setDeleteDialog((d) => ({ ...d, open: o }))}>
407
411
  <AlertDialogContent>
408
412
  <AlertDialogHeader>
409
- <AlertDialogTitle>{t('dialog.delete.title')}</AlertDialogTitle>
413
+ <AlertDialogTitle>{m['admin-users_dialog_delete_title']()}</AlertDialogTitle>
410
414
  <AlertDialogDescription>
411
- {t('dialog.delete.description', { username: user.username })}
415
+ {m['admin-users_dialog_delete_description']({ username: user.username })}
412
416
  </AlertDialogDescription>
413
417
  </AlertDialogHeader>
414
- <Input placeholder={t('dialog.delete.placeholder')} value={deleteDialog.confirmName} onChange={(e) => setDeleteDialog((d) => ({ ...d, confirmName: e.target.value }))} />
418
+ <Input placeholder={m['admin-users_dialog_delete_placeholder']()} value={deleteDialog.confirmName} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setDeleteDialog((d) => ({ ...d, confirmName: e.target.value }))} />
415
419
  <AlertDialogFooter>
416
- <AlertDialogCancel>{t('common:button.cancel')}</AlertDialogCancel>
420
+ <AlertDialogCancel>{m.common_button_cancel()}</AlertDialogCancel>
417
421
  <AlertDialogAction
418
422
  disabled={deleteDialog.confirmName !== user.username || isPending}
419
423
  onClick={() => startTransition(async () => {
420
424
  try {
421
425
  await deleteUserFn({ data: { userId: user.id } })
422
- toast.success(t('toast.userDeleted'))
426
+ toast.success(m['admin-users_toast_userDeleted']())
423
427
  window.location.href = '/users'
424
- } catch { toast.error(t('toast.deleteFailed')) }
428
+ } catch { toast.error(m['admin-users_toast_deleteFailed']()) }
425
429
  })}
426
- >{t('dialog.delete.confirm')}</AlertDialogAction>
430
+ >{m['admin-users_dialog_delete_confirm']()}</AlertDialogAction>
427
431
  </AlertDialogFooter>
428
432
  </AlertDialogContent>
429
433
  </AlertDialog>
@@ -16,8 +16,8 @@
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
18
  import { Outlet, createLazyFileRoute, useMatches } from '@tanstack/react-router'
19
+ import { m } from '@/paraglide/messages.js'
19
20
  import { useState } from 'react'
20
- import { useTranslation } from 'react-i18next'
21
21
  import { UsersTable } from './-components/users-table'
22
22
  import { UserFilters } from '@/components/user/user-filters'
23
23
  import { UserCreateDialog } from '@/components/user/user-create-dialog'
@@ -46,7 +46,6 @@ function UsersLayout() {
46
46
  }
47
47
 
48
48
  function UsersPage() {
49
- const { t } = useTranslation('admin-users')
50
49
  const { items, total } = Route.useLoaderData()
51
50
  const { search, role, status } = Route.useSearch()
52
51
  const [createOpen, setCreateOpen] = useState(false)
@@ -55,12 +54,12 @@ function UsersPage() {
55
54
  <div className="space-y-4 p-6">
56
55
  <div className="flex items-center justify-between">
57
56
  <div>
58
- <h2 className="text-xl font-bold">{t('page.title')}</h2>
59
- <p className="text-sm text-muted-foreground">{t('page.total', { total })}</p>
57
+ <h2 className="text-xl font-bold">{m['admin-users_page_title']()}</h2>
58
+ <p className="text-sm text-muted-foreground">{m['admin-users_page_total']({ total })}</p>
60
59
  </div>
61
60
  <Button onClick={() => setCreateOpen(true)}>
62
61
  <PlusIcon className="mr-1 h-4 w-4" />
63
- {t('page.createUser')}
62
+ {m['admin-users_page_createUser']()}
64
63
  </Button>
65
64
  </div>
66
65
 
@@ -16,8 +16,8 @@
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
18
  import { createLazyFileRoute, useNavigate } from '@tanstack/react-router'
19
+ import { m } from '@/paraglide/messages.js'
19
20
  import { useState } from 'react'
20
- import { useTranslation } from 'react-i18next'
21
21
  import { toast } from '@/components/ui/sonner'
22
22
  import { Loader2, ShieldCheck, User, Mail, Database } from 'lucide-react'
23
23
  import { AuthLayout, AuthLayoutLeft, AuthLayoutRight } from '@/components/auth/auth-layout'
@@ -33,7 +33,6 @@ export const Route = createLazyFileRoute('/setup')({
33
33
  })
34
34
 
35
35
  function SetupPage() {
36
- const { t } = useTranslation('common')
37
36
  const navigate = useNavigate()
38
37
  const [username, setUsername] = useState('')
39
38
  const [email, setEmail] = useState('')
@@ -42,30 +41,30 @@ function SetupPage() {
42
41
  const [loading, setLoading] = useState(false)
43
42
 
44
43
  const setupBrandFeatures = [
45
- { icon: ShieldCheck, label: t('setup.brandFeatures.superAdmin.label'), desc: t('setup.brandFeatures.superAdmin.desc') },
46
- { icon: Database, label: t('setup.brandFeatures.dataMgmt.label'), desc: t('setup.brandFeatures.dataMgmt.desc') },
47
- { icon: User, label: t('setup.brandFeatures.accountSecurity.label'), desc: t('setup.brandFeatures.accountSecurity.desc') },
44
+ { icon: ShieldCheck, label: m.common_setup_brandFeatures_superAdmin_label(), desc: m.common_setup_brandFeatures_superAdmin_desc() },
45
+ { icon: Database, label: m.common_setup_brandFeatures_dataMgmt_label(), desc: m.common_setup_brandFeatures_dataMgmt_desc() },
46
+ { icon: User, label: m.common_setup_brandFeatures_accountSecurity_label(), desc: m.common_setup_brandFeatures_accountSecurity_desc() },
48
47
  ]
49
48
 
50
49
  const handleSubmit = async (e: React.FormEvent) => {
51
50
  e.preventDefault()
52
51
 
53
52
  if (!username || !email || !password) {
54
- toast.error(t('setup.validation.fillAllFields'))
53
+ toast.error(m.common_setup_validation_fillAllFields())
55
54
  return
56
55
  }
57
56
  if (password !== confirmPassword) {
58
- toast.error(t('setup.validation.passwordMismatch'))
57
+ toast.error(m.common_setup_validation_passwordMismatch())
59
58
  return
60
59
  }
61
60
 
62
61
  setLoading(true)
63
62
  try {
64
63
  await setupAdminFn({ data: { username, email, password } })
65
- toast.success(t('setup.success'))
64
+ toast.success(m.common_setup_success())
66
65
  setTimeout(() => navigate({ to: '/login' }), 500)
67
66
  } catch (err) {
68
- toast.error(err instanceof Error ? err.message : t('setup.error'))
67
+ toast.error(err instanceof Error ? err.message : m.common_setup_error())
69
68
  } finally {
70
69
  setLoading(false)
71
70
  }
@@ -74,20 +73,20 @@ function SetupPage() {
74
73
  return (
75
74
  <AuthLayout>
76
75
  <AuthLayoutLeft>
77
- <AuthHeader title={t('setup.form.title')} subtitle={t('setup.form.subtitle')} />
76
+ <AuthHeader title={m.common_setup_form_title()} subtitle={m.common_setup_form_subtitle()} />
78
77
  <form onSubmit={handleSubmit} className="space-y-6">
79
- <IconInput icon={User} label={t('setup.form.username.label')} placeholder={t('setup.form.username.placeholder')} value={username} onChange={(e) => setUsername(e.target.value)} />
80
- <IconInput icon={Mail} label={t('setup.form.email.label')} placeholder={t('setup.form.email.placeholder')} value={email} onChange={(e) => setEmail(e.target.value)} />
81
- <PasswordInput label={t('setup.form.password.label')} placeholder={t('setup.form.password.placeholder')} value={password} onChange={(e) => setPassword(e.target.value)} />
82
- <PasswordInput label={t('setup.form.confirmPassword.label')} placeholder={t('setup.form.confirmPassword.placeholder')} value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} />
78
+ <IconInput icon={User} label={m.common_setup_form_username_label()} placeholder={m.common_setup_form_username_placeholder()} value={username} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setUsername(e.target.value)} />
79
+ <IconInput icon={Mail} label={m.common_setup_form_email_label()} placeholder={m.common_setup_form_email_placeholder()} value={email} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)} />
80
+ <PasswordInput label={m.common_setup_form_password_label()} placeholder={m.common_setup_form_password_placeholder()} value={password} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPassword(e.target.value)} />
81
+ <PasswordInput label={m.common_setup_form_confirmPassword_label()} placeholder={m.common_setup_form_confirmPassword_placeholder()} value={confirmPassword} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setConfirmPassword(e.target.value)} />
83
82
  <Button type="submit" className="w-full" disabled={loading}>
84
83
  {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
85
- {loading ? t('setup.form.loading') : t('setup.form.submit')}
84
+ {loading ? m.common_setup_form_loading() : m.common_setup_form_submit()}
86
85
  </Button>
87
86
  </form>
88
87
  </AuthLayoutLeft>
89
88
  <AuthLayoutRight>
90
- <AuthBrandPanel title={t('setup.brand.title')} subtitle={t('setup.brand.subtitle')} features={setupBrandFeatures} />
89
+ <AuthBrandPanel title={m.common_setup_brand_title()} subtitle={m.common_setup_brand_subtitle()} features={setupBrandFeatures} />
91
90
  </AuthLayoutRight>
92
91
  </AuthLayout>
93
92
  )
package/src/start.ts CHANGED
@@ -72,11 +72,11 @@ const apiCorsMiddleware = createMiddleware().server(async ({ request, next }: an
72
72
  if (typeof process !== 'undefined' && process.versions?.node && process.env.NODE_ENV !== 'test') {
73
73
  initErrorCollector()
74
74
 
75
- getHealthCheckEnabled().then((enabled) => {
75
+ getHealthCheckEnabled().then((enabled: boolean) => {
76
76
  if (enabled) {
77
77
  startHealthCheckTimer(30_000)
78
78
  }
79
- }).catch((e) => console.error('[HealthCheckInit]', e))
79
+ }).catch((e: unknown) => console.error('[HealthCheckInit]', e))
80
80
  }
81
81
 
82
82
  export const startInstance = createStart(() => ({
@@ -1,32 +0,0 @@
1
- // SPDX-License-Identifier: AGPL-3.0-or-later
2
- import i18next, { type i18n as I18nInstance } from 'i18next'
3
- import { initReactI18next } from 'react-i18next'
4
- import LanguageDetector from 'i18next-browser-languagedetector'
5
- import { resources, fallbackLng, defaultNs, supportedLngs } from './config'
6
-
7
- let clientI18n: I18nInstance | null = null
8
-
9
- export function getClientI18n(): I18nInstance {
10
- if (!clientI18n) {
11
- clientI18n = i18next.createInstance()
12
- clientI18n
13
- .use(initReactI18next)
14
- .use(LanguageDetector)
15
- .init({
16
- fallbackLng,
17
- defaultNS: defaultNs,
18
- supportedLngs: [...supportedLngs],
19
- resources,
20
- interpolation: { escapeValue: false },
21
- react: { useSuspense: false },
22
- detection: {
23
- order: ['querystring', 'cookie', 'localStorage', 'navigator'],
24
- lookupQuerystring: 'lng',
25
- lookupCookie: 'lng',
26
- lookupLocalStorage: 'lng',
27
- caches: ['cookie', 'localStorage'],
28
- },
29
- })
30
- }
31
- return clientI18n
32
- }