diff options
Diffstat (limited to 'src')
-rwxr-xr-x | src/app.ts | 200 | ||||
-rw-r--r-- | src/helpers.ts | 93 | ||||
-rw-r--r-- | src/routes/frontend.ts | 8 | ||||
-rw-r--r-- | src/types/i18next-fs-backend.d.ts | 5 |
4 files changed, 218 insertions, 88 deletions
@@ -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 |