@byline/host-tanstack-start 2.5.2 → 2.6.1

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
@@ -8,10 +8,12 @@
8
8
 
9
9
  import { useState } from 'react'
10
10
 
11
+ import { FormRenderer } from '@byline/admin/react'
11
12
  import type { CollectionAdminConfig, CollectionDefinition } from '@byline/core'
12
13
  import { getDefaultStatus, getWorkflowStatuses } from '@byline/core'
13
14
  import type { AnyCollectionSchemaTypes } from '@byline/core/zod-schemas'
14
- import { Container, FormRenderer, Section, useToastManager } from '@byline/ui/react'
15
+ import { useTranslation } from '@byline/i18n/react'
16
+ import { Container, Section, useToastManager } from '@byline/ui/react'
15
17
 
16
18
  import {
17
19
  copyDocumentToLocale,
@@ -47,12 +49,15 @@ export const EditView = ({
47
49
  defaultContentLocale: string
48
50
  }) => {
49
51
  const toastManager = useToastManager()
52
+ const { t } = useTranslation('byline-admin')
50
53
  const [_editState, setEditState] = useState<EditState>({
51
54
  status: 'idle',
52
55
  message: '',
53
56
  })
54
57
  const navigate = useNavigate()
55
58
  const { labels, path, fields } = collectionDefinition
59
+ const singular = labels.singular
60
+ const singularLower = singular.toLowerCase()
56
61
 
57
62
  // Compute the next forward workflow status for the status button.
58
63
  const workflowStatuses = getWorkflowStatuses(collectionDefinition)
@@ -77,9 +82,10 @@ export const EditView = ({
77
82
  await updateDocumentStatus({
78
83
  data: { collection: path, id: String(initialData.id), status },
79
84
  })
85
+ const description = t('collections.edit.statusChangedDescription', { status })
80
86
  toastManager.add({
81
- title: `${labels.singular} Status Update`,
82
- description: `Status changed to "${status}"`,
87
+ title: t('collections.edit.statusUpdateTitle', { label: singular }),
88
+ description,
83
89
  data: {
84
90
  intent: 'success',
85
91
  iconType: 'success',
@@ -87,10 +93,7 @@ export const EditView = ({
87
93
  close: true,
88
94
  },
89
95
  })
90
- setEditState({
91
- status: 'success',
92
- message: `Status changed to "${status}"`,
93
- })
96
+ setEditState({ status: 'success', message: description })
94
97
  // Refresh the page to reflect the new status.
95
98
  navigate({
96
99
  to: '/admin/collections/$collection/$id' as never,
@@ -99,9 +102,12 @@ export const EditView = ({
99
102
  })
100
103
  } catch (err) {
101
104
  console.error('Status change error:', err)
105
+ const description = t('collections.edit.statusChangeFailedDescription', {
106
+ message: (err as Error).message,
107
+ })
102
108
  toastManager.add({
103
- title: `${labels.singular} Status Update`,
104
- description: `Failed to change status: ${(err as Error).message}`,
109
+ title: t('collections.edit.statusUpdateTitle', { label: singular }),
110
+ description,
105
111
  data: {
106
112
  intent: 'danger',
107
113
  iconType: 'danger',
@@ -109,10 +115,7 @@ export const EditView = ({
109
115
  close: true,
110
116
  },
111
117
  })
112
- setEditState({
113
- status: 'failed',
114
- message: `Failed to change status: ${(err as Error).message}`,
115
- })
118
+ setEditState({ status: 'failed', message: description })
116
119
  }
117
120
  }
118
121
 
@@ -131,9 +134,10 @@ export const EditView = ({
131
134
  const handleUnpublish = async () => {
132
135
  try {
133
136
  await unpublishDocument({ data: { collection: path, id: String(initialData.id) } })
137
+ const description = t('collections.edit.unpublishedDescription')
134
138
  toastManager.add({
135
- title: `${labels.singular} Unpublish`,
136
- description: 'Published version has been taken offline.',
139
+ title: t('collections.edit.unpublishTitle', { label: singular }),
140
+ description,
137
141
  data: {
138
142
  intent: 'success',
139
143
  iconType: 'success',
@@ -141,10 +145,7 @@ export const EditView = ({
141
145
  close: true,
142
146
  },
143
147
  })
144
- setEditState({
145
- status: 'success',
146
- message: 'Published version has been taken offline.',
147
- })
148
+ setEditState({ status: 'success', message: description })
148
149
  navigate({
149
150
  to: '/admin/collections/$collection/$id' as never,
150
151
  params: { collection: path, id: String(initialData.id) },
@@ -152,9 +153,12 @@ export const EditView = ({
152
153
  })
153
154
  } catch (err) {
154
155
  console.error('Unpublish error:', err)
156
+ const description = t('collections.edit.unpublishFailedDescription', {
157
+ message: (err as Error).message,
158
+ })
155
159
  toastManager.add({
156
- title: `${labels.singular} Unpublish`,
157
- description: `Failed to unpublish: ${(err as Error).message}`,
160
+ title: t('collections.edit.unpublishTitle', { label: singular }),
161
+ description,
158
162
  data: {
159
163
  intent: 'danger',
160
164
  iconType: 'danger',
@@ -162,10 +166,7 @@ export const EditView = ({
162
166
  close: true,
163
167
  },
164
168
  })
165
- setEditState({
166
- status: 'failed',
167
- message: `Failed to unpublish: ${(err as Error).message}`,
168
- })
169
+ setEditState({ status: 'failed', message: description })
169
170
  }
170
171
  }
171
172
 
@@ -174,11 +175,12 @@ export const EditView = ({
174
175
  const result = await duplicateCollectionDocument({
175
176
  data: { collection: path, id: String(initialData.id) },
176
177
  })
178
+ const description = result.pathRetried
179
+ ? t('collections.edit.duplicatedAutoPathDescription', { path: result.newPath })
180
+ : t('collections.edit.duplicatedPathDescription', { path: result.newPath })
177
181
  toastManager.add({
178
- title: `${labels.singular} Duplicated`,
179
- description: result.pathRetried
180
- ? `Created with auto-generated path "${result.newPath}" (the preferred slug was already in use).`
181
- : `Created with path "${result.newPath}". Update the title and path in the new document.`,
182
+ title: t('collections.edit.duplicatedTitle', { label: singular }),
183
+ description,
182
184
  data: {
183
185
  intent: 'success',
184
186
  iconType: 'success',
@@ -188,7 +190,7 @@ export const EditView = ({
188
190
  })
189
191
  setEditState({
190
192
  status: 'success',
191
- message: `${labels.singular} duplicated.`,
193
+ message: t('collections.edit.duplicatedSuccessMessage', { label: singular }),
192
194
  })
193
195
  // Navigate to the new document's edit view.
194
196
  navigate({
@@ -197,9 +199,12 @@ export const EditView = ({
197
199
  })
198
200
  } catch (err) {
199
201
  console.error('Duplicate error:', err)
202
+ const description = t('collections.edit.duplicateFailedDescription', {
203
+ message: (err as Error).message,
204
+ })
200
205
  toastManager.add({
201
- title: `${labels.singular} Duplicate`,
202
- description: `Failed to duplicate: ${(err as Error).message}`,
206
+ title: t('collections.edit.duplicateTitle', { label: singular }),
207
+ description,
203
208
  data: {
204
209
  intent: 'danger',
205
210
  iconType: 'danger',
@@ -207,10 +212,7 @@ export const EditView = ({
207
212
  close: true,
208
213
  },
209
214
  })
210
- setEditState({
211
- status: 'failed',
212
- message: `Failed to duplicate: ${(err as Error).message}`,
213
- })
215
+ setEditState({ status: 'failed', message: description })
214
216
  }
215
217
  }
216
218
 
@@ -235,12 +237,20 @@ export const EditView = ({
235
237
  contentLocales.find((l) => l.code === result.sourceLocale)?.label ?? result.sourceLocale
236
238
  const targetLabel =
237
239
  contentLocales.find((l) => l.code === result.targetLocale)?.label ?? result.targetLocale
240
+ const description =
241
+ result.fieldsUpdated > 0
242
+ ? t('collections.edit.copiedFieldsDescription', {
243
+ count: result.fieldsUpdated,
244
+ source: sourceLabel,
245
+ target: targetLabel,
246
+ })
247
+ : t('collections.edit.copiedNoFieldsDescription', {
248
+ source: sourceLabel,
249
+ target: targetLabel,
250
+ })
238
251
  toastManager.add({
239
- title: `${labels.singular} Copy to Locale`,
240
- description:
241
- result.fieldsUpdated > 0
242
- ? `Copied ${result.fieldsUpdated} field${result.fieldsUpdated === 1 ? '' : 's'} from ${sourceLabel} to ${targetLabel}.`
243
- : `No fields needed copying from ${sourceLabel} to ${targetLabel} under the current rule.`,
252
+ title: t('collections.edit.copyToLocaleTitle', { label: singular }),
253
+ description,
244
254
  data: {
245
255
  intent: 'success',
246
256
  iconType: 'success',
@@ -250,7 +260,10 @@ export const EditView = ({
250
260
  })
251
261
  setEditState({
252
262
  status: 'success',
253
- message: `Copied ${sourceLabel} → ${targetLabel}.`,
263
+ message: t('collections.edit.copiedSuccessMessage', {
264
+ source: sourceLabel,
265
+ target: targetLabel,
266
+ }),
254
267
  })
255
268
  // Switch the form to the target locale so the editor sees the
256
269
  // copied content immediately.
@@ -261,9 +274,12 @@ export const EditView = ({
261
274
  })
262
275
  } catch (err) {
263
276
  console.error('Copy to locale error:', err)
277
+ const description = t('collections.edit.copyFailedDescription', {
278
+ message: (err as Error).message,
279
+ })
264
280
  toastManager.add({
265
- title: `${labels.singular} Copy to Locale`,
266
- description: `Failed to copy: ${(err as Error).message}`,
281
+ title: t('collections.edit.copyToLocaleTitle', { label: singular }),
282
+ description,
267
283
  data: {
268
284
  intent: 'danger',
269
285
  iconType: 'danger',
@@ -271,19 +287,17 @@ export const EditView = ({
271
287
  close: true,
272
288
  },
273
289
  })
274
- setEditState({
275
- status: 'failed',
276
- message: `Failed to copy: ${(err as Error).message}`,
277
- })
290
+ setEditState({ status: 'failed', message: description })
278
291
  }
279
292
  }
280
293
 
281
294
  const handleDelete = async () => {
282
295
  try {
283
296
  await deleteDocument({ data: { collection: path, id: String(initialData.id) } })
297
+ const description = t('collections.edit.deletedDescription', { label: singular })
284
298
  toastManager.add({
285
- title: `${labels.singular} Deletion`,
286
- description: `${labels.singular} has been deleted.`,
299
+ title: t('collections.edit.deleteTitle', { label: singular }),
300
+ description,
287
301
  data: {
288
302
  intent: 'success',
289
303
  iconType: 'success',
@@ -291,10 +305,7 @@ export const EditView = ({
291
305
  close: true,
292
306
  },
293
307
  })
294
- setEditState({
295
- status: 'success',
296
- message: `${labels.singular} has been deleted.`,
297
- })
308
+ setEditState({ status: 'success', message: description })
298
309
  // Navigate back to the collection list after deletion.
299
310
  navigate({
300
311
  to: '/admin/collections/$collection' as never,
@@ -302,9 +313,12 @@ export const EditView = ({
302
313
  })
303
314
  } catch (err) {
304
315
  console.error('Delete error:', err)
316
+ const description = t('collections.edit.deleteFailedDescription', {
317
+ message: (err as Error).message,
318
+ })
305
319
  toastManager.add({
306
- title: `${labels.singular} Deletion`,
307
- description: `Failed to delete: ${(err as Error).message}`,
320
+ title: t('collections.edit.deleteTitle', { label: singular }),
321
+ description,
308
322
  data: {
309
323
  intent: 'danger',
310
324
  iconType: 'danger',
@@ -312,10 +326,7 @@ export const EditView = ({
312
326
  close: true,
313
327
  },
314
328
  })
315
- setEditState({
316
- status: 'failed',
317
- message: `Failed to delete: ${(err as Error).message}`,
318
- })
329
+ setEditState({ status: 'failed', message: description })
319
330
  }
320
331
  }
321
332
 
@@ -342,9 +353,10 @@ export const EditView = ({
342
353
  },
343
354
  })
344
355
 
356
+ const description = t('collections.edit.updatedDescription', { label: singularLower })
345
357
  toastManager.add({
346
- title: `${labels.singular} Update`,
347
- description: `Successfully updated ${labels.singular.toLowerCase()}`,
358
+ title: t('collections.edit.updateTitle', { label: singular }),
359
+ description,
348
360
  data: {
349
361
  intent: 'success',
350
362
  iconType: 'success',
@@ -353,10 +365,7 @@ export const EditView = ({
353
365
  },
354
366
  })
355
367
 
356
- setEditState({
357
- status: 'success',
358
- message: `Successfully updated ${labels.singular.toLowerCase()}`,
359
- })
368
+ setEditState({ status: 'success', message: description })
360
369
 
361
370
  // Re-navigate to the same route so the loader re-fetches the document.
362
371
  // The new version will have a fresh version ID, draft status, and
@@ -368,9 +377,10 @@ export const EditView = ({
368
377
  })
369
378
  } catch (err) {
370
379
  console.error('Network error:', err)
380
+ const description = t('collections.edit.updateFailedDescription', { label: singularLower })
371
381
  toastManager.add({
372
- title: `${labels.singular} Update`,
373
- description: `An error occurred while updating ${labels.singular.toLowerCase()}`,
382
+ title: t('collections.edit.updateTitle', { label: singular }),
383
+ description,
374
384
  data: {
375
385
  intent: 'danger',
376
386
  iconType: 'danger',
@@ -379,10 +389,7 @@ export const EditView = ({
379
389
  },
380
390
  })
381
391
 
382
- setEditState({
383
- status: 'failed',
384
- message: `An error occurred while updating ${labels.singular.toLowerCase()}`,
385
- })
392
+ setEditState({ status: 'failed', message: description })
386
393
  }
387
394
  }
388
395
 
@@ -9,19 +9,19 @@
9
9
  import { lazy, Suspense, useState } from 'react'
10
10
  import { useParams, useRouterState } from '@tanstack/react-router'
11
11
 
12
+ import { renderFormatted, StatusBadge } from '@byline/admin/react'
12
13
  import { useBylineAdminServices } from '@byline/admin/services'
13
14
  import type { CollectionAdminConfig, CollectionDefinition, WorkflowStatus } from '@byline/core'
14
15
  import type { AnyCollectionSchemaTypes } from '@byline/core/zod-schemas'
16
+ import { useTranslation } from '@byline/i18n/react'
15
17
  import {
16
18
  Button,
17
19
  CloseIcon,
18
20
  Container,
19
21
  IconButton,
20
22
  Modal,
21
- renderFormatted,
22
23
  Section,
23
24
  Select,
24
- StatusBadge,
25
25
  Table,
26
26
  } from '@byline/ui/react'
27
27
  import cx from 'classnames'
@@ -48,7 +48,7 @@ function getColumnValue(document: any, fieldName: string): any {
48
48
 
49
49
  // Lazy-load DiffModal because react-diff-viewer-continued uses a web worker
50
50
  // bundle that cannot be resolved by Node during SSR.
51
- const DiffModal = lazy(() => import('@byline/ui/react').then((m) => ({ default: m.DiffModal })))
51
+ const DiffModal = lazy(() => import('@byline/admin/react').then((m) => ({ default: m.DiffModal })))
52
52
 
53
53
  /**
54
54
  * Safely extract a displayable string from a field value that may be a plain
@@ -116,6 +116,7 @@ export const HistoryView = ({
116
116
  })
117
117
  const navigate = useNavigate()
118
118
  const { getCollectionDocumentVersion } = useBylineAdminServices()
119
+ const { t } = useTranslation('byline-admin')
119
120
  const columns = adminConfig?.columns || []
120
121
  const { labels } = collectionDefinition
121
122
  const location = useRouterState({ select: (s) => s.location })
@@ -152,7 +153,8 @@ export const HistoryView = ({
152
153
  <Container>
153
154
  <div className={cx('byline-coll-history-head', styles.head)}>
154
155
  <h2 className={cx('byline-coll-history-title', styles.title)}>
155
- {labels.singular} History <Stats total={data?.meta.total} />
156
+ {t('collections.history.title', { label: labels.singular })}{' '}
157
+ <Stats total={data?.meta.total} />
156
158
  </h2>
157
159
  <ViewMenu
158
160
  collection={collection}
@@ -174,7 +176,7 @@ export const HistoryView = ({
174
176
  showFirstButton
175
177
  showLastButton
176
178
  componentName="pagerTop"
177
- aria-label="Top Pager"
179
+ aria-label={t('collections.list.pagerTopAriaLabel')}
178
180
  />
179
181
  </div>
180
182
  <Table.Container className={cx('byline-coll-history-table-wrap', styles.tableWrap)}>
@@ -229,8 +231,8 @@ export const HistoryView = ({
229
231
  size="xs"
230
232
  variant="outlined"
231
233
  intent="noeffect"
232
- aria-label="Compare this version with the current version"
233
- title="Compare with current"
234
+ aria-label={t('collections.history.compareAriaLabel')}
235
+ title={t('collections.history.compareTitle')}
234
236
  className={cx(
235
237
  'byline-coll-history-version-button',
236
238
  styles.versionButton
@@ -349,9 +351,9 @@ export const HistoryView = ({
349
351
  'byline-coll-history-restore-button',
350
352
  styles.restoreButton
351
353
  )}
352
- title="Restore this version as the current draft"
354
+ title={t('collections.history.restoreButtonTitle')}
353
355
  >
354
- Restore
356
+ {t('collections.history.restoreButton')}
355
357
  </Button>
356
358
  ) : null}
357
359
  </Table.Cell>,
@@ -394,7 +396,7 @@ export const HistoryView = ({
394
396
  showFirstButton
395
397
  showLastButton
396
398
  componentName="pagerBottom"
397
- aria-label="Bottom Pager"
399
+ aria-label={t('collections.list.pagerBottomAriaLabel')}
398
400
  />
399
401
  </div>
400
402
  </Container>
@@ -425,8 +427,12 @@ export const HistoryView = ({
425
427
  <Modal.Header
426
428
  className={cx('byline-coll-history-restore-modal-head', styles.restoreModalHead)}
427
429
  >
428
- <h3 className="m-0">Restore version</h3>
429
- <IconButton aria-label="Close" size="xs" onClick={() => setRestoreTarget(null)}>
430
+ <h3 className="m-0">{t('collections.history.restoreModalTitle')}</h3>
431
+ <IconButton
432
+ aria-label={t('common.actions.close')}
433
+ size="xs"
434
+ onClick={() => setRestoreTarget(null)}
435
+ >
430
436
  <CloseIcon width="14px" height="14px" svgClassName="white-icon" />
431
437
  </IconButton>
432
438
  </Modal.Header>
@@ -9,19 +9,20 @@
9
9
  import { useEffect, useMemo, useState } from 'react'
10
10
  import { useRouter, useRouterState } from '@tanstack/react-router'
11
11
 
12
+ import { renderFormatted, StatusBadge } from '@byline/admin/react'
12
13
  import type { ColumnDefinition, WorkflowStatus } from '@byline/core'
13
14
  import type { AnyCollectionSchemaTypes } from '@byline/core/zod-schemas'
15
+ import type { UseTranslationReturn } from '@byline/i18n/react'
16
+ import { useTranslation } from '@byline/i18n/react'
14
17
  import {
15
18
  Container,
16
19
  GripperVerticalIcon,
17
20
  IconButton,
18
21
  LoaderRing,
19
22
  PlusIcon,
20
- renderFormatted,
21
23
  Search,
22
24
  Section,
23
25
  Select,
24
- StatusBadge,
25
26
  Table,
26
27
  useToastManager,
27
28
  } from '@byline/ui/react'
@@ -108,10 +109,12 @@ function padRows(value: number) {
108
109
  function SortableTableRow({
109
110
  id,
110
111
  disabled,
112
+ t,
111
113
  children,
112
114
  }: {
113
115
  id: string
114
116
  disabled: boolean
117
+ t: UseTranslationReturn['t']
115
118
  children: React.ReactNode
116
119
  }) {
117
120
  const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
@@ -132,7 +135,9 @@ function SortableTableRow({
132
135
  type="button"
133
136
  className={cx('byline-coll-list-drag-handle', styles.dragHandle)}
134
137
  aria-label={
135
- disabled ? 'Drag disabled while filters or search are active' : 'Drag to reorder'
138
+ disabled
139
+ ? t('collections.list.dragDisabledAriaLabel')
140
+ : t('collections.list.dragHandleAriaLabel')
136
141
  }
137
142
  disabled={disabled}
138
143
  {...attributes}
@@ -166,6 +171,7 @@ export const ListView = ({
166
171
  const navigate = useNavigate()
167
172
  const router = useRouter()
168
173
  const toastManager = useToastManager()
174
+ const { t } = useTranslation('byline-admin')
169
175
  const location = useRouterState({ select: (s) => s.location })
170
176
 
171
177
  // Local mirror of the loader docs so drag-and-drop can paint the new
@@ -229,8 +235,8 @@ export const ListView = ({
229
235
  } catch (_err) {
230
236
  setLocalDocs(previousDocs)
231
237
  toastManager.add({
232
- title: 'Could not save the new order',
233
- description: 'Please try again.',
238
+ title: t('collections.list.reorderFailedToast'),
239
+ description: t('collections.list.reorderFailedDescription'),
234
240
  data: { intent: 'danger', iconType: 'danger', icon: true, close: true },
235
241
  })
236
242
  } finally {
@@ -244,10 +250,10 @@ export const ListView = ({
244
250
  // exceeded" inside SelectRoot after navigations that cause a re-render).
245
251
  const statusItems = useMemo(
246
252
  () => [
247
- { value: '_all', label: 'All' },
253
+ { value: '_all', label: t('collections.list.statusFilterAll') },
248
254
  ...(workflowStatuses?.map((ws) => ({ value: ws.name, label: ws.label ?? ws.name })) ?? []),
249
255
  ],
250
- [workflowStatuses]
256
+ [workflowStatuses, t]
251
257
  )
252
258
 
253
259
  const handleOnSearch = (query: string): void => {
@@ -311,7 +317,7 @@ export const ListView = ({
311
317
  </h1>
312
318
  <Stats total={data?.meta.total} />
313
319
  <IconButton
314
- aria-label="Create New"
320
+ aria-label={t('collections.list.createAriaLabel')}
315
321
  render={
316
322
  <Link
317
323
  to={'/admin/collections/$collection/create' as never}
@@ -327,7 +333,7 @@ export const ListView = ({
327
333
  onSearch={handleOnSearch}
328
334
  onClear={handleOnClear}
329
335
  inputSize="sm"
330
- placeholder="Search"
336
+ placeholder={t('collections.list.searchPlaceholder')}
331
337
  className={cx('byline-coll-list-search', styles.search)}
332
338
  />
333
339
 
@@ -348,7 +354,7 @@ export const ListView = ({
348
354
  showFirstButton
349
355
  showLastButton
350
356
  componentName="pagerTop"
351
- aria-label="Top Pager"
357
+ aria-label={t('collections.list.pagerTopAriaLabel')}
352
358
  />
353
359
  </div>
354
360
  <Table.Container className={cx('byline-coll-list-table-wrap', styles.tableWrap)}>
@@ -388,8 +394,8 @@ export const ListView = ({
388
394
  search: params,
389
395
  })
390
396
  }}
391
- aria-label="Sort by manual order"
392
- title="Sort by manual order"
397
+ aria-label={t('collections.list.sortManualOrderAriaLabel')}
398
+ title={t('collections.list.sortManualOrderAriaLabel')}
393
399
  >
394
400
  <SortAscendingIcon />
395
401
  </button>
@@ -412,7 +418,12 @@ export const ListView = ({
412
418
 
413
419
  <Table.Body>
414
420
  {localDocs.map((document) => (
415
- <SortableTableRow key={document.id} id={document.id} disabled={!dragEnabled}>
421
+ <SortableTableRow
422
+ key={document.id}
423
+ id={document.id}
424
+ disabled={!dragEnabled}
425
+ t={t}
426
+ >
416
427
  {columns.map((column) => (
417
428
  <Table.Cell
418
429
  key={String(column.fieldName)}
@@ -567,7 +578,7 @@ export const ListView = ({
567
578
  showFirstButton
568
579
  showLastButton
569
580
  componentName="pagerBottom"
570
- aria-label="Bottom Pager"
581
+ aria-label={t('collections.list.pagerBottomAriaLabel')}
571
582
  />
572
583
  </div>
573
584
  </Container>
@@ -18,11 +18,15 @@
18
18
  * `window.location.assign(url)`.
19
19
  *
20
20
  * The preview URL comes from `CollectionAdminConfig.preview.url(doc, ctx)`
21
- * when configured; otherwise it falls back to the conventional
22
- * `/${collectionPath}/${doc.path}`. When the configured `url(...)` returns
23
- * `null` (missing slug, missing required relation, etc.), the icon is
24
- * not rendered at all — there is no public URL meaningful for this
25
- * document yet, so offering a preview link would just lead to a 404.
21
+ * when configured; otherwise it falls back through the schema's
22
+ * `CollectionDefinition.buildDocumentPath` (the single source of truth
23
+ * the richtext embed walker also reads) and finally to the conventional
24
+ * `/${collectionPath}/${doc.path}`. When the configured `url(...)`
25
+ * returns `null` (missing slug, missing required relation, etc.), the
26
+ * icon is not rendered at all — there is no public URL meaningful for
27
+ * this document yet, so offering a preview link would just lead to a
28
+ * 404. Hosts that need a locale prefix, query string, or other request-
29
+ * scoped composition still write their own `preview.url`.
26
30
  *
27
31
  * The component does not load the document itself. Callers pass the
28
32
  * already-loaded `doc` from their route loader. The edit-view loader
@@ -42,10 +46,14 @@
42
46
  import { useState } from 'react'
43
47
 
44
48
  import type { CollectionAdminConfig, PreviewDocument } from '@byline/core'
49
+ import { useTranslation } from '@byline/i18n/react'
45
50
  import { ExternalLinkIcon, IconButton, useToastManager } from '@byline/ui/react'
46
51
  import cx from 'classnames'
47
52
 
48
53
  import { enablePreviewModeFn } from '../../server-fns/preview/index.js'
54
+ import { resolvePreviewUrl } from './resolve-preview-url.js'
55
+
56
+ export { resolvePreviewUrl } from './resolve-preview-url.js'
49
57
 
50
58
  export interface PreviewLinkProps {
51
59
  /** Collection path (e.g. `'news'`, `'pages'`). */
@@ -60,30 +68,6 @@ export interface PreviewLinkProps {
60
68
  className?: string
61
69
  }
62
70
 
63
- /**
64
- * Resolve the preview URL for a doc against an admin config. Exported so
65
- * other surfaces (list-row preview links in the future) can share the
66
- * same fallback logic.
67
- *
68
- * Returns:
69
- * - `string` → URL to open
70
- * - `null` → no preview URL meaningful for this doc; hide affordance
71
- */
72
- export function resolvePreviewUrl(
73
- doc: PreviewDocument,
74
- collectionPath: string,
75
- adminConfig: CollectionAdminConfig | undefined,
76
- locale: string | undefined
77
- ): string | null {
78
- if (adminConfig?.preview) {
79
- return adminConfig.preview.url(doc, { locale })
80
- }
81
- // Default convention: collection lives at `/${collectionPath}/${path}`.
82
- // Returns null when the doc has no path yet (unsaved, awaiting slug).
83
- if (!doc.path) return null
84
- return `/${collectionPath}/${doc.path}`
85
- }
86
-
87
71
  export const PreviewLink = ({
88
72
  collectionPath,
89
73
  doc,
@@ -92,6 +76,7 @@ export const PreviewLink = ({
92
76
  className,
93
77
  }: PreviewLinkProps) => {
94
78
  const toastManager = useToastManager()
79
+ const { t } = useTranslation('byline-admin')
95
80
  const [busy, setBusy] = useState(false)
96
81
 
97
82
  const url = resolvePreviewUrl(doc, collectionPath, adminConfig, locale)
@@ -108,8 +93,10 @@ export const PreviewLink = ({
108
93
  window.location.assign(url)
109
94
  } catch (err) {
110
95
  toastManager.add({
111
- title: 'Preview',
112
- description: `Could not enable preview mode: ${(err as Error).message}`,
96
+ title: t('collections.preview.toastTitle'),
97
+ description: t('collections.preview.failedDescription', {
98
+ message: (err as Error).message,
99
+ }),
113
100
  data: {
114
101
  intent: 'danger',
115
102
  iconType: 'danger',
@@ -129,8 +116,8 @@ export const PreviewLink = ({
129
116
  variant="text"
130
117
  disabled={busy}
131
118
  onClick={handleClick}
132
- aria-label="Open preview"
133
- title="Preview"
119
+ aria-label={t('collections.preview.openAriaLabel')}
120
+ title={t('collections.preview.title')}
134
121
  >
135
122
  <ExternalLinkIcon width="20px" height="20px" className="byline-preview-link-icon" />
136
123
  </IconButton>