@byline/admin 2.5.2 → 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
@@ -22,9 +22,10 @@
22
22
  * `admin.users.versionConflict` and we prompt for reload.
23
23
  */
24
24
 
25
- import { useState } from 'react'
25
+ import { useMemo, useState } from 'react'
26
26
  import { revalidateLogic, useForm } from '@tanstack/react-form-start'
27
27
 
28
+ import { useTranslation } from '@byline/i18n/react'
28
29
  import { Alert, Button, Input, LoaderEllipsis } from '@byline/ui/react'
29
30
  import cx from 'classnames'
30
31
  import { z } from 'zod'
@@ -33,17 +34,18 @@ import { useBylineAdminServices } from '../../../services/admin-services-context
33
34
  import styles from './update.module.css'
34
35
  import type { AccountResponse } from '../index.js'
35
36
 
36
- const updateAccountSchema = z.object({
37
- given_name: z.string().max(100, 'Given name must not exceed 100 characters'),
38
- family_name: z.string().max(100, 'Family name must not exceed 100 characters'),
39
- username: z.string().max(100, 'Username must not exceed 100 characters'),
40
- email: z
41
- .email({ message: 'Enter a valid email address' })
42
- .min(3)
43
- .max(254, 'Email must not exceed 254 characters'),
44
- })
37
+ // Field-length ceilings sourced from the `adminUsers` schema in @byline/admin
38
+ // (100/100/100/254). Kept as constants so error messages can ICU-format them.
39
+ const MAX_NAME = 100
40
+ const MAX_USERNAME = 100
41
+ const MAX_EMAIL = 254
45
42
 
46
- type UpdateAccountValues = z.infer<typeof updateAccountSchema>
43
+ type UpdateAccountValues = {
44
+ given_name: string
45
+ family_name: string
46
+ username: string
47
+ email: string
48
+ }
47
49
 
48
50
  function defaultsFrom(account: AccountResponse): UpdateAccountValues {
49
51
  return {
@@ -80,9 +82,34 @@ interface UpdateAccountProps {
80
82
 
81
83
  export function UpdateAccount({ account, onClose, onSuccess }: UpdateAccountProps) {
82
84
  const { updateAccount } = useBylineAdminServices()
85
+ const { t } = useTranslation('byline-admin')
83
86
  const [formError, setFormError] = useState<string | null>(null)
84
87
  const [successMessage, setSuccessMessage] = useState<string | null>(null)
85
88
 
89
+ // Schema is rebuilt per-render so error messages reflect the active
90
+ // locale. Cheap — small schema, no ahead-of-time compilation cost
91
+ // that matters at this scale. `useMemo` keeps Tanstack Form's
92
+ // validator-identity stable across re-renders.
93
+ const updateAccountSchema = useMemo(
94
+ () =>
95
+ z.object({
96
+ given_name: z
97
+ .string()
98
+ .max(MAX_NAME, t('account.update.errors.givenNameTooLong', { max: MAX_NAME })),
99
+ family_name: z
100
+ .string()
101
+ .max(MAX_NAME, t('account.update.errors.familyNameTooLong', { max: MAX_NAME })),
102
+ username: z
103
+ .string()
104
+ .max(MAX_USERNAME, t('account.update.errors.usernameTooLong', { max: MAX_USERNAME })),
105
+ email: z
106
+ .email({ message: t('account.update.errors.invalidEmail') })
107
+ .min(3)
108
+ .max(MAX_EMAIL, t('account.update.errors.emailTooLong', { max: MAX_EMAIL })),
109
+ }),
110
+ [t]
111
+ )
112
+
86
113
  const form = useForm({
87
114
  defaultValues: defaultsFrom(account),
88
115
  validationLogic: revalidateLogic({
@@ -97,34 +124,33 @@ export function UpdateAccount({ account, onClose, onSuccess }: UpdateAccountProp
97
124
  setSuccessMessage(null)
98
125
  const patch = buildPatch(value, account)
99
126
  if (Object.keys(patch).length === 0) {
100
- setSuccessMessage('No changes to save.')
127
+ setSuccessMessage(t('common.feedback.noChanges'))
101
128
  return
102
129
  }
103
130
  try {
104
131
  const updated = await updateAccount({ data: { vid: account.vid, patch } })
105
- setSuccessMessage('Saved.')
132
+ setSuccessMessage(t('common.feedback.saved'))
106
133
  onSuccess?.(updated)
107
134
  } catch (err) {
108
135
  const code = getErrorCode(err)
109
136
  if (code === 'admin.users.emailInUse') {
137
+ const message = t('account.update.errors.emailInUse')
110
138
  form.setFieldMeta('email', (meta) => ({
111
139
  ...meta,
112
- errorMap: { ...meta.errorMap, onServer: 'This email is already in use.' },
113
- errors: ['This email is already in use.'],
140
+ errorMap: { ...meta.errorMap, onServer: message },
141
+ errors: [message],
114
142
  }))
115
143
  return
116
144
  }
117
145
  if (code === 'admin.users.versionConflict') {
118
- setFormError(
119
- 'Your account has been modified elsewhere since you opened this form. Reload to refresh and try again.'
120
- )
146
+ setFormError(t('common.errors.versionConflict'))
121
147
  return
122
148
  }
123
149
  if (code === 'admin.account.notFound') {
124
- setFormError('Your admin account could not be found. Please sign in again.')
150
+ setFormError(t('common.errors.accountNotFound'))
125
151
  return
126
152
  }
127
- setFormError('Could not save changes. Please try again.')
153
+ setFormError(t('common.errors.couldNotSave'))
128
154
  }
129
155
  },
130
156
  })
@@ -146,7 +172,7 @@ export function UpdateAccount({ account, onClose, onSuccess }: UpdateAccountProp
146
172
  <form.Field name="given_name">
147
173
  {(field) => (
148
174
  <Input
149
- label="Given name"
175
+ label={t('account.update.fields.givenName')}
150
176
  id="given_name"
151
177
  name={field.name}
152
178
  value={field.state.value}
@@ -162,7 +188,7 @@ export function UpdateAccount({ account, onClose, onSuccess }: UpdateAccountProp
162
188
  <form.Field name="family_name">
163
189
  {(field) => (
164
190
  <Input
165
- label="Family name"
191
+ label={t('account.update.fields.familyName')}
166
192
  id="family_name"
167
193
  name={field.name}
168
194
  value={field.state.value}
@@ -178,7 +204,7 @@ export function UpdateAccount({ account, onClose, onSuccess }: UpdateAccountProp
178
204
  <form.Field name="username">
179
205
  {(field) => (
180
206
  <Input
181
- label="Username"
207
+ label={t('account.update.fields.username')}
182
208
  id="username"
183
209
  name={field.name}
184
210
  value={field.state.value}
@@ -186,7 +212,7 @@ export function UpdateAccount({ account, onClose, onSuccess }: UpdateAccountProp
186
212
  onChange={(e) => field.handleChange(e.currentTarget.value)}
187
213
  error={field.state.meta.errors.length > 0}
188
214
  errorText={firstError(field.state.meta.errors)}
189
- helpText="Optional. Leave blank to clear."
215
+ helpText={t('account.update.fields.usernameHelp')}
190
216
  autoComplete="username"
191
217
  />
192
218
  )}
@@ -195,7 +221,7 @@ export function UpdateAccount({ account, onClose, onSuccess }: UpdateAccountProp
195
221
  <form.Field name="email">
196
222
  {(field) => (
197
223
  <Input
198
- label="Email"
224
+ label={t('common.fields.email')}
199
225
  id="email"
200
226
  name={field.name}
201
227
  type="email"
@@ -218,7 +244,7 @@ export function UpdateAccount({ account, onClose, onSuccess }: UpdateAccountProp
218
244
  onClick={onClose}
219
245
  className={cx('byline-account-update-action', styles.action)}
220
246
  >
221
- {successMessage ? 'Close' : 'Cancel'}
247
+ {successMessage ? t('common.actions.close') : t('common.actions.cancel')}
222
248
  </Button>
223
249
  <form.Subscribe
224
250
  selector={(state) => ({
@@ -235,7 +261,7 @@ export function UpdateAccount({ account, onClose, onSuccess }: UpdateAccountProp
235
261
  disabled={!canSubmit || isSubmitting}
236
262
  className={cx('byline-account-update-action', styles.action)}
237
263
  >
238
- {isSubmitting === true ? <LoaderEllipsis size={42} /> : 'Save'}
264
+ {isSubmitting === true ? <LoaderEllipsis size={42} /> : t('common.actions.save')}
239
265
  </Button>
240
266
  )}
241
267
  </form.Subscribe>
@@ -33,6 +33,7 @@
33
33
  export {
34
34
  changeAccountPasswordCommand,
35
35
  getAccountCommand,
36
+ setPreferredLocaleCommand,
36
37
  updateAccountCommand,
37
38
  } from './commands.js'
38
39
  export {
@@ -47,6 +48,7 @@ export {
47
48
  changeAccountPasswordRequestSchema,
48
49
  getAccountRequestSchema,
49
50
  okResponseSchema,
51
+ setPreferredLocaleRequestSchema,
50
52
  updateAccountRequestSchema,
51
53
  } from './schemas.js'
52
54
  export { AdminAccountService } from './service.js'
@@ -56,5 +58,6 @@ export type {
56
58
  ChangeAccountPasswordRequest,
57
59
  GetAccountRequest,
58
60
  OkResponse,
61
+ SetPreferredLocaleRequest,
59
62
  UpdateAccountRequest,
60
63
  } from './schemas.js'
@@ -76,6 +76,19 @@ export const changeAccountPasswordRequestSchema = z.object({
76
76
  })
77
77
  export type ChangeAccountPasswordRequest = z.infer<typeof changeAccountPasswordRequestSchema>
78
78
 
79
+ // Vid-less — admin interface locale is a personal preference, not content
80
+ // state, so the menu can persist without a fresh-vid round trip. `null`
81
+ // clears the column and lets detection take over.
82
+ export const setPreferredLocaleRequestSchema = z.object({
83
+ locale: z
84
+ .string()
85
+ .min(2)
86
+ .max(16)
87
+ .regex(/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/, 'must be a BCP 47 locale tag')
88
+ .nullable(),
89
+ })
90
+ export type SetPreferredLocaleRequest = z.infer<typeof setPreferredLocaleRequestSchema>
91
+
79
92
  // ---------------------------------------------------------------------------
80
93
  // Responses (re-exports — same shape as the admin-users module)
81
94
  // ---------------------------------------------------------------------------
@@ -71,6 +71,18 @@ export class AdminAccountService {
71
71
  return toAdminUser(row)
72
72
  }
73
73
 
74
+ async setPreferredLocale(actorId: string, locale: string | null): Promise<AccountResponse> {
75
+ const current = await this.#repo.getById(actorId)
76
+ if (!current) throw ERR_ADMIN_ACCOUNT_NOT_FOUND()
77
+ await this.#repo.setPreferredLocale(actorId, locale)
78
+ // Re-read for the post-write vid + updated_at — the vid-less repo
79
+ // method bumps `vid`, so the response carries the fresh shape callers
80
+ // need for any subsequent vid-gated edit.
81
+ const updated = await this.#repo.getById(actorId)
82
+ if (!updated) throw ERR_ADMIN_ACCOUNT_NOT_FOUND()
83
+ return toAdminUser(updated)
84
+ }
85
+
74
86
  async changePassword(
75
87
  actorId: string,
76
88
  request: ChangeAccountPasswordRequest
@@ -28,6 +28,7 @@
28
28
 
29
29
  import { useState } from 'react'
30
30
 
31
+ import { useTranslation } from '@byline/i18n/react'
31
32
  import { Button, Container, LoaderRing, Section } from '@byline/ui/react'
32
33
  import cx from 'classnames'
33
34
 
@@ -82,15 +83,16 @@ function displayUser(user: WhoHasAbilityResponse['users'][number]): string {
82
83
  // --- expandable matrix row ------------------------------------------------
83
84
 
84
85
  function MatrixPanel({ matrix }: { matrix: WhoHasAbilityResponse }) {
86
+ const { t } = useTranslation('byline-admin')
85
87
  return (
86
88
  <div className={cx('byline-inspector-matrix', styles.matrix)}>
87
89
  <div>
88
90
  <h4 className={cx('byline-inspector-matrix-title', styles['matrix-title'])}>
89
- Roles ({matrix.roles.length})
91
+ {t('adminPermissions.matrix.rolesTitle', { count: matrix.roles.length })}
90
92
  </h4>
91
93
  {matrix.roles.length === 0 ? (
92
94
  <p className={cx('muted', 'byline-inspector-matrix-empty', styles['matrix-empty'])}>
93
- No role grants this ability.
95
+ {t('adminPermissions.matrix.rolesEmpty')}
94
96
  </p>
95
97
  ) : (
96
98
  <ul className={cx('byline-inspector-matrix-list', styles['matrix-list'])}>
@@ -110,11 +112,11 @@ function MatrixPanel({ matrix }: { matrix: WhoHasAbilityResponse }) {
110
112
  </div>
111
113
  <div>
112
114
  <h4 className={cx('byline-inspector-matrix-title', styles['matrix-title'])}>
113
- Admin users ({matrix.users.length})
115
+ {t('adminPermissions.matrix.usersTitle', { count: matrix.users.length })}
114
116
  </h4>
115
117
  {matrix.users.length === 0 ? (
116
118
  <p className={cx('muted', 'byline-inspector-matrix-empty', styles['matrix-empty'])}>
117
- No admin user holds this ability.
119
+ {t('adminPermissions.matrix.usersEmpty')}
118
120
  </p>
119
121
  ) : (
120
122
  <ul className={cx('byline-inspector-matrix-list', styles['matrix-list'])}>
@@ -142,7 +144,9 @@ interface AbilityRowProps {
142
144
  }
143
145
 
144
146
  function AbilityRow({ ability, matrix, loading, onToggle, expanded }: AbilityRowProps) {
147
+ const { t } = useTranslation('byline-admin')
145
148
  const sv = sourceVariant(ability.source)
149
+ const sourceKey = ability.source ?? 'unknown'
146
150
  return (
147
151
  <div className={cx('byline-inspector-row', styles.row)}>
148
152
  <div className={cx('byline-inspector-row-head', styles['row-head'])}>
@@ -157,7 +161,7 @@ function AbilityRow({ ability, matrix, loading, onToggle, expanded }: AbilityRow
157
161
  sv.local
158
162
  )}
159
163
  >
160
- {ability.source ?? 'unknown'}
164
+ {t(`adminPermissions.source.${sourceKey}`)}
161
165
  </span>
162
166
  </div>
163
167
  <p className={cx('byline-inspector-row-label', styles['row-label'])}>{ability.label}</p>
@@ -170,14 +174,16 @@ function AbilityRow({ ability, matrix, loading, onToggle, expanded }: AbilityRow
170
174
  ) : null}
171
175
  </div>
172
176
  <Button size="xs" intent="secondary" onClick={onToggle}>
173
- {expanded ? 'Hide' : 'Holders'}
177
+ {expanded
178
+ ? t('adminPermissions.row.hideButton')
179
+ : t('adminPermissions.row.holdersButton')}
174
180
  </Button>
175
181
  </div>
176
182
  {expanded ? (
177
183
  loading ? (
178
184
  <div className={cx('byline-inspector-loader', styles.loader)}>
179
185
  <LoaderRing size={20} color="#888" />
180
- <span className="muted">Loading…</span>
186
+ <span className="muted">{t('common.loading')}</span>
181
187
  </div>
182
188
  ) : matrix ? (
183
189
  <MatrixPanel matrix={matrix} />
@@ -198,6 +204,7 @@ interface GroupSectionProps {
198
204
  }
199
205
 
200
206
  function GroupSection({ group, matrices, loading, expanded, onToggle }: GroupSectionProps) {
207
+ const { t } = useTranslation('byline-admin')
201
208
  return (
202
209
  <details open className={cx('byline-inspector-group', styles.group)}>
203
210
  <summary className={cx('byline-inspector-group-summary', styles['group-summary'])}>
@@ -205,7 +212,7 @@ function GroupSection({ group, matrices, loading, expanded, onToggle }: GroupSec
205
212
  {group.group}
206
213
  </span>
207
214
  <span className={cx('muted', 'byline-inspector-group-count', styles['group-count'])}>
208
- {group.abilities.length} abilities
215
+ {t('adminPermissions.group.abilitiesCount', { count: group.abilities.length })}
209
216
  </span>
210
217
  </summary>
211
218
  <div className={cx('byline-inspector-group-body', styles['group-body'])}>
@@ -228,6 +235,7 @@ function GroupSection({ group, matrices, loading, expanded, onToggle }: GroupSec
228
235
 
229
236
  export function AbilitiesInspector({ data }: { data: ListRegisteredAbilitiesResponse }) {
230
237
  const { whoHasAbility } = useBylineAdminServices()
238
+ const { t } = useTranslation('byline-admin')
231
239
  const [expanded, setExpanded] = useState<Set<string>>(new Set())
232
240
  const [loading, setLoading] = useState<Set<string>>(new Set())
233
241
  const [matrices, setMatrices] = useState<Record<string, WhoHasAbilityResponse>>({})
@@ -264,19 +272,19 @@ export function AbilitiesInspector({ data }: { data: ListRegisteredAbilitiesResp
264
272
  <Section>
265
273
  <Container>
266
274
  <div className={cx('byline-inspector-head', styles.head)}>
267
- <h1 className={cx('byline-inspector-title', styles.title)}>Abilities Inspector</h1>
275
+ <h1 className={cx('byline-inspector-title', styles.title)}>
276
+ {t('adminPermissions.title')}
277
+ </h1>
268
278
  <span className={cx('byline-inspector-count-pill', styles['count-pill'])}>
269
- {data.total} registered
279
+ {t('adminPermissions.countPill', { count: data.total })}
270
280
  </span>
271
281
  </div>
272
282
  <p className={cx('muted', 'byline-inspector-lead', styles.lead)}>
273
- Read-only view of every ability registered through <code>bylineCore.abilities</code>.
274
- Collections auto-register CRUD + workflow abilities; admin subsystems contribute their own
275
- keys at composition root via <code>registerAdminAbilities</code>.
283
+ {t('adminPermissions.lead')}
276
284
  </p>
277
285
  {data.groups.length === 0 ? (
278
286
  <p className={cx('muted', 'byline-inspector-empty', styles.empty)}>
279
- No abilities are registered.
287
+ {t('adminPermissions.empty')}
280
288
  </p>
281
289
  ) : (
282
290
  <div className={cx('byline-inspector-groups', styles.groups)}>
@@ -17,9 +17,10 @@
17
17
  * (see the repository contract).
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'
@@ -28,19 +29,15 @@ import { useBylineAdminServices } from '../../../services/admin-services-context
28
29
  import styles from './create.module.css'
29
30
  import type { AdminRoleResponse } from '../index.js'
30
31
 
31
- const createAdminRoleFormSchema = z.object({
32
- name: z.string().min(1, 'Name is required').max(128, 'Name must not exceed 128 characters'),
33
- machine_name: z
34
- .string()
35
- .min(1, 'Machine name is required')
36
- .max(128, 'Machine name must not exceed 128 characters')
37
- .regex(/^[a-z0-9][a-z0-9_-]*$/, {
38
- message: 'Lowercase letters, numbers, hyphens, and underscores only',
39
- }),
40
- description: z.string().max(2000, 'Description must not exceed 2000 characters'),
41
- })
32
+ const MAX_NAME = 128
33
+ const MAX_MACHINE_NAME = 128
34
+ const MAX_DESCRIPTION = 2000
42
35
 
43
- type CreateAdminRoleValues = z.infer<typeof createAdminRoleFormSchema>
36
+ type CreateAdminRoleValues = {
37
+ name: string
38
+ machine_name: string
39
+ description: string
40
+ }
44
41
 
45
42
  const initialValues: CreateAdminRoleValues = {
46
43
  name: '',
@@ -59,8 +56,38 @@ interface CreateAdminRoleProps {
59
56
 
60
57
  export function CreateAdminRole({ onClose, onSuccess }: CreateAdminRoleProps) {
61
58
  const { createAdminRole } = useBylineAdminServices()
59
+ const { t } = useTranslation('byline-admin')
62
60
  const [formError, setFormError] = useState<string | null>(null)
63
61
 
62
+ // Schema rebuilt per-render so error messages reflect the active
63
+ // locale; wrapped in useMemo([t]) to keep validator identity stable.
64
+ const createAdminRoleFormSchema = useMemo(
65
+ () =>
66
+ z.object({
67
+ name: z
68
+ .string()
69
+ .min(1, t('adminRoles.create.errors.nameRequired'))
70
+ .max(MAX_NAME, t('adminRoles.create.errors.nameTooLong', { max: MAX_NAME })),
71
+ machine_name: z
72
+ .string()
73
+ .min(1, t('adminRoles.create.errors.machineNameRequired'))
74
+ .max(
75
+ MAX_MACHINE_NAME,
76
+ t('adminRoles.create.errors.machineNameTooLong', { max: MAX_MACHINE_NAME })
77
+ )
78
+ .regex(/^[a-z0-9][a-z0-9_-]*$/, {
79
+ message: t('adminRoles.create.errors.machineNameInvalid'),
80
+ }),
81
+ description: z
82
+ .string()
83
+ .max(
84
+ MAX_DESCRIPTION,
85
+ t('adminRoles.create.errors.descriptionTooLong', { max: MAX_DESCRIPTION })
86
+ ),
87
+ }),
88
+ [t]
89
+ )
90
+
64
91
  const form = useForm({
65
92
  defaultValues: initialValues,
66
93
  validationLogic: revalidateLogic({
@@ -85,14 +112,15 @@ export function CreateAdminRole({ onClose, onSuccess }: CreateAdminRoleProps) {
85
112
  } catch (err) {
86
113
  const code = getErrorCode(err)
87
114
  if (code === 'admin.roles.machineNameInUse') {
115
+ const message = t('adminRoles.create.errors.machineNameInUse')
88
116
  form.setFieldMeta('machine_name', (meta) => ({
89
117
  ...meta,
90
- errorMap: { ...meta.errorMap, onServer: 'This machine name is already in use.' },
91
- errors: ['This machine name is already in use.'],
118
+ errorMap: { ...meta.errorMap, onServer: message },
119
+ errors: [message],
92
120
  }))
93
121
  return
94
122
  }
95
- setFormError('Could not create this admin role. Please try again.')
123
+ setFormError(t('adminRoles.create.errors.fallback'))
96
124
  }
97
125
  },
98
126
  })
@@ -113,7 +141,7 @@ export function CreateAdminRole({ onClose, onSuccess }: CreateAdminRoleProps) {
113
141
  <form.Field name="name">
114
142
  {(field) => (
115
143
  <Input
116
- label="Name"
144
+ label={t('adminRoles.fields.name')}
117
145
  id="new-role-name"
118
146
  name={field.name}
119
147
  value={field.state.value}
@@ -121,7 +149,7 @@ export function CreateAdminRole({ onClose, onSuccess }: CreateAdminRoleProps) {
121
149
  onChange={(e) => field.handleChange(e.currentTarget.value)}
122
150
  error={field.state.meta.errors.length > 0}
123
151
  errorText={firstError(field.state.meta.errors)}
124
- helpText="Human-readable label, e.g. 'Editor'."
152
+ helpText={t('adminRoles.create.fields.nameHelp')}
125
153
  required
126
154
  />
127
155
  )}
@@ -130,7 +158,7 @@ export function CreateAdminRole({ onClose, onSuccess }: CreateAdminRoleProps) {
130
158
  <form.Field name="machine_name">
131
159
  {(field) => (
132
160
  <Input
133
- label="Machine name"
161
+ label={t('adminRoles.fields.machineName')}
134
162
  id="new-role-machine-name"
135
163
  name={field.name}
136
164
  value={field.state.value}
@@ -138,7 +166,7 @@ export function CreateAdminRole({ onClose, onSuccess }: CreateAdminRoleProps) {
138
166
  onChange={(e) => field.handleChange(e.currentTarget.value)}
139
167
  error={field.state.meta.errors.length > 0}
140
168
  errorText={firstError(field.state.meta.errors)}
141
- helpText="Stable code-side handle, e.g. 'editor'. Cannot be changed later."
169
+ helpText={t('adminRoles.create.fields.machineNameHelp')}
142
170
  required
143
171
  />
144
172
  )}
@@ -147,7 +175,7 @@ export function CreateAdminRole({ onClose, onSuccess }: CreateAdminRoleProps) {
147
175
  <form.Field name="description">
148
176
  {(field) => (
149
177
  <TextArea
150
- label="Description"
178
+ label={t('adminRoles.fields.description')}
151
179
  id="new-role-description"
152
180
  name={field.name}
153
181
  value={field.state.value}
@@ -168,7 +196,7 @@ export function CreateAdminRole({ onClose, onSuccess }: CreateAdminRoleProps) {
168
196
  onClick={onClose}
169
197
  className={cx('byline-role-create-action', styles.action)}
170
198
  >
171
- Cancel
199
+ {t('common.actions.cancel')}
172
200
  </Button>
173
201
  <form.Subscribe
174
202
  selector={(state) => ({
@@ -184,7 +212,7 @@ export function CreateAdminRole({ onClose, onSuccess }: CreateAdminRoleProps) {
184
212
  disabled={!canSubmit || isSubmitting}
185
213
  className={cx('byline-role-create-action', styles.action)}
186
214
  >
187
- {isSubmitting === true ? <LoaderEllipsis size={42} /> : 'Save'}
215
+ {isSubmitting === true ? <LoaderEllipsis size={42} /> : t('common.actions.save')}
188
216
  </Button>
189
217
  )}
190
218
  </form.Subscribe>
@@ -31,6 +31,7 @@
31
31
 
32
32
  import { useMemo, useState } from 'react'
33
33
 
34
+ import { useTranslation } from '@byline/i18n/react'
34
35
  import { Alert, Button, Checkbox, LoaderEllipsis } from '@byline/ui/react'
35
36
  import cx from 'classnames'
36
37
 
@@ -78,6 +79,7 @@ function GroupSection({
78
79
  onSelectAll,
79
80
  onClearAll,
80
81
  }: GroupSectionProps) {
82
+ const { t } = useTranslation('byline-admin')
81
83
  const groupKeys = useMemo(() => group.abilities.map((a) => a.key), [group.abilities])
82
84
  const selectedInGroup = groupKeys.filter((key) => selected.has(key)).length
83
85
  const isEdit = mode === 'edit'
@@ -92,7 +94,11 @@ function GroupSection({
92
94
  <span
93
95
  className={cx('muted', 'byline-role-permissions-group-count', styles['group-count'])}
94
96
  >
95
- {selectedInGroup} of {group.abilities.length} {isEdit ? 'selected' : 'granted'}
97
+ {t('adminRoles.permissions.groupCount', {
98
+ selected: selectedInGroup,
99
+ total: group.abilities.length,
100
+ mode,
101
+ })}
96
102
  </span>
97
103
  </div>
98
104
  {isEdit ? (
@@ -104,7 +110,7 @@ function GroupSection({
104
110
  disabled={saving || selectedInGroup === group.abilities.length}
105
111
  onClick={() => onSelectAll(groupKeys)}
106
112
  >
107
- Select all
113
+ {t('adminRoles.permissions.selectAll')}
108
114
  </Button>
109
115
  <Button
110
116
  size="xs"
@@ -113,7 +119,7 @@ function GroupSection({
113
119
  disabled={saving || selectedInGroup === 0}
114
120
  onClick={() => onClearAll(groupKeys)}
115
121
  >
116
- Clear
122
+ {t('adminRoles.permissions.clear')}
117
123
  </Button>
118
124
  </div>
119
125
  ) : null}
@@ -175,6 +181,7 @@ export function RolePermissions({
175
181
  onSaved,
176
182
  }: RolePermissionsProps) {
177
183
  const { setRoleAbilities } = useBylineAdminServices()
184
+ const { t } = useTranslation('byline-admin')
178
185
  const [mode, setMode] = useState<Mode>('view')
179
186
  const [initialSet, setInitialSet] = useState<ReadonlySet<string>>(() => new Set(initialAbilities))
180
187
  const [selected, setSelected] = useState<Set<string>>(() => new Set(initialAbilities))
@@ -246,13 +253,11 @@ export function RolePermissions({
246
253
  } catch (err) {
247
254
  const code = getErrorCode(err)
248
255
  if (code === 'admin.permissions.roleNotFound') {
249
- setError('This role no longer exists.')
256
+ setError(t('adminRoles.permissions.errors.roleNotFound'))
250
257
  } else if (code === 'admin.permissions.abilityUnregistered') {
251
- setError(
252
- 'One or more selected abilities are no longer registered. Reload the page and try again.'
253
- )
258
+ setError(t('adminRoles.permissions.errors.abilityUnregistered'))
254
259
  } else {
255
- setError('Could not save permissions. Please try again.')
260
+ setError(t('adminRoles.permissions.errors.fallback'))
256
261
  }
257
262
  } finally {
258
263
  setSaving(false)
@@ -272,14 +277,12 @@ export function RolePermissions({
272
277
  onEdit={handleEnterEdit}
273
278
  />
274
279
  <p className={cx('muted', 'byline-role-permissions-counter', styles.counter)}>
275
- <span className={cx('byline-role-permissions-counter-num', styles['counter-num'])}>
276
- {totalSelected}
277
- </span>{' '}
278
- of{' '}
279
- <span className={cx('byline-role-permissions-counter-num', styles['counter-num'])}>
280
- {registered.total}
281
- </span>{' '}
282
- {isEdit ? 'selected' : 'granted'} for {role.name}
280
+ {t('adminRoles.permissions.counter', {
281
+ selected: totalSelected,
282
+ total: registered.total,
283
+ mode,
284
+ role: role.name,
285
+ })}
283
286
  </p>
284
287
  {isEdit && isDirty ? (
285
288
  <div className={cx('byline-role-permissions-actions', styles.actions)}>
@@ -291,7 +294,7 @@ export function RolePermissions({
291
294
  disabled={saving}
292
295
  className={cx('byline-role-permissions-action', styles.action)}
293
296
  >
294
- Cancel
297
+ {t('common.actions.cancel')}
295
298
  </Button>
296
299
  <Button
297
300
  type="button"
@@ -301,7 +304,7 @@ export function RolePermissions({
301
304
  disabled={saving}
302
305
  className={cx('byline-role-permissions-action', styles.action)}
303
306
  >
304
- {saving ? <LoaderEllipsis size={30} /> : 'Save'}
307
+ {saving ? <LoaderEllipsis size={30} /> : t('common.actions.save')}
305
308
  </Button>
306
309
  </div>
307
310
  ) : null}
@@ -336,6 +339,7 @@ interface ModeToggleProps {
336
339
  }
337
340
 
338
341
  function ModeToggle({ mode, dirty, saving, onView, onEdit }: ModeToggleProps) {
342
+ const { t } = useTranslation('byline-admin')
339
343
  // Segmented two-state toggle. View is disabled while dirty so the
340
344
  // user has to commit to Save or Cancel — avoids accidentally
341
345
  // discarding a draft selection.
@@ -346,7 +350,7 @@ function ModeToggle({ mode, dirty, saving, onView, onEdit }: ModeToggleProps) {
346
350
  return (
347
351
  <div
348
352
  role="group"
349
- aria-label="Permissions mode"
353
+ aria-label={t('adminRoles.permissions.modeAriaLabel')}
350
354
  className={cx('byline-role-permissions-mode-toggle', styles['mode-toggle'])}
351
355
  >
352
356
  <button
@@ -364,7 +368,7 @@ function ModeToggle({ mode, dirty, saving, onView, onEdit }: ModeToggleProps) {
364
368
  ]
365
369
  )}
366
370
  >
367
- View
371
+ {t('adminRoles.permissions.viewMode')}
368
372
  </button>
369
373
  <button
370
374
  type="button"
@@ -383,7 +387,7 @@ function ModeToggle({ mode, dirty, saving, onView, onEdit }: ModeToggleProps) {
383
387
  ]
384
388
  )}
385
389
  >
386
- Edit
390
+ {t('adminRoles.permissions.editMode')}
387
391
  </button>
388
392
  </div>
389
393
  )