@tribe-nest/forge 3.29.0 → 3.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/package.json +11 -3
  2. package/src/_tests/publishedResolvability.spec.ts +184 -0
  3. package/src/_tests/specsRunWorkspaceSource.spec.ts +116 -0
  4. package/src/_tests/workspaceAliases.ts +40 -0
  5. package/src/contexts/CartContext.tsx +17 -1
  6. package/src/contexts/PublicAuthContext.tsx +34 -5
  7. package/src/contexts/_tests/CartContext.spec.tsx +36 -0
  8. package/src/contexts/_tests/PublicAuthRefetch.spec.tsx +147 -0
  9. package/src/data/queries/useBroadcasts.ts +151 -0
  10. package/src/data/queries/useMyBookings.ts +18 -1
  11. package/src/i18n/_tests/translationKeys.spec.ts +15 -0
  12. package/src/i18n/de.json +97 -0
  13. package/src/i18n/en.json +97 -0
  14. package/src/ui/format/_tests/membershipPwyw.spec.ts +185 -0
  15. package/src/ui/format/_tests/pwyw.spec.ts +65 -8
  16. package/src/ui/format/membershipPwyw.ts +164 -0
  17. package/src/ui/format/pwyw.ts +37 -0
  18. package/src/ui/headless/broadcast/_tests/broadcastState.spec.ts +235 -0
  19. package/src/ui/headless/broadcast/broadcastState.ts +158 -0
  20. package/src/ui/headless/broadcast/useBroadcastWatch.ts +174 -21
  21. package/src/ui/headless/event/useEventCheckout.ts +8 -13
  22. package/src/ui/headless/index.ts +14 -0
  23. package/src/ui/headless/membership/useMembershipCheckout.ts +160 -32
  24. package/src/ui/index.ts +61 -0
  25. package/src/ui/media/BookingCallScreen.tsx +33 -0
  26. package/src/ui/media/CallHelpHint.tsx +87 -0
  27. package/src/ui/media/CallStage.tsx +633 -0
  28. package/src/ui/media/_tests/CallStage.spec.tsx +931 -0
  29. package/src/ui/media/_tests/bookingSession.spec.tsx +227 -0
  30. package/src/ui/media/_tests/callState.spec.ts +499 -0
  31. package/src/ui/media/_tests/fakeNode.ts +178 -0
  32. package/src/ui/media/bookingSession.tsx +182 -0
  33. package/src/ui/media/bookingWindow.ts +81 -0
  34. package/src/ui/media/callState.ts +360 -0
  35. package/src/ui/media/index.ts +135 -0
  36. package/src/ui/styled/AccountDashboard.tsx +193 -4
  37. package/src/ui/styled/BroadcastWatch.tsx +107 -0
  38. package/src/ui/styled/ForgotPasswordForm.tsx +5 -0
  39. package/src/ui/styled/LiveBroadcastList.tsx +171 -0
  40. package/src/ui/styled/LoginForm.tsx +10 -0
  41. package/src/ui/styled/MembershipCheckout.tsx +318 -45
  42. package/src/ui/styled/MembershipTiers.tsx +10 -3
  43. package/src/ui/styled/ResetPasswordForm.tsx +5 -0
  44. package/src/ui/styled/SignupForm.tsx +5 -0
  45. package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +166 -0
  46. package/src/ui/styled/_tests/AccountDashboardCommunity.spec.tsx +134 -0
  47. package/src/ui/styled/_tests/BroadcastPassValidation.spec.tsx +125 -0
  48. package/src/ui/styled/_tests/MembershipCheckout.spec.tsx +364 -0
  49. package/src/ui/styled/broadcast/BroadcastPassValidation.tsx +187 -0
  50. package/src/ui/styled/broadcast/BroadcastPlayer.tsx +536 -0
  51. package/src/ui/styled/broadcast/BroadcastTicketPurchase.tsx +74 -0
  52. package/src/ui/styled/broadcast/EndedBroadcast.tsx +103 -0
  53. package/src/ui/styled/community/CommunityComposer.tsx +182 -3
  54. package/src/ui/styled/community/CommunityFeed.tsx +36 -51
  55. package/src/ui/styled/community/CommunityPostDetail.tsx +16 -2
  56. package/src/ui/styled/community/_tests/CommunityComposer.spec.tsx +281 -0
  57. package/src/ui/styled/community/_tests/CommunityPostDetail.spec.tsx +175 -0
  58. package/src/ui/styled/forge-utilities.css +832 -0
@@ -29,3 +29,154 @@ export function useValidateBroadcastPass() {
29
29
  },
30
30
  });
31
31
  }
32
+
33
+ /**
34
+ * ONE broadcast, by id.
35
+ *
36
+ * Not the same thing as picking the id out of `useLiveBroadcasts()`: the list is
37
+ * what is on now, and a broadcast that has ENDED still has to render its own
38
+ * page (the "broadcast ended, keep this tab open" screen). Reading the detail
39
+ * endpoint is what makes that page survive the end of the stream.
40
+ */
41
+ export function useLiveBroadcast(broadcastId?: string) {
42
+ const { client, profileId } = useForge();
43
+
44
+ return useQuery<ILiveBroadcast>({
45
+ queryKey: ["live-broadcast", profileId, broadcastId],
46
+ queryFn: async () => {
47
+ const res = await client.get(`/public/broadcasts/${broadcastId}`, {
48
+ params: { profileId },
49
+ });
50
+ return res.data;
51
+ },
52
+ enabled: !!profileId && !!client && !!broadcastId,
53
+ });
54
+ }
55
+
56
+ /**
57
+ * The same broadcast, re-read on an interval, purely to notice that it ended.
58
+ *
59
+ * A separate query from `useLiveBroadcast` on purpose. The page keeps rendering
60
+ * the broadcast it opened with, and only the END is allowed to come from the
61
+ * poll, so a mid-stream edit to the title or the thumbnail cannot swap the
62
+ * player out from under someone who is watching.
63
+ */
64
+ export function useLiveBroadcastPoll(broadcastId?: string, intervalMs = 5000) {
65
+ const { client, profileId } = useForge();
66
+
67
+ return useQuery<ILiveBroadcast>({
68
+ queryKey: ["live-broadcast-poll", profileId, broadcastId],
69
+ queryFn: async () => {
70
+ const res = await client.get(`/public/broadcasts/${broadcastId}`, {
71
+ params: { profileId },
72
+ });
73
+ return res.data;
74
+ },
75
+ enabled: !!profileId && !!client && !!broadcastId,
76
+ refetchInterval: intervalMs,
77
+ });
78
+ }
79
+
80
+ /** How many people are watching right now. Polled while the broadcast is on. */
81
+ export function useBroadcastAudience(broadcastId?: string, enabled = true, intervalMs = 3000) {
82
+ const { client, profileId } = useForge();
83
+
84
+ return useQuery<{ count: number }>({
85
+ queryKey: ["broadcast-audience", broadcastId],
86
+ queryFn: async () => {
87
+ const res = await client.get(`/public/broadcasts/${broadcastId}/audience`, {
88
+ params: { profileId },
89
+ });
90
+ return res.data;
91
+ },
92
+ enabled: !!profileId && !!client && !!broadcastId && enabled,
93
+ refetchInterval: intervalMs,
94
+ });
95
+ }
96
+
97
+ /** Which chat surface a broadcast comment arrived from. */
98
+ export const BroadcastChannelProvider = {
99
+ Youtube: "youtube",
100
+ Twitch: "twitch",
101
+ Website: "website",
102
+ CustomRTMP: "custom_rtmp",
103
+ } as const;
104
+
105
+ export type BroadcastChannelProviderValue =
106
+ (typeof BroadcastChannelProvider)[keyof typeof BroadcastChannelProvider];
107
+
108
+ /** One message in a broadcast's live chat. */
109
+ export type BroadcastComment = {
110
+ id: string;
111
+ name: string;
112
+ content: string;
113
+ publishedAt: string;
114
+ channelProvider: BroadcastChannelProviderValue;
115
+ };
116
+
117
+ /** The chat backlog a viewer sees when they join. Realtime arrives over the socket. */
118
+ export function useBroadcastComments(broadcastId?: string) {
119
+ const { client, profileId } = useForge();
120
+
121
+ return useQuery<BroadcastComment[]>({
122
+ queryKey: ["broadcast-comments", profileId, broadcastId],
123
+ queryFn: async () => {
124
+ const res = await client.get(`/public/broadcasts/${broadcastId}/comments`, {
125
+ params: { profileId },
126
+ });
127
+ return res.data;
128
+ },
129
+ enabled: !!profileId && !!client && !!broadcastId,
130
+ });
131
+ }
132
+
133
+ /** The offer/answer exchange that subscribes a viewer to a WebRTC broadcast. */
134
+ export type BroadcastSubscribeAnswer = {
135
+ requiresImmediateRenegotiation: boolean;
136
+ sessionId: string;
137
+ tracks: { trackName: string; mid: string; trackSid: string }[];
138
+ sessionDescription: { sdp: string; type: string };
139
+ };
140
+
141
+ /**
142
+ * The imperative half of watching a broadcast: the calls that are made in
143
+ * response to something happening (a pass validated, a heartbeat, a viewer
144
+ * closing the tab), not on a render.
145
+ *
146
+ * They live here rather than inline in a component so both rendering stacks make
147
+ * the SAME call to the same address. A session that is pinged on one stack and
148
+ * not the other reports a different audience count for the same room.
149
+ */
150
+ export function useBroadcastSessionApi() {
151
+ const { client } = useForge();
152
+
153
+ return {
154
+ /** Re-open a session held in storage from an earlier visit. */
155
+ validateSession: async (broadcastId: string, sessionId: string): Promise<IBroadcastPass> => {
156
+ const res = await client.post(`/public/broadcasts/${broadcastId}/validate-session`, { sessionId });
157
+ return res.data;
158
+ },
159
+ /** Heartbeat. Silence is what marks a viewer as gone. */
160
+ sessionPing: async (broadcastId: string, sessionId: string): Promise<void> => {
161
+ await client.post(`/public/broadcasts/${broadcastId}/session-ping`, { sessionId });
162
+ },
163
+ /** Leave deliberately, so the count drops now rather than on a timeout. */
164
+ leave: async (broadcastId: string, sessionId?: string): Promise<void> => {
165
+ await client.post(`/public/broadcasts/${broadcastId}/leave`, { sessionId });
166
+ },
167
+ /** Subscribe to the WebRTC tracks of a realtime broadcast. */
168
+ subscribe: async (payload: {
169
+ sdp?: string;
170
+ type?: string;
171
+ trackIds?: string[];
172
+ sessionId?: string;
173
+ }): Promise<BroadcastSubscribeAnswer> => {
174
+ const res = await client.post(`/public/broadcasts/subscribe`, payload);
175
+ return res.data;
176
+ },
177
+ /** Pin the viewer to one simulcast layer, or back to auto. */
178
+ switchQuality: async (payload: { track: Record<string, unknown>; sessionId?: string }): Promise<void> => {
179
+ await client.post(`/public/broadcasts/switch-quality`, payload);
180
+ },
181
+ };
182
+ }
@@ -93,6 +93,15 @@ export type MyBooking = {
93
93
  /** What became of the money: `refunded`, `voided_free`, `manual_refund_required`, … */
94
94
  cancellationOutcome: string | null;
95
95
  createdAt: string;
96
+ /**
97
+ * The call's address, or null when this session has no call.
98
+ *
99
+ * Present only for a CONFIRMED session on video, because that is when the
100
+ * room record is written. It is the id in `/video-calls/:callId`, opaque on
101
+ * purpose: it is not derived from the booking id, so a link in an email
102
+ * reveals nothing about the booking behind it.
103
+ */
104
+ videoCallId?: string | null;
96
105
  /** The slot the booking currently points at — what a reschedule moves. */
97
106
  coachingBookingSlotId: string;
98
107
  /**
@@ -148,7 +157,15 @@ export type MyBooking = {
148
157
  */
149
158
  export type BookingLocation =
150
159
  | { type: "in_person"; address: string }
151
- | { type: "online"; url: string };
160
+ | { type: "online"; url: string }
161
+ /**
162
+ * A call on the platform's own media network. No payload, and there never
163
+ * will be one: entry is a ticket minted per attempt and valid for minutes, so
164
+ * a URL here would be a forwardable key to somebody else's session. Draw the
165
+ * Join control from `bookingCallWindow` in `@tribe-nest/forge/media` and let
166
+ * `<BookingCallProvider>` fetch the ticket.
167
+ */
168
+ | { type: "video" };
152
169
 
153
170
  export type CancelBookingResult = {
154
171
  bookingId: string;
@@ -150,4 +150,19 @@ describe("Forge i18n: every key a component uses exists in every bundle", () =>
150
150
  }
151
151
  expect(mismatches, `Interpolation drift:\n${mismatches.join("\n")}`).toEqual([]);
152
152
  });
153
+
154
+ it("no bundle value uses i18next's double braces, which Forge's interpolator does not read", () => {
155
+ // `translateForge` substitutes `{name}`. On `{{date}}` the regex matches
156
+ // the INNER braces, so the visitor saw "{Aug 20, 2026}" with a stray pair
157
+ // of braces around the value. Two keys shipped that way (copied from the
158
+ // admin bundle, which is i18next). The convention is single braces, and
159
+ // this pins it for every value in every bundle.
160
+ const offenders: string[] = [];
161
+ for (const [lang, bundle] of Object.entries(LOCALES)) {
162
+ for (const [key, value] of Object.entries(bundle)) {
163
+ if (value.includes("{{") || value.includes("}}")) offenders.push(`${lang}: ${key}`);
164
+ }
165
+ }
166
+ expect(offenders, `Double-brace placeholders:\n${offenders.join("\n")}`).toEqual([]);
167
+ });
153
168
  });
package/src/i18n/de.json CHANGED
@@ -4,14 +4,25 @@
4
4
  "forge.account_dashboard.booking_cancel_error": "Wir konnten diese Session nicht stornieren. Bitte versuche es erneut.",
5
5
  "forge.account_dashboard.booking_duration": "{minutes} Minuten",
6
6
  "forge.account_dashboard.booking_join_label": "Teilnehmen:",
7
+ "forge.account_dashboard.booking_video_call": "Videoanruf auf dieser Website",
8
+ "forge.account_dashboard.booking_join_video": "Videoanruf beitreten",
9
+ "forge.account_dashboard.booking_video_opens_at": "Videoanruf: Der Raum öffnet {datetime}",
10
+ "forge.account_dashboard.booking_video_closed": "Videoanruf: Der Raum dieser Session ist geschlossen",
7
11
  "forge.account_dashboard.booking_where_label": "Wo:",
8
12
  "forge.account_dashboard.cancel": "Abbrechen",
13
+ "forge.account_dashboard.cancel_confirm_confirm": "Mitgliedschaft kündigen",
14
+ "forge.account_dashboard.cancel_confirm_free": "Ihre Vorteile enden sofort.",
15
+ "forge.account_dashboard.cancel_confirm_keep": "Mitgliedschaft behalten",
16
+ "forge.account_dashboard.cancel_confirm_paid": "Sie wird nicht verlängert, und Ihre Vorteile bleiben bis zum Ende des bezahlten Zeitraums bestehen.",
17
+ "forge.account_dashboard.cancel_confirm_paid_until": "Sie wird nicht verlängert, und Ihre Vorteile bleiben bis zum {date} bestehen.",
18
+ "forge.account_dashboard.cancel_confirm_title": "Mitgliedschaft kündigen?",
9
19
  "forge.account_dashboard.cancel_deletion": "Löschung abbrechen",
10
20
  "forge.account_dashboard.cancel_session": "Session stornieren",
11
21
  "forge.account_dashboard.cancel_ticket": "Ticket stornieren",
12
22
  "forge.account_dashboard.cancelled": "Storniert",
13
23
  "forge.account_dashboard.cancelling": "Wird storniert…",
14
24
  "forge.account_dashboard.close": "Schließen",
25
+ "forge.account_dashboard.community_spaces": "Community-Bereiche",
15
26
  "forge.account_dashboard.confirm_password": "Neues Passwort bestätigen",
16
27
  "forge.account_dashboard.current_password": "Aktuelles Passwort",
17
28
  "forge.account_dashboard.data_export_description": "Exportiere eine Kopie deiner persönlichen Daten.",
@@ -139,6 +150,28 @@
139
150
  "forge.blog_post.not_found": "Beitrag nicht gefunden.",
140
151
  "forge.blog_post.pause": "Pause",
141
152
  "forge.blog_post.play": "Abspielen",
153
+ "forge.broadcast_pass_validation.input_placeholder": "Gib deine Ticket-ID ein (TN-XXXXXXX)",
154
+ "forge.broadcast_pass_validation.join": "Übertragung beitreten",
155
+ "forge.broadcast_pass_validation.or": "ODER",
156
+ "forge.broadcast_pass_validation.prompt": "Gib deine Ticket-ID ein, um der Übertragung beizutreten (TN-XXXXXXX)",
157
+ "forge.broadcast_pass_validation.validating": "Wird geprüft…",
158
+ "forge.broadcast_player.anonymous": "Anonym",
159
+ "forge.broadcast_player.chat": "Chat",
160
+ "forge.broadcast_player.fullscreen": "Vollbild",
161
+ "forge.broadcast_player.message_label": "Nachricht",
162
+ "forge.broadcast_player.pinned": "Angepinnt",
163
+ "forge.broadcast_player.quality": "Qualität",
164
+ "forge.broadcast_player.quality_auto": "Automatisch",
165
+ "forge.broadcast_player.quality_high": "Hoch",
166
+ "forge.broadcast_player.quality_low": "Niedrig",
167
+ "forge.broadcast_player.quality_medium": "Mittel",
168
+ "forge.broadcast_player.send": "Senden",
169
+ "forge.broadcast_player.started": "Vor {duration} gestartet",
170
+ "forge.broadcast_player.watching_now": "{count} schauen gerade zu",
171
+ "forge.broadcast_ticket_purchase.buy_tickets": "Tickets kaufen",
172
+ "forge.broadcast_ticket_purchase.from_price": "Ab {price}",
173
+ "forge.broadcast_watch.leave": "Übertragung verlassen",
174
+ "forge.broadcast_watch.load_error": "Übertragung konnte nicht geladen werden",
142
175
  "forge.bundle_confirmation.continue_shopping": "Weiter einkaufen",
143
176
  "forge.bundle_confirmation.eyebrow": "Bestellung bestätigt",
144
177
  "forge.bundle_confirmation.failed_body": "Es wurde nichts abgebucht und dein Warenkorb ist noch da. Du kannst es erneut versuchen.",
@@ -154,6 +187,41 @@
154
187
  "forge.bundle_confirmation.tickets_emailed": "Tickets per E-Mail an dich unterwegs",
155
188
  "forge.bundle_confirmation.title": "Alles erledigt",
156
189
  "forge.bundle_confirmation.view_orders": "Bestellungen ansehen",
190
+ "forge.call_stage.status_ended": "Der Anruf ist beendet",
191
+ "forge.call_stage.status_connected": "Verbunden",
192
+ "forge.call_stage.status_connecting": "Verbindung zum Anruf wird hergestellt",
193
+ "forge.call_stage.status_left": "Du hast den Anruf verlassen",
194
+ "forge.call_stage.status_refused": "Beitritt zu diesem Anruf nicht möglich",
195
+ "forge.call_stage.status_moving": "Du wirst auf einen anderen Server verschoben",
196
+ "forge.call_stage.status_moving_detail": "Das dauert ein paar Sekunden, und niemand sonst wird getrennt.",
197
+ "forge.call_stage.status_move_failed": "Du konntest nicht auf einen anderen Server verschoben werden",
198
+ "forge.call_stage.status_move_failed_detail": "Niemand sonst wurde getrennt. Versuche es erneut, um wieder beizutreten.",
199
+ "forge.call_stage.status_reconnecting": "Verbindung unterbrochen, wird wiederhergestellt",
200
+ "forge.call_stage.status_reconnecting_detail": "Alle anderen sind weiterhin im Anruf.",
201
+ "forge.call_stage.status_lost": "Verbindung unterbrochen",
202
+ "forge.call_stage.status_lost_detail": "Alle anderen sind weiterhin im Anruf. Versuche es erneut, um wieder beizutreten.",
203
+ "forge.call_stage.retry": "Erneut versuchen",
204
+ "forge.call_stage.recording": "Dieser Anruf wird aufgezeichnet",
205
+ "forge.call_stage.head_count": "In diesem Anruf: {count}",
206
+ "forge.call_stage.help_hear_not_see_label": "Warum höre ich jemanden, sehe ihn aber nicht?",
207
+ "forge.call_stage.help_hear_not_see": "Eine Kachel mit Namen, aber ohne Bild bedeutet, dass diese Person ihre Kamera ausgeschaltet hat oder dass der Anruf so voll ist, dass nur die gerade sprechenden Personen in voller Größe übertragen werden. Beides ist normal, und der Ton ist davon nicht betroffen.",
208
+ "forge.call_stage.listening": "(hört zu)",
209
+ "forge.call_stage.you": "Du",
210
+ "forge.call_stage.you_not_connected": "Du (noch nicht verbunden)",
211
+ "forge.call_stage.mute": "Stummschalten",
212
+ "forge.call_stage.unmute": "Stummschaltung aufheben",
213
+ "forge.call_stage.camera_start": "Kamera starten",
214
+ "forge.call_stage.camera_stop": "Kamera stoppen",
215
+ "forge.call_stage.help_camera_stop_label": "Was macht „Kamera stoppen“?",
216
+ "forge.call_stage.help_camera_stop": "Dein Bild wird nicht mehr gesendet: Die anderen sehen stattdessen deinen Namen. Der Browser hält die Kamera geöffnet, damit das erneute Starten sofort geht und nicht ein zweites Mal um Erlaubnis fragt. Deshalb kann die Kameraleuchte anbleiben, bis du den Anruf verlässt.",
217
+ "forge.call_stage.screen_start": "Bildschirm teilen",
218
+ "forge.call_stage.screen_stop": "Teilen beenden",
219
+ "forge.call_stage.help_screen_label": "Was wird beim Teilen meines Bildschirms gesendet?",
220
+ "forge.call_stage.help_screen": "Was auch immer du im Browser-Dialog auswählst, live an alle im Anruf. Deine Kamera läuft dabei weiter, statt ersetzt zu werden, sodass dich die anderen weiterhin sehen können, während du präsentierst.",
221
+ "forge.call_stage.leave": "Verlassen",
222
+ "forge.call_stage.lost_microphone": "Dein Mikrofon wurde ausgeschaltet, als die Verbindung abbrach. Hebe die Stummschaltung auf, um wieder gehört zu werden.",
223
+ "forge.call_stage.lost_camera": "Deine Kamera wurde ausgeschaltet, als die Verbindung abbrach. Starte sie erneut, um wieder gesehen zu werden.",
224
+ "forge.call_stage.lost_screen": "Deine Bildschirmfreigabe wurde beendet, als die Verbindung abbrach. Teile ihn erneut, um fortzufahren.",
157
225
  "forge.cancellation_terms.title": "Stornobedingungen",
158
226
  "forge.cart.aria_close": "Warenkorb schließen",
159
227
  "forge.cart.aria_open": "Warenkorb öffnen",
@@ -519,6 +587,12 @@
519
587
  "forge.email_list_form.submitting": "Wird eingetragen…",
520
588
  "forge.email_list_form.success": "Danke für deine Anmeldung!",
521
589
  "forge.email_list_form.title": "Meinem E-Mail-Verteiler beitreten",
590
+ "forge.ended_broadcast.banner": "Übertragung beendet",
591
+ "forge.ended_broadcast.close_instruction": "Schließe die Übertragungsseite mit dem \"X\" oben links, dann landest du auf der Seite mit den Live-Events des Hosts.",
592
+ "forge.ended_broadcast.notice_title": "Wichtige Information",
593
+ "forge.ended_broadcast.refresh_instruction": "Du kannst diese Seite immer wieder neu laden, bis die Übertragung wieder läuft. Tritt ihr dann bei und du kannst das Event weiter ansehen. Du bekommst außerdem eine E-Mail, sobald die Übertragung wieder läuft.",
594
+ "forge.ended_broadcast.why_body": "Wenn das mitten im Event passiert ist, lag es sehr wahrscheinlich an einer instabilen Internetverbindung beim Host.",
595
+ "forge.ended_broadcast.why_label": "Warum wurde die Übertragung beendet?",
522
596
  "forge.event_confirmation.body_incomplete": "Deine Bestellung ist {status}. Falls dir etwas berechnet wurde, melde dich beim Support.",
523
597
  "forge.event_confirmation.body_paid": "Wir haben deine Tickets an {email} geschickt.",
524
598
  "forge.event_confirmation.explore": "Mehr Events ansehen",
@@ -708,6 +782,12 @@
708
782
  "forge.lead_magnet.invalid_link": "Ungültiger Link",
709
783
  "forge.lead_magnet.preparing": "Wird vorbereitet…",
710
784
  "forge.lead_magnet.thank_you": "Danke für deine Anmeldung!",
785
+ "forge.live_broadcast_list.empty_body": "Zurzeit sind keine Live-Übertragungen verfügbar.",
786
+ "forge.live_broadcast_list.empty_title": "Keine Live-Übertragungen",
787
+ "forge.live_broadcast_list.error_body": "Live-Übertragungen können gerade nicht geladen werden.",
788
+ "forge.live_broadcast_list.error_title": "Fehler beim Laden der Übertragungen",
789
+ "forge.live_broadcast_list.subtitle": "Live-Streams und Events ansehen",
790
+ "forge.live_broadcast_list.title": "Live-Übertragungen",
711
791
  "forge.loading.loading": "Wird geladen",
712
792
  "forge.login_form.back_to_login": "Zurück zur Anmeldung",
713
793
  "forge.login_form.code_sent_to": "Wir haben einen 6-stelligen Code geschickt an",
@@ -725,17 +805,33 @@
725
805
  "forge.login_form.verify_submit": "Bestätigen & anmelden",
726
806
  "forge.login_form.verify_title": "Bestätige deine E-Mail",
727
807
  "forge.login_form.verifying": "Wird geprüft…",
808
+ "forge.membership_checkout.amount_below_minimum": "Bitte {minimum} oder mehr eingeben.",
809
+ "forge.membership_checkout.amount_not_positive": "Bitte einen Betrag größer als null eingeben.",
728
810
  "forge.membership_checkout.back": "Zurück",
729
811
  "forge.membership_checkout.billing_cycle_label": "Abrechnung",
730
812
  "forge.membership_checkout.billing_monthly": "Monatlich",
813
+ "forge.membership_checkout.billing_monthly_price": "Monatlich ({amount})",
814
+ "forge.membership_checkout.billing_monthly_pwyw": "Zahle was du willst ab {amount}/Mon.",
731
815
  "forge.membership_checkout.billing_yearly": "Jährlich",
816
+ "forge.membership_checkout.billing_yearly_price": "Jährlich ({amount})",
817
+ "forge.membership_checkout.billing_yearly_pwyw": "Zahle was du willst ab {amount}/Jahr",
818
+ "forge.membership_checkout.choose_title": "Mitgliedschaft wählen",
819
+ "forge.membership_checkout.confirm_change": "Wechsel bestätigen",
820
+ "forge.membership_checkout.current_plan": "Aktueller Tarif",
821
+ "forge.membership_checkout.empty_title": "Keine Mitgliedschaften verfügbar",
822
+ "forge.membership_checkout.free": "Kostenlos",
823
+ "forge.membership_checkout.more_benefits": "+{count} weitere",
732
824
  "forge.membership_checkout.no_tier_body": "Wähle eine Mitgliedschaft, um fortzufahren.",
825
+ "forge.membership_checkout.on_this_plan": "Du hast diesen Tarif",
733
826
  "forge.membership_checkout.pay": "{amount} zahlen",
734
827
  "forge.membership_checkout.payment_summary": "{tier_name} · {amount} / {cycle}",
735
828
  "forge.membership_checkout.payment_title": "Zahlung",
736
829
  "forge.membership_checkout.per_month": "pro Monat",
737
830
  "forge.membership_checkout.per_year": "pro Jahr",
831
+ "forge.membership_checkout.pwyw_charged_as": "Ihnen werden {amount} berechnet.",
738
832
  "forge.membership_checkout.pwyw_label": "Wie viel möchtest du zahlen?",
833
+ "forge.membership_checkout.pwyw_min": "(mindestens {amount})",
834
+ "forge.membership_checkout.select_tier": "Diesen Tarif wählen",
739
835
  "forge.membership_checkout.submit": "Abonnieren",
740
836
  "forge.membership_checkout.submitting": "Wird verarbeitet…",
741
837
  "forge.membership_checkout.title": "{tier_name} beitreten",
@@ -746,6 +842,7 @@
746
842
  "forge.membership_tiers.price_free": "Kostenlos",
747
843
  "forge.membership_tiers.price_monthly": "{amount}/Mon.",
748
844
  "forge.membership_tiers.price_pwyw": "Zahl, was du willst, ab {amount}/Mon.",
845
+ "forge.membership_tiers.price_pwyw_yearly": "Zahle was du willst ab {amount}/Jahr",
749
846
  "forge.membership_tiers.price_yearly": "{amount}/Jahr",
750
847
  "forge.membership_tiers.select": "Wählen",
751
848
  "forge.offer_button.buy_now": "Jetzt kaufen",
package/src/i18n/en.json CHANGED
@@ -4,14 +4,25 @@
4
4
  "forge.account_dashboard.booking_cancel_error": "We could not cancel this session. Please try again.",
5
5
  "forge.account_dashboard.booking_duration": "{minutes} minutes",
6
6
  "forge.account_dashboard.booking_join_label": "Join:",
7
+ "forge.account_dashboard.booking_video_call": "Video call on this site",
8
+ "forge.account_dashboard.booking_join_video": "Join video call",
9
+ "forge.account_dashboard.booking_video_opens_at": "Video call: the room opens {datetime}",
10
+ "forge.account_dashboard.booking_video_closed": "Video call: this session's room has closed",
7
11
  "forge.account_dashboard.booking_where_label": "Where:",
8
12
  "forge.account_dashboard.cancel": "Cancel",
13
+ "forge.account_dashboard.cancel_confirm_confirm": "Cancel membership",
14
+ "forge.account_dashboard.cancel_confirm_free": "Your benefits will stop right away.",
15
+ "forge.account_dashboard.cancel_confirm_keep": "Keep membership",
16
+ "forge.account_dashboard.cancel_confirm_paid": "It will not renew, and you keep your benefits until the end of the period you have paid for.",
17
+ "forge.account_dashboard.cancel_confirm_paid_until": "It will not renew, and you keep your benefits until {date}.",
18
+ "forge.account_dashboard.cancel_confirm_title": "Cancel your membership?",
9
19
  "forge.account_dashboard.cancel_deletion": "Cancel Deletion",
10
20
  "forge.account_dashboard.cancel_session": "Cancel session",
11
21
  "forge.account_dashboard.cancel_ticket": "Cancel ticket",
12
22
  "forge.account_dashboard.cancelled": "Cancelled",
13
23
  "forge.account_dashboard.cancelling": "Cancelling…",
14
24
  "forge.account_dashboard.close": "Close",
25
+ "forge.account_dashboard.community_spaces": "Community spaces",
15
26
  "forge.account_dashboard.confirm_password": "Confirm new password",
16
27
  "forge.account_dashboard.current_password": "Current password",
17
28
  "forge.account_dashboard.data_export_description": "Export a copy of your personal data.",
@@ -139,6 +150,28 @@
139
150
  "forge.blog_post.not_found": "Post not found.",
140
151
  "forge.blog_post.pause": "Pause",
141
152
  "forge.blog_post.play": "Play",
153
+ "forge.broadcast_pass_validation.input_placeholder": "Enter your ticket ID (TN-XXXXXXX)",
154
+ "forge.broadcast_pass_validation.join": "Join Broadcast",
155
+ "forge.broadcast_pass_validation.or": "OR",
156
+ "forge.broadcast_pass_validation.prompt": "Enter your ticket ID to join the broadcast (TN-XXXXXXX)",
157
+ "forge.broadcast_pass_validation.validating": "Validating…",
158
+ "forge.broadcast_player.anonymous": "Anonymous",
159
+ "forge.broadcast_player.chat": "Chat",
160
+ "forge.broadcast_player.fullscreen": "Fullscreen",
161
+ "forge.broadcast_player.message_label": "Message",
162
+ "forge.broadcast_player.pinned": "Pinned",
163
+ "forge.broadcast_player.quality": "Quality",
164
+ "forge.broadcast_player.quality_auto": "Auto",
165
+ "forge.broadcast_player.quality_high": "High",
166
+ "forge.broadcast_player.quality_low": "Low",
167
+ "forge.broadcast_player.quality_medium": "Medium",
168
+ "forge.broadcast_player.send": "Send",
169
+ "forge.broadcast_player.started": "Started {duration} ago",
170
+ "forge.broadcast_player.watching_now": "{count} watching now",
171
+ "forge.broadcast_ticket_purchase.buy_tickets": "Buy tickets",
172
+ "forge.broadcast_ticket_purchase.from_price": "From {price}",
173
+ "forge.broadcast_watch.leave": "Leave broadcast",
174
+ "forge.broadcast_watch.load_error": "Unable to load broadcast",
142
175
  "forge.bundle_confirmation.continue_shopping": "Continue shopping",
143
176
  "forge.bundle_confirmation.eyebrow": "Order confirmed",
144
177
  "forge.bundle_confirmation.failed_body": "Nothing was charged and your cart is still here. You can try again.",
@@ -154,6 +187,41 @@
154
187
  "forge.bundle_confirmation.tickets_emailed": "Tickets emailed to you",
155
188
  "forge.bundle_confirmation.title": "You're all set",
156
189
  "forge.bundle_confirmation.view_orders": "View your orders",
190
+ "forge.call_stage.status_ended": "The call has ended",
191
+ "forge.call_stage.status_connected": "Connected",
192
+ "forge.call_stage.status_connecting": "Connecting to the call",
193
+ "forge.call_stage.status_left": "You have left the call",
194
+ "forge.call_stage.status_refused": "Could not join this call",
195
+ "forge.call_stage.status_moving": "Moving you to another server",
196
+ "forge.call_stage.status_moving_detail": "This takes a few seconds and nobody else is dropped.",
197
+ "forge.call_stage.status_move_failed": "Could not move you to another server",
198
+ "forge.call_stage.status_move_failed_detail": "Nobody else was dropped. Try again to rejoin.",
199
+ "forge.call_stage.status_reconnecting": "Connection lost, reconnecting",
200
+ "forge.call_stage.status_reconnecting_detail": "Everyone else is still in the call.",
201
+ "forge.call_stage.status_lost": "Connection lost",
202
+ "forge.call_stage.status_lost_detail": "Everyone else is still in the call. Try again to rejoin.",
203
+ "forge.call_stage.retry": "Try again",
204
+ "forge.call_stage.recording": "This call is being recorded",
205
+ "forge.call_stage.head_count": "In this call: {count}",
206
+ "forge.call_stage.help_hear_not_see_label": "Why can I hear someone but not see them?",
207
+ "forge.call_stage.help_hear_not_see": "A tile with a name and no picture means that person has their camera off, or the call is busy enough that only the people currently speaking are sent in full. Both are normal, and their audio is unaffected.",
208
+ "forge.call_stage.listening": "(listening)",
209
+ "forge.call_stage.you": "You",
210
+ "forge.call_stage.you_not_connected": "You (not connected yet)",
211
+ "forge.call_stage.mute": "Mute",
212
+ "forge.call_stage.unmute": "Unmute",
213
+ "forge.call_stage.camera_start": "Start camera",
214
+ "forge.call_stage.camera_stop": "Stop camera",
215
+ "forge.call_stage.help_camera_stop_label": "What does Stop camera do?",
216
+ "forge.call_stage.help_camera_stop": "It stops sending your picture: the others see your name instead. The browser keeps the camera open so that starting it again is instant and does not ask for permission a second time, which means its indicator light can stay on until you leave the call.",
217
+ "forge.call_stage.screen_start": "Share screen",
218
+ "forge.call_stage.screen_stop": "Stop sharing",
219
+ "forge.call_stage.help_screen_label": "What does sharing my screen send?",
220
+ "forge.call_stage.help_screen": "Whatever you pick in the browser prompt, live, to everyone in the call. Your camera keeps running alongside it rather than being replaced, so people can still see you while you present.",
221
+ "forge.call_stage.leave": "Leave",
222
+ "forge.call_stage.lost_microphone": "Your microphone was turned off when the connection dropped. Unmute to be heard again.",
223
+ "forge.call_stage.lost_camera": "Your camera was turned off when the connection dropped. Start it again to be seen.",
224
+ "forge.call_stage.lost_screen": "Your screen share ended when the connection dropped. Share it again to continue.",
157
225
  "forge.cancellation_terms.title": "Cancellation policy",
158
226
  "forge.cart.aria_close": "Close cart",
159
227
  "forge.cart.aria_open": "Open cart",
@@ -519,6 +587,12 @@
519
587
  "forge.email_list_form.submitting": "Joining…",
520
588
  "forge.email_list_form.success": "Thanks for subscribing!",
521
589
  "forge.email_list_form.title": "Join my email list",
590
+ "forge.ended_broadcast.banner": "Broadcast Ended",
591
+ "forge.ended_broadcast.close_instruction": "Close the broadcast page with the \"X\" at the top left and you will land on the host's live events page.",
592
+ "forge.ended_broadcast.notice_title": "Important information",
593
+ "forge.ended_broadcast.refresh_instruction": "You can keep refreshing this page until the broadcast is live again. Join it and you should be able to watch the event again. You will also get an email when the broadcast is live again.",
594
+ "forge.ended_broadcast.why_body": "If this happened in the middle of the event, it was most likely caused by an unstable internet connection at the host's end.",
595
+ "forge.ended_broadcast.why_label": "Why did the broadcast end?",
522
596
  "forge.event_confirmation.body_incomplete": "Your order is {status}. If you were charged, contact support.",
523
597
  "forge.event_confirmation.body_paid": "We’ve sent your tickets to {email}.",
524
598
  "forge.event_confirmation.explore": "Browse more events",
@@ -708,6 +782,12 @@
708
782
  "forge.lead_magnet.invalid_link": "Invalid link",
709
783
  "forge.lead_magnet.preparing": "Preparing…",
710
784
  "forge.lead_magnet.thank_you": "Thank you for subscribing!",
785
+ "forge.live_broadcast_list.empty_body": "There are currently no live broadcasts available.",
786
+ "forge.live_broadcast_list.empty_title": "No Live Broadcasts",
787
+ "forge.live_broadcast_list.error_body": "Unable to load live broadcasts at this time.",
788
+ "forge.live_broadcast_list.error_title": "Error Loading Broadcasts",
789
+ "forge.live_broadcast_list.subtitle": "Watch live streams and events",
790
+ "forge.live_broadcast_list.title": "Live Broadcasts",
711
791
  "forge.loading.loading": "Loading",
712
792
  "forge.login_form.back_to_login": "Back to login",
713
793
  "forge.login_form.code_sent_to": "We sent a 6-digit code to",
@@ -725,17 +805,33 @@
725
805
  "forge.login_form.verify_submit": "Verify & Sign In",
726
806
  "forge.login_form.verify_title": "Verify your email",
727
807
  "forge.login_form.verifying": "Verifying…",
808
+ "forge.membership_checkout.amount_below_minimum": "Enter {minimum} or more.",
809
+ "forge.membership_checkout.amount_not_positive": "Enter an amount greater than zero.",
728
810
  "forge.membership_checkout.back": "Back",
729
811
  "forge.membership_checkout.billing_cycle_label": "Billing cycle",
730
812
  "forge.membership_checkout.billing_monthly": "Monthly",
813
+ "forge.membership_checkout.billing_monthly_price": "Monthly ({amount})",
814
+ "forge.membership_checkout.billing_monthly_pwyw": "Pay what you want from {amount}/mo",
731
815
  "forge.membership_checkout.billing_yearly": "Yearly",
816
+ "forge.membership_checkout.billing_yearly_price": "Yearly ({amount})",
817
+ "forge.membership_checkout.billing_yearly_pwyw": "Pay what you want from {amount}/yr",
818
+ "forge.membership_checkout.choose_title": "Choose your membership",
819
+ "forge.membership_checkout.confirm_change": "Confirm change",
820
+ "forge.membership_checkout.current_plan": "Current plan",
821
+ "forge.membership_checkout.empty_title": "No memberships available",
822
+ "forge.membership_checkout.free": "Free",
823
+ "forge.membership_checkout.more_benefits": "+{count} more",
732
824
  "forge.membership_checkout.no_tier_body": "Select a membership tier to continue.",
825
+ "forge.membership_checkout.on_this_plan": "You are on this plan",
733
826
  "forge.membership_checkout.pay": "Pay {amount}",
734
827
  "forge.membership_checkout.payment_summary": "{tier_name} · {amount} / {cycle}",
735
828
  "forge.membership_checkout.payment_title": "Payment",
736
829
  "forge.membership_checkout.per_month": "per month",
737
830
  "forge.membership_checkout.per_year": "per year",
831
+ "forge.membership_checkout.pwyw_charged_as": "You will be charged {amount}.",
738
832
  "forge.membership_checkout.pwyw_label": "How much would you like to pay?",
833
+ "forge.membership_checkout.pwyw_min": "(minimum {amount})",
834
+ "forge.membership_checkout.select_tier": "Select this tier",
739
835
  "forge.membership_checkout.submit": "Subscribe",
740
836
  "forge.membership_checkout.submitting": "Processing…",
741
837
  "forge.membership_checkout.title": "Join {tier_name}",
@@ -746,6 +842,7 @@
746
842
  "forge.membership_tiers.price_free": "Free",
747
843
  "forge.membership_tiers.price_monthly": "{amount}/mo",
748
844
  "forge.membership_tiers.price_pwyw": "Pay what you want from {amount}/mo",
845
+ "forge.membership_tiers.price_pwyw_yearly": "Pay what you want from {amount}/yr",
749
846
  "forge.membership_tiers.price_yearly": "{amount}/yr",
750
847
  "forge.membership_tiers.select": "Select",
751
848
  "forge.offer_button.buy_now": "Buy now",