@easyling/sanity-connector 1.2.0 → 1.3.0-rc.9

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.
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Debug DNT Badge Component
3
+ *
4
+ * Displays debug information about the DNT status of a field.
5
+ * This badge is shown on ALL fields (not just translatable ones) when debug mode is enabled.
6
+ * It shows whether the field will be included in the translation request and its effective DNT status.
7
+ */
8
+ import { FieldProps } from 'sanity';
9
+ export type DebugDNTBadgeProps = FieldProps & {
10
+ /** Name of the group this field belongs to */
11
+ groupName?: string;
12
+ /** Name of the fieldset this field belongs to */
13
+ fieldsetName?: string;
14
+ };
15
+ /**
16
+ * Debug DNT Badge Component
17
+ * Shows the effective DNT status for all fields when debug mode is enabled
18
+ */
19
+ export declare function DebugDNTBadge(props: DebugDNTBadgeProps): import("react/jsx-runtime").JSX.Element;
@@ -4,3 +4,4 @@
4
4
  export { DNTFieldBadge } from './DNTFieldBadge';
5
5
  export { withDNTBadge } from './DNTFieldComponent';
6
6
  export { DNTFieldInput } from './DNTFieldInput';
7
+ export { DebugDNTBadge } from './DebugDNTBadge';
@@ -1,2 +1,12 @@
1
+ /**
2
+ * Context for plugin configuration state
3
+ */
4
+ interface PluginConfigContextType {
5
+ debugMode: boolean;
6
+ }
7
+ /**
8
+ * Hook to access plugin configuration
9
+ */
10
+ export declare function usePluginConfig(): PluginConfigContextType;
1
11
  declare const _default: import("sanity").Plugin<void>;
2
12
  export default _default;
@@ -8,6 +8,12 @@ export interface ExtendedDocumentContent extends DocumentContent {
8
8
  slug?: string;
9
9
  /** Fields can be either flat values or TranslatableField objects with value and dnt properties */
10
10
  fields?: Record<string, any>;
11
+ /** Whether the title was found at the document's top level (false if extracted from nested fields) */
12
+ titleFoundAtTopLevel?: boolean;
13
+ /** Whether the body/content was found at the document's top level (false if extracted from nested fields) */
14
+ bodyFoundAtTopLevel?: boolean;
15
+ /** The field name where body content was found (e.g., 'body', 'content', 'text', or nested path) */
16
+ bodyFieldName?: string;
11
17
  }
12
18
  export interface ExtractedContent {
13
19
  documentId: string;
@@ -24,6 +30,46 @@ export declare class ContentExtractor {
24
30
  * @returns The inferred Sanity type string
25
31
  */
26
32
  private inferSanityType;
33
+ /**
34
+ * Check if a value is an atomic Sanity type that should NOT be recursively flattened
35
+ * These include types like slug, date, datetime, reference, image, file, etc.
36
+ * Custom object types (e.g., articleInfo, footnoteInfo) are NOT atomic and should be flattened
37
+ */
38
+ private isSanitySpecialType;
39
+ /**
40
+ * Check if an object should be recursively flattened into separate fields
41
+ * Plain objects (fieldsets, custom object types without _type) should be flattened
42
+ * Sanity special types should NOT be flattened
43
+ */
44
+ private shouldFlattenObject;
45
+ /**
46
+ * Recursively flatten a nested object into individual TranslatableField entries
47
+ * Each leaf field gets its own entry with a dotted path key
48
+ *
49
+ * Arrays of objects with _key are flattened with key-based paths like:
50
+ * article.footnotes[_key == "abc123"].footnote
51
+ *
52
+ * @param obj - The object to flatten
53
+ * @param basePath - The base path prefix for keys
54
+ * @param fields - The fields record to populate
55
+ * @param docContext - Document context for GROQ path generation
56
+ */
57
+ private flattenObjectToFields;
58
+ /**
59
+ * Flatten an array into individual TranslatableField entries
60
+ * Arrays of objects with _key get key-based paths
61
+ *
62
+ * @param array - The array to flatten
63
+ * @param fieldPath - The field path for this array
64
+ * @param fieldName - The field name (last segment of path)
65
+ * @param fields - The fields record to populate
66
+ * @param docContext - Document context for GROQ path generation
67
+ */
68
+ private flattenArrayToFields;
69
+ /**
70
+ * Add a leaf field (primitive or atomic Sanity type) to the fields record
71
+ */
72
+ private addLeafField;
27
73
  /**
28
74
  * Extract content from a single Sanity document and convert to HTML
29
75
  */
@@ -40,6 +86,13 @@ export declare class ContentExtractor {
40
86
  /**
41
87
  * Apply DNT preferences to extracted fields based on document type
42
88
  * Now uses document-type-based configuration that applies to all documents of the same type
89
+ *
90
+ * DNT priority:
91
+ * 1. Stored user preference (explicit override)
92
+ * 2. Default based on field type (slug, date, datetime, reference, etc.)
93
+ * 3. Default based on field name (containing "author" or "contributor")
94
+ * 4. Default based on group/fieldset name (containing "date" or "time")
95
+ * 5. Default to translatable (dnt: false)
43
96
  */
44
97
  private applyDNTPreferences;
45
98
  /**
@@ -77,16 +130,30 @@ export declare class ContentExtractor {
77
130
  /**
78
131
  * Separate main content from metadata fields
79
132
  * All fields are immediately wrapped in TranslatableField format with type information
133
+ * DNT defaults are computed based on field type and name patterns
134
+ *
135
+ * Complex types (objects/fieldsets) are recursively flattened into individual fields
136
+ * using dotted path notation (e.g., "metadata.version", "metadata.status")
80
137
  */
81
138
  private separateContentAndFields;
139
+ /**
140
+ * Recursively search nested objects and fieldsets for a body/content field
141
+ */
142
+ private findBodyInNestedFields;
82
143
  /**
83
144
  * Extract slug from document
84
145
  */
85
146
  private extractSlug;
86
147
  /**
87
148
  * Extract title from document
149
+ * Searches top-level fields first, then descends into nested objects/fieldsets
150
+ * @returns Object with title string and flag indicating if found at top level
88
151
  */
89
152
  private extractTitle;
153
+ /**
154
+ * Recursively search nested objects and fieldsets for a title field
155
+ */
156
+ private findTitleInNestedFields;
90
157
  /**
91
158
  * Escape HTML special characters
92
159
  */
@@ -44,7 +44,7 @@ export declare class DNTStorageAdapter implements DNTStorage {
44
44
  * @deprecated Use getFieldsForType instead. This method now uses document type, not document ID.
45
45
  * Get DNT preferences - now based on document type, not individual document ID
46
46
  */
47
- getPreferences(documentId: string): Promise<DNTPreferences | null>;
47
+ getPreferences(_documentId: string): Promise<DNTPreferences | null>;
48
48
  /**
49
49
  * @deprecated Use setFieldDNT instead
50
50
  */
@@ -52,7 +52,7 @@ export declare class DNTStorageAdapter implements DNTStorage {
52
52
  /**
53
53
  * @deprecated Use clearFieldsForType instead
54
54
  */
55
- clearPreferences(documentId: string): Promise<void>;
55
+ clearPreferences(_documentId: string): Promise<void>;
56
56
  }
57
57
  /**
58
58
  * Legacy DNT Storage Adapter using localStorage (deprecated)
@@ -14,6 +14,23 @@ export interface DocumentCreationOptions {
14
14
  parentDocumentId?: string;
15
15
  /** Creation mode: 'draft' creates a draft document, 'published' creates a published document */
16
16
  creationMode?: 'draft' | 'published';
17
+ /**
18
+ * Whether the title was found at the document's top level during extraction.
19
+ * When false, the translated title should not be written as a top-level field.
20
+ * This is tracked in-memory by the plugin, not sent to the translation backend.
21
+ */
22
+ titleFoundAtTopLevel?: boolean;
23
+ /**
24
+ * Whether the body/content was found at the document's top level during extraction.
25
+ * When false, the translated body should not be written as a top-level field.
26
+ * This is tracked in-memory by the plugin, not sent to the translation backend.
27
+ */
28
+ bodyFoundAtTopLevel?: boolean;
29
+ /**
30
+ * The field name where body content was originally found (e.g., 'body', 'content', or nested path).
31
+ * Used to write translated content back to the correct location.
32
+ */
33
+ bodyFieldName?: string;
17
34
  }
18
35
  export interface SlugFallbackInfo {
19
36
  documentId: string;
@@ -69,8 +86,13 @@ export declare class DocumentCreationService {
69
86
  private validateBusinessRules;
70
87
  /**
71
88
  * Check if document has meaningful content
89
+ * Searches both top-level and nested fields for title/body content
72
90
  */
73
91
  private documentHasContent;
92
+ /**
93
+ * Recursively search nested objects for title or body content
94
+ */
95
+ private hasContentInNestedFields;
74
96
  /**
75
97
  * Format error messages with additional context
76
98
  * Requirements: 4.3, 4.4
@@ -96,6 +118,33 @@ export declare class DocumentCreationService {
96
118
  * Requirements: 4.3, 4.4
97
119
  */
98
120
  createRollbackPlan(createdDocumentIds: string[]): RollbackInfo;
121
+ /**
122
+ * Parse a field path that may contain array key selectors
123
+ * e.g., "article.footnotes[_key == \"c82437b14d71\"].footnote"
124
+ *
125
+ * @param fieldPath - The field path (may be from the field key or from the path property)
126
+ * @returns Array of path segments with type information
127
+ */
128
+ private parseFieldPath;
129
+ /**
130
+ * Unflatten fields with complex paths back into nested objects
131
+ * Handles both simple dotted paths and GROQ-style array key paths:
132
+ * - "metadata.version" → { metadata: { version: ... } }
133
+ * - "article.footnotes[_key == \"abc\"].footnote" → { article: { footnotes: [{ _key: "abc", footnote: ... }] } }
134
+ *
135
+ * @param fields - Flattened fields with path keys
136
+ * @param originalDocument - Original document for preserving array structure
137
+ * @returns Unflattened nested object structure
138
+ */
139
+ private unflattenFields;
140
+ /**
141
+ * Get the original nested value from the original document using a dotted path
142
+ *
143
+ * @param document - The original document
144
+ * @param fieldPath - The dotted path to the field
145
+ * @returns The original value at the path, or undefined
146
+ */
147
+ private getOriginalValueByPath;
99
148
  /**
100
149
  * Merge translated content back into original document structure
101
150
  * Requirements: 1.2, 3.2, 3.4
@@ -3,12 +3,17 @@ import { ExtendedDocumentContent } from './contentExtractor';
3
3
  * Field object structure supporting translation-invariant fields
4
4
  */
5
5
  export interface TranslatableField {
6
- /** The actual field value */
6
+ /** The actual field value (serialized as string for the protobuf) */
7
7
  value: unknown;
8
8
  /** Do Not Translate flag - if true, the field should not be translated */
9
9
  dnt?: boolean;
10
10
  /** Type of the field value from Sanity (e.g., 'string', 'number', 'date', 'slug', 'array', 'object') */
11
11
  type?: string;
12
+ /**
13
+ * GROQ-style path for reconstructing the document structure
14
+ * Example: *[_type == "blogPost" && _id == $documentId][0].article.footnotes[_key == "abc123"].footnote
15
+ */
16
+ path?: string;
12
17
  }
13
18
  export interface TranslationRequest {
14
19
  documentId: string;
@@ -92,6 +97,8 @@ export interface TranslationRequestPayload {
92
97
  targetLanguage?: string;
93
98
  /** Array of target locale codes (e.g., ['es', 'fr', 'de']) - for multi-locale support */
94
99
  targetLocales?: string[];
100
+ /** Whether the title was found at the document's top level (false if extracted from nested fields) */
101
+ titleFoundAtTopLevel?: boolean;
95
102
  };
96
103
  }
97
104
  /**
@@ -130,6 +137,11 @@ export interface TranslatedDocument {
130
137
  * Target locale code for this translation (e.g., 'es', 'fr', 'de')
131
138
  */
132
139
  locale?: string;
140
+ /**
141
+ * Whether the title was found at the document's top level
142
+ * When false, the translated title should not be written as a top-level field
143
+ */
144
+ titleFoundAtTopLevel?: boolean;
133
145
  }
134
146
  /**
135
147
  * Response format expected from the translation service
@@ -29,6 +29,8 @@ export interface UnifiedPluginConfig {
29
29
  defaultLocale?: string;
30
30
  /** DNT field map: { [documentType]: { [fieldPath]: boolean } } */
31
31
  dntFieldMap?: Record<string, Record<string, boolean>>;
32
+ /** Debug mode - when enabled, shows additional DNT badges on all fields */
33
+ debugMode?: boolean;
32
34
  /** Configuration version for migrations */
33
35
  version?: string;
34
36
  /** Last updated timestamp */
@@ -120,4 +122,12 @@ export declare class UnifiedConfigStorage {
120
122
  * Get default document creation mode with fallback to default
121
123
  */
122
124
  getDefaultDocumentCreationMode(): Promise<'draft' | 'published'>;
125
+ /**
126
+ * Get debug mode status
127
+ */
128
+ getDebugMode(): Promise<boolean>;
129
+ /**
130
+ * Set debug mode status
131
+ */
132
+ setDebugMode(enabled: boolean): Promise<void>;
123
133
  }
@@ -37,6 +37,8 @@ export interface PluginConfiguration {
37
37
  defaultLocale?: string;
38
38
  /** Document-type-based DNT field configurations */
39
39
  dntFieldConfigurations?: DNTTypeConfig[];
40
+ /** When enabled, displays additional DNT status badges on all fields */
41
+ debugMode?: boolean;
40
42
  /** Configuration schema version for migration purposes */
41
43
  version?: string;
42
44
  /** ISO 8601 timestamp of last configuration update */
@@ -0,0 +1,101 @@
1
+ /**
2
+ * DNT (Do Not Translate) Defaults Utility
3
+ *
4
+ * Determines whether a field should default to DNT based on:
5
+ * - Field type (slug, date, datetime, reference, image, file, geopoint, number, boolean)
6
+ * - Field name patterns (containing "author" or "contributor")
7
+ * - Group/fieldset name patterns (containing "date" or "time")
8
+ */
9
+ /**
10
+ * Field types that are inherently non-translatable
11
+ * These fields contain data that should not be modified by translation
12
+ */
13
+ export declare const NON_TRANSLATABLE_TYPES: string[];
14
+ /**
15
+ * Field name patterns that indicate DNT by default (case-insensitive)
16
+ * Fields with names containing these strings should not be translated
17
+ */
18
+ export declare const DNT_FIELD_NAME_PATTERNS: string[];
19
+ /**
20
+ * Group/fieldset name patterns that indicate all contained fields should be DNT (case-insensitive)
21
+ */
22
+ export declare const DNT_GROUP_NAME_PATTERNS: string[];
23
+ /**
24
+ * Context information about a field's location in the schema
25
+ */
26
+ export interface FieldContext {
27
+ /** The field path (e.g., 'title', 'content.author') */
28
+ fieldPath: string;
29
+ /** The field name (last segment of path) */
30
+ fieldName: string;
31
+ /** The Sanity field type (e.g., 'string', 'date', 'slug') */
32
+ fieldType?: string;
33
+ /** Name of the group this field belongs to, if any */
34
+ groupName?: string;
35
+ /** Name of the fieldset this field belongs to, if any */
36
+ fieldsetName?: string;
37
+ }
38
+ /**
39
+ * Result of DNT default computation
40
+ */
41
+ export interface DNTDefaultResult {
42
+ /** Whether the field should default to DNT */
43
+ shouldBeDNT: boolean;
44
+ /** Reason for the DNT default decision */
45
+ reason: string;
46
+ }
47
+ /**
48
+ * Check if a field type is inherently non-translatable
49
+ *
50
+ * @param fieldType - The Sanity field type
51
+ * @returns True if the field type should not be translated
52
+ */
53
+ export declare function isNonTranslatableType(fieldType: string | undefined): boolean;
54
+ /**
55
+ * Check if a field name matches DNT patterns
56
+ *
57
+ * @param fieldName - The field name to check
58
+ * @returns Object with match status and matched pattern
59
+ */
60
+ export declare function matchesDNTFieldNamePattern(fieldName: string): {
61
+ matches: boolean;
62
+ pattern?: string;
63
+ };
64
+ /**
65
+ * Check if a group or fieldset name matches DNT patterns
66
+ *
67
+ * @param groupOrFieldsetName - The group or fieldset name to check
68
+ * @returns Object with match status and matched pattern
69
+ */
70
+ export declare function matchesDNTGroupPattern(groupOrFieldsetName: string | undefined): {
71
+ matches: boolean;
72
+ pattern?: string;
73
+ };
74
+ /**
75
+ * Compute the default DNT status for a field based on its context
76
+ *
77
+ * Priority order:
78
+ * 1. Non-translatable field type (slug, date, etc.)
79
+ * 2. Field name contains "author" or "contributor"
80
+ * 3. Field is in a group/fieldset containing "date" or "time"
81
+ *
82
+ * @param context - Field context information
83
+ * @returns DNT default result with reason
84
+ */
85
+ export declare function computeDNTDefault(context: FieldContext): DNTDefaultResult;
86
+ /**
87
+ * Determine if a field should show DNT badge based on translatability
88
+ * Only translatable field types should show the badge
89
+ *
90
+ * @param fieldType - The Sanity field type
91
+ * @returns True if the field should show a DNT badge
92
+ */
93
+ export declare function shouldShowDNTBadge(fieldType: string | undefined): boolean;
94
+ /**
95
+ * Get a human-readable description of the DNT status for debugging
96
+ *
97
+ * @param context - Field context information
98
+ * @param storedDNT - The stored DNT status (if any)
99
+ * @returns Debug description
100
+ */
101
+ export declare function getDNTDebugInfo(context: FieldContext, storedDNT?: boolean): string;
@@ -7,8 +7,10 @@ export * from './logger';
7
7
  export * from './validator';
8
8
  export * from './oauthLogger';
9
9
  export * from './oauthErrorFeedback';
10
+ export * from './dntDefaults';
10
11
  export { HtmlFormatter } from './htmlFormatter';
11
12
  export { Logger, LogLevel, defaultLogger } from './logger';
12
13
  export { Validator } from './validator';
13
14
  export { OAuthLogger, createOAuthLogger } from './oauthLogger';
14
15
  export { OAuthErrorFeedback, createOAuthErrorFeedback } from './oauthErrorFeedback';
16
+ export { computeDNTDefault, isNonTranslatableType, matchesDNTFieldNamePattern, matchesDNTGroupPattern, NON_TRANSLATABLE_TYPES, DNT_FIELD_NAME_PATTERNS, DNT_GROUP_NAME_PATTERNS } from './dntDefaults';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easyling/sanity-connector",
3
- "version": "1.2.0",
3
+ "version": "1.3.0-rc.9",
4
4
  "description": "A *Sanity Studio v4* plugin that enables document translation with support for single and bulk operations, using Easyling's AI translation capabilities.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -72,7 +72,7 @@
72
72
  "sanity": "^4.16.0"
73
73
  },
74
74
  "devDependencies": {
75
- "@easyling/sanity": "^1.0.6",
75
+ "@easyling/sanity": "^2.0.0",
76
76
  "@sanity/icons": "^3.7.4",
77
77
  "@sanity/ui": "^3.1.11",
78
78
  "@semantic-release/changelog": "^6.0.3",
@@ -87,16 +87,16 @@
87
87
  "@types/jest": "^29.0.0",
88
88
  "@types/react": "^19.0.0",
89
89
  "@types/react-dom": "^19.0.0",
90
- "@typescript-eslint/eslint-plugin": "^6.21.0",
91
- "@typescript-eslint/parser": "^6.21.0",
92
- "eslint": "^8.0.0",
90
+ "@typescript-eslint/eslint-plugin": "^8.48.1",
91
+ "@typescript-eslint/parser": "^8.48.1",
92
+ "eslint": "^9.39.1",
93
93
  "get-random-values-esm": "^1.0.2",
94
94
  "jest": "^29.0.0",
95
95
  "jest-environment-jsdom": "^30.2.0",
96
96
  "jsdom": "^27.1.0",
97
97
  "react": "^19.0.0",
98
98
  "react-dom": "^19.0.0",
99
- "rimraf": "^5.0.0",
99
+ "rimraf": "^6.1.2",
100
100
  "sanity": "^4.0.0",
101
101
  "ts-jest": "^29.4.5",
102
102
  "ts-loader": "^9.5.4",