@byline/admin 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 (260) hide show
  1. package/dist/fields/array/array-field.d.ts +14 -0
  2. package/dist/fields/array/array-field.js +177 -0
  3. package/dist/fields/array/array-field.module.js +11 -0
  4. package/dist/fields/array/array-field_module.css +32 -0
  5. package/dist/fields/blocks/blocks-field.d.ts +13 -0
  6. package/dist/fields/blocks/blocks-field.js +245 -0
  7. package/dist/fields/blocks/blocks-field.module.js +26 -0
  8. package/dist/fields/blocks/blocks-field_module.css +107 -0
  9. package/dist/fields/checkbox/checkbox-field.d.ts +16 -0
  10. package/dist/fields/checkbox/checkbox-field.js +28 -0
  11. package/dist/fields/checkbox/checkbox-field.module.js +6 -0
  12. package/dist/fields/checkbox/checkbox-field_module.css +4 -0
  13. package/dist/fields/column-formatter.d.ts +20 -0
  14. package/dist/fields/column-formatter.js +15 -0
  15. package/dist/fields/date-time-formatter.d.ts +16 -0
  16. package/dist/fields/date-time-formatter.js +8 -0
  17. package/dist/fields/datetime/datetime-field.d.ts +16 -0
  18. package/dist/fields/datetime/datetime-field.js +37 -0
  19. package/dist/fields/datetime/datetime-field.module.js +5 -0
  20. package/dist/fields/datetime/datetime-field_module.css +4 -0
  21. package/dist/fields/draggable-context-menu.d.ts +6 -0
  22. package/dist/fields/draggable-context-menu.js +85 -0
  23. package/dist/fields/draggable-context-menu.module.js +15 -0
  24. package/dist/fields/draggable-context-menu_module.css +91 -0
  25. package/dist/fields/field-helpers.d.ts +26 -0
  26. package/dist/fields/field-helpers.js +50 -0
  27. package/dist/fields/field-renderer.d.ts +37 -0
  28. package/dist/fields/field-renderer.js +206 -0
  29. package/dist/fields/field-renderer.module.js +8 -0
  30. package/dist/fields/field-renderer_module.css +11 -0
  31. package/dist/fields/field-services-context.d.ts +16 -0
  32. package/dist/fields/field-services-context.js +13 -0
  33. package/dist/fields/field-services-types.d.ts +63 -0
  34. package/dist/fields/field-services-types.js +1 -0
  35. package/dist/fields/file/file-field.d.ts +19 -0
  36. package/dist/fields/file/file-field.js +225 -0
  37. package/dist/fields/file/file-field.module.js +18 -0
  38. package/dist/fields/file/file-field_module.css +131 -0
  39. package/dist/fields/file/file-upload-field.d.ts +21 -0
  40. package/dist/fields/file/file-upload-field.js +130 -0
  41. package/dist/fields/file/file-upload-field.module.js +15 -0
  42. package/dist/fields/file/file-upload-field_module.css +74 -0
  43. package/dist/fields/group/group-field.d.ts +15 -0
  44. package/dist/fields/group/group-field.js +59 -0
  45. package/dist/fields/group/group-field.module.js +9 -0
  46. package/dist/fields/group/group-field_module.css +27 -0
  47. package/dist/fields/image/image-field.d.ts +19 -0
  48. package/dist/fields/image/image-field.js +241 -0
  49. package/dist/fields/image/image-field.module.js +22 -0
  50. package/dist/fields/image/image-field_module.css +121 -0
  51. package/dist/fields/image/image-upload-field.d.ts +21 -0
  52. package/dist/fields/image/image-upload-field.js +190 -0
  53. package/dist/fields/image/image-upload-field.module.js +19 -0
  54. package/dist/fields/image/image-upload-field_module.css +92 -0
  55. package/dist/fields/local-date-time.d.ts +27 -0
  56. package/dist/fields/local-date-time.js +49 -0
  57. package/dist/fields/locale-badge.d.ts +18 -0
  58. package/dist/fields/locale-badge.js +10 -0
  59. package/dist/fields/locale-badge.module.js +5 -0
  60. package/dist/fields/locale-badge_module.css +27 -0
  61. package/dist/fields/numerical/numerical-field.d.ts +18 -0
  62. package/dist/fields/numerical/numerical-field.js +74 -0
  63. package/dist/fields/relation/relation-display.d.ts +40 -0
  64. package/dist/fields/relation/relation-display.js +58 -0
  65. package/dist/fields/relation/relation-display.module.js +9 -0
  66. package/dist/fields/relation/relation-display_module.css +21 -0
  67. package/dist/fields/relation/relation-field.d.ts +18 -0
  68. package/dist/fields/relation/relation-field.js +138 -0
  69. package/dist/fields/relation/relation-field.module.js +13 -0
  70. package/dist/fields/relation/relation-field_module.css +62 -0
  71. package/dist/fields/relation/relation-picker.d.ts +49 -0
  72. package/dist/fields/relation/relation-picker.js +236 -0
  73. package/dist/fields/relation/relation-picker.module.js +26 -0
  74. package/dist/fields/relation/relation-picker_module.css +124 -0
  75. package/dist/fields/relation/relation-summary.d.ts +31 -0
  76. package/dist/fields/relation/relation-summary.js +50 -0
  77. package/dist/fields/relation/relation-summary.module.js +11 -0
  78. package/dist/fields/relation/relation-summary_module.css +37 -0
  79. package/dist/fields/select/select-field.d.ts +16 -0
  80. package/dist/fields/select/select-field.js +50 -0
  81. package/dist/fields/select/select-field.module.js +5 -0
  82. package/dist/fields/select/select-field_module.css +4 -0
  83. package/dist/fields/sortable-item.d.ts +15 -0
  84. package/dist/fields/sortable-item.js +81 -0
  85. package/dist/fields/sortable-item.module.js +22 -0
  86. package/dist/fields/sortable-item_module.css +124 -0
  87. package/dist/fields/text/text-field.d.ts +20 -0
  88. package/dist/fields/text/text-field.js +104 -0
  89. package/dist/fields/text/text-field.module.js +6 -0
  90. package/dist/fields/text/text-field_module.css +5 -0
  91. package/dist/fields/text-area/text-area-field.d.ts +20 -0
  92. package/dist/fields/text-area/text-area-field.js +105 -0
  93. package/dist/fields/text-area/text-area-field.module.js +6 -0
  94. package/dist/fields/text-area/text-area-field_module.css +5 -0
  95. package/dist/fields/use-field-change-handler.d.ts +23 -0
  96. package/dist/fields/use-field-change-handler.js +52 -0
  97. package/dist/forms/document-actions.d.ts +48 -0
  98. package/dist/forms/document-actions.js +475 -0
  99. package/dist/forms/document-actions.module.js +34 -0
  100. package/dist/forms/document-actions_module.css +118 -0
  101. package/dist/forms/form-context.d.ts +89 -0
  102. package/dist/forms/form-context.js +466 -0
  103. package/dist/forms/form-renderer.d.ts +98 -0
  104. package/dist/forms/form-renderer.js +597 -0
  105. package/dist/forms/form-renderer.module.js +46 -0
  106. package/dist/forms/form-renderer_module.css +245 -0
  107. package/dist/forms/navigation-guard.d.ts +54 -0
  108. package/dist/forms/navigation-guard.js +22 -0
  109. package/dist/forms/path-widget.d.ts +36 -0
  110. package/dist/forms/path-widget.js +116 -0
  111. package/dist/forms/path-widget.module.js +8 -0
  112. package/dist/forms/path-widget_module.css +29 -0
  113. package/dist/forms/upload-executor.d.ts +57 -0
  114. package/dist/forms/upload-executor.js +94 -0
  115. package/dist/lib/translate-validation-error.d.ts +36 -0
  116. package/dist/lib/translate-validation-error.js +11 -0
  117. package/dist/modules/admin-account/commands.d.ts +2 -1
  118. package/dist/modules/admin-account/commands.js +13 -2
  119. package/dist/modules/admin-account/components/change-password.js +45 -36
  120. package/dist/modules/admin-account/components/container.js +185 -134
  121. package/dist/modules/admin-account/components/preferences.d.ts +8 -0
  122. package/dist/modules/admin-account/components/preferences.js +152 -0
  123. package/dist/modules/admin-account/components/preferences.module.js +11 -0
  124. package/dist/modules/admin-account/components/preferences_module.css +41 -0
  125. package/dist/modules/admin-account/components/update.js +50 -31
  126. package/dist/modules/admin-account/index.d.ts +3 -3
  127. package/dist/modules/admin-account/index.js +2 -2
  128. package/dist/modules/admin-account/schemas.d.ts +4 -0
  129. package/dist/modules/admin-account/schemas.js +4 -1
  130. package/dist/modules/admin-account/service.d.ts +1 -0
  131. package/dist/modules/admin-account/service.js +8 -0
  132. package/dist/modules/admin-permissions/components/inspector.js +31 -41
  133. package/dist/modules/admin-roles/components/create.js +43 -26
  134. package/dist/modules/admin-roles/components/permissions.js +26 -35
  135. package/dist/modules/admin-roles/components/update.js +26 -16
  136. package/dist/modules/admin-users/components/create.js +60 -40
  137. package/dist/modules/admin-users/components/roles.js +9 -15
  138. package/dist/modules/admin-users/components/set-password.js +30 -31
  139. package/dist/modules/admin-users/components/update.js +58 -39
  140. package/dist/modules/admin-users/dto.js +1 -0
  141. package/dist/modules/admin-users/repository.d.ts +17 -0
  142. package/dist/modules/admin-users/schemas.d.ts +4 -0
  143. package/dist/modules/admin-users/schemas.js +6 -2
  144. package/dist/modules/auth/components/sign-in-form.js +10 -8
  145. package/dist/presentation/group.d.ts +27 -0
  146. package/dist/presentation/group.js +14 -0
  147. package/dist/presentation/group.module.js +6 -0
  148. package/dist/presentation/group_module.css +19 -0
  149. package/dist/presentation/row.d.ts +25 -0
  150. package/dist/presentation/row.js +8 -0
  151. package/dist/presentation/row.module.js +5 -0
  152. package/dist/presentation/row_module.css +18 -0
  153. package/dist/presentation/tabs.d.ts +25 -0
  154. package/dist/presentation/tabs.js +39 -0
  155. package/dist/presentation/tabs.module.js +10 -0
  156. package/dist/presentation/tabs_module.css +68 -0
  157. package/dist/react.d.ts +66 -0
  158. package/dist/react.js +36 -0
  159. package/dist/services/admin-services-types.d.ts +16 -0
  160. package/dist/widgets/diff-viewer/diff-modal.d.ts +22 -0
  161. package/dist/widgets/diff-viewer/diff-modal.js +149 -0
  162. package/dist/widgets/diff-viewer/diff-modal.module.js +14 -0
  163. package/dist/widgets/diff-viewer/diff-modal_module.css +56 -0
  164. package/dist/widgets/status-badge/status-badge.d.ts +25 -0
  165. package/dist/widgets/status-badge/status-badge.js +37 -0
  166. package/dist/widgets/status-badge/status-badge.module.js +7 -0
  167. package/dist/widgets/status-badge/status-badge_module.css +20 -0
  168. package/package.json +14 -4
  169. package/src/fields/array/array-field.module.css +48 -0
  170. package/src/fields/array/array-field.tsx +267 -0
  171. package/src/fields/blocks/blocks-field.module.css +148 -0
  172. package/src/fields/blocks/blocks-field.tsx +323 -0
  173. package/src/fields/checkbox/checkbox-field.module.css +4 -0
  174. package/src/fields/checkbox/checkbox-field.tsx +54 -0
  175. package/src/fields/column-formatter.tsx +31 -0
  176. package/src/fields/date-time-formatter.tsx +22 -0
  177. package/src/fields/datetime/datetime-field.module.css +13 -0
  178. package/src/fields/datetime/datetime-field.tsx +54 -0
  179. package/src/fields/draggable-context-menu.module.css +127 -0
  180. package/src/fields/draggable-context-menu.tsx +87 -0
  181. package/src/fields/field-helpers.ts +69 -0
  182. package/src/fields/field-renderer.module.css +22 -0
  183. package/src/fields/field-renderer.tsx +288 -0
  184. package/src/fields/field-services-context.tsx +35 -0
  185. package/src/fields/field-services-types.ts +68 -0
  186. package/src/fields/file/file-field.module.css +153 -0
  187. package/src/fields/file/file-field.tsx +286 -0
  188. package/src/fields/file/file-upload-field.module.css +101 -0
  189. package/src/fields/file/file-upload-field.tsx +187 -0
  190. package/src/fields/group/group-field.module.css +43 -0
  191. package/src/fields/group/group-field.tsx +84 -0
  192. package/src/fields/image/image-field.module.css +155 -0
  193. package/src/fields/image/image-field.tsx +306 -0
  194. package/src/fields/image/image-upload-field.module.css +123 -0
  195. package/src/fields/image/image-upload-field.tsx +276 -0
  196. package/src/fields/local-date-time.tsx +88 -0
  197. package/src/fields/locale-badge.module.css +37 -0
  198. package/src/fields/locale-badge.tsx +32 -0
  199. package/src/fields/numerical/numerical-field.tsx +114 -0
  200. package/src/fields/relation/relation-display.module.css +36 -0
  201. package/src/fields/relation/relation-display.tsx +130 -0
  202. package/src/fields/relation/relation-field.module.css +83 -0
  203. package/src/fields/relation/relation-field.tsx +211 -0
  204. package/src/fields/relation/relation-picker.module.css +168 -0
  205. package/src/fields/relation/relation-picker.tsx +326 -0
  206. package/src/fields/relation/relation-summary.module.css +55 -0
  207. package/src/fields/relation/relation-summary.tsx +123 -0
  208. package/src/fields/select/select-field.module.css +13 -0
  209. package/src/fields/select/select-field.tsx +61 -0
  210. package/src/fields/sortable-item.module.css +167 -0
  211. package/src/fields/sortable-item.tsx +106 -0
  212. package/src/fields/text/text-field.module.css +13 -0
  213. package/src/fields/text/text-field.tsx +146 -0
  214. package/src/fields/text-area/text-area-field.module.css +13 -0
  215. package/src/fields/text-area/text-area-field.tsx +147 -0
  216. package/src/fields/use-field-change-handler.ts +112 -0
  217. package/src/forms/document-actions.module.css +160 -0
  218. package/src/forms/document-actions.tsx +482 -0
  219. package/src/forms/form-context.tsx +704 -0
  220. package/src/forms/form-renderer.module.css +321 -0
  221. package/src/forms/form-renderer.tsx +891 -0
  222. package/src/forms/navigation-guard.tsx +98 -0
  223. package/src/forms/path-widget.module.css +41 -0
  224. package/src/forms/path-widget.test.tsx +217 -0
  225. package/src/forms/path-widget.tsx +183 -0
  226. package/src/forms/upload-executor.ts +192 -0
  227. package/src/lib/translate-validation-error.ts +56 -0
  228. package/src/modules/admin-account/commands.ts +13 -0
  229. package/src/modules/admin-account/components/change-password.tsx +46 -31
  230. package/src/modules/admin-account/components/container.tsx +83 -38
  231. package/src/modules/admin-account/components/preferences.module.css +60 -0
  232. package/src/modules/admin-account/components/preferences.tsx +203 -0
  233. package/src/modules/admin-account/components/update.tsx +53 -27
  234. package/src/modules/admin-account/index.ts +3 -0
  235. package/src/modules/admin-account/schemas.ts +13 -0
  236. package/src/modules/admin-account/service.ts +12 -0
  237. package/src/modules/admin-permissions/components/inspector.tsx +22 -14
  238. package/src/modules/admin-roles/components/create.tsx +51 -23
  239. package/src/modules/admin-roles/components/permissions.tsx +25 -21
  240. package/src/modules/admin-roles/components/update.tsx +37 -19
  241. package/src/modules/admin-users/components/create.tsx +63 -34
  242. package/src/modules/admin-users/components/roles.tsx +9 -8
  243. package/src/modules/admin-users/components/set-password.tsx +34 -28
  244. package/src/modules/admin-users/components/update.tsx +58 -36
  245. package/src/modules/admin-users/dto.ts +1 -0
  246. package/src/modules/admin-users/repository.ts +17 -0
  247. package/src/modules/admin-users/schemas.ts +12 -0
  248. package/src/modules/auth/components/sign-in-form.tsx +14 -8
  249. package/src/presentation/group.module.css +41 -0
  250. package/src/presentation/group.tsx +40 -0
  251. package/src/presentation/row.module.css +32 -0
  252. package/src/presentation/row.tsx +33 -0
  253. package/src/presentation/tabs.module.css +107 -0
  254. package/src/presentation/tabs.tsx +84 -0
  255. package/src/react.ts +84 -0
  256. package/src/services/admin-services-types.ts +18 -0
  257. package/src/widgets/diff-viewer/diff-modal.module.css +79 -0
  258. package/src/widgets/diff-viewer/diff-modal.tsx +186 -0
  259. package/src/widgets/status-badge/status-badge.module.css +31 -0
  260. package/src/widgets/status-badge/status-badge.tsx +71 -0
@@ -17,9 +17,10 @@
17
17
  * time and immutable thereafter.
18
18
  */
19
19
 
20
- import { useState } from 'react'
20
+ import { useMemo, useState } from 'react'
21
21
  import { revalidateLogic, useForm } from '@tanstack/react-form-start'
22
22
 
23
+ import { useTranslation } from '@byline/i18n/react'
23
24
  import { Alert, Button, Input, LoaderEllipsis, TextArea } from '@byline/ui/react'
24
25
  import cx from 'classnames'
25
26
  import { z } from 'zod'
@@ -29,12 +30,13 @@ import styles from './update.module.css'
29
30
  import type { UpdateAdminRoleInput } from '../../../services/admin-services-types.js'
30
31
  import type { AdminRoleResponse } from '../index.js'
31
32
 
32
- const updateRoleSchema = z.object({
33
- name: z.string().min(1, 'Name is required').max(128, 'Name must not exceed 128 characters'),
34
- description: z.string().max(2000, 'Description must not exceed 2000 characters'),
35
- })
33
+ const MAX_NAME = 128
34
+ const MAX_DESCRIPTION = 2000
36
35
 
37
- type UpdateRoleValues = z.infer<typeof updateRoleSchema>
36
+ type UpdateRoleValues = {
37
+ name: string
38
+ description: string
39
+ }
38
40
 
39
41
  function defaultsFrom(role: AdminRoleResponse): UpdateRoleValues {
40
42
  return {
@@ -66,9 +68,27 @@ interface UpdateRoleProps {
66
68
 
67
69
  export function UpdateRole({ role, onClose, onSuccess }: UpdateRoleProps) {
68
70
  const { updateAdminRole } = useBylineAdminServices()
71
+ const { t } = useTranslation('byline-admin')
69
72
  const [formError, setFormError] = useState<string | null>(null)
70
73
  const [successMessage, setSuccessMessage] = useState<string | null>(null)
71
74
 
75
+ const updateRoleSchema = useMemo(
76
+ () =>
77
+ z.object({
78
+ name: z
79
+ .string()
80
+ .min(1, t('adminRoles.create.errors.nameRequired'))
81
+ .max(MAX_NAME, t('adminRoles.create.errors.nameTooLong', { max: MAX_NAME })),
82
+ description: z
83
+ .string()
84
+ .max(
85
+ MAX_DESCRIPTION,
86
+ t('adminRoles.create.errors.descriptionTooLong', { max: MAX_DESCRIPTION })
87
+ ),
88
+ }),
89
+ [t]
90
+ )
91
+
72
92
  const form = useForm({
73
93
  defaultValues: defaultsFrom(role),
74
94
  validationLogic: revalidateLogic({
@@ -83,7 +103,7 @@ export function UpdateRole({ role, onClose, onSuccess }: UpdateRoleProps) {
83
103
  setSuccessMessage(null)
84
104
  const patch = buildPatch(value, role)
85
105
  if (Object.keys(patch).length === 0) {
86
- setSuccessMessage('No changes to save.')
106
+ setSuccessMessage(t('common.feedback.noChanges'))
87
107
  return
88
108
  }
89
109
 
@@ -91,21 +111,19 @@ export function UpdateRole({ role, onClose, onSuccess }: UpdateRoleProps) {
91
111
  const updated = await updateAdminRole({
92
112
  data: { id: role.id, vid: role.vid, patch },
93
113
  })
94
- setSuccessMessage('Saved.')
114
+ setSuccessMessage(t('common.feedback.saved'))
95
115
  onSuccess?.(updated)
96
116
  } catch (err) {
97
117
  const code = getErrorCode(err)
98
118
  if (code === 'admin.roles.versionConflict') {
99
- setFormError(
100
- 'This role has been modified elsewhere since you opened this form. Reload to get the latest values and try again.'
101
- )
119
+ setFormError(t('adminRoles.update.errors.versionConflict'))
102
120
  return
103
121
  }
104
122
  if (code === 'admin.roles.notFound') {
105
- setFormError('This role no longer exists.')
123
+ setFormError(t('adminRoles.update.errors.notFound'))
106
124
  return
107
125
  }
108
- setFormError('Could not save changes. Please try again.')
126
+ setFormError(t('common.errors.couldNotSave'))
109
127
  }
110
128
  },
111
129
  })
@@ -127,7 +145,7 @@ export function UpdateRole({ role, onClose, onSuccess }: UpdateRoleProps) {
127
145
  <form.Field name="name">
128
146
  {(field) => (
129
147
  <Input
130
- label="Name"
148
+ label={t('adminRoles.fields.name')}
131
149
  id="role-name"
132
150
  name={field.name}
133
151
  value={field.state.value}
@@ -141,19 +159,19 @@ export function UpdateRole({ role, onClose, onSuccess }: UpdateRoleProps) {
141
159
  </form.Field>
142
160
 
143
161
  <Input
144
- label="Machine name"
162
+ label={t('adminRoles.fields.machineName')}
145
163
  id="role-machine-name"
146
164
  name="machine_name"
147
165
  value={role.machine_name}
148
166
  readOnly
149
167
  disabled
150
- helpText="The stable code-side handle. Cannot be changed after creation."
168
+ helpText={t('adminRoles.update.fields.machineNameHelp')}
151
169
  />
152
170
 
153
171
  <form.Field name="description">
154
172
  {(field) => (
155
173
  <TextArea
156
- label="Description"
174
+ label={t('adminRoles.fields.description')}
157
175
  id="role-description"
158
176
  name={field.name}
159
177
  value={field.state.value}
@@ -174,7 +192,7 @@ export function UpdateRole({ role, onClose, onSuccess }: UpdateRoleProps) {
174
192
  onClick={onClose}
175
193
  className={cx('byline-role-update-action', styles.action)}
176
194
  >
177
- {successMessage ? 'Close' : 'Cancel'}
195
+ {successMessage ? t('common.actions.close') : t('common.actions.cancel')}
178
196
  </Button>
179
197
  <form.Subscribe
180
198
  selector={(state) => ({
@@ -190,7 +208,7 @@ export function UpdateRole({ role, onClose, onSuccess }: UpdateRoleProps) {
190
208
  disabled={!canSubmit || isSubmitting}
191
209
  className={cx('byline-role-update-action', styles.action)}
192
210
  >
193
- {isSubmitting === true ? <LoaderEllipsis size={42} /> : 'Save'}
211
+ {isSubmitting === true ? <LoaderEllipsis size={42} /> : t('common.actions.save')}
194
212
  </Button>
195
213
  )}
196
214
  </form.Subscribe>
@@ -23,33 +23,34 @@
23
23
  * change in the loader and a prop here.
24
24
  */
25
25
 
26
- import { useState } from 'react'
26
+ import { useMemo, useState } from 'react'
27
27
  import { revalidateLogic, useForm } from '@tanstack/react-form-start'
28
28
 
29
29
  import { passwordSchema } from '@byline/core/validation'
30
+ import { useTranslation } from '@byline/i18n/react'
30
31
  import { Alert, Button, Checkbox, Input, LoaderEllipsis } from '@byline/ui/react'
31
32
  import cx from 'classnames'
32
33
  import { z } from 'zod'
33
34
 
35
+ import { translateValidationError } from '../../../lib/translate-validation-error.js'
34
36
  import { useBylineAdminServices } from '../../../services/admin-services-context.js'
35
37
  import styles from './create.module.css'
36
38
  import type { AdminUserResponse } from '../index.js'
37
39
 
38
- const createAdminUserFormSchema = z.object({
39
- email: z
40
- .email({ message: 'Enter a valid email address' })
41
- .min(3)
42
- .max(254, 'Email must not exceed 254 characters'),
43
- password: passwordSchema,
44
- given_name: z.string().max(100, 'Given name must not exceed 100 characters'),
45
- family_name: z.string().max(100, 'Family name must not exceed 100 characters'),
46
- username: z.string().max(100, 'Username must not exceed 100 characters'),
47
- is_super_admin: z.boolean(),
48
- is_enabled: z.boolean(),
49
- is_email_verified: z.boolean(),
50
- })
40
+ const MAX_NAME = 100
41
+ const MAX_USERNAME = 100
42
+ const MAX_EMAIL = 254
51
43
 
52
- type CreateAdminUserValues = z.infer<typeof createAdminUserFormSchema>
44
+ type CreateAdminUserValues = {
45
+ email: string
46
+ password: string
47
+ given_name: string
48
+ family_name: string
49
+ username: string
50
+ is_super_admin: boolean
51
+ is_enabled: boolean
52
+ is_email_verified: boolean
53
+ }
53
54
 
54
55
  const initialValues: CreateAdminUserValues = {
55
56
  email: '',
@@ -77,8 +78,35 @@ interface CreateAdminUserProps {
77
78
 
78
79
  export function CreateAdminUser({ onClose, onSuccess }: CreateAdminUserProps) {
79
80
  const { createAdminUser } = useBylineAdminServices()
81
+ const { t } = useTranslation('byline-admin')
80
82
  const [formError, setFormError] = useState<string | null>(null)
81
83
 
84
+ // Schema rebuilt per-render so error messages reflect the active
85
+ // locale; wrapped in useMemo([t]) to keep validator identity stable.
86
+ const createAdminUserFormSchema = useMemo(
87
+ () =>
88
+ z.object({
89
+ email: z
90
+ .email({ message: t('account.update.errors.invalidEmail') })
91
+ .min(3)
92
+ .max(MAX_EMAIL, t('account.update.errors.emailTooLong', { max: MAX_EMAIL })),
93
+ password: passwordSchema,
94
+ given_name: z
95
+ .string()
96
+ .max(MAX_NAME, t('account.update.errors.givenNameTooLong', { max: MAX_NAME })),
97
+ family_name: z
98
+ .string()
99
+ .max(MAX_NAME, t('account.update.errors.familyNameTooLong', { max: MAX_NAME })),
100
+ username: z
101
+ .string()
102
+ .max(MAX_USERNAME, t('account.update.errors.usernameTooLong', { max: MAX_USERNAME })),
103
+ is_super_admin: z.boolean(),
104
+ is_enabled: z.boolean(),
105
+ is_email_verified: z.boolean(),
106
+ }),
107
+ [t]
108
+ )
109
+
82
110
  const form = useForm({
83
111
  defaultValues: initialValues,
84
112
  validationLogic: revalidateLogic({
@@ -111,14 +139,15 @@ export function CreateAdminUser({ onClose, onSuccess }: CreateAdminUserProps) {
111
139
  } catch (err) {
112
140
  const code = getErrorCode(err)
113
141
  if (code === 'admin.users.emailInUse') {
142
+ const message = t('account.update.errors.emailInUse')
114
143
  form.setFieldMeta('email', (meta) => ({
115
144
  ...meta,
116
- errorMap: { ...meta.errorMap, onServer: 'This email is already in use.' },
117
- errors: ['This email is already in use.'],
145
+ errorMap: { ...meta.errorMap, onServer: message },
146
+ errors: [message],
118
147
  }))
119
148
  return
120
149
  }
121
- setFormError('Could not create this admin user. Please try again.')
150
+ setFormError(t('adminUsers.create.errors.fallback'))
122
151
  }
123
152
  },
124
153
  })
@@ -140,7 +169,7 @@ export function CreateAdminUser({ onClose, onSuccess }: CreateAdminUserProps) {
140
169
  <form.Field name="given_name">
141
170
  {(field) => (
142
171
  <Input
143
- label="Given name"
172
+ label={t('account.update.fields.givenName')}
144
173
  id="new-given-name"
145
174
  name={field.name}
146
175
  value={field.state.value}
@@ -156,7 +185,7 @@ export function CreateAdminUser({ onClose, onSuccess }: CreateAdminUserProps) {
156
185
  <form.Field name="family_name">
157
186
  {(field) => (
158
187
  <Input
159
- label="Family name"
188
+ label={t('account.update.fields.familyName')}
160
189
  id="new-family-name"
161
190
  name={field.name}
162
191
  value={field.state.value}
@@ -173,7 +202,7 @@ export function CreateAdminUser({ onClose, onSuccess }: CreateAdminUserProps) {
173
202
  <form.Field name="username">
174
203
  {(field) => (
175
204
  <Input
176
- label="Username"
205
+ label={t('account.update.fields.username')}
177
206
  id="new-username"
178
207
  name={field.name}
179
208
  value={field.state.value}
@@ -181,7 +210,7 @@ export function CreateAdminUser({ onClose, onSuccess }: CreateAdminUserProps) {
181
210
  onChange={(e) => field.handleChange(e.currentTarget.value)}
182
211
  error={field.state.meta.errors.length > 0}
183
212
  errorText={firstError(field.state.meta.errors)}
184
- helpText="Optional."
213
+ helpText={t('adminUsers.create.fields.usernameHelp')}
185
214
  autoComplete="username"
186
215
  />
187
216
  )}
@@ -190,7 +219,7 @@ export function CreateAdminUser({ onClose, onSuccess }: CreateAdminUserProps) {
190
219
  <form.Field name="email">
191
220
  {(field) => (
192
221
  <Input
193
- label="Email"
222
+ label={t('common.fields.email')}
194
223
  id="new-email"
195
224
  name={field.name}
196
225
  type="email"
@@ -208,7 +237,7 @@ export function CreateAdminUser({ onClose, onSuccess }: CreateAdminUserProps) {
208
237
  <form.Field name="password">
209
238
  {(field) => (
210
239
  <Input
211
- label="Initial password"
240
+ label={t('adminUsers.create.fields.password')}
212
241
  id="new-password"
213
242
  name={field.name}
214
243
  type="password"
@@ -216,8 +245,8 @@ export function CreateAdminUser({ onClose, onSuccess }: CreateAdminUserProps) {
216
245
  onBlur={field.handleBlur}
217
246
  onChange={(e) => field.handleChange(e.currentTarget.value)}
218
247
  error={field.state.meta.errors.length > 0}
219
- errorText={firstError(field.state.meta.errors)}
220
- helpText="The user can change it from their own account after signing in."
248
+ errorText={translateValidationError(t, firstError(field.state.meta.errors))}
249
+ helpText={t('adminUsers.create.fields.passwordHelp')}
221
250
  autoComplete="new-password"
222
251
  required
223
252
  />
@@ -230,10 +259,10 @@ export function CreateAdminUser({ onClose, onSuccess }: CreateAdminUserProps) {
230
259
  <Checkbox
231
260
  id="new-is-enabled"
232
261
  name={field.name}
233
- label="Enabled"
262
+ label={t('adminUsers.create.flags.enabledLabel')}
234
263
  checked={field.state.value}
235
264
  onCheckedChange={(checked) => field.handleChange(checked === true)}
236
- helpText="Disabled accounts cannot sign in."
265
+ helpText={t('adminUsers.create.flags.enabledHelp')}
237
266
  />
238
267
  )}
239
268
  </form.Field>
@@ -243,10 +272,10 @@ export function CreateAdminUser({ onClose, onSuccess }: CreateAdminUserProps) {
243
272
  <Checkbox
244
273
  id="new-is-email-verified"
245
274
  name={field.name}
246
- label="Email verified"
275
+ label={t('adminUsers.create.flags.emailVerifiedLabel')}
247
276
  checked={field.state.value}
248
277
  onCheckedChange={(checked) => field.handleChange(checked === true)}
249
- helpText="Skip the verification flow for this account."
278
+ helpText={t('adminUsers.create.flags.emailVerifiedHelp')}
250
279
  />
251
280
  )}
252
281
  </form.Field>
@@ -256,10 +285,10 @@ export function CreateAdminUser({ onClose, onSuccess }: CreateAdminUserProps) {
256
285
  <Checkbox
257
286
  id="new-is-super-admin"
258
287
  name={field.name}
259
- label="Super admin"
288
+ label={t('adminUsers.create.flags.superAdminLabel')}
260
289
  checked={field.state.value}
261
290
  onCheckedChange={(checked) => field.handleChange(checked === true)}
262
- helpText="Super admins bypass every ability check — grant with care."
291
+ helpText={t('adminUsers.create.flags.superAdminHelp')}
263
292
  />
264
293
  )}
265
294
  </form.Field>
@@ -273,7 +302,7 @@ export function CreateAdminUser({ onClose, onSuccess }: CreateAdminUserProps) {
273
302
  onClick={onClose}
274
303
  className={cx('byline-user-create-action', styles.action)}
275
304
  >
276
- Cancel
305
+ {t('common.actions.cancel')}
277
306
  </Button>
278
307
  <form.Subscribe
279
308
  selector={(state) => ({
@@ -289,7 +318,7 @@ export function CreateAdminUser({ onClose, onSuccess }: CreateAdminUserProps) {
289
318
  disabled={!canSubmit || isSubmitting}
290
319
  className={cx('byline-user-create-action', styles.action)}
291
320
  >
292
- {isSubmitting === true ? <LoaderEllipsis size={42} /> : 'Save'}
321
+ {isSubmitting === true ? <LoaderEllipsis size={42} /> : t('common.actions.save')}
293
322
  </Button>
294
323
  )}
295
324
  </form.Subscribe>
@@ -25,6 +25,7 @@
25
25
 
26
26
  import { useState } from 'react'
27
27
 
28
+ import { useTranslation } from '@byline/i18n/react'
28
29
  import { Alert, Button, Checkbox, LoaderEllipsis } from '@byline/ui/react'
29
30
  import cx from 'classnames'
30
31
 
@@ -49,6 +50,7 @@ function setsEqual(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {
49
50
 
50
51
  export function UserRoles({ user, allRoles, initialRoleIds, onClose, onSaved }: UserRolesProps) {
51
52
  const { setUserRoles } = useBylineAdminServices()
53
+ const { t } = useTranslation('byline-admin')
52
54
  const [initialSet, setInitialSet] = useState<ReadonlySet<string>>(() => new Set(initialRoleIds))
53
55
  const [selected, setSelected] = useState<Set<string>>(() => new Set(initialRoleIds))
54
56
  const [saving, setSaving] = useState(false)
@@ -79,16 +81,16 @@ export function UserRoles({ user, allRoles, initialRoleIds, onClose, onSaved }:
79
81
  const storedSet = new Set(response.roles.map((r) => r.id))
80
82
  setInitialSet(storedSet)
81
83
  setSelected(new Set(storedSet))
82
- setSuccessMessage('Saved.')
84
+ setSuccessMessage(t('common.feedback.saved'))
83
85
  onSaved?.(response)
84
86
  } catch (err) {
85
87
  const code = getErrorCode(err)
86
88
  if (code === 'admin.roles.userNotFound') {
87
- setError('This user no longer exists.')
89
+ setError(t('adminUsers.roles.errors.userNotFound'))
88
90
  } else if (code === 'admin.roles.notFound') {
89
- setError('One or more selected roles no longer exist. Reload the page and try again.')
91
+ setError(t('adminUsers.roles.errors.roleNotFound'))
90
92
  } else {
91
- setError('Could not save roles. Please try again.')
93
+ setError(t('adminUsers.roles.errors.fallback'))
92
94
  }
93
95
  } finally {
94
96
  setSaving(false)
@@ -102,8 +104,7 @@ export function UserRoles({ user, allRoles, initialRoleIds, onClose, onSaved }:
102
104
 
103
105
  {allRoles.length === 0 ? (
104
106
  <p className={cx('muted', 'byline-user-roles-empty', styles.empty)}>
105
- No roles have been created yet. Create roles in{' '}
106
- <span className="muted">/admin/roles</span> first.
107
+ {t('adminUsers.roles.emptyCatalog')}
107
108
  </p>
108
109
  ) : (
109
110
  <div className={cx('byline-user-roles-list', styles.list)}>
@@ -148,7 +149,7 @@ export function UserRoles({ user, allRoles, initialRoleIds, onClose, onSaved }:
148
149
  disabled={saving}
149
150
  className={cx('byline-user-roles-action', styles.action)}
150
151
  >
151
- {successMessage ? 'Close' : 'Cancel'}
152
+ {successMessage ? t('common.actions.close') : t('common.actions.cancel')}
152
153
  </Button>
153
154
  <Button
154
155
  type="button"
@@ -158,7 +159,7 @@ export function UserRoles({ user, allRoles, initialRoleIds, onClose, onSaved }:
158
159
  disabled={saving || !isDirty}
159
160
  className={cx('byline-user-roles-action', styles.action)}
160
161
  >
161
- {saving ? <LoaderEllipsis size={30} /> : 'Save'}
162
+ {saving ? <LoaderEllipsis size={30} /> : t('common.actions.save')}
162
163
  </Button>
163
164
  </div>
164
165
  </div>
@@ -21,29 +21,24 @@
21
21
  * `vid` back into the container; the drawer doesn't need to re-fetch.
22
22
  */
23
23
 
24
- import { useState } from 'react'
24
+ import { useMemo, useState } from 'react'
25
25
  import { revalidateLogic, useForm } from '@tanstack/react-form-start'
26
26
 
27
27
  import { passwordSchema } from '@byline/core/validation'
28
+ import { useTranslation } from '@byline/i18n/react'
28
29
  import { Alert, Button, InputPassword, LoaderEllipsis } from '@byline/ui/react'
29
30
  import cx from 'classnames'
30
31
  import { z } from 'zod'
31
32
 
33
+ import { translateValidationError } from '../../../lib/translate-validation-error.js'
32
34
  import { useBylineAdminServices } from '../../../services/admin-services-context.js'
33
35
  import styles from './set-password.module.css'
34
36
  import type { AdminUserResponse } from '../index.js'
35
37
 
36
- const setPasswordFormSchema = z
37
- .object({
38
- password: passwordSchema,
39
- confirm: z.string({ message: 'Please confirm the password' }),
40
- })
41
- .refine((v) => v.password === v.confirm, {
42
- message: 'Passwords do not match',
43
- path: ['confirm'],
44
- })
45
-
46
- type SetPasswordValues = z.infer<typeof setPasswordFormSchema>
38
+ type SetPasswordValues = {
39
+ password: string
40
+ confirm: string
41
+ }
47
42
 
48
43
  interface SetPasswordProps {
49
44
  user: AdminUserResponse
@@ -53,9 +48,26 @@ interface SetPasswordProps {
53
48
 
54
49
  export function SetPassword({ user, onClose, onSuccess }: SetPasswordProps) {
55
50
  const { setAdminUserPassword } = useBylineAdminServices()
51
+ const { t } = useTranslation('byline-admin')
56
52
  const [formError, setFormError] = useState<string | null>(null)
57
53
  const [successMessage, setSuccessMessage] = useState<string | null>(null)
58
54
 
55
+ const setPasswordFormSchema = useMemo(
56
+ () =>
57
+ z
58
+ .object({
59
+ password: passwordSchema,
60
+ confirm: z.string({
61
+ message: t('account.changePassword.errors.confirmRequired'),
62
+ }),
63
+ })
64
+ .refine((v) => v.password === v.confirm, {
65
+ message: t('account.changePassword.errors.mismatch'),
66
+ path: ['confirm'],
67
+ }),
68
+ [t]
69
+ )
70
+
59
71
  const form = useForm({
60
72
  defaultValues: { password: '', confirm: '' } as SetPasswordValues,
61
73
  validationLogic: revalidateLogic({
@@ -72,22 +84,20 @@ export function SetPassword({ user, onClose, onSuccess }: SetPasswordProps) {
72
84
  const updated = await setAdminUserPassword({
73
85
  data: { id: user.id, vid: user.vid, password: value.password },
74
86
  })
75
- setSuccessMessage('Password updated.')
87
+ setSuccessMessage(t('account.changePassword.feedback.updated'))
76
88
  form.reset({ password: '', confirm: '' })
77
89
  onSuccess?.(updated)
78
90
  } catch (err) {
79
91
  const code = getErrorCode(err)
80
92
  if (code === 'admin.users.versionConflict') {
81
- setFormError(
82
- 'This user has been modified elsewhere since you opened this form. Reload to refresh and try again.'
83
- )
93
+ setFormError(t('adminUsers.update.errors.versionConflict'))
84
94
  return
85
95
  }
86
96
  if (code === 'admin.users.notFound') {
87
- setFormError('This user no longer exists.')
97
+ setFormError(t('adminUsers.update.errors.notFound'))
88
98
  return
89
99
  }
90
- setFormError('Could not set the password. Please try again.')
100
+ setFormError(t('adminUsers.setPassword.errors.fallback'))
91
101
  }
92
102
  },
93
103
  })
@@ -106,23 +116,19 @@ export function SetPassword({ user, onClose, onSuccess }: SetPasswordProps) {
106
116
  {formError ? <Alert intent="danger">{formError}</Alert> : null}
107
117
  {successMessage ? <Alert intent="success">{successMessage}</Alert> : null}
108
118
 
109
- <p className="muted">
110
- Sets a new password for{' '}
111
- <span className={cx('byline-user-set-password-target', styles.target)}>{user.email}</span>
112
- . The user will need to sign in again with the new password.
113
- </p>
119
+ <p className="muted">{t('adminUsers.setPassword.intro', { email: user.email })}</p>
114
120
 
115
121
  <form.Field name="password">
116
122
  {(field) => (
117
123
  <InputPassword
118
- label="New password"
124
+ label={t('account.changePassword.fields.new')}
119
125
  id="password"
120
126
  name={field.name}
121
127
  value={field.state.value}
122
128
  onBlur={field.handleBlur}
123
129
  onChange={(e) => field.handleChange(e.currentTarget.value)}
124
130
  error={field.state.meta.errors.length > 0}
125
- errorText={firstError(field.state.meta.errors)}
131
+ errorText={translateValidationError(t, firstError(field.state.meta.errors))}
126
132
  autoComplete="new-password"
127
133
  required
128
134
  />
@@ -132,7 +138,7 @@ export function SetPassword({ user, onClose, onSuccess }: SetPasswordProps) {
132
138
  <form.Field name="confirm">
133
139
  {(field) => (
134
140
  <InputPassword
135
- label="Confirm new password"
141
+ label={t('account.changePassword.fields.confirm')}
136
142
  id="confirm"
137
143
  name={field.name}
138
144
  value={field.state.value}
@@ -154,7 +160,7 @@ export function SetPassword({ user, onClose, onSuccess }: SetPasswordProps) {
154
160
  onClick={onClose}
155
161
  className={cx('byline-user-set-password-action', styles.action)}
156
162
  >
157
- {successMessage ? 'Close' : 'Cancel'}
163
+ {successMessage ? t('common.actions.close') : t('common.actions.cancel')}
158
164
  </Button>
159
165
  <form.Subscribe
160
166
  selector={(state) => ({
@@ -171,7 +177,7 @@ export function SetPassword({ user, onClose, onSuccess }: SetPasswordProps) {
171
177
  disabled={!canSubmit || isSubmitting}
172
178
  className={cx('byline-user-set-password-action', styles.action)}
173
179
  >
174
- {isSubmitting === true ? <LoaderEllipsis size={42} /> : 'Save'}
180
+ {isSubmitting === true ? <LoaderEllipsis size={42} /> : t('common.actions.save')}
175
181
  </Button>
176
182
  )}
177
183
  </form.Subscribe>