@byline/host-tanstack-start 2.7.0 → 3.0.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.
@@ -10,9 +10,17 @@
10
10
  * These are designed to be used as `errorComponent` and `notFoundComponent`
11
11
  * on layout routes (root, admin, public) so that errors render inside the
12
12
  * appropriate shell rather than blowing away the entire page.
13
+ *
14
+ * `RouteError` / `RouteNotFound` self-mount their own `<I18nProvider>`. A
15
+ * route's `errorComponent` renders *in place of that route's `component`*, so
16
+ * the admin layout's own `<I18nProvider>` (mounted inside that component) is
17
+ * gone by the time the error screen renders — calling `useTranslation` against
18
+ * the absent provider previously threw a *second* error that masked the real
19
+ * one. Self-mounting (mirroring `sign-in-page.tsx`) makes the screen localised
20
+ * and provider-independent wherever it renders.
13
21
  */
14
22
  import type { ErrorComponentProps, NotFoundRouteProps } from '@tanstack/react-router';
15
- export declare function RouteError({ error, reset }: ErrorComponentProps): import("react").JSX.Element;
23
+ export declare function RouteError(props: ErrorComponentProps): import("react").JSX.Element;
16
24
  export declare function RouteNotFound(_props: NotFoundRouteProps): import("react").JSX.Element;
17
25
  /**
18
26
  * Minimal fallback for when providers may be broken — used at the root
@@ -1,7 +1,8 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { useContext } from "react";
2
3
  import { useRouter } from "@tanstack/react-router";
3
- import { BylineError, ErrorCodes } from "@byline/core";
4
- import { useTranslation } from "@byline/i18n/react";
4
+ import { BylineError, ErrorCodes, getClientConfig } from "@byline/core";
5
+ import { I18nContext, I18nProvider, useTranslation } from "@byline/i18n/react";
5
6
  import { Alert, Button, Container, Section } from "@byline/ui/react";
6
7
  import classnames from "classnames";
7
8
  import route_error_module from "./route-error.module.js";
@@ -24,7 +25,26 @@ function getErrorMessage(error, t) {
24
25
  if (error instanceof Error) return error.message;
25
26
  return t('routeError.defaultMessage');
26
27
  }
27
- function RouteError({ error, reset }) {
28
+ function ErrorScreenI18nProvider({ children }) {
29
+ if (null != useContext(I18nContext)) return /*#__PURE__*/ jsx(Fragment, {
30
+ children: children
31
+ });
32
+ let bundle = {};
33
+ let locale = 'en';
34
+ try {
35
+ const { i18n } = getClientConfig();
36
+ bundle = i18n.translations ?? {};
37
+ locale = i18n.interface.defaultLocale;
38
+ } catch {}
39
+ return /*#__PURE__*/ jsx(I18nProvider, {
40
+ bundle: bundle,
41
+ activeLocale: locale,
42
+ defaultLocale: locale,
43
+ localeDefinitions: [],
44
+ children: children
45
+ });
46
+ }
47
+ function RouteErrorContent({ error, reset }) {
28
48
  const router = useRouter();
29
49
  const { t } = useTranslation('byline-admin');
30
50
  return /*#__PURE__*/ jsx(Section, {
@@ -66,7 +86,14 @@ function RouteError({ error, reset }) {
66
86
  })
67
87
  });
68
88
  }
69
- function RouteNotFound(_props) {
89
+ function RouteError(props) {
90
+ return /*#__PURE__*/ jsx(ErrorScreenI18nProvider, {
91
+ children: /*#__PURE__*/ jsx(RouteErrorContent, {
92
+ ...props
93
+ })
94
+ });
95
+ }
96
+ function RouteNotFoundContent() {
70
97
  const { t } = useTranslation('byline-admin');
71
98
  return /*#__PURE__*/ jsx(Section, {
72
99
  className: classnames('byline-route-error', route_error_module.section),
@@ -95,6 +122,11 @@ function RouteNotFound(_props) {
95
122
  })
96
123
  });
97
124
  }
125
+ function RouteNotFound(_props) {
126
+ return /*#__PURE__*/ jsx(ErrorScreenI18nProvider, {
127
+ children: /*#__PURE__*/ jsx(RouteNotFoundContent, {})
128
+ });
129
+ }
98
130
  function RootError({ error, reset }) {
99
131
  function rootMessage(err) {
100
132
  if (err instanceof Error) return err.message;
@@ -15,7 +15,7 @@ const CreateView = ({ collectionDefinition, adminConfig, initialData })=>{
15
15
  });
16
16
  const navigate = useNavigate();
17
17
  const { labels, path, fields } = collectionDefinition;
18
- const handleSubmit = async ({ data, systemPath })=>{
18
+ const handleSubmit = async ({ data, systemPath, systemAvailableLocales })=>{
19
19
  try {
20
20
  await createCollectionDocument({
21
21
  data: {
@@ -23,6 +23,9 @@ const CreateView = ({ collectionDefinition, adminConfig, initialData })=>{
23
23
  data,
24
24
  ...systemPath ? {
25
25
  path: systemPath
26
+ } : {},
27
+ ...systemAvailableLocales ? {
28
+ availableLocales: systemAvailableLocales
26
29
  } : {}
27
30
  }
28
31
  });
@@ -68,6 +71,7 @@ const CreateView = ({ collectionDefinition, adminConfig, initialData })=>{
68
71
  adminConfig: adminConfig,
69
72
  useAsTitle: collectionDefinition.useAsTitle,
70
73
  useAsPath: collectionDefinition.useAsPath,
74
+ advertiseLocales: collectionDefinition.advertiseLocales,
71
75
  headingLabel: labels.singular,
72
76
  useNavigationGuard: useTanStackNavigationGuard,
73
77
  onCancel: ()=>navigate({
@@ -346,7 +346,7 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
346
346
  });
347
347
  }
348
348
  };
349
- const handleSubmit = async ({ data: _data, patches, systemPath })=>{
349
+ const handleSubmit = async ({ data: _data, patches, systemPath, systemAvailableLocales })=>{
350
350
  try {
351
351
  await updateCollectionDocumentWithPatches({
352
352
  data: {
@@ -357,6 +357,9 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
357
357
  locale,
358
358
  ...systemPath ? {
359
359
  path: systemPath
360
+ } : {},
361
+ ...systemAvailableLocales ? {
362
+ availableLocales: systemAvailableLocales
360
363
  } : {}
361
364
  }
362
365
  });
@@ -422,6 +425,7 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
422
425
  adminConfig: adminConfig,
423
426
  useAsTitle: collectionDefinition.useAsTitle,
424
427
  useAsPath: collectionDefinition.useAsPath,
428
+ advertiseLocales: collectionDefinition.advertiseLocales,
425
429
  headingLabel: labels.singular,
426
430
  initialLocale: locale,
427
431
  onLocaleChange: handleLocaleChange,
@@ -10,11 +10,13 @@ export declare const createCollectionDocument: import("@tanstack/react-start").R
10
10
  data: any;
11
11
  locale?: string;
12
12
  path?: string;
13
+ availableLocales?: string[];
13
14
  }) => {
14
15
  collection: string;
15
16
  data: any;
16
17
  locale?: string;
17
18
  path?: string;
19
+ availableLocales?: string[];
18
20
  }, Promise<{
19
21
  status: "ok";
20
22
  }>>;
@@ -6,7 +6,7 @@ import { ensureCollection } from "../../integrations/api-utils.js";
6
6
  const createCollectionDocument = createServerFn({
7
7
  method: 'POST'
8
8
  }).inputValidator((input)=>input).handler(async ({ data: input })=>{
9
- const { collection: path, data: documentData, locale, path: explicitPath } = input;
9
+ const { collection: path, data: documentData, locale, path: explicitPath, availableLocales } = input;
10
10
  const logger = getLogger();
11
11
  const config = await ensureCollection(path);
12
12
  if (!config) throw ERR_NOT_FOUND({
@@ -31,7 +31,8 @@ const createCollectionDocument = createServerFn({
31
31
  data: structuredClone(documentData),
32
32
  status: documentData.status,
33
33
  locale: locale ?? serverConfig.i18n.content.defaultLocale,
34
- path: explicitPath
34
+ path: explicitPath,
35
+ availableLocales
35
36
  });
36
37
  return {
37
38
  status: 'ok'
@@ -16,6 +16,8 @@
16
16
  * `{target_document_id, target_collection_id}` refs.
17
17
  */
18
18
  export declare function getCollectionDocument(collection: string, id: string, locale?: string, depth?: number, populateRelations?: boolean): Promise<{
19
+ _availableVersionLocales?: string[] | undefined;
20
+ availableLocales?: string[] | undefined;
19
21
  _restoreWarnings?: string[] | undefined;
20
22
  _publishedVersion: Record<string, any> | null;
21
23
  } | null>;
@@ -39,6 +39,7 @@ const getDocumentFn = createServerFn({
39
39
  populate,
40
40
  depth: resolvedDepth,
41
41
  status: 'any',
42
+ onMissingLocale: 'empty',
42
43
  lenient: true
43
44
  });
44
45
  if (!document) throw ERR_NOT_FOUND({
@@ -69,11 +70,19 @@ const getDocumentFn = createServerFn({
69
70
  }) : null;
70
71
  }
71
72
  const restoreWarnings = serialised._restoreWarnings;
73
+ const availableLocales = serialised.availableLocales;
74
+ const availableVersionLocales = serialised._availableVersionLocales;
72
75
  return {
73
76
  ...parsed,
74
77
  _publishedVersion: publishedVersion,
75
78
  ...restoreWarnings && restoreWarnings.length > 0 ? {
76
79
  _restoreWarnings: restoreWarnings
80
+ } : {},
81
+ ...Array.isArray(availableLocales) ? {
82
+ availableLocales
83
+ } : {},
84
+ ...Array.isArray(availableVersionLocales) ? {
85
+ _availableVersionLocales: availableVersionLocales
77
86
  } : {}
78
87
  };
79
88
  });
@@ -29,6 +29,7 @@ export declare const getCollectionDocumentHistory: import("@tanstack/react-start
29
29
  updatedAt: string;
30
30
  versionId?: string | undefined;
31
31
  path?: string | undefined;
32
+ sourceLocale?: string | undefined;
32
33
  hasPublishedVersion?: boolean | undefined;
33
34
  }[];
34
35
  meta: {
@@ -30,6 +30,7 @@ export declare const getCollectionDocuments: import("@tanstack/react-start").Req
30
30
  updatedAt: string;
31
31
  versionId?: string | undefined;
32
32
  path?: string | undefined;
33
+ sourceLocale?: string | undefined;
33
34
  hasPublishedVersion?: boolean | undefined;
34
35
  }[];
35
36
  meta: {
@@ -33,7 +33,8 @@ const getCollectionDocuments = createServerFn({
33
33
  page: params.page,
34
34
  pageSize,
35
35
  select: params.fields,
36
- status: 'any'
36
+ status: 'any',
37
+ onMissingLocale: 'empty'
37
38
  });
38
39
  const documentIds = result.docs.map((d)=>d.id);
39
40
  const publishedSet = documentIds.length > 0 ? await getServerConfig().db.queries.documents.getPublishedDocumentIds({
@@ -13,6 +13,7 @@ export declare const updateCollectionDocumentWithPatches: import("@tanstack/reac
13
13
  versionId?: string;
14
14
  locale?: string;
15
15
  path?: string;
16
+ availableLocales?: string[];
16
17
  }) => {
17
18
  collection: string;
18
19
  id: string;
@@ -20,6 +21,7 @@ export declare const updateCollectionDocumentWithPatches: import("@tanstack/reac
20
21
  versionId?: string;
21
22
  locale?: string;
22
23
  path?: string;
24
+ availableLocales?: string[];
23
25
  }, Promise<{
24
26
  status: "ok";
25
27
  }>>;
@@ -6,7 +6,7 @@ import { ensureCollection } from "../../integrations/api-utils.js";
6
6
  const updateCollectionDocumentWithPatches = createServerFn({
7
7
  method: 'POST'
8
8
  }).inputValidator((input)=>input).handler(async ({ data: input })=>{
9
- const { collection: path, id, patches, versionId, locale, path: explicitPath } = input;
9
+ const { collection: path, id, patches, versionId, locale, path: explicitPath, availableLocales } = input;
10
10
  const logger = getLogger();
11
11
  const config = await ensureCollection(path);
12
12
  if (!config) throw ERR_NOT_FOUND({
@@ -32,7 +32,8 @@ const updateCollectionDocumentWithPatches = createServerFn({
32
32
  patches,
33
33
  documentVersionId: versionId,
34
34
  locale: locale ?? serverConfig.i18n.content.defaultLocale,
35
- path: explicitPath
35
+ path: explicitPath,
36
+ availableLocales
36
37
  });
37
38
  return {
38
39
  status: 'ok'
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "private": false,
4
4
  "type": "module",
5
5
  "license": "MPL-2.0",
6
- "version": "2.7.0",
6
+ "version": "3.0.1",
7
7
  "engines": {
8
8
  "node": ">=20.9.0"
9
9
  },
@@ -115,13 +115,13 @@
115
115
  "react-swipeable": "^7.0.2",
116
116
  "uuid": "^14.0.0",
117
117
  "zod": "^4.4.3",
118
- "@byline/ai": "2.7.0",
119
- "@byline/core": "2.7.0",
120
- "@byline/i18n": "2.7.0",
121
- "@byline/client": "2.7.0",
122
- "@byline/admin": "2.7.0",
123
- "@byline/auth": "2.7.0",
124
- "@byline/ui": "2.7.0"
118
+ "@byline/admin": "3.0.1",
119
+ "@byline/auth": "3.0.1",
120
+ "@byline/core": "3.0.1",
121
+ "@byline/ui": "3.0.1",
122
+ "@byline/i18n": "3.0.1",
123
+ "@byline/client": "3.0.1",
124
+ "@byline/ai": "3.0.1"
125
125
  },
126
126
  "peerDependencies": {
127
127
  "@tanstack/react-router": "^1.167.0",
@@ -10,14 +10,24 @@
10
10
  * These are designed to be used as `errorComponent` and `notFoundComponent`
11
11
  * on layout routes (root, admin, public) so that errors render inside the
12
12
  * appropriate shell rather than blowing away the entire page.
13
+ *
14
+ * `RouteError` / `RouteNotFound` self-mount their own `<I18nProvider>`. A
15
+ * route's `errorComponent` renders *in place of that route's `component`*, so
16
+ * the admin layout's own `<I18nProvider>` (mounted inside that component) is
17
+ * gone by the time the error screen renders — calling `useTranslation` against
18
+ * the absent provider previously threw a *second* error that masked the real
19
+ * one. Self-mounting (mirroring `sign-in-page.tsx`) makes the screen localised
20
+ * and provider-independent wherever it renders.
13
21
  */
14
22
 
23
+ import { type ReactNode, useContext } from 'react'
15
24
  import type { ErrorComponentProps, NotFoundRouteProps } from '@tanstack/react-router'
16
25
  import { useRouter } from '@tanstack/react-router'
17
26
 
18
- import { BylineError, ErrorCodes } from '@byline/core'
27
+ import { BylineError, ErrorCodes, getClientConfig } from '@byline/core'
28
+ import type { TranslationBundle } from '@byline/i18n'
19
29
  import type { UseTranslationReturn } from '@byline/i18n/react'
20
- import { useTranslation } from '@byline/i18n/react'
30
+ import { I18nContext, I18nProvider, useTranslation } from '@byline/i18n/react'
21
31
  import { Alert, Button, Container, Section } from '@byline/ui/react'
22
32
  import cx from 'classnames'
23
33
 
@@ -56,7 +66,52 @@ function getErrorMessage(error: unknown, t: Translate): string {
56
66
  return t('routeError.defaultMessage')
57
67
  }
58
68
 
59
- export function RouteError({ error, reset }: ErrorComponentProps) {
69
+ /**
70
+ * Self-mounts the byline-admin `<I18nProvider>` for the error/not-found
71
+ * screens (see the file header for why the layout-level provider is absent
72
+ * here).
73
+ *
74
+ * When a provider is already in scope (e.g. a not-found that renders inside
75
+ * the admin layout) it is reused as-is, preserving the user's active locale.
76
+ * Only when none is present (the error-boundary case) does it self-mount.
77
+ *
78
+ * Resilient by design (an error component that throws masks the real error):
79
+ * if the client config can't be read, it falls back to an English-default
80
+ * provider with an empty bundle. With a provider always present,
81
+ * `useTranslation` never throws; a missing key then renders as the raw key
82
+ * rather than crashing (see `I18nProvider`'s `onMissing`).
83
+ */
84
+ function ErrorScreenI18nProvider({ children }: { children: ReactNode }) {
85
+ // Already inside a provider — reuse it (keeps the active locale) and skip the
86
+ // self-mount entirely.
87
+ if (useContext(I18nContext) != null) {
88
+ return <>{children}</>
89
+ }
90
+ let bundle: TranslationBundle = {}
91
+ let locale = 'en'
92
+ try {
93
+ const { i18n } = getClientConfig()
94
+ bundle = i18n.translations ?? {}
95
+ // No active provider, so render in the default locale — recovering the
96
+ // per-user active locale isn't worth another lookup that could itself throw.
97
+ locale = i18n.interface.defaultLocale
98
+ } catch {
99
+ // Config unavailable (very early boot / broken provider chain) — fall
100
+ // through with the English defaults above.
101
+ }
102
+ return (
103
+ <I18nProvider
104
+ bundle={bundle}
105
+ activeLocale={locale}
106
+ defaultLocale={locale}
107
+ localeDefinitions={[]}
108
+ >
109
+ {children}
110
+ </I18nProvider>
111
+ )
112
+ }
113
+
114
+ function RouteErrorContent({ error, reset }: ErrorComponentProps) {
60
115
  const router = useRouter()
61
116
  const { t } = useTranslation('byline-admin')
62
117
 
@@ -89,7 +144,15 @@ export function RouteError({ error, reset }: ErrorComponentProps) {
89
144
  )
90
145
  }
91
146
 
92
- export function RouteNotFound(_props: NotFoundRouteProps) {
147
+ export function RouteError(props: ErrorComponentProps) {
148
+ return (
149
+ <ErrorScreenI18nProvider>
150
+ <RouteErrorContent {...props} />
151
+ </ErrorScreenI18nProvider>
152
+ )
153
+ }
154
+
155
+ function RouteNotFoundContent() {
93
156
  const { t } = useTranslation('byline-admin')
94
157
  return (
95
158
  <Section className={cx('byline-route-error', styles.section)}>
@@ -109,6 +172,14 @@ export function RouteNotFound(_props: NotFoundRouteProps) {
109
172
  )
110
173
  }
111
174
 
175
+ export function RouteNotFound(_props: NotFoundRouteProps) {
176
+ return (
177
+ <ErrorScreenI18nProvider>
178
+ <RouteNotFoundContent />
179
+ </ErrorScreenI18nProvider>
180
+ )
181
+ }
182
+
112
183
  /**
113
184
  * Minimal fallback for when providers may be broken — used at the root
114
185
  * route where the i18n provider itself may not be mounted. Stays in
@@ -44,10 +44,12 @@ export const CreateView = ({
44
44
  const handleSubmit = async ({
45
45
  data,
46
46
  systemPath,
47
+ systemAvailableLocales,
47
48
  }: {
48
49
  // biome-ignore lint/suspicious/noExplicitAny: data is collection-specific
49
50
  data: any
50
51
  systemPath?: string | null
52
+ systemAvailableLocales?: string[]
51
53
  }) => {
52
54
  try {
53
55
  await createCollectionDocument({
@@ -55,6 +57,7 @@ export const CreateView = ({
55
57
  collection: path,
56
58
  data,
57
59
  ...(systemPath ? { path: systemPath } : {}),
60
+ ...(systemAvailableLocales ? { availableLocales: systemAvailableLocales } : {}),
58
61
  },
59
62
  })
60
63
  navigate({
@@ -97,6 +100,7 @@ export const CreateView = ({
97
100
  adminConfig={adminConfig}
98
101
  useAsTitle={collectionDefinition.useAsTitle}
99
102
  useAsPath={collectionDefinition.useAsPath}
103
+ advertiseLocales={collectionDefinition.advertiseLocales}
100
104
  headingLabel={labels.singular}
101
105
  useNavigationGuard={useTanStackNavigationGuard}
102
106
  onCancel={() =>
@@ -334,12 +334,14 @@ export const EditView = ({
334
334
  data: _data,
335
335
  patches,
336
336
  systemPath,
337
+ systemAvailableLocales,
337
338
  }: {
338
339
  // biome-ignore lint/suspicious/noExplicitAny: data is collection-specific
339
340
  data: any
340
341
  // biome-ignore lint/suspicious/noExplicitAny: patches list shape
341
342
  patches: any[]
342
343
  systemPath?: string | null
344
+ systemAvailableLocales?: string[]
343
345
  }) => {
344
346
  try {
345
347
  await updateCollectionDocumentWithPatches({
@@ -350,6 +352,7 @@ export const EditView = ({
350
352
  versionId: initialData.versionId as string | undefined,
351
353
  locale,
352
354
  ...(systemPath ? { path: systemPath } : {}),
355
+ ...(systemAvailableLocales ? { availableLocales: systemAvailableLocales } : {}),
353
356
  },
354
357
  })
355
358
 
@@ -404,6 +407,7 @@ export const EditView = ({
404
407
  adminConfig={adminConfig}
405
408
  useAsTitle={collectionDefinition.useAsTitle}
406
409
  useAsPath={collectionDefinition.useAsPath}
410
+ advertiseLocales={collectionDefinition.advertiseLocales}
407
411
  headingLabel={labels.singular}
408
412
  initialLocale={locale}
409
413
  onLocaleChange={handleLocaleChange}
@@ -21,10 +21,22 @@ import { ensureCollection } from '../../integrations/api-utils.js'
21
21
 
22
22
  export const createCollectionDocument = createServerFn({ method: 'POST' })
23
23
  .inputValidator(
24
- (input: { collection: string; data: any; locale?: string; path?: string }) => input
24
+ (input: {
25
+ collection: string
26
+ data: any
27
+ locale?: string
28
+ path?: string
29
+ availableLocales?: string[]
30
+ }) => input
25
31
  )
26
32
  .handler(async ({ data: input }) => {
27
- const { collection: path, data: documentData, locale, path: explicitPath } = input
33
+ const {
34
+ collection: path,
35
+ data: documentData,
36
+ locale,
37
+ path: explicitPath,
38
+ availableLocales,
39
+ } = input
28
40
  const logger = getLogger()
29
41
  const config = await ensureCollection(path)
30
42
  if (!config) {
@@ -52,6 +64,7 @@ export const createCollectionDocument = createServerFn({ method: 'POST' })
52
64
  status: documentData.status,
53
65
  locale: locale ?? serverConfig.i18n.content.defaultLocale,
54
66
  path: explicitPath,
67
+ availableLocales,
55
68
  })
56
69
 
57
70
  return { status: 'ok' as const }
@@ -88,6 +88,10 @@ const getDocumentFn = createServerFn({ method: 'GET' })
88
88
  populate,
89
89
  depth: resolvedDepth,
90
90
  status: 'any',
91
+ // Admin edit path: show the RAW per-locale values — untranslated localized
92
+ // fields stay empty (the signal to use "Copy to Locale"), never falling
93
+ // back to the default locale. Overrides the client's `'fallback'` default.
94
+ onMissingLocale: 'empty',
91
95
  // Admin edit path: tolerate schema-mismatch warnings rather than
92
96
  // hard-failing the load. Warnings (if any) come back on the document
93
97
  // as `_restoreWarnings` and the edit form surfaces them via an Alert.
@@ -148,12 +152,29 @@ const getDocumentFn = createServerFn({ method: 'GET' })
148
152
  | string[]
149
153
  | undefined
150
154
 
155
+ // Same preservation for the available-locales metadata — both are stripped
156
+ // by the Zod parse but the sidebar widget needs them at edit time:
157
+ // `availableLocales` — the stored editorial set → initialises the
158
+ // form-context slot / the checked state.
159
+ // `_availableVersionLocales` — the ledger fact → drives each row's intent.
160
+ // See docs/I18N.md.
161
+ const availableLocales = (serialised as Record<string, any>).availableLocales as
162
+ | string[]
163
+ | undefined
164
+ const availableVersionLocales = (serialised as Record<string, any>)._availableVersionLocales as
165
+ | string[]
166
+ | undefined
167
+
151
168
  return {
152
169
  ...(parsed as Record<string, any>),
153
170
  _publishedVersion: publishedVersion,
154
171
  ...(restoreWarnings && restoreWarnings.length > 0
155
172
  ? { _restoreWarnings: restoreWarnings }
156
173
  : {}),
174
+ ...(Array.isArray(availableLocales) ? { availableLocales } : {}),
175
+ ...(Array.isArray(availableVersionLocales)
176
+ ? { _availableVersionLocales: availableVersionLocales }
177
+ : {}),
157
178
  }
158
179
  })
159
180
 
@@ -82,6 +82,11 @@ export const getCollectionDocuments = createServerFn({ method: 'GET' })
82
82
  pageSize,
83
83
  select: params.fields,
84
84
  status: 'any',
85
+ // Admin list: show the raw per-locale state (untranslated docs render
86
+ // empty in the active locale's columns) rather than falling back to the
87
+ // default locale. Consistent with the edit view; overrides the client's
88
+ // `'fallback'` default.
89
+ onMissingLocale: 'empty',
85
90
  })
86
91
 
87
92
  // Decorate each doc with `hasPublishedVersion` so the list UI can show a
@@ -29,10 +29,19 @@ export const updateCollectionDocumentWithPatches = createServerFn({ method: 'POS
29
29
  versionId?: string
30
30
  locale?: string
31
31
  path?: string
32
+ availableLocales?: string[]
32
33
  }) => input
33
34
  )
34
35
  .handler(async ({ data: input }) => {
35
- const { collection: path, id, patches, versionId, locale, path: explicitPath } = input
36
+ const {
37
+ collection: path,
38
+ id,
39
+ patches,
40
+ versionId,
41
+ locale,
42
+ path: explicitPath,
43
+ availableLocales,
44
+ } = input
36
45
  const logger = getLogger()
37
46
  const config = await ensureCollection(path)
38
47
  if (!config) {
@@ -61,6 +70,7 @@ export const updateCollectionDocumentWithPatches = createServerFn({ method: 'POS
61
70
  documentVersionId: versionId,
62
71
  locale: locale ?? serverConfig.i18n.content.defaultLocale,
63
72
  path: explicitPath,
73
+ availableLocales,
64
74
  })
65
75
 
66
76
  return { status: 'ok' as const }