1.0.2: Add multilingual support, redesign share view, and iCloud sync improvements

- Add localisations for German, French, Italian, and Brazilian Portuguese
- Redesign WeekPlanShareView with richer layout and sharing options
- Improve HomeView with various UI enhancements
- Update AppSettings and Tag models for new features
- Minor iCloud sync and Settings fixes
- Add archive & upload script for App Store submissions

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
alexandrev-tibco
2026-03-07 19:02:33 +01:00
parent 63c9ba57de
commit c66132f494
15 changed files with 1946 additions and 109 deletions
+16 -4
View File
@@ -122,6 +122,7 @@
89C47DD7B8F4B1441780E611 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
8CFA001E1BFA916ABC4C44C1 /* PremiumUpsellBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumUpsellBanner.swift; sourceTree = "<group>"; };
8EE10285FEE9BE0684CC66A3 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
93B4B31A8F6E4D7AAB1B6A01 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
965555A7774FF7EB975F0A07 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
9F86EB5DDB126D73486FD956 /* MealMood.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MealMood.entitlements; sourceTree = "<group>"; };
9FBA11C8A81D5C0CA8A3C13D /* MealWindowsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealWindowsStepView.swift; sourceTree = "<group>"; };
@@ -141,6 +142,9 @@
D63CA2A8666E1FBBAD65F141 /* DishFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DishFormView.swift; sourceTree = "<group>"; };
DBD33453B7DF86EDBAEB80F1 /* DateHelpersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateHelpersTests.swift; sourceTree = "<group>"; };
DFA97AD958C922D8AFF73314 /* PrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryButton.swift; sourceTree = "<group>"; };
E18A2F6B56D64C1EBC91E219 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
E18A2F6B56D64C1EBC91E21A /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
E18A2F6B56D64C1EBC91E21B /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
E8472961AE65E2A3460B67C3 /* DishViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DishViewModel.swift; sourceTree = "<group>"; };
E8825212ACFA0D3F6C716E94 /* ReviewPromptService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewPromptService.swift; sourceTree = "<group>"; };
F0200C0C11C912711C724558 /* PremiumAccess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumAccess.swift; sourceTree = "<group>"; };
@@ -436,8 +440,12 @@
hasScannedForEncodings = 0;
knownRegions = (
Base,
de,
en,
es,
fr,
it,
"pt-BR",
);
mainGroup = 011822327F563ED89CB571FB;
minimizedProjectReferenceProxies = 1;
@@ -556,6 +564,10 @@
children = (
FF2DE77AD57DB6DAB188CD9E /* en */,
965555A7774FF7EB975F0A07 /* es */,
93B4B31A8F6E4D7AAB1B6A01 /* fr */,
E18A2F6B56D64C1EBC91E219 /* de */,
E18A2F6B56D64C1EBC91E21A /* it */,
E18A2F6B56D64C1EBC91E21B /* pt-BR */,
);
name = Localizable.strings;
sourceTree = "<group>";
@@ -643,7 +655,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = MealMood/Resources/MealMood.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 6;
DEVELOPMENT_TEAM = 2825Q76T7H;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = MealMood/Resources/Info.plist;
@@ -652,7 +664,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1;
MARKETING_VERSION = 1.0.2;
PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.mealmood;
PRODUCT_NAME = MealMood;
SDKROOT = iphoneos;
@@ -747,7 +759,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = MealMood/Resources/MealMood.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 6;
DEVELOPMENT_TEAM = 2825Q76T7H;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = MealMood/Resources/Info.plist;
@@ -756,7 +768,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1;
MARKETING_VERSION = 1.0.2;
PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.mealmood;
PRODUCT_NAME = MealMood;
SDKROOT = iphoneos;
+41 -1
View File
@@ -16,6 +16,7 @@ final class AppSettings {
var eventPrefix: String
var reminderMinutesBefore: Int?
var iCloudSyncEnabled: Bool?
var weekExportStyle: String?
var isPremium: Bool
var onboardingCompleted: Bool
@@ -41,6 +42,7 @@ final class AppSettings {
self.eventPrefix = "🍽️"
self.reminderMinutesBefore = nil
self.iCloudSyncEnabled = true
self.weekExportStyle = nil
self.isPremium = false
self.onboardingCompleted = false
}
@@ -64,6 +66,11 @@ final class AppSettings {
get { iCloudSyncEnabled ?? true }
set { iCloudSyncEnabled = newValue }
}
var weekExportStyleEnum: WeekExportStyle {
get { WeekExportStyle(rawValue: weekExportStyle ?? "") ?? .defaultStyle }
set { weekExportStyle = newValue.rawValue }
}
}
enum MealWindows: String, CaseIterable {
@@ -92,12 +99,20 @@ enum AppLanguage: String, CaseIterable {
case system
case spanish
case english
case french
case german
case italian
case portugueseBrazil
var displayName: String {
switch self {
case .system: return "System"
case .spanish: return "Español"
case .english: return "English"
case .french: return "Français"
case .german: return "Deutsch"
case .italian: return "Italiano"
case .portugueseBrazil: return "Português (Brasil)"
}
}
@@ -106,12 +121,23 @@ enum AppLanguage: String, CaseIterable {
case .system: return resolved().localeIdentifier
case .spanish: return "es"
case .english: return "en"
case .french: return "fr"
case .german: return "de"
case .italian: return "it"
case .portugueseBrazil: return "pt-BR"
}
}
func resolved() -> AppLanguage {
if self != .system { return self }
return Locale.current.identifier.lowercased().hasPrefix("es") ? .spanish : .english
let localeIdentifier = Locale.current.identifier.lowercased()
if localeIdentifier.hasPrefix("es") { return .spanish }
if localeIdentifier.hasPrefix("fr") { return .french }
if localeIdentifier.hasPrefix("de") { return .german }
if localeIdentifier.hasPrefix("it") { return .italian }
if localeIdentifier.hasPrefix("pt") { return .portugueseBrazil }
return .english
}
}
@@ -138,3 +164,17 @@ enum CalendarSyncMode: String, CaseIterable {
}
}
}
enum WeekExportStyle: String, CaseIterable {
case defaultStyle
case schoolTimetable
case vertical
var localizedKey: String {
switch self {
case .defaultStyle: return "share_export_style_default"
case .schoolTimetable: return "share_export_style_school"
case .vertical: return "share_export_style_vertical"
}
}
}
+3 -3
View File
@@ -41,8 +41,8 @@ final class Tag {
func localizedName(language: AppLanguage) -> String {
switch language.resolved() {
case .spanish: return name
case .english: return nameEN.isEmpty ? name : nameEN
case .system: return name
case .english, .french, .german, .italian, .portugueseBrazil: return nameEN.isEmpty ? name : nameEN
case .system: return nameEN.isEmpty ? name : nameEN
}
}
@@ -57,7 +57,7 @@ final class Tag {
switch resolvedLanguage {
case .spanish:
parts.append("Max \(max)/semana")
case .english:
case .english, .french, .german, .italian, .portugueseBrazil:
parts.append("Max \(max)/week")
case .system:
parts.append("Max \(max)/week")
@@ -0,0 +1,286 @@
/* Onboarding */
"onboarding_welcome_title" = "Organisiere deine Mahlzeiten in 1 Minute";
"onboarding_welcome_subtitle" = "Beantworte 4 kurze Schritte und plane noch heute los.";
"onboarding_welcome_step_1" = "Wähle, ob du Mittagessen, Abendessen oder beides planen möchtest";
"onboarding_welcome_step_2" = "Entscheide, ob Wochenenden eingeschlossen werden sollen";
"onboarding_welcome_step_3" = "Verbinde optional deinen Kalender";
"onboarding_welcome_step_4" = "Füge deine typischen Gerichte hinzu, um schnell zu starten";
"onboarding_nav_intro" = "Schnelleinrichtung";
"onboarding_nav_setup" = "Einrichtung";
"onboarding_nav_step_short" = "Schritt %d";
"onboarding_nav_step_counter" = "Schritt %d/%d";
"onboarding_start" = "Loslegen";
"onboarding_continue" = "Weiter";
"onboarding_skip" = "Überspringen";
"onboarding_finish" = "Fertig";
/* Meal Windows */
"meal_windows_title" = "Schritt 1: Mittagessen, Abendessen oder beides?";
"meal_windows_dinner_only" = "Nur Abendessen";
"meal_windows_lunch_only" = "Nur Mittagessen";
"meal_windows_both" = "Mittag- und Abendessen";
/* Weekends */
"weekends_title" = "Schritt 2: Auch Wochenenden planen?";
"weekends_toggle" = "Auch Samstage und Sonntage planen";
"weekends_note" = "Das kannst du später in den Einstellungen ändern";
/* Calendar */
"calendar_title" = "Schritt 3: Kalender verbinden?";
"calendar_sync" = "Synchronisieren";
"calendar_select" = "Kalender";
"calendar_lunch_time" = "Mittagszeit";
"calendar_dinner_time" = "Abendessenszeit";
"calendar_optional" = "Optional, du kannst es später aktivieren";
"calendar_permission_title" = "Kalenderzugriff";
"calendar_permission_message" = "MealMood benötigt Zugriff auf deinen Kalender, um Ereignisse zu synchronisieren.";
"calendar_permission_settings" = "Zu Einstellungen";
/* First Dishes */
"first_dishes_title" = "Schritt 4: Füge deine Lieblingsgerichte hinzu";
"first_dishes_subtitle" = "Füge mindestens 2 Gerichte hinzu, um zu starten";
"first_dishes_name_placeholder" = "Gerichtsname";
"first_dishes_add_tag" = "Tag hinzufügen";
"first_dishes_add_dish" = "Gericht hinzufügen";
"first_dishes_added" = "Hinzugefügte Gerichte:";
/* Home */
"home_title" = "MealMood";
"home_previous" = "Zurück";
"home_next" = "Weiter";
"home_complete" = "Abschließen";
"home_undo_last_action" = "Letzte Aktion rückgängig machen";
"home_reset" = "Zurücksetzen";
"home_copy_previous_week" = "Vorherige Woche kopieren";
"home_my_dishes" = "Meine Gerichte";
"home_add_first_dish" = "Füge dein erstes Gericht hinzu, um zu starten";
"home_add_dish" = "Gericht hinzufügen";
"home_my_dishes_search" = "Gerichte suchen";
"home_my_dishes_search_empty" = "Keine Gerichte gefunden";
"home_my_dishes_hide_used" = "Diese Woche verwendete ausblenden";
"home_pick_dish_title" = "Gericht auswählen";
"home_pick_dish_search" = "Gerichte suchen";
"home_pick_dish_empty" = "Keine Gerichte passen zu deiner Suche";
"home_pick_dish_add_new" = "Neues Gericht hinzufügen";
"home_pick_dish_no_dishes" = "Noch keine Gerichte. Füge eines hinzu, um zu starten.";
"home_select_week" = "Woche auswählen";
"home_week_picker_title" = "Woche auswählen";
"home_week_picker_date" = "Datum";
"home_week_picker_go" = "Los";
/* Reset Alert */
"reset_title" = "Die ganze Woche löschen?";
"reset_message" = "Dies kann nicht rückgängig gemacht werden";
"reset_cancel" = "Abbrechen";
"reset_confirm" = "Löschen";
/* Toasts */
"toast_dish_assigned" = "Gericht zugewiesen ✓";
"toast_week_complete" = "Woche abgeschlossen! 🎉";
"toast_week_reset" = "Woche gelöscht 🔄";
"toast_dish_saved" = "Gericht gespeichert ✓";
"toast_event_deleted" = "Kalendereintrag gelöscht";
"toast_cannot_complete" = "Nicht alle Plätze konnten gefüllt werden";
"toast_dish_deleted" = "Gericht gelöscht";
"toast_no_free_slots" = "Keine freien Plätze verfügbar";
"toast_calendar_synced" = "Kalender synchronisiert ✓";
"toast_copied_previous_week" = "Aus vorheriger Woche kopiert ✓";
"toast_previous_week_empty" = "Kein Plan für die vorherige Woche gefunden";
"toast_undo_applied" = "Letzte Aktion rückgängig gemacht";
"copy_previous_confirm_title" = "Aktuelle Woche ersetzen?";
"copy_previous_confirm_message" = "Es sind bereits Gerichte zugewiesen. Das Kopieren der vorherigen Woche überschreibt die aktuellen Zuweisungen.";
"copy_previous_confirm_confirm" = "Woche ersetzen";
/* Dish Form */
"dish_new_title" = "Neues Gericht";
"dish_edit_title" = "Gericht bearbeiten";
"dish_name_label" = "Gerichtsname *";
"dish_name_placeholder" = "Z. B.: Brathähnchen";
"dish_description_label" = "Beschreibung (optional)";
"dish_description_placeholder" = "Z. B.: Saftig und mit Kräutern im Ofen gebacken";
"dish_tags_label" = "Tags (optional)";
"dish_add_tag" = "Tag hinzufügen";
"dish_delete" = "Gericht löschen";
"dish_delete_title" = "Dieses Gericht löschen?";
"dish_delete_message" = "Diese Aktion kann nicht rückgängig gemacht werden";
"dish_cancel" = "Abbrechen";
"dish_delete_blocked_title" = "Löschen nicht möglich";
"dish_delete_blocked_message" = "Du kannst kein Gericht löschen, das der aktuellen Woche zugewiesen ist.";
"dish_delete_blocked_ok" = "OK";
"dish_list_empty" = "Du hast noch keine Gerichte";
/* Tag Selector */
"tag_selector_title" = "Tags auswählen";
"tag_selector_done" = "Fertig";
/* Tags */
"tags_title" = "Tags";
"tags_max_per_week" = "Max pro Woche";
"tags_no_consecutive" = "Nicht hintereinander";
"tags_no_consecutive_desc" = "Verhindert aufeinanderfolgende Tage";
"tags_no_duplicate" = "Keine Wiederholung am selben Tag";
"tags_no_duplicate_desc" = "Kann nicht mittags und abends am selben Tag erscheinen";
"tags_restriction" = "Zeitbeschränkung";
"tags_no_restriction" = "Keine Beschränkung";
"tags_lunch_only" = "Nur Mittagessen";
"tags_dinner_only" = "Nur Abendessen";
"tags_no_limit" = "Keine Begrenzung";
/* Settings */
"settings_title" = "Einstellungen";
"settings_planning" = "Planung";
"settings_meal_windows" = "Mahlzeitenfenster";
"settings_include_weekends" = "Wochenenden einschließen";
"settings_export_style" = "Wöchentlicher Exportstil";
"settings_calendar" = "Kalender";
"settings_icloud_sync" = "Daten mit iCloud synchronisieren";
"settings_sync" = "Synchronisieren";
"settings_sync_mode" = "Synchronisierungsmodus";
"settings_sync_mode_week_complete" = "Automatisch (wenn die Woche vollständig ist)";
"settings_sync_mode_manual" = "Manuell (Synchronisieren-Taste)";
"settings_sync_now" = "Jetzt synchronisieren";
"settings_lunch_time" = "Mittagszeit";
"settings_dinner_time" = "Abendessenszeit";
"settings_event_duration" = "Ereignisdauer";
"settings_event_prefix" = "Titelpräfix";
"settings_reminder" = "Erinnerung";
"settings_reminder_none" = "Keine Erinnerung";
"settings_tags" = "Tags";
"settings_manage_tags" = "Tags verwalten";
"settings_language" = "Sprache";
"settings_premium" = "Premium";
"settings_premium_status" = "Status";
"settings_remove_ads" = "Werbung entfernen";
"settings_about" = "Info";
"settings_website" = "Website";
"settings_support" = "Support-E-Mail";
"settings_rate_app" = "MealMood bewerten";
"settings_app_version" = "Version";
"settings_app_build" = "Build";
"settings_danger_zone" = "Gefahrenbereich";
"settings_reset_all_data" = "Alle Daten zurücksetzen";
"settings_reset_all_data_title" = "Alle App-Daten zurücksetzen?";
"settings_reset_all_data_message" = "Dadurch werden Gerichte, Tags, Pläne und Einstellungen gelöscht und das Onboarding neu gestartet.";
"settings_reset_all_data_confirm" = "Alles zurücksetzen";
/* Premium */
"premium_title" = "MealMood Premium";
"premium_subtitle" = "Nutze alle Funktionen";
"premium_no_ads" = "Keine Werbung";
"premium_unlimited_dishes" = "Unbegrenzte Gerichte";
"premium_custom_tags" = "Benutzerdefinierte Tags";
"premium_advanced_rules" = "Erweiterte Tag-Regeln";
"premium_future_weeks" = "Unbegrenzt zukünftige Wochen planen";
"premium_share_week" = "Wöchentlicher Bildexport";
"premium_family_sharing" = "Unterstützt Familienfreigabe";
"premium_plans" = "Verfügbare Pläne:";
"premium_monthly" = "Monatlich";
"premium_yearly" = "Jährlich";
"premium_save" = "44 % sparen";
"premium_month" = "Monat";
"premium_price_note" = "2,99 EUR pro Monat";
"premium_terms_note" = "Automatisch verlängerbares Abo. Jederzeit in deinen Apple-Kontoeinstellungen kündbar.";
"premium_active" = "Premium ist aktiv";
"premium_subscribe" = "Abonnieren";
"premium_restore" = "Käufe wiederherstellen";
"premium_processing" = "Wird verarbeitet...";
"premium_loading_products" = "Kaufoptionen werden geladen...";
"premium_loading_products_hint" = "Wenn es zu lange dauert, tippe auf Erneut versuchen.";
"premium_loading_timeout" = "Laden hat zu lange gedauert";
"premium_loading_timeout_hint" = "StoreKit hat nicht rechtzeitig geantwortet. Tippe auf Erneut versuchen.";
"premium_products_not_found" = "Keine Produkte verfügbar";
"premium_products_not_found_hint" = "Prüfe die Produkt-IDs in App Store Connect / der StoreKit-Konfiguration.";
"premium_loading_failed" = "Produkte konnten nicht geladen werden";
"premium_loading_failed_hint" = "Bitte prüfe deine StoreKit-Konfiguration und versuche es erneut.";
"premium_retry_products" = "Erneut versuchen";
"premium_purchase_pending" = "Der Kauf wartet auf Genehmigung.";
"premium_purchase_cancelled" = "Der Kauf wurde abgebrochen.";
"premium_purchase_failed" = "Der Kauf ist fehlgeschlagen. Bitte versuche es erneut.";
"tag_selector_empty" = "Noch keine Tags verfügbar";
"premium_limit_dishes" = "Limit des Gratisplans erreicht: 20 Gerichte";
"premium_limit_rules" = "Erweiterte Tag-Regeln sind Premium";
"premium_limit_future_weeks" = "Der Gratisplan erlaubt Planung bis zur nächsten Woche";
/* Home extras */
"rule_override_title" = "Regelkonflikt";
"rule_override_message" = "Dieses Gericht verletzt eine oder mehrere Tag-Regeln. Trotzdem zuweisen?";
"rule_override_confirm" = "Trotzdem zuweisen";
"share_week_button" = "Woche teilen";
"share_week_title" = "MealMood Wochenplan";
"share_week_callout_title" = "Deine Woche ist bereit zum Export";
"share_week_callout_subtitle" = "Erstelle ein schönes Bild zum Teilen oder Drucken deines Essensplans.";
"share_export_style_default" = "Standard";
"share_export_style_school" = "Stundenplan";
"share_export_style_vertical" = "Vertikal";
"share_export_style_picker_title" = "Exportstil wählen";
"share_export_style_picker_subtitle" = "Du kannst ihn jederzeit in den Einstellungen ändern.";
"share_export_style_picker_apply" = "Diesen Stil verwenden";
"share_export_school_title" = "Wöchentlicher Essensplan";
"share_export_school_subtitle" = "Stundenplan";
"share_export_vertical_title" = "Vertikaler Essensplan";
"ads_placeholder" = "Anzeige";
"ads_loading" = "Anzeige wird geladen...";
"ads_unavailable" = "Anzeige nicht verfügbar";
"history_title" = "Monatlicher Verlauf";
"history_month_empty" = "In diesem Monat gibt es keine geplanten Wochen";
"history_week_complete" = "Woche abgeschlossen";
"history_week_incomplete" = "Woche in Arbeit";
/* Onboarding suggestions */
"first_dishes_suggestions" = "Schnelle Vorschläge";
"first_dishes_suggestion_add" = "Vorschlag hinzufügen";
"first_dishes_suggestion_added" = "Hinzugefügt";
"first_dishes_no_tags" = "Standard-Tags werden geladen, bitte versuche es gleich noch einmal";
/* Onboarding trial */
"onboarding_trial_title" = "Premium freischalten";
"onboarding_trial_subtitle" = "Schalte jetzt alle Premium-Funktionen frei.";
"onboarding_trial_cta" = "Premium freischalten";
"onboarding_trial_skip" = "Mit Gratisplan fortfahren";
"onboarding_trial_price_format" = "%@ / Monat";
/* Review funnel */
"review_funnel_title" = "Wie gefällt dir MealMood?";
"review_funnel_message" = "Deine Meinung hilft uns, MealMood zu verbessern.";
"review_funnel_positive" = "Gefällt mir";
"review_funnel_negative" = "Verbesserungsbedarf";
"review_feedback_title" = "Sag uns, was wir verbessern sollen";
"review_feedback_message" = "Sende uns dein Feedback, damit wir die App verbessern können.";
"review_feedback_contact" = "Feedback senden";
"onboarding_auto_assign_title" = "Sollen wir deine Woche automatisch füllen?";
"onboarding_auto_assign_message" = "Wir können diese Woche automatisch mit deinen Gerichten und Regeln füllen.";
"onboarding_auto_assign_yes" = "Ja, automatisch zuweisen";
"onboarding_auto_assign_no" = "Nein, ich mache es";
"onboarding_premium_prompt_title" = "Premium freischalten?";
"onboarding_premium_prompt_message" = "Keine Werbung, unbegrenzte Gerichte, erweiterte Regeln und Planung zukünftiger Wochen.";
"onboarding_premium_prompt_cta" = "Premium ansehen";
/* Notifications */
"notification_planning_title" = "Plane deine nächste Woche";
"notification_planning_body" = "Deine neue Woche beginnt morgen und ist noch nicht geplant.";
/* Meal Types */
"lunch" = "Mittagessen";
"dinner" = "Abendessen";
/* Days */
"day_mon" = "Mo";
"day_tue" = "Di";
"day_wed" = "Mi";
"day_thu" = "Do";
"day_fri" = "Fr";
"day_sat" = "Sa";
"day_sun" = "So";
/* Durations */
"duration_30" = "30 Min.";
"duration_60" = "1 Stunde";
"duration_90" = "1,5 Stunden";
"duration_120" = "2 Stunden";
/* Reminders */
"reminder_30" = "30 Min. vorher";
"reminder_60" = "1 Stunde vorher";
"reminder_120" = "2 Stunden vorher";
"first_dishes_limit_reached" = "Du hast 10 Gerichte erreicht. Tippe auf Fertig, wenn du bereit bist.";
@@ -131,6 +131,7 @@
"settings_planning" = "Planning";
"settings_meal_windows" = "Meal windows";
"settings_include_weekends" = "Include weekends";
"settings_export_style" = "Weekly export style";
"settings_calendar" = "Calendar";
"settings_icloud_sync" = "Sync data with iCloud";
"settings_sync" = "Sync";
@@ -208,6 +209,15 @@
"share_week_title" = "MealMood weekly plan";
"share_week_callout_title" = "Your week is ready to export";
"share_week_callout_subtitle" = "Create a beautiful image to share or print your meal plan.";
"share_export_style_default" = "Default";
"share_export_style_school" = "School Timetable";
"share_export_style_vertical" = "Vertical";
"share_export_style_picker_title" = "Choose export style";
"share_export_style_picker_subtitle" = "You can change it anytime in Settings.";
"share_export_style_picker_apply" = "Use this style";
"share_export_school_title" = "Weekly Meal Plan";
"share_export_school_subtitle" = "School Timetable";
"share_export_vertical_title" = "Vertical Meal Plan";
"ads_placeholder" = "Ad";
"ads_loading" = "Loading ad...";
"ads_unavailable" = "Ad unavailable";
@@ -131,6 +131,7 @@
"settings_planning" = "Planificación";
"settings_meal_windows" = "Ventanas de comida";
"settings_include_weekends" = "Incluir fines de semana";
"settings_export_style" = "Estilo de exportación semanal";
"settings_calendar" = "Calendario";
"settings_icloud_sync" = "Sincronizar datos con iCloud";
"settings_sync" = "Sincronizar";
@@ -208,6 +209,15 @@
"share_week_title" = "Plan semanal MealMood";
"share_week_callout_title" = "Tu semana está lista para exportar";
"share_week_callout_subtitle" = "Genera una imagen bonita para compartir o imprimir tu planificación.";
"share_export_style_default" = "Default";
"share_export_style_school" = "School Timetable";
"share_export_style_vertical" = "Vertical";
"share_export_style_picker_title" = "Elige estilo de exportación";
"share_export_style_picker_subtitle" = "Puedes cambiarlo cuando quieras en Ajustes.";
"share_export_style_picker_apply" = "Usar este estilo";
"share_export_school_title" = "Weekly Meal Plan";
"share_export_school_subtitle" = "School Timetable";
"share_export_vertical_title" = "Meal Plan Vertical";
"ads_placeholder" = "Publicidad";
"ads_loading" = "Cargando anuncio...";
"ads_unavailable" = "Anuncio no disponible";
@@ -0,0 +1,286 @@
/* Onboarding */
"onboarding_welcome_title" = "Organisez vos repas en 1 minute";
"onboarding_welcome_subtitle" = "Répondez à 4 étapes rapides et commencez à planifier dès aujourd'hui.";
"onboarding_welcome_step_1" = "Choisissez si vous voulez planifier les déjeuners, les dîners ou les deux";
"onboarding_welcome_step_2" = "Décidez si vous voulez inclure les week-ends";
"onboarding_welcome_step_3" = "Connectez éventuellement votre calendrier";
"onboarding_welcome_step_4" = "Ajoutez vos plats habituels pour démarrer vite";
"onboarding_nav_intro" = "Configuration rapide";
"onboarding_nav_setup" = "Configuration";
"onboarding_nav_step_short" = "Étape %d";
"onboarding_nav_step_counter" = "Étape %d/%d";
"onboarding_start" = "Commencer";
"onboarding_continue" = "Continuer";
"onboarding_skip" = "Ignorer";
"onboarding_finish" = "Terminer";
/* Meal Windows */
"meal_windows_title" = "Étape 1 : Déjeuners, dîners ou les deux ?";
"meal_windows_dinner_only" = "Dîners seulement";
"meal_windows_lunch_only" = "Déjeuners seulement";
"meal_windows_both" = "Déjeuners et dîners";
/* Weekends */
"weekends_title" = "Étape 2 : Planifier aussi les week-ends ?";
"weekends_toggle" = "Planifier aussi les samedis et dimanches";
"weekends_note" = "Vous pourrez le modifier plus tard dans les réglages";
/* Calendar */
"calendar_title" = "Étape 3 : Connecter votre calendrier ?";
"calendar_sync" = "Synchroniser";
"calendar_select" = "Calendrier";
"calendar_lunch_time" = "Heure du déjeuner";
"calendar_dinner_time" = "Heure du dîner";
"calendar_optional" = "Facultatif, vous pourrez l'activer plus tard";
"calendar_permission_title" = "Accès au calendrier";
"calendar_permission_message" = "MealMood a besoin d'accéder à votre calendrier pour synchroniser les événements.";
"calendar_permission_settings" = "Aller aux réglages";
/* First Dishes */
"first_dishes_title" = "Étape 4 : Ajoutez vos plats favoris";
"first_dishes_subtitle" = "Ajoutez au moins 2 plats pour commencer";
"first_dishes_name_placeholder" = "Nom du plat";
"first_dishes_add_tag" = "Ajouter un tag";
"first_dishes_add_dish" = "Ajouter un plat";
"first_dishes_added" = "Plats ajoutés :";
/* Home */
"home_title" = "MealMood";
"home_previous" = "Précédent";
"home_next" = "Suivant";
"home_complete" = "Terminer";
"home_undo_last_action" = "Annuler la dernière action";
"home_reset" = "Réinitialiser";
"home_copy_previous_week" = "Copier la semaine précédente";
"home_my_dishes" = "Mes plats";
"home_add_first_dish" = "Ajoutez votre premier plat pour commencer";
"home_add_dish" = "Ajouter un plat";
"home_my_dishes_search" = "Rechercher des plats";
"home_my_dishes_search_empty" = "Aucun plat trouvé";
"home_my_dishes_hide_used" = "Masquer ceux utilisés cette semaine";
"home_pick_dish_title" = "Choisir un plat";
"home_pick_dish_search" = "Rechercher des plats";
"home_pick_dish_empty" = "Aucun plat ne correspond à votre recherche";
"home_pick_dish_add_new" = "Ajouter un nouveau plat";
"home_pick_dish_no_dishes" = "Aucun plat pour le moment. Ajoutez-en un pour commencer.";
"home_select_week" = "Choisir une semaine";
"home_week_picker_title" = "Choisir une semaine";
"home_week_picker_date" = "Date";
"home_week_picker_go" = "Aller";
/* Reset Alert */
"reset_title" = "Effacer toute la semaine ?";
"reset_message" = "Cette action est irréversible";
"reset_cancel" = "Annuler";
"reset_confirm" = "Effacer";
/* Toasts */
"toast_dish_assigned" = "Plat assigné ✓";
"toast_week_complete" = "Semaine terminée ! 🎉";
"toast_week_reset" = "Semaine effacée 🔄";
"toast_dish_saved" = "Plat enregistré ✓";
"toast_event_deleted" = "Événement du calendrier supprimé";
"toast_cannot_complete" = "Impossible de remplir tous les créneaux";
"toast_dish_deleted" = "Plat supprimé";
"toast_no_free_slots" = "Aucun créneau libre disponible";
"toast_calendar_synced" = "Calendrier synchronisé ✓";
"toast_copied_previous_week" = "Copié depuis la semaine précédente ✓";
"toast_previous_week_empty" = "Aucun plan trouvé pour la semaine précédente";
"toast_undo_applied" = "Dernière action annulée";
"copy_previous_confirm_title" = "Remplacer la semaine actuelle ?";
"copy_previous_confirm_message" = "Vous avez déjà des plats assignés. Copier la semaine précédente remplacera les assignations actuelles.";
"copy_previous_confirm_confirm" = "Remplacer la semaine";
/* Dish Form */
"dish_new_title" = "Nouveau plat";
"dish_edit_title" = "Modifier le plat";
"dish_name_label" = "Nom du plat *";
"dish_name_placeholder" = "Ex. : Poulet rôti";
"dish_description_label" = "Description (facultative)";
"dish_description_placeholder" = "Ex. : Savoureux et rôti au four avec des herbes";
"dish_tags_label" = "Tags (facultatif)";
"dish_add_tag" = "Ajouter un tag";
"dish_delete" = "Supprimer le plat";
"dish_delete_title" = "Supprimer ce plat ?";
"dish_delete_message" = "Cette action est irréversible";
"dish_cancel" = "Annuler";
"dish_delete_blocked_title" = "Suppression impossible";
"dish_delete_blocked_message" = "Vous ne pouvez pas supprimer un plat assigné à la semaine en cours.";
"dish_delete_blocked_ok" = "OK";
"dish_list_empty" = "Vous n'avez pas encore de plats";
/* Tag Selector */
"tag_selector_title" = "Sélectionner des tags";
"tag_selector_done" = "Terminé";
/* Tags */
"tags_title" = "Tags";
"tags_max_per_week" = "Max par semaine";
"tags_no_consecutive" = "Pas consécutif";
"tags_no_consecutive_desc" = "Empêche l'apparition sur des jours consécutifs";
"tags_no_duplicate" = "Pas de répétition le même jour";
"tags_no_duplicate_desc" = "Impossible au déjeuner et au dîner le même jour";
"tags_restriction" = "Restriction horaire";
"tags_no_restriction" = "Aucune restriction";
"tags_lunch_only" = "Déjeuner seulement";
"tags_dinner_only" = "Dîner seulement";
"tags_no_limit" = "Aucune limite";
/* Settings */
"settings_title" = "Réglages";
"settings_planning" = "Planification";
"settings_meal_windows" = "Créneaux de repas";
"settings_include_weekends" = "Inclure les week-ends";
"settings_export_style" = "Style d'export hebdomadaire";
"settings_calendar" = "Calendrier";
"settings_icloud_sync" = "Synchroniser les données avec iCloud";
"settings_sync" = "Synchroniser";
"settings_sync_mode" = "Mode de synchronisation";
"settings_sync_mode_week_complete" = "Auto (quand la semaine est complète)";
"settings_sync_mode_manual" = "Manuel (bouton de synchro)";
"settings_sync_now" = "Synchroniser maintenant";
"settings_lunch_time" = "Heure du déjeuner";
"settings_dinner_time" = "Heure du dîner";
"settings_event_duration" = "Durée de l'événement";
"settings_event_prefix" = "Préfixe du titre";
"settings_reminder" = "Rappel";
"settings_reminder_none" = "Aucun rappel";
"settings_tags" = "Tags";
"settings_manage_tags" = "Gérer les tags";
"settings_language" = "Langue";
"settings_premium" = "Premium";
"settings_premium_status" = "Statut";
"settings_remove_ads" = "Supprimer les publicités";
"settings_about" = "À propos";
"settings_website" = "Site web";
"settings_support" = "E-mail d'assistance";
"settings_rate_app" = "Noter MealMood";
"settings_app_version" = "Version";
"settings_app_build" = "Build";
"settings_danger_zone" = "Zone dangereuse";
"settings_reset_all_data" = "Réinitialiser toutes les données";
"settings_reset_all_data_title" = "Réinitialiser toutes les données de l'app ?";
"settings_reset_all_data_message" = "Cela supprimera les plats, tags, plans et réglages, puis relancera l'onboarding.";
"settings_reset_all_data_confirm" = "Tout réinitialiser";
/* Premium */
"premium_title" = "MealMood Premium";
"premium_subtitle" = "Profitez de toutes les fonctionnalités";
"premium_no_ads" = "Sans publicités";
"premium_unlimited_dishes" = "Plats illimités";
"premium_custom_tags" = "Tags personnalisés";
"premium_advanced_rules" = "Règles de tags avancées";
"premium_future_weeks" = "Planification illimitée des semaines à venir";
"premium_share_week" = "Export hebdomadaire en image";
"premium_family_sharing" = "Compatible avec le partage familial";
"premium_plans" = "Forfaits disponibles :";
"premium_monthly" = "Mensuel";
"premium_yearly" = "Annuel";
"premium_save" = "Économisez 44 %";
"premium_month" = "mois";
"premium_price_note" = "2,99 EUR par mois";
"premium_terms_note" = "Abonnement à renouvellement automatique. Annulez à tout moment dans les réglages de votre compte Apple.";
"premium_active" = "Premium est actif";
"premium_subscribe" = "S'abonner";
"premium_restore" = "Restaurer les achats";
"premium_processing" = "Traitement...";
"premium_loading_products" = "Chargement des options d'achat...";
"premium_loading_products_hint" = "Si cela prend trop de temps, touchez Réessayer.";
"premium_loading_timeout" = "Temps de chargement écoulé";
"premium_loading_timeout_hint" = "StoreKit n'a pas répondu à temps. Touchez Réessayer.";
"premium_products_not_found" = "Aucun produit disponible";
"premium_products_not_found_hint" = "Vérifiez les identifiants produit dans App Store Connect / la configuration StoreKit.";
"premium_loading_failed" = "Impossible de charger les produits";
"premium_loading_failed_hint" = "Vérifiez votre configuration StoreKit et réessayez.";
"premium_retry_products" = "Réessayer";
"premium_purchase_pending" = "L'achat est en attente d'approbation.";
"premium_purchase_cancelled" = "L'achat a été annulé.";
"premium_purchase_failed" = "L'achat a échoué. Veuillez réessayer.";
"tag_selector_empty" = "Aucun tag disponible pour le moment";
"premium_limit_dishes" = "Limite du plan gratuit atteinte : 20 plats";
"premium_limit_rules" = "Les règles de tags avancées sont réservées au Premium";
"premium_limit_future_weeks" = "Le plan gratuit permet de planifier jusqu'à la semaine prochaine";
/* Home extras */
"rule_override_title" = "Conflit de règles";
"rule_override_message" = "Ce plat enfreint une ou plusieurs règles de tags. L'assigner quand même ?";
"rule_override_confirm" = "Assigner quand même";
"share_week_button" = "Partager la semaine";
"share_week_title" = "Planning hebdomadaire MealMood";
"share_week_callout_title" = "Votre semaine est prête à être exportée";
"share_week_callout_subtitle" = "Créez une belle image à partager ou à imprimer avec votre planning de repas.";
"share_export_style_default" = "Par défaut";
"share_export_style_school" = "Emploi du temps scolaire";
"share_export_style_vertical" = "Vertical";
"share_export_style_picker_title" = "Choisir le style d'export";
"share_export_style_picker_subtitle" = "Vous pouvez le modifier à tout moment dans les réglages.";
"share_export_style_picker_apply" = "Utiliser ce style";
"share_export_school_title" = "Planning hebdomadaire des repas";
"share_export_school_subtitle" = "Emploi du temps scolaire";
"share_export_vertical_title" = "Planning vertical des repas";
"ads_placeholder" = "Publicité";
"ads_loading" = "Chargement de la publicité...";
"ads_unavailable" = "Publicité indisponible";
"history_title" = "Historique mensuel";
"history_month_empty" = "Aucune semaine planifiée ce mois-ci";
"history_week_complete" = "Semaine terminée";
"history_week_incomplete" = "Semaine en cours";
/* Onboarding suggestions */
"first_dishes_suggestions" = "Suggestions rapides";
"first_dishes_suggestion_add" = "Ajouter la suggestion";
"first_dishes_suggestion_added" = "Ajouté";
"first_dishes_no_tags" = "Les tags par défaut sont en cours de chargement, veuillez réessayer dans un instant";
/* Onboarding trial */
"onboarding_trial_title" = "Débloquer Premium";
"onboarding_trial_subtitle" = "Débloquez maintenant toutes les fonctionnalités premium.";
"onboarding_trial_cta" = "Débloquer Premium";
"onboarding_trial_skip" = "Continuer avec le plan gratuit";
"onboarding_trial_price_format" = "%@ / mois";
/* Review funnel */
"review_funnel_title" = "Comment se passe votre expérience avec MealMood ?";
"review_funnel_message" = "Votre avis nous aide à améliorer MealMood.";
"review_funnel_positive" = "J'aime";
"review_funnel_negative" = "À améliorer";
"review_feedback_title" = "Dites-nous quoi améliorer";
"review_feedback_message" = "Envoyez-nous votre retour et nous l'utiliserons pour améliorer l'app.";
"review_feedback_contact" = "Envoyer un retour";
"onboarding_auto_assign_title" = "Voulez-vous que nous remplissions automatiquement votre semaine ?";
"onboarding_auto_assign_message" = "Nous pouvons remplir cette semaine automatiquement en utilisant vos plats et vos règles.";
"onboarding_auto_assign_yes" = "Oui, assigner automatiquement";
"onboarding_auto_assign_no" = "Non, je le ferai";
"onboarding_premium_prompt_title" = "Voulez-vous débloquer Premium ?";
"onboarding_premium_prompt_message" = "Profitez de l'absence de publicités, de plats illimités, de règles avancées et de la planification des semaines futures.";
"onboarding_premium_prompt_cta" = "Voir Premium";
/* Notifications */
"notification_planning_title" = "Planifiez votre semaine suivante";
"notification_planning_body" = "Votre nouvelle semaine commence demain et elle n'est pas encore planifiée.";
/* Meal Types */
"lunch" = "Déjeuner";
"dinner" = "Dîner";
/* Days */
"day_mon" = "Lun";
"day_tue" = "Mar";
"day_wed" = "Mer";
"day_thu" = "Jeu";
"day_fri" = "Ven";
"day_sat" = "Sam";
"day_sun" = "Dim";
/* Durations */
"duration_30" = "30 min";
"duration_60" = "1 heure";
"duration_90" = "1,5 heure";
"duration_120" = "2 heures";
/* Reminders */
"reminder_30" = "30 min avant";
"reminder_60" = "1 heure avant";
"reminder_120" = "2 heures avant";
"first_dishes_limit_reached" = "Vous avez atteint 10 plats. Touchez Terminer quand vous êtes prêt.";
@@ -0,0 +1,286 @@
/* Onboarding */
"onboarding_welcome_title" = "Organizza i tuoi pasti in 1 minuto";
"onboarding_welcome_subtitle" = "Rispondi a 4 passaggi rapidi e inizia a pianificare oggi stesso.";
"onboarding_welcome_step_1" = "Scegli se vuoi pianificare pranzi, cene o entrambi";
"onboarding_welcome_step_2" = "Decidi se includere i weekend";
"onboarding_welcome_step_3" = "Collega facoltativamente il tuo calendario";
"onboarding_welcome_step_4" = "Aggiungi i tuoi piatti abituali per iniziare in fretta";
"onboarding_nav_intro" = "Configurazione rapida";
"onboarding_nav_setup" = "Configurazione";
"onboarding_nav_step_short" = "Passo %d";
"onboarding_nav_step_counter" = "Passo %d/%d";
"onboarding_start" = "Inizia";
"onboarding_continue" = "Continua";
"onboarding_skip" = "Salta";
"onboarding_finish" = "Fine";
/* Meal Windows */
"meal_windows_title" = "Passo 1: Pranzi, cene o entrambi?";
"meal_windows_dinner_only" = "Solo cene";
"meal_windows_lunch_only" = "Solo pranzi";
"meal_windows_both" = "Pranzi e cene";
/* Weekends */
"weekends_title" = "Passo 2: Pianificare anche i weekend?";
"weekends_toggle" = "Pianifica anche sabati e domeniche";
"weekends_note" = "Potrai cambiarlo più tardi nelle impostazioni";
/* Calendar */
"calendar_title" = "Passo 3: Collegare il calendario?";
"calendar_sync" = "Sincronizza";
"calendar_select" = "Calendario";
"calendar_lunch_time" = "Ora di pranzo";
"calendar_dinner_time" = "Ora di cena";
"calendar_optional" = "Facoltativo, potrai attivarlo più tardi";
"calendar_permission_title" = "Accesso al calendario";
"calendar_permission_message" = "MealMood ha bisogno di accedere al tuo calendario per sincronizzare gli eventi.";
"calendar_permission_settings" = "Vai alle Impostazioni";
/* First Dishes */
"first_dishes_title" = "Passo 4: Aggiungi i tuoi piatti preferiti";
"first_dishes_subtitle" = "Aggiungi almeno 2 piatti per iniziare";
"first_dishes_name_placeholder" = "Nome del piatto";
"first_dishes_add_tag" = "Aggiungi tag";
"first_dishes_add_dish" = "Aggiungi piatto";
"first_dishes_added" = "Piatti aggiunti:";
/* Home */
"home_title" = "MealMood";
"home_previous" = "Precedente";
"home_next" = "Successivo";
"home_complete" = "Completa";
"home_undo_last_action" = "Annulla l'ultima azione";
"home_reset" = "Reimposta";
"home_copy_previous_week" = "Copia la settimana precedente";
"home_my_dishes" = "I miei piatti";
"home_add_first_dish" = "Aggiungi il tuo primo piatto per iniziare";
"home_add_dish" = "Aggiungi piatto";
"home_my_dishes_search" = "Cerca piatti";
"home_my_dishes_search_empty" = "Nessun piatto trovato";
"home_my_dishes_hide_used" = "Nascondi quelli usati questa settimana";
"home_pick_dish_title" = "Scegli un piatto";
"home_pick_dish_search" = "Cerca piatti";
"home_pick_dish_empty" = "Nessun piatto corrisponde alla ricerca";
"home_pick_dish_add_new" = "Aggiungi nuovo piatto";
"home_pick_dish_no_dishes" = "Ancora nessun piatto. Aggiungine uno per iniziare.";
"home_select_week" = "Seleziona settimana";
"home_week_picker_title" = "Seleziona settimana";
"home_week_picker_date" = "Data";
"home_week_picker_go" = "Vai";
/* Reset Alert */
"reset_title" = "Cancellare tutta la settimana?";
"reset_message" = "Questa azione non può essere annullata";
"reset_cancel" = "Annulla";
"reset_confirm" = "Cancella";
/* Toasts */
"toast_dish_assigned" = "Piatto assegnato ✓";
"toast_week_complete" = "Settimana completata! 🎉";
"toast_week_reset" = "Settimana cancellata 🔄";
"toast_dish_saved" = "Piatto salvato ✓";
"toast_event_deleted" = "Evento del calendario eliminato";
"toast_cannot_complete" = "Impossibile completare tutti gli slot";
"toast_dish_deleted" = "Piatto eliminato";
"toast_no_free_slots" = "Nessuno slot libero disponibile";
"toast_calendar_synced" = "Calendario sincronizzato ✓";
"toast_copied_previous_week" = "Copiato dalla settimana precedente ✓";
"toast_previous_week_empty" = "Nessun piano trovato per la settimana precedente";
"toast_undo_applied" = "Ultima azione annullata";
"copy_previous_confirm_title" = "Sostituire la settimana attuale?";
"copy_previous_confirm_message" = "Hai già piatti assegnati. Copiare la settimana precedente sovrascriverà le assegnazioni attuali.";
"copy_previous_confirm_confirm" = "Sostituisci settimana";
/* Dish Form */
"dish_new_title" = "Nuovo piatto";
"dish_edit_title" = "Modifica piatto";
"dish_name_label" = "Nome del piatto *";
"dish_name_placeholder" = "Es.: Pollo arrosto";
"dish_description_label" = "Descrizione (facoltativa)";
"dish_description_placeholder" = "Es.: Succoso e cotto al forno con erbe";
"dish_tags_label" = "Tag (facoltativi)";
"dish_add_tag" = "Aggiungi tag";
"dish_delete" = "Elimina piatto";
"dish_delete_title" = "Eliminare questo piatto?";
"dish_delete_message" = "Questa azione non può essere annullata";
"dish_cancel" = "Annulla";
"dish_delete_blocked_title" = "Impossibile eliminare";
"dish_delete_blocked_message" = "Non puoi eliminare un piatto assegnato alla settimana corrente.";
"dish_delete_blocked_ok" = "OK";
"dish_list_empty" = "Non hai ancora piatti";
/* Tag Selector */
"tag_selector_title" = "Seleziona tag";
"tag_selector_done" = "Fatto";
/* Tags */
"tags_title" = "Tag";
"tags_max_per_week" = "Max a settimana";
"tags_no_consecutive" = "Non consecutivo";
"tags_no_consecutive_desc" = "Evita che compaia in giorni consecutivi";
"tags_no_duplicate" = "Nessuna ripetizione nello stesso giorno";
"tags_no_duplicate_desc" = "Non può essere sia a pranzo che a cena nello stesso giorno";
"tags_restriction" = "Restrizione oraria";
"tags_no_restriction" = "Nessuna restrizione";
"tags_lunch_only" = "Solo pranzo";
"tags_dinner_only" = "Solo cena";
"tags_no_limit" = "Nessun limite";
/* Settings */
"settings_title" = "Impostazioni";
"settings_planning" = "Pianificazione";
"settings_meal_windows" = "Finestre dei pasti";
"settings_include_weekends" = "Includi i weekend";
"settings_export_style" = "Stile di esportazione settimanale";
"settings_calendar" = "Calendario";
"settings_icloud_sync" = "Sincronizza i dati con iCloud";
"settings_sync" = "Sincronizza";
"settings_sync_mode" = "Modalità di sincronizzazione";
"settings_sync_mode_week_complete" = "Auto (quando la settimana è completa)";
"settings_sync_mode_manual" = "Manuale (pulsante sincronizza)";
"settings_sync_now" = "Sincronizza ora";
"settings_lunch_time" = "Ora di pranzo";
"settings_dinner_time" = "Ora di cena";
"settings_event_duration" = "Durata evento";
"settings_event_prefix" = "Prefisso titolo";
"settings_reminder" = "Promemoria";
"settings_reminder_none" = "Nessun promemoria";
"settings_tags" = "Tag";
"settings_manage_tags" = "Gestisci tag";
"settings_language" = "Lingua";
"settings_premium" = "Premium";
"settings_premium_status" = "Stato";
"settings_remove_ads" = "Rimuovi annunci";
"settings_about" = "Informazioni";
"settings_website" = "Sito web";
"settings_support" = "Email di supporto";
"settings_rate_app" = "Valuta MealMood";
"settings_app_version" = "Versione";
"settings_app_build" = "Build";
"settings_danger_zone" = "Zona di pericolo";
"settings_reset_all_data" = "Reimposta tutti i dati";
"settings_reset_all_data_title" = "Reimpostare tutti i dati dell'app?";
"settings_reset_all_data_message" = "Questo eliminerà piatti, tag, piani e impostazioni, poi riavvierà l'onboarding.";
"settings_reset_all_data_confirm" = "Reimposta tutto";
/* Premium */
"premium_title" = "MealMood Premium";
"premium_subtitle" = "Goditi tutte le funzionalità";
"premium_no_ads" = "Niente annunci";
"premium_unlimited_dishes" = "Piatti illimitati";
"premium_custom_tags" = "Tag personalizzati";
"premium_advanced_rules" = "Regole avanzate dei tag";
"premium_future_weeks" = "Pianifica settimane future illimitate";
"premium_share_week" = "Esportazione settimanale in immagine";
"premium_family_sharing" = "Supporto In famiglia";
"premium_plans" = "Piani disponibili:";
"premium_monthly" = "Mensile";
"premium_yearly" = "Annuale";
"premium_save" = "Risparmia il 44%";
"premium_month" = "mese";
"premium_price_note" = "2,99 EUR al mese";
"premium_terms_note" = "Abbonamento con rinnovo automatico. Annulla quando vuoi dalle impostazioni del tuo account Apple.";
"premium_active" = "Premium è attivo";
"premium_subscribe" = "Abbonati";
"premium_restore" = "Ripristina acquisti";
"premium_processing" = "Elaborazione...";
"premium_loading_products" = "Caricamento delle opzioni di acquisto...";
"premium_loading_products_hint" = "Se richiede troppo tempo, tocca Riprova.";
"premium_loading_timeout" = "Tempo di caricamento scaduto";
"premium_loading_timeout_hint" = "StoreKit non ha risposto in tempo. Tocca Riprova.";
"premium_products_not_found" = "Nessun prodotto disponibile";
"premium_products_not_found_hint" = "Controlla gli ID prodotto in App Store Connect / configurazione StoreKit.";
"premium_loading_failed" = "Impossibile caricare i prodotti";
"premium_loading_failed_hint" = "Controlla la configurazione StoreKit e riprova.";
"premium_retry_products" = "Riprova";
"premium_purchase_pending" = "L'acquisto è in attesa di approvazione.";
"premium_purchase_cancelled" = "L'acquisto è stato annullato.";
"premium_purchase_failed" = "L'acquisto non è riuscito. Riprova.";
"tag_selector_empty" = "Nessun tag disponibile al momento";
"premium_limit_dishes" = "Limite del piano gratuito raggiunto: 20 piatti";
"premium_limit_rules" = "Le regole avanzate dei tag sono Premium";
"premium_limit_future_weeks" = "Il piano gratuito consente di pianificare fino alla prossima settimana";
/* Home extras */
"rule_override_title" = "Conflitto di regole";
"rule_override_message" = "Questo piatto viola una o più regole dei tag. Assegnarlo comunque?";
"rule_override_confirm" = "Assegna comunque";
"share_week_button" = "Condividi settimana";
"share_week_title" = "Piano settimanale MealMood";
"share_week_callout_title" = "La tua settimana è pronta per l'esportazione";
"share_week_callout_subtitle" = "Crea una bella immagine da condividere o stampare con il tuo piano pasti.";
"share_export_style_default" = "Predefinito";
"share_export_style_school" = "Orario scolastico";
"share_export_style_vertical" = "Verticale";
"share_export_style_picker_title" = "Scegli stile di esportazione";
"share_export_style_picker_subtitle" = "Puoi cambiarlo in qualsiasi momento nelle Impostazioni.";
"share_export_style_picker_apply" = "Usa questo stile";
"share_export_school_title" = "Piano pasti settimanale";
"share_export_school_subtitle" = "Orario scolastico";
"share_export_vertical_title" = "Piano pasti verticale";
"ads_placeholder" = "Annuncio";
"ads_loading" = "Caricamento annuncio...";
"ads_unavailable" = "Annuncio non disponibile";
"history_title" = "Storico mensile";
"history_month_empty" = "Nessuna settimana pianificata in questo mese";
"history_week_complete" = "Settimana completa";
"history_week_incomplete" = "Settimana in corso";
/* Onboarding suggestions */
"first_dishes_suggestions" = "Suggerimenti rapidi";
"first_dishes_suggestion_add" = "Aggiungi suggerimento";
"first_dishes_suggestion_added" = "Aggiunto";
"first_dishes_no_tags" = "I tag predefiniti si stanno caricando, riprova tra un momento";
/* Onboarding trial */
"onboarding_trial_title" = "Sblocca Premium";
"onboarding_trial_subtitle" = "Sblocca ora tutte le funzionalità premium.";
"onboarding_trial_cta" = "Sblocca Premium";
"onboarding_trial_skip" = "Continua con il piano gratuito";
"onboarding_trial_price_format" = "%@ / mese";
/* Review funnel */
"review_funnel_title" = "Come sta andando MealMood?";
"review_funnel_message" = "La tua opinione ci aiuta a migliorare MealMood.";
"review_funnel_positive" = "Mi piace";
"review_funnel_negative" = "Da migliorare";
"review_feedback_title" = "Dicci cosa migliorare";
"review_feedback_message" = "Inviaci il tuo feedback e lo useremo per migliorare l'app.";
"review_feedback_contact" = "Invia feedback";
"onboarding_auto_assign_title" = "Vuoi che assegnamo automaticamente la tua settimana?";
"onboarding_auto_assign_message" = "Possiamo riempire automaticamente questa settimana usando i tuoi piatti e le tue regole.";
"onboarding_auto_assign_yes" = "Sì, assegna automaticamente";
"onboarding_auto_assign_no" = "No, lo faccio io";
"onboarding_premium_prompt_title" = "Vuoi sbloccare Premium?";
"onboarding_premium_prompt_message" = "Ottieni niente annunci, piatti illimitati, regole avanzate e pianificazione delle settimane future.";
"onboarding_premium_prompt_cta" = "Vedi Premium";
/* Notifications */
"notification_planning_title" = "Pianifica la tua prossima settimana";
"notification_planning_body" = "La tua nuova settimana inizia domani e non è ancora pianificata.";
/* Meal Types */
"lunch" = "Pranzo";
"dinner" = "Cena";
/* Days */
"day_mon" = "Lun";
"day_tue" = "Mar";
"day_wed" = "Mer";
"day_thu" = "Gio";
"day_fri" = "Ven";
"day_sat" = "Sab";
"day_sun" = "Dom";
/* Durations */
"duration_30" = "30 min";
"duration_60" = "1 ora";
"duration_90" = "1,5 ore";
"duration_120" = "2 ore";
/* Reminders */
"reminder_30" = "30 min prima";
"reminder_60" = "1 ora prima";
"reminder_120" = "2 ore prima";
"first_dishes_limit_reached" = "Hai raggiunto 10 piatti. Tocca Fine quando sei pronto.";
@@ -0,0 +1,286 @@
/* Onboarding */
"onboarding_welcome_title" = "Organize suas refeições em 1 minuto";
"onboarding_welcome_subtitle" = "Responda 4 etapas rápidas e comece a planejar hoje mesmo.";
"onboarding_welcome_step_1" = "Escolha se você quer planejar almoços, jantares ou ambos";
"onboarding_welcome_step_2" = "Decida se quer incluir fins de semana";
"onboarding_welcome_step_3" = "Conecte seu calendário opcionalmente";
"onboarding_welcome_step_4" = "Adicione seus pratos habituais para começar rápido";
"onboarding_nav_intro" = "Configuração rápida";
"onboarding_nav_setup" = "Configuração";
"onboarding_nav_step_short" = "Etapa %d";
"onboarding_nav_step_counter" = "Etapa %d/%d";
"onboarding_start" = "Começar";
"onboarding_continue" = "Continuar";
"onboarding_skip" = "Pular";
"onboarding_finish" = "Concluir";
/* Meal Windows */
"meal_windows_title" = "Etapa 1: Almoços, jantares ou ambos?";
"meal_windows_dinner_only" = "Só jantares";
"meal_windows_lunch_only" = "Só almoços";
"meal_windows_both" = "Almoços e jantares";
/* Weekends */
"weekends_title" = "Etapa 2: Planejar fins de semana também?";
"weekends_toggle" = "Planejar sábados e domingos também";
"weekends_note" = "Você pode alterar isso depois nas configurações";
/* Calendar */
"calendar_title" = "Etapa 3: Conectar seu calendário?";
"calendar_sync" = "Sincronizar";
"calendar_select" = "Calendário";
"calendar_lunch_time" = "Horário do almoço";
"calendar_dinner_time" = "Horário do jantar";
"calendar_optional" = "Opcional, você pode ativar depois";
"calendar_permission_title" = "Acesso ao Calendário";
"calendar_permission_message" = "O MealMood precisa acessar seu calendário para sincronizar eventos.";
"calendar_permission_settings" = "Ir para Ajustes";
/* First Dishes */
"first_dishes_title" = "Etapa 4: Adicione seus pratos favoritos";
"first_dishes_subtitle" = "Adicione pelo menos 2 pratos para começar";
"first_dishes_name_placeholder" = "Nome do prato";
"first_dishes_add_tag" = "Adicionar tag";
"first_dishes_add_dish" = "Adicionar prato";
"first_dishes_added" = "Pratos adicionados:";
/* Home */
"home_title" = "MealMood";
"home_previous" = "Anterior";
"home_next" = "Próximo";
"home_complete" = "Concluir";
"home_undo_last_action" = "Desfazer última ação";
"home_reset" = "Redefinir";
"home_copy_previous_week" = "Copiar semana anterior";
"home_my_dishes" = "Meus pratos";
"home_add_first_dish" = "Adicione seu primeiro prato para começar";
"home_add_dish" = "Adicionar prato";
"home_my_dishes_search" = "Buscar pratos";
"home_my_dishes_search_empty" = "Nenhum prato encontrado";
"home_my_dishes_hide_used" = "Ocultar usados nesta semana";
"home_pick_dish_title" = "Escolha um prato";
"home_pick_dish_search" = "Buscar pratos";
"home_pick_dish_empty" = "Nenhum prato corresponde à sua busca";
"home_pick_dish_add_new" = "Adicionar novo prato";
"home_pick_dish_no_dishes" = "Ainda não há pratos. Adicione um para começar.";
"home_select_week" = "Selecionar semana";
"home_week_picker_title" = "Selecionar semana";
"home_week_picker_date" = "Data";
"home_week_picker_go" = "Ir";
/* Reset Alert */
"reset_title" = "Limpar a semana inteira?";
"reset_message" = "Isso não pode ser desfeito";
"reset_cancel" = "Cancelar";
"reset_confirm" = "Limpar";
/* Toasts */
"toast_dish_assigned" = "Prato atribuído ✓";
"toast_week_complete" = "Semana concluída! 🎉";
"toast_week_reset" = "Semana limpa 🔄";
"toast_dish_saved" = "Prato salvo ✓";
"toast_event_deleted" = "Evento do calendário excluído";
"toast_cannot_complete" = "Não foi possível completar todos os slots";
"toast_dish_deleted" = "Prato excluído";
"toast_no_free_slots" = "Não há slots livres disponíveis";
"toast_calendar_synced" = "Calendário sincronizado ✓";
"toast_copied_previous_week" = "Copiado da semana anterior ✓";
"toast_previous_week_empty" = "Nenhum plano da semana anterior encontrado";
"toast_undo_applied" = "Última ação desfeita";
"copy_previous_confirm_title" = "Substituir a semana atual?";
"copy_previous_confirm_message" = "Você já tem pratos atribuídos. Copiar a semana anterior substituirá as atribuições atuais.";
"copy_previous_confirm_confirm" = "Substituir semana";
/* Dish Form */
"dish_new_title" = "Novo prato";
"dish_edit_title" = "Editar prato";
"dish_name_label" = "Nome do prato *";
"dish_name_placeholder" = "Ex.: Frango assado";
"dish_description_label" = "Descrição (opcional)";
"dish_description_placeholder" = "Ex.: Suculento e assado no forno com ervas";
"dish_tags_label" = "Tags (opcional)";
"dish_add_tag" = "Adicionar tag";
"dish_delete" = "Excluir prato";
"dish_delete_title" = "Excluir este prato?";
"dish_delete_message" = "Essa ação não pode ser desfeita";
"dish_cancel" = "Cancelar";
"dish_delete_blocked_title" = "Não é possível excluir";
"dish_delete_blocked_message" = "Você não pode excluir um prato atribuído à semana atual.";
"dish_delete_blocked_ok" = "OK";
"dish_list_empty" = "Você ainda não tem pratos";
/* Tag Selector */
"tag_selector_title" = "Selecionar tags";
"tag_selector_done" = "Concluído";
/* Tags */
"tags_title" = "Tags";
"tags_max_per_week" = "Máx. por semana";
"tags_no_consecutive" = "Sem consecutivos";
"tags_no_consecutive_desc" = "Evita aparecer em dias consecutivos";
"tags_no_duplicate" = "Sem repetição no mesmo dia";
"tags_no_duplicate_desc" = "Não pode estar no almoço e no jantar no mesmo dia";
"tags_restriction" = "Restrição de horário";
"tags_no_restriction" = "Sem restrição";
"tags_lunch_only" = "Só almoço";
"tags_dinner_only" = "Só jantar";
"tags_no_limit" = "Sem limite";
/* Settings */
"settings_title" = "Ajustes";
"settings_planning" = "Planejamento";
"settings_meal_windows" = "Janelas de refeição";
"settings_include_weekends" = "Incluir fins de semana";
"settings_export_style" = "Estilo de exportação semanal";
"settings_calendar" = "Calendário";
"settings_icloud_sync" = "Sincronizar dados com o iCloud";
"settings_sync" = "Sincronizar";
"settings_sync_mode" = "Modo de sincronização";
"settings_sync_mode_week_complete" = "Automático (quando a semana estiver completa)";
"settings_sync_mode_manual" = "Manual (botão sincronizar)";
"settings_sync_now" = "Sincronizar agora";
"settings_lunch_time" = "Horário do almoço";
"settings_dinner_time" = "Horário do jantar";
"settings_event_duration" = "Duração do evento";
"settings_event_prefix" = "Prefixo do título";
"settings_reminder" = "Lembrete";
"settings_reminder_none" = "Sem lembrete";
"settings_tags" = "Tags";
"settings_manage_tags" = "Gerenciar tags";
"settings_language" = "Idioma";
"settings_premium" = "Premium";
"settings_premium_status" = "Status";
"settings_remove_ads" = "Remover anúncios";
"settings_about" = "Sobre";
"settings_website" = "Site";
"settings_support" = "E-mail de suporte";
"settings_rate_app" = "Avaliar MealMood";
"settings_app_version" = "Versão";
"settings_app_build" = "Build";
"settings_danger_zone" = "Zona de perigo";
"settings_reset_all_data" = "Redefinir todos os dados";
"settings_reset_all_data_title" = "Redefinir todos os dados do app?";
"settings_reset_all_data_message" = "Isso apagará pratos, tags, planos e ajustes, depois reiniciará o onboarding.";
"settings_reset_all_data_confirm" = "Redefinir tudo";
/* Premium */
"premium_title" = "MealMood Premium";
"premium_subtitle" = "Aproveite todos os recursos";
"premium_no_ads" = "Sem anúncios";
"premium_unlimited_dishes" = "Pratos ilimitados";
"premium_custom_tags" = "Tags personalizadas";
"premium_advanced_rules" = "Regras avançadas de tags";
"premium_future_weeks" = "Planeje semanas futuras sem limite";
"premium_share_week" = "Exportação semanal em imagem";
"premium_family_sharing" = "Compatível com Compartilhamento Familiar";
"premium_plans" = "Planos disponíveis:";
"premium_monthly" = "Mensal";
"premium_yearly" = "Anual";
"premium_save" = "Economize 44%";
"premium_month" = "mês";
"premium_price_note" = "2,99 EUR por mês";
"premium_terms_note" = "Assinatura com renovação automática. Cancele a qualquer momento nos ajustes da sua conta Apple.";
"premium_active" = "O Premium está ativo";
"premium_subscribe" = "Assinar";
"premium_restore" = "Restaurar compras";
"premium_processing" = "Processando...";
"premium_loading_products" = "Carregando opções de compra...";
"premium_loading_products_hint" = "Se isso demorar demais, toque em Tentar novamente.";
"premium_loading_timeout" = "Tempo de carregamento esgotado";
"premium_loading_timeout_hint" = "O StoreKit não respondeu a tempo. Toque em Tentar novamente.";
"premium_products_not_found" = "Nenhum produto disponível";
"premium_products_not_found_hint" = "Verifique os IDs de produto no App Store Connect / configuração do StoreKit.";
"premium_loading_failed" = "Não foi possível carregar os produtos";
"premium_loading_failed_hint" = "Verifique sua configuração do StoreKit e tente novamente.";
"premium_retry_products" = "Tentar novamente";
"premium_purchase_pending" = "A compra está aguardando aprovação.";
"premium_purchase_cancelled" = "A compra foi cancelada.";
"premium_purchase_failed" = "A compra falhou. Tente novamente.";
"tag_selector_empty" = "Ainda não há tags disponíveis";
"premium_limit_dishes" = "Limite do plano grátis atingido: 20 pratos";
"premium_limit_rules" = "Regras avançadas de tags são Premium";
"premium_limit_future_weeks" = "O plano grátis permite planejar até a próxima semana";
/* Home extras */
"rule_override_title" = "Conflito de regras";
"rule_override_message" = "Este prato quebra uma ou mais regras de tags. Atribuir mesmo assim?";
"rule_override_confirm" = "Atribuir mesmo assim";
"share_week_button" = "Compartilhar semana";
"share_week_title" = "Plano semanal MealMood";
"share_week_callout_title" = "Sua semana está pronta para exportar";
"share_week_callout_subtitle" = "Crie uma imagem bonita para compartilhar ou imprimir seu plano de refeições.";
"share_export_style_default" = "Padrão";
"share_export_style_school" = "Grade escolar";
"share_export_style_vertical" = "Vertical";
"share_export_style_picker_title" = "Escolha o estilo de exportação";
"share_export_style_picker_subtitle" = "Você pode mudar isso a qualquer momento nos Ajustes.";
"share_export_style_picker_apply" = "Usar este estilo";
"share_export_school_title" = "Plano semanal de refeições";
"share_export_school_subtitle" = "Grade escolar";
"share_export_vertical_title" = "Plano vertical de refeições";
"ads_placeholder" = "Anúncio";
"ads_loading" = "Carregando anúncio...";
"ads_unavailable" = "Anúncio indisponível";
"history_title" = "Histórico mensal";
"history_month_empty" = "Nenhuma semana planejada neste mês";
"history_week_complete" = "Semana concluída";
"history_week_incomplete" = "Semana em andamento";
/* Onboarding suggestions */
"first_dishes_suggestions" = "Sugestões rápidas";
"first_dishes_suggestion_add" = "Adicionar sugestão";
"first_dishes_suggestion_added" = "Adicionado";
"first_dishes_no_tags" = "As tags padrão estão carregando, tente novamente em instantes";
/* Onboarding trial */
"onboarding_trial_title" = "Desbloquear Premium";
"onboarding_trial_subtitle" = "Desbloqueie agora todos os recursos premium.";
"onboarding_trial_cta" = "Desbloquear Premium";
"onboarding_trial_skip" = "Continuar com o plano grátis";
"onboarding_trial_price_format" = "%@ / mês";
/* Review funnel */
"review_funnel_title" = "Como está sendo usar o MealMood?";
"review_funnel_message" = "Sua opinião nos ajuda a melhorar o MealMood.";
"review_funnel_positive" = "Estou gostando";
"review_funnel_negative" = "Precisa melhorar";
"review_feedback_title" = "Conte o que devemos melhorar";
"review_feedback_message" = "Envie seu feedback e vamos usá-lo para melhorar o app.";
"review_feedback_contact" = "Enviar feedback";
"onboarding_auto_assign_title" = "Quer que a gente preencha sua semana automaticamente?";
"onboarding_auto_assign_message" = "Podemos preencher esta semana automaticamente usando seus pratos e regras.";
"onboarding_auto_assign_yes" = "Sim, preencher automaticamente";
"onboarding_auto_assign_no" = "Não, eu faço";
"onboarding_premium_prompt_title" = "Quer desbloquear o Premium?";
"onboarding_premium_prompt_message" = "Tenha zero anúncios, pratos ilimitados, regras avançadas e planejamento de semanas futuras.";
"onboarding_premium_prompt_cta" = "Ver Premium";
/* Notifications */
"notification_planning_title" = "Planeje sua próxima semana";
"notification_planning_body" = "Sua nova semana começa amanhã e ainda não foi planejada.";
/* Meal Types */
"lunch" = "Almoço";
"dinner" = "Jantar";
/* Days */
"day_mon" = "Seg";
"day_tue" = "Ter";
"day_wed" = "Qua";
"day_thu" = "Qui";
"day_fri" = "Sex";
"day_sat" = "Sáb";
"day_sun" = "Dom";
/* Durations */
"duration_30" = "30 min";
"duration_60" = "1 hora";
"duration_90" = "1,5 hora";
"duration_120" = "2 horas";
/* Reminders */
"reminder_30" = "30 min antes";
"reminder_60" = "1 hora antes";
"reminder_120" = "2 horas antes";
"first_dishes_limit_reached" = "Você chegou a 10 pratos. Toque em Concluir quando estiver pronto.";
@@ -70,6 +70,7 @@ final class ICloudSyncService {
eventPrefix: $0.eventPrefix,
reminderMinutesBefore: $0.reminderMinutesBefore,
iCloudSyncEnabled: $0.iCloudSyncEnabled,
weekExportStyle: $0.weekExportStyle,
isPremium: $0.isPremium,
onboardingCompleted: $0.onboardingCompleted
)
@@ -151,6 +152,7 @@ final class ICloudSyncService {
settings.eventPrefix = settingsPayload.eventPrefix
settings.reminderMinutesBefore = settingsPayload.reminderMinutesBefore
settings.iCloudSyncEnabled = settingsPayload.iCloudSyncEnabled
settings.weekExportStyle = settingsPayload.weekExportStyle
settings.isPremium = settingsPayload.isPremium
settings.onboardingCompleted = settingsPayload.onboardingCompleted
context.insert(settings)
@@ -233,6 +235,7 @@ private struct SettingsPayload: Codable {
let eventPrefix: String
let reminderMinutesBefore: Int?
let iCloudSyncEnabled: Bool?
let weekExportStyle: String?
let isPremium: Bool
let onboardingCompleted: Bool
}
@@ -137,6 +137,7 @@ final class SettingsViewModel: ObservableObject {
settings.eventDuration = defaults.eventDuration
settings.eventPrefix = defaults.eventPrefix
settings.reminderMinutesBefore = defaults.reminderMinutesBefore
settings.weekExportStyle = defaults.weekExportStyle
settings.iCloudSyncEnabled = false
settings.onboardingCompleted = false
}
+236 -11
View File
@@ -1,5 +1,6 @@
import SwiftUI
import SwiftData
import UIKit
struct HomeView: View {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@@ -24,6 +25,10 @@ struct HomeView: View {
@State private var showPostOnboardingAutoAssignPrompt: Bool = false
@State private var showPostOnboardingPremiumPrompt: Bool = false
@State private var hasEvaluatedPostOnboardingPrompts: Bool = false
@State private var showExportStylePicker: Bool = false
@State private var pendingExportPlan: WeekPlan?
@State private var shareImageURL: URL?
@State private var showShareSheet: Bool = false
private var settings: AppSettings? { allSettings.first }
@@ -383,6 +388,26 @@ struct HomeView: View {
PremiumView(settings: settings)
}
}
.sheet(isPresented: $showExportStylePicker) {
WeekExportStylePickerSheet(
selectedStyle: settings.weekExportStyleEnum
) { selectedStyle in
settings.weekExportStyleEnum = selectedStyle
try? context.save()
showExportStylePicker = false
if let planToShare = pendingExportPlan {
prepareShareImage(plan: planToShare, settings: settings)
}
pendingExportPlan = nil
}
.presentationDetents([.medium, .large])
}
.sheet(isPresented: $showShareSheet) {
if let shareImageURL {
ShareSheet(activityItems: [shareImageURL])
}
}
.sheet(isPresented: $showMonthlyHistory) {
MonthlyHistoryView(
weekPlans: weekPlans,
@@ -584,13 +609,10 @@ struct HomeView: View {
.font(.mealMoodCaption)
.foregroundColor(.mealMoodTextSecondary)
if settings.isPremium,
let image = renderWeekShareImage(plan: plan, settings: settings),
let shareURL = persistShareImage(image) {
ShareLink(
item: shareURL,
preview: SharePreview(String(localized: "share_week_title"), image: Image(uiImage: image))
) {
if settings.isPremium {
Button {
startShareFlow(plan: plan, settings: settings)
} label: {
Label("share_week_button", systemImage: "square.and.arrow.up")
.font(.mealMoodBodyBold)
.foregroundColor(.mealMoodTextPrimary)
@@ -599,6 +621,7 @@ struct HomeView: View {
.background(Color.white.opacity(0.75))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
.buttonStyle(.plain)
} else {
Button {
showPremiumFromExport = true
@@ -637,19 +660,42 @@ struct HomeView: View {
)
}
private func startShareFlow(plan: WeekPlan, settings: AppSettings) {
if settings.weekExportStyle == nil {
pendingExportPlan = plan
showExportStylePicker = true
return
}
prepareShareImage(plan: plan, settings: settings)
}
private func prepareShareImage(plan: WeekPlan, settings: AppSettings) {
showShareSheet = false
shareImageURL = nil
guard let image = renderWeekShareImage(plan: plan, settings: settings),
let url = persistShareImage(image, style: settings.weekExportStyleEnum) else {
return
}
shareImageURL = url
showShareSheet = true
}
private func renderWeekShareImage(plan: WeekPlan, settings: AppSettings) -> UIImage? {
let renderer = ImageRenderer(content: WeekPlanShareView(plan: plan, settings: settings, dishes: dishes, tags: tags))
let canvasSize = WeekPlanShareView.canvasSize(for: settings.weekExportStyleEnum)
renderer.proposedSize = ProposedViewSize(
width: WeekPlanShareView.a4LandscapeWidth,
height: WeekPlanShareView.a4LandscapeHeight
width: canvasSize.width,
height: canvasSize.height
)
renderer.scale = 1
return renderer.uiImage
}
private func persistShareImage(_ image: UIImage) -> URL? {
private func persistShareImage(_ image: UIImage, style: WeekExportStyle) -> URL? {
guard let data = image.pngData() else { return nil }
let url = FileManager.default.temporaryDirectory.appendingPathComponent("mealmood-week-plan.png")
let timestamp = Int(Date().timeIntervalSince1970 * 1000)
let filename = "mealmood-week-plan-\(style.rawValue)-\(timestamp).png"
let url = FileManager.default.temporaryDirectory.appendingPathComponent(filename)
try? data.write(to: url, options: .atomic)
return url
}
@@ -731,6 +777,185 @@ struct HomeView: View {
}
}
private struct ShareSheet: UIViewControllerRepresentable {
let activityItems: [Any]
func makeUIViewController(context: Context) -> UIActivityViewController {
UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
}
private struct WeekExportStylePickerSheet: View {
let selectedStyle: WeekExportStyle
let onConfirm: (WeekExportStyle) -> Void
@State private var temporarySelection: WeekExportStyle
init(selectedStyle: WeekExportStyle, onConfirm: @escaping (WeekExportStyle) -> Void) {
self.selectedStyle = selectedStyle
self.onConfirm = onConfirm
_temporarySelection = State(initialValue: selectedStyle)
}
var body: some View {
NavigationStack {
VStack(spacing: 14) {
Text("share_export_style_picker_title")
.font(.mealMoodH2)
.foregroundColor(.mealMoodTextPrimary)
.padding(.top, 8)
Text("share_export_style_picker_subtitle")
.font(.mealMoodCaption)
.foregroundColor(.mealMoodTextSecondary)
ScrollView {
VStack(spacing: 12) {
ForEach(WeekExportStyle.allCases, id: \.self) { style in
Button {
temporarySelection = style
} label: {
VStack(alignment: .leading, spacing: 10) {
WeekExportStyleThumbnail(style: style)
.frame(height: 110)
.clipShape(RoundedRectangle(cornerRadius: 10))
HStack {
Text(LocalizedStringKey(style.localizedKey))
.font(.mealMoodBodyBold)
.foregroundColor(.mealMoodTextPrimary)
Spacer()
Image(systemName: temporarySelection == style ? "checkmark.circle.fill" : "circle")
.foregroundColor(temporarySelection == style ? .mealMoodCoral : .mealMoodTextSecondary)
}
}
.padding(10)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 12))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(
temporarySelection == style ? Color.mealMoodCoral : Color.mealMoodMint.opacity(0.55),
lineWidth: temporarySelection == style ? 2 : 1
)
)
}
.buttonStyle(.plain)
}
}
.padding(.vertical, 6)
}
Button("share_export_style_picker_apply") {
onConfirm(temporarySelection)
}
.buttonStyle(.borderedProminent)
.tint(.mealMoodCoral)
}
.padding(.horizontal, 16)
.padding(.bottom, 12)
.background(Color.mealMoodBackground.ignoresSafeArea())
.navigationBarTitleDisplayMode(.inline)
}
}
}
private struct WeekExportStyleThumbnail: View {
let style: WeekExportStyle
var body: some View {
ZStack {
switch style {
case .defaultStyle:
defaultPreview
case .schoolTimetable:
schoolPreview
case .vertical:
verticalPreview
}
}
}
private var defaultPreview: some View {
ZStack {
LinearGradient(colors: [Color(hex: "#FFEFE6"), Color(hex: "#F1FBF5")], startPoint: .topLeading, endPoint: .bottomTrailing)
VStack(spacing: 6) {
RoundedRectangle(cornerRadius: 5)
.fill(Color.white.opacity(0.9))
.frame(height: 20)
RoundedRectangle(cornerRadius: 8)
.fill(Color.white.opacity(0.8))
.frame(height: 62)
.overlay(
VStack(spacing: 4) {
Rectangle().fill(Color.mealMoodCoral.opacity(0.35)).frame(height: 8)
Rectangle().fill(Color.mealMoodMint.opacity(0.35)).frame(height: 8)
Rectangle().fill(Color(hex: "#E9EDF7")).frame(height: 8)
}
.padding(8)
)
}
.padding(8)
}
}
private var schoolPreview: some View {
ZStack {
Color(hex: "#FFF9F5")
VStack(spacing: 5) {
RoundedRectangle(cornerRadius: 5)
.fill(Color.white)
.frame(height: 16)
HStack(spacing: 4) {
RoundedRectangle(cornerRadius: 4)
.fill(Color.mealMoodCoral.opacity(0.5))
.frame(width: 42)
VStack(spacing: 4) {
Rectangle().fill(Color.mealMoodMint.opacity(0.4)).frame(height: 12)
Rectangle().fill(Color(hex: "#FFFDFB")).frame(height: 20)
Rectangle().fill(Color(hex: "#FFFDFB")).frame(height: 20)
}
VStack(spacing: 4) {
Rectangle().fill(Color.mealMoodMint.opacity(0.4)).frame(height: 12)
Rectangle().fill(Color(hex: "#FFFDFB")).frame(height: 20)
Rectangle().fill(Color(hex: "#FFFDFB")).frame(height: 20)
}
}
}
.padding(8)
}
}
private var verticalPreview: some View {
ZStack {
Color(hex: "#FFFDF8")
VStack(spacing: 6) {
RoundedRectangle(cornerRadius: 5)
.fill(Color.white)
.frame(height: 18)
ForEach(0..<3, id: \.self) { _ in
HStack(spacing: 6) {
Capsule()
.fill(Color.mealMoodCoral.opacity(0.45))
.frame(width: 46, height: 14)
VStack(spacing: 4) {
Rectangle().fill(Color.mealMoodMint.opacity(0.35)).frame(height: 7)
Rectangle().fill(Color(hex: "#EDE8E1")).frame(height: 7)
}
}
.padding(.horizontal, 6)
.padding(.vertical, 4)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 6))
}
}
.padding(8)
}
}
}
private struct MonthlyHistoryView: View {
let weekPlans: [WeekPlan]
let currentWeekStart: Date
+333 -90
View File
@@ -6,14 +6,40 @@ struct WeekPlanShareView: View {
let settings: AppSettings
let dishes: [Dish]
let tags: [Tag]
let exportStyleOverride: WeekExportStyle?
static let a4LandscapeWidth: CGFloat = 3508
static let a4LandscapeHeight: CGFloat = 2480
static let a4PortraitWidth: CGFloat = 2480
static let a4PortraitHeight: CGFloat = 3508
private let shareURL = "https://mealmood.app"
private let qrContext = CIContext()
private let qrFilter = CIFilter.qrCodeGenerator()
init(
plan: WeekPlan,
settings: AppSettings,
dishes: [Dish],
tags: [Tag],
exportStyleOverride: WeekExportStyle? = nil
) {
self.plan = plan
self.settings = settings
self.dishes = dishes
self.tags = tags
self.exportStyleOverride = exportStyleOverride
}
static func canvasSize(for style: WeekExportStyle) -> CGSize {
switch style {
case .vertical:
return CGSize(width: a4PortraitWidth, height: a4PortraitHeight)
case .defaultStyle, .schoolTimetable:
return CGSize(width: a4LandscapeWidth, height: a4LandscapeHeight)
}
}
private var dayRange: ClosedRange<Int> {
settings.includeWeekends ? 0...6 : 0...4
}
@@ -26,6 +52,18 @@ struct WeekPlanShareView: View {
}
}
private var exportStyle: WeekExportStyle {
exportStyleOverride ?? settings.weekExportStyleEnum
}
private var canvasSize: CGSize {
Self.canvasSize(for: exportStyle)
}
private var locale: Locale {
Locale(identifier: settings.languageEnum.resolved().localeIdentifier)
}
private var exportDateString: String {
Date.now.formatted(date: .abbreviated, time: .omitted)
}
@@ -41,77 +79,42 @@ struct WeekPlanShareView: View {
var body: some View {
ZStack {
backgroundLayer
switch exportStyle {
case .defaultStyle:
defaultBackground
case .schoolTimetable:
schoolBackground
case .vertical:
verticalBackground
}
VStack(alignment: .leading, spacing: 34) {
VStack(spacing: 24) {
headerBlock
ZStack {
RoundedRectangle(cornerRadius: 30)
.fill(Color.white.opacity(0.62))
RoundedRectangle(cornerRadius: 30)
.stroke(Color.white.opacity(0.75), lineWidth: 2)
Grid(horizontalSpacing: 10, verticalSpacing: 10) {
GridRow {
gridHeaderCell("", day: nil)
.frame(minWidth: 220)
ForEach(Array(dayRange), id: \.self) { day in
gridHeaderCell(dayTitle(for: day), day: day)
}
}
ForEach(mealTypes, id: \.self) { mealType in
GridRow {
gridMealCell(mealTypeLabel(mealType), icon: mealType.icon, mealType: mealType)
.frame(minWidth: 220)
ForEach(Array(dayRange), id: \.self) { day in
gridDishCell(dishName(day: day, mealType: mealType), day: day)
}
}
}
Group {
switch exportStyle {
case .defaultStyle:
defaultGridLayout
case .schoolTimetable:
schoolTimetableLayout
case .vertical:
verticalLayout
}
.padding(18)
}
.shadow(color: Color(hex: "#DDAA99").opacity(0.18), radius: 26, x: 0, y: 16)
Spacer()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
footerBlock
}
.padding(.horizontal, 84)
.padding(.vertical, 72)
.padding(.horizontal, exportStyle == .vertical ? 72 : 84)
.padding(.vertical, exportStyle == .vertical ? 58 : 70)
}
.frame(width: Self.a4LandscapeWidth, height: Self.a4LandscapeHeight)
.frame(width: canvasSize.width, height: canvasSize.height)
}
private func dayTitle(for day: Int) -> String {
let keys = ["day_mon", "day_tue", "day_wed", "day_thu", "day_fri", "day_sat", "day_sun"]
let localizedDay = day >= 0 && day < keys.count ? NSLocalizedString(keys[day], comment: "") : ""
let dayDate = plan.weekStartDate.addingDays(day)
let dayNumber = Calendar.current.component(.day, from: dayDate)
return "\(localizedDay) \(dayNumber)"
}
private func mealTypeLabel(_ mealType: MealType) -> String {
mealType == .lunch ? String(localized: "lunch") : String(localized: "dinner")
}
private func dishName(day: Int, mealType: MealType) -> String {
let slot = plan.slots.first { $0.dayOfWeek == day && $0.mealType == mealType.rawValue }
guard let slot, let dishId = slot.dishId, let dish = dishes.first(where: { $0.id == dishId }) else {
return ""
}
return dish.name
}
private var backgroundLayer: some View {
private var defaultBackground: some View {
ZStack {
LinearGradient(
colors: [
Color(hex: "#FFEFE6"),
Color(hex: "#F1FBF5")
],
colors: [Color(hex: "#FFEFE6"), Color(hex: "#F1FBF5")],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
@@ -127,57 +130,262 @@ struct WeekPlanShareView: View {
.frame(width: 680, height: 680)
.blur(radius: 20)
.offset(x: 1050, y: 760)
GeometryReader { geo in
Canvas { context, size in
let spacing: CGFloat = 80
var x: CGFloat = -size.height
while x < size.width {
var path = Path()
path.move(to: CGPoint(x: x, y: 0))
path.addLine(to: CGPoint(x: x + size.height, y: size.height))
context.stroke(path, with: .color(Color(hex: "#FFFFFF").opacity(0.26)), lineWidth: 1.2)
x += spacing
}
}
.frame(width: geo.size.width, height: geo.size.height)
}
}
}
private var schoolBackground: some View {
ZStack {
Color(hex: "#FFF9F5")
RoundedRectangle(cornerRadius: 220)
.fill(Color.mealMoodMint.opacity(0.22))
.frame(width: 1500, height: 520)
.offset(x: 920, y: -920)
RoundedRectangle(cornerRadius: 220)
.fill(Color.mealMoodCoral.opacity(0.18))
.frame(width: 1600, height: 560)
.offset(x: -920, y: 940)
}
}
private var verticalBackground: some View {
ZStack {
Color(hex: "#FFFDF8")
Ellipse()
.fill(Color.mealMoodMint.opacity(0.28))
.frame(width: 1100, height: 900)
.offset(x: 800, y: -1200)
Ellipse()
.fill(Color.mealMoodCoral.opacity(0.2))
.frame(width: 900, height: 680)
.offset(x: -800, y: 1200)
}
}
@ViewBuilder
private var headerBlock: some View {
switch exportStyle {
case .defaultStyle:
defaultHeader
case .schoolTimetable, .vertical:
elegantHeader
}
}
private var defaultHeader: some View {
HStack(alignment: .center, spacing: 24) {
AppIconPlaceholder(size: 84)
VStack(alignment: .leading, spacing: 8) {
Text("MealMood Premium")
.font(.system(size: 50, weight: .bold, design: .rounded))
VStack(alignment: .leading, spacing: 6) {
Text(exportHeaderTitle)
.font(.system(size: 52, weight: .bold, design: .rounded))
.foregroundColor(.mealMoodTextPrimary)
Text(String(localized: "share_week_title"))
Text(plan.weekStartDate.formattedWeekRange())
.font(.system(size: 28, weight: .semibold, design: .rounded))
.foregroundColor(Color(hex: "#5D645F"))
}
Spacer()
VStack(alignment: .trailing, spacing: 8) {
Text(plan.weekStartDate.formattedWeekRange())
.font(.system(size: 36, weight: .bold, design: .rounded))
.foregroundColor(Color(hex: "#4A4F4B"))
Text(exportDateString)
.font(.system(size: 24, weight: .medium, design: .rounded))
.foregroundColor(.mealMoodTextSecondary)
}
Text(exportDateString)
.font(.system(size: 24, weight: .medium, design: .rounded))
.foregroundColor(.mealMoodTextSecondary)
}
.padding(.horizontal, 30)
.padding(.vertical, 24)
.background(Color.white.opacity(0.76))
.background(Color.white.opacity(0.8))
.clipShape(RoundedRectangle(cornerRadius: 24))
.overlay(
RoundedRectangle(cornerRadius: 24)
.stroke(Color(hex: "#F3C4B5").opacity(0.7), lineWidth: 1)
.stroke(Color.mealMoodCoral.opacity(0.5), lineWidth: 1)
)
.shadow(color: Color(hex: "#DDAA99").opacity(0.15), radius: 20, x: 0, y: 12)
}
private var elegantHeader: some View {
VStack(spacing: 8) {
Text(plan.weekStartDate.formattedWeekRange())
.font(.custom("Didot", size: exportStyle == .vertical ? 126 : 108))
.foregroundColor(Color(hex: "#30473F"))
.multilineTextAlignment(.center)
.lineLimit(1)
.minimumScaleFactor(0.45)
.frame(maxWidth: .infinity)
Text(exportHeaderTitle)
.font(.system(size: exportStyle == .vertical ? 58 : 54, weight: .semibold, design: .serif))
.foregroundColor(.mealMoodTextPrimary)
Text(exportDateString)
.font(.system(size: 34, weight: .medium, design: .serif))
.foregroundColor(.mealMoodTextSecondary)
}
.padding(.horizontal, 24)
.padding(.vertical, 24)
.background(Color.white.opacity(0.88))
.clipShape(RoundedRectangle(cornerRadius: 24))
.overlay(
RoundedRectangle(cornerRadius: 24)
.stroke(Color.mealMoodCoral.opacity(0.45), lineWidth: 1)
)
}
private var exportHeaderTitle: String {
switch exportStyle {
case .defaultStyle:
return String(localized: "share_week_title")
case .schoolTimetable:
return String(localized: "share_export_school_title")
case .vertical:
return String(localized: "share_export_vertical_title")
}
}
private var defaultGridLayout: some View {
ZStack {
RoundedRectangle(cornerRadius: 30)
.fill(Color.white.opacity(0.62))
RoundedRectangle(cornerRadius: 30)
.stroke(Color.white.opacity(0.75), lineWidth: 2)
Grid(horizontalSpacing: 10, verticalSpacing: 10) {
GridRow {
gridHeaderCell("", day: nil)
.frame(minWidth: 220)
ForEach(Array(dayRange), id: \.self) { day in
gridHeaderCell(dayShortTitle(for: day), day: day)
}
}
ForEach(mealTypes, id: \.self) { mealType in
GridRow {
gridMealCell(mealTypeLabel(mealType), icon: mealType.icon, mealType: mealType)
.frame(minWidth: 220)
ForEach(Array(dayRange), id: \.self) { day in
gridDishCell(dishName(day: day, mealType: mealType), day: day)
}
}
}
}
.padding(18)
}
.shadow(color: Color(hex: "#DDAA99").opacity(0.18), radius: 26, x: 0, y: 16)
}
private var schoolTimetableLayout: some View {
ZStack {
RoundedRectangle(cornerRadius: 28)
.fill(Color.white)
.shadow(color: Color.black.opacity(0.06), radius: 20, x: 0, y: 10)
VStack(spacing: 18) {
Text(String(localized: "share_export_school_subtitle"))
.font(.custom("Didot", size: 48))
.foregroundColor(.mealMoodTextSecondary)
Grid(horizontalSpacing: 12, verticalSpacing: 12) {
GridRow {
Text(" ")
.frame(width: 280)
ForEach(mealTypes, id: \.self) { mealType in
Text(mealTypeLabel(mealType).uppercased(with: locale))
.font(.system(size: 34, weight: .bold, design: .serif))
.foregroundColor(Color(hex: "#355548"))
.frame(maxWidth: .infinity, minHeight: 88)
.background(Color.mealMoodMint.opacity(0.3))
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
ForEach(Array(dayRange), id: \.self) { day in
GridRow {
Text(dayHeaderTitle(for: day).uppercased(with: locale))
.font(.system(size: 30, weight: .bold, design: .serif))
.foregroundColor(.mealMoodTextPrimary)
.multilineTextAlignment(.center)
.frame(width: 280)
.frame(minHeight: 138)
.background(dayHeaderBackground(day: day))
.clipShape(RoundedRectangle(cornerRadius: 12))
ForEach(mealTypes, id: \.self) { mealType in
Text(dishName(day: day, mealType: mealType))
.font(.system(size: 31, weight: .medium, design: .serif))
.foregroundColor(.mealMoodTextPrimary)
.multilineTextAlignment(.leading)
.lineLimit(3)
.frame(maxWidth: .infinity, minHeight: 138, alignment: .leading)
.padding(.horizontal, 16)
.background(Color(hex: "#FFFDFB"))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color(hex: "#ECDDD5"), lineWidth: 1)
)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
}
}
}
.padding(.horizontal, 24)
.padding(.vertical, 22)
}
}
private var verticalLayout: some View {
ZStack {
RoundedRectangle(cornerRadius: 30)
.fill(Color.white.opacity(0.95))
.overlay(
RoundedRectangle(cornerRadius: 30)
.stroke(Color.mealMoodMint.opacity(0.45), lineWidth: 2)
)
VStack(spacing: 16) {
ForEach(Array(dayRange), id: \.self) { day in
VStack(alignment: .leading, spacing: 10) {
Text(dayHeaderTitle(for: day).uppercased(with: locale))
.font(.custom("Didot", size: 50))
.foregroundColor(.mealMoodTextPrimary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background(dayHeaderBackground(day: day))
.clipShape(RoundedRectangle(cornerRadius: 12))
VStack(alignment: .leading, spacing: 8) {
ForEach(mealTypes, id: \.self) { mealType in
HStack(alignment: .top, spacing: 10) {
Text("\(mealTypeLabel(mealType)):")
.font(.system(size: 34, weight: .bold, design: .serif))
.foregroundColor(Color(hex: "#385549"))
Text(dishName(day: day, mealType: mealType))
.font(.system(size: 35, weight: .regular, design: .serif))
.foregroundColor(.mealMoodTextPrimary)
.lineLimit(2)
.minimumScaleFactor(0.7)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 6)
}
.padding(.horizontal, 16)
.padding(.vertical, 14)
.background(Color.white)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(Color(hex: "#EEE4DE"), lineWidth: 1)
)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
}
.padding(.horizontal, 22)
.padding(.vertical, 20)
}
}
private var footerBlock: some View {
@@ -211,13 +419,48 @@ struct WeekPlanShareView: View {
}
.padding(.horizontal, 26)
.padding(.vertical, 18)
.background(Color.white.opacity(0.8))
.background(Color.white.opacity(0.82))
.clipShape(RoundedRectangle(cornerRadius: 22))
.overlay(
RoundedRectangle(cornerRadius: 22)
.stroke(Color(hex: "#DDEEE6"), lineWidth: 1)
)
.shadow(color: Color(hex: "#7F9E90").opacity(0.14), radius: 16, x: 0, y: 8)
}
private func dayShortTitle(for day: Int) -> String {
let dayDate = plan.weekStartDate.addingDays(day)
let formatter = DateFormatter()
formatter.locale = locale
formatter.setLocalizedDateFormatFromTemplate("EEE d")
return formatter.string(from: dayDate).capitalized(with: locale)
}
private func dayLongTitle(for day: Int) -> String {
let dayDate = plan.weekStartDate.addingDays(day)
let formatter = DateFormatter()
formatter.locale = locale
formatter.setLocalizedDateFormatFromTemplate("EEEE")
return formatter.string(from: dayDate).capitalized(with: locale)
}
private func dayHeaderTitle(for day: Int) -> String {
let dayDate = plan.weekStartDate.addingDays(day)
let formatter = DateFormatter()
formatter.locale = locale
formatter.setLocalizedDateFormatFromTemplate("EEEE d")
return formatter.string(from: dayDate).capitalized(with: locale)
}
private func mealTypeLabel(_ mealType: MealType) -> String {
mealType == .lunch ? String(localized: "lunch") : String(localized: "dinner")
}
private func dishName(day: Int, mealType: MealType) -> String {
let slot = plan.slots.first { $0.dayOfWeek == day && $0.mealType == mealType.rawValue }
guard let slot, let dishId = slot.dishId, let dish = dishes.first(where: { $0.id == dishId }) else {
return ""
}
return dish.name
}
private func gridHeaderCell(_ title: String, day: Int?) -> some View {
@@ -45,6 +45,15 @@ struct SettingsView: View {
set: { settings.includeWeekends = $0 }
))
.tint(.mealMoodCoral)
Picker("settings_export_style", selection: Binding(
get: { settings.weekExportStyleEnum },
set: { settings.weekExportStyleEnum = $0 }
)) {
ForEach(WeekExportStyle.allCases, id: \.self) { style in
Text(LocalizedStringKey(style.localizedKey)).tag(style)
}
}
} header: {
Label("settings_planning", systemImage: "fork.knife")
}
+140
View File
@@ -0,0 +1,140 @@
#!/bin/zsh
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
PROJECT_PATH="${PROJECT_PATH:-$ROOT_DIR/MealMood.xcodeproj}"
SCHEME="${SCHEME:-MealMood}"
CONFIGURATION="${CONFIGURATION:-Release}"
ARCHIVE_DIR="${ARCHIVE_DIR:-$ROOT_DIR/build/appstore}"
ARCHIVE_PATH="${ARCHIVE_PATH:-$ARCHIVE_DIR/${SCHEME}.xcarchive}"
EXPORT_PATH="${EXPORT_PATH:-$ARCHIVE_DIR/export}"
EXPORT_OPTIONS_PLIST="${EXPORT_OPTIONS_PLIST:-$ARCHIVE_DIR/ExportOptions.plist}"
KEY_ID="${APPSTORE_KEY_ID:-YJLSGZGP79}"
ISSUER_ID="${APPSTORE_ISSUER_ID:-33f82bb1-0dbd-4b3f-a0ef-1a863dfd9453}"
TEAM_ID="${DEVELOPMENT_TEAM_ID:-2825Q76T7H}"
P8_PATH="${APPSTORE_P8_PATH:-}"
P8_DIR=""
fail() {
echo "Error: $*" >&2
exit 1
}
find_p8_file() {
if [[ -n "$P8_PATH" ]]; then
[[ -f "$P8_PATH" ]] || fail "No existe el fichero APPSTORE_P8_PATH=$P8_PATH"
echo "$P8_PATH"
return
fi
local default_path="$HOME/AuthKey_${KEY_ID}.p8"
if [[ -f "$default_path" ]]; then
echo "$default_path"
return
fi
local matches=("${HOME}"/*.p8(N))
if (( ${#matches[@]} == 1 )); then
echo "${matches[1]}"
return
fi
if (( ${#matches[@]} > 1 )); then
fail "Hay varios ficheros .p8 en $HOME. Define APPSTORE_P8_PATH con la ruta correcta."
fi
fail "No se encontro AuthKey_${KEY_ID}.p8 ni un unico fichero .p8 en $HOME."
}
create_export_options() {
mkdir -p "$ARCHIVE_DIR"
cat > "$EXPORT_OPTIONS_PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>destination</key>
<string>upload</string>
<key>manageAppVersionAndBuildNumber</key>
<false/>
<key>method</key>
<string>app-store-connect</string>
<key>signingStyle</key>
<string>automatic</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>${TEAM_ID}</string>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>
EOF
}
archive_app() {
echo "==> Archivando ${SCHEME}"
xcodebuild \
-project "$PROJECT_PATH" \
-scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-archivePath "$ARCHIVE_PATH" \
-destination "generic/platform=iOS" \
clean archive
}
export_ipa() {
echo "==> Exportando IPA"
xcodebuild \
-exportArchive \
-archivePath "$ARCHIVE_PATH" \
-exportPath "$EXPORT_PATH" \
-exportOptionsPlist "$EXPORT_OPTIONS_PLIST"
}
upload_ipa() {
local ipa
if [[ ! -d "$EXPORT_PATH" ]]; then
echo "==> Xcode ha subido el build directamente a App Store Connect"
return
fi
ipa="$(find "$EXPORT_PATH" -maxdepth 1 -name '*.ipa' -print -quit)"
if [[ -z "$ipa" ]]; then
echo "==> No hay .ipa exportado. La subida probablemente ya se ha completado desde xcodebuild."
return
fi
echo "==> Subiendo $(basename "$ipa") a App Store Connect"
API_PRIVATE_KEYS_DIR="$P8_DIR" \
xcrun altool \
--upload-app \
--type ios \
--file "$ipa" \
--apiKey "$KEY_ID" \
--apiIssuer "$ISSUER_ID"
}
main() {
command -v xcodebuild >/dev/null 2>&1 || fail "xcodebuild no esta disponible"
command -v xcrun >/dev/null 2>&1 || fail "xcrun no esta disponible"
P8_PATH="$(find_p8_file)"
P8_DIR="$(dirname "$P8_PATH")"
echo "Usando proyecto: $PROJECT_PATH"
echo "Usando scheme: $SCHEME"
echo "Usando clave API: $KEY_ID"
echo "Usando fichero .p8: $P8_PATH"
rm -rf "$ARCHIVE_PATH" "$EXPORT_PATH"
create_export_options
archive_app
export_ipa
upload_ipa
echo "==> Proceso completado"
}
main "$@"