mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18:41:08 +02:00
i18n(wardrobe): translate all 5 locales — 36 strings
Adds wardrobe namespace (de/en/es/fr/it) covering ListView,
GridView, OutfitsView, DetailGarmentView, DetailOutfitView,
GarmentForm, OutfitComposer, GarmentTryOnButton, TryOnButton,
TryOnModelPicker, CategoryTabs, GarmentCard, OutfitCard, plus
the /wardrobe/compose route. Categories/occasions/seasons routed
through dynamic `wardrobe.categories.{key}` lookups so constants.ts
keeps the order-tuples without leaking DE labels into UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8804a20a7f
commit
5959f66387
21 changed files with 1677 additions and 274 deletions
257
apps/mana/apps/web/src/lib/i18n/locales/wardrobe/de.json
Normal file
257
apps/mana/apps/web/src/lib/i18n/locales/wardrobe/de.json
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
{
|
||||
"categories": {
|
||||
"all": "Alle",
|
||||
"top": "Oberteile",
|
||||
"bottom": "Hosen",
|
||||
"dress": "Kleider",
|
||||
"outerwear": "Jacken",
|
||||
"shoes": "Schuhe",
|
||||
"bag": "Taschen",
|
||||
"accessory": "Accessoires",
|
||||
"glasses": "Brillen",
|
||||
"jewelry": "Schmuck",
|
||||
"hat": "Kopfbedeckung",
|
||||
"other": "Sonstiges"
|
||||
},
|
||||
"categories_singular": {
|
||||
"top": "Oberteil",
|
||||
"bottom": "Hose",
|
||||
"dress": "Kleid",
|
||||
"outerwear": "Jacke",
|
||||
"shoes": "Schuh",
|
||||
"bag": "Tasche",
|
||||
"accessory": "Accessoire",
|
||||
"glasses": "Brille",
|
||||
"jewelry": "Schmuck",
|
||||
"hat": "Kopfbedeckung",
|
||||
"other": "Item"
|
||||
},
|
||||
"occasions": {
|
||||
"casual": "Casual",
|
||||
"work": "Arbeit",
|
||||
"formal": "Festlich",
|
||||
"workout": "Sport",
|
||||
"date": "Date",
|
||||
"travel": "Reise",
|
||||
"event": "Event",
|
||||
"sleep": "Schlafanzug",
|
||||
"other": "Sonstiges"
|
||||
},
|
||||
"seasons": {
|
||||
"spring": "Frühling",
|
||||
"summer": "Sommer",
|
||||
"autumn": "Herbst",
|
||||
"winter": "Winter"
|
||||
},
|
||||
"piece_singular": "Stück",
|
||||
"piece_plural": "Stücke",
|
||||
"upload_failed": "Upload fehlgeschlagen",
|
||||
"list_view": {
|
||||
"aria_tabs": "Ansicht wechseln",
|
||||
"tab_garments": "Kleidung",
|
||||
"tab_outfits": "Outfits",
|
||||
"face_saved_title": "Gesichtsbild gespeichert",
|
||||
"face_saved_hint": "Perfekt — als nächstes lädst du unten dein erstes Kleidungsstück hoch.",
|
||||
"dismiss": "Schließen",
|
||||
"face_prompt_title": "Lade ein Gesichtsbild hoch",
|
||||
"face_prompt_desc": "Wir brauchen dich auf Bild, damit Try-On Kleidung an dir visualisieren kann. Das Bild bleibt lokal und wird nur für deine eigenen Generierungen genutzt.",
|
||||
"face_uploading_label": "Wird hochgeladen…",
|
||||
"face_upload_label": "Gesichtsbild hochladen",
|
||||
"face_upload_hint": "Kopf + Schulter, möglichst neutrale Beleuchtung",
|
||||
"face_uploading_chip": "Lade…"
|
||||
},
|
||||
"grid_view": {
|
||||
"upload_label_all": "Kleidungsstück hochladen",
|
||||
"upload_label_for_category": "{category} hochladen",
|
||||
"upload_hint": "Foto frontal, heller Hintergrund — bessere Try-On-Ergebnisse",
|
||||
"empty_title": "Noch nichts im Schrank.",
|
||||
"empty_hint": "Zieh ein Foto in die Zone oben — oder klick sie an, um eins auszuwählen.",
|
||||
"no_entries_under": "Keine Einträge unter {category}.",
|
||||
"space_footer": "Dieser Schrank gehört zu {name} — Uploads landen nur hier, nicht in deinem persönlichen Schrank."
|
||||
},
|
||||
"outfits_view": {
|
||||
"title": "Outfits",
|
||||
"count_singular": "Zusammenstellung",
|
||||
"count_plural": "Zusammenstellungen",
|
||||
"action_new": "Neues Outfit",
|
||||
"empty_title": "Noch keine Outfits.",
|
||||
"empty_no_garments": "Füge zuerst ein paar Kleidungsstücke im Tab \"Kleidung\" hinzu — danach lassen sie sich hier zu Outfits kombinieren.",
|
||||
"empty_with_garments": "Kombiniere deine Kleidungsstücke zu Looks, die du dann mit KI an dir selbst anprobieren kannst.",
|
||||
"action_compose_first": "Erstes Outfit komponieren"
|
||||
},
|
||||
"detail_garment": {
|
||||
"loading": "Lädt…",
|
||||
"not_found_title": "Nicht gefunden.",
|
||||
"not_found_desc": "Das Kleidungsstück wurde gelöscht oder gehört zu einem anderen Space.",
|
||||
"action_enlarge": "Foto vergrößern",
|
||||
"action_edit": "Bearbeiten",
|
||||
"label_brand": "Marke",
|
||||
"label_color": "Farbe",
|
||||
"label_size": "Größe",
|
||||
"label_material": "Material",
|
||||
"label_price": "Preis",
|
||||
"label_wear_count": "Getragen",
|
||||
"wear_count_value": "{count}×",
|
||||
"last_worn_suffix": " · zuletzt {date}",
|
||||
"action_comic": "Als Comic-Character",
|
||||
"action_comic_title": "Aus diesem Kleidungsstück einen Comic-Character generieren",
|
||||
"action_marking": "Gespeichert…",
|
||||
"action_mark_worn": "Heute getragen",
|
||||
"action_unarchive": "Wieder aktiv setzen",
|
||||
"action_archive": "Archivieren",
|
||||
"action_delete": "Löschen",
|
||||
"section_try_ons": "Anproben · {count}",
|
||||
"section_outfits": "In Outfits · {count}",
|
||||
"no_try_on_yet": "Noch keine Anprobe",
|
||||
"action_open_picture": "In Picture öffnen",
|
||||
"confirm_delete": "\"{name}\" wirklich löschen?"
|
||||
},
|
||||
"detail_outfit": {
|
||||
"back": "Zurück zum Kleiderschrank",
|
||||
"breadcrumb": "Kleiderschrank · Outfits",
|
||||
"loading": "Lädt…",
|
||||
"not_found_title": "Outfit nicht gefunden.",
|
||||
"not_found_desc": "Gelöscht oder in einem anderen Space.",
|
||||
"try_on_preview_alt": "Try-On Vorschau",
|
||||
"no_garments": "Keine Kleidungsstücke",
|
||||
"action_comic": "Als Comic-Character",
|
||||
"action_comic_title": "Aus diesem Outfit einen Comic-Character generieren",
|
||||
"try_on_history": "Try-On Verlauf",
|
||||
"action_unfavorite": "Favorit entfernen",
|
||||
"action_favorite": "Als Favorit markieren",
|
||||
"action_edit": "Bearbeiten",
|
||||
"label_visibility": "Sichtbarkeit",
|
||||
"section_composition": "Zusammenstellung",
|
||||
"composition_missing": "Referenzierte Kleidungsstücke wurden entfernt oder gehören zu einem anderen Space.",
|
||||
"action_unarchive": "Wieder aktiv",
|
||||
"action_archive": "Archivieren",
|
||||
"action_delete": "Löschen",
|
||||
"confirm_delete": "Outfit \"{name}\" wirklich löschen?"
|
||||
},
|
||||
"garment_form": {
|
||||
"err_name_required": "Name darf nicht leer sein",
|
||||
"err_save_failed": "Speichern fehlgeschlagen",
|
||||
"label_name": "Name",
|
||||
"placeholder_name": "z.B. Blau-weiß gestreiftes Hemd",
|
||||
"label_category": "Kategorie",
|
||||
"label_brand": "Marke",
|
||||
"placeholder_brand": "z.B. Uniqlo",
|
||||
"label_color": "Farbe",
|
||||
"placeholder_color": "z.B. navy",
|
||||
"label_size": "Größe",
|
||||
"placeholder_size": "z.B. M oder 42",
|
||||
"label_material": "Material",
|
||||
"placeholder_material": "z.B. Baumwolle",
|
||||
"label_tags": "Tags",
|
||||
"tags_hint": "(komma-getrennt)",
|
||||
"placeholder_tags": "formal, sommer, lieblingsstück",
|
||||
"label_price": "Preis",
|
||||
"aria_currency": "Währung",
|
||||
"label_notes": "Notizen",
|
||||
"placeholder_notes": "Anlass, Tragevorschriften, …",
|
||||
"action_saving": "Speichere…",
|
||||
"action_save": "Speichern",
|
||||
"action_cancel": "Abbrechen"
|
||||
},
|
||||
"composer": {
|
||||
"err_name_required": "Gib dem Outfit einen Namen.",
|
||||
"err_no_garments": "Wähle mindestens ein Kleidungsstück aus.",
|
||||
"err_save_failed": "Speichern fehlgeschlagen",
|
||||
"section_library": "Kleiderschrank",
|
||||
"available_singular": "{count} Stück verfügbar",
|
||||
"available_plural": "{count} Stücke verfügbar",
|
||||
"empty_title": "Nichts zum Kombinieren.",
|
||||
"empty_hint_prefix": "Lade zuerst ein paar Kleidungsstücke im Tab",
|
||||
"tab_garments_link": "Kleidung",
|
||||
"empty_hint_suffix": "hoch.",
|
||||
"label_name": "Name",
|
||||
"placeholder_name": "z.B. Bürooutfit Juni",
|
||||
"label_description": "Beschreibung",
|
||||
"placeholder_description": "Für welchen Anlass? Besonderheiten?",
|
||||
"label_occasion": "Anlass",
|
||||
"no_occasion": "— kein Anlass —",
|
||||
"label_seasons": "Jahreszeit",
|
||||
"label_tags": "Tags",
|
||||
"tags_hint": "(komma-getrennt)",
|
||||
"placeholder_tags": "minimal, layering, meeting",
|
||||
"section_composition": "Zusammenstellung",
|
||||
"composition_count_singular": "· {count} Stück",
|
||||
"composition_count_plural": "· {count} Stücke",
|
||||
"composition_empty": "Klicke links auf Kleidungsstücke, um sie dem Outfit hinzuzufügen.",
|
||||
"action_remove": "Aus Outfit entfernen",
|
||||
"action_saving": "Speichere…",
|
||||
"action_save_edit": "Änderungen speichern",
|
||||
"action_save_new": "Outfit anlegen",
|
||||
"action_cancel": "Abbrechen"
|
||||
},
|
||||
"try_on_garment": {
|
||||
"err_failed": "Try-On fehlgeschlagen",
|
||||
"err_upload": "Upload fehlgeschlagen",
|
||||
"no_photo": "Lade erst ein Foto hoch, um dieses Stück an dir zu visualisieren.",
|
||||
"refs_title": "Für Solo-Try-On brauchen wir dich auf Bild.",
|
||||
"refs_accessory": "Ein Gesichtsbild reicht — das Stück wird darauf montiert.",
|
||||
"refs_full": "Ein Gesichts- und ein Ganzkörperbild. Beide werden nur für deine eigenen Generierungen genutzt.",
|
||||
"upload_face": "Gesichtsbild hochladen",
|
||||
"upload_body": "Ganzkörperbild hochladen",
|
||||
"face_hint": "Kopf + Schulter, möglichst neutrale Beleuchtung",
|
||||
"body_hint": "Stehend, freier Hintergrund, gut erkennbare Haltung",
|
||||
"refs_more_prefix": "Weitere Referenzen oder AI-Opt-ins pro Bild:",
|
||||
"refs_link": "Meine Bilder",
|
||||
"rendering": "Rendere…",
|
||||
"cta": "An mir anprobieren",
|
||||
"credits": "{count} Credits",
|
||||
"accessory_hint": "Accessoire-Modus — nur das Gesicht wird gerendert (spart Credits).",
|
||||
"space_hint_prefix": "Try-On nutzt deine Referenzbilder aus diesem Space",
|
||||
"space_hint_suffix": ", nicht aus Persönlich.",
|
||||
"result_label": "Ergebnis",
|
||||
"try_on_alt": "Try-On",
|
||||
"result_hint_prefix": "Gefunden in der",
|
||||
"picture_gallery_link": "Picture-Galerie",
|
||||
"result_hint_suffix": "als normale Generierung."
|
||||
},
|
||||
"try_on_outfit": {
|
||||
"err_failed": "Try-On fehlgeschlagen",
|
||||
"err_upload": "Upload fehlgeschlagen",
|
||||
"refs_title": "Für Try-On brauchen wir dich auf Bild.",
|
||||
"refs_accessory": "Ein Gesichtsbild reicht — der Rest bleibt wie auf deinem Foto.",
|
||||
"refs_full": "Ein Gesichts- und ein Ganzkörperbild. Beide werden nur für deine eigenen Generierungen genutzt.",
|
||||
"upload_face": "Gesichtsbild hochladen",
|
||||
"upload_body": "Ganzkörperbild hochladen",
|
||||
"face_hint": "Kopf + Schulter, möglichst neutrale Beleuchtung",
|
||||
"body_hint": "Stehend, freier Hintergrund, gut erkennbare Haltung",
|
||||
"refs_more_prefix": "Weitere Referenzen oder AI-Opt-ins pro Bild:",
|
||||
"refs_link": "Meine Bilder",
|
||||
"rendering": "Rendere…",
|
||||
"cta": "Anprobieren",
|
||||
"credits": "{count} Credits",
|
||||
"accessory_hint": "Accessoire-Modus — nur das Gesicht wird gerendert (spart Credits).",
|
||||
"many_garments_hint": "Mit {count} Kleidungsstücken ist der Referenz-Slot knapp — ältere Items werden evtl. nicht mitgezogen.",
|
||||
"space_hint_prefix": "Try-On nutzt deine Referenzbilder aus diesem Space",
|
||||
"space_hint_suffix": ", nicht aus Persönlich.",
|
||||
"family_hint": "Kinder-Outfits werden trotzdem auf dein Gesicht gerendert.",
|
||||
"empty_garments": "Füge mindestens ein {category} hinzu, um Try-On zu aktivieren."
|
||||
},
|
||||
"model_picker": {
|
||||
"legend": "Modell",
|
||||
"option_openai_label": "OpenAI",
|
||||
"option_openai_hint": "GPT-image · Standard",
|
||||
"option_pro_label": "Nano Banana Pro",
|
||||
"option_pro_hint": "Google · hohe Konsistenz",
|
||||
"option_flash_label": "Nano Banana 2",
|
||||
"option_flash_hint": "Google · neuestes · günstig"
|
||||
},
|
||||
"garment_card": {
|
||||
"wear_count_title": "{count}× getragen"
|
||||
},
|
||||
"outfit_card": {
|
||||
"try_on_badge": "Try-On",
|
||||
"try_on_preview_title": "Try-On Vorschau",
|
||||
"empty": "Leer",
|
||||
"favorite": "Favorit"
|
||||
},
|
||||
"compose": {
|
||||
"title_edit": "Outfit bearbeiten",
|
||||
"title_new": "Neues Outfit",
|
||||
"back": "Zurück"
|
||||
}
|
||||
}
|
||||
257
apps/mana/apps/web/src/lib/i18n/locales/wardrobe/en.json
Normal file
257
apps/mana/apps/web/src/lib/i18n/locales/wardrobe/en.json
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
{
|
||||
"categories": {
|
||||
"all": "All",
|
||||
"top": "Tops",
|
||||
"bottom": "Bottoms",
|
||||
"dress": "Dresses",
|
||||
"outerwear": "Jackets",
|
||||
"shoes": "Shoes",
|
||||
"bag": "Bags",
|
||||
"accessory": "Accessories",
|
||||
"glasses": "Glasses",
|
||||
"jewelry": "Jewelry",
|
||||
"hat": "Hats",
|
||||
"other": "Other"
|
||||
},
|
||||
"categories_singular": {
|
||||
"top": "Top",
|
||||
"bottom": "Bottom",
|
||||
"dress": "Dress",
|
||||
"outerwear": "Jacket",
|
||||
"shoes": "Shoe",
|
||||
"bag": "Bag",
|
||||
"accessory": "Accessory",
|
||||
"glasses": "Glasses",
|
||||
"jewelry": "Jewelry",
|
||||
"hat": "Hat",
|
||||
"other": "Item"
|
||||
},
|
||||
"occasions": {
|
||||
"casual": "Casual",
|
||||
"work": "Work",
|
||||
"formal": "Formal",
|
||||
"workout": "Workout",
|
||||
"date": "Date",
|
||||
"travel": "Travel",
|
||||
"event": "Event",
|
||||
"sleep": "Sleepwear",
|
||||
"other": "Other"
|
||||
},
|
||||
"seasons": {
|
||||
"spring": "Spring",
|
||||
"summer": "Summer",
|
||||
"autumn": "Autumn",
|
||||
"winter": "Winter"
|
||||
},
|
||||
"piece_singular": "piece",
|
||||
"piece_plural": "pieces",
|
||||
"upload_failed": "Upload failed",
|
||||
"list_view": {
|
||||
"aria_tabs": "Switch view",
|
||||
"tab_garments": "Clothing",
|
||||
"tab_outfits": "Outfits",
|
||||
"face_saved_title": "Face photo saved",
|
||||
"face_saved_hint": "Perfect — next, upload your first piece of clothing below.",
|
||||
"dismiss": "Close",
|
||||
"face_prompt_title": "Upload a face photo",
|
||||
"face_prompt_desc": "We need you in a photo so Try-On can visualize clothing on you. The image stays local and is only used for your own generations.",
|
||||
"face_uploading_label": "Uploading…",
|
||||
"face_upload_label": "Upload face photo",
|
||||
"face_upload_hint": "Head + shoulders, neutral lighting if possible",
|
||||
"face_uploading_chip": "Loading…"
|
||||
},
|
||||
"grid_view": {
|
||||
"upload_label_all": "Upload garment",
|
||||
"upload_label_for_category": "Upload {category}",
|
||||
"upload_hint": "Front-on photo, bright background — better Try-On results",
|
||||
"empty_title": "Nothing in the wardrobe yet.",
|
||||
"empty_hint": "Drag a photo into the zone above — or click it to pick one.",
|
||||
"no_entries_under": "No items under {category}.",
|
||||
"space_footer": "This wardrobe belongs to {name} — uploads only land here, not in your personal wardrobe."
|
||||
},
|
||||
"outfits_view": {
|
||||
"title": "Outfits",
|
||||
"count_singular": "outfit",
|
||||
"count_plural": "outfits",
|
||||
"action_new": "New outfit",
|
||||
"empty_title": "No outfits yet.",
|
||||
"empty_no_garments": "First add a few pieces of clothing in the \"Clothing\" tab — then you can combine them into outfits here.",
|
||||
"empty_with_garments": "Combine your clothing pieces into looks that you can then try on yourself with AI.",
|
||||
"action_compose_first": "Compose first outfit"
|
||||
},
|
||||
"detail_garment": {
|
||||
"loading": "Loading…",
|
||||
"not_found_title": "Not found.",
|
||||
"not_found_desc": "The garment was deleted or belongs to another space.",
|
||||
"action_enlarge": "Enlarge photo",
|
||||
"action_edit": "Edit",
|
||||
"label_brand": "Brand",
|
||||
"label_color": "Color",
|
||||
"label_size": "Size",
|
||||
"label_material": "Material",
|
||||
"label_price": "Price",
|
||||
"label_wear_count": "Worn",
|
||||
"wear_count_value": "{count}×",
|
||||
"last_worn_suffix": " · last on {date}",
|
||||
"action_comic": "As comic character",
|
||||
"action_comic_title": "Generate a comic character from this garment",
|
||||
"action_marking": "Saving…",
|
||||
"action_mark_worn": "Worn today",
|
||||
"action_unarchive": "Set active again",
|
||||
"action_archive": "Archive",
|
||||
"action_delete": "Delete",
|
||||
"section_try_ons": "Try-Ons · {count}",
|
||||
"section_outfits": "In outfits · {count}",
|
||||
"no_try_on_yet": "No try-on yet",
|
||||
"action_open_picture": "Open in Picture",
|
||||
"confirm_delete": "Really delete \"{name}\"?"
|
||||
},
|
||||
"detail_outfit": {
|
||||
"back": "Back to wardrobe",
|
||||
"breadcrumb": "Wardrobe · Outfits",
|
||||
"loading": "Loading…",
|
||||
"not_found_title": "Outfit not found.",
|
||||
"not_found_desc": "Deleted or in another space.",
|
||||
"try_on_preview_alt": "Try-On preview",
|
||||
"no_garments": "No garments",
|
||||
"action_comic": "As comic character",
|
||||
"action_comic_title": "Generate a comic character from this outfit",
|
||||
"try_on_history": "Try-On history",
|
||||
"action_unfavorite": "Remove favorite",
|
||||
"action_favorite": "Mark as favorite",
|
||||
"action_edit": "Edit",
|
||||
"label_visibility": "Visibility",
|
||||
"section_composition": "Composition",
|
||||
"composition_missing": "Referenced garments were removed or belong to another space.",
|
||||
"action_unarchive": "Set active",
|
||||
"action_archive": "Archive",
|
||||
"action_delete": "Delete",
|
||||
"confirm_delete": "Really delete outfit \"{name}\"?"
|
||||
},
|
||||
"garment_form": {
|
||||
"err_name_required": "Name cannot be empty",
|
||||
"err_save_failed": "Save failed",
|
||||
"label_name": "Name",
|
||||
"placeholder_name": "e.g. Blue and white striped shirt",
|
||||
"label_category": "Category",
|
||||
"label_brand": "Brand",
|
||||
"placeholder_brand": "e.g. Uniqlo",
|
||||
"label_color": "Color",
|
||||
"placeholder_color": "e.g. navy",
|
||||
"label_size": "Size",
|
||||
"placeholder_size": "e.g. M or 42",
|
||||
"label_material": "Material",
|
||||
"placeholder_material": "e.g. cotton",
|
||||
"label_tags": "Tags",
|
||||
"tags_hint": "(comma-separated)",
|
||||
"placeholder_tags": "formal, summer, favorite",
|
||||
"label_price": "Price",
|
||||
"aria_currency": "Currency",
|
||||
"label_notes": "Notes",
|
||||
"placeholder_notes": "Occasion, care instructions, …",
|
||||
"action_saving": "Saving…",
|
||||
"action_save": "Save",
|
||||
"action_cancel": "Cancel"
|
||||
},
|
||||
"composer": {
|
||||
"err_name_required": "Give the outfit a name.",
|
||||
"err_no_garments": "Pick at least one garment.",
|
||||
"err_save_failed": "Save failed",
|
||||
"section_library": "Wardrobe",
|
||||
"available_singular": "{count} piece available",
|
||||
"available_plural": "{count} pieces available",
|
||||
"empty_title": "Nothing to combine.",
|
||||
"empty_hint_prefix": "First upload a few garments in the",
|
||||
"tab_garments_link": "Clothing",
|
||||
"empty_hint_suffix": "tab.",
|
||||
"label_name": "Name",
|
||||
"placeholder_name": "e.g. Office outfit June",
|
||||
"label_description": "Description",
|
||||
"placeholder_description": "For what occasion? Anything special?",
|
||||
"label_occasion": "Occasion",
|
||||
"no_occasion": "— no occasion —",
|
||||
"label_seasons": "Season",
|
||||
"label_tags": "Tags",
|
||||
"tags_hint": "(comma-separated)",
|
||||
"placeholder_tags": "minimal, layering, meeting",
|
||||
"section_composition": "Composition",
|
||||
"composition_count_singular": "· {count} piece",
|
||||
"composition_count_plural": "· {count} pieces",
|
||||
"composition_empty": "Click garments on the left to add them to the outfit.",
|
||||
"action_remove": "Remove from outfit",
|
||||
"action_saving": "Saving…",
|
||||
"action_save_edit": "Save changes",
|
||||
"action_save_new": "Create outfit",
|
||||
"action_cancel": "Cancel"
|
||||
},
|
||||
"try_on_garment": {
|
||||
"err_failed": "Try-On failed",
|
||||
"err_upload": "Upload failed",
|
||||
"no_photo": "Upload a photo first to visualize this piece on yourself.",
|
||||
"refs_title": "We need you in a photo for solo try-on.",
|
||||
"refs_accessory": "A face photo is enough — the piece is mounted onto it.",
|
||||
"refs_full": "A face and a full-body photo. Both are only used for your own generations.",
|
||||
"upload_face": "Upload face photo",
|
||||
"upload_body": "Upload full-body photo",
|
||||
"face_hint": "Head + shoulders, neutral lighting if possible",
|
||||
"body_hint": "Standing, free background, posture clearly visible",
|
||||
"refs_more_prefix": "More references or per-image AI opt-ins:",
|
||||
"refs_link": "My Images",
|
||||
"rendering": "Rendering…",
|
||||
"cta": "Try on me",
|
||||
"credits": "{count} credits",
|
||||
"accessory_hint": "Accessory mode — only the face is rendered (saves credits).",
|
||||
"space_hint_prefix": "Try-On uses your reference images from this space",
|
||||
"space_hint_suffix": ", not from Personal.",
|
||||
"result_label": "Result",
|
||||
"try_on_alt": "Try-On",
|
||||
"result_hint_prefix": "Found in the",
|
||||
"picture_gallery_link": "Picture gallery",
|
||||
"result_hint_suffix": "as a regular generation."
|
||||
},
|
||||
"try_on_outfit": {
|
||||
"err_failed": "Try-On failed",
|
||||
"err_upload": "Upload failed",
|
||||
"refs_title": "We need you in a photo for try-on.",
|
||||
"refs_accessory": "A face photo is enough — the rest stays as in your photo.",
|
||||
"refs_full": "A face and a full-body photo. Both are only used for your own generations.",
|
||||
"upload_face": "Upload face photo",
|
||||
"upload_body": "Upload full-body photo",
|
||||
"face_hint": "Head + shoulders, neutral lighting if possible",
|
||||
"body_hint": "Standing, free background, posture clearly visible",
|
||||
"refs_more_prefix": "More references or per-image AI opt-ins:",
|
||||
"refs_link": "My Images",
|
||||
"rendering": "Rendering…",
|
||||
"cta": "Try on",
|
||||
"credits": "{count} credits",
|
||||
"accessory_hint": "Accessory mode — only the face is rendered (saves credits).",
|
||||
"many_garments_hint": "With {count} garments the reference slot is tight — older items may not be carried over.",
|
||||
"space_hint_prefix": "Try-On uses your reference images from this space",
|
||||
"space_hint_suffix": ", not from Personal.",
|
||||
"family_hint": "Kids' outfits are still rendered onto your face.",
|
||||
"empty_garments": "Add at least one {category} to enable Try-On."
|
||||
},
|
||||
"model_picker": {
|
||||
"legend": "Model",
|
||||
"option_openai_label": "OpenAI",
|
||||
"option_openai_hint": "GPT-image · Standard",
|
||||
"option_pro_label": "Nano Banana Pro",
|
||||
"option_pro_hint": "Google · high consistency",
|
||||
"option_flash_label": "Nano Banana 2",
|
||||
"option_flash_hint": "Google · newest · cheap"
|
||||
},
|
||||
"garment_card": {
|
||||
"wear_count_title": "Worn {count}×"
|
||||
},
|
||||
"outfit_card": {
|
||||
"try_on_badge": "Try-On",
|
||||
"try_on_preview_title": "Try-On preview",
|
||||
"empty": "Empty",
|
||||
"favorite": "Favorite"
|
||||
},
|
||||
"compose": {
|
||||
"title_edit": "Edit outfit",
|
||||
"title_new": "New outfit",
|
||||
"back": "Back"
|
||||
}
|
||||
}
|
||||
257
apps/mana/apps/web/src/lib/i18n/locales/wardrobe/es.json
Normal file
257
apps/mana/apps/web/src/lib/i18n/locales/wardrobe/es.json
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
{
|
||||
"categories": {
|
||||
"all": "Todos",
|
||||
"top": "Tops",
|
||||
"bottom": "Pantalones",
|
||||
"dress": "Vestidos",
|
||||
"outerwear": "Chaquetas",
|
||||
"shoes": "Zapatos",
|
||||
"bag": "Bolsos",
|
||||
"accessory": "Accesorios",
|
||||
"glasses": "Gafas",
|
||||
"jewelry": "Joyería",
|
||||
"hat": "Sombreros",
|
||||
"other": "Otros"
|
||||
},
|
||||
"categories_singular": {
|
||||
"top": "Top",
|
||||
"bottom": "Pantalón",
|
||||
"dress": "Vestido",
|
||||
"outerwear": "Chaqueta",
|
||||
"shoes": "Zapato",
|
||||
"bag": "Bolso",
|
||||
"accessory": "Accesorio",
|
||||
"glasses": "Gafas",
|
||||
"jewelry": "Joya",
|
||||
"hat": "Sombrero",
|
||||
"other": "Item"
|
||||
},
|
||||
"occasions": {
|
||||
"casual": "Casual",
|
||||
"work": "Trabajo",
|
||||
"formal": "Formal",
|
||||
"workout": "Deporte",
|
||||
"date": "Cita",
|
||||
"travel": "Viaje",
|
||||
"event": "Evento",
|
||||
"sleep": "Pijama",
|
||||
"other": "Otros"
|
||||
},
|
||||
"seasons": {
|
||||
"spring": "Primavera",
|
||||
"summer": "Verano",
|
||||
"autumn": "Otoño",
|
||||
"winter": "Invierno"
|
||||
},
|
||||
"piece_singular": "pieza",
|
||||
"piece_plural": "piezas",
|
||||
"upload_failed": "Error al subir",
|
||||
"list_view": {
|
||||
"aria_tabs": "Cambiar vista",
|
||||
"tab_garments": "Ropa",
|
||||
"tab_outfits": "Outfits",
|
||||
"face_saved_title": "Foto de cara guardada",
|
||||
"face_saved_hint": "Perfecto — ahora sube tu primera prenda abajo.",
|
||||
"dismiss": "Cerrar",
|
||||
"face_prompt_title": "Sube una foto de tu cara",
|
||||
"face_prompt_desc": "Te necesitamos en una foto para que Try-On pueda visualizar la ropa en ti. La imagen se queda local y solo se usa para tus propias generaciones.",
|
||||
"face_uploading_label": "Subiendo…",
|
||||
"face_upload_label": "Subir foto de cara",
|
||||
"face_upload_hint": "Cabeza + hombros, iluminación neutra a poder ser",
|
||||
"face_uploading_chip": "Cargando…"
|
||||
},
|
||||
"grid_view": {
|
||||
"upload_label_all": "Subir prenda",
|
||||
"upload_label_for_category": "Subir {category}",
|
||||
"upload_hint": "Foto frontal, fondo claro — mejores resultados de Try-On",
|
||||
"empty_title": "Aún no hay nada en el armario.",
|
||||
"empty_hint": "Arrastra una foto a la zona de arriba — o haz clic para elegir una.",
|
||||
"no_entries_under": "No hay entradas en {category}.",
|
||||
"space_footer": "Este armario pertenece a {name} — los archivos subidos solo aterrizan aquí, no en tu armario personal."
|
||||
},
|
||||
"outfits_view": {
|
||||
"title": "Outfits",
|
||||
"count_singular": "combinación",
|
||||
"count_plural": "combinaciones",
|
||||
"action_new": "Nuevo outfit",
|
||||
"empty_title": "Aún no hay outfits.",
|
||||
"empty_no_garments": "Primero añade unas prendas en la pestaña \"Ropa\" — luego podrás combinarlas en outfits aquí.",
|
||||
"empty_with_garments": "Combina tus prendas en looks que luego puedes probarte con IA.",
|
||||
"action_compose_first": "Crear primer outfit"
|
||||
},
|
||||
"detail_garment": {
|
||||
"loading": "Cargando…",
|
||||
"not_found_title": "No encontrado.",
|
||||
"not_found_desc": "La prenda fue eliminada o pertenece a otro espacio.",
|
||||
"action_enlarge": "Ampliar foto",
|
||||
"action_edit": "Editar",
|
||||
"label_brand": "Marca",
|
||||
"label_color": "Color",
|
||||
"label_size": "Talla",
|
||||
"label_material": "Material",
|
||||
"label_price": "Precio",
|
||||
"label_wear_count": "Usado",
|
||||
"wear_count_value": "{count}×",
|
||||
"last_worn_suffix": " · última vez {date}",
|
||||
"action_comic": "Como personaje cómic",
|
||||
"action_comic_title": "Generar un personaje cómic a partir de esta prenda",
|
||||
"action_marking": "Guardando…",
|
||||
"action_mark_worn": "Usado hoy",
|
||||
"action_unarchive": "Reactivar",
|
||||
"action_archive": "Archivar",
|
||||
"action_delete": "Eliminar",
|
||||
"section_try_ons": "Pruebas · {count}",
|
||||
"section_outfits": "En outfits · {count}",
|
||||
"no_try_on_yet": "Aún sin prueba",
|
||||
"action_open_picture": "Abrir en Picture",
|
||||
"confirm_delete": "¿Eliminar realmente \"{name}\"?"
|
||||
},
|
||||
"detail_outfit": {
|
||||
"back": "Volver al armario",
|
||||
"breadcrumb": "Armario · Outfits",
|
||||
"loading": "Cargando…",
|
||||
"not_found_title": "Outfit no encontrado.",
|
||||
"not_found_desc": "Eliminado o en otro espacio.",
|
||||
"try_on_preview_alt": "Vista previa Try-On",
|
||||
"no_garments": "Sin prendas",
|
||||
"action_comic": "Como personaje cómic",
|
||||
"action_comic_title": "Generar un personaje cómic a partir de este outfit",
|
||||
"try_on_history": "Historial Try-On",
|
||||
"action_unfavorite": "Quitar favorito",
|
||||
"action_favorite": "Marcar como favorito",
|
||||
"action_edit": "Editar",
|
||||
"label_visibility": "Visibilidad",
|
||||
"section_composition": "Composición",
|
||||
"composition_missing": "Las prendas referenciadas fueron eliminadas o pertenecen a otro espacio.",
|
||||
"action_unarchive": "Reactivar",
|
||||
"action_archive": "Archivar",
|
||||
"action_delete": "Eliminar",
|
||||
"confirm_delete": "¿Eliminar realmente el outfit \"{name}\"?"
|
||||
},
|
||||
"garment_form": {
|
||||
"err_name_required": "El nombre no puede estar vacío",
|
||||
"err_save_failed": "Error al guardar",
|
||||
"label_name": "Nombre",
|
||||
"placeholder_name": "p. ej. Camisa rayas azul y blanco",
|
||||
"label_category": "Categoría",
|
||||
"label_brand": "Marca",
|
||||
"placeholder_brand": "p. ej. Uniqlo",
|
||||
"label_color": "Color",
|
||||
"placeholder_color": "p. ej. azul marino",
|
||||
"label_size": "Talla",
|
||||
"placeholder_size": "p. ej. M o 42",
|
||||
"label_material": "Material",
|
||||
"placeholder_material": "p. ej. algodón",
|
||||
"label_tags": "Etiquetas",
|
||||
"tags_hint": "(separadas por comas)",
|
||||
"placeholder_tags": "formal, verano, favorita",
|
||||
"label_price": "Precio",
|
||||
"aria_currency": "Moneda",
|
||||
"label_notes": "Notas",
|
||||
"placeholder_notes": "Ocasión, instrucciones de cuidado, …",
|
||||
"action_saving": "Guardando…",
|
||||
"action_save": "Guardar",
|
||||
"action_cancel": "Cancelar"
|
||||
},
|
||||
"composer": {
|
||||
"err_name_required": "Dale un nombre al outfit.",
|
||||
"err_no_garments": "Elige al menos una prenda.",
|
||||
"err_save_failed": "Error al guardar",
|
||||
"section_library": "Armario",
|
||||
"available_singular": "{count} pieza disponible",
|
||||
"available_plural": "{count} piezas disponibles",
|
||||
"empty_title": "Nada que combinar.",
|
||||
"empty_hint_prefix": "Primero sube unas prendas en la pestaña",
|
||||
"tab_garments_link": "Ropa",
|
||||
"empty_hint_suffix": ".",
|
||||
"label_name": "Nombre",
|
||||
"placeholder_name": "p. ej. Outfit oficina junio",
|
||||
"label_description": "Descripción",
|
||||
"placeholder_description": "¿Para qué ocasión? ¿Algo especial?",
|
||||
"label_occasion": "Ocasión",
|
||||
"no_occasion": "— sin ocasión —",
|
||||
"label_seasons": "Temporada",
|
||||
"label_tags": "Etiquetas",
|
||||
"tags_hint": "(separadas por comas)",
|
||||
"placeholder_tags": "minimal, capas, reunión",
|
||||
"section_composition": "Composición",
|
||||
"composition_count_singular": "· {count} pieza",
|
||||
"composition_count_plural": "· {count} piezas",
|
||||
"composition_empty": "Haz clic en las prendas a la izquierda para añadirlas al outfit.",
|
||||
"action_remove": "Quitar del outfit",
|
||||
"action_saving": "Guardando…",
|
||||
"action_save_edit": "Guardar cambios",
|
||||
"action_save_new": "Crear outfit",
|
||||
"action_cancel": "Cancelar"
|
||||
},
|
||||
"try_on_garment": {
|
||||
"err_failed": "Error en Try-On",
|
||||
"err_upload": "Error al subir",
|
||||
"no_photo": "Sube primero una foto para visualizar esta prenda en ti.",
|
||||
"refs_title": "Para Try-On en solitario te necesitamos en una foto.",
|
||||
"refs_accessory": "Una foto de cara basta — la prenda se monta sobre ella.",
|
||||
"refs_full": "Una foto de cara y otra de cuerpo entero. Ambas solo se usan para tus propias generaciones.",
|
||||
"upload_face": "Subir foto de cara",
|
||||
"upload_body": "Subir foto de cuerpo entero",
|
||||
"face_hint": "Cabeza + hombros, iluminación neutra a poder ser",
|
||||
"body_hint": "De pie, fondo libre, postura claramente visible",
|
||||
"refs_more_prefix": "Más referencias u opt-ins de IA por imagen:",
|
||||
"refs_link": "Mis imágenes",
|
||||
"rendering": "Renderizando…",
|
||||
"cta": "Probarme",
|
||||
"credits": "{count} créditos",
|
||||
"accessory_hint": "Modo accesorio — solo se renderiza la cara (ahorra créditos).",
|
||||
"space_hint_prefix": "Try-On usa tus imágenes de referencia de este espacio",
|
||||
"space_hint_suffix": ", no del Personal.",
|
||||
"result_label": "Resultado",
|
||||
"try_on_alt": "Try-On",
|
||||
"result_hint_prefix": "Encontrado en la",
|
||||
"picture_gallery_link": "Galería Picture",
|
||||
"result_hint_suffix": "como una generación normal."
|
||||
},
|
||||
"try_on_outfit": {
|
||||
"err_failed": "Error en Try-On",
|
||||
"err_upload": "Error al subir",
|
||||
"refs_title": "Para Try-On te necesitamos en una foto.",
|
||||
"refs_accessory": "Una foto de cara basta — el resto se queda como en tu foto.",
|
||||
"refs_full": "Una foto de cara y otra de cuerpo entero. Ambas solo se usan para tus propias generaciones.",
|
||||
"upload_face": "Subir foto de cara",
|
||||
"upload_body": "Subir foto de cuerpo entero",
|
||||
"face_hint": "Cabeza + hombros, iluminación neutra a poder ser",
|
||||
"body_hint": "De pie, fondo libre, postura claramente visible",
|
||||
"refs_more_prefix": "Más referencias u opt-ins de IA por imagen:",
|
||||
"refs_link": "Mis imágenes",
|
||||
"rendering": "Renderizando…",
|
||||
"cta": "Probar",
|
||||
"credits": "{count} créditos",
|
||||
"accessory_hint": "Modo accesorio — solo se renderiza la cara (ahorra créditos).",
|
||||
"many_garments_hint": "Con {count} prendas el slot de referencias está justo — los items más antiguos pueden no entrar.",
|
||||
"space_hint_prefix": "Try-On usa tus imágenes de referencia de este espacio",
|
||||
"space_hint_suffix": ", no del Personal.",
|
||||
"family_hint": "Los outfits de niños se renderizan igualmente en tu cara.",
|
||||
"empty_garments": "Añade al menos un {category} para activar Try-On."
|
||||
},
|
||||
"model_picker": {
|
||||
"legend": "Modelo",
|
||||
"option_openai_label": "OpenAI",
|
||||
"option_openai_hint": "GPT-image · Estándar",
|
||||
"option_pro_label": "Nano Banana Pro",
|
||||
"option_pro_hint": "Google · alta consistencia",
|
||||
"option_flash_label": "Nano Banana 2",
|
||||
"option_flash_hint": "Google · más nuevo · barato"
|
||||
},
|
||||
"garment_card": {
|
||||
"wear_count_title": "Usado {count}×"
|
||||
},
|
||||
"outfit_card": {
|
||||
"try_on_badge": "Try-On",
|
||||
"try_on_preview_title": "Vista previa Try-On",
|
||||
"empty": "Vacío",
|
||||
"favorite": "Favorito"
|
||||
},
|
||||
"compose": {
|
||||
"title_edit": "Editar outfit",
|
||||
"title_new": "Nuevo outfit",
|
||||
"back": "Volver"
|
||||
}
|
||||
}
|
||||
257
apps/mana/apps/web/src/lib/i18n/locales/wardrobe/fr.json
Normal file
257
apps/mana/apps/web/src/lib/i18n/locales/wardrobe/fr.json
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
{
|
||||
"categories": {
|
||||
"all": "Tous",
|
||||
"top": "Hauts",
|
||||
"bottom": "Pantalons",
|
||||
"dress": "Robes",
|
||||
"outerwear": "Vestes",
|
||||
"shoes": "Chaussures",
|
||||
"bag": "Sacs",
|
||||
"accessory": "Accessoires",
|
||||
"glasses": "Lunettes",
|
||||
"jewelry": "Bijoux",
|
||||
"hat": "Chapeaux",
|
||||
"other": "Autres"
|
||||
},
|
||||
"categories_singular": {
|
||||
"top": "Haut",
|
||||
"bottom": "Pantalon",
|
||||
"dress": "Robe",
|
||||
"outerwear": "Veste",
|
||||
"shoes": "Chaussure",
|
||||
"bag": "Sac",
|
||||
"accessory": "Accessoire",
|
||||
"glasses": "Lunettes",
|
||||
"jewelry": "Bijou",
|
||||
"hat": "Chapeau",
|
||||
"other": "Article"
|
||||
},
|
||||
"occasions": {
|
||||
"casual": "Décontracté",
|
||||
"work": "Travail",
|
||||
"formal": "Habillé",
|
||||
"workout": "Sport",
|
||||
"date": "Rendez-vous",
|
||||
"travel": "Voyage",
|
||||
"event": "Événement",
|
||||
"sleep": "Pyjama",
|
||||
"other": "Autre"
|
||||
},
|
||||
"seasons": {
|
||||
"spring": "Printemps",
|
||||
"summer": "Été",
|
||||
"autumn": "Automne",
|
||||
"winter": "Hiver"
|
||||
},
|
||||
"piece_singular": "pièce",
|
||||
"piece_plural": "pièces",
|
||||
"upload_failed": "Échec de l'envoi",
|
||||
"list_view": {
|
||||
"aria_tabs": "Changer de vue",
|
||||
"tab_garments": "Vêtements",
|
||||
"tab_outfits": "Tenues",
|
||||
"face_saved_title": "Photo de visage enregistrée",
|
||||
"face_saved_hint": "Parfait — ensuite, charge ton premier vêtement ci-dessous.",
|
||||
"dismiss": "Fermer",
|
||||
"face_prompt_title": "Charge une photo de ton visage",
|
||||
"face_prompt_desc": "Nous avons besoin de toi en photo pour que Try-On puisse visualiser les vêtements sur toi. L'image reste locale et n'est utilisée que pour tes propres générations.",
|
||||
"face_uploading_label": "Envoi…",
|
||||
"face_upload_label": "Charger photo de visage",
|
||||
"face_upload_hint": "Tête + épaules, éclairage neutre si possible",
|
||||
"face_uploading_chip": "Chargement…"
|
||||
},
|
||||
"grid_view": {
|
||||
"upload_label_all": "Charger un vêtement",
|
||||
"upload_label_for_category": "Charger {category}",
|
||||
"upload_hint": "Photo de face, fond clair — meilleurs résultats Try-On",
|
||||
"empty_title": "Encore rien dans la garde-robe.",
|
||||
"empty_hint": "Glisse une photo dans la zone ci-dessus — ou clique dessus pour en choisir une.",
|
||||
"no_entries_under": "Aucune entrée sous {category}.",
|
||||
"space_footer": "Cette garde-robe appartient à {name} — les fichiers chargés n'atterrissent qu'ici, pas dans ta garde-robe personnelle."
|
||||
},
|
||||
"outfits_view": {
|
||||
"title": "Tenues",
|
||||
"count_singular": "composition",
|
||||
"count_plural": "compositions",
|
||||
"action_new": "Nouvelle tenue",
|
||||
"empty_title": "Encore aucune tenue.",
|
||||
"empty_no_garments": "Ajoute d'abord quelques vêtements dans l'onglet \"Vêtements\" — ensuite tu pourras les combiner en tenues ici.",
|
||||
"empty_with_garments": "Combine tes vêtements en looks que tu pourras ensuite essayer sur toi avec l'IA.",
|
||||
"action_compose_first": "Composer la première tenue"
|
||||
},
|
||||
"detail_garment": {
|
||||
"loading": "Chargement…",
|
||||
"not_found_title": "Introuvable.",
|
||||
"not_found_desc": "Le vêtement a été supprimé ou appartient à un autre espace.",
|
||||
"action_enlarge": "Agrandir la photo",
|
||||
"action_edit": "Modifier",
|
||||
"label_brand": "Marque",
|
||||
"label_color": "Couleur",
|
||||
"label_size": "Taille",
|
||||
"label_material": "Matière",
|
||||
"label_price": "Prix",
|
||||
"label_wear_count": "Porté",
|
||||
"wear_count_value": "{count}×",
|
||||
"last_worn_suffix": " · dernière fois {date}",
|
||||
"action_comic": "Comme personnage BD",
|
||||
"action_comic_title": "Générer un personnage BD à partir de ce vêtement",
|
||||
"action_marking": "Enregistrement…",
|
||||
"action_mark_worn": "Porté aujourd'hui",
|
||||
"action_unarchive": "Réactiver",
|
||||
"action_archive": "Archiver",
|
||||
"action_delete": "Supprimer",
|
||||
"section_try_ons": "Essayages · {count}",
|
||||
"section_outfits": "Dans des tenues · {count}",
|
||||
"no_try_on_yet": "Pas encore d'essayage",
|
||||
"action_open_picture": "Ouvrir dans Picture",
|
||||
"confirm_delete": "Vraiment supprimer \"{name}\" ?"
|
||||
},
|
||||
"detail_outfit": {
|
||||
"back": "Retour à la garde-robe",
|
||||
"breadcrumb": "Garde-robe · Tenues",
|
||||
"loading": "Chargement…",
|
||||
"not_found_title": "Tenue introuvable.",
|
||||
"not_found_desc": "Supprimée ou dans un autre espace.",
|
||||
"try_on_preview_alt": "Aperçu Try-On",
|
||||
"no_garments": "Aucun vêtement",
|
||||
"action_comic": "Comme personnage BD",
|
||||
"action_comic_title": "Générer un personnage BD à partir de cette tenue",
|
||||
"try_on_history": "Historique Try-On",
|
||||
"action_unfavorite": "Retirer des favoris",
|
||||
"action_favorite": "Marquer comme favori",
|
||||
"action_edit": "Modifier",
|
||||
"label_visibility": "Visibilité",
|
||||
"section_composition": "Composition",
|
||||
"composition_missing": "Les vêtements référencés ont été supprimés ou appartiennent à un autre espace.",
|
||||
"action_unarchive": "Réactiver",
|
||||
"action_archive": "Archiver",
|
||||
"action_delete": "Supprimer",
|
||||
"confirm_delete": "Vraiment supprimer la tenue \"{name}\" ?"
|
||||
},
|
||||
"garment_form": {
|
||||
"err_name_required": "Le nom ne peut pas être vide",
|
||||
"err_save_failed": "Échec de l'enregistrement",
|
||||
"label_name": "Nom",
|
||||
"placeholder_name": "p. ex. Chemise rayée bleu et blanc",
|
||||
"label_category": "Catégorie",
|
||||
"label_brand": "Marque",
|
||||
"placeholder_brand": "p. ex. Uniqlo",
|
||||
"label_color": "Couleur",
|
||||
"placeholder_color": "p. ex. marine",
|
||||
"label_size": "Taille",
|
||||
"placeholder_size": "p. ex. M ou 42",
|
||||
"label_material": "Matière",
|
||||
"placeholder_material": "p. ex. coton",
|
||||
"label_tags": "Étiquettes",
|
||||
"tags_hint": "(séparées par virgules)",
|
||||
"placeholder_tags": "habillé, été, préférée",
|
||||
"label_price": "Prix",
|
||||
"aria_currency": "Devise",
|
||||
"label_notes": "Notes",
|
||||
"placeholder_notes": "Occasion, instructions d'entretien, …",
|
||||
"action_saving": "Enregistrement…",
|
||||
"action_save": "Enregistrer",
|
||||
"action_cancel": "Annuler"
|
||||
},
|
||||
"composer": {
|
||||
"err_name_required": "Donne un nom à la tenue.",
|
||||
"err_no_garments": "Choisis au moins un vêtement.",
|
||||
"err_save_failed": "Échec de l'enregistrement",
|
||||
"section_library": "Garde-robe",
|
||||
"available_singular": "{count} pièce disponible",
|
||||
"available_plural": "{count} pièces disponibles",
|
||||
"empty_title": "Rien à combiner.",
|
||||
"empty_hint_prefix": "Charge d'abord quelques vêtements dans l'onglet",
|
||||
"tab_garments_link": "Vêtements",
|
||||
"empty_hint_suffix": ".",
|
||||
"label_name": "Nom",
|
||||
"placeholder_name": "p. ex. Tenue bureau juin",
|
||||
"label_description": "Description",
|
||||
"placeholder_description": "Pour quelle occasion ? Particularités ?",
|
||||
"label_occasion": "Occasion",
|
||||
"no_occasion": "— sans occasion —",
|
||||
"label_seasons": "Saison",
|
||||
"label_tags": "Étiquettes",
|
||||
"tags_hint": "(séparées par virgules)",
|
||||
"placeholder_tags": "minimal, layering, réunion",
|
||||
"section_composition": "Composition",
|
||||
"composition_count_singular": "· {count} pièce",
|
||||
"composition_count_plural": "· {count} pièces",
|
||||
"composition_empty": "Clique sur les vêtements à gauche pour les ajouter à la tenue.",
|
||||
"action_remove": "Retirer de la tenue",
|
||||
"action_saving": "Enregistrement…",
|
||||
"action_save_edit": "Enregistrer les modifications",
|
||||
"action_save_new": "Créer la tenue",
|
||||
"action_cancel": "Annuler"
|
||||
},
|
||||
"try_on_garment": {
|
||||
"err_failed": "Échec Try-On",
|
||||
"err_upload": "Échec de l'envoi",
|
||||
"no_photo": "Charge d'abord une photo pour visualiser cette pièce sur toi.",
|
||||
"refs_title": "Pour Try-On solo, nous avons besoin de toi en photo.",
|
||||
"refs_accessory": "Une photo de visage suffit — la pièce y est montée.",
|
||||
"refs_full": "Une photo de visage et une photo en pied. Les deux ne sont utilisées que pour tes propres générations.",
|
||||
"upload_face": "Charger photo de visage",
|
||||
"upload_body": "Charger photo en pied",
|
||||
"face_hint": "Tête + épaules, éclairage neutre si possible",
|
||||
"body_hint": "Debout, fond libre, posture clairement visible",
|
||||
"refs_more_prefix": "Plus de références ou opt-ins IA par image :",
|
||||
"refs_link": "Mes images",
|
||||
"rendering": "Rendu…",
|
||||
"cta": "Essayer sur moi",
|
||||
"credits": "{count} crédits",
|
||||
"accessory_hint": "Mode accessoire — seul le visage est rendu (économise des crédits).",
|
||||
"space_hint_prefix": "Try-On utilise tes images de référence de cet espace",
|
||||
"space_hint_suffix": ", pas de Personnel.",
|
||||
"result_label": "Résultat",
|
||||
"try_on_alt": "Try-On",
|
||||
"result_hint_prefix": "Trouvé dans la",
|
||||
"picture_gallery_link": "galerie Picture",
|
||||
"result_hint_suffix": "comme une génération normale."
|
||||
},
|
||||
"try_on_outfit": {
|
||||
"err_failed": "Échec Try-On",
|
||||
"err_upload": "Échec de l'envoi",
|
||||
"refs_title": "Pour Try-On, nous avons besoin de toi en photo.",
|
||||
"refs_accessory": "Une photo de visage suffit — le reste reste comme sur ta photo.",
|
||||
"refs_full": "Une photo de visage et une photo en pied. Les deux ne sont utilisées que pour tes propres générations.",
|
||||
"upload_face": "Charger photo de visage",
|
||||
"upload_body": "Charger photo en pied",
|
||||
"face_hint": "Tête + épaules, éclairage neutre si possible",
|
||||
"body_hint": "Debout, fond libre, posture clairement visible",
|
||||
"refs_more_prefix": "Plus de références ou opt-ins IA par image :",
|
||||
"refs_link": "Mes images",
|
||||
"rendering": "Rendu…",
|
||||
"cta": "Essayer",
|
||||
"credits": "{count} crédits",
|
||||
"accessory_hint": "Mode accessoire — seul le visage est rendu (économise des crédits).",
|
||||
"many_garments_hint": "Avec {count} vêtements, le slot de référence est juste — les items plus anciens peuvent ne pas être inclus.",
|
||||
"space_hint_prefix": "Try-On utilise tes images de référence de cet espace",
|
||||
"space_hint_suffix": ", pas de Personnel.",
|
||||
"family_hint": "Les tenues d'enfants sont quand même rendues sur ton visage.",
|
||||
"empty_garments": "Ajoute au moins un {category} pour activer Try-On."
|
||||
},
|
||||
"model_picker": {
|
||||
"legend": "Modèle",
|
||||
"option_openai_label": "OpenAI",
|
||||
"option_openai_hint": "GPT-image · Standard",
|
||||
"option_pro_label": "Nano Banana Pro",
|
||||
"option_pro_hint": "Google · forte cohérence",
|
||||
"option_flash_label": "Nano Banana 2",
|
||||
"option_flash_hint": "Google · le plus récent · pas cher"
|
||||
},
|
||||
"garment_card": {
|
||||
"wear_count_title": "Porté {count}×"
|
||||
},
|
||||
"outfit_card": {
|
||||
"try_on_badge": "Try-On",
|
||||
"try_on_preview_title": "Aperçu Try-On",
|
||||
"empty": "Vide",
|
||||
"favorite": "Favori"
|
||||
},
|
||||
"compose": {
|
||||
"title_edit": "Modifier la tenue",
|
||||
"title_new": "Nouvelle tenue",
|
||||
"back": "Retour"
|
||||
}
|
||||
}
|
||||
257
apps/mana/apps/web/src/lib/i18n/locales/wardrobe/it.json
Normal file
257
apps/mana/apps/web/src/lib/i18n/locales/wardrobe/it.json
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
{
|
||||
"categories": {
|
||||
"all": "Tutti",
|
||||
"top": "Top",
|
||||
"bottom": "Pantaloni",
|
||||
"dress": "Vestiti",
|
||||
"outerwear": "Giacche",
|
||||
"shoes": "Scarpe",
|
||||
"bag": "Borse",
|
||||
"accessory": "Accessori",
|
||||
"glasses": "Occhiali",
|
||||
"jewelry": "Gioielli",
|
||||
"hat": "Cappelli",
|
||||
"other": "Altro"
|
||||
},
|
||||
"categories_singular": {
|
||||
"top": "Top",
|
||||
"bottom": "Pantaloni",
|
||||
"dress": "Vestito",
|
||||
"outerwear": "Giacca",
|
||||
"shoes": "Scarpa",
|
||||
"bag": "Borsa",
|
||||
"accessory": "Accessorio",
|
||||
"glasses": "Occhiali",
|
||||
"jewelry": "Gioiello",
|
||||
"hat": "Cappello",
|
||||
"other": "Articolo"
|
||||
},
|
||||
"occasions": {
|
||||
"casual": "Casual",
|
||||
"work": "Lavoro",
|
||||
"formal": "Elegante",
|
||||
"workout": "Sport",
|
||||
"date": "Appuntamento",
|
||||
"travel": "Viaggio",
|
||||
"event": "Evento",
|
||||
"sleep": "Pigiama",
|
||||
"other": "Altro"
|
||||
},
|
||||
"seasons": {
|
||||
"spring": "Primavera",
|
||||
"summer": "Estate",
|
||||
"autumn": "Autunno",
|
||||
"winter": "Inverno"
|
||||
},
|
||||
"piece_singular": "pezzo",
|
||||
"piece_plural": "pezzi",
|
||||
"upload_failed": "Caricamento fallito",
|
||||
"list_view": {
|
||||
"aria_tabs": "Cambia vista",
|
||||
"tab_garments": "Abbigliamento",
|
||||
"tab_outfits": "Outfit",
|
||||
"face_saved_title": "Foto del viso salvata",
|
||||
"face_saved_hint": "Perfetto — ora carica il tuo primo capo qui sotto.",
|
||||
"dismiss": "Chiudi",
|
||||
"face_prompt_title": "Carica una foto del viso",
|
||||
"face_prompt_desc": "Abbiamo bisogno di te in una foto perché Try-On possa visualizzare gli abiti su di te. L'immagine resta locale e viene usata solo per le tue generazioni.",
|
||||
"face_uploading_label": "Caricamento…",
|
||||
"face_upload_label": "Carica foto del viso",
|
||||
"face_upload_hint": "Testa + spalle, illuminazione neutra se possibile",
|
||||
"face_uploading_chip": "Carico…"
|
||||
},
|
||||
"grid_view": {
|
||||
"upload_label_all": "Carica capo",
|
||||
"upload_label_for_category": "Carica {category}",
|
||||
"upload_hint": "Foto frontale, sfondo chiaro — risultati Try-On migliori",
|
||||
"empty_title": "Ancora nulla nell'armadio.",
|
||||
"empty_hint": "Trascina una foto nella zona sopra — o cliccala per sceglierne una.",
|
||||
"no_entries_under": "Nessuna voce sotto {category}.",
|
||||
"space_footer": "Questo armadio appartiene a {name} — i caricamenti finiscono solo qui, non nel tuo armadio personale."
|
||||
},
|
||||
"outfits_view": {
|
||||
"title": "Outfit",
|
||||
"count_singular": "composizione",
|
||||
"count_plural": "composizioni",
|
||||
"action_new": "Nuovo outfit",
|
||||
"empty_title": "Ancora nessun outfit.",
|
||||
"empty_no_garments": "Prima aggiungi qualche capo nella scheda \"Abbigliamento\" — poi potrai combinarli in outfit qui.",
|
||||
"empty_with_garments": "Combina i tuoi capi in look che potrai poi provare su di te con l'IA.",
|
||||
"action_compose_first": "Comporre il primo outfit"
|
||||
},
|
||||
"detail_garment": {
|
||||
"loading": "Caricamento…",
|
||||
"not_found_title": "Non trovato.",
|
||||
"not_found_desc": "Il capo è stato eliminato o appartiene a un altro spazio.",
|
||||
"action_enlarge": "Ingrandisci foto",
|
||||
"action_edit": "Modifica",
|
||||
"label_brand": "Marca",
|
||||
"label_color": "Colore",
|
||||
"label_size": "Taglia",
|
||||
"label_material": "Materiale",
|
||||
"label_price": "Prezzo",
|
||||
"label_wear_count": "Indossato",
|
||||
"wear_count_value": "{count}×",
|
||||
"last_worn_suffix": " · l'ultima volta {date}",
|
||||
"action_comic": "Come personaggio comic",
|
||||
"action_comic_title": "Genera un personaggio comic da questo capo",
|
||||
"action_marking": "Salvataggio…",
|
||||
"action_mark_worn": "Indossato oggi",
|
||||
"action_unarchive": "Riattiva",
|
||||
"action_archive": "Archivia",
|
||||
"action_delete": "Elimina",
|
||||
"section_try_ons": "Prove · {count}",
|
||||
"section_outfits": "In outfit · {count}",
|
||||
"no_try_on_yet": "Ancora nessuna prova",
|
||||
"action_open_picture": "Apri in Picture",
|
||||
"confirm_delete": "Eliminare davvero \"{name}\"?"
|
||||
},
|
||||
"detail_outfit": {
|
||||
"back": "Torna all'armadio",
|
||||
"breadcrumb": "Armadio · Outfit",
|
||||
"loading": "Caricamento…",
|
||||
"not_found_title": "Outfit non trovato.",
|
||||
"not_found_desc": "Eliminato o in un altro spazio.",
|
||||
"try_on_preview_alt": "Anteprima Try-On",
|
||||
"no_garments": "Nessun capo",
|
||||
"action_comic": "Come personaggio comic",
|
||||
"action_comic_title": "Genera un personaggio comic da questo outfit",
|
||||
"try_on_history": "Cronologia Try-On",
|
||||
"action_unfavorite": "Rimuovi preferito",
|
||||
"action_favorite": "Segna come preferito",
|
||||
"action_edit": "Modifica",
|
||||
"label_visibility": "Visibilità",
|
||||
"section_composition": "Composizione",
|
||||
"composition_missing": "I capi referenziati sono stati rimossi o appartengono a un altro spazio.",
|
||||
"action_unarchive": "Riattiva",
|
||||
"action_archive": "Archivia",
|
||||
"action_delete": "Elimina",
|
||||
"confirm_delete": "Eliminare davvero l'outfit \"{name}\"?"
|
||||
},
|
||||
"garment_form": {
|
||||
"err_name_required": "Il nome non può essere vuoto",
|
||||
"err_save_failed": "Salvataggio fallito",
|
||||
"label_name": "Nome",
|
||||
"placeholder_name": "es. Camicia a righe blu e bianca",
|
||||
"label_category": "Categoria",
|
||||
"label_brand": "Marca",
|
||||
"placeholder_brand": "es. Uniqlo",
|
||||
"label_color": "Colore",
|
||||
"placeholder_color": "es. blu navy",
|
||||
"label_size": "Taglia",
|
||||
"placeholder_size": "es. M o 42",
|
||||
"label_material": "Materiale",
|
||||
"placeholder_material": "es. cotone",
|
||||
"label_tags": "Tag",
|
||||
"tags_hint": "(separati da virgola)",
|
||||
"placeholder_tags": "elegante, estate, preferito",
|
||||
"label_price": "Prezzo",
|
||||
"aria_currency": "Valuta",
|
||||
"label_notes": "Note",
|
||||
"placeholder_notes": "Occasione, istruzioni di cura, …",
|
||||
"action_saving": "Salvataggio…",
|
||||
"action_save": "Salva",
|
||||
"action_cancel": "Annulla"
|
||||
},
|
||||
"composer": {
|
||||
"err_name_required": "Dai un nome all'outfit.",
|
||||
"err_no_garments": "Scegli almeno un capo.",
|
||||
"err_save_failed": "Salvataggio fallito",
|
||||
"section_library": "Armadio",
|
||||
"available_singular": "{count} pezzo disponibile",
|
||||
"available_plural": "{count} pezzi disponibili",
|
||||
"empty_title": "Niente da combinare.",
|
||||
"empty_hint_prefix": "Prima carica qualche capo nella scheda",
|
||||
"tab_garments_link": "Abbigliamento",
|
||||
"empty_hint_suffix": ".",
|
||||
"label_name": "Nome",
|
||||
"placeholder_name": "es. Outfit ufficio giugno",
|
||||
"label_description": "Descrizione",
|
||||
"placeholder_description": "Per quale occasione? Particolarità?",
|
||||
"label_occasion": "Occasione",
|
||||
"no_occasion": "— nessuna occasione —",
|
||||
"label_seasons": "Stagione",
|
||||
"label_tags": "Tag",
|
||||
"tags_hint": "(separati da virgola)",
|
||||
"placeholder_tags": "minimal, layering, riunione",
|
||||
"section_composition": "Composizione",
|
||||
"composition_count_singular": "· {count} pezzo",
|
||||
"composition_count_plural": "· {count} pezzi",
|
||||
"composition_empty": "Clicca i capi a sinistra per aggiungerli all'outfit.",
|
||||
"action_remove": "Rimuovi dall'outfit",
|
||||
"action_saving": "Salvataggio…",
|
||||
"action_save_edit": "Salva modifiche",
|
||||
"action_save_new": "Crea outfit",
|
||||
"action_cancel": "Annulla"
|
||||
},
|
||||
"try_on_garment": {
|
||||
"err_failed": "Try-On fallito",
|
||||
"err_upload": "Caricamento fallito",
|
||||
"no_photo": "Carica prima una foto per visualizzare questo capo su di te.",
|
||||
"refs_title": "Per il Try-On in solitaria abbiamo bisogno di te in foto.",
|
||||
"refs_accessory": "Una foto del viso basta — il capo viene montato sopra.",
|
||||
"refs_full": "Una foto del viso e una a figura intera. Entrambe sono usate solo per le tue generazioni.",
|
||||
"upload_face": "Carica foto del viso",
|
||||
"upload_body": "Carica foto a figura intera",
|
||||
"face_hint": "Testa + spalle, illuminazione neutra se possibile",
|
||||
"body_hint": "In piedi, sfondo libero, postura ben visibile",
|
||||
"refs_more_prefix": "Altri riferimenti o opt-in IA per immagine:",
|
||||
"refs_link": "Le mie immagini",
|
||||
"rendering": "Rendering…",
|
||||
"cta": "Provami addosso",
|
||||
"credits": "{count} crediti",
|
||||
"accessory_hint": "Modalità accessorio — viene reso solo il viso (risparmia crediti).",
|
||||
"space_hint_prefix": "Try-On usa le tue immagini di riferimento di questo spazio",
|
||||
"space_hint_suffix": ", non da Personale.",
|
||||
"result_label": "Risultato",
|
||||
"try_on_alt": "Try-On",
|
||||
"result_hint_prefix": "Trovato nella",
|
||||
"picture_gallery_link": "galleria Picture",
|
||||
"result_hint_suffix": "come una generazione normale."
|
||||
},
|
||||
"try_on_outfit": {
|
||||
"err_failed": "Try-On fallito",
|
||||
"err_upload": "Caricamento fallito",
|
||||
"refs_title": "Per il Try-On abbiamo bisogno di te in foto.",
|
||||
"refs_accessory": "Una foto del viso basta — il resto resta come nella tua foto.",
|
||||
"refs_full": "Una foto del viso e una a figura intera. Entrambe sono usate solo per le tue generazioni.",
|
||||
"upload_face": "Carica foto del viso",
|
||||
"upload_body": "Carica foto a figura intera",
|
||||
"face_hint": "Testa + spalle, illuminazione neutra se possibile",
|
||||
"body_hint": "In piedi, sfondo libero, postura ben visibile",
|
||||
"refs_more_prefix": "Altri riferimenti o opt-in IA per immagine:",
|
||||
"refs_link": "Le mie immagini",
|
||||
"rendering": "Rendering…",
|
||||
"cta": "Prova",
|
||||
"credits": "{count} crediti",
|
||||
"accessory_hint": "Modalità accessorio — viene reso solo il viso (risparmia crediti).",
|
||||
"many_garments_hint": "Con {count} capi lo slot di riferimento è stretto — gli articoli più vecchi potrebbero non essere inclusi.",
|
||||
"space_hint_prefix": "Try-On usa le tue immagini di riferimento di questo spazio",
|
||||
"space_hint_suffix": ", non da Personale.",
|
||||
"family_hint": "Gli outfit dei bambini vengono comunque resi sul tuo viso.",
|
||||
"empty_garments": "Aggiungi almeno un {category} per attivare Try-On."
|
||||
},
|
||||
"model_picker": {
|
||||
"legend": "Modello",
|
||||
"option_openai_label": "OpenAI",
|
||||
"option_openai_hint": "GPT-image · Standard",
|
||||
"option_pro_label": "Nano Banana Pro",
|
||||
"option_pro_hint": "Google · alta coerenza",
|
||||
"option_flash_label": "Nano Banana 2",
|
||||
"option_flash_hint": "Google · ultimo · economico"
|
||||
},
|
||||
"garment_card": {
|
||||
"wear_count_title": "Indossato {count}×"
|
||||
},
|
||||
"outfit_card": {
|
||||
"try_on_badge": "Try-On",
|
||||
"try_on_preview_title": "Anteprima Try-On",
|
||||
"empty": "Vuoto",
|
||||
"favorite": "Preferito"
|
||||
},
|
||||
"compose": {
|
||||
"title_edit": "Modifica outfit",
|
||||
"title_new": "Nuovo outfit",
|
||||
"back": "Indietro"
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
-->
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { CheckCircle, SpinnerGap, UserCircle } from '@mana/shared-icons';
|
||||
import { useImageByPrimary } from '$lib/modules/profile/queries';
|
||||
import MeImageUploadZone from '$lib/modules/profile/components/MeImageUploadZone.svelte';
|
||||
|
|
@ -24,10 +25,10 @@
|
|||
|
||||
let activeTab = $state<Tab>('garments');
|
||||
|
||||
const TABS: { key: Tab; label: string }[] = [
|
||||
{ key: 'garments', label: 'Kleidung' },
|
||||
{ key: 'outfits', label: 'Outfits' },
|
||||
];
|
||||
const TABS = $derived<{ key: Tab; label: string }[]>([
|
||||
{ key: 'garments', label: $_('wardrobe.list_view.tab_garments') },
|
||||
{ key: 'outfits', label: $_('wardrobe.list_view.tab_outfits') },
|
||||
]);
|
||||
|
||||
// Face-ref banner: the minimum requirement for *any* wardrobe try-on
|
||||
// (outfit or solo-garment, accessory or full). Body-ref is asked for
|
||||
|
|
@ -74,7 +75,7 @@
|
|||
successTimeout = null;
|
||||
}, 2500);
|
||||
} catch (err) {
|
||||
faceUploadError = err instanceof Error ? err.message : 'Upload fehlgeschlagen';
|
||||
faceUploadError = err instanceof Error ? err.message : $_('wardrobe.upload_failed');
|
||||
uploadPhase = 'idle';
|
||||
}
|
||||
}
|
||||
|
|
@ -90,7 +91,7 @@
|
|||
</script>
|
||||
|
||||
<div class="wardrobe-root">
|
||||
<nav class="wardrobe-tabs" aria-label="Ansicht wechseln">
|
||||
<nav class="wardrobe-tabs" aria-label={$_('wardrobe.list_view.aria_tabs')}>
|
||||
{#each TABS as tab (tab.key)}
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -128,10 +129,10 @@
|
|||
<div class="flex-1 space-y-0.5">
|
||||
<p class="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||||
<CheckCircle size={14} weight="fill" class="text-primary" />
|
||||
Gesichtsbild gespeichert
|
||||
{$_('wardrobe.list_view.face_saved_title')}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Perfekt — als nächstes lädst du unten dein erstes Kleidungsstück hoch.
|
||||
{$_('wardrobe.list_view.face_saved_hint')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
|
|
@ -139,25 +140,28 @@
|
|||
onclick={dismissSuccess}
|
||||
class="text-xs font-medium text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Schließen
|
||||
{$_('wardrobe.list_view.dismiss')}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-start gap-3 text-sm">
|
||||
<UserCircle size={18} weight="regular" class="mt-0.5 flex-shrink-0 text-primary" />
|
||||
<div class="space-y-1">
|
||||
<p class="font-medium text-foreground">Lade ein Gesichtsbild hoch</p>
|
||||
<p class="font-medium text-foreground">
|
||||
{$_('wardrobe.list_view.face_prompt_title')}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Wir brauchen dich auf Bild, damit Try-On Kleidung an dir visualisieren kann. Das Bild
|
||||
bleibt lokal und wird nur für deine eigenen Generierungen genutzt.
|
||||
{$_('wardrobe.list_view.face_prompt_desc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<MeImageUploadZone
|
||||
variant="compact"
|
||||
label={uploadPhase === 'uploading' ? 'Wird hochgeladen…' : 'Gesichtsbild hochladen'}
|
||||
hint="Kopf + Schulter, möglichst neutrale Beleuchtung"
|
||||
label={uploadPhase === 'uploading'
|
||||
? $_('wardrobe.list_view.face_uploading_label')
|
||||
: $_('wardrobe.list_view.face_upload_label')}
|
||||
hint={$_('wardrobe.list_view.face_upload_hint')}
|
||||
disabled={uploadPhase === 'uploading'}
|
||||
onFiles={handleFaceUpload}
|
||||
/>
|
||||
|
|
@ -168,7 +172,7 @@
|
|||
aria-live="polite"
|
||||
>
|
||||
<SpinnerGap size={12} class="spinner" weight="bold" />
|
||||
Lade…
|
||||
{$_('wardrobe.list_view.face_uploading_chip')}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
category, so "Oberteile aktiv → Datei droppen → landet als top").
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { CATEGORY_LABELS, CATEGORY_ORDER } from '../constants';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { CATEGORY_ORDER } from '../constants';
|
||||
import type { GarmentCategory } from '../types';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -26,7 +27,7 @@
|
|||
? 'border-primary bg-primary text-primary-foreground'
|
||||
: 'border-border bg-background text-muted-foreground hover:border-primary/50 hover:text-foreground'}"
|
||||
>
|
||||
Alle
|
||||
{$_('wardrobe.categories.all')}
|
||||
{#if counts?.all !== undefined}
|
||||
<span class="ml-1 text-xs opacity-70">{counts.all}</span>
|
||||
{/if}
|
||||
|
|
@ -39,7 +40,7 @@
|
|||
? 'border-primary bg-primary text-primary-foreground'
|
||||
: 'border-border bg-background text-muted-foreground hover:border-primary/50 hover:text-foreground'}"
|
||||
>
|
||||
{CATEGORY_LABELS[category]}
|
||||
{$_('wardrobe.categories.' + category)}
|
||||
{#if counts?.[category] !== undefined && counts[category]! > 0}
|
||||
<span class="ml-1 text-xs opacity-70">{counts[category]}</span>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
component trusts the input.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { CATEGORY_LABELS_SINGULAR } from '../constants';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { garmentPrimaryMediaId } from '../types';
|
||||
import { garmentPhotoUrl } from '../api/media-url';
|
||||
import type { Garment } from '../types';
|
||||
|
|
@ -34,12 +34,14 @@
|
|||
<span
|
||||
class="absolute left-2 top-2 rounded-md bg-background/90 px-2 py-0.5 text-xs font-medium text-foreground shadow-sm backdrop-blur-sm"
|
||||
>
|
||||
{CATEGORY_LABELS_SINGULAR[garment.category] ?? garment.category}
|
||||
{$_('wardrobe.categories_singular.' + garment.category)}
|
||||
</span>
|
||||
{#if garment.wearCount && garment.wearCount > 0}
|
||||
<span
|
||||
class="absolute right-2 top-2 rounded-full bg-primary/90 px-2 py-0.5 text-xs font-medium text-primary-foreground shadow-sm"
|
||||
title="{garment.wearCount}× getragen"
|
||||
title={$_('wardrobe.garment_card.wear_count_title', {
|
||||
values: { count: garment.wearCount },
|
||||
})}
|
||||
>
|
||||
{garment.wearCount}×
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
and the detail view immediately opens in edit mode.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { CATEGORY_LABELS, CATEGORY_ORDER } from '../constants';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { CATEGORY_ORDER } from '../constants';
|
||||
import type { Garment, GarmentCategory } from '../types';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -62,7 +63,7 @@
|
|||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
if (!name.trim()) {
|
||||
error = 'Name darf nicht leer sein';
|
||||
error = $_('wardrobe.garment_form.err_name_required');
|
||||
return;
|
||||
}
|
||||
error = null;
|
||||
|
|
@ -85,7 +86,7 @@
|
|||
currency: currency.trim() || null,
|
||||
});
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Speichern fehlgeschlagen';
|
||||
error = e instanceof Error ? e.message : $_('wardrobe.garment_form.err_save_failed');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -94,7 +95,7 @@
|
|||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div class="sm:col-span-2">
|
||||
<label for="garment-name" class="mb-1.5 block text-sm font-medium text-foreground">
|
||||
Name <span class="text-error">*</span>
|
||||
{$_('wardrobe.garment_form.label_name')} <span class="text-error">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="garment-name"
|
||||
|
|
@ -102,14 +103,14 @@
|
|||
bind:value={name}
|
||||
disabled={saving}
|
||||
required
|
||||
placeholder="z.B. Blau-weiß gestreiftes Hemd"
|
||||
placeholder={$_('wardrobe.garment_form.placeholder_name')}
|
||||
class="w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="garment-category" class="mb-1.5 block text-sm font-medium text-foreground">
|
||||
Kategorie
|
||||
{$_('wardrobe.garment_form.label_category')}
|
||||
</label>
|
||||
<select
|
||||
id="garment-category"
|
||||
|
|
@ -118,84 +119,85 @@
|
|||
class="w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus:border-primary focus:ring-1 focus:ring-primary disabled:opacity-50"
|
||||
>
|
||||
{#each CATEGORY_ORDER as c}
|
||||
<option value={c}>{CATEGORY_LABELS[c]}</option>
|
||||
<option value={c}>{$_('wardrobe.categories.' + c)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="garment-brand" class="mb-1.5 block text-sm font-medium text-foreground">
|
||||
Marke
|
||||
{$_('wardrobe.garment_form.label_brand')}
|
||||
</label>
|
||||
<input
|
||||
id="garment-brand"
|
||||
type="text"
|
||||
bind:value={brand}
|
||||
disabled={saving}
|
||||
placeholder="z.B. Uniqlo"
|
||||
placeholder={$_('wardrobe.garment_form.placeholder_brand')}
|
||||
class="w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="garment-color" class="mb-1.5 block text-sm font-medium text-foreground">
|
||||
Farbe
|
||||
{$_('wardrobe.garment_form.label_color')}
|
||||
</label>
|
||||
<input
|
||||
id="garment-color"
|
||||
type="text"
|
||||
bind:value={color}
|
||||
disabled={saving}
|
||||
placeholder="z.B. navy"
|
||||
placeholder={$_('wardrobe.garment_form.placeholder_color')}
|
||||
class="w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="garment-size" class="mb-1.5 block text-sm font-medium text-foreground">
|
||||
Größe
|
||||
{$_('wardrobe.garment_form.label_size')}
|
||||
</label>
|
||||
<input
|
||||
id="garment-size"
|
||||
type="text"
|
||||
bind:value={size}
|
||||
disabled={saving}
|
||||
placeholder="z.B. M oder 42"
|
||||
placeholder={$_('wardrobe.garment_form.placeholder_size')}
|
||||
class="w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="garment-material" class="mb-1.5 block text-sm font-medium text-foreground">
|
||||
Material
|
||||
{$_('wardrobe.garment_form.label_material')}
|
||||
</label>
|
||||
<input
|
||||
id="garment-material"
|
||||
type="text"
|
||||
bind:value={material}
|
||||
disabled={saving}
|
||||
placeholder="z.B. Baumwolle"
|
||||
placeholder={$_('wardrobe.garment_form.placeholder_material')}
|
||||
class="w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="sm:col-span-2">
|
||||
<label for="garment-tags" class="mb-1.5 block text-sm font-medium text-foreground">
|
||||
Tags <span class="text-muted-foreground">(komma-getrennt)</span>
|
||||
{$_('wardrobe.garment_form.label_tags')}
|
||||
<span class="text-muted-foreground">{$_('wardrobe.garment_form.tags_hint')}</span>
|
||||
</label>
|
||||
<input
|
||||
id="garment-tags"
|
||||
type="text"
|
||||
bind:value={tagsText}
|
||||
disabled={saving}
|
||||
placeholder="formal, sommer, lieblingsstück"
|
||||
placeholder={$_('wardrobe.garment_form.placeholder_tags')}
|
||||
class="w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="garment-price" class="mb-1.5 block text-sm font-medium text-foreground">
|
||||
Preis
|
||||
{$_('wardrobe.garment_form.label_price')}
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
|
|
@ -213,7 +215,7 @@
|
|||
bind:value={currency}
|
||||
disabled={saving}
|
||||
maxlength="3"
|
||||
aria-label="Währung"
|
||||
aria-label={$_('wardrobe.garment_form.aria_currency')}
|
||||
class="w-16 rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -221,14 +223,14 @@
|
|||
|
||||
<div class="sm:col-span-2">
|
||||
<label for="garment-notes" class="mb-1.5 block text-sm font-medium text-foreground">
|
||||
Notizen
|
||||
{$_('wardrobe.garment_form.label_notes')}
|
||||
</label>
|
||||
<textarea
|
||||
id="garment-notes"
|
||||
bind:value={notes}
|
||||
disabled={saving}
|
||||
rows="2"
|
||||
placeholder="Anlass, Tragevorschriften, …"
|
||||
placeholder={$_('wardrobe.garment_form.placeholder_notes')}
|
||||
class="w-full resize-none rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary disabled:opacity-50"
|
||||
></textarea>
|
||||
</div>
|
||||
|
|
@ -249,7 +251,7 @@
|
|||
disabled={saving || !name.trim()}
|
||||
class="flex-1 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{saving ? 'Speichere…' : 'Speichern'}
|
||||
{saving ? $_('wardrobe.garment_form.action_saving') : $_('wardrobe.garment_form.action_save')}
|
||||
</button>
|
||||
{#if onCancel}
|
||||
<button
|
||||
|
|
@ -258,7 +260,7 @@
|
|||
disabled={saving}
|
||||
class="rounded-md border border-border bg-background px-4 py-2 text-sm text-foreground transition-colors hover:bg-muted disabled:opacity-50"
|
||||
>
|
||||
Abbrechen
|
||||
{$_('wardrobe.garment_form.action_cancel')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
outfit's try-on history.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { Sparkle, UserCircle, Info } from '@mana/shared-icons';
|
||||
import { getActiveSpace } from '$lib/data/scope';
|
||||
import { useImageByPrimary } from '$lib/modules/profile/queries';
|
||||
|
|
@ -78,7 +79,7 @@
|
|||
});
|
||||
lastResultUrl = result.imageUrl;
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Try-On fehlgeschlagen';
|
||||
error = err instanceof Error ? err.message : $_('wardrobe.try_on_garment.err_failed');
|
||||
} finally {
|
||||
running = false;
|
||||
}
|
||||
|
|
@ -95,7 +96,8 @@
|
|||
try {
|
||||
await ingestMeImageFile(files[0], { kind, claimSlot: slot });
|
||||
} catch (err) {
|
||||
uploadRefError = err instanceof Error ? err.message : 'Upload fehlgeschlagen';
|
||||
uploadRefError =
|
||||
err instanceof Error ? err.message : $_('wardrobe.try_on_garment.err_upload');
|
||||
} finally {
|
||||
uploadingRef = false;
|
||||
}
|
||||
|
|
@ -104,18 +106,18 @@
|
|||
|
||||
{#if !hasPhoto}
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Lade erst ein Foto hoch, um dieses Stück an dir zu visualisieren.
|
||||
{$_('wardrobe.try_on_garment.no_photo')}
|
||||
</p>
|
||||
{:else if missingFace || missingBody}
|
||||
<div class="space-y-3 rounded-xl border border-dashed border-border bg-background/50 p-4">
|
||||
<div class="flex items-start gap-3 text-sm">
|
||||
<UserCircle size={18} weight="regular" class="mt-0.5 flex-shrink-0 text-primary" />
|
||||
<div class="space-y-1">
|
||||
<p class="font-medium text-foreground">Für Solo-Try-On brauchen wir dich auf Bild.</p>
|
||||
<p class="font-medium text-foreground">{$_('wardrobe.try_on_garment.refs_title')}</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{accessoryOnly
|
||||
? 'Ein Gesichtsbild reicht — das Stück wird darauf montiert.'
|
||||
: 'Ein Gesichts- und ein Ganzkörperbild. Beide werden nur für deine eigenen Generierungen genutzt.'}
|
||||
? $_('wardrobe.try_on_garment.refs_accessory')
|
||||
: $_('wardrobe.try_on_garment.refs_full')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -123,8 +125,8 @@
|
|||
{#if missingFace}
|
||||
<MeImageUploadZone
|
||||
variant="compact"
|
||||
label="Gesichtsbild hochladen"
|
||||
hint="Kopf + Schulter, möglichst neutrale Beleuchtung"
|
||||
label={$_('wardrobe.try_on_garment.upload_face')}
|
||||
hint={$_('wardrobe.try_on_garment.face_hint')}
|
||||
disabled={uploadingRef}
|
||||
onFiles={(files) => handleRefUpload(files, 'face', 'face-ref')}
|
||||
/>
|
||||
|
|
@ -132,8 +134,8 @@
|
|||
{#if missingBody}
|
||||
<MeImageUploadZone
|
||||
variant="compact"
|
||||
label="Ganzkörperbild hochladen"
|
||||
hint="Stehend, freier Hintergrund, gut erkennbare Haltung"
|
||||
label={$_('wardrobe.try_on_garment.upload_body')}
|
||||
hint={$_('wardrobe.try_on_garment.body_hint')}
|
||||
disabled={uploadingRef}
|
||||
onFiles={(files) => handleRefUpload(files, 'fullbody', 'body-ref')}
|
||||
/>
|
||||
|
|
@ -149,9 +151,9 @@
|
|||
{/if}
|
||||
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Weitere Referenzen oder AI-Opt-ins pro Bild:
|
||||
{$_('wardrobe.try_on_garment.refs_more_prefix')}
|
||||
<a href="/profile/me-images" class="font-medium text-primary hover:underline">
|
||||
Meine Bilder
|
||||
{$_('wardrobe.try_on_garment.refs_link')}
|
||||
</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -182,21 +184,23 @@
|
|||
<span
|
||||
class="h-5 w-5 animate-spin rounded-full border-2 border-current border-r-transparent"
|
||||
></span>
|
||||
Rendere…
|
||||
{$_('wardrobe.try_on_garment.rendering')}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="flex items-center gap-2.5">
|
||||
<Sparkle size={20} weight="fill" />
|
||||
An mir anprobieren
|
||||
{$_('wardrobe.try_on_garment.cta')}
|
||||
</span>
|
||||
<span class="text-xs font-normal opacity-80">{estimatedCredits} Credits</span>
|
||||
<span class="text-xs font-normal opacity-80"
|
||||
>{$_('wardrobe.try_on_garment.credits', { values: { count: estimatedCredits } })}</span
|
||||
>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if accessoryOnly}
|
||||
<p class="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Info size={12} weight="regular" class="flex-shrink-0" />
|
||||
Accessoire-Modus — nur das Gesicht wird gerendert (spart Credits).
|
||||
{$_('wardrobe.try_on_garment.accessory_hint')}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
|
|
@ -204,8 +208,10 @@
|
|||
<p class="flex items-start gap-1.5 text-xs text-muted-foreground">
|
||||
<Info size={12} weight="regular" class="mt-0.5 flex-shrink-0" />
|
||||
<span>
|
||||
Try-On nutzt deine Referenzbilder aus diesem Space
|
||||
<strong class="text-foreground">({activeSpace.name})</strong>, nicht aus Persönlich.
|
||||
{$_('wardrobe.try_on_garment.space_hint_prefix')}
|
||||
<strong class="text-foreground">({activeSpace.name})</strong>{$_(
|
||||
'wardrobe.try_on_garment.space_hint_suffix'
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
{/if}
|
||||
|
|
@ -221,16 +227,20 @@
|
|||
|
||||
{#if lastResultUrl}
|
||||
<div class="space-y-1.5 rounded-xl border border-border bg-card p-3">
|
||||
<p class="text-xs font-medium uppercase tracking-wider text-muted-foreground">Ergebnis</p>
|
||||
<p class="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
{$_('wardrobe.try_on_garment.result_label')}
|
||||
</p>
|
||||
<img
|
||||
src={lastResultUrl}
|
||||
alt="Try-On"
|
||||
alt={$_('wardrobe.try_on_garment.try_on_alt')}
|
||||
class="w-full rounded-md border border-border bg-muted"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Gefunden in der
|
||||
<a href="/picture" class="font-medium text-primary hover:underline">Picture-Galerie</a>
|
||||
als normale Generierung.
|
||||
{$_('wardrobe.try_on_garment.result_hint_prefix')}
|
||||
<a href="/picture" class="font-medium text-primary hover:underline"
|
||||
>{$_('wardrobe.try_on_garment.picture_gallery_link')}</a
|
||||
>
|
||||
{$_('wardrobe.try_on_garment.result_hint_suffix')}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@
|
|||
favorite) lives on the detail page.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { Heart, Sparkle } from '@mana/shared-icons';
|
||||
import { garmentPhotoUrl } from '../api/media-url';
|
||||
import { OCCASION_LABELS } from '../constants';
|
||||
import type { Garment, Outfit } from '../types';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -51,10 +51,10 @@
|
|||
<img src={tryOnUrl} alt={outfit.name} loading="lazy" class="h-full w-full object-cover" />
|
||||
<span
|
||||
class="absolute left-2 top-2 flex items-center gap-1 rounded-md bg-primary/90 px-2 py-0.5 text-xs font-medium text-primary-foreground shadow-sm backdrop-blur-sm"
|
||||
title="Try-On Vorschau"
|
||||
title={$_('wardrobe.outfit_card.try_on_preview_title')}
|
||||
>
|
||||
<Sparkle size={11} weight="fill" />
|
||||
Try-On
|
||||
{$_('wardrobe.outfit_card.try_on_badge')}
|
||||
</span>
|
||||
{:else if resolvedGarments.length > 0}
|
||||
<div class="grid h-full w-full grid-cols-2 grid-rows-2 gap-0.5 bg-border">
|
||||
|
|
@ -79,14 +79,14 @@
|
|||
</div>
|
||||
{:else}
|
||||
<div class="flex h-full w-full items-center justify-center text-xs text-muted-foreground">
|
||||
Leer
|
||||
{$_('wardrobe.outfit_card.empty')}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if outfit.isFavorite}
|
||||
<span
|
||||
class="absolute right-2 top-2 flex h-7 w-7 items-center justify-center rounded-full bg-background/90 text-rose-500 shadow-sm backdrop-blur-sm"
|
||||
title="Favorit"
|
||||
title={$_('wardrobe.outfit_card.favorite')}
|
||||
>
|
||||
<Heart size={14} weight="fill" />
|
||||
</span>
|
||||
|
|
@ -95,10 +95,15 @@
|
|||
<div class="px-3 py-2">
|
||||
<p class="truncate text-sm font-medium text-foreground">{outfit.name}</p>
|
||||
<div class="mt-0.5 flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>{outfit.garmentIds.length}{outfit.garmentIds.length === 1 ? ' Stück' : ' Stücke'}</span>
|
||||
<span
|
||||
>{outfit.garmentIds.length}
|
||||
{outfit.garmentIds.length === 1
|
||||
? $_('wardrobe.piece_singular')
|
||||
: $_('wardrobe.piece_plural')}</span
|
||||
>
|
||||
{#if outfit.occasion}
|
||||
<span class="text-border">·</span>
|
||||
<span>{OCCASION_LABELS[outfit.occasion]}</span>
|
||||
<span>{$_('wardrobe.occasions.' + outfit.occasion)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,17 +13,14 @@
|
|||
of the workflow and is keyboard-accessible for free).
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { Check, Plus, X } from '@mana/shared-icons';
|
||||
import { garmentPhotoUrl } from '../api/media-url';
|
||||
import {
|
||||
CATEGORY_LABELS,
|
||||
CATEGORY_ORDER,
|
||||
OCCASION_LABELS,
|
||||
OCCASION_ORDER,
|
||||
SEASON_LABELS,
|
||||
} from '../constants';
|
||||
import { CATEGORY_ORDER, OCCASION_ORDER } from '../constants';
|
||||
import type { Garment, GarmentCategory, Outfit, OutfitOccasion, OutfitSeason } from '../types';
|
||||
|
||||
const SEASON_KEYS: OutfitSeason[] = ['spring', 'summer', 'autumn', 'winter'];
|
||||
|
||||
interface Props {
|
||||
/** Full library of garments available in the active space. */
|
||||
garments: Garment[];
|
||||
|
|
@ -108,11 +105,11 @@
|
|||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
if (!name.trim()) {
|
||||
error = 'Gib dem Outfit einen Namen.';
|
||||
error = $_('wardrobe.composer.err_name_required');
|
||||
return;
|
||||
}
|
||||
if (selectedIds.length === 0) {
|
||||
error = 'Wähle mindestens ein Kleidungsstück aus.';
|
||||
error = $_('wardrobe.composer.err_no_garments');
|
||||
return;
|
||||
}
|
||||
error = null;
|
||||
|
|
@ -130,7 +127,7 @@
|
|||
tags: tagList,
|
||||
});
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Speichern fehlgeschlagen';
|
||||
error = e instanceof Error ? e.message : $_('wardrobe.composer.err_save_failed');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -140,21 +137,24 @@
|
|||
<section class="space-y-4">
|
||||
<header class="flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Kleiderschrank
|
||||
{$_('wardrobe.composer.section_library')}
|
||||
</h2>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{garments.length}
|
||||
{garments.length === 1 ? 'Stück' : 'Stücke'} verfügbar
|
||||
{garments.length === 1
|
||||
? $_('wardrobe.composer.available_singular', { values: { count: garments.length } })
|
||||
: $_('wardrobe.composer.available_plural', { values: { count: garments.length } })}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
{#if garments.length === 0}
|
||||
<div class="rounded-xl border border-dashed border-border bg-background/50 p-6 text-center">
|
||||
<p class="text-sm font-medium text-foreground">Nichts zum Kombinieren.</p>
|
||||
<p class="text-sm font-medium text-foreground">{$_('wardrobe.composer.empty_title')}</p>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
Lade zuerst ein paar Kleidungsstücke im Tab
|
||||
<a href="/wardrobe" class="font-medium text-primary hover:underline">Kleidung</a>
|
||||
hoch.
|
||||
{$_('wardrobe.composer.empty_hint_prefix')}
|
||||
<a href="/wardrobe" class="font-medium text-primary hover:underline"
|
||||
>{$_('wardrobe.composer.tab_garments_link')}</a
|
||||
>
|
||||
{$_('wardrobe.composer.empty_hint_suffix')}
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
|
|
@ -166,7 +166,7 @@
|
|||
{#if list.length > 0}
|
||||
<div>
|
||||
<h3 class="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{CATEGORY_LABELS[category]}
|
||||
{$_('wardrobe.categories.' + category)}
|
||||
<span class="text-border"> · {list.length}</span>
|
||||
</h3>
|
||||
<div class="grid grid-cols-3 gap-2 sm:grid-cols-4">
|
||||
|
|
@ -216,7 +216,7 @@
|
|||
<div class="space-y-3 rounded-2xl border border-border bg-card p-4">
|
||||
<div>
|
||||
<label for="outfit-name" class="mb-1.5 block text-sm font-medium text-foreground">
|
||||
Name <span class="text-error">*</span>
|
||||
{$_('wardrobe.composer.label_name')} <span class="text-error">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="outfit-name"
|
||||
|
|
@ -224,28 +224,28 @@
|
|||
bind:value={name}
|
||||
required
|
||||
disabled={saving}
|
||||
placeholder="z.B. Bürooutfit Juni"
|
||||
placeholder={$_('wardrobe.composer.placeholder_name')}
|
||||
class="w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="outfit-description" class="mb-1.5 block text-sm font-medium text-foreground">
|
||||
Beschreibung
|
||||
{$_('wardrobe.composer.label_description')}
|
||||
</label>
|
||||
<textarea
|
||||
id="outfit-description"
|
||||
bind:value={description}
|
||||
disabled={saving}
|
||||
rows="2"
|
||||
placeholder="Für welchen Anlass? Besonderheiten?"
|
||||
placeholder={$_('wardrobe.composer.placeholder_description')}
|
||||
class="w-full resize-none rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary disabled:opacity-50"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="outfit-occasion" class="mb-1.5 block text-sm font-medium text-foreground">
|
||||
Anlass
|
||||
{$_('wardrobe.composer.label_occasion')}
|
||||
</label>
|
||||
<select
|
||||
id="outfit-occasion"
|
||||
|
|
@ -253,18 +253,19 @@
|
|||
disabled={saving}
|
||||
class="w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus:border-primary focus:ring-1 focus:ring-primary disabled:opacity-50"
|
||||
>
|
||||
<option value="">— kein Anlass —</option>
|
||||
<option value="">{$_('wardrobe.composer.no_occasion')}</option>
|
||||
{#each OCCASION_ORDER as o}
|
||||
<option value={o}>{OCCASION_LABELS[o]}</option>
|
||||
<option value={o}>{$_('wardrobe.occasions.' + o)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<legend class="mb-1.5 text-sm font-medium text-foreground">Jahreszeit</legend>
|
||||
<legend class="mb-1.5 text-sm font-medium text-foreground"
|
||||
>{$_('wardrobe.composer.label_seasons')}</legend
|
||||
>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{#each Object.entries(SEASON_LABELS) as [season, label]}
|
||||
{@const s = season as OutfitSeason}
|
||||
{#each SEASON_KEYS as s}
|
||||
{@const on = selectedSeasons.includes(s)}
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -275,7 +276,7 @@
|
|||
? 'border-primary bg-primary text-primary-foreground'
|
||||
: 'border-border bg-background text-muted-foreground hover:border-primary/50 hover:text-foreground'} disabled:opacity-50"
|
||||
>
|
||||
{label}
|
||||
{$_('wardrobe.seasons.' + s)}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -283,14 +284,15 @@
|
|||
|
||||
<div>
|
||||
<label for="outfit-tags" class="mb-1.5 block text-sm font-medium text-foreground">
|
||||
Tags <span class="text-muted-foreground">(komma-getrennt)</span>
|
||||
{$_('wardrobe.composer.label_tags')}
|
||||
<span class="text-muted-foreground">{$_('wardrobe.composer.tags_hint')}</span>
|
||||
</label>
|
||||
<input
|
||||
id="outfit-tags"
|
||||
type="text"
|
||||
bind:value={tagsText}
|
||||
disabled={saving}
|
||||
placeholder="minimal, layering, meeting"
|
||||
placeholder={$_('wardrobe.composer.placeholder_tags')}
|
||||
class="w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -299,10 +301,15 @@
|
|||
<div class="space-y-3 rounded-2xl border border-border bg-card p-4">
|
||||
<header class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium text-foreground">
|
||||
Zusammenstellung
|
||||
{$_('wardrobe.composer.section_composition')}
|
||||
<span class="ml-1 text-xs text-muted-foreground">
|
||||
· {selectedGarments.length}
|
||||
{selectedGarments.length === 1 ? 'Stück' : 'Stücke'}
|
||||
{selectedGarments.length === 1
|
||||
? $_('wardrobe.composer.composition_count_singular', {
|
||||
values: { count: selectedGarments.length },
|
||||
})
|
||||
: $_('wardrobe.composer.composition_count_plural', {
|
||||
values: { count: selectedGarments.length },
|
||||
})}
|
||||
</span>
|
||||
</h3>
|
||||
</header>
|
||||
|
|
@ -310,7 +317,7 @@
|
|||
<p
|
||||
class="rounded-md border border-dashed border-border bg-background/50 p-3 text-xs text-muted-foreground"
|
||||
>
|
||||
Klicke links auf Kleidungsstücke, um sie dem Outfit hinzuzufügen.
|
||||
{$_('wardrobe.composer.composition_empty')}
|
||||
</p>
|
||||
{:else}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
|
|
@ -331,8 +338,8 @@
|
|||
<button
|
||||
type="button"
|
||||
onclick={() => removeGarment(g.id)}
|
||||
aria-label="Aus Outfit entfernen"
|
||||
title="Aus Outfit entfernen"
|
||||
aria-label={$_('wardrobe.composer.action_remove')}
|
||||
title={$_('wardrobe.composer.action_remove')}
|
||||
class="absolute right-0.5 top-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-background/90 text-muted-foreground opacity-0 shadow-sm transition-opacity hover:text-error group-hover:opacity-100"
|
||||
>
|
||||
<X size={12} weight="bold" />
|
||||
|
|
@ -358,7 +365,11 @@
|
|||
disabled={saving || !name.trim() || selectedIds.length === 0}
|
||||
class="flex-1 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{saving ? 'Speichere…' : outfit ? 'Änderungen speichern' : 'Outfit anlegen'}
|
||||
{saving
|
||||
? $_('wardrobe.composer.action_saving')
|
||||
: outfit
|
||||
? $_('wardrobe.composer.action_save_edit')
|
||||
: $_('wardrobe.composer.action_save_new')}
|
||||
</button>
|
||||
{#if onCancel}
|
||||
<button
|
||||
|
|
@ -367,7 +378,7 @@
|
|||
disabled={saving}
|
||||
class="rounded-md border border-border bg-background px-4 py-2 text-sm text-foreground transition-colors hover:bg-muted disabled:opacity-50"
|
||||
>
|
||||
Abbrechen
|
||||
{$_('wardrobe.composer.action_cancel')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
loading (request in flight).
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { Sparkle, UserCircle, Info } from '@mana/shared-icons';
|
||||
import { getActiveSpace } from '$lib/data/scope';
|
||||
import { useImageByPrimary } from '$lib/modules/profile/queries';
|
||||
|
|
@ -17,7 +18,6 @@
|
|||
type TryOnModel,
|
||||
} from '../api/try-on';
|
||||
import TryOnModelPicker from './TryOnModelPicker.svelte';
|
||||
import { CATEGORY_LABELS_SINGULAR } from '../constants';
|
||||
import type { Garment, Outfit } from '../types';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -76,7 +76,7 @@
|
|||
model: selectedModel,
|
||||
});
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Try-On fehlgeschlagen';
|
||||
error = err instanceof Error ? err.message : $_('wardrobe.try_on_outfit.err_failed');
|
||||
} finally {
|
||||
running = false;
|
||||
}
|
||||
|
|
@ -96,7 +96,7 @@
|
|||
// face$ / body$ live-queries re-run automatically, so the
|
||||
// missing-block disappears and the button becomes active.
|
||||
} catch (err) {
|
||||
uploadRefError = err instanceof Error ? err.message : 'Upload fehlgeschlagen';
|
||||
uploadRefError = err instanceof Error ? err.message : $_('wardrobe.try_on_outfit.err_upload');
|
||||
} finally {
|
||||
uploadingRef = false;
|
||||
}
|
||||
|
|
@ -108,11 +108,11 @@
|
|||
<div class="flex items-start gap-3 text-sm">
|
||||
<UserCircle size={18} weight="regular" class="mt-0.5 flex-shrink-0 text-primary" />
|
||||
<div class="space-y-1">
|
||||
<p class="font-medium text-foreground">Für Try-On brauchen wir dich auf Bild.</p>
|
||||
<p class="font-medium text-foreground">{$_('wardrobe.try_on_outfit.refs_title')}</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{accessoryOnly
|
||||
? 'Ein Gesichtsbild reicht — der Rest bleibt wie auf deinem Foto.'
|
||||
: 'Ein Gesichts- und ein Ganzkörperbild. Beide werden nur für deine eigenen Generierungen genutzt.'}
|
||||
? $_('wardrobe.try_on_outfit.refs_accessory')
|
||||
: $_('wardrobe.try_on_outfit.refs_full')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -120,8 +120,8 @@
|
|||
{#if missingFace}
|
||||
<MeImageUploadZone
|
||||
variant="compact"
|
||||
label="Gesichtsbild hochladen"
|
||||
hint="Kopf + Schulter, möglichst neutrale Beleuchtung"
|
||||
label={$_('wardrobe.try_on_outfit.upload_face')}
|
||||
hint={$_('wardrobe.try_on_outfit.face_hint')}
|
||||
disabled={uploadingRef}
|
||||
onFiles={(files) => handleRefUpload(files, 'face', 'face-ref')}
|
||||
/>
|
||||
|
|
@ -129,8 +129,8 @@
|
|||
{#if missingBody}
|
||||
<MeImageUploadZone
|
||||
variant="compact"
|
||||
label="Ganzkörperbild hochladen"
|
||||
hint="Stehend, freier Hintergrund, gut erkennbare Haltung"
|
||||
label={$_('wardrobe.try_on_outfit.upload_body')}
|
||||
hint={$_('wardrobe.try_on_outfit.body_hint')}
|
||||
disabled={uploadingRef}
|
||||
onFiles={(files) => handleRefUpload(files, 'fullbody', 'body-ref')}
|
||||
/>
|
||||
|
|
@ -146,9 +146,9 @@
|
|||
{/if}
|
||||
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Weitere Referenzen oder AI-Opt-ins pro Bild:
|
||||
{$_('wardrobe.try_on_outfit.refs_more_prefix')}
|
||||
<a href="/profile/me-images" class="font-medium text-primary hover:underline">
|
||||
Meine Bilder
|
||||
{$_('wardrobe.try_on_outfit.refs_link')}
|
||||
</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -175,27 +175,28 @@
|
|||
<span
|
||||
class="h-5 w-5 animate-spin rounded-full border-2 border-current border-r-transparent"
|
||||
></span>
|
||||
Rendere…
|
||||
{$_('wardrobe.try_on_outfit.rendering')}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="flex items-center gap-2.5">
|
||||
<Sparkle size={20} weight="fill" />
|
||||
Anprobieren
|
||||
{$_('wardrobe.try_on_outfit.cta')}
|
||||
</span>
|
||||
<span class="text-xs font-normal opacity-80">{estimatedCredits} Credits</span>
|
||||
<span class="text-xs font-normal opacity-80"
|
||||
>{$_('wardrobe.try_on_outfit.credits', { values: { count: estimatedCredits } })}</span
|
||||
>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if accessoryOnly}
|
||||
<p class="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Info size={12} weight="regular" class="flex-shrink-0" />
|
||||
Accessoire-Modus — nur das Gesicht wird gerendert (spart Credits).
|
||||
{$_('wardrobe.try_on_outfit.accessory_hint')}
|
||||
</p>
|
||||
{:else if garments.length > 6}
|
||||
<p class="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Info size={12} weight="regular" class="flex-shrink-0" />
|
||||
Mit {garments.length} Kleidungsstücken ist der Referenz-Slot knapp — ältere Items werden evtl.
|
||||
nicht mitgezogen.
|
||||
{$_('wardrobe.try_on_outfit.many_garments_hint', { values: { count: garments.length } })}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
|
|
@ -203,10 +204,12 @@
|
|||
<p class="flex items-start gap-1.5 text-xs text-muted-foreground">
|
||||
<Info size={12} weight="regular" class="mt-0.5 flex-shrink-0" />
|
||||
<span>
|
||||
Try-On nutzt deine Referenzbilder aus diesem Space
|
||||
<strong class="text-foreground">({activeSpace.name})</strong>, nicht aus Persönlich.
|
||||
{$_('wardrobe.try_on_outfit.space_hint_prefix')}
|
||||
<strong class="text-foreground">({activeSpace.name})</strong>{$_(
|
||||
'wardrobe.try_on_outfit.space_hint_suffix'
|
||||
)}
|
||||
{#if activeSpace.type === 'family'}
|
||||
Kinder-Outfits werden trotzdem auf dein Gesicht gerendert.
|
||||
{$_('wardrobe.try_on_outfit.family_hint')}
|
||||
{/if}
|
||||
</span>
|
||||
</p>
|
||||
|
|
@ -225,6 +228,8 @@
|
|||
|
||||
{#if !missingFace && !missingBody && garments.length === 0}
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Füge mindestens ein {CATEGORY_LABELS_SINGULAR.top ?? 'Kleidungsstück'} hinzu, um Try-On zu aktivieren.
|
||||
{$_('wardrobe.try_on_outfit.empty_garments', {
|
||||
values: { category: $_('wardrobe.categories_singular.top') },
|
||||
})}
|
||||
</p>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
on the detail route.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import type { TryOnModel } from '../api/try-on';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -23,31 +24,33 @@
|
|||
|
||||
let { value, onChange, disabled = false }: Props = $props();
|
||||
|
||||
const OPTIONS: Array<{
|
||||
id: TryOnModel;
|
||||
label: string;
|
||||
hint: string;
|
||||
}> = [
|
||||
const OPTIONS = $derived<
|
||||
Array<{
|
||||
id: TryOnModel;
|
||||
label: string;
|
||||
hint: string;
|
||||
}>
|
||||
>([
|
||||
{
|
||||
id: 'openai/gpt-image-2',
|
||||
label: 'OpenAI',
|
||||
hint: 'GPT-image · Standard',
|
||||
label: $_('wardrobe.model_picker.option_openai_label'),
|
||||
hint: $_('wardrobe.model_picker.option_openai_hint'),
|
||||
},
|
||||
{
|
||||
id: 'google/gemini-3-pro-image-preview',
|
||||
label: 'Nano Banana Pro',
|
||||
hint: 'Google · hohe Konsistenz',
|
||||
label: $_('wardrobe.model_picker.option_pro_label'),
|
||||
hint: $_('wardrobe.model_picker.option_pro_hint'),
|
||||
},
|
||||
{
|
||||
id: 'google/gemini-3.1-flash-image-preview',
|
||||
label: 'Nano Banana 2',
|
||||
hint: 'Google · neuestes · günstig',
|
||||
label: $_('wardrobe.model_picker.option_flash_label'),
|
||||
hint: $_('wardrobe.model_picker.option_flash_hint'),
|
||||
},
|
||||
];
|
||||
]);
|
||||
</script>
|
||||
|
||||
<fieldset class="picker" {disabled}>
|
||||
<legend class="legend">Modell</legend>
|
||||
<legend class="legend">{$_('wardrobe.model_picker.legend')}</legend>
|
||||
<div class="options">
|
||||
{#each OPTIONS as opt (opt.id)}
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@
|
|||
-->
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { deriveUpdatedAt } from '$lib/data/sync';
|
||||
import { CheckCircle, PencilSimple, Archive, Sparkle, Trash } from '@mana/shared-icons';
|
||||
import { useGarment, useGarmentSoloTryOns, useOutfitsContainingGarment } from '../queries';
|
||||
import { wardrobeGarmentsStore } from '../stores/garments.svelte';
|
||||
import { garmentPhotoUrl } from '../api/media-url';
|
||||
import { CATEGORY_LABELS } from '../constants';
|
||||
import GarmentForm from '../components/GarmentForm.svelte';
|
||||
import GarmentTryOnButton from '../components/GarmentTryOnButton.svelte';
|
||||
import ImageLightbox from '$lib/modules/picture/components/ImageLightbox.svelte';
|
||||
|
|
@ -86,7 +86,8 @@
|
|||
|
||||
async function handleDelete() {
|
||||
if (!garment) return;
|
||||
if (!confirm(`"${garment.name}" wirklich löschen?`)) return;
|
||||
if (!confirm($_('wardrobe.detail_garment.confirm_delete', { values: { name: garment.name } })))
|
||||
return;
|
||||
await wardrobeGarmentsStore.deleteGarment(garment.id);
|
||||
goto('/wardrobe');
|
||||
}
|
||||
|
|
@ -111,12 +112,14 @@
|
|||
<div class="mx-auto max-w-3xl space-y-5 p-4 sm:p-6">
|
||||
{#if !garment}
|
||||
{#if garment$.loading}
|
||||
<p class="text-sm text-muted-foreground">Lädt…</p>
|
||||
<p class="text-sm text-muted-foreground">{$_('wardrobe.detail_garment.loading')}</p>
|
||||
{:else}
|
||||
<div class="rounded-2xl border border-dashed border-border bg-background/50 p-8 text-center">
|
||||
<p class="text-sm font-medium text-foreground">Nicht gefunden.</p>
|
||||
<p class="text-sm font-medium text-foreground">
|
||||
{$_('wardrobe.detail_garment.not_found_title')}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
Das Kleidungsstück wurde gelöscht oder gehört zu einem anderen Space.
|
||||
{$_('wardrobe.detail_garment.not_found_desc')}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -131,7 +134,7 @@
|
|||
<button
|
||||
type="button"
|
||||
onclick={openPhotoLightbox}
|
||||
aria-label="Foto vergrößern"
|
||||
aria-label={$_('wardrobe.detail_garment.action_enlarge')}
|
||||
class="group block overflow-hidden rounded-2xl border border-border bg-muted transition-all hover:border-primary/50 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
|
||||
>
|
||||
<img
|
||||
|
|
@ -153,7 +156,9 @@
|
|||
<header class="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<h1 class="text-lg font-semibold text-foreground">{garment.name}</h1>
|
||||
<p class="text-sm text-muted-foreground">{CATEGORY_LABELS[garment.category]}</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{$_('wardrobe.categories.' + garment.category)}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Edit affordance uses the same primary-tinted hover as
|
||||
the Try-On thumbs / model picker so interactive elements
|
||||
|
|
@ -163,42 +168,52 @@
|
|||
<button
|
||||
type="button"
|
||||
onclick={() => (editing = true)}
|
||||
aria-label="Bearbeiten"
|
||||
aria-label={$_('wardrobe.detail_garment.action_edit')}
|
||||
class="flex items-center gap-1.5 rounded-md border border-border bg-background px-2.5 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-primary/50 hover:bg-primary/5 hover:text-foreground"
|
||||
>
|
||||
<PencilSimple size={14} />
|
||||
Bearbeiten
|
||||
{$_('wardrobe.detail_garment.action_edit')}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<dl class="grid grid-cols-2 gap-3 text-sm">
|
||||
{#if garment.brand}
|
||||
<div>
|
||||
<dt class="text-xs uppercase tracking-wider text-muted-foreground">Marke</dt>
|
||||
<dt class="text-xs uppercase tracking-wider text-muted-foreground">
|
||||
{$_('wardrobe.detail_garment.label_brand')}
|
||||
</dt>
|
||||
<dd class="text-foreground">{garment.brand}</dd>
|
||||
</div>
|
||||
{/if}
|
||||
{#if garment.color}
|
||||
<div>
|
||||
<dt class="text-xs uppercase tracking-wider text-muted-foreground">Farbe</dt>
|
||||
<dt class="text-xs uppercase tracking-wider text-muted-foreground">
|
||||
{$_('wardrobe.detail_garment.label_color')}
|
||||
</dt>
|
||||
<dd class="text-foreground">{garment.color}</dd>
|
||||
</div>
|
||||
{/if}
|
||||
{#if garment.size}
|
||||
<div>
|
||||
<dt class="text-xs uppercase tracking-wider text-muted-foreground">Größe</dt>
|
||||
<dt class="text-xs uppercase tracking-wider text-muted-foreground">
|
||||
{$_('wardrobe.detail_garment.label_size')}
|
||||
</dt>
|
||||
<dd class="text-foreground">{garment.size}</dd>
|
||||
</div>
|
||||
{/if}
|
||||
{#if garment.material}
|
||||
<div>
|
||||
<dt class="text-xs uppercase tracking-wider text-muted-foreground">Material</dt>
|
||||
<dt class="text-xs uppercase tracking-wider text-muted-foreground">
|
||||
{$_('wardrobe.detail_garment.label_material')}
|
||||
</dt>
|
||||
<dd class="text-foreground">{garment.material}</dd>
|
||||
</div>
|
||||
{/if}
|
||||
{#if garment.priceCents}
|
||||
<div>
|
||||
<dt class="text-xs uppercase tracking-wider text-muted-foreground">Preis</dt>
|
||||
<dt class="text-xs uppercase tracking-wider text-muted-foreground">
|
||||
{$_('wardrobe.detail_garment.label_price')}
|
||||
</dt>
|
||||
<dd class="text-foreground">
|
||||
{(garment.priceCents / 100).toFixed(2)}
|
||||
{garment.currency ?? ''}
|
||||
|
|
@ -207,10 +222,16 @@
|
|||
{/if}
|
||||
{#if garment.wearCount && garment.wearCount > 0}
|
||||
<div>
|
||||
<dt class="text-xs uppercase tracking-wider text-muted-foreground">Getragen</dt>
|
||||
<dt class="text-xs uppercase tracking-wider text-muted-foreground">
|
||||
{$_('wardrobe.detail_garment.label_wear_count')}
|
||||
</dt>
|
||||
<dd class="text-foreground">
|
||||
{garment.wearCount}×{garment.lastWornAt
|
||||
? ` · zuletzt ${garment.lastWornAt}`
|
||||
{$_('wardrobe.detail_garment.wear_count_value', {
|
||||
values: { count: garment.wearCount },
|
||||
})}{garment.lastWornAt
|
||||
? $_('wardrobe.detail_garment.last_worn_suffix', {
|
||||
values: { date: garment.lastWornAt },
|
||||
})
|
||||
: ''}
|
||||
</dd>
|
||||
</div>
|
||||
|
|
@ -242,10 +263,10 @@
|
|||
<a
|
||||
href={`/comic/character/new?title=${encodeURIComponent(garment.name)}&prompt=${encodeURIComponent('wearing ' + garment.name)}`}
|
||||
class="flex w-full items-center justify-center gap-1.5 rounded-md border border-border bg-background px-3 py-2 text-xs font-medium text-foreground transition-colors hover:border-primary/40 hover:bg-primary/5"
|
||||
title="Aus diesem Kleidungsstück einen Comic-Character generieren"
|
||||
title={$_('wardrobe.detail_garment.action_comic_title')}
|
||||
>
|
||||
<Sparkle size={12} />
|
||||
Als Comic-Character
|
||||
{$_('wardrobe.detail_garment.action_comic')}
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
|
|
@ -264,13 +285,19 @@
|
|||
class="flex flex-1 items-center justify-center gap-2 rounded-md border border-border bg-background px-4 py-2 text-sm text-foreground transition-colors hover:border-primary/50 hover:bg-primary/5 disabled:opacity-50 disabled:hover:border-border disabled:hover:bg-background"
|
||||
>
|
||||
<CheckCircle size={14} />
|
||||
{markingWorn ? 'Gespeichert…' : 'Heute getragen'}
|
||||
{markingWorn
|
||||
? $_('wardrobe.detail_garment.action_marking')
|
||||
: $_('wardrobe.detail_garment.action_mark_worn')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleArchive}
|
||||
aria-label={garment.isArchived ? 'Wieder aktiv setzen' : 'Archivieren'}
|
||||
title={garment.isArchived ? 'Wieder aktiv setzen' : 'Archivieren'}
|
||||
aria-label={garment.isArchived
|
||||
? $_('wardrobe.detail_garment.action_unarchive')
|
||||
: $_('wardrobe.detail_garment.action_archive')}
|
||||
title={garment.isArchived
|
||||
? $_('wardrobe.detail_garment.action_unarchive')
|
||||
: $_('wardrobe.detail_garment.action_archive')}
|
||||
class="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md border border-border bg-background text-muted-foreground transition-colors hover:border-primary/50 hover:bg-primary/5 hover:text-foreground"
|
||||
>
|
||||
<Archive size={16} />
|
||||
|
|
@ -278,8 +305,8 @@
|
|||
<button
|
||||
type="button"
|
||||
onclick={handleDelete}
|
||||
aria-label="Löschen"
|
||||
title="Löschen"
|
||||
aria-label={$_('wardrobe.detail_garment.action_delete')}
|
||||
title={$_('wardrobe.detail_garment.action_delete')}
|
||||
class="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md border border-border bg-background text-error transition-colors hover:border-error/50 hover:bg-error/10"
|
||||
>
|
||||
<Trash size={16} />
|
||||
|
|
@ -298,7 +325,9 @@
|
|||
<section class="space-y-2">
|
||||
<header class="flex items-baseline justify-between">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Anproben · {soloTryOns.length}
|
||||
{$_('wardrobe.detail_garment.section_try_ons', {
|
||||
values: { count: soloTryOns.length },
|
||||
})}
|
||||
</h2>
|
||||
</header>
|
||||
<div class="flex gap-3 overflow-x-auto pb-1">
|
||||
|
|
@ -331,7 +360,9 @@
|
|||
<section class="space-y-2">
|
||||
<header class="flex items-baseline justify-between">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
In Outfits · {outfits.length}
|
||||
{$_('wardrobe.detail_garment.section_outfits', {
|
||||
values: { count: outfits.length },
|
||||
})}
|
||||
</h2>
|
||||
</header>
|
||||
<div class="flex gap-3 overflow-x-auto pb-1">
|
||||
|
|
@ -355,7 +386,7 @@
|
|||
<div
|
||||
class="flex h-full w-full items-center justify-center text-xs text-muted-foreground"
|
||||
>
|
||||
Noch keine Anprobe
|
||||
{$_('wardrobe.detail_garment.no_try_on_yet')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -378,7 +409,7 @@
|
|||
href="/picture"
|
||||
class="flex items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-muted"
|
||||
>
|
||||
In Picture öffnen
|
||||
{$_('wardrobe.detail_garment.action_open_picture')}
|
||||
</a>
|
||||
{/snippet}
|
||||
</ImageLightbox>
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@
|
|||
-->
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { ArrowLeft, Archive, Heart, PencilSimple, Sparkle, Trash } from '@mana/shared-icons';
|
||||
import { VisibilityPicker, type VisibilityLevel } from '@mana/shared-privacy';
|
||||
import { useAllGarments, useOutfit, useOutfitTryOns } from '../queries';
|
||||
import { wardrobeOutfitsStore } from '../stores/outfits.svelte';
|
||||
import { garmentPhotoUrl } from '../api/media-url';
|
||||
import { CATEGORY_LABELS_SINGULAR, OCCASION_LABELS, SEASON_LABELS } from '../constants';
|
||||
import TryOnButton from '../components/TryOnButton.svelte';
|
||||
import type { Garment } from '../types';
|
||||
|
||||
|
|
@ -66,7 +66,8 @@
|
|||
|
||||
async function handleDelete() {
|
||||
if (!outfit) return;
|
||||
if (!confirm(`Outfit "${outfit.name}" wirklich löschen?`)) return;
|
||||
if (!confirm($_('wardrobe.detail_outfit.confirm_delete', { values: { name: outfit.name } })))
|
||||
return;
|
||||
await wardrobeOutfitsStore.deleteOutfit(outfit.id);
|
||||
goto('/wardrobe');
|
||||
}
|
||||
|
|
@ -82,20 +83,24 @@
|
|||
<a
|
||||
href="/wardrobe"
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted"
|
||||
aria-label="Zurück zum Kleiderschrank"
|
||||
aria-label={$_('wardrobe.detail_outfit.back')}
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
</a>
|
||||
<span class="text-muted-foreground">Kleiderschrank · Outfits</span>
|
||||
<span class="text-muted-foreground">{$_('wardrobe.detail_outfit.breadcrumb')}</span>
|
||||
</nav>
|
||||
|
||||
{#if !outfit}
|
||||
{#if outfit$.loading}
|
||||
<p class="text-sm text-muted-foreground">Lädt…</p>
|
||||
<p class="text-sm text-muted-foreground">{$_('wardrobe.detail_outfit.loading')}</p>
|
||||
{:else}
|
||||
<div class="rounded-2xl border border-dashed border-border bg-background/50 p-8 text-center">
|
||||
<p class="text-sm font-medium text-foreground">Outfit nicht gefunden.</p>
|
||||
<p class="mt-1 text-sm text-muted-foreground">Gelöscht oder in einem anderen Space.</p>
|
||||
<p class="text-sm font-medium text-foreground">
|
||||
{$_('wardrobe.detail_outfit.not_found_title')}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
{$_('wardrobe.detail_outfit.not_found_desc')}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
|
|
@ -106,7 +111,7 @@
|
|||
{#if outfit.lastTryOn?.imageUrl}
|
||||
<img
|
||||
src={outfit.lastTryOn.imageUrl}
|
||||
alt="Try-On Vorschau"
|
||||
alt={$_('wardrobe.detail_outfit.try_on_preview_alt')}
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
{:else if resolvedGarments.length > 0}
|
||||
|
|
@ -128,7 +133,7 @@
|
|||
<div
|
||||
class="flex aspect-square items-center justify-center text-sm text-muted-foreground"
|
||||
>
|
||||
Keine Kleidungsstücke
|
||||
{$_('wardrobe.detail_outfit.no_garments')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -143,17 +148,17 @@
|
|||
<a
|
||||
href={`/comic/character/new?title=${encodeURIComponent(outfit.name)}&prompt=${encodeURIComponent('wearing the ' + outfit.name + ' outfit')}`}
|
||||
class="flex w-full items-center justify-center gap-1.5 rounded-md border border-border bg-background px-3 py-2 text-xs font-medium text-foreground transition-colors hover:border-primary/40 hover:bg-primary/5"
|
||||
title="Aus diesem Outfit einen Comic-Character generieren"
|
||||
title={$_('wardrobe.detail_outfit.action_comic_title')}
|
||||
>
|
||||
<Sparkle size={12} />
|
||||
Als Comic-Character
|
||||
{$_('wardrobe.detail_outfit.action_comic')}
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
{#if tryOns.length > 0}
|
||||
<div>
|
||||
<h3 class="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Try-On Verlauf
|
||||
{$_('wardrobe.detail_outfit.try_on_history')}
|
||||
</h3>
|
||||
<div class="flex gap-2 overflow-x-auto">
|
||||
{#each tryOns as t (t.id)}
|
||||
|
|
@ -180,15 +185,17 @@
|
|||
<div class="mt-0.5 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>
|
||||
{outfit.garmentIds.length}
|
||||
{outfit.garmentIds.length === 1 ? 'Stück' : 'Stücke'}
|
||||
{outfit.garmentIds.length === 1
|
||||
? $_('wardrobe.piece_singular')
|
||||
: $_('wardrobe.piece_plural')}
|
||||
</span>
|
||||
{#if outfit.occasion}
|
||||
<span class="text-border">·</span>
|
||||
<span>{OCCASION_LABELS[outfit.occasion]}</span>
|
||||
<span>{$_('wardrobe.occasions.' + outfit.occasion)}</span>
|
||||
{/if}
|
||||
{#if outfit.season && outfit.season.length > 0}
|
||||
<span class="text-border">·</span>
|
||||
<span>{outfit.season.map((s) => SEASON_LABELS[s]).join(', ')}</span>
|
||||
<span>{outfit.season.map((s) => $_('wardrobe.seasons.' + s)).join(', ')}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -196,8 +203,12 @@
|
|||
<button
|
||||
type="button"
|
||||
onclick={handleToggleFavorite}
|
||||
aria-label={outfit.isFavorite ? 'Favorit entfernen' : 'Als Favorit markieren'}
|
||||
title={outfit.isFavorite ? 'Favorit entfernen' : 'Als Favorit markieren'}
|
||||
aria-label={outfit.isFavorite
|
||||
? $_('wardrobe.detail_outfit.action_unfavorite')
|
||||
: $_('wardrobe.detail_outfit.action_favorite')}
|
||||
title={outfit.isFavorite
|
||||
? $_('wardrobe.detail_outfit.action_unfavorite')
|
||||
: $_('wardrobe.detail_outfit.action_favorite')}
|
||||
class="flex h-8 w-8 items-center justify-center rounded-md transition-colors {outfit.isFavorite
|
||||
? 'text-rose-500 hover:bg-rose-500/10'
|
||||
: 'text-muted-foreground hover:bg-muted hover:text-foreground'}"
|
||||
|
|
@ -206,8 +217,8 @@
|
|||
</button>
|
||||
<a
|
||||
href="/wardrobe/compose/{outfit.id}"
|
||||
aria-label="Bearbeiten"
|
||||
title="Bearbeiten"
|
||||
aria-label={$_('wardrobe.detail_outfit.action_edit')}
|
||||
title={$_('wardrobe.detail_outfit.action_edit')}
|
||||
class="flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
<PencilSimple size={16} />
|
||||
|
|
@ -216,7 +227,9 @@
|
|||
</header>
|
||||
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="text-xs text-muted-foreground">Sichtbarkeit</span>
|
||||
<span class="text-xs text-muted-foreground"
|
||||
>{$_('wardrobe.detail_outfit.label_visibility')}</span
|
||||
>
|
||||
<VisibilityPicker
|
||||
level={outfit.visibility ?? 'private'}
|
||||
onChange={handleVisibilityChange}
|
||||
|
|
@ -241,7 +254,7 @@
|
|||
<!-- Garments in this outfit -->
|
||||
<div class="space-y-3 rounded-2xl border border-border bg-card p-5">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Zusammenstellung
|
||||
{$_('wardrobe.detail_outfit.section_composition')}
|
||||
</h2>
|
||||
{#if resolvedGarments.length > 0}
|
||||
<div class="grid grid-cols-3 gap-2 sm:grid-cols-4">
|
||||
|
|
@ -264,7 +277,7 @@
|
|||
<div class="px-1.5 py-1">
|
||||
<p class="truncate text-xs font-medium text-foreground">{g.name}</p>
|
||||
<p class="truncate text-[10px] text-muted-foreground">
|
||||
{CATEGORY_LABELS_SINGULAR[g.category]}
|
||||
{$_('wardrobe.categories_singular.' + g.category)}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
|
@ -272,7 +285,7 @@
|
|||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Referenzierte Kleidungsstücke wurden entfernt oder gehören zu einem anderen Space.
|
||||
{$_('wardrobe.detail_outfit.composition_missing')}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -285,7 +298,9 @@
|
|||
class="flex flex-1 items-center justify-center gap-2 rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground transition-colors hover:bg-muted"
|
||||
>
|
||||
<Archive size={14} />
|
||||
{outfit.isArchived ? 'Wieder aktiv' : 'Archivieren'}
|
||||
{outfit.isArchived
|
||||
? $_('wardrobe.detail_outfit.action_unarchive')
|
||||
: $_('wardrobe.detail_outfit.action_archive')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -293,7 +308,7 @@
|
|||
class="flex flex-1 items-center justify-center gap-2 rounded-md border border-border bg-background px-3 py-2 text-sm text-error transition-colors hover:bg-error/10"
|
||||
>
|
||||
<Trash size={14} />
|
||||
Löschen
|
||||
{$_('wardrobe.detail_outfit.action_delete')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@
|
|||
to the filename-sans-extension, as in the picture module's upload.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import MeImageUploadZone from '$lib/modules/profile/components/MeImageUploadZone.svelte';
|
||||
import { readImageDimensions } from '$lib/modules/profile/api/me-images';
|
||||
import { useAllGarments } from '../queries';
|
||||
import { wardrobeGarmentsStore } from '../stores/garments.svelte';
|
||||
import { uploadGarmentPhoto } from '../api/upload';
|
||||
import { CATEGORY_LABELS, CATEGORY_LABELS_SINGULAR } from '../constants';
|
||||
import CategoryTabs from '../components/CategoryTabs.svelte';
|
||||
import GarmentCard from '../components/GarmentCard.svelte';
|
||||
import { prettifyUploadName } from '../utils/name';
|
||||
|
|
@ -74,7 +74,7 @@
|
|||
});
|
||||
}
|
||||
} catch (err) {
|
||||
uploadError = err instanceof Error ? err.message : 'Upload fehlgeschlagen';
|
||||
uploadError = err instanceof Error ? err.message : $_('wardrobe.upload_failed');
|
||||
} finally {
|
||||
uploading = false;
|
||||
}
|
||||
|
|
@ -91,9 +91,11 @@
|
|||
<MeImageUploadZone
|
||||
variant="compact"
|
||||
label={activeTab === 'all'
|
||||
? 'Kleidungsstück hochladen'
|
||||
: `${CATEGORY_LABELS_SINGULAR[activeTab]} hochladen`}
|
||||
hint="Foto frontal, heller Hintergrund — bessere Try-On-Ergebnisse"
|
||||
? $_('wardrobe.grid_view.upload_label_all')
|
||||
: $_('wardrobe.grid_view.upload_label_for_category', {
|
||||
values: { category: $_('wardrobe.categories_singular.' + activeTab) },
|
||||
})}
|
||||
hint={$_('wardrobe.grid_view.upload_hint')}
|
||||
disabled={uploading}
|
||||
onFiles={ingestFiles}
|
||||
/>
|
||||
|
|
@ -113,17 +115,22 @@
|
|||
</div>
|
||||
{:else if garments.length === 0}
|
||||
<div class="rounded-2xl border border-dashed border-border bg-background/50 p-6 text-center">
|
||||
<p class="text-sm font-medium text-foreground">Noch nichts im Schrank.</p>
|
||||
<p class="text-sm font-medium text-foreground">{$_('wardrobe.grid_view.empty_title')}</p>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
Zieh ein Foto in die Zone oben — oder klick sie an, um eins auszuwählen.
|
||||
{$_('wardrobe.grid_view.empty_hint')}
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="rounded-2xl border border-dashed border-border bg-background/50 p-6 text-center">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Keine Einträge unter <strong class="text-foreground"
|
||||
>{activeTab === 'all' ? 'Alle' : CATEGORY_LABELS[activeTab]}</strong
|
||||
>.
|
||||
{$_('wardrobe.grid_view.no_entries_under', {
|
||||
values: {
|
||||
category:
|
||||
activeTab === 'all'
|
||||
? $_('wardrobe.categories.all')
|
||||
: $_('wardrobe.categories.' + activeTab),
|
||||
},
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -134,8 +141,7 @@
|
|||
view clean. -->
|
||||
{#if activeSpace && activeSpace.type !== 'personal'}
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Dieser Schrank gehört zu <strong class="text-foreground">{activeSpace.name}</strong> — Uploads landen
|
||||
nur hier, nicht in deinem persönlichen Schrank.
|
||||
{$_('wardrobe.grid_view.space_footer', { values: { name: activeSpace.name } })}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
composer; existing outfits open their detail view on click.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { Plus, Sparkle } from '@mana/shared-icons';
|
||||
import { useAllGarments, useAllOutfits } from '../queries';
|
||||
import OutfitCard from '../components/OutfitCard.svelte';
|
||||
|
|
@ -26,11 +27,15 @@
|
|||
<div class="space-y-4">
|
||||
<header class="flex items-center justify-between gap-2">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wider text-muted-foreground">Outfits</h2>
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{$_('wardrobe.outfits_view.title')}
|
||||
</h2>
|
||||
{#if outfits.length > 0}
|
||||
<p class="mt-0.5 text-xs text-muted-foreground">
|
||||
{outfits.length}
|
||||
{outfits.length === 1 ? 'Zusammenstellung' : 'Zusammenstellungen'}
|
||||
{outfits.length === 1
|
||||
? $_('wardrobe.outfits_view.count_singular')
|
||||
: $_('wardrobe.outfits_view.count_plural')}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -39,7 +44,7 @@
|
|||
class="flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
>
|
||||
<Plus size={14} weight="bold" />
|
||||
Neues Outfit
|
||||
{$_('wardrobe.outfits_view.action_new')}
|
||||
</a>
|
||||
</header>
|
||||
|
||||
|
|
@ -52,26 +57,24 @@
|
|||
{:else if garments.length === 0}
|
||||
<div class="rounded-2xl border border-dashed border-border bg-background/50 p-8 text-center">
|
||||
<Sparkle size={24} weight="fill" class="mx-auto mb-3 text-primary/60" />
|
||||
<p class="text-sm font-medium text-foreground">Noch keine Outfits.</p>
|
||||
<p class="text-sm font-medium text-foreground">{$_('wardrobe.outfits_view.empty_title')}</p>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
Füge zuerst ein paar Kleidungsstücke im Tab "Kleidung" hinzu — danach lassen sie sich hier
|
||||
zu Outfits kombinieren.
|
||||
{$_('wardrobe.outfits_view.empty_no_garments')}
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="rounded-2xl border border-dashed border-border bg-background/50 p-8 text-center">
|
||||
<Sparkle size={24} weight="fill" class="mx-auto mb-3 text-primary/60" />
|
||||
<p class="text-sm font-medium text-foreground">Noch keine Outfits.</p>
|
||||
<p class="text-sm font-medium text-foreground">{$_('wardrobe.outfits_view.empty_title')}</p>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
Kombiniere deine Kleidungsstücke zu Looks, die du dann mit KI an dir selbst anprobieren
|
||||
kannst.
|
||||
{$_('wardrobe.outfits_view.empty_with_garments')}
|
||||
</p>
|
||||
<a
|
||||
href="/wardrobe/compose"
|
||||
class="mt-4 inline-flex items-center gap-1.5 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
>
|
||||
<Plus size={14} weight="bold" />
|
||||
Erstes Outfit komponieren
|
||||
{$_('wardrobe.outfits_view.action_compose_first')}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { ArrowLeft } from '@mana/shared-icons';
|
||||
import { RoutePage } from '$lib/components/shell';
|
||||
import OutfitComposer from '$lib/modules/wardrobe/components/OutfitComposer.svelte';
|
||||
|
|
@ -54,7 +55,9 @@
|
|||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{outfitId ? 'Outfit bearbeiten' : 'Neues Outfit'} · Mana</title>
|
||||
<title
|
||||
>{outfitId ? $_('wardrobe.compose.title_edit') : $_('wardrobe.compose.title_new')} · Mana</title
|
||||
>
|
||||
</svelte:head>
|
||||
|
||||
<RoutePage appId="wardrobe" backHref="/wardrobe">
|
||||
|
|
@ -63,19 +66,23 @@
|
|||
<a
|
||||
href={outfitId ? `/wardrobe/outfit/${outfitId}` : '/wardrobe'}
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted"
|
||||
aria-label="Zurück"
|
||||
aria-label={$_('wardrobe.compose.back')}
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
</a>
|
||||
<h1 class="text-xl font-bold text-foreground">
|
||||
{outfitId ? 'Outfit bearbeiten' : 'Neues Outfit'}
|
||||
{outfitId ? $_('wardrobe.compose.title_edit') : $_('wardrobe.compose.title_new')}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
{#if outfitId && !outfit && !existingOutfit$.loading}
|
||||
<div class="rounded-2xl border border-dashed border-border bg-background/50 p-8 text-center">
|
||||
<p class="text-sm font-medium text-foreground">Outfit nicht gefunden.</p>
|
||||
<p class="mt-1 text-sm text-muted-foreground">Gelöscht oder in einem anderen Space.</p>
|
||||
<p class="text-sm font-medium text-foreground">
|
||||
{$_('wardrobe.detail_outfit.not_found_title')}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
{$_('wardrobe.detail_outfit.not_found_desc')}
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Remount when we switch from /compose (new) to /compose/:id (edit)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue