diff --git a/backend/melodies/models.py b/backend/melodies/models.py index dfd5177..6000c37 100644 --- a/backend/melodies/models.py +++ b/backend/melodies/models.py @@ -1,5 +1,5 @@ from pydantic import BaseModel, Field -from typing import Dict, List, Optional +from typing import List, Optional from enum import Enum @@ -17,8 +17,8 @@ class MelodyTone(str, Enum): class MelodyInfo(BaseModel): - name: Dict[str, str] = {} - description: Dict[str, str] = {} + name: str = "" + description: str = "" melodyTone: MelodyTone = MelodyTone.normal customTags: List[str] = [] minSpeed: int = 0 diff --git a/backend/melodies/service.py b/backend/melodies/service.py index fa780e3..9ffcda7 100644 --- a/backend/melodies/service.py +++ b/backend/melodies/service.py @@ -1,3 +1,5 @@ +import json + from shared.firebase import get_db, get_bucket from shared.exceptions import NotFoundError from melodies.models import MelodyCreate, MelodyUpdate, MelodyInDB @@ -5,16 +7,22 @@ from melodies.models import MelodyCreate, MelodyUpdate, MelodyInDB COLLECTION = "melodies" +def _parse_localized_string(value: str) -> dict: + """Parse a JSON-encoded localized string into a dict. Returns {} on failure.""" + if not value: + return {} + try: + parsed = json.loads(value) + if isinstance(parsed, dict): + return parsed + except (json.JSONDecodeError, TypeError): + pass + return {} + + def _doc_to_melody(doc) -> MelodyInDB: """Convert a Firestore document snapshot to a MelodyInDB model.""" data = doc.to_dict() - # Backward compat: if name/description are plain strings, wrap as dict - info = data.get("information", {}) - if isinstance(info.get("name"), str): - info["name"] = {"en": info["name"]} if info["name"] else {} - if isinstance(info.get("description"), str): - info["description"] = {"en": info["description"]} if info["description"] else {} - data["information"] = info return MelodyInDB(id=doc.id, **data) @@ -48,14 +56,16 @@ def list_melodies( continue if search: search_lower = search.lower() + name_dict = _parse_localized_string(melody.information.name) + desc_dict = _parse_localized_string(melody.information.description) name_match = any( search_lower in v.lower() - for v in melody.information.name.values() + for v in name_dict.values() if isinstance(v, str) ) desc_match = any( search_lower in v.lower() - for v in melody.information.description.values() + for v in desc_dict.values() if isinstance(v, str) ) tag_match = any(search_lower in t.lower() for t in melody.information.customTags) diff --git a/frontend/src/melodies/MelodyForm.jsx b/frontend/src/melodies/MelodyForm.jsx index 46dded6..f926330 100644 --- a/frontend/src/melodies/MelodyForm.jsx +++ b/frontend/src/melodies/MelodyForm.jsx @@ -7,14 +7,16 @@ import { getLanguageName, normalizeColor, formatDuration, + parseLocalizedString, + serializeLocalizedString, } from "./melodyUtils"; const MELODY_TYPES = ["orthodox", "catholic", "all"]; const MELODY_TONES = ["normal", "festive", "cheerful", "lamentation"]; const defaultInfo = { - name: {}, - description: {}, + name: "", + description: "", melodyTone: "normal", customTags: [], minSpeed: 0, @@ -155,7 +157,9 @@ export default function MelodyForm() { // Update a localized field for the current edit language const updateLocalizedField = (fieldKey, text) => { - updateInfo(fieldKey, { ...information[fieldKey], [editLang]: text }); + const dict = parseLocalizedString(information[fieldKey]); + dict[editLang] = text; + updateInfo(fieldKey, serializeLocalizedString(dict)); }; const handleSubmit = async (e) => { @@ -836,8 +840,8 @@ export default function MelodyForm() { setTranslationModal((prev) => ({ ...prev, open: false })) } field={translationModal.field} - value={information[translationModal.fieldKey] || {}} - onChange={(updated) => updateInfo(translationModal.fieldKey, updated)} + value={parseLocalizedString(information[translationModal.fieldKey])} + onChange={(updated) => updateInfo(translationModal.fieldKey, serializeLocalizedString(updated))} languages={languages} multiline={translationModal.multiline} /> diff --git a/frontend/src/melodies/melodyUtils.js b/frontend/src/melodies/melodyUtils.js index e3c7949..be0aab0 100644 --- a/frontend/src/melodies/melodyUtils.js +++ b/frontend/src/melodies/melodyUtils.js @@ -10,11 +10,36 @@ export function formatDuration(seconds) { } /** - * Get a localized value from a dict, with fallback chain: + * Parse a JSON-encoded localized string into a dict. + * If already a dict, returns it as-is. Returns {} on failure. + */ +export function parseLocalizedString(value) { + if (!value) return {}; + if (typeof value === "object") return value; + try { + const parsed = JSON.parse(value); + if (typeof parsed === "object" && parsed !== null) return parsed; + } catch { + // Not valid JSON — treat as plain string + return { en: value }; + } + return {}; +} + +/** + * Serialize a localized dict back to a JSON string for storage. + */ +export function serializeLocalizedString(dict) { + if (!dict || typeof dict !== "object") return ""; + return JSON.stringify(dict); +} + +/** + * Get a localized value from a JSON string or dict, with fallback chain: * selected lang → "en" → first available value → fallback */ -export function getLocalizedValue(dict, lang, fallback = "") { - if (!dict || typeof dict !== "object") return fallback; +export function getLocalizedValue(value, lang, fallback = "") { + const dict = parseLocalizedString(value); return dict[lang] || dict["en"] || Object.values(dict)[0] || fallback; }