summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--locales/en.json178
-rw-r--r--locales/ja.json178
-rw-r--r--package.json11
-rw-r--r--pnpm-lock.yaml173
-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
-rw-r--r--tsconfig.json2
-rw-r--r--utils.ts0
-rwxr-xr-xviews/404.handlebars4
-rw-r--r--views/createEventMagicLink.handlebars10
-rwxr-xr-xviews/event.handlebars139
-rwxr-xr-xviews/eventgroup.handlebars70
-rwxr-xr-xviews/home.handlebars52
-rwxr-xr-xviews/layouts/main.handlebars2
-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.handlebars32
-rw-r--r--views/partials/eventList.handlebars2
-rw-r--r--views/partials/importeventform.handlebars14
-rw-r--r--views/partials/instanceRules.handlebars2
-rwxr-xr-xviews/partials/sidebar.handlebars6
-rw-r--r--views/publicEventList.handlebars14
27 files changed, 989 insertions, 338 deletions
diff --git a/locales/en.json b/locales/en.json
new file mode 100644
index 0000000..1861471
--- /dev/null
+++ b/locales/en.json
@@ -0,0 +1,178 @@
+{
+ "del": "Delete",
+ "edittoken": "Enter editing password",
+ "edittokendesc": "Enter the editing password you received by email or were shown when the event was created.",
+ "copied": "Copied!",
+ "incorrectpwd": "That editing password is incorrect. Try again.",
+ "event.started": "event.started",
+ "event.ended": "Ended",
+ "event.partof": "Part of <a href='/group/{{eventData.eventGroup.id}}'>{{eventData.eventGroup.name}}</a>",
+ "copy": "Copy",
+ "about": "About",
+ "event.attendeenum": "How many people in your party?",
+ "event.attendeeemail": "Your Email (optional)",
+ "joinemaildesc": "If you provide your email, you will receive updates to the event.",
+ "event.nospam": "We won't spam you",
+ "event.removepswd": "Deletion password",
+ "event.removepswddesc": "You will need this password if you want 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>.",
+ "close": "Close",
+ "event.addmyself": "Add myself",
+ "event.removemyselfdesc": "Remove yourself from '{{eventData.name}}'",
+ "event.lostpswd": "Lost your password? Get in touch with the event organiser.",
+ "event.removemyself": "Remove myself",
+ "event.removeattendeedesc": "Remove attendee from '{{eventData.name}}'",
+ "event.confremoveattendee": "Are you sure you want to remove this attendee from the event? This action cannot be undone.",
+ "event.discussion": "event.discussion",
+ "event.postbutton": "event.postbutton",
+ "group.edit": "Edit {{eventGroupData.name}}",
+ "group.feedlinkdesc": "Paste this URL into your calendar app\nto subscribe to a live feed of events from this group.",
+ "group.addevent": "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.",
+ "group.p.eventgroupid": "Event group ID",
+ "upcomingevents": "Upcoming events",
+ "noevents": "No events!",
+ "group.delconfirm": "Are you sure you want to delete this event group? This action cannot be undone.",
+ "group.deldesc": "This will <strong>not</strong> delete the individual events contained in this group. They can be linked to another group later.",
+ "group.del": "Delete event group",
+ "group.subscribe": "Subscribe to '{{eventGroupData.name}}'",
+ "group.subscribedesc": "Enter your email address to receive updates\nwhenever a new event is created in this group.",
+ "group.subscribebutton": "Subscribe",
+ "home.privacy": "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.",
+ "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.",
+ "emailaddr": "Email address",
+ "forgotpwd": "Forgot password",
+ "newevent.pagetitle": "What would you like to do?",
+ "newevent.neweventbutton": "Create a new event",
+ "newevent.neweventgroup": "Create a new event group",
+ "event.options": "event.options",
+ "join": "Users can mark themselves as attending this event",
+ "options.showlistattendees": "Display the list of attendees",
+ "interaction": "Users can post comments on this event",
+ "group.p.eventgroupdescription": "Description",
+ "group.p.eventgroupurl": "Link",
+ "group.p.hostname": "Host or organisation name",
+ "coverimg": "Cover image",
+ "recommendeddimensions": "Recommended dimensions (w x h): 920px by 300px.",
+ "group.p.del": "Delete this event group",
+ "group.p.delbutton": "Delete event group",
+ "save": "Save changes",
+ "mdsupport": "<a href='https://commonmark.org/help/'>Markdown</a> formatting\nsupported.",
+ "choosefile": "Choose file",
+ "imgdel": "Delete image",
+ "event.p.eventgroupdata": "Link this event to an event group",
+ "event.p.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.",
+ "group.p.eventgroupedittoken": "Event group secret\nediting code",
+ "event.p.eventgroupedittoken": "You can find this long string of characters in the\nconfirmation email you received when you created the event group.",
+ "event.p.maxattendees": "Attendee limit",
+ "event.p.delthis": "Delete this event",
+ "newevent.importevent": "Import an existing event",
+ "selectfile": "Select file",
+ "youremail": "Your email",
+ "emaildesc": "We will send your secret editing link to this email address.",
+ "eventpwd": "Event password",
+ "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.",
+ "changefile": "Change file",
+ "group.p.eventgroupname": "Event group name",
+ "create": "Create",
+ "right": "Get it right!",
+ "snappy": "Make it snappy.",
+ "group.p.isshowgroup": "Will be shown on the event group page (optional).",
+ "event.locationdesc": "Be specific.",
+ "group.p.eventgroupurldesc": "For tickets or another event page (optional).",
+ "event.enternum": "Enter a number.",
+ "event.editlater": "You can always edit it later.",
+ "wontshow": "Will not be shown anywhere (optional).",
+ "sidebar.events": "View events",
+ "ml.requestml": "Request a link to create a new event",
+ "fixerrors": "Please fix these errors:",
+ "pastevents": "Past events",
+ "eventgroups": "Event groups",
+ "group.p.publicgroup": "Display this group on the public group list",
+ "saving": "Saving...",
+ "group.editmode": "Switch to editing mode",
+ "creating": "Creating...",
+ "newevent.visiblealert": "Events are visible to anyone who knows the link.",
+ "event.p.eventgrouplinker": "Choose a group you've edited before",
+ "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>",
+ "home.about": "About",
+ "home.intro": "Gathio is a simple, federated, privacy-first event hosting platform.",
+ "home.aboutgathio": "About Gathio",
+ "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.",
+ "home.privacytitle": "Privacy-first",
+ "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.",
+ "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!",
+ "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.",
+ "home.conftitle": "Configurable",
+ "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.",
+ "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!",
+ "home.fedtitle": "Federation and self-hosting",
+ "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.",
+ "home.opensource": "Open source",
+ "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.",
+ "home.kofi": "Support Me on Ko-fi",
+ "home.kofidesc": "If you find yourself using and enjoying <strong>gath<span class='text-muted'>io</span></strong>, consider buying me a coffee. It'll help keep the site running! <i class=\"far fa-heart\"></i>",
+ "404.notfound": "Event not found!",
+ "404.desc": "It may have never existed, or if it finished more than some days, 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.",
+ "event.hostedby": "Hosted by</span> {{eventData.hostName}}",
+ "event.ICSexport": "Export as ICS",
+ "event.addtoGC": "Add to Google Calendar",
+ "event.showonGM": "Show on Google Maps",
+ "event.showonOM": "Show on OpenStreetMap",
+ "event.concludeddel": " This event has concluded. It can no longer be edited{{#if eventWillBeDeleted}}, and will be automatically deleted {{daysUntilDeletion}}{{/if}}.",
+ "event.welcome": "Welcome to your event!",
+ "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!",
+ "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!",
+ "event.attendees": "Attendees",
+ "event.addme": "Add me",
+ "event.removeme": "Remove me",
+ "event.capacity": "This event is at capacity.",
+ "event.remaining": "{{spotsRemaining}} {{plural spotsRemaining \"spot(s)\"}} remaining - add yourself now!",
+ "event.removeuser": "Remove user from event",
+ "event.noattendees": "No attendees yet!",
+ "event.addself": "Add yourself to '{{eventData.name}}'",
+ "event.attendeename": "Your name",
+ "event.comment": "Comment",
+ "event.reply": "Reply",
+ "event.replycontent": "What would you like to reply?",
+ "event.edit": "Edit event",
+ "event.delconfirm": "Are you sure you want to delete this event? This action cannot be undone.",
+ "event.del": "Delete event",
+ "event.numlimit": "Please enter a number between 1 and ${response.data.freeSpots}",
+ "event.attendeenamedesc": "Or an alias, perhaps...",
+ "event.commentcontent": "What would you like to say?",
+ "group.editpswd": "Event group editing password",
+ "group.editpswddesc": "Event group secret editing code",
+ "group.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!",
+ "event.removeAttendee": "Remove attendee",
+ "event.p.eventname": "Event name",
+ "event.p.eventlocation": "Location",
+ "event.p.eventstart": "Starts",
+ "event.p.eventend": "Ends",
+ "event.p.timezone": "Timezone",
+ "event.p.eventdescription": "Description",
+ "event.p.eventurl": "Link",
+ "event.p.eventurldesc": "For tickets or another event page (optional).",
+ "event.p.groupbutton": "Enter group details manually",
+ "event.p.hostname": "Host name",
+ "event.p.hostnamedesc": "Will be shown on the event page (optional).",
+ "event.p.creatoremail": "Your email",
+ "event.p.publicevent": "Display this event on the public event list",
+ "event.p.eventgroup": "This event is part of an event group",
+ "event.p.maxattendeestitle": "Set a limit on the maximum number of attendees",
+ "group.subscribetitle": "Subscribe to updates",
+ "ml.requestmlbutton": "Request magic link",
+ "ml.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.",
+ "newevent.newgroup": "Create an event group",
+ "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.",
+ "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.",
+ "group.p.creatoremail": "Your email",
+ "publiclist.events": "Events",
+ "publiclist.groups": "Groups",
+ "publiclist.nogroups": "No groups!",
+ "publiclist.numoevents": "{{this.numberOfEvents}} {{plural this.numberOfEvents \"event(s)\"}}",
+ "event.p.edit": "Edit '{{eventData.name}}'",
+ "newevent.p.import": "Import",
+ "newevent.p.importing": "Importing...",
+ "newevent.p.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.",
+ "instancesettings": "Instance Settings",
+ "sidebar.createevent": "Create an event"
+}
diff --git a/locales/ja.json b/locales/ja.json
new file mode 100644
index 0000000..e72e39d
--- /dev/null
+++ b/locales/ja.json
@@ -0,0 +1,178 @@
+{
+ "copy": "コピー",
+ "about": "説明",
+ "joinemaildesc": "メールアドレスを入力しておくと、このイベントについての情報を送信します。",
+ "close": "閉じる",
+ "del": "削除",
+ "copied": "コピーしました !",
+ "incorrectpwd": "編集パスワードが一致しません。もう一度やり直してください。",
+ "noevents": "イベントの予定はありません!",
+ "event.options": "設定",
+ "save": "変更を保存",
+ "mdsupport": "<a href='https://commonmark.org/help/'>Markdown</a> 書式対応",
+ "choosefile": "ファイルを選択",
+ "selectfile": "ファイルを選択",
+ "emaildesc": "メールアドレスを入力すると、秘密の編集パスワードを送信します。また、イベント情報の更新があった際にはお知らせします。",
+ "creatoremaildesc": "メールアドレスを入力すると、秘密の編集パスワードを送信します。またイベントについての更新情報も送信します。",
+ "changefile": "ファイルを変更",
+ "create": "作成",
+ "right": "了解!",
+ "snappy": "ズバッと。",
+ "event.removemyself": "自分を削除",
+ "event.lostpswd": "パスワードをなくしましたか ? イベントの主催者に連絡を。",
+ "event.discussion": "メッセージ",
+ "event.postbutton": "送信",
+ "event.started": "開始済み",
+ "event.ended": "終了済み",
+ "event.partof": "<a href='/group/{{eventData.eventGroup.id}}'>{{eventData.eventGroup.name}}</a> グループのイベント",
+ "event.attendeenum": "何人で参加しますか?",
+ "event.p.delthis": "このイベントを削除",
+ "group.feedlinkdesc": "この URL をあなたのカレンダーアプリに登録することで、このグループのイベントの情報を確認することができます",
+ "group.subscribedesc": "メールアドレスを入力してください ( 任意 )。このグループに新しいイベントが追加されたらお知らせします。",
+ "group.subscribebutton": "登録",
+ "group.p.eventgroupname": "グループ名",
+ "group.p.eventgroupdescription": "説明",
+ "group.p.eventgroupurl": "リンク",
+ "group.p.hostname": "主催者またはグループ名",
+ "recommendeddimensions": "推奨画面サイズ ( 幅✕高さ ): 920 ✕ 300 ピクセル",
+ "group.p.del": "このグループを削除",
+ "event.p.eventgroupid": "この短い文字列は、イベントグループのリンク、確認メール、イベントグループのページに記載しています。",
+ "group.p.eventgroupedittoken": "グループの秘密の編集コード",
+ "event.p.eventgroupedittoken": "この長い文字列は、グループを作成したときに送信する確認メールにも記載しています。",
+ "event.p.maxattendees": "定員",
+ "newevent.groupdesc": "イベントグループは、リンクしたイベントをまとめてるエリアです。例えば、シリーズものの映画上映会、いくつかのイベントに分かれるフェス、音楽バンドのツアーなどで便利な機能です。個々のイベントへの公開リンクのように、グループの公開リンクを共有することができます。また秘密の編集パスワード(グループ作成時にメール送信)を知っているメンバーは、今後のイベントをグループに追加することができます。",
+ "event.locationdesc": "具体的に。",
+ "event.enternum": "数字を入力してください。",
+ "ml.requestmldesc": "この Gathio インスタンスの管理者は、イベントの作成権限を特定のメールアドレスに限定しています。あなたのメールアドレスでの作成が許可されていれば、マジックリンクがメールで届くはずです。許可されていなければ、メールは届きません。",
+ "fixerrors": "エラーを修正してください :",
+ "pastevents": "過去のイベント",
+ "eventgroups": "イベントグループ",
+ "publiclist.nogroups": "グループがありません!",
+ "upcomingevents": "今後のイベント",
+ "options.showlistattendees": "参加者リストを表示",
+ "group.p.publicgroup": "公開グループとして表示する",
+ "saving": "保存中...",
+ "event.p.eventgrouplinker": "登録してあるイベントグループを選択",
+ "event.p.groupbutton": "グループの詳細を入力してください",
+ "event.addmyself": "参加",
+ "newevent.neweventbutton": "新しいイベントを作成",
+ "group.p.delbutton": "イベントグループを削除",
+ "event.removepswd": "取消パスワード",
+ "event.removepswddesc": "参加を取消・辞退する際、このパスワードが使います。メールアドレスを入力すればメールにも送信します。この画面を閉じると<strong>二度と表示されません</strong>",
+ "group.edit": "グループ編集",
+ "event.editlater": "後でいつでも編集できます。",
+ "edittoken": "編集パスワードを入力します",
+ "edittokendesc": "編集パスワードを入力します。イベント作成時に表示したもので、アドレスを入力していればメールでも送信しています。",
+ "emailaddr": "メールアドレス",
+ "group.p.eventgroupid": "イベントグループ ID",
+ "eventpwd": "イベントパスワード",
+ "group.addevent": "このグループにイベントをリンクするには、新しくイベントを作る際、もしくは既存のイベントを編集して、以下の 2 つのコードをコピー & 貼りつけします。",
+ "forgotpwd": "パスワードがわからない?",
+ "newevent.groupattention": "イベントグループは、イベントのように自動削除されることはありません。しかし、{{siteName}} から削除されたイベントは",
+ "group.del": "このイベントグループを削除",
+ "group.delconfirm": "このイベントグループを削除します。よろしいですか? この操作は取り消しできません。",
+ "group.deldesc": "この操作では、このグループの個々のイベントは<strong>削除しません</strong>。それらのイベントは後で別のグループにリンクすることもできます。",
+ "group.subscribe": "'{{eventGroupData.name}}' のイベント情報を購読",
+ "imgdel": "画像を削除",
+ "group.p.eventgroupurldesc": "ほかのイベントのページや外部のチケット購入等のページ ( 任意 )",
+ "home.attention": "ご注意 : すべてのイベントはリンクを知ればだれでも見ることができます。そのため Gathio をサプライズの誕生日パーティーや秘密の革命事業には使わない方がいいでしょうが、どうぞご自由に。",
+ "interaction": "ユーザーにコメントを許可する",
+ "event.confremoveattendee": "この参加者をイベントから削除します。よろしいですか? この操作は取り消しできません。",
+ "coverimg": "カバー画像",
+ "newevent.importevent": "既存のイベントをインポート",
+ "group.p.isshowgroup": "イベントグループのページに表示します ( 任意 )。",
+ "event.attendeeemail": "あなたのメールアドレス ( 任意 )",
+ "event.p.eventgroupdata": "このイベントをイベントグループにリンク",
+ "youremail": "あなたのメールアドレス",
+ "newevent.pagetitle": "さあ、何をはじめますか?",
+ "newevent.neweventgroup": "新しいイベントグループを作成",
+ "event.nospam": "SPAM は送りませんよ",
+ "home.privacy": "Gathio には基本的にアカウントはありません。イベントを作成した時点で、後に編集するためのパスワードを生成しお知らせします。参加を呼びかける相手には、参加用リンクを伝えるだけ。共同主催者には、秘密のパスワード込みの編集用リンクを伝えます。",
+ "event.removeattendeedesc": "'{{eventData.name}}' から参加者を削除",
+ "join": "ユーザーは自分で参加登録する",
+ "event.removemyselfdesc": "'{{eventData.name}}' から自分を削除する",
+ "ml.requestml": "新しいイベントを作成するリンクを申請",
+ "sidebar.events": "イベント表示",
+ "wontshow": "どこにも表示しません ( 任意 )。",
+ "group.editmode": "編集モードへ",
+ "creating": "作成中...",
+ "instancesettings": "インスタンス設定",
+ "newevent.visiblealert": "イベントは、リンクを知ればだれでも見ることができます。",
+ "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> を込めて作成しています。 ",
+ "home.about": "紹介、",
+ "home.intro": "Gathio は、簡単、プライバシー・ファーストでイベントを作成・共有するシステムです。連合プロトコルにも対応しています。",
+ "home.aboutgathio": "Gathio は…",
+ "home.imgexample": "ピクニックに行くイベントページの例です。イベントの場所、主催、日時と説明を記載しています。また Google カレンダーに保存、エクスポート、場所を OpenStreetMap と Google マップから開くこともできます。",
+ "home.privacytitle": "プライバシーファースト",
+ "home.privdesc": "Gathio にはアカウントはありません。イベントを作成した時点で、後に編集するためのパスワードを生成しお知らせします。参加を呼びかける相手には、参加用リンクを伝えるだけ。共同主催者には、秘密のパスワード込みの編集用リンクを伝えます。",
+ "home.privmail": "メールアドレスを入力すると編集パスワードを受信できます。けっしてなくさないように――それでもメールアドレスの入力は任意です!",
+ "home.autodelete": "このインスタンスサーバーに設定されていれば、イベント終了後の一定のタイミングでイベントを自動的に削除します。関連するあなたについてのデータもいっしょに、データベースから完全に削除します。",
+ "home.conftitle": "設定可能",
+ "home.flagshipsetting": "<a href = \"https://gath.io\">Gathio のフラッグシップインスタンスは gath.io </a>です。だれでも、イベントは終わり次第順次削除される、URL を知っている人にしか開けないイベントを作成できるよう設計しています。\nもう一度言います、ここではだれでもイベントを作成できます。イベントはパブリックな場所に公開されることはありません。そしてイベント終了の 7 日後に削除します。",
+ "home.onpre": "あなたのコミュニティーで自分たちの Gathio インスタンスを建てるなら、そこではイベントの作成を特定の人しかできないようにしたり、ホームページに便利なイベント一覧を表示したり、イベント削除を一切しない…といった制限・設定を加えることもできます。",
+ "home.fedtitle": "連合プロトコルとセルフホスト",
+ "home.selfhost": "Gathio は簡単にセルフホストでき、Mastodon、Pleroma、Friendica などの ActivityPub サービスをサポートしているので、Fediverse のどこからでもイベントにアクセスできます。\nコミュニティーのために自前のインスタンスを建てることを応援します。 \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>を参考にしてください。",
+ "home.opensource": "オープンソース",
+ "home.osdesc": "Gathio はオープンソースであることを喜んでおり、素敵な人びとのグループによって作成しています。 \nどんな問題でも<a href = \"https://github.com/lowercasename/gathio/issues\">トラッカー</a>で質問してください。",
+ "home.kofi": "Ko-fi で支援を",
+ "home.kofidesc": "<strong>gath<span class='text-muted'>io</span></strong> 、よく使ってるし便利だよね…と思ったら、コーヒー 1 杯分を出してくれませんか? かならずサイトの運営に役立てます! <i class=\"far fa-heart\"></i>",
+ "404.notfound": "イベントが見つかりません !",
+ "404.desc": "もともと存在しなかったか、終了して一定期間を経過したのなら、サーバーから削除されたかのいずれかです。がっかりしないで――あなたがイベントを企画してみたら? あなたのオカリナ・リサイタルにぜひ行きたい、と思う人は私のほかにもきっといますよ。",
+ "event.hostedby": "主催 : </span> {{eventData.hostName}}",
+ "event.ICSexport": "ICS ファイル出力",
+ "event.addtoGC": "Google カレンダーに登録",
+ "event.showonGM": "Google マップで表示",
+ "event.showonOM": "OpenStreetMap で表示",
+ "event.concludeddel": "このイベントは終了しました。すでに編集できません。また自動的に削除します<span class='daysToDeletion'></span>.",
+ "event.welcome": "あなたが作成したイベントです。ようこそ!",
+ "event.attention": "このイベントを編集するための秘密のパスワードはこちら : <strong>{{eventData.editToken}}</strong> 。このパスワードは Web ブラウザに記憶しています。またアドレスを入力していたら、メールでも送信しています。アドレスを入力しなかったのなら、このパスワードを<strong>どこか安全な場所に記録を</strong>。再表示はできません !",
+ "event.share": "イベントを共有するには、このメッセージ上部の参加用リンクをご利用ください。参加者はイベントを編集・削除することはできません!",
+ "event.attendees": "参加者",
+ "event.addme": "参加",
+ "event.removeme": "取消 ( 辞退 )",
+ "event.capacity": "このイベントは満員です。",
+ "event.remaining": "残り {{spotsRemaining}} 枠 - 参加登録しましょう !",
+ "event.removeuser": "このユーザーをイベント参加者から削除",
+ "event.noattendees": "まだ参加者がいません !",
+ "event.addself": "'{{eventData.name}}' に自分も参加",
+ "event.attendeename": "お名前",
+ "event.comment": "コメント",
+ "event.reply": "返信",
+ "event.replycontent": "返信の内容は?",
+ "event.edit": "イベント編集",
+ "event.delconfirm": "このイベントを削除します。よろしいですか? この操作は取り消しできません。",
+ "event.del": "イベント削除",
+ "event.numlimit": "1 〜 ${response.data.freeSpots} で人数を入力してください。",
+ "event.p.eventname": "イベント名",
+ "event.p.eventlocation": "場所",
+ "event.p.eventstart": "開始",
+ "event.p.eventend": "終了",
+ "event.p.timezone": "タイムゾーン",
+ "event.p.eventdescription": "説明",
+ "event.p.eventurl": "リンク",
+ "event.p.eventurldesc": "ほかのイベントのページや外部のチケット購入等のページ ( 任意 )",
+ "event.p.hostname": "主催者名",
+ "event.p.hostnamedesc": "イベントのページに表示します ( 任意 )。",
+ "event.p.creatoremail": "あなたのメールアドレス",
+ "event.p.publicevent": "このイベントを公開イベントリストに表示",
+ "event.p.eventgroup": "イベントグループにリンクする",
+ "event.p.maxattendeestitle": "定員を設定します",
+ "ml.requestmlbutton": "マジックリンクをリクエスト",
+ "group.p.creatoremail": "あなたのメールアドレス",
+ "publiclist.events": "イベント",
+ "publiclist.groups": "グループ",
+ "publiclist.numoevents": "{{this.numberOfEvents}} 件のイベント",
+ "event.p.edit": "'{{eventData.name}}' を編集",
+ "newevent.p.import": "インポート",
+ "newevent.p.importing": "インポート中...",
+ "newevent.p.importdesc": ".ics ファイルをアップロードすると、簡単にイベントを作成できます。Facebook のイベントは、コンテキストメニューをクリックし、「カレンダーに追加」を選ぶことでファイルをダウンロードできます。",
+ "newevent.newgroup": "グループを作成",
+ "event.attendeenamedesc": "またはニックネームなどなど...",
+ "event.commentcontent": "コメントをどうぞ",
+ "group.editpswd": "イベントグループ編集パスワード",
+ "group.editpswddesc": "イベントグループの秘密の編集コード",
+ "group.welcome": "あなたが作ったグループです。ようこそ! すでに秘密の編集用リンクをメールで送信しています。(現在、アドレスバーの上にも表示しています)。メールが届いていませんか ? SPAM フォルダーや迷惑メールフォルダーもご確認を。イベントグループを共有するには、このメッセージの下に表示しているリンクをご利用ください。参加者がこのリンクからグループにアクセスしても、あなたが作ったこのグループを編集したり削除したりすることはできません !",
+ "event.removeAttendee": "参加者を削除",
+ "group.subscribetitle": "更新通知の登録",
+ "sidebar.createevent": "イベントを作成"
+}
diff --git a/package.json b/package.json
index 26d1d00..8efcfcb 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "gathio",
"version": "1.5.0",
- "description": "A simple, federated, privacy-first event hosting platform",
+ "group.p.eventgroupdescription": "A simple, federated, privacy-first event hosting platform",
"main": "index.js",
"type": "module",
"scripts": {
@@ -30,6 +30,13 @@
"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",
@@ -43,6 +50,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",
@@ -52,6 +60,7 @@
"devDependencies": {
"@types/dompurify": "^3.0.5",
"@types/express": "^4.17.21",
+ "@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 5139a4a..4792565 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -44,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
@@ -83,6 +104,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
@@ -105,6 +129,9 @@ importers:
'@types/express':
specifier: ^4.17.21
version: 4.17.21
+ '@types/i18next-fs-backend':
+ specifier: ^1.2.0
+ version: 1.2.0
'@types/ical':
specifier: ^0.8.3
version: 0.8.3
@@ -139,6 +166,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'}
@@ -417,6 +448,10 @@ packages:
'@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==}
@@ -771,6 +806,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'}
@@ -1167,6 +1205,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'}
@@ -1208,6 +1252,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==}
@@ -1235,6 +1282,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'}
@@ -1601,6 +1668,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==}
@@ -1847,6 +1923,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==}
@@ -2119,6 +2212,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'}
@@ -2227,6 +2323,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'}
@@ -2236,6 +2336,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'}
@@ -2252,6 +2355,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'}
@@ -2325,6 +2431,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
@@ -2717,6 +2827,10 @@ snapshots:
'@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
@@ -3071,6 +3185,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
@@ -3592,6 +3712,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
@@ -3628,6 +3753,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
@@ -3672,6 +3801,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
@@ -4045,6 +4194,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
@@ -4251,6 +4404,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
@@ -4559,6 +4721,8 @@ snapshots:
universalify: 0.2.0
url-parse: 1.5.10
+ tr46@0.0.3: {}
+
tr46@4.1.1:
dependencies:
punycode: 2.3.1
@@ -4640,6 +4804,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
@@ -4654,6 +4820,8 @@ snapshots:
transitivePeerDependencies:
- debug
+ webidl-conversions@3.0.1: {}
+
webidl-conversions@7.0.0: {}
whatwg-encoding@2.0.0:
@@ -4667,6 +4835,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 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
diff --git a/tsconfig.json b/tsconfig.json
index fef389a..347572c 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..8b821c3 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 "404.notfound"}}</h1>
+ <p>{{t "404.desc"}}</p>
</main>
diff --git a/views/createEventMagicLink.handlebars b/views/createEventMagicLink.handlebars
index d0a0a49..59017eb 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 "ml.requestml" }}</h2>
<form
action="/magic-link/event/create"
@@ -8,7 +8,7 @@
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 "ml.requestmldesc" }}
</p>
<p>
If you run into any issues, please contact the instance administrator.
@@ -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 "emailaddr" }}</label>
+ <input type="email" class="form-control" id="email" placeholder="{{t "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 "ml.requestmlbutton" }}</button>
</div>
</form>
</main>
diff --git a/views/event.handlebars b/views/event.handlebars
index 1b1022e..e78ce88 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 "eventedit"}}</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 "event.started"}} {{else}}{{t "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 "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 "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 "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 "copy" }}
</button>
</li>
{{/if}}
@@ -94,16 +94,16 @@
<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 "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 "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 "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 "event.showonOM" }}
</a>
</div>
@@ -115,21 +115,20 @@
{{#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 "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 "event.welcome" }}</p>
+ <p>{{t "event.attention" }}</p>
+ <p>{{t "event.share" }}</p>
</div>
{{/if}}
<div class="card mb-4" id="eventDescription">
- <h5 class="card-header">About</h5>
+ <h5 class="card-header">{{t "about" }}</h5>
<div class="card-body p-summary">
{{{parsedDescription}}}
</div>
@@ -137,30 +136,30 @@
{{#if eventData.usersCanAttend}}
<div class="card mb-4" id="eventAttendees">
- <h5 class="card-header">Attendees {{#if numberOfAttendees}}({{numberOfAttendees}}){{/if}}
+ <h5 class="card-header">{{t "event.attendees" }} {{#if numberOfAttendees}}({{numberOfAttendees}}){{/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 "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 "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 "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">{{t "event.remaining" }}</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 "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}} (hidden from public list)</span>{{#if ../editingEnabled}} <a href="#" class="remove-attendee" data-toggle="modal" data-target="#removeAttendeeModal" title="{{t "event.removeuser" }}"><i class="fas fa-user-times"></i></a>{{/if}}</li>
{{/each}}
{{/if}}
</ul>
@@ -170,7 +169,7 @@
{{/if}}
{{/unless}}
{{else}}
- <p class="text-center text-muted mb-0">No attendees yet!</p>
+ <p class="text-center text-muted mb-0">{{t "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 "event.addself" }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
@@ -187,22 +186,22 @@
<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 "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 "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 "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 "event.attendeeemail" }}</label>
+ <p class="form-text small">{{t "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 "event.nospam" }} <3" data-validation="email" data-validation-optional="true">
</div>
</div>
<div class="form-check">
@@ -213,15 +212,15 @@
<p class="form-text small">If you choose to hide your name, only the event organiser will be able to see it.</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 "event.removepswd" }}</label>
+ <p class="form-text small">{{t "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 "close" }}</button>
+ <button type="submit" class="button button--primary">{{t "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 "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 "event.removepswd" }}</label>
+ <p class="form-text small">{{t "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 "close" }}</button>
+ <button type="submit" class="button button--primary">{{t "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 "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 "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 "close" }}</button>
+ <button type="submit" class="button button--danger">{{t "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 "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 "event.attendeename" }}</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 "event.attendeename" }}" required>
</div>
- <label for="commentContent">Comment</label>
+ <label for="commentContent">{{t "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "close" }}</button>
+ <button type="submit" class="button button--primary">{{t "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 "del" }} '{{eventData.name}}'</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 "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 "close" }}</button>
+ <button type="submit" class="button button--danger">{{t "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 "copied" }}');
+ setTimeout(function(){ $("#copyEventLink").html('<i class="fas fa-copy"></i> {{t "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 "copied" }}');
+ setTimeout(function(){ $("#copyAPLink").html('<i class="fas fa-copy"></i> {{t "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 "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 "incorrectpwd" }}').show();
}
});
});
diff --git a/views/eventgroup.handlebars b/views/eventgroup.handlebars
index 3151aea..73e84ad 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 "group.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 "group.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 "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 "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 "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 "group.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 "group.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 "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 "group.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 "group.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 "group.p.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 "group.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 "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 "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 "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 "delete" }} '{{eventGroupData.name}}'</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 "group.delconfirm" }}</p>
+ <p>{{t "group.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 "close" }}</button>
+ <button type="submit" class="button button--danger">{{t "group.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 "group.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 "group.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 "close" }}</button>
+ <button type="submit" class="button button--primary">{{t "group.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 "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 "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 "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 "close" }}</button>
+ <button type="submit" class="button button--primary">{{t "group.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 "copied" }}!');
+ setTimeout(function(){ $("#copyEventLink").html('<i class="fas fa-copy"></i> {{t "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 "copied" }}');
+ setTimeout(function(){ $("#copyFeedLink").html('<i class="fas fa-copy"></i> {{t "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 "incorrectpwd" }}').show();
}
});
});
diff --git a/views/home.handlebars b/views/home.handlebars
index 700d875..eb40646 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 "home.about" }}} {{siteName}}</h2>
{{#if instanceDescription}}
<div class="instance-description mb-4">
@@ -9,63 +9,49 @@
{{> 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 "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 "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 "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 "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 "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 "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 "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 "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 "home.attention" }} </p>
- <h3>Configurable</h3>
+ <h3>{{t "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 "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 "home.onpre" }}</p>
- <h3>Federation and self-hosting</h3>
+ <h3>{{t "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 "home.selfhost" }}}</p>
- <h3>Open source</h3>
+ <h3>{{t "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 "home.osdesc" }}}</p>
{{#if showKofi}}
<div class="card border-secondary mt-5 mb-3 mx-auto" style="min-width:300px;max-width:50%;">
<div class="card-body text-secondary">
- <p>If you find yourself using and enjoying Gathio, consider buying Raphael a coffee. It'll help keep the project
- and main site running! <i class="far fa-heart"></i></p>
+ <p>{{{t "home.kofidesc" }}}</p>
<script type='text/javascript' src='https://ko-fi.com/widgets/widget_2.js'></script>
<script
- type='text/javascript'>kofiwidget2.init('Support Me on Ko-fi', '#46b798', 'Q5Q2O7T5'); kofiwidget2.draw();</script>
+ type='text/javascript'>kofiwidget2.init('{{t "home.kofi" }}', '#46b798', 'Q5Q2O7T5'); kofiwidget2.draw();</script>
</div>
</div>
{{/if}}
diff --git a/views/layouts/main.handlebars b/views/layouts/main.handlebars
index e967019..c702a6d 100755
--- a/views/layouts/main.handlebars
+++ b/views/layouts/main.handlebars
@@ -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 "main.footnote" }}}
</p>
</footer>
</section>
diff --git a/views/newevent.handlebars b/views/newevent.handlebars
index d6d7024..1862ebe 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 "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 "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 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 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 "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 "newevent" }}</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 "creating" }}' : '{{t "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 "newevent.newgroup" }}</h4>
+ <p class="text-muted">{{t "newevent.groupdesc" }}</p>
+ <p class="text-muted">{{{t "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 "creating" }}' : '{{t "create" }}'"
></button>
</div>
</div>
diff --git a/views/optionsform.handlebars b/views/optionsform.handlebars
index 85ebd9f..06dbe95 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 "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 "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 "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 "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 "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 "interaction" }}
</label>
</div>
</div>
diff --git a/views/partials/editeventgroupmodal.handlebars b/views/partials/editeventgroupmodal.handlebars
index 046d15e..fb3033d 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 "group.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 "group.p.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 "group.p.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 "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 "saving" }}' : '{{t "save" }}'"
></button>
</div>
</div>
diff --git a/views/partials/editeventmodal.handlebars b/views/partials/editeventmodal.handlebars
index 986da9c..6deac7f 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 "event.p.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 "event.p.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 "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 "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 "saving" }}' : '{{t "save" }}'"
></button>
</div>
</div>
diff --git a/views/partials/eventForm.handlebars b/views/partials/eventForm.handlebars
index 93e8e84..91e5a7c 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 "event.p.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 "snappy" }}" x-model="data.eventName" >
</div>
</div>
<div class="form-group">
- <label for="eventLocation" >Location</label>
+ <label for="eventLocation" >{{t "event.p.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 "event.locationdesc" }}" x-model="data.eventLocation">
</div>
</div>
<div class="form-group">
- <label for="eventStart" >Starts</label>
+ <label for="eventStart" >{{t "event.p.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 "event.p.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 "event.p.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 "event.p.description" }}</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 "event.editlater" }}" x-model="data.eventDescription" ></textarea>
+ <small class="form-text">{{{t "mdsupport" }}}</small>
</div>
</div>
<div class="form-group">
- <label for="eventURL">Link</label>
+ <label for="eventURL">{{t "event.p.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 "event.p.eventurldesc" }}</small>
</div>
</div>
<div class="form-group">
- <label for="eventImage" >Cover image</label>
+ <label for="eventImage" >{{t "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 "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 "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 "imgdel" }}</button>
</div>
{{/if}}
</div>
</div>
<div class="form-group">
- <label for="hostName" >Host name</label>
+ <label for="hostName" >{{t "event.p.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 "event.p.hostnamedesc" }}" x-model="data.hostName" >
</div>
</div>
<div class="form-group">
- <label for="creatorEmail" >Your email</label>
+ <label for="creatorEmail" >{{t "event.p.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 "wontshow" }}" x-model.fill="data.creatorEmail" {{#if creatorEmail}}value="{{creatorEmail}}" readonly{{/if}}>
+ <small class="form-text">{{t "creatoremaildesc" }}</small>
</div>
</div>
<div class="form-group">
- <label>Options</label>
+ <label>{{t "event.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 "event.p.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 "event.p.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 "event.p.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 "event.p.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 "event.p.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 "group.p.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 "event.p.eventgroupid" }}</small>
</div>
- <label for="eventGroupEditToken">Event group secret editing code</label>
+ <label for="eventGroupEditToken">{{t "group.p.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 "event.p.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 "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 "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 "event.p.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 "event.p.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 "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 "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..d5d7baa 100644
--- a/views/partials/eventGroupForm.handlebars
+++ b/views/partials/eventGroupForm.handlebars
@@ -1,36 +1,36 @@
<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 "group.p.eventgroupname" }}</label>
+ <input type="text" class="form-control" id="eventGroupName" name="eventGroupName" placeholder="{{t "snappy" }}" x-model="data.eventGroupName">
</div>
<div class="form-group">
- <label for="eventGroupDescription">Description</label>
+ <label for="eventGroupDescription">{{t "group.p.eventgroupdescription" }}</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 "mdsupport" }}}</small>
</div>
<div class="form-group">
- <label for="eventGroupURL">Link</label>
+ <label for="eventGroupURL">{{t "group.p.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 "group.p.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"group.p.hostname" }}</label>
+ <input type="text" class="form-control" id="eventGroupHostName" name="hostName" placeholder="{{t "group.p.isshowgroup" }}" x-model="data.hostName">
</div>
<div class="form-group">
- <label for="creatorEmail">Your email</label>
+ <label for="creatorEmail">{{t "group.p.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 "wontshow" }}" x-model.fill="data.creatorEmail" {{#if creatorEmail}}value="{{creatorEmail}}" readonly{{/if}}>
+ <small class="form-text">{{t "creatoremaildesc" }}</small>
</div>
</div>
<div class="form-group">
- <label>Cover image</label>
+ <label>{{t "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 "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 "recommendeddimensions" }}</small>
</div>
{{#if showPublicEventList}}
<div class="form-group">
@@ -38,7 +38,7 @@
<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 "group.p.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 "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..c36c428 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 "noevents" }}</div>
{{/if}}
</div>
diff --git a/views/partials/importeventform.handlebars b/views/partials/importeventform.handlebars
index a8c0f0e..e45f16f 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 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 "newevent.p.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 "selectfile" }}
</label>
</div>
</div>
<div class="form-group">
- <label for="creatorEmail" class="form-label">Your email</label>
+ <label for="creatorEmail" class="form-label">{{t "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 "wontshow" }}" x-model.fill="data.creatorEmail" {{#if creatorEmail}}value="{{creatorEmail}}" readonly{{/if}}>
+ <small class="form-text">{{t "creatoremaildesc" }}</small>
</div>
</div>
<div class="form-group">
@@ -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 "newevent.p.importing" }}' : '{{t "newevent.p.import" }}'"
></button>
</form>
diff --git a/views/partials/instanceRules.handlebars b/views/partials/instanceRules.handlebars
index c7fa9be..c0a6c6c 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 "instancesettings" }}</h6>
</div>
<ul class="list-group list-group-flush">
diff --git a/views/partials/sidebar.handlebars b/views/partials/sidebar.handlebars
index c1184be..942f1ff 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 "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 "sidebar.events" }}</a></li>
+ <li><a href="/about">{{t "about" }}</a></li>
{{/if}}
</ul>
</div>
diff --git a/views/publicEventList.handlebars b/views/publicEventList.handlebars
index b8cacd0..5c81496 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 "publiclist.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 "publiclist.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 "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 "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 "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">{{{t "publiclist.numoevents" }}}</span>
</a>
{{/each}}
{{else}}
- <div class="list-group-item">No groups!</div>
+ <div class="list-group-item">{{t "publiclist.nogroups" }}</div>
{{/if}}
</div>