fix(admin): resolve options display and add full 8-language translations for product attributes

This commit is contained in:
Your Name committed 2026-10-19 19:39:13 +07:00
1 parent 593d357ad4
commit 911614f862
4 files changed
+1252 -192

No files matched your search

+120 -52
View File
@@ -139,25 +139,51 @@ function emptyDefinition(): EditableDefinition {
function parseDefinition(definition: AttributeDefinition): EditableDefinition { function parseDefinition(definition: AttributeDefinition): EditableDefinition {
const translations = Object.fromEntries( const translations = Object.fromEntries(
LANGUAGES.map((lang) => [ LANGUAGES.map((lang) => {
lang, let label = ''
{ let description = ''
label:
typeof definition.label === 'string' if (typeof definition.label === 'string') {
? lang === 'th' label = lang === 'th' ? definition.label : ''
? definition.label } else if (definition.label && typeof definition.label === 'object') {
: '' label = (definition.label as any)[lang] || ''
: definition.label?.[lang] || '', }
description:
typeof definition.description === 'string' if (typeof definition.description === 'string') {
? lang === 'th' description = lang === 'th' ? definition.description : ''
? definition.description } else if (definition.description && typeof definition.description === 'object') {
: '' description = (definition.description as any)[lang] || ''
: definition.description?.[lang] || '', }
},
]) // Check translations array from API if label is still empty
if (!label && Array.isArray((definition as any).translations)) {
const tr = (definition as any).translations.find((t: any) => t.langCode === lang)
if (tr) {
label = tr.label || ''
description = tr.description || description
}
}
return [lang, { label, description }]
})
) )
const rawOptions = Array.isArray(definition.options) ? definition.options : []
const options = rawOptions.map((option: any) => {
if (typeof option === 'string') {
return { value: option, th: option, en: option }
}
if (typeof option === 'object' && option !== null) {
const norm: Record<string, string> = { ...option }
// Normalize th and en labels from label_th / label_en / label
if (!norm.th) norm.th = norm.label_th || norm.label || norm.value || ''
if (!norm.en) norm.en = norm.label_en || norm.label || norm.value || ''
if (!norm.value) norm.value = norm.key || norm.id || norm.en || norm.th || ''
return norm
}
return {}
})
return { return {
id: definition.id, id: definition.id,
code: definition.code, code: definition.code,
@@ -168,9 +194,7 @@ function parseDefinition(definition: AttributeDefinition): EditableDefinition {
sortOrder: definition.sortOrder, sortOrder: definition.sortOrder,
categoryId: definition.categoryId ?? null, categoryId: definition.categoryId ?? null,
translations, translations,
options: Array.isArray(definition.options) options,
? definition.options.map((option) => (typeof option === 'string' ? { th: option } : option))
: [],
} }
} }
@@ -438,11 +462,21 @@ export default function ProductAttributeEditPage() {
options: options:
form.type === 'SELECT' form.type === 'SELECT'
? activeOptions ? activeOptions
.map((opt) => .map((opt) => {
Object.fromEntries( const cleaned = Object.fromEntries(
Object.entries(opt).filter(([, val]) => typeof val === 'string' && val.trim()) Object.entries(opt).filter(([, val]) => typeof val === 'string' && val.trim())
) )
) if (!cleaned.value && (cleaned.en || cleaned.th)) {
cleaned.value = (cleaned.en || cleaned.th)
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '')
}
if (!cleaned.label_th && cleaned.th) cleaned.label_th = cleaned.th
if (!cleaned.label_en && cleaned.en) cleaned.label_en = cleaned.en
if (!cleaned.label) cleaned.label = cleaned.th || cleaned.en || cleaned.value || ''
return cleaned
})
.filter((opt) => Object.keys(opt).length > 0) .filter((opt) => Object.keys(opt).length > 0)
: undefined, : undefined,
} }
@@ -1391,37 +1425,71 @@ Return ONLY a valid JSON Array of objects in this exact structure (no markdown f
</div> </div>
) : ( ) : (
<div className="space-y-2.5"> <div className="space-y-2.5">
{form.options.map((opt, idx) => ( {form.options.map((opt, idx) => {
<div const optValue = opt.value || opt.key || ''
key={idx} const currentLabel = opt[activeLang] || ''
className="flex items-center gap-3 p-2.5 bg-white/[0.02] border border-white/5 rounded-xl hover:border-cyan-glow/30 transition-all" const fallbackLabel = opt.th || opt.en || opt.label || ''
>
<span className="w-7 text-center font-mono text-xs text-slate-500">#{idx + 1}</span>
<input
type="text"
value={opt[activeLang] || ''}
onChange={(e) => {
const nextOptions = [...form.options]
nextOptions[idx] = { ...nextOptions[idx], [activeLang]: e.target.value }
setForm({ ...form, options: nextOptions })
}}
className="flex-1 px-4 py-2 bg-night-950 border border-white/10 rounded-xl text-sm text-white focus:border-cyan-glow/40 outline-none"
placeholder={`Option value in ${activeLang.toUpperCase()} (e.g. CBR 650R, Mono Shock)`}
/>
<button return (
type="button" <div
onClick={() => { key={idx}
const nextOptions = form.options.filter((_, i) => i !== idx) className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 p-3 bg-white/[0.02] border border-white/5 rounded-xl hover:border-cyan-glow/30 transition-all"
setForm({ ...form, options: nextOptions })
}}
className="p-2 text-slate-500 hover:text-rose-400 hover:bg-rose-500/10 rounded-xl transition-all cursor-pointer"
title="Remove Option"
> >
<Trash2 className="w-4 h-4" /> <span className="w-7 text-center font-mono text-xs text-slate-500 self-center">#{idx + 1}</span>
</button>
</div> {/* System Code / Key */}
))} <div className="w-full sm:w-44 flex-shrink-0">
<label className="sm:hidden text-[10px] uppercase font-bold text-slate-500 mb-1 block">System Code</label>
<input
type="text"
value={optValue}
onChange={(e) => {
const nextOptions = [...form.options]
nextOptions[idx] = { ...nextOptions[idx], value: e.target.value }
setForm({ ...form, options: nextOptions })
}}
placeholder="code_value"
className="w-full px-3 py-2 bg-black/40 border border-white/10 rounded-lg text-xs font-mono text-cyan-glow placeholder-slate-600 focus:border-cyan-glow/40 outline-none"
title="System code stored in database / API"
/>
</div>
{/* Localized Label */}
<div className="flex-1">
<label className="sm:hidden text-[10px] uppercase font-bold text-slate-500 mb-1 block">
Label ({activeLang.toUpperCase()})
</label>
<input
type="text"
value={currentLabel}
onChange={(e) => {
const nextOptions = [...form.options]
nextOptions[idx] = { ...nextOptions[idx], [activeLang]: e.target.value }
setForm({ ...form, options: nextOptions })
}}
className="w-full px-4 py-2 bg-night-950 border border-white/10 rounded-xl text-sm text-white focus:border-cyan-glow/40 outline-none"
placeholder={
fallbackLabel && !currentLabel
? `${fallbackLabel} (${activeLang === 'th' ? 'TH' : 'Fallback'})`
: `Label in ${activeLang.toUpperCase()}`
}
/>
</div>
<button
type="button"
onClick={() => {
const nextOptions = form.options.filter((_, i) => i !== idx)
setForm({ ...form, options: nextOptions })
}}
className="p-2 text-slate-500 hover:text-rose-400 hover:bg-rose-500/10 rounded-xl transition-all cursor-pointer self-center"
title="Remove Option"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
)
})}
</div> </div>
)} )}
</section> </section>
+13 -2
View File
@@ -843,8 +843,19 @@ export default function ProductEditPage() {
> >
<option value="">Select an option</option> <option value="">Select an option</option>
{(definition.options || []).map((option, optionIdx) => { {(definition.options || []).map((option, optionIdx) => {
const optionValue = typeof option === 'string' ? option : option.th || option.en || Object.values(option)[0] const optionValue =
const optionLabel = typeof option === 'string' ? option : option[activeLang] || option.th || option.en || Object.values(option)[0] typeof option === 'string'
? option
: option.value || option.key || option.id || option.th || option.en || Object.values(option)[0]
const optionLabel =
typeof option === 'string'
? option
: option[activeLang] ||
option[`label_${activeLang}`] ||
option.label ||
option.th ||
option.en ||
optionValue
return ( return (
<option key={`${optionValue}-${optionIdx}`} value={optionValue}> <option key={`${optionValue}-${optionIdx}`} value={optionValue}>
{optionLabel} {optionLabel}
+514 -138
View File
@@ -5,44 +5,6 @@
"description": "การจัดกลุ่มโทนกลิ่นหลักของเครื่องหอม เพื่อให้ค้นหาตามสไตล์กลิ่นที่ชอบได้ง่าย", "description": "การจัดกลุ่มโทนกลิ่นหลักของเครื่องหอม เพื่อให้ค้นหาตามสไตล์กลิ่นที่ชอบได้ง่าย",
"type": "SELECT", "type": "SELECT",
"unit": null, "unit": null,
"options": [
{
"value": "floral",
"label": "Floral (ดอกไม้)",
"label_th": "กลิ่นแนวดอกไม้",
"label_en": "Floral"
},
{
"value": "fresh_herbal",
"label": "Fresh & Herbal (สดชื่น/สมุนไพร)",
"label_th": "กลิ่นสดชื่นและสมุนไพร",
"label_en": "Fresh & Herbal"
},
{
"value": "fruity",
"label": "Fruity (ผลไม้)",
"label_th": "กลิ่นแนวผลไม้",
"label_en": "Fruity"
},
{
"value": "spa_relaxing",
"label": "Spa & Relaxing (สปา/ผ่อนคลาย)",
"label_th": "กลิ่นแนวสปาและผ่อนคลาย",
"label_en": "Spa & Relaxing"
},
{
"value": "sweet_warm",
"label": "Sweet & Warm (หวานละมุน/อบอุ่น)",
"label_th": "กลิ่นแนวหวานละมุนและอบอุ่น",
"label_en": "Sweet & Warm"
},
{
"value": "aquatic_ozone",
"label": "Aquatic & Ozone (อากาศ/ทะเล)",
"label_th": "กลิ่นแนวปลอดโปร่งและทะเล",
"label_en": "Aquatic & Ozone"
}
],
"isRequired": false, "isRequired": false,
"isFilterable": true, "isFilterable": true,
"sortOrder": 1, "sortOrder": 1,
@@ -50,13 +12,123 @@
"translations": { "translations": {
"th": { "th": {
"label": "กลุ่มโทนกลิ่น", "label": "กลุ่มโทนกลิ่น",
"description": "การจัดกลุ่มโทนกลิ่นหลักของเครื่องหอม" "description": "การจัดกลุ่มโทนกลิ่นหลักของเครื่องหอม เพื่อให้ค้นหาตามสไตล์กลิ่นที่ชอบได้ง่าย"
}, },
"en": { "en": {
"label": "Fragrance Family", "label": "Fragrance Family",
"description": "Primary olfactory classification for fragrances" "description": "Primary olfactory classification for fragrances to easily find your preferred scent style"
},
"ja": {
"label": "香りの系統",
"description": "お好みの香りを簡単に見つけるためのフレグランスの主な系統分類"
},
"zh": {
"label": "香调分类",
"description": "香氛的主要香调分类,方便根据喜好轻松挑选香型"
},
"vi": {
"label": "Nhóm hương thơm",
"description": "Phân loại mùi hương chính giúp bạn dễ dàng tìm kiếm mùi hương yêu thích"
},
"ko": {
"label": "향기 계열",
"description": "취향에 맞는 향기를 쉽게 찾을 수 있는 주요 향 계열 분류"
},
"lo": {
"label": "ກຸ່ມໂທນກິ່ນ",
"description": "ການຈັດກຸ່ມກິ່ນຫຼັກຂອງເຄື່ອງຫອມ ເພື່ອໃຫ້ຄົ້ນຫາຕາມສະໄຕລ໌ກິ່ນທີ່ມັກໄດ້ງ່າຍ"
},
"it": {
"label": "Famiglia olfattiva",
"description": "Classificazione olfattiva principale per fragranze e diffusori d'ambiente"
} }
} },
"options": [
{
"value": "floral",
"label": "Floral (ดอกไม้)",
"th": "กลิ่นแนวดอกไม้",
"en": "Floral",
"ja": "フローラル (花の香り)",
"zh": "花香调",
"vi": "Hương hoa cỏ",
"ko": "플로럴 (꽃향)",
"lo": "ກິ່ນດອກໄມ້",
"it": "Floreale",
"label_th": "กลิ่นแนวดอกไม้",
"label_en": "Floral"
},
{
"value": "fresh_herbal",
"label": "Fresh & Herbal (สดชื่น/สมุนไพร)",
"th": "กลิ่นสดชื่นและสมุนไพร",
"en": "Fresh & Herbal",
"ja": "フレッシュ&ハーバル",
"zh": "清新草本调",
"vi": "Tươi mát & Thảo mộc",
"ko": "프레시 & 허브",
"lo": "ກິ່ນສົດຊື່ນແລະສະໝຸນໄພ",
"it": "Fresco ed Erbaceo",
"label_th": "กลิ่นสดชื่นและสมุนไพร",
"label_en": "Fresh & Herbal"
},
{
"value": "fruity",
"label": "Fruity (ผลไม้)",
"th": "กลิ่นแนวผลไม้",
"en": "Fruity",
"ja": "フルーティー (果実の香り)",
"zh": "果香调",
"vi": "Hương trái cây",
"ko": "프루티 (과일향)",
"lo": "ກິ່ນໝາກໄມ້",
"it": "Fruttato",
"label_th": "กลิ่นแนวผลไม้",
"label_en": "Fruity"
},
{
"value": "spa_relaxing",
"label": "Spa & Relaxing (สปา/ผ่อนคลาย)",
"th": "กลิ่นแนวสปาและผ่อนคลาย",
"en": "Spa & Relaxing",
"ja": "スパ&リラックス",
"zh": "水疗舒缓调",
"vi": "Spa & Thư giãn",
"ko": "스파 & 릴랙싱",
"lo": "ກິ່ນສະປາແລະຜ່ອນຄາຍ",
"it": "Spa e Rilassante",
"label_th": "กลิ่นแนวสปาและผ่อนคลาย",
"label_en": "Spa & Relaxing"
},
{
"value": "sweet_warm",
"label": "Sweet & Warm (หวานละมุน/อบอุ่น)",
"th": "กลิ่นแนวหวานละมุนและอบอุ่น",
"en": "Sweet & Warm",
"ja": "スイート&ウォーム",
"zh": "甜美温馨调",
"vi": "Ngọt ngào & Ấm áp",
"ko": "스위트 & 웜",
"lo": "ກິ່ນຫວານລະມຸນແລະອົບອຸ່ນ",
"it": "Dolce e Caldo",
"label_th": "กลิ่นแนวหวานละมุนและอบอุ่น",
"label_en": "Sweet & Warm"
},
{
"value": "aquatic_ozone",
"label": "Aquatic & Ozone (อากาศ/ทะเล)",
"th": "กลิ่นแนวปลอดโปร่งและทะเล",
"en": "Aquatic & Ozone",
"ja": "アクアティック&オゾン (海の風)",
"zh": "海洋清新调",
"vi": "Hương biển & Không khí",
"ko": "아쿠아틱 & 오존",
"lo": "ກິ່ນທະເລແລະໂອໂຊນ",
"it": "Acquatico e Ozono",
"label_th": "กลิ่นแนวปลอดโปร่งและทะเล",
"label_en": "Aquatic & Ozone"
}
]
}, },
{ {
"code": "mood_benefit", "code": "mood_benefit",
@@ -64,38 +136,6 @@
"description": "ความรู้สึกหรือบรรยากาศที่ได้รับจากกลิ่นหอมนี้", "description": "ความรู้สึกหรือบรรยากาศที่ได้รับจากกลิ่นหอมนี้",
"type": "SELECT", "type": "SELECT",
"unit": null, "unit": null,
"options": [
{
"value": "relax_sleep",
"label": "Relax & Sleep (ผ่อนคลาย หลับสบาย)",
"label_th": "ผ่อนคลาย หลับสบาย",
"label_en": "Relax & Sleep"
},
{
"value": "refresh_energize",
"label": "Refresh & Energize (สดชื่น เติมพลัง)",
"label_th": "สดชื่น เติมพลัง",
"label_en": "Refresh & Energize"
},
{
"value": "focus_mind",
"label": "Focus & Mind (สร้างสมาธิ กระตุ้นไอเดีย)",
"label_th": "สร้างสมาธิ กระตุ้นไอเดีย",
"label_en": "Focus & Mind"
},
{
"value": "odor_eliminator",
"label": "Odor Eliminator (ดับกลิ่นอับ ปรับอากาศ)",
"label_th": "ดับกลิ่นอับ ปรับอากาศ",
"label_en": "Odor Eliminator"
},
{
"value": "romantic_cozy",
"label": "Romantic & Cozy (อบอุ่น โรแมนติก)",
"label_th": "อบอุ่น โรแมนติก",
"label_en": "Romantic & Cozy"
}
],
"isRequired": false, "isRequired": false,
"isFilterable": true, "isFilterable": true,
"sortOrder": 2, "sortOrder": 2,
@@ -107,9 +147,105 @@
}, },
"en": { "en": {
"label": "Mood & Benefit", "label": "Mood & Benefit",
"description": "Atmosphere and emotional benefits" "description": "Atmosphere and emotional benefits provided by the aroma"
},
"ja": {
"label": "気分と効能",
"description": "香りがもたらす心地よい雰囲気とリラクゼーション効果"
},
"zh": {
"label": "情绪与功效",
"description": "香气带来的氛围感与身心情绪舒缓功效"
},
"vi": {
"label": "Cảm xúc & Lợi ích",
"description": "Bầu không khí và lợi ích tinh thần từ mùi hương"
},
"ko": {
"label": "무드 & 효능",
"description": "향기가 전달하는 감성적인 분위기와 힐링 효과"
},
"lo": {
"label": "ອາລົມແລະຄຸນປະໂຫຍດ",
"description": "ບັນຍາກາດຫຼືຄວາມຮູ້ສຶກທີ່ໄດ້ຈາກກິ່ນ"
},
"it": {
"label": "Umore e Benefici",
"description": "Atmosfera e benefici emotivi donati dalla fragranza"
} }
} },
"options": [
{
"value": "relax_sleep",
"label": "Relax & Sleep (ผ่อนคลาย หลับสบาย)",
"th": "ผ่อนคลาย หลับสบาย",
"en": "Relax & Sleep",
"ja": "リラックス・快眠",
"zh": "放松助眠",
"vi": "Thư giãn & Ngủ ngon",
"ko": "릴랙스 & 숙면",
"lo": "ຜ່ອນຄາຍ ນອນຫຼັບສະບາຍ",
"it": "Rilassamento e Sonno",
"label_th": "ผ่อนคลาย หลับสบาย",
"label_en": "Relax & Sleep"
},
{
"value": "refresh_energize",
"label": "Refresh & Energize (สดชื่น เติมพลัง)",
"th": "สดชื่น เติมพลัง",
"en": "Refresh & Energize",
"ja": "リフレッシュ・活力",
"zh": "提神充能",
"vi": "Sảng khoái & Tươi mới",
"ko": "리프레시 & 에너지",
"lo": "ສົດຊື່ນ ເພີ່ມພະລັງ",
"it": "Rinfrescante ed Energizzante",
"label_th": "สดชื่น เติมพลัง",
"label_en": "Refresh & Energize"
},
{
"value": "focus_mind",
"label": "Focus & Mind (สร้างสมาธิ กระตุ้นไอเดีย)",
"th": "สร้างสมาธิ กระตุ้นไอเดีย",
"en": "Focus & Mind",
"ja": "集中力・アイデア活性",
"zh": "专注冥想・激发灵感",
"vi": "Tập trung & Sáng tạo",
"ko": "집중력 & 아이디어 자극",
"lo": "ສ້າງສະມາທິ ກະຕຸ້ນໄອເດຍ",
"it": "Concentrazione e Ispirazione",
"label_th": "สร้างสมาธิ กระตุ้นไอเดีย",
"label_en": "Focus & Mind"
},
{
"value": "odor_eliminator",
"label": "Odor Eliminator (ดับกลิ่นอับ ปรับอากาศ)",
"th": "ดับกลิ่นอับ ปรับอากาศ",
"en": "Odor Eliminator",
"ja": "消臭・空気リフレッシュ",
"zh": "祛味除臭・清新空气",
"vi": "Khử mùi & Làm sạch không khí",
"ko": "악취 제거 & 공기 정화",
"lo": "ດັບກິ່ນອັບ ປັບອາກາດ",
"it": "Elimina Odori e Rinfresca",
"label_th": "ดับกลิ่นอับ ปรับอากาศ",
"label_en": "Odor Eliminator"
},
{
"value": "romantic_cozy",
"label": "Romantic & Cozy (อบอุ่น โรแมนติก)",
"th": "อบอุ่น โรแมนติก",
"en": "Romantic & Cozy",
"ja": "ロマンチック・コージー",
"zh": "浪漫温馨・惬意舒适",
"vi": "Lãng mạn & Ấm cúng",
"ko": "로맨틱 & 아늑함",
"lo": "ອົບອຸ່ນ ໂຣແມນຕິກ",
"it": "Romantico e Accogliente",
"label_th": "อบอุ่น โรแมนติก",
"label_en": "Romantic & Cozy"
}
]
}, },
{ {
"code": "scent_intensity", "code": "scent_intensity",
@@ -117,26 +253,6 @@
"description": "ระดับการกระจายตัวและความชัดของกลิ่นหอม", "description": "ระดับการกระจายตัวและความชัดของกลิ่นหอม",
"type": "SELECT", "type": "SELECT",
"unit": null, "unit": null,
"options": [
{
"value": "light",
"label": "Light / Subtle (หอมละมุน บางเบา)",
"label_th": "หอมละมุน บางเบา",
"label_en": "Light / Subtle"
},
{
"value": "medium",
"label": "Medium (หอมปานกลาง กำลังพอดี)",
"label_th": "หอมปานกลาง กำลังพอดี",
"label_en": "Medium"
},
{
"value": "strong",
"label": "Strong / Rich (หอมชัดเจน เข้มข้น)",
"label_th": "หอมชัดเจน เข้มข้น",
"label_en": "Strong / Rich"
}
],
"isRequired": false, "isRequired": false,
"isFilterable": true, "isFilterable": true,
"sortOrder": 3, "sortOrder": 3,
@@ -148,9 +264,77 @@
}, },
"en": { "en": {
"label": "Scent Intensity", "label": "Scent Intensity",
"description": "Diffusion strength and fragrance throw" "description": "Diffusion strength and fragrance throw level"
},
"ja": {
"label": "香りの強さ",
"description": "香りの広がりと強さのレベル"
},
"zh": {
"label": "香气浓度",
"description": "香气的扩散力与浓郁程度"
},
"vi": {
"label": "Độ đậm mùi",
"description": "Mức độ khuếch tán và độ rõ của mùi hương"
},
"ko": {
"label": "발향 강도",
"description": "향의 확산력 및 발향 강도 레벨"
},
"lo": {
"label": "ລະດັບຄວາມເຂັ້ມຂຸ້ນຂອງກິ່ນ",
"description": "ລະດັບການກະຈາຍຕົວແລະຄວາມຊັດເຈນຂອງກິ່ນ"
},
"it": {
"label": "Intensità della fragranza",
"description": "Livello di forza diffusiva e persistenza olfattiva"
} }
} },
"options": [
{
"value": "light",
"label": "Light / Subtle (หอมละมุน บางเบา)",
"th": "หอมละมุน บางเบา",
"en": "Light / Subtle",
"ja": "ほのか・マイルド",
"zh": "柔和清淡",
"vi": "Nhẹ nhàng & Tinh tế",
"ko": "은은함 (라이트)",
"lo": "ຫອມລະມຸນ ບາງເບົາ",
"it": "Delicato / Leggero",
"label_th": "หอมละมุน บางเบา",
"label_en": "Light / Subtle"
},
{
"value": "medium",
"label": "Medium (หอมปานกลาง กำลังพอดี)",
"th": "หอมปานกลาง กำลังพอดี",
"en": "Medium",
"ja": "適度・ミディアム",
"zh": "适中平衡",
"vi": "Vừa phải",
"ko": "적당함 (미디엄)",
"lo": "ຫອມປານກາງ ພໍດີ",
"it": "Medio",
"label_th": "หอมปานกลาง กำลังพอดี",
"label_en": "Medium"
},
{
"value": "strong",
"label": "Strong / Rich (หอมชัดเจน เข้มข้น)",
"th": "หอมชัดเจน เข้มข้น",
"en": "Strong / Rich",
"ja": "しっかり・リッチ",
"zh": "浓郁显著",
"vi": "Đậm đà & Lan tỏa",
"ko": "풍부하고 진함 (스트롱)",
"lo": "ຫອມຊັດເຈນ ເຂັ້ມຂຸ້ນ",
"it": "Intenso / Forte",
"label_th": "หอมชัดเจน เข้มข้น",
"label_en": "Strong / Rich"
}
]
}, },
{ {
"code": "room_placement", "code": "room_placement",
@@ -158,44 +342,6 @@
"description": "ห้องหรือบริเวณที่เหมาะสมที่สุดสำหรับการกระจายกลิ่น", "description": "ห้องหรือบริเวณที่เหมาะสมที่สุดสำหรับการกระจายกลิ่น",
"type": "SELECT", "type": "SELECT",
"unit": null, "unit": null,
"options": [
{
"value": "bedroom",
"label": "ห้องนอน (Bedroom)",
"label_th": "ห้องนอน",
"label_en": "Bedroom"
},
{
"value": "living_room",
"label": "ห้องนั่งเล่น / โถงกลาง (Living Room)",
"label_th": "ห้องนั่งเล่น / โถงกลาง",
"label_en": "Living Room"
},
{
"value": "workspace",
"label": "ห้องทำงาน / โต๊ะทำงาน (Workspace)",
"label_th": "ห้องทำงาน / โต๊ะทำงาน",
"label_en": "Workspace"
},
{
"value": "bathroom",
"label": "ห้องน้ำ (Bathroom)",
"label_th": "ห้องน้ำ",
"label_en": "Bathroom"
},
{
"value": "car",
"label": "ห้องโดยสารรถยนต์ (Car Interior)",
"label_th": "ห้องโดยสารรถยนต์",
"label_en": "Car Interior"
},
{
"value": "commercial_spa",
"label": "ร้านสปา / โรงแรม / คาเฟ่ (Commercial / Spa)",
"label_th": "ร้านสปา / โรงแรม / คาเฟ่",
"label_en": "Commercial / Spa"
}
],
"isRequired": false, "isRequired": false,
"isFilterable": true, "isFilterable": true,
"sortOrder": 4, "sortOrder": 4,
@@ -208,8 +354,118 @@
"en": { "en": {
"label": "Recommended Space", "label": "Recommended Space",
"description": "Ideal placement areas for optimal aroma diffusion" "description": "Ideal placement areas for optimal aroma diffusion"
},
"ja": {
"label": "おすすめの設置場所",
"description": "香りを最大限に楽しむための最適な設置空間"
},
"zh": {
"label": "适用空间推荐",
"description": "最适宜散发香氛的推荐空间或房间"
},
"vi": {
"label": "Không gian sử dụng",
"description": "Khu vực phù hợp nhất để khuếch tán hương thơm"
},
"ko": {
"label": "추천 사용 공간",
"description": "최적의 발향을 위한 권장 배치 공간"
},
"lo": {
"label": "ພື້ນທີ່ໃຊ້ງານທີ່ແນະນຳ",
"description": "ຫ້ອງຫຼືບໍລິເວນທີ່ເໝາະສົມສຳລັບການກະຈາຍກິ່ນ"
},
"it": {
"label": "Ambiente consigliato",
"description": "Aree di posizionamento ideali per una diffusione ottimale"
} }
} },
"options": [
{
"value": "bedroom",
"label": "ห้องนอน (Bedroom)",
"th": "ห้องนอน",
"en": "Bedroom",
"ja": "寝室 (Bedroom)",
"zh": "卧室 (Bedroom)",
"vi": "Phòng ngủ",
"ko": "침실 (Bedroom)",
"lo": "ຫ້ອງນອນ",
"it": "Camera da letto",
"label_th": "ห้องนอน",
"label_en": "Bedroom"
},
{
"value": "living_room",
"label": "ห้องนั่งเล่น / โถงกลาง (Living Room)",
"th": "ห้องนั่งเล่น / โถงกลาง",
"en": "Living Room",
"ja": "リビングルーム",
"zh": "客厅 / 起居室",
"vi": "Phòng khách",
"ko": "거실 (Living Room)",
"lo": "ຫ້ອງນັ່ງຫຼິ້ນ",
"it": "Soggiorno",
"label_th": "ห้องนั่งเล่น / โถงกลาง",
"label_en": "Living Room"
},
{
"value": "workspace",
"label": "ห้องทำงาน / โต๊ะทำงาน (Workspace)",
"th": "ห้องทำงาน / โต๊ะทำงาน",
"en": "Workspace",
"ja": "書斎 / デスク",
"zh": "书房 / 办公区",
"vi": "Phòng làm việc",
"ko": "서재 / 업무 공간",
"lo": "ຫ້ອງເຮັດວຽກ",
"it": "Ufficio / Studio",
"label_th": "ห้องทำงาน / โต๊ะทำงาน",
"label_en": "Workspace"
},
{
"value": "bathroom",
"label": "ห้องน้ำ (Bathroom)",
"th": "ห้องน้ำ",
"en": "Bathroom",
"ja": "バスルーム・洗面所",
"zh": "卫浴洗手间",
"vi": "Phòng tắm",
"ko": "욕실 / 화장실",
"lo": "ຫ້ອງນ້ຳ",
"it": "Bagno",
"label_th": "ห้องน้ำ",
"label_en": "Bathroom"
},
{
"value": "car",
"label": "ห้องโดยสารรถยนต์ (Car Interior)",
"th": "ห้องโดยสารรถยนต์",
"en": "Car Interior",
"ja": "車内・カーインテリア",
"zh": "车内空间",
"vi": "Nội thất ô tô",
"ko": "차량 내부",
"lo": "ຫ້ອງໂດຍສານລົດ",
"it": "Interno Auto",
"label_th": "ห้องโดยสารรถยนต์",
"label_en": "Car Interior"
},
{
"value": "commercial_spa",
"label": "ร้านสปา / โรงแรม / คาเฟ่ (Commercial / Spa)",
"th": "ร้านสปา / โรงแรม / คาเฟ่",
"en": "Commercial / Spa",
"ja": "スパ・ホテル・カフェ",
"zh": "SPA / 酒店 / 咖啡馆",
"vi": "Spa / Khách sạn / Quán Cafe",
"ko": "스파 / 호텔 / 카페",
"lo": "ຮ້ານສະປາ / ໂຮງແຮມ / ຄາເຟ່",
"it": "Spa / Hotel / Lounge",
"label_th": "ร้านสปา / โรงแรม / คาเฟ่",
"label_en": "Commercial / Spa"
}
]
}, },
{ {
"code": "compatible_device", "code": "compatible_device",
@@ -230,6 +486,30 @@
"en": { "en": {
"label": "Compatible Devices", "label": "Compatible Devices",
"description": "Supported diffusers and diffusion equipment" "description": "Supported diffusers and diffusion equipment"
},
"ja": {
"label": "対応機器・器具",
"description": "使用可能なディフューザーおよび芳香器具"
},
"zh": {
"label": "适配设备",
"description": "适用的香薰机、扩香器具或配件"
},
"vi": {
"label": "Thiết bị tương thích",
"description": "Các loại máy khuếch tán và phụ kiện hỗ trợ"
},
"ko": {
"label": "호환 기기",
"description": "사용 가능한 디퓨저 기기 및 발향 도구"
},
"lo": {
"label": "ອຸປະກອນທີ່ຮອງຮັບ",
"description": "ອຸປະກອນຫຼືເຄື່ອງພົ່ນທີ່ສາມາດໃຊ້ຮ່ວມກັນໄດ້"
},
"it": {
"label": "Dispositivi compatibili",
"description": "Diffusori e strumenti di diffusione compatibili"
} }
} }
}, },
@@ -252,6 +532,30 @@
"en": { "en": {
"label": "Coverage Area", "label": "Coverage Area",
"description": "Effective room coverage area in square meters" "description": "Effective room coverage area in square meters"
},
"ja": {
"label": "推奨適用範囲",
"description": "香りが効果的に広がる推奨床面積(平米)"
},
"zh": {
"label": "适用面积",
"description": "香氛有效扩散的推荐房间面积(平方米)"
},
"vi": {
"label": "Diện tích khuếch tán",
"description": "Diện tích phòng hiệu quả tính theo mét vuông"
},
"ko": {
"label": "권장 사용 면적",
"description": "향기가 효과적으로 퍼지는 권장 면적 (제곱미터)"
},
"lo": {
"label": "ຂະໜາດພື້ນທີ່ຄອບຄຸມ",
"description": "ຂະໜາດພື້ນທີ່ຫ້ອງທີ່ກິ່ນກະຈາຍຕົວໄດ້ດີ"
},
"it": {
"label": "Area di copertura",
"description": "Area di copertura efficace in metri quadrati"
} }
} }
}, },
@@ -274,6 +578,30 @@
"en": { "en": {
"label": "Estimated Duration", "label": "Estimated Duration",
"description": "Approximate lifespan and continuous scent duration" "description": "Approximate lifespan and continuous scent duration"
},
"ja": {
"label": "使用持続期間",
"description": "香りが継続して香る推定期間・寿命"
},
"zh": {
"label": "留香持久时间",
"description": "香气持续散发的大约使用周期"
},
"vi": {
"label": "Thời gian sử dụng",
"description": "Tuổi thọ ước tính và thời gian tỏa hương liên tục"
},
"ko": {
"label": "사용 지속 시간",
"description": "향기가 지속적으로 발향되는 예상 사용 기간"
},
"lo": {
"label": "ໄລຍະເວລາການໃຊ້ງານ",
"description": "ໄລຍະເວລາທີ່ກິ່ນກະຈາຍຕົວຕໍ່ເນື່ອງ"
},
"it": {
"label": "Durata stimata",
"description": "Durata approssimativa e diffusione continua del profumo"
} }
} }
}, },
@@ -295,7 +623,31 @@
}, },
"en": { "en": {
"label": "Formula Base", "label": "Formula Base",
"description": "Base formula and carrier type" "description": "Base formula and carrier oil type"
},
"ja": {
"label": "ベース成分・処方",
"description": "調合および拡散に使用されるキャリアオイルベース"
},
"zh": {
"label": "基底配方",
"description": "用于调配与扩香的溶剂基底和成分类型"
},
"vi": {
"label": "Công thức nền",
"description": "Loại dung môi và gốc tinh dầu khuếch tán"
},
"ko": {
"label": "베이스 성분",
"description": "디퓨저 발향에 사용된 캐리어 베이스 및 포뮬러"
},
"lo": {
"label": "ປະເພດເບສແລະສ່ວນປະສົມ",
"description": "ຊະນິດຂອງເບສທີ່ໃຊ້ໃນການປະສົມແລະກະຈາຍກິ່ນ"
},
"it": {
"label": "Base formulativa",
"description": "Formula base e tipo di solvente/olio vettore"
} }
} }
}, },
@@ -318,7 +670,31 @@
"en": { "en": {
"label": "Usage Instructions", "label": "Usage Instructions",
"description": "Recommended steps for best diffusion experience" "description": "Recommended steps for best diffusion experience"
},
"ja": {
"label": "使用方法・注意点",
"description": "最高の香り体験を得るための正しい使用手順"
},
"zh": {
"label": "使用方法与建议",
"description": "获得最佳扩香效果的推荐使用步骤与保养指南"
},
"vi": {
"label": "Hướng dẫn sử dụng",
"description": "Các bước sử dụng khuyến nghị để đạt hiệu quả tỏa hương tốt nhất"
},
"ko": {
"label": "사용 방법 및 권장사항",
"description": "최상의 발향 효과를 위한 올바른 사용 가이드"
},
"lo": {
"label": "ວິທີໃຊ້ງານແລະຄຳແນະນຳ",
"description": "ຂັ້ນຕອນການໃຊ້ງານເພື່ອປະສິດທິພາບສູງສຸດ"
},
"it": {
"label": "Istruzioni per l'uso",
"description": "Passaggi consigliati per una diffusione e resa ottimali"
} }
} }
} }
] ]
@@ -0,0 +1,605 @@
import * as fs from "fs";
import * as path from "path";
import prisma from "../apps/backend/src/lib/prisma";
const API_BASE = process.env.API_BASE || "https://api.homaroma.shop/api";
const ADMIN_USER = process.env.ADMIN_USER || "admin";
const ADMIN_PASS = process.env.ADMIN_PASS || "homaroma123";
// 9 Attributes complete 8-languages definitions
const FULL_8LANG_DEFINITIONS = [
{
code: "scent_family",
label: "กลุ่มโทนกลิ่น",
description: "การจัดกลุ่มโทนกลิ่นหลักของเครื่องหอม เพื่อให้ค้นหาตามสไตล์กลิ่นที่ชอบได้ง่าย",
type: "SELECT",
unit: null,
isRequired: false,
isFilterable: true,
sortOrder: 1,
categoryId: null,
translations: {
th: { label: "กลุ่มโทนกลิ่น", description: "การจัดกลุ่มโทนกลิ่นหลักของเครื่องหอม เพื่อให้ค้นหาตามสไตล์กลิ่นที่ชอบได้ง่าย" },
en: { label: "Fragrance Family", description: "Primary olfactory classification for fragrances to easily find your preferred scent style" },
ja: { label: "香りの系統", description: "お好みの香りを簡単に見つけるためのフレグランスの主な系統分類" },
zh: { label: "香调分类", description: "香氛的主要香调分类,方便根据喜好轻松挑选香型" },
vi: { label: "Nhóm hương thơm", description: "Phân loại mùi hương chính giúp bạn dễ dàng tìm kiếm mùi hương yêu thích" },
ko: { label: "향기 계열", description: "취향에 맞는 향기를 쉽게 찾을 수 있는 주요 향 계열 분류" },
lo: { label: "ກຸ່ມໂທນກິ່ນ", description: "ການຈັດກຸ່ມກິ່ນຫຼັກຂອງເຄື່ອງຫອມ ເພື່ອໃຫ້ຄົ້ນຫາຕາມສະໄຕລ໌ກິ່ນທີ່ມັກໄດ້ງ່າຍ" },
it: { label: "Famiglia olfattiva", description: "Classificazione olfattiva principale per fragranze e diffusori d'ambiente" }
},
options: [
{
value: "floral",
label: "Floral (ดอกไม้)",
th: "กลิ่นแนวดอกไม้",
en: "Floral",
ja: "フローラル (花の香り)",
zh: "花香调",
vi: "Hương hoa cỏ",
ko: "플로럴 (꽃향)",
lo: "ກິ່ນດອກໄມ້",
it: "Floreale",
label_th: "กลิ่นแนวดอกไม้",
label_en: "Floral"
},
{
value: "fresh_herbal",
label: "Fresh & Herbal (สดชื่น/สมุนไพร)",
th: "กลิ่นสดชื่นและสมุนไพร",
en: "Fresh & Herbal",
ja: "フレッシュ&ハーバル",
zh: "清新草本调",
vi: "Tươi mát & Thảo mộc",
ko: "프레시 & 허브",
lo: "ກິ່ນສົດຊື່ນແລະສະໝຸນໄພ",
it: "Fresco ed Erbaceo",
label_th: "กลิ่นสดชื่นและสมุนไพร",
label_en: "Fresh & Herbal"
},
{
value: "fruity",
label: "Fruity (ผลไม้)",
th: "กลิ่นแนวผลไม้",
en: "Fruity",
ja: "フルーティー (果実の香り)",
zh: "果香调",
vi: "Hương trái cây",
ko: "프루티 (과일향)",
lo: "ກິ່ນໝາກໄມ້",
it: "Fruttato",
label_th: "กลิ่นแนวผลไม้",
label_en: "Fruity"
},
{
value: "spa_relaxing",
label: "Spa & Relaxing (สปา/ผ่อนคลาย)",
th: "กลิ่นแนวสปาและผ่อนคลาย",
en: "Spa & Relaxing",
ja: "スパ&リラックス",
zh: "水疗舒缓调",
vi: "Spa & Thư giãn",
ko: "스파 & 릴랙싱",
lo: "ກິ່ນສະປາແລະຜ່ອນຄາຍ",
it: "Spa e Rilassante",
label_th: "กลิ่นแนวสปาและผ่อนคลาย",
label_en: "Spa & Relaxing"
},
{
value: "sweet_warm",
label: "Sweet & Warm (หวานละมุน/อบอุ่น)",
th: "กลิ่นแนวหวานละมุนและอบอุ่น",
en: "Sweet & Warm",
ja: "スイート&ウォーム",
zh: "甜美温馨调",
vi: "Ngọt ngào & Ấm áp",
ko: "스위트 & 웜",
lo: "ກິ່ນຫວານລະມຸນແລະອົບອຸ່ນ",
it: "Dolce e Caldo",
label_th: "กลิ่นแนวหวานละมุนและอบอุ่น",
label_en: "Sweet & Warm"
},
{
value: "aquatic_ozone",
label: "Aquatic & Ozone (อากาศ/ทะเล)",
th: "กลิ่นแนวปลอดโปร่งและทะเล",
en: "Aquatic & Ozone",
ja: "アクアティック&オゾン (海の風)",
zh: "海洋清新调",
vi: "Hương biển & Không khí",
ko: "아쿠아틱 & 오존",
lo: "ກິ່ນທະເລແລະໂອໂຊນ",
it: "Acquatico e Ozono",
label_th: "กลิ่นแนวปลอดโปร่งและทะเล",
label_en: "Aquatic & Ozone"
}
]
},
{
code: "mood_benefit",
label: "อารมณ์และคุณประโยชน์",
description: "ความรู้สึกหรือบรรยากาศที่ได้รับจากกลิ่นหอมนี้",
type: "SELECT",
unit: null,
isRequired: false,
isFilterable: true,
sortOrder: 2,
categoryId: null,
translations: {
th: { label: "อารมณ์และคุณประโยชน์", description: "บรรยากาศหรือความรู้สึกที่ได้จากกลิ่น" },
en: { label: "Mood & Benefit", description: "Atmosphere and emotional benefits provided by the aroma" },
ja: { label: "気分と効能", description: "香りがもたらす心地よい雰囲気とリラクゼーション効果" },
zh: { label: "情绪与功效", description: "香气带来的氛围感与身心情绪舒缓功效" },
vi: { label: "Cảm xúc & Lợi ích", description: "Bầu không khí và lợi ích tinh thần từ mùi hương" },
ko: { label: "무드 & 효능", description: "향기가 전달하는 감성적인 분위기와 힐링 효과" },
lo: { label: "ອາລົມແລະຄຸນປະໂຫຍດ", description: "ບັນຍາກາດຫຼືຄວາມຮູ້ສຶກທີ່ໄດ້ຈາກກິ່ນ" },
it: { label: "Umore e Benefici", description: "Atmosfera e benefici emotivi donati dalla fragranza" }
},
options: [
{
value: "relax_sleep",
label: "Relax & Sleep (ผ่อนคลาย หลับสบาย)",
th: "ผ่อนคลาย หลับสบาย",
en: "Relax & Sleep",
ja: "リラックス・快眠",
zh: "放松助眠",
vi: "Thư giãn & Ngủ ngon",
ko: "릴랙스 & 숙면",
lo: "ຜ່ອນຄາຍ ນອນຫຼັບສະບາຍ",
it: "Rilassamento e Sonno",
label_th: "ผ่อนคลาย หลับสบาย",
label_en: "Relax & Sleep"
},
{
value: "refresh_energize",
label: "Refresh & Energize (สดชื่น เติมพลัง)",
th: "สดชื่น เติมพลัง",
en: "Refresh & Energize",
ja: "リフレッシュ・活力",
zh: "提神充能",
vi: "Sảng khoái & Tươi mới",
ko: "리프레시 & 에너지",
lo: "ສົດຊື່ນ ເພີ່ມພະລັງ",
it: "Rinfrescante ed Energizzante",
label_th: "สดชื่น เติมพลัง",
label_en: "Refresh & Energize"
},
{
value: "focus_mind",
label: "Focus & Mind (สร้างสมาธิ กระตุ้นไอเดีย)",
th: "สร้างสมาธิ กระตุ้นไอเดีย",
en: "Focus & Mind",
ja: "集中力・アイデア活性",
zh: "专注冥想・激发灵感",
vi: "Tập trung & Sáng tạo",
ko: "집중력 & 아이디어 자극",
lo: "ສ້າງສະມາທິ ກະຕຸ້ນໄອເດຍ",
it: "Concentrazione e Ispirazione",
label_th: "สร้างสมาธิ กระตุ้นไอเดีย",
label_en: "Focus & Mind"
},
{
value: "odor_eliminator",
label: "Odor Eliminator (ดับกลิ่นอับ ปรับอากาศ)",
th: "ดับกลิ่นอับ ปรับอากาศ",
en: "Odor Eliminator",
ja: "消臭・空気リフレッシュ",
zh: "祛味除臭・清新空气",
vi: "Khử mùi & Làm sạch không khí",
ko: "악취 제거 & 공기 정화",
lo: "ດັບກິ່ນອັບ ປັບອາກາດ",
it: "Elimina Odori e Rinfresca",
label_th: "ดับกลิ่นอับ ปรับอากาศ",
label_en: "Odor Eliminator"
},
{
value: "romantic_cozy",
label: "Romantic & Cozy (อบอุ่น โรแมนติก)",
th: "อบอุ่น โรแมนติก",
en: "Romantic & Cozy",
ja: "ロマンチック・コージー",
zh: "浪漫温馨・惬意舒适",
vi: "Lãng mạn & Ấm cúng",
ko: "로맨틱 & 아늑함",
lo: "ອົບອຸ່ນ ໂຣແມນຕິກ",
it: "Romantico e Accogliente",
label_th: "อบอุ่น โรแมนติก",
label_en: "Romantic & Cozy"
}
]
},
{
code: "scent_intensity",
label: "ระดับความเข้มข้นของกลิ่น",
description: "ระดับการกระจายตัวและความชัดของกลิ่นหอม",
type: "SELECT",
unit: null,
isRequired: false,
isFilterable: true,
sortOrder: 3,
categoryId: null,
translations: {
th: { label: "ระดับความเข้มข้นของกลิ่น", description: "ระดับการกระจายตัวและความชัดของกลิ่น" },
en: { label: "Scent Intensity", description: "Diffusion strength and fragrance throw level" },
ja: { label: "香りの強さ", description: "香りの広がりと強さのレベル" },
zh: { label: "香气浓度", description: "香气的扩散力与浓郁程度" },
vi: { label: "Độ đậm mùi", description: "Mức độ khuếch tán và độ rõ của mùi hương" },
ko: { label: "발향 강도", description: "향의 확산력 및 발향 강도 레벨" },
lo: { label: "ລະດັບຄວາມເຂັ້ມຂຸ້ນຂອງກິ່ນ", description: "ລະດັບການກະຈາຍຕົວແລະຄວາມຊັດເຈນຂອງກິ່ນ" },
it: { label: "Intensità della fragranza", description: "Livello di forza diffusiva e persistenza olfattiva" }
},
options: [
{
value: "light",
label: "Light / Subtle (หอมละมุน บางเบา)",
th: "หอมละมุน บางเบา",
en: "Light / Subtle",
ja: "ほのか・マイルド",
zh: "柔和清淡",
vi: "Nhẹ nhàng & Tinh tế",
ko: "은은함 (라이트)",
lo: "ຫອມລະມຸນ ບາງເບົາ",
it: "Delicato / Leggero",
label_th: "หอมละมุน บางเบา",
label_en: "Light / Subtle"
},
{
value: "medium",
label: "Medium (หอมปานกลาง กำลังพอดี)",
th: "หอมปานกลาง กำลังพอดี",
en: "Medium",
ja: "適度・ミディアム",
zh: "适中平衡",
vi: "Vừa phải",
ko: "적당함 (미디엄)",
lo: "ຫອມປານກາງ ພໍດີ",
it: "Medio",
label_th: "หอมปานกลาง กำลังพอดี",
label_en: "Medium"
},
{
value: "strong",
label: "Strong / Rich (หอมชัดเจน เข้มข้น)",
th: "หอมชัดเจน เข้มข้น",
en: "Strong / Rich",
ja: "しっかり・リッチ",
zh: "浓郁显著",
vi: "Đậm đà & Lan tỏa",
ko: "풍부하고 진함 (스트롱)",
lo: "ຫອມຊັດເຈນ ເຂັ້ມຂຸ້ນ",
it: "Intenso / Forte",
label_th: "หอมชัดเจน เข้มข้น",
label_en: "Strong / Rich"
}
]
},
{
code: "room_placement",
label: "พื้นที่ใช้งานที่แนะนำ",
description: "ห้องหรือบริเวณที่เหมาะสมที่สุดสำหรับการกระจายกลิ่น",
type: "SELECT",
unit: null,
isRequired: false,
isFilterable: true,
sortOrder: 4,
categoryId: null,
translations: {
th: { label: "พื้นที่ใช้งานที่แนะนำ", description: "ห้องหรือบริเวณที่เหมาะสมสำหรับการกระจายกลิ่น" },
en: { label: "Recommended Space", description: "Ideal placement areas for optimal aroma diffusion" },
ja: { label: "おすすめの設置場所", description: "香りを最大限に楽しむための最適な設置空間" },
zh: { label: "适用空间推荐", description: "最适宜散发香氛的推荐空间或房间" },
vi: { label: "Không gian sử dụng", description: "Khu vực phù hợp nhất để khuếch tán hương thơm" },
ko: { label: "추천 사용 공간", description: "최적의 발향을 위한 권장 배치 공간" },
lo: { label: "ພື້ນທີ່ໃຊ້ງານທີ່ແນະນຳ", description: "ຫ້ອງຫຼືບໍລິເວນທີ່ເໝາະສົມສຳລັບການກະຈາຍກິ່ນ" },
it: { label: "Ambiente consigliato", description: "Aree di posizionamento ideali per una diffusione ottimale" }
},
options: [
{
value: "bedroom",
label: "ห้องนอน (Bedroom)",
th: "ห้องนอน",
en: "Bedroom",
ja: "寝室 (Bedroom)",
zh: "卧室 (Bedroom)",
vi: "Phòng ngủ",
ko: "침실 (Bedroom)",
lo: "ຫ້ອງນອນ",
it: "Camera da letto",
label_th: "ห้องนอน",
label_en: "Bedroom"
},
{
value: "living_room",
label: "ห้องนั่งเล่น / โถงกลาง (Living Room)",
th: "ห้องนั่งเล่น / โถงกลาง",
en: "Living Room",
ja: "リビングルーム",
zh: "客厅 / 起居室",
vi: "Phòng khách",
ko: "거실 (Living Room)",
lo: "ຫ້ອງນັ່ງຫຼິ້ນ",
it: "Soggiorno",
label_th: "ห้องนั่งเล่น / โถงกลาง",
label_en: "Living Room"
},
{
value: "workspace",
label: "ห้องทำงาน / โต๊ะทำงาน (Workspace)",
th: "ห้องทำงาน / โต๊ะทำงาน",
en: "Workspace",
ja: "書斎 / デスク",
zh: "书房 / 办公区",
vi: "Phòng làm việc",
ko: "서재 / 업무 공간",
lo: "ຫ້ອງເຮັດວຽກ",
it: "Ufficio / Studio",
label_th: "ห้องทำงาน / โต๊ะทำงาน",
label_en: "Workspace"
},
{
value: "bathroom",
label: "ห้องน้ำ (Bathroom)",
th: "ห้องน้ำ",
en: "Bathroom",
ja: "バスルーム・洗面所",
zh: "卫浴洗手间",
vi: "Phòng tắm",
ko: "욕실 / 화장실",
lo: "ຫ້ອງນ້ຳ",
it: "Bagno",
label_th: "ห้องน้ำ",
label_en: "Bathroom"
},
{
value: "car",
label: "ห้องโดยสารรถยนต์ (Car Interior)",
th: "ห้องโดยสารรถยนต์",
en: "Car Interior",
ja: "車内・カーインテリア",
zh: "车内空间",
vi: "Nội thất ô tô",
ko: "차량 내부",
lo: "ຫ້ອງໂດຍສານລົດ",
it: "Interno Auto",
label_th: "ห้องโดยสารรถยนต์",
label_en: "Car Interior"
},
{
value: "commercial_spa",
label: "ร้านสปา / โรงแรม / คาเฟ่ (Commercial / Spa)",
th: "ร้านสปา / โรงแรม / คาเฟ่",
en: "Commercial / Spa",
ja: "スパ・ホテル・カフェ",
zh: "SPA / 酒店 / 咖啡馆",
vi: "Spa / Khách sạn / Quán Cafe",
ko: "스파 / 호텔 / 카페",
lo: "ຮ້ານສະປາ / ໂຮງແຮມ / ຄາເຟ່",
it: "Spa / Hotel / Lounge",
label_th: "ร้านสปา / โรงแรม / คาเฟ่",
label_en: "Commercial / Spa"
}
]
},
{
code: "compatible_device",
label: "อุปกรณ์ที่รองรับ",
description: "ประเภทเครื่องพ่นหรืออุปกรณ์กระจายกลิ่นที่สามารถใช้ร่วมกันได้",
type: "TEXT",
unit: null,
options: null,
isRequired: false,
isFilterable: false,
sortOrder: 5,
categoryId: null,
translations: {
th: { label: "อุปกรณ์ที่รองรับ", description: "อุปกรณ์หรือเครื่องพ่นที่สามารถใช้ร่วมกันได้" },
en: { label: "Compatible Devices", description: "Supported diffusers and diffusion equipment" },
ja: { label: "対応機器・器具", description: "使用可能なディフューザーおよび芳香器具" },
zh: { label: "适配设备", description: "适用的香薰机、扩香器具或配件" },
vi: { label: "Thiết bị tương thích", description: "Các loại máy khuếch tán và phụ kiện hỗ trợ" },
ko: { label: "호환 기기", description: "사용 가능한 디퓨저 기기 및 발향 도구" },
lo: { label: "ອຸປະກອນທີ່ຮອງຮັບ", description: "ອຸປະກອນຫຼືເຄື່ອງພົ່ນທີ່ສາມາດໃຊ້ຮ່ວມກັນໄດ້" },
it: { label: "Dispositivi compatibili", description: "Diffusori e strumenti di diffusione compatibili" }
}
},
{
code: "coverage_area",
label: "ขนาดพื้นที่ครอบคลุม",
description: "ขนาดพื้นที่ห้องที่กลิ่นสามารถกระจายตัวได้อย่างมีประสิทธิภาพ",
type: "TEXT",
unit: "ตร.ม.",
options: null,
isRequired: false,
isFilterable: false,
sortOrder: 6,
categoryId: null,
translations: {
th: { label: "ขนาดพื้นที่ครอบคลุม", description: "ขนาดพื้นที่ห้องที่กลิ่นกระจายตัวได้ดี" },
en: { label: "Coverage Area", description: "Effective room coverage area in square meters" },
ja: { label: "推奨適用範囲", description: "香りが効果的に広がる推奨床面積(平米)" },
zh: { label: "适用面积", description: "香氛有效扩散的推荐房间面积(平方米)" },
vi: { label: "Diện tích khuếch tán", description: "Diện tích phòng hiệu quả tính theo mét vuông" },
ko: { label: "권장 사용 면적", description: "향기가 효과적으로 퍼지는 권장 면적 (제곱미터)" },
lo: { label: "ຂະໜາດພື້ນທີ່ຄອບຄຸມ", description: "ຂະໜາດພື້ນທີ່ຫ້ອງທີ່ກິ່ນກະຈາຍຕົວໄດ້ດີ" },
it: { label: "Area di copertura", description: "Area di copertura efficace in metri quadrati" }
}
},
{
code: "scent_duration",
label: "ระยะเวลาการใช้งาน",
description: "อายุการใช้งานหรือระยะเวลาที่กลิ่นหอมกระจายตัวต่อเนื่อง",
type: "TEXT",
unit: null,
options: null,
isRequired: false,
isFilterable: false,
sortOrder: 7,
categoryId: null,
translations: {
th: { label: "ระยะเวลาการใช้งาน", description: "ระยะเวลาที่กลิ่นกระจายตัวต่อเนื่อง" },
en: { label: "Estimated Duration", description: "Approximate lifespan and continuous scent duration" },
ja: { label: "使用持続期間", description: "香りが継続して香る推定期間・寿命" },
zh: { label: "留香持久时间", description: "香气持续散发的大约使用周期" },
vi: { label: "Thời gian sử dụng", description: "Tuổi thọ ước tính và thời gian tỏa hương liên tục" },
ko: { label: "사용 지속 시간", description: "향기가 지속적으로 발향되는 예상 사용 기간" },
lo: { label: "ໄລຍະເວລາການໃຊ້ງານ", description: "ໄລຍະເວລາທີ່ກິ່ນກະຈາຍຕົວຕໍ່ເນື່ອງ" },
it: { label: "Durata stimata", description: "Durata approssimativa e diffusione continua del profumo" }
}
},
{
code: "formula_base",
label: "ประเภทเบสและส่วนผสม",
description: "ชนิดของเบสน้ำมันหรือส่วนประกอบในการกระจายกลิ่น",
type: "TEXT",
unit: null,
options: null,
isRequired: false,
isFilterable: false,
sortOrder: 8,
categoryId: null,
translations: {
th: { label: "ประเภทเบสและส่วนผสม", description: "ชนิดของเบสที่ใช้ในการผสมและกระจายกลิ่น" },
en: { label: "Formula Base", description: "Base formula and carrier oil type" },
ja: { label: "ベース成分・処方", description: "調合および拡散に使用されるキャリアオイルベース" },
zh: { label: "基底配方", description: "用于调配与扩香的溶剂基底和成分类型" },
vi: { label: "Công thức nền", description: "Loại dung môi và gốc tinh dầu khuếch tán" },
ko: { label: "베이스 성분", description: "디퓨저 발향에 사용된 캐리어 베이스 및 포뮬러" },
lo: { label: "ປະເພດເບສແລະສ່ວນປະສົມ", description: "ຊະນິດຂອງເບສທີ່ໃຊ້ໃນການປະສົມແລະກະຈາຍກິ່ນ" },
it: { label: "Base formulativa", description: "Formula base e tipo di solvente/olio vettore" }
}
},
{
code: "usage_instructions",
label: "วิธีใช้งานและคำแนะนำ",
description: "ขั้นตอนการใช้งานที่ถูกต้องเพื่อประสิทธิภาพสูงสุดในการกระจายกลิ่น",
type: "TEXT",
unit: null,
options: null,
isRequired: false,
isFilterable: false,
sortOrder: 9,
categoryId: null,
translations: {
th: { label: "วิธีใช้งานและคำแนะนำ", description: "ขั้นตอนการใช้งานเพื่อประสิทธิภาพสูงสุด" },
en: { label: "Usage Instructions", description: "Recommended steps for best diffusion experience" },
ja: { label: "使用方法・注意点", description: "最高の香り体験を得るための正しい使用手順" },
zh: { label: "使用方法与建议", description: "获得最佳扩香效果的推荐使用步骤与保养指南" },
vi: { label: "Hướng dẫn sử dụng", description: "Các bước sử dụng khuyến nghị để đạt hiệu quả tỏa hương tốt nhất" },
ko: { label: "사용 방법 및 권장사항", description: "최상의 발향 효과를 위한 올바른 사용 가이드" },
lo: { label: "ວິທີໃຊ້ງານແລະຄຳແນະນຳ", description: "ຂັ້ນຕອນການໃຊ້ງານເພື່ອປະສິດທິພາບສູງສຸດ" },
it: { label: "Istruzioni per l'uso", description: "Passaggi consigliati per una diffusione e resa ottimali" }
}
}
];
async function main() {
console.log("=== Enriching Product Attribute Definitions with All 8 Languages ===");
// 1. Save to data/homaroma_attribute_definitions.json
const jsonPath = path.resolve(__dirname, "../data/homaroma_attribute_definitions.json");
fs.writeFileSync(jsonPath, JSON.stringify(FULL_8LANG_DEFINITIONS, null, 2), "utf-8");
console.log(`✓ Updated ${jsonPath} with full 8-language translations.`);
// 2. Authenticate to get JWT token
console.log(`2. Logging in as '${ADMIN_USER}' on ${API_BASE}...`);
const loginRes = await fetch(`${API_BASE}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: ADMIN_USER, password: ADMIN_PASS }),
});
if (!loginRes.ok) {
throw new Error(`Login failed (${loginRes.status}): ${await loginRes.text()}`);
}
const { token } = (await loginRes.json()) as { token: string };
console.log("✓ Authenticated successfully. JWT token acquired.\n");
// 3. Post bulk-import to Production API
console.log("3. Sending enriched 8-language definitions to Production API...");
const importRes = await fetch(`${API_BASE}/product-attributes/definitions/bulk-import`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(FULL_8LANG_DEFINITIONS),
});
if (!importRes.ok) {
throw new Error(`Bulk import failed (${importRes.status}): ${await importRes.text()}`);
}
const importResult = await importRes.json();
console.log("✓ API Bulk import result:", JSON.stringify(importResult, null, 2));
// 4. Sync to Local Database via Prisma
try {
console.log("\n4. Syncing enriched definitions to Local Database via Prisma...");
for (const def of FULL_8LANG_DEFINITIONS) {
const savedDef = await prisma.attributeDefinition.upsert({
where: { code: def.code },
create: {
code: def.code,
label: def.label,
description: def.description,
type: def.type,
unit: def.unit,
options: def.options ?? undefined,
isRequired: def.isRequired,
isFilterable: def.isFilterable,
sortOrder: def.sortOrder,
categoryId: def.categoryId,
},
update: {
label: def.label,
description: def.description,
type: def.type,
unit: def.unit,
options: def.options ?? undefined,
isRequired: def.isRequired,
isFilterable: def.isFilterable,
sortOrder: def.sortOrder,
categoryId: def.categoryId,
},
});
// Upsert translations for all 8 languages
if (def.translations) {
for (const [langCode, trans] of Object.entries(def.translations)) {
await prisma.attributeDefinitionTranslation.upsert({
where: {
attributeDefinitionId_langCode: {
attributeDefinitionId: savedDef.id,
langCode,
},
},
create: {
attributeDefinitionId: savedDef.id,
langCode,
label: trans.label,
description: trans.description,
},
update: {
label: trans.label,
description: trans.description,
},
});
}
}
}
console.log("✓ All 9 definitions and 8-language translations synced to Local Database.");
} catch (err: any) {
console.warn(`Local DB sync warning: ${err.message}`);
}
}
main()
.then(() => {
console.log("\n✅ Attribute Definitions successfully enriched and synced across all 8 languages!");
process.exit(0);
})
.catch((err) => {
console.error("\n❌ Error:", err);
process.exit(1);
});