diff --git a/Makefile b/Makefile index 6f98d8f..a9d2d58 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help build up db-only logs db-clean prune +.PHONY: help build up db-only logs db-clean prune i18n lint format # Default target help: @@ -15,6 +15,7 @@ help: @echo " make logs - Show logs from all services" @echo " make db-clean - Stop & remove database container" @echo " make prune - Remove all containers, images, and volumes" + @echo " make i18n - Validate translation files against messages.json" @echo " make help - Show this help message" # Build the Docker images @@ -46,4 +47,21 @@ prune: @echo "Cleaning up all Docker resources..." docker compose down -v --rmi all +# Validate translation files +i18n: + @echo "Validating translation files..." + @if [ -n "$(FILE)" ]; then \ + ./scripts/i18n-check.sh $(FILE); \ + else \ + ./scripts/i18n-check.sh; \ + fi + +lint: + @echo "Linting the project..." + npm run lint + +format: + @echo "Formatting the project..." + npm run format + diff --git a/README.md b/README.md index 34cbef4..914e3c4 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,24 @@ Your app will be available at `http://localhost:5173`. You can use the Makefile Use the `database/seed.sql` if you want to populate your database with dummy data. +### i18n + +There is no proper i18n implemented, we have an `/i18n` folder with specific languages. To use an existing translation, just rename the language code JSON file to `messages.json` and you are ready to go. If you would like to add a new translation (which is really appreciated), just create a new `.json` file and add the translations from the `messages.json`. + +The project includes a translation validation script to ensure all translation files are complete and up-to-date with the source `messages.json` file. + +```bash +# Validate all translation files +make i18n +``` + +```bash +# Validate a specific translation file +make i18n FILE=src/lib/i18n/it.json +``` + ### License -This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details. +This project is licensed under the `AGPL-3.0 License` - see the [LICENSE](./LICENSE) file for details. **Made with ❤️ by @polaroi8d** diff --git a/scripts/i18n-check.sh b/scripts/i18n-check.sh new file mode 100755 index 0000000..865e69a --- /dev/null +++ b/scripts/i18n-check.sh @@ -0,0 +1,191 @@ +#!/bin/bash + +# Translation validation script +# Compares a translation file against the source messages.json to find missing keys + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Default paths +SOURCE_FILE="src/lib/i18n/messages.json" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +# Function to show usage +show_usage() { + echo "Usage: $0 [LANGUAGE_FILE]" + echo "" + echo "Validates a translation file against the source messages.json" + echo "" + echo "Arguments:" + echo " LANGUAGE_FILE Path to the translation file to validate (e.g., src/lib/i18n/it.json)" + echo "" + echo "Examples:" + echo " $0 src/lib/i18n/it.json" + echo " $0 src/lib/i18n/fr.json" + echo "" + echo "If no file is provided, it will check all .json files in src/lib/i18n/ except messages.json" +} + +# Function to get all keys from a JSON file recursively +get_keys() { + local file="$1" + local prefix="$2" + + # Use jq to extract all keys recursively + jq -r 'paths(scalars) as $p | $p | join(".")' "$file" | while read -r key; do + if [ -n "$prefix" ]; then + echo "${prefix}.${key}" + else + echo "$key" + fi + done +} + +# Function to validate a single translation file +validate_file() { + local translation_file="$1" + local source_file="$2" + + echo -e "${YELLOW}Validating: $translation_file${NC}" + echo "----------------------------------------" + + # Check if files exist + if [ ! -f "$source_file" ]; then + echo -e "${RED}Error: Source file $source_file not found${NC}" + return 1 + fi + + if [ ! -f "$translation_file" ]; then + echo -e "${RED}Error: Translation file $translation_file not found${NC}" + return 1 + fi + + # Get all keys from source file + local source_keys + source_keys=$(get_keys "$source_file") + + # Get all keys from translation file + local translation_keys + translation_keys=$(get_keys "$translation_file") + + # Find missing keys + local missing_keys + missing_keys=$(comm -23 <(echo "$source_keys" | sort) <(echo "$translation_keys" | sort)) + + # Find extra keys (in translation but not in source) + local extra_keys + extra_keys=$(comm -13 <(echo "$source_keys" | sort) <(echo "$translation_keys" | sort)) + + # Count missing and extra keys + local missing_count + if [ -z "$missing_keys" ]; then + missing_count=0 + else + missing_count=$(echo "$missing_keys" | wc -l | tr -d ' ') + fi + + local extra_count + if [ -z "$extra_keys" ]; then + extra_count=0 + else + extra_count=$(echo "$extra_keys" | wc -l | tr -d ' ') + fi + + # Report results + if [ "$missing_count" -eq 0 ] && [ "$extra_count" -eq 0 ]; then + echo -e "${GREEN} Perfect! All keys match.${NC}" + return 0 + fi + + if [ "$missing_count" -gt 0 ]; then + echo -e "${RED} Missing $missing_count key(s) in translation:${NC}" + echo "$missing_keys" | while read -r key; do + echo -e " ${RED}• $key${NC}" + done + echo "" + fi + + if [ "$extra_count" -gt 0 ]; then + echo -e "${YELLOW} Extra $extra_count key(s) in translation (not in source):${NC}" + echo "$extra_keys" | while read -r key; do + echo -e " ${YELLOW}• $key${NC}" + done + echo "" + fi + + # Return error code if there are missing keys + if [ "$missing_count" -gt 0 ]; then + return 1 + fi + + return 0 +} + +# Main function +main() { + local translation_file="$1" + local source_file="$PROJECT_ROOT/$SOURCE_FILE" + local exit_code=0 + + # Change to project root directory + cd "$PROJECT_ROOT" + + # If no file specified, check all translation files + if [ -z "$translation_file" ]; then + echo -e "${YELLOW}No file specified. Checking all translation files...${NC}" + echo "" + + # Find all .json files in i18n directory except messages.json + local files + files=$(find src/lib/i18n -name "*.json" -not -name "messages.json" 2>/dev/null || true) + + if [ -z "$files" ]; then + echo -e "${YELLOW}No translation files found in src/lib/i18n/${NC}" + return 0 + fi + + # Validate each file + echo "$files" | while read -r file; do + if [ -n "$file" ]; then + if ! validate_file "$file" "$source_file"; then + exit_code=1 + fi + echo "" + fi + done + + return $exit_code + fi + + # Validate the specified file + if ! validate_file "$translation_file" "$source_file"; then + exit_code=1 + fi + + return $exit_code +} + +# Check if jq is installed +if ! command -v jq &> /dev/null; then + echo -e "${RED}Error: jq is required but not installed.${NC}" + echo "Please install jq:" + echo " macOS: brew install jq" + echo " Ubuntu/Debian: sudo apt-get install jq" + echo " CentOS/RHEL: sudo yum install jq" + exit 1 +fi + +# Handle help flag +if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then + show_usage + exit 0 +fi + +# Run main function +main "$1" diff --git a/src/lib/i18n/hu.json b/src/lib/i18n/hu.json new file mode 100644 index 0000000..f6f6f9f --- /dev/null +++ b/src/lib/i18n/hu.json @@ -0,0 +1,259 @@ +{ + "common": { + "required": "*", + "cancel": "Annulla", + "create": "Crea", + "edit": "Modifica", + "delete": "Elimina", + "view": "Visualizza", + "home": "Home", + "loading": "Caricamento...", + "error": "Errore", + "success": "Successo", + "name": "Nome", + "date": "Data", + "time": "Ora", + "location": "Luogo", + "locationType": "Tipo di Luogo", + "locationNone": "Nessuno", + "locationText": "Testo", + "locationMaps": "Google Maps", + "locationNoneDescription": "Nessun luogo specificato", + "locationTextDescription": "Inserisci il luogo come testo semplice.", + "locationMapsDescription": "Inserisci il link di Google Maps.", + "googleMapsUrl": "URL di Google Maps", + "googleMapsUrlPlaceholder": "https://maps.google.com/...", + "type": "Tipo", + "visibility": "Visibilità", + "public": "Pubblico", + "private": "Privato", + "limited": "Limitato", + "unlimited": "Illimitato", + "capacity": "Capacità", + "attendees": "Partecipanti", + "attendeeLimit": "Limite di Partecipanti", + "enterLimit": "Inserisci il limite", + "enterEventName": "Inserisci il nome dell'evento", + "enterLocation": "Inserisci il luogo", + "enterYourName": "Inserisci il tuo nome", + "enterNumberOfGuests": "Inserisci il numero di ospiti", + "yourName": "Il tuo nome", + "numberOfGuests": "Numero di Ospiti", + "addGuests": "Aggiungi ospiti", + "joinEvent": "Partecipa all'Evento", + "copyLink": "Copia Link", + "addToCalendar": "Aggiungi al Calendario", + "close": "Chiudi", + "closeModal": "Chiudi finestra", + "removeRSVP": "Rimuovi RSVP", + "updating": "Aggiornamento...", + "creating": "Creazione...", + "adding": "Aggiunta...", + "updateEvent": "Aggiorna Evento", + "createEvent": "Crea Evento", + "createNewEvent": "Crea Nuovo Evento", + "createYourFirstEvent": "Crea il Tuo Primo Evento", + "editEvent": "Modifica Evento", + "deleteEvent": "Elimina Evento", + "myEvents": "I Miei Eventi", + "discover": "Scopri", + "noEventsYet": "Ancora Nessun Evento", + "noPublicEventsYet": "Ancora Nessun Evento Pubblico", + "noAttendeesYet": "Ancora nessun partecipante", + "beFirstToJoin": "Sii il primo a partecipare!", + "eventNotFound": "Evento Non Trovato", + "eventIsFull": "L'Evento è Pieno!", + "maximumCapacityReached": "Raggiunta la capacità massima", + "eventLinkCopied": "Link dell'evento copiato negli appunti!", + "rsvpAddedSuccessfully": "RSVP aggiunto con successo!", + "removedRsvpSuccessfully": "RSVP rimosso con successo.", + "anUnexpectedErrorOccurred": "Si è verificato un errore inaspettato.", + "somethingWentWrong": "Qualcosa è andato storto. Riprova.", + "failedToAddRsvp": "Impossibile aggiungere RSVP", + "failedToRemoveRsvp": "Impossibile rimuovere RSVP", + "failedToDeleteEvent": "Impossibile eliminare l'evento", + "youMayNotHavePermission": "Potresti non avere il permesso di eliminare questo evento.", + "anErrorOccurredWhileDeleting": "Si è verificato un errore durante l'eliminazione dell'evento:", + "databaseUnreachable": "Database non raggiungibile.", + "eventIdNotFound": "EventId non trovato", + "eventNotExists": "Evento non trovato", + "failedToLoadEvent": "Impossibile caricare l'evento", + "nameAndUserIdRequired": "Nome e ID utente sono obbligatori", + "eventCapacityExceeded": "Capacità dell'evento superata. Stai cercando di aggiungere {guests} partecipanti (te compreso/a), ma rimangono solo {remaining} posti.", + "nameAlreadyExists": "Il nome esiste già per questo evento", + "missingOrEmptyFields": "Campi mancanti o vuoti: {fields}", + "dateCannotBeInPast": "La data non può essere nel passato.", + "limitMustBeAtLeast2": "Il limite deve essere almeno 2 per eventi limitati.", + "unauthorized": "Non autorizzato", + "youCanOnlyEditYourOwnEvents": "Puoi modificare solo i tuoi eventi", + "youDoNotHavePermissionToDelete": "Non hai il permesso di eliminare questo evento", + "eventIdAndUserIdRequired": "ID evento e ID utente sono obbligatori", + "guestsWillBeAddedAs": "Gli ospiti verranno aggiunti come \"Ospite #1 di {name}\", \"Ospite #2 di {name}\", ecc.", + "yourNamePlaceholder": "Il tuo nome", + "atTime": "alle" + }, + "navigation": { + "home": "Home", + "discover": "Scopri", + "create": "Crea", + "myEvents": "I Miei Eventi" + }, + "home": { + "title": "Cactoide - Il sito per gli RSVP", + "description": "Crea e gestisci gli RSVP degli eventi. Nessuna registrazione richiesta, condivisione immediata.", + "mainTitle": "Cactoide(ea)", + "subtitle": "La Piattaforma Definitiva per gli RSVP", + "tagline": "Crea, condividi e gestisci eventi senza intoppi.", + "whyCactoideTitle": "Perché Cactoide(ae)? 🌵", + "whyCactoideDescription": "Come il cactus, i grandi eventi fioriscono in ogni condizione se gestiti con cura. Cactoide(ae) ti aiuta a semplificare gli RSVP, coordinare in modo semplice e mantenere ogni dettaglio efficiente: così i tuoi incontri sono resilienti, vivaci e indimenticabili.", + "createEventNow": "Crea Evento Ora", + "discoverPublicEventsTitle": "Scopri Eventi Pubblici", + "discoverPublicEventsDescription": "Guarda cosa stanno pianificando gli altri e lasciati ispirare", + "browseAllPublicEvents": "Sfoglia Tutti gli Eventi Pubblici", + "whyCactoideFeatureTitle": "Perché Cactoide?", + "instantEventCreationTitle": "Creazione Istantanea di Eventi", + "instantEventCreationDescription": "Crea eventi in pochi secondi con il nostro modulo semplificato. Nessun account, nessuna attesa, solo pura efficienza.", + "oneClickSharingTitle": "Condivisione con un Clic", + "oneClickSharingDescription": "Ogni evento ottiene un URL unico e memorabile. Condividi istantaneamente tramite qualsiasi piattaforma o app di messaggistica.", + "allInOneClarityTitle": "Chiarezza Tutto-in-Uno", + "allInOneClarityDescription": "Niente più scorrimento infinito tra chat e reazioni. Visualizza la disponibilità e le risposte di tutti in un unico posto.", + "noHassleNoSignUpsTitle": "Nessun Problema, Nessuna Registrazione", + "noHassleNoSignUpsDescription": "Salta le registrazioni e i moduli infiniti. A differenza di altre piattaforme di eventi, crei e condividi istantaneamente: nessun account, nessuna barriera.", + "smartLimitsTitle": "Limiti Intelligenti", + "smartLimitsDescription": "Scegli tra RSVP illimitati o imposta una capacità limitata. Perfetto per eventi di qualsiasi dimensione.", + "effortlessSimplicityTitle": "Semplicità Senza Sforzo", + "effortlessSimplicityDescription": "Progettato per essere immediatamente chiaro e facile. Nessuna curva di apprendimento: apri, crea e vai.", + "howItWorksTitle": "Come Funziona", + "step1Title": "Crea Evento", + "step1Description": "Compila un semplice modulo con i dettagli dell'evento. Scegli tra capacità limitata o illimitata.", + "step2Title": "Ottieni URL Unico", + "step2Description": "Ricevi un URL casuale e memorabile per il tuo evento. Perfetto per la condivisione ovunque.", + "step3Title": "Raccogli gli RSVP", + "step3Description": "Le persone visitano il tuo link e partecipano solo con il loro nome. Nessun account necessario.", + "ctaTitle": "Pronto a Creare il Tuo Primo Evento?", + "ctaDescription": "Unisciti a migliaia di organizzatori di eventi che si fidano di Cactoide", + "ctaButton": "Crea" + }, + "create": { + "title": "Crea Evento - Cactoide", + "formTitle": "Crea Nuovo Evento", + "eventNameLabel": "Nome", + "eventNamePlaceholder": "Inserisci il nome dell'evento", + "dateLabel": "Data", + "timeLabel": "Ora", + "locationLabel": "Luogo", + "locationPlaceholder": "Inserisci il luogo", + "locationTypeLabel": "Tipo di Luogo", + "locationNoneOption": "Nessuno", + "locationTextOption": "Testo Semplice", + "locationMapsOption": "Google Maps", + "locationNoneDescription": "Nessun luogo specificato.", + "locationTextDescription": "Inserisci il luogo come testo semplice.", + "locationMapsDescription": "Inserisci il link di Google Maps.", + "googleMapsUrlLabel": "URL di Google Maps", + "googleMapsUrlPlaceholder": "https://maps.google.com/...", + "typeLabel": "Tipo", + "unlimitedOption": "Illimitato", + "limitedOption": "Limitato", + "attendeeLimitLabel": "Limite di Partecipanti", + "attendeeLimitPlaceholder": "Inserisci il limite", + "visibilityLabel": "Visibilità", + "publicOption": "🌍 Pubblico", + "privateOption": "🔒 Privato", + "publicDescription": "Gli eventi pubblici sono visibili a tutti e possono essere scoperti da altri.", + "privateDescription": "Gli eventi privati sono visibili solo a te e alle persone con cui condividi il link.", + "creatingEvent": "Creazione Evento...", + "createEventButton": "Crea Evento" + }, + "event": { + "title": "{eventName} - Cactoide", + "eventTitle": "Evento - Cactoide", + "editTitle": "Modifica Evento - {eventName} - Cactoide", + "myEventsTitle": "I Miei Eventi - Cactoide", + "eventNotFoundTitle": "Evento Non Trovato", + "eventNotFoundDescription": "L'evento che stai cercando non esiste o è stato rimosso.", + "joinThisEvent": "Partecipa a Questo Evento", + "eventIsFull": "L'Evento è Pieno!", + "maximumCapacityReached": "Raggiunta la capacità massima", + "yourNameLabel": "Il tuo nome", + "yourNamePlaceholder": "Inserisci il tuo nome", + "addGuestsLabel": "Aggiungi ospiti", + "numberOfGuestsLabel": "Numero di Ospiti", + "numberOfGuestsPlaceholder": "Inserisci il numero di ospiti", + "guestsWillBeAddedAs": "Gli ospiti verranno aggiunti come \"Ospite #1 di {name}\", \"Ospite #2 di {name}\", ecc.", + "joinEventButton": "Partecipa all'Evento", + "joinEventWithGuests": "Partecipa all'Evento + {count} Ospite{plural}", + "adding": "Aggiunta...", + "attendeesTitle": "Partecipanti", + "noAttendeesYet": "Ancora nessun partecipante", + "beFirstToJoin": "Sii il primo a partecipare!", + "copyLinkButton": "Copia Link", + "addToCalendarButton": "Aggiungi al Calendario", + "eventLinkCopied": "Link dell'evento copiato negli appunti!", + "rsvpAddedSuccessfully": "RSVP aggiunto con successo!", + "removedRsvpSuccessfully": "RSVP rimosso con successo.", + "failedToAddRsvp": "Impossibile aggiungere RSVP", + "failedToRemoveRsvp": "Impossibile rimuovere RSVP", + "editEventTitle": "Modifica Evento", + "editEventDescription": "Aggiorna i dettagli del tuo evento", + "updatingEvent": "Aggiornamento...", + "updateEventButton": "Aggiorna Evento", + "myEventsDescription": "Gestisci i tuoi eventi creati", + "noEventsYetTitle": "Ancora Nessun Evento", + "noEventsYetDescription": "Non hai ancora creato nessun evento. Inizia creando il tuo primo evento!", + "createYourFirstEventButton": "Crea il Tuo Primo Evento", + "deleteEventTitle": "Elimina Evento", + "deleteEventDescription": "Sei sicuro di voler eliminare \"{eventName}\"? Questa azione non può essere annullata e rimuoverà tutti gli RSVP.", + "deleteButton": "Elimina", + "viewEventAriaLabel": "Visualizza evento", + "editEventAriaLabel": "Modifica evento", + "deleteEventAriaLabel": "Elimina evento", + "removeRsvpAriaLabel": "Rimuovi RSVP" + }, + "discover": { + "title": "Scopri Eventi - Cactoide", + "noPublicEventsTitle": "Ancora Nessun Evento Pubblico", + "noPublicEventsDescription": "Al momento non ci sono eventi pubblici disponibili. Sii il primo a crearne uno!", + "createButton": "Crea", + "publicEventsTitle": "Eventi Pubblici ({count})", + "publicEventsDescription": "Scopri eventi creati dalla comunità", + "searchPlaceholder": "Cerca eventi per nome, luogo...", + "typeFilterUnlimited": "Illimitati", + "statusFilterLabel": "Stato:", + "statusFilterAll": "Tutti gli eventi", + "statusFilterUpcoming": "Eventi imminenti", + "statusFilterPast": "Eventi passati", + "timeFilterLabel": "Orario:", + "timeFilterAny": "Qualsiasi orario", + "timeFilterNextWeek": "Prossima settimana", + "timeFilterNextMonth": "Prossimo mese", + "sortOrderLabel": "Ordina:", + "sortOrderEarliest": "Prima i più vicini", + "sortOrderLatest": "Prima i più recenti", + "viewButton": "Visualizza", + "noEventsFoundTitle": "Nessun evento trovato", + "noEventsFoundDescription": "Prova a modificare i termini di ricerca o sfoglia tutti gli eventi" + }, + "calendar": { + "addToCalendarTitle": "Aggiungi al Calendario", + "googleCalendarTitle": "Google Calendar", + "googleCalendarDescription": "Aggiungi a Google Calendar", + "microsoftOutlookTitle": "Microsoft Outlook", + "microsoftOutlookDescription": "Aggiungi a Outlook Calendar", + "downloadICalTitle": "Scarica File iCal", + "downloadICalDescription": "Scarica file .ics per qualsiasi app di calendario" + }, + "errors": { + "title": "Errore - Cactoide", + "errorTitle": "Errore", + "anUnexpectedErrorOccurred": "Si è verificato un errore inaspettato.", + "homeButton": "Home" + }, + "layout": { + "defaultTitle": "Cactoide -", + "defaultDescription": "Crea e gestisci gli RSVP degli eventi", + "userIdCookieText": "Il tuo UserID memorizzato come cookie:", + "firstTimeVisiting": "Prima visita. Generazione di un nuovo UserID...", + "copyright": "© 2025 Cactoide" + } +} diff --git a/src/lib/i18n/it.json b/src/lib/i18n/it.json new file mode 100644 index 0000000..cfba74f --- /dev/null +++ b/src/lib/i18n/it.json @@ -0,0 +1,264 @@ +{ + "common": { + "required": "*", + "cancel": "Annulla", + "create": "Crea", + "edit": "Modifica", + "delete": "Elimina", + "view": "Visualizza", + "home": "Home", + "loading": "Caricamento...", + "error": "Errore", + "success": "Successo", + "name": "Nome", + "date": "Data", + "time": "Ora", + "location": "Luogo", + "locationType": "Tipo di Luogo", + "locationNone": "Nessuno", + "locationText": "Testo", + "locationMaps": "Google Maps", + "locationNoneDescription": "Nessun luogo specificato", + "locationTextDescription": "Inserisci il luogo come testo semplice.", + "locationMapsDescription": "Inserisci il link di Google Maps.", + "googleMapsUrl": "URL di Google Maps", + "googleMapsUrlPlaceholder": "https://maps.google.com/...", + "type": "Tipo", + "visibility": "Visibilità", + "public": "Pubblico", + "private": "Privato", + "limited": "Limitato", + "unlimited": "Illimitato", + "capacity": "Capacità", + "attendees": "Partecipanti", + "attendeeLimit": "Limite di Partecipanti", + "enterLimit": "Inserisci il limite", + "enterEventName": "Inserisci il nome dell'evento", + "enterLocation": "Inserisci il luogo", + "enterYourName": "Inserisci il tuo nome", + "enterNumberOfGuests": "Inserisci il numero di ospiti", + "yourName": "Il tuo nome", + "numberOfGuests": "Numero di Ospiti", + "addGuests": "Aggiungi ospiti", + "joinEvent": "Partecipa all'Evento", + "copyLink": "Copia Link", + "addToCalendar": "Aggiungi al Calendario", + "close": "Chiudi", + "closeModal": "Chiudi finestra", + "removeRSVP": "Rimuovi RSVP", + "updating": "Aggiornamento...", + "creating": "Creazione...", + "adding": "Aggiunta...", + "updateEvent": "Aggiorna Evento", + "createEvent": "Crea Evento", + "createNewEvent": "Crea Nuovo Evento", + "createYourFirstEvent": "Crea il Tuo Primo Evento", + "editEvent": "Modifica Evento", + "deleteEvent": "Elimina Evento", + "myEvents": "I Miei Eventi", + "discover": "Scopri", + "noEventsYet": "Ancora Nessun Evento", + "noPublicEventsYet": "Ancora Nessun Evento Pubblico", + "noAttendeesYet": "Ancora nessun partecipante", + "beFirstToJoin": "Sii il primo a partecipare!", + "eventNotFound": "Evento Non Trovato", + "eventIsFull": "L'Evento è Pieno!", + "maximumCapacityReached": "Raggiunta la capacità massima", + "eventLinkCopied": "Link dell'evento copiato negli appunti!", + "rsvpAddedSuccessfully": "RSVP aggiunto con successo!", + "removedRsvpSuccessfully": "RSVP rimosso con successo.", + "anUnexpectedErrorOccurred": "Si è verificato un errore inaspettato.", + "somethingWentWrong": "Qualcosa è andato storto. Riprova.", + "failedToAddRsvp": "Impossibile aggiungere RSVP", + "failedToRemoveRsvp": "Impossibile rimuovere RSVP", + "failedToDeleteEvent": "Impossibile eliminare l'evento", + "youMayNotHavePermission": "Potresti non avere il permesso di eliminare questo evento.", + "anErrorOccurredWhileDeleting": "Si è verificato un errore durante l'eliminazione dell'evento:", + "databaseUnreachable": "Database non raggiungibile.", + "eventIdNotFound": "EventId non trovato", + "eventNotExists": "Evento non trovato", + "failedToLoadEvent": "Impossibile caricare l'evento", + "nameAndUserIdRequired": "Nome e ID utente sono obbligatori", + "eventCapacityExceeded": "Capacità dell'evento superata. Stai cercando di aggiungere {guests} partecipanti (te compreso/a), ma rimangono solo {remaining} posti.", + "nameAlreadyExists": "Il nome esiste già per questo evento", + "missingOrEmptyFields": "Campi mancanti o vuoti: {fields}", + "dateCannotBeInPast": "La data non può essere nel passato.", + "limitMustBeAtLeast2": "Il limite deve essere almeno 2 per eventi limitati.", + "unauthorized": "Non autorizzato", + "youCanOnlyEditYourOwnEvents": "Puoi modificare solo i tuoi eventi", + "youDoNotHavePermissionToDelete": "Non hai il permesso di eliminare questo evento", + "eventIdAndUserIdRequired": "ID evento e ID utente sono obbligatori", + "guestsWillBeAddedAs": "Gli ospiti verranno aggiunti come \"Ospite #1 di {name}\", \"Ospite #2 di {name}\", ecc.", + "yourNamePlaceholder": "Il tuo nome", + "atTime": "alle" + }, + "navigation": { + "home": "Home", + "discover": "Scopri", + "create": "Crea", + "myEvents": "I Miei Eventi" + }, + "home": { + "title": "Cactoide - Il sito per gli RSVP", + "description": "Crea e gestisci gli RSVP degli eventi. Nessuna registrazione richiesta, condivisione immediata.", + "mainTitle": "Cactoide(ea)", + "subtitle": "La Piattaforma Definitiva per gli RSVP", + "tagline": "Crea, condividi e gestisci eventi senza intoppi.", + "whyCactoideTitle": "Perché Cactoide(ae)? 🌵", + "whyCactoideDescription": "Come il cactus, i grandi eventi fioriscono in ogni condizione se gestiti con cura. Cactoide(ae) ti aiuta a semplificare gli RSVP, coordinare in modo semplice e mantenere ogni dettaglio efficiente: così i tuoi incontri sono resilienti, vivaci e indimenticabili.", + "createEventNow": "Crea Evento Ora", + "discoverPublicEventsTitle": "Scopri Eventi Pubblici", + "discoverPublicEventsDescription": "Guarda cosa stanno pianificando gli altri e lasciati ispirare", + "browseAllPublicEvents": "Sfoglia Tutti gli Eventi Pubblici", + "whyCactoideFeatureTitle": "Perché Cactoide?", + "instantEventCreationTitle": "Creazione Istantanea di Eventi", + "instantEventCreationDescription": "Crea eventi in pochi secondi con il nostro modulo semplificato. Nessun account, nessuna attesa, solo pura efficienza.", + "oneClickSharingTitle": "Condivisione con un Clic", + "oneClickSharingDescription": "Ogni evento ottiene un URL unico e memorabile. Condividi istantaneamente tramite qualsiasi piattaforma o app di messaggistica.", + "allInOneClarityTitle": "Chiarezza Tutto-in-Uno", + "allInOneClarityDescription": "Niente più scorrimento infinito tra chat e reazioni. Visualizza la disponibilità e le risposte di tutti in un unico posto.", + "noHassleNoSignUpsTitle": "Nessun Problema, Nessuna Registrazione", + "noHassleNoSignUpsDescription": "Salta le registrazioni e i moduli infiniti. A differenza di altre piattaforme di eventi, crei e condividi istantaneamente: nessun account, nessuna barriera.", + "smartLimitsTitle": "Limiti Intelligenti", + "smartLimitsDescription": "Scegli tra RSVP illimitati o imposta una capacità limitata. Perfetto per eventi di qualsiasi dimensione.", + "effortlessSimplicityTitle": "Semplicità Senza Sforzo", + "effortlessSimplicityDescription": "Progettato per essere immediatamente chiaro e facile. Nessuna curva di apprendimento: apri, crea e vai.", + "howItWorksTitle": "Come Funziona", + "step1Title": "Crea Evento", + "step1Description": "Compila un semplice modulo con i dettagli dell'evento. Scegli tra capacità limitata o illimitata.", + "step2Title": "Ottieni URL Unico", + "step2Description": "Ricevi un URL casuale e memorabile per il tuo evento. Perfetto per la condivisione ovunque.", + "step3Title": "Raccogli gli RSVP", + "step3Description": "Le persone visitano il tuo link e partecipano solo con il loro nome. Nessun account necessario.", + "ctaTitle": "Pronto a Creare il Tuo Primo Evento?", + "ctaDescription": "Unisciti a migliaia di organizzatori di eventi che si fidano di Cactoide", + "ctaButton": "Crea" + }, + "create": { + "title": "Crea Evento - Cactoide", + "formTitle": "Crea Nuovo Evento", + "eventNameLabel": "Nome", + "eventNamePlaceholder": "Inserisci il nome dell'evento", + "dateLabel": "Data", + "timeLabel": "Ora", + "locationLabel": "Luogo", + "locationPlaceholder": "Inserisci il luogo", + "locationTypeLabel": "Tipo di Luogo", + "locationNoneOption": "Nessuno", + "locationTextOption": "Testo Semplice", + "locationMapsOption": "Google Maps", + "locationNoneDescription": "Nessun luogo specificato.", + "locationTextDescription": "Inserisci il luogo come testo semplice.", + "locationMapsDescription": "Inserisci il link di Google Maps.", + "googleMapsUrlLabel": "URL di Google Maps", + "googleMapsUrlPlaceholder": "https://maps.google.com/...", + "typeLabel": "Tipo", + "unlimitedOption": "Illimitato", + "limitedOption": "Limitato", + "attendeeLimitLabel": "Limite di Partecipanti", + "attendeeLimitPlaceholder": "Inserisci il limite", + "visibilityLabel": "Visibilità", + "publicOption": "🌍 Pubblico", + "privateOption": "🔒 Privato", + "publicDescription": "Gli eventi pubblici sono visibili a tutti e possono essere scoperti da altri.", + "privateDescription": "Gli eventi privati sono visibili solo a te e alle persone con cui condividi il link.", + "creatingEvent": "Creazione Evento...", + "createEventButton": "Crea Evento" + }, + "event": { + "title": "{eventName} - Cactoide", + "eventTitle": "Evento - Cactoide", + "editTitle": "Modifica Evento - {eventName} - Cactoide", + "myEventsTitle": "I Miei Eventi - Cactoide", + "eventNotFoundTitle": "Evento Non Trovato", + "eventNotFoundDescription": "L'evento che stai cercando non esiste o è stato rimosso.", + "joinThisEvent": "Partecipa a Questo Evento", + "eventIsFull": "L'Evento è Pieno!", + "maximumCapacityReached": "Raggiunta la capacità massima", + "yourNameLabel": "Il tuo nome", + "yourNamePlaceholder": "Inserisci il tuo nome", + "addGuestsLabel": "Aggiungi ospiti", + "numberOfGuestsLabel": "Numero di Ospiti", + "numberOfGuestsPlaceholder": "Inserisci il numero di ospiti", + "guestsWillBeAddedAs": "Gli ospiti verranno aggiunti come \"Ospite #1 di {name}\", \"Ospite #2 di {name}\", ecc.", + "joinEventButton": "Partecipa all'Evento", + "joinEventWithGuests": "Partecipa all'Evento + {count} Ospite{plural}", + "adding": "Aggiunta...", + "attendeesTitle": "Partecipanti", + "noAttendeesYet": "Ancora nessun partecipante", + "beFirstToJoin": "Sii il primo a partecipare!", + "copyLinkButton": "Copia Link", + "addToCalendarButton": "Aggiungi al Calendario", + "eventLinkCopied": "Link dell'evento copiato negli appunti!", + "rsvpAddedSuccessfully": "RSVP aggiunto con successo!", + "removedRsvpSuccessfully": "RSVP rimosso con successo.", + "failedToAddRsvp": "Impossibile aggiungere RSVP", + "failedToRemoveRsvp": "Impossibile rimuovere RSVP", + "editEventTitle": "Modifica Evento", + "editEventDescription": "Aggiorna i dettagli del tuo evento", + "updatingEvent": "Aggiornamento...", + "updateEventButton": "Aggiorna Evento", + "myEventsDescription": "Gestisci i tuoi eventi creati", + "noEventsYetTitle": "Ancora Nessun Evento", + "noEventsYetDescription": "Non hai ancora creato nessun evento. Inizia creando il tuo primo evento!", + "createYourFirstEventButton": "Crea il Tuo Primo Evento", + "deleteEventTitle": "Elimina Evento", + "deleteEventDescription": "Sei sicuro di voler eliminare \"{eventName}\"? Questa azione non può essere annullata e rimuoverà tutti gli RSVP.", + "deleteButton": "Elimina", + "viewEventAriaLabel": "Visualizza evento", + "editEventAriaLabel": "Modifica evento", + "deleteEventAriaLabel": "Elimina evento", + "removeRsvpAriaLabel": "Rimuovi RSVP" + }, + "discover": { + "title": "Scopri Eventi - Cactoide", + "noPublicEventsTitle": "Ancora Nessun Evento Pubblico", + "noPublicEventsDescription": "Al momento non ci sono eventi pubblici disponibili. Sii il primo a crearne uno!", + "createButton": "Crea", + "publicEventsTitle": "Eventi Pubblici ({count})", + "publicEventsDescription": "Scopri eventi creati dalla comunità", + "searchPlaceholder": "Cerca eventi per nome, luogo...", + "searchInputAriaLabel": "Input di ricerca", + "toggleFiltersAriaLabel": "Attiva/Disattiva filtri", + "typeFilterLabel": "Tipo:", + "typeFilterAll": "Tutti", + "typeFilterLimited": "Limitati", + "typeFilterUnlimited": "Illimitati", + "statusFilterLabel": "Stato:", + "statusFilterAll": "Tutti gli eventi", + "statusFilterUpcoming": "Eventi imminenti", + "statusFilterPast": "Eventi passati", + "timeFilterLabel": "Orario:", + "timeFilterAny": "Qualsiasi orario", + "timeFilterNextWeek": "Prossima settimana", + "timeFilterNextMonth": "Prossimo mese", + "sortOrderLabel": "Ordina:", + "sortOrderEarliest": "Prima i più vicini", + "sortOrderLatest": "Prima i più recenti", + "viewButton": "Visualizza", + "noEventsFoundTitle": "Nessun evento trovato", + "noEventsFoundDescription": "Prova a modificare i termini di ricerca o sfoglia tutti gli eventi" + }, + "calendar": { + "addToCalendarTitle": "Aggiungi al Calendario", + "googleCalendarTitle": "Google Calendar", + "googleCalendarDescription": "Aggiungi a Google Calendar", + "microsoftOutlookTitle": "Microsoft Outlook", + "microsoftOutlookDescription": "Aggiungi a Outlook Calendar", + "downloadICalTitle": "Scarica File iCal", + "downloadICalDescription": "Scarica file .ics per qualsiasi app di calendario" + }, + "errors": { + "title": "Errore - Cactoide", + "errorTitle": "Errore", + "anUnexpectedErrorOccurred": "Si è verificato un errore inaspettato.", + "homeButton": "Home" + }, + "layout": { + "defaultTitle": "Cactoide -", + "defaultDescription": "Crea e gestisci gli RSVP degli eventi", + "userIdCookieText": "Il tuo UserID memorizzato come cookie:", + "firstTimeVisiting": "Prima visita. Generazione di un nuovo UserID...", + "copyright": "© 2025 Cactoide" + } +}