From af6047fb4d3e53772ef6df05b4e1b1a8b3cc57b2 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 5 Oct 2026 06:01:05 +0700 Subject: [PATCH] x --- .../page-builder/BlockTranslationModal.tsx | 2040 +++++++++++++++++ apps/admin/src/pages/PageBuilderPage.tsx | 64 +- 2 files changed, 2095 insertions(+), 9 deletions(-) create mode 100644 apps/admin/src/components/page-builder/BlockTranslationModal.tsx diff --git a/apps/admin/src/components/page-builder/BlockTranslationModal.tsx b/apps/admin/src/components/page-builder/BlockTranslationModal.tsx new file mode 100644 index 0000000..cbd60a4 --- /dev/null +++ b/apps/admin/src/components/page-builder/BlockTranslationModal.tsx @@ -0,0 +1,2040 @@ +import React, { useState, useMemo } from 'react'; +import { + X, + Copy, + Check, + Download, + Upload, + FileJson, + AlertCircle, + CheckCircle2, + Sparkles, + Bot, + Languages, + ArrowRight, + Layers, + HelpCircle, + Bell, + Award, + Grid, + FileText, + Image as ImageIcon, + BookOpen, + Bookmark, + FileDown, + Table as TableIcon, +} from 'lucide-react'; +import type { PageSection } from '@homaroma/types'; + +export const SUPPORTED_LOCALES = [ + { code: 'th', label: 'TH', full: 'ภาษาไทย' }, + { code: 'en', label: 'EN', full: 'English' }, + { code: 'ja', label: 'JA', full: '日本語' }, + { code: 'zh', label: 'ZH', full: '中文' }, + { code: 'vi', label: 'VI', full: 'Tiếng Việt' }, + { code: 'ko', label: 'KO', full: '한국어' }, + { code: 'lo', label: 'LO', full: 'ພາສາລາວ' }, + { code: 'it', label: 'IT', full: 'Italiano' }, +] as const; + +export interface TranslatableFieldDef { + key: string; + label: string; + multiline?: boolean; +} + +export const BLOCK_TRANSLATABLE_FIELDS: Record = { + // Hero & Banners + HERO: [ + { key: 'tagline', label: 'Tagline / Sub-header' }, + { key: 'title', label: 'Main Title (supports [brackets] for accent)' }, + { key: 'description', label: 'Description Copy', multiline: true }, + { key: 'buttonText', label: 'Primary Button Text (CTA)' }, + ], + HERO_V2: [ + { key: 'tagline', label: 'Tagline / Sub-header' }, + { key: 'title', label: 'Main Title (supports [brackets] for accent)' }, + { key: 'description', label: 'Description Copy', multiline: true }, + { key: 'primaryButtonText', label: 'Primary Button Text (CTA)' }, + { key: 'secondaryButtonText', label: 'Secondary Button Text (Optional)' }, + ], + HERO_VIDEO: [ + { key: 'tagline', label: 'Tagline / Sub-header' }, + { key: 'title', label: 'Main Title (supports [brackets] for accent)' }, + { key: 'description', label: 'Description Copy', multiline: true }, + { key: 'buttonText', label: 'Primary Button Text (CTA)' }, + { key: 'secondaryButtonText', label: 'Secondary Button Text (Optional)' }, + ], + HERO_ARTICLE: [ + { key: 'title', label: 'Article Title' }, + { key: 'subtitle', label: 'Article Subtitle' }, + { key: 'author', label: 'Author / Writer' }, + { key: 'category', label: 'Category Label' }, + ], + HERO_MINIMAL: [ + { key: 'tagline', label: 'Tagline' }, + { key: 'title', label: 'Main Title' }, + ], + HERO_SPLIT_MINIMAL: [ + { key: 'title', label: 'Main Title' }, + { key: 'subtitle', label: 'Subtitle' }, + ], + HERO_VIDEO_MINIMAL: [ + { key: 'tagline', label: 'Tagline' }, + { key: 'title', label: 'Main Title' }, + ], + BANNER: [ + { key: 'title', label: 'Banner Title' }, + { key: 'description', label: 'Banner Description', multiline: true }, + { key: 'buttonText', label: 'Button Text' }, + ], + BANNER_MINIMAL: [ + { key: 'title', label: 'Banner Title' }, + { key: 'subtitle', label: 'Banner Subtitle' }, + ], + + // Editorial & Storytelling (Group 1) + EDITORIAL_TILE: [ + { key: 'title', label: 'Story Title' }, + { key: 'description', label: 'Story Description', multiline: true }, + { key: 'buttonText', label: 'Button Text (CTA)' }, + ], + TEXT: [ + { key: 'title', label: 'Title / Heading' }, + { key: 'content', label: 'Text Body Copy', multiline: true }, + ], + RICH_TEXT: [ + { key: 'title', label: 'Section Title' }, + { key: 'content', label: 'Rich Text / Markdown Content', multiline: true }, + ], + NEWSLETTER: [ + { key: 'title', label: 'Newsletter Headline' }, + { key: 'description', label: 'Invitation Description', multiline: true }, + { key: 'buttonText', label: 'Subscribe Button' }, + { key: 'placeholderText', label: 'Email Input Placeholder' }, + { key: 'privacyText', label: 'Privacy Policy Consent Text' }, + ], + QUOTE_CALLOUT: [ + { key: 'quote', label: 'Quote / Highlighted Words', multiline: true }, + { key: 'author', label: 'Author / Attributed Person' }, + { key: 'role', label: 'Role / Title' }, + ], + SCENT_NOTES_BREAKDOWN: [ + { key: 'title', label: 'Fragrance Pyramid Title' }, + { key: 'description', label: 'Scent Description', multiline: true }, + { key: 'topNotes', label: 'Top Notes (กลิ่นเปิด)' }, + { key: 'heartNotes', label: 'Heart Notes (กลิ่นกลาง)' }, + { key: 'middleNotes', label: 'Middle Notes' }, + { key: 'baseNotes', label: 'Base Notes (กลิ่นฐาน)' }, + ], + CONTACT_INFO: [ + { key: 'title', label: 'Contact Section Title' }, + { key: 'address', label: 'Physical Store Address', multiline: true }, + { key: 'hours', label: 'Operating Business Hours' }, + ], + CONTACT_FORM: [ + { key: 'title', label: 'Contact Form Heading' }, + { key: 'subtitle', label: 'Form Instruction / Subtitle' }, + ], + ARTICLE_CTA: [ + { key: 'title', label: 'CTA Heading' }, + { key: 'description', label: 'CTA Description', multiline: true }, + { key: 'primaryText', label: 'Primary Button Text' }, + { key: 'secondaryText', label: 'Secondary Button Text' }, + ], + ARTICLE_VIDEO: [ + { key: 'title', label: 'Video Heading' }, + { key: 'caption', label: 'Video Caption / Subtitle' }, + ], + + // Catalog & Product Showcases (Group 1) + PRODUCTS: [ + { key: 'title', label: 'Section Title' }, + { key: 'subtitle', label: 'Section Subtitle' }, + ], + CATEGORIES: [ + { key: 'title', label: 'Section Title' }, + { key: 'subtitle', label: 'Section Subtitle' }, + ], + PRODUCT_CAROUSEL: [ + { key: 'title', label: 'Carousel Title' }, + { key: 'description', label: 'Carousel Subtitle / Description', multiline: true }, + ], + PRODUCT_MAIN: [ + { key: 'title', label: 'Section Title' }, + { key: 'subtitle', label: 'Section Subtitle' }, + ], + PRODUCT_DETAILS: [ + { key: 'title', label: 'Section Title' }, + { key: 'subtitle', label: 'Section Subtitle' }, + ], + PRODUCT_SPECS: [ + { key: 'title', label: 'Section Title' }, + { key: 'subtitle', label: 'Section Subtitle' }, + ], + PRODUCT_REVIEWS: [ + { key: 'title', label: 'Section Title' }, + { key: 'subtitle', label: 'Section Subtitle' }, + ], + RELATED_PRODUCTS: [ + { key: 'title', label: 'Section Title' }, + { key: 'subtitle', label: 'Section Subtitle' }, + ], + + // Deep Magazine / Journal Editorial (Group 3 Flat blocks) + ARTICLE_HEADER: [ + { key: 'title', label: 'Article Title' }, + { key: 'subtitle', label: 'Article Subtitle' }, + { key: 'category', label: 'Journal Category' }, + { key: 'author', label: 'Author / Contributor' }, + { key: 'imageAlt', label: 'Cover Image Alt Text' }, + ], + ARTICLE_MEDIA: [ + { key: 'heading', label: 'Media Paragraph Heading' }, + { key: 'body', label: 'Editorial Paragraph Body Copy', multiline: true }, + { key: 'imageAlt', label: 'Image Alt Text' }, + { key: 'imageCaption', label: 'Image Caption / Credit' }, + ], +}; + +export const CAROUSEL_SLIDE_TRANSLATABLE_FIELDS: TranslatableFieldDef[] = [ + { key: 'title', label: 'Slide Title (supports [brackets])' }, + { key: 'subtitle', label: 'Slide Subtitle / Description', multiline: true }, + { key: 'buttonText', label: 'Button Text (CTA)' }, + { key: 'alt', label: 'Image Alt Text' }, +]; + +export const COMPLEX_BLOCK_TYPES = [ + 'HERO_CAROUSEL', + 'ANNOUNCEMENT_BAR', + 'ANNOUNCEMENT_BAR_MINIMAL', + 'USP_BAR', + 'USP_MINIMAL', + 'EDITORIAL_CAROUSEL', + 'FEATURED_TILES', + 'ACCORDION', + // Group 3 Deep Editorial Blocks + 'ARTICLE_GALLERY', + 'ARTICLE_FACTS', + 'ARTICLE_REFERENCES', + 'ARTICLE_DOWNLOAD', + 'ARTICLE_TABLE', +] as const; + +export type ComplexBlockType = typeof COMPLEX_BLOCK_TYPES[number]; + +export function isTranslatableBlock(type?: string): boolean { + if (!type) return false; + const upper = type.toUpperCase(); + return Boolean(BLOCK_TRANSLATABLE_FIELDS[upper]) || COMPLEX_BLOCK_TYPES.includes(upper as any); +} + +export type SlideTranslations = Record>; + +export interface ParsedAccordionCategory { + name: Record; + questions: Array<{ + q: Record; + a: Record; + }>; +} + +export interface ParsedTableData { + columns: SlideTranslations[]; + rows: Array<{ + label: Record; + cells: SlideTranslations[]; + }>; +} + +export interface ParsedImportResult { + error?: string; + mode?: string; + isCarousel?: boolean; + slides?: SlideTranslations[]; + detectedSlidesCount?: number; + data?: Record>; + detectedFields?: string[]; + languageCounts?: Record; + isFormatB?: boolean; + statsText?: string; + title?: Record; + subtitle?: Record; + description?: Record; + note?: Record; + categories?: ParsedAccordionCategory[]; + items?: SlideTranslations[]; + tiles?: SlideTranslations[]; + images?: SlideTranslations[]; + files?: SlideTranslations[]; + table?: ParsedTableData; +} + +function getLocalesObject(val: any): Record { + const result: Record = {}; + SUPPORTED_LOCALES.forEach(({ code }) => { + if (typeof val === 'string') { + result[code] = code === 'th' ? val : ''; + } else if (typeof val === 'object' && val !== null) { + result[code] = val[code] || ''; + } else { + result[code] = ''; + } + }); + return result; +} + +function mergeLocalesObject(existing: any, incoming: any): Record { + const base: Record = + typeof existing === 'object' && existing !== null + ? { ...existing } + : typeof existing === 'string' + ? { th: existing } + : {}; + + if (typeof incoming === 'object' && incoming !== null) { + const map = incoming.text && typeof incoming.text === 'object' ? incoming.text : incoming; + SUPPORTED_LOCALES.forEach(({ code }) => { + if (typeof map[code] === 'string' && map[code].trim()) { + base[code] = map[code].trim(); + } + }); + } else if (typeof incoming === 'string' && incoming.trim()) { + base.th = incoming.trim(); + } + return base; +} + +function extractFieldTranslations(obj: any, fieldKey: string): Record { + const res: Record = {}; + if (!obj || typeof obj !== 'object') return res; + + if (typeof obj[fieldKey] === 'object' && obj[fieldKey] !== null) { + SUPPORTED_LOCALES.forEach(({ code }) => { + res[code] = obj[fieldKey][code] || ''; + }); + return res; + } + + SUPPORTED_LOCALES.forEach(({ code }) => { + if (obj[code] && typeof obj[code] === 'object' && obj[code][fieldKey]) { + res[code] = obj[code][fieldKey] || ''; + } + }); + + if (typeof obj[fieldKey] === 'string') { + res.th = obj[fieldKey]; + } + return res; +} + +interface BlockTranslationModalProps { + isOpen: boolean; + onClose: () => void; + section: Partial | null; + pageSlug?: string; + initialTab?: 'export' | 'import'; + onApplyTranslations: (updatedPayload: any) => void; +} + +export default function BlockTranslationModal({ + isOpen, + onClose, + section, + pageSlug: _pageSlug = 'home', + initialTab = 'export', + onApplyTranslations, +}: BlockTranslationModalProps) { + const [activeTab, setActiveTab] = useState<'export' | 'import'>(initialTab); + const [copiedJson, setCopiedJson] = useState(false); + const [copiedPrompt, setCopiedPrompt] = useState(false); + const [importText, setImportText] = useState(''); + const [parseError, setParseError] = useState(null); + const [successMsg, setSuccessMsg] = useState(null); + + React.useEffect(() => { + if (isOpen) { + setActiveTab(initialTab); + setParseError(null); + setSuccessMsg(null); + } + }, [isOpen, initialTab]); + + const blockType = (section?.type || 'HERO_VIDEO').toUpperCase(); + const payload = section?.payload || {}; + + const mode = useMemo(() => { + switch (blockType) { + case 'HERO_CAROUSEL': + return 'hero_carousel'; + case 'ACCORDION': + return 'accordion'; + case 'ANNOUNCEMENT_BAR': + case 'ANNOUNCEMENT_BAR_MINIMAL': + return 'announcement'; + case 'USP_BAR': + return 'usp_bar'; + case 'USP_MINIMAL': + return 'usp_minimal'; + case 'EDITORIAL_CAROUSEL': + return 'editorial_carousel'; + case 'FEATURED_TILES': + return 'featured_tiles'; + // Group 3 Deep Editorial + case 'ARTICLE_GALLERY': + return 'article_gallery'; + case 'ARTICLE_FACTS': + return 'article_facts'; + case 'ARTICLE_REFERENCES': + return 'article_references'; + case 'ARTICLE_DOWNLOAD': + return 'article_download'; + case 'ARTICLE_TABLE': + return 'article_table'; + default: + return 'flat'; + } + }, [blockType]); + + const fields = useMemo(() => BLOCK_TRANSLATABLE_FIELDS[blockType] || [], [blockType]); + + // Export JSON construction per mode + const exportData = useMemo(() => { + if (mode === 'hero_carousel') { + const slides = Array.isArray(payload.slides) ? payload.slides : []; + return { + slides: slides.map((slide: any, idx: number) => { + const slideData: Record = { slide: idx + 1 }; + CAROUSEL_SLIDE_TRANSLATABLE_FIELDS.forEach(({ key }) => { + slideData[key] = getLocalesObject(slide[key]); + }); + return slideData; + }), + }; + } + + if (mode === 'accordion') { + const categories = Array.isArray(payload.categories) ? payload.categories : []; + return { + title: getLocalesObject(payload.title), + categories: categories.map((cat: any, cIdx: number) => ({ + category: cIdx + 1, + name: getLocalesObject(cat.name), + questions: (Array.isArray(cat.questions) ? cat.questions : []).map((q: any, qIdx: number) => ({ + question: qIdx + 1, + q: getLocalesObject(q.q), + a: getLocalesObject(q.a), + })), + })), + }; + } + + if (mode === 'announcement') { + const slides = Array.isArray(payload.slides) ? payload.slides : []; + return { + slides: slides.map((s: any, idx: number) => ({ + slide: idx + 1, + text: getLocalesObject(s), + })), + }; + } + + if (mode === 'usp_bar') { + const items = Array.isArray(payload.items) ? payload.items : []; + return { + items: items.map((item: any, idx: number) => ({ + item: idx + 1, + title: getLocalesObject(item.title), + subtitle: getLocalesObject(item.subtitle), + })), + }; + } + + if (mode === 'usp_minimal') { + const items = Array.isArray(payload.items) ? payload.items : []; + return { + items: items.map((item: any, idx: number) => ({ + item: idx + 1, + text: getLocalesObject(item), + })), + }; + } + + if (mode === 'editorial_carousel') { + const slides = Array.isArray(payload.slides) ? payload.slides : []; + return { + title: getLocalesObject(payload.title), + slides: slides.map((s: any, idx: number) => ({ + slide: idx + 1, + category: getLocalesObject(s.category), + title: getLocalesObject(s.title), + description: getLocalesObject(s.description), + buttonText: getLocalesObject(s.buttonText), + })), + }; + } + + if (mode === 'featured_tiles') { + const tiles = Array.isArray(payload.tiles) ? payload.tiles : []; + return { + title: getLocalesObject(payload.title), + subtitle: getLocalesObject(payload.subtitle), + tiles: tiles.map((t: any, idx: number) => ({ + tile: idx + 1, + title: getLocalesObject(t.title), + description: getLocalesObject(t.description), + buttonText: getLocalesObject(t.buttonText), + })), + }; + } + + // Group 3 Deep Editorial Exports + if (mode === 'article_gallery') { + const images = Array.isArray(payload.images) ? payload.images : []; + return { + title: getLocalesObject(payload.title), + images: images.map((img: any, idx: number) => ({ + image: idx + 1, + alt: getLocalesObject(img.alt), + caption: getLocalesObject(img.caption), + })), + }; + } + + if (mode === 'article_facts') { + const items = Array.isArray(payload.items) ? payload.items : []; + return { + title: getLocalesObject(payload.title), + items: items.map((it: any, idx: number) => ({ + fact: idx + 1, + title: getLocalesObject(it.title), + description: getLocalesObject(it.description), + })), + }; + } + + if (mode === 'article_references') { + const items = Array.isArray(payload.items) ? payload.items : []; + return { + title: getLocalesObject(payload.title), + items: items.map((it: any, idx: number) => ({ + reference: idx + 1, + title: getLocalesObject(it.title), + note: getLocalesObject(it.note), + })), + }; + } + + if (mode === 'article_download') { + const files = Array.isArray(payload.files) ? payload.files : []; + return { + title: getLocalesObject(payload.title), + description: getLocalesObject(payload.description), + files: files.map((f: any, idx: number) => ({ + file: idx + 1, + title: getLocalesObject(f.title), + })), + }; + } + + if (mode === 'article_table') { + const columns = Array.isArray(payload.columns) ? payload.columns : []; + const rows = Array.isArray(payload.rows) ? payload.rows : []; + return { + title: getLocalesObject(payload.title), + columns: columns.map((col: any, idx: number) => ({ + column: idx + 1, + name: getLocalesObject(col), + })), + rows: rows.map((r: any, rIdx: number) => ({ + row: rIdx + 1, + label: getLocalesObject(r.label), + cells: (Array.isArray(r.cells) ? r.cells : []).map((cell: any, cIdx: number) => ({ + col: cIdx + 1, + text: getLocalesObject(cell), + })), + })), + note: getLocalesObject(payload.note), + }; + } + + // Flat mode + const flatResult: Record> = {}; + fields.forEach(({ key }) => { + flatResult[key] = getLocalesObject(payload[key]); + }); + return flatResult; + }, [mode, payload, fields]); + + const exportJsonString = useMemo(() => { + return JSON.stringify(exportData, null, 2); + }, [exportData]); + + // AI Prompt custom per mode + const aiPromptString = useMemo(() => { + let modeInstructions = ''; + + if (mode === 'accordion') { + modeInstructions = `บล็อกนี้คือ FAQ Accordion (คำถามที่พบบ่อย): +1. รักษาโครงสร้าง Array "categories" และ "questions" ให้ครบถ้วน +2. แปลฟิลด์ "name" (ชื่อหมวดหมู่), "q" (คำถาม), และ "a" (คำตอบ) รวมถึง "title" ของบล็อก`; + } else if (mode === 'announcement') { + modeInstructions = `บล็อกนี้คือ Announcement Bar (แถบข้อความประกาศวิ่งด้านบน): +1. รักษาโครงสร้าง Array "slides" +2. แปลข้อความในฟิลด์ "text" ให้กระชับ สละสลวย เหมาะกับแถบประกาศสั้น`; + } else if (mode === 'usp_bar' || mode === 'usp_minimal') { + modeInstructions = `บล็อกนี้คือ USP / Trust Badges (จุดเด่นและสิทธิประโยชน์ของแบรนด์): +1. รักษาโครงสร้าง Array "items" +2. แปลฟิลด์ ${mode === 'usp_bar' ? '"title" และ "subtitle"' : '"text"'} ให้กระชับ ชัดเจน สร้างความน่าเชื่อถือ`; + } else if (mode === 'editorial_carousel') { + modeInstructions = `บล็อกนี้คือ Editorial Carousel (สไลด์เล่าเรื่องราวแบรนด์): +1. รักษาโครงสร้าง Array "slides" และฟิลด์ "title", "category", "description", "buttonText"`; + } else if (mode === 'featured_tiles') { + modeInstructions = `บล็อกนี้คือ Featured Tiles (แคมเปญการ์ดแบบกริด): +1. รักษาโครงสร้าง Array "tiles" และฟิลด์ "title", "description", "buttonText"`; + } else if (mode === 'hero_carousel') { + modeInstructions = `บล็อกนี้คือ HERO_CAROUSEL (สไลด์โปรโมชันมุมกว้าง): +1. รักษาโครงสร้าง Array "slides" และฟิลด์ (title, subtitle, buttonText, alt)`; + } else if (mode === 'article_gallery') { + modeInstructions = `บล็อกนี้คือ Photo Gallery ในบทความ: +1. รักษาโครงสร้าง Array "images" และฟิลด์ "alt" (คำอธิบายภาพ) กับ "caption" (คำบรรยายใต้ภาพ)`; + } else if (mode === 'article_facts') { + modeInstructions = `บล็อกนี้คือ Key Facts / Takeaways ในบทความ: +1. รักษาโครงสร้าง Array "items" และฟิลด์ "title", "description"`; + } else if (mode === 'article_references') { + modeInstructions = `บล็อกนี้คือ References / เอกสารอ้างอิง: +1. รักษาโครงสร้าง Array "items" และฟิลด์ "title", "note"`; + } else if (mode === 'article_download') { + modeInstructions = `บล็อกนี้คือ Downloadable Documents: +1. รักษาโครงสร้าง Array "files" และฟิลด์ "title", "description"`; + } else if (mode === 'article_table') { + modeInstructions = `บล็อกนี้คือ Comparison Table (ตารางเปรียบเทียบข้อมูล): +1. รักษาโครงสร้าง "columns" (หัวคอลัมน์) และ "rows" (แถวข้อมูล) พร้อม "label" และ "cells" ทุกช่อง`; + } else { + modeInstructions = `บล็อกนี้คือ ${blockType}: +1. รักษาคีย์ฟิลด์เดิมทั้งหมด (${fields.map((f) => f.key).join(', ')})`; + } + + return `คุณคือผู้เชี่ยวชาญด้านการแปลภาษาและ Localize เนื้อหาสำหรับแบรนด์เครื่องหอมและลักชัวรีไลฟ์สไตล์ (HomAroma) +กรุณาแปลข้อความใน JSON ด้านล่างนี้ให้ครบทุกภาษาที่กำหนด ได้แก่: +- th: ภาษาไทย +- en: English +- ja: ภาษาญี่ปุ่น (日本語) +- zh: ภาษาจีนตัวย่อ (中文) +- vi: ภาษาเวียดนาม (Tiếng Việt) +- ko: ภาษาเกาหลี (한국어) +- lo: ภาษาลาว (ພາສາລາວ) +- it: ภาษาอิตาลี (Italiano) + +ข้อกำหนดสำคัญ: +${modeInstructions} +2. หากมีคำในวงเล็บก้ามปู เช่น "[Sanctuary]" ให้แปลคำด้านในแล้วคงวงเล็บไว้ เช่น "[聖域]" หรือ "[Sanctuary]" เพราะระบบนำไปใช้ตกแต่งสีเน้นคำ (Accent Highlight) +3. สไตล์การแปล: หรูหรา สละสลวย อบอุ่น ผ่อนคลาย (Quiet Luxury & Sensory Fragrance Tone) +4. ตอบกลับมาเฉพาะโค้ด JSON ที่ถูกต้องเท่านั้น (Valid JSON only) ห้ามใส่คำอธิบายเพิ่มเติม + +\`\`\`json +${exportJsonString} +\`\`\``; + }, [mode, blockType, fields, exportJsonString]); + + const handleCopyJson = async () => { + try { + await navigator.clipboard.writeText(exportJsonString); + setCopiedJson(true); + setTimeout(() => setCopiedJson(false), 2000); + } catch { + // fallback + } + }; + + const handleCopyPrompt = async () => { + try { + await navigator.clipboard.writeText(aiPromptString); + setCopiedPrompt(true); + setTimeout(() => setCopiedPrompt(false), 2000); + } catch { + // fallback + } + }; + + const handleDownloadJson = () => { + const blob = new Blob([exportJsonString], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `block-${section?.id || 'new'}-${blockType.toLowerCase()}-translations.json`; + a.click(); + URL.revokeObjectURL(url); + }; + + // Parser & validator for pasted import JSON per mode + const parsedImport = useMemo(() => { + if (!importText.trim()) return null; + + let clean = importText.trim(); + if (clean.startsWith('```json')) clean = clean.slice(7); + else if (clean.startsWith('```')) clean = clean.slice(3); + if (clean.endsWith('```')) clean = clean.slice(0, -3); + clean = clean.trim(); + + try { + const parsed = JSON.parse(clean); + + // --- Mode: ACCORDION --- + if (mode === 'accordion') { + const rawCats = Array.isArray(parsed?.categories) + ? parsed.categories + : Array.isArray(parsed) + ? parsed + : null; + + if (!rawCats) { + return { error: 'รูปแบบ JSON ของ FAQ ต้องมี Array "categories"' }; + } + + const normCats: ParsedAccordionCategory[] = rawCats.map((rawCat: any) => { + const name = extractFieldTranslations(rawCat, 'name'); + const rawQuestions = Array.isArray(rawCat.questions) ? rawCat.questions : []; + const questions = rawQuestions.map((rawQ: any) => ({ + q: extractFieldTranslations(rawQ, 'q'), + a: extractFieldTranslations(rawQ, 'a'), + })); + return { name, questions }; + }); + + const title = extractFieldTranslations(parsed, 'title'); + let totalQ = 0; + const languageCounts: Record = {}; + + SUPPORTED_LOCALES.forEach(({ code }) => { + let count = 0; + normCats.forEach((c) => { + c.questions.forEach((q) => { + if (q.q[code]?.trim() || q.a[code]?.trim()) count++; + }); + }); + languageCounts[code] = count; + }); + + normCats.forEach((c) => { + totalQ += c.questions.length; + }); + + return { + mode: 'accordion', + title, + categories: normCats, + languageCounts, + statsText: `ตรวจพบ ${normCats.length} หมวดหมู่, ${totalQ} คำถาม`, + }; + } + + // --- Mode: ANNOUNCEMENT --- + if (mode === 'announcement') { + const rawSlides = Array.isArray(parsed?.slides) + ? parsed.slides + : Array.isArray(parsed) + ? parsed + : null; + + if (!rawSlides) { + return { error: 'รูปแบบ JSON ต้องมี Array "slides"' }; + } + + const normSlides: SlideTranslations[] = rawSlides.map((rawSlide: any) => { + const text = extractFieldTranslations(rawSlide, 'text'); + return { text: Object.keys(text).length > 0 ? text : getLocalesObject(rawSlide) }; + }); + + const languageCounts: Record = {}; + SUPPORTED_LOCALES.forEach(({ code }) => { + let count = 0; + normSlides.forEach((s) => { + if (s.text?.[code]?.trim()) count++; + }); + languageCounts[code] = count; + }); + + return { + mode: 'announcement', + slides: normSlides, + languageCounts, + statsText: `ตรวจพบ ${normSlides.length} แถบข้อความประกาศ`, + }; + } + + // --- Mode: USP_MINIMAL --- + if (mode === 'usp_minimal') { + const rawItems = Array.isArray(parsed?.items) + ? parsed.items + : Array.isArray(parsed) + ? parsed + : null; + + if (!rawItems) { + return { error: 'รูปแบบ JSON ต้องมี Array "items"' }; + } + + const normItems: SlideTranslations[] = rawItems.map((rawItem: any) => { + const text = extractFieldTranslations(rawItem, 'text'); + return { text: Object.keys(text).length > 0 ? text : getLocalesObject(rawItem) }; + }); + + const languageCounts: Record = {}; + SUPPORTED_LOCALES.forEach(({ code }) => { + let count = 0; + normItems.forEach((it) => { + if (it.text?.[code]?.trim()) count++; + }); + languageCounts[code] = count; + }); + + return { + mode: 'usp_minimal', + items: normItems, + languageCounts, + statsText: `ตรวจพบ ${normItems.length} ข้อความจุดเด่น`, + }; + } + + // --- Mode: USP_BAR --- + if (mode === 'usp_bar') { + const rawItems = Array.isArray(parsed?.items) + ? parsed.items + : Array.isArray(parsed) + ? parsed + : null; + + if (!rawItems) { + return { error: 'รูปแบบ JSON ต้องมี Array "items"' }; + } + + const normItems: SlideTranslations[] = rawItems.map((rawItem: any) => ({ + title: extractFieldTranslations(rawItem, 'title'), + subtitle: extractFieldTranslations(rawItem, 'subtitle'), + })); + + const languageCounts: Record = {}; + SUPPORTED_LOCALES.forEach(({ code }) => { + let count = 0; + normItems.forEach((it) => { + if (it.title?.[code]?.trim()) count++; + }); + languageCounts[code] = count; + }); + + return { + mode: 'usp_bar', + items: normItems, + languageCounts, + statsText: `ตรวจพบ ${normItems.length} จุดเด่น (USP)`, + }; + } + + // --- Mode: EDITORIAL_CAROUSEL --- + if (mode === 'editorial_carousel') { + const rawSlides = Array.isArray(parsed?.slides) + ? parsed.slides + : Array.isArray(parsed) + ? parsed + : null; + + if (!rawSlides) { + return { error: 'รูปแบบ JSON ต้องมี Array "slides"' }; + } + + const normSlides: SlideTranslations[] = rawSlides.map((rawSlide: any) => ({ + category: extractFieldTranslations(rawSlide, 'category'), + title: extractFieldTranslations(rawSlide, 'title'), + description: extractFieldTranslations(rawSlide, 'description'), + buttonText: extractFieldTranslations(rawSlide, 'buttonText'), + })); + + const title = extractFieldTranslations(parsed, 'title'); + const languageCounts: Record = {}; + SUPPORTED_LOCALES.forEach(({ code }) => { + let count = 0; + normSlides.forEach((s) => { + if (s.title?.[code]?.trim()) count++; + }); + languageCounts[code] = count; + }); + + return { + mode: 'editorial_carousel', + title, + slides: normSlides, + languageCounts, + statsText: `ตรวจพบ ${normSlides.length} สไลด์เล่าเรื่อง`, + }; + } + + // --- Mode: FEATURED_TILES --- + if (mode === 'featured_tiles') { + const rawTiles = Array.isArray(parsed?.tiles) + ? parsed.tiles + : Array.isArray(parsed) + ? parsed + : null; + + if (!rawTiles) { + return { error: 'รูปแบบ JSON ต้องมี Array "tiles"' }; + } + + const normTiles: SlideTranslations[] = rawTiles.map((rawTile: any) => ({ + title: extractFieldTranslations(rawTile, 'title'), + description: extractFieldTranslations(rawTile, 'description'), + buttonText: extractFieldTranslations(rawTile, 'buttonText'), + })); + + const title = extractFieldTranslations(parsed, 'title'); + const subtitle = extractFieldTranslations(parsed, 'subtitle'); + const languageCounts: Record = {}; + SUPPORTED_LOCALES.forEach(({ code }) => { + let count = 0; + normTiles.forEach((t) => { + if (t.title?.[code]?.trim()) count++; + }); + languageCounts[code] = count; + }); + + return { + mode: 'featured_tiles', + title, + subtitle, + tiles: normTiles, + languageCounts, + statsText: `ตรวจพบ ${normTiles.length} แคมเปญการ์ด`, + }; + } + + // --- Mode: HERO_CAROUSEL --- + if (mode === 'hero_carousel') { + const rawSlides = Array.isArray(parsed) + ? parsed + : Array.isArray(parsed?.slides) + ? parsed.slides + : null; + + if (!rawSlides) { + return { error: 'รูปแบบ JSON ของ Carousel ต้องมี Array "slides" หรือเป็น Array ของสไลด์' }; + } + + const normalizedSlides: SlideTranslations[] = rawSlides.map((rawSlide: any) => { + const slideNorm: Record> = {}; + CAROUSEL_SLIDE_TRANSLATABLE_FIELDS.forEach(({ key: fKey }) => { + slideNorm[fKey] = extractFieldTranslations(rawSlide, fKey); + }); + return slideNorm; + }); + + const languageCounts: Record = {}; + SUPPORTED_LOCALES.forEach(({ code }) => { + let count = 0; + normalizedSlides.forEach((s: SlideTranslations) => { + const hasAny = CAROUSEL_SLIDE_TRANSLATABLE_FIELDS.some((f) => s[f.key]?.[code]?.trim()); + if (hasAny) count++; + }); + languageCounts[code] = count; + }); + + return { + mode: 'hero_carousel', + isCarousel: true, + slides: normalizedSlides, + detectedSlidesCount: normalizedSlides.length, + languageCounts, + statsText: `ตรวจพบ ${normalizedSlides.length} สไลด์`, + }; + } + + // --- Group 3 Mode: ARTICLE_GALLERY --- + if (mode === 'article_gallery') { + const rawImages = Array.isArray(parsed?.images) + ? parsed.images + : Array.isArray(parsed) + ? parsed + : null; + + if (!rawImages) { + return { error: 'รูปแบบ JSON ต้องมี Array "images"' }; + } + + const normImages: SlideTranslations[] = rawImages.map((rawImg: any) => ({ + alt: extractFieldTranslations(rawImg, 'alt'), + caption: extractFieldTranslations(rawImg, 'caption'), + })); + + const title = extractFieldTranslations(parsed, 'title'); + const languageCounts: Record = {}; + SUPPORTED_LOCALES.forEach(({ code }) => { + let count = 0; + normImages.forEach((img) => { + if (img.alt?.[code]?.trim() || img.caption?.[code]?.trim()) count++; + }); + languageCounts[code] = count; + }); + + return { + mode: 'article_gallery', + title, + images: normImages, + languageCounts, + statsText: `ตรวจพบ ${normImages.length} ภาพแกลเลอรี`, + }; + } + + // --- Group 3 Mode: ARTICLE_FACTS --- + if (mode === 'article_facts') { + const rawItems = Array.isArray(parsed?.items) + ? parsed.items + : Array.isArray(parsed) + ? parsed + : null; + + if (!rawItems) { + return { error: 'รูปแบบ JSON ต้องมี Array "items"' }; + } + + const normItems: SlideTranslations[] = rawItems.map((rawIt: any) => ({ + title: extractFieldTranslations(rawIt, 'title'), + description: extractFieldTranslations(rawIt, 'description'), + })); + + const title = extractFieldTranslations(parsed, 'title'); + const languageCounts: Record = {}; + SUPPORTED_LOCALES.forEach(({ code }) => { + let count = 0; + normItems.forEach((it) => { + if (it.title?.[code]?.trim()) count++; + }); + languageCounts[code] = count; + }); + + return { + mode: 'article_facts', + title, + items: normItems, + languageCounts, + statsText: `ตรวจพบ ${normItems.length} เกร็ดความรู้ (Facts)`, + }; + } + + // --- Group 3 Mode: ARTICLE_REFERENCES --- + if (mode === 'article_references') { + const rawItems = Array.isArray(parsed?.items) + ? parsed.items + : Array.isArray(parsed) + ? parsed + : null; + + if (!rawItems) { + return { error: 'รูปแบบ JSON ต้องมี Array "items"' }; + } + + const normItems: SlideTranslations[] = rawItems.map((rawIt: any) => ({ + title: extractFieldTranslations(rawIt, 'title'), + note: extractFieldTranslations(rawIt, 'note'), + })); + + const title = extractFieldTranslations(parsed, 'title'); + const languageCounts: Record = {}; + SUPPORTED_LOCALES.forEach(({ code }) => { + let count = 0; + normItems.forEach((it) => { + if (it.title?.[code]?.trim()) count++; + }); + languageCounts[code] = count; + }); + + return { + mode: 'article_references', + title, + items: normItems, + languageCounts, + statsText: `ตรวจพบ ${normItems.length} เอกสารอ้างอิง`, + }; + } + + // --- Group 3 Mode: ARTICLE_DOWNLOAD --- + if (mode === 'article_download') { + const rawFiles = Array.isArray(parsed?.files) + ? parsed.files + : Array.isArray(parsed) + ? parsed + : null; + + if (!rawFiles) { + return { error: 'รูปแบบ JSON ต้องมี Array "files"' }; + } + + const normFiles: SlideTranslations[] = rawFiles.map((rawF: any) => ({ + title: extractFieldTranslations(rawF, 'title'), + })); + + const title = extractFieldTranslations(parsed, 'title'); + const description = extractFieldTranslations(parsed, 'description'); + const languageCounts: Record = {}; + SUPPORTED_LOCALES.forEach(({ code }) => { + let count = 0; + normFiles.forEach((f) => { + if (f.title?.[code]?.trim()) count++; + }); + languageCounts[code] = count; + }); + + return { + mode: 'article_download', + title, + description, + files: normFiles, + languageCounts, + statsText: `ตรวจพบ ${normFiles.length} ไฟล์ดาวน์โหลด`, + }; + } + + // --- Group 3 Mode: ARTICLE_TABLE --- + if (mode === 'article_table') { + const rawCols = Array.isArray(parsed?.columns) ? parsed.columns : []; + const rawRows = Array.isArray(parsed?.rows) ? parsed.rows : []; + + const normCols: SlideTranslations[] = rawCols.map((c: any) => { + const name = extractFieldTranslations(c, 'name'); + return { name: Object.keys(name).length > 0 ? name : getLocalesObject(c) }; + }); + + const normRows: Array<{ label: Record; cells: SlideTranslations[] }> = rawRows.map((r: any) => { + const label = extractFieldTranslations(r, 'label'); + const rawCells = Array.isArray(r.cells) ? r.cells : []; + const cells: SlideTranslations[] = rawCells.map((cell: any) => { + const text = extractFieldTranslations(cell, 'text'); + return { text: Object.keys(text).length > 0 ? text : getLocalesObject(cell) }; + }); + return { label, cells }; + }); + + const title = extractFieldTranslations(parsed, 'title'); + const note = extractFieldTranslations(parsed, 'note'); + const languageCounts: Record = {}; + + SUPPORTED_LOCALES.forEach(({ code }) => { + let count = 0; + normRows.forEach((r: { label: Record; cells: SlideTranslations[] }) => { + if (r.label[code]?.trim()) count++; + }); + languageCounts[code] = count; + }); + + return { + mode: 'article_table', + title, + note, + table: { columns: normCols, rows: normRows }, + languageCounts, + statsText: `ตรวจพบตาราง ${normCols.length} คอลัมน์, ${normRows.length} แถว`, + }; + } + + // --- Mode: FLAT --- + if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { + return { error: 'รูปแบบ JSON ต้องเป็น Object' }; + } + + const topKeys = Object.keys(parsed); + const isFormatB = topKeys.some((k) => SUPPORTED_LOCALES.some((l) => l.code === k)); + const normalized: Record> = {}; + const knownFieldKeys = fields.map((f) => f.key); + + if (isFormatB) { + knownFieldKeys.forEach((fKey) => { + normalized[fKey] = {}; + SUPPORTED_LOCALES.forEach(({ code }) => { + const langObj = parsed[code]; + if (langObj && typeof langObj === 'object') { + normalized[fKey][code] = langObj[fKey] || ''; + } + }); + }); + } else { + topKeys.forEach((fKey) => { + if (typeof parsed[fKey] === 'object' && parsed[fKey] !== null) { + normalized[fKey] = {}; + SUPPORTED_LOCALES.forEach(({ code }) => { + normalized[fKey][code] = parsed[fKey][code] || ''; + }); + } + }); + } + + const detectedFields = Object.keys(normalized); + const languageCounts: Record = {}; + SUPPORTED_LOCALES.forEach(({ code }) => { + let count = 0; + detectedFields.forEach((fKey) => { + if (normalized[fKey]?.[code]?.trim()) count++; + }); + languageCounts[code] = count; + }); + + return { + mode: 'flat', + isCarousel: false, + data: normalized, + detectedFields, + languageCounts, + isFormatB, + statsText: `ตรวจพบ ${detectedFields.length} ฟิลด์`, + }; + } catch (err: any) { + return { error: err.message || 'JSON ไม่ถูกต้อง' }; + } + }, [importText, mode, fields]); + + // Apply imported translations to section + const handleApplyImport = () => { + if (!parsedImport || parsedImport.error) { + setParseError('กรุณาวาง JSON ที่ถูกต้องก่อนกดอัปเดต'); + return; + } + + const updatedPayload = { ...payload }; + + if (mode === 'accordion' && parsedImport.categories) { + if (parsedImport.title && Object.keys(parsedImport.title).length > 0) { + updatedPayload.title = mergeLocalesObject(updatedPayload.title, parsedImport.title); + } + const existingCats = Array.isArray(updatedPayload.categories) ? [...updatedPayload.categories] : []; + parsedImport.categories.forEach((catTrans, cIdx) => { + if (cIdx < existingCats.length) { + const cat = { ...existingCats[cIdx] }; + cat.name = mergeLocalesObject(cat.name, catTrans.name); + const existingQs = Array.isArray(cat.questions) ? [...cat.questions] : []; + catTrans.questions.forEach((qTrans, qIdx) => { + if (qIdx < existingQs.length) { + const qObj = { ...existingQs[qIdx] }; + qObj.q = mergeLocalesObject(qObj.q, qTrans.q); + qObj.a = mergeLocalesObject(qObj.a, qTrans.a); + existingQs[qIdx] = qObj; + } + }); + cat.questions = existingQs; + existingCats[cIdx] = cat; + } + }); + updatedPayload.categories = existingCats; + } else if (mode === 'announcement' && parsedImport.slides) { + const existingSlides = Array.isArray(updatedPayload.slides) ? [...updatedPayload.slides] : []; + parsedImport.slides.forEach((sTrans, idx) => { + if (idx < existingSlides.length) { + existingSlides[idx] = mergeLocalesObject(existingSlides[idx], sTrans.text); + } + }); + updatedPayload.slides = existingSlides; + } else if (mode === 'usp_minimal' && parsedImport.items) { + const existingItems = Array.isArray(updatedPayload.items) ? [...updatedPayload.items] : []; + parsedImport.items.forEach((itTrans, idx) => { + if (idx < existingItems.length) { + existingItems[idx] = mergeLocalesObject(existingItems[idx], itTrans.text); + } + }); + updatedPayload.items = existingItems; + } else if (mode === 'usp_bar' && parsedImport.items) { + const existingItems = Array.isArray(updatedPayload.items) ? [...updatedPayload.items] : []; + parsedImport.items.forEach((itTrans, idx) => { + if (idx < existingItems.length) { + const it = { ...existingItems[idx] }; + it.title = mergeLocalesObject(it.title, itTrans.title); + if (itTrans.subtitle) { + it.subtitle = mergeLocalesObject(it.subtitle, itTrans.subtitle); + } + existingItems[idx] = it; + } + }); + updatedPayload.items = existingItems; + } else if (mode === 'editorial_carousel' && parsedImport.slides) { + if (parsedImport.title && Object.keys(parsedImport.title).length > 0) { + updatedPayload.title = mergeLocalesObject(updatedPayload.title, parsedImport.title); + } + const existingSlides = Array.isArray(updatedPayload.slides) ? [...updatedPayload.slides] : []; + parsedImport.slides.forEach((sTrans, idx) => { + if (idx < existingSlides.length) { + const s = { ...existingSlides[idx] }; + s.category = mergeLocalesObject(s.category, sTrans.category); + s.title = mergeLocalesObject(s.title, sTrans.title); + s.description = mergeLocalesObject(s.description, sTrans.description); + s.buttonText = mergeLocalesObject(s.buttonText, sTrans.buttonText); + existingSlides[idx] = s; + } + }); + updatedPayload.slides = existingSlides; + } else if (mode === 'featured_tiles' && parsedImport.tiles) { + if (parsedImport.title && Object.keys(parsedImport.title).length > 0) { + updatedPayload.title = mergeLocalesObject(updatedPayload.title, parsedImport.title); + } + if (parsedImport.subtitle && Object.keys(parsedImport.subtitle).length > 0) { + updatedPayload.subtitle = mergeLocalesObject(updatedPayload.subtitle, parsedImport.subtitle); + } + const existingTiles = Array.isArray(updatedPayload.tiles) ? [...updatedPayload.tiles] : []; + parsedImport.tiles.forEach((tTrans, idx) => { + if (idx < existingTiles.length) { + const t = { ...existingTiles[idx] }; + t.title = mergeLocalesObject(t.title, tTrans.title); + t.description = mergeLocalesObject(t.description, tTrans.description); + t.buttonText = mergeLocalesObject(t.buttonText, tTrans.buttonText); + existingTiles[idx] = t; + } + }); + updatedPayload.tiles = existingTiles; + } else if (mode === 'hero_carousel' && parsedImport.slides) { + const existingSlides = Array.isArray(updatedPayload.slides) ? [...updatedPayload.slides] : []; + parsedImport.slides.forEach((transSlide: SlideTranslations, idx: number) => { + if (idx < existingSlides.length) { + const origSlide = { ...existingSlides[idx] }; + CAROUSEL_SLIDE_TRANSLATABLE_FIELDS.forEach(({ key }) => { + if (transSlide[key] && Object.keys(transSlide[key]).length > 0) { + origSlide[key] = mergeLocalesObject(origSlide[key], transSlide[key]); + } + }); + existingSlides[idx] = origSlide; + } + }); + updatedPayload.slides = existingSlides; + } else if (mode === 'article_gallery' && parsedImport.images) { + if (parsedImport.title && Object.keys(parsedImport.title).length > 0) { + updatedPayload.title = mergeLocalesObject(updatedPayload.title, parsedImport.title); + } + const existingImages = Array.isArray(updatedPayload.images) ? [...updatedPayload.images] : []; + parsedImport.images.forEach((imgTrans, idx) => { + if (idx < existingImages.length) { + const img = { ...existingImages[idx] }; + if (imgTrans.alt) img.alt = mergeLocalesObject(img.alt, imgTrans.alt); + if (imgTrans.caption) img.caption = mergeLocalesObject(img.caption, imgTrans.caption); + existingImages[idx] = img; + } + }); + updatedPayload.images = existingImages; + } else if (mode === 'article_facts' && parsedImport.items) { + if (parsedImport.title && Object.keys(parsedImport.title).length > 0) { + updatedPayload.title = mergeLocalesObject(updatedPayload.title, parsedImport.title); + } + const existingItems = Array.isArray(updatedPayload.items) ? [...updatedPayload.items] : []; + parsedImport.items.forEach((itTrans, idx) => { + if (idx < existingItems.length) { + const it = { ...existingItems[idx] }; + it.title = mergeLocalesObject(it.title, itTrans.title); + if (itTrans.description) { + it.description = mergeLocalesObject(it.description, itTrans.description); + } + existingItems[idx] = it; + } + }); + updatedPayload.items = existingItems; + } else if (mode === 'article_references' && parsedImport.items) { + if (parsedImport.title && Object.keys(parsedImport.title).length > 0) { + updatedPayload.title = mergeLocalesObject(updatedPayload.title, parsedImport.title); + } + const existingItems = Array.isArray(updatedPayload.items) ? [...updatedPayload.items] : []; + parsedImport.items.forEach((itTrans, idx) => { + if (idx < existingItems.length) { + const it = { ...existingItems[idx] }; + it.title = mergeLocalesObject(it.title, itTrans.title); + if (itTrans.note) { + it.note = mergeLocalesObject(it.note, itTrans.note); + } + existingItems[idx] = it; + } + }); + updatedPayload.items = existingItems; + } else if (mode === 'article_download' && parsedImport.files) { + if (parsedImport.title && Object.keys(parsedImport.title).length > 0) { + updatedPayload.title = mergeLocalesObject(updatedPayload.title, parsedImport.title); + } + if (parsedImport.description && Object.keys(parsedImport.description).length > 0) { + updatedPayload.description = mergeLocalesObject(updatedPayload.description, parsedImport.description); + } + const existingFiles = Array.isArray(updatedPayload.files) ? [...updatedPayload.files] : []; + parsedImport.files.forEach((fTrans, idx) => { + if (idx < existingFiles.length) { + const f = { ...existingFiles[idx] }; + f.title = mergeLocalesObject(f.title, fTrans.title); + existingFiles[idx] = f; + } + }); + updatedPayload.files = existingFiles; + } else if (mode === 'article_table' && parsedImport.table) { + if (parsedImport.title && Object.keys(parsedImport.title).length > 0) { + updatedPayload.title = mergeLocalesObject(updatedPayload.title, parsedImport.title); + } + if (parsedImport.note && Object.keys(parsedImport.note).length > 0) { + updatedPayload.note = mergeLocalesObject(updatedPayload.note, parsedImport.note); + } + const existingCols = Array.isArray(updatedPayload.columns) ? [...updatedPayload.columns] : []; + parsedImport.table.columns.forEach((cTrans, idx) => { + if (idx < existingCols.length) { + existingCols[idx] = mergeLocalesObject(existingCols[idx], cTrans.name || cTrans); + } + }); + updatedPayload.columns = existingCols; + + const existingRows = Array.isArray(updatedPayload.rows) ? [...updatedPayload.rows] : []; + parsedImport.table.rows.forEach((rTrans, rIdx) => { + if (rIdx < existingRows.length) { + const r = { ...existingRows[rIdx] }; + r.label = mergeLocalesObject(r.label, rTrans.label); + const existingCells = Array.isArray(r.cells) ? [...r.cells] : []; + rTrans.cells.forEach((cellTrans, cIdx) => { + if (cIdx < existingCells.length) { + existingCells[cIdx] = mergeLocalesObject(existingCells[cIdx], cellTrans.text || cellTrans); + } + }); + r.cells = existingCells; + existingRows[rIdx] = r; + } + }); + updatedPayload.rows = existingRows; + } else if (parsedImport.data) { + Object.entries(parsedImport.data).forEach(([fieldKey, langMap]) => { + updatedPayload[fieldKey] = mergeLocalesObject(updatedPayload[fieldKey], langMap); + }); + } + + onApplyTranslations(updatedPayload); + setSuccessMsg('อัปเดตข้อความหลายภาษาลงบล็อกสำเร็จแล้ว!'); + setTimeout(() => { + onClose(); + }, 1200); + }; + + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+
+
+ +
+
+
+

+ จัดการข้อความหลายภาษา (Multi-Language JSON) +

+ + {blockType} + + + {mode === 'accordion' && ( + + + FAQ System + + )} + {mode === 'announcement' && ( + + + Ticker + + )} + {(mode === 'usp_bar' || mode === 'usp_minimal') && ( + + + USP Badges + + )} + {mode === 'featured_tiles' && ( + + + Grid Tiles + + )} + {(mode === 'hero_carousel' || mode === 'editorial_carousel') && ( + + + Carousel + + )} + {mode === 'article_gallery' && ( + + + Gallery + + )} + {mode === 'article_facts' && ( + + + Facts + + )} + {mode === 'article_references' && ( + + + References + + )} + {mode === 'article_download' && ( + + + Downloads + + )} + {mode === 'article_table' && ( + + + Table + + )} + {(blockType === 'ARTICLE_HEADER' || blockType === 'ARTICLE_MEDIA') && ( + + + Article + + )} + + {section?.id && ( + + ID: {section.id} + + )} +
+

+ ส่งออกให้ LLM แปลภาษา (ChatGPT / Claude / Gemini) แล้วนำ JSON กลับมาวางเพื่ออัปเดตทุกภาษาพร้อมกัน +

+
+
+ +
+ + {/* Navigation Tabs */} +
+ + + +
+ + {/* Modal Body */} +
+ {successMsg && ( +
+ + {successMsg} +
+ )} + + {activeTab === 'export' ? ( +
+ {/* Instructions Banner */} +
+
+
+ + ขั้นตอนการแปลภาษาด้วย AI: +
+

+ 1. กดปุ่ม "คัดลอกพร้อมท์ AI" ด้านขวา → 2. นำไปวางใน ChatGPT / Claude / Gemini → 3. สลับไปแท็บ "วางผลแปล & อัปเดต" แล้วนำผลลัพธ์มาวาง +

+
+ +
+ + + + + +
+
+ + {/* Supported Languages Chips */} +
+
+ ภาษาที่รองรับในระบบ (8 ภาษา): + + โครงสร้าง: {mode.toUpperCase()} + +
+
+ {SUPPORTED_LOCALES.map(({ code, label, full }) => ( + + {label} + ({full}) + + ))} +
+
+ + {/* JSON Code Viewer */} +
+
+ + + (UTF-8 Encoded) + +
+
+
+                    {exportJsonString}
+                  
+
+
+
+ ) : ( +
+ {/* Import Instructions */} +
+ +

+ ระบบรองรับทั้งโค้ด JSON ธรรมดา และบล็อกโค้ดมาร์กดาวน์ (```json ... ```) รวมถึงจับคู่ภาษาทั้ง 8 ภาษาให้อัตโนมัติ +

+
+ + {/* Paste Textarea */} +
+