convex-cms 0.0.2 → 0.0.3

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 (265) hide show
  1. package/admin-dist/nitro.json +15 -0
  2. package/admin-dist/public/assets/CmsEmptyState-CRswfTzk.js +5 -0
  3. package/admin-dist/public/assets/CmsPageHeader-CirpXndm.js +1 -0
  4. package/admin-dist/public/assets/CmsStatusBadge-CbEUpQu-.js +1 -0
  5. package/admin-dist/public/assets/CmsToolbar-BI2nZOXp.js +1 -0
  6. package/admin-dist/public/assets/ContentEntryEditor-CBeCyK_m.js +4 -0
  7. package/admin-dist/public/assets/ErrorState-BIVaWmom.js +1 -0
  8. package/admin-dist/public/assets/TaxonomyFilter-ChaY6Y_x.js +1 -0
  9. package/admin-dist/public/assets/_contentTypeId-DQ8k_Rvw.js +1 -0
  10. package/admin-dist/public/assets/_entryId-CKU_glsK.js +1 -0
  11. package/admin-dist/public/assets/alert-BXjTqrwQ.js +1 -0
  12. package/admin-dist/public/assets/badge-hvUOzpVZ.js +1 -0
  13. package/admin-dist/public/assets/circle-check-big-CF_pR17r.js +1 -0
  14. package/admin-dist/public/assets/command-DU82cJlt.js +1 -0
  15. package/admin-dist/public/assets/content-_LXl3pp7.js +1 -0
  16. package/admin-dist/public/assets/content-types-KjxaXGxY.js +2 -0
  17. package/admin-dist/public/assets/globals-CS6BZ0zp.css +1 -0
  18. package/admin-dist/public/assets/index-DNGIZHL-.js +1 -0
  19. package/admin-dist/public/assets/label-KNtpL71g.js +1 -0
  20. package/admin-dist/public/assets/link-2-Bw2aI4V4.js +1 -0
  21. package/admin-dist/public/assets/list-sYepHjt_.js +1 -0
  22. package/admin-dist/public/assets/main-CKj5yfEi.js +97 -0
  23. package/admin-dist/public/assets/media-Bkrkffm7.js +1 -0
  24. package/admin-dist/public/assets/new._contentTypeId-C3LstjNs.js +1 -0
  25. package/admin-dist/public/assets/plus-DUn8v_Xf.js +1 -0
  26. package/admin-dist/public/assets/rotate-ccw-DJEoHcRI.js +1 -0
  27. package/admin-dist/public/assets/scroll-area-DfIlT0in.js +1 -0
  28. package/admin-dist/public/assets/search-MuAUDJKR.js +1 -0
  29. package/admin-dist/public/assets/select-BD29IXCI.js +1 -0
  30. package/admin-dist/public/assets/settings-DmMyn_6A.js +1 -0
  31. package/admin-dist/public/assets/switch-h3Rrnl5i.js +1 -0
  32. package/admin-dist/public/assets/tabs-imc8h-Dp.js +1 -0
  33. package/admin-dist/public/assets/taxonomies-dAsrT65H.js +1 -0
  34. package/admin-dist/public/assets/textarea-BTy7nwzR.js +1 -0
  35. package/admin-dist/public/assets/trash-SAWKZZHv.js +1 -0
  36. package/admin-dist/public/assets/triangle-alert-E52Vfeuh.js +1 -0
  37. package/admin-dist/public/assets/useBreadcrumbLabel-BECBMCzM.js +1 -0
  38. package/admin-dist/public/assets/usePermissions-Basjs9BT.js +1 -0
  39. package/admin-dist/public/favicon.ico +0 -0
  40. package/admin-dist/server/_chunks/_libs/@date-fns/tz.mjs +217 -0
  41. package/admin-dist/server/_chunks/_libs/@floating-ui/core.mjs +719 -0
  42. package/admin-dist/server/_chunks/_libs/@floating-ui/dom.mjs +622 -0
  43. package/admin-dist/server/_chunks/_libs/@floating-ui/react-dom.mjs +292 -0
  44. package/admin-dist/server/_chunks/_libs/@floating-ui/utils.mjs +320 -0
  45. package/admin-dist/server/_chunks/_libs/@radix-ui/number.mjs +6 -0
  46. package/admin-dist/server/_chunks/_libs/@radix-ui/primitive.mjs +11 -0
  47. package/admin-dist/server/_chunks/_libs/@radix-ui/react-arrow.mjs +23 -0
  48. package/admin-dist/server/_chunks/_libs/@radix-ui/react-avatar.mjs +119 -0
  49. package/admin-dist/server/_chunks/_libs/@radix-ui/react-checkbox.mjs +270 -0
  50. package/admin-dist/server/_chunks/_libs/@radix-ui/react-collection.mjs +69 -0
  51. package/admin-dist/server/_chunks/_libs/@radix-ui/react-compose-refs.mjs +39 -0
  52. package/admin-dist/server/_chunks/_libs/@radix-ui/react-context.mjs +137 -0
  53. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dialog.mjs +325 -0
  54. package/admin-dist/server/_chunks/_libs/@radix-ui/react-direction.mjs +9 -0
  55. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dismissable-layer.mjs +210 -0
  56. package/admin-dist/server/_chunks/_libs/@radix-ui/react-dropdown-menu.mjs +253 -0
  57. package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-guards.mjs +29 -0
  58. package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-scope.mjs +206 -0
  59. package/admin-dist/server/_chunks/_libs/@radix-ui/react-id.mjs +14 -0
  60. package/admin-dist/server/_chunks/_libs/@radix-ui/react-label.mjs +23 -0
  61. package/admin-dist/server/_chunks/_libs/@radix-ui/react-menu.mjs +812 -0
  62. package/admin-dist/server/_chunks/_libs/@radix-ui/react-popover.mjs +300 -0
  63. package/admin-dist/server/_chunks/_libs/@radix-ui/react-popper.mjs +286 -0
  64. package/admin-dist/server/_chunks/_libs/@radix-ui/react-portal.mjs +16 -0
  65. package/admin-dist/server/_chunks/_libs/@radix-ui/react-presence.mjs +128 -0
  66. package/admin-dist/server/_chunks/_libs/@radix-ui/react-primitive.mjs +141 -0
  67. package/admin-dist/server/_chunks/_libs/@radix-ui/react-roving-focus.mjs +224 -0
  68. package/admin-dist/server/_chunks/_libs/@radix-ui/react-scroll-area.mjs +721 -0
  69. package/admin-dist/server/_chunks/_libs/@radix-ui/react-select.mjs +1163 -0
  70. package/admin-dist/server/_chunks/_libs/@radix-ui/react-separator.mjs +28 -0
  71. package/admin-dist/server/_chunks/_libs/@radix-ui/react-slot.mjs +601 -0
  72. package/admin-dist/server/_chunks/_libs/@radix-ui/react-switch.mjs +152 -0
  73. package/admin-dist/server/_chunks/_libs/@radix-ui/react-tabs.mjs +189 -0
  74. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-callback-ref.mjs +11 -0
  75. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-controllable-state.mjs +69 -0
  76. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-effect-event.mjs +1 -0
  77. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-escape-keydown.mjs +17 -0
  78. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-is-hydrated.mjs +15 -0
  79. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-layout-effect.mjs +6 -0
  80. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-previous.mjs +14 -0
  81. package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-size.mjs +39 -0
  82. package/admin-dist/server/_chunks/_libs/@radix-ui/react-visually-hidden.mjs +33 -0
  83. package/admin-dist/server/_chunks/_libs/@tanstack/history.mjs +409 -0
  84. package/admin-dist/server/_chunks/_libs/@tanstack/react-router.mjs +1711 -0
  85. package/admin-dist/server/_chunks/_libs/@tanstack/react-store.mjs +56 -0
  86. package/admin-dist/server/_chunks/_libs/@tanstack/router-core.mjs +4829 -0
  87. package/admin-dist/server/_chunks/_libs/@tanstack/store.mjs +134 -0
  88. package/admin-dist/server/_chunks/_libs/react-dom.mjs +10781 -0
  89. package/admin-dist/server/_chunks/_libs/react.mjs +513 -0
  90. package/admin-dist/server/_libs/aria-hidden.mjs +122 -0
  91. package/admin-dist/server/_libs/class-variance-authority.mjs +44 -0
  92. package/admin-dist/server/_libs/clsx.mjs +16 -0
  93. package/admin-dist/server/_libs/cmdk.mjs +315 -0
  94. package/admin-dist/server/_libs/convex.mjs +4841 -0
  95. package/admin-dist/server/_libs/cookie-es.mjs +58 -0
  96. package/admin-dist/server/_libs/croner.mjs +1 -0
  97. package/admin-dist/server/_libs/crossws.mjs +1 -0
  98. package/admin-dist/server/_libs/date-fns.mjs +1716 -0
  99. package/admin-dist/server/_libs/detect-node-es.mjs +1 -0
  100. package/admin-dist/server/_libs/get-nonce.mjs +9 -0
  101. package/admin-dist/server/_libs/h3-v2.mjs +277 -0
  102. package/admin-dist/server/_libs/h3.mjs +401 -0
  103. package/admin-dist/server/_libs/hookable.mjs +1 -0
  104. package/admin-dist/server/_libs/isbot.mjs +20 -0
  105. package/admin-dist/server/_libs/lucide-react.mjs +850 -0
  106. package/admin-dist/server/_libs/ohash.mjs +1 -0
  107. package/admin-dist/server/_libs/react-day-picker.mjs +2201 -0
  108. package/admin-dist/server/_libs/react-remove-scroll-bar.mjs +82 -0
  109. package/admin-dist/server/_libs/react-remove-scroll.mjs +328 -0
  110. package/admin-dist/server/_libs/react-style-singleton.mjs +69 -0
  111. package/admin-dist/server/_libs/rou3.mjs +8 -0
  112. package/admin-dist/server/_libs/seroval-plugins.mjs +58 -0
  113. package/admin-dist/server/_libs/seroval.mjs +1765 -0
  114. package/admin-dist/server/_libs/srvx.mjs +719 -0
  115. package/admin-dist/server/_libs/tailwind-merge.mjs +3010 -0
  116. package/admin-dist/server/_libs/tiny-invariant.mjs +12 -0
  117. package/admin-dist/server/_libs/tiny-warning.mjs +5 -0
  118. package/admin-dist/server/_libs/tslib.mjs +39 -0
  119. package/admin-dist/server/_libs/ufo.mjs +54 -0
  120. package/admin-dist/server/_libs/unctx.mjs +1 -0
  121. package/admin-dist/server/_libs/unstorage.mjs +1 -0
  122. package/admin-dist/server/_libs/use-callback-ref.mjs +66 -0
  123. package/admin-dist/server/_libs/use-sidecar.mjs +106 -0
  124. package/admin-dist/server/_libs/use-sync-external-store.mjs +139 -0
  125. package/admin-dist/server/_libs/zod.mjs +4223 -0
  126. package/admin-dist/server/_ssr/CmsEmptyState-DU7-7-mV.mjs +290 -0
  127. package/admin-dist/server/_ssr/CmsPageHeader-CseW0AHm.mjs +24 -0
  128. package/admin-dist/server/_ssr/CmsStatusBadge-B_pi4KCp.mjs +127 -0
  129. package/admin-dist/server/_ssr/CmsToolbar-X75ex6ek.mjs +49 -0
  130. package/admin-dist/server/_ssr/ContentEntryEditor-CepusRsA.mjs +3720 -0
  131. package/admin-dist/server/_ssr/ErrorState-cI-bKLez.mjs +89 -0
  132. package/admin-dist/server/_ssr/TaxonomyFilter-Bwrq0-cz.mjs +188 -0
  133. package/admin-dist/server/_ssr/_contentTypeId-BqYKEcLr.mjs +379 -0
  134. package/admin-dist/server/_ssr/_entryId-CRfnqeDf.mjs +161 -0
  135. package/admin-dist/server/_ssr/_tanstack-start-manifest_v-BwDlABVk.mjs +4 -0
  136. package/admin-dist/server/_ssr/alert-CVt45UUP.mjs +92 -0
  137. package/admin-dist/server/_ssr/badge-6BsP37vG.mjs +125 -0
  138. package/admin-dist/server/_ssr/command-fy8epIKf.mjs +128 -0
  139. package/admin-dist/server/_ssr/config.server-D7JHDcDv.mjs +117 -0
  140. package/admin-dist/server/_ssr/content-B5RhL7uW.mjs +532 -0
  141. package/admin-dist/server/_ssr/content-types-BIOqCQYN.mjs +1166 -0
  142. package/admin-dist/server/_ssr/index-DHSHDPt1.mjs +193 -0
  143. package/admin-dist/server/_ssr/index.mjs +1275 -0
  144. package/admin-dist/server/_ssr/label-C8Dko1j7.mjs +22 -0
  145. package/admin-dist/server/_ssr/media-CSx3XttC.mjs +1832 -0
  146. package/admin-dist/server/_ssr/new._contentTypeId-DzanEZQM.mjs +144 -0
  147. package/admin-dist/server/_ssr/router-DDWcF-kt.mjs +1556 -0
  148. package/admin-dist/server/_ssr/scroll-area-bjPYwhXN.mjs +59 -0
  149. package/admin-dist/server/_ssr/select-BUhDDf4T.mjs +142 -0
  150. package/admin-dist/server/_ssr/settings-DAsxnw2q.mjs +348 -0
  151. package/admin-dist/server/_ssr/start-HYkvq4Ni.mjs +4 -0
  152. package/admin-dist/server/_ssr/switch-BgyRtQ1Z.mjs +31 -0
  153. package/admin-dist/server/_ssr/tabs-DzMdRB1A.mjs +628 -0
  154. package/admin-dist/server/_ssr/taxonomies-C8j8g5Q5.mjs +915 -0
  155. package/admin-dist/server/_ssr/textarea-9jNeYJSc.mjs +18 -0
  156. package/admin-dist/server/_ssr/trash-DYMxwhZB.mjs +291 -0
  157. package/admin-dist/server/_ssr/useBreadcrumbLabel-FNSAr2Ha.mjs +16 -0
  158. package/admin-dist/server/_ssr/usePermissions-BJGGahrJ.mjs +68 -0
  159. package/admin-dist/server/favicon.ico +0 -0
  160. package/admin-dist/server/index.mjs +627 -0
  161. package/dist/cli/index.js +0 -0
  162. package/dist/client/admin-config.d.ts +0 -1
  163. package/dist/client/admin-config.d.ts.map +1 -1
  164. package/dist/client/admin-config.js +0 -1
  165. package/dist/client/admin-config.js.map +1 -1
  166. package/dist/client/adminApi.d.ts.map +1 -1
  167. package/dist/client/agentTools.d.ts +1237 -135
  168. package/dist/client/agentTools.d.ts.map +1 -1
  169. package/dist/client/agentTools.js +33 -9
  170. package/dist/client/agentTools.js.map +1 -1
  171. package/dist/client/index.d.ts +1 -1
  172. package/dist/client/index.d.ts.map +1 -1
  173. package/dist/client/index.js.map +1 -1
  174. package/dist/component/_generated/component.d.ts +9 -0
  175. package/dist/component/_generated/component.d.ts.map +1 -1
  176. package/dist/component/mediaAssets.d.ts +35 -0
  177. package/dist/component/mediaAssets.d.ts.map +1 -1
  178. package/dist/component/mediaAssets.js +81 -0
  179. package/dist/component/mediaAssets.js.map +1 -1
  180. package/dist/test.d.ts.map +1 -1
  181. package/dist/test.js +2 -1
  182. package/dist/test.js.map +1 -1
  183. package/package.json +9 -5
  184. package/dist/component/auditLog.d.ts +0 -410
  185. package/dist/component/auditLog.d.ts.map +0 -1
  186. package/dist/component/auditLog.js +0 -607
  187. package/dist/component/auditLog.js.map +0 -1
  188. package/dist/component/types.d.ts +0 -4
  189. package/dist/component/types.d.ts.map +0 -1
  190. package/dist/component/types.js +0 -2
  191. package/dist/component/types.js.map +0 -1
  192. package/src/cli/commands/admin.ts +0 -104
  193. package/src/cli/index.ts +0 -21
  194. package/src/cli/utils/detectConvexUrl.ts +0 -54
  195. package/src/cli/utils/openBrowser.ts +0 -16
  196. package/src/client/admin-config.ts +0 -138
  197. package/src/client/adminApi.ts +0 -942
  198. package/src/client/agentTools.ts +0 -1311
  199. package/src/client/argTypes.ts +0 -316
  200. package/src/client/field-types.ts +0 -187
  201. package/src/client/index.ts +0 -1301
  202. package/src/client/queryBuilder.ts +0 -1100
  203. package/src/client/schema/codegen.ts +0 -500
  204. package/src/client/schema/defineContentType.ts +0 -501
  205. package/src/client/schema/index.ts +0 -169
  206. package/src/client/schema/schemaDrift.ts +0 -574
  207. package/src/client/schema/typedClient.ts +0 -688
  208. package/src/client/schema/types.ts +0 -666
  209. package/src/client/types.ts +0 -723
  210. package/src/client/workflows.ts +0 -141
  211. package/src/client/wrapper.ts +0 -4304
  212. package/src/component/_generated/api.ts +0 -140
  213. package/src/component/_generated/component.ts +0 -5029
  214. package/src/component/_generated/dataModel.ts +0 -60
  215. package/src/component/_generated/server.ts +0 -156
  216. package/src/component/authorization.ts +0 -647
  217. package/src/component/authorizationHooks.ts +0 -668
  218. package/src/component/bulkOperations.ts +0 -687
  219. package/src/component/contentEntries.ts +0 -1976
  220. package/src/component/contentEntryMutations.ts +0 -1223
  221. package/src/component/contentEntryValidation.ts +0 -707
  222. package/src/component/contentLock.ts +0 -550
  223. package/src/component/contentTypeMigration.ts +0 -1064
  224. package/src/component/contentTypeMutations.ts +0 -969
  225. package/src/component/contentTypes.ts +0 -346
  226. package/src/component/convex.config.ts +0 -44
  227. package/src/component/documentTypes.ts +0 -240
  228. package/src/component/eventEmitter.ts +0 -485
  229. package/src/component/exportImport.ts +0 -1169
  230. package/src/component/index.ts +0 -491
  231. package/src/component/lib/deepReferenceResolver.ts +0 -999
  232. package/src/component/lib/errors.ts +0 -816
  233. package/src/component/lib/index.ts +0 -145
  234. package/src/component/lib/mediaReferenceResolver.ts +0 -495
  235. package/src/component/lib/metadataExtractor.ts +0 -792
  236. package/src/component/lib/mutationAuth.ts +0 -199
  237. package/src/component/lib/queries.ts +0 -79
  238. package/src/component/lib/ragContentChunker.ts +0 -1371
  239. package/src/component/lib/referenceResolver.ts +0 -430
  240. package/src/component/lib/slugGenerator.ts +0 -262
  241. package/src/component/lib/slugUniqueness.ts +0 -333
  242. package/src/component/lib/softDelete.ts +0 -44
  243. package/src/component/localeFallbackChain.ts +0 -673
  244. package/src/component/localeFields.ts +0 -896
  245. package/src/component/mediaAssetMutations.ts +0 -725
  246. package/src/component/mediaAssets.ts +0 -932
  247. package/src/component/mediaFolderMutations.ts +0 -1046
  248. package/src/component/mediaUploadMutations.ts +0 -224
  249. package/src/component/mediaVariantMutations.ts +0 -900
  250. package/src/component/mediaVariants.ts +0 -793
  251. package/src/component/ragContentIndexer.ts +0 -1067
  252. package/src/component/rateLimitHooks.ts +0 -572
  253. package/src/component/roles.ts +0 -1360
  254. package/src/component/scheduledPublish.ts +0 -358
  255. package/src/component/schema.ts +0 -617
  256. package/src/component/taxonomies.ts +0 -949
  257. package/src/component/taxonomyMutations.ts +0 -1210
  258. package/src/component/trash.ts +0 -724
  259. package/src/component/userContext.ts +0 -898
  260. package/src/component/validation.ts +0 -1388
  261. package/src/component/validators.ts +0 -949
  262. package/src/component/versionMutations.ts +0 -392
  263. package/src/component/webhookTrigger.ts +0 -1922
  264. package/src/react/index.ts +0 -898
  265. package/src/test.ts +0 -1580
@@ -1,896 +0,0 @@
1
- /**
2
- * Locale-Specific Content Field Storage and Resolution
3
- *
4
- * This module provides the schema and storage structure for locale-specific
5
- * content field values. It allows fields marked as `localized: true` in their
6
- * field definition to store translations keyed by locale code within content entries.
7
- *
8
- * Storage Structure:
9
- * - Non-localized fields: { title: "Hello World" }
10
- * - Localized fields: { title: { "en-US": "Hello World", "es-ES": "Hola Mundo" } }
11
- *
12
- * @example
13
- * ```typescript
14
- * // Content entry data with localized fields
15
- * const data = {
16
- * // Non-localized field (same value for all locales)
17
- * slug: "my-post",
18
- *
19
- * // Localized field (translations keyed by locale)
20
- * title: {
21
- * "en-US": "My Blog Post",
22
- * "es-ES": "Mi Entrada de Blog",
23
- * "fr-FR": "Mon Article de Blog",
24
- * },
25
- *
26
- * // Localized rich text field
27
- * content: {
28
- * "en-US": "<p>Welcome to my blog!</p>",
29
- * "es-ES": "<p>¡Bienvenido a mi blog!</p>",
30
- * },
31
- * };
32
- * ```
33
- */
34
-
35
- import type { FieldDefinition } from "../client/types.js";
36
-
37
- // =============================================================================
38
- // Types
39
- // =============================================================================
40
-
41
- /**
42
- * Represents a localized field value - a mapping from locale codes to translated values.
43
- *
44
- * @example
45
- * ```typescript
46
- * const localizedTitle: LocalizedFieldValue<string> = {
47
- * "en-US": "Hello World",
48
- * "es-ES": "Hola Mundo",
49
- * "fr-FR": "Bonjour le Monde",
50
- * };
51
- * ```
52
- */
53
- export type LocalizedFieldValue<T = unknown> = {
54
- [localeCode: string]: T;
55
- };
56
-
57
- /**
58
- * Represents either a plain field value or a localized field value.
59
- * Used when working with field values that may or may not be localized.
60
- */
61
- export type FieldValue<T = unknown> = T | LocalizedFieldValue<T>;
62
-
63
- /**
64
- * Options for resolving localized field values.
65
- */
66
- export interface LocaleResolutionOptions {
67
- /**
68
- * The primary locale to attempt to resolve first.
69
- */
70
- locale: string;
71
-
72
- /**
73
- * Fallback chain of locales to try if the primary locale is not found.
74
- * Tried in order until a value is found.
75
- *
76
- * @example
77
- * ```typescript
78
- * // Try en-US first, then en, then the default locale
79
- * fallbackChain: ["en", "en-US"]
80
- * ```
81
- */
82
- fallbackChain?: string[];
83
-
84
- /**
85
- * The default locale to use as final fallback.
86
- * If not specified, returns undefined when no locale matches.
87
- *
88
- * @default "en"
89
- */
90
- defaultLocale?: string;
91
- }
92
-
93
- /**
94
- * Result of resolving a localized field value.
95
- */
96
- export interface LocaleResolutionResult<T = unknown> {
97
- /**
98
- * The resolved value, or undefined if no matching locale was found.
99
- */
100
- value: T | undefined;
101
-
102
- /**
103
- * The locale code that was used to resolve the value.
104
- * Undefined if no matching locale was found.
105
- */
106
- resolvedLocale: string | undefined;
107
-
108
- /**
109
- * Whether the value was resolved from the primary requested locale
110
- * (as opposed to a fallback).
111
- */
112
- isExactMatch: boolean;
113
- }
114
-
115
- // =============================================================================
116
- // Type Guards
117
- // =============================================================================
118
-
119
- /**
120
- * Checks if a value is a LocalizedFieldValue structure.
121
- *
122
- * A value is considered localized if:
123
- * - It is a non-null object
124
- * - It is not an array
125
- * - At least one key contains a hyphen (BCP 47 locale codes like "en-US")
126
- * - All keys match the BCP 47 pattern (language[-Script][-REGION])
127
- *
128
- * Note: For fields that should store localized content, use the `localized: true`
129
- * property in the field definition. This function is a heuristic type guard.
130
- *
131
- * @param value - The value to check
132
- * @returns true if the value appears to be a LocalizedFieldValue
133
- *
134
- * @example
135
- * ```typescript
136
- * isLocalizedFieldValue({ "en-US": "Hello" }); // true
137
- * isLocalizedFieldValue({ "en-US": "Hello", "es-ES": "Hola" }); // true
138
- * isLocalizedFieldValue("Hello"); // false
139
- * isLocalizedFieldValue({ foo: "bar" }); // false (no hyphenated locale key)
140
- * isLocalizedFieldValue({ en: "Hello" }); // false (no hyphen - ambiguous)
141
- * ```
142
- */
143
- export function isLocalizedFieldValue(
144
- value: unknown,
145
- ): value is LocalizedFieldValue {
146
- if (value === null || value === undefined) {
147
- return false;
148
- }
149
-
150
- if (typeof value !== "object" || Array.isArray(value)) {
151
- return false;
152
- }
153
-
154
- const keys = Object.keys(value);
155
-
156
- // Empty objects are not localized values
157
- if (keys.length === 0) {
158
- return false;
159
- }
160
-
161
- // Check if all keys are valid BCP 47 locale codes
162
- // Valid patterns (with hyphen required for disambiguation):
163
- // - "en-US" (language + ISO 3166-1 alpha-2 region, uppercase)
164
- // At least one key must be hyphenated to confirm this is a localized structure
165
- const hasHyphenatedKey = keys.some((key) => key.includes("-"));
166
-
167
- if (!hasHyphenatedKey) {
168
- return false;
169
- }
170
-
171
- // All keys must be valid locale patterns
172
- const localePattern = /^[a-z]{2,3}(-[A-Z][a-z]{3})?(-[A-Z]{2})?$/;
173
-
174
- return keys.every((key) => {
175
- // Must have hyphen if longer than 3 chars
176
- if (key.length > 3 && !key.includes("-")) {
177
- return false;
178
- }
179
- return localePattern.test(key);
180
- });
181
- }
182
-
183
- /**
184
- * Checks if a field definition indicates the field should be localized.
185
- *
186
- * @param fieldDef - The field definition to check
187
- * @returns true if the field is marked as localized
188
- */
189
- export function isFieldLocalized(fieldDef: FieldDefinition): boolean {
190
- return fieldDef.localized === true;
191
- }
192
-
193
- // =============================================================================
194
- // Field Value Operations
195
- // =============================================================================
196
-
197
- /**
198
- * Gets a value from a potentially localized field.
199
- *
200
- * If the field value is a LocalizedFieldValue, resolves using the specified locale
201
- * with fallback chain. If it's a plain value, returns it directly.
202
- *
203
- * @param value - The field value (may be localized or plain)
204
- * @param options - Locale resolution options
205
- * @returns The resolved value result
206
- *
207
- * @example
208
- * ```typescript
209
- * // With localized value
210
- * const localizedTitle = { "en-US": "Hello", "es-ES": "Hola" };
211
- * const result = getLocalizedValue(localizedTitle, { locale: "es-ES" });
212
- * // result: { value: "Hola", resolvedLocale: "es-ES", isExactMatch: true }
213
- *
214
- * // With fallback
215
- * const result2 = getLocalizedValue(localizedTitle, {
216
- * locale: "fr-FR",
217
- * fallbackChain: ["en-US"],
218
- * });
219
- * // result2: { value: "Hello", resolvedLocale: "en-US", isExactMatch: false }
220
- *
221
- * // With plain value (non-localized field)
222
- * const result3 = getLocalizedValue("Plain text", { locale: "en-US" });
223
- * // result3: { value: "Plain text", resolvedLocale: undefined, isExactMatch: true }
224
- * ```
225
- */
226
- export function getLocalizedValue<T>(
227
- value: FieldValue<T>,
228
- options: LocaleResolutionOptions,
229
- ): LocaleResolutionResult<T> {
230
- // If not a localized value, return directly
231
- if (!isLocalizedFieldValue(value)) {
232
- return {
233
- value: value as T,
234
- resolvedLocale: undefined,
235
- isExactMatch: true,
236
- };
237
- }
238
-
239
- const localizedValue = value as LocalizedFieldValue<T>;
240
- const { locale, fallbackChain = [], defaultLocale = "en" } = options;
241
-
242
- // Try the primary locale first
243
- if (locale in localizedValue) {
244
- return {
245
- value: localizedValue[locale],
246
- resolvedLocale: locale,
247
- isExactMatch: true,
248
- };
249
- }
250
-
251
- // Try fallback chain
252
- for (const fallbackLocale of fallbackChain) {
253
- if (fallbackLocale in localizedValue) {
254
- return {
255
- value: localizedValue[fallbackLocale],
256
- resolvedLocale: fallbackLocale,
257
- isExactMatch: false,
258
- };
259
- }
260
- }
261
-
262
- // Try default locale
263
- if (defaultLocale && defaultLocale in localizedValue) {
264
- return {
265
- value: localizedValue[defaultLocale],
266
- resolvedLocale: defaultLocale,
267
- isExactMatch: false,
268
- };
269
- }
270
-
271
- // Try to find any available locale as last resort
272
- const availableLocales = Object.keys(localizedValue);
273
- if (availableLocales.length > 0) {
274
- const firstAvailable = availableLocales[0];
275
- return {
276
- value: localizedValue[firstAvailable],
277
- resolvedLocale: firstAvailable,
278
- isExactMatch: false,
279
- };
280
- }
281
-
282
- // No value found
283
- return {
284
- value: undefined,
285
- resolvedLocale: undefined,
286
- isExactMatch: false,
287
- };
288
- }
289
-
290
- /**
291
- * Sets a value for a specific locale in a localized field.
292
- *
293
- * If the existing value is not localized, it converts it to a localized structure
294
- * with the new value for the specified locale.
295
- *
296
- * @param existingValue - The current field value (may be localized or plain)
297
- * @param locale - The locale to set the value for
298
- * @param newValue - The new value to set
299
- * @param preserveExisting - If true and converting from plain to localized,
300
- * preserves the existing value under the default locale
301
- * @param defaultLocale - The locale to use for preserving existing plain values
302
- * @returns The updated localized field value
303
- *
304
- * @example
305
- * ```typescript
306
- * // Set value for a locale
307
- * const existing = { "en-US": "Hello" };
308
- * const updated = setLocalizedValue(existing, "es-ES", "Hola");
309
- * // updated: { "en-US": "Hello", "es-ES": "Hola" }
310
- *
311
- * // Convert plain to localized
312
- * const plain = "Hello";
313
- * const converted = setLocalizedValue(plain, "es-ES", "Hola", true, "en-US");
314
- * // converted: { "en-US": "Hello", "es-ES": "Hola" }
315
- * ```
316
- */
317
- export function setLocalizedValue<T>(
318
- existingValue: FieldValue<T> | undefined,
319
- locale: string,
320
- newValue: T,
321
- preserveExisting = true,
322
- defaultLocale = "en",
323
- ): LocalizedFieldValue<T> {
324
- // If already localized, just add/update the locale
325
- if (isLocalizedFieldValue(existingValue)) {
326
- return {
327
- ...existingValue,
328
- [locale]: newValue,
329
- };
330
- }
331
-
332
- // Converting from plain value to localized
333
- const result: LocalizedFieldValue<T> = {
334
- [locale]: newValue,
335
- };
336
-
337
- // Preserve existing plain value under default locale if requested
338
- if (
339
- preserveExisting &&
340
- existingValue !== undefined &&
341
- existingValue !== null &&
342
- locale !== defaultLocale
343
- ) {
344
- result[defaultLocale] = existingValue as T;
345
- }
346
-
347
- return result;
348
- }
349
-
350
- /**
351
- * Removes a specific locale from a localized field value.
352
- *
353
- * @param localizedValue - The localized field value
354
- * @param locale - The locale to remove
355
- * @returns The updated localized field value, or undefined if empty
356
- *
357
- * @example
358
- * ```typescript
359
- * const value = { "en-US": "Hello", "es-ES": "Hola" };
360
- * const updated = removeLocale(value, "es-ES");
361
- * // updated: { "en-US": "Hello" }
362
- * ```
363
- */
364
- export function removeLocale<T>(
365
- localizedValue: LocalizedFieldValue<T>,
366
- locale: string,
367
- ): LocalizedFieldValue<T> | undefined {
368
- const { [locale]: _removed, ...rest } = localizedValue;
369
-
370
- // Return undefined if no locales remain
371
- if (Object.keys(rest).length === 0) {
372
- return undefined;
373
- }
374
-
375
- return rest;
376
- }
377
-
378
- /**
379
- * Merges translations from one localized value into another.
380
- *
381
- * @param target - The base localized value
382
- * @param source - The localized value to merge in (overwrites existing locales)
383
- * @returns The merged localized field value
384
- *
385
- * @example
386
- * ```typescript
387
- * const target = { "en-US": "Hello", "es-ES": "Hola" };
388
- * const source = { "es-ES": "Hola!", "fr-FR": "Bonjour" };
389
- * const merged = mergeLocalizedValues(target, source);
390
- * // merged: { "en-US": "Hello", "es-ES": "Hola!", "fr-FR": "Bonjour" }
391
- * ```
392
- */
393
- export function mergeLocalizedValues<T>(
394
- target: LocalizedFieldValue<T> | undefined,
395
- source: LocalizedFieldValue<T>,
396
- ): LocalizedFieldValue<T> {
397
- return {
398
- ...(target ?? {}),
399
- ...source,
400
- };
401
- }
402
-
403
- /**
404
- * Gets all available locales from a localized field value.
405
- *
406
- * @param value - The field value (may be localized or plain)
407
- * @returns Array of locale codes, or empty array if not localized
408
- *
409
- * @example
410
- * ```typescript
411
- * const value = { "en-US": "Hello", "es-ES": "Hola" };
412
- * const locales = getAvailableLocales(value);
413
- * // locales: ["en-US", "es-ES"]
414
- *
415
- * const plain = "Hello";
416
- * const locales2 = getAvailableLocales(plain);
417
- * // locales2: []
418
- * ```
419
- */
420
- export function getAvailableLocales(value: FieldValue): string[] {
421
- if (!isLocalizedFieldValue(value)) {
422
- return [];
423
- }
424
-
425
- return Object.keys(value);
426
- }
427
-
428
- /**
429
- * Checks if a localized field has a translation for a specific locale.
430
- *
431
- * @param value - The field value (may be localized or plain)
432
- * @param locale - The locale to check
433
- * @returns true if the locale has a translation
434
- */
435
- export function hasLocale(value: FieldValue, locale: string): boolean {
436
- if (!isLocalizedFieldValue(value)) {
437
- return false;
438
- }
439
-
440
- return locale in value;
441
- }
442
-
443
- // =============================================================================
444
- // Content Data Operations
445
- // =============================================================================
446
-
447
- /**
448
- * Options for resolving all localized fields in content data.
449
- */
450
- export interface ResolveContentDataOptions extends LocaleResolutionOptions {
451
- /**
452
- * Field definitions for the content type.
453
- * Used to determine which fields are localized.
454
- */
455
- fields: FieldDefinition[];
456
- }
457
-
458
- /**
459
- * Result of resolving all localized fields in content data.
460
- */
461
- export interface ResolvedContentData {
462
- /**
463
- * The resolved data with all localized fields resolved to single values.
464
- */
465
- data: Record<string, unknown>;
466
-
467
- /**
468
- * Map of field names to their resolution results.
469
- */
470
- resolutions: Record<string, LocaleResolutionResult>;
471
-
472
- /**
473
- * Fields that were missing translations for the requested locale.
474
- */
475
- missingTranslations: string[];
476
- }
477
-
478
- /**
479
- * Resolves all localized fields in content data to single values for a specific locale.
480
- *
481
- * This function takes raw content entry data (which may contain LocalizedFieldValue
482
- * structures for localized fields) and resolves each field to a single value
483
- * based on the requested locale and fallback chain.
484
- *
485
- * @param data - The raw content entry data
486
- * @param options - Resolution options including fields and locale settings
487
- * @returns The resolved content data with metadata about resolution
488
- *
489
- * @example
490
- * ```typescript
491
- * const rawData = {
492
- * slug: "my-post", // non-localized
493
- * title: { "en-US": "Hello", "es-ES": "Hola" }, // localized
494
- * content: { "en-US": "Content here" }, // localized, missing es-ES
495
- * };
496
- *
497
- * const result = resolveContentData(rawData, {
498
- * locale: "es-ES",
499
- * fallbackChain: ["en-US"],
500
- * fields: [
501
- * { name: "slug", localized: false, ... },
502
- * { name: "title", localized: true, ... },
503
- * { name: "content", localized: true, ... },
504
- * ],
505
- * });
506
- *
507
- * // result.data: { slug: "my-post", title: "Hola", content: "Content here" }
508
- * // result.missingTranslations: ["content"]
509
- * ```
510
- */
511
- export function resolveContentData(
512
- data: Record<string, unknown>,
513
- options: ResolveContentDataOptions,
514
- ): ResolvedContentData {
515
- const { fields, locale, fallbackChain, defaultLocale } = options;
516
- const resolvedData: Record<string, unknown> = {};
517
- const resolutions: Record<string, LocaleResolutionResult> = {};
518
- const missingTranslations: string[] = [];
519
-
520
- // Create a map of field names to their definitions for quick lookup
521
- const fieldMap = new Map<string, FieldDefinition>();
522
- for (const field of fields) {
523
- fieldMap.set(field.name, field);
524
- }
525
-
526
- // Process each field in the data
527
- for (const [fieldName, fieldValue] of Object.entries(data)) {
528
- const fieldDef = fieldMap.get(fieldName);
529
-
530
- // If field is localized, resolve using locale
531
- if (fieldDef && isFieldLocalized(fieldDef)) {
532
- const result = getLocalizedValue(fieldValue, {
533
- locale,
534
- fallbackChain,
535
- defaultLocale,
536
- });
537
-
538
- resolvedData[fieldName] = result.value;
539
- resolutions[fieldName] = result;
540
-
541
- // Track if this was not an exact match (missing translation)
542
- if (!result.isExactMatch && result.resolvedLocale !== undefined) {
543
- missingTranslations.push(fieldName);
544
- }
545
- } else {
546
- // Non-localized field - pass through as-is
547
- resolvedData[fieldName] = fieldValue;
548
- }
549
- }
550
-
551
- return {
552
- data: resolvedData,
553
- resolutions,
554
- missingTranslations,
555
- };
556
- }
557
-
558
- /**
559
- * Sets values for multiple localized fields for a specific locale.
560
- *
561
- * This function takes a partial data object with values for a specific locale
562
- * and merges them into the existing content data's localized field structures.
563
- *
564
- * @param existingData - The existing content entry data
565
- * @param newValues - New values to set (field name -> value)
566
- * @param locale - The locale to set values for
567
- * @param fields - Field definitions to determine which fields are localized
568
- * @param defaultLocale - Default locale for preserving existing values
569
- * @returns Updated content data with localized values merged in
570
- *
571
- * @example
572
- * ```typescript
573
- * const existingData = {
574
- * slug: "my-post",
575
- * title: { "en-US": "Hello" },
576
- * };
577
- *
578
- * const updated = setLocalizedContentData(
579
- * existingData,
580
- * { title: "Hola", slug: "mi-post" }, // slug will be overwritten, title merged
581
- * "es-ES",
582
- * fields
583
- * );
584
- *
585
- * // updated: {
586
- * // slug: "mi-post", // non-localized, just replaced
587
- * // title: { "en-US": "Hello", "es-ES": "Hola" }, // localized, merged
588
- * // }
589
- * ```
590
- */
591
- export function setLocalizedContentData(
592
- existingData: Record<string, unknown>,
593
- newValues: Record<string, unknown>,
594
- locale: string,
595
- fields: FieldDefinition[],
596
- defaultLocale = "en",
597
- ): Record<string, unknown> {
598
- // Create a map of field names to their definitions for quick lookup
599
- const fieldMap = new Map<string, FieldDefinition>();
600
- for (const field of fields) {
601
- fieldMap.set(field.name, field);
602
- }
603
-
604
- const result: Record<string, unknown> = { ...existingData };
605
-
606
- for (const [fieldName, newValue] of Object.entries(newValues)) {
607
- const fieldDef = fieldMap.get(fieldName);
608
-
609
- if (fieldDef && isFieldLocalized(fieldDef)) {
610
- // Localized field - merge into localized structure
611
- result[fieldName] = setLocalizedValue(
612
- existingData[fieldName] as FieldValue,
613
- locale,
614
- newValue,
615
- true,
616
- defaultLocale,
617
- );
618
- } else {
619
- // Non-localized field - just replace
620
- result[fieldName] = newValue;
621
- }
622
- }
623
-
624
- return result;
625
- }
626
-
627
- /**
628
- * Result of resolving locale content for an entry with metadata.
629
- */
630
- export interface LocaleResolvedEntry<T = Record<string, unknown>> {
631
- /**
632
- * The resolved data with all localized fields resolved to single values.
633
- */
634
- data: T;
635
-
636
- /**
637
- * Metadata about the locale resolution process.
638
- */
639
- localeResolution: {
640
- /**
641
- * The locale that was requested.
642
- */
643
- requestedLocale: string;
644
-
645
- /**
646
- * The fallback chain that was used for resolution.
647
- */
648
- fallbackChain: string[];
649
-
650
- /**
651
- * The default locale used as final fallback.
652
- */
653
- defaultLocale: string;
654
-
655
- /**
656
- * Fields that were resolved from a fallback locale (missing in requested locale).
657
- */
658
- fieldsFromFallback: string[];
659
-
660
- /**
661
- * Map of field names to the locale they were resolved from.
662
- * Only includes fields that were resolved from a fallback (not the requested locale).
663
- */
664
- fieldResolutions: Record<string, string>;
665
- };
666
- }
667
-
668
- /**
669
- * Options for resolving locale content for entries.
670
- */
671
- export interface ResolveLocaleOptions {
672
- /**
673
- * The locale to resolve content in.
674
- */
675
- locale: string;
676
-
677
- /**
678
- * Fallback chain of locales to try if the primary locale is not found.
679
- */
680
- fallbackChain?: string[];
681
-
682
- /**
683
- * The default locale to use as final fallback.
684
- * @default "en"
685
- */
686
- defaultLocale?: string;
687
-
688
- /**
689
- * Field definitions for the content type.
690
- * Used to determine which fields are localized.
691
- */
692
- fields: FieldDefinition[];
693
-
694
- /**
695
- * Whether to include locale resolution metadata in the result.
696
- * @default true
697
- */
698
- includeResolutionMetadata?: boolean;
699
- }
700
-
701
- /**
702
- * Resolves all localized fields in a content entry to the requested locale with fallback support.
703
- *
704
- * This function takes raw content entry data (which may contain LocalizedFieldValue
705
- * structures for localized fields) and resolves each field to a single value
706
- * based on the requested locale and fallback chain. It merges localized and
707
- * non-localized field values into a unified data structure.
708
- *
709
- * Resolution order for each localized field:
710
- * 1. Try the requested locale
711
- * 2. Try each locale in the fallback chain (in order)
712
- * 3. Try the default locale
713
- * 4. Return first available locale as last resort
714
- * 5. Return undefined if no value exists
715
- *
716
- * @param entry - The content entry with potentially localized data
717
- * @param options - Locale resolution options
718
- * @returns The entry with resolved locale data and resolution metadata
719
- *
720
- * @example
721
- * ```typescript
722
- * const entry = {
723
- * _id: "abc123",
724
- * slug: "my-post",
725
- * data: {
726
- * slug: "my-post", // non-localized
727
- * title: { "en-US": "Hello", "es-ES": "Hola" }, // localized
728
- * content: { "en-US": "Content here" }, // localized, missing es-ES
729
- * },
730
- * };
731
- *
732
- * const resolved = resolveLocaleContent(entry, {
733
- * locale: "es-ES",
734
- * fallbackChain: ["en-US"],
735
- * defaultLocale: "en-US",
736
- * fields: contentType.fields,
737
- * });
738
- *
739
- * // resolved.data: { slug: "my-post", title: "Hola", content: "Content here" }
740
- * // resolved.localeResolution.fieldsFromFallback: ["content"]
741
- * // resolved.localeResolution.fieldResolutions: { content: "en-US" }
742
- * ```
743
- */
744
- export function resolveLocaleContent<
745
- T extends { data: Record<string, unknown> }
746
- >(
747
- entry: T,
748
- options: ResolveLocaleOptions,
749
- ): T & LocaleResolvedEntry<Record<string, unknown>> {
750
- const {
751
- locale,
752
- fallbackChain = [],
753
- defaultLocale = "en",
754
- fields,
755
- includeResolutionMetadata = true,
756
- } = options;
757
-
758
- // Resolve content data using existing function
759
- const resolved = resolveContentData(entry.data, {
760
- locale,
761
- fallbackChain,
762
- defaultLocale,
763
- fields,
764
- });
765
-
766
- // Build field resolution map for fields that used fallback
767
- const fieldResolutions: Record<string, string> = {};
768
- for (const [fieldName, resolution] of Object.entries(resolved.resolutions)) {
769
- if (!resolution.isExactMatch && resolution.resolvedLocale) {
770
- fieldResolutions[fieldName] = resolution.resolvedLocale;
771
- }
772
- }
773
-
774
- // Return entry with resolved data and metadata
775
- return {
776
- ...entry,
777
- data: resolved.data,
778
- localeResolution: includeResolutionMetadata
779
- ? {
780
- requestedLocale: locale,
781
- fallbackChain,
782
- defaultLocale,
783
- fieldsFromFallback: resolved.missingTranslations,
784
- fieldResolutions,
785
- }
786
- : undefined,
787
- } as T & LocaleResolvedEntry<Record<string, unknown>>;
788
- }
789
-
790
- /**
791
- * Resolves locale content for an array of content entries.
792
- *
793
- * This is a convenience function for batch-resolving multiple entries.
794
- * Useful when fetching lists of content entries that need locale resolution.
795
- *
796
- * @param entries - Array of content entries to resolve
797
- * @param options - Locale resolution options (applied to all entries)
798
- * @returns Array of entries with resolved locale data
799
- *
800
- * @example
801
- * ```typescript
802
- * const entries = await cms.contentEntries.list(ctx, {
803
- * contentTypeName: "blog_post",
804
- * status: "published",
805
- * paginationOpts: { numItems: 10 },
806
- * });
807
- *
808
- * const resolvedEntries = resolveLocaleContentBatch(entries.page, {
809
- * locale: "es-ES",
810
- * fallbackChain: cms.getLocaleFallbackChain("es-ES"),
811
- * defaultLocale: cms.config.defaultLocale,
812
- * fields: blogPostContentType.fields,
813
- * });
814
- * ```
815
- */
816
- export function resolveLocaleContentBatch<
817
- T extends { data: Record<string, unknown> }
818
- >(
819
- entries: T[],
820
- options: ResolveLocaleOptions,
821
- ): Array<T & LocaleResolvedEntry<Record<string, unknown>>> {
822
- return entries.map((entry) => resolveLocaleContent(entry, options));
823
- }
824
-
825
- /**
826
- * Gets the translation completeness status for content data across locales.
827
- *
828
- * @param data - The content entry data
829
- * @param fields - Field definitions for the content type
830
- * @param requiredLocales - Locales that should have translations
831
- * @returns Translation status per locale
832
- *
833
- * @example
834
- * ```typescript
835
- * const data = {
836
- * title: { "en-US": "Hello", "es-ES": "Hola" },
837
- * content: { "en-US": "Content here" }, // missing es-ES
838
- * };
839
- *
840
- * const status = getTranslationStatus(data, fields, ["en-US", "es-ES"]);
841
- * // status: {
842
- * // "en-US": { complete: true, missingFields: [], percentage: 100 },
843
- * // "es-ES": { complete: false, missingFields: ["content"], percentage: 50 },
844
- * // }
845
- * ```
846
- */
847
- export function getTranslationStatus(
848
- data: Record<string, unknown>,
849
- fields: FieldDefinition[],
850
- requiredLocales: string[],
851
- ): Record<
852
- string,
853
- { complete: boolean; missingFields: string[]; percentage: number }
854
- > {
855
- const result: Record<
856
- string,
857
- { complete: boolean; missingFields: string[]; percentage: number }
858
- > = {};
859
-
860
- // Get all localized fields
861
- const localizedFields = fields.filter(isFieldLocalized);
862
-
863
- if (localizedFields.length === 0) {
864
- // No localized fields - all locales are "complete"
865
- for (const locale of requiredLocales) {
866
- result[locale] = { complete: true, missingFields: [], percentage: 100 };
867
- }
868
- return result;
869
- }
870
-
871
- for (const locale of requiredLocales) {
872
- const missingFields: string[] = [];
873
-
874
- for (const field of localizedFields) {
875
- const fieldValue = data[field.name];
876
-
877
- if (!hasLocale(fieldValue as FieldValue, locale)) {
878
- missingFields.push(field.name);
879
- }
880
- }
881
-
882
- const percentage = Math.round(
883
- ((localizedFields.length - missingFields.length) /
884
- localizedFields.length) *
885
- 100,
886
- );
887
-
888
- result[locale] = {
889
- complete: missingFields.length === 0,
890
- missingFields,
891
- percentage,
892
- };
893
- }
894
-
895
- return result;
896
- }