if (Array.isArray(data)) {
data.forEach(item => {
const rewardId = item.reward && item.reward.id ? item.reward.id : '';
- const eventType = item.event && item.event.type ? item.event.type : '';
- const eventPart = eventType ? ('event|' + eventType + '|') : '';
+ const eventPart = window.StreamVaultChat.chatEventKey(item);
const rewardPart = rewardId ? ('reward|' + rewardId + '|') : '';
const key = item.rel + '|' + item.user + '|' + eventPart + rewardPart + (item.msg || '');
if (!seenChatKeys.has(key)) {
diff --git a/WEBUI/translations/de/LC_MESSAGES/messages.mo b/WEBUI/translations/de/LC_MESSAGES/messages.mo
index c70e5c5..b3ca2b9 100644
Binary files a/WEBUI/translations/de/LC_MESSAGES/messages.mo and b/WEBUI/translations/de/LC_MESSAGES/messages.mo differ
diff --git a/WEBUI/translations/de/LC_MESSAGES/messages.po b/WEBUI/translations/de/LC_MESSAGES/messages.po
index 8682ece..11080b9 100644
--- a/WEBUI/translations/de/LC_MESSAGES/messages.po
+++ b/WEBUI/translations/de/LC_MESSAGES/messages.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: StreamVault VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2026-11-12 15:41+0100\n"
+"POT-Creation-Date: 2026-11-12 15:50+0100\n"
"PO-Revision-Date: 2026-11-03 19:25+0100\n"
"Last-Translator: FULL NAME \n"
"Language: de\n"
@@ -151,6 +151,66 @@ msgstr "%(num)s Tage"
msgid "%(provider)s does not support OAuth."
msgstr "%(provider)s unterstützt OAuth nicht."
+#, python-format
+msgid "%(user)s converted from a Prime sub to a Tier %(tier)s sub!"
+msgstr "%(user)s ist von einem Prime-Sub auf ein Tier-%(tier)s-Sub umgestiegen!"
+
+#, python-format
+msgid "%(user)s donated %(amount)s to %(charity)s!"
+msgstr "%(user)s hat %(amount)s an %(charity)s gespendet!"
+
+#, python-format
+msgid "%(user)s gifted %(months)s months of Tier %(tier)s to %(recipient)s!"
+msgstr "%(user)s hat %(recipient)s %(months)s Monate Tier %(tier)s geschenkt!"
+
+#, python-format
+msgid "%(user)s gifted a Tier %(tier)s sub to %(recipient)s!"
+msgstr "%(user)s hat %(recipient)s ein Tier-%(tier)s-Sub geschenkt!"
+
+#, python-format
+msgid "%(user)s is continuing the gift sub they got from %(other)s!"
+msgstr "%(user)s verlängert das geschenkte Sub von %(other)s!"
+
+#, python-format
+msgid "%(user)s is gifting %(num)d Tier %(tier)s sub to the community!"
+msgid_plural "%(user)s is gifting %(num)d Tier %(tier)s subs to the community!"
+msgstr[0] "%(user)s verschenkt %(num)d Tier-%(tier)s-Sub an die Community!"
+msgstr[1] "%(user)s verschenkt %(num)d Tier-%(tier)s-Subs an die Community!"
+
+#, python-format
+msgid "%(user)s is new here. Say hello!"
+msgstr "%(user)s ist neu hier. Sag Hallo!"
+
+#, python-format
+msgid "%(user)s is paying forward the gift they got from %(other)s to %(recipient)s!"
+msgstr "%(user)s gibt das Geschenk von %(other)s an %(recipient)s weiter!"
+
+#, python-format
+msgid "%(user)s is paying forward the gift they got from %(other)s to the community!"
+msgstr "%(user)s gibt das Geschenk von %(other)s an die Community weiter!"
+
+#, python-format
+msgid "%(user)s is raiding with %(num)d viewer!"
+msgid_plural "%(user)s is raiding with %(num)d viewers!"
+msgstr[0] "%(user)s raidet mit %(num)d Zuschauer!"
+msgstr[1] "%(user)s raidet mit %(num)d Zuschauern!"
+
+#, python-format
+msgid "%(user)s just earned a new %(bits)s Bits badge!"
+msgstr "%(user)s hat ein neues %(bits)s-Bits-Abzeichen erhalten!"
+
+#, python-format
+msgid "%(user)s subscribed at Tier %(tier)s."
+msgstr "%(user)s hat mit Tier %(tier)s abonniert."
+
+#, python-format
+msgid "%(user)s subscribed with Prime."
+msgstr "%(user)s hat mit Prime abonniert."
+
+#, python-format
+msgid "%(user)s watched %(num)s consecutive streams and sparked a watch streak!"
+msgstr "%(user)s hat %(num)s Streams in Folge geschaut und eine Watch-Streak gestartet!"
+
msgid "0 = delete immediately once the cloud copy is verified."
msgstr "0 = sofort löschen, sobald die Cloud-Kopie verifiziert ist."
@@ -304,9 +364,15 @@ msgstr "Überträgt zusätzlich %(path)s."
msgid "Also transfers preview images from %(path)s."
msgstr "Überträgt zusätzlich Vorschaubilder aus %(path)s."
+msgid "An anonymous user"
+msgstr "Ein anonymer Nutzer"
+
msgid "An upload for this file is already running."
msgstr "Es läuft bereits ein Upload für diese Datei."
+msgid "Announcement"
+msgstr "Ankündigung"
+
msgid "Anonymous"
msgstr "Anonym"
@@ -567,6 +633,9 @@ msgstr "Chatverlauf: %(user)s"
msgid "Chat messages"
msgstr "Chat-Nachrichten"
+msgid "Chat notice"
+msgstr "Chat-Hinweis"
+
msgid "Chat replay"
msgstr "Chat Replay"
@@ -1218,6 +1287,9 @@ msgstr "Chat filtern…"
msgid "First"
msgstr "Erste"
+msgid "First message"
+msgstr "Erste Nachricht"
+
msgid "First page"
msgstr "Erste Seite"
@@ -2057,6 +2129,10 @@ msgstr "Seiten-Navigation"
msgid "Pagination (top)"
msgstr "Seiten-Navigation (Oben)"
+#, python-format
+msgid "Paid for %(months)s months in advance."
+msgstr "Für %(months)s Monate im Voraus bezahlt."
+
msgid "Password"
msgstr "Passwort"
@@ -2365,6 +2441,10 @@ msgstr "Neues Passwort wiederholen"
msgid "Repeat password"
msgstr "Passwort wiederholen"
+#, python-format
+msgid "Replying to @%(user)s: %(msg)s"
+msgstr "Antwort an @%(user)s: %(msg)s"
+
#, python-format
msgid "Request '%(username)s' denied and deleted."
msgstr "Antrag '%(username)s' abgelehnt und gelöscht."
@@ -2630,6 +2710,9 @@ msgstr "Größe"
msgid "Skip to content"
msgstr "Zum Inhalt springen"
+msgid "Someone"
+msgstr "Jemand"
+
msgid "Sort"
msgstr "Sortierung"
@@ -2945,6 +3028,20 @@ msgstr "Das Passwort des System-Administrators kann nur über Umgebungsvariablen
msgid "The video file could not be loaded or is still being transcoded."
msgstr "Die Videodatei konnte nicht geladen werden oder wird aktuell noch transkodiert."
+#, python-format
+msgid "They've gifted %(num)d sub in the channel!"
+msgid_plural "They've gifted %(num)d subs in the channel!"
+msgstr[0] "Insgesamt %(num)d Sub in diesem Kanal verschenkt!"
+msgstr[1] "Insgesamt %(num)d Subs in diesem Kanal verschenkt!"
+
+#, python-format
+msgid "They've subscribed for %(months)s months!"
+msgstr "Abonniert seit %(months)s Monaten!"
+
+#, python-format
+msgid "They've subscribed for %(months)s months, currently on a %(streak)s month streak!"
+msgstr "Abonniert seit %(months)s Monaten, aktuell %(streak)s Monate in Folge!"
+
msgid "This admin user is protected and cannot be deleted."
msgstr "Dieser Admin-User ist geschützt und kann nicht gelöscht werden."
@@ -3405,6 +3502,9 @@ msgstr "laut Objekt-Datenbank"
msgid "active"
msgstr "aktiv"
+msgid "an anonymous user"
+msgstr "einem anonymen Nutzer"
+
msgid "available"
msgstr "vorhanden"
@@ -3461,6 +3561,9 @@ msgstr "nie"
msgid "not created"
msgstr "nicht erstellt"
+msgid "someone"
+msgstr "jemandem"
+
#, python-format
msgid "today %(time)s"
msgstr "heute %(time)s"
diff --git a/WEBUI/translations/es/LC_MESSAGES/messages.mo b/WEBUI/translations/es/LC_MESSAGES/messages.mo
index b105723..4c7a0ec 100644
Binary files a/WEBUI/translations/es/LC_MESSAGES/messages.mo and b/WEBUI/translations/es/LC_MESSAGES/messages.mo differ
diff --git a/WEBUI/translations/es/LC_MESSAGES/messages.po b/WEBUI/translations/es/LC_MESSAGES/messages.po
index 8d68611..08a7d41 100644
--- a/WEBUI/translations/es/LC_MESSAGES/messages.po
+++ b/WEBUI/translations/es/LC_MESSAGES/messages.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: StreamVault VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2026-11-12 15:41+0100\n"
+"POT-Creation-Date: 2026-11-12 15:50+0100\n"
"PO-Revision-Date: 2026-11-03 19:31+0100\n"
"Last-Translator: FULL NAME \n"
"Language: es\n"
@@ -151,6 +151,66 @@ msgstr "%(num)s días"
msgid "%(provider)s does not support OAuth."
msgstr "%(provider)s no admite OAuth."
+#, python-format
+msgid "%(user)s converted from a Prime sub to a Tier %(tier)s sub!"
+msgstr "¡%(user)s pasó de una suscripción Prime a una de nivel %(tier)s!"
+
+#, python-format
+msgid "%(user)s donated %(amount)s to %(charity)s!"
+msgstr "¡%(user)s donó %(amount)s a %(charity)s!"
+
+#, python-format
+msgid "%(user)s gifted %(months)s months of Tier %(tier)s to %(recipient)s!"
+msgstr "¡%(user)s regaló %(months)s meses de nivel %(tier)s a %(recipient)s!"
+
+#, python-format
+msgid "%(user)s gifted a Tier %(tier)s sub to %(recipient)s!"
+msgstr "¡%(user)s regaló una suscripción de nivel %(tier)s a %(recipient)s!"
+
+#, python-format
+msgid "%(user)s is continuing the gift sub they got from %(other)s!"
+msgstr "¡%(user)s continúa la suscripción de regalo que recibió de %(other)s!"
+
+#, python-format
+msgid "%(user)s is gifting %(num)d Tier %(tier)s sub to the community!"
+msgid_plural "%(user)s is gifting %(num)d Tier %(tier)s subs to the community!"
+msgstr[0] "¡%(user)s regala %(num)d suscripción de nivel %(tier)s a la comunidad!"
+msgstr[1] "¡%(user)s regala %(num)d suscripciones de nivel %(tier)s a la comunidad!"
+
+#, python-format
+msgid "%(user)s is new here. Say hello!"
+msgstr "%(user)s es nuevo aquí. ¡Salúdalo!"
+
+#, python-format
+msgid "%(user)s is paying forward the gift they got from %(other)s to %(recipient)s!"
+msgstr "¡%(user)s pasa el regalo que recibió de %(other)s a %(recipient)s!"
+
+#, python-format
+msgid "%(user)s is paying forward the gift they got from %(other)s to the community!"
+msgstr "¡%(user)s pasa el regalo que recibió de %(other)s a la comunidad!"
+
+#, python-format
+msgid "%(user)s is raiding with %(num)d viewer!"
+msgid_plural "%(user)s is raiding with %(num)d viewers!"
+msgstr[0] "¡%(user)s está haciendo un raid con %(num)d espectador!"
+msgstr[1] "¡%(user)s está haciendo un raid con %(num)d espectadores!"
+
+#, python-format
+msgid "%(user)s just earned a new %(bits)s Bits badge!"
+msgstr "¡%(user)s acaba de conseguir una nueva insignia de %(bits)s Bits!"
+
+#, python-format
+msgid "%(user)s subscribed at Tier %(tier)s."
+msgstr "%(user)s se suscribió con nivel %(tier)s."
+
+#, python-format
+msgid "%(user)s subscribed with Prime."
+msgstr "%(user)s se suscribió con Prime."
+
+#, python-format
+msgid "%(user)s watched %(num)s consecutive streams and sparked a watch streak!"
+msgstr "¡%(user)s vio %(num)s directos seguidos y comenzó una racha de visualización!"
+
msgid "0 = delete immediately once the cloud copy is verified."
msgstr "0 = eliminar inmediatamente en cuanto se verifique la copia en la nube."
@@ -304,9 +364,15 @@ msgstr "También transfiere %(path)s."
msgid "Also transfers preview images from %(path)s."
msgstr "También transfiere las imágenes de vista previa de %(path)s."
+msgid "An anonymous user"
+msgstr "Un usuario anónimo"
+
msgid "An upload for this file is already running."
msgstr "Ya hay una subida en curso para este archivo."
+msgid "Announcement"
+msgstr "Anuncio"
+
msgid "Anonymous"
msgstr "Anónimo"
@@ -567,6 +633,9 @@ msgstr "Historial del chat: %(user)s"
msgid "Chat messages"
msgstr "Mensajes del chat"
+msgid "Chat notice"
+msgstr "Aviso del chat"
+
msgid "Chat replay"
msgstr "Repetición del chat"
@@ -1218,6 +1287,9 @@ msgstr "Filtrar chat…"
msgid "First"
msgstr "Primero"
+msgid "First message"
+msgstr "Primer mensaje"
+
msgid "First page"
msgstr "Primera página"
@@ -2057,6 +2129,10 @@ msgstr "Paginación"
msgid "Pagination (top)"
msgstr "Paginación (arriba)"
+#, python-format
+msgid "Paid for %(months)s months in advance."
+msgstr "Pagado con %(months)s meses de antelación."
+
msgid "Password"
msgstr "Contraseña"
@@ -2365,6 +2441,10 @@ msgstr "Repite la nueva contraseña"
msgid "Repeat password"
msgstr "Repite la contraseña"
+#, python-format
+msgid "Replying to @%(user)s: %(msg)s"
+msgstr "Respondiendo a @%(user)s: %(msg)s"
+
#, python-format
msgid "Request '%(username)s' denied and deleted."
msgstr "Solicitud «%(username)s» rechazada y eliminada."
@@ -2630,6 +2710,9 @@ msgstr "Tamaño"
msgid "Skip to content"
msgstr "Saltar al contenido"
+msgid "Someone"
+msgstr "Alguien"
+
msgid "Sort"
msgstr "Ordenar"
@@ -2945,6 +3028,20 @@ msgstr "La contraseña del administrador del sistema solo se puede cambiar media
msgid "The video file could not be loaded or is still being transcoded."
msgstr "No se pudo cargar el archivo de vídeo o todavía se está transcodificando."
+#, python-format
+msgid "They've gifted %(num)d sub in the channel!"
+msgid_plural "They've gifted %(num)d subs in the channel!"
+msgstr[0] "¡Ha regalado %(num)d suscripción en el canal!"
+msgstr[1] "¡Ha regalado %(num)d suscripciones en el canal!"
+
+#, python-format
+msgid "They've subscribed for %(months)s months!"
+msgstr "¡Lleva %(months)s meses suscrito!"
+
+#, python-format
+msgid "They've subscribed for %(months)s months, currently on a %(streak)s month streak!"
+msgstr "¡Lleva %(months)s meses suscrito, con una racha actual de %(streak)s meses!"
+
msgid "This admin user is protected and cannot be deleted."
msgstr "Este usuario administrador está protegido y no se puede eliminar."
@@ -3405,6 +3502,9 @@ msgstr "según la base de datos de objetos"
msgid "active"
msgstr "activo"
+msgid "an anonymous user"
+msgstr "un usuario anónimo"
+
msgid "available"
msgstr "disponible"
@@ -3461,6 +3561,9 @@ msgstr "nunca"
msgid "not created"
msgstr "no creado"
+msgid "someone"
+msgstr "alguien"
+
#, python-format
msgid "today %(time)s"
msgstr "hoy %(time)s"
diff --git a/WEBUI/translations/fr/LC_MESSAGES/messages.mo b/WEBUI/translations/fr/LC_MESSAGES/messages.mo
index ad0a4c1..3718aaa 100644
Binary files a/WEBUI/translations/fr/LC_MESSAGES/messages.mo and b/WEBUI/translations/fr/LC_MESSAGES/messages.mo differ
diff --git a/WEBUI/translations/fr/LC_MESSAGES/messages.po b/WEBUI/translations/fr/LC_MESSAGES/messages.po
index 9bf6493..0d4e1d3 100644
--- a/WEBUI/translations/fr/LC_MESSAGES/messages.po
+++ b/WEBUI/translations/fr/LC_MESSAGES/messages.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: StreamVault VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2026-11-12 15:41+0100\n"
+"POT-Creation-Date: 2026-11-12 15:50+0100\n"
"PO-Revision-Date: 2026-11-03 19:31+0100\n"
"Last-Translator: FULL NAME \n"
"Language: fr\n"
@@ -151,6 +151,66 @@ msgstr "%(num)s jours"
msgid "%(provider)s does not support OAuth."
msgstr "%(provider)s ne prend pas en charge OAuth."
+#, python-format
+msgid "%(user)s converted from a Prime sub to a Tier %(tier)s sub!"
+msgstr "%(user)s est passé d’un abonnement Prime à un abonnement de niveau %(tier)s !"
+
+#, python-format
+msgid "%(user)s donated %(amount)s to %(charity)s!"
+msgstr "%(user)s a fait un don de %(amount)s à %(charity)s !"
+
+#, python-format
+msgid "%(user)s gifted %(months)s months of Tier %(tier)s to %(recipient)s!"
+msgstr "%(user)s a offert %(months)s mois de niveau %(tier)s à %(recipient)s !"
+
+#, python-format
+msgid "%(user)s gifted a Tier %(tier)s sub to %(recipient)s!"
+msgstr "%(user)s a offert un abonnement de niveau %(tier)s à %(recipient)s !"
+
+#, python-format
+msgid "%(user)s is continuing the gift sub they got from %(other)s!"
+msgstr "%(user)s prolonge l’abonnement offert par %(other)s !"
+
+#, python-format
+msgid "%(user)s is gifting %(num)d Tier %(tier)s sub to the community!"
+msgid_plural "%(user)s is gifting %(num)d Tier %(tier)s subs to the community!"
+msgstr[0] "%(user)s offre %(num)d abonnement de niveau %(tier)s à la communauté !"
+msgstr[1] "%(user)s offre %(num)d abonnements de niveau %(tier)s à la communauté !"
+
+#, python-format
+msgid "%(user)s is new here. Say hello!"
+msgstr "%(user)s est nouveau ici. Dites bonjour !"
+
+#, python-format
+msgid "%(user)s is paying forward the gift they got from %(other)s to %(recipient)s!"
+msgstr "%(user)s transmet le cadeau reçu de %(other)s à %(recipient)s !"
+
+#, python-format
+msgid "%(user)s is paying forward the gift they got from %(other)s to the community!"
+msgstr "%(user)s transmet le cadeau reçu de %(other)s à la communauté !"
+
+#, python-format
+msgid "%(user)s is raiding with %(num)d viewer!"
+msgid_plural "%(user)s is raiding with %(num)d viewers!"
+msgstr[0] "%(user)s lance un raid avec %(num)d spectateur !"
+msgstr[1] "%(user)s lance un raid avec %(num)d spectateurs !"
+
+#, python-format
+msgid "%(user)s just earned a new %(bits)s Bits badge!"
+msgstr "%(user)s vient d’obtenir un nouveau badge de %(bits)s Bits !"
+
+#, python-format
+msgid "%(user)s subscribed at Tier %(tier)s."
+msgstr "%(user)s s’est abonné au niveau %(tier)s."
+
+#, python-format
+msgid "%(user)s subscribed with Prime."
+msgstr "%(user)s s’est abonné avec Prime."
+
+#, python-format
+msgid "%(user)s watched %(num)s consecutive streams and sparked a watch streak!"
+msgstr "%(user)s a regardé %(num)s streams d’affilée et lancé une série de visionnage !"
+
msgid "0 = delete immediately once the cloud copy is verified."
msgstr "0 = supprimer immédiatement dès que la copie cloud est vérifiée."
@@ -304,9 +364,15 @@ msgstr "Transfère aussi %(path)s."
msgid "Also transfers preview images from %(path)s."
msgstr "Transfère aussi les images d'aperçu de %(path)s."
+msgid "An anonymous user"
+msgstr "Un utilisateur anonyme"
+
msgid "An upload for this file is already running."
msgstr "Un envoi est déjà en cours pour ce fichier."
+msgid "Announcement"
+msgstr "Annonce"
+
msgid "Anonymous"
msgstr "Anonyme"
@@ -567,6 +633,9 @@ msgstr "Historique du chat : %(user)s"
msgid "Chat messages"
msgstr "Messages du chat"
+msgid "Chat notice"
+msgstr "Notification du chat"
+
msgid "Chat replay"
msgstr "Replay du chat"
@@ -1218,6 +1287,9 @@ msgstr "Filtrer le chat…"
msgid "First"
msgstr "Premier"
+msgid "First message"
+msgstr "Premier message"
+
msgid "First page"
msgstr "Première page"
@@ -2057,6 +2129,10 @@ msgstr "Pagination"
msgid "Pagination (top)"
msgstr "Pagination (haut)"
+#, python-format
+msgid "Paid for %(months)s months in advance."
+msgstr "Payé %(months)s mois à l’avance."
+
msgid "Password"
msgstr "Mot de passe"
@@ -2365,6 +2441,10 @@ msgstr "Répéter le nouveau mot de passe"
msgid "Repeat password"
msgstr "Répéter le mot de passe"
+#, python-format
+msgid "Replying to @%(user)s: %(msg)s"
+msgstr "En réponse à @%(user)s : %(msg)s"
+
#, python-format
msgid "Request '%(username)s' denied and deleted."
msgstr "Demande « %(username)s » refusée et supprimée."
@@ -2630,6 +2710,9 @@ msgstr "Taille"
msgid "Skip to content"
msgstr "Aller au contenu"
+msgid "Someone"
+msgstr "Quelqu’un"
+
msgid "Sort"
msgstr "Trier"
@@ -2945,6 +3028,20 @@ msgstr "Le mot de passe de l'administrateur système ne peut être modifié que
msgid "The video file could not be loaded or is still being transcoded."
msgstr "Le fichier vidéo n'a pas pu être chargé ou est encore en cours de transcodage."
+#, python-format
+msgid "They've gifted %(num)d sub in the channel!"
+msgid_plural "They've gifted %(num)d subs in the channel!"
+msgstr[0] "%(num)d abonnement offert sur la chaîne au total !"
+msgstr[1] "%(num)d abonnements offerts sur la chaîne au total !"
+
+#, python-format
+msgid "They've subscribed for %(months)s months!"
+msgstr "Abonné depuis %(months)s mois !"
+
+#, python-format
+msgid "They've subscribed for %(months)s months, currently on a %(streak)s month streak!"
+msgstr "Abonné depuis %(months)s mois, série en cours de %(streak)s mois !"
+
msgid "This admin user is protected and cannot be deleted."
msgstr "Cet utilisateur admin est protégé et ne peut pas être supprimé."
@@ -3405,6 +3502,9 @@ msgstr "selon la base de données des objets"
msgid "active"
msgstr "actif"
+msgid "an anonymous user"
+msgstr "un utilisateur anonyme"
+
msgid "available"
msgstr "disponible"
@@ -3461,6 +3561,9 @@ msgstr "jamais"
msgid "not created"
msgstr "non créé"
+msgid "someone"
+msgstr "quelqu’un"
+
#, python-format
msgid "today %(time)s"
msgstr "aujourd'hui %(time)s"
diff --git a/WEBUI/translations/it/LC_MESSAGES/messages.mo b/WEBUI/translations/it/LC_MESSAGES/messages.mo
index 3d9e29e..738dc33 100644
Binary files a/WEBUI/translations/it/LC_MESSAGES/messages.mo and b/WEBUI/translations/it/LC_MESSAGES/messages.mo differ
diff --git a/WEBUI/translations/it/LC_MESSAGES/messages.po b/WEBUI/translations/it/LC_MESSAGES/messages.po
index b13127f..ef856c1 100644
--- a/WEBUI/translations/it/LC_MESSAGES/messages.po
+++ b/WEBUI/translations/it/LC_MESSAGES/messages.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: StreamVault VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2026-11-12 15:41+0100\n"
+"POT-Creation-Date: 2026-11-12 15:50+0100\n"
"PO-Revision-Date: 2026-11-03 19:31+0100\n"
"Last-Translator: FULL NAME \n"
"Language: it\n"
@@ -151,6 +151,66 @@ msgstr "%(num)s giorni"
msgid "%(provider)s does not support OAuth."
msgstr "%(provider)s non supporta OAuth."
+#, python-format
+msgid "%(user)s converted from a Prime sub to a Tier %(tier)s sub!"
+msgstr "%(user)s è passato da un abbonamento Prime a uno di livello %(tier)s!"
+
+#, python-format
+msgid "%(user)s donated %(amount)s to %(charity)s!"
+msgstr "%(user)s ha donato %(amount)s a %(charity)s!"
+
+#, python-format
+msgid "%(user)s gifted %(months)s months of Tier %(tier)s to %(recipient)s!"
+msgstr "%(user)s ha regalato %(months)s mesi di livello %(tier)s a %(recipient)s!"
+
+#, python-format
+msgid "%(user)s gifted a Tier %(tier)s sub to %(recipient)s!"
+msgstr "%(user)s ha regalato un abbonamento di livello %(tier)s a %(recipient)s!"
+
+#, python-format
+msgid "%(user)s is continuing the gift sub they got from %(other)s!"
+msgstr "%(user)s continua l’abbonamento regalato da %(other)s!"
+
+#, python-format
+msgid "%(user)s is gifting %(num)d Tier %(tier)s sub to the community!"
+msgid_plural "%(user)s is gifting %(num)d Tier %(tier)s subs to the community!"
+msgstr[0] "%(user)s regala %(num)d abbonamento di livello %(tier)s alla community!"
+msgstr[1] "%(user)s regala %(num)d abbonamenti di livello %(tier)s alla community!"
+
+#, python-format
+msgid "%(user)s is new here. Say hello!"
+msgstr "%(user)s è nuovo qui. Saluta!"
+
+#, python-format
+msgid "%(user)s is paying forward the gift they got from %(other)s to %(recipient)s!"
+msgstr "%(user)s passa il regalo ricevuto da %(other)s a %(recipient)s!"
+
+#, python-format
+msgid "%(user)s is paying forward the gift they got from %(other)s to the community!"
+msgstr "%(user)s passa il regalo ricevuto da %(other)s alla community!"
+
+#, python-format
+msgid "%(user)s is raiding with %(num)d viewer!"
+msgid_plural "%(user)s is raiding with %(num)d viewers!"
+msgstr[0] "%(user)s sta facendo un raid con %(num)d spettatore!"
+msgstr[1] "%(user)s sta facendo un raid con %(num)d spettatori!"
+
+#, python-format
+msgid "%(user)s just earned a new %(bits)s Bits badge!"
+msgstr "%(user)s ha appena ottenuto un nuovo badge da %(bits)s Bits!"
+
+#, python-format
+msgid "%(user)s subscribed at Tier %(tier)s."
+msgstr "%(user)s si è abbonato al livello %(tier)s."
+
+#, python-format
+msgid "%(user)s subscribed with Prime."
+msgstr "%(user)s si è abbonato con Prime."
+
+#, python-format
+msgid "%(user)s watched %(num)s consecutive streams and sparked a watch streak!"
+msgstr "%(user)s ha guardato %(num)s live di fila e ha avviato una serie di visione!"
+
msgid "0 = delete immediately once the cloud copy is verified."
msgstr "0 = elimina subito dopo la verifica della copia nel cloud."
@@ -304,9 +364,15 @@ msgstr "Trasferisce anche %(path)s."
msgid "Also transfers preview images from %(path)s."
msgstr "Trasferisce anche le anteprime da %(path)s."
+msgid "An anonymous user"
+msgstr "Un utente anonimo"
+
msgid "An upload for this file is already running."
msgstr "È già in corso un caricamento per questo file."
+msgid "Announcement"
+msgstr "Annuncio"
+
msgid "Anonymous"
msgstr "Anonimo"
@@ -567,6 +633,9 @@ msgstr "Cronologia chat: %(user)s"
msgid "Chat messages"
msgstr "Messaggi della chat"
+msgid "Chat notice"
+msgstr "Avviso della chat"
+
msgid "Chat replay"
msgstr "Replay della chat"
@@ -1218,6 +1287,9 @@ msgstr "Filtra chat…"
msgid "First"
msgstr "Prima"
+msgid "First message"
+msgstr "Primo messaggio"
+
msgid "First page"
msgstr "Prima pagina"
@@ -2057,6 +2129,10 @@ msgstr "Paginazione"
msgid "Pagination (top)"
msgstr "Paginazione (in alto)"
+#, python-format
+msgid "Paid for %(months)s months in advance."
+msgstr "Pagato %(months)s mesi in anticipo."
+
msgid "Password"
msgstr "Password"
@@ -2365,6 +2441,10 @@ msgstr "Ripeti la nuova password"
msgid "Repeat password"
msgstr "Ripeti la password"
+#, python-format
+msgid "Replying to @%(user)s: %(msg)s"
+msgstr "In risposta a @%(user)s: %(msg)s"
+
#, python-format
msgid "Request '%(username)s' denied and deleted."
msgstr "Richiesta di '%(username)s' rifiutata ed eliminata."
@@ -2630,6 +2710,9 @@ msgstr "Dimensione"
msgid "Skip to content"
msgstr "Vai al contenuto"
+msgid "Someone"
+msgstr "Qualcuno"
+
msgid "Sort"
msgstr "Ordina"
@@ -2945,6 +3028,20 @@ msgstr "La password dell'amministratore di sistema può essere modificata solo t
msgid "The video file could not be loaded or is still being transcoded."
msgstr "Impossibile caricare il file video oppure è ancora in fase di transcodifica."
+#, python-format
+msgid "They've gifted %(num)d sub in the channel!"
+msgid_plural "They've gifted %(num)d subs in the channel!"
+msgstr[0] "Ha regalato %(num)d abbonamento nel canale!"
+msgstr[1] "Ha regalato %(num)d abbonamenti nel canale!"
+
+#, python-format
+msgid "They've subscribed for %(months)s months!"
+msgstr "Abbonato da %(months)s mesi!"
+
+#, python-format
+msgid "They've subscribed for %(months)s months, currently on a %(streak)s month streak!"
+msgstr "Abbonato da %(months)s mesi, con una serie attuale di %(streak)s mesi!"
+
msgid "This admin user is protected and cannot be deleted."
msgstr "Questo utente admin è protetto e non può essere eliminato."
@@ -3405,6 +3502,9 @@ msgstr "secondo il database degli oggetti"
msgid "active"
msgstr "attivo"
+msgid "an anonymous user"
+msgstr "un utente anonimo"
+
msgid "available"
msgstr "disponibile"
@@ -3461,6 +3561,9 @@ msgstr "mai"
msgid "not created"
msgstr "non creato"
+msgid "someone"
+msgstr "qualcuno"
+
#, python-format
msgid "today %(time)s"
msgstr "oggi %(time)s"
diff --git a/WEBUI/translations/ja/LC_MESSAGES/messages.mo b/WEBUI/translations/ja/LC_MESSAGES/messages.mo
index 3f232dd..26bb12a 100644
Binary files a/WEBUI/translations/ja/LC_MESSAGES/messages.mo and b/WEBUI/translations/ja/LC_MESSAGES/messages.mo differ
diff --git a/WEBUI/translations/ja/LC_MESSAGES/messages.po b/WEBUI/translations/ja/LC_MESSAGES/messages.po
index f2b986d..7e5f395 100644
--- a/WEBUI/translations/ja/LC_MESSAGES/messages.po
+++ b/WEBUI/translations/ja/LC_MESSAGES/messages.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: StreamVault VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2026-11-12 15:41+0100\n"
+"POT-Creation-Date: 2026-11-12 15:50+0100\n"
"PO-Revision-Date: 2026-11-03 19:31+0100\n"
"Last-Translator: FULL NAME \n"
"Language: ja\n"
@@ -145,6 +145,64 @@ msgstr "%(num)s 日"
msgid "%(provider)s does not support OAuth."
msgstr "%(provider)s は OAuth に対応していません。"
+#, python-format
+msgid "%(user)s converted from a Prime sub to a Tier %(tier)s sub!"
+msgstr "%(user)s さんがPrimeサブからティア%(tier)sサブに切り替えました!"
+
+#, python-format
+msgid "%(user)s donated %(amount)s to %(charity)s!"
+msgstr "%(user)s さんが%(charity)sに%(amount)sを寄付しました!"
+
+#, python-format
+msgid "%(user)s gifted %(months)s months of Tier %(tier)s to %(recipient)s!"
+msgstr "%(user)s さんが%(recipient)s さんにティア%(tier)sサブ%(months)sか月分をギフトしました!"
+
+#, python-format
+msgid "%(user)s gifted a Tier %(tier)s sub to %(recipient)s!"
+msgstr "%(user)s さんが%(recipient)s さんにティア%(tier)sサブをギフトしました!"
+
+#, python-format
+msgid "%(user)s is continuing the gift sub they got from %(other)s!"
+msgstr "%(user)s さんは%(other)s さんからもらったギフトサブを継続しています!"
+
+#, python-format
+msgid "%(user)s is gifting %(num)d Tier %(tier)s sub to the community!"
+msgid_plural "%(user)s is gifting %(num)d Tier %(tier)s subs to the community!"
+msgstr[0] "%(user)s さんがコミュニティにティア%(tier)sサブを%(num)d個ギフトしています!"
+
+#, python-format
+msgid "%(user)s is new here. Say hello!"
+msgstr "%(user)s さんは初めての参加です。あいさつしましょう!"
+
+#, python-format
+msgid "%(user)s is paying forward the gift they got from %(other)s to %(recipient)s!"
+msgstr "%(user)s さんは%(other)s さんからのギフトを%(recipient)s さんに恩送りしています!"
+
+#, python-format
+msgid "%(user)s is paying forward the gift they got from %(other)s to the community!"
+msgstr "%(user)s さんは%(other)s さんからのギフトをコミュニティに恩送りしています!"
+
+#, python-format
+msgid "%(user)s is raiding with %(num)d viewer!"
+msgid_plural "%(user)s is raiding with %(num)d viewers!"
+msgstr[0] "%(user)s さんが%(num)d人の視聴者とレイド中です!"
+
+#, python-format
+msgid "%(user)s just earned a new %(bits)s Bits badge!"
+msgstr "%(user)s さんが新しい%(bits)sビッツバッジを獲得しました!"
+
+#, python-format
+msgid "%(user)s subscribed at Tier %(tier)s."
+msgstr "%(user)s さんがティア%(tier)sでサブスクライブしました。"
+
+#, python-format
+msgid "%(user)s subscribed with Prime."
+msgstr "%(user)s さんがPrimeでサブスクライブしました。"
+
+#, python-format
+msgid "%(user)s watched %(num)s consecutive streams and sparked a watch streak!"
+msgstr "%(user)s さんが%(num)s回連続で配信を視聴し、視聴ストリークを達成しました!"
+
msgid "0 = delete immediately once the cloud copy is verified."
msgstr "0 = クラウドのコピーを確認後すぐに削除します。"
@@ -298,9 +356,15 @@ msgstr "%(path)s も転送します。"
msgid "Also transfers preview images from %(path)s."
msgstr "%(path)s のプレビュー画像も転送します。"
+msgid "An anonymous user"
+msgstr "匿名ユーザー"
+
msgid "An upload for this file is already running."
msgstr "このファイルのアップロードはすでに実行中です。"
+msgid "Announcement"
+msgstr "お知らせ"
+
msgid "Anonymous"
msgstr "匿名"
@@ -561,6 +625,9 @@ msgstr "チャット履歴: %(user)s"
msgid "Chat messages"
msgstr "チャットメッセージ"
+msgid "Chat notice"
+msgstr "チャット通知"
+
msgid "Chat replay"
msgstr "チャットリプレイ"
@@ -1212,6 +1279,9 @@ msgstr "チャットを絞り込み…"
msgid "First"
msgstr "最初"
+msgid "First message"
+msgstr "初めてのメッセージ"
+
msgid "First page"
msgstr "最初のページ"
@@ -2051,6 +2121,10 @@ msgstr "ページ送り"
msgid "Pagination (top)"
msgstr "ページ送り(上部)"
+#, python-format
+msgid "Paid for %(months)s months in advance."
+msgstr "%(months)sか月分を前払いしました。"
+
msgid "Password"
msgstr "パスワード"
@@ -2359,6 +2433,10 @@ msgstr "新しいパスワード(確認)"
msgid "Repeat password"
msgstr "パスワード(確認)"
+#, python-format
+msgid "Replying to @%(user)s: %(msg)s"
+msgstr "@%(user)s さんへの返信: %(msg)s"
+
#, python-format
msgid "Request '%(username)s' denied and deleted."
msgstr "「%(username)s」のリクエストを拒否して削除しました。"
@@ -2624,6 +2702,9 @@ msgstr "サイズ"
msgid "Skip to content"
msgstr "本文へスキップ"
+msgid "Someone"
+msgstr "誰か"
+
msgid "Sort"
msgstr "並べ替え"
@@ -2939,6 +3020,19 @@ msgstr "システム管理者のパスワードは環境変数でのみ変更で
msgid "The video file could not be loaded or is still being transcoded."
msgstr "動画ファイルを読み込めないか、まだトランスコード中です。"
+#, python-format
+msgid "They've gifted %(num)d sub in the channel!"
+msgid_plural "They've gifted %(num)d subs in the channel!"
+msgstr[0] "このチャンネルで合計%(num)d個のサブをギフトしました!"
+
+#, python-format
+msgid "They've subscribed for %(months)s months!"
+msgstr "%(months)sか月間サブスクライブしています!"
+
+#, python-format
+msgid "They've subscribed for %(months)s months, currently on a %(streak)s month streak!"
+msgstr "%(months)sか月間サブスクライブ中、現在%(streak)sか月連続です!"
+
msgid "This admin user is protected and cannot be deleted."
msgstr "この管理者ユーザーは保護されているため削除できません。"
@@ -3398,6 +3492,9 @@ msgstr "オブジェクトデータベースによる"
msgid "active"
msgstr "有効"
+msgid "an anonymous user"
+msgstr "匿名ユーザー"
+
msgid "available"
msgstr "あり"
@@ -3454,6 +3551,9 @@ msgstr "なし"
msgid "not created"
msgstr "未作成"
+msgid "someone"
+msgstr "誰か"
+
#, python-format
msgid "today %(time)s"
msgstr "今日 %(time)s"
diff --git a/WEBUI/translations/ko/LC_MESSAGES/messages.mo b/WEBUI/translations/ko/LC_MESSAGES/messages.mo
index 731c6d7..8397b41 100644
Binary files a/WEBUI/translations/ko/LC_MESSAGES/messages.mo and b/WEBUI/translations/ko/LC_MESSAGES/messages.mo differ
diff --git a/WEBUI/translations/ko/LC_MESSAGES/messages.po b/WEBUI/translations/ko/LC_MESSAGES/messages.po
index e11b13f..fb94e19 100644
--- a/WEBUI/translations/ko/LC_MESSAGES/messages.po
+++ b/WEBUI/translations/ko/LC_MESSAGES/messages.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: StreamVault VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2026-11-12 15:41+0100\n"
+"POT-Creation-Date: 2026-11-12 15:50+0100\n"
"PO-Revision-Date: 2026-11-03 19:31+0100\n"
"Last-Translator: FULL NAME \n"
"Language: ko\n"
@@ -145,6 +145,64 @@ msgstr "%(num)s일"
msgid "%(provider)s does not support OAuth."
msgstr "%(provider)s은(는) OAuth를 지원하지 않습니다."
+#, python-format
+msgid "%(user)s converted from a Prime sub to a Tier %(tier)s sub!"
+msgstr "%(user)s 님이 Prime 구독에서 티어 %(tier)s 구독으로 전환했습니다!"
+
+#, python-format
+msgid "%(user)s donated %(amount)s to %(charity)s!"
+msgstr "%(user)s 님이 %(charity)s에 %(amount)s을(를) 기부했습니다!"
+
+#, python-format
+msgid "%(user)s gifted %(months)s months of Tier %(tier)s to %(recipient)s!"
+msgstr "%(user)s 님이 %(recipient)s 님에게 티어 %(tier)s 구독 %(months)s개월을 선물했습니다!"
+
+#, python-format
+msgid "%(user)s gifted a Tier %(tier)s sub to %(recipient)s!"
+msgstr "%(user)s 님이 %(recipient)s 님에게 티어 %(tier)s 구독을 선물했습니다!"
+
+#, python-format
+msgid "%(user)s is continuing the gift sub they got from %(other)s!"
+msgstr "%(user)s 님이 %(other)s 님에게 받은 선물 구독을 이어가고 있습니다!"
+
+#, python-format
+msgid "%(user)s is gifting %(num)d Tier %(tier)s sub to the community!"
+msgid_plural "%(user)s is gifting %(num)d Tier %(tier)s subs to the community!"
+msgstr[0] "%(user)s 님이 커뮤니티에 티어 %(tier)s 구독 %(num)d개를 선물합니다!"
+
+#, python-format
+msgid "%(user)s is new here. Say hello!"
+msgstr "%(user)s 님은 처음 오셨습니다. 인사해 주세요!"
+
+#, python-format
+msgid "%(user)s is paying forward the gift they got from %(other)s to %(recipient)s!"
+msgstr "%(user)s 님이 %(other)s 님에게 받은 선물을 %(recipient)s 님에게 전달합니다!"
+
+#, python-format
+msgid "%(user)s is paying forward the gift they got from %(other)s to the community!"
+msgstr "%(user)s 님이 %(other)s 님에게 받은 선물을 커뮤니티에 전달합니다!"
+
+#, python-format
+msgid "%(user)s is raiding with %(num)d viewer!"
+msgid_plural "%(user)s is raiding with %(num)d viewers!"
+msgstr[0] "%(user)s 님이 시청자 %(num)d명과 함께 레이드 중입니다!"
+
+#, python-format
+msgid "%(user)s just earned a new %(bits)s Bits badge!"
+msgstr "%(user)s 님이 새로운 %(bits)s 비트 배지를 획득했습니다!"
+
+#, python-format
+msgid "%(user)s subscribed at Tier %(tier)s."
+msgstr "%(user)s 님이 티어 %(tier)s(으)로 구독했습니다."
+
+#, python-format
+msgid "%(user)s subscribed with Prime."
+msgstr "%(user)s 님이 Prime으로 구독했습니다."
+
+#, python-format
+msgid "%(user)s watched %(num)s consecutive streams and sparked a watch streak!"
+msgstr "%(user)s 님이 %(num)s회 연속으로 방송을 시청하여 시청 연속 기록을 세웠습니다!"
+
msgid "0 = delete immediately once the cloud copy is verified."
msgstr "0 = 클라우드 사본이 확인되면 즉시 삭제합니다."
@@ -298,9 +356,15 @@ msgstr "%(path)s도 함께 전송합니다."
msgid "Also transfers preview images from %(path)s."
msgstr "%(path)s의 미리보기 이미지도 함께 전송합니다."
+msgid "An anonymous user"
+msgstr "익명의 사용자"
+
msgid "An upload for this file is already running."
msgstr "이 파일의 업로드가 이미 실행 중입니다."
+msgid "Announcement"
+msgstr "공지"
+
msgid "Anonymous"
msgstr "익명"
@@ -561,6 +625,9 @@ msgstr "채팅 기록: %(user)s"
msgid "Chat messages"
msgstr "채팅 메시지"
+msgid "Chat notice"
+msgstr "채팅 알림"
+
msgid "Chat replay"
msgstr "채팅 다시보기"
@@ -1212,6 +1279,9 @@ msgstr "채팅 필터…"
msgid "First"
msgstr "처음"
+msgid "First message"
+msgstr "첫 메시지"
+
msgid "First page"
msgstr "첫 페이지"
@@ -2051,6 +2121,10 @@ msgstr "페이지 탐색"
msgid "Pagination (top)"
msgstr "페이지 탐색 (상단)"
+#, python-format
+msgid "Paid for %(months)s months in advance."
+msgstr "%(months)s개월분을 선결제했습니다."
+
msgid "Password"
msgstr "비밀번호"
@@ -2359,6 +2433,10 @@ msgstr "새 비밀번호 확인"
msgid "Repeat password"
msgstr "비밀번호 확인"
+#, python-format
+msgid "Replying to @%(user)s: %(msg)s"
+msgstr "@%(user)s 님에게 답장: %(msg)s"
+
#, python-format
msgid "Request '%(username)s' denied and deleted."
msgstr "'%(username)s' 신청을 거부하고 삭제했습니다."
@@ -2624,6 +2702,9 @@ msgstr "크기"
msgid "Skip to content"
msgstr "본문으로 건너뛰기"
+msgid "Someone"
+msgstr "누군가"
+
msgid "Sort"
msgstr "정렬"
@@ -2939,6 +3020,19 @@ msgstr "시스템 관리자의 비밀번호는 환경 변수로만 변경할 수
msgid "The video file could not be loaded or is still being transcoded."
msgstr "동영상 파일을 불러올 수 없거나 아직 트랜스코딩 중입니다."
+#, python-format
+msgid "They've gifted %(num)d sub in the channel!"
+msgid_plural "They've gifted %(num)d subs in the channel!"
+msgstr[0] "이 채널에서 총 %(num)d개의 구독을 선물했습니다!"
+
+#, python-format
+msgid "They've subscribed for %(months)s months!"
+msgstr "%(months)s개월째 구독 중입니다!"
+
+#, python-format
+msgid "They've subscribed for %(months)s months, currently on a %(streak)s month streak!"
+msgstr "%(months)s개월째 구독 중이며, 현재 %(streak)s개월 연속입니다!"
+
msgid "This admin user is protected and cannot be deleted."
msgstr "이 관리자 사용자는 보호되어 있어 삭제할 수 없습니다."
@@ -3398,6 +3492,9 @@ msgstr "객체 데이터베이스 기준"
msgid "active"
msgstr "활성"
+msgid "an anonymous user"
+msgstr "익명의 사용자"
+
msgid "available"
msgstr "있음"
@@ -3454,6 +3551,9 @@ msgstr "안 함"
msgid "not created"
msgstr "생성 안 됨"
+msgid "someone"
+msgstr "누군가"
+
#, python-format
msgid "today %(time)s"
msgstr "오늘 %(time)s"
diff --git a/WEBUI/translations/messages.pot b/WEBUI/translations/messages.pot
index 0afcac0..ab03485 100644
--- a/WEBUI/translations/messages.pot
+++ b/WEBUI/translations/messages.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2026-11-12 15:41+0100\n"
+"POT-Creation-Date: 2026-11-12 15:50+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -74,7 +74,9 @@ msgid "%(count)s viewers"
msgstr ""
#, python-format
-msgid "%(day)s: %(total)s (login %(logins)s, pages %(pages)s, video %(media)s, chat %(chats)s)"
+msgid ""
+"%(day)s: %(total)s (login %(logins)s, pages %(pages)s, video %(media)s, "
+"chat %(chats)s)"
msgstr ""
#, python-format
@@ -149,6 +151,70 @@ msgstr ""
msgid "%(provider)s does not support OAuth."
msgstr ""
+#, python-format
+msgid "%(user)s converted from a Prime sub to a Tier %(tier)s sub!"
+msgstr ""
+
+#, python-format
+msgid "%(user)s donated %(amount)s to %(charity)s!"
+msgstr ""
+
+#, python-format
+msgid "%(user)s gifted %(months)s months of Tier %(tier)s to %(recipient)s!"
+msgstr ""
+
+#, python-format
+msgid "%(user)s gifted a Tier %(tier)s sub to %(recipient)s!"
+msgstr ""
+
+#, python-format
+msgid "%(user)s is continuing the gift sub they got from %(other)s!"
+msgstr ""
+
+#, python-format
+msgid "%(user)s is gifting %(num)d Tier %(tier)s sub to the community!"
+msgid_plural "%(user)s is gifting %(num)d Tier %(tier)s subs to the community!"
+msgstr[0] ""
+msgstr[1] ""
+
+#, python-format
+msgid "%(user)s is new here. Say hello!"
+msgstr ""
+
+#, python-format
+msgid ""
+"%(user)s is paying forward the gift they got from %(other)s to "
+"%(recipient)s!"
+msgstr ""
+
+#, python-format
+msgid ""
+"%(user)s is paying forward the gift they got from %(other)s to the "
+"community!"
+msgstr ""
+
+#, python-format
+msgid "%(user)s is raiding with %(num)d viewer!"
+msgid_plural "%(user)s is raiding with %(num)d viewers!"
+msgstr[0] ""
+msgstr[1] ""
+
+#, python-format
+msgid "%(user)s just earned a new %(bits)s Bits badge!"
+msgstr ""
+
+#, python-format
+msgid "%(user)s subscribed at Tier %(tier)s."
+msgstr ""
+
+#, python-format
+msgid "%(user)s subscribed with Prime."
+msgstr ""
+
+#, python-format
+msgid "%(user)s watched %(num)s consecutive streams and sparked a watch streak!"
+msgstr ""
+
msgid "0 = delete immediately once the cloud copy is verified."
msgstr ""
@@ -185,7 +251,9 @@ msgstr ""
msgid "A saved value is kept if the field is left empty."
msgstr ""
-msgid "AWS S3, Cloudflare R2, Backblaze B2, Wasabi, MinIO, Hetzner or any S3-compatible endpoint."
+msgid ""
+"AWS S3, Cloudflare R2, Backblaze B2, Wasabi, MinIO, Hetzner or any "
+"S3-compatible endpoint."
msgstr ""
msgid "Accept rules"
@@ -218,10 +286,14 @@ msgstr ""
msgid "Active shares"
msgstr ""
-msgid "Active: new streams & folders are unlocked automatically. Below you can disable individual folders (uncheck them)."
+msgid ""
+"Active: new streams & folders are unlocked automatically. Below you can "
+"disable individual folders (uncheck them)."
msgstr ""
-msgid "Active: new streams are unlocked automatically. Below you can disable / block individual folders."
+msgid ""
+"Active: new streams are unlocked automatically. Below you can disable / "
+"block individual folders."
msgstr ""
msgid "Actively monitored by the recorder core"
@@ -282,7 +354,9 @@ msgstr ""
msgid "All saved settings of this account will be deleted completely."
msgstr ""
-msgid "All streamers are currently offline. Past broadcasts can be watched in the archive."
+msgid ""
+"All streamers are currently offline. Past broadcasts can be watched in "
+"the archive."
msgstr ""
msgid "All streams (+ auto-new)"
@@ -302,9 +376,15 @@ msgstr ""
msgid "Also transfers preview images from %(path)s."
msgstr ""
+msgid "An anonymous user"
+msgstr ""
+
msgid "An upload for this file is already running."
msgstr ""
+msgid "Announcement"
+msgstr ""
+
msgid "Anonymous"
msgstr ""
@@ -347,7 +427,9 @@ msgid "Archive: %(name)s"
msgstr ""
#, python-format
-msgid "Archive: %(channels)s channels • %(vods)s VODs"
+msgid ""
+"Archive: %(channels)s channels • "
+"%(vods)s VODs"
msgstr ""
msgid "Archived airtime"
@@ -393,7 +475,9 @@ msgstr ""
msgid "Automatic (first active)"
msgstr ""
-msgid "Automatic and manual backups (SQLite database, configuration and metadata)."
+msgid ""
+"Automatic and manual backups (SQLite database, configuration and "
+"metadata)."
msgstr ""
msgid "Automatically unlock future streams & folders"
@@ -499,7 +583,9 @@ msgstr ""
msgid "Buffering…"
msgstr ""
-msgid "By default nothing is uploaded or deleted automatically. Enable these features deliberately."
+msgid ""
+"By default nothing is uploaded or deleted automatically. Enable these "
+"features deliberately."
msgstr ""
msgid "Cancel"
@@ -565,6 +651,9 @@ msgstr ""
msgid "Chat messages"
msgstr ""
+msgid "Chat notice"
+msgstr ""
+
msgid "Chat replay"
msgstr ""
@@ -672,7 +761,9 @@ msgstr ""
msgid "Collapse chat"
msgstr ""
-msgid "Configure dynamic permissions for streams, the video archive and channel access."
+msgid ""
+"Configure dynamic permissions for streams, the video archive and channel "
+"access."
msgstr ""
msgid "Configure recording channels and manage archive assignments"
@@ -844,7 +935,9 @@ msgid "Delete"
msgstr ""
#, python-format
-msgid "Delete %(name)s? The file is removed from the archive and cannot be restored."
+msgid ""
+"Delete %(name)s? The file is removed from the archive and cannot be "
+"restored."
msgstr ""
#, python-format
@@ -872,7 +965,9 @@ msgstr ""
msgid "Delete this streamer folder and all recordings? This cannot be undone."
msgstr ""
-msgid "Delete this video? The file is removed from the archive and cannot be restored."
+msgid ""
+"Delete this video? The file is removed from the archive and cannot be "
+"restored."
msgstr ""
msgid "Delete user"
@@ -955,7 +1050,9 @@ msgstr ""
msgid "Download all"
msgstr ""
-msgid "Download all VODs of this streamer as one archive? This can take a while for large channels."
+msgid ""
+"Download all VODs of this streamer as one archive? This can take a while "
+"for large channels."
msgstr ""
msgid "Download archive & VODs"
@@ -978,7 +1075,9 @@ msgid "Downloading... %(percent)s"
msgstr ""
#, python-format
-msgid "Drive quota active — do not press Play again (it extends the block). Seek back into the already loaded part, or wait about %(minutes)s min."
+msgid ""
+"Drive quota active — do not press Play again (it extends the block). Seek"
+" back into the already loaded part, or wait about %(minutes)s min."
msgstr ""
msgid "Drop a file here or click to select"
@@ -996,7 +1095,9 @@ msgstr ""
msgid "Duration:"
msgstr ""
-msgid "Dynamic user account and group management with granular stream and permission control"
+msgid ""
+"Dynamic user account and group management with granular stream and "
+"permission control"
msgstr ""
msgid "END"
@@ -1032,7 +1133,9 @@ msgstr ""
msgid "Empty = AWS. For R2, B2, Wasabi, MinIO etc. enter the S3 endpoint."
msgstr ""
-msgid "Empty = us-east-1 for AWS. Cloudflare R2 uses 'auto'; other S3 providers use their own region."
+msgid ""
+"Empty = us-east-1 for AWS. Cloudflare R2 uses 'auto'; other S3 providers "
+"use their own region."
msgstr ""
msgid "Empty folder"
@@ -1044,7 +1147,9 @@ msgstr ""
msgid "Enable maintenance mode"
msgstr ""
-msgid "Enable maintenance mode? Everyone except allowlisted users sees the maintenance page until you end it."
+msgid ""
+"Enable maintenance mode? Everyone except allowlisted users sees the "
+"maintenance page until you end it."
msgstr ""
msgid "Enable recording"
@@ -1147,7 +1252,9 @@ msgstr ""
msgid "Everywhere"
msgstr ""
-msgid "Existing members of this group are automatically assigned to the default group Viewer."
+msgid ""
+"Existing members of this group are automatically assigned to the default "
+"group Viewer."
msgstr ""
msgid "Exit fullscreen"
@@ -1216,6 +1323,9 @@ msgstr ""
msgid "First"
msgstr ""
+msgid "First message"
+msgstr ""
+
msgid "First page"
msgstr ""
@@ -1228,10 +1338,15 @@ msgstr ""
msgid "Folder not found or invalid."
msgstr ""
-msgid "For Google Drive, Dropbox and OneDrive this callback URL must be registered with the provider:"
+msgid ""
+"For Google Drive, Dropbox and OneDrive this callback URL must be "
+"registered with the provider:"
msgstr ""
-msgid "For encrypted credential storage, VOD_SECRET_KEY (or STORAGE_ENCRYPTION_KEY) must be set to a strong, unique value (at least 16 characters)."
+msgid ""
+"For encrypted credential storage, VOD_SECRET_KEY (or "
+"STORAGE_ENCRYPTION_KEY) must be set to a strong, unique value (at least "
+"16 characters)."
msgstr ""
msgid "Forget"
@@ -1293,13 +1408,17 @@ msgstr ""
msgid "Go to terms"
msgstr ""
-msgid "Google Cloud Console → APIs & Services → Credentials → OAuth client (web application)."
+msgid ""
+"Google Cloud Console → APIs & Services → Credentials → OAuth client (web "
+"application)."
msgstr ""
msgid "Google Drive / Workspace Drive with OAuth 2.0 and resumable uploads."
msgstr ""
-msgid "Google Drive, S3-compatible services, Dropbox, OneDrive and WebDAV. Credentials are stored with AES-GCM encryption."
+msgid ""
+"Google Drive, S3-compatible services, Dropbox, OneDrive and WebDAV. "
+"Credentials are stored with AES-GCM encryption."
msgstr ""
msgid "Group"
@@ -1319,7 +1438,9 @@ msgstr ""
msgid "Group deleted"
msgstr ""
-msgid "Group deleted successfully. Existing members were moved to the 'Viewer' group."
+msgid ""
+"Group deleted successfully. Existing members were moved to the 'Viewer' "
+"group."
msgstr ""
msgid "Group name"
@@ -1358,21 +1479,29 @@ msgstr ""
msgid "Hotkey"
msgstr ""
-msgid "House rules for the invite-only circle. No “official” framing — just binding terms of use."
+msgid ""
+"House rules for the invite-only circle. No “official” framing — just "
+"binding terms of use."
msgstr ""
-msgid "I have read and accept the StreamVault rules. I understand that violations lead to immediate revocation of access."
+msgid ""
+"I have read and accept the StreamVault rules. I understand that "
+"violations lead to immediate revocation of access."
msgstr ""
#, python-format
-msgid "I have read and accept the terms (version %(version)s). Violations can lead to immediate revocation of access."
+msgid ""
+"I have read and accept the terms (version %(version)s). Violations can "
+"lead to immediate revocation of access."
msgstr ""
msgid "IP"
msgstr ""
#, python-format
-msgid "If needed, set %(var)s to your public HTTPS address so the URL matches exactly."
+msgid ""
+"If needed, set %(var)s to your public HTTPS address so the URL matches "
+"exactly."
msgstr ""
msgid "Image controls"
@@ -1382,7 +1511,9 @@ msgstr ""
msgid "Inactive (%(count)s)"
msgstr ""
-msgid "Inactive: new streams stay blocked. Only folders checked below are unlocked."
+msgid ""
+"Inactive: new streams stay blocked. Only folders checked below are "
+"unlocked."
msgstr ""
msgid "Include audio"
@@ -1391,7 +1522,9 @@ msgstr ""
msgid "Info about the VOD…"
msgstr ""
-msgid "Information on data processing within the invite-only circle. No legal notice."
+msgid ""
+"Information on data processing within the invite-only circle. No legal "
+"notice."
msgstr ""
msgid "Initial password"
@@ -1721,7 +1854,9 @@ msgstr ""
msgid "Message"
msgstr ""
-msgid "Mobile live is temporarily disabled. Please use the desktop version or the installed app."
+msgid ""
+"Mobile live is temporarily disabled. Please use the desktop version or "
+"the installed app."
msgstr ""
msgid "Mods"
@@ -1784,19 +1919,25 @@ msgstr ""
msgid "Next page"
msgstr ""
-msgid "Nextcloud, ownCloud, Hetzner Storage Box or any WebDAV server with an app password."
+msgid ""
+"Nextcloud, ownCloud, Hetzner Storage Box or any WebDAV server with an app"
+" password."
msgstr ""
msgid "No (self-signed only)"
msgstr ""
-msgid "No VODs yet. Recordings appear here automatically as soon as a monitored channel goes live."
+msgid ""
+"No VODs yet. Recordings appear here automatically as soon as a monitored "
+"channel goes live."
msgstr ""
msgid "No acceptances yet."
msgstr ""
-msgid "No account yet? Accounts are issued by the administrators of this instance."
+msgid ""
+"No account yet? Accounts are issued by the administrators of this "
+"instance."
msgstr ""
msgid "No accounts match this filter."
@@ -1835,7 +1976,9 @@ msgstr ""
msgid "No cloud storage configured yet. Add a provider or use the console:"
msgstr ""
-msgid "No connection. As soon as the device is back online, the app continues loading the page normally."
+msgid ""
+"No connection. As soon as the device is back online, the app continues "
+"loading the page normally."
msgstr ""
msgid "No discovery streams right now."
@@ -1856,7 +1999,9 @@ msgstr ""
msgid "No live streams among the monitored channels. Use the search."
msgstr ""
-msgid "No local entries found for this search term. Press [Enter] to search the server."
+msgid ""
+"No local entries found for this search term. Press [Enter] to search the "
+"server."
msgstr ""
#, python-format
@@ -1893,7 +2038,9 @@ msgstr ""
msgid "No transcript output received"
msgstr ""
-msgid "No transcript yet. It is generated locally on the server and can take a while for long VODs."
+msgid ""
+"No transcript yet. It is generated locally on the server and can take a "
+"while for long VODs."
msgstr ""
msgid "No transcription active"
@@ -2030,11 +2177,15 @@ msgid "Page %(page)s of %(total)s (%(count)s VODs)"
msgstr ""
#, python-format
-msgid "Page %(page)s / %(pages)s (%(count)s VODs)"
+msgid ""
+"Page %(page)s / %(pages)s (%(count)s "
+"VODs)"
msgstr ""
#, python-format
-msgid "Page %(page)s of %(pages)s • %(count)s VODs in quick access"
+msgid ""
+"Page %(page)s of %(pages)s • "
+"%(count)s VODs in quick access"
msgstr ""
msgid "Page navigation"
@@ -2055,6 +2206,10 @@ msgstr ""
msgid "Pagination (top)"
msgstr ""
+#, python-format
+msgid "Paid for %(months)s months in advance."
+msgstr ""
+
msgid "Password"
msgstr ""
@@ -2256,7 +2411,10 @@ msgstr ""
msgid "Recently watched"
msgstr ""
-msgid "Recommended browser-source URLs. Replace TOKEN with the value of TTS_OVERLAY_TOKEN or ALERT_OVERLAY_TOKEN. The part after # stays in OBS and reaches the server only as a request header, never in a log line."
+msgid ""
+"Recommended browser-source URLs. Replace TOKEN with the value of "
+"TTS_OVERLAY_TOKEN or ALERT_OVERLAY_TOKEN. The part after # stays in OBS "
+"and reaches the server only as a request header, never in a log line."
msgstr ""
msgid "Recommended. Prevents data loss from incomplete uploads."
@@ -2304,7 +2462,9 @@ msgid "Recording not available."
msgstr ""
#, python-format
-msgid "Recording stopped: %(count)s source(s) for %(archive)s disabled. The process will be terminated at the next check."
+msgid ""
+"Recording stopped: %(count)s source(s) for %(archive)s disabled. The "
+"process will be terminated at the next check."
msgstr ""
#, python-format
@@ -2329,7 +2489,9 @@ msgid "Remove channel"
msgstr ""
#, python-format
-msgid "Remove channel %(name)s? Monitoring stops; existing recordings stay in the archive."
+msgid ""
+"Remove channel %(name)s? Monitoring stops; existing recordings stay in "
+"the archive."
msgstr ""
msgid "Remove only the database entry?"
@@ -2345,7 +2507,9 @@ msgstr ""
msgid "Rename file"
msgstr ""
-msgid "Rename the file? Existing links and shares to the old file name stop working."
+msgid ""
+"Rename the file? Existing links and shares to the old file name stop "
+"working."
msgstr ""
msgid "Renamed."
@@ -2363,6 +2527,10 @@ msgstr ""
msgid "Repeat password"
msgstr ""
+#, python-format
+msgid "Replying to @%(user)s: %(msg)s"
+msgstr ""
+
#, python-format
msgid "Request '%(username)s' denied and deleted."
msgstr ""
@@ -2373,7 +2541,9 @@ msgstr ""
msgid "Restart services"
msgstr ""
-msgid "Restart the recorder and chat logger now? Running recordings are interrupted for a few seconds."
+msgid ""
+"Restart the recorder and chat logger now? Running recordings are "
+"interrupted for a few seconds."
msgstr ""
msgid "Restore"
@@ -2406,7 +2576,10 @@ msgstr ""
msgid "Role access grant not found."
msgstr ""
-msgid "Role model: full access (admin), archive viewer (viewer), live only (live_only) & Discord member."
+msgid ""
+"Role model: full access (admin), archive viewer "
+"(viewer), live only (live_only) & Discord "
+"member."
msgstr ""
msgid "Rules"
@@ -2577,7 +2750,9 @@ msgid "Set"
msgstr ""
#, python-format
-msgid "Set %(key)s (at least 16 characters) or %(alt_key)s before credentials are saved."
+msgid ""
+"Set %(key)s (at least 16 characters) or %(alt_key)s before credentials "
+"are saved."
msgstr ""
msgid "Set end to current position"
@@ -2610,7 +2785,9 @@ msgstr ""
msgid "Sharing failed:"
msgstr ""
-msgid "Short rules / Discord house rules. Part of the terms; accepted together with the consent below."
+msgid ""
+"Short rules / Discord house rules. Part of the terms; accepted together "
+"with the consent below."
msgstr ""
msgid "Shown in the navigation, sidebar and on streamer cards."
@@ -2619,7 +2796,9 @@ msgstr ""
msgid "Sign in"
msgstr ""
-msgid "Sign out on every device? Every session of this account ends, including this one."
+msgid ""
+"Sign out on every device? Every session of this account ends, including "
+"this one."
msgstr ""
msgid "Size"
@@ -2628,6 +2807,9 @@ msgstr ""
msgid "Skip to content"
msgstr ""
+msgid "Someone"
+msgstr ""
+
msgid "Sort"
msgstr ""
@@ -2647,7 +2829,9 @@ msgstr ""
msgid "Source %(name)s not found."
msgstr ""
-msgid "Stale lock removed, transcription queued in the worker. The result appears on the next page load after completion."
+msgid ""
+"Stale lock removed, transcription queued in the worker. The result "
+"appears on the next page load after completion."
msgstr ""
msgid "Start"
@@ -2680,7 +2864,9 @@ msgstr ""
msgid "Status"
msgstr ""
-msgid "Status, progress and actions of the most recently synced files. “Forget” only removes the database entry."
+msgid ""
+"Status, progress and actions of the most recently synced files. “Forget” "
+"only removes the database entry."
msgstr ""
msgid "Status:"
@@ -2745,10 +2931,14 @@ msgstr ""
msgid "Stream stopped — reconnecting…"
msgstr ""
-msgid "StreamVault is temporarily unavailable due to maintenance. Please be patient — the site will be back soon."
+msgid ""
+"StreamVault is temporarily unavailable due to maintenance. Please be "
+"patient — the site will be back soon."
msgstr ""
-msgid "StreamVault is temporarily unavailable due to ongoing work. Please be patient — the site will be available again later."
+msgid ""
+"StreamVault is temporarily unavailable due to ongoing work. Please be "
+"patient — the site will be available again later."
msgstr ""
msgid "Streamer"
@@ -2850,13 +3040,18 @@ msgid "Terms & rules"
msgstr ""
#, python-format
-msgid "Terms (version %(terms)s) including house rules (%(rules)s) not yet accepted — approval only after consent on the website or on re-registration."
+msgid ""
+"Terms (version %(terms)s) including house rules (%(rules)s) not yet "
+"accepted — approval only after consent on the website or on re-"
+"registration."
msgstr ""
msgid "Terms accepted. Continuing to the dashboard…"
msgstr ""
-msgid "Terms of use and house rules for the invite-only circle. No usage rights towards rights holders or platforms."
+msgid ""
+"Terms of use and house rules for the invite-only circle. No usage rights "
+"towards rights holders or platforms."
msgstr ""
msgid "Terms version outdated or not accepted. Acceptance required at /agb."
@@ -2871,10 +3066,14 @@ msgstr ""
msgid "Text-to-speech overlay"
msgstr ""
-msgid "Thanks for your understanding. Authorized users can sign in via the login page."
+msgid ""
+"Thanks for your understanding. Authorized users can sign in via the login"
+" page."
msgstr ""
-msgid "The 'can_manage_users' permission cannot be removed from the Administrator group."
+msgid ""
+"The 'can_manage_users' permission cannot be removed from the "
+"Administrator group."
msgstr ""
msgid "The OAuth link has expired. Please start again."
@@ -2898,7 +3097,9 @@ msgstr ""
msgid "The local file has already been removed. Please restore it first."
msgstr ""
-msgid "The mobile version of StreamVault is currently under maintenance. Please use the desktop version."
+msgid ""
+"The mobile version of StreamVault is currently under maintenance. Please "
+"use the desktop version."
msgstr ""
msgid "The name of system groups cannot be changed."
@@ -2919,10 +3120,14 @@ msgstr ""
msgid "The provider name must be at most 80 characters long."
msgstr ""
-msgid "The provider type cannot be changed while archive objects are assigned. Create a new provider instead."
+msgid ""
+"The provider type cannot be changed while archive objects are assigned. "
+"Create a new provider instead."
msgstr ""
-msgid "The recorder worker performs the sync. Unchanged files that were already uploaded are skipped."
+msgid ""
+"The recorder worker performs the sync. Unchanged files that were already "
+"uploaded are skipped."
msgstr ""
msgid "The remote path must not be empty."
@@ -2931,18 +3136,38 @@ msgstr ""
msgid "The selected default provider does not exist."
msgstr ""
-msgid "The selected default provider is disabled. Enable it first or choose another provider."
+msgid ""
+"The selected default provider is disabled. Enable it first or choose "
+"another provider."
msgstr ""
msgid "The selected group does not exist."
msgstr ""
-msgid "The system administrator's password can only be changed via environment variables."
+msgid ""
+"The system administrator's password can only be changed via environment "
+"variables."
msgstr ""
msgid "The video file could not be loaded or is still being transcoded."
msgstr ""
+#, python-format
+msgid "They've gifted %(num)d sub in the channel!"
+msgid_plural "They've gifted %(num)d subs in the channel!"
+msgstr[0] ""
+msgstr[1] ""
+
+#, python-format
+msgid "They've subscribed for %(months)s months!"
+msgstr ""
+
+#, python-format
+msgid ""
+"They've subscribed for %(months)s months, currently on a %(streak)s month"
+" streak!"
+msgstr ""
+
msgid "This admin user is protected and cannot be deleted."
msgstr ""
@@ -2961,7 +3186,9 @@ msgstr ""
msgid "Time (UTC)"
msgstr ""
-msgid "Tip: a shared archive folder combines Twitch and YouTube streams of the same streamer."
+msgid ""
+"Tip: a shared archive folder combines Twitch and YouTube streams of the "
+"same streamer."
msgstr ""
msgid "Title & description"
@@ -2980,7 +3207,9 @@ msgstr ""
msgid "Today, %(time)s"
msgstr ""
-msgid "Too many VODs are playing right now. Live is not affected. Try again in a few minutes — a slot frees up as soon as someone closes the player."
+msgid ""
+"Too many VODs are playing right now. Live is not affected. Try again in a"
+" few minutes — a slot frees up as soon as someone closes the player."
msgstr ""
#, python-format
@@ -2989,7 +3218,9 @@ msgid_plural "Too many login attempts. Please try again in %(num)d minutes."
msgstr[0] ""
msgstr[1] ""
-msgid "Top streams are currently unavailable. Use monitored channels or the search."
+msgid ""
+"Top streams are currently unavailable. Use monitored channels or the "
+"search."
msgstr ""
msgid "Total in period"
@@ -3062,7 +3293,9 @@ msgstr ""
msgid "Transcription is already running. Please reload later."
msgstr ""
-msgid "Transcription queued in the worker. The result appears on the next page load after completion."
+msgid ""
+"Transcription queued in the worker. The result appears on the next page "
+"load after completion."
msgstr ""
msgid "Transcription running..."
@@ -3228,14 +3461,18 @@ msgstr ""
msgid "Username is required."
msgstr ""
-msgid "Username must be 3-32 alphanumeric characters (hyphens and underscores allowed)."
+msgid ""
+"Username must be 3-32 alphanumeric characters (hyphens and underscores "
+"allowed)."
msgstr ""
msgid "Users & groups"
msgstr ""
#, python-format
-msgid "Users A–Z · last %(days)s days (%(since)s – %(until)s). Heatmap = presence, bars = login/pages/video/chat per day."
+msgid ""
+"Users A–Z · last %(days)s days (%(since)s – %(until)s). Heatmap = "
+"presence, bars = login/pages/video/chat per day."
msgstr ""
msgid "VOD access grants are currently only available for members."
@@ -3254,7 +3491,9 @@ msgstr ""
msgid "VOD recordings (%(count)s)"
msgstr ""
-msgid "VOD_SECRET_KEY must be set to a strong value before credentials can be stored."
+msgid ""
+"VOD_SECRET_KEY must be set to a strong value before credentials can be "
+"stored."
msgstr ""
msgid "VODs are recorded to disk"
@@ -3347,7 +3586,9 @@ msgstr ""
msgid "WebDAV base URL must not contain a query or fragment."
msgstr ""
-msgid "WebDAV base URL must not contain credentials; use the username and password fields."
+msgid ""
+"WebDAV base URL must not contain credentials; use the username and "
+"password fields."
msgstr ""
msgid "Webhook active."
@@ -3403,6 +3644,9 @@ msgstr ""
msgid "active"
msgstr ""
+msgid "an anonymous user"
+msgstr ""
+
msgid "available"
msgstr ""
@@ -3418,7 +3662,9 @@ msgstr ""
msgid "done – please reload the page"
msgstr ""
-msgid "drive.file = only files created by StreamVault (recommended). For existing folders, use https://www.googleapis.com/auth/drive if needed."
+msgid ""
+"drive.file = only files created by StreamVault (recommended). For "
+"existing folders, use https://www.googleapis.com/auth/drive if needed."
msgstr ""
msgid "e.g. Google Drive archive"
@@ -3459,6 +3705,9 @@ msgstr ""
msgid "not created"
msgstr ""
+msgid "someone"
+msgstr ""
+
#, python-format
msgid "today %(time)s"
msgstr ""
diff --git a/WEBUI/translations/nl/LC_MESSAGES/messages.mo b/WEBUI/translations/nl/LC_MESSAGES/messages.mo
index 67c9ad2..0cc63e1 100644
Binary files a/WEBUI/translations/nl/LC_MESSAGES/messages.mo and b/WEBUI/translations/nl/LC_MESSAGES/messages.mo differ
diff --git a/WEBUI/translations/nl/LC_MESSAGES/messages.po b/WEBUI/translations/nl/LC_MESSAGES/messages.po
index e0ee794..e121c47 100644
--- a/WEBUI/translations/nl/LC_MESSAGES/messages.po
+++ b/WEBUI/translations/nl/LC_MESSAGES/messages.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: StreamVault VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2026-11-12 15:41+0100\n"
+"POT-Creation-Date: 2026-11-12 15:50+0100\n"
"PO-Revision-Date: 2026-11-03 19:31+0100\n"
"Last-Translator: FULL NAME \n"
"Language: nl\n"
@@ -151,6 +151,66 @@ msgstr "%(num)s dagen"
msgid "%(provider)s does not support OAuth."
msgstr "%(provider)s ondersteunt geen OAuth."
+#, python-format
+msgid "%(user)s converted from a Prime sub to a Tier %(tier)s sub!"
+msgstr "%(user)s is overgestapt van een Prime-sub naar een Tier %(tier)s-sub!"
+
+#, python-format
+msgid "%(user)s donated %(amount)s to %(charity)s!"
+msgstr "%(user)s heeft %(amount)s gedoneerd aan %(charity)s!"
+
+#, python-format
+msgid "%(user)s gifted %(months)s months of Tier %(tier)s to %(recipient)s!"
+msgstr "%(user)s heeft %(recipient)s %(months)s maanden Tier %(tier)s cadeau gedaan!"
+
+#, python-format
+msgid "%(user)s gifted a Tier %(tier)s sub to %(recipient)s!"
+msgstr "%(user)s heeft %(recipient)s een Tier %(tier)s-sub cadeau gedaan!"
+
+#, python-format
+msgid "%(user)s is continuing the gift sub they got from %(other)s!"
+msgstr "%(user)s zet de cadeausub van %(other)s voort!"
+
+#, python-format
+msgid "%(user)s is gifting %(num)d Tier %(tier)s sub to the community!"
+msgid_plural "%(user)s is gifting %(num)d Tier %(tier)s subs to the community!"
+msgstr[0] "%(user)s geeft %(num)d Tier %(tier)s-sub weg aan de community!"
+msgstr[1] "%(user)s geeft %(num)d Tier %(tier)s-subs weg aan de community!"
+
+#, python-format
+msgid "%(user)s is new here. Say hello!"
+msgstr "%(user)s is hier nieuw. Zeg hallo!"
+
+#, python-format
+msgid "%(user)s is paying forward the gift they got from %(other)s to %(recipient)s!"
+msgstr "%(user)s geeft het cadeau van %(other)s door aan %(recipient)s!"
+
+#, python-format
+msgid "%(user)s is paying forward the gift they got from %(other)s to the community!"
+msgstr "%(user)s geeft het cadeau van %(other)s door aan de community!"
+
+#, python-format
+msgid "%(user)s is raiding with %(num)d viewer!"
+msgid_plural "%(user)s is raiding with %(num)d viewers!"
+msgstr[0] "%(user)s raidt met %(num)d kijker!"
+msgstr[1] "%(user)s raidt met %(num)d kijkers!"
+
+#, python-format
+msgid "%(user)s just earned a new %(bits)s Bits badge!"
+msgstr "%(user)s heeft net een nieuwe %(bits)s Bits-badge verdiend!"
+
+#, python-format
+msgid "%(user)s subscribed at Tier %(tier)s."
+msgstr "%(user)s heeft zich geabonneerd op Tier %(tier)s."
+
+#, python-format
+msgid "%(user)s subscribed with Prime."
+msgstr "%(user)s heeft zich geabonneerd met Prime."
+
+#, python-format
+msgid "%(user)s watched %(num)s consecutive streams and sparked a watch streak!"
+msgstr "%(user)s heeft %(num)s streams op rij gekeken en een kijkreeks gestart!"
+
msgid "0 = delete immediately once the cloud copy is verified."
msgstr "0 = direct verwijderen zodra de cloudkopie is geverifieerd."
@@ -304,9 +364,15 @@ msgstr "Draagt ook %(path)s over."
msgid "Also transfers preview images from %(path)s."
msgstr "Draagt ook voorbeeldafbeeldingen uit %(path)s over."
+msgid "An anonymous user"
+msgstr "Een anonieme gebruiker"
+
msgid "An upload for this file is already running."
msgstr "Er loopt al een upload voor dit bestand."
+msgid "Announcement"
+msgstr "Aankondiging"
+
msgid "Anonymous"
msgstr "Anoniem"
@@ -567,6 +633,9 @@ msgstr "Chatgeschiedenis: %(user)s"
msgid "Chat messages"
msgstr "Chatberichten"
+msgid "Chat notice"
+msgstr "Chatmelding"
+
msgid "Chat replay"
msgstr "Chatreplay"
@@ -1218,6 +1287,9 @@ msgstr "Chat filteren…"
msgid "First"
msgstr "Eerste"
+msgid "First message"
+msgstr "Eerste bericht"
+
msgid "First page"
msgstr "Eerste pagina"
@@ -2057,6 +2129,10 @@ msgstr "Paginering"
msgid "Pagination (top)"
msgstr "Paginering (boven)"
+#, python-format
+msgid "Paid for %(months)s months in advance."
+msgstr "%(months)s maanden vooruitbetaald."
+
msgid "Password"
msgstr "Wachtwoord"
@@ -2365,6 +2441,10 @@ msgstr "Nieuw wachtwoord herhalen"
msgid "Repeat password"
msgstr "Wachtwoord herhalen"
+#, python-format
+msgid "Replying to @%(user)s: %(msg)s"
+msgstr "Antwoord aan @%(user)s: %(msg)s"
+
#, python-format
msgid "Request '%(username)s' denied and deleted."
msgstr "Aanvraag '%(username)s' afgewezen en verwijderd."
@@ -2630,6 +2710,9 @@ msgstr "Grootte"
msgid "Skip to content"
msgstr "Naar inhoud springen"
+msgid "Someone"
+msgstr "Iemand"
+
msgid "Sort"
msgstr "Sorteren"
@@ -2945,6 +3028,20 @@ msgstr "Het wachtwoord van de systeembeheerder kan alleen via omgevingsvariabele
msgid "The video file could not be loaded or is still being transcoded."
msgstr "Het videobestand kon niet worden geladen of wordt nog getranscodeerd."
+#, python-format
+msgid "They've gifted %(num)d sub in the channel!"
+msgid_plural "They've gifted %(num)d subs in the channel!"
+msgstr[0] "In totaal %(num)d sub weggegeven in dit kanaal!"
+msgstr[1] "In totaal %(num)d subs weggegeven in dit kanaal!"
+
+#, python-format
+msgid "They've subscribed for %(months)s months!"
+msgstr "Al %(months)s maanden geabonneerd!"
+
+#, python-format
+msgid "They've subscribed for %(months)s months, currently on a %(streak)s month streak!"
+msgstr "Al %(months)s maanden geabonneerd, momenteel %(streak)s maanden op rij!"
+
msgid "This admin user is protected and cannot be deleted."
msgstr "Deze admingebruiker is beschermd en kan niet worden verwijderd."
@@ -3405,6 +3502,9 @@ msgstr "volgens de objectdatabase"
msgid "active"
msgstr "actief"
+msgid "an anonymous user"
+msgstr "een anonieme gebruiker"
+
msgid "available"
msgstr "aanwezig"
@@ -3461,6 +3561,9 @@ msgstr "nooit"
msgid "not created"
msgstr "niet aangemaakt"
+msgid "someone"
+msgstr "iemand"
+
#, python-format
msgid "today %(time)s"
msgstr "vandaag %(time)s"
diff --git a/WEBUI/translations/pl/LC_MESSAGES/messages.mo b/WEBUI/translations/pl/LC_MESSAGES/messages.mo
index a61d9f4..5c78b3f 100644
Binary files a/WEBUI/translations/pl/LC_MESSAGES/messages.mo and b/WEBUI/translations/pl/LC_MESSAGES/messages.mo differ
diff --git a/WEBUI/translations/pl/LC_MESSAGES/messages.po b/WEBUI/translations/pl/LC_MESSAGES/messages.po
index 1e79be6..bffd0ee 100644
--- a/WEBUI/translations/pl/LC_MESSAGES/messages.po
+++ b/WEBUI/translations/pl/LC_MESSAGES/messages.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: StreamVault VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2026-11-12 15:41+0100\n"
+"POT-Creation-Date: 2026-11-12 15:50+0100\n"
"PO-Revision-Date: 2026-11-03 19:31+0100\n"
"Last-Translator: FULL NAME \n"
"Language: pl\n"
@@ -157,6 +157,68 @@ msgstr "Dni: %(num)s"
msgid "%(provider)s does not support OAuth."
msgstr "%(provider)s nie obsługuje OAuth."
+#, python-format
+msgid "%(user)s converted from a Prime sub to a Tier %(tier)s sub!"
+msgstr "%(user)s zmienia subskrypcję Prime na subskrypcję poziomu %(tier)s!"
+
+#, python-format
+msgid "%(user)s donated %(amount)s to %(charity)s!"
+msgstr "%(user)s wpłaca %(amount)s na rzecz %(charity)s!"
+
+#, python-format
+msgid "%(user)s gifted %(months)s months of Tier %(tier)s to %(recipient)s!"
+msgstr "%(user)s funduje %(months)s mies. subskrypcji poziomu %(tier)s dla %(recipient)s!"
+
+#, python-format
+msgid "%(user)s gifted a Tier %(tier)s sub to %(recipient)s!"
+msgstr "%(user)s funduje subskrypcję poziomu %(tier)s dla %(recipient)s!"
+
+#, python-format
+msgid "%(user)s is continuing the gift sub they got from %(other)s!"
+msgstr "%(user)s kontynuuje subskrypcję otrzymaną w prezencie od %(other)s!"
+
+#, python-format
+msgid "%(user)s is gifting %(num)d Tier %(tier)s sub to the community!"
+msgid_plural "%(user)s is gifting %(num)d Tier %(tier)s subs to the community!"
+msgstr[0] "%(user)s funduje %(num)d subskrypcję poziomu %(tier)s dla społeczności!"
+msgstr[1] "%(user)s funduje %(num)d subskrypcje poziomu %(tier)s dla społeczności!"
+msgstr[2] "%(user)s funduje %(num)d subskrypcji poziomu %(tier)s dla społeczności!"
+
+#, python-format
+msgid "%(user)s is new here. Say hello!"
+msgstr "%(user)s pojawia się tu pierwszy raz. Przywitaj się!"
+
+#, python-format
+msgid "%(user)s is paying forward the gift they got from %(other)s to %(recipient)s!"
+msgstr "%(user)s przekazuje dalej prezent od %(other)s dla %(recipient)s!"
+
+#, python-format
+msgid "%(user)s is paying forward the gift they got from %(other)s to the community!"
+msgstr "%(user)s przekazuje dalej prezent od %(other)s społeczności!"
+
+#, python-format
+msgid "%(user)s is raiding with %(num)d viewer!"
+msgid_plural "%(user)s is raiding with %(num)d viewers!"
+msgstr[0] "%(user)s robi raid z %(num)d widzem!"
+msgstr[1] "%(user)s robi raid z %(num)d widzami!"
+msgstr[2] "%(user)s robi raid z %(num)d widzami!"
+
+#, python-format
+msgid "%(user)s just earned a new %(bits)s Bits badge!"
+msgstr "%(user)s zdobywa nową odznakę %(bits)s Bits!"
+
+#, python-format
+msgid "%(user)s subscribed at Tier %(tier)s."
+msgstr "%(user)s subskrybuje na poziomie %(tier)s."
+
+#, python-format
+msgid "%(user)s subscribed with Prime."
+msgstr "%(user)s subskrybuje z Prime."
+
+#, python-format
+msgid "%(user)s watched %(num)s consecutive streams and sparked a watch streak!"
+msgstr "%(user)s ogląda już %(num)s transmisji z rzędu i rozpoczyna serię oglądania!"
+
msgid "0 = delete immediately once the cloud copy is verified."
msgstr "0 = usuń natychmiast po zweryfikowaniu kopii w chmurze."
@@ -310,9 +372,15 @@ msgstr "Przenosi również %(path)s."
msgid "Also transfers preview images from %(path)s."
msgstr "Przenosi również miniatury z %(path)s."
+msgid "An anonymous user"
+msgstr "Anonimowy użytkownik"
+
msgid "An upload for this file is already running."
msgstr "Przesyłanie tego pliku już trwa."
+msgid "Announcement"
+msgstr "Ogłoszenie"
+
msgid "Anonymous"
msgstr "Anonim"
@@ -573,6 +641,9 @@ msgstr "Historia czatu: %(user)s"
msgid "Chat messages"
msgstr "Wiadomości czatu"
+msgid "Chat notice"
+msgstr "Powiadomienie czatu"
+
msgid "Chat replay"
msgstr "Powtórka czatu"
@@ -1224,6 +1295,9 @@ msgstr "Filtruj czat…"
msgid "First"
msgstr "Pierwsza"
+msgid "First message"
+msgstr "Pierwsza wiadomość"
+
msgid "First page"
msgstr "Pierwsza strona"
@@ -2063,6 +2137,10 @@ msgstr "Paginacja"
msgid "Pagination (top)"
msgstr "Paginacja (góra)"
+#, python-format
+msgid "Paid for %(months)s months in advance."
+msgstr "Opłacone z góry na %(months)s mies."
+
msgid "Password"
msgstr "Hasło"
@@ -2371,6 +2449,10 @@ msgstr "Powtórz nowe hasło"
msgid "Repeat password"
msgstr "Powtórz hasło"
+#, python-format
+msgid "Replying to @%(user)s: %(msg)s"
+msgstr "Odpowiedź do @%(user)s: %(msg)s"
+
#, python-format
msgid "Request '%(username)s' denied and deleted."
msgstr "Wniosek '%(username)s' odrzucono i usunięto."
@@ -2636,6 +2718,9 @@ msgstr "Rozmiar"
msgid "Skip to content"
msgstr "Przejdź do treści"
+msgid "Someone"
+msgstr "Ktoś"
+
msgid "Sort"
msgstr "Sortuj"
@@ -2951,6 +3036,21 @@ msgstr "Hasło administratora systemu można zmienić tylko przez zmienne środo
msgid "The video file could not be loaded or is still being transcoded."
msgstr "Nie udało się wczytać pliku wideo lub jest on jeszcze transkodowany."
+#, python-format
+msgid "They've gifted %(num)d sub in the channel!"
+msgid_plural "They've gifted %(num)d subs in the channel!"
+msgstr[0] "Łącznie %(num)d podarowana subskrypcja na kanale!"
+msgstr[1] "Łącznie %(num)d podarowane subskrypcje na kanale!"
+msgstr[2] "Łącznie %(num)d podarowanych subskrypcji na kanale!"
+
+#, python-format
+msgid "They've subscribed for %(months)s months!"
+msgstr "Subskrybuje od %(months)s mies.!"
+
+#, python-format
+msgid "They've subscribed for %(months)s months, currently on a %(streak)s month streak!"
+msgstr "Subskrybuje od %(months)s mies., obecna seria: %(streak)s mies.!"
+
msgid "This admin user is protected and cannot be deleted."
msgstr "Ten administrator jest chroniony i nie można go usunąć."
@@ -3412,6 +3512,9 @@ msgstr "według bazy obiektów"
msgid "active"
msgstr "aktywne"
+msgid "an anonymous user"
+msgstr "anonimowego użytkownika"
+
msgid "available"
msgstr "dostępne"
@@ -3468,6 +3571,9 @@ msgstr "nigdy"
msgid "not created"
msgstr "nie utworzono"
+msgid "someone"
+msgstr "kogoś"
+
#, python-format
msgid "today %(time)s"
msgstr "dzisiaj %(time)s"
diff --git a/WEBUI/translations/pt_BR/LC_MESSAGES/messages.mo b/WEBUI/translations/pt_BR/LC_MESSAGES/messages.mo
index da76047..a0ea6aa 100644
Binary files a/WEBUI/translations/pt_BR/LC_MESSAGES/messages.mo and b/WEBUI/translations/pt_BR/LC_MESSAGES/messages.mo differ
diff --git a/WEBUI/translations/pt_BR/LC_MESSAGES/messages.po b/WEBUI/translations/pt_BR/LC_MESSAGES/messages.po
index f160745..3383a4a 100644
--- a/WEBUI/translations/pt_BR/LC_MESSAGES/messages.po
+++ b/WEBUI/translations/pt_BR/LC_MESSAGES/messages.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: StreamVault VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2026-11-12 15:41+0100\n"
+"POT-Creation-Date: 2026-11-12 15:50+0100\n"
"PO-Revision-Date: 2026-11-03 19:31+0100\n"
"Last-Translator: FULL NAME \n"
"Language: pt_BR\n"
@@ -151,6 +151,66 @@ msgstr "%(num)s dias"
msgid "%(provider)s does not support OAuth."
msgstr "%(provider)s não oferece suporte a OAuth."
+#, python-format
+msgid "%(user)s converted from a Prime sub to a Tier %(tier)s sub!"
+msgstr "%(user)s mudou de uma inscrição Prime para uma de Nível %(tier)s!"
+
+#, python-format
+msgid "%(user)s donated %(amount)s to %(charity)s!"
+msgstr "%(user)s doou %(amount)s para %(charity)s!"
+
+#, python-format
+msgid "%(user)s gifted %(months)s months of Tier %(tier)s to %(recipient)s!"
+msgstr "%(user)s presenteou %(recipient)s com %(months)s meses de Nível %(tier)s!"
+
+#, python-format
+msgid "%(user)s gifted a Tier %(tier)s sub to %(recipient)s!"
+msgstr "%(user)s presenteou %(recipient)s com uma inscrição de Nível %(tier)s!"
+
+#, python-format
+msgid "%(user)s is continuing the gift sub they got from %(other)s!"
+msgstr "%(user)s está mantendo a inscrição de presente que ganhou de %(other)s!"
+
+#, python-format
+msgid "%(user)s is gifting %(num)d Tier %(tier)s sub to the community!"
+msgid_plural "%(user)s is gifting %(num)d Tier %(tier)s subs to the community!"
+msgstr[0] "%(user)s está presenteando a comunidade com %(num)d inscrição de Nível %(tier)s!"
+msgstr[1] "%(user)s está presenteando a comunidade com %(num)d inscrições de Nível %(tier)s!"
+
+#, python-format
+msgid "%(user)s is new here. Say hello!"
+msgstr "%(user)s é novo por aqui. Diga oi!"
+
+#, python-format
+msgid "%(user)s is paying forward the gift they got from %(other)s to %(recipient)s!"
+msgstr "%(user)s está repassando o presente que ganhou de %(other)s para %(recipient)s!"
+
+#, python-format
+msgid "%(user)s is paying forward the gift they got from %(other)s to the community!"
+msgstr "%(user)s está repassando o presente que ganhou de %(other)s para a comunidade!"
+
+#, python-format
+msgid "%(user)s is raiding with %(num)d viewer!"
+msgid_plural "%(user)s is raiding with %(num)d viewers!"
+msgstr[0] "%(user)s está fazendo uma raid com %(num)d espectador!"
+msgstr[1] "%(user)s está fazendo uma raid com %(num)d espectadores!"
+
+#, python-format
+msgid "%(user)s just earned a new %(bits)s Bits badge!"
+msgstr "%(user)s acabou de ganhar um novo emblema de %(bits)s Bits!"
+
+#, python-format
+msgid "%(user)s subscribed at Tier %(tier)s."
+msgstr "%(user)s se inscreveu no Nível %(tier)s."
+
+#, python-format
+msgid "%(user)s subscribed with Prime."
+msgstr "%(user)s se inscreveu com o Prime."
+
+#, python-format
+msgid "%(user)s watched %(num)s consecutive streams and sparked a watch streak!"
+msgstr "%(user)s assistiu a %(num)s transmissões seguidas e iniciou uma sequência!"
+
msgid "0 = delete immediately once the cloud copy is verified."
msgstr "0 = excluir imediatamente após a cópia na nuvem ser verificada."
@@ -304,9 +364,15 @@ msgstr "Também transfere %(path)s."
msgid "Also transfers preview images from %(path)s."
msgstr "Também transfere as miniaturas de %(path)s."
+msgid "An anonymous user"
+msgstr "Um usuário anônimo"
+
msgid "An upload for this file is already running."
msgstr "Já há um envio em andamento para este arquivo."
+msgid "Announcement"
+msgstr "Anúncio"
+
msgid "Anonymous"
msgstr "Anônimo"
@@ -567,6 +633,9 @@ msgstr "Histórico do chat: %(user)s"
msgid "Chat messages"
msgstr "Mensagens do chat"
+msgid "Chat notice"
+msgstr "Aviso do chat"
+
msgid "Chat replay"
msgstr "Replay do chat"
@@ -1218,6 +1287,9 @@ msgstr "Filtrar chat…"
msgid "First"
msgstr "Primeira"
+msgid "First message"
+msgstr "Primeira mensagem"
+
msgid "First page"
msgstr "Primeira página"
@@ -2057,6 +2129,10 @@ msgstr "Paginação"
msgid "Pagination (top)"
msgstr "Paginação (topo)"
+#, python-format
+msgid "Paid for %(months)s months in advance."
+msgstr "Pagou %(months)s meses adiantados."
+
msgid "Password"
msgstr "Senha"
@@ -2365,6 +2441,10 @@ msgstr "Repita a nova senha"
msgid "Repeat password"
msgstr "Repita a senha"
+#, python-format
+msgid "Replying to @%(user)s: %(msg)s"
+msgstr "Respondendo a @%(user)s: %(msg)s"
+
#, python-format
msgid "Request '%(username)s' denied and deleted."
msgstr "Solicitação de '%(username)s' recusada e excluída."
@@ -2630,6 +2710,9 @@ msgstr "Tamanho"
msgid "Skip to content"
msgstr "Pular para o conteúdo"
+msgid "Someone"
+msgstr "Alguém"
+
msgid "Sort"
msgstr "Ordenar"
@@ -2945,6 +3028,20 @@ msgstr "A senha do administrador do sistema só pode ser alterada por variáveis
msgid "The video file could not be loaded or is still being transcoded."
msgstr "Não foi possível carregar o arquivo de vídeo ou ele ainda está sendo transcodificado."
+#, python-format
+msgid "They've gifted %(num)d sub in the channel!"
+msgid_plural "They've gifted %(num)d subs in the channel!"
+msgstr[0] "Já presenteou %(num)d inscrição no canal!"
+msgstr[1] "Já presenteou %(num)d inscrições no canal!"
+
+#, python-format
+msgid "They've subscribed for %(months)s months!"
+msgstr "Inscrito há %(months)s meses!"
+
+#, python-format
+msgid "They've subscribed for %(months)s months, currently on a %(streak)s month streak!"
+msgstr "Inscrito há %(months)s meses, com uma sequência atual de %(streak)s meses!"
+
msgid "This admin user is protected and cannot be deleted."
msgstr "Este usuário admin é protegido e não pode ser excluído."
@@ -3405,6 +3502,9 @@ msgstr "segundo o banco de dados de objetos"
msgid "active"
msgstr "ativo"
+msgid "an anonymous user"
+msgstr "um usuário anônimo"
+
msgid "available"
msgstr "disponível"
@@ -3461,6 +3561,9 @@ msgstr "nunca"
msgid "not created"
msgstr "não criado"
+msgid "someone"
+msgstr "alguém"
+
#, python-format
msgid "today %(time)s"
msgstr "hoje %(time)s"
diff --git a/WEBUI/translations/tr/LC_MESSAGES/messages.mo b/WEBUI/translations/tr/LC_MESSAGES/messages.mo
index 27df36b..624512d 100644
Binary files a/WEBUI/translations/tr/LC_MESSAGES/messages.mo and b/WEBUI/translations/tr/LC_MESSAGES/messages.mo differ
diff --git a/WEBUI/translations/tr/LC_MESSAGES/messages.po b/WEBUI/translations/tr/LC_MESSAGES/messages.po
index 35bb06f..0a9a47c 100644
--- a/WEBUI/translations/tr/LC_MESSAGES/messages.po
+++ b/WEBUI/translations/tr/LC_MESSAGES/messages.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: StreamVault VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2026-11-12 15:41+0100\n"
+"POT-Creation-Date: 2026-11-12 15:50+0100\n"
"PO-Revision-Date: 2026-11-03 19:31+0100\n"
"Last-Translator: FULL NAME \n"
"Language: tr\n"
@@ -145,6 +145,64 @@ msgstr "%(num)s gün"
msgid "%(provider)s does not support OAuth."
msgstr "%(provider)s OAuth desteklemiyor."
+#, python-format
+msgid "%(user)s converted from a Prime sub to a Tier %(tier)s sub!"
+msgstr "%(user)s, Prime aboneliğinden Seviye %(tier)s aboneliğine geçti!"
+
+#, python-format
+msgid "%(user)s donated %(amount)s to %(charity)s!"
+msgstr "%(user)s, %(charity)s için %(amount)s bağışladı!"
+
+#, python-format
+msgid "%(user)s gifted %(months)s months of Tier %(tier)s to %(recipient)s!"
+msgstr "%(user)s, %(recipient)s adlı kullanıcıya %(months)s aylık Seviye %(tier)s hediye etti!"
+
+#, python-format
+msgid "%(user)s gifted a Tier %(tier)s sub to %(recipient)s!"
+msgstr "%(user)s, %(recipient)s adlı kullanıcıya Seviye %(tier)s abonelik hediye etti!"
+
+#, python-format
+msgid "%(user)s is continuing the gift sub they got from %(other)s!"
+msgstr "%(user)s, %(other)s tarafından hediye edilen aboneliği sürdürüyor!"
+
+#, python-format
+msgid "%(user)s is gifting %(num)d Tier %(tier)s sub to the community!"
+msgid_plural "%(user)s is gifting %(num)d Tier %(tier)s subs to the community!"
+msgstr[0] "%(user)s topluluğa %(num)d adet Seviye %(tier)s abonelik hediye ediyor!"
+
+#, python-format
+msgid "%(user)s is new here. Say hello!"
+msgstr "%(user)s burada yeni. Merhaba deyin!"
+
+#, python-format
+msgid "%(user)s is paying forward the gift they got from %(other)s to %(recipient)s!"
+msgstr "%(user)s, %(other)s tarafından gelen hediyeyi %(recipient)s adlı kullanıcıya aktarıyor!"
+
+#, python-format
+msgid "%(user)s is paying forward the gift they got from %(other)s to the community!"
+msgstr "%(user)s, %(other)s tarafından gelen hediyeyi topluluğa aktarıyor!"
+
+#, python-format
+msgid "%(user)s is raiding with %(num)d viewer!"
+msgid_plural "%(user)s is raiding with %(num)d viewers!"
+msgstr[0] "%(user)s, %(num)d izleyiciyle baskın yapıyor!"
+
+#, python-format
+msgid "%(user)s just earned a new %(bits)s Bits badge!"
+msgstr "%(user)s yeni bir %(bits)s Bits rozeti kazandı!"
+
+#, python-format
+msgid "%(user)s subscribed at Tier %(tier)s."
+msgstr "%(user)s, Seviye %(tier)s ile abone oldu."
+
+#, python-format
+msgid "%(user)s subscribed with Prime."
+msgstr "%(user)s, Prime ile abone oldu."
+
+#, python-format
+msgid "%(user)s watched %(num)s consecutive streams and sparked a watch streak!"
+msgstr "%(user)s art arda %(num)s yayın izledi ve bir izleme serisi başlattı!"
+
msgid "0 = delete immediately once the cloud copy is verified."
msgstr "0 = bulut kopyası doğrulanır doğrulanmaz hemen sil."
@@ -298,9 +356,15 @@ msgstr "%(path)s de aktarılır."
msgid "Also transfers preview images from %(path)s."
msgstr "%(path)s içindeki önizleme görselleri de aktarılır."
+msgid "An anonymous user"
+msgstr "Anonim bir kullanıcı"
+
msgid "An upload for this file is already running."
msgstr "Bu dosya için zaten bir yükleme çalışıyor."
+msgid "Announcement"
+msgstr "Duyuru"
+
msgid "Anonymous"
msgstr "Anonim"
@@ -561,6 +625,9 @@ msgstr "Sohbet geçmişi: %(user)s"
msgid "Chat messages"
msgstr "Sohbet mesajları"
+msgid "Chat notice"
+msgstr "Sohbet bildirimi"
+
msgid "Chat replay"
msgstr "Sohbet tekrarı"
@@ -1212,6 +1279,9 @@ msgstr "Sohbeti filtrele…"
msgid "First"
msgstr "İlk"
+msgid "First message"
+msgstr "İlk mesaj"
+
msgid "First page"
msgstr "İlk sayfa"
@@ -2051,6 +2121,10 @@ msgstr "Sayfalama"
msgid "Pagination (top)"
msgstr "Sayfalama (üst)"
+#, python-format
+msgid "Paid for %(months)s months in advance."
+msgstr "%(months)s ay peşin ödendi."
+
msgid "Password"
msgstr "Şifre"
@@ -2359,6 +2433,10 @@ msgstr "Yeni şifreyi tekrarla"
msgid "Repeat password"
msgstr "Şifreyi tekrarla"
+#, python-format
+msgid "Replying to @%(user)s: %(msg)s"
+msgstr "@%(user)s kullanıcısına yanıt: %(msg)s"
+
#, python-format
msgid "Request '%(username)s' denied and deleted."
msgstr "'%(username)s' talebi reddedildi ve silindi."
@@ -2624,6 +2702,9 @@ msgstr "Boyut"
msgid "Skip to content"
msgstr "İçeriğe geç"
+msgid "Someone"
+msgstr "Birisi"
+
msgid "Sort"
msgstr "Sırala"
@@ -2939,6 +3020,19 @@ msgstr "Sistem yöneticisinin şifresi yalnızca ortam değişkenleriyle değiş
msgid "The video file could not be loaded or is still being transcoded."
msgstr "Video dosyası yüklenemedi veya hâlâ dönüştürülüyor."
+#, python-format
+msgid "They've gifted %(num)d sub in the channel!"
+msgid_plural "They've gifted %(num)d subs in the channel!"
+msgstr[0] "Kanalda toplam %(num)d abonelik hediye etti!"
+
+#, python-format
+msgid "They've subscribed for %(months)s months!"
+msgstr "%(months)s aydır abone!"
+
+#, python-format
+msgid "They've subscribed for %(months)s months, currently on a %(streak)s month streak!"
+msgstr "%(months)s aydır abone, şu anda %(streak)s aylık seri!"
+
msgid "This admin user is protected and cannot be deleted."
msgstr "Bu yönetici kullanıcı korumalıdır ve silinemez."
@@ -3398,6 +3492,9 @@ msgstr "nesne veritabanına göre"
msgid "active"
msgstr "etkin"
+msgid "an anonymous user"
+msgstr "anonim bir kullanıcı"
+
msgid "available"
msgstr "mevcut"
@@ -3454,6 +3551,9 @@ msgstr "hiçbir zaman"
msgid "not created"
msgstr "oluşturulmadı"
+msgid "someone"
+msgstr "birisi"
+
#, python-format
msgid "today %(time)s"
msgstr "bugün %(time)s"
diff --git a/WEBUI/translations/zh_Hans/LC_MESSAGES/messages.mo b/WEBUI/translations/zh_Hans/LC_MESSAGES/messages.mo
index 9684ac6..3949b02 100644
Binary files a/WEBUI/translations/zh_Hans/LC_MESSAGES/messages.mo and b/WEBUI/translations/zh_Hans/LC_MESSAGES/messages.mo differ
diff --git a/WEBUI/translations/zh_Hans/LC_MESSAGES/messages.po b/WEBUI/translations/zh_Hans/LC_MESSAGES/messages.po
index 91b9d47..a0e6a0e 100644
--- a/WEBUI/translations/zh_Hans/LC_MESSAGES/messages.po
+++ b/WEBUI/translations/zh_Hans/LC_MESSAGES/messages.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: StreamVault VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2026-11-12 15:41+0100\n"
+"POT-Creation-Date: 2026-11-12 15:50+0100\n"
"PO-Revision-Date: 2026-11-03 19:31+0100\n"
"Last-Translator: FULL NAME \n"
"Language: zh_Hans\n"
@@ -145,6 +145,64 @@ msgstr "%(num)s 天"
msgid "%(provider)s does not support OAuth."
msgstr "%(provider)s 不支持 OAuth。"
+#, python-format
+msgid "%(user)s converted from a Prime sub to a Tier %(tier)s sub!"
+msgstr "%(user)s 已从 Prime 订阅转为 %(tier)s 级订阅!"
+
+#, python-format
+msgid "%(user)s donated %(amount)s to %(charity)s!"
+msgstr "%(user)s 向 %(charity)s 捐赠了 %(amount)s!"
+
+#, python-format
+msgid "%(user)s gifted %(months)s months of Tier %(tier)s to %(recipient)s!"
+msgstr "%(user)s 向 %(recipient)s 赠送了 %(months)s 个月的 %(tier)s 级订阅!"
+
+#, python-format
+msgid "%(user)s gifted a Tier %(tier)s sub to %(recipient)s!"
+msgstr "%(user)s 向 %(recipient)s 赠送了一个 %(tier)s 级订阅!"
+
+#, python-format
+msgid "%(user)s is continuing the gift sub they got from %(other)s!"
+msgstr "%(user)s 正在延续 %(other)s 赠送的订阅!"
+
+#, python-format
+msgid "%(user)s is gifting %(num)d Tier %(tier)s sub to the community!"
+msgid_plural "%(user)s is gifting %(num)d Tier %(tier)s subs to the community!"
+msgstr[0] "%(user)s 正在向社区赠送 %(num)d 个 %(tier)s 级订阅!"
+
+#, python-format
+msgid "%(user)s is new here. Say hello!"
+msgstr "%(user)s 是新来的。打个招呼吧!"
+
+#, python-format
+msgid "%(user)s is paying forward the gift they got from %(other)s to %(recipient)s!"
+msgstr "%(user)s 将从 %(other)s 处收到的礼物传递给 %(recipient)s!"
+
+#, python-format
+msgid "%(user)s is paying forward the gift they got from %(other)s to the community!"
+msgstr "%(user)s 将从 %(other)s 处收到的礼物传递给社区!"
+
+#, python-format
+msgid "%(user)s is raiding with %(num)d viewer!"
+msgid_plural "%(user)s is raiding with %(num)d viewers!"
+msgstr[0] "%(user)s 正带着 %(num)d 名观众突袭!"
+
+#, python-format
+msgid "%(user)s just earned a new %(bits)s Bits badge!"
+msgstr "%(user)s 刚刚获得了新的 %(bits)s Bits 徽章!"
+
+#, python-format
+msgid "%(user)s subscribed at Tier %(tier)s."
+msgstr "%(user)s 订阅了 %(tier)s 级。"
+
+#, python-format
+msgid "%(user)s subscribed with Prime."
+msgstr "%(user)s 使用 Prime 订阅了。"
+
+#, python-format
+msgid "%(user)s watched %(num)s consecutive streams and sparked a watch streak!"
+msgstr "%(user)s 连续观看了 %(num)s 场直播,开启了观看连续纪录!"
+
msgid "0 = delete immediately once the cloud copy is verified."
msgstr "0 = 云端副本验证后立即删除。"
@@ -298,9 +356,15 @@ msgstr "同时传输 %(path)s。"
msgid "Also transfers preview images from %(path)s."
msgstr "同时传输 %(path)s 中的预览图。"
+msgid "An anonymous user"
+msgstr "一位匿名用户"
+
msgid "An upload for this file is already running."
msgstr "该文件已有上传任务正在运行。"
+msgid "Announcement"
+msgstr "公告"
+
msgid "Anonymous"
msgstr "匿名"
@@ -561,6 +625,9 @@ msgstr "聊天记录:%(user)s"
msgid "Chat messages"
msgstr "聊天消息"
+msgid "Chat notice"
+msgstr "聊天通知"
+
msgid "Chat replay"
msgstr "聊天回放"
@@ -1212,6 +1279,9 @@ msgstr "筛选聊天…"
msgid "First"
msgstr "第一个"
+msgid "First message"
+msgstr "首条消息"
+
msgid "First page"
msgstr "第一页"
@@ -2051,6 +2121,10 @@ msgstr "分页"
msgid "Pagination (top)"
msgstr "分页(顶部)"
+#, python-format
+msgid "Paid for %(months)s months in advance."
+msgstr "已预付 %(months)s 个月。"
+
msgid "Password"
msgstr "密码"
@@ -2359,6 +2433,10 @@ msgstr "再次输入新密码"
msgid "Repeat password"
msgstr "再次输入密码"
+#, python-format
+msgid "Replying to @%(user)s: %(msg)s"
+msgstr "回复 @%(user)s:%(msg)s"
+
#, python-format
msgid "Request '%(username)s' denied and deleted."
msgstr "已拒绝并删除申请“%(username)s”。"
@@ -2624,6 +2702,9 @@ msgstr "大小"
msgid "Skip to content"
msgstr "跳到内容"
+msgid "Someone"
+msgstr "某人"
+
msgid "Sort"
msgstr "排序"
@@ -2939,6 +3020,19 @@ msgstr "系统管理员的密码只能通过环境变量更改。"
msgid "The video file could not be loaded or is still being transcoded."
msgstr "视频文件无法加载或仍在转码中。"
+#, python-format
+msgid "They've gifted %(num)d sub in the channel!"
+msgid_plural "They've gifted %(num)d subs in the channel!"
+msgstr[0] "已在本频道共赠送 %(num)d 个订阅!"
+
+#, python-format
+msgid "They've subscribed for %(months)s months!"
+msgstr "已订阅 %(months)s 个月!"
+
+#, python-format
+msgid "They've subscribed for %(months)s months, currently on a %(streak)s month streak!"
+msgstr "已订阅 %(months)s 个月,当前连续 %(streak)s 个月!"
+
msgid "This admin user is protected and cannot be deleted."
msgstr "此管理员用户受保护,无法删除。"
@@ -3398,6 +3492,9 @@ msgstr "根据对象数据库"
msgid "active"
msgstr "活跃"
+msgid "an anonymous user"
+msgstr "一位匿名用户"
+
msgid "available"
msgstr "可用"
@@ -3454,6 +3551,9 @@ msgstr "从不"
msgid "not created"
msgstr "未创建"
+msgid "someone"
+msgstr "某人"
+
#, python-format
msgid "today %(time)s"
msgstr "今天 %(time)s"
diff --git a/WEBUI/webui.py b/WEBUI/webui.py
index 0bf8902..f772bbb 100644
--- a/WEBUI/webui.py
+++ b/WEBUI/webui.py
@@ -6674,6 +6674,151 @@ def chat_event_from_row(data):
return chat_events.sanitize_event(data.get("event") if isinstance(data, dict) else None)
+def chat_meta_from_row(data):
+ return chat_events.sanitize_meta(data.get("meta") if isinstance(data, dict) else None)
+
+
+_NOTICE_TIERS = {"1000": "1", "2000": "2", "3000": "3"}
+
+
+def _notice_tier(event):
+ return _NOTICE_TIERS.get(str(event.get("plan") or ""), "1")
+
+
+def _notice_gift_total(event):
+ total = event.get("sender_count")
+ if not total:
+ return ""
+ return ngettext(
+ "They've gifted %(num)d sub in the channel!",
+ "They've gifted %(num)d subs in the channel!",
+ total,
+ )
+
+
+def format_notice_text(event, user):
+ """Twitch-style headline for a USERNOTICE chat event in the request's language."""
+ if not isinstance(event, dict) or event.get("type") != "notice":
+ return ""
+ kind = str(event.get("kind") or "")
+ name = str(user or "").strip() or gettext("Someone")
+ anon_subject = gettext("An anonymous user")
+ anon_object = gettext("an anonymous user")
+ if event.get("anon"):
+ name = anon_subject
+ text = ""
+ extra = []
+ if kind in ("sub", "resub"):
+ if event.get("plan") == "Prime":
+ text = gettext("%(user)s subscribed with Prime.", user=name)
+ else:
+ text = gettext("%(user)s subscribed at Tier %(tier)s.", user=name, tier=_notice_tier(event))
+ months = event.get("months") or 0
+ streak = event.get("streak") or 0
+ if months > 1 and streak > 0:
+ extra.append(gettext(
+ "They've subscribed for %(months)s months, currently on a %(streak)s month streak!",
+ months=months,
+ streak=streak,
+ ))
+ elif months > 1:
+ extra.append(gettext("They've subscribed for %(months)s months!", months=months))
+ if (event.get("multimonth") or 0) > 1:
+ extra.append(gettext("Paid for %(months)s months in advance.", months=event.get("multimonth")))
+ elif kind in ("subgift", "anonsubgift"):
+ recipient = event.get("recipient") or gettext("someone")
+ gift_months = event.get("gift_months") or 0
+ if gift_months > 1:
+ text = gettext(
+ "%(user)s gifted %(months)s months of Tier %(tier)s to %(recipient)s!",
+ user=name,
+ months=gift_months,
+ tier=_notice_tier(event),
+ recipient=recipient,
+ )
+ else:
+ text = gettext(
+ "%(user)s gifted a Tier %(tier)s sub to %(recipient)s!",
+ user=name,
+ tier=_notice_tier(event),
+ recipient=recipient,
+ )
+ total = _notice_gift_total(event)
+ if total:
+ extra.append(total)
+ elif kind in ("submysterygift", "anonsubmysterygift"):
+ count = event.get("count") or 1
+ text = ngettext(
+ "%(user)s is gifting %(num)d Tier %(tier)s sub to the community!",
+ "%(user)s is gifting %(num)d Tier %(tier)s subs to the community!",
+ count,
+ user=name,
+ tier=_notice_tier(event),
+ )
+ total = _notice_gift_total(event)
+ if total:
+ extra.append(total)
+ elif kind in ("giftpaidupgrade", "anongiftpaidupgrade"):
+ text = gettext(
+ "%(user)s is continuing the gift sub they got from %(other)s!",
+ user=name,
+ other=event.get("other") or anon_object,
+ )
+ elif kind == "primepaidupgrade":
+ text = gettext(
+ "%(user)s converted from a Prime sub to a Tier %(tier)s sub!",
+ user=name,
+ tier=_notice_tier(event),
+ )
+ elif kind == "standardpayforward":
+ text = gettext(
+ "%(user)s is paying forward the gift they got from %(other)s to %(recipient)s!",
+ user=name,
+ other=event.get("other") or anon_object,
+ recipient=event.get("recipient") or gettext("someone"),
+ )
+ elif kind == "communitypayforward":
+ text = gettext(
+ "%(user)s is paying forward the gift they got from %(other)s to the community!",
+ user=name,
+ other=event.get("other") or anon_object,
+ )
+ elif kind == "raid":
+ text = ngettext(
+ "%(user)s is raiding with %(num)d viewer!",
+ "%(user)s is raiding with %(num)d viewers!",
+ event.get("count") or 0,
+ user=name,
+ )
+ elif kind == "bitsbadgetier" and event.get("count"):
+ text = gettext(
+ "%(user)s just earned a new %(bits)s Bits badge!",
+ user=name,
+ bits=f"{event.get('count'):,}",
+ )
+ elif kind == "announcement":
+ text = gettext("Announcement")
+ elif kind == "ritual":
+ text = gettext("%(user)s is new here. Say hello!", user=name)
+ elif kind == "viewermilestone" and event.get("other") == "watch-streak" and event.get("count"):
+ text = gettext(
+ "%(user)s watched %(num)s consecutive streams and sparked a watch streak!",
+ user=name,
+ num=event.get("count"),
+ )
+ elif kind == "charitydonation" and event.get("charity") and event.get("amount"):
+ text = gettext(
+ "%(user)s donated %(amount)s to %(charity)s!",
+ user=name,
+ amount=event.get("amount"),
+ charity=event.get("charity"),
+ )
+ if not text:
+ # Unknown or incomplete kinds: Twitch's own system text is the best fallback.
+ return str(event.get("system") or "") or gettext("Chat notice")
+ return " ".join([text, *extra])
+
+
def chat_item_from_row(data, *, time_key):
"""Build a sanitized chat API item. time_key is 't' (live) or 'rel' (vod)."""
if not isinstance(data, dict):
@@ -6693,7 +6838,8 @@ def chat_item_from_row(data, *, time_key):
msg_key = " ".join(msg.split())
reward = chat_reward_from_row(data)
event = chat_event_from_row(data)
- if event and not msg_key:
+ is_notice = bool(event) and event.get("type") == "notice"
+ if event and not is_notice and not msg_key:
msg = chat_events.format_moderation_message(
event.get("type"),
user,
@@ -6705,8 +6851,17 @@ def chat_item_from_row(data, *, time_key):
item = {time_key: time_val, "user": user, "msg": msg}
if reward:
item["reward"] = reward
+ notice_text = ""
+ if is_notice:
+ notice_text = format_notice_text(event, user)
+ # Twitch's raw system-msg stays server-side; the client gets the localized text.
+ event = {k: v for k, v in event.items() if k != "system"}
+ event["text"] = notice_text
if event:
item["event"] = event
+ meta = chat_meta_from_row(data)
+ if meta:
+ item["meta"] = meta
emotes = chat_emotes_from_row(data)
if emotes:
item["emotes"] = emotes
@@ -6717,7 +6872,11 @@ def chat_item_from_row(data, *, time_key):
if color:
item["color"] = color
dedupe_token = msg_key
- if event:
+ if is_notice:
+ # kind + text keep e.g. several gift subs from one gifter to different
+ # recipients apart in the ~1.25 s duplicate window.
+ dedupe_token = f"notice|{event.get('kind')}|{user}|{notice_text}|{msg_key}"
+ elif event:
dedupe_token = (
f"event|{event.get('type')}|{event.get('seconds') or ''}|"
f"{user}|{msg_key}"
@@ -11202,6 +11361,36 @@ def public_download(token):
return send_mp4_with_range(file_path, accel_path=accel_path, attachment_name=filename)
+def chat_items_from_chunk_file(chunk_file):
+ """Sanitized, de-duplicated VOD chat items for one static chat chunk."""
+ try:
+ with open(chunk_file, "r", encoding="utf-8") as handle:
+ rows = json.load(handle)
+ except (OSError, ValueError) as exc:
+ logging.warning("Chat chunk unreadable %s: %s", chunk_file, exc)
+ return []
+ if not isinstance(rows, list):
+ return []
+ items = []
+ seen = set()
+ recent = {}
+ for data in rows:
+ parsed = chat_item_from_row(data, time_key="rel")
+ if not parsed:
+ continue
+ item, dedupe_token, rel_val, user = parsed
+ key = f"{rel_val}|{user}|{dedupe_token}"
+ close_key = f"{user}|{dedupe_token}"
+ if key in seen:
+ continue
+ if close_key in recent and abs(rel_val - recent[close_key]) < 1.25:
+ continue
+ recent[close_key] = rel_val
+ seen.add(key)
+ items.append(item)
+ return items
+
+
@vod_bp.route("/chat/vod//")
@login_required
def chat_vod(streamer, filename):
@@ -11224,8 +11413,15 @@ def chat_vod(streamer, filename):
)
if not chunk_file or not os.path.isfile(chunk_file):
return Response("[]", 200, mimetype="application/json")
- resp = send_file(chunk_file, mimetype="application/json")
- resp.headers["Cache-Control"] = "public, max-age=86400, immutable"
+ # Chunks hold raw log rows: run them through the same allowlist as the
+ # range API (no raw tags or system-msg to the client, localized notice text).
+ resp = Response(
+ json.dumps(chat_items_from_chunk_file(chunk_file), ensure_ascii=False),
+ 200,
+ mimetype="application/json",
+ )
+ resp.headers["Cache-Control"] = "private, max-age=600"
+ resp.headers["Vary"] = "Cookie, Accept-Language"
return resp
if request.args.get("meta") == "1":
diff --git a/core/tts_bot.py b/core/tts_bot.py
index b3ce8d3..e44ab14 100644
--- a/core/tts_bot.py
+++ b/core/tts_bot.py
@@ -62,10 +62,15 @@ def parse_privmsg(line: str) -> tuple[str, str, str, dict] | None:
continue
key, value = tag.split("=", 1)
tags[key] = _unescape_tag(value)
- if " PRIVMSG " not in rest:
+ # Route on the real command token: text such as "x PRIVMSG y" inside a
+ # USERNOTICE or another command must not be read as a chat message.
+ prefix, _sep, rest = rest.partition(" ")
+ if not prefix.startswith(":"):
+ return None
+ command, _sep, payload = rest.lstrip(" ").partition(" ")
+ if command.upper() != "PRIVMSG":
return None
try:
- prefix, payload = rest.split(" PRIVMSG ", 1)
target, message = payload.split(" :", 1)
except ValueError:
return None
diff --git a/docs/screenshots/chat_twitch_events.png b/docs/screenshots/chat_twitch_events.png
new file mode 100644
index 0000000..8e044cf
Binary files /dev/null and b/docs/screenshots/chat_twitch_events.png differ
diff --git a/recorder/Streamrecorder.py b/recorder/Streamrecorder.py
index c1a27e9..4528985 100644
--- a/recorder/Streamrecorder.py
+++ b/recorder/Streamrecorder.py
@@ -2252,7 +2252,7 @@ def refresh_recording_info():
# Persistente File-Handles um Open/Close Overhead zu minimieren
handles = {'live': None, 'session': None, 'session_path': None}
- def append_chat_log(username, message, emotes=None, badges=None, color=None, reward=None, event=None):
+ def append_chat_log(username, message, emotes=None, badges=None, color=None, reward=None, event=None, meta=None):
now = time.time()
entry_base = {"user": username or "", "msg": message or ""}
if emotes:
@@ -2275,6 +2275,9 @@ def append_chat_log(username, message, emotes=None, badges=None, color=None, rew
clean_event = chat_events.sanitize_event(event)
if clean_event:
entry_base["event"] = clean_event
+ clean_meta = chat_events.sanitize_meta(meta)
+ if clean_meta:
+ entry_base["meta"] = clean_meta
try:
# 1. Live Log (WebUI)
@@ -2377,9 +2380,10 @@ def append_chat_log(username, message, emotes=None, badges=None, color=None, rew
last_ping = time.time()
while '\r\n' in buffer:
line, buffer = buffer.split('\r\n', 1)
- if line.startswith('PING'):
+ irc_cmd = chat_events.irc_command(line)
+ if irc_cmd == 'PING':
sock.sendall(b"PONG :tmi.twitch.tv\r\n")
- elif 'CLEARCHAT #' in line:
+ elif irc_cmd == 'CLEARCHAT':
irc_tags, command_line = parse_irc_tags(line)
parsed_event = chat_events.parse_clearchat(irc_tags, command_line)
if parsed_event:
@@ -2397,7 +2401,7 @@ def append_chat_log(username, message, emotes=None, badges=None, color=None, rew
source_channel,
chat_exc,
)
- elif 'CLEARMSG #' in line:
+ elif irc_cmd == 'CLEARMSG':
irc_tags, command_line = parse_irc_tags(line)
parsed_event = chat_events.parse_clearmsg(irc_tags, command_line)
if parsed_event:
@@ -2415,7 +2419,30 @@ def append_chat_log(username, message, emotes=None, badges=None, color=None, rew
source_channel,
chat_exc,
)
- elif 'PRIVMSG' in line:
+ elif irc_cmd == 'USERNOTICE':
+ parsed = process_twitch_message(line)
+ notice_text, emotes, badges, color = parsed[1:5]
+ irc_tags = parsed[5] if len(parsed) > 5 else {}
+ notice = chat_events.parse_usernotice(irc_tags, notice_text)
+ if notice:
+ refresh_recording_info()
+ try:
+ append_chat_log(
+ notice.get("user") or "",
+ notice.get("msg") or "",
+ emotes if notice.get("msg") else None,
+ badges,
+ color,
+ event=notice.get("event"),
+ )
+ msg_count += 1
+ except Exception as chat_exc:
+ logger.error(
+ "[%s] USERNOTICE-Log fehlgeschlagen: %s",
+ source_channel,
+ chat_exc,
+ )
+ elif irc_cmd == 'PRIVMSG':
parsed = process_twitch_message(line)
username, message, emotes, badges, color = parsed[:5]
irc_tags = parsed[5] if len(parsed) > 5 else {}
@@ -2497,6 +2524,7 @@ def append_chat_log(username, message, emotes=None, badges=None, color=None, rew
badges,
color,
reward=reward_meta,
+ meta=chat_events.parse_privmsg_meta(irc_tags),
)
except Exception as chat_exc:
logger.error(
diff --git a/recorder/chat_events.py b/recorder/chat_events.py
index 6c2aa8c..d912061 100644
--- a/recorder/chat_events.py
+++ b/recorder/chat_events.py
@@ -12,7 +12,238 @@
r"(?:^|\s)CLEARMSG\s+#\S+(?:\s+:([\s\S]*))?$",
re.IGNORECASE,
)
-_ALLOWED_EVENT_TYPES = frozenset({"ban", "timeout", "delete", "clear"})
+_ALLOWED_EVENT_TYPES = frozenset({"ban", "timeout", "delete", "clear", "notice"})
+
+# --- USERNOTICE (subs, gifts, raids, announcements, ...) -------------------
+_NOTICE_KIND_RE = re.compile(r"^[a-z0-9_-]{1,40}$")
+_NOTICE_PLANS = frozenset({"Prime", "1000", "2000", "3000"})
+_NOTICE_COLORS = frozenset({"PRIMARY", "BLUE", "GREEN", "ORANGE", "PURPLE"})
+# Upper bounds keep a forged or corrupt log line from carrying absurd numbers.
+_NOTICE_INT_CAPS = {
+ "months": 1200,
+ "streak": 1200,
+ "multimonth": 120,
+ "gift_months": 120,
+ "sender_count": 1_000_000,
+ "count": 100_000_000,
+}
+_NOTICE_TEXT_FIELDS = ("recipient", "other", "charity", "amount")
+_NOTICE_TEXT_MAX = 80
+_NOTICE_SYSTEM_MAX = 300
+_ANON_GIFTER_LOGIN = "ananonymousgifter"
+_ANON_KINDS = frozenset({"anonsubgift", "anonsubmysterygift"})
+
+# --- PRIVMSG meta (bits, highlight, first message, reply, power-ups) -------
+_META_POWER_BY_MSG_ID = {
+ "gigantified-emote-message": "gigantified",
+ "animated-message": "animated",
+}
+_META_POWERS = frozenset(_META_POWER_BY_MSG_ID.values())
+_META_BITS_CAP = 10_000_000
+_META_REPLY_USER_MAX = 80
+_META_REPLY_MSG_MAX = 140
+
+
+def irc_command(line: str) -> str:
+ """Return the IRC command of a raw line (``PRIVMSG``, ``USERNOTICE``, ``PING`` ...).
+
+ Skips the IRCv3 tag block (``@...``) and the source prefix (``:...``), so
+ text inside a chat message can never be mistaken for a command.
+ """
+ rest = str(line or "").strip("\r\n").lstrip(" ")
+ if rest.startswith("@"):
+ _tags, _sep, rest = rest.partition(" ")
+ rest = rest.lstrip(" ")
+ if rest.startswith(":"):
+ _prefix, _sep, rest = rest.partition(" ")
+ rest = rest.lstrip(" ")
+ command, _sep, _params = rest.partition(" ")
+ return command.upper()
+
+
+def _clean_text(value, limit: int) -> str:
+ text = " ".join(str(value or "").split())
+ return text[:limit]
+
+
+def _positive_int(value, cap: int) -> int | None:
+ try:
+ number = int(str(value).strip())
+ except (TypeError, ValueError):
+ return None
+ if number <= 0:
+ return None
+ return min(number, cap)
+
+
+def _charity_amount(tags: dict) -> str:
+ raw_amount = _positive_int(tags.get("msg-param-donation-amount"), 10**12)
+ if raw_amount is None:
+ return ""
+ try:
+ exponent = int(str(tags.get("msg-param-exponent") or "0").strip())
+ except (TypeError, ValueError):
+ exponent = 0
+ exponent = max(0, min(exponent, 6))
+ value = raw_amount / (10**exponent)
+ amount = f"{value:.{exponent}f}"
+ currency = _clean_text(tags.get("msg-param-donation-currency"), 8).upper()
+ if currency and re.fullmatch(r"[A-Z]{3}", currency):
+ return f"{amount} {currency}"
+ return amount
+
+
+def parse_usernotice(tags: dict | None, message: str = "") -> dict | None:
+ """Turn a Twitch USERNOTICE into a chat-log payload.
+
+ Returns ``{"user", "msg", "event"}`` where ``msg`` is the user's optional
+ text and ``event`` is a sanitized ``{"type": "notice", "kind": ...}`` dict,
+ or None when the notice has no usable msg-id.
+ """
+ tags = tags if isinstance(tags, dict) else {}
+ kind = str(tags.get("msg-id") or "").strip().lower()
+ if kind == "sharedchatnotice":
+ kind = str(tags.get("source-msg-id") or "").strip().lower()
+ if not _NOTICE_KIND_RE.match(kind):
+ return None
+ login = str(tags.get("login") or "").strip()
+ user = str(tags.get("display-name") or "").strip() or login
+ event: dict = {"type": "notice", "kind": kind}
+
+ plan = str(tags.get("msg-param-sub-plan") or "").strip()
+ if plan:
+ event["plan"] = plan
+
+ if kind in ("sub", "resub"):
+ event["months"] = tags.get("msg-param-cumulative-months")
+ if str(tags.get("msg-param-should-share-streak") or "").strip() == "1":
+ event["streak"] = tags.get("msg-param-streak-months")
+ event["multimonth"] = tags.get("msg-param-multimonth-duration")
+ elif kind in ("subgift", "anonsubgift"):
+ event["recipient"] = tags.get("msg-param-recipient-display-name") or tags.get(
+ "msg-param-recipient-user-name"
+ )
+ event["gift_months"] = tags.get("msg-param-gift-months")
+ event["months"] = tags.get("msg-param-months")
+ event["sender_count"] = tags.get("msg-param-sender-count")
+ elif kind in ("submysterygift", "anonsubmysterygift"):
+ event["count"] = tags.get("msg-param-mass-gift-count")
+ event["sender_count"] = tags.get("msg-param-sender-count")
+ elif kind == "giftpaidupgrade":
+ event["other"] = tags.get("msg-param-sender-name") or tags.get("msg-param-sender-login")
+ elif kind in ("standardpayforward", "communitypayforward"):
+ prior_anonymous = str(tags.get("msg-param-prior-gifter-anonymous") or "").strip().lower()
+ if prior_anonymous != "true":
+ event["other"] = tags.get("msg-param-prior-gifter-display-name")
+ event["recipient"] = tags.get("msg-param-recipient-display-name")
+ elif kind == "raid":
+ user = str(tags.get("msg-param-displayName") or "").strip() or str(
+ tags.get("msg-param-login") or ""
+ ).strip() or user
+ event["count"] = tags.get("msg-param-viewerCount")
+ elif kind == "bitsbadgetier":
+ event["count"] = tags.get("msg-param-threshold")
+ elif kind == "announcement":
+ event["color"] = str(tags.get("msg-param-color") or "").strip().upper()
+ elif kind == "viewermilestone":
+ event["count"] = tags.get("msg-param-value")
+ event["other"] = tags.get("msg-param-category")
+ elif kind == "charitydonation":
+ event["charity"] = tags.get("msg-param-charity-name")
+ event["amount"] = _charity_amount(tags)
+
+ if login.lower() == _ANON_GIFTER_LOGIN or kind in _ANON_KINDS:
+ event["anon"] = True
+ system = str(tags.get("system-msg") or "")
+ if system:
+ event["system"] = system
+
+ clean = sanitize_event(event)
+ if not clean:
+ return None
+ msg = str(message or "")
+ if msg.startswith("\x01ACTION "):
+ msg = msg[len("\x01ACTION "):].removesuffix("\x01")
+ return {"user": user, "msg": msg.strip(), "event": clean}
+
+
+def _sanitize_notice(raw: dict) -> dict | None:
+ kind = str(raw.get("kind") or "").strip().lower()
+ if not _NOTICE_KIND_RE.match(kind):
+ return None
+ out: dict = {"type": "notice", "kind": kind}
+ plan = str(raw.get("plan") or "").strip()
+ if plan in _NOTICE_PLANS:
+ out["plan"] = plan
+ color = str(raw.get("color") or "").strip().upper()
+ if color in _NOTICE_COLORS:
+ out["color"] = color
+ for field, cap in _NOTICE_INT_CAPS.items():
+ number = _positive_int(raw.get(field), cap)
+ if number is not None:
+ out[field] = number
+ for field in _NOTICE_TEXT_FIELDS:
+ text = _clean_text(raw.get(field), _NOTICE_TEXT_MAX)
+ if text:
+ out[field] = text
+ if raw.get("anon") is True:
+ out["anon"] = True
+ system = _clean_text(raw.get("system"), _NOTICE_SYSTEM_MAX)
+ if system:
+ out["system"] = system
+ return out
+
+
+def parse_privmsg_meta(tags: dict | None) -> dict | None:
+ """Extract Twitch-only PRIVMSG decorations (bits, highlight, first, reply, power-ups)."""
+ tags = tags if isinstance(tags, dict) else {}
+ meta: dict = {}
+ bits = _positive_int(tags.get("bits"), _META_BITS_CAP)
+ if bits is not None:
+ meta["bits"] = bits
+ msg_id = str(tags.get("msg-id") or "").strip().lower()
+ if msg_id == "highlighted-message":
+ meta["highlight"] = True
+ if msg_id in _META_POWER_BY_MSG_ID:
+ meta["power"] = _META_POWER_BY_MSG_ID[msg_id]
+ if str(tags.get("first-msg") or "").strip() == "1":
+ meta["first"] = True
+ parent_user = str(
+ tags.get("reply-parent-display-name") or tags.get("reply-parent-user-login") or ""
+ ).strip()
+ parent_msg = str(tags.get("reply-parent-msg-body") or "")
+ if parent_user or parent_msg.strip():
+ meta["reply"] = {"user": parent_user, "msg": parent_msg}
+ return sanitize_meta(meta)
+
+
+def sanitize_meta(raw) -> dict | None:
+ """Allowlist filter for the optional ``meta`` block of a chat-log entry."""
+ if not isinstance(raw, dict):
+ return None
+ out: dict = {}
+ bits = _positive_int(raw.get("bits"), _META_BITS_CAP)
+ if bits is not None:
+ out["bits"] = bits
+ if raw.get("highlight") is True:
+ out["highlight"] = True
+ if raw.get("first") is True:
+ out["first"] = True
+ power = str(raw.get("power") or "").strip().lower()
+ if power in _META_POWERS:
+ out["power"] = power
+ reply = raw.get("reply")
+ if isinstance(reply, dict):
+ reply_user = _clean_text(reply.get("user"), _META_REPLY_USER_MAX)
+ reply_msg = _clean_text(reply.get("msg"), _META_REPLY_MSG_MAX)
+ if reply_user or reply_msg:
+ clean_reply = {}
+ if reply_user:
+ clean_reply["user"] = reply_user
+ if reply_msg:
+ clean_reply["msg"] = reply_msg
+ out["reply"] = clean_reply
+ return out or None
def format_timeout_duration(seconds: int) -> str:
@@ -53,7 +284,9 @@ def sanitize_event(raw) -> dict | None:
etype = str(raw.get("type") or "").strip().lower()
if etype not in _ALLOWED_EVENT_TYPES:
return None
- out = {"type": etype}
+ if etype == "notice":
+ return _sanitize_notice(raw)
+ out: dict = {"type": etype}
if etype == "timeout":
try:
seconds = int(raw.get("seconds") or 0)
diff --git a/tests/test_chat_events.py b/tests/test_chat_events.py
new file mode 100644
index 0000000..67b23f3
--- /dev/null
+++ b/tests/test_chat_events.py
@@ -0,0 +1,425 @@
+"""Twitch chat fidelity: IRC command routing, USERNOTICE, PRIVMSG meta, chat API.
+
+Runs with the standard library runner:
+
+ python -m unittest tests.test_chat_events -v
+"""
+
+from __future__ import annotations
+
+import json
+import os
+import sys
+import tempfile
+import time
+import types
+import unittest
+from unittest import mock
+
+# Same test environment as tests/test_recorder_reliability.py. setdefault keeps
+# whatever that module already set when both run in one discovery pass; heavy
+# modules (config, webui, recorder) are only imported inside the tests.
+_BASE = tempfile.mkdtemp(prefix="streamvault-chat-events-tests-")
+for _key, _value in {
+ "VOD_USER": "vaultadmin",
+ "VOD_PASS": "VaultTestPass123!",
+ "VOD_SECRET_KEY": "test-session-secret-key-0123456789",
+ "STORAGE_ENCRYPTION_KEY": "test-storage-encryption-key-0123456789",
+ "VOD_BASE_DIR": _BASE,
+ "STREAMRECORDER_LOG_DIR": _BASE,
+ "VOD_ALLOW_ENV_LOGIN": "1",
+ "REDIS_HOST": "127.0.0.1",
+ "REDIS_PORT": "1",
+ "TWITCH_BADGE_AUTO_SYNC": "0",
+}.items():
+ os.environ.setdefault(_key, _value)
+
+if os.name == "nt" and "fcntl" not in sys.modules:
+ _fcntl = types.ModuleType("fcntl")
+ _fcntl.LOCK_EX = 2
+ _fcntl.LOCK_NB = 4
+ _fcntl.LOCK_UN = 8
+ _fcntl.flock = lambda *args, **kwargs: None
+ sys.modules["fcntl"] = _fcntl
+
+from recorder import chat_events # noqa: E402
+
+
+class IrcCommandTests(unittest.TestCase):
+ def test_usernotice_with_tags_and_prefix(self):
+ line = "@badge-info=;msg-id=resub;login=alice :tmi.twitch.tv USERNOTICE #chan :hello"
+ self.assertEqual(chat_events.irc_command(line), "USERNOTICE")
+
+ def test_privmsg_text_mentioning_clearchat_stays_privmsg(self):
+ line = "@display-name=Bob :bob!bob@bob.tmi.twitch.tv PRIVMSG #chan :CLEARCHAT #x lol"
+ self.assertEqual(chat_events.irc_command(line), "PRIVMSG")
+
+ def test_ping_without_prefix(self):
+ self.assertEqual(chat_events.irc_command("PING :tmi.twitch.tv"), "PING")
+
+ def test_clearmsg_and_empty_line(self):
+ self.assertEqual(
+ chat_events.irc_command("@login=x;target-msg-id=1 :tmi.twitch.tv CLEARMSG #chan :bad"),
+ "CLEARMSG",
+ )
+ self.assertEqual(chat_events.irc_command(""), "")
+
+
+class ParseUsernoticeTests(unittest.TestCase):
+ def test_resub_with_message_and_streak(self):
+ tags = {
+ "msg-id": "resub",
+ "display-name": "Alice",
+ "login": "alice",
+ "msg-param-sub-plan": "1000",
+ "msg-param-cumulative-months": "14",
+ "msg-param-should-share-streak": "1",
+ "msg-param-streak-months": "6",
+ "system-msg": "Alice subscribed at Tier 1. They've subscribed for 14 months!",
+ }
+ parsed = chat_events.parse_usernotice(tags, "still here Kappa")
+ self.assertEqual(parsed["user"], "Alice")
+ self.assertEqual(parsed["msg"], "still here Kappa")
+ event = parsed["event"]
+ self.assertEqual(event["type"], "notice")
+ self.assertEqual(event["kind"], "resub")
+ self.assertEqual(event["plan"], "1000")
+ self.assertEqual(event["months"], 14)
+ self.assertEqual(event["streak"], 6)
+
+ def test_resub_streak_hidden_unless_shared(self):
+ tags = {
+ "msg-id": "resub",
+ "login": "alice",
+ "msg-param-cumulative-months": "3",
+ "msg-param-should-share-streak": "0",
+ "msg-param-streak-months": "3",
+ }
+ event = chat_events.parse_usernotice(tags, "")["event"]
+ self.assertNotIn("streak", event)
+ # display-name missing -> login
+ self.assertEqual(chat_events.parse_usernotice(tags, "")["user"], "alice")
+
+ def test_subgift_with_sender_count(self):
+ tags = {
+ "msg-id": "subgift",
+ "display-name": "Gifter",
+ "login": "gifter",
+ "msg-param-sub-plan": "2000",
+ "msg-param-recipient-display-name": "Lucky",
+ "msg-param-gift-months": "1",
+ "msg-param-months": "5",
+ "msg-param-sender-count": "42",
+ }
+ parsed = chat_events.parse_usernotice(tags, "")
+ event = parsed["event"]
+ self.assertEqual(parsed["user"], "Gifter")
+ self.assertEqual(parsed["msg"], "")
+ self.assertEqual(event["kind"], "subgift")
+ self.assertEqual(event["recipient"], "Lucky")
+ self.assertEqual(event["sender_count"], 42)
+ self.assertEqual(event["gift_months"], 1)
+ self.assertEqual(event["months"], 5)
+ self.assertNotIn("anon", event)
+
+ def test_anonymous_submysterygift(self):
+ tags = {
+ "msg-id": "submysterygift",
+ "display-name": "AnAnonymousGifter",
+ "login": "ananonymousgifter",
+ "msg-param-sub-plan": "1000",
+ "msg-param-mass-gift-count": "10",
+ }
+ event = chat_events.parse_usernotice(tags, "")["event"]
+ self.assertEqual(event["count"], 10)
+ self.assertIs(event["anon"], True)
+ anon_kind = chat_events.parse_usernotice(
+ {"msg-id": "anonsubmysterygift", "msg-param-mass-gift-count": "5"}, ""
+ )["event"]
+ self.assertIs(anon_kind["anon"], True)
+
+ def test_anongiftpaidupgrade_is_not_marked_anon(self):
+ event = chat_events.parse_usernotice(
+ {"msg-id": "anongiftpaidupgrade", "display-name": "Kept", "login": "kept"}, ""
+ )["event"]
+ self.assertNotIn("anon", event)
+
+ def test_raid_uses_raider_display_name_and_viewer_count(self):
+ tags = {
+ "msg-id": "raid",
+ "display-name": "RaiderLogin",
+ "login": "raider",
+ "msg-param-displayName": "Raider",
+ "msg-param-viewerCount": "1234",
+ }
+ parsed = chat_events.parse_usernotice(tags, "")
+ self.assertEqual(parsed["user"], "Raider")
+ self.assertEqual(parsed["event"]["kind"], "raid")
+ self.assertEqual(parsed["event"]["count"], 1234)
+
+ def test_announcement_with_color(self):
+ parsed = chat_events.parse_usernotice(
+ {"msg-id": "announcement", "display-name": "Mod", "msg-param-color": "blue"},
+ "Stream starts soon!",
+ )
+ self.assertEqual(parsed["event"]["color"], "BLUE")
+ self.assertEqual(parsed["msg"], "Stream starts soon!")
+
+ def test_shared_chat_notice_uses_source_msg_id(self):
+ parsed = chat_events.parse_usernotice(
+ {"msg-id": "sharedchatnotice", "source-msg-id": "sub", "display-name": "X"}, ""
+ )
+ self.assertEqual(parsed["event"]["kind"], "sub")
+
+ def test_unknown_kind_keeps_system_msg_capped(self):
+ system = "Something new happened " * 30
+ parsed = chat_events.parse_usernotice(
+ {"msg-id": "brandnewthing", "display-name": "X", "system-msg": system}, ""
+ )
+ event = parsed["event"]
+ self.assertEqual(event["kind"], "brandnewthing")
+ self.assertTrue(event["system"].startswith("Something new happened"))
+ self.assertLessEqual(len(event["system"]), 300)
+
+ def test_invalid_kind_returns_none(self):
+ self.assertIsNone(chat_events.parse_usernotice({"msg-id": "