@lodashventure/medusa-product-content 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # Medusa Product Content Plugin
2
+
3
+ Enhanced product content management with rich text editor and i18n specifications for Medusa Admin.
4
+
5
+ ## Features
6
+
7
+ - **Rich Text Editor**: TipTap-based editor for long product descriptions
8
+ - **i18n Specifications**: Multilingual key-value product specifications
9
+ - **Version History**: Automatic versioning of content changes
10
+ - **Import/Export**: CSV/JSON import and export for specifications
11
+ - **SEO-Friendly**: HTML sanitization and auto-excerpt generation
12
+ - **Drag & Drop**: Reorder specification groups and items
13
+ - **Locale Support**: Full internationalization support
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @medusajs/admin-product-content
19
+ # or
20
+ yarn add @medusajs/admin-product-content
21
+ ```
22
+
23
+ ## Configuration
24
+
25
+ Add the plugin to your `medusa-config.js`:
26
+
27
+ ```js
28
+ module.exports = {
29
+ // ... other config
30
+ plugins: [
31
+ {
32
+ resolve: '@medusajs/admin-product-content',
33
+ options: {
34
+ locales: [
35
+ { code: 'th-TH', label: 'ไทย', isDefault: true },
36
+ { code: 'en-US', label: 'English' }
37
+ ],
38
+ defaultLocale: 'th-TH',
39
+ versioning: {
40
+ enabled: true,
41
+ maxVersions: 10
42
+ }
43
+ }
44
+ }
45
+ ]
46
+ }
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ ### Admin Interface
52
+
53
+ 1. Navigate to any product in Medusa Admin
54
+ 2. Find the "Product Content" widget on the product detail page
55
+ 3. Click "Manage Content" to open the content editor
56
+ 4. Use the tabs to manage:
57
+ - **Overview**: View product information and content status
58
+ - **Long Description**: Edit rich text content
59
+ - **Specifications**: Manage i18n product specs
60
+ - **Versions**: View and restore content history
61
+
62
+ ### Storefront Integration
63
+
64
+ Fetch product content via the Store API:
65
+
66
+ ```js
67
+ // Fetch product with metadata
68
+ const product = await fetch('/store/products/prod_123?expand=metadata')
69
+ .then(res => res.json())
70
+
71
+ // Access long description
72
+ const longDescription = product.metadata?.long_description
73
+
74
+ // Access specifications
75
+ const specs = product.metadata?.specs
76
+
77
+ // Helper function for locale fallback
78
+ function getSpecValue(item, locale, fallbackLocale) {
79
+ return item.i18n[locale]?.value ||
80
+ item.i18n[fallbackLocale]?.value ||
81
+ ''
82
+ }
83
+ ```
84
+
85
+ ## Data Structure
86
+
87
+ ### Long Description
88
+
89
+ ```json
90
+ {
91
+ "metadata": {
92
+ "long_description": {
93
+ "html": "<h1>Product Title</h1><p>...</p>",
94
+ "richjson": { /* TipTap JSON */ },
95
+ "locale": "th-TH",
96
+ "updated_at": "2025-01-01T00:00:00Z",
97
+ "updated_by": "user@example.com"
98
+ }
99
+ }
100
+ }
101
+ ```
102
+
103
+ ### Specifications
104
+
105
+ ```json
106
+ {
107
+ "metadata": {
108
+ "specs": {
109
+ "default_locale": "th-TH",
110
+ "groups": [
111
+ {
112
+ "key": "electrical",
113
+ "position": 1,
114
+ "i18n": {
115
+ "th-TH": { "label": "ข้อมูลไฟฟ้า" },
116
+ "en-US": { "label": "Electrical" }
117
+ },
118
+ "items": [
119
+ {
120
+ "position": 1,
121
+ "i18n": {
122
+ "th-TH": { "key": "กำลังไฟ", "value": "18 วัตต์" },
123
+ "en-US": { "key": "Power", "value": "18 W" }
124
+ },
125
+ "visible": true
126
+ }
127
+ ]
128
+ }
129
+ ]
130
+ }
131
+ }
132
+ }
133
+ ```
134
+
135
+ ## Import/Export
136
+
137
+ ### CSV Format
138
+
139
+ ```csv
140
+ group_key,group_position,group_label.th-TH,group_label.en-US,item_position,key.th-TH,value.th-TH,key.en-US,value.en-US,visible
141
+ electrical,1,ข้อมูลไฟฟ้า,Electrical,1,กำลังไฟ,18 วัตต์,Power,18 W,true
142
+ electrical,1,ข้อมูลไฟฟ้า,Electrical,2,ความสว่าง,1800 ลูเมน,Brightness,1800 lm,true
143
+ ```
144
+
145
+ ### JSON Format
146
+
147
+ Export and import full specification structure as JSON for complete data preservation.
148
+
149
+ ## API
150
+
151
+ ### Hooks
152
+
153
+ ```typescript
154
+ import { useProductContent } from '@medusajs/admin-product-content'
155
+
156
+ const {
157
+ product,
158
+ longDescription,
159
+ specs,
160
+ versions,
161
+ updateLongDescription,
162
+ updateSpecs,
163
+ restoreVersion
164
+ } = useProductContent({ productId: 'prod_123' })
165
+ ```
166
+
167
+ ### Utilities
168
+
169
+ ```typescript
170
+ import {
171
+ sanitizeHTML,
172
+ generateExcerpt,
173
+ localeUtils,
174
+ getLocalizedSpec
175
+ } from '@medusajs/admin-product-content'
176
+
177
+ // Sanitize HTML
178
+ const clean = sanitizeHTML(dirtyHtml)
179
+
180
+ // Generate excerpt
181
+ const excerpt = generateExcerpt(html, 160)
182
+
183
+ // Get localized value
184
+ const value = getLocalizedSpec(item, 'en-US', 'th-TH', 'value')
185
+
186
+ // Calculate completeness
187
+ const completeness = localeUtils.calculateLocaleCompleteness(specs, 'en-US')
188
+ ```
189
+
190
+ ## Security
191
+
192
+ - All HTML content is sanitized using DOMPurify
193
+ - Scripts and dangerous attributes are removed
194
+ - Only admin users with `product:update` permission can edit content
195
+
196
+ ## License
197
+
198
+ MIT
199
+
200
+ ## Support
201
+
202
+ For issues and feature requests, please visit the [GitHub repository](https://github.com/medusajs/admin-product-content).
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Custom hook for product content management
3
+ */
4
+ import { ProductContentMetadata, LongDescription, LongDescriptionVersion, ProductSpecs } from "../../types";
5
+ export interface UseProductContentOptions {
6
+ productId: string;
7
+ maxVersions?: number;
8
+ autoSanitize?: boolean;
9
+ }
10
+ export interface UseProductContentReturn {
11
+ product: any;
12
+ isLoading: boolean;
13
+ error: Error | null;
14
+ metadata: ProductContentMetadata;
15
+ longDescription: LongDescription | null;
16
+ specs: ProductSpecs | null;
17
+ versions: LongDescriptionVersion[];
18
+ updateLongDescription: (html: string, richjson?: any, locale?: string) => Promise<void>;
19
+ updateSpecs: (specs: ProductSpecs) => Promise<void>;
20
+ createVersion: (description: LongDescription) => Promise<void>;
21
+ restoreVersion: (version: number) => Promise<void>;
22
+ deleteVersion: (version: number) => Promise<void>;
23
+ updateMetadata: (updates: Partial<ProductContentMetadata>) => Promise<void>;
24
+ }
25
+ export declare function useProductContent(options: UseProductContentOptions): UseProductContentReturn;
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ /**
3
+ * Custom hook for product content management
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.useProductContent = useProductContent;
7
+ const react_1 = require("react");
8
+ const react_query_1 = require("@tanstack/react-query");
9
+ const sdk_1 = require("../lib/sdk");
10
+ const sanitize_1 = require("../utils/sanitize");
11
+ function useProductContent(options) {
12
+ const { productId, maxVersions = 10, autoSanitize = true } = options;
13
+ // Fetch product with expanded metadata
14
+ const { data: product, isLoading, error, } = (0, react_query_1.useQuery)({
15
+ queryKey: ["product", productId],
16
+ queryFn: async () => {
17
+ const response = await sdk_1.sdk.admin.product.retrieve(productId, {
18
+ fields: "*variants,*variants.prices,*variants.options,*options,*images,*collection,*categories,*metadata,*tags",
19
+ });
20
+ return response.product;
21
+ },
22
+ });
23
+ // Update mutation
24
+ const updateProduct = (0, react_query_1.useMutation)({
25
+ mutationFn: async (data) => {
26
+ return await sdk_1.sdk.admin.product.update(productId, data);
27
+ },
28
+ });
29
+ // Extract metadata
30
+ const metadata = (0, react_1.useMemo)(() => {
31
+ if (!product || !product.metadata)
32
+ return {};
33
+ return product.metadata;
34
+ }, [product]);
35
+ const longDescription = (0, react_1.useMemo)(() => {
36
+ return metadata.long_description || null;
37
+ }, [metadata.long_description]);
38
+ const specs = (0, react_1.useMemo)(() => {
39
+ return metadata.specs || null;
40
+ }, [metadata.specs]);
41
+ const versions = (0, react_1.useMemo)(() => {
42
+ return metadata.long_description_versions || [];
43
+ }, [metadata.long_description_versions]);
44
+ // Update long description
45
+ const updateLongDescription = (0, react_1.useCallback)(async (html, richjson, locale = "th-TH") => {
46
+ const sanitizedHtml = autoSanitize ? (0, sanitize_1.sanitizeHTML)(html) : html;
47
+ const newDescription = {
48
+ html: sanitizedHtml,
49
+ richjson,
50
+ locale,
51
+ updated_at: new Date().toISOString(),
52
+ updated_by: "current_user", // TODO: Get from auth context
53
+ };
54
+ // Create version if exists
55
+ let newVersions = [...versions];
56
+ if (longDescription) {
57
+ const version = {
58
+ ...longDescription,
59
+ version: versions.length + 1,
60
+ created_at: longDescription.updated_at || new Date().toISOString(),
61
+ };
62
+ newVersions = [version, ...versions].slice(0, maxVersions);
63
+ }
64
+ await updateProduct.mutateAsync({
65
+ metadata: {
66
+ ...metadata,
67
+ long_description: newDescription,
68
+ long_description_versions: newVersions,
69
+ content_last_updated: new Date().toISOString(),
70
+ content_updated_by: "current_user",
71
+ },
72
+ });
73
+ }, [
74
+ autoSanitize,
75
+ longDescription,
76
+ maxVersions,
77
+ metadata,
78
+ updateProduct,
79
+ versions,
80
+ ]);
81
+ // Update specifications
82
+ const updateSpecs = (0, react_1.useCallback)(async (newSpecs) => {
83
+ await updateProduct.mutateAsync({
84
+ metadata: {
85
+ ...metadata,
86
+ specs: newSpecs,
87
+ content_last_updated: new Date().toISOString(),
88
+ content_updated_by: "current_user",
89
+ },
90
+ });
91
+ }, [metadata, updateProduct]);
92
+ // Create version
93
+ const createVersion = (0, react_1.useCallback)(async (description) => {
94
+ const version = {
95
+ ...description,
96
+ version: versions.length + 1,
97
+ created_at: new Date().toISOString(),
98
+ };
99
+ const newVersions = [version, ...versions].slice(0, maxVersions);
100
+ await updateProduct.mutateAsync({
101
+ metadata: {
102
+ ...metadata,
103
+ long_description_versions: newVersions,
104
+ },
105
+ });
106
+ }, [maxVersions, metadata, updateProduct, versions]);
107
+ // Restore version
108
+ const restoreVersion = (0, react_1.useCallback)(async (versionNumber) => {
109
+ const version = versions.find((v) => v.version === versionNumber);
110
+ if (!version) {
111
+ throw new Error(`Version ${versionNumber} not found`);
112
+ }
113
+ const restoredDescription = {
114
+ html: version.html,
115
+ richjson: version.richjson,
116
+ locale: version.locale,
117
+ updated_at: new Date().toISOString(),
118
+ updated_by: "current_user",
119
+ };
120
+ await updateProduct.mutateAsync({
121
+ metadata: {
122
+ ...metadata,
123
+ long_description: restoredDescription,
124
+ content_last_updated: new Date().toISOString(),
125
+ content_updated_by: "current_user",
126
+ },
127
+ });
128
+ }, [metadata, updateProduct, versions]);
129
+ // Delete version
130
+ const deleteVersion = (0, react_1.useCallback)(async (versionNumber) => {
131
+ const newVersions = versions.filter((v) => v.version !== versionNumber);
132
+ await updateProduct.mutateAsync({
133
+ metadata: {
134
+ ...metadata,
135
+ long_description_versions: newVersions,
136
+ },
137
+ });
138
+ }, [metadata, updateProduct, versions]);
139
+ // Update metadata
140
+ const updateMetadata = (0, react_1.useCallback)(async (updates) => {
141
+ await updateProduct.mutateAsync({
142
+ metadata: {
143
+ ...metadata,
144
+ ...updates,
145
+ content_last_updated: new Date().toISOString(),
146
+ content_updated_by: "current_user",
147
+ },
148
+ });
149
+ }, [metadata, updateProduct]);
150
+ return {
151
+ product,
152
+ isLoading,
153
+ error,
154
+ metadata,
155
+ longDescription,
156
+ specs,
157
+ versions,
158
+ updateLongDescription,
159
+ updateSpecs,
160
+ createVersion,
161
+ restoreVersion,
162
+ deleteVersion,
163
+ updateMetadata,
164
+ };
165
+ }
@@ -0,0 +1,2 @@
1
+ import Medusa from "@medusajs/js-sdk";
2
+ export declare const sdk: Medusa;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.sdk = void 0;
7
+ // @ts-nocheck
8
+ const js_sdk_1 = __importDefault(require("@medusajs/js-sdk"));
9
+ exports.sdk = new js_sdk_1.default({
10
+ baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
11
+ debug: import.meta.env.DEV,
12
+ auth: {
13
+ type: "session",
14
+ },
15
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Product Content Management Page
3
+ * Main listing page accessible from admin menu
4
+ */
5
+ import React from "react";
6
+ declare const ProductContentPage: () => React.JSX.Element;
7
+ export declare const config: import("@medusajs/admin-sdk").RouteConfig;
8
+ export default ProductContentPage;
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.config = void 0;
37
+ /**
38
+ * Product Content Management Page
39
+ * Main listing page accessible from admin menu
40
+ */
41
+ // @ts-nocheck
42
+ const react_1 = __importStar(require("react"));
43
+ const ui_1 = require("@medusajs/ui");
44
+ const icons_1 = require("@medusajs/icons");
45
+ const admin_sdk_1 = require("@medusajs/admin-sdk");
46
+ const sdk_1 = require("../../lib/sdk");
47
+ const ProductContentPage = () => {
48
+ const [products, setProducts] = (0, react_1.useState)([]);
49
+ const [loading, setLoading] = (0, react_1.useState)(true);
50
+ const [error, setError] = (0, react_1.useState)(null);
51
+ const [searchQuery, setSearchQuery] = (0, react_1.useState)("");
52
+ (0, react_1.useEffect)(() => {
53
+ fetchProducts();
54
+ }, []);
55
+ const fetchProducts = async () => {
56
+ setLoading(true);
57
+ setError(null);
58
+ try {
59
+ const response = await sdk_1.sdk.client.fetch("/admin/products?limit=100", {
60
+ credentials: "include",
61
+ });
62
+ if (response.ok) {
63
+ const data = await response.json();
64
+ setProducts(data.products || []);
65
+ }
66
+ else {
67
+ setError("Failed to fetch products");
68
+ }
69
+ }
70
+ catch (err) {
71
+ setError("Error fetching products");
72
+ console.error("Error fetching products:", err);
73
+ }
74
+ finally {
75
+ setLoading(false);
76
+ }
77
+ };
78
+ const handleManageContent = (productId) => {
79
+ window.location.href = `/app/products/${productId}`;
80
+ };
81
+ const filteredProducts = products.filter((product) => {
82
+ if (!searchQuery)
83
+ return true;
84
+ const query = searchQuery.toLowerCase();
85
+ return (product.title?.toLowerCase().includes(query) ||
86
+ product.handle?.toLowerCase().includes(query));
87
+ });
88
+ const hasContent = (product) => {
89
+ return product.metadata?.long_description || product.metadata?.specs;
90
+ };
91
+ const getContentStatus = (product) => {
92
+ const hasLongDesc = !!product.metadata?.long_description;
93
+ const hasSpecs = !!product.metadata?.specs;
94
+ if (hasLongDesc && hasSpecs)
95
+ return { label: "Complete", color: "green" };
96
+ if (hasLongDesc || hasSpecs)
97
+ return { label: "Partial", color: "orange" };
98
+ return { label: "Empty", color: "grey" };
99
+ };
100
+ if (loading) {
101
+ return (react_1.default.createElement(ui_1.Container, null,
102
+ react_1.default.createElement("div", { className: "flex h-64 items-center justify-center" },
103
+ react_1.default.createElement(ui_1.Text, null, "Loading products..."))));
104
+ }
105
+ if (error) {
106
+ return (react_1.default.createElement(ui_1.Container, null,
107
+ react_1.default.createElement(ui_1.Alert, { variant: "error", dismissible: true }, error)));
108
+ }
109
+ return (react_1.default.createElement(ui_1.Container, null,
110
+ react_1.default.createElement("div", { className: "mb-8" },
111
+ react_1.default.createElement("div", { className: "flex items-center justify-between mb-4" },
112
+ react_1.default.createElement("div", null,
113
+ react_1.default.createElement(ui_1.Heading, { level: "h1", className: "mb-2" }, "Product Content"),
114
+ react_1.default.createElement(ui_1.Text, { className: "text-ui-fg-subtle" }, "Manage long descriptions and specifications for all products"))),
115
+ react_1.default.createElement("div", { className: "flex items-center gap-4" },
116
+ react_1.default.createElement("div", { className: "relative flex-1 max-w-md" },
117
+ react_1.default.createElement(ui_1.Input, { placeholder: "Search products...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), className: "pl-10" }),
118
+ react_1.default.createElement(icons_1.MagnifyingGlass, { className: "absolute left-3 top-1/2 -translate-y-1/2 text-ui-fg-muted" })))),
119
+ filteredProducts.length === 0 ? (react_1.default.createElement("div", { className: "flex h-64 flex-col items-center justify-center rounded-lg border-2 border-dashed border-ui-border-base" },
120
+ react_1.default.createElement(icons_1.SquareTwoStack, { className: "mb-4 text-ui-fg-subtle", style: { width: "3rem", height: "3rem" } }),
121
+ react_1.default.createElement(ui_1.Text, { className: "mb-2 text-lg font-medium" }, searchQuery ? "No products found" : "No products yet"),
122
+ react_1.default.createElement(ui_1.Text, { className: "mb-4 text-ui-fg-subtle" }, searchQuery
123
+ ? "Try adjusting your search query"
124
+ : "Create products to manage their content"))) : (react_1.default.createElement("div", { className: "overflow-hidden rounded-lg border" },
125
+ react_1.default.createElement(ui_1.Table, null,
126
+ react_1.default.createElement(ui_1.Table.Header, null,
127
+ react_1.default.createElement(ui_1.Table.Row, null,
128
+ react_1.default.createElement(ui_1.Table.HeaderCell, null, "Product"),
129
+ react_1.default.createElement(ui_1.Table.HeaderCell, null, "Handle"),
130
+ react_1.default.createElement(ui_1.Table.HeaderCell, null, "Status"),
131
+ react_1.default.createElement(ui_1.Table.HeaderCell, null, "Content Status"),
132
+ react_1.default.createElement(ui_1.Table.HeaderCell, { className: "text-right" }, "Actions"))),
133
+ react_1.default.createElement(ui_1.Table.Body, null, filteredProducts.map((product) => {
134
+ const contentStatus = getContentStatus(product);
135
+ return (react_1.default.createElement(ui_1.Table.Row, { key: product.id },
136
+ react_1.default.createElement(ui_1.Table.Cell, null,
137
+ react_1.default.createElement(ui_1.Text, { className: "font-medium" }, product.title)),
138
+ react_1.default.createElement(ui_1.Table.Cell, null,
139
+ react_1.default.createElement(ui_1.Badge, { color: "blue" }, product.handle || "—")),
140
+ react_1.default.createElement(ui_1.Table.Cell, null,
141
+ react_1.default.createElement(ui_1.Badge, { color: product.status === "published" ? "green" : "grey" }, product.status || "draft")),
142
+ react_1.default.createElement(ui_1.Table.Cell, null,
143
+ react_1.default.createElement("div", { className: "flex items-center gap-2" },
144
+ react_1.default.createElement(ui_1.Badge, { color: contentStatus.color }, contentStatus.label),
145
+ product.metadata?.long_description && (react_1.default.createElement(ui_1.Badge, { color: "green", size: "xsmall" }, "Description")),
146
+ product.metadata?.specs && (react_1.default.createElement(ui_1.Badge, { color: "green", size: "xsmall" }, "Specs")))),
147
+ react_1.default.createElement(ui_1.Table.Cell, null,
148
+ react_1.default.createElement("div", { className: "flex items-center justify-end" },
149
+ react_1.default.createElement(ui_1.Button, { variant: "secondary", size: "small", onClick: () => handleManageContent(product.id) },
150
+ react_1.default.createElement(icons_1.PencilSquare, { className: "mr-1" }),
151
+ "View Product")))));
152
+ })))))));
153
+ };
154
+ exports.config = (0, admin_sdk_1.defineRouteConfig)({
155
+ label: "Product Content",
156
+ icon: icons_1.SquareTwoStack,
157
+ });
158
+ exports.default = ProductContentPage;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Import/Export utility for specifications
3
+ */
4
+ import { ProductSpecs, ImportValidationResult } from '../../types';
5
+ /**
6
+ * Export specifications to CSV
7
+ */
8
+ export declare function exportSpecsToCSV(specs: ProductSpecs): string;
9
+ /**
10
+ * Export specifications to JSON
11
+ */
12
+ export declare function exportSpecsToJSON(specs: ProductSpecs): string;
13
+ /**
14
+ * Import specifications from CSV
15
+ */
16
+ export declare function importSpecsFromCSV(csvContent: string, defaultLocale?: string): Promise<{
17
+ specs: ProductSpecs | null;
18
+ validation: ImportValidationResult;
19
+ }>;
20
+ /**
21
+ * Import specifications from JSON
22
+ */
23
+ export declare function importSpecsFromJSON(jsonContent: string): Promise<{
24
+ specs: ProductSpecs | null;
25
+ validation: ImportValidationResult;
26
+ }>;
27
+ /**
28
+ * Merge imported specs with existing specs
29
+ */
30
+ export declare function mergeSpecs(existing: ProductSpecs, imported: ProductSpecs, strategy?: 'replace' | 'merge' | 'append'): ProductSpecs;