From f66fd03d70de5ddbdaaa8f732fd362dd944e3f5c Mon Sep 17 00:00:00 2001 From: Levente Orban Date: Tue, 16 Sep 2025 11:05:59 +0200 Subject: [PATCH] feat: Add translation support --- src/lib/components/CalendarModal.svelte | 19 +- src/lib/components/Navbar.svelte | 9 +- src/lib/i18n/i18n.ts | 55 ++++++ src/lib/i18n/messages.json | 246 ++++++++++++++++++++++++ src/routes/+error.svelte | 9 +- src/routes/+layout.svelte | 14 +- src/routes/+page.svelte | 81 ++++---- src/routes/create/+page.svelte | 48 ++--- src/routes/discover/+page.svelte | 75 +++++--- src/routes/event/+page.svelte | 38 ++-- src/routes/event/[id]/+page.svelte | 61 +++--- src/routes/event/[id]/edit/+page.svelte | 49 ++--- 12 files changed, 517 insertions(+), 187 deletions(-) create mode 100644 src/lib/i18n/i18n.ts create mode 100644 src/lib/i18n/messages.json diff --git a/src/lib/components/CalendarModal.svelte b/src/lib/components/CalendarModal.svelte index 380360b..7481b79 100644 --- a/src/lib/components/CalendarModal.svelte +++ b/src/lib/components/CalendarModal.svelte @@ -6,6 +6,7 @@ addToOutlookCalendar, downloadICalFile } from '../calendarHelpers.js'; + import { t } from '$lib/i18n/i18n.js'; export let isOpen: boolean = false; export let event: CalendarEvent; @@ -66,11 +67,13 @@ >
-

Add to Calendar

+

+ {t('calendar.addToCalendarTitle')} +

-

Google Calendar

-

Add to Google Calendar

+

{t('calendar.googleCalendarTitle')}

+

{t('calendar.googleCalendarDescription')}

@@ -129,8 +132,8 @@
-

Microsoft Outlook

-

Add to Outlook Calendar

+

{t('calendar.microsoftOutlookTitle')}

+

{t('calendar.microsoftOutlookDescription')}

@@ -156,8 +159,8 @@
-

Download iCal File

-

Download .ics file for any calendar app

+

{t('calendar.downloadICalTitle')}

+

{t('calendar.downloadICalDescription')}

diff --git a/src/lib/components/Navbar.svelte b/src/lib/components/Navbar.svelte index 19a6866..49d956c 100644 --- a/src/lib/components/Navbar.svelte +++ b/src/lib/components/Navbar.svelte @@ -1,6 +1,7 @@ - Error - Cactoide + {t('errors.title')}
@@ -15,10 +16,10 @@
🚨
-

Error

+

{t('errors.errorTitle')}

- {error?.message || 'An unexpected error occurred.'} + {error?.message || t('errors.anUnexpectedErrorOccurred')}

@@ -26,7 +27,7 @@ on:click={() => goto('/')} class="border-white-500 bg-white-400/20 mt-2 w-48 rounded-sm border px-6 py-3 font-semibold text-white duration-400 hover:scale-110 hover:bg-white/10" > - Home + {t('errors.homeButton')}
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 3f4c1a3..554cf59 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,13 +1,14 @@ - Cactoide - - + {t('layout.defaultTitle')} + @@ -28,13 +29,12 @@

- Your UserID storated as a cookie: {data.cactoideUserId - ? data.cactoideUserId - : 'First time visiting. Generating new UserID...'}{data.cactoideUserId ? data.cactoideUserId : t('layout.firstTimeVisiting')}

-

© 2025 Cactoide

+

{t('layout.copyright')}

diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index f98070f..888524a 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,45 +1,41 @@ - Cactoide - The RSVP site - + {t('home.title')} +

- Cactoide(ea)* 🌡

-

The Ultimate RSVP Platform

+

{t('home.subtitle')}

- Create, share, and manage events with zero friction. + {t('home.tagline')}

- Why Cactoide(ae)*?🌡

- Like the cactus, great events bloom under any condition when managed with care. Cactoide(ae) - helps you streamline RSVPs, simplify coordination, and keep every detail efficientβ€”so your - gatherings are resilient, vibrant, and unforgettable. + {t('home.whyCactoideDescription')}

@@ -48,8 +44,8 @@
-

Discover Public Events

-

See what others are planning and get inspired

+

{t('home.discoverPublicEventsTitle')}

+

{t('home.discoverPublicEventsDescription')}

@@ -57,7 +53,7 @@ on:click={() => goto('/discover')} class="rounded-sm border-2 border-violet-500 px-8 py-4 font-bold duration-400 hover:scale-110 hover:bg-violet-500/10" > - Browse All Public Events + {t('home.browseAllPublicEvents')}
@@ -67,7 +63,7 @@

- Why Cactoide? + {t('home.whyCactoideFeatureTitle')}

@@ -75,10 +71,9 @@
🎯
-

Instant Event Creation

+

{t('home.instantEventCreationTitle')}

- Create events in seconds with our streamlined form. No accounts, no waiting, just pure - efficiency. + {t('home.instantEventCreationDescription')}

@@ -87,10 +82,9 @@
πŸ”—
-

One-Click Sharing

+

{t('home.oneClickSharingTitle')}

- Each event gets a unique, memorable URL. Share instantly via any platform or messaging - app. + {t('home.oneClickSharingDescription')}

@@ -99,10 +93,9 @@
πŸ”
-

All-in-One Clarity

+

{t('home.allInOneClarityTitle')}

- No more scrolling through endless chats and reactions. See everyone’s availability and - responses neatly in one place. + {t('home.allInOneClarityDescription')}

@@ -111,10 +104,9 @@
πŸ‘€
-

No Hassle, No Sign-Ups

+

{t('home.noHassleNoSignUpsTitle')}

- Skip registrations and endless forms. Unlike other event platforms, you create and share - instantly β€” no accounts, no barriers. + {t('home.noHassleNoSignUpsDescription')}

@@ -123,9 +115,9 @@
πŸ›‘οΈ
-

Smart Limits

+

{t('home.smartLimitsTitle')}

- Choose between unlimited RSVPs or set a limited capacity. Perfect for any event size. + {t('home.smartLimitsDescription')}

@@ -134,9 +126,9 @@
✨
-

Effortless Simplicity

+

{t('home.effortlessSimplicityTitle')}

- Designed to be instantly clear and easy. No learning curve β€” just open, create, and go. + {t('home.effortlessSimplicityDescription')}

@@ -146,35 +138,38 @@
-

How It Works

+

{t('home.howItWorksTitle')}

- 1. Create Event + 1. + {t('home.step1Title')}

- Fill out a simple form with event details. Choose between limited or unlimited capacity. + {t('home.step1Description')}

- 2. Get Unique URL + 2. + {t('home.step2Title')}

- Receive a random, memorable URL for your event. Perfect for sharing anywhere. + {t('home.step2Description')}

- 3. Collect RSVPs + 3. + {t('home.step3Title')}

-

People visit your link and join with just their name. No accounts needed.

+

{t('home.step3Description')}

@@ -184,14 +179,14 @@

- Ready to Create Your First Event? + {t('home.ctaTitle')}

-

Join thousands of event organizers who trust Cactoide

+

{t('home.ctaDescription')}

diff --git a/src/routes/create/+page.svelte b/src/routes/create/+page.svelte index 0474261..7aaf365 100644 --- a/src/routes/create/+page.svelte +++ b/src/routes/create/+page.svelte @@ -2,6 +2,7 @@ import type { CreateEventData, EventType } from '$lib/types'; import { enhance } from '$app/forms'; import { goto } from '$app/navigation'; + import { t } from '$lib/i18n/i18n.js'; export let form; @@ -51,7 +52,7 @@ - Create Event - Cactoide + {t('create.title')}
@@ -60,7 +61,7 @@
-

Create New Event

+

{t('create.formTitle')}

@@ -113,7 +114,7 @@
@@ -170,7 +171,8 @@
{t('common.required')}
@@ -200,7 +202,8 @@ {#if eventData.type === 'limited'}
{#if errors.attendee_limit} @@ -222,7 +225,8 @@
{t('common.required')}

{eventData.visibility === 'public' - ? 'Public events are visible to everyone and can be discovered by others' - : 'Private events are only visible to you and people you share the link with'} + ? t('create.publicDescription') + : t('create.privateDescription')}

@@ -259,7 +263,7 @@ on:click={handleCancel} class="flex-1 rounded-sm border-2 border-slate-300 bg-slate-200 px-4 py-3 font-semibold text-slate-700 transition-all duration-200 hover:bg-slate-400 hover:text-slate-200" > - Cancel + {t('common.cancel')}
{:else} - Create Event + {t('create.createEventButton')} {/if}
diff --git a/src/routes/discover/+page.svelte b/src/routes/discover/+page.svelte index ec73d1f..b7f08a1 100644 --- a/src/routes/discover/+page.svelte +++ b/src/routes/discover/+page.svelte @@ -3,6 +3,7 @@ import { goto } from '$app/navigation'; import type { PageData } from '../$types'; import { formatTime, formatDate, isEventInTimeRange } from '$lib/dateHelpers'; + import { t } from '$lib/i18n/i18n.js'; import Fuse from 'fuse.js'; let publicEvents: Event[] = []; @@ -17,7 +18,7 @@ export let data: PageData; // Use the server-side data - $: publicEvents = data.events; + $: publicEvents = data?.events || []; // Initialize Fuse.js with search options $: fuse = new Fuse(publicEvents, { @@ -81,7 +82,7 @@ - Discover Events - Cactoide + {t('discover.title')}
@@ -90,34 +91,36 @@ {#if error}
⚠️
-

Something went wrong. Please try again.

+

{t('common.somethingWentWrong')}

{error}

{:else if publicEvents.length === 0}
πŸ”
-

No Public Events Yet

+

{t('discover.noPublicEventsTitle')}

- There are no public events available at the moment. Be the first to create one! + {t('discover.noPublicEventsDescription')}

{:else}
-

Public Events ({filteredEvents.length})

-

Discover events created by the community

+

+ {t('discover.publicEventsTitle', { count: filteredEvents.length })} +

+

{t('discover.publicEventsDescription')}

@@ -143,14 +146,14 @@ {#if searchQuery}
@@ -263,7 +270,9 @@ d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" > - {formatDate(event.date)} at {formatTime(event.time)} + {formatDate(event.date)} {t('common.atTime')} {formatTime(event.time)}
@@ -289,7 +298,7 @@ ? 'border-amber-600 text-amber-600' : 'border-teal-500 text-teal-500'}" > - {event.type === 'limited' ? 'Limited' : 'Unlimited'} + {event.type === 'limited' ? t('common.limited') : t('common.unlimited')}
@@ -300,7 +309,7 @@ on:click={() => goto(`/event/${event.id}`)} class="flex-1 rounded-sm border-2 border-violet-500 bg-violet-400/20 px-4 py-2 font-semibold duration-200 hover:bg-violet-400/70" > - View + {t('discover.viewButton')}
@@ -310,8 +319,10 @@ {#if searchQuery && filteredEvents.length === 0}
πŸ”
-

No events found

-

Try adjusting your search terms or browse all events

+

+ {t('discover.noEventsFoundTitle')} +

+

{t('discover.noEventsFoundDescription')}

{/if}
diff --git a/src/routes/event/+page.svelte b/src/routes/event/+page.svelte index 0b7fdd9..dd856b0 100644 --- a/src/routes/event/+page.svelte +++ b/src/routes/event/+page.svelte @@ -2,6 +2,7 @@ import type { Event } from '$lib/types'; import { goto } from '$app/navigation'; import { formatTime, formatDate } from '$lib/dateHelpers'; + import { t } from '$lib/i18n/i18n.js'; export let data: { events: Event[] }; @@ -60,7 +61,7 @@ - My Events - Cactoide + {t('event.myEventsTitle')}
@@ -69,22 +70,24 @@ {#if userEvents.length === 0}
πŸŽ‰
-

No Events Yet

+

{t('event.noEventsYetTitle')}

- You haven't created any events yet. Start by creating your first event! + {t('event.noEventsYetDescription')}

{:else}
-

My Events ({userEvents.length})

-

Manage your created events

+

+ {t('event.myEventsTitle')} ({userEvents.length}) +

+

{t('event.myEventsDescription')}

@@ -104,7 +107,9 @@ d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" > - {formatDate(event.date)} at {formatTime(event.time)} + {formatDate(event.date)} {t('common.atTime')} {formatTime(event.time)}
@@ -130,7 +135,7 @@ ? 'border-amber-600 text-amber-600' : 'border-teal-500 text-teal-500'}" > - {event.type === 'limited' ? 'Limited' : 'Unlimited'} + {event.type === 'limited' ? t('common.limited') : t('common.unlimited')} - {event.visibility === 'public' ? 'Public' : 'Private'} + {event.visibility === 'public' ? t('common.public') : t('common.private')}
@@ -149,7 +154,7 @@
-

Delete Event

+

{t('event.deleteEventTitle')}

- Are you sure you want to delete "{eventToDelete.name}"? - This action cannot be undone and will remove all RSVPs. + {t('event.deleteEventDescription', { eventName: eventToDelete.name })}

@@ -240,13 +244,13 @@ on:click={closeDeleteModal} class="flex-1 rounded-sm border-2 border-slate-300 bg-slate-200 px-4 py-2 font-semibold text-slate-700 transition-all duration-200 hover:bg-slate-300" > - Cancel + {t('common.cancel')}
diff --git a/src/routes/event/[id]/+page.svelte b/src/routes/event/[id]/+page.svelte index 9d832ca..c8d0a6e 100644 --- a/src/routes/event/[id]/+page.svelte +++ b/src/routes/event/[id]/+page.svelte @@ -6,6 +6,7 @@ import { formatTime, formatDate } from '$lib/dateHelpers.js'; import CalendarModal from '$lib/components/CalendarModal.svelte'; import type { CalendarEvent } from '$lib/calendarHelpers.js'; + import { t } from '$lib/i18n/i18n.js'; export let data: { event: Event; rsvps: RSVP[]; userId: string }; export let form; @@ -80,7 +81,7 @@ - {event?.name || 'Event'} - Cactoide + {event?.name || t('event.eventTitle')}
@@ -91,13 +92,13 @@
⚠️
-

Event Not Found

-

The event you're looking for doesn't exist or has been removed.

+

{t('event.eventNotFoundTitle')}

+

{t('event.eventNotFoundDescription')}

@@ -163,7 +164,7 @@ ? 'border-amber-600 text-amber-600' : 'border-teal-500 text-teal-500'}" > - {event.type === 'limited' ? 'Limited' : 'Unlimited'} + {event.type === 'limited' ? t('common.limited') : t('common.unlimited')} - {event.visibility === 'public' ? 'Public' : 'Private'} + {event.visibility === 'public' ? t('common.public') : t('common.private')}
{#if event.type === 'limited' && event.attendee_limit}
-

Capacity

+

{t('common.capacity')}

{rsvps.length}/{event.attendee_limit}

@@ -189,13 +190,13 @@
-

Join This Event

+

{t('event.joinThisEvent')}

{#if event.type === 'limited' && event.attendee_limit && rsvps.length >= event.attendee_limit}
🚫
-

Event is Full!

-

Maximum capacity reached

+

{t('event.eventIsFull')}

+

{t('event.maximumCapacityReached')}

{:else}
@@ -240,7 +242,7 @@ class="h-4 w-4 rounded border-gray-300 text-violet-600 focus:ring-violet-500" />
@@ -248,7 +250,8 @@ {#if addGuests}

- Guests will be added as "{newAttendeeName || 'Your Name'}'s Guest #1", "{newAttendeeName || - 'Your Name'}'s Guest #2", etc. + {t('event.guestsWillBeAddedAs', { + name: newAttendeeName || t('common.yourNamePlaceholder') + })}

{/if} @@ -280,12 +284,15 @@
- Adding... + {t('event.adding')}
{:else if addGuests && numberOfGuests > 0} - Join Event + {numberOfGuests} Guest{numberOfGuests > 1 ? 's' : ''} + {t('event.joinEventWithGuests', { + count: numberOfGuests, + plural: numberOfGuests > 1 ? 's' : '' + })} {:else} - Join Event + {t('event.joinEventButton')} {/if} @@ -295,14 +302,14 @@
-

Attendees

+

{t('event.attendeesTitle')}

{rsvps.length}
{#if rsvps.length === 0}
-

No attendees yet

-

Be the first to join!

+

{t('event.noAttendeesYet')}

+

{t('event.beFirstToJoin')}

{:else}
@@ -361,7 +368,7 @@
@@ -423,7 +430,7 @@
- Removed RSVP successfully. + {t('event.removedRsvpSuccessfully')}
{/if} {/if} diff --git a/src/routes/event/[id]/edit/+page.svelte b/src/routes/event/[id]/edit/+page.svelte index 8b98e75..7fa11f1 100644 --- a/src/routes/event/[id]/edit/+page.svelte +++ b/src/routes/event/[id]/edit/+page.svelte @@ -2,6 +2,7 @@ import type { EventType } from '$lib/types'; import { enhance } from '$app/forms'; import { goto } from '$app/navigation'; + import { t } from '$lib/i18n/i18n.js'; export let data; export let form; @@ -33,7 +34,9 @@ eventData = { ...eventData, ...values, - attendee_limit: values.attendee_limit ? parseInt(String(values.attendee_limit)) : null + attendee_limit: (values as any).attendee_limit + ? parseInt(String((values as any).attendee_limit)) + : null }; } @@ -50,7 +53,7 @@ - Edit Event - {data.event.name} - Cactoide + {t('event.editTitle', { eventName: data.event.name })}
@@ -60,8 +63,8 @@
-

Edit Event

-

Update your event details

+

{t('event.editEventTitle')}

+

{t('event.editEventDescription')}

@@ -114,7 +117,7 @@
@@ -172,7 +175,7 @@
- Type * + {t('common.type')} {t('common.required')}
@@ -203,7 +206,7 @@ {#if eventData.type === 'limited'}
{#if errors.attendee_limit} @@ -226,7 +229,7 @@
- Visibility * + {t('common.visibility')} {t('common.required')}

{eventData.visibility === 'public' - ? 'Public events are visible to everyone and can be discovered by others' - : 'Private events are only visible to you and people you share the link with'} + ? t('create.publicDescription') + : t('create.privateDescription')}

@@ -265,7 +268,7 @@ on:click={handleCancel} class="flex-1 rounded-sm border-2 border-slate-300 bg-slate-200 px-4 py-3 font-semibold text-slate-700 transition-all duration-200 hover:bg-slate-400 hover:text-slate-200" > - Cancel + {t('common.cancel')}
{:else} - Update Event + {t('event.updateEventButton')} {/if}