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.
- package/admin-dist/nitro.json +15 -0
- package/admin-dist/public/assets/CmsEmptyState-CRswfTzk.js +5 -0
- package/admin-dist/public/assets/CmsPageHeader-CirpXndm.js +1 -0
- package/admin-dist/public/assets/CmsStatusBadge-CbEUpQu-.js +1 -0
- package/admin-dist/public/assets/CmsToolbar-BI2nZOXp.js +1 -0
- package/admin-dist/public/assets/ContentEntryEditor-CBeCyK_m.js +4 -0
- package/admin-dist/public/assets/ErrorState-BIVaWmom.js +1 -0
- package/admin-dist/public/assets/TaxonomyFilter-ChaY6Y_x.js +1 -0
- package/admin-dist/public/assets/_contentTypeId-DQ8k_Rvw.js +1 -0
- package/admin-dist/public/assets/_entryId-CKU_glsK.js +1 -0
- package/admin-dist/public/assets/alert-BXjTqrwQ.js +1 -0
- package/admin-dist/public/assets/badge-hvUOzpVZ.js +1 -0
- package/admin-dist/public/assets/circle-check-big-CF_pR17r.js +1 -0
- package/admin-dist/public/assets/command-DU82cJlt.js +1 -0
- package/admin-dist/public/assets/content-_LXl3pp7.js +1 -0
- package/admin-dist/public/assets/content-types-KjxaXGxY.js +2 -0
- package/admin-dist/public/assets/globals-CS6BZ0zp.css +1 -0
- package/admin-dist/public/assets/index-DNGIZHL-.js +1 -0
- package/admin-dist/public/assets/label-KNtpL71g.js +1 -0
- package/admin-dist/public/assets/link-2-Bw2aI4V4.js +1 -0
- package/admin-dist/public/assets/list-sYepHjt_.js +1 -0
- package/admin-dist/public/assets/main-CKj5yfEi.js +97 -0
- package/admin-dist/public/assets/media-Bkrkffm7.js +1 -0
- package/admin-dist/public/assets/new._contentTypeId-C3LstjNs.js +1 -0
- package/admin-dist/public/assets/plus-DUn8v_Xf.js +1 -0
- package/admin-dist/public/assets/rotate-ccw-DJEoHcRI.js +1 -0
- package/admin-dist/public/assets/scroll-area-DfIlT0in.js +1 -0
- package/admin-dist/public/assets/search-MuAUDJKR.js +1 -0
- package/admin-dist/public/assets/select-BD29IXCI.js +1 -0
- package/admin-dist/public/assets/settings-DmMyn_6A.js +1 -0
- package/admin-dist/public/assets/switch-h3Rrnl5i.js +1 -0
- package/admin-dist/public/assets/tabs-imc8h-Dp.js +1 -0
- package/admin-dist/public/assets/taxonomies-dAsrT65H.js +1 -0
- package/admin-dist/public/assets/textarea-BTy7nwzR.js +1 -0
- package/admin-dist/public/assets/trash-SAWKZZHv.js +1 -0
- package/admin-dist/public/assets/triangle-alert-E52Vfeuh.js +1 -0
- package/admin-dist/public/assets/useBreadcrumbLabel-BECBMCzM.js +1 -0
- package/admin-dist/public/assets/usePermissions-Basjs9BT.js +1 -0
- package/admin-dist/public/favicon.ico +0 -0
- package/admin-dist/server/_chunks/_libs/@date-fns/tz.mjs +217 -0
- package/admin-dist/server/_chunks/_libs/@floating-ui/core.mjs +719 -0
- package/admin-dist/server/_chunks/_libs/@floating-ui/dom.mjs +622 -0
- package/admin-dist/server/_chunks/_libs/@floating-ui/react-dom.mjs +292 -0
- package/admin-dist/server/_chunks/_libs/@floating-ui/utils.mjs +320 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/number.mjs +6 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/primitive.mjs +11 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-arrow.mjs +23 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-avatar.mjs +119 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-checkbox.mjs +270 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-collection.mjs +69 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-compose-refs.mjs +39 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-context.mjs +137 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-dialog.mjs +325 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-direction.mjs +9 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-dismissable-layer.mjs +210 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-dropdown-menu.mjs +253 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-guards.mjs +29 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-scope.mjs +206 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-id.mjs +14 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-label.mjs +23 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-menu.mjs +812 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-popover.mjs +300 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-popper.mjs +286 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-portal.mjs +16 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-presence.mjs +128 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-primitive.mjs +141 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-roving-focus.mjs +224 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-scroll-area.mjs +721 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-select.mjs +1163 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-separator.mjs +28 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-slot.mjs +601 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-switch.mjs +152 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-tabs.mjs +189 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-callback-ref.mjs +11 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-controllable-state.mjs +69 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-effect-event.mjs +1 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-escape-keydown.mjs +17 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-is-hydrated.mjs +15 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-layout-effect.mjs +6 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-previous.mjs +14 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-size.mjs +39 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-visually-hidden.mjs +33 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/history.mjs +409 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/react-router.mjs +1711 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/react-store.mjs +56 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/router-core.mjs +4829 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/store.mjs +134 -0
- package/admin-dist/server/_chunks/_libs/react-dom.mjs +10781 -0
- package/admin-dist/server/_chunks/_libs/react.mjs +513 -0
- package/admin-dist/server/_libs/aria-hidden.mjs +122 -0
- package/admin-dist/server/_libs/class-variance-authority.mjs +44 -0
- package/admin-dist/server/_libs/clsx.mjs +16 -0
- package/admin-dist/server/_libs/cmdk.mjs +315 -0
- package/admin-dist/server/_libs/convex.mjs +4841 -0
- package/admin-dist/server/_libs/cookie-es.mjs +58 -0
- package/admin-dist/server/_libs/croner.mjs +1 -0
- package/admin-dist/server/_libs/crossws.mjs +1 -0
- package/admin-dist/server/_libs/date-fns.mjs +1716 -0
- package/admin-dist/server/_libs/detect-node-es.mjs +1 -0
- package/admin-dist/server/_libs/get-nonce.mjs +9 -0
- package/admin-dist/server/_libs/h3-v2.mjs +277 -0
- package/admin-dist/server/_libs/h3.mjs +401 -0
- package/admin-dist/server/_libs/hookable.mjs +1 -0
- package/admin-dist/server/_libs/isbot.mjs +20 -0
- package/admin-dist/server/_libs/lucide-react.mjs +850 -0
- package/admin-dist/server/_libs/ohash.mjs +1 -0
- package/admin-dist/server/_libs/react-day-picker.mjs +2201 -0
- package/admin-dist/server/_libs/react-remove-scroll-bar.mjs +82 -0
- package/admin-dist/server/_libs/react-remove-scroll.mjs +328 -0
- package/admin-dist/server/_libs/react-style-singleton.mjs +69 -0
- package/admin-dist/server/_libs/rou3.mjs +8 -0
- package/admin-dist/server/_libs/seroval-plugins.mjs +58 -0
- package/admin-dist/server/_libs/seroval.mjs +1765 -0
- package/admin-dist/server/_libs/srvx.mjs +719 -0
- package/admin-dist/server/_libs/tailwind-merge.mjs +3010 -0
- package/admin-dist/server/_libs/tiny-invariant.mjs +12 -0
- package/admin-dist/server/_libs/tiny-warning.mjs +5 -0
- package/admin-dist/server/_libs/tslib.mjs +39 -0
- package/admin-dist/server/_libs/ufo.mjs +54 -0
- package/admin-dist/server/_libs/unctx.mjs +1 -0
- package/admin-dist/server/_libs/unstorage.mjs +1 -0
- package/admin-dist/server/_libs/use-callback-ref.mjs +66 -0
- package/admin-dist/server/_libs/use-sidecar.mjs +106 -0
- package/admin-dist/server/_libs/use-sync-external-store.mjs +139 -0
- package/admin-dist/server/_libs/zod.mjs +4223 -0
- package/admin-dist/server/_ssr/CmsEmptyState-DU7-7-mV.mjs +290 -0
- package/admin-dist/server/_ssr/CmsPageHeader-CseW0AHm.mjs +24 -0
- package/admin-dist/server/_ssr/CmsStatusBadge-B_pi4KCp.mjs +127 -0
- package/admin-dist/server/_ssr/CmsToolbar-X75ex6ek.mjs +49 -0
- package/admin-dist/server/_ssr/ContentEntryEditor-CepusRsA.mjs +3720 -0
- package/admin-dist/server/_ssr/ErrorState-cI-bKLez.mjs +89 -0
- package/admin-dist/server/_ssr/TaxonomyFilter-Bwrq0-cz.mjs +188 -0
- package/admin-dist/server/_ssr/_contentTypeId-BqYKEcLr.mjs +379 -0
- package/admin-dist/server/_ssr/_entryId-CRfnqeDf.mjs +161 -0
- package/admin-dist/server/_ssr/_tanstack-start-manifest_v-BwDlABVk.mjs +4 -0
- package/admin-dist/server/_ssr/alert-CVt45UUP.mjs +92 -0
- package/admin-dist/server/_ssr/badge-6BsP37vG.mjs +125 -0
- package/admin-dist/server/_ssr/command-fy8epIKf.mjs +128 -0
- package/admin-dist/server/_ssr/config.server-D7JHDcDv.mjs +117 -0
- package/admin-dist/server/_ssr/content-B5RhL7uW.mjs +532 -0
- package/admin-dist/server/_ssr/content-types-BIOqCQYN.mjs +1166 -0
- package/admin-dist/server/_ssr/index-DHSHDPt1.mjs +193 -0
- package/admin-dist/server/_ssr/index.mjs +1275 -0
- package/admin-dist/server/_ssr/label-C8Dko1j7.mjs +22 -0
- package/admin-dist/server/_ssr/media-CSx3XttC.mjs +1832 -0
- package/admin-dist/server/_ssr/new._contentTypeId-DzanEZQM.mjs +144 -0
- package/admin-dist/server/_ssr/router-DDWcF-kt.mjs +1556 -0
- package/admin-dist/server/_ssr/scroll-area-bjPYwhXN.mjs +59 -0
- package/admin-dist/server/_ssr/select-BUhDDf4T.mjs +142 -0
- package/admin-dist/server/_ssr/settings-DAsxnw2q.mjs +348 -0
- package/admin-dist/server/_ssr/start-HYkvq4Ni.mjs +4 -0
- package/admin-dist/server/_ssr/switch-BgyRtQ1Z.mjs +31 -0
- package/admin-dist/server/_ssr/tabs-DzMdRB1A.mjs +628 -0
- package/admin-dist/server/_ssr/taxonomies-C8j8g5Q5.mjs +915 -0
- package/admin-dist/server/_ssr/textarea-9jNeYJSc.mjs +18 -0
- package/admin-dist/server/_ssr/trash-DYMxwhZB.mjs +291 -0
- package/admin-dist/server/_ssr/useBreadcrumbLabel-FNSAr2Ha.mjs +16 -0
- package/admin-dist/server/_ssr/usePermissions-BJGGahrJ.mjs +68 -0
- package/admin-dist/server/favicon.ico +0 -0
- package/admin-dist/server/index.mjs +627 -0
- package/dist/cli/index.js +0 -0
- package/dist/client/admin-config.d.ts +0 -1
- package/dist/client/admin-config.d.ts.map +1 -1
- package/dist/client/admin-config.js +0 -1
- package/dist/client/admin-config.js.map +1 -1
- package/dist/client/adminApi.d.ts.map +1 -1
- package/dist/client/agentTools.d.ts +1237 -135
- package/dist/client/agentTools.d.ts.map +1 -1
- package/dist/client/agentTools.js +33 -9
- package/dist/client/agentTools.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +9 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/mediaAssets.d.ts +35 -0
- package/dist/component/mediaAssets.d.ts.map +1 -1
- package/dist/component/mediaAssets.js +81 -0
- package/dist/component/mediaAssets.js.map +1 -1
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +2 -1
- package/dist/test.js.map +1 -1
- package/package.json +9 -5
- package/dist/component/auditLog.d.ts +0 -410
- package/dist/component/auditLog.d.ts.map +0 -1
- package/dist/component/auditLog.js +0 -607
- package/dist/component/auditLog.js.map +0 -1
- package/dist/component/types.d.ts +0 -4
- package/dist/component/types.d.ts.map +0 -1
- package/dist/component/types.js +0 -2
- package/dist/component/types.js.map +0 -1
- package/src/cli/commands/admin.ts +0 -104
- package/src/cli/index.ts +0 -21
- package/src/cli/utils/detectConvexUrl.ts +0 -54
- package/src/cli/utils/openBrowser.ts +0 -16
- package/src/client/admin-config.ts +0 -138
- package/src/client/adminApi.ts +0 -942
- package/src/client/agentTools.ts +0 -1311
- package/src/client/argTypes.ts +0 -316
- package/src/client/field-types.ts +0 -187
- package/src/client/index.ts +0 -1301
- package/src/client/queryBuilder.ts +0 -1100
- package/src/client/schema/codegen.ts +0 -500
- package/src/client/schema/defineContentType.ts +0 -501
- package/src/client/schema/index.ts +0 -169
- package/src/client/schema/schemaDrift.ts +0 -574
- package/src/client/schema/typedClient.ts +0 -688
- package/src/client/schema/types.ts +0 -666
- package/src/client/types.ts +0 -723
- package/src/client/workflows.ts +0 -141
- package/src/client/wrapper.ts +0 -4304
- package/src/component/_generated/api.ts +0 -140
- package/src/component/_generated/component.ts +0 -5029
- package/src/component/_generated/dataModel.ts +0 -60
- package/src/component/_generated/server.ts +0 -156
- package/src/component/authorization.ts +0 -647
- package/src/component/authorizationHooks.ts +0 -668
- package/src/component/bulkOperations.ts +0 -687
- package/src/component/contentEntries.ts +0 -1976
- package/src/component/contentEntryMutations.ts +0 -1223
- package/src/component/contentEntryValidation.ts +0 -707
- package/src/component/contentLock.ts +0 -550
- package/src/component/contentTypeMigration.ts +0 -1064
- package/src/component/contentTypeMutations.ts +0 -969
- package/src/component/contentTypes.ts +0 -346
- package/src/component/convex.config.ts +0 -44
- package/src/component/documentTypes.ts +0 -240
- package/src/component/eventEmitter.ts +0 -485
- package/src/component/exportImport.ts +0 -1169
- package/src/component/index.ts +0 -491
- package/src/component/lib/deepReferenceResolver.ts +0 -999
- package/src/component/lib/errors.ts +0 -816
- package/src/component/lib/index.ts +0 -145
- package/src/component/lib/mediaReferenceResolver.ts +0 -495
- package/src/component/lib/metadataExtractor.ts +0 -792
- package/src/component/lib/mutationAuth.ts +0 -199
- package/src/component/lib/queries.ts +0 -79
- package/src/component/lib/ragContentChunker.ts +0 -1371
- package/src/component/lib/referenceResolver.ts +0 -430
- package/src/component/lib/slugGenerator.ts +0 -262
- package/src/component/lib/slugUniqueness.ts +0 -333
- package/src/component/lib/softDelete.ts +0 -44
- package/src/component/localeFallbackChain.ts +0 -673
- package/src/component/localeFields.ts +0 -896
- package/src/component/mediaAssetMutations.ts +0 -725
- package/src/component/mediaAssets.ts +0 -932
- package/src/component/mediaFolderMutations.ts +0 -1046
- package/src/component/mediaUploadMutations.ts +0 -224
- package/src/component/mediaVariantMutations.ts +0 -900
- package/src/component/mediaVariants.ts +0 -793
- package/src/component/ragContentIndexer.ts +0 -1067
- package/src/component/rateLimitHooks.ts +0 -572
- package/src/component/roles.ts +0 -1360
- package/src/component/scheduledPublish.ts +0 -358
- package/src/component/schema.ts +0 -617
- package/src/component/taxonomies.ts +0 -949
- package/src/component/taxonomyMutations.ts +0 -1210
- package/src/component/trash.ts +0 -724
- package/src/component/userContext.ts +0 -898
- package/src/component/validation.ts +0 -1388
- package/src/component/validators.ts +0 -949
- package/src/component/versionMutations.ts +0 -392
- package/src/component/webhookTrigger.ts +0 -1922
- package/src/react/index.ts +0 -898
- 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
|
-
}
|