summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/app.ts208
-rw-r--r--src/helpers.ts93
-rw-r--r--src/lib/config.ts24
-rw-r--r--src/lib/event.ts4
-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
10 files changed, 335 insertions, 190 deletions
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 6642eef..5b74e4a 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;
@@ -110,44 +111,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",
},
);
@@ -157,12 +158,13 @@ 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.";
+ i18next.t("config.defaultinstancedesc");
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/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..a64bce4 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",
});
}