summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/app.ts200
-rw-r--r--src/helpers.ts93
-rw-r--r--src/routes/frontend.ts8
-rw-r--r--src/types/i18next-fs-backend.d.ts5
4 files changed, 218 insertions, 88 deletions
diff --git a/src/app.ts b/src/app.ts
index 0708081..febc67d 100755
--- a/src/app.ts
+++ b/src/app.ts
@@ -1,6 +1,20 @@
import express from "express";
import hbs from "express-handlebars";
import cookieParser from "cookie-parser";
+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');
+
+// ESモジュールで__dirnameを再現
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
import routes from "./routes.js";
import frontend from "./routes/frontend.js";
@@ -11,6 +25,7 @@ import staticPages from "./routes/static.js";
import magicLink from "./routes/magicLink.js";
import { initEmailService } from "./lib/email.js";
+import { getI18nHelpers } from "./helpers.js";
import {
activityPubContentType,
alternateActivityPubContentType,
@@ -20,55 +35,142 @@ const app = express();
app.locals.sendEmails = initEmailService();
-// View engine //
-const hbsInstance = hbs.create({
- defaultLayout: "main",
- partialsDir: ["views/partials/"],
- layoutsDir: "views/layouts/",
- helpers: {
- plural: function (number: number, text: string) {
- var 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)
- var 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: any) {
- return JSON.stringify(context);
+// ESモジュールで__dirnameを再現する部分を関数化
+const getLocalesPath = () => {
+ const __filename = fileURLToPath(import.meta.url);
+ const __dirname = dirname(__filename);
+ return path.join(__dirname, '..', 'locales');
+};
+
+async function initializeApp() {
+ // Cookies //
+ app.use(cookieParser());
+
+ // カスタム言語検出ミドルウェア
+ // app.use((req, res, next) => {
+ // const acceptLanguage = req.headers['accept-language'];
+ // if (acceptLanguage && acceptLanguage.includes('ja')) {
+ // res.cookie('i18next', 'ja', {
+ // maxAge: 365 * 24 * 60 * 60 * 1000,
+ // httpOnly: true,
+ // sameSite: 'lax'
+ // });
+ // }
+ // next();
+ // });
+
+ // 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: true,
+ detection: {
+ order: ['header', 'cookie'],
+ lookupHeader: 'accept-language',
+ lookupCookie: 'i18next',
+ caches: ['cookie']
+ },
+ interpolation: {
+ escapeValue: false
+ }
+ });
+
+ app.use(handle(i18next));
+
+ // 言語を明示的に切り替える
+ app.use((req, res, next) => {
+ const currentLanguage = i18next.language;
+ i18next.changeLanguage(req.language);
+ const newLanguage = i18next.language;
+ console.log('Language Change:', {
+ header: req.headers['accept-language'],
+ detected: req.language,
+ currentLanguage: currentLanguage,
+ newLanguage: newLanguage
+ });
+ next();
+ });
+
+ // デバッグ用
+ 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 = hbs.create({
+ defaultLayout: "main",
+ partialsDir: ["views/partials/"],
+ layoutsDir: "views/layouts/",
+ helpers: {
+ plural: function (number: number, text: string) {
+ var 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)
+ var 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: any) {
+ return JSON.stringify(context);
+ },
+ // i18nextヘルパーを追加
+ ...getI18nHelpers()
},
- },
-});
-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);
+ });
+
+ // 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');
+ }
+
+ 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/routes/frontend.ts b/src/routes/frontend.ts
index 14bb779..96d7587 100644
--- a/src/routes/frontend.ts
+++ b/src/routes/frontend.ts
@@ -8,7 +8,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 {
@@ -546,7 +546,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) {
@@ -568,7 +568,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) {
@@ -594,7 +594,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/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