summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cypress/e2e/event.cy.ts10
-rw-r--r--locales/en.json308
-rw-r--r--locales/ja.json308
-rw-r--r--package.json13
-rw-r--r--pnpm-lock.yaml193
-rwxr-xr-xsrc/app.ts208
-rw-r--r--src/helpers.ts93
-rw-r--r--src/lib/config.ts27
-rw-r--r--src/lib/event.ts4
-rw-r--r--src/lib/middleware.ts2
-rwxr-xr-xsrc/routes.js15
-rw-r--r--src/routes/event.ts25
-rw-r--r--src/routes/frontend.ts101
-rw-r--r--src/routes/magicLink.ts9
-rw-r--r--src/types/i18next-fs-backend.d.ts5
-rw-r--r--src/util/validation.ts41
-rw-r--r--static/instance-description-en.md (renamed from static/instance-description.md)0
-rw-r--r--static/instance-description-ja.md1
-rw-r--r--tsconfig.json2
-rw-r--r--utils.ts0
-rwxr-xr-xviews/404.handlebars4
-rw-r--r--views/createEventMagicLink.handlebars12
-rw-r--r--views/emails/addEventAttendee/addEventAttendeeHtml.handlebars14
-rw-r--r--views/emails/addEventAttendee/addEventAttendeeText.handlebars13
-rw-r--r--views/emails/addEventComment/addEventCommentHtml.handlebars10
-rw-r--r--views/emails/addEventComment/addEventCommentText.handlebars8
-rw-r--r--views/emails/createEvent/createEventHtml.handlebars8
-rw-r--r--views/emails/createEvent/createEventText.handlebars7
-rw-r--r--views/emails/createEventGroup/createEventGroupHtml.handlebars22
-rw-r--r--views/emails/createEventGroup/createEventGroupText.handlebars21
-rw-r--r--views/emails/createEventMagicLink/createEventMagicLinkHtml.handlebars10
-rw-r--r--views/emails/createEventMagicLink/createEventMagicLinkText.handlebars9
-rw-r--r--views/emails/deleteEvent/deleteEventHtml.handlebars6
-rw-r--r--views/emails/deleteEvent/deleteEventText.handlebars4
-rw-r--r--views/emails/editEvent/editEventHtml.handlebars10
-rw-r--r--views/emails/editEvent/editEventText.handlebars10
-rw-r--r--views/emails/eventGroupUpdated/eventGroupUpdatedHtml.handlebars12
-rw-r--r--views/emails/eventGroupUpdated/eventGroupUpdatedText.handlebars11
-rw-r--r--views/emails/removeEventAttendee/removeEventAttendeeHtml.handlebars6
-rw-r--r--views/emails/removeEventAttendee/removeEventAttendeeText.handlebars4
-rw-r--r--views/emails/subscribed/subscribedHtml.handlebars11
-rw-r--r--views/emails/subscribed/subscribedText.handlebars10
-rw-r--r--views/emails/unattendEvent/unattendEventHtml.handlebars12
-rw-r--r--views/emails/unattendEvent/unattendEventText.handlebars11
-rwxr-xr-xviews/event.handlebars147
-rwxr-xr-xviews/eventgroup.handlebars70
-rwxr-xr-xviews/home.handlebars51
-rwxr-xr-xviews/layouts/main.handlebars6
-rwxr-xr-xviews/newevent.handlebars22
-rwxr-xr-xviews/optionsform.handlebars16
-rw-r--r--views/partials/editeventgroupmodal.handlebars10
-rw-r--r--views/partials/editeventmodal.handlebars10
-rwxr-xr-xviews/partials/eventForm.handlebars74
-rw-r--r--views/partials/eventGroupForm.handlebars34
-rw-r--r--views/partials/eventList.handlebars2
-rw-r--r--views/partials/importeventform.handlebars16
-rw-r--r--views/partials/instanceRules.handlebars2
-rwxr-xr-xviews/partials/sidebar.handlebars6
-rw-r--r--views/publicEventList.handlebars14
59 files changed, 1513 insertions, 577 deletions
diff --git a/cypress/e2e/event.cy.ts b/cypress/e2e/event.cy.ts
index eeaa629..5d77fc7 100644
--- a/cypress/e2e/event.cy.ts
+++ b/cypress/e2e/event.cy.ts
@@ -304,9 +304,13 @@ describe("Events", () => {
cy.clearAllLocalStorage();
localStorage.setItem("editTokens", "invalid");
cy.visit(`/${this.eventID}`).then(() => {
- expect(localStorage.getItem("editTokens")).to.not.include(
- "invalid",
- );
+ const editTokens = localStorage.getItem("editTokens");
+ if (editTokens !== null) {
+ expect(editTokens).to.not.include("invalid");
+ } else {
+ // If it's null, the invalid token was successfully removed
+ expect(editTokens).to.be.null;
+ }
});
});
});
diff --git a/locales/en.json b/locales/en.json
new file mode 100644
index 0000000..fe7330a
--- /dev/null
+++ b/locales/en.json
@@ -0,0 +1,308 @@
+{
+ "common.close": "Close",
+ "common.copied": "Copied!",
+ "common.copy": "Copy",
+ "common.coverimg": "Cover image",
+ "common.create": "Create",
+ "common.creating": "Creating...",
+ "common.datetimeformat": "dddd D MMMM YYYY h:mm a",
+ "common.emailaddr": "Email address",
+ "common.eventend": "Ends",
+ "common.eventgroupid": "Event group ID",
+ "common.eventgroupname": "Event group name",
+ "common.eventgroups": "Event groups",
+ "common.eventlocation": "Location",
+ "common.eventname": "Event name",
+ "common.eventstart": "Starts",
+ "common.timezone": "Timezone",
+ "common.year-month-format": "MMMM YYYY",
+ "common.youremail": "Your email",
+ "config.defaultinstancedesc": "**{{ siteName }}** is running on Gathio — a simple, federated, privacy-first event hosting platform.",
+ "config.instancerule.creatoremail-false": "Anyone can create events and groups",
+ "config.instancerule.creatoremail-true": "Only specific people can create events and groups",
+ "config.instancerule.deleteafterdays-false": "Events are permanent, and are never automatically deleted",
+ "config.instancerule.deleteafterdays-true": "Events are automatically deleted {{ days }} days after they end",
+ "config.instancerule.isfederated-false": "This instance does not federate with other instances",
+ "config.instancerule.isfederated-true": "This instance federates with other instances using ActivityPub",
+ "config.instancerule.showpubliceventlist-false": "Events and groups can only be accessed by direct link",
+ "config.instancerule.showpubliceventlist-true": "Public events and groups are displayed on the homepage",
+ "frontend.dateformat": "dddd D MMMM YYYY",
+ "frontend.displaydate-days": "{{ startdate }} <span class=\"text-muted\">at</span> {{ starttime }} <span class=\"text-muted\">-</span> {{ enddate }} <span class=\"text-muted\">at</span> {{ endtime }} <span class=\"text-muted\">{{ timezone }}</span>",
+ "frontend.displaydate-sameday": "{{ startdate }} <span class=\"text-muted\">from</span> {{ starttime }} <span class=\"text-muted\">to</span> {{ endtime }} <span class=\"text-muted\">{{ timezone }}</span>",
+ "frontend.elnumber": "({{count}} people)",
+ "frontend.eventattendees": "people",
+ "frontend.newevent": "New event",
+ "frontend.publicevents": "Public events",
+ "frontend.timeformat": "h:mm a",
+ "routes.addeventattendeesubject": "You're RSVPed to {{ eventName }}",
+ "routes.addeventcommentsubject": "New comment in {{eventName}}",
+ "routes.deleteeventsubject": "{{ eventName }} was deleted",
+ "routes.event.datetimeformat": "{{thedate, intlDate}}",
+ "routes.event.descriptionchanged": "the event description changed",
+ "routes.event.difftext": "This event was just updated with new information.",
+ "routes.event.editedsubject": "{{ eventname }} was just edited",
+ "routes.event.endtimechanged": "the end time changed to {{ endtime }}",
+ "routes.event.locationchanged": "the location changed to {{ location }}",
+ "routes.event.namechanged": "the event name changed to {{ eventname }}",
+ "routes.event.starttimechanged": "the start time changed to {{ starttime }}",
+ "routes.event.timezonechanged": "the time zone changed to {{ timezone }}",
+ "routes.magiclink-invalid": "This magic link is invalid or has expired. Please request a new one here.",
+ "routes.magiclink.mailsubject": "Magic link to create an event",
+ "routes.magiclink.provideemail": "Please provide an email address.",
+ "routes.magiclink.requestmlbutton": "Request magic link",
+ "routes.magiclink.thanks": "Thanks! If this email address can create events, you should receive an email with a magic link.",
+ "routes.removeeventattendeesubject": "You have been removed from an event",
+ "routes.subscribedsubject": "You have subscribed to an event group",
+ "util.validation.eventdata.creatoremail": "Email address is invalid.",
+ "util.validation.eventdata.eventdescription": "Event description is required.",
+ "util.validation.eventdata.eventend": "Event end time is required.",
+ "util.validation.eventdata.eventgroupboolean": "Event group ID is required.",
+ "util.validation.eventdata.eventgroupedittoken": "Event group edit token is required.",
+ "util.validation.eventdata.eventlocation": "Event location is required.",
+ "util.validation.eventdata.eventname": "Event name is required.",
+ "util.validation.eventdata.eventstart": "Event start time is required.",
+ "util.validation.eventdata.eventurl": "Event link is invalid.",
+ "util.validation.eventdata.maxattendees": "Max number of attendees must be a number.",
+ "util.validation.eventdata.maxattendeesboolean": "Max number of attendees is required.",
+ "util.validation.eventdata.timezone": "Event timezone is required.",
+ "util.validation.eventtime.endisbefore": "End time must be in the future.",
+ "util.validation.eventtime.endyears": "Event duration cannot be longer than 1 year.",
+ "util.validation.eventtime.startisafter": "Start time must be before end time.",
+ "util.validation.eventtime.startisbefore": "Start time must be in the future.",
+ "util.validation.groupdata.creatoremail": "Email address is invalid.",
+ "util.validation.groupdata.eventgroupdescription": "Event group description is required.",
+ "util.validation.groupdata.eventgroupname": "Event group name is required.",
+ "util.validation.groupdata.eventgroupurl": "Group link is invalid.",
+ "views.404desc": "It may have never existed, or it's been removed from the server. Don't despair - why not create a new one? I for one would love to come to your ocarina recital.",
+ "views.404notfound": "Event not found!",
+ "views.createeventmagiclink.requestmlcontact": "If you run into any issues, please contact the instance administrator.",
+ "views.createeventmagiclink.requestmldesc": "The administrator of this instance has limited event creation to a set of specific email addresses. If your email address is allowed to create events, you will be sent a magic link. If not, you won't receive anything.",
+ "views.createeventmagiclink.requestmltitle": "Request a link to create a new event",
+ "views.del": "Delete",
+ "views.edittoken": "Enter editing password",
+ "views.edittokendesc": "Enter the editing password you received by email or were shown when the event was created.",
+ "views.emails.addeventattendee.clicktocancel": "Click this link",
+ "views.emails.addeventattendee.dontknowhtml": "If you didn't mark yourself as attending an event on {{ siteName }}, someone may have accidentally typed your email instead of theirs. If you don't want to attend this event, use the <strong>deletion password</strong> above to remove yourself from the event page.",
+ "views.emails.addeventattendee.eventlink": "Follow this link to open the event page any time",
+ "views.emails.addeventattendee.preface": "You just marked yourself as attending an event on {{ siteName }}. Thank you! We'll send you another email if there are any updates to the event. Your email will be automatically removed from the database once the event finishes.",
+ "views.emails.addeventattendee.removapasswordhtml": "You can also head to the event page and use this <strong>deletion password</strong>",
+ "views.emails.addeventattendee.removelink": "Need to remove yourself from this event? Click this link",
+ "views.emails.addeventattendee.removepassword": "You can also head to the event page and use this deletion password",
+ "views.emails.addeventattendee.toremove": "Need to remove yourself from this event?",
+ "views.emails.addeventcomment.link": "Click here to see the comment",
+ "views.emails.addeventcomment.preface": "{{ commentAuthor }} has just posted a comment on an event you're attending on {{ siteName }}.",
+ "views.emails.addeventcomment.prefacehtml": "<strong>{{ commentAuthor }}</strong> has just posted a comment on an event you're attending on {{ siteName }}.",
+ "views.emails.createevent.desc": "Use the following link to edit your event. DO NOT SHARE THIS, as anyone with this link can edit your event.",
+ "views.emails.createevent.editpswddesc": "Event group secret editing code",
+ "views.emails.createevent.preface": "Your event has been created!",
+ "views.emails.createevent.sharelink": "Use this link to share it with people",
+ "views.emails.createeventgroup.done": "And that's it - have a great day!",
+ "views.emails.createeventgroup.dontknow": "If you didn't make an event group on {{ siteName }}, someone may have accidentally typed your email instead of theirs when they were making the group. Just click on the edit link above and delete that event group, which removes your email from the system as well.",
+ "views.emails.createeventgroup.editgrouplink": "Edit the event group here",
+ "views.emails.createeventgroup.editgrouplinkhtml": "Edit event group",
+ "views.emails.createeventgroup.eventlink": "To add events to this group (whether brand new events or ones you've already made), click the 'This event is part of an event group' checkbox. You will need to copy the following two codes into the box which opens:",
+ "views.emails.createeventgroup.link": "You can edit your event group by clicking the button below, or just following this link",
+ "views.emails.createeventgroup.preface": "You just created a new event group on {{ siteName }}! Thanks a bunch - we're delighted to have you.",
+ "views.emails.createeventgroup.sharelink": "To let others know about your event group, send them this link",
+ "views.emails.createeventhtml.desc": "Click this button to edit your event. <strong>DO NOT SHARE THIS</strong>, as anyone with this link can edit your event.",
+ "views.emails.createeventhtml.editevent": "Edit Your Event",
+ "views.emails.deleteevent.done": "If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. Don't worry - that event, and your email, is deleted from the system now.",
+ "views.emails.deleteevent.preface": "The {{ eventName }} event you're attending on {{ siteName }} was just deleted by its creator.",
+ "views.emails.dontknow": "If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. If you don't want to attend this event, use the deletion password above to remove yourself from the event page.",
+ "views.emails.editevent.dontknow": "If you didn't mark yourself as attending an event on {{ siteName }}, someone may have accidentally typed your email instead of theirs. Don't worry - there isn't anything you need to do. Your email will be removed from the system when the event finishes.",
+ "views.emails.editevent.link": "Click here to see the event",
+ "views.emails.editevent.preface": "An event you're attending on {{ siteName }} has just been edited.",
+ "views.emails.eventgroupupdate.afterlink": " ",
+ "views.emails.eventgroupupdate.beforelink": "The event is ",
+ "views.emails.eventgroupupdate.dontknow": "If you didn't subscribe yourself to this event group on {{ siteName }}, someone may have accidentally typed your email instead of theirs.",
+ "views.emails.eventgroupupdate.dontknowhtml": "If you didn't subscribe yourself to this event group on {{ siteName }}, someone may have accidentally typed your email instead of theirs. Click here to unsubscribe",
+ "views.emails.eventgroupupdate.grouplink": "Click here to see the event group",
+ "views.emails.eventgroupupdate.holduphtml": "Hold up - I don't want to receive these emails any more!",
+ "views.emails.eventgroupupdate.link": "The event is '{{ eventName }}'",
+ "views.emails.eventgroupupdate.preface": "A new event has been added to the event group '{{ eventGroupName }}' on {{ siteName }}",
+ "views.emails.eventgroupupdate.prefacehtml": "A new event has been added to the event group '{eventGroupName}' on {{ siteName }}.",
+ "views.emails.eventgroupupdate.unsubscribe": "Click here to unsubscribe",
+ "views.emails.holdup": "Hold up - I have no idea what this email is about!",
+ "views.emails.love": "Love,",
+ "views.emails.magiclink.dontknow": "If you didn't try to create an event on {{ siteName }}, someone may have accidentally typed your email instead of theirs. Don't worry - there isn't anything you need to do. Your email address will be deleted after the magic link expires.",
+ "views.emails.magiclink.link": "This link will expire in 24 hours and can be used multiple times before then. Don't share it publicly, because it will allow anyone to create an event on your behalf!",
+ "views.emails.magiclink.preface": "Here's a magic link which will allow you to create an event on {{ siteName }}.",
+ "views.emails.removeeventattendee.dontknow": "If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. Don't worry - you won't receive any more of these emails for this event, and your email has been removed from the database.",
+ "views.emails.removeeventattendee.preface": "You have been removed from the event {{ eventName }} on {{ siteName }} by the organizer of the event.",
+ "views.emails.subscribed.desc": "You will receive emails when new events are added to the group, and can unsubscribe at any time.",
+ "views.emails.subscribed.preface": "You have been subscribed to the event group '{{eventGroupName}}' on {{ siteName }}.",
+ "views.emails.unattendevent.desc": "If you didn't mean to do this, an admin may have removed you from the event.",
+ "views.emails.unattendevent.dontknow": "If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs, then removed it. Don't worry - you won't receive any more emails linked to this event.",
+ "views.emails.unattendevent.preface": "You just removed yourself from an event on {{ siteName }}. You will no longer receive update emails for this event.",
+ "views.event.about": "About",
+ "views.event.addme": "Add me",
+ "views.event.addmyself": "Add myself",
+ "views.event.addself": "Add yourself to '{{eventData.name}}'",
+ "views.event.addtoGC": "Add to Google Calendar",
+ "views.event.attendeeemail": "Your email (optional)",
+ "views.event.attendeename": "Your name",
+ "views.event.attendeenamedesc": "Or an alias, perhaps...",
+ "views.event.attendeenum": "How many people in your party?",
+ "views.event.attendees": "Attendees",
+ "views.event.attendeevisible": "Show my name in the public list of attendees",
+ "views.event.attendeevisibledesc": "If you choose to hide your name, only the event organiser will be able to see it.",
+ "views.event.attention": "Your secret editing password for this event is: <strong>{{eventData.editToken}}</strong>. It's been saved in your browser storage, and if you supplied your email, it's also been sent to you. If you didn't supply your email, you <strong>must save it somewhere safe</strong>, because it won't be shown again!",
+ "views.event.capacity": "This event is at capacity.",
+ "views.event.comment": "Comment",
+ "views.event.commentauthor": "Name",
+ "views.event.commentauthorph": "Your name",
+ "views.event.commentcontent": "What would you like to say?",
+ "views.event.concludeddel": " This event has concluded. It can no longer be edited{{#if eventWillBeDeleted}}, and will be automatically deleted {{daysUntilDeletion}}{{/if}}.",
+ "views.event.confremoveattendee": "Are you sure you want to remove this attendee from the event? This action cannot be undone.",
+ "views.event.del": "Delete event",
+ "views.event.delconfirm": "Are you sure you want to delete this event? This action cannot be undone.",
+ "views.event.deletetitle": "Delete '{{ eventData.name }}'",
+ "views.event.discussion": "Discussion",
+ "views.event.edit": "Edit event",
+ "views.event.editlater": "You can always edit it later.",
+ "views.event.ended": "Ended",
+ "views.event.enternum": "Enter a number.",
+ "views.event.hidden": "(hidden from public list)",
+ "views.event.hiddenattendee_one": "{{count}} hidden attendee",
+ "views.event.hiddenattendee_other": "{{count}} hidden attendees",
+ "views.event.hiddenattendee_zero": "No hidden attendee",
+ "views.event.hostedby": "Hosted by</span> {{eventData.hostName}}",
+ "views.event.ICSexport": "Export as ICS",
+ "views.event.joinemaildesc": "If you provide your email, you will receive updates to the event.",
+ "views.event.locationdesc": "Be specific.",
+ "views.event.lostpswd": "Forgot password? Get in touch with the event organiser.",
+ "views.event.noattendees": "No attendees yet!",
+ "views.event.nospam": "We won't spam you <3",
+ "views.event.numberofattende": "({{numberOfAttendees}})",
+ "views.event.numlimit": "Please enter a number between 1 and ${response.data.freeSpots}",
+ "views.event.partof": "<a href='/group/{{eventData.eventGroup.id}}'>{{eventData.eventGroup.name}}</a>",
+ "views.event.postbutton": "Post comment",
+ "views.event.remaining_one": "{{count}} spot remaining - add yourself now!",
+ "views.event.remaining_other": "{{count}} spots remaining - add yourself now!",
+ "views.event.remaining_zero": "This event is at capacity.",
+ "views.event.removeAttendee": "Remove attendee",
+ "views.event.removeattendeedesc": "Remove attendee from '{{eventData.name}}'",
+ "views.event.removeme": "Remove me",
+ "views.event.removemyself": "Remove myself",
+ "views.event.removemyselfdesc": "Remove yourself from '{{eventData.name}}'",
+ "views.event.removepswd": "Removal password",
+ "views.event.removepswddesc": "You can use this password to remove yourself from the list of event attendees. If you provided your email, you'll receive it by email. Otherwise, write it down now because it will <strong>not be shown again</strong>.",
+ "views.event.removetitle": "Remove {{ attendeeName }} from {{ eventName }}",
+ "views.event.removeuser": "Remove user from event",
+ "views.event.reply": "Reply",
+ "views.event.replycontent": "What would you like to reply?",
+ "views.event.share": "To share your event, use the link you can see just above this message - that way your attendees won't be able to edit or delete your event!",
+ "views.event.showonGM": "Show on Google Maps",
+ "views.event.showonOM": "Show on OpenStreetMap",
+ "views.event.started": "Started",
+ "views.event.welcome": "Welcome to your event!",
+ "views.eventgroup.about": "About",
+ "views.eventgroup.addevent": "To link an existing event to this group, copy and paste the two codes below into the 'Event Group' box when creating a new event or editing an existing event.",
+ "views.eventgroup.del": "Delete this event group",
+ "views.eventgroup.delconfirm": "Are you sure you want to delete this event group? This action cannot be undone.",
+ "views.eventgroup.deldesc": "<p>This will <strong>not</strong> delete the individual events contained in this group. They can be linked to another group later.",
+ "views.eventgroup.deletetitle": "Delete '{{ eventGroupData.name }}'",
+ "views.eventgroup.edit": "Edit group",
+ "views.eventgroup.editmode": "Switch to editing mode",
+ "views.eventgroup.editpswd": "Event group editing password",
+ "views.eventgroup.feedlinkdesc": "Paste this URL into your calendar app\nto subscribe to a live feed of events from this group.",
+ "views.eventgroup.hostedby": "Hosted by</span> {{eventGroupData.hostName}}",
+ "views.eventgroup.ICSexport": "Export as ICS",
+ "views.eventgroup.pastevents": "Past events",
+ "views.eventgroup.subscribe": "Subscribe to events from\n'{{eventGroupData.name}}'",
+ "views.eventgroup.subscribebutton": "Subscribe",
+ "views.eventgroup.subscribedesc": "Enter your email address (optional) to receive\nupdates\nwhenever a new event is created in this group.",
+ "views.eventgroup.subscribetitle": "Subscribe to updates",
+ "views.eventgroup.upcomingevents": "Upcoming events",
+ "views.eventgroup.welcome": "Welcome to your event group! We've just sent you an email with your secret editing link, which you can also see in the address bar above. Haven't got the email? Check your spam or junk folder. To share your event group, use the link you can see just below this message - that way your attendees won't be able to edit or delete your event group!",
+ "views.home.about": "About {{ siteName }}",
+ "views.home.aboutgathio": "About Gathio",
+ "views.home.attention": "But remember: all events are visible to anyone who knows the link, so probably don't use Gathio to plot your surprise birthday party or revolution. Or whatever, you do you.",
+ "views.home.autodelete": "If this instance automatically deletes its events, sometime after the event finishes, it's deleted from the database for ever, and your data goes with it.",
+ "views.home.conftitle": "Configurable",
+ "views.home.fedtitle": "Federation and self-hosting",
+ "views.home.flagshipsetting": "The <a href=\"https://gath.io\">flagship Gathio instance at gath.io</a> is designed for anyone to create ephemeral, hidden events. Anyone can create an event; events are never displayed anywhere public; and they're deleted 7 days after they end.",
+ "views.home.imgexample": "An example event page for a picnic. The page shows the event's location, host, date and time, and description, as well as buttons to save the event to Google Calendar, export it, and open the location in OpenStreetMap and Google Maps.",
+ "views.home.intro": "Gathio is a simple, federated, privacy-first event hosting platform.",
+ "views.home.kofi": "Support Me on Ko-fi",
+ "views.home.onpre": "But if your community sets up their own instance, you can limit event creation to a specific list of people, display events on a handy list on the homepage, and disable event deletion entirely!",
+ "views.home.opensource": "Open source",
+ "views.home.osdesc": "Gathio is delighted to be open source, and is built by a lovely group of people. Leave a question in our <a href=\"https://github.com/lowercasename/gathio/issues\">tracker</a> if you encounter any issues.",
+ "views.home.privacy": "Also, Gathio doesn't show you ads, doesn't sell your data, and never sends you unnecessary emails.",
+ "views.home.privacytitle": "Privacy-first",
+ "views.home.privdesc": "There are no accounts on Gathio. When you create an event, we generate a password which allows you to edit the event. Send all your guests the public link, and all your co-hosts the secret editing link containing the password.",
+ "views.home.privmail": "If you supply your email, we'll send you the editing password so you don't lose it - but supplying your email is optional!",
+ "views.home.selfhost": "Gathio can easily be self-hosted, and supports ActivityPub services like Mastodon, Pleroma, and Friendica, allowing you to access events from anywhere on the Fediverse. We encourage you to spin up your own instance for your community. Detailed instructions on <a href=\"https://docs.gath.io/using-gathio/fediverse/\">ActivityPub access</a> and <a href=\"https://docs.gath.io/running-gathio/installation/\">self-hosted installation</a> live on our GitHub wiki.",
+ "views.home.sponsor": "If you find yourself using and enjoying Gathio, consider <a href=\"https://github.com/sponsors/lowercasename\" class=\"text-success\">supporting Raphael via GitHub Sponsors</a>. It'll help keep the project and main site running! <i class=\"far fa-heart\"></i>",
+ "views.incorrectpswd": "That editing password is incorrect. Try again.",
+ "views.interaction": "Users can post comments on this event",
+ "views.join": "Users can mark themselves as attending this event",
+ "views.layouts.main.defaultmetadata": "An easier, quicker, and much less privacy-invading way to make and share events",
+ "views.layouts.main.footnote": " <strong>Gathio</strong> version {{version}} &middot; <a href=\"https://github.com/lowercasename/gathio\">GitHub</a> &middot; Made with <i class=\"far fa-heart\"></i> by <a href=\"https://raphaelkabo.com\">Raphael</a> and <a href=\"https://github.com/lowercasename/gathio/graphs/contributors\">contributors</a>",
+ "views.newevent.createnew": "Create an event",
+ "views.newevent.groupattention": "Event groups do not get automatically removed like events do, but events which have been removed from {{siteName}} will of course not show up in an event group.",
+ "views.newevent.groupdesc": "An event group is a holding area for a set of linked events, like a series of film nights, a festival, or a band tour. You can share a public link to your event group just like an individual event link, and people who know the secret editing code (sent in an email when you create the event group) will be able to add future events to the group.",
+ "views.newevent.importevent": "Import an existing event",
+ "views.newevent.neweventbutton": "Create a new event",
+ "views.newevent.neweventgroup": "Create a new event group",
+ "views.newevent.newgroup": "Create an event group",
+ "views.newevent.pagetitle": "What would you like to do?",
+ "views.newevent.visiblealert": "Events are visible to anyone who knows the link.",
+ "views.options.showlistattendees": "Display the list of attendees",
+ "views.partials.choosefile": "Choose file",
+ "views.partials.creatoremaildesc": "If you provide your email, we will send your secret editing password here, and use it to notify you of updates to the event.",
+ "views.partials.delimg": "Delete image",
+ "views.partials.editevent.delthis": "Delete this event",
+ "views.partials.editevent.edit": "Edit '{{eventData.name}}'",
+ "views.partials.editeventgroup.del": "Delete this event group",
+ "views.partials.editeventgroup.delbutton": "Delete event group",
+ "views.partials.eventform.creatoremail": "Your email",
+ "views.partials.eventform.eventdescription": "Description",
+ "views.partials.eventform.eventgroup": "This event is part of an event group",
+ "views.partials.eventform.eventgroupdata": "Link this event to an event group",
+ "views.partials.eventform.eventgroupedittoken": "You can find this long string of characters in the\nconfirmation email you received when you created the event group.",
+ "views.partials.eventform.eventgroupid": "You can find this short string of characters in the event group's link, in your confirmation email, or on the event group's page.",
+ "views.partials.eventform.eventgrouplinker": "Choose a group you've edited before",
+ "views.partials.eventform.eventurl": "Link",
+ "views.partials.eventform.eventurldesc": "For tickets or another event page (optional).",
+ "views.partials.eventform.groupbutton": "Enter group details manually",
+ "views.partials.eventform.hostname": "Host name",
+ "views.partials.eventform.hostnamedesc": "Will be shown on the event page (optional).",
+ "views.partials.eventform.maxattendees": "Attendee limit",
+ "views.partials.eventform.maxattendeestitle": "Set a limit on the maximum number of attendees",
+ "views.partials.eventform.options": "Options",
+ "views.partials.eventform.publicevent": "Display this event on the public event list",
+ "views.partials.eventgroup.options": "Options",
+ "views.partials.eventgroupform.creatoremail": "Your email",
+ "views.partials.eventgroupform.eventgroupddesc": "Description",
+ "views.partials.eventgroupform.eventgroupurl": "Link",
+ "views.partials.eventgroupform.eventgroupurldesc": "For tickets or another event page (optional).",
+ "views.partials.eventgroupform.hostname": "Host or organisation name",
+ "views.partials.eventgroupform.isshowningroup": "Will be shown on the event group page (optional).",
+ "views.partials.eventgroupform.publicgroup": "Display this group on the public group list",
+ "views.partials.eventlist.noevents": "No events!",
+ "views.partials.fixerrors": "Please fix these errors:",
+ "views.partials.importevent.selectfile": "Select file",
+ "views.partials.importeventform.import": "Import",
+ "views.partials.importeventform.importdesc": "Upload an .ics file here to instantly create an event. You can save a Facebook event as an .ics file by clicking on the context menu next to the 'Import' and 'Edit' buttons on the event page and choosing the 'Export Event' option. Then select the 'Save to calendar' option and save the file on your computer.",
+ "views.partials.importeventform.importing": "Importing...",
+ "views.partials.instancerules.instancesettings": "Instance settings",
+ "views.partials.mdsupport": "<a href='https://commonmark.org/help/'>Markdown</a> formatting\nsupported.",
+ "views.partials.recommendeddimensions": "Recommended dimensions (w x h): 920px by 300px.",
+ "views.partials.save": "Save changes",
+ "views.partials.saving": "Saving...",
+ "views.partials.sidebar.about": "About",
+ "views.partials.sidebar.createevent": "Create an event",
+ "views.partials.sidebar.events": "View events",
+ "views.partials.snappy": "Make it snappy.",
+ "views.partials.wontshow": "Will not be shown anywhere (optional).",
+ "views.publiceventlist.events": "Events",
+ "views.publiceventlist.groups": "Groups",
+ "views.publiceventlist.nogroups": "No groups!",
+ "views.publiceventlist.numoevents_one": "{{count}} event",
+ "views.publiceventlist.numoevents_other": "{{count}} events",
+ "views.publiceventlist.numoevents_zero": "No event",
+ "views.publiceventlist.pastevents": "Past events",
+ "views.publiceventlist.upcomingevents": "Upcoming events",
+ "views.right": "Get it right!"
+}
diff --git a/locales/ja.json b/locales/ja.json
new file mode 100644
index 0000000..4b2ec2d
--- /dev/null
+++ b/locales/ja.json
@@ -0,0 +1,308 @@
+{
+ "common.close": "閉じる",
+ "common.copied": "コピーしました !",
+ "common.copy": "コピー",
+ "common.coverimg": "カバー画像",
+ "common.create": "作成",
+ "common.creating": "作成しています...",
+ "common.datetimeformat": "LL (dd) LT",
+ "common.emailaddr": "メールアドレス",
+ "common.eventend": "終了日時",
+ "common.eventgroupid": "イベントグループ ID",
+ "common.eventgroupname": "グループ名",
+ "common.eventgroups": "イベントグループ",
+ "common.eventlocation": "場所",
+ "common.eventname": "イベント名",
+ "common.eventstart": "開始日時",
+ "common.timezone": "タイムゾーン",
+ "common.year-month-format": "YYYY年MMM",
+ "common.youremail": "あなたのメールアドレス",
+ "config.defaultinstancedesc": "**{{ siteName }}** は Gathio――簡単に、プライバシーファーストで、連合プロトコルにも対応したイベントホストプラットフォームで稼働しています。",
+ "config.instancerule.creatoremail-false": "だれでもイベントとグループをつくれます",
+ "config.instancerule.creatoremail-true": "イベントとグループをつくることができるのは特定の人だけです",
+ "config.instancerule.deleteafterdays-false": "イベントは永続的で、自動的に削除されることはありません",
+ "config.instancerule.deleteafterdays-true": "イベントは、終了して {{ days }} 日後に自動削除します",
+ "config.instancerule.isfederated-false": "このインスタンスは、他のインスタンスとの連合プロトコルを有効にしていません",
+ "config.instancerule.isfederated-true": "このインスタンスは ActivityPub を通じて他のインスタンスと連合しています",
+ "config.instancerule.showpubliceventlist-false": "イベントとグループは、各々のリンクからしかアクセスできません",
+ "config.instancerule.showpubliceventlist-true": "公開イベントと公開グループをページ上に掲載します",
+ "frontend.dateformat": "LL (dd)",
+ "frontend.displaydate-days": "{{ startdate }} {{ starttime }} <span class=\"text-muted\">-</span> {{ enddate }} {{ endtime }} {{ timezone }}",
+ "frontend.displaydate-sameday": "{{ startdate }} {{ starttime }} <span class=\"text-muted\">~</span> {{ endtime }} <span class=\"text-muted\">{{ timezone }}</span>",
+ "frontend.elnumber": "( {{count}} 人)",
+ "frontend.eventattendees": "人で",
+ "frontend.newevent": "イベントの作成",
+ "frontend.publicevents": "公開イベント",
+ "frontend.timeformat": "LT",
+ "routes.addeventattendeesubject": "{{ eventName }} への参加を登録しました",
+ "routes.addeventcommentsubject": "{{ eventName }} にコメントがきました",
+ "routes.deleteeventsubject": "{{ eventName }} は削除されました",
+ "routes.event.datetimeformat": "{{thedate, long}}",
+ "routes.event.descriptionchanged": "説明が変更",
+ "routes.event.difftext": "このイベントの新しい情報です。",
+ "routes.event.editedsubject": "{{ eventname }} に変更がありました",
+ "routes.event.endtimechanged": "終了日時が変更 : {{ endtime }}",
+ "routes.event.locationchanged": "場所が変更 : {{ location }}",
+ "routes.event.namechanged": "イベント名が変更 : {{ eventname }}",
+ "routes.event.starttimechanged": "開始日時が変更 : {{ starttime }}",
+ "routes.event.timezonechanged": "タイムゾーンが変更 : {{ timezone }}",
+ "routes.magiclink-invalid": "このマジックリンクは無効、または期限が切れています。再度リクエストしてください。",
+ "routes.magiclink.mailsubject": "イベント作成の「マジックリンク」",
+ "routes.magiclink.provideemail": "メールアドレスを入力してください。",
+ "routes.magiclink.requestmlbutton": "マジックリンクをリクエスト",
+ "routes.magiclink.thanks": "ありがとうございます! このメールアドレスでのイベント作成が許可されていたら、メールにマジックリンクが届きます。",
+ "routes.removeeventattendeesubject": "イベント参加をキャンセルしました",
+ "routes.subscribedsubject": "イベントグループ情報を購読登録しました",
+ "util.validation.eventdata.creatoremail": "メールアドレスが無効です。",
+ "util.validation.eventdata.eventdescription": "イベントの説明は必須です。",
+ "util.validation.eventdata.eventend": "終了日時は必須です。",
+ "util.validation.eventdata.eventgroupboolean": "イベントグループ ID は必須です。",
+ "util.validation.eventdata.eventgroupedittoken": "イベントグループの編集用パスワードが必要です。",
+ "util.validation.eventdata.eventlocation": "場所は必須です。",
+ "util.validation.eventdata.eventname": "イベント名は必須です。",
+ "util.validation.eventdata.eventstart": "開始日時は必須です。",
+ "util.validation.eventdata.eventurl": "イベントリンクが無効です。",
+ "util.validation.eventdata.maxattendees": "定員は数字で入力してください。",
+ "util.validation.eventdata.maxattendeesboolean": "定員数は必須です。",
+ "util.validation.eventdata.timezone": "タイムゾーンは必須です。",
+ "util.validation.eventtime.endisbefore": "終了日時は未来にしてください。",
+ "util.validation.eventtime.endyears": "1 年を超えるイベントは作成できません。",
+ "util.validation.eventtime.startisafter": "開始日時は、終了日時より前にしてください。",
+ "util.validation.eventtime.startisbefore": "開始日時は未来にしてください。",
+ "util.validation.groupdata.creatoremail": "メールアドレスが無効です。",
+ "util.validation.groupdata.eventgroupdescription": "説明は必須です。",
+ "util.validation.groupdata.eventgroupname": "グループ名は必須です。",
+ "util.validation.groupdata.eventgroupurl": "グループのリンクが無効です。",
+ "views.404desc": "もともと存在しなかったか、終了して一定期間を経過してサーバーから削除されたかのいずれかです。がっかりしないで――あなたがイベントを企画してみたら? あなたのオカリナ・リサイタルにぜひ行きたい、と思う人は私のほかにもきっといますよ。",
+ "views.404notfound": "イベントが見つかりません !",
+ "views.createeventmagiclink.requestmlcontact": "問題が生じたら、インスタンス管理者にご連絡ください。",
+ "views.createeventmagiclink.requestmldesc": "この Gathio インスタンスの管理者は、イベントの作成権限を特定のメールアドレスに限定しています。確認のため、以下から「マジックリンク」――イベント作成の入口へのリンクをリクエストしてください。あなたのメールアドレスでの作成が許可されていれば、マジックリンクがメールで届くはずです。許可されていなければ、メールは届きません。",
+ "views.createeventmagiclink.requestmltitle": "イベント作成のマジックリンクをリクエスト",
+ "views.del": "削除",
+ "views.edittoken": "編集パスワードを入力します",
+ "views.edittokendesc": "編集パスワードを入力してください。イベント作成時に表示したほか、メールアドレスを入力していたらメールでも送信しています。",
+ "views.emails.addeventattendee.clicktocancel": "クリックしてキャンセル",
+ "views.emails.addeventattendee.dontknowhtml": "{{siteName}} でイベントに参加した覚えがありませんか? どなたかが自分のメールアドレスを入力したつもりで、たまたまこちらのあなた様あてに届いてしまった可能性があります。このイベントに参加したくない場合は、上のキャンセルリンクまたは削除パスワードで手続きしていただけると助かります。",
+ "views.emails.addeventattendee.eventlink": "イベントページを確認したい場合は、こちらのリンクから",
+ "views.emails.addeventattendee.preface": "{{ siteName }}でイベントへの参加を受け付けました。ありがとうございます。このイベントに更新があればメールで情報を送信いたします。イベント終了後、こちらのメールアドレスはイベントのデータとともに削除します。",
+ "views.emails.addeventattendee.removapasswordhtml": "また、イベントページからこちらの<strong>参加者パスワード</strong>でのキャンセルもできます",
+ "views.emails.addeventattendee.removelink": "イベント参加をキャンセル(辞退)しますか? こちらのリンクをクリック",
+ "views.emails.addeventattendee.removepassword": "また、イベントページからこちらの参加者パスワードでのキャンセルもできます",
+ "views.emails.addeventattendee.toremove": "このイベントへの参加をキャンセル(辞退)しますか?",
+ "views.emails.addeventcomment.link": "コメントを確認",
+ "views.emails.addeventcomment.preface": "{{ siteName }} であなたが参加を募っているイベントに {{ commentAuthor }} さんがコメントしました。",
+ "views.emails.addeventcomment.prefacehtml": "{{ siteName }} であなたが参加を募っているイベントに、<strong>{{ commentAuthor }}</strong> さんがコメントしました。",
+ "views.emails.createevent.desc": "イベントの編集には、こちらのボタンをクリックします。【このリンクは共有してはいけません!】 このリンクを知られることで、イベントを編集・削除されたり、匿名の参加者の氏名を知られたりする危険があります。",
+ "views.emails.createevent.editpswddesc": "イベントグループの編集コード(要秘匿)",
+ "views.emails.createevent.preface": "イベントを作成しました!",
+ "views.emails.createevent.sharelink": "こちらのリンクで参加をよびかけましょう",
+ "views.emails.createeventgroup.done": "以上です。お元気で!",
+ "views.emails.createeventgroup.dontknow": "{{siteName}} でイベントグループを作成した覚えがありませんか? どなたかがグループを作る際に自分のメールアドレスを入力したつもりで、たまたまこちらのあなた様あてに届いてしまったのかもしれません。申し訳ございませんが、上の編集リンクからイベントグループを削除していただけば、あなた様のメールアドレスをシステムから完全に削除いたします。",
+ "views.emails.createeventgroup.editgrouplink": "イベントグループの編集",
+ "views.emails.createeventgroup.editgrouplinkhtml": "イベントグループを編集",
+ "views.emails.createeventgroup.eventlink": "このグループにイベント(新たに作成するもの、既存のもののいずれでも)を追加する際は、「イベントグループにリンクする」のチェックボックスにチェックしてください。場合によっては、開いた入力欄に以下の 2 つのコードをコピーする必要があります :",
+ "views.emails.createeventgroup.link": "イベントグループを編集するには下のボタンをクリックするか、こちらのリンクをご利用ください",
+ "views.emails.createeventgroup.preface": "{{ siteName }} にイベントグループを作成しました。心から感謝し、歓迎します。",
+ "views.emails.createeventgroup.sharelink": "こちらのリンクで、このイベントグループのことを知らせましょう",
+ "views.emails.createeventhtml.desc": "イベントの編集には、下のボタンをクリックします。<strong>このリンクは共有してはいけません。</strong>このリンクを知っていれば、だれでもイベントの編集が可能となります。",
+ "views.emails.createeventhtml.editevent": "イベントを編集",
+ "views.emails.deleteevent.done": "{{siteName}} でイベントに参加した覚えがありませんか? どなたかがグループを作る際に自分のメールアドレスを入力したつもりで、たまたまこちらのあなた様あてに届いてしまったのかもしれません。ご心配にはおよびません。このイベントとともにあなた様のメールアドレスも、システムから削除しました。",
+ "views.emails.deleteevent.preface": "あなたが参加を予定していた {{ eventName }} のイベントは、作成した主催者により {{ siteName }} から削除されました。",
+ "views.emails.dontknow": "{{siteName}} でイベントに参加した覚えがありませんか? どなたかが自分のメールアドレスを入力したつもりで、たまたまこちらのあなた様あてに届いてしまった可能性があります。このイベントに参加したくない場合は、上のキャンセルリンクまたは削除パスワードで手続きしていただけると助かります。",
+ "views.emails.editevent.dontknow": "{{siteName}} でイベントに参加した覚えがありませんか? どなたかが自分のメールアドレスを入力したつもりで、たまたまこちらのあなた様あてに届いてしまったのかもしれません。ご心配には及びません。対応は不要です。あなた様のメールアドレスは、イベント終了後にイベントの情報とともに削除いたします。",
+ "views.emails.editevent.link": "イベントを確認",
+ "views.emails.editevent.preface": "{{ siteName }} であなたが参加を予定しているイベントに変更がありました。",
+ "views.emails.eventgroupupdate.afterlink": " ",
+ "views.emails.eventgroupupdate.beforelink": "追加されたイベント : ",
+ "views.emails.eventgroupupdate.dontknow": "{{siteName}} でイベントグループ情報を購読した覚えがありませんか? どなたかが自分のメールアドレスを入力したつもりで、たまたまこちらのあなた様あてに届いてしまったのかもしれません。",
+ "views.emails.eventgroupupdate.dontknowhtml": "{{siteName}} でイベントグループ情報を購読した覚えがありませんか? どなたかが自分のメールアドレスを入力したつもりで、たまたまこちらのあなた様あてに届いてしまったのかもしれません。こちらをクリックして購読解除してください",
+ "views.emails.eventgroupupdate.grouplink": "イベントグループを確認",
+ "views.emails.eventgroupupdate.holduphtml": "待った! - このようなメールは不要です",
+ "views.emails.eventgroupupdate.link": "追加されたイベントは '{{ eventName }}'",
+ "views.emails.eventgroupupdate.preface": "{{ siteName }} のイベントグループ、{{ eventGroupName }} に新しいイベントが登録されました",
+ "views.emails.eventgroupupdate.prefacehtml": "{{ siteName }} のイベントグループ、{{ eventGroup }} に新しいイベントが登録されました。",
+ "views.emails.eventgroupupdate.unsubscribe": "クリックして購読解除",
+ "views.emails.holdup": "待った! - このメールについて覚えがありません",
+ "views.emails.love": "今後ともよろしくお願いいたします。",
+ "views.emails.magiclink.dontknow": "{{siteName}} でイベントを作成した覚えがありませんか? どなたかが自分のメールアドレスを入力したつもりで、たまたまこちらのあなた様あてに届いてしまったのかもしれません。ご心配には及びません。対応は不要です。リンクの期限が切れた後に、こちらのあなた様のメールアドレスも削除いたします。",
+ "views.emails.magiclink.link": "このリンクの有効期限は 24 時間です。有効期限内では何度でも利用できます。このリンクを知ってさえいれば、だれでもあなたを詐称してイベントを作成できてしまいます。そのため SNS などで広く共有することは禁止します。",
+ "views.emails.magiclink.preface": "{{ siteName }} でイベントを作成するマジックリンクはこちらです。",
+ "views.emails.removeeventattendee.dontknow": "{{siteName}} でイベントに参加した覚えがありませんか? どなたかが自分のメールアドレスを入力したつもりで、たまたまこちらのあなた様あてに届いてしまったのかもしれません。ご心配には及びません。このイベントについてのメールはもう送信いたしません。こちらのあなた様のメールアドレスは、データベースから削除しました。",
+ "views.emails.removeeventattendee.preface": "{{ siteName }} のイベント、{{ eventName }} について、イベント主催者があなたの参加をキャンセルしました。",
+ "views.emails.subscribed.desc": "今後、グループに新たなイベントが登録されるたびにメールを送信します。またいつでも購読解除できます。",
+ "views.emails.subscribed.preface": "{{ siteName }} のイベントグループ、{{ eventGroupName }} の情報を購読登録しました。",
+ "views.emails.unattendevent.desc": "覚えがない場合は、管理者があなたをイベントからキャンセルした可能性があります。",
+ "views.emails.unattendevent.dontknow": "{{siteName}} でイベントに参加した覚えがありませんか? どなたかが自分のメールアドレスを入力したつもりで、たまたまこちらのあなた様あてに届いてしまったのかもしれません。ご心配には及びません。このイベントに関連するあなた様のメールアドレスは削除しました。",
+ "views.emails.unattendevent.preface": "{{ siteName }} でイベント参加をキャンセルしました。今後、このイベントについてのメールは送信しません。",
+ "views.event.about": "このイベントについて",
+ "views.event.addme": "参加",
+ "views.event.addmyself": "参加する",
+ "views.event.addself": "'{{eventData.name}}' に参加",
+ "views.event.addtoGC": "Google カレンダーに追加",
+ "views.event.attendeeemail": "あなたのメールアドレス(任意)",
+ "views.event.attendeename": "参加者名",
+ "views.event.attendeenamedesc": "またはニックネームとか...",
+ "views.event.attendeenum": "何人で参加しますか?",
+ "views.event.attendees": "参加者",
+ "views.event.attendeevisible": "参加者リストに参加者名を載せる",
+ "views.event.attendeevisibledesc": "参加者名を匿名にする場合でも、イベント主催者だけは確認できます。",
+ "views.event.attention": "このイベントの編集用パスワード(要、秘匿) : <strong>{{eventData.editToken}}</strong></br>ブラウザストレージに保存しました。メールアドレスを入力したのならメールでも送信しています。</br>メールアドレスを入力しなかったのなら、<strong>すぐに安全な場所に保存</strong>してください。もう二度と表示しません!",
+ "views.event.capacity": "このイベントは満員です。",
+ "views.event.comment": "コメント",
+ "views.event.commentauthor": "お名前",
+ "views.event.commentauthorph": "お名前をお願いします",
+ "views.event.commentcontent": "コメントをどうぞ",
+ "views.event.concludeddel": "このイベントは終了しました。編集もできなくなりました。{{#if eventWillBeDeleted}}また {{daysUntilDeletion}}に自動的に削除します。{{/if}}",
+ "views.event.confremoveattendee": "この参加者をイベントから削除します。よろしいですか? この操作は取り消しできません。",
+ "views.event.del": "イベント削除",
+ "views.event.delconfirm": "このイベントを削除します。よろしいですか? この操作は取り消しできません。",
+ "views.event.deletetitle": "'{{ eventData.name }}' を削除",
+ "views.event.discussion": "コメント",
+ "views.event.edit": "イベントを編集",
+ "views.event.editlater": "後で編集することもできます(必須)",
+ "views.event.ended": "終了済み",
+ "views.event.enternum": "人数を入力してください",
+ "views.event.hidden": " - 匿名",
+ "views.event.hiddenattendee_one": "匿名 {{count}} 人",
+ "views.event.hiddenattendee_other": "匿名 {{count}} 人",
+ "views.event.hiddenattendee_zero": "匿名 なし",
+ "views.event.hostedby": "主催 : </span> {{eventData.hostName}}",
+ "views.event.ICSexport": "iCalendar ファイル出力",
+ "views.event.joinemaildesc": "ご自身のメールアドレスを入力すると、このイベントの情報をメールで受けられます。",
+ "views.event.locationdesc": "具体的に",
+ "views.event.lostpswd": "パスワードをなくしましたか ? イベントの主催者に連絡を。",
+ "views.event.noattendees": "まだ参加者がいません !",
+ "views.event.nospam": "SPAM を送ることはありません!",
+ "views.event.numberofattende": "(現在 : {{numberOfAttendees}} 人)",
+ "views.event.numlimit": "1 〜 ${response.data.freeSpots} で人数を入力してください。",
+ "views.event.partof": "<a href='/group/{{eventData.eventGroup.id}}'>{{eventData.eventGroup.name}}</a> グループのイベント",
+ "views.event.postbutton": "送信",
+ "views.event.remaining_one": "残り {{count}} 枠 - 参加登録しましょう !",
+ "views.event.remaining_other": "残り {{count}} 枠 - 参加登録しましょう !",
+ "views.event.remaining_zero": "このイベントは満員です。",
+ "views.event.removeAttendee": "参加者をキャンセル",
+ "views.event.removeattendeedesc": "'{{eventData.name}}' の参加者をキャンセル",
+ "views.event.removeme": "キャンセル(辞退)",
+ "views.event.removemyself": "キャンセル(辞退)する",
+ "views.event.removemyselfdesc": "'{{eventData.name}}' への参加をキャンセル(辞退)する",
+ "views.event.removepswd": "参加者パスワード",
+ "views.event.removepswddesc": "この参加者パスワードは、イベントへの参加をキャンセル(辞退)する際に使います。\nメールアドレスを入力すれば、メールに送信します。\nメールを入力しない場合は、<strong>二度と表示されない</strong>ので、今すぐ書き留めてください。",
+ "views.event.removetitle": "{{ attendeeName }} を {{ eventName }} から削除",
+ "views.event.removeuser": "ユーザーをイベントから削除",
+ "views.event.reply": "返信",
+ "views.event.replycontent": "返信の内容は?",
+ "views.event.share": "イベントを共有するには、このメッセージのすぐ上に表示しているリンクをご利用ください。参加者にはイベントを編集したり削除したりすることはできません。",
+ "views.event.showonGM": "Google マップで表示",
+ "views.event.showonOM": "OpenStreetMap で表示",
+ "views.event.started": "開催中",
+ "views.event.welcome": "イベントを作成しました。ようこそ!",
+ "views.eventgroup.about": "このグループについて",
+ "views.eventgroup.addevent": "このグループにイベントをリンクするには、新しくイベントを作る際に、もしくは既存のイベントを編集して、以下の 2 つのコードをコピー・貼り付けします。",
+ "views.eventgroup.del": "このイベントグループを削除",
+ "views.eventgroup.delconfirm": "このイベントグループを削除します。よろしいですか? この操作は取り消しできません。",
+ "views.eventgroup.deldesc": "この操作では、グループの個々のイベントは<strong>削除しません</strong>。それらのイベントを後で別のグループにリンクすることもできます。",
+ "views.eventgroup.deletetitle": "'{{ eventGroupData.name }}' を削除",
+ "views.eventgroup.edit": "グループ編集",
+ "views.eventgroup.editmode": "編集モードへ",
+ "views.eventgroup.editpswd": "イベントグループ編集パスワード",
+ "views.eventgroup.feedlinkdesc": "この URL をカレンダーアプリに登録することで、このグループのイベントスケジュールを購読できます。",
+ "views.eventgroup.hostedby": "主催 : {{eventGroupData.hostName}}",
+ "views.eventgroup.ICSexport": "iCalendar ファイル",
+ "views.eventgroup.pastevents": "過去のイベント",
+ "views.eventgroup.subscribe": "'{{eventGroupData.name}}' のイベント情報を購読",
+ "views.eventgroup.subscribebutton": "登録",
+ "views.eventgroup.subscribedesc": "このグループが新しいイベントを追加した際にメールでお知らせします。希望される場合は、メールアドレスを入力してください。",
+ "views.eventgroup.subscribetitle": "更新通知の登録",
+ "views.eventgroup.upcomingevents": "今後のイベント",
+ "views.eventgroup.welcome": "あなたのイベントグループ ページへ、ようこそ!</br>すでに編集用リンク(要、秘匿)をメールで送信していますし、いまアドレスバーの URL にも表示しています。メールが届いてない? SPAM ・迷惑メールのフォルダー内もご確認を。</br>イベントグループを共有するには、このメッセージの下に表示しているリンクをご利用ください。このリンクから参加者がグループにアクセスしても、あなたが作ったこのグループを編集・削除することはできません !",
+ "views.home.about": "{{ siteName }} のご紹介",
+ "views.home.aboutgathio": "Gathio について",
+ "views.home.attention": "ご注意 : すべてのイベントは URL さえ入手すれば、だれでも見ることができます。ですから Gathio はサプライズの誕生日パーティーや秘密の革命事業には使わない方がいいでしょう…とはいえ、どうぞご自由に。",
+ "views.home.autodelete": "このインスタンスサーバーの設定によっては、イベント終了後の一定のタイミングでイベントを自動的に削除します。関連するあなたご自身のデータも、いっしょにデータベースから完全に削除します。",
+ "views.home.conftitle": "お好みの設定に",
+ "views.home.fedtitle": "連合プロトコルとセルフホスト",
+ "views.home.flagshipsetting": "<a href = \"https://gath.io\">Gathio のフラッグシップインスタンスは gath.io </a>です。終わり次第イベントは順次削除、URL を知っている人にしか開けないイベントを、だれでも作成できるよう設計しています。</br>もう一度言います、ここではだれでもイベントを作成できます。イベントがパブリックな場所に公開されることはありません。またイベントは終了して 7 日後に自動削除します。",
+ "views.home.imgexample": "ピクニックに出かけるイベントページの例です。イベントの場所、主催、日時と説明を記載しています。また Google カレンダーに保存、エクスポート、場所を OpenStreetMap と Google マップで開くこともできます。",
+ "views.home.intro": "Gathio は、簡単、プライバシーファーストで、イベントの主催と参加、共有をサポートするシステムです。連合プロトコルにも対応しています。",
+ "views.home.kofi": "Ko-fi で支援を",
+ "views.home.onpre": "あなたのコミュニティーで自分たちの Gathio インスタンスを建てるなら、そこではイベントの作成を特定の人にしかできないようにしたり、便利な公開イベントリストをホームページに表示したり、終了後にイベントを削除しない…といった設定・制限を加えることもできます。",
+ "views.home.opensource": "オープンソース",
+ "views.home.osdesc": "Gathio はオープンソースであることを光栄に思っており、素敵な人たちが集まるグループで作成しています。 \nどんな問題でも<a href = \"https://github.com/lowercasename/gathio/issues\">トラッカー</a>で質問してください。",
+ "views.home.privacy": "また、Gathio は広告を表示しません。あなたのデータを第三者に提供・売却することはしません。不要なメールを送ることもありません。",
+ "views.home.privacytitle": "プライバシーファースト",
+ "views.home.privdesc": "Gathio にはアカウントはありません。イベントを作成した時点で、後で編集する際のパスワードを生成しお知らせします。参加を呼びかける相手には、参加用リンクを伝えるだけ。共同主催者には、編集用リンク(パスワード埋め込み済み)を伝えます。",
+ "views.home.privmail": "メールアドレスを入力すると編集パスワードを受信できます。けっしてなくさないように――それでもメールアドレスの入力は任意です!",
+ "views.home.selfhost": "Gathio は簡単にセルフホストできます。Mastodon、Pleroma、Friendica などの ActivityPub サービスをサポートしているので、Fediverse のどこからでもイベントにアクセスできます。\n私たちは、あなた方が自分たちのコミュニティー用に自前のインスタンスを建てることを応援します。詳しい手順・説明は、<a href = \"https://docs.gath.io/using-gathio/fediverse/\">AactivityPub access</a> や GitHub wiki の<a href = \"https://docs.gath.io/running-gathio/installation/\">セルフホスト インストール</a>を参考にしてください。",
+ "views.home.sponsor": "Gathio、よく使ってるし便利だよね…と思ったら、 <a href=\"https://github.com/sponsors/lowercasename\" class=\"text-success\">GitHub スポンサーで Raphael への支援</a>をご検討ください。プロジェクトの維持とメインサイトの運営に役立てます! <i class=\"far fa-heart\"></i>",
+ "views.incorrectpswd": "編集パスワードが一致しません。もう一度やり直してください。",
+ "views.interaction": "ユーザーにコメントを許可する",
+ "views.join": "ユーザーは自分で参加登録する",
+ "views.layouts.main.defaultmetadata": "より簡単に、より速く、プライバシー侵害のより小さく、イベントを作成・共有する方法",
+ "views.layouts.main.footnote": "<strong>Gathio</strong> {{version}} バージョン &middot; <a href=\"https://github.com/lowercasename/gathio\">GitHub</a> &middot; <a href=\"https://raphaelkabo.com\">Raphael</a> と <a href=\"https://github.com/lowercasename/gathio/graphs/contributors\">協力者たち</a> が <i class=\"far fa-heart\"></i> を込めて作成しています。",
+ "views.newevent.createnew": "イベントを作成",
+ "views.newevent.groupattention": "イベントグループは、イベントのように自動削除されることはありません。しかし、{{siteName}} から削除されたイベントは、当然ですが、イベントグループに表示されません。",
+ "views.newevent.groupdesc": "イベントグループは、リンクしたイベントをまとめる機能です。例えば、シリーズものの映画上映会、いくつかのイベントに分かれるフェス、バンドのツアーなどに便利です。個々のイベントへの公開リンクのように、グループの公開リンクを共有することができます。また秘密の編集パスワード(グループ作成時にメール送信)を知っているメンバーは、今後のイベントをグループに追加することができます。",
+ "views.newevent.importevent": "既存のイベントをインポート",
+ "views.newevent.neweventbutton": "イベントを作成",
+ "views.newevent.neweventgroup": "イベントグループを作成",
+ "views.newevent.newgroup": "グループを作成",
+ "views.newevent.pagetitle": "どれからはじめますか?",
+ "views.newevent.visiblealert": "イベントは、リンクさえ知っていれば、だれでも見られます。",
+ "views.options.showlistattendees": "参加者リストを表示",
+ "views.partials.choosefile": "ファイルを選択",
+ "views.partials.creatoremaildesc": "メールアドレスを入力すると、編集用秘密パスワードを受信できます。また、イベントについての更新情報も届きます。",
+ "views.partials.delimg": "画像を削除",
+ "views.partials.editevent.delthis": "このイベントを削除",
+ "views.partials.editevent.edit": "'{{eventData.name}}' を編集",
+ "views.partials.editeventgroup.del": "このイベントグループを削除する",
+ "views.partials.editeventgroup.delbutton": "イベントグループを削除",
+ "views.partials.eventform.creatoremail": "あなたのメールアドレス",
+ "views.partials.eventform.eventdescription": "説明",
+ "views.partials.eventform.eventgroup": "イベントグループにリンクする",
+ "views.partials.eventform.eventgroupdata": "このイベントをイベントグループにリンク",
+ "views.partials.eventform.eventgroupedittoken": "この長い文字列は、グループを作成したときに送信する確認メールにも記載しています。",
+ "views.partials.eventform.eventgroupid": "この短い文字列は、イベントグループのリンク、確認メール、イベントグループのページに記載しています。",
+ "views.partials.eventform.eventgrouplinker": "登録してあるイベントグループを選択",
+ "views.partials.eventform.eventurl": "リンク",
+ "views.partials.eventform.eventurldesc": "ほかのイベントのページや外部のチケット購入等のページ(任意)",
+ "views.partials.eventform.groupbutton": "イベントグループのコードを入力",
+ "views.partials.eventform.hostname": "主催者名",
+ "views.partials.eventform.hostnamedesc": "イベントのページに表示します(任意)。",
+ "views.partials.eventform.maxattendees": "定員",
+ "views.partials.eventform.maxattendeestitle": "定員を設定する",
+ "views.partials.eventform.options": "設定",
+ "views.partials.eventform.publicevent": "このイベントを公開イベントリストに表示",
+ "views.partials.eventgroup.options": "オプション項目",
+ "views.partials.eventgroupform.creatoremail": "あなたのメールアドレス",
+ "views.partials.eventgroupform.eventgroupddesc": "説明",
+ "views.partials.eventgroupform.eventgroupurl": "リンク",
+ "views.partials.eventgroupform.eventgroupurldesc": "ほかのイベントのページや外部のチケット購入等のページ(任意)",
+ "views.partials.eventgroupform.hostname": "主催者またはグループ名",
+ "views.partials.eventgroupform.isshowningroup": "イベントグループのページに表示します(任意)。",
+ "views.partials.eventgroupform.publicgroup": "公開グループとして表示する",
+ "views.partials.eventlist.noevents": "イベントなし!",
+ "views.partials.fixerrors": "エラーを修正してください :",
+ "views.partials.importevent.selectfile": "ファイルを選択",
+ "views.partials.importeventform.import": "インポート",
+ "views.partials.importeventform.importdesc": ".ics ファイルをアップロードすることで、簡単にイベントを作成できます。Facebook のイベントは、コンテキストメニューをクリックし、「カレンダーに追加」を選ぶことでファイルをダウンロードできます。",
+ "views.partials.importeventform.importing": "インポートしています...",
+ "views.partials.instancerules.instancesettings": "インスタンス設定",
+ "views.partials.mdsupport": "<a href='https://commonmark.org/help/'>Markdown</a> 書式対応",
+ "views.partials.recommendeddimensions": "推奨する画像サイズ(幅 ✕ 高さ): 920 ✕ 300 ピクセル",
+ "views.partials.save": "変更を保存",
+ "views.partials.saving": "保存しています...",
+ "views.partials.sidebar.about": "{{ siteName }} について",
+ "views.partials.sidebar.createevent": "イベントを作成",
+ "views.partials.sidebar.events": "イベントを探す",
+ "views.partials.snappy": "簡潔に(必須)",
+ "views.partials.wontshow": "どこにも表示しません(任意)",
+ "views.publiceventlist.events": "公開イベント",
+ "views.publiceventlist.groups": "公開グループ",
+ "views.publiceventlist.nogroups": "公開グループなし!",
+ "views.publiceventlist.numoevents_one": "{{count}} 件のイベント",
+ "views.publiceventlist.numoevents_other": "{{count}} 件のイベント",
+ "views.publiceventlist.numoevents_zero": "イベントなし",
+ "views.publiceventlist.pastevents": "過去の公開イベント",
+ "views.publiceventlist.upcomingevents": "今後の公開イベント",
+ "views.right": "了解!"
+} \ No newline at end of file
diff --git a/package.json b/package.json
index 6f60eb5..4f74f3d 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,6 @@
"dependencies": {
"@sendgrid/helpers": "^8.0.0",
"@sendgrid/mail": "^6.5.5",
- "@types/cookie-parser": "^1.4.7",
"activitypub-types": "^1.1.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
@@ -31,12 +30,20 @@
"express-session": "^1.18.0",
"express-validator": "^6.15.0",
"generate-rsa-keypair": "^0.2.1",
+ "handlebars": "^4.7.8",
+ "handlebars-i18next": "^1.0.3",
+ "i18next": "^24.2.2",
+ "i18next-browser-languagedetector": "^8.0.4",
+ "i18next-fs-backend": "^2.6.0",
+ "i18next-http-backend": "^3.0.2",
+ "i18next-http-middleware": "^3.7.1",
"ical": "^0.6.0",
"ical-generator": "^1.15.4",
"jimp": "^0.16.13",
"jsdom": "^22.1.0",
"mailgun.js": "^12.0.2",
"marked": "^12.0.2",
+ "moment": "^2.30.1",
"moment-timezone": "^0.5.45",
"mongoose": "^5.13.22",
"multer": "1.4.5-lts.1",
@@ -45,6 +52,7 @@
"node-schedule": "^1.3.3",
"nodemailer": "^6.9.13",
"randomstring": "^1.3.0",
+ "react-i18next": "^15.4.1",
"request": "^2.88.2",
"sanitize-html": "^2.13.0",
"toml": "^3.0.0",
@@ -53,8 +61,11 @@
"wait-on": "^7.2.0"
},
"devDependencies": {
+ "@types/cookie-parser": "^1.4.7",
"@types/dompurify": "^3.0.5",
"@types/express": "^4.17.21",
+ "@types/handlebars": "^4.1.0",
+ "@types/i18next-fs-backend": "^1.2.0",
"@types/ical": "^0.8.3",
"@types/jsdom": "^21.1.6",
"@types/multer": "^1.4.11",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ad78dbb..41bd9f0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,9 +14,6 @@ importers:
'@sendgrid/mail':
specifier: ^6.5.5
version: 6.5.5
- '@types/cookie-parser':
- specifier: ^1.4.7
- version: 1.4.7
activitypub-types:
specifier: ^1.1.0
version: 1.1.0
@@ -47,6 +44,27 @@ importers:
generate-rsa-keypair:
specifier: ^0.2.1
version: 0.2.1
+ handlebars:
+ specifier: ^4.7.8
+ version: 4.7.8
+ handlebars-i18next:
+ specifier: ^1.0.3
+ version: 1.0.3(handlebars@4.7.8)(i18next@24.2.2(typescript@5.4.5))
+ i18next:
+ specifier: ^24.2.2
+ version: 24.2.2(typescript@5.4.5)
+ i18next-browser-languagedetector:
+ specifier: ^8.0.4
+ version: 8.0.4
+ i18next-fs-backend:
+ specifier: ^2.6.0
+ version: 2.6.0
+ i18next-http-backend:
+ specifier: ^3.0.2
+ version: 3.0.2
+ i18next-http-middleware:
+ specifier: ^3.7.1
+ version: 3.7.1
ical:
specifier: ^0.6.0
version: 0.6.0
@@ -65,6 +83,9 @@ importers:
marked:
specifier: ^12.0.2
version: 12.0.2
+ moment:
+ specifier: ^2.30.1
+ version: 2.30.1
moment-timezone:
specifier: ^0.5.45
version: 0.5.45
@@ -89,6 +110,9 @@ importers:
randomstring:
specifier: ^1.3.0
version: 1.3.0
+ react-i18next:
+ specifier: ^15.4.1
+ version: 15.4.1(i18next@24.2.2(typescript@5.4.5))(react@19.0.0)
request:
specifier: ^2.88.2
version: 2.88.2
@@ -108,12 +132,21 @@ importers:
specifier: ^7.2.0
version: 7.2.0
devDependencies:
+ '@types/cookie-parser':
+ specifier: ^1.4.7
+ version: 1.4.7
'@types/dompurify':
specifier: ^3.0.5
version: 3.0.5
'@types/express':
specifier: ^4.17.21
version: 4.17.21
+ '@types/handlebars':
+ specifier: ^4.1.0
+ version: 4.1.0
+ '@types/i18next-fs-backend':
+ specifier: ^1.2.0
+ version: 1.2.0
'@types/ical':
specifier: ^0.8.3
version: 0.8.3
@@ -148,6 +181,10 @@ packages:
resolution: {integrity: sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==}
engines: {node: '>=6.9.0'}
+ '@babel/runtime@7.26.9':
+ resolution: {integrity: sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==}
+ engines: {node: '>=6.9.0'}
+
'@colors/colors@1.5.0':
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
engines: {node: '>=0.1.90'}
@@ -427,9 +464,17 @@ packages:
'@types/express@4.17.21':
resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==}
+ '@types/handlebars@4.1.0':
+ resolution: {integrity: sha512-gq9YweFKNNB1uFK71eRqsd4niVkXrxHugqWFQkeLRJvGjnxsLr16bYtcsG4tOFwmYi0Bax+wCkbf1reUfdl4kA==}
+ deprecated: This is a stub types definition. handlebars provides its own type definitions, so you do not need this installed.
+
'@types/http-errors@2.0.4':
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
+ '@types/i18next-fs-backend@1.2.0':
+ resolution: {integrity: sha512-30XnjBF+SLndemvooRQRtcoD5xmF2nNNTa5RqE63+OWssaZOxN9r895ZvXUMbkk8YZr1Cy3scpQBZ6YXLH+jEg==}
+ deprecated: This is a stub types definition. i18next-fs-backend provides its own type definitions, so you do not need this installed.
+
'@types/ical@0.8.3':
resolution: {integrity: sha512-qPejGORaXOstmqyKzp0Qw9nXDPiWiahiJJcx4zMB0zJVg0rLfJ6bDip/naqagEqYTjKl/LI91399hR8zFwRJ5A==}
@@ -790,6 +835,9 @@ packages:
resolution: {integrity: sha512-s4odpheTyydAbTBQepsqd2rNWGa2iV3cyo8g7zbI2QQYGLVsfbhmwukayS1XHppe02Oy1fg7mg6xoaraVJeEcg==}
engines: {node: '>=0.8'}
+ cross-fetch@4.0.0:
+ resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
+
cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
@@ -1186,6 +1234,12 @@ packages:
graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
+ handlebars-i18next@1.0.3:
+ resolution: {integrity: sha512-B4AEDBcBo4cJ+ghY9DWpABNNEQye1TEI4bebXhPaXJ6qM/1jfIVPXD0mx82qj3ceD7LUeRuCYp5CPd+mK6SMGw==}
+ peerDependencies:
+ handlebars: '4'
+ i18next: '>=11'
+
handlebars@4.7.8:
resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
engines: {node: '>=0.4.7'}
@@ -1227,6 +1281,9 @@ packages:
resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==}
engines: {node: '>=12'}
+ html-parse-stringify@3.0.1:
+ resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
+
htmlparser2@8.0.2:
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
@@ -1254,6 +1311,26 @@ packages:
resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==}
engines: {node: '>=8.12.0'}
+ i18next-browser-languagedetector@8.0.4:
+ resolution: {integrity: sha512-f3frU3pIxD50/Tz20zx9TD9HobKYg47fmAETb117GKGPrhwcSSPJDoCposXlVycVebQ9GQohC3Efbpq7/nnJ5w==}
+
+ i18next-fs-backend@2.6.0:
+ resolution: {integrity: sha512-3ZlhNoF9yxnM8pa8bWp5120/Ob6t4lVl1l/tbLmkml/ei3ud8IWySCHt2lrY5xWRlSU5D9IV2sm5bEbGuTqwTw==}
+
+ i18next-http-backend@3.0.2:
+ resolution: {integrity: sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==}
+
+ i18next-http-middleware@3.7.1:
+ resolution: {integrity: sha512-nVTSGB1P4Gad5PFQYf3xVUOzJ4tVSQYD8Rs0luyWkjEMwqdqAcZ9CqIzqYwVLgB5/BKr1COI0oAei5dlYzmGbg==}
+
+ i18next@24.2.2:
+ resolution: {integrity: sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ==}
+ peerDependencies:
+ typescript: ^5
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
ical-generator@1.15.4:
resolution: {integrity: sha512-drXe4RLkfNlvDvdy/E6BUI9p+01L3ySK1ufNEYI9TxNKG9ZA3G60QWoZvD1dtmH4scwDxYu6/sZBPJvYVNrj8A==}
engines: {node: '>=6.0.0'}
@@ -1624,6 +1701,15 @@ packages:
niceware@3.0.0:
resolution: {integrity: sha512-DbeDuqe836Ba4S9vjim4jTbbqmjCMwuAXFCVdh4QAvbmLOhmIQs84IakYrcXd/87VCsj1XKhSmmg7bAmwAEh5A==}
+ node-fetch@2.7.0:
+ resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
+ engines: {node: 4.x || >=6.0.0}
+ peerDependencies:
+ encoding: ^0.1.0
+ peerDependenciesMeta:
+ encoding:
+ optional: true
+
node-schedule@1.3.3:
resolution: {integrity: sha512-uF9Ubn6luOPrcAYKfsXWimcJ1tPFtQ8I85wb4T3NgJQrXazEzojcFZVk46ZlLHby3eEJChgkV/0T689IsXh2Gw==}
@@ -1870,6 +1956,23 @@ packages:
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
engines: {node: '>= 0.8'}
+ react-i18next@15.4.1:
+ resolution: {integrity: sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw==}
+ peerDependencies:
+ i18next: '>= 23.2.3'
+ react: '>= 16.8.0'
+ react-dom: '*'
+ react-native: '*'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ react-native:
+ optional: true
+
+ react@19.0.0:
+ resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
+ engines: {node: '>=0.10.0'}
+
readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
@@ -2142,6 +2245,9 @@ packages:
resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
engines: {node: '>=6'}
+ tr46@0.0.3:
+ resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+
tr46@4.1.1:
resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==}
engines: {node: '>=14'}
@@ -2257,6 +2363,10 @@ packages:
resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==}
engines: {'0': node >=0.6.0}
+ void-elements@3.1.0:
+ resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
+ engines: {node: '>=0.10.0'}
+
w3c-xmlserializer@4.0.0:
resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==}
engines: {node: '>=14'}
@@ -2266,6 +2376,9 @@ packages:
engines: {node: '>=12.0.0'}
hasBin: true
+ webidl-conversions@3.0.1:
+ resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+
webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
@@ -2282,6 +2395,9 @@ packages:
resolution: {integrity: sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==}
engines: {node: '>=14'}
+ whatwg-url@5.0.0:
+ resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@@ -2355,6 +2471,10 @@ snapshots:
dependencies:
regenerator-runtime: 0.14.1
+ '@babel/runtime@7.26.9':
+ dependencies:
+ regenerator-runtime: 0.14.1
+
'@colors/colors@1.5.0':
optional: true
@@ -2749,8 +2869,16 @@ snapshots:
'@types/qs': 6.9.15
'@types/serve-static': 1.15.7
+ '@types/handlebars@4.1.0':
+ dependencies:
+ handlebars: 4.7.8
+
'@types/http-errors@2.0.4': {}
+ '@types/i18next-fs-backend@1.2.0':
+ dependencies:
+ i18next-fs-backend: 2.6.0
+
'@types/ical@0.8.3':
dependencies:
rrule: 2.6.4
@@ -3115,6 +3243,12 @@ snapshots:
is-nan: 1.3.2
moment-timezone: 0.5.45
+ cross-fetch@4.0.0:
+ dependencies:
+ node-fetch: 2.7.0
+ transitivePeerDependencies:
+ - encoding
+
cross-spawn@7.0.3:
dependencies:
path-key: 3.1.1
@@ -3636,6 +3770,11 @@ snapshots:
graphemer@1.4.0: {}
+ handlebars-i18next@1.0.3(handlebars@4.7.8)(i18next@24.2.2(typescript@5.4.5)):
+ dependencies:
+ handlebars: 4.7.8
+ i18next: 24.2.2(typescript@5.4.5)
+
handlebars@4.7.8:
dependencies:
minimist: 1.2.8
@@ -3672,6 +3811,10 @@ snapshots:
dependencies:
whatwg-encoding: 2.0.0
+ html-parse-stringify@3.0.1:
+ dependencies:
+ void-elements: 3.1.0
+
htmlparser2@8.0.2:
dependencies:
domelementtype: 2.3.0
@@ -3716,6 +3859,26 @@ snapshots:
human-signals@1.1.1: {}
+ i18next-browser-languagedetector@8.0.4:
+ dependencies:
+ '@babel/runtime': 7.24.6
+
+ i18next-fs-backend@2.6.0: {}
+
+ i18next-http-backend@3.0.2:
+ dependencies:
+ cross-fetch: 4.0.0
+ transitivePeerDependencies:
+ - encoding
+
+ i18next-http-middleware@3.7.1: {}
+
+ i18next@24.2.2(typescript@5.4.5):
+ dependencies:
+ '@babel/runtime': 7.24.6
+ optionalDependencies:
+ typescript: 5.4.5
+
ical-generator@1.15.4(@types/node@20.12.12):
dependencies:
'@types/node': 20.12.12
@@ -4097,6 +4260,10 @@ snapshots:
binary-search: 1.3.6
randombytes: 2.1.0
+ node-fetch@2.7.0:
+ dependencies:
+ whatwg-url: 5.0.0
+
node-schedule@1.3.3:
dependencies:
cron-parser: 2.18.0
@@ -4303,6 +4470,15 @@ snapshots:
iconv-lite: 0.4.24
unpipe: 1.0.0
+ react-i18next@15.4.1(i18next@24.2.2(typescript@5.4.5))(react@19.0.0):
+ dependencies:
+ '@babel/runtime': 7.26.9
+ html-parse-stringify: 3.0.1
+ i18next: 24.2.2(typescript@5.4.5)
+ react: 19.0.0
+
+ react@19.0.0: {}
+
readable-stream@2.3.8:
dependencies:
core-util-is: 1.0.3
@@ -4611,6 +4787,8 @@ snapshots:
universalify: 0.2.0
url-parse: 1.5.10
+ tr46@0.0.3: {}
+
tr46@4.1.1:
dependencies:
punycode: 2.3.1
@@ -4696,6 +4874,8 @@ snapshots:
core-util-is: 1.0.2
extsprintf: 1.3.0
+ void-elements@3.1.0: {}
+
w3c-xmlserializer@4.0.0:
dependencies:
xml-name-validator: 4.0.0
@@ -4710,6 +4890,8 @@ snapshots:
transitivePeerDependencies:
- debug
+ webidl-conversions@3.0.1: {}
+
webidl-conversions@7.0.0: {}
whatwg-encoding@2.0.0:
@@ -4723,6 +4905,11 @@ snapshots:
tr46: 4.1.1
webidl-conversions: 7.0.0
+ whatwg-url@5.0.0:
+ dependencies:
+ tr46: 0.0.3
+ webidl-conversions: 3.0.1
+
which@2.0.2:
dependencies:
isexe: 2.0.0
diff --git a/src/app.ts b/src/app.ts
index 7ed535c..0f8e1f7 100755
--- a/src/app.ts
+++ b/src/app.ts
@@ -1,6 +1,20 @@
import express from "express";
import cookieParser from "cookie-parser";
import { create as createHandlebars, ExpressHandlebars } from "express-handlebars";
+import i18next from "i18next";
+import Backend from "i18next-fs-backend";
+import { LanguageDetector, handle } from 'i18next-http-middleware';
+import { createRequire } from 'module';
+import { fileURLToPath } from 'url';
+import { dirname } from 'path';
+import path from 'path';
+
+const require = createRequire(import.meta.url);
+const handlebarsI18next = require('handlebars-i18next');
+
+// Recreate __dirname in ES module
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
import routes from "./routes.js";
import frontend from "./routes/frontend.js";
@@ -9,76 +23,150 @@ import event from "./routes/event.js";
import group from "./routes/group.js";
import staticPages from "./routes/static.js";
import magicLink from "./routes/magicLink.js";
+import { getI18nHelpers } from "./helpers.js";
import {
activityPubContentType,
alternateActivityPubContentType,
} from "./lib/activitypub.js";
+import moment from "moment";
import { EmailService } from "./lib/email.js";
import getConfig from "./lib/config.js";
const app = express();
const config = getConfig();
-const hbsInstance = createHandlebars({
- defaultLayout: "main",
- partialsDir: ["views/partials/"],
- layoutsDir: "views/layouts/",
- helpers: {
- plural: function (number: number, text: string) {
- const singular = number === 1;
- // If no text parameter was given, just return a conditional s.
- if (typeof text !== "string") return singular ? "" : "s";
- // Split with regex into group1/group2 or group1(group3)
- const match = text.match(/^([^()\/]+)(?:\/(.+))?(?:\((\w+)\))?/);
- // If no match, just append a conditional s.
- if (!match) return text + (singular ? "" : "s");
- // We have a good match, so fire away
- return (
- (singular && match[1]) || // Singular case
- match[2] || // Plural case: 'bagel/bagels' --> bagels
- match[1] + (match[3] || "s")
- ); // Plural case: 'bagel(s)' or 'bagel' --> bagels
- },
- json: function (context: object) {
- return JSON.stringify(context);
+// function to construct __dirname with ES module
+const getLocalesPath = () => {
+ const __filename = fileURLToPath(import.meta.url);
+ const __dirname = dirname(__filename);
+ return path.join(__dirname, '..', 'locales');
+};
+
+async function initializeApp() {
+ // Cookies //
+ app.use(cookieParser());
+
+ // i18next configuration
+ await i18next
+ .use(Backend)
+ .use(LanguageDetector)
+ .init({
+ backend: {
+ loadPath: path.join(getLocalesPath(), '{{lng}}.json'),
+ },
+ fallbackLng: 'en',
+ preload: ['en', 'ja'],
+ supportedLngs: ['en', 'ja'],
+ nonExplicitSupportedLngs: true,
+ load: 'languageOnly',
+ debug: false,
+ detection: {
+ order: ['header', 'cookie'],
+ lookupHeader: 'accept-language',
+ lookupCookie: 'i18next',
+ caches: ['cookie']
+ },
+ interpolation: {
+ escapeValue: false
+ }
+ });
+
+ app.use(handle(i18next));
+
+ // to Switch language
+ app.use((req, res, next) => {
+ const currentLanguage = i18next.language;
+ i18next.changeLanguage(req.language);
+ const newLanguage = i18next.language;
+// Uncomment for debugging
+// console.log('Language Change:', {
+// header: req.headers['accept-language'],
+// detected: req.language,
+// currentLanguage: currentLanguage,
+// newLanguage: newLanguage
+// });
+ next();
+ });
+
+// Uncomment for debugging
+// app.use((req, res, next) => {
+// console.log('Language Detection:', {
+// header: req.headers['accept-language'],
+// detected: req.language,
+// i18next: i18next.language
+// });
+// next();
+// });
+
+ // View engine //
+ const hbsInstance = createHandlebars({
+ defaultLayout: "main",
+ partialsDir: ["views/partials/"],
+ layoutsDir: "views/layouts/",
+ helpers: {
+ // add i18next helpers
+ ...getI18nHelpers(),
+ plural: function (key: string, count: number, options: any) { // Register the plural helper
+ const translation = i18next.t(key, { count: count });
+ return translation;
+ },
+ json: function (context: object) {
+ return JSON.stringify(context);
+ }
},
- },
-});
-
-const emailService = new EmailService(config, hbsInstance);
-emailService.verify();
-
-app.use((req: express.Request, _: express.Response, next: express.NextFunction) => {
- req.hbsInstance = hbsInstance;
- req.emailService = emailService;
- next()
- return
-})
-
-// View engine //
-app.engine("handlebars", hbsInstance.engine);
-app.set("view engine", "handlebars");
-app.set("hbsInstance", hbsInstance);
-
-// Static files //
-app.use(express.static("public"));
-
-// Body parser //
-app.use(express.json({ type: alternateActivityPubContentType }));
-app.use(express.json({ type: activityPubContentType }));
-app.use(express.json({ type: "application/json" }));
-app.use(express.urlencoded({ extended: true }));
-
-// Cookies //
-app.use(cookieParser());
-
-// Router //
-app.use("/", staticPages);
-app.use("/", frontend);
-app.use("/", activitypub);
-app.use("/", event);
-app.use("/", group);
-app.use("/", magicLink);
-app.use("/", routes);
+ });
+
+ const emailService = new EmailService(config, hbsInstance);
+ emailService.verify();
+
+ app.use((req: express.Request, _: express.Response, next: express.NextFunction) => {
+ req.hbsInstance = hbsInstance;
+ req.emailService = emailService;
+ next()
+ return
+ })
+
+ // View engine //
+ app.engine("handlebars", hbsInstance.engine);
+ app.set("view engine", "handlebars");
+ app.set("hbsInstance", hbsInstance);
+
+ // calling i18nextHelper
+ if (typeof handlebarsI18next === 'function') {
+ handlebarsI18next(hbsInstance.handlebars, i18next);
+ } else if (typeof handlebarsI18next.default === 'function') {
+ handlebarsI18next.default(hbsInstance.handlebars, i18next);
+ } else {
+ console.error('handlebars-i18next helper is not properly loaded');
+ }
+
+ i18next.on('languageChanged', function(lng) {
+ moment.locale(lng);
+ });
+
+ app.engine("handlebars", hbsInstance.engine);
+ app.set("view engine", "handlebars");
+ app.set("hbsInstance", hbsInstance);
+
+ // Static files //
+ app.use(express.static("public"));
+
+ // Body parser //
+ app.use(express.json({ type: alternateActivityPubContentType }));
+ app.use(express.json({ type: activityPubContentType }));
+ app.use(express.json({ type: "application/json" }));
+ app.use(express.urlencoded({ extended: true }));
+
+ // Router //
+ app.use("/", staticPages);
+ app.use("/", frontend);
+ app.use("/", activitypub);
+ app.use("/", event);
+ app.use("/", group);
+ app.use("/", magicLink);
+ app.use("/", routes);
+}
+
+initializeApp().catch(console.error);
export default app;
diff --git a/src/helpers.ts b/src/helpers.ts
index 47b380f..5590912 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -1,5 +1,8 @@
-import moment from "moment-timezone";
-import icalGenerator from "ical-generator";
+import mongoose from 'mongoose';
+import moment from 'moment-timezone';
+import icalGenerator from 'ical-generator';
+import i18next from 'i18next';
+import handlebars from 'handlebars';
import Log from "./models/Log.js";
import { getConfig } from "./lib/config.js";
import { IEvent } from "./models/Event.js";
@@ -10,41 +13,61 @@ const siteName = config.general.site_name;
// LOGGING
export function addToLog(process: string, status: string, message: string) {
- const logEntry = {
- status,
- process,
- message,
- timestamp: new Date(),
- };
- new Log(logEntry).save().catch(() => {
- console.log("Error saving log entry!");
- });
+ const logEntry = {
+ status,
+ process,
+ message,
+ timestamp: new Date(),
+ };
+ new Log(logEntry).save().catch(() => {
+ console.log("Error saving log entry!");
+ });
}
-export function exportICal(events: IEvent[], calendarName: string) {
- if (!events || events.length < 1) return;
+export function exportIcal(events: IEvent | IEvent[], calendarName?: string) { // Ical -> ICal
+ // Create a new icalGenerator... generator
+ const cal = icalGenerator({
+ name: calendarName || siteName,
+ timezone: 'UTC'
+ });
- // Create a new icalGenerator... generator
- const cal = icalGenerator({
- name: calendarName || siteName,
- });
- events.forEach((event) => {
- // Add the event to the generator
- cal.createEvent({
- start: moment.tz(event.start, event.timezone),
- end: moment.tz(event.end, event.timezone),
- timezone: event.timezone,
- summary: event.name,
- description: event.description,
- organizer: {
- name: event.hostName || "Anonymous",
- email: event.creatorEmail || "anonymous@anonymous.com",
- },
- location: event.location,
- url: "https://" + domain + "/" + event.id,
- });
+ const eventArray = Array.isArray(events) ? events : [events];
+ eventArray.forEach(event => {
+ cal.createEvent({
+ start: moment.tz(event.start, event.timezone),
+ end: moment.tz(event.end, event.timezone),
+ timezone: event.timezone,
+ summary: event.name,
+ description: event.description,
+ organizer: {
+ name: event.hostName || "Anonymous",
+ email: event.creatorEmail || 'anonymous@anonymous.com',
+ },
+ location: event.location,
+ url: 'https://' + domain + '/' + event.id
});
- // Stringify it!
- const string = cal.toString();
- return string;
+ });
+
+ return cal.toString();
+}
+
+interface I18nHelpers {
+ t: (key: string, options?: object) => string;
+ tn: (key: string, options?: object) => string;
+ count?: number;
+}
+
+export function getI18nHelpers(): I18nHelpers {
+ return {
+ t: function(key: string, options?: object) {
+ const translation = i18next.t(key, { ...this, ...options });
+ const template = handlebars.compile(translation);
+ return template(this);
+ },
+ tn: function(key: string, options?: object) {
+ const translation = i18next.t(key, { count: this.count, ...options });
+ const template = handlebars.compile(translation);
+ return template(this);
+ }
+ };
}
diff --git a/src/lib/config.ts b/src/lib/config.ts
index 3fd6eb7..35fc42c 100644
--- a/src/lib/config.ts
+++ b/src/lib/config.ts
@@ -3,6 +3,7 @@ import toml from "toml";
import { exitWithError } from "./process.js";
import { Response } from "express";
import { markdownToSanitizedHTML } from "../util/markdown.js";
+import i18next from "i18next";
interface StaticPage {
title: string;
@@ -115,44 +116,44 @@ export const instanceRules = (): InstanceRule[] => {
rules.push(
config.general.show_public_event_list
? {
- text: "Public events and groups are displayed on the homepage",
+ text: i18next.t("config.instancerule.showpubliceventlist-true"),
icon: "fas fa-eye",
}
: {
- text: "Events and groups can only be accessed by direct link",
+ text: i18next.t("config.instancerule.showpubliceventlist-false"),
icon: "fas fa-eye-slash",
},
);
rules.push(
config.general.creator_email_addresses?.length
? {
- text: "Only specific people can create events and groups",
+ text: i18next.t("config.instancerule.creatoremail-true"),
icon: "fas fa-user-check",
}
: {
- text: "Anyone can create events and groups",
+ text: i18next.t("config.instancerule.creatoremail-false"),
icon: "fas fa-users",
},
);
rules.push(
config.general.delete_after_days > 0
? {
- text: `Events are automatically deleted ${config.general.delete_after_days} days after they end`,
+ text: i18next.t("config.instancerule.deleteafterdays-true", { days: config.general.delete_after_days } ),
icon: "far fa-calendar-times",
}
: {
- text: "Events are permanent, and are never automatically deleted",
+ text: i18next.t("config.instancerule.deleteafterdays-false"),
icon: "far fa-calendar-check",
},
);
rules.push(
config.general.is_federated
? {
- text: "This instance federates with other instances using ActivityPub",
+ text: i18next.t("config.instancerule.isfederated-true"),
icon: "fas fa-globe",
}
: {
- text: "This instance does not federate with other instances",
+ text: i18next.t("config.instancerule.isfederated-false"),
icon: "fas fa-globe",
},
);
@@ -161,13 +162,15 @@ export const instanceRules = (): InstanceRule[] => {
export const instanceDescription = (): string => {
const config = getConfig();
- const defaultInstanceDescription =
- "**{{ siteName }}** is running on Gathio — a simple, federated, privacy-first event hosting platform.";
+ const defaultInstanceDescription = markdownToSanitizedHTML(
+ i18next.t("config.defaultinstancedesc", "Welcome to this Gathio instance!")
+ );
let instanceDescription = defaultInstanceDescription;
+ let instancedescfile = "./static/instance-description-" + i18next.language + ".md";
try {
- if (fs.existsSync("./static/instance-description.md")) {
+ if (fs.existsSync(instancedescfile)) {
const fileBody = fs.readFileSync(
- "./static/instance-description.md",
+ instancedescfile,
"utf-8",
);
instanceDescription = markdownToSanitizedHTML(fileBody);
diff --git a/src/lib/event.ts b/src/lib/event.ts
index 334ddf6..bcb7cd9 100644
--- a/src/lib/event.ts
+++ b/src/lib/event.ts
@@ -1,3 +1,4 @@
+import i18next from "i18next";
import { IEventGroup } from "../models/EventGroup.js";
export interface EventListEvent {
@@ -15,7 +16,8 @@ export const bucketEventsByMonth = (
acc: Record<string, any>[],
event: EventListEvent,
) => {
- const month = event.startMoment.format("MMMM YYYY");
+ event.startMoment.locale(i18next.language);
+ const month = event.startMoment.format(i18next.t("common.year-month-format" ));
const matchingBucket = acc.find((bucket) => bucket.title === month);
if (!matchingBucket) {
acc.push({
diff --git a/src/lib/middleware.ts b/src/lib/middleware.ts
index 69fbe4e..be05c3f 100644
--- a/src/lib/middleware.ts
+++ b/src/lib/middleware.ts
@@ -62,7 +62,7 @@ export const getConfigMiddleware = (
if (process.env.CYPRESS === "true" && req.cookies?.cypressConfigOverride) {
console.log("Overriding config with Cypress config");
const override = JSON.parse(req.cookies.cypressConfigOverride);
- res.locals.config = deepMerge<GathioConfig>(config, override);
+ res.locals.config = deepMerge(config, override);
return next();
}
res.locals.config = config;
diff --git a/src/routes.js b/src/routes.js
index d0fd9fc..eb7a6a1 100755
--- a/src/routes.js
+++ b/src/routes.js
@@ -22,6 +22,7 @@ import EventGroup from "./models/EventGroup.js";
import path from "path";
import { activityPubContentType } from "./lib/activitypub.js";
import { hashString } from "./util/generator.js";
+import i18next from "i18next";
import { EmailService } from "./lib/email.js";
const config = getConfig();
@@ -318,7 +319,7 @@ router.post("/deleteevent/:eventID/:editToken", (req, res) => {
);
req.emailService.sendEmailFromTemplate({
to: attendeeEmails,
- subject: `${event?.name} was deleted`,
+ subject: i18next.t("routes.deleteeventsubject", {eventName: event?.name}),
templateName: "deleteEvent",
templateData: {
eventName: event?.name,
@@ -631,7 +632,7 @@ router.post("/attendevent/:eventID", async (req, res) => {
if (req.body.attendeeEmail) {
req.emailService.sendEmailFromTemplate({
to: req.body.attendeeEmail,
- subject: `You're RSVPed to ${event.name}`,
+ subject: i18next.t("routes.addeventattendeesubject", {eventName: event?.name}),
templateName: "addEventAttendee",
templateData:{
eventID: req.params.eventID,
@@ -689,7 +690,7 @@ router.get("/oneclickunattendevent/:eventID/:attendeeID", (req, res) => {
if (req.body.attendeeEmail) {
req.emailService.sendEmailFromTemplate({
to: req.body.attendeeEmail,
- subject: `You have been removed from an event`,
+ subject: i18next.t("routes.removeeventattendeesubject"),
templateName: "removeEventAttendee",
templateData:{
eventName: event.name,
@@ -735,7 +736,7 @@ router.post("/removeattendee/:eventID/:attendeeID", (req, res) => {
if (req.body.attendeeEmail) {
req.emailService.sendEmailFromTemplate({
to: req.body.attendeeEmail,
- subject: `You have been removed from an event`,
+ subject: i18next.t("routes.removeeventattendeesubject"),
templateName: "removeEventAttendee",
templateData: {
eventName: event.name,
@@ -785,7 +786,7 @@ router.post("/subscribe/:eventGroupID", (req, res) => {
eventGroup.save();
req.emailService.sendEmailFromTemplate({
to: subscriber.email,
- subject: "You have subscribed to an event group",
+ subject: i18next.t("routes.subscribedsubject"),
templateName: "subscribed",
templateData:{
eventGroupName: eventGroup.name,
@@ -906,7 +907,7 @@ router.post("/post/comment/:eventID", (req, res) => {
req.emailService.sendEmailFromTemplate({
to: event?.creatorEmail || config.general.email,
bcc: attendeeEmails,
- subject: `New comment in ${event.name}`,
+ subject: i18next.t("routes.addeventcommentsubject", { eventName: event?.name }),
templateName: "addEventComment",
templateData:{
eventID: req.params.eventID,
@@ -1004,7 +1005,7 @@ router.post("/post/reply/:eventID/:commentID", (req, res) => {
req.emailService.sendEmailFromTemplate({
to: event?.creatorEmail || config.general.email,
bcc: attendeeEmails,
- subject: `New comment in ${event.name}`,
+ subject: i18next.t("routes.addeventcommentsubject", { eventName: event.name }),
templateName: "addEventComment",
templateData: {
eventID: req.params.eventID,
diff --git a/src/routes/event.ts b/src/routes/event.ts
index ee45d96..84a7c6b 100644
--- a/src/routes/event.ts
+++ b/src/routes/event.ts
@@ -27,7 +27,8 @@ import ical from "ical";
import { markdownToSanitizedHTML } from "../util/markdown.js";
import { checkMagicLink, getConfigMiddleware } from "../lib/middleware.js";
import { getConfig } from "../lib/config.js";
-
+import i18next from "i18next";
+moment.locale(i18next.language);
const config = getConfig();
const storage = multer.memoryStorage();
@@ -397,33 +398,33 @@ router.put(
: undefined,
};
let diffText =
- "<p>This event was just updated with new information.</p><ul>";
+ "<p>" + i18next.t("routes.event.difftext") + "</p><ul>";
let displayDate;
if (event.name !== updatedEvent.name) {
- diffText += `<li>the event name changed to ${updatedEvent.name}</li>`;
+ diffText += `<li>` + i18next.t("routes.event.namechanged", { eventname: updatedEvent.name} ) + `</li>`;
}
if (event.location !== updatedEvent.location) {
- diffText += `<li>the location changed to ${updatedEvent.location}</li>`;
+ diffText += `<li>` + i18next.t("routes.event.locationchanged", { location: updatedEvent.location} ) + `</li>`;
}
if (
event.start.toISOString() !== updatedEvent.start.toISOString()
) {
displayDate = moment
.tz(updatedEvent.start, updatedEvent.timezone)
- .format("dddd D MMMM YYYY h:mm a");
- diffText += `<li>the start time changed to ${displayDate}</li>`;
+ .format(i18next.t("common.datetimeformat"));
+ diffText += `<li>` + i18next.t("routes.event.starttimechanged", { starttime: displayDate }) + `</li>`;
}
if (event.end.toISOString() !== updatedEvent.end.toISOString()) {
displayDate = moment
.tz(updatedEvent.end, updatedEvent.timezone)
- .format("dddd D MMMM YYYY h:mm a");
- diffText += `<li>the end time changed to ${displayDate}</li>`;
+ .format(i18next.t("common.datetimeformat"));
+ diffText += `<li>` + i18next.t("routes.event.endtimechanged", { endtime: displayDate }) + `</li>`;
}
if (event.timezone !== updatedEvent.timezone) {
- diffText += `<li>the time zone changed to ${updatedEvent.timezone}</li>`;
+ diffText += `<li>` + i18next.t("routes.event.timezonechanged", { timezone: updatedEvent.timezone }) + `</li>`;
}
if (event.description !== updatedEvent.description) {
- diffText += `<li>the event description changed</li>`;
+ diffText += `<li>` + i18next.t("routes.event.descriptionchanged") + `</li>`;
}
diffText += `</ul>`;
const updatedEventObject = await Event.findOneAndUpdate(
@@ -487,7 +488,7 @@ router.put(
req.emailService.sendEmailFromTemplate({
to: config.general.email,
bcc: attendeeEmails,
- subject: `${event.name} was just edited`,
+ subject: i18next.t("routes.event.editedsubject", { eventname: event.name}),
templateName: "editEvent",
templateData: {
diffText,
@@ -672,7 +673,7 @@ router.delete(
if (attendeeEmail) {
await req.emailService.sendEmailFromTemplate({
to: attendeeEmail,
- subject: "You have been removed from an event",
+ subject: i18next.t("routes.removeeventattendeesubject"),
templateName: "unattendEvent",
templateData: {
eventID: req.params.eventID,
diff --git a/src/routes/frontend.ts b/src/routes/frontend.ts
index 1b95763..fca14c6 100644
--- a/src/routes/frontend.ts
+++ b/src/routes/frontend.ts
@@ -7,7 +7,7 @@ import {
instanceDescription,
instanceRules,
} from "../lib/config.js";
-import { addToLog, exportICal } from "../helpers.js";
+import { addToLog, exportIcal } from "../helpers.js";
import Event from "../models/Event.js";
import EventGroup, { IEventGroup } from "../models/EventGroup.js";
import {
@@ -18,6 +18,7 @@ import MagicLink from "../models/MagicLink.js";
import { getConfigMiddleware } from "../lib/middleware.js";
import { getMessage } from "../util/messages.js";
import { EventListEvent, bucketEventsByMonth } from "../lib/event.js";
+import i18next from "i18next";
const router = Router();
@@ -48,7 +49,7 @@ router.get("/new", (_: Request, res: Response) => {
return res.render("createEventMagicLink", frontendConfig(res));
}
return res.render("newevent", {
- title: "New event",
+ title: i18next.t("frontend.newevent"),
...frontendConfig(res),
});
});
@@ -69,12 +70,12 @@ router.get("/new/:magicLinkToken", async (req: Request, res: Response) => {
...frontendConfig(res),
message: {
type: "danger",
- text: "This magic link is invalid or has expired. Please request a new one here.",
+ text: i18next.t("routes.magiclink-invalid"),
},
});
}
res.render("newevent", {
- title: "New event",
+ title: i18next.t("frontend.newevent"),
...frontendConfig(res),
magicLinkToken: req.params.magicLinkToken,
creatorEmail: magicLink.email,
@@ -99,9 +100,9 @@ router.get("/events", async (_: Request, res: Response) => {
name: event.name,
location: event.location,
displayDate: isSameDay
- ? startMoment.format("D MMM YYYY")
- : `${startMoment.format("D MMM YYYY")} - ${endMoment.format(
- "D MMM YYYY",
+ ? startMoment.format("LL")
+ : `${startMoment.format("LL")} - ${endMoment.format(
+ "LL",
)}`,
eventHasConcluded: endMoment.isBefore(moment.tz(event.timezone)),
eventGroup: event.eventGroup as any as IEventGroup,
@@ -131,7 +132,7 @@ router.get("/events", async (_: Request, res: Response) => {
});
res.render("publicEventList", {
- title: "Public events",
+ title: i18next.t("frontend.publicevents"),
upcomingEvents: upcomingEventsInMonthBuckets,
pastEvents: pastEventsInMonthBuckets,
eventGroups: updatedEventGroups,
@@ -153,31 +154,53 @@ router.get("/:eventID", async (req: Request, res: Response) => {
}
const parsedLocation = event.location.replace(/\s+/g, "+");
let displayDate;
+ const dateformat = i18next.t("frontend.dateformat");
+ const timeformat = i18next.t('frontend.timeformat');
if (moment.tz(event.end, event.timezone).isSame(event.start, "day")) {
// Happening during one day
- displayDate =
- moment
- .tz(event.start, event.timezone)
- .format(
- 'dddd D MMMM YYYY [<span class="text-muted">from</span>] h:mm a',
- ) +
- moment
- .tz(event.end, event.timezone)
- .format(
- ' [<span class="text-muted">to</span>] h:mm a [<span class="text-muted">](z)[</span>]',
- );
+ displayDate = i18next.t("frontend.displaydate-sameday",
+ {
+ startdate:
+ moment
+ .tz(event.start, event.timezone)
+ .format(dateformat),
+ starttime:
+ moment
+ .tz(event.start, event.timezone)
+ .format(timeformat),
+ endtime:
+ moment
+ .tz(event.end, event.timezone)
+ .format(timeformat),
+ timezone:
+ moment
+ .tz(event.end, event.timezone)
+ .format('(z)',)
+ });
} else {
- displayDate =
- moment
- .tz(event.start, event.timezone)
- .format(
- 'dddd D MMMM YYYY [<span class="text-muted">at</span>] h:mm a',
- ) +
- moment
- .tz(event.end, event.timezone)
- .format(
- ' [<span class="text-muted">–</span>] dddd D MMMM YYYY [<span class="text-muted">at</span>] h:mm a [<span class="text-muted">](z)[</span>]',
- );
+ displayDate = i18next.t("frontend.displaydate-days",
+ {
+ startdate:
+ moment
+ .tz(event.start, event.timezone)
+ .format(dateformat),
+ starttime:
+ moment
+ .tz(event.start, event.timezone)
+ .format(timeformat),
+ enddate:
+ moment
+ .tz(event.end, event.timezone)
+ .format(dateformat),
+ endtime:
+ moment
+ .tz(event.end, event.timezone)
+ .format(timeformat),
+ timezone:
+ moment
+ .tz(event.end, event.timezone)
+ .format('(z)',)
+ });
}
let eventStartISO = moment.tz(event.start, "Etc/UTC").toISOString();
let eventEndISO = moment.tz(event.end, "Etc/UTC").toISOString();
@@ -256,7 +279,7 @@ router.get("/:eventID", async (req: Request, res: Response) => {
el.id = el._id;
}
if (el.number && el.number > 1) {
- el.name = `${el.name} (${el.number} people)`;
+ el.name = `${el.name} ${i18next.t("frontend.elnumber", { count: el.number })}`;
}
return {
...el,
@@ -428,8 +451,8 @@ router.get("/group/:eventGroupID", async (req: Request, res: Response) => {
.sort("start");
const updatedEvents: EventListEvent[] = events.map((event) => {
- const startMoment = moment.tz(event.start, event.timezone);
- const endMoment = moment.tz(event.end, event.timezone);
+ const startMoment = moment.tz(event.start, event.timezone).locale(i18next.language);
+ const endMoment = moment.tz(event.end, event.timezone).locale(i18next.language);
const isSameDay = startMoment.isSame(endMoment, "day");
return {
@@ -437,10 +460,8 @@ router.get("/group/:eventGroupID", async (req: Request, res: Response) => {
name: event.name,
location: event.location,
displayDate: isSameDay
- ? startMoment.format("D MMM YYYY")
- : `${startMoment.format("D MMM YYYY")} - ${endMoment.format(
- "D MMM YYYY",
- )}`,
+ ? startMoment.format("LL")
+ : `${startMoment.format("LL")} - ${endMoment.format("LL")}`,
eventHasConcluded: endMoment.isBefore(
moment.tz(event.timezone),
),
@@ -545,7 +566,7 @@ router.get(
const events = await Event.find({
eventGroup: eventGroup._id,
}).sort("start");
- const string = exportICal(events, eventGroup.name);
+ const string = exportIcal(events, eventGroup.name);
res.set("Content-Type", "text/calendar").send(string);
}
} catch (err) {
@@ -567,7 +588,7 @@ router.get("/export/event/:eventID", async (req: Request, res: Response) => {
}).populate("eventGroup");
if (event) {
- const string = exportICal([event], event.name);
+ const string = exportIcal([event], event.name);
res.set("Content-Type", "text/calendar").send(string);
}
} catch (err) {
@@ -593,7 +614,7 @@ router.get(
const events = await Event.find({
eventGroup: eventGroup._id,
}).sort("start");
- const string = exportICal(events, eventGroup.name);
+ const string = exportIcal(events, eventGroup.name);
res.set("Content-Type", "text/calendar").send(string);
}
} catch (err) {
diff --git a/src/routes/magicLink.ts b/src/routes/magicLink.ts
index e0a6310..1e0f87b 100644
--- a/src/routes/magicLink.ts
+++ b/src/routes/magicLink.ts
@@ -3,6 +3,7 @@ import { frontendConfig } from "../lib/config.js";
import { generateMagicLinkToken } from "../util/generator.js";
import MagicLink from "../models/MagicLink.js";
import { getConfigMiddleware } from "../lib/middleware.js";
+import i18next from "i18next";
const router = Router();
@@ -15,7 +16,7 @@ router.post("/magic-link/event/create", async (req: Request, res: Response) => {
...frontendConfig(res),
message: {
type: "danger",
- text: "Please provide an email address.",
+ text: i18next.t("routes.magiclink.provideemail"),
},
});
return;
@@ -30,7 +31,7 @@ router.post("/magic-link/event/create", async (req: Request, res: Response) => {
...frontendConfig(res),
message: {
type: "success",
- text: "Thanks! If this email address can create events, you should receive an email with a magic link.",
+ text: i18next.t("routes.magiclink.thanks"),
},
});
return;
@@ -49,7 +50,7 @@ router.post("/magic-link/event/create", async (req: Request, res: Response) => {
req.emailService.sendEmailFromTemplate({
to: email,
- subject: "Magic link to create an event",
+ subject: i18next.t("routes.magiclink.mailsubject"),
templateName: "createEventMagicLink",
templateData: {
token
@@ -59,7 +60,7 @@ router.post("/magic-link/event/create", async (req: Request, res: Response) => {
...frontendConfig(res),
message: {
type: "success",
- text: "Thanks! If this email address can create events, you should receive an email with a magic link.",
+ text: i18next.t("routes.magiclink.thanks"),
},
});
});
diff --git a/src/types/i18next-fs-backend.d.ts b/src/types/i18next-fs-backend.d.ts
new file mode 100644
index 0000000..33714e7
--- /dev/null
+++ b/src/types/i18next-fs-backend.d.ts
@@ -0,0 +1,5 @@
+declare module 'i18next-fs-backend' {
+ import { BackendModule } from 'i18next';
+ const backend: BackendModule;
+ export default backend;
+} \ No newline at end of file
diff --git a/src/util/validation.ts b/src/util/validation.ts
index a3bea63..42b524a 100644
--- a/src/util/validation.ts
+++ b/src/util/validation.ts
@@ -1,3 +1,4 @@
+import i18next from "i18next";
import moment from "moment-timezone";
type Error = {
@@ -90,26 +91,26 @@ const validateUrl = (url: string) => {
export const validateEventTime = (start: Date, end: Date): Error | boolean => {
if (moment(start).isAfter(moment(end))) {
return {
- message: "Start time must be before end time.",
+ message: i18next.t('util.validation.eventtime.startisafter'),
field: "eventStart",
};
}
if (moment(start).isBefore(moment())) {
return {
- message: "Start time must be in the future.",
+ message: i18next.t('util.validation.eventtime.startisbefore'),
field: "eventStart",
};
}
if (moment(end).isBefore(moment())) {
return {
- message: "End time must be in the future.",
+ message: i18next.t('util.validation.eventtime.endisbefore'),
field: "eventEnd",
};
}
// Duration cannot be longer than 1 year
if (moment(end).diff(moment(start), "years") > 1) {
return {
- message: "Event duration cannot be longer than 1 year.",
+ message: i18next.t("util.validation.eventtime.endyears"),
field: "eventEnd",
};
}
@@ -130,25 +131,25 @@ export const validateEventData = (
const errors: Error[] = [];
if (!validatedData.eventName) {
errors.push({
- message: "Event name is required.",
+ message: i18next.t('util.validation.eventdata.eventname'),
field: "eventName",
});
}
if (!validatedData.eventLocation) {
errors.push({
- message: "Event location is required.",
+ message: i18next.t("util.validation.eventdata.eventlocation"),
field: "eventLocation",
});
}
if (!validatedData.eventStart) {
errors.push({
- message: "Event start time is required.",
+ message: i18next.t("util.validation.eventdata.eventstart"),
field: "eventStart",
});
}
if (!validatedData.eventEnd) {
errors.push({
- message: "Event end time is required.",
+ message: i18next.t("util.validation.eventdata.eventend"),
field: "eventEnd",
});
}
@@ -163,26 +164,26 @@ export const validateEventData = (
}
if (!validatedData.timezone) {
errors.push({
- message: "Event timezone is required.",
+ message: i18next.t("util.validation.eventdata.timezone"),
field: "timezone",
});
}
if (!validatedData.eventDescription) {
errors.push({
- message: "Event description is required.",
+ message: i18next.t("util.validation.eventdata.eventdescription"),
field: "eventDescription",
});
}
if (validatedData.eventGroupBoolean) {
if (!validatedData.eventGroupID) {
errors.push({
- message: "Event group ID is required.",
+ message: i18next.t("util.validation.eventdata.eventgroupboolean"),
field: "eventGroupID",
});
}
if (!validatedData.eventGroupEditToken) {
errors.push({
- message: "Event group edit token is required.",
+ message: i18next.t("util.validation.eventdata.eventgroupedittoken"),
field: "eventGroupEditToken",
});
}
@@ -190,13 +191,13 @@ export const validateEventData = (
if (validatedData.maxAttendeesBoolean) {
if (!validatedData.maxAttendees) {
errors.push({
- message: "Max number of attendees is required.",
+ message: i18next.t("util.validation.eventdata.maxattendeesboolean"),
field: "maxAttendees",
});
}
if (isNaN(validatedData.maxAttendees)) {
errors.push({
- message: "Max number of attendees must be a number.",
+ message: i18next.t("util.validation.eventdata.maxattendees"),
field: "maxAttendees",
});
}
@@ -204,7 +205,7 @@ export const validateEventData = (
if (validatedData.creatorEmail) {
if (!validateEmail(validatedData.creatorEmail)) {
errors.push({
- message: "Email address is invalid.",
+ message: i18next.t("util.validation.eventdata.creatoremail"),
field: "creatorEmail",
});
}
@@ -212,7 +213,7 @@ export const validateEventData = (
if (validatedData.eventURL) {
if (!validateUrl(validatedData.eventURL)) {
errors.push({
- message: "Event link is invalid.",
+ message: i18next.t("util.validation.eventdata.eventurl"),
field: "eventURL",
});
}
@@ -230,20 +231,20 @@ export const validateGroupData = (
const errors: Error[] = [];
if (!groupData.eventGroupName) {
errors.push({
- message: "Event group name is required.",
+ message: i18next.t("util.validation.groupdata.eventgroupname"),
field: "eventGroupName",
});
}
if (!groupData.eventGroupDescription) {
errors.push({
- message: "Event group description is required.",
+ message: i18next.t("util.validation.groupdata.eventgroupdescription"),
field: "eventGroupDescription",
});
}
if (groupData.creatorEmail) {
if (!validateEmail(groupData.creatorEmail)) {
errors.push({
- message: "Email address is invalid.",
+ message: i18next.t("util.validation.groupdata.creatoremail"),
field: "creatorEmail",
});
}
@@ -251,7 +252,7 @@ export const validateGroupData = (
if (groupData.eventGroupURL) {
if (!validateUrl(groupData.eventGroupURL)) {
errors.push({
- message: "Group link is invalid.",
+ message: i18next.t("util.validation.groupdata.eventgroupurl"),
field: "eventGroupURL",
});
}
diff --git a/static/instance-description.md b/static/instance-description-en.md
index 747850d..747850d 100644
--- a/static/instance-description.md
+++ b/static/instance-description-en.md
diff --git a/static/instance-description-ja.md b/static/instance-description-ja.md
new file mode 100644
index 0000000..78cfe3e
--- /dev/null
+++ b/static/instance-description-ja.md
@@ -0,0 +1 @@
+**{{ siteName }}** は Gathio――簡単に、プライバシーファーストで、連合プロトコルにも対応したイベントの主催と参加、共有を支援するプラットフォームで稼働しています。 \ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index cd651a3..b9ad173 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -6,7 +6,7 @@
"checkJs": true,
"removeComments": true,
"resolveJsonModule": true,
- "typeRoots": ["./node_modules/@types"],
+ "typeRoots": ["./node_modules/@types", "./src/types"],
"sourceMap": true,
"outDir": "dist",
"strict": true,
diff --git a/utils.ts b/utils.ts
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/utils.ts
diff --git a/views/404.handlebars b/views/404.handlebars
index ec8fcb5..89d3f33 100755
--- a/views/404.handlebars
+++ b/views/404.handlebars
@@ -1,4 +1,4 @@
<main class="page">
- <h1>404 - Event not found</h1>
- <p>It may have never existed, or it's been removed from the server. Don't despair - why not create a new one? I for one would love to come to your ocarina recital.</p>
+ <h1>{{t "views.404notfound"}}</h1>
+ <p>{{t "views.404desc"}}</p>
</main>
diff --git a/views/createEventMagicLink.handlebars b/views/createEventMagicLink.handlebars
index d0a0a49..7c01ebc 100644
--- a/views/createEventMagicLink.handlebars
+++ b/views/createEventMagicLink.handlebars
@@ -1,5 +1,5 @@
<main class="page">
- <h2 class="mb-4">Request a link to create a new event</h2>
+ <h2 class="mb-4">{{t "views.createeventmagiclink.requestmltitle" }}</h2>
<form
action="/magic-link/event/create"
@@ -8,10 +8,10 @@
hx-target="article"
>
<p>
- The administrator of this instance has limited event creation to a set of specific email addresses. If your email address is allowed to create events, you will be sent a magic link. If not, you won't receive anything.
+ {{t "views.createeventmagiclink.requestmldesc" }}
</p>
<p>
- If you run into any issues, please contact the instance administrator.
+ {{t "views.createeventmagiclink.requestmlcontact" }}
</p>
{{#if message}}
<div class="alert alert-{{message.type}}" role="alert">
@@ -19,11 +19,11 @@
</div>
{{/if}}
<div class="form-group">
- <label for="email">Email address</label>
- <input type="email" class="form-control" id="email" placeholder="Email address" required name="email">
+ <label for="email">{{t "common.emailaddr" }}</label>
+ <input type="email" class="form-control" id="email" placeholder="{{t "common.emailaddr" }}" required name="email">
</div>
<div class="form-group text-center">
- <button type="submit" class="button button--primary w-50">Request magic link</button>
+ <button type="submit" class="button button--primary w-50">{{t "routes.magiclink.requestmlbutton" }}</button>
</div>
</form>
</main>
diff --git a/views/emails/addEventAttendee/addEventAttendeeHtml.handlebars b/views/emails/addEventAttendee/addEventAttendeeHtml.handlebars
index 48cdb48..fcd68b4 100644
--- a/views/emails/addEventAttendee/addEventAttendeeHtml.handlebars
+++ b/views/emails/addEventAttendee/addEventAttendeeHtml.handlebars
@@ -1,9 +1,9 @@
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">You just marked yourself as attending an event on {{siteName}}. Thank you! We'll send you another email if there are any updates to the event. Your email will be automatically removed from the database once the event finishes.</p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Follow this link to open the event page any time: <a href="https://{{domain}}/{{eventID}}">https://{{domain}}/{{eventID}}</a></p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Need to remove yourself from this event? <a href="https://{{domain}}/event/{{eventID}}/unattend/{{removalPasswordHash}}">Click this link</a>.</p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">You can also head to the event page and use this <strong>deletion password</strong>: {{removalPassword}}</p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Love,</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.addeventattendee.preface" }}</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.addeventattendee.eventlink" }}: <a href="https://{{domain}}/{{eventID}}">https://{{domain}}/{{eventID}}</a></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.addeventattendee.toremove" }}: <a href="https://{{domain}}/event/{{eventID}}/unattend/{{removalPasswordHash}}">{{t "views.emails.addeventattendee.clicktocancel" }}</a>.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{{t "views.emails.addeventattendee.removapasswordhtml" }}}: {{removalPassword}}</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.love" }}</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{siteName}}</p>
<hr/>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Hold up - I have no idea what this email is about!</strong></p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. If you don't want to attend this event, use the <strong>deletion password</strong> above to remove yourself from the event page.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>{{t "views.emails.holdup" }}</strong></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{{t "views.emails.addeventattendee.dontknowhtml" }}}</p>
diff --git a/views/emails/addEventAttendee/addEventAttendeeText.handlebars b/views/emails/addEventAttendee/addEventAttendeeText.handlebars
index 3930e28..c264989 100644
--- a/views/emails/addEventAttendee/addEventAttendeeText.handlebars
+++ b/views/emails/addEventAttendee/addEventAttendeeText.handlebars
@@ -1,13 +1,12 @@
-You just marked yourself as attending an event on {{siteName}}. Thank you! We'll send you another email if there are any updates to the event. Your email will be automatically removed from the database once the event finishes.
+{{t "views.emails.addeventattendee.preface" }}
-Follow this link to open the event page any time: https://{{domain}}/{{eventID}}
+{{t "views.emails.addeventattendee.eventlink" }}: https://{{domain}}/{{eventID}}
-Need to remove yourself from this event? Click this link: https://{{domain}}/event/{{eventID}}/unattend/{{removalPasswordHash}}
+{{t "views.emails.addeventattendee.removelink" }}: https://{{domain}}/event/{{eventID}}/unattend/{{removalPasswordHash}}
-You can also head to the event page and use this deletion password: {{removalPassword}}
-
-Love,
+{{t "views.emails.addeventattendee.removepassword" }}: {{removalPassword}}
+{{t "views.emails.love" }}
{{siteName}}
-If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. If you don't want to attend this event, use the deletion password above to remove yourself from the event page.
+{{t "views.emails.dontknow" }} \ No newline at end of file
diff --git a/views/emails/addEventComment/addEventCommentHtml.handlebars b/views/emails/addEventComment/addEventCommentHtml.handlebars
index 8ab7ec1..d15eb00 100644
--- a/views/emails/addEventComment/addEventCommentHtml.handlebars
+++ b/views/emails/addEventComment/addEventCommentHtml.handlebars
@@ -1,7 +1,7 @@
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>{{commentAuthor}}</strong> has just posted a comment on an event you're attending on {{siteName}}.</p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Click here to see the comment: <a href="https://{{domain}}/{{eventID}}">https://{{domain}}/{{eventID}}</a></p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Love,</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{{t "views.emails.addeventcomment.prefacehtml" }}}</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.addeventcomment.link" }}: <a href="https://{{domain}}/{{eventID}}">https://{{domain}}/{{eventID}}</a></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.love" }}</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{siteName}}</p>
<hr/>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Hold up - I have no idea what this email is about!</strong></p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. Don't worry - there isn't anything you need to do. Your email will be removed from the system when the event finishes.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>{{t "views.emails.holdup" }}</strong></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.dontknow" }}</p>
diff --git a/views/emails/addEventComment/addEventCommentText.handlebars b/views/emails/addEventComment/addEventCommentText.handlebars
index d7c045e..ec0b87c 100644
--- a/views/emails/addEventComment/addEventCommentText.handlebars
+++ b/views/emails/addEventComment/addEventCommentText.handlebars
@@ -1,9 +1,9 @@
-{{commentAuthor}} has just posted a comment on an event you're attending on {{siteName}}.
+{{t "views.emails.addeventcomment.preface" commentAuthor }}
-Click here to see the comment: https://{{domain}}/{{eventID}}
+{{t "views.emails.addeventcomment.link" }}: https://{{domain}}/{{eventID}}
-Love,
+{{t "views.emails.love" }}
{{siteName}}
-If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. Don't worry - there isn't anything you need to do. Your email will be removed from the system when the event finishes.
+{{t "views.emails.dontknow" }}
diff --git a/views/emails/createEvent/createEventHtml.handlebars b/views/emails/createEvent/createEventHtml.handlebars
index 030ee58..9310c2a 100644
--- a/views/emails/createEvent/createEventHtml.handlebars
+++ b/views/emails/createEvent/createEventHtml.handlebars
@@ -1,6 +1,6 @@
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Your event has been created!</p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Use this link to share it with people: <a href="https://{{domain}}/{{eventID}}">https://{{domain}}/{{eventID}}</a></p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Click this button to edit your event. <strong>DO NOT SHARE THIS</strong>, as anyone with this link can edit your event.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.createevent.preface" }}</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.createevent.sharelink" }}: <a href="https://{{domain}}/{{eventID}}">https://{{domain}}/{{eventID}}</a></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{{t "views.emails.createeventhtml.desc" }}}</p>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
<tbody>
@@ -9,7 +9,7 @@
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
<tbody>
<tr>
- <td style="font-family: sans-serif; font-size: 14px; vertical-align: top; background-color: #28a745; border-radius: 5px; text-align: center;"> <a href="https://{{domain}}/{{eventID}}?e={{editToken}}" target="_blank" style="display: inline-block; color: #ffffff; background-color: #28a745; border: solid 1px #28a745; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize; border-color: #28a745;">Edit Your Event</a> </td>
+ <td style="font-family: sans-serif; font-size: 14px; vertical-align: top; background-color: #28a745; border-radius: 5px; text-align: center;"> <a href="https://{{domain}}/{{eventID}}?e={{editToken}}" target="_blank" style="display: inline-block; color: #ffffff; background-color: #28a745; border: solid 1px #28a745; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize; border-color: #28a745;">{{t "views.emails.createeventhtml.editevent" }}</a> </td>
</tr>
</tbody>
</table>
diff --git a/views/emails/createEvent/createEventText.handlebars b/views/emails/createEvent/createEventText.handlebars
index e3c3a91..7c85d5c 100644
--- a/views/emails/createEvent/createEventText.handlebars
+++ b/views/emails/createEvent/createEventText.handlebars
@@ -1,7 +1,6 @@
-Your event has been created!
+{{t "views.emails.createevent.preface" }}
-Use this link to share it with people: https://{{domain}}/{{eventID}}
-
-Use the following link to edit your event. DO NOT SHARE THIS, as anyone with this link can edit your event.
+{{t "views.emails.createevent.sharelink" }}: https://{{domain}}/{{eventID}}
+{{t "views.emails.createevent.desc" }}
https://{{domain}}/{{eventID}}?e={{editToken}}
diff --git a/views/emails/createEventGroup/createEventGroupHtml.handlebars b/views/emails/createEventGroup/createEventGroupHtml.handlebars
index baa104d..1f73b75 100644
--- a/views/emails/createEventGroup/createEventGroupHtml.handlebars
+++ b/views/emails/createEventGroup/createEventGroupHtml.handlebars
@@ -1,8 +1,8 @@
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">You just created a new event group on {{siteName}}! Thanks a bunch - we're delighted to have you.</p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">You can edit your event group by clicking the button below, or just following this link: <a href="https://{{domain}}/group/{{eventGroupID}}?e={{editToken}}">https://{{domain}}/group/{{eventGroupID}}?e={{editToken}}</a></p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">To add events to this group (whether brand new events or ones you've already made), click the 'This event is part of an event group' checkbox. You will need to copy the following two codes into the box which opens:</p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Event group ID</strong>: {{eventGroupID}}</p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Event group secret editing code</strong>: {{editToken}}</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.createeventgroup.preface" }}</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.createeventgroup.link" }}: <a href="https://{{domain}}/group/{{eventGroupID}}?e={{editToken}}">https://{{domain}}/group/{{eventGroupID}}?e={{editToken}}</a></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.createeventgroup.eventlink" }}</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>{{t "common.eventgroupid" }}</strong>: {{eventGroupID}}</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>{{t "views.emails.createevent.editpswddesc" }}</strong>: {{editToken}}</p>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
<tbody>
<tr>
@@ -10,7 +10,7 @@
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
<tbody>
<tr>
- <td style="font-family: sans-serif; font-size: 14px; vertical-align: top; background-color: #28a745; border-radius: 5px; text-align: center;"> <a href="https://{{domain}}/group/{{eventGroupID}}?e={{editToken}}" target="_blank" style="display: inline-block; color: #ffffff; background-color: #28a745; border: solid 1px #28a745; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize; border-color: #28a745;">Edit event group</a> </td>
+ <td style="font-family: sans-serif; font-size: 14px; vertical-align: top; background-color: #28a745; border-radius: 5px; text-align: center;"> <a href="https://{{domain}}/group/{{eventGroupID}}?e={{editToken}}" target="_blank" style="display: inline-block; color: #ffffff; background-color: #28a745; border: solid 1px #28a745; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize; border-color: #28a745;">{{t "views.emails.createeventgroup.editgrouplinkhtml" }}</a> </td>
</tr>
</tbody>
</table>
@@ -18,10 +18,10 @@
</tr>
</tbody>
</table>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">To let others know about your event group, send them this link: <a href="https://{{domain}}/group/{{eventGroupID}}?e={{editToken}}">https://{{domain}}/group/{{eventGroupID}}</a></p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">And that's it - have a great day!</p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Love,</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.createeventgroup.sharelink" }}: <a href="https://{{domain}}/group/{{eventGroupID}}?e={{editToken}}">https://{{domain}}/group/{{eventGroupID}}</a></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.createeventgroup.done" }}</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.love" }}</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{siteName}}</p>
<hr/>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Hold up - I have no idea what this email is about!</strong></p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you didn't make an event group on {{siteName}}, someone may have accidentally typed your email instead of theirs when they were making the group. Just click on the edit link above and delete that event group, which removes your email from the system as well.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>{{t "views.emails.holdup" }}</strong></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.createeventgroup.dontknow" }}</p>
diff --git a/views/emails/createEventGroup/createEventGroupText.handlebars b/views/emails/createEventGroup/createEventGroupText.handlebars
index d5007d9..7599bda 100644
--- a/views/emails/createEventGroup/createEventGroupText.handlebars
+++ b/views/emails/createEventGroup/createEventGroupText.handlebars
@@ -1,21 +1,20 @@
-You just created a new event group on {{siteName}}! Thanks a bunch - we're delighted to have you.
+{{{t "views.emails.createeventgroup.preface" }}}
-You can edit your event group by clicking the button below, or just following this link: https://{{domain}}/group/{{eventGroupID}}?e={{editToken}}
+{{t "views.emails.createeventgroup.link" }}: https://{{domain}}/group/{{eventGroupID}}?e={{editToken}}
-To add events to this group (whether brand new events or ones you've already made), click the 'This event is part of an event group' checkbox. You will need to copy the following two codes into the box which opens:
+{{t "views.emails.createeventgroup.eventlink" }}
-Event group ID: {{eventGroupID}}
+{{t "common.eventgroupid" }}: {{eventGroupID}}
-Event group secret editing code: {{editToken}}
+{{t "views.emails.createevent.editpswddesc" }}: {{editToken}}
-Edit the event group here: https://{{domain}}/group/{{eventGroupID}}?e={{editToken}}
+{{t "views.emails.createeventgroup.editgrouplink" }}: https://{{domain}}/group/{{eventGroupID}}?e={{editToken}}
-To let others know about your event group, send them this link: https://{{domain}}/group/{{eventGroupID}}
+{{t "views.emails.createeventgroup.sharelink" }}: https://{{domain}}/group/{{eventGroupID}}
-And that's it - have a great day!
-
-Love,
+{{t "views.emails.createeventgroup.done" }}
+{{t "views.emails.love" }}
{{siteName}}
-If you didn't make an event group on {{siteName}}, someone may have accidentally typed your email instead of theirs when they were making the group. Just click on the edit link above and delete that event group, which removes your email from the system as well.
+{{t "views.emails.createeventgroup.dontknow" }} \ No newline at end of file
diff --git a/views/emails/createEventMagicLink/createEventMagicLinkHtml.handlebars b/views/emails/createEventMagicLink/createEventMagicLinkHtml.handlebars
index 1379607..5afd37e 100644
--- a/views/emails/createEventMagicLink/createEventMagicLinkHtml.handlebars
+++ b/views/emails/createEventMagicLink/createEventMagicLinkHtml.handlebars
@@ -1,8 +1,8 @@
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Here's a magic link which will allow you to create an event on {{siteName}}.</p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">This link will expire in 24 hours and can be used multiple times before then. Don't share it publicly, because it will allow anyone to create an event on your behalf!</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.magiclink.preface" }}</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.magiclink.link" }}</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><a href="https://{{domain}}/new/{{token}}">https://{{domain}}/new/{{token}}</a></p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Love,</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.love" }}</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{siteName}}</p>
<hr/>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Hold up - I have no idea what this email is about!</strong></p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you didn't try to create an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. Don't worry - there isn't anything you need to do. Your email address will be deleted after the magic link expires.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>{{t "views.emails.holdup" }}</strong></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.magiclink.dontknow" }}</p>
diff --git a/views/emails/createEventMagicLink/createEventMagicLinkText.handlebars b/views/emails/createEventMagicLink/createEventMagicLinkText.handlebars
index e3b4f96..7f01539 100644
--- a/views/emails/createEventMagicLink/createEventMagicLinkText.handlebars
+++ b/views/emails/createEventMagicLink/createEventMagicLinkText.handlebars
@@ -1,11 +1,10 @@
-Here's a magic link which will allow you to create an event on {{siteName}}.
+{{t "views.emails.magiclink.preface" }}
-This link will expire in 24 hours and can be used multiple times before then. Don't share it publicly, because it will allow anyone to create an event on your behalf!
+{{t "views.emails.magiclink.link" }}
https://{{domain}}/new/{{token}}
-Love,
-
+{{t "views.emails.love" }}
{{siteName}}
-If you didn't try to create an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. Don't worry - there isn't anything you need to do. Your email address will be deleted after the magic link expires.
+{{t "views.emails.magiclink.dontknow" }}
diff --git a/views/emails/deleteEvent/deleteEventHtml.handlebars b/views/emails/deleteEvent/deleteEventHtml.handlebars
index 5a3670c..1298e9f 100644
--- a/views/emails/deleteEvent/deleteEventHtml.handlebars
+++ b/views/emails/deleteEvent/deleteEventHtml.handlebars
@@ -1,4 +1,4 @@
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">The {{eventName}} event you're attending on {{siteName}} was just deleted by its creator.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.deleteevent.preface" }}</p>
<hr/>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Hold up - I have no idea what this email is about!</strong></p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. Don't worry - that event, and your email, is deleted from the system now.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>{{t "views.emails.holdup" }}</strong></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.deleteevent.done" }}</p>
diff --git a/views/emails/deleteEvent/deleteEventText.handlebars b/views/emails/deleteEvent/deleteEventText.handlebars
index 77c1cc3..02ba7f6 100644
--- a/views/emails/deleteEvent/deleteEventText.handlebars
+++ b/views/emails/deleteEvent/deleteEventText.handlebars
@@ -1,3 +1,3 @@
-The {{eventName}} event you're attending on {{siteName}} was just deleted by its creator.
+{{t "views.emails.deleteevent.preface" }}
-If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. Don't worry - that event, and your email, is deleted from the system now.
+{{t "views.emails.deleteevent.done" }}
diff --git a/views/emails/editEvent/editEventHtml.handlebars b/views/emails/editEvent/editEventHtml.handlebars
index ddb9885..269edf2 100644
--- a/views/emails/editEvent/editEventHtml.handlebars
+++ b/views/emails/editEvent/editEventHtml.handlebars
@@ -1,8 +1,8 @@
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">An event you're attending on {{siteName}} has just been edited.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.editevent.preface" }}</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{{diffText}}}</p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Click here to see the event: <a href="https://{{domain}}/{{eventID}}">https://{{domain}}/{{eventID}}</a></p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Love,</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.editevent.link" }}: <a href="https://{{domain}}/{{eventID}}">https://{{domain}}/{{eventID}}</a></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.love" }}</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{siteName}}</p>
<hr/>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Hold up - I have no idea what this email is about!</strong></p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. Don't worry - there isn't anything you need to do. Your email will be removed from the system when the event finishes.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>{{t "views.emails.holdup" }}</strong></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.editevent.dontknow" }}</p>
diff --git a/views/emails/editEvent/editEventText.handlebars b/views/emails/editEvent/editEventText.handlebars
index cdcffd3..760d16d 100644
--- a/views/emails/editEvent/editEventText.handlebars
+++ b/views/emails/editEvent/editEventText.handlebars
@@ -1,11 +1,9 @@
-An event you're attending on {{siteName}} has just been edited.
-
+{{t "views.emails.editevent.preface" }}
{{{diffText}}}
-Click here to see the event: https://{{domain}}/{{eventID}}
-
-Love,
+{{t "views.emails.editevent.link" }}: https://{{domain}}/{{eventID}}
+{{t "views.emails.love" }}
{{siteName}}
-If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. Don't worry - there isn't anything you need to do. Your email will be removed from the system when the event finishes.
+{{t "views.emails.editevent.dontknow" }}
diff --git a/views/emails/eventGroupUpdated/eventGroupUpdatedHtml.handlebars b/views/emails/eventGroupUpdated/eventGroupUpdatedHtml.handlebars
index 3231327..3cb255e 100644
--- a/views/emails/eventGroupUpdated/eventGroupUpdatedHtml.handlebars
+++ b/views/emails/eventGroupUpdated/eventGroupUpdatedHtml.handlebars
@@ -1,8 +1,8 @@
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">A new event has been added to the event group '{{eventGroupName}}' on {{siteName}}.</p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">The event is <a href="https://{{domain}}/{{eventID}}">'{{eventName}}'</a>.</p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Click here to see the event group: <a href="https://{{domain}}/group/{{eventGroupID}}">https://{{domain}}/group/{{eventGroupID}}</a></p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Love,</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.eventgroupupdate.preface" }}</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.eventgroupupdate.beforelink" }} <a href="https://{{domain}}/{{eventID}}">'{{eventName}}'</a>{{t "views.emails.eventgroupupdate.afterlink" }}.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.eventgroupupdate.grouplink" }}: <a href="https://{{domain}}/group/{{eventGroupID}}">https://{{domain}}/group/{{eventGroupID}}</a></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.love" }}</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{siteName}}</p>
<hr/>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Hold up - I don't want to receive these emails any more!</strong></p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you didn't subscribe yourself to this event group on {{siteName}}, someone may have accidentally typed your email instead of theirs. <a href="https://{{domain}}/unsubscribe/{{eventGroupID}}?email={{emailAddress}}">Click here to unsubscribe</a>.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>{{t "views.emails.eventgroupupdate.holduphtml" }}</strong></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.eventgroupupdate.dontknow" }} <a href="https://{{domain}}/unsubscribe/{{eventGroupID}}?email={{emailAddress}}">{{t "views.emails.eventgroupupdate.unsubscribe" }}</a></p>
diff --git a/views/emails/eventGroupUpdated/eventGroupUpdatedText.handlebars b/views/emails/eventGroupUpdated/eventGroupUpdatedText.handlebars
index 3ed5cb2..73e7c40 100644
--- a/views/emails/eventGroupUpdated/eventGroupUpdatedText.handlebars
+++ b/views/emails/eventGroupUpdated/eventGroupUpdatedText.handlebars
@@ -1,11 +1,10 @@
-A new event has been added to the event group '{{eventGroupName}}' on {{siteName}}.
+{{t "views.emails.eventgroupupdate.preface" }}
-The event is '{{eventName}}': https://{{domain}}/{{eventID}}.
+{{t "views.emails.eventgroupupdate.link" }}: https://{{domain}}/{{eventID}} {{t "views.emails.eventgroupupdate.afterlink" }}.
-Click here to see the event group: https://{{domain}}/group/{{eventGroupID}}
-
-Love,
+{{t "views.emails.eventgroupupdate.grouplink" }}: https://{{domain}}/group/{{eventGroupID}}
+{{t "views.emails.love" }}
{{siteName}}
-If you didn't subscribe yourself to this event group on {{siteName}}, someone may have accidentally typed your email instead of theirs. Click here to unsubscribe: https://{{domain}}/unsubscribe/{{eventGroupID}}?email={{emailAddress}}.
+{{t "views.emails.eventgroupupdate.dontknowhtml" }}: https://{{domain}}/unsubscribe/{{eventGroupID}}?email={{emailAddress}}
diff --git a/views/emails/removeEventAttendee/removeEventAttendeeHtml.handlebars b/views/emails/removeEventAttendee/removeEventAttendeeHtml.handlebars
index 66ca858..70765a1 100644
--- a/views/emails/removeEventAttendee/removeEventAttendeeHtml.handlebars
+++ b/views/emails/removeEventAttendee/removeEventAttendeeHtml.handlebars
@@ -1,4 +1,4 @@
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">You have been removed from the event {{eventName}} on {{siteName}} by the organizer of the event.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.removeeventattendee.preface" }}</p>
<hr/>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Hold up - I have no idea what this email is about!</strong></p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. Don't worry - you won't receive any more of these emails for this event, and your email has been removed from the database.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>{{t "views.emails.holdup" }}</strong></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.removeeventattendee.dontknow" }}</p>
diff --git a/views/emails/removeEventAttendee/removeEventAttendeeText.handlebars b/views/emails/removeEventAttendee/removeEventAttendeeText.handlebars
index 0a94121..70672d9 100644
--- a/views/emails/removeEventAttendee/removeEventAttendeeText.handlebars
+++ b/views/emails/removeEventAttendee/removeEventAttendeeText.handlebars
@@ -1,3 +1,3 @@
-You have been removed from the event {{eventName}} on {{siteName}} by the organizer of the event.
+{{t "views.emails.remoeeventattendee.preface"} }}
-If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. Don't worry - you won't receive any more of these emails for this event, and your email has been removed from the database.
+{{t "views.emails.removeeventattendee.dontknow" }} \ No newline at end of file
diff --git a/views/emails/subscribed/subscribedHtml.handlebars b/views/emails/subscribed/subscribedHtml.handlebars
index 3a3c4ad..343b304 100644
--- a/views/emails/subscribed/subscribedHtml.handlebars
+++ b/views/emails/subscribed/subscribedHtml.handlebars
@@ -1,9 +1,8 @@
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">You have been subscribed to the event group '{{eventGroupName}}' on {{siteName}}.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.subscribed.preface" }}</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin:
-0; Margin-bottom: 15px;">You will receive emails when new events are added to
-the group, and can unsubscribe at any time.</p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Love,</p>
+0; Margin-bottom: 15px;">{{t "views.emails.subscribed.desc" }}</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.love" }}</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{siteName}}</p>
<hr/>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Hold up - I don't want to receive these emails any more!</strong></p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you didn't subscribe yourself to this event group on {{siteName}}, someone may have accidentally typed your email instead of theirs. <a href="https://{{domain}}/unsubscribe/{{eventGroupID}}?email={{emailAddress}}">Click here to unsubscribe</a>.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>{{t "views.emails.eventgroupupdate.holduphtml" }}</strong></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.eventgroupupdate.dontknow" }} <a href="https://{{domain}}/unsubscribe/{{eventGroupID}}?email={{emailAddress}}">{{t "views.emails.eventgroupupdate.unsubscribe" }}</a></p>
diff --git a/views/emails/subscribed/subscribedText.handlebars b/views/emails/subscribed/subscribedText.handlebars
index 68418bc..d9970b2 100644
--- a/views/emails/subscribed/subscribedText.handlebars
+++ b/views/emails/subscribed/subscribedText.handlebars
@@ -1,9 +1,7 @@
-You have been subscribed to the event group '{{eventGroupName}}' on {{siteName}}.
-
-You will receive emails when new events are added to the group, and can unsubscribe at any time.
-
-Love,
+{{t "views.emails.subscribed.preface", {eventGroupName: {{eventGroupName}}} }}
+{{t "views.emails.subscribed.desc" }}
+{{t "views.emails.love" }}
{{siteName}}
-If you didn't subscribe yourself to this event group on {{siteName}}, someone may have accidentally typed your email instead of theirs. Click here to unsubscribe: https://{{domain}}/unsubscribe/{{eventGroupID}}?email={{emailAddress}}.
+{{t "views.emails.eventgroupupdate.dontknowhtml" }}: https://{{domain}}/unsubscribe/{{eventGroupID}}?email={{emailAddress}}
diff --git a/views/emails/unattendEvent/unattendEventHtml.handlebars b/views/emails/unattendEvent/unattendEventHtml.handlebars
index bc20d27..5f59ea8 100644
--- a/views/emails/unattendEvent/unattendEventHtml.handlebars
+++ b/views/emails/unattendEvent/unattendEventHtml.handlebars
@@ -1,8 +1,8 @@
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">You just removed yourself from an event on {{siteName}}. You will no longer receive update emails for this event.</p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you didn't mean to do this, an admin may have removed you from the event.</p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Follow this link to open the event page any time: <a href="https://{{domain}}/{{eventID}}">https://{{domain}}/{{eventID}}</a></p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Love,</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.unattendevent.preface" }}</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.unattendevent.desc" }}</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.addeventattendee.eventlink" }}: <a href="https://{{domain}}/{{eventID}}">https://{{domain}}/{{eventID}}</a></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.love" }}</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{siteName}}</p>
<hr/>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Hold up - I have no idea what this email is about!</strong></p>
-<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs, then removed it. Don't worry - you won't receive any more emails linked to this event.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>{{t "views.emails.holdup" }}</strong></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{t "views.emails.unattendevent.dontknow" }}</p>
diff --git a/views/emails/unattendEvent/unattendEventText.handlebars b/views/emails/unattendEvent/unattendEventText.handlebars
index 7e60dbf..f936d54 100644
--- a/views/emails/unattendEvent/unattendEventText.handlebars
+++ b/views/emails/unattendEvent/unattendEventText.handlebars
@@ -1,11 +1,10 @@
-You just removed yourself from an event on {{siteName}}. You will no longer receive update emails for this event.
+{{t "views.emails.unattendevent.preface" }}
-If you didn't mean to do this, an admin may have removed you from the event.
+{{t "views.emails.unattendevent.desc" }}
-Follow this link to open the event page any time: https://{{domain}}/{{eventID}}
-
-Love,
+{{t "views.emails.addeventattendee.eventlink" }}: https://{{domain}}/{{eventID}}
+{{t "views.emails.love" }}
{{siteName}}
-If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs, then removed it. Don't worry - you won't receive any more emails linked to this event.
+{{t "views.emails.unattendevent.dontknow" }}
diff --git a/views/event.handlebars b/views/event.handlebars
index 1b1022e..53ee39c 100755
--- a/views/event.handlebars
+++ b/views/event.handlebars
@@ -11,7 +11,7 @@
</div>
<div class="col-lg-3 ml-2 edit-buttons">
{{#if editingEnabled}}
- <button type="button" id="editEvent" class="button button--primary ml-auto d-block" {{#if eventHasConcluded}}disabled{{/if}} data-event-id="{{eventData.id}}" data-toggle="modal" data-target="#editModal"><i class="fas fa-edit"></i> Edit event</button>
+ <button type="button" id="editEvent" class="button button--primary ml-auto d-block" {{#if eventHasConcluded}}disabled{{/if}} data-event-id="{{eventData.id}}" data-toggle="modal" data-target="#editModal"><i class="fas fa-edit"></i> {{t "views.event.edit" }}</button>
{{/if}}
</div>
</div>
@@ -39,7 +39,7 @@
<time class="dt-end" datetime="{{eventEndISO}}"></time>
<br>
<span class="text-muted">
- {{#if eventHasBegun}}{{#unless eventHasConcluded}}Started {{else}}Ended {{/unless}}{{/if}}{{fromNow}}
+ {{#if eventHasBegun}}{{#unless eventHasConcluded}}{{t "views.event.started"}} {{else}}{{t "views.event.ended"}} {{/unless}}{{/if}}{{fromNow}}
</span>
</li>
{{#if eventHasHost}}
@@ -47,7 +47,7 @@
<span class="fa-li">
<i class="fas fa-fw fa-user-circle"></i>
</span>
- <span class="text-muted">Hosted by</span> {{eventData.hostName}}
+ <span class="text-muted">{{{t "views.event.hostedby" }}}
</li>
{{/if}}
{{#if eventData.eventGroup}}
@@ -55,7 +55,7 @@
<span class="fa-li">
<i class="fas fa-fw fa-calendar-alt"></i>
</span>
- <span class="text-muted">Part of</span> <a href="/group/{{eventData.eventGroup.id}}">{{eventData.eventGroup.name}}</a>
+ <span class="text-muted">{{{t "views.event.partof" }}}</span>
</li>
{{/if}}
{{#if eventData.url}}
@@ -74,7 +74,7 @@
</span>
<a class="u-url" href="https://{{domain}}/{{eventData.id}}">https://{{domain}}/{{eventData.id}}</a>
<button type="button" id="copyEventLink" class="eventInformationAction button button--outline-secondary button--sm" data-clipboard-text="https://{{domain}}/{{eventData.id}}">
- <i class="fas fa-copy"></i> Copy
+ <i class="fas fa-copy"></i> {{t "common.copy" }}
</button>
</li>
{{#if isFederated}}
@@ -84,7 +84,7 @@
</span>
@{{eventData.id}}@{{domain}}
<button type="button" id="copyAPLink" class="eventInformationAction button button--outline-secondary button--sm" data-clipboard-text="@{{eventData.id}}@{{domain}}">
- <i class="fas fa-copy"></i> Copy
+ <i class="fas fa-copy"></i> {{t "common.copy" }}
</button>
</li>
{{/if}}
@@ -94,42 +94,41 @@
<aside id="event__actions">
<div class="button-stack" role="group" aria-label="Event actions">
<a href="http://www.google.com/calendar/event?action=TEMPLATE&dates={{parsedStart}}%2F{{parsedEnd}}&text={{escapedName}}&location={{parsedLocation}}&ctz={{timezone}}" class="button button--outline-secondary button--sm">
- <i class="far fa-calendar-plus"></i> Add to Google Calendar
+ <i class="far fa-calendar-plus"></i> {{t "views.event.addtoGC" }}
</a>
<button type="button" id="exportICS" class="button button--outline-secondary button--sm" data-event-id="{{eventData.id}}">
- <i class="fas fa-download"></i> Export as ICS
+ <i class="fas fa-download"></i> {{t "views.event.ICSexport" }}
</button>
<a target="_blank" href="http://maps.google.com/?q={{parsedLocation}}" class="button button--outline-secondary button--sm">
- <i class="fas fa-map-marked"></i> Show on Google Maps
+ <i class="fas fa-map-marked"></i> {{t "views.event.showonGM" }}
</a>
<a target="_blank" href="https://www.openstreetmap.org/search?query={{parsedLocation}}" class="button button--outline-secondary button--sm">
- <i class="fas fa-map-marked"></i> Show on OpenStreetMap
+ <i class="fas fa-map-marked"></i> {{t "views.event.showonOM" }}
</a>
</div>
{{#unless editingEnabled}}
- <button type="button" id="editEvent" class="button button--outline-secondary button--sm" {{#if eventHasConcluded}}disabled{{/if}} data-event-id="{{eventData.id}}" data-toggle="modal" data-target="#editTokenModal"><i class="fas fa-edit"></i> Switch to editing mode</button>
+ <button type="button" id="editEvent" class="button button--outline-secondary button--sm" {{#if eventHasConcluded}}disabled{{/if}} data-event-id="{{eventData.id}}" data-toggle="modal" data-target="#editTokenModal"><i class="fas fa-edit"></i> {{t "views.eventgroup.editmode" }}</button>
{{/unless}}
</aside> <!-- #event__actions -->
</div>
{{#if eventHasConcluded}}
<div class="alert alert-warning mb-4" role="alert">
- This event has concluded. It can no longer be edited{{#if eventWillBeDeleted}}, and will be automatically deleted {{daysUntilDeletion}}{{/if}}.
-</div>
+{{t "views.event.concludeddel" }}</div>
{{/if}}
{{#if firstLoad}}
<div class="alert alert-success alert-dismissible fade show" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
- <p>Welcome to your event!</p>
- <p>Your secret editing password for this event is: <strong id="eventEditToken">{{eventData.editToken}}</strong>. It's been saved in your browser storage, and if you supplied your email, it's also been sent to you. If you didn't supply your email, you <strong>must save it somewhere safe</strong>, because it won't be shown again!</p>
- <p>To share your event, use the link you can see just above this message - that way your attendees won't be able to edit or delete your event!</p>
+ <p>{{t "views.event.welcome" }}</p>
+ <p>{{{t "views.event.attention" }}}</p>
+ <p>{{{t "views.event.share" }}}</p>
</div>
{{/if}}
<div class="card mb-4" id="eventDescription">
- <h5 class="card-header">About</h5>
+ <h5 class="card-header">{{t "views.event.about" }}</h5>
<div class="card-body p-summary">
{{{parsedDescription}}}
</div>
@@ -137,40 +136,40 @@
{{#if eventData.usersCanAttend}}
<div class="card mb-4" id="eventAttendees">
- <h5 class="card-header">Attendees {{#if numberOfAttendees}}({{numberOfAttendees}}){{/if}}
+ <h5 class="card-header">{{t "views.event.attendees" }} {{#if numberOfAttendees}}{{t "views.event.numberofattende" }}{{/if}}
<div class="button--group" role="group" aria-label="Attendance controls">
{{#unless noMoreSpots}}
- <button type="button" id="attendEvent" class="button button--primary" data-event-id="{{eventData.id}}"><i class="fas fa-user-plus"></i> Add me</button>
+ <button type="button" id="attendEvent" class="button button--primary" data-event-id="{{eventData.id}}"><i class="fas fa-user-plus"></i> {{t "views.event.addme" }}</button>
{{/unless}}
- <button type="button" id="unattendEvent" class="button button--secondary" data-toggle="modal" data-target="#unattendModal"><i class="fas fa-user-times"></i> Remove me</button>
+ <button type="button" id="unattendEvent" class="button button--secondary" data-toggle="modal" data-target="#unattendModal"><i class="fas fa-user-times"></i> {{t "views.event.removeme" }}</button>
</div>
</h5>
<div class="card-body text-center">
{{#if eventData.maxAttendees}}
{{#if noMoreSpots}}
- <div class="alert alert-warning text-center" id="attendees-alert">This event is at capacity.</div>
+ <div class="alert alert-warning text-center" id="attendees-alert">{{t "views.event.capacity" }}</div>
{{else}}
- <div class="alert alert-warning text-center" id="attendees-alert">{{spotsRemaining}} {{plural spotsRemaining "spot(s)"}} remaining - add yourself now!</div>
+ <div class="alert alert-warning text-center" id="attendees-alert">{{plural "views.event.remaining" spotsRemaining }}</div>
{{/if}}
{{/if}}
{{#if numberOfAttendees}}
<ul class="attendeesList">
{{#each visibleAttendees}}
- <li{{#if ../editingEnabled}} data-attendee-name="{{this.name}}" data-attendee-id="{{this._id}}"{{/if}}><span class="attendee-name">{{this.name}}</span>{{#if ../editingEnabled}} <a href="#" class="remove-attendee" data-toggle="modal" data-target="#removeAttendeeModal" title="Remove user from event"><i class="fas fa-user-times"></i></a>{{/if}}</li>
+ <li{{#if ../editingEnabled}} data-attendee-name="{{this.name}}" data-attendee-id="{{this._id}}"{{/if}}><span class="attendee-name">{{this.name}}</span>{{#if ../editingEnabled}} <a href="#" class="remove-attendee" data-toggle="modal" data-target="#removeAttendeeModal" title="{{t "views.event.removeuser" }}"><i class="fas fa-user-times"></i></a>{{/if}}</li>
{{/each}}
{{#if editingEnabled}}
{{#each hiddenAttendees}}
- <li{{#if ../editingEnabled}} data-attendee-name="{{this.name}}" data-attendee-id="{{this._id}}"{{/if}} class="hidden-attendee"><span class="attendee-name">{{this.name}} (hidden from public list)</span>{{#if ../editingEnabled}} <a href="#" class="remove-attendee" data-toggle="modal" data-target="#removeAttendeeModal" title="Remove user from event"><i class="fas fa-user-times"></i></a>{{/if}}</li>
+ <li{{#if ../editingEnabled}} data-attendee-name="{{this.name}}" data-attendee-id="{{this._id}}"{{/if}} class="hidden-attendee"><span class="attendee-name">{{this.name}} {{t "views.event.hidden" }}</span>{{#if ../editingEnabled}} <a href="#" class="remove-attendee" data-toggle="modal" data-target="#removeAttendeeModal" title="{{t "views.event.removeuser" }}"><i class="fas fa-user-times"></i></a>{{/if}}</li>
{{/each}}
{{/if}}
</ul>
{{#unless editingEnabled}}
{{#if numberOfHiddenAttendees}}
- <div class="hidden-attendees-message">{{numberOfHiddenAttendees}} hidden attendee{{plural numberOfHiddenAttendees ""}}</div>
+ <div class="hidden-attendees-message">{{plural "views.event.hiddenattendee" numberOfHiddenAttendees }}</div>
{{/if}}
{{/unless}}
{{else}}
- <p class="text-center text-muted mb-0">No attendees yet!</p>
+ <p class="text-center text-muted mb-0">{{t "views.event.noattendees" }}</p>
{{/if}}
</div>
</div>
@@ -179,7 +178,7 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
- <h5 class="modal-title" id="attendModalLabel">Add yourself to '{{eventData.name}}'</h5>
+ <h5 class="modal-title" id="attendModalLabel">{{t "views.event.addself" }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
@@ -187,41 +186,41 @@
<form id="attendEventForm" action="/attendevent/{{eventData.id}}" method="post">
<div class="modal-body">
<div class="form-group">
- <label for="attendeeName">Your name</label>
+ <label for="attendeeName">{{t "views.event.attendeename" }}</label>
<div class="form-group">
- <input type="text" class="form-control" id="attendeeName" name="attendeeName" placeholder="Or an alias, perhaps..." data-validation="required length" data-validation-length="1-30">
+ <input type="text" class="form-control" id="attendeeName" name="attendeeName" placeholder="{{t "views.event.attendeenamedesc" }}" data-validation="required length" data-validation-length="1-30">
</div>
</div>
<div class="form-group">
- <label for="attendeeName">How many people in your party?</label>
+ <label for="attendeeName">{{t "views.event.attendeenum" }}</label>
<div class="form-group">
<input type="number" class="form-control" id="attendeeNumber" name="attendeeNumber" value="1" data-validation="required number" >
</div>
</div>
<div class="form-group">
- <label for="attendeeEmail">Your email (optional)</label>
- <p class="form-text small">If you provide your email, you will receive updates to the event.</p>
+ <label for="attendeeEmail">{{t "views.event.attendeeemail" }}</label>
+ <p class="form-text small">{{t "views.event.joinemaildesc" }}</p>
<div class="form-group">
- <input type="email" class="form-control" id="attendeeEmail" name="attendeeEmail" placeholder="We won't spam you <3" data-validation="email" data-validation-optional="true">
+ <input type="email" class="form-control" id="attendeeEmail" name="attendeeEmail" placeholder="{{t "views.event.nospam" }}" data-validation="email" data-validation-optional="true">
</div>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="attendeeVisible" name="attendeeVisible" checked>
<label class="form-check-label" for="attendeeVisible">
- Show my name in the public list of attendees
+ {{t "views.event.attendeevisible" }}
</label>
- <p class="form-text small">If you choose to hide your name, only the event organiser will be able to see it.</p>
+ <p class="form-text small">{{t "views.event.attendeevisibledesc" }}</p>
</div>
<div class="form-group">
- <label for="removalPassword">Deletion password</label>
- <p class="form-text small">You can use this password to remove yourself from the list of event attendees. If you provided your email, you'll receive it by email. Otherwise, write it down now because it will <strong>not be shown again</strong>.</p>
+ <label for="removalPassword">{{t "views.event.removepswd" }}</label>
+ <p class="form-text small">{{{t "views.event.removepswddesc" }}}</p>
<input type="text" class="form-control" readonly id="removalPassword"
name="removalPassword">
</div>
</div>
<div class="modal-footer">
- <button type="button" class="button button--secondary" data-dismiss="modal">Close</button>
- <button type="submit" class="button button--primary">Add myself</button>
+ <button type="button" class="button button--secondary" data-dismiss="modal">{{t "common.close" }}</button>
+ <button type="submit" class="button button--primary">{{t "views.event.addmyself" }}</button>
</div>
</form>
</div>
@@ -232,7 +231,7 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
- <h5 class="modal-title" id="unattendModalLabel">Remove yourself from '{{eventData.name}}'</h5>
+ <h5 class="modal-title" id="unattendModalLabel">{{{t "views.event.removemyselfdesc" }}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
@@ -240,15 +239,15 @@
<form id="unattendEventForm" x-on:submit.prevent="fetch(`/event/attendee/{{eventData.id}}?${new URLSearchParams({ p: password }).toString()}`, { method: 'DELETE' }).then(response => response.ok ? window.location.reload() : response.json()).then(data => message = data)">
<div class="modal-body">
<div class="form-group">
- <label for="removalPassword" class="form-label">Your deletion password</label>
- <p class="form-text small">Lost your password? Get in touch with the event organiser.</p>
+ <label for="removalPassword" class="form-label">{{t "views.event.removepswd" }}</label>
+ <p class="form-text small">{{t "views.event.lostpswd" }}</p>
<div x-bind:class="{ 'alert-danger': message?.error, 'alert-success': message?.success }" class="alert" x-text="message?.error || message?.success" x-show="message?.error || message?.success"></div>
<input type="password" class="form-control" id="removalPassword" name="removalPassword" x-model="password" required>
</div>
</div>
<div class="modal-footer">
- <button type="button" class="button button--secondary" data-dismiss="modal">Close</button>
- <button type="submit" class="button button--primary">Remove myself</button>
+ <button type="button" class="button button--secondary" data-dismiss="modal">{{t "common.close" }}</button>
+ <button type="submit" class="button button--primary">{{t "views.event.removemyself" }}</button>
</div>
</form>
</div>
@@ -260,18 +259,18 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
- <h5 class="modal-title" id="removeAttendeeModalLabel">Remove attendee from '{{eventData.name}}'</h5>
+ <h5 class="modal-title" id="removeAttendeeModalLabel">{{{t "views.event.removeattendeedesc" }}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form id="removeAttendeeForm" action="/removeattendee/{{eventData.id}}/" method="post">
<div class="modal-body">
- <p>Are you sure you want to remove this attendee from the event? This action cannot be undone.</p>
+ <p>{{t "views.event.confremoveattendee" }}</p>
</div>
<div class="modal-footer">
- <button type="button" class="button button--secondary" data-dismiss="modal">Close</button>
- <button type="submit" class="button button--danger">Remove attendee</button>
+ <button type="button" class="button button--secondary" data-dismiss="modal">{{t "common.close" }}</button>
+ <button type="submit" class="button button--danger">{{t "views.event.removeAttendee" }}</button>
</div>
</form>
</div>
@@ -283,19 +282,19 @@
{{#if eventData.usersCanComment}}
<div class="card mb-4" id="eventComments">
- <h5 class="card-header">Discussion</h5>
+ <h5 class="card-header">{{t "views.event.discussion" }}</h5>
<div class="card-body">
<form id="commentForm" action="/post/comment/{{eventData.id}}/" method="post">
- <label for="commentAuthor">Name</label>
+ <label for="commentAuthor">{{t "views.event.commentauthor" }}</label>
<div class="form-group">
- <input type="text" class="form-control" id="commentAuthor" name="commentAuthor" placeholder="Your name" required>
+ <input type="text" class="form-control" id="commentAuthor" name="commentAuthor" placeholder="{{t "views.event.commentauthorph" }}" required>
</div>
- <label for="commentContent">Comment</label>
+ <label for="commentContent">{{t "views.event.comment" }}</label>
<div class="form-group">
<div class="d-flex flex-gap">
- <textarea class="form-control" id="commentContent" name="commentContent" style="resize: none;" placeholder="What would you like to say?" required></textarea>
+ <textarea class="form-control" id="commentContent" name="commentContent" style="resize: none;" placeholder="{{t "views.event.commentcontent" }}" required></textarea>
<div class="input-group-append">
- <button type="submit" class="button button--primary" id="postComment">Send <i class="fas fa-chevron-right"></i></button>
+ <button type="submit" class="button button--primary" id="postComment">{{t "views.event.postbutton" }} <i class="fas fa-chevron-right"></i></button>
</div>
</div>
</div>
@@ -325,12 +324,12 @@
</div>
<div class="col-lg-3 commentMetadata text-right">
<button type="button" class="button button--outline button--sm openReplyBox">
- <i class="fas fa-comment"></i> Reply
+ <i class="fas fa-comment"></i> {{t "views.event.reply" }}
</button>
{{#if ../editingEnabled}}
<form class="d-inline" action="/deletecomment/{{../eventData.id}}/{{this._id}}/{{../eventData.editToken}}" method="post">
<button type="submit" class="button button--outline button--sm deleteComment">
- <i class="fas fa-trash"></i> Delete
+ <i class="fas fa-trash"></i> {{t "views.del" }}
</button>
</form>
{{/if}}
@@ -340,14 +339,14 @@
<div class="col-md">
<form id="replyForm" action="/post/reply/{{../eventData.id}}/{{this._id}}" method="post">
<div class="form-group">
- <input type="text" class="form-control form-control-sm" id="replyAuthor" name="replyAuthor" placeholder="Your name" required>
+ <input type="text" class="form-control form-control-sm" id="replyAuthor" name="replyAuthor" placeholder="Y{{t "views.event.attendeename" }}" required>
</div>
<div class="form-group">
<div class="d-flex flex-gap">
- <textarea class="form-control form-control-sm" id="replyContent" name="replyContent" style="resize: none;" placeholder="What would you like to reply?" required></textarea>
+ <textarea class="form-control form-control-sm" id="replyContent" name="replyContent" style="resize: none;" placeholder="{{t "views.event.replycontent" }}" required></textarea>
<div class="input-group-append">
- <button type="submit" class="button button--primary button--sm" id="postReply">Reply <i class="fas fa-chevron-right"></i></button>
</div>
+ <button type="submit" class="button button--primary button--sm" id="postReply">{{t "views.event.reply" }} <i class="fas fa-chevron-right"></i></button>
</div>
</div>
</form>
@@ -365,7 +364,7 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
- <h5 class="modal-title" id="editTokenModalLabel">Enter editing password</h5>
+ <h5 class="modal-title" id="editTokenModalLabel">{{t "views.edittoken" }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
@@ -373,9 +372,9 @@
<form id="verifyTokenForm" action="/verifytoken/event/{{eventData.id}}" method="post">
<div class="modal-body">
<div class="form-group">
- <p class="form-text small">Enter the editing password you received by email or were shown when the event was created.</p>
+ <p class="form-text small">{{t "views.edittokendesc" }}</p>
<div class="form-group">
- <input type="text" class="form-control" id="editToken" name="editToken" placeholder="Get it right!" data-validation="required">
+ <input type="text" class="form-control" id="editToken" name="editToken" placeholder="{{t "views.right" }}" data-validation="required">
</div>
<div class="form-group">
<div class="alert alert-danger" style="display:none;"></div>
@@ -383,8 +382,8 @@
</div>
</div>
<div class="modal-footer">
- <button type="button" class="button button--secondary" data-dismiss="modal">Close</button>
- <button type="submit" class="button button--primary">Edit event</button>
+ <button type="button" class="button button--secondary" data-dismiss="modal">{{t "common.close" }}</button>
+ <button type="submit" class="button button--primary">{{t "views.event.edit" }}</button>
</div>
</form>
</div>
@@ -400,18 +399,18 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
- <h5 class="modal-title" id="deleteModalLabel">Delete '{{eventData.name}}'</h5>
+ <h5 class="modal-title" id="deleteModalLabel">{{t "views.event.deletetitle" }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form id="deleteEventForm" action="/deleteevent/{{eventData.id}}/{{eventData.editToken}}" method="post">
<div class="modal-body">
- <p>Are you sure you want to delete this event? This action cannot be undone.</p>
+ <p>{{t "views.event.delconfirm" }}</p>
</div>
<div class="modal-footer">
- <button type="button" class="button button--secondary" data-dismiss="modal">Close</button>
- <button type="submit" class="button button--danger">Delete event</button>
+ <button type="button" class="button button--secondary" data-dismiss="modal">{{t "common.close" }}</button>
+ <button type="submit" class="button button--danger">{{t "views.event.del" }}</button>
</div>
</form>
</div>
@@ -531,13 +530,13 @@ window.eventData = {{{ json jsonData }}};
})
})
$("#copyEventLink").click(function(){
- $(this).html('<i class="fas fa-copy"></i> Copied!');
- setTimeout(function(){ $("#copyEventLink").html('<i class="fas fa-copy"></i> Copy');}, 5000);
+ $(this).html('<i class="fas fa-copy"></i> {{t "common.copied" }}');
+ setTimeout(function(){ $("#copyEventLink").html('<i class="fas fa-copy"></i> {{t "common.copy" }}');}, 5000);
})
new ClipboardJS('#copyAPLink');
$("#copyAPLink").click(function(){
- $(this).html('<i class="fas fa-copy"></i> Copied!');
- setTimeout(function(){ $("#copyAPLink").html('<i class="fas fa-copy"></i> Copy');}, 5000);
+ $(this).html('<i class="fas fa-copy"></i> {{t "common.copied" }}');
+ setTimeout(function(){ $("#copyAPLink").html('<i class="fas fa-copy"></i> {{t "common.copy" }}');}, 5000);
})
if ($("#joinCheckbox").is(':checked')){
$("#maxAttendeesCheckboxContainer").css("display","flex");
@@ -553,7 +552,7 @@ window.eventData = {{{ json jsonData }}};
if (response.data.freeSpots !== undefined) {
modal.find('#attendeeNumber')
.attr('data-validation-allowing', `range[1;${response.data.freeSpots}]`)
- .attr('data-validation-error-msg', `Please enter a number between 1 and ${response.data.freeSpots}`);
+ .attr('data-validation-error-msg', `{{t "views.event.numlimit" }}`);
}
modal.modal();
})
@@ -578,7 +577,7 @@ window.eventData = {{{ json jsonData }}};
}
},
error: function(response, status, xhr) {
- form.find('.alert').text('That editing password is incorrect. Try again.').show();
+ form.find('.alert').text('{{t "views.incorrectpswd" }}').show();
}
});
});
diff --git a/views/eventgroup.handlebars b/views/eventgroup.handlebars
index 3151aea..6dcf555 100755
--- a/views/eventgroup.handlebars
+++ b/views/eventgroup.handlebars
@@ -10,7 +10,7 @@
</div>
<div class="col-lg-2 ml-2 edit-buttons">
{{#if editingEnabled}}
- <button type="button" id="editGroup" class="button button--primary text-nowrap ml-auto d-block" data-event-id="{{eventGroupData.id}}" data-toggle="modal" data-target="#editModal"><i class="fas fa-edit"></i> Edit group</button>
+ <button type="button" id="editGroup" class="button button--primary text-nowrap ml-auto d-block" data-event-id="{{eventGroupData.id}}" data-toggle="modal" data-target="#editModal"><i class="fas fa-edit"></i> {{t "views.eventgroup.edit" }}</button>
{{/if}}
</div>
</div>
@@ -20,7 +20,7 @@
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
- Welcome to your event group! We've just sent you an email with your secret editing link, which you can also see in the address bar above. Haven't got the email? Check your spam or junk folder. To share your event group, use the link you can see just below this message - that way your attendees won't be able to edit or delete your event group!
+ {{{t "views.eventgroup.welcome" }}}
</div>
{{/if}}
<div id="event__basics">
@@ -32,7 +32,7 @@
<span class="fa-li">
<i class="fas fa-fw fa-user-circle"></i>
</span>
- <span class="text-muted">Hosted by</span> {{eventGroupData.hostName}}
+ <span class="text-muted">{{{t "views.eventgroup.hostedby" }}}
</li>
{{/if}}
{{#if eventGroupData.url}}
@@ -51,7 +51,7 @@
</span>
<a href="https://{{domain}}/group/{{eventGroupData.id}}">https://{{domain}}/group/{{eventGroupData.id}}</a>
<button type="button" id="copyEventLink" class="eventInformationAction button button--outline-secondary button--sm" data-clipboard-text="https://{{domain}}/group/{{eventGroupData.id}}">
- <i class="fas fa-copy"></i> Copy
+ <i class="fas fa-copy"></i> {{t "common.copy" }}
</button>
</li>
<li>
@@ -63,10 +63,9 @@
<button type="button" id="copyFeedLink"
class="eventInformationAction button button--outline-secondary button--sm"
data-clipboard-text="https://{{domain}}/group/{{eventGroupData.id}}/feed.ics">
- <i class="fas fa-copy"></i> Copy
+ <i class="fas fa-copy"></i> {{t "common.copy" }}
</button>
- <p class="text-muted small">Paste this URL into your calendar app
- to subscribe to a live feed of events from this group.</p>
+ <p class="text-muted small">{{t "views.eventgroup.feedlinkdesc" }}</p>
</li>
</ul>
</div> <!-- /card -->
@@ -76,31 +75,31 @@
<button type="button" class="button button--outline-secondary button--sm"
data-event-id="{{eventGroupData.id}}" data-toggle="modal"
data-target="#subscribeModal">
- <i class="fas fa-envelope"></i> Subscribe to updates
+ <i class="fas fa-envelope"></i> {{t "views.eventgroup.subscribetitle" }}
</button>
<button type="button" id="exportICS" class="button button--outline-secondary
button--sm" data-event-id="{{eventGroupData.id}}">
- <i class="fas fa-download"></i> Export as ICS
+ <i class="fas fa-download"></i> {{t "views.eventgroup.ICSexport" }}
</button>
</div>
{{#unless editingEnabled}}
- <button type="button" id="editGroup" class="button button--outline-secondary button--sm" data-event-id="{{eventGroupData.id}}" data-toggle="modal" data-target="#editTokenModal"><i class="fas fa-edit"></i> Switch to editing mode</button>
+ <button type="button" id="editGroup" class="button button--outline-secondary button--sm" data-event-id="{{eventGroupData.id}}" data-toggle="modal" data-target="#editTokenModal"><i class="fas fa-edit"></i> {{t "views.eventgroup.editmode" }}</button>
{{/unless}}
</aside>
</div>
{{#if editingEnabled}}
<div class="alert alert-info mb-4">
- <p>To add an event to this group, copy and paste the two codes below into the 'Event Group' box when creating a new event or editing an existing event.</p>
+ <p>{{t "views.eventgroup.addevent" }}</p>
<div class="table-responsive">
<table style="width:100%">
<tr style="border-bottom:1px solid rgba(0,0,0,0.2)">
- <td><strong>Event group ID</strong></td>
+ <td><strong>{{t "common.eventgroupid" }}</strong></td>
<td><span class="code" id="eventGroupID">{{eventGroupData.id}}</span></td>
</tr>
<tr>
- <td><strong>Event group editing password</strong></td>
+ <td><strong>{{t "views.eventgroup.editpswd" }}</strong></td>
<td><span class="code" id="eventGroupEditToken">{{eventGroupData.editToken}}</span></td>
</tr>
</table>
@@ -110,19 +109,19 @@
{{/if}}
<div class="card mb-4" id="eventDescription">
- <h5 class="card-header">About</h5>
+ <h5 class="card-header">{{t "views.eventgroup.about" }}</h5>
<div class="card-body">
{{{parsedDescription}}}
</div>
</div>
<div class="card mt-4 mb-4" id="upcomingEvents">
- <h5 class="card-header">Upcoming events</h5>
+ <h5 class="card-header">{{t "views.eventgroup.upcomingevents" }}</h5>
{{> eventList upcomingEvents}}
</div>
<div class="card mt-4 mb-4" id="pastEvents">
- <h5 class="card-header">Past events</h5>
+ <h5 class="card-header">{{t "views.eventgroup.pastevents" }}</h5>
{{> eventList pastEvents}}
</div>
</div>
@@ -134,19 +133,19 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
- <h5 class="modal-title" id="deleteModalLabel">Delete '{{eventGroupData.name}}'</h5>
+ <h5 class="modal-title" id="deleteModalLabel">{{t "views.eventgroup.deletetitle" }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form id="deleteEventGroupForm" action="/deleteeventgroup/{{eventGroupData.id}}/{{eventGroupData.editToken}}" method="post">
<div class="modal-body">
- <p>Are you sure you want to delete this event group? This action cannot be undone.</p>
- <p>This will <strong>not</strong> delete the individual events contained in this group. They can be linked to another group later.</p>
+ <p>{{t "views.eventgroup.delconfirm" }}</p>
+ <p>{{{t "views.eventgroup.deldesc" }}}</p>
</div>
<div class="modal-footer">
- <button type="button" class="button button--secondary" data-dismiss="modal">Close</button>
- <button type="submit" class="button button--danger">Delete event group</button>
+ <button type="button" class="button button--secondary" data-dismiss="modal">{{t "common.close" }}</button>
+ <button type="submit" class="button button--danger">{{t "views.eventgroup.del" }}</button>
</div>
</form>
</div>
@@ -160,7 +159,7 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
- <h5 class="modal-title" id="subscribeModalLabel">Subscribe to '{{eventGroupData.name}}'</h5>
+ <h5 class="modal-title" id="subscribeModalLabel">{{t "views.eventgroup.subscribe" }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
@@ -168,8 +167,7 @@
<form id="subscribeForm" action="/subscribe/{{eventGroupData.id}}" method="post">
<div class="modal-body">
<div class="form-group">
- <p class="form-text small">Enter your email address to receive updates
- whenever a new event is created in this group.</p>
+ <p class="form-text small">{{t "views.eventgroup.subscribedesc" }}</p>
</div>
<div class="form-group">
<input type="email" class="form-control" id="emailAddress"
@@ -177,8 +175,8 @@
</div>
</div>
<div class="modal-footer">
- <button type="button" class="button button--secondary" data-dismiss="modal">Close</button>
- <button type="submit" class="button button--primary">Subscribe</button>
+ <button type="button" class="button button--secondary" data-dismiss="modal">{{t "common.close" }}</button>
+ <button type="submit" class="button button--primary">{{t "views.eventgroup.subscribebutton" }}</button>
</div>
</form>
</div>
@@ -189,7 +187,7 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
- <h5 class="modal-title" id="editTokenModalLabel">Enter editing password</h5>
+ <h5 class="modal-title" id="editTokenModalLabel">{{t "views.edittoken" }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
@@ -197,9 +195,9 @@
<form id="verifyTokenForm" action="/verifytoken/group/{{eventGroupData.id}}" method="post">
<div class="modal-body">
<div class="form-group">
- <p class="form-text small">Enter the editing password you received by email or were shown when the event was created.</p>
+ <p class="form-text small">{{t "views.edittokendesc" }}</p>
<div class="form-group">
- <input type="text" class="form-control" id="editToken" name="editToken" placeholder="Get it right!" data-validation="required">
+ <input type="text" class="form-control" id="editToken" name="editToken" placeholder="{{t "views.right" }}" data-validation="required">
</div>
<div class="form-group">
<div class="alert alert-danger" style="display:none;"></div>
@@ -207,8 +205,8 @@
</div>
</div>
<div class="modal-footer">
- <button type="button" class="button button--secondary" data-dismiss="modal">Close</button>
- <button type="submit" class="button button--primary">Edit group</button>
+ <button type="button" class="button button--secondary" data-dismiss="modal">{{t "common.close" }}</button>
+ <button type="submit" class="button button--primary">{{t "views.eventgroup.edit" }}</button>
</div>
</form>
</div>
@@ -275,12 +273,12 @@ window.groupData = {{{ json jsonData }}};
})
})
$("#copyEventLink").click(function(){
- $(this).html('<i class="fas fa-copy"></i> Copied!');
- setTimeout(function(){ $("#copyEventLink").html('<i class="fas fa-copy"></i> Copy');}, 5000);
+ $(this).html('<i class="fas fa-copy"></i> {{t "common.copied" }}!');
+ setTimeout(function(){ $("#copyEventLink").html('<i class="fas fa-copy"></i> {{t "common.copy" }}');}, 5000);
});
$("#copyFeedLink").click(function(){
- $(this).html('<i class="fas fa-copy"></i> Copied!');
- setTimeout(function(){ $("#copyFeedLink").html('<i class="fas fa-copy"></i> Copy');}, 5000);
+ $(this).html('<i class="fas fa-copy"></i> {{t "common.copied" }}');
+ setTimeout(function(){ $("#copyFeedLink").html('<i class="fas fa-copy"></i> {{t "common.copy" }}');}, 5000);
});
$('#verifyTokenForm').on('submit', function(e) {
@@ -298,7 +296,7 @@ window.groupData = {{{ json jsonData }}};
}
},
error: function(response, status, xhr) {
- form.find('.alert').text('That editing password is incorrect. Try again.').show();
+ form.find('.alert').text('{{t "views.incorrectpswd" }}').show();
}
});
});
diff --git a/views/home.handlebars b/views/home.handlebars
index d5fdb81..4a0ab3f 100755
--- a/views/home.handlebars
+++ b/views/home.handlebars
@@ -1,5 +1,5 @@
<main class="page">
- <h2 class="mb-3 pb-2 text-center border-bottom">About {{siteName}}</h2>
+ <h2 class="mb-3 pb-2 text-center border-bottom">{{{t "views.home.about" }}}</h2>
{{#if instanceDescription}}
<div class="instance-description mb-4">
@@ -9,61 +9,46 @@
{{> instanceRules }}
- <h2 class="mb-3 mt-5 pb-2 text-center border-bottom">About Gathio</h2>
+ <h2 class="mb-3 mt-5 pb-2 text-center border-bottom">{{t "views.home.aboutgathio"}}</h2>
- <p class="lead text-center">Gathio is a simple, federated, privacy-first event hosting platform.</p>
+ <p class="lead text-center">{{t "views.home.intro"}}</p>
<div id="example-event" class="text-center w-100 mt-4 mb-5">
<img
- alt="An example event page for a picnic. The page shows the event's location, host, date and time, and description, as well as buttons to save the event to Google Calendar, export it, and open the location in OpenStreetMap and Google Maps."
+ alt="{{t "views.home.imgexample" }}"
src="images/example-event-2023.png" class="img-fluid w-75 mx-auto shadow-lg rounded">
</div>
- <h3>Privacy-first</h3>
+ <h3>{{t "views.home.privacytitle" }}</h3>
- <p>There are no accounts on Gathio. When you create an event, we generate a password which allows you to edit the
- event. Send all your guests the public link, and all your co-hosts the secret editing link containing the password.
- </p>
+ <p>{{t "views.home.privdesc" }} </p>
- <p>If you supply your email, we'll send you the editing password so you don't lose it - but supplying your email is
- optional!</p>
+ <p>{{t "views.home.privmail" }}</p>
- <p>If this instance automatically deletes its events, sometime after the event finishes, it's deleted from the
- database for ever, and your data goes with it.</p>
+ <p>{{t "views.home.autodelete" }}</p>
- <p>Also, Gathio doesn't show you ads, doesn't sell your data, and never sends you unnecessary emails.</p>
+ <p>{{t "views.home.privacy" }} </p>
- <p>But remember: all events are visible to anyone who knows the link, so probably don't use Gathio to plot your
- surprise birthday party or revolution. Or whatever, you do you.</p>
+ <p>{{t "views.home.attention" }} </p>
- <h3>Configurable</h3>
+ <h3>{{t "views.home.conftitle" }}</h3>
- <p>The <a href="https://gath.io">flagship Gathio instance at gath.io</a> is designed for anyone to create ephemeral,
- hidden events. Anyone can create an event; events are never displayed anywhere public; and they're deleted 7 days
- after they end.</p>
+ <p>{{{t "views.home.flagshipsetting" }}}</p>
- <p>But if your community sets up their own instance, you can limit event creation to a specific list of people,
- display events on a handy list on the homepage, and disable event deletion entirely!</p>
+ <p>{{t "views.home.onpre" }}</p>
- <h3>Federation and self-hosting</h3>
+ <h3>{{t "views.home.fedtitle" }}</h3>
- <p>Gathio can easily be self-hosted, and supports ActivityPub services like Mastodon, Pleroma, and Friendica, allowing
- you to access events from anywhere on the Fediverse. We encourage you to spin up your own instance for your
- community. Detailed instructions on <a href="https://docs.gath.io/using-gathio/fediverse/">ActivityPub access</a>
- and <a href="https://docs.gath.io/running-gathio/installation/">self-hosted installation</a>
- live on our GitHub wiki.
+ <p>{{{t "views.home.selfhost" }}}</p>
- <h3>Open source</h3>
+ <h3>{{t "views.home.opensource" }}</h3>
- <p>Gathio is delighted to be open source, and is built by a lovely group of people. Leave a question in our <a
- href="https://github.com/lowercasename/gathio/issues">tracker</a> if you encounter any issues.</p>
+ <p>{{{t "views.home.osdesc" }}}</p>
{{#if showKofi}}
<div class="card border-success mt-5 mb-3 mx-auto" style="min-width:300px;max-width:50%;">
<div class="card-body">
- <p>
- If you find yourself using and enjoying Gathio, consider <a href="https://github.com/sponsors/lowercasename" class="text-success">supporting Raphael via GitHub Sponsors</a>. It'll help keep the project and main site running! <i class="far fa-heart"></i>
- </p>
+ <p>{{{t "views.home.sponsor" }}}</p>
</div>
</div>
{{/if}}
diff --git a/views/layouts/main.handlebars b/views/layouts/main.handlebars
index e967019..f3b39d4 100755
--- a/views/layouts/main.handlebars
+++ b/views/layouts/main.handlebars
@@ -16,14 +16,14 @@
<meta property="og:type" content="website">
<meta property="og:image:width" content="260">
<meta property="og:image:height" content="260">
- <meta property="og:description" content="{{#if metadata.description}}{{metadata.description}}{{else}}An easier, quicker, and much less privacy-invading way to make and share events{{/if}}">
+ <meta property="og:description" content="{{#if metadata.description}}{{metadata.description}}{{else}}{{t "views.layouts.main.defaultmetadata" }}{{/if}}">
<meta property="og:title" content="{{#if metadata.title}}{{metadata.title}} &middot; {{siteName}}{{else}}{{siteName}}{{/if}}">
<meta property="og:url" content="{{#if metadata.url}}{{metadata.url}}{{else}}https://{{domain}}/{{/if}}">
<meta property="og:image" content="{{#if metadata.image}}{{metadata.image}}{{else}}https://{{domain}}/og-image.jpg{{/if}}">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="{{#if metadata.title}}{{metadata.title}} &middot; {{siteName}}{{else}}{{siteName}}{{/if}}">
- <meta name="twitter:description" content="{{#if metadata.description}}{{metadata.description}}{{else}}An easier, quicker, and much less privacy-invading way to make and share events{{/if}}">
+ <meta name="twitter:description" content="{{#if metadata.description}}{{metadata.description}}{{else}}{{t "views.layouts.main.defaultmetadata" }}{{/if}}">
<meta name="twitter:image" content="{{#if metadata.image}}{{metadata.image}}{{else}}https://{{domain}}/og-image.jpg{{/if}}">
@@ -77,7 +77,7 @@
</p>
{{/if}}
<p class="small text-muted">
- <strong>Gathio</strong> version {{version}} &middot; <a href="https://github.com/lowercasename/gathio">GitHub</a> &middot; Made with <i class="far fa-heart"></i> by <a href="https://raphaelkabo.com">Raphael</a> and <a href="https://github.com/lowercasename/gathio/graphs/contributors">contributors</a>
+ {{{t "views.layouts.main.footnote" }}}
</p>
</footer>
</section>
diff --git a/views/newevent.handlebars b/views/newevent.handlebars
index d6d7024..9de2efd 100755
--- a/views/newevent.handlebars
+++ b/views/newevent.handlebars
@@ -1,25 +1,25 @@
<main class="page" x-data="{currentTab: null}">
-<h2 class="mb-3 pb-2 text-center border-bottom">What would you like to do?</h2>
+<h2 class="mb-3 pb-2 text-center border-bottom">{{t "views.newevent.pagetitle" }}</h2>
<div class="container-fluid mb-4">
<div class="row">
<div class="col-lg-4 p-2">
- <button type="button" id="showNewEventFormButton" class="button w-100" x-bind:class="currentTab === 'event' ? 'button--primary' : 'button--secondary'" x-on:click="currentTab = 'event'"><i class="fas fa-calendar-day"></i> Create a new event</button>
+ <button type="button" id="showNewEventFormButton" class="button w-100" x-bind:class="currentTab === 'event' ? 'button--primary' : 'button--secondary'" x-on:click="currentTab = 'event'"><i class="fas fa-calendar-day"></i> {{t "views.newevent.neweventbutton" }}</button>
</div>
<div class="col-lg-4 p-2">
- <button type="button" id="showImportEventFormButton" class="button w-100" x-bind:class="currentTab === 'importEvent' ? 'button--primary' : 'button--secondary'" x-on:click="currentTab = 'importEvent'"><i class="fas fa-file-import"></i> Import an existing event</button>
+ <button type="button" id="showImportEventFormButton" class="button w-100" x-bind:class="currentTab === 'importEvent' ? 'button--primary' : 'button--secondary'" x-on:click="currentTab = 'importEvent'"><i class="fas fa-file-import"></i> {{t "views.newevent.importevent" }}</button>
</div>
<div class="col-lg-4 p-2">
- <button type="button" id="showNewEventGroupFormButton" class="button w-100" x-bind:class="currentTab === 'group' ? 'button--primary' : 'button--secondary'" x-on:click="currentTab = 'group'"><i class="fas fa-calendar-alt"></i> Create a new event group </button>
+ <button type="button" id="showNewEventGroupFormButton" class="button w-100" x-bind:class="currentTab === 'group' ? 'button--primary' : 'button--secondary'" x-on:click="currentTab = 'group'"><i class="fas fa-calendar-alt"></i> {{t "views.newevent.neweventgroup" }} </button>
</div>
</div>
</div>
<div class="alert alert-info mb-4 text-center" role="alert">
- <i class="fas fa-exclamation-circle"></i> Events are visible to anyone who knows the link.
+ <i class="fas fa-exclamation-circle"></i> {{{t "views.newevent.visiblealert" }}}
</div>
<div id="newEventFormContainer" x-show="currentTab === 'event'" style="display: none">
- <h4 class="mb-2">Create an event</h4>
+ <h4 class="mb-2">{{t "views.newevent.createnew" }}</h4>
<form
id="newEventForm"
enctype="multipart/form-data"
@@ -38,7 +38,7 @@
class="button button--primary w-50"
x-bind:disabled="submitting"
x-bind:class="submitting ? 'button--loading' : ''"
- x-text="submitting ? 'Creating...' : 'Create'"
+ x-text="submitting ? '{{t "common.creating" }}' : '{{t "common.create" }}'"
></button>
</div>
</div>
@@ -50,9 +50,9 @@
</div>
<div id="newEventGroupFormContainer" x-show="currentTab === 'group'" style="display: none">
- <h4 class="mb-2">Create an event group</h4>
- <p class="text-muted">An event group is a holding area for a set of linked events, like a recurring game night, a festival, or a band tour. You can share a public link to your event group just like an individual event link, and people who know the secret editing code will be able to add future events to the group.</p>
- <p class="text-muted">Event groups do not get automatically removed like events do, but events which have been removed from {{siteName}} will of course not show up in an event group.</p>
+ <h4 class="mb-2">{{t "views.newevent.newgroup" }}</h4>
+ <p class="text-muted">{{t "views.newevent.groupdesc" }}</p>
+ <p class="text-muted">{{{t "views.newevent.groupattention" }}}</p>
<form id="newEventGroupForm" enctype="multipart/form-data" x-data="newEventGroupForm()" @submit.prevent="submitForm">
{{> eventGroupForm }}
<div class="form-group row">
@@ -62,7 +62,7 @@
class="button button--primary w-50"
x-bind:disabled="submitting"
x-bind:class="submitting ? 'button--loading' : ''"
- x-text="submitting ? 'Creating...' : 'Create'"
+ x-text="submitting ? '{{t "common.creating" }}' : '{{t "common.create" }}'"
></button>
</div>
</div>
diff --git a/views/optionsform.handlebars b/views/optionsform.handlebars
index 85ebd9f..78e184b 100755
--- a/views/optionsform.handlebars
+++ b/views/optionsform.handlebars
@@ -1,46 +1,46 @@
<div class="form-group row">
- <div class="col-sm-2">Options</div>
+ <div class="col-sm-2">{{t "options.title" }}</div>
<div class="col-sm-10">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="joinCheckbox" name="joinCheckbox" {{#if data.joinCheckbox}}checked{{/if}}>
<label class="form-check-label" for="joinCheckbox">
- Users can mark themselves as attending this event
+ {{t "views.join" }}
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="guestlistCheckbox" name="guestlistCheckbox" {{#if data.guestlistCheckbox}}checked{{/if}}>
<label class="form-check-label" for="guestlistCheckbox">
- Display the list of attendees
+ {{t "views.options.showlistattendees" }}
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="interactionCheckbox" name="interactionCheckbox" {{#if data.interactionCheckbox}}checked{{/if}}>
<label class="form-check-label" for="interactionCheckbox">
- Users can post comments on this event
+ {{t "views.interaction" }}
</label>
</div>
</div>
</div>
<div class="form-group row">
- <div class="col-sm-2">Options</div>
+ <div class="col-sm-2">{{t "options.title" }}</div>
<div class="col-sm-10">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="joinCheckbox" name="joinCheckbox" {{#if eventData.usersCanAttend}}checked{{/if}}>
<label class="form-check-label" for="joinCheckbox">
- Users can mark themselves as attending this event
+ {{t "views.join" }}
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="guestlistCheckbox" name="guestlistCheckbox" {{#if eventData.showUsersList}}checked{{/if}}>
<label class="form-check-label" for="guestlistCheckbox">
- Display the list of attendees
+ {{t "views.options.showlistattendees" }}
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="interactionCheckbox" name="interactionCheckbox" {{#if eventData.usersCanComment}}checked{{/if}}>
<label class="form-check-label" for="interactionCheckbox">
- Users can post comments on this event
+ {{t "views.interaction" }}
</label>
</div>
</div>
diff --git a/views/partials/editeventgroupmodal.handlebars b/views/partials/editeventgroupmodal.handlebars
index 046d15e..8a6a347 100644
--- a/views/partials/editeventgroupmodal.handlebars
+++ b/views/partials/editeventgroupmodal.handlebars
@@ -2,7 +2,7 @@
<div class="modal-dialog modal-xl modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
- <h5 class="modal-title" id="editModalLabel">Edit '{{eventGroupData.name}}'</h5>
+ <h5 class="modal-title" id="editModalLabel">{{{t "views.eventgroup.edit" }}} </h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
@@ -14,23 +14,23 @@
<div class="form-group">
<div class="card border-danger mb-3">
- <div class="card-header text-danger">Delete this event group</div>
+ <div class="card-header text-danger">{{t "views.partials.editeventgroup.del" }}</div>
<div class="card-body text-danger">
- <button type="button" id="deleteEvent" class="button button--danger" data-toggle="modal" data-target="#deleteModal"><i class="fas fa-trash"></i> Delete event group</button>
+ <button type="button" id="deleteEvent" class="button button--danger" data-toggle="modal" data-target="#deleteModal"><i class="fas fa-trash"></i> {{t "views.partials.editeventgroup.delbutton" }}</button>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
- <button type="button" class="button button--secondary" data-dismiss="modal">Close</button>
+ <button type="button" class="button button--secondary" data-dismiss="modal">{{t "common.close" }}</button>
<button
type="submit"
class="button button--primary"
@click="submitForm"
x-bind:disabled="submitting"
x-bind:class="submitting ? 'button--loading' : ''"
- x-text="submitting ? 'Saving...' : 'Save'"
+ x-text="submitting ? '{{t "views.partials.saving" }}' : '{{t "views.partials.save" }}'"
></button>
</div>
</div>
diff --git a/views/partials/editeventmodal.handlebars b/views/partials/editeventmodal.handlebars
index 986da9c..2848125 100644
--- a/views/partials/editeventmodal.handlebars
+++ b/views/partials/editeventmodal.handlebars
@@ -7,7 +7,7 @@
<div class="modal-dialog modal-xl modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
- <h5 class="modal-title" id="editModalLabel">Edit '{{eventData.name}}'</h5>
+ <h5 class="modal-title" id="editModalLabel">{{{t "views.partials.editevent.edit" }}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
@@ -19,25 +19,25 @@
<div class="form-group">
<div class="card border-danger mb-3">
- <div class="card-header text-danger">Delete this event</div>
+ <div class="card-header text-danger">{{t "views.partials.editevent.delthis" }}</div>
<div class="card-body text-danger">
<button type="button" id="deleteEvent" class="button button--danger" data-toggle="modal"
data-target="#deleteModal" data-event-id="{{eventData.id}}"><i class="fas fa-trash"></i>
- Delete</button>
+ {{t "views.del" }}</button>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
- <button type="button" class="button button--secondary" data-dismiss="modal">Close</button>
+ <button type="button" class="button button--secondary" data-dismiss="modal">{{t "common.close" }}</button>
<button
type="submit"
class="button button--primary"
@click="submitForm"
x-bind:disabled="submitting"
x-bind:class="submitting ? 'button--loading' : ''"
- x-text="submitting ? 'Saving...' : 'Save'"
+ x-text="submitting ? '{{t "views.partials.saving" }}' : '{{t "views.partials.save" }}'"
></button>
</div>
</div>
diff --git a/views/partials/eventForm.handlebars b/views/partials/eventForm.handlebars
index 93e8e84..8714058 100755
--- a/views/partials/eventForm.handlebars
+++ b/views/partials/eventForm.handlebars
@@ -1,100 +1,100 @@
<input type="hidden" name="magicLinkToken" value="{{magicLinkToken}}" x-ref="magicLinkToken">
<div class="form-group">
- <label for="eventName" >Event name</label>
+ <label for="eventName" >{{t "common.eventname" }}</label>
<div class="form-group ">
- <input type="text" class="form-control" id="eventName" name="eventName" placeholder="Make it snappy." x-model="data.eventName" >
+ <input type="text" class="form-control" id="eventName" name="eventName" placeholder="{{t "views.partials.snappy" }}" x-model="data.eventName" >
</div>
</div>
<div class="form-group">
- <label for="eventLocation" >Location</label>
+ <label for="eventLocation" >{{t "common.eventlocation" }}</label>
<div class="form-group ">
- <input type="text" class="form-control" id="eventLocation" name="eventLocation" placeholder="Be specific." x-model="data.eventLocation">
+ <input type="text" class="form-control" id="eventLocation" name="eventLocation" placeholder="{{t "views.event.locationdesc" }}" x-model="data.eventLocation">
</div>
</div>
<div class="form-group">
- <label for="eventStart" >Starts</label>
+ <label for="eventStart" >{{t "common.eventstart" }}</label>
<div class="form-group">
<input type="datetime-local" class="form-control" id="eventStart" name="eventStart" x-model="data.eventStart" x-on:blur="updateEventEnd">
</div>
</div>
<div class="form-group">
- <label for="eventEnd" >Ends</label>
+ <label for="eventEnd" >{{t "common.eventend" }}</label>
<div class="form-group ">
<input type="datetime-local" class="form-control" id="eventEnd" name="eventEnd" x-model="data.eventEnd">
</div>
</div>
<div class="form-group">
- <label for="timezone" >Timezone</label>
+ <label for="timezone" >{{t "common.timezone" }}</label>
<div class="form-group ">
<select class="select2" id="timezone" name="timezone" x-ref="timezone"></select>
</div>
</div>
<div class="form-group">
- <label for="eventDescription" >Description</label>
+ <label for="eventDescription" >{{t "views.partials.eventform.eventdescription" }}</label>
<div class="form-group ">
- <textarea class="form-control expand" id="eventDescription" name="eventDescription" placeholder="You can always edit it later." x-model="data.eventDescription" ></textarea>
- <small class="form-text"><a href="https://commonmark.org/help/">Markdown</a> formatting supported.</small>
+ <textarea class="form-control expand" id="eventDescription" name="eventDescription" placeholder="{{t "views.event.editlater" }}" x-model="data.eventDescription" ></textarea>
+ <small class="form-text">{{{t "views.partials.mdsupport" }}}</small>
</div>
</div>
<div class="form-group">
- <label for="eventURL">Link</label>
+ <label for="eventURL">{{t "views.partials.eventform.eventurl" }}</label>
<div class="form-group ">
<input type="url" class="form-control" id="eventURL" name="eventURL" placeholder="https://example.com" x-model="data.eventURL">
- <small class="form-text">For tickets or another event page (optional).</small>
+ <small class="form-text">{{t "views.partials.eventform.eventurldesc" }}</small>
</div>
</div>
<div class="form-group">
- <label for="eventImage" >Cover image</label>
+ <label for="eventImage" >{{t "common.coverimg" }}</label>
<div class="form-group ">
<div class="image-preview" id="event-image-preview">
- <label for="image-upload" id="event-image-label">Choose file</label>
+ <label for="image-upload" id="event-image-label">{{t "views.partials.choosefile" }}</label>
<input type="file" name="imageUpload" id="event-image-upload" accept="image/jpeg,image/gif,image/png" x-ref="eventImageUpload" />
</div>
- <small class="form-text">Recommended dimensions (w x h): 920px by 300px.</small>
+ <small class="form-text">{{t "views.partials.recommendeddimensions" }}</small>
{{#if eventData.image}}
<div class="form-group my-2">
- <button type="button" class="button button--danger" id="deleteImage">Delete image</button>
+ <button type="button" class="button button--danger" id="deleteImage">{{t "views.partials.delimg" }}</button>
</div>
{{/if}}
</div>
</div>
<div class="form-group">
- <label for="hostName" >Host name</label>
+ <label for="hostName" >{{t "views.partials.eventform.hostname" }}</label>
<div class="form-group ">
- <input type="text" class="form-control" id="hostName" name="hostName" placeholder="Will be shown on the event page (optional)." x-model="data.hostName" >
+ <input type="text" class="form-control" id="hostName" name="hostName" placeholder="{{t "views.partials.eventform.hostnamedesc" }}" x-model="data.hostName" >
</div>
</div>
<div class="form-group">
- <label for="creatorEmail" >Your email</label>
+ <label for="creatorEmail" >{{t "views.partials.eventform.creatoremail" }}</label>
<div class="form-group ">
- <input type="email" class="form-control" id="creatorEmail" name="creatorEmail" placeholder="Will not be shown anywhere (optional)." x-model.fill="data.creatorEmail" {{#if creatorEmail}}value="{{creatorEmail}}" readonly{{/if}}>
- <small class="form-text">If you provide your email, we will send your secret editing password here, and use it to notify you of updates to the event.</small>
+ <input type="email" class="form-control" id="creatorEmail" name="creatorEmail" placeholder="{{t "views.partials.wontshow" }}" x-model.fill="data.creatorEmail" {{#if creatorEmail}}value="{{creatorEmail}}" readonly{{/if}}>
+ <small class="form-text">{{t "views.partials.creatoremaildesc" }}</small>
</div>
</div>
<div class="form-group">
- <label>Options</label>
+ <label>{{t "views.partials.eventform.options" }}</label>
<div >
{{#if showPublicEventList}}
<div class="form-check">
<input class="form-check-input" type="checkbox" id="publicEventCheckbox" name="publicCheckbox" x-model="data.publicCheckbox">
<label class="form-check-label" for="publicEventCheckbox">
- Display this event on the public event list
+ {{t "views.partials.eventform.publicevent" }}
</label>
</div>
{{/if}}
<div class="form-check">
<input class="form-check-input" type="checkbox" id="eventGroupCheckbox" name="eventGroupCheckbox" x-model="data.eventGroupCheckbox">
<label class="form-check-label" for="eventGroupCheckbox">
- This event is part of an event group
+ {{t "views.partials.eventform.eventgroup" }}
</label>
</div>
<div class="card my-2" id="eventGroupData" x-show="data.eventGroupCheckbox">
<div class="card-header">
- <strong>Link this event to an event group</strong>
+ <strong>{{t "views.partials.eventform.eventgroupdata" }}</strong>
</div>
<div class="card-body" x-data="eventGroupLinker()">
<div class="form-group-label" x-show="data.groups.length > 0">
- <label>Choose a group you've edited before</label>
+ <label>{{t "views.partials.eventform.eventgrouplinker" }}</label>
</div>
<div class="form-group" x-show="data.groups.length > 0">
<select
@@ -111,7 +111,7 @@
</select>
</div>
<button type="button" class="button button--outline-primary w-100 text-center" x-on:click="manualGroupInputVisible = !manualGroupInputVisible">
- Enter group details manually <i class="fas" :class="{'fa-caret-down': !manualGroupInputVisible, 'fa-caret-up': manualGroupInputVisible}"></i>
+ {{t "views.partials.eventform.groupbutton" }} <i class="fas" :class="{'fa-caret-down': !manualGroupInputVisible, 'fa-caret-up': manualGroupInputVisible}"></i>
</button>
<div
class="form-group slider"
@@ -120,15 +120,15 @@
:style="manualGroupInputVisible && {height: $el.scrollHeight+`px`}"
:aria-hidden="!manualGroupInputVisible"
>
- <label for="eventGroupID" class="mt-2">Event group ID</label>
+ <label for="eventGroupID" class="mt-2">{{t "common.eventgroupid" }}</label>
<div class="form-group">
<input type="text" class="form-control text-monospace" id="eventGroupID" name="eventGroupID" x-model="data.eventGroupID" x-on:input="resetGroupSelector">
- <small class="form-text">You can find this short string of characters in the event group's link, in your confirmation email, or on the event group's page.</small>
+ <small class="form-text">{{t "views.partials.eventform.eventgroupid" }}</small>
</div>
- <label for="eventGroupEditToken">Event group secret editing code</label>
+ <label for="eventGroupEditToken">{{t "views.partials.eventform.eventgroupedittoken" }}</label>
<div class="form-group mb-0">
<input type="text" class="form-control text-monospace" id="eventGroupEditToken" name="eventGroupEditToken" x-model="data.eventGroupEditToken" x-on:input="resetGroupSelector">
- <small class="form-text">You can find this long string of characters in the confirmation email you received when you created the event group.</small>
+ <small class="form-text">{{t "views.partials.eventform.eventgroupedittoken" }}</small>
</div>
</div>
</div>
@@ -136,27 +136,27 @@
<div class="form-check">
<input class="form-check-input" type="checkbox" id="interactionCheckbox" name="interactionCheckbox" x-model="data.interactionCheckbox">
<label class="form-check-label" for="interactionCheckbox">
- Users can post comments on this event
+ {{t "views.interaction" }}
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="joinCheckbox" name="joinCheckbox" x-model="data.joinCheckbox">
<label class="form-check-label" for="joinCheckbox">
- Users can mark themselves as attending this event
+ {{t "views.join" }}
</label>
</div>
<div class="form-check" id="maxAttendeesCheckboxContainer" x-show="data.joinCheckbox">
<input class="form-check-input" type="checkbox" id="maxAttendeesCheckbox" name="maxAttendeesCheckbox" x-model="data.maxAttendeesCheckbox">
<label class="form-check-label" for="maxAttendeesCheckbox">
- Set a limit on the maximum number of attendees
+ {{t "views.partials.eventform.maxattendeestitle" }}
</label>
</div>
</div>
</div>
<div class="form-group" id="maxAttendeesContainer" x-show="data.maxAttendeesCheckbox && data.joinCheckbox">
- <label for="maxAttendees" >Attendee limit</label>
+ <label for="maxAttendees" >{{t "views.partials.eventform.maxattendees" }}</label>
<div class="form-group ">
- <input type="number" class="form-control" id="maxAttendees" name="maxAttendees" placeholder="Enter a number." x-model="data.maxAttendees" >
+ <input type="number" class="form-control" id="maxAttendees" name="maxAttendees" placeholder="{{t "views.event.enternum" }}" x-model="data.maxAttendees" >
</div>
</div>
<div class="form-group">
@@ -166,7 +166,7 @@
role="alert"
x-show="errors.length > 0"
>
- <p><i class="fas fa-exclamation-triangle"></i> Please fix these errors:</p>
+ <p><i class="fas fa-exclamation-triangle"></i> {{t "views.partials.fixerrors" }}</p>
<ul>
<template x-for="error in errors">
<li x-html="error.message"></li>
diff --git a/views/partials/eventGroupForm.handlebars b/views/partials/eventGroupForm.handlebars
index 5536e49..5cc8b40 100644
--- a/views/partials/eventGroupForm.handlebars
+++ b/views/partials/eventGroupForm.handlebars
@@ -1,44 +1,44 @@
<input type="hidden" name="magicLinkToken" value="{{magicLinkToken}}" x-ref="magicLinkToken">
<div class="form-group">
- <label for="eventGroupName">Name</label>
- <input type="text" class="form-control" id="eventGroupName" name="eventGroupName" placeholder="Make it snappy." x-model="data.eventGroupName">
+ <label for="eventGroupName">{{t "common.eventgroupname" }}</label>
+ <input type="text" class="form-control" id="eventGroupName" name="eventGroupName" placeholder="{{t "views.partials.snappy" }}" x-model="data.eventGroupName">
</div>
<div class="form-group">
- <label for="eventGroupDescription">Description</label>
+ <label for="eventGroupDescription">{{t "views.partials.eventgroupform.eventgroupddesc" }}</label>
<textarea class="form-control" id="eventGroupDescription" name="eventGroupDescription" x-model="data.eventGroupDescription">{{eventGroupData.description}}</textarea>
- <small class="form-text"><a href="https://commonmark.org/help/">Markdown</a> formatting supported.</small>
+ <small class="form-text">{{{t "views.partials.mdsupport" }}}</small>
</div>
<div class="form-group">
- <label for="eventGroupURL">Link</label>
+ <label for="eventGroupURL">{{t "views.partials.eventgroupform.eventgroupurl" }}</label>
<input type="url" class="form-control" id="eventGroupURL" name="eventGroupURL" placeholder="https://example.com" x-model="data.eventGroupURL">
- <small class="form-text">For tickets or a page with more information (optional).</small>
+ <small class="form-text">{{t "views.partials.eventgroupform.eventgroupurldesc" }}</small>
</div>
<div class="form-group">
- <label for="hostName">Host or organisation name</label>
- <input type="text" class="form-control" id="eventGroupHostName" name="hostName" placeholder="Will be shown on the event group page (optional)." x-model="data.hostName">
+ <label for="hostName">{{t "views.partials.eventgroupform.hostname" }}</label>
+ <input type="text" class="form-control" id="eventGroupHostName" name="hostName" placeholder="{{t "views.partials.eventgroupform.isshowningroup" }}" x-model="data.hostName">
</div>
<div class="form-group">
- <label for="creatorEmail">Your email</label>
+ <label for="creatorEmail">{{t "views.partials.eventgroupform.creatoremail" }}</label>
<div class="form-group">
- <input type="email" class="form-control" id="eventGroupCreatorEmail" name="creatorEmail" placeholder="Will not be shown anywhere (optional)." x-model.fill="data.creatorEmail" {{#if creatorEmail}}value="{{creatorEmail}}" readonly{{/if}}>
- <small class="form-text">If you provide your email, we will send your secret editing password here, and use it to notify you of updates to the event.</small>
+ <input type="email" class="form-control" id="eventGroupCreatorEmail" name="creatorEmail" placeholder="{{t "views.partials.wontshow" }}" x-model.fill="data.creatorEmail" {{#if creatorEmail}}value="{{creatorEmail}}" readonly{{/if}}>
+ <small class="form-text">{{t "views.partials.creatoremaildesc" }}</small>
</div>
</div>
<div class="form-group">
- <label>Cover image</label>
+ <label>{{t "common.coverimg" }}</label>
<div class="image-preview" id="group-image-preview">
- <label for="eventGroupImageUpload" id="group-image-label">Choose file</label>
+ <label for="eventGroupImageUpload" id="group-image-label">{{t "views.partials.choosefile" }}</label>
<input type="file" name="imageUpload" id="group-image-upload" accept="image/jpeg,image/gif,image/png" x-ref="eventGroupImageUpload"/>
</div>
- <small class="form-text">Recommended dimensions (w x h): 920px by 300px.</small>
+ <small class="form-text">{{t "views.partials.recommendeddimensions" }}</small>
</div>
{{#if showPublicEventList}}
<div class="form-group">
- <label>Options</label>
+ <label>{{{t "views.partials.eventgroup.options" }}}</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="publicGroupCheckbox" name="publicCheckbox" x-model="data.publicCheckbox">
<label class="form-check-label" for="publicGroupCheckbox">
- Display this group on the public group list
+ {{t "views.partials.eventgroupform.publicgroup" }}
</label>
</div>
</div>
@@ -50,7 +50,7 @@
role="alert"
x-show="errors.length > 0"
>
- <p><i class="fas fa-exclamation-triangle"></i> Please fix these errors:</p>
+ <p><i class="fas fa-exclamation-triangle"></i> {{t "views.partials.fixerrors" }}</p>
<ul>
<template x-for="error in errors">
<li x-html="error.message"></li>
diff --git a/views/partials/eventList.handlebars b/views/partials/eventList.handlebars
index 6c8e7a4..b694fa3 100644
--- a/views/partials/eventList.handlebars
+++ b/views/partials/eventList.handlebars
@@ -17,6 +17,6 @@
{{/each}}
{{/each}}
{{else}}
- <div class="list-group-item">No events!</div>
+ <div class="list-group-item">{{t "views.partials.eventlist.noevents" }}</div>
{{/if}}
</div>
diff --git a/views/partials/importeventform.handlebars b/views/partials/importeventform.handlebars
index a8c0f0e..2d91e36 100644
--- a/views/partials/importeventform.handlebars
+++ b/views/partials/importeventform.handlebars
@@ -1,6 +1,6 @@
-<h4 class="mb-2">Import an existing event</h4>
+<h4 class="mb-2">{{t "views.newevent.importevent" }}</h4>
<p>
- Upload an .ics file here to instantly create an event. You can save a Facebook event as an .ics file by clicking on the context menu next to the 'Import' and 'Edit' buttons on the event page and choosing the 'Export Event' option. Then select the 'Save to calendar' option and save the file on your computer.
+ {{t "views.partials.importeventform.importdesc" }}
</p>
<img class="img-thumbnail mb-3 d-block mx-auto" src="/images/facebook-export.png" alt="Image showing the location of the export option on Facebook" />
@@ -11,15 +11,15 @@
<div class="custom-file" id="icsImportContainer">
<input required name="icsImportControl" type="file" class="custom-file-input" id="icsImportControl" aria-describedby="fileHelp" accept="text/calendar" x-ref="icsImportControl"/>
<label name="icsImportLabel" class="custom-file-label" id="icsImportLabel" for="icsImportControl">
- <i class="far fa-file-alt"></i> Select file
+ <i class="far fa-file-alt"></i> {{t "views.partials.importevent.selectfile" }}
</label>
</div>
</div>
<div class="form-group">
- <label for="creatorEmail" class="form-label">Your email</label>
+ <label for="creatorEmail" class="form-label">{{t "common.youremail" }}</label>
<div class="form-group">
- <input type="email" class="form-control" id="importCreatorEmail" name="creatorEmail" placeholder="Will not be shown anywhere (optional)." x-model.fill="data.creatorEmail" {{#if creatorEmail}}value="{{creatorEmail}}" readonly{{/if}}>
- <small class="form-text">If you provide your email, we will send your secret editing password here, and use it to notify you of updates to the event.</small>
+ <input type="email" class="form-control" id="importCreatorEmail" name="creatorEmail" placeholder="{{t "views.partials.wontshow" }}" x-model.fill="data.creatorEmail" {{#if creatorEmail}}value="{{creatorEmail}}" readonly{{/if}}>
+ <small class="form-text">{{t "views.partials.creatoremaildesc" }}</small>
</div>
</div>
<div class="form-group">
@@ -29,7 +29,7 @@
role="alert"
x-show="errors.length > 0"
>
- <p><i class="fas fa-exclamation-triangle"></i> Please fix these errors:</p>
+ <p><i class="fas fa-exclamation-triangle"></i> {{t "views.partials.fixerrors" }}</p>
<ul>
<template x-for="error in errors">
<li x-html="error.message"></li>
@@ -43,6 +43,6 @@
class="d-block mx-auto button button--primary w-50"
x-bind:disabled="submitting"
x-bind:class="submitting ? 'button--loading' : ''"
- x-text="submitting ? 'Importing...' : 'Import'"
+ x-text="submitting ? '{{t "views.partials.importeventform.importing" }}' : '{{t "views.partials.importeventform.import" }}'"
></button>
</form>
diff --git a/views/partials/instanceRules.handlebars b/views/partials/instanceRules.handlebars
index c7fa9be..5cf3bcf 100644
--- a/views/partials/instanceRules.handlebars
+++ b/views/partials/instanceRules.handlebars
@@ -1,6 +1,6 @@
<div class="card mb-4">
<div class="card-header">
- <h6 class="mb-1">Instance settings</h6>
+ <h6 class="mb-1">{{t "views.partials.instancerules.instancesettings" }}</h6>
</div>
<ul class="list-group list-group-flush">
diff --git a/views/partials/sidebar.handlebars b/views/partials/sidebar.handlebars
index c1184be..ae4929d 100755
--- a/views/partials/sidebar.handlebars
+++ b/views/partials/sidebar.handlebars
@@ -2,10 +2,10 @@
<h1><a href="/">gathio</a></h1>
<ul id="sidebar__nav">
- <li><a class="button button--primary" href="/new"><i class="far fa-calendar-plus"></i> Create an event</a></li>
+ <li><a class="button button--primary" href="/new"><i class="far fa-calendar-plus"></i> {{t "views.partials.sidebar.createevent" }}</a></li>
{{#if showPublicEventList}}
- <li><a href="/events">View events</a></li>
- <li><a href="/about">About</a></li>
+ <li><a href="/events">{{t "views.partials.sidebar.events" }}</a></li>
+ <li><a href="/about">{{t "views.partials.sidebar.about" }}</a></li>
{{/if}}
</ul>
</div>
diff --git a/views/publicEventList.handlebars b/views/publicEventList.handlebars
index b8cacd0..a16735b 100644
--- a/views/publicEventList.handlebars
+++ b/views/publicEventList.handlebars
@@ -12,39 +12,39 @@
<ul class="nav d-flex flex-gap--small">
<li>
- <a id="eventsTab" class="button button--lg" x-bind:class="currentTab === 'events' ? 'button--primary' : 'button--secondary'" aria-current="page" href="#" x-on:click.prevent="currentTab = 'events'">Events</a>
+ <a id="eventsTab" class="button button--lg" x-bind:class="currentTab === 'events' ? 'button--primary' : 'button--secondary'" aria-current="page" href="#" x-on:click.prevent="currentTab = 'events'">{{t "views.publiceventlist.events" }}</a>
</li>
<li>
- <a id="groupsTab" class="button button--lg" x-bind:class="currentTab === 'groups' ? 'button--primary' : 'button--secondary'" href="#" x-on:click.prevent="currentTab = 'groups'">Groups</a>
+ <a id="groupsTab" class="button button--lg" x-bind:class="currentTab === 'groups' ? 'button--primary' : 'button--secondary'" href="#" x-on:click.prevent="currentTab = 'groups'">{{t "views.publiceventlist.groups" }}</a>
</li>
</ul>
<div x-show="currentTab === 'events'">
<div class="card mt-4 mb-4" id="upcomingEvents">
- <h5 class="card-header">Upcoming events</h5>
+ <h5 class="card-header">{{t "views.publiceventlist.upcomingevents" }}</h5>
{{> eventList upcomingEvents }}
</div>
<div class="card mt-4 mb-4" id="pastEvents">
- <h5 class="card-header">Past events</h5>
+ <h5 class="card-header">{{t "views.publiceventlist.pastevents" }}</h5>
{{> eventList pastEvents }}
</div>
</div>
<div x-show="currentTab === 'groups'">
<div class="card mt-4 mb-4" id="eventGroups">
- <h5 class="card-header">Event groups</h5>
+ <h5 class="card-header">{{t "common.eventgroups" }}</h5>
<div class="list-group list-group-flush">
{{#if eventGroups}}
{{#each eventGroups}}
<a href="/group/{{this.id}}" class="list-group-item list-group-item-action">
<i class="fas fa-fw fa-calendar-alt"></i>
<strong>{{this.name}}</strong>
- <span class="badge badge-secondary ml-2">{{this.numberOfEvents}} {{plural this.numberOfEvents "event(s)"}}</span>
+ <span class="badge badge-secondary ml-2">{{plural "views.publiceventlist.numoevents" this.numberOfEvents }}</span>
</a>
{{/each}}
{{else}}
- <div class="list-group-item">No groups!</div>
+ <div class="list-group-item">{{t "views.publiceventlist.nogroups" }}</div>
{{/if}}
</div>