@nitrogenbuilder/connector-payload 1.1.0 → 1.2.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.
@@ -1,4 +1,5 @@
1
1
  import { getNitrogenSettings, buildDynamicData, buildResolvedPageResponse, buildResolvedListItemResponse, getDocumentPermalink, getDocumentRelativePermalink, getTemplateForType, requireAuth, } from './helpers.js';
2
+ import { buildDynamicDataMeta, computeDocTranslationStatus, fetchLocaleAllDoc, getDefaultLocaleTitle, getLocalizationSettings, getPayloadLocalization, getRequestLanguage, resolveLocaleOptions, } from '../localization.js';
2
3
  function isContentTemplateCollection(collection) {
3
4
  return !!collection && collection !== 'header' && collection !== 'footer';
4
5
  }
@@ -25,6 +26,7 @@ export const templatesEndpoints = [
25
26
  handler: async (req) => {
26
27
  const { payload, routeParams } = req;
27
28
  const slug = routeParams?.slug;
29
+ const localeOptions = resolveLocaleOptions(payload, getRequestLanguage(req));
28
30
  try {
29
31
  const [result, headerTemplate, footerTemplate] = await Promise.all([
30
32
  payload.find({
@@ -32,6 +34,7 @@ export const templatesEndpoints = [
32
34
  where: { slug: { equals: slug } },
33
35
  limit: 1,
34
36
  depth: 1,
37
+ ...localeOptions,
35
38
  }),
36
39
  getTemplateForType(payload, 'header'),
37
40
  getTemplateForType(payload, 'footer'),
@@ -50,6 +53,7 @@ export const templatesEndpoints = [
50
53
  collection: associatedCollection,
51
54
  limit: 1,
52
55
  depth: 1,
56
+ ...localeOptions,
53
57
  });
54
58
  if (associated.docs.length) {
55
59
  associatedDoc = associated.docs[0];
@@ -61,9 +65,24 @@ export const templatesEndpoints = [
61
65
  }
62
66
  }
63
67
  const response = await buildResolvedPageResponse(payload, doc, settings, dynamicData);
68
+ // When a language was requested, the top-level `title` must stay the
69
+ // default-locale one — the editor edits default-language field data
70
+ // only and PATCHes `title` back on save. The meta may describe the
71
+ // associated doc, but the round-tripped title is the template's own.
72
+ const templateAllDoc = localeOptions.locale
73
+ ? await fetchLocaleAllDoc(payload, 'nitrogen-templates', doc.id)
74
+ : undefined;
75
+ const dynamicDataMeta = associatedDoc
76
+ ? await buildDynamicDataMeta(payload, associatedCollection, associatedDoc.id)
77
+ : await buildDynamicDataMeta(payload, 'nitrogen-templates', doc.id, templateAllDoc);
78
+ const titleOverride = localeOptions.locale
79
+ ? getDefaultLocaleTitle(payload, templateAllDoc ?? null)
80
+ : null;
64
81
  const previewTarget = getTemplatePreviewTarget(doc, settings, associatedCollection, associatedDoc);
65
82
  return Response.json({
66
83
  ...response,
84
+ ...(titleOverride !== null ? { title: titleOverride } : {}),
85
+ ...(dynamicDataMeta ? { dynamic_data_meta: dynamicDataMeta } : {}),
67
86
  slug: previewTarget.slug,
68
87
  permalink: previewTarget.permalink,
69
88
  relative_permalink: previewTarget.relative_permalink,
@@ -87,6 +106,7 @@ export const templatesEndpoints = [
87
106
  const settings = await getNitrogenSettings(payload);
88
107
  const url = new URL(req.url || '', 'http://localhost');
89
108
  const templateType = url.searchParams.get('templateType');
109
+ const localeOptions = resolveLocaleOptions(payload, getRequestLanguage(req));
90
110
  const where = {
91
111
  status: { equals: 'published' },
92
112
  };
@@ -98,6 +118,7 @@ export const templatesEndpoints = [
98
118
  where,
99
119
  limit: 0,
100
120
  depth: 1,
121
+ ...localeOptions,
101
122
  });
102
123
  const items = await Promise.all(result.docs.map(async (doc) => {
103
124
  const response = await buildResolvedListItemResponse(payload, doc, settings);
@@ -118,12 +139,14 @@ export const templatesEndpoints = [
118
139
  handler: async (req) => {
119
140
  const { payload, routeParams } = req;
120
141
  const id = routeParams?.id;
142
+ const localeOptions = resolveLocaleOptions(payload, getRequestLanguage(req));
121
143
  try {
122
144
  const [doc, headerTemplate, footerTemplate] = await Promise.all([
123
145
  payload.findByID({
124
146
  collection: 'nitrogen-templates',
125
147
  id,
126
148
  depth: 1,
149
+ ...localeOptions,
127
150
  }),
128
151
  getTemplateForType(payload, 'header'),
129
152
  getTemplateForType(payload, 'footer'),
@@ -140,6 +163,7 @@ export const templatesEndpoints = [
140
163
  collection: associatedCollection,
141
164
  limit: 1,
142
165
  depth: 1,
166
+ ...localeOptions,
143
167
  });
144
168
  if (associated.docs.length) {
145
169
  associatedDoc = associated.docs[0];
@@ -151,9 +175,22 @@ export const templatesEndpoints = [
151
175
  }
152
176
  }
153
177
  const response = await buildResolvedPageResponse(payload, doc, settings, dynamicData);
178
+ // See GET slug/:slug — keep the round-tripped `title` at the default
179
+ // locale.
180
+ const templateAllDoc = localeOptions.locale
181
+ ? await fetchLocaleAllDoc(payload, 'nitrogen-templates', doc.id)
182
+ : undefined;
183
+ const dynamicDataMeta = associatedDoc
184
+ ? await buildDynamicDataMeta(payload, associatedCollection, associatedDoc.id)
185
+ : await buildDynamicDataMeta(payload, 'nitrogen-templates', doc.id, templateAllDoc);
186
+ const titleOverride = localeOptions.locale
187
+ ? getDefaultLocaleTitle(payload, templateAllDoc ?? null)
188
+ : null;
154
189
  const previewTarget = getTemplatePreviewTarget(doc, settings, associatedCollection, associatedDoc);
155
190
  return Response.json({
156
191
  ...response,
192
+ ...(titleOverride !== null ? { title: titleOverride } : {}),
193
+ ...(dynamicDataMeta ? { dynamic_data_meta: dynamicDataMeta } : {}),
157
194
  slug: previewTarget.slug,
158
195
  permalink: previewTarget.permalink,
159
196
  relative_permalink: previewTarget.relative_permalink,
@@ -168,6 +205,56 @@ export const templatesEndpoints = [
168
205
  }
169
206
  },
170
207
  },
208
+ // GET /api/nitrogen/v1/nitrogen-templates/:id/dynamic-data — Dynamic data
209
+ // (+ meta) for a template, optionally in a specific language (?lang=es).
210
+ {
211
+ path: '/nitrogen/v1/nitrogen-templates/:id/dynamic-data',
212
+ method: 'get',
213
+ handler: async (req) => {
214
+ const { payload, routeParams } = req;
215
+ const id = routeParams?.id;
216
+ const localeOptions = resolveLocaleOptions(payload, getRequestLanguage(req));
217
+ try {
218
+ const doc = (await payload.findByID({
219
+ collection: 'nitrogen-templates',
220
+ id,
221
+ depth: 1,
222
+ ...localeOptions,
223
+ }));
224
+ const settings = await getNitrogenSettings(payload);
225
+ let dynamicData = buildDynamicData(doc, settings);
226
+ let associatedDoc;
227
+ const associatedCollection = doc.associatedCollection;
228
+ if (isContentTemplateCollection(associatedCollection)) {
229
+ try {
230
+ const associated = await payload.find({
231
+ collection: associatedCollection,
232
+ limit: 1,
233
+ depth: 1,
234
+ ...localeOptions,
235
+ });
236
+ if (associated.docs.length) {
237
+ associatedDoc = associated.docs[0];
238
+ dynamicData = buildDynamicData(associatedDoc, settings);
239
+ }
240
+ }
241
+ catch {
242
+ // Collection may not exist — use template's own data
243
+ }
244
+ }
245
+ const dynamicDataMeta = associatedDoc
246
+ ? await buildDynamicDataMeta(payload, associatedCollection, associatedDoc.id)
247
+ : await buildDynamicDataMeta(payload, 'nitrogen-templates', doc.id);
248
+ return Response.json({
249
+ dynamic_data: dynamicData,
250
+ ...(dynamicDataMeta ? { dynamic_data_meta: dynamicDataMeta } : {}),
251
+ });
252
+ }
253
+ catch {
254
+ return Response.json({ error: 'Not found' }, { status: 404 });
255
+ }
256
+ },
257
+ },
171
258
  // PATCH /api/nitrogen/v1/templates/:id — Update a template
172
259
  {
173
260
  path: '/nitrogen/v1/nitrogen-templates/:id',
@@ -206,10 +293,39 @@ export const templatesEndpoints = [
206
293
  if (Object.keys(updateData).length === 0) {
207
294
  return Response.json({ error: 'No update data provided' }, { status: 400 });
208
295
  }
296
+ // Localization enabled → recompute per-language translation status from
297
+ // the saved module tree. Advisory only — never blocks the save.
298
+ if (updateData.nitrogenData !== undefined) {
299
+ try {
300
+ const settings = await getNitrogenSettings(payload);
301
+ const localization = getLocalizationSettings(settings);
302
+ if (localization) {
303
+ const translationStatus = await computeDocTranslationStatus({
304
+ payload,
305
+ collectionSlug: 'nitrogen-templates',
306
+ docId: id,
307
+ nitrogenData: updateData.nitrogenData,
308
+ localization,
309
+ });
310
+ if (translationStatus) {
311
+ updateData.nitrogenTranslationStatus = translationStatus;
312
+ }
313
+ }
314
+ }
315
+ catch {
316
+ // Status is advisory — never block the save.
317
+ }
318
+ }
319
+ // The editor edits default-language field data only — pin the write to
320
+ // the default locale so it can never target a request-derived locale.
321
+ const payloadLocalization = getPayloadLocalization(payload);
209
322
  await payload.update({
210
323
  collection: 'nitrogen-templates',
211
324
  id,
212
325
  data: updateData,
326
+ ...(payloadLocalization
327
+ ? { locale: payloadLocalization.defaultLocale }
328
+ : {}),
213
329
  });
214
330
  return Response.json({ success: true });
215
331
  },
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Translation status route — lists per-language translation coverage for
3
+ * every Nitrogen-enabled document and template, for the editor's translation
4
+ * dashboard.
5
+ *
6
+ * The status is computed and stored (in `nitrogenTranslationStatus`) by the
7
+ * save handlers; this route reads the stored value and only computes on the
8
+ * fly for docs that have never been saved since localization was enabled.
9
+ */
10
+ import type { Endpoint } from 'payload';
11
+ export declare function createTranslationStatusEndpoints(collections: string[]): Endpoint[];
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Translation status route — lists per-language translation coverage for
3
+ * every Nitrogen-enabled document and template, for the editor's translation
4
+ * dashboard.
5
+ *
6
+ * The status is computed and stored (in `nitrogenTranslationStatus`) by the
7
+ * save handlers; this route reads the stored value and only computes on the
8
+ * fly for docs that have never been saved since localization was enabled.
9
+ */
10
+ import { getNitrogenSettings, requireAuth } from './helpers.js';
11
+ import { findAllDocs } from '../inventory/indexing.js';
12
+ import { computeDocTranslationStatus, getLocalizationSettings, getPayloadLocalization, loadTranslatablePropDefs, } from '../localization.js';
13
+ function isStoredStatus(value) {
14
+ return !!value && typeof value === 'object' && !Array.isArray(value);
15
+ }
16
+ /**
17
+ * Docs fetched at locale 'all' carry localized fields as locale-keyed
18
+ * objects — read the default language's title in that case.
19
+ */
20
+ function docTitle(doc, defaultLanguage) {
21
+ const title = doc.title;
22
+ if (typeof title === 'string')
23
+ return title;
24
+ if (title && typeof title === 'object' && !Array.isArray(title)) {
25
+ const record = title;
26
+ const value = record[defaultLanguage];
27
+ if (typeof value === 'string')
28
+ return value;
29
+ // No default-language title — fall back to the first stored one, matching
30
+ // the renderer's language-resolution fallback chain.
31
+ for (const candidate of Object.values(record)) {
32
+ if (typeof candidate === 'string' && candidate)
33
+ return candidate;
34
+ }
35
+ }
36
+ return '';
37
+ }
38
+ export function createTranslationStatusEndpoints(collections) {
39
+ return [
40
+ // GET /api/nitrogen/v1/translation-status — TranslationStatusEntry[] across
41
+ // configured collections + nitrogen-templates
42
+ {
43
+ path: '/nitrogen/v1/translation-status',
44
+ method: 'get',
45
+ handler: async (req) => {
46
+ const authError = requireAuth(req);
47
+ if (authError)
48
+ return authError;
49
+ const { payload } = req;
50
+ const settings = await getNitrogenSettings(payload);
51
+ const localization = getLocalizationSettings(settings);
52
+ if (!localization) {
53
+ return Response.json([]);
54
+ }
55
+ // One catalog load shared by every on-the-fly computation.
56
+ const propDefs = await loadTranslatablePropDefs(payload);
57
+ // With Payload-native localization, fetch docs at locale 'all' so
58
+ // field-level coverage can be read without per-doc re-fetches.
59
+ const locale = getPayloadLocalization(payload) ? 'all' : undefined;
60
+ const entries = [];
61
+ const allCollections = Array.from(new Set([...collections, 'nitrogen-templates']));
62
+ for (const collectionSlug of allCollections) {
63
+ let docs;
64
+ try {
65
+ docs = await findAllDocs(payload, collectionSlug, { locale });
66
+ }
67
+ catch {
68
+ // One bad collection shouldn't 500 the whole listing — skip it.
69
+ continue;
70
+ }
71
+ const type = collectionSlug === 'nitrogen-templates' ? 'template' : 'page';
72
+ for (const doc of docs) {
73
+ let languages = null;
74
+ if (isStoredStatus(doc.nitrogenTranslationStatus)) {
75
+ languages = doc.nitrogenTranslationStatus;
76
+ }
77
+ else {
78
+ try {
79
+ languages = await computeDocTranslationStatus({
80
+ payload,
81
+ collectionSlug,
82
+ docId: doc.id,
83
+ nitrogenData: doc.nitrogenData,
84
+ localization,
85
+ propDefs,
86
+ localeAllDoc: locale ? doc : null,
87
+ });
88
+ }
89
+ catch {
90
+ languages = null;
91
+ }
92
+ }
93
+ entries.push({
94
+ id: doc.id,
95
+ title: docTitle(doc, localization.defaultLanguage),
96
+ type,
97
+ languages: languages || {},
98
+ });
99
+ }
100
+ }
101
+ return Response.json(entries);
102
+ },
103
+ },
104
+ ];
105
+ }
@@ -5,6 +5,11 @@ export interface NitrogenPageClientProps {
5
5
  dynamicData: Record<string, unknown>;
6
6
  isBuilder?: boolean;
7
7
  collection?: string;
8
+ /**
9
+ * Language to render translatable builder props in (a configured language
10
+ * code). Forwarded to NitrogenRenderer. Omit for the default language.
11
+ */
12
+ language?: string;
8
13
  }
9
14
  /**
10
15
  * Client component that renders Nitrogen pages using React.
@@ -12,5 +17,5 @@ export interface NitrogenPageClientProps {
12
17
  * Uses `useNitrogenData` hook for live preview updates in builder mode,
13
18
  * and `NitrogenRenderer` for React-based rendering of the component tree.
14
19
  */
15
- export declare function NitrogenPageClient({ pageId, pageData, dynamicData: initialDynamicData, isBuilder, collection, }: NitrogenPageClientProps): import("react/jsx-runtime").JSX.Element;
20
+ export declare function NitrogenPageClient({ pageId, pageData, dynamicData: initialDynamicData, isBuilder, collection, language, }: NitrogenPageClientProps): import("react/jsx-runtime").JSX.Element;
16
21
  export default NitrogenPageClient;
@@ -9,7 +9,7 @@ import { Nitrogen } from "@nitrogenbuilder/client-core";
9
9
  * Uses `useNitrogenData` hook for live preview updates in builder mode,
10
10
  * and `NitrogenRenderer` for React-based rendering of the component tree.
11
11
  */
12
- export function NitrogenPageClient({ pageId, pageData, dynamicData: initialDynamicData, isBuilder = false, collection, }) {
12
+ export function NitrogenPageClient({ pageId, pageData, dynamicData: initialDynamicData, isBuilder = false, collection, language, }) {
13
13
  // Use the hook for live preview updates
14
14
  const page = useNitrogenData({
15
15
  type: "page",
@@ -50,6 +50,10 @@ export function NitrogenPageClient({ pageId, pageData, dynamicData: initialDynam
50
50
  }
51
51
  };
52
52
  }, [isBuilder]);
53
- return (_jsx("div", { "data-nitrogen-location": "page", "data-page-id": pageId, "data-collection": collection, children: _jsx(NitrogenRenderer, { page: page?.data ?? [], dynamicData: page?.dynamicData ?? initialDynamicData, requestedData: {} }) }));
53
+ // The `language` prop is being added to NitrogenRenderer in the client-react
54
+ // package; spread it loosely so this compiles against both old and new
55
+ // renderer typings (older renderers simply ignore the extra prop).
56
+ const languageProp = language ? { language } : {};
57
+ return (_jsx("div", { "data-nitrogen-location": "page", "data-page-id": pageId, "data-collection": collection, "data-language": language, children: _jsx(NitrogenRenderer, { page: page?.data ?? [], dynamicData: page?.dynamicData ?? initialDynamicData, requestedData: {}, ...languageProp }) }));
54
58
  }
55
59
  export default NitrogenPageClient;
@@ -25,6 +25,13 @@ export interface NitrogenWrapperProps {
25
25
  * Regular page content to render when not in Nitrogen mode.
26
26
  */
27
27
  children: React.ReactNode;
28
+ /**
29
+ * Language to render the page in (a configured locale code, e.g. 'es').
30
+ * Dynamic data is rebuilt from the doc fetched at that locale, and the
31
+ * language is forwarded to the renderer so translatable builder props
32
+ * resolve to it. Omit for the default language.
33
+ */
34
+ language?: string;
28
35
  }
29
36
  /**
30
37
  * Server component that wraps your page content and automatically handles
@@ -45,5 +52,5 @@ export interface NitrogenWrapperProps {
45
52
  * }
46
53
  * ```
47
54
  */
48
- export declare function NitrogenWrapper({ page, searchParams, collection, config, children, }: NitrogenWrapperProps): Promise<import("react/jsx-runtime").JSX.Element>;
55
+ export declare function NitrogenWrapper({ page, searchParams, collection, config, children, language, }: NitrogenWrapperProps): Promise<import("react/jsx-runtime").JSX.Element>;
49
56
  export default NitrogenWrapper;
@@ -2,6 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { getPayload } from 'payload';
3
3
  import { NitrogenPageClient } from './NitrogenPageClient.js';
4
4
  import { buildDynamicData, getNitrogenSettings } from '../endpoints/helpers.js';
5
+ import { resolveLocaleOptions } from '../localization.js';
5
6
  /**
6
7
  * Server component that wraps your page content and automatically handles
7
8
  * Nitrogen builder mode and Nitrogen-rendered pages.
@@ -21,7 +22,7 @@ import { buildDynamicData, getNitrogenSettings } from '../endpoints/helpers.js';
21
22
  * }
22
23
  * ```
23
24
  */
24
- export async function NitrogenWrapper({ page, searchParams, collection, config, children, }) {
25
+ export async function NitrogenWrapper({ page, searchParams, collection, config, children, language, }) {
25
26
  const isBuilder = searchParams['nitrogen-builder'] !== undefined;
26
27
  const hasNitrogenData = Array.isArray(page.nitrogenData) && page.nitrogenData.length > 0;
27
28
  // If not in builder mode and no Nitrogen data, render regular content
@@ -31,7 +32,28 @@ export async function NitrogenWrapper({ page, searchParams, collection, config,
31
32
  // Fetch Nitrogen settings and build dynamic data
32
33
  const payload = await getPayload({ config });
33
34
  const settings = await getNitrogenSettings(payload);
34
- const dynamicData = buildDynamicData(page, settings);
35
- return (_jsx(NitrogenPageClient, { pageId: String(page.id), pageData: page.nitrogenData || [], dynamicData: dynamicData, isBuilder: isBuilder, collection: collection }));
35
+ // When a language is requested (and Payload localization is configured),
36
+ // re-fetch the doc at that locale so dynamic data carries the translated
37
+ // field values. Falls back to the passed-in page on any failure.
38
+ let sourceDoc = page;
39
+ const localeOptions = resolveLocaleOptions(payload, language);
40
+ if (localeOptions.locale) {
41
+ try {
42
+ const localized = await payload.findByID({
43
+ collection: collection,
44
+ id: page.id,
45
+ depth: 1,
46
+ ...localeOptions,
47
+ });
48
+ if (localized) {
49
+ sourceDoc = localized;
50
+ }
51
+ }
52
+ catch {
53
+ // Locale fetch is best-effort — keep the caller's doc.
54
+ }
55
+ }
56
+ const dynamicData = buildDynamicData(sourceDoc, settings);
57
+ return (_jsx(NitrogenPageClient, { pageId: String(page.id), pageData: page.nitrogenData || [], dynamicData: dynamicData, isBuilder: isBuilder, collection: collection, language: language }));
36
58
  }
37
59
  export default NitrogenWrapper;
package/dist/index.d.ts CHANGED
@@ -34,6 +34,27 @@ export interface NitrogenConnectorPluginOptions {
34
34
  * `users`).
35
35
  */
36
36
  userCollection?: string;
37
+ /**
38
+ * Enables Payload-native localization for field data. When set (and the
39
+ * Payload config does not already configure `localization` itself), the
40
+ * plugin injects `localization: { locales, defaultLocale, fallback: true }`
41
+ * into the Payload config and marks eligible text fields (`title` on
42
+ * Nitrogen-enabled collections and on the plugin's own template collection)
43
+ * as `localized: true`. Localize your own custom fields yourself.
44
+ *
45
+ * IMPORTANT: the locales here must match the site languages configured in
46
+ * the Nitrogen settings (`nitrogenConfig.localization.languages`), and
47
+ * `defaultLocale` must match `defaultLanguage` — Payload stores the field
48
+ * translations, Nitrogen stores the builder-content translations, and the
49
+ * language codes are the join key.
50
+ *
51
+ * If your Payload config already has `localization` configured, that config
52
+ * is respected and this option does not override it.
53
+ */
54
+ localization?: {
55
+ locales: string[];
56
+ defaultLocale: string;
57
+ };
37
58
  }
38
59
  export declare const nitrogenConnectorPlugin: (options?: NitrogenConnectorPluginOptions) => any;
39
60
  export { NitrogenTemplates } from "./collections/NitrogenTemplates.js";
@@ -41,6 +62,7 @@ export { NitrogenSettings } from "./globals/NitrogenSettings.js";
41
62
  export { NitrogenEditButton } from "./components/NitrogenEditButton.js";
42
63
  export { NitrogenViewButton } from "./components/NitrogenViewButton.js";
43
64
  export { buildDynamicData, getNitrogenSettings } from "./endpoints/helpers.js";
65
+ export { buildDynamicDataMeta, getRequestLanguage, resolveLocaleOptions, } from "./localization.js";
44
66
  export { createCollectionEndpoints } from "./endpoints/collection-endpoints.js";
45
67
  export { nitrogen } from "@nitrogenbuilder/client-core";
46
68
  export type { ComponentSettings, ComponentSettingsToProps, } from "@nitrogenbuilder/types";
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import { createCollectionEndpoints } from "./endpoints/collection-endpoints.js";
13
13
  import { createComponentInventoryEndpoints } from './endpoints/component-inventory.js';
14
14
  import { batchEndpoints } from "./endpoints/batch.js";
15
15
  import { sitemapEndpoints } from "./endpoints/sitemap.js";
16
+ import { createTranslationStatusEndpoints } from "./endpoints/translation-status.js";
16
17
  import { createAgentAuthEndpoints } from "./endpoints/agent-auth.js";
17
18
  import { registerCollection } from "./collection-registry.js";
18
19
  import { deleteDocumentUsage, reindexDocumentUsage, syncComponentCatalog, } from './inventory/indexing.js';
@@ -38,12 +39,57 @@ const nitrogenRequiredFields = [
38
39
  },
39
40
  },
40
41
  },
42
+ {
43
+ name: "nitrogenTranslationStatus",
44
+ type: "json",
45
+ admin: {
46
+ hidden: true,
47
+ description: "Per-language translation coverage, computed by Nitrogen on save",
48
+ },
49
+ },
41
50
  ];
51
+ /**
52
+ * Marks `title` text fields as `localized: true` (recursing into rows, tabs,
53
+ * groups, collapsibles). Fields that already declare `localized` explicitly
54
+ * are left untouched.
55
+ */
56
+ function localizeTitleFields(fields) {
57
+ return fields.map((field) => {
58
+ const f = field;
59
+ if (f.type === "text" && f.name === "title" && f.localized === undefined) {
60
+ return { ...field, localized: true };
61
+ }
62
+ if (Array.isArray(f.fields)) {
63
+ return { ...field, fields: localizeTitleFields(f.fields) };
64
+ }
65
+ if (Array.isArray(f.tabs)) {
66
+ return {
67
+ ...field,
68
+ tabs: f.tabs.map((tab) => ({
69
+ ...tab,
70
+ fields: localizeTitleFields(tab.fields || []),
71
+ })),
72
+ };
73
+ }
74
+ return field;
75
+ });
76
+ }
42
77
  export const nitrogenConnectorPlugin = (options = {}) => (incomingConfig) => {
43
78
  if (options.disabled) {
44
79
  return incomingConfig;
45
80
  }
46
81
  const config = { ...incomingConfig };
82
+ // Payload-native localization for field data. Only injected when the host
83
+ // config doesn't configure localization itself — an existing config is
84
+ // always respected.
85
+ if (options.localization && !incomingConfig.localization) {
86
+ config.localization = {
87
+ locales: options.localization.locales,
88
+ defaultLocale: options.localization.defaultLocale,
89
+ fallback: true,
90
+ };
91
+ }
92
+ const localizationActive = Boolean(config.localization);
47
93
  // The auth collection that owns MCP agent credentials. Prefer the explicit
48
94
  // option, else the first auth-enabled collection, else `users`.
49
95
  const userCollectionSlug = options.userCollection ||
@@ -83,10 +129,19 @@ export const nitrogenConnectorPlugin = (options = {}) => (incomingConfig) => {
83
129
  },
84
130
  };
85
131
  };
86
- // Add Nitrogen templates collection
132
+ // Add Nitrogen templates collection. With localization active, template
133
+ // titles are localized (translated in the Payload admin's locale
134
+ // switcher); internal catalog/usage collections stay non-localized —
135
+ // their text fields are machine identifiers, not content.
136
+ const templatesCollection = localizationActive
137
+ ? {
138
+ ...NitrogenTemplates,
139
+ fields: localizeTitleFields(NitrogenTemplates.fields),
140
+ }
141
+ : NitrogenTemplates;
87
142
  config.collections = [
88
143
  ...(config.collections || []),
89
- withInventoryHooks(NitrogenTemplates, 'nitrogen-templates'),
144
+ withInventoryHooks(templatesCollection, 'nitrogen-templates'),
90
145
  NitrogenComponentCatalog,
91
146
  NitrogenComponentUsage,
92
147
  ];
@@ -103,6 +158,7 @@ export const nitrogenConnectorPlugin = (options = {}) => (incomingConfig) => {
103
158
  ...menuEndpoints,
104
159
  ...batchEndpoints,
105
160
  ...sitemapEndpoints,
161
+ ...createTranslationStatusEndpoints(options.collections || []),
106
162
  ...createAgentAuthEndpoints(userCollectionSlug),
107
163
  ...createComponentInventoryEndpoints({
108
164
  collections: inventoryCollections,
@@ -191,6 +247,16 @@ export const nitrogenConnectorPlugin = (options = {}) => (incomingConfig) => {
191
247
  fields: [...existing.fields, ...fieldsToAdd],
192
248
  };
193
249
  }
250
+ // With localization active, the collection's title becomes localized so
251
+ // it can be translated via the Payload admin's native locale switcher.
252
+ // (Users localize their own custom fields themselves.)
253
+ if (localizationActive) {
254
+ const current = config.collections[existingIndex];
255
+ config.collections[existingIndex] = {
256
+ ...current,
257
+ fields: localizeTitleFields(current.fields),
258
+ };
259
+ }
194
260
  // Inject NitrogenEditButton admin component
195
261
  const col = config.collections[existingIndex];
196
262
  const adminConfig = col.admin || {};
@@ -301,6 +367,7 @@ export { NitrogenEditButton } from "./components/NitrogenEditButton.js";
301
367
  export { NitrogenViewButton } from "./components/NitrogenViewButton.js";
302
368
  // Helper exports for consumers building custom frontend routes
303
369
  export { buildDynamicData, getNitrogenSettings } from "./endpoints/helpers.js";
370
+ export { buildDynamicDataMeta, getRequestLanguage, resolveLocaleOptions, } from "./localization.js";
304
371
  // Collection endpoint factory for consumers who need custom endpoint generation
305
372
  export { createCollectionEndpoints } from "./endpoints/collection-endpoints.js";
306
373
  // Re-export @nitrogenbuilder packages for convenience
@@ -6,12 +6,7 @@ export declare const NITROGEN_COMPONENT_CATALOG_COLLECTION = "nitrogen-component
6
6
  export declare const NITROGEN_COMPONENT_USAGE_COLLECTION = "nitrogen-component-usage";
7
7
  type ComponentUsageRecord = {
8
8
  componentName: string;
9
- depth: number;
10
9
  moduleId: string;
11
- modulePath: string;
12
- parentComponentName?: string;
13
- parentModuleId?: string;
14
- parentModulePath?: string;
15
10
  };
16
11
  type ReindexResult = {
17
12
  catalogCount: number;
@@ -21,6 +16,8 @@ type ReindexResult = {
21
16
  export declare function extractComponentUsageRecords(modules: unknown): ComponentUsageRecord[];
22
17
  export declare function findAllDocs<T extends object>(payload: Payload, collection: string, options?: {
23
18
  limit?: number;
19
+ /** e.g. 'all' to fetch every locale's value for localized fields */
20
+ locale?: string;
24
21
  select?: Record<string, true>;
25
22
  where?: Where;
26
23
  }, req?: PayloadRequest): Promise<T[]>;