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:
Till JS 2026-04-27 00:12:18 +02:00
parent 8804a20a7f
commit 5959f66387
21 changed files with 1677 additions and 274 deletions

View 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"
}
}

View 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"
}
}

View 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"
}
}

View 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"
}
}

View 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"
}
}

View file

@ -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>

View file

@ -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}

View file

@ -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>

View file

@ -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>

View file

@ -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}

View file

@ -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>

View file

@ -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>

View file

@ -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}

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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}

View file

@ -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)

View file

@ -10,6 +10,8 @@
"apps/mana/apps/web/src/lib/components/dashboard/widgets/CardsProgressWidget.svelte": 4,
"apps/mana/apps/web/src/lib/components/dashboard/widgets/MusicLibraryWidget.svelte": 1,
"apps/mana/apps/web/src/lib/components/DetailViewShell.svelte": 1,
"apps/mana/apps/web/src/lib/components/feedback/FeedbackQuickModal.svelte": 6,
"apps/mana/apps/web/src/lib/components/feedback/GlobalFeedbackPill.svelte": 1,
"apps/mana/apps/web/src/lib/components/KeyboardShortcutsModal.svelte": 1,
"apps/mana/apps/web/src/lib/components/landing/LandingEditor.svelte": 16,
"apps/mana/apps/web/src/lib/components/layout/SpaceCreateDialog.svelte": 4,
@ -219,16 +221,6 @@
"apps/mana/apps/web/src/lib/modules/todo/views/DetailView.svelte": 8,
"apps/mana/apps/web/src/lib/modules/uload/ListView.svelte": 2,
"apps/mana/apps/web/src/lib/modules/uload/views/DetailView.svelte": 7,
"apps/mana/apps/web/src/lib/modules/wardrobe/components/GarmentForm.svelte": 4,
"apps/mana/apps/web/src/lib/modules/wardrobe/components/GarmentTryOnButton.svelte": 2,
"apps/mana/apps/web/src/lib/modules/wardrobe/components/OutfitComposer.svelte": 5,
"apps/mana/apps/web/src/lib/modules/wardrobe/components/TryOnButton.svelte": 1,
"apps/mana/apps/web/src/lib/modules/wardrobe/components/TryOnModelPicker.svelte": 1,
"apps/mana/apps/web/src/lib/modules/wardrobe/ListView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/wardrobe/views/DetailGarmentView.svelte": 13,
"apps/mana/apps/web/src/lib/modules/wardrobe/views/DetailOutfitView.svelte": 5,
"apps/mana/apps/web/src/lib/modules/wardrobe/views/GridView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/wardrobe/views/OutfitsView.svelte": 3,
"apps/mana/apps/web/src/lib/modules/website/components/BlockInspector.svelte": 3,
"apps/mana/apps/web/src/lib/modules/website/components/DomainsSection.svelte": 4,
"apps/mana/apps/web/src/lib/modules/website/components/GalleryInspector.svelte": 9,
@ -375,13 +367,16 @@
"apps/mana/apps/web/src/routes/(app)/uload/links/+page.svelte": 7,
"apps/mana/apps/web/src/routes/(app)/uload/settings/+page.svelte": 6,
"apps/mana/apps/web/src/routes/(app)/uload/tags/+page.svelte": 4,
"apps/mana/apps/web/src/routes/(app)/wardrobe/compose/[[outfitId]]/+page.svelte": 4,
"apps/mana/apps/web/src/routes/(app)/wetter/+page.svelte": 2,
"apps/mana/apps/web/src/routes/(auth)/reset-password/+page.svelte": 1,
"apps/mana/apps/web/src/routes/+error.svelte": 1,
"apps/mana/apps/web/src/routes/accept-invitation/+page.svelte": 7,
"apps/mana/apps/web/src/routes/auth/callback/+page.svelte": 3,
"apps/mana/apps/web/src/routes/auth/reset-password/+page.svelte": 1,
"apps/mana/apps/web/src/routes/community/+layout.svelte": 5,
"apps/mana/apps/web/src/routes/community/+page.svelte": 1,
"apps/mana/apps/web/src/routes/community/admin/+page.svelte": 6,
"apps/mana/apps/web/src/routes/community/roadmap/+page.svelte": 1,
"apps/mana/apps/web/src/routes/g/[code]/+page.svelte": 6,
"apps/mana/apps/web/src/routes/rsvp/[token]/+page.svelte": 1,
"apps/mana/apps/web/src/routes/share/[token]/+layout.svelte": 1,

View file

@ -1,37 +1,55 @@
{
"apps/mana/apps/web/src/lib/components/dashboard/widgets/TasksTodayWidget.svelte": 1,
"apps/mana/apps/web/src/lib/components/dashboard/widgets/TransactionsWidget.svelte": 1,
"apps/mana/apps/web/src/lib/components/OfflineIndicator.svelte": 3,
"apps/mana/apps/web/src/lib/components/PwaUpdatePrompt.svelte": 3,
"apps/mana/apps/web/src/lib/modules/period/ListView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/plants/ListView.svelte": 5,
"apps/mana/apps/web/src/lib/modules/quotes/components/QuoteCard.svelte": 4,
"apps/mana/apps/web/src/lib/modules/times/components/EntryForm.svelte": 6,
"apps/mana/apps/web/src/lib/modules/times/components/EntryItem.svelte": 6,
"apps/mana/apps/web/src/lib/modules/times/components/EntryList.svelte": 2,
"apps/mana/apps/web/src/lib/modules/times/components/TimerCard.svelte": 6,
"apps/mana/apps/web/src/lib/modules/times/components/TimerIndicator.svelte": 2,
"apps/mana/apps/web/src/routes/(app)/citycorners/+page.svelte": 10,
"apps/mana/apps/web/src/routes/(app)/citycorners/add-city/+page.svelte": 21,
"apps/mana/apps/web/src/routes/(app)/citycorners/cities/[slug]/+page.svelte": 10,
"apps/mana/apps/web/src/routes/(app)/citycorners/cities/[slug]/add/+page.svelte": 27,
"apps/mana/apps/web/src/routes/(app)/citycorners/cities/[slug]/locations/[id]/+page.svelte": 22,
"apps/mana/apps/web/src/routes/(app)/citycorners/cities/[slug]/locations/[id]/edit/+page.svelte": 26,
"apps/mana/apps/web/src/routes/(app)/citycorners/cities/[slug]/map/+page.svelte": 6,
"apps/mana/apps/web/src/routes/(app)/citycorners/favorites/+page.svelte": 6,
"apps/mana/apps/web/src/routes/(app)/quotes/+page.svelte": 3,
"apps/mana/apps/web/src/routes/(app)/quotes/categories/+page.svelte": 2,
"apps/mana/apps/web/src/routes/(app)/quotes/category/[category]/+page.svelte": 8,
"apps/mana/apps/web/src/routes/(app)/quotes/favorites/+page.svelte": 8,
"apps/mana/apps/web/src/routes/(app)/quotes/lists/[id]/+page.svelte": 31,
"apps/mana/apps/web/src/routes/(app)/quotes/lists/+page.svelte": 16,
"apps/mana/apps/web/src/routes/(app)/times/+page.svelte": 4,
"apps/mana/apps/web/src/routes/(app)/times/clients/[id]/+page.svelte": 6,
"apps/mana/apps/web/src/routes/(app)/times/clients/+page.svelte": 12,
"apps/mana/apps/web/src/routes/(app)/times/clock/alarms/+page.svelte": 8,
"apps/mana/apps/web/src/routes/(app)/times/entries/+page.svelte": 6,
"apps/mana/apps/web/src/routes/(app)/times/projects/[id]/+page.svelte": 12,
"apps/mana/apps/web/src/routes/(app)/times/projects/+page.svelte": 11,
"apps/mana/apps/web/src/routes/(app)/times/reports/+page.svelte": 12,
"apps/mana/apps/web/src/routes/(app)/times/templates/+page.svelte": 8
"apps/mana/apps/web/src/lib/components/dashboard/widgets/TasksTodayWidget.svelte": 1,
"apps/mana/apps/web/src/lib/components/dashboard/widgets/TransactionsWidget.svelte": 1,
"apps/mana/apps/web/src/lib/components/OfflineIndicator.svelte": 3,
"apps/mana/apps/web/src/lib/components/PwaUpdatePrompt.svelte": 3,
"apps/mana/apps/web/src/lib/modules/invoices/components/StatusBadge.svelte": 1,
"apps/mana/apps/web/src/lib/modules/invoices/constants.ts": 1,
"apps/mana/apps/web/src/lib/modules/invoices/ListView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/invoices/views/DetailView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/period/ListView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/plants/ListView.svelte": 5,
"apps/mana/apps/web/src/lib/modules/quotes/components/QuoteCard.svelte": 4,
"apps/mana/apps/web/src/lib/modules/times/components/EntryForm.svelte": 6,
"apps/mana/apps/web/src/lib/modules/times/components/EntryItem.svelte": 6,
"apps/mana/apps/web/src/lib/modules/times/components/EntryList.svelte": 2,
"apps/mana/apps/web/src/lib/modules/times/components/TimerCard.svelte": 6,
"apps/mana/apps/web/src/lib/modules/times/components/TimerIndicator.svelte": 2,
"apps/mana/apps/web/src/lib/modules/wardrobe/components/CategoryTabs.svelte": 1,
"apps/mana/apps/web/src/lib/modules/wardrobe/components/GarmentCard.svelte": 1,
"apps/mana/apps/web/src/lib/modules/wardrobe/components/GarmentForm.svelte": 1,
"apps/mana/apps/web/src/lib/modules/wardrobe/components/OutfitCard.svelte": 1,
"apps/mana/apps/web/src/lib/modules/wardrobe/components/OutfitComposer.svelte": 3,
"apps/mana/apps/web/src/lib/modules/wardrobe/views/DetailGarmentView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/wardrobe/views/DetailOutfitView.svelte": 3,
"apps/mana/apps/web/src/lib/modules/wardrobe/views/GridView.svelte": 2,
"apps/mana/apps/web/src/lib/modules/writing/components/BriefingForm.svelte": 2,
"apps/mana/apps/web/src/lib/modules/writing/components/ReferencePicker.svelte": 1,
"apps/mana/apps/web/src/lib/modules/writing/views/DetailView.svelte": 2,
"apps/mana/apps/web/src/lib/modules/writing/views/ListView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/writing/views/StylesView.svelte": 1,
"apps/mana/apps/web/src/routes/(app)/citycorners/+page.svelte": 10,
"apps/mana/apps/web/src/routes/(app)/citycorners/add-city/+page.svelte": 21,
"apps/mana/apps/web/src/routes/(app)/citycorners/cities/[slug]/+page.svelte": 10,
"apps/mana/apps/web/src/routes/(app)/citycorners/cities/[slug]/add/+page.svelte": 27,
"apps/mana/apps/web/src/routes/(app)/citycorners/cities/[slug]/locations/[id]/+page.svelte": 22,
"apps/mana/apps/web/src/routes/(app)/citycorners/cities/[slug]/locations/[id]/edit/+page.svelte": 26,
"apps/mana/apps/web/src/routes/(app)/citycorners/cities/[slug]/map/+page.svelte": 6,
"apps/mana/apps/web/src/routes/(app)/citycorners/favorites/+page.svelte": 6,
"apps/mana/apps/web/src/routes/(app)/invoices/[id]/edit/+page.svelte": 1,
"apps/mana/apps/web/src/routes/(app)/quotes/+page.svelte": 3,
"apps/mana/apps/web/src/routes/(app)/quotes/categories/+page.svelte": 2,
"apps/mana/apps/web/src/routes/(app)/quotes/category/[category]/+page.svelte": 8,
"apps/mana/apps/web/src/routes/(app)/quotes/favorites/+page.svelte": 8,
"apps/mana/apps/web/src/routes/(app)/quotes/lists/[id]/+page.svelte": 31,
"apps/mana/apps/web/src/routes/(app)/quotes/lists/+page.svelte": 16,
"apps/mana/apps/web/src/routes/(app)/times/+page.svelte": 4,
"apps/mana/apps/web/src/routes/(app)/times/clients/[id]/+page.svelte": 6,
"apps/mana/apps/web/src/routes/(app)/times/clients/+page.svelte": 12,
"apps/mana/apps/web/src/routes/(app)/times/clock/alarms/+page.svelte": 8,
"apps/mana/apps/web/src/routes/(app)/times/entries/+page.svelte": 6,
"apps/mana/apps/web/src/routes/(app)/times/projects/[id]/+page.svelte": 12,
"apps/mana/apps/web/src/routes/(app)/times/projects/+page.svelte": 11,
"apps/mana/apps/web/src/routes/(app)/times/reports/+page.svelte": 12,
"apps/mana/apps/web/src/routes/(app)/times/templates/+page.svelte": 8
}