@byline/host-tanstack-start 2.5.1 → 2.6.0

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 (123) hide show
  1. package/dist/admin-shell/admin-roles/container.js +38 -24
  2. package/dist/admin-shell/admin-roles/delete.js +9 -7
  3. package/dist/admin-shell/admin-roles/list.js +20 -16
  4. package/dist/admin-shell/admin-users/container.js +79 -56
  5. package/dist/admin-shell/admin-users/delete.js +10 -8
  6. package/dist/admin-shell/admin-users/list.js +27 -18
  7. package/dist/admin-shell/chrome/admin-app-bar.js +5 -2
  8. package/dist/admin-shell/chrome/breadcrumbs/breadcrumbs.js +3 -1
  9. package/dist/admin-shell/chrome/dashboard.js +13 -11
  10. package/dist/admin-shell/chrome/hamburger.js +3 -1
  11. package/dist/admin-shell/chrome/menu-drawer.js +7 -5
  12. package/dist/admin-shell/chrome/preview-toggle.js +5 -3
  13. package/dist/admin-shell/chrome/route-error.d.ts +3 -2
  14. package/dist/admin-shell/chrome/route-error.js +29 -22
  15. package/dist/admin-shell/chrome/sign-in-page.d.ts +16 -4
  16. package/dist/admin-shell/chrome/sign-in-page.js +38 -13
  17. package/dist/admin-shell/chrome/sign-in-page.module.js +1 -0
  18. package/dist/admin-shell/chrome/sign-in-page_module.css +8 -1
  19. package/dist/admin-shell/collections/api.js +6 -5
  20. package/dist/admin-shell/collections/create.js +12 -4
  21. package/dist/admin-shell/collections/edit.js +112 -37
  22. package/dist/admin-shell/collections/history.js +17 -12
  23. package/dist/admin-shell/collections/list.js +18 -13
  24. package/dist/admin-shell/collections/preview-link.d.ts +1 -10
  25. package/dist/admin-shell/collections/preview-link.js +9 -11
  26. package/dist/admin-shell/collections/resolve-preview-url.d.ts +34 -0
  27. package/dist/admin-shell/collections/resolve-preview-url.js +17 -0
  28. package/dist/admin-shell/collections/restore-version-modal.js +13 -14
  29. package/dist/admin-shell/collections/tanstack-navigation-guard.d.ts +1 -1
  30. package/dist/admin-shell/collections/view-menu.js +7 -5
  31. package/dist/i18n/index.d.ts +19 -0
  32. package/dist/i18n/index.js +4 -0
  33. package/dist/i18n/locale-cookie.d.ts +17 -0
  34. package/dist/i18n/locale-cookie.js +26 -0
  35. package/dist/i18n/locale-definitions.d.ts +29 -0
  36. package/dist/i18n/locale-definitions.js +27 -0
  37. package/dist/i18n/resolve-locale.d.ts +20 -0
  38. package/dist/i18n/resolve-locale.js +43 -0
  39. package/dist/i18n/server-translator.d.ts +33 -0
  40. package/dist/i18n/server-translator.js +19 -0
  41. package/dist/integrations/byline-admin-services.js +2 -0
  42. package/dist/integrations/byline-field-services.d.ts +3 -3
  43. package/dist/routes/create-admin-account-route.js +6 -3
  44. package/dist/routes/create-admin-dashboard-route.js +3 -1
  45. package/dist/routes/create-admin-layout-route.js +48 -25
  46. package/dist/routes/create-admin-permissions-route.js +4 -2
  47. package/dist/routes/create-admin-role-edit-route.js +5 -3
  48. package/dist/routes/create-admin-roles-list-route.js +4 -2
  49. package/dist/routes/create-admin-user-edit-route.js +5 -3
  50. package/dist/routes/create-admin-users-list-route.js +4 -2
  51. package/dist/routes/create-collection-api-route.js +5 -3
  52. package/dist/routes/create-collection-create-route.js +4 -2
  53. package/dist/routes/create-collection-edit-route.js +4 -2
  54. package/dist/routes/create-collection-history-route.js +5 -3
  55. package/dist/routes/create-collection-list-route.js +11 -5
  56. package/dist/routes/create-sign-in-route.js +10 -1
  57. package/dist/server-fns/admin-account/change-password.d.ts +1 -0
  58. package/dist/server-fns/admin-account/get.d.ts +1 -0
  59. package/dist/server-fns/admin-account/update.d.ts +1 -0
  60. package/dist/server-fns/admin-users/create.d.ts +1 -0
  61. package/dist/server-fns/admin-users/get.d.ts +1 -0
  62. package/dist/server-fns/admin-users/list.d.ts +1 -0
  63. package/dist/server-fns/admin-users/set-password.d.ts +1 -0
  64. package/dist/server-fns/admin-users/update.d.ts +1 -0
  65. package/dist/server-fns/auth/sign-in.js +18 -0
  66. package/dist/server-fns/i18n/get-active-locale.d.ts +8 -0
  67. package/dist/server-fns/i18n/get-active-locale.js +6 -0
  68. package/dist/server-fns/i18n/index.d.ts +10 -0
  69. package/dist/server-fns/i18n/index.js +2 -0
  70. package/dist/server-fns/i18n/set-locale.d.ts +25 -0
  71. package/dist/server-fns/i18n/set-locale.js +42 -0
  72. package/package.json +16 -7
  73. package/src/admin-shell/admin-roles/container.tsx +41 -31
  74. package/src/admin-shell/admin-roles/delete.tsx +10 -11
  75. package/src/admin-shell/admin-roles/list.tsx +29 -16
  76. package/src/admin-shell/admin-users/container.tsx +77 -50
  77. package/src/admin-shell/admin-users/delete.tsx +11 -12
  78. package/src/admin-shell/admin-users/list.tsx +39 -18
  79. package/src/admin-shell/chrome/admin-app-bar.tsx +5 -2
  80. package/src/admin-shell/chrome/breadcrumbs/breadcrumbs.tsx +3 -1
  81. package/src/admin-shell/chrome/dashboard.tsx +9 -3
  82. package/src/admin-shell/chrome/hamburger.tsx +3 -1
  83. package/src/admin-shell/chrome/menu-drawer.tsx +7 -5
  84. package/src/admin-shell/chrome/preview-toggle.tsx +6 -4
  85. package/src/admin-shell/chrome/route-error.tsx +39 -26
  86. package/src/admin-shell/chrome/sign-in-page.module.css +10 -1
  87. package/src/admin-shell/chrome/sign-in-page.tsx +46 -12
  88. package/src/admin-shell/collections/api.tsx +5 -1
  89. package/src/admin-shell/collections/create.tsx +10 -4
  90. package/src/admin-shell/collections/edit.tsx +79 -72
  91. package/src/admin-shell/collections/history.tsx +18 -12
  92. package/src/admin-shell/collections/list.tsx +25 -14
  93. package/src/admin-shell/collections/preview-link.tsx +20 -33
  94. package/src/admin-shell/collections/resolve-preview-url.test.node.ts +167 -0
  95. package/src/admin-shell/collections/resolve-preview-url.ts +67 -0
  96. package/src/admin-shell/collections/restore-version-modal.tsx +11 -12
  97. package/src/admin-shell/collections/tanstack-navigation-guard.ts +1 -1
  98. package/src/admin-shell/collections/view-menu.tsx +9 -5
  99. package/src/i18n/index.ts +26 -0
  100. package/src/i18n/locale-cookie.ts +68 -0
  101. package/src/i18n/locale-definitions.ts +48 -0
  102. package/src/i18n/resolve-locale.ts +96 -0
  103. package/src/i18n/server-translator.ts +60 -0
  104. package/src/integrations/byline-admin-services.ts +2 -0
  105. package/src/integrations/byline-field-services.ts +7 -3
  106. package/src/routes/create-admin-account-route.tsx +6 -4
  107. package/src/routes/create-admin-dashboard-route.tsx +5 -1
  108. package/src/routes/create-admin-layout-route.tsx +53 -20
  109. package/src/routes/create-admin-permissions-route.tsx +4 -2
  110. package/src/routes/create-admin-role-edit-route.tsx +5 -3
  111. package/src/routes/create-admin-roles-list-route.tsx +5 -2
  112. package/src/routes/create-admin-user-edit-route.tsx +5 -3
  113. package/src/routes/create-admin-users-list-route.tsx +4 -2
  114. package/src/routes/create-collection-api-route.tsx +5 -3
  115. package/src/routes/create-collection-create-route.tsx +7 -2
  116. package/src/routes/create-collection-edit-route.tsx +4 -2
  117. package/src/routes/create-collection-history-route.tsx +5 -3
  118. package/src/routes/create-collection-list-route.tsx +8 -10
  119. package/src/routes/create-sign-in-route.tsx +14 -1
  120. package/src/server-fns/auth/sign-in.ts +45 -0
  121. package/src/server-fns/i18n/get-active-locale.ts +26 -0
  122. package/src/server-fns/i18n/index.ts +11 -0
  123. package/src/server-fns/i18n/set-locale.ts +103 -0
@@ -15,13 +15,14 @@ import { useRouter } from '@tanstack/react-router'
15
15
  import { UserRoles } from '@byline/admin/admin-users/components/roles'
16
16
  import { SetPassword } from '@byline/admin/admin-users/components/set-password'
17
17
  import { UpdateUser } from '@byline/admin/admin-users/components/update'
18
+ import { LocalDateTime } from '@byline/admin/react'
19
+ import { useTranslation } from '@byline/i18n/react'
18
20
  import {
19
21
  Button,
20
22
  CloseIcon,
21
23
  Drawer,
22
24
  EditIcon,
23
25
  IconButton,
24
- LocalDateTime,
25
26
  Modal,
26
27
  useToastManager,
27
28
  } from '@byline/ui/react'
@@ -61,45 +62,50 @@ interface PanelProps {
61
62
  onSuccess?: (user: AdminUserResponse) => void
62
63
  }
63
64
 
64
- const panels: Record<
65
- ComponentKey,
66
- { title: string; drawerWidth: 'medium' | 'large'; component: React.ComponentType<PanelProps> }
67
- > = {
65
+ type PanelMeta = {
66
+ drawerWidth: 'medium' | 'large'
67
+ component: React.ComponentType<PanelProps>
68
+ titleKey: string
69
+ }
70
+
71
+ const panelMeta: Record<ComponentKey, PanelMeta> = {
68
72
  update: {
69
- title: 'Account Details',
70
73
  drawerWidth: 'medium',
71
74
  component: UpdateUser,
75
+ titleKey: 'adminUsers.detail.panels.update',
72
76
  },
73
77
  set_password: {
74
- title: 'Set Password',
75
78
  drawerWidth: 'medium',
76
79
  component: SetPassword,
80
+ titleKey: 'adminUsers.detail.panels.setPassword',
77
81
  },
78
82
  delete_user: {
79
- title: 'Delete Admin User',
80
83
  drawerWidth: 'medium',
81
84
  component: DeleteUser,
85
+ titleKey: 'adminUsers.detail.panels.delete',
82
86
  },
83
87
  roles: {
84
- title: 'User Roles',
85
88
  drawerWidth: 'medium',
86
89
  // See container header docstring — rendered inline, this is a stub.
87
90
  component: () => null,
91
+ titleKey: 'adminUsers.detail.panels.roles',
88
92
  },
89
93
  empty: {
90
- title: '',
91
94
  drawerWidth: 'medium',
92
95
  component: () => null,
96
+ titleKey: '',
93
97
  },
94
98
  }
95
99
 
96
100
  function ContainerSection({
97
101
  title,
98
102
  onEdit,
103
+ editAriaLabel,
99
104
  children,
100
105
  }: {
101
106
  title: string
102
107
  onEdit?: () => void
108
+ editAriaLabel?: string
103
109
  children: React.ReactNode
104
110
  }) {
105
111
  return (
@@ -107,7 +113,7 @@ function ContainerSection({
107
113
  <div className={cx('byline-admin-user-section-head', styles.sectionHead)}>
108
114
  <h2>{title}</h2>
109
115
  {onEdit ? (
110
- <IconButton variant="text" onClick={onEdit} aria-label={`Edit ${title}`}>
116
+ <IconButton variant="text" onClick={onEdit} aria-label={editAriaLabel ?? title}>
111
117
  <EditIcon width="20px" height="20px" />
112
118
  </IconButton>
113
119
  ) : null}
@@ -137,6 +143,7 @@ interface AccountContainerProps {
137
143
  export function AccountContainer({ user, allRoles, initialUserRoles }: AccountContainerProps) {
138
144
  const router = useRouter()
139
145
  const toastManager = useToastManager()
146
+ const { t } = useTranslation('byline-admin')
140
147
  const [currentUser, setCurrentUser] = useState<AdminUserResponse>(user)
141
148
  const [currentUserRoles, setCurrentUserRoles] = useState<AdminRoleResponse[]>(initialUserRoles)
142
149
  const [current, setCurrent] = useState<ComponentKey>('empty')
@@ -168,56 +175,66 @@ export function AccountContainer({ user, allRoles, initialUserRoles }: AccountCo
168
175
  setCurrentUserRoles(response.roles)
169
176
  void router.invalidate()
170
177
  toastManager.add({
171
- title: 'Roles saved',
172
- description: `${response.roles.length} role${response.roles.length === 1 ? '' : 's'} assigned to ${currentUser.email}.`,
178
+ title: t('adminUsers.detail.rolesSavedToast'),
179
+ description: t('adminUsers.detail.rolesAssignedDescription', {
180
+ count: response.roles.length,
181
+ email: currentUser.email,
182
+ }),
173
183
  data: { intent: 'success' },
174
184
  })
175
185
  }
176
186
 
177
- const Panel = panels[current].component
187
+ const currentMeta = panelMeta[current]
188
+ const Panel = currentMeta.component
189
+ const currentTitle = currentMeta.titleKey ? t(currentMeta.titleKey) : ''
190
+ const editAriaFor = (section: string) => t('account.editAriaLabel', { section })
178
191
 
179
192
  return (
180
193
  <>
181
194
  <div className={cx('byline-admin-user-grid', styles.grid)}>
182
195
  <div className={cx('byline-admin-user-column', styles.column)}>
183
- <ContainerSection title="Account Details" onEdit={openDrawer('update')}>
196
+ <ContainerSection
197
+ title={t('adminUsers.detail.sections.account')}
198
+ onEdit={openDrawer('update')}
199
+ editAriaLabel={editAriaFor(t('adminUsers.detail.sections.account'))}
200
+ >
184
201
  <p className={cx('byline-admin-user-line', styles.line)}>
185
- <span className="muted">Email:</span> {currentUser.email}
202
+ <span className="muted">{t('account.profile.emailColon')}</span> {currentUser.email}
186
203
  </p>
187
204
  <p className={cx('byline-admin-user-line', styles.line)}>
188
- <span className="muted">Given name:</span>{' '}
205
+ <span className="muted">{t('account.profile.givenName')}</span>{' '}
189
206
  {currentUser.given_name ?? (
190
207
  <span className={cx('muted byline-admin-user-not-set', styles.notSet)}>
191
- Not set
208
+ {t('common.notSet')}
192
209
  </span>
193
210
  )}
194
211
  </p>
195
212
  <p className={cx('byline-admin-user-line', styles.line)}>
196
- <span className="muted">Family name:</span>{' '}
213
+ <span className="muted">{t('account.profile.familyName')}</span>{' '}
197
214
  {currentUser.family_name ?? (
198
215
  <span className={cx('muted byline-admin-user-not-set', styles.notSet)}>
199
- Not set
216
+ {t('common.notSet')}
200
217
  </span>
201
218
  )}
202
219
  </p>
203
220
  <p className={cx('byline-admin-user-line', styles.line)}>
204
- <span className="muted">Username:</span>{' '}
221
+ <span className="muted">{t('account.profile.username')}</span>{' '}
205
222
  {currentUser.username ?? (
206
223
  <span className={cx('muted byline-admin-user-not-set', styles.notSet)}>
207
- Not set
224
+ {t('common.notSet')}
208
225
  </span>
209
226
  )}
210
227
  </p>
211
228
  <p className={cx('byline-admin-user-line', styles.line)}>
212
- <span className="muted">Super admin:</span>{' '}
213
- {currentUser.is_super_admin ? 'Yes' : 'No'}
229
+ <span className="muted">{t('account.status.superAdmin')}</span>{' '}
230
+ {currentUser.is_super_admin ? t('common.boolean.yes') : t('common.boolean.no')}
214
231
  </p>
215
232
  <p className={cx('byline-admin-user-line', styles.line)}>
216
- <span className="muted">Email verified:</span>{' '}
217
- {currentUser.is_email_verified ? 'Yes' : 'No'}
233
+ <span className="muted">{t('account.status.emailVerified')}</span>{' '}
234
+ {currentUser.is_email_verified ? t('common.boolean.yes') : t('common.boolean.no')}
218
235
  </p>
219
236
  <p className={cx('byline-admin-user-line-spaced', styles.lineSpaced)}>
220
- <span className="muted">Status:</span>{' '}
237
+ <span className="muted">{t('account.status.status')}</span>{' '}
221
238
  <span
222
239
  className={cx({
223
240
  'byline-admin-user-status-on': currentUser.is_enabled,
@@ -226,34 +243,40 @@ export function AccountContainer({ user, allRoles, initialUserRoles }: AccountCo
226
243
  [styles.statusOff]: !currentUser.is_enabled,
227
244
  })}
228
245
  >
229
- {currentUser.is_enabled ? 'Enabled' : 'Disabled'}
246
+ {currentUser.is_enabled
247
+ ? t('account.status.enabled')
248
+ : t('account.status.disabled')}
230
249
  </span>
231
250
  </p>
232
251
  <Button size="sm" onClick={openDrawer('update')}>
233
- Update Details
252
+ {t('adminUsers.detail.updateButton')}
234
253
  </Button>
235
254
  <div className={cx('muted byline-admin-user-meta', styles.meta)}>
236
255
  <p>
237
- <span className="font-bold">Created:&nbsp;</span>
256
+ <span className="font-bold">{t('account.profile.created')}&nbsp;</span>
238
257
  <LocalDateTime value={currentUser.created_at} />
239
258
  </p>
240
259
  <p>
241
- <span className="font-bold">Updated:&nbsp;</span>
260
+ <span className="font-bold">{t('account.profile.updated')}&nbsp;</span>
242
261
  <LocalDateTime value={currentUser.updated_at} />
243
262
  </p>
244
263
  <p className={cx('byline-admin-user-line', styles.line)}>
245
- <span className="font-bold">Last login:&nbsp;</span>
246
- <LocalDateTime value={currentUser.last_login} fallback="Never" />
264
+ <span className="font-bold">{t('account.profile.lastLogin')}&nbsp;</span>
265
+ <LocalDateTime value={currentUser.last_login} fallback={t('common.never')} />
247
266
  </p>
248
267
  </div>
249
268
  </ContainerSection>
250
269
  </div>
251
270
 
252
271
  <div className={cx('byline-admin-user-column', styles.column)}>
253
- <ContainerSection title="Roles" onEdit={openDrawer('roles')}>
272
+ <ContainerSection
273
+ title={t('adminUsers.detail.sections.roles')}
274
+ onEdit={openDrawer('roles')}
275
+ editAriaLabel={editAriaFor(t('adminUsers.detail.sections.roles'))}
276
+ >
254
277
  {currentUserRoles.length === 0 ? (
255
278
  <p className={cx('muted byline-admin-user-role-empty', styles.roleEmpty)}>
256
- No roles assigned.
279
+ {t('adminUsers.detail.rolesEmpty')}
257
280
  </p>
258
281
  ) : (
259
282
  <div className={cx('byline-admin-user-role-list', styles.roleList)}>
@@ -263,25 +286,29 @@ export function AccountContainer({ user, allRoles, initialUserRoles }: AccountCo
263
286
  </div>
264
287
  )}
265
288
  <Button size="sm" onClick={openDrawer('roles')}>
266
- Edit Roles
289
+ {t('adminUsers.detail.editRolesButton')}
267
290
  </Button>
268
291
  </ContainerSection>
269
292
 
270
- <ContainerSection title="Password" onEdit={openDrawer('set_password')}>
293
+ <ContainerSection
294
+ title={t('adminUsers.detail.sections.password')}
295
+ onEdit={openDrawer('set_password')}
296
+ editAriaLabel={editAriaFor(t('adminUsers.detail.sections.password'))}
297
+ >
271
298
  <p className={cx('byline-admin-user-line-spaced', styles.lineSpaced)}>
272
- Set a new password for this user.
299
+ {t('adminUsers.detail.password.intro')}
273
300
  </p>
274
301
  <Button size="sm" onClick={openDrawer('set_password')}>
275
- Set Password
302
+ {t('adminUsers.detail.password.setButton')}
276
303
  </Button>
277
304
  </ContainerSection>
278
305
 
279
- <ContainerSection title="Delete Admin User">
306
+ <ContainerSection title={t('adminUsers.detail.sections.delete')}>
280
307
  <p className={cx('byline-admin-user-line-spaced', styles.lineSpaced)}>
281
- Permanently delete this admin user.
308
+ {t('adminUsers.detail.delete.intro')}
282
309
  </p>
283
310
  <Button size="sm" intent="danger" onClick={openModal('delete_user')}>
284
- Delete Admin User
311
+ {t('adminUsers.detail.delete.button')}
285
312
  </Button>
286
313
  </ContainerSection>
287
314
  </div>
@@ -295,10 +322,10 @@ export function AccountContainer({ user, allRoles, initialUserRoles }: AccountCo
295
322
  isOpen={isDrawerOpen}
296
323
  onDismiss={closeDrawer}
297
324
  className={cx({
298
- 'byline-admin-user-drawer-large': panels[current].drawerWidth === 'large',
299
- [styles.drawerLarge]: panels[current].drawerWidth === 'large',
300
- 'byline-admin-user-drawer': panels[current].drawerWidth !== 'large',
301
- [styles.drawer]: panels[current].drawerWidth !== 'large',
325
+ 'byline-admin-user-drawer-large': currentMeta.drawerWidth === 'large',
326
+ [styles.drawerLarge]: currentMeta.drawerWidth === 'large',
327
+ 'byline-admin-user-drawer': currentMeta.drawerWidth !== 'large',
328
+ [styles.drawer]: currentMeta.drawerWidth !== 'large',
302
329
  })}
303
330
  >
304
331
  <Drawer.Container aria-hidden={!isDrawerOpen} className="p-2">
@@ -306,12 +333,12 @@ export function AccountContainer({ user, allRoles, initialUserRoles }: AccountCo
306
333
  <button type="button" tabIndex={0} className="sr-only">
307
334
  no action
308
335
  </button>
309
- <IconButton aria-label="Close" size="sm" onClick={closeDrawer}>
336
+ <IconButton aria-label={t('common.actions.close')} size="sm" onClick={closeDrawer}>
310
337
  <CloseIcon width="14px" height="14px" svgClassName="white-icon stroke-white" />
311
338
  </IconButton>
312
339
  </Drawer.TopActions>
313
340
  <Drawer.Header>
314
- <h2>{panels[current].title}</h2>
341
+ <h2>{currentTitle}</h2>
315
342
  </Drawer.Header>
316
343
  <Drawer.Content>
317
344
  <div className={cx('byline-admin-user-drawer-scroll', styles.drawerScroll)}>
@@ -334,8 +361,8 @@ export function AccountContainer({ user, allRoles, initialUserRoles }: AccountCo
334
361
  <Modal isOpen={isModalOpen} onDismiss={closeModal} closeOnOverlayClick={false}>
335
362
  <Modal.Container className={cx('byline-admin-user-modal', styles.modal)}>
336
363
  <Modal.Header className={cx('byline-admin-user-modal-head', styles.modalHead)}>
337
- <h3 className="m-0">{panels[current].title}</h3>
338
- <IconButton aria-label="Close" size="sm" onClick={closeModal}>
364
+ <h3 className="m-0">{currentTitle}</h3>
365
+ <IconButton aria-label={t('common.actions.close')} size="sm" onClick={closeModal}>
339
366
  <CloseIcon width="14px" height="14px" svgClassName="white-icon" />
340
367
  </IconButton>
341
368
  </Modal.Header>
@@ -23,6 +23,7 @@
23
23
  import { useState } from 'react'
24
24
  import { useRouter } from '@tanstack/react-router'
25
25
 
26
+ import { useTranslation } from '@byline/i18n/react'
26
27
  import { Alert, Button, LoaderEllipsis, Modal } from '@byline/ui/react'
27
28
  import cx from 'classnames'
28
29
 
@@ -46,6 +47,7 @@ function displayNameFor(user: AdminUserResponse): string {
46
47
  export function DeleteUser({ user, onClose }: DeleteUserProps) {
47
48
  const navigate = useNavigate()
48
49
  const router = useRouter()
50
+ const { t } = useTranslation('byline-admin')
49
51
  const [error, setError] = useState<string | null>(null)
50
52
  const [pending, setPending] = useState(false)
51
53
 
@@ -64,15 +66,13 @@ export function DeleteUser({ user, onClose }: DeleteUserProps) {
64
66
  } catch (err) {
65
67
  const code = getErrorCode(err)
66
68
  if (code === 'admin.users.selfDeleteForbidden') {
67
- setError('You cannot delete your own admin account.')
69
+ setError(t('adminUsers.delete.errors.selfDelete'))
68
70
  } else if (code === 'admin.users.versionConflict') {
69
- setError(
70
- 'This user has been modified elsewhere since you opened this dialog. Close and reload before trying again.'
71
- )
71
+ setError(t('adminUsers.delete.errors.versionConflict'))
72
72
  } else if (code === 'admin.users.notFound') {
73
- setError('This user has already been deleted.')
73
+ setError(t('adminUsers.delete.errors.notFound'))
74
74
  } else {
75
- setError('Could not delete this admin user. Please try again.')
75
+ setError(t('adminUsers.delete.errors.fallback'))
76
76
  }
77
77
  setPending(false)
78
78
  }
@@ -87,14 +87,13 @@ export function DeleteUser({ user, onClose }: DeleteUserProps) {
87
87
  </Alert>
88
88
  ) : null}
89
89
  <p className={cx('byline-admin-user-delete-row', styles.row)}>
90
- <span className="muted">User:</span> {displayNameFor(user)}
90
+ <span className="muted">{t('adminUsers.delete.userLabel')}</span> {displayNameFor(user)}
91
91
  </p>
92
92
  <p className={cx('byline-admin-user-delete-row', styles.row)}>
93
- <span className="muted">Email:</span> {user.email}
93
+ <span className="muted">{t('adminUsers.delete.emailLabel')}</span> {user.email}
94
94
  </p>
95
95
  <p className={cx('byline-admin-user-delete-warning', styles.warning)}>
96
- This will permanently delete the admin user. The action cannot be undone. Any active
97
- sessions will be invalidated at the next refresh.
96
+ {t('adminUsers.delete.warning')}
98
97
  </p>
99
98
  </div>
100
99
  <div className={cx('byline-admin-user-delete-actions', styles.actions)}>
@@ -106,7 +105,7 @@ export function DeleteUser({ user, onClose }: DeleteUserProps) {
106
105
  disabled={pending}
107
106
  className={cx('byline-admin-user-delete-button', styles.button)}
108
107
  >
109
- Cancel
108
+ {t('common.actions.cancel')}
110
109
  </Button>
111
110
  <Button
112
111
  size="sm"
@@ -115,7 +114,7 @@ export function DeleteUser({ user, onClose }: DeleteUserProps) {
115
114
  disabled={pending}
116
115
  className={cx('byline-admin-user-delete-button', styles.button)}
117
116
  >
118
- {pending === true ? <LoaderEllipsis size={42} /> : 'Delete User'}
117
+ {pending === true ? <LoaderEllipsis size={42} /> : t('adminUsers.delete.confirmButton')}
119
118
  </Button>
120
119
  </div>
121
120
  </Modal.Content>
@@ -8,16 +8,17 @@
8
8
  * Copyright (c) Infonomic Company Limited
9
9
  */
10
10
 
11
- import { useState } from 'react'
11
+ import { useMemo, useState } from 'react'
12
12
  import { useRouter, useRouterState } from '@tanstack/react-router'
13
13
 
14
14
  import { CreateAdminUser } from '@byline/admin/admin-users/components/create'
15
+ import { LocalDateTime } from '@byline/admin/react'
16
+ import { useTranslation } from '@byline/i18n/react'
15
17
  import {
16
18
  CloseIcon,
17
19
  Container,
18
20
  Drawer,
19
21
  IconButton,
20
- LocalDateTime,
21
22
  PlusIcon,
22
23
  Search,
23
24
  Section,
@@ -40,10 +41,18 @@ import type {
40
41
  AdminUserResponse,
41
42
  } from '../../server-fns/admin-users/index.js'
42
43
 
43
- const tableColumnDefs: Omit<TableHeadingCellSortableProps, 'lng'>[] = [
44
+ // Structural column-def template: position / sort-key / alignment / class
45
+ // are fixed at module scope; the `label` field is filled in per-render
46
+ // inside the component so it can flow through `t(...)`. Same pattern the
47
+ // account container's `panels` map uses.
48
+ type ColumnTemplate = Omit<TableHeadingCellSortableProps, 'lng' | 'label'> & {
49
+ labelKey: string
50
+ }
51
+
52
+ const columnTemplates: ColumnTemplate[] = [
44
53
  {
45
54
  fieldName: 'given_name',
46
- label: 'Given Name',
55
+ labelKey: 'adminUsers.list.columns.givenName',
47
56
  path: '/admin/users',
48
57
  sortable: true,
49
58
  scope: 'col',
@@ -52,7 +61,7 @@ const tableColumnDefs: Omit<TableHeadingCellSortableProps, 'lng'>[] = [
52
61
  },
53
62
  {
54
63
  fieldName: 'family_name',
55
- label: 'Family Name',
64
+ labelKey: 'adminUsers.list.columns.familyName',
56
65
  path: '/admin/users',
57
66
  sortable: true,
58
67
  scope: 'col',
@@ -61,7 +70,7 @@ const tableColumnDefs: Omit<TableHeadingCellSortableProps, 'lng'>[] = [
61
70
  },
62
71
  {
63
72
  fieldName: 'email',
64
- label: 'Email',
73
+ labelKey: 'adminUsers.list.columns.email',
65
74
  path: '/admin/users',
66
75
  sortable: true,
67
76
  scope: 'col',
@@ -70,7 +79,7 @@ const tableColumnDefs: Omit<TableHeadingCellSortableProps, 'lng'>[] = [
70
79
  },
71
80
  {
72
81
  fieldName: 'updated_at',
73
- label: 'Updated',
82
+ labelKey: 'adminUsers.list.columns.updated',
74
83
  path: '/admin/users',
75
84
  sortable: true,
76
85
  scope: 'col',
@@ -79,7 +88,7 @@ const tableColumnDefs: Omit<TableHeadingCellSortableProps, 'lng'>[] = [
79
88
  },
80
89
  {
81
90
  fieldName: 'created_at',
82
- label: 'Created',
91
+ labelKey: 'adminUsers.list.columns.created',
83
92
  path: '/admin/users',
84
93
  sortable: true,
85
94
  scope: 'col',
@@ -123,8 +132,14 @@ export function AdminUsersListView({ data }: { data: AdminUserListResponse }) {
123
132
  const router = useRouter()
124
133
  const toastManager = useToastManager()
125
134
  const pathname = useRouterState({ select: (s) => s.location.pathname })
135
+ const { t } = useTranslation('byline-admin')
126
136
  const [isCreateDrawerOpen, setIsCreateDrawerOpen] = useState(false)
127
137
 
138
+ const tableColumnDefs = useMemo(
139
+ () => columnTemplates.map(({ labelKey, ...rest }) => ({ ...rest, label: t(labelKey) })),
140
+ [t]
141
+ )
142
+
128
143
  const openCreateDrawer = () => setIsCreateDrawerOpen(true)
129
144
  const closeCreateDrawer = () => setIsCreateDrawerOpen(false)
130
145
 
@@ -134,7 +149,7 @@ export function AdminUsersListView({ data }: { data: AdminUserListResponse }) {
134
149
  // current page of the table.
135
150
  void router.invalidate()
136
151
  toastManager.add({
137
- title: 'Admin user created',
152
+ title: t('adminUsers.list.createdToastTitle'),
138
153
  description: created.email,
139
154
  data: { intent: 'success' },
140
155
  })
@@ -173,9 +188,11 @@ export function AdminUsersListView({ data }: { data: AdminUserListResponse }) {
173
188
  <Section>
174
189
  <Container>
175
190
  <div className={cx('byline-admin-users-list-head', styles.head)}>
176
- <h1 className={cx('byline-admin-users-list-title', styles.title)}>Admin Users</h1>
191
+ <h1 className={cx('byline-admin-users-list-title', styles.title)}>
192
+ {t('adminUsers.list.title')}
193
+ </h1>
177
194
  <Stats total={data.meta.total} />
178
- <IconButton aria-label="Create New Admin User" onClick={openCreateDrawer}>
195
+ <IconButton aria-label={t('adminUsers.list.createAriaLabel')} onClick={openCreateDrawer}>
179
196
  <PlusIcon height="18px" width="18px" svgClassName="stroke-white" />
180
197
  </IconButton>
181
198
  </div>
@@ -184,7 +201,7 @@ export function AdminUsersListView({ data }: { data: AdminUserListResponse }) {
184
201
  onSearch={handleOnSearch}
185
202
  onClear={handleOnClear}
186
203
  inputSize="sm"
187
- placeholder="Search by name or email"
204
+ placeholder={t('adminUsers.list.searchPlaceholder')}
188
205
  className={cx('byline-admin-users-list-search', styles.search)}
189
206
  />
190
207
  <RouterPager
@@ -193,7 +210,7 @@ export function AdminUsersListView({ data }: { data: AdminUserListResponse }) {
193
210
  showFirstButton
194
211
  showLastButton
195
212
  componentName="pagerTop"
196
- aria-label="Top Pager"
213
+ aria-label={t('adminUsers.list.pagerTopAriaLabel')}
197
214
  />
198
215
  </div>
199
216
  <Table.Container className={cx('byline-admin-users-list-table-wrap', styles.tableWrap)}>
@@ -220,7 +237,7 @@ export function AdminUsersListView({ data }: { data: AdminUserListResponse }) {
220
237
  <span
221
238
  className={cx('muted byline-admin-users-list-not-set', styles.notSet)}
222
239
  >
223
- Not set
240
+ {t('common.notSet')}
224
241
  </span>
225
242
  )}
226
243
  </Link>
@@ -228,7 +245,7 @@ export function AdminUsersListView({ data }: { data: AdminUserListResponse }) {
228
245
  <Table.Cell>
229
246
  {user.family_name ?? (
230
247
  <span className={cx('muted byline-admin-users-list-not-set', styles.notSet)}>
231
- Not set
248
+ {t('common.notSet')}
232
249
  </span>
233
250
  )}
234
251
  </Table.Cell>
@@ -277,7 +294,7 @@ export function AdminUsersListView({ data }: { data: AdminUserListResponse }) {
277
294
  showFirstButton
278
295
  showLastButton
279
296
  componentName="pagerBottom"
280
- aria-label="Bottom Pager"
297
+ aria-label={t('adminUsers.list.pagerBottomAriaLabel')}
281
298
  />
282
299
  </div>
283
300
  </Container>
@@ -296,12 +313,16 @@ export function AdminUsersListView({ data }: { data: AdminUserListResponse }) {
296
313
  <button type="button" tabIndex={0} className="sr-only">
297
314
  no action
298
315
  </button>
299
- <IconButton aria-label="Close" size="sm" onClick={closeCreateDrawer}>
316
+ <IconButton
317
+ aria-label={t('common.actions.close')}
318
+ size="sm"
319
+ onClick={closeCreateDrawer}
320
+ >
300
321
  <CloseIcon width="14px" height="14px" svgClassName="white-icon stroke-white" />
301
322
  </IconButton>
302
323
  </Drawer.TopActions>
303
324
  <Drawer.Header>
304
- <h2>New Admin User</h2>
325
+ <h2>{t('adminUsers.list.newDrawerTitle')}</h2>
305
326
  </Drawer.Header>
306
327
  <Drawer.Content>
307
328
  <div className={cx('byline-admin-users-list-drawer-scroll', styles.drawerScroll)}>
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { useState } from 'react'
12
12
 
13
+ import { LanguageMenu, useTranslation } from '@byline/i18n/react'
13
14
  import { Button } from '@byline/ui/react'
14
15
  import cx from 'classnames'
15
16
 
@@ -32,6 +33,7 @@ function displayNameFor(user: CurrentAdminUser): string {
32
33
 
33
34
  export function AdminAppBar({ user }: AdminAppBarProps) {
34
35
  const { breadCrumbSettings } = useBreadcrumbs()
36
+ const { t } = useTranslation('byline-admin')
35
37
  const [signingOut, setSigningOut] = useState(false)
36
38
 
37
39
  async function handleSignOut() {
@@ -59,14 +61,15 @@ export function AdminAppBar({ user }: AdminAppBarProps) {
59
61
  />
60
62
  </div>
61
63
  <div className={cx('byline-admin-app-bar-right', styles.right)}>
64
+ <LanguageMenu />
62
65
  <span className={cx('byline-admin-app-bar-user', styles.user)}>
63
- Signed in as{' '}
66
+ {t('chrome.appBar.signedInAs')}{' '}
64
67
  <span className={cx('byline-admin-app-bar-user-name', styles.userName)}>
65
68
  {displayNameFor(user)}
66
69
  </span>
67
70
  </span>
68
71
  <Button size="xs" intent="secondary" onClick={handleSignOut} disabled={signingOut}>
69
- {signingOut ? 'Signing out…' : 'Sign out'}
72
+ {signingOut ? t('common.actions.signingOut') : t('common.actions.signOut')}
70
73
  </Button>
71
74
  </div>
72
75
  </header>
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
12
12
 
13
+ import { useTranslation } from '@byline/i18n/react'
13
14
  import { Dropdown, EllipsisIcon } from '@byline/ui/react'
14
15
  import cx from 'classnames'
15
16
 
@@ -228,12 +229,13 @@ function ChevronIcon({ isLeaf = false }: { isLeaf?: boolean }): React.JSX.Elemen
228
229
  }
229
230
 
230
231
  function OverflowDropdown({ items }: { items: Breadcrumb[] }): React.JSX.Element {
232
+ const { t } = useTranslation('byline-admin')
231
233
  return (
232
234
  <li className={cx('byline-breadcrumbs-item', styles.item)}>
233
235
  <ChevronIcon />
234
236
  <Dropdown.Root>
235
237
  <Dropdown.Trigger
236
- aria-label="Show hidden breadcrumbs"
238
+ aria-label={t('chrome.breadcrumbs.showHiddenAriaLabel')}
237
239
  className={cx('byline-breadcrumbs-overflow-trigger', styles.overflowTrigger)}
238
240
  >
239
241
  <EllipsisIcon className={cx('byline-breadcrumbs-overflow-icon', styles.overflowIcon)} />
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { WorkflowStatus } from '@byline/core'
10
10
  import { getClientConfig, getWorkflowStatuses } from '@byline/core'
11
+ import { useTranslation } from '@byline/i18n/react'
11
12
  import { Card, Container, Section } from '@byline/ui/react'
12
13
  import cx from 'classnames'
13
14
 
@@ -62,6 +63,7 @@ interface AdminDashboardProps {
62
63
 
63
64
  export function AdminDashboard({ statsMap }: AdminDashboardProps) {
64
65
  const config = getClientConfig()
66
+ const { t } = useTranslation('byline-admin')
65
67
 
66
68
  return (
67
69
  <Section>
@@ -86,10 +88,12 @@ export function AdminDashboard({ statsMap }: AdminDashboardProps) {
86
88
  {collection.labels.plural}
87
89
  </span>
88
90
  <span className={cx('muted byline-dashboard-title-meta', styles.titleMeta)}>
89
- {total} total
91
+ {t('dashboard.totalCount', { count: total })}
90
92
  </span>
91
93
  </Card.Title>
92
- <Card.Description className="muted">{`${collection.labels.plural} collection`}</Card.Description>
94
+ <Card.Description className="muted">
95
+ {t('dashboard.collectionDescription', { label: collection.labels.plural })}
96
+ </Card.Description>
93
97
  </div>
94
98
  </Card.Header>
95
99
  </Link>
@@ -114,7 +118,9 @@ export function AdminDashboard({ statsMap }: AdminDashboardProps) {
114
118
  params={{ collection: collection.path }}
115
119
  className={cx('byline-dashboard-empty-link', styles.emptyLink)}
116
120
  >
117
- <p>{collection.labels.plural} collection</p>
121
+ <p>
122
+ {t('dashboard.collectionDescription', { label: collection.labels.plural })}
123
+ </p>
118
124
  </Link>
119
125
  )}
120
126
  </Card.Content>