nextjs-cms 0.8.9 → 0.9.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 (188) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +290 -290
  3. package/dist/api/index.d.ts +92 -9
  4. package/dist/api/index.d.ts.map +1 -1
  5. package/dist/api/lib/serverActions.d.ts +64 -9
  6. package/dist/api/lib/serverActions.d.ts.map +1 -1
  7. package/dist/api/lib/serverActions.js +463 -90
  8. package/dist/api/root.d.ts +184 -18
  9. package/dist/api/root.d.ts.map +1 -1
  10. package/dist/api/routers/accountSettings.d.ts +2 -2
  11. package/dist/api/routers/accountSettings.js +10 -10
  12. package/dist/api/routers/admins.js +11 -11
  13. package/dist/api/routers/auth.d.ts +1 -1
  14. package/dist/api/routers/config.d.ts +13 -0
  15. package/dist/api/routers/config.d.ts.map +1 -1
  16. package/dist/api/routers/config.js +4 -0
  17. package/dist/api/routers/cpanel.js +7 -7
  18. package/dist/api/routers/fields.d.ts +1 -0
  19. package/dist/api/routers/fields.d.ts.map +1 -1
  20. package/dist/api/routers/fields.js +39 -6
  21. package/dist/api/routers/gallery.js +1 -1
  22. package/dist/api/routers/hasItemsSection.d.ts +41 -2
  23. package/dist/api/routers/hasItemsSection.d.ts.map +1 -1
  24. package/dist/api/routers/hasItemsSection.js +43 -2
  25. package/dist/api/routers/logs.js +1 -1
  26. package/dist/api/routers/navigation.d.ts +3 -3
  27. package/dist/api/routers/simpleSection.d.ts +31 -1
  28. package/dist/api/routers/simpleSection.d.ts.map +1 -1
  29. package/dist/api/routers/simpleSection.js +44 -2
  30. package/dist/api/trpc.js +2 -2
  31. package/dist/auth/index.d.ts +1 -1
  32. package/dist/auth/index.d.ts.map +1 -1
  33. package/dist/auth/index.js +1 -1
  34. package/dist/auth/lib/actions.d.ts +3 -3
  35. package/dist/auth/lib/actions.d.ts.map +1 -1
  36. package/dist/auth/lib/actions.js +14 -14
  37. package/dist/auth/react.d.ts +2 -2
  38. package/dist/auth/react.d.ts.map +1 -1
  39. package/dist/auth/react.js +7 -7
  40. package/dist/cli/lib/db-config.js +10 -10
  41. package/dist/cli/lib/update-sections.d.ts.map +1 -1
  42. package/dist/cli/lib/update-sections.js +145 -9
  43. package/dist/cli/utils/schema-generator.d.ts +20 -0
  44. package/dist/cli/utils/schema-generator.d.ts.map +1 -1
  45. package/dist/cli/utils/schema-generator.js +40 -0
  46. package/dist/core/config/config-loader.d.ts +49 -5
  47. package/dist/core/config/config-loader.d.ts.map +1 -1
  48. package/dist/core/config/config-loader.js +100 -21
  49. package/dist/core/config/index.d.ts +2 -2
  50. package/dist/core/config/index.d.ts.map +1 -1
  51. package/dist/core/config/index.js +1 -1
  52. package/dist/core/db/table-checker/MysqlTable.js +8 -8
  53. package/dist/core/factories/FieldFactory.d.ts +5 -3
  54. package/dist/core/factories/FieldFactory.d.ts.map +1 -1
  55. package/dist/core/factories/FieldFactory.js +74 -16
  56. package/dist/core/factories/section-factory-with-esbuild.d.ts.map +1 -1
  57. package/dist/core/factories/section-factory-with-esbuild.js +15 -9
  58. package/dist/core/factories/section-factory-with-jiti.d.ts.map +1 -1
  59. package/dist/core/factories/section-factory-with-jiti.js +15 -9
  60. package/dist/core/fields/checkbox.d.ts +4 -1
  61. package/dist/core/fields/checkbox.d.ts.map +1 -1
  62. package/dist/core/fields/color.d.ts +4 -1
  63. package/dist/core/fields/color.d.ts.map +1 -1
  64. package/dist/core/fields/color.js +2 -2
  65. package/dist/core/fields/date.d.ts +4 -1
  66. package/dist/core/fields/date.d.ts.map +1 -1
  67. package/dist/core/fields/date.js +2 -2
  68. package/dist/core/fields/document.d.ts +4 -1
  69. package/dist/core/fields/document.d.ts.map +1 -1
  70. package/dist/core/fields/document.js +27 -18
  71. package/dist/core/fields/field-group.d.ts +3 -3
  72. package/dist/core/fields/field-group.d.ts.map +1 -1
  73. package/dist/core/fields/field.d.ts +11 -8
  74. package/dist/core/fields/field.d.ts.map +1 -1
  75. package/dist/core/fields/field.js +15 -11
  76. package/dist/core/fields/map.d.ts +4 -1
  77. package/dist/core/fields/map.d.ts.map +1 -1
  78. package/dist/core/fields/map.js +2 -2
  79. package/dist/core/fields/number.d.ts +26 -1
  80. package/dist/core/fields/number.d.ts.map +1 -1
  81. package/dist/core/fields/number.js +16 -7
  82. package/dist/core/fields/password.d.ts +4 -1
  83. package/dist/core/fields/password.d.ts.map +1 -1
  84. package/dist/core/fields/password.js +3 -3
  85. package/dist/core/fields/photo.d.ts +4 -1
  86. package/dist/core/fields/photo.d.ts.map +1 -1
  87. package/dist/core/fields/photo.js +17 -17
  88. package/dist/core/fields/richText.d.ts +17 -3
  89. package/dist/core/fields/richText.d.ts.map +1 -1
  90. package/dist/core/fields/richText.js +20 -8
  91. package/dist/core/fields/select.d.ts +10 -3
  92. package/dist/core/fields/select.d.ts.map +1 -1
  93. package/dist/core/fields/select.js +27 -34
  94. package/dist/core/fields/selectMultiple.d.ts +8 -4
  95. package/dist/core/fields/selectMultiple.d.ts.map +1 -1
  96. package/dist/core/fields/selectMultiple.js +32 -24
  97. package/dist/core/fields/slug.d.ts +16 -1
  98. package/dist/core/fields/slug.d.ts.map +1 -1
  99. package/dist/core/fields/slug.js +3 -3
  100. package/dist/core/fields/tags.d.ts +6 -3
  101. package/dist/core/fields/tags.d.ts.map +1 -1
  102. package/dist/core/fields/tags.js +26 -19
  103. package/dist/core/fields/text.d.ts +24 -1
  104. package/dist/core/fields/text.d.ts.map +1 -1
  105. package/dist/core/fields/text.js +12 -3
  106. package/dist/core/fields/textArea.d.ts +24 -1
  107. package/dist/core/fields/textArea.d.ts.map +1 -1
  108. package/dist/core/fields/textArea.js +9 -0
  109. package/dist/core/fields/video.d.ts +4 -1
  110. package/dist/core/fields/video.d.ts.map +1 -1
  111. package/dist/core/fields/video.js +14 -12
  112. package/dist/core/index.d.ts +1 -0
  113. package/dist/core/index.d.ts.map +1 -1
  114. package/dist/core/index.js +1 -0
  115. package/dist/core/localization/index.d.ts +3 -0
  116. package/dist/core/localization/index.d.ts.map +1 -0
  117. package/dist/core/localization/index.js +1 -0
  118. package/dist/core/localization/resolve-locale.d.ts +29 -0
  119. package/dist/core/localization/resolve-locale.d.ts.map +1 -0
  120. package/dist/core/localization/resolve-locale.js +43 -0
  121. package/dist/core/sections/category.d.ts +56 -44
  122. package/dist/core/sections/category.d.ts.map +1 -1
  123. package/dist/core/sections/category.js +3 -3
  124. package/dist/core/sections/hasItems.d.ts +80 -44
  125. package/dist/core/sections/hasItems.d.ts.map +1 -1
  126. package/dist/core/sections/section.d.ts +55 -28
  127. package/dist/core/sections/section.d.ts.map +1 -1
  128. package/dist/core/sections/section.js +22 -0
  129. package/dist/core/sections/simple.d.ts +8 -8
  130. package/dist/core/sections/simple.d.ts.map +1 -1
  131. package/dist/core/submit/ItemEditSubmit.d.ts +24 -16
  132. package/dist/core/submit/ItemEditSubmit.d.ts.map +1 -1
  133. package/dist/core/submit/ItemEditSubmit.js +62 -38
  134. package/dist/core/submit/LocaleSubmit.d.ts +97 -0
  135. package/dist/core/submit/LocaleSubmit.d.ts.map +1 -0
  136. package/dist/core/submit/LocaleSubmit.js +435 -0
  137. package/dist/core/submit/NewItemSubmit.d.ts +0 -8
  138. package/dist/core/submit/NewItemSubmit.d.ts.map +1 -1
  139. package/dist/core/submit/NewItemSubmit.js +6 -12
  140. package/dist/core/submit/index.d.ts +1 -0
  141. package/dist/core/submit/index.d.ts.map +1 -1
  142. package/dist/core/submit/index.js +1 -0
  143. package/dist/core/submit/submit.d.ts +35 -12
  144. package/dist/core/submit/submit.d.ts.map +1 -1
  145. package/dist/core/submit/submit.js +88 -69
  146. package/dist/db/schema.d.ts +17 -0
  147. package/dist/db/schema.d.ts.map +1 -1
  148. package/dist/db/schema.js +1 -0
  149. package/dist/logging/log.d.ts +1 -1
  150. package/dist/logging/log.d.ts.map +1 -1
  151. package/dist/plugins/index.d.ts +1 -1
  152. package/dist/plugins/index.d.ts.map +1 -1
  153. package/dist/plugins/loader.d.ts +3 -3
  154. package/dist/plugins/loader.d.ts.map +1 -1
  155. package/dist/translations/base/en.d.ts +24 -2
  156. package/dist/translations/base/en.d.ts.map +1 -1
  157. package/dist/translations/base/en.js +24 -2
  158. package/dist/translations/client.d.ts +292 -28
  159. package/dist/translations/client.d.ts.map +1 -1
  160. package/dist/translations/client.js +2 -2
  161. package/dist/translations/dict-store.d.ts +2 -2
  162. package/dist/translations/dict-store.d.ts.map +1 -1
  163. package/dist/translations/dict-store.js +9 -9
  164. package/dist/translations/index.d.ts +5 -5
  165. package/dist/translations/index.d.ts.map +1 -1
  166. package/dist/translations/index.js +6 -6
  167. package/dist/translations/language-cookie.d.ts +24 -0
  168. package/dist/translations/language-cookie.d.ts.map +1 -0
  169. package/dist/translations/language-cookie.js +44 -0
  170. package/dist/translations/language-utils.d.ts +42 -0
  171. package/dist/translations/language-utils.d.ts.map +1 -0
  172. package/dist/translations/language-utils.js +52 -0
  173. package/dist/translations/server.d.ts +293 -29
  174. package/dist/translations/server.d.ts.map +1 -1
  175. package/dist/translations/server.js +5 -5
  176. package/dist/validators/number.d.ts +1 -1
  177. package/dist/validators/number.d.ts.map +1 -1
  178. package/dist/validators/number.js +1 -1
  179. package/dist/validators/select-multiple.d.ts +2 -2
  180. package/dist/validators/select-multiple.d.ts.map +1 -1
  181. package/dist/validators/select-multiple.js +1 -1
  182. package/package.json +7 -3
  183. package/dist/translations/dictionaries/ar.d.ts +0 -433
  184. package/dist/translations/dictionaries/ar.d.ts.map +0 -1
  185. package/dist/translations/dictionaries/ar.js +0 -444
  186. package/dist/translations/dictionaries/en.d.ts +0 -433
  187. package/dist/translations/dictionaries/en.d.ts.map +0 -1
  188. package/dist/translations/dictionaries/en.js +0 -444
@@ -14,12 +14,13 @@ import sharp from 'sharp';
14
14
  import { readChunk } from 'read-chunk';
15
15
  import { fileTypeFromBuffer } from 'file-type';
16
16
  import { getCMSConfig } from '../../core/config/index.js';
17
+ import { resolveLocale } from '../../core/localization/index.js';
17
18
  import { getPluginRoutes } from '../../plugins/loader.js';
18
19
  import through2 from 'through2';
19
20
  import { recordLog } from '../../logging/index.js';
20
21
  import getString from '../../translations/index.js';
21
- import { resolveLocalizedString } from '../../translations/localization.js';
22
- import { resolveLocale } from '../../translations/locale-utils.js';
22
+ import { resolveMultilingualString } from '../../translations/language-utils.js';
23
+ import { resolveLanguage } from '../../translations/language-utils.js';
23
24
  export const isAccessAllowed = async ({ sectionName, role, userId, }) => {
24
25
  /**
25
26
  * Check admin privileges
@@ -57,14 +58,14 @@ export const getDocument = async (session, input) => {
57
58
  if (!section?.name) {
58
59
  throw new TRPCError({
59
60
  code: 'BAD_REQUEST',
60
- message: getString('invalidFilePath', session.user.locale),
61
+ message: getString('invalidFilePath', session.user.language),
61
62
  });
62
63
  }
63
64
  const fieldConfig = section.fields.find((field) => field.name === fieldName);
64
65
  if (!fieldConfig || typeof fieldConfig.build !== 'function') {
65
66
  throw new TRPCError({
66
67
  code: 'BAD_REQUEST',
67
- message: getString('invalidRequest', session.user.locale),
68
+ message: getString('invalidRequest', session.user.language),
68
69
  });
69
70
  }
70
71
  const field = fieldConfig.build();
@@ -74,7 +75,7 @@ export const getDocument = async (session, input) => {
74
75
  if (!field || !field.name || !field.extensions || field.extensions.length === 0) {
75
76
  throw new TRPCError({
76
77
  code: 'BAD_REQUEST',
77
- message: getString('invalidRequest', session.user.locale),
78
+ message: getString('invalidRequest', session.user.language),
78
79
  });
79
80
  }
80
81
  /**
@@ -90,7 +91,7 @@ export const getDocument = async (session, input) => {
90
91
  if (!fs.existsSync(pathToFile)) {
91
92
  throw new TRPCError({
92
93
  code: 'BAD_REQUEST',
93
- message: getString('fileNotFound', session.user.locale),
94
+ message: getString('fileNotFound', session.user.language),
94
95
  });
95
96
  }
96
97
  /**
@@ -107,7 +108,7 @@ export const getDocument = async (session, input) => {
107
108
  if (!fileType) {
108
109
  throw new TRPCError({
109
110
  code: 'BAD_REQUEST',
110
- message: getString('invalidFileType', session.user.locale),
111
+ message: getString('invalidFileType', session.user.language),
111
112
  });
112
113
  }
113
114
  /**
@@ -116,7 +117,7 @@ export const getDocument = async (session, input) => {
116
117
  if (!documentAllowedExtensions.includes(fileType.ext)) {
117
118
  throw new TRPCError({
118
119
  code: 'BAD_REQUEST',
119
- message: getString('invalidFileType', session.user.locale),
120
+ message: getString('invalidFileType', session.user.language),
120
121
  });
121
122
  }
122
123
  /**
@@ -252,8 +253,8 @@ export async function streamPhoto(input) {
252
253
  const nodeStream = processedImage.pipe(through2());
253
254
  return nodeStreamToWebStream(nodeStream, processedImage);
254
255
  }
255
- export const getVideo = async (input) => {
256
- throw new Error(getString('useVideoApiRoute', 'en'));
256
+ export const getVideo = async (session, input) => {
257
+ throw new Error(getString('useVideoApiRoute', session.user.language));
257
258
  };
258
259
  export const getAdminPrivileges = async (adminId) => {
259
260
  const rows = await db
@@ -266,17 +267,17 @@ export const getAllPrivileges = async (session) => {
266
267
  const [sections, pluginRoutes] = await Promise.all([SectionFactory.getSections(), getPluginRoutes()]);
267
268
  // Get the locale for resolving localized titles
268
269
  const cmsConfig = await getCMSConfig();
269
- const locale = resolveLocale(session.user.locale, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
270
+ const language = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
270
271
  // First, let's assign the static privileges
271
272
  const privilegesList = [];
272
273
  // Now, let's add the rest of the privileges to the list
273
274
  sections.forEach((section, index) => {
274
275
  let title;
275
276
  if (typeof section.title === 'string') {
276
- title = resolveLocalizedString(section.title, locale, cmsConfig.i18n.fallbackLanguage);
277
+ title = resolveMultilingualString(section.title, language, cmsConfig.i18n.fallbackLanguage);
277
278
  }
278
279
  else {
279
- title = resolveLocalizedString(section.title.section, locale, cmsConfig.i18n.fallbackLanguage);
280
+ title = resolveMultilingualString(section.title.section, language, cmsConfig.i18n.fallbackLanguage);
280
281
  }
281
282
  privilegesList.push({
282
283
  title,
@@ -290,7 +291,7 @@ export const getAllPrivileges = async (session) => {
290
291
  if (pluginPrivileges.has(route.pluginName))
291
292
  return;
292
293
  pluginPrivileges.set(route.pluginName, {
293
- title: resolveLocalizedString(route.title, locale, cmsConfig.i18n.fallbackLanguage),
294
+ title: resolveMultilingualString(route.title, language, cmsConfig.i18n.fallbackLanguage),
294
295
  });
295
296
  });
296
297
  Array.from(pluginPrivileges.entries()).forEach(([pluginName, meta], index) => {
@@ -301,7 +302,7 @@ export const getAllPrivileges = async (session) => {
301
302
  sectionType: 'plugin',
302
303
  });
303
304
  });
304
- privilegesList.push({ title: 'Admins', order: 0, sectionName: 'admins', sectionType: 'static' }, { title: 'Log', order: 2, sectionName: 'log', sectionType: 'static' });
305
+ privilegesList.push({ title: getString('admins', language), order: 0, sectionName: 'admins', sectionType: 'static' }, { title: getString('log', language), order: 2, sectionName: 'log', sectionType: 'static' });
305
306
  return privilegesList;
306
307
  };
307
308
  export const getAdminsList = async () => {
@@ -326,8 +327,30 @@ export const getAdminsList = async () => {
326
327
  });
327
328
  return adminsWithRoles;
328
329
  };
329
- export const createSimpleSectionPage = async (session, sectionName) => {
330
+ export const createSimpleSectionPage = async (session, sectionName, locale) => {
330
331
  try {
332
+ const cmsConfig = await getCMSConfig();
333
+ const localeResult = resolveLocale({
334
+ localization: cmsConfig.localization,
335
+ locale,
336
+ });
337
+ if (locale && !localeResult.resolvedLocale) {
338
+ if (localeResult.localizationEnabled === false) {
339
+ return {
340
+ error: {
341
+ message: getString('localizationNotEnabledForSection', session.user.language),
342
+ },
343
+ };
344
+ }
345
+ return {
346
+ error: {
347
+ message: getString('invalidLocale', session.user.language, {
348
+ locale,
349
+ locales: localeResult.availableLocales.map((l) => l.code).join(', '),
350
+ }),
351
+ },
352
+ };
353
+ }
331
354
  /**
332
355
  * Let's fetch the section information
333
356
  */
@@ -346,41 +369,90 @@ export const createSimpleSectionPage = async (session, sectionName) => {
346
369
  };
347
370
  }
348
371
  await fieldsFactory.generateFields();
349
- /**
350
- * Let's check for variants
351
- */
352
- /*const variants = section[0].variants
353
- const sectionVariants: { name: string }[] = []
354
-
355
- if (variants && variants.trim() !== '') {
356
- /!**
357
- * Convert to JSON
358
- *!/
359
- const variantsJson = JSON.parse(variants)
360
-
361
- /!**
362
- * Loop through the variants
363
- *!/
364
-
365
- variantsJson.forEach((variant: any) => {
366
- const variantName = variant.name
367
- const variantInfo = variant.info
368
- })
369
- }*/
370
372
  const sectionInfo = fieldsFactory.sectionInfo;
371
373
  const gallery = await sectionInfo.getGallery();
372
374
  // Get the locale for resolving localized titles
373
- const cmsConfig = await getCMSConfig();
374
- const locale = resolveLocale(session.user.locale, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
375
- const resolvedTitle = resolveLocalizedString(sectionInfo.title, locale, cmsConfig.i18n.fallbackLanguage);
375
+ const uiLanguage = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
376
+ const resolvedTitle = resolveMultilingualString(sectionInfo.title, uiLanguage, cmsConfig.i18n.fallbackLanguage);
377
+ /**
378
+ * Fetch localization metadata for sections with localized fields
379
+ */
380
+ let localizationData = null;
381
+ if (localeResult.localizationEnabled) {
382
+ let existingTranslations = [];
383
+ if (sectionInfo.hasLocalizedFields) {
384
+ const localesTableName = sectionInfo.localesTableName;
385
+ const localeColumns = await MysqlTableChecker.getColumns(localesTableName);
386
+ const localeTableExists = localeColumns.length > 0;
387
+ if (localeTableExists) {
388
+ // Simple sections always have itemId = 1
389
+ const sectionItemId = 1;
390
+ const [rows] = await db.execute(sql `SELECT locale FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId}`);
391
+ existingTranslations = rows.map((r) => r.locale);
392
+ // Override localized field values with locale-specific data
393
+ if (locale && localeResult.resolvedLocale && localeResult.isDefault === false) {
394
+ const [localeRows] = await db.execute(sql `SELECT * FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId} AND locale = ${localeResult.resolvedLocale.code} LIMIT 1`);
395
+ const localeRow = localeRows[0] ?? null;
396
+ for (const field of sectionInfo.fields) {
397
+ if (!field.localized)
398
+ continue;
399
+ // For select/tags with destinationDb, fetch locale-scoped junction values
400
+ const f = field;
401
+ if (f.destinationDb && (field.type === 'select' || field.type === 'select_multiple')) {
402
+ if (f.db) {
403
+ const [_rows] = await db.execute(sql `SELECT * FROM \`${sql.raw(f.destinationDb.table)}\` a JOIN \`${sql.raw(f.db.table)}\` b ON a.\`${sql.raw(f.destinationDb.selectIdentifier)}\` = b.\`${sql.raw(f.db.identifier)}\` WHERE a.\`${sql.raw(f.destinationDb.itemIdentifier)}\` = ${sectionItemId} AND a.\`locale\` = ${localeResult.resolvedLocale.code}`);
404
+ const values = Array.isArray(_rows)
405
+ ? _rows.map((row) => ({
406
+ value: row[f.destinationDb.selectIdentifier],
407
+ label: row[f.db.label],
408
+ }))
409
+ : [];
410
+ field.setValue(values);
411
+ }
412
+ else {
413
+ const [_rows] = await db.execute(sql `SELECT * FROM \`${sql.raw(f.destinationDb.table)}\` WHERE \`${sql.raw(f.destinationDb.itemIdentifier)}\` = ${sectionItemId} AND \`locale\` = ${localeResult.resolvedLocale.code}`);
414
+ const values = Array.isArray(_rows)
415
+ ? _rows.map((row) => ({
416
+ value: row[f.destinationDb.selectIdentifier],
417
+ label: f.options?.find((opt) => opt.value?.toString() ===
418
+ row[f.destinationDb.selectIdentifier]?.toString())?.label ?? '',
419
+ }))
420
+ : [];
421
+ field.setValue(values);
422
+ }
423
+ }
424
+ else if (f.destinationDb && field.type === 'tags') {
425
+ const [_rows] = await db.execute(sql `SELECT \`${sql.raw(f.destinationDb.selectIdentifier)}\` FROM \`${sql.raw(f.destinationDb.table)}\` WHERE \`${sql.raw(f.destinationDb.itemIdentifier)}\` = ${sectionItemId} AND \`locale\` = ${localeResult.resolvedLocale.code}`);
426
+ const tags = Array.isArray(_rows)
427
+ ? _rows.map((row) => row[f.destinationDb.selectIdentifier])
428
+ : [];
429
+ field.setValue(tags.join(','));
430
+ }
431
+ else {
432
+ // Simple field: override from locale row
433
+ field.setValue(localeRow ? (localeRow[field.name] ?? null) : null);
434
+ }
435
+ }
436
+ }
437
+ }
438
+ }
439
+ localizationData = {
440
+ defaultLocale: localeResult.defaultLocale,
441
+ currentLocale: localeResult.resolvedLocale ?? localeResult.defaultLocale,
442
+ existingTranslations,
443
+ locales: localeResult.availableLocales,
444
+ };
445
+ }
376
446
  return {
377
447
  section: {
378
448
  name: sectionInfo.name,
379
449
  title: resolvedTitle,
380
450
  gallery: gallery,
381
451
  variants: sectionInfo.variants,
452
+ configFile: sectionInfo.configFile,
382
453
  },
383
454
  inputGroups: await fieldsFactory.getGroupedFields(),
455
+ localization: localizationData,
384
456
  };
385
457
  }
386
458
  catch (err) {
@@ -410,7 +482,7 @@ export const deleteSectionItem = async (session, sectionName, sectionItemId, rec
410
482
  if (!section) {
411
483
  return {
412
484
  error: {
413
- message: getString('sectionNotFound', session.user.locale),
485
+ message: getString('sectionNotFound', session.user.language),
414
486
  },
415
487
  };
416
488
  }
@@ -420,7 +492,7 @@ export const deleteSectionItem = async (session, sectionName, sectionItemId, rec
420
492
  if (!columns.includes(identifierFieldName)) {
421
493
  return {
422
494
  error: {
423
- message: getString('sectionTableIdentifierNotFound', session.user.locale),
495
+ message: getString('sectionTableIdentifierNotFound', session.user.language),
424
496
  },
425
497
  };
426
498
  }
@@ -435,21 +507,38 @@ export const deleteSectionItem = async (session, sectionName, sectionItemId, rec
435
507
  * Run beforeDelete hook before the actual deletion
436
508
  */
437
509
  if (section.hooks?.beforeDelete) {
438
- await section.hooks.beforeDelete({
510
+ const hook = section.hooks.beforeDelete;
511
+ const handler = typeof hook === 'function' ? hook : hook.handler;
512
+ await handler({
439
513
  itemId: sectionItemId,
440
514
  values: sectionItemRow ?? {},
441
515
  section: section,
442
516
  });
443
517
  }
444
518
  /**
445
- * Delete the row from the table
519
+ * Cascade delete translations from the locales table.
520
+ * Use deleteLocaleTranslation per locale to ensure localized files are cleaned up from disk.
446
521
  */
447
- await db.execute(sql `delete from \`${sql.raw(tableName)}\` where \`${sql.raw(identifierFieldName)}\` = ${sectionItemId}`);
522
+ const cmsConfig = await getCMSConfig();
523
+ if (cmsConfig.localization?.enabled && section.hasLocalizedFields) {
524
+ const localesTableName = section.localesTableName;
525
+ const localeColumns = await MysqlTableChecker.getColumns(localesTableName);
526
+ if (localeColumns.length > 0) {
527
+ const [localeRows] = await db.execute(sql `SELECT locale FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId}`);
528
+ const locales = localeRows.map((r) => r.locale);
529
+ for (const locale of locales) {
530
+ const result = await deleteLocaleTranslation(session, sectionName, sectionItemId, locale);
531
+ if (result && typeof result === 'object' && 'error' in result) {
532
+ return result;
533
+ }
534
+ }
535
+ }
536
+ }
448
537
  /**
449
538
  * Grab the file fields to delete the files
450
539
  */
451
540
  const fileFieldConfigs = section.fieldConfigs.filter((fieldConfig) => fieldConfig.type === 'document' || fieldConfig.type === 'photo' || fieldConfig.type === 'video');
452
- const uploadsFolder = (await getCMSConfig()).media.upload.path;
541
+ const uploadsFolder = cmsConfig.media.upload.path;
453
542
  if (sectionItemRow) {
454
543
  for (const field of fileFieldConfigs) {
455
544
  // @ts-ignore
@@ -464,6 +553,15 @@ export const deleteSectionItem = async (session, sectionName, sectionItemId, rec
464
553
  console.error('Error deleting document', error);
465
554
  }
466
555
  }
556
+ else if (field.type === 'video') {
557
+ try {
558
+ await fs.promises.unlink(path.join(uploadsFolder, '.videos', section.name, value));
559
+ }
560
+ catch (error) {
561
+ // Log but continue - file may not exist or already deleted
562
+ console.error('Error deleting video', error);
563
+ }
564
+ }
467
565
  else {
468
566
  try {
469
567
  await fs.promises.unlink(path.join(uploadsFolder, '.photos', section.name, value));
@@ -554,12 +652,18 @@ export const deleteSectionItem = async (session, sectionName, sectionItemId, rec
554
652
  .delete(EditorPhotosTable)
555
653
  .where(and(eq(EditorPhotosTable.section, sectionName), eq(EditorPhotosTable.itemId, sectionItemId)));
556
654
  }
655
+ /**
656
+ * Delete the row from the table
657
+ */
658
+ await db.execute(sql `delete from \`${sql.raw(tableName)}\` where \`${sql.raw(identifierFieldName)}\` = ${sectionItemId}`);
557
659
  /**
558
660
  * Run afterDelete hook after the deletion
559
661
  */
560
662
  if (section.hooks?.afterDelete) {
663
+ const hook = section.hooks.afterDelete;
664
+ const handler = typeof hook === 'function' ? hook : hook.handler;
561
665
  try {
562
- await section.hooks.afterDelete({
666
+ await handler({
563
667
  itemId: sectionItemId,
564
668
  values: sectionItemRow ?? {},
565
669
  section: section,
@@ -600,7 +704,170 @@ export const deleteSectionItem = async (session, sectionName, sectionItemId, rec
600
704
  };
601
705
  }
602
706
  };
603
- export const createEditPage = async (session, sectionName, sectionItemId) => {
707
+ export const deleteLocaleTranslation = async (session, sectionName, sectionItemId, locale) => {
708
+ sectionItemId = sectionItemId.toString();
709
+ try {
710
+ const _s = (await SectionFactory.getSectionForAdmin({
711
+ name: sectionName,
712
+ admin: {
713
+ id: session.user.id,
714
+ requiredRole: 'D',
715
+ },
716
+ }));
717
+ const section = _s?.build();
718
+ if (!section) {
719
+ return {
720
+ error: {
721
+ message: getString('sectionNotFound', session.user.language),
722
+ },
723
+ };
724
+ }
725
+ const cmsConfig = await getCMSConfig();
726
+ if (!cmsConfig.localization?.enabled || !section.hasLocalizedFields) {
727
+ return {
728
+ error: {
729
+ message: getString('localizationNotEnabledForSection', session.user.language),
730
+ },
731
+ };
732
+ }
733
+ if (locale === cmsConfig.localization.defaultLocale) {
734
+ return {
735
+ error: {
736
+ message: getString('cannotDeleteBaseLocaleTranslation', session.user.language),
737
+ },
738
+ };
739
+ }
740
+ const localesTableName = section.localesTableName;
741
+ const localeColumns = await MysqlTableChecker.getColumns(localesTableName);
742
+ if (localeColumns.length === 0) {
743
+ return {
744
+ error: {
745
+ message: getString('localesTableDoesNotExist', session.user.language),
746
+ },
747
+ };
748
+ }
749
+ // Fetch the locale row before deleting so we can clean up files
750
+ const [localeRows] = await db.execute(sql `SELECT * FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId} AND locale = ${locale} LIMIT 1`);
751
+ const localeRow = localeRows[0] ?? null;
752
+ const beforeDeleteHook = section.hooks?.beforeDelete;
753
+ if (beforeDeleteHook && typeof beforeDeleteHook === 'object' && beforeDeleteHook.runForLocales) {
754
+ await beforeDeleteHook.handler({
755
+ itemId: sectionItemId,
756
+ values: localeRow ?? {},
757
+ section: section,
758
+ locale,
759
+ });
760
+ }
761
+ // Delete files for localized photo/document/video fields
762
+ if (localeRow) {
763
+ const uploadsFolder = cmsConfig.media.upload.path;
764
+ const localizedFileFields = section.fieldConfigs.filter((f) => f.localized === true && (f.type === 'photo' || f.type === 'document' || f.type === 'video'));
765
+ for (const field of localizedFileFields) {
766
+ const value = localeRow[field.name];
767
+ if (!value)
768
+ continue;
769
+ if (field.type === 'document') {
770
+ try {
771
+ await fs.promises.unlink(path.join(uploadsFolder, '.documents', section.name, value));
772
+ }
773
+ catch (error) {
774
+ console.error('Error deleting document', error);
775
+ }
776
+ }
777
+ else if (field.type === 'video') {
778
+ try {
779
+ await fs.promises.unlink(path.join(uploadsFolder, '.videos', section.name, value));
780
+ }
781
+ catch (error) {
782
+ console.error('Error deleting video', error);
783
+ }
784
+ }
785
+ else {
786
+ // photo
787
+ try {
788
+ await fs.promises.unlink(path.join(uploadsFolder, '.photos', section.name, value));
789
+ await fs.promises.unlink(path.join(uploadsFolder, '.thumbs', section.name, value));
790
+ }
791
+ catch (error) {
792
+ console.error('Error deleting photo', error);
793
+ }
794
+ }
795
+ }
796
+ }
797
+ // Delete locale-scoped editor photos for localized rich_text fields
798
+ const editorPhotos = await db
799
+ .select()
800
+ .from(EditorPhotosTable)
801
+ .where(and(eq(EditorPhotosTable.section, sectionName), eq(EditorPhotosTable.itemId, sectionItemId.toString()), eq(EditorPhotosTable.locale, locale)));
802
+ if (editorPhotos.length > 0) {
803
+ const uploadsFolder = cmsConfig.media.upload.path;
804
+ for (const photo of editorPhotos) {
805
+ try {
806
+ await fs.promises.unlink(path.join(uploadsFolder, '.photos', section.name, photo.name));
807
+ }
808
+ catch (error) {
809
+ console.error('Error deleting editor photo', error);
810
+ }
811
+ }
812
+ await db
813
+ .delete(EditorPhotosTable)
814
+ .where(and(eq(EditorPhotosTable.section, sectionName), eq(EditorPhotosTable.itemId, sectionItemId.toString()), eq(EditorPhotosTable.locale, locale)));
815
+ }
816
+ // Delete locale-scoped junction table rows for localized select/tags fields
817
+ const localizedJunctionFields = section.fieldConfigs.filter((f) => f.localized === true &&
818
+ f.destinationDb &&
819
+ (f.type === 'select' || f.type === 'select_multiple' || f.type === 'tags'));
820
+ for (const field of localizedJunctionFields) {
821
+ const destDb = field.destinationDb;
822
+ if (!destDb)
823
+ continue;
824
+ try {
825
+ await db.execute(sql `DELETE FROM \`${sql.raw(destDb.table)}\` WHERE \`${sql.raw(destDb.itemIdentifier)}\` = ${sectionItemId} AND \`locale\` = ${locale}`);
826
+ }
827
+ catch (error) {
828
+ console.error(`Error deleting junction table rows for ${field.name}`, error);
829
+ }
830
+ }
831
+ await db.execute(sql `DELETE FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId} AND locale = ${locale}`);
832
+ const afterDeleteHook = section.hooks?.afterDelete;
833
+ if (afterDeleteHook && typeof afterDeleteHook === 'object' && afterDeleteHook.runForLocales) {
834
+ try {
835
+ await afterDeleteHook.handler({
836
+ itemId: sectionItemId,
837
+ values: localeRow ?? {},
838
+ section: section,
839
+ locale,
840
+ });
841
+ }
842
+ catch (e) {
843
+ console.error('afterDelete hook failed:', e);
844
+ }
845
+ }
846
+ await recordLog({
847
+ eventType: 'section.item.locale.delete',
848
+ actorId: session.user.id,
849
+ actorUsername: session.user.name,
850
+ entityType: 'section_item_locale',
851
+ entityId: sectionItemId,
852
+ entityLabel: locale,
853
+ sectionName: section.name,
854
+ metadata: {
855
+ locale,
856
+ },
857
+ });
858
+ return true;
859
+ }
860
+ catch (err) {
861
+ const errorMessage = err instanceof Error ? err.message : getString('unknownErrorOccurred', session.user.language);
862
+ console.error('Error deleting locale translation', err);
863
+ return {
864
+ error: {
865
+ message: getString('deleteLocaleTranslationFailed', session.user.language, { detail: errorMessage }),
866
+ },
867
+ };
868
+ }
869
+ };
870
+ export const createEditPage = async (session, sectionName, sectionItemId, locale) => {
604
871
  /**
605
872
  * Generate the fields for the edit page
606
873
  */
@@ -611,6 +878,28 @@ export const createEditPage = async (session, sectionName, sectionItemId) => {
611
878
  itemId: sectionItemId,
612
879
  });
613
880
  try {
881
+ const cmsConfig = await getCMSConfig();
882
+ const localeResult = resolveLocale({
883
+ localization: cmsConfig.localization,
884
+ locale,
885
+ });
886
+ if (locale && !localeResult.resolvedLocale) {
887
+ if (localeResult.localizationEnabled === false) {
888
+ return {
889
+ error: {
890
+ message: getString('localizationNotEnabledForSection', session.user.language),
891
+ },
892
+ };
893
+ }
894
+ return {
895
+ error: {
896
+ message: getString('invalidLocale', session.user.language, {
897
+ locale,
898
+ locales: localeResult.availableLocales.map((l) => l.code).join(', '),
899
+ }),
900
+ },
901
+ };
902
+ }
614
903
  await fieldsFactory.initialize();
615
904
  await fieldsFactory.generateFields();
616
905
  if (fieldsFactory.error) {
@@ -665,23 +954,91 @@ export const createEditPage = async (session, sectionName, sectionItemId) => {
665
954
  }
666
955
  }
667
956
  const sectionInfo = fieldsFactory.sectionInfo;
668
- // Get the locale for resolving localized titles
669
- const cmsConfig = await getCMSConfig();
670
- const locale = resolveLocale(session.user.locale, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
957
+ // Resolve localized titles using the user's language
958
+ const uiLanguage = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
671
959
  const resolvedTitle = {
672
- section: resolveLocalizedString(sectionInfo.title.section, locale, cmsConfig.i18n.fallbackLanguage),
673
- singular: resolveLocalizedString(sectionInfo.title.singular, locale, cmsConfig.i18n.fallbackLanguage),
674
- plural: resolveLocalizedString(sectionInfo.title.plural, locale, cmsConfig.i18n.fallbackLanguage),
960
+ section: resolveMultilingualString(sectionInfo.title.section, uiLanguage, cmsConfig.i18n.fallbackLanguage),
961
+ singular: resolveMultilingualString(sectionInfo.title.singular, uiLanguage, cmsConfig.i18n.fallbackLanguage),
962
+ plural: resolveMultilingualString(sectionInfo.title.plural, uiLanguage, cmsConfig.i18n.fallbackLanguage),
675
963
  };
964
+ /**
965
+ * Fetch localization metadata for sections with localized fields
966
+ */
967
+ let localizationData = null;
968
+ if (localeResult.localizationEnabled) {
969
+ let existingTranslations = [];
970
+ if (sectionInfo.hasLocalizedFields) {
971
+ const localesTableName = sectionInfo.localesTableName;
972
+ const localeColumns = await MysqlTableChecker.getColumns(localesTableName);
973
+ const localeTableExists = localeColumns.length > 0;
974
+ if (localeTableExists) {
975
+ const [rows] = await db.execute(sql `SELECT locale FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId}`);
976
+ existingTranslations = rows.map((r) => r.locale);
977
+ // Override localized field values with locale-specific data
978
+ if (locale && localeResult.resolvedLocale && localeResult.isDefault === false) {
979
+ const [localeRows] = await db.execute(sql `SELECT * FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId} AND locale = ${localeResult.resolvedLocale.code} LIMIT 1`);
980
+ const localeRow = localeRows[0] ?? null;
981
+ for (const field of sectionInfo.fields) {
982
+ if (!field.localized)
983
+ continue;
984
+ // For select/tags with destinationDb, fetch locale-scoped junction values
985
+ const f = field;
986
+ if (f.destinationDb && (field.type === 'select' || field.type === 'select_multiple')) {
987
+ if (f.db) {
988
+ const [_rows] = await db.execute(sql `SELECT * FROM \`${sql.raw(f.destinationDb.table)}\` a JOIN \`${sql.raw(f.db.table)}\` b ON a.\`${sql.raw(f.destinationDb.selectIdentifier)}\` = b.\`${sql.raw(f.db.identifier)}\` WHERE a.\`${sql.raw(f.destinationDb.itemIdentifier)}\` = ${sectionItemId} AND a.\`locale\` = ${localeResult.resolvedLocale.code}`);
989
+ const values = Array.isArray(_rows)
990
+ ? _rows.map((row) => ({
991
+ value: row[f.destinationDb.selectIdentifier],
992
+ label: row[f.db.label],
993
+ }))
994
+ : [];
995
+ field.setValue(values);
996
+ }
997
+ else {
998
+ const [_rows] = await db.execute(sql `SELECT * FROM \`${sql.raw(f.destinationDb.table)}\` WHERE \`${sql.raw(f.destinationDb.itemIdentifier)}\` = ${sectionItemId} AND \`locale\` = ${localeResult.resolvedLocale.code}`);
999
+ const values = Array.isArray(_rows)
1000
+ ? _rows.map((row) => ({
1001
+ value: row[f.destinationDb.selectIdentifier],
1002
+ label: f.options?.find((opt) => opt.value?.toString() ===
1003
+ row[f.destinationDb.selectIdentifier]?.toString())?.label ?? '',
1004
+ }))
1005
+ : [];
1006
+ field.setValue(values);
1007
+ }
1008
+ }
1009
+ else if (f.destinationDb && field.type === 'tags') {
1010
+ const [_rows] = await db.execute(sql `SELECT \`${sql.raw(f.destinationDb.selectIdentifier)}\` FROM \`${sql.raw(f.destinationDb.table)}\` WHERE \`${sql.raw(f.destinationDb.itemIdentifier)}\` = ${sectionItemId} AND \`locale\` = ${localeResult.resolvedLocale.code}`);
1011
+ const tags = Array.isArray(_rows)
1012
+ ? _rows.map((row) => row[f.destinationDb.selectIdentifier])
1013
+ : [];
1014
+ field.setValue(tags.join(','));
1015
+ }
1016
+ else {
1017
+ // Simple field: override from locale row
1018
+ field.setValue(localeRow ? (localeRow[field.name] ?? null) : null);
1019
+ }
1020
+ }
1021
+ }
1022
+ }
1023
+ }
1024
+ localizationData = {
1025
+ defaultLocale: localeResult.defaultLocale,
1026
+ currentLocale: localeResult.resolvedLocale ?? localeResult.defaultLocale,
1027
+ existingTranslations,
1028
+ locales: localeResult.availableLocales,
1029
+ };
1030
+ }
676
1031
  return {
677
1032
  section: {
678
1033
  name: sectionInfo.name,
679
1034
  title: resolvedTitle,
680
1035
  gallery: gallery,
681
1036
  variants: sectionInfo.variants,
1037
+ configFile: sectionInfo.configFile,
682
1038
  },
683
1039
  inputGroups: await fieldsFactory.getGroupedFields(),
684
1040
  gallery: galleryItems,
1041
+ localization: localizationData,
685
1042
  };
686
1043
  }
687
1044
  catch (error) {
@@ -745,20 +1102,25 @@ export const createNewPage = async (session, sectionName) => {
745
1102
  const gallery = await sectionInfo.getGallery();
746
1103
  // Get the locale for resolving localized titles
747
1104
  const cmsConfig = await getCMSConfig();
748
- const locale = resolveLocale(session.user.locale, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
1105
+ const language = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
749
1106
  const resolvedTitle = {
750
- section: resolveLocalizedString(sectionInfo.title.section, locale, cmsConfig.i18n.fallbackLanguage),
751
- singular: resolveLocalizedString(sectionInfo.title.singular, locale, cmsConfig.i18n.fallbackLanguage),
752
- plural: resolveLocalizedString(sectionInfo.title.plural, locale, cmsConfig.i18n.fallbackLanguage),
1107
+ section: resolveMultilingualString(sectionInfo.title.section, language, cmsConfig.i18n.fallbackLanguage),
1108
+ singular: resolveMultilingualString(sectionInfo.title.singular, language, cmsConfig.i18n.fallbackLanguage),
1109
+ plural: resolveMultilingualString(sectionInfo.title.plural, language, cmsConfig.i18n.fallbackLanguage),
753
1110
  };
1111
+ const defaultLocale = cmsConfig.localization?.enabled
1112
+ ? (cmsConfig.localization.locales.find((l) => l.code === cmsConfig.localization.defaultLocale) ?? null)
1113
+ : null;
754
1114
  return {
755
1115
  section: {
756
1116
  name: sectionInfo.name,
757
1117
  title: resolvedTitle,
758
1118
  gallery: gallery,
759
1119
  variants: sectionInfo.variants,
1120
+ configFile: sectionInfo.configFile,
760
1121
  },
761
1122
  inputGroups: await fieldsFactory.getGroupedFields(),
1123
+ defaultLocale,
762
1124
  };
763
1125
  }
764
1126
  catch (err) {
@@ -787,7 +1149,7 @@ export const getCategorySectionChildren = async ({ session, id, sectionName, lev
787
1149
  if (!section) {
788
1150
  return {
789
1151
  error: {
790
- message: getString('sectionNotFound', session.user.locale),
1152
+ message: getString('sectionNotFound', session.user.language),
791
1153
  },
792
1154
  };
793
1155
  }
@@ -835,7 +1197,7 @@ export const getCategorySection = async (session, sectionName) => {
835
1197
  if (!section) {
836
1198
  return {
837
1199
  error: {
838
- message: getString('sectionNotFound', session.user.locale),
1200
+ message: getString('sectionNotFound', session.user.language),
839
1201
  },
840
1202
  };
841
1203
  }
@@ -862,11 +1224,11 @@ export const getCategorySection = async (session, sectionName) => {
862
1224
  s.setValue(undefined);
863
1225
  // Get the locale for resolving localized titles (label must be a string for React)
864
1226
  const cmsConfig = await getCMSConfig();
865
- const locale = resolveLocale(session.user.locale, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
1227
+ const language = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
866
1228
  const resolvedTitle = {
867
- section: resolveLocalizedString(section.title.section, locale, cmsConfig.i18n.fallbackLanguage),
868
- singular: resolveLocalizedString(section.title.singular, locale, cmsConfig.i18n.fallbackLanguage),
869
- plural: resolveLocalizedString(section.title.plural, locale, cmsConfig.i18n.fallbackLanguage),
1229
+ section: resolveMultilingualString(section.title.section, language, cmsConfig.i18n.fallbackLanguage),
1230
+ singular: resolveMultilingualString(section.title.singular, language, cmsConfig.i18n.fallbackLanguage),
1231
+ plural: resolveMultilingualString(section.title.plural, language, cmsConfig.i18n.fallbackLanguage),
870
1232
  };
871
1233
  const categorySectionSelect = {
872
1234
  options: s.options,
@@ -902,7 +1264,7 @@ export const getBrowsePage = async (session, sectionName, page = 1, q) => {
902
1264
  if (!section) {
903
1265
  return {
904
1266
  error: {
905
- message: getString('sectionNotFound', session.user.locale),
1267
+ message: getString('sectionNotFound', session.user.language),
906
1268
  },
907
1269
  };
908
1270
  }
@@ -913,26 +1275,38 @@ export const getBrowsePage = async (session, sectionName, page = 1, q) => {
913
1275
  const sqlChunks = [];
914
1276
  const totalRowsSqlChunks = [];
915
1277
  const sectionSearch = section.search?.searchFields ? section.search?.searchFields.length > 0 : false;
916
- sqlChunks.push(sql `select * from ${sql.raw(tableName)}`);
917
- totalRowsSqlChunks.push(sql `select count(*) as total from ${sql.raw(tableName)}`);
1278
+ // Check if we need to JOIN _locales for search
1279
+ const cmsConfig = await getCMSConfig();
1280
+ const hasLocalization = !!cmsConfig.localization?.enabled;
1281
+ const hasLocalizedSearchFields = hasLocalization && q && q.trim().length > 0 && section.search?.searchFields?.some((f) => f.localized);
1282
+ const localesTable = hasLocalizedSearchFields ? section.localesTableName : null;
1283
+ if (localesTable) {
1284
+ // Use DISTINCT to avoid duplicate rows from the LEFT JOIN
1285
+ sqlChunks.push(sql `select distinct ${sql.raw(`\`${tableName}\``)}.* from ${sql.raw(`\`${tableName}\``)} left join ${sql.raw(`\`${localesTable}\``)} on ${sql.raw(`\`${localesTable}\``)}.parent_id = ${sql.raw(`\`${tableName}\``)}.${sql.raw(`\`${section.db.identifier.name}\``)}`);
1286
+ totalRowsSqlChunks.push(sql `select count(distinct ${sql.raw(`\`${tableName}\``)}.${sql.raw(`\`${section.db.identifier.name}\``)} ) as total from ${sql.raw(`\`${tableName}\``)} left join ${sql.raw(`\`${localesTable}\``)} on ${sql.raw(`\`${localesTable}\``)}.parent_id = ${sql.raw(`\`${tableName}\``)}.${sql.raw(`\`${section.db.identifier.name}\``)}`);
1287
+ }
1288
+ else {
1289
+ sqlChunks.push(sql `select * from ${sql.raw(`\`${tableName}\``)}`);
1290
+ totalRowsSqlChunks.push(sql `select count(*) as total from ${sql.raw(`\`${tableName}\``)}`);
1291
+ }
918
1292
  if (q && q.trim().length > 0) {
919
1293
  if (section.search?.searchFields.length) {
920
- let i = 1;
1294
+ const whereParts = [];
1295
+ for (const field of section.search.searchFields) {
1296
+ // Search in main table
1297
+ whereParts.push(sql `${sql.raw(`\`${tableName}\``)}.${sql.raw(`\`${field.name}\``)} LIKE CONCAT('%',${sql `${q.trim()}`},'%')`);
1298
+ // Also search in _locales table for localized fields
1299
+ if (localesTable && field.localized) {
1300
+ whereParts.push(sql `${sql.raw(`\`${localesTable}\``)}.${sql.raw(`\`${field.name}\``)} LIKE CONCAT('%',${sql `${q.trim()}`},'%')`);
1301
+ }
1302
+ }
921
1303
  sqlChunks.push(sql `where`);
1304
+ sqlChunks.push(sql.join(whereParts, sql ` or `));
922
1305
  totalRowsSqlChunks.push(sql `where`);
923
- for (const field of section.search?.searchFields) {
924
- // sqlChunks.push(sql`${sql.raw(field.name)} like '%${q}%'`)
925
- sqlChunks.push(sql `${sql.raw(field.name)} LIKE CONCAT('%',${sql `${q.trim()}`},'%')`);
926
- totalRowsSqlChunks.push(sql `${sql.raw(field.name)} LIKE CONCAT('%',${sql `${q.trim()}`},'%')`);
927
- if (i === section.search?.searchFields.length)
928
- continue;
929
- sqlChunks.push(sql `or`);
930
- totalRowsSqlChunks.push(sql `or`);
931
- i++;
932
- }
1306
+ totalRowsSqlChunks.push(sql.join(whereParts, sql ` or `));
933
1307
  }
934
1308
  }
935
- sqlChunks.push(sql `ORDER BY \`${sql.raw(orderByFieldName)}\` DESC LIMIT ${limit} OFFSET ${offset}`);
1309
+ sqlChunks.push(sql `ORDER BY ${sql.raw(`\`${tableName}\``)}.${sql.raw(`\`${orderByFieldName}\``)} DESC LIMIT ${limit} OFFSET ${offset}`);
936
1310
  const finalSql = sql.join(sqlChunks, sql.raw(' '));
937
1311
  const totalRowsSql = sql.join(totalRowsSqlChunks, sql.raw(' '));
938
1312
  // Now, let's get the section items from the table
@@ -940,10 +1314,9 @@ export const getBrowsePage = async (session, sectionName, page = 1, q) => {
940
1314
  const totalCountResult = await db.execute(totalRowsSql);
941
1315
  const totalCountRows = totalCountResult[0];
942
1316
  const rows = sectionItems[0];
943
- // Get the locale for resolving localized titles
944
- const cmsConfig = await getCMSConfig();
945
- const locale = resolveLocale(session.user.locale, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
946
- const resolvedTitle = resolveLocalizedString(section.title.section, locale, cmsConfig.i18n.fallbackLanguage);
1317
+ // Resolve localized section title for the browse page header
1318
+ const language = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
1319
+ const resolvedTitle = resolveMultilingualString(section.title.section, language, cmsConfig.i18n.fallbackLanguage);
947
1320
  return {
948
1321
  section: {
949
1322
  // tableName: tableName,
@@ -1001,25 +1374,25 @@ export const getSidebar = async (session) => {
1001
1374
  // Include dashboard override plugin even without explicit privilege
1002
1375
  const allowedPluginRoutes = pluginRoutes.filter(({ pluginName, path }) => privilegeSet.has(pluginName) || path === dashboardOverridePath);
1003
1376
  // Get the locale for resolving localized titles
1004
- const locale = resolveLocale(session.user.locale, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
1377
+ const language = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
1005
1378
  // Let's loop through the sections and add path, icon and title to each section item
1006
1379
  const simpleSectionItems = simple.map((section) => {
1007
1380
  return {
1008
- title: resolveLocalizedString(section.title, locale, cmsConfig.i18n.fallbackLanguage),
1381
+ title: resolveMultilingualString(section.title, language, cmsConfig.i18n.fallbackLanguage),
1009
1382
  path: `/section/${section.name}`,
1010
1383
  icon: section.icon,
1011
1384
  };
1012
1385
  });
1013
1386
  const hasItemsSectionItems = has_items.map((section) => {
1014
1387
  return {
1015
- title: resolveLocalizedString(section.title.section, locale, cmsConfig.i18n.fallbackLanguage),
1388
+ title: resolveMultilingualString(section.title.section, language, cmsConfig.i18n.fallbackLanguage),
1016
1389
  path: `/${section.name}`,
1017
1390
  icon: section.icon,
1018
1391
  };
1019
1392
  });
1020
1393
  const categorySectionsItems = category.map((section) => {
1021
1394
  return {
1022
- title: resolveLocalizedString(section.title.section, locale, cmsConfig.i18n.fallbackLanguage),
1395
+ title: resolveMultilingualString(section.title.section, language, cmsConfig.i18n.fallbackLanguage),
1023
1396
  path: `/categorized/${section.name}`,
1024
1397
  icon: section.icon,
1025
1398
  };
@@ -1028,7 +1401,7 @@ export const getSidebar = async (session) => {
1028
1401
  const pluginSections = allowedPluginRoutes
1029
1402
  .filter((route) => route.path !== dashboardOverridePath)
1030
1403
  .map((route) => ({
1031
- title: resolveLocalizedString(route.title, locale, cmsConfig.i18n.fallbackLanguage),
1404
+ title: resolveMultilingualString(route.title, language, cmsConfig.i18n.fallbackLanguage),
1032
1405
  path: route.path,
1033
1406
  icon: route.icon ?? '',
1034
1407
  }));
@@ -1037,7 +1410,7 @@ export const getSidebar = async (session) => {
1037
1410
  * Add the dashboard section (points to override if configured)
1038
1411
  */
1039
1412
  {
1040
- title: getString('dashboard', session.user.locale),
1413
+ title: getString('dashboard', session.user.language),
1041
1414
  path: dashboardOverridePath ?? '/dashboard',
1042
1415
  icon: 'home',
1043
1416
  },
@@ -1046,7 +1419,7 @@ export const getSidebar = async (session) => {
1046
1419
  */
1047
1420
  ...pluginSections,
1048
1421
  ...fixed.map((section) => ({
1049
- title: getString(section.name, session.user.locale),
1422
+ title: getString(section.name, session.user.language),
1050
1423
  path: `/${section.name}`,
1051
1424
  icon: section.icon,
1052
1425
  })),