From d2b70bbc6bf32c05c3a57256118e844ebb74b3fb Mon Sep 17 00:00:00 2001 From: WarmUpTill <19472752+WarmUpTill@users.noreply.github.com> Date: Wed, 27 Aug 2025 20:21:49 +0200 Subject: [PATCH] Add option to change stream language --- data/locale/en-US.ini | 37 +++ plugins/twitch/CMakeLists.txt | 2 + plugins/twitch/language-selection.cpp | 314 +++++++++++++++++++++++++ plugins/twitch/language-selection.hpp | 41 ++++ plugins/twitch/macro-action-twitch.cpp | 25 ++ plugins/twitch/macro-action-twitch.hpp | 4 + 6 files changed, 423 insertions(+) create mode 100644 plugins/twitch/language-selection.cpp create mode 100644 plugins/twitch/language-selection.hpp diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 0cf0af12..3fd1c5ba 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -1207,6 +1207,7 @@ AdvSceneSwitcher.action.twitch="Twitch" AdvSceneSwitcher.action.twitch.type.channel.info.title.set="Set stream title" AdvSceneSwitcher.action.twitch.type.channel.info.category.set="Set stream category" AdvSceneSwitcher.action.twitch.type.channel.info.tags.set="Set stream tags" +AdvSceneSwitcher.action.twitch.type.channel.info.language.set="Set stream language" AdvSceneSwitcher.action.twitch.type.raid.start="Start raid" AdvSceneSwitcher.action.twitch.type.commercial.start="Start commercial" AdvSceneSwitcher.action.twitch.type.marker.create="Create stream marker" @@ -1250,6 +1251,42 @@ AdvSceneSwitcher.action.twitch.contentClassification.violentGraphic="Simulations AdvSceneSwitcher.action.twitch.contentClassification.gambling="Participating in or promoting online or in-person gambling, poker, or fantasy sports that involve the exchange of real money." AdvSceneSwitcher.action.twitch.contentClassification.profanityVulgarity="Prolonged and repeated use of obscenities, profanities, and vulgarities, especially as a regular part of speech." AdvSceneSwitcher.action.twitch.contentClassification.getCurrent="Get current content classification labels" +AdvSceneSwitcher.action.twitch.language.english="English" +AdvSceneSwitcher.action.twitch.language.indonesian="Bahasa Indonesia" +AdvSceneSwitcher.action.twitch.language.catalan="Català" +AdvSceneSwitcher.action.twitch.language.danish="Dansk" +AdvSceneSwitcher.action.twitch.language.german="Deutsch" +AdvSceneSwitcher.action.twitch.language.spanish="Español" +AdvSceneSwitcher.action.twitch.language.french="Français" +AdvSceneSwitcher.action.twitch.language.italian="Italiano" +AdvSceneSwitcher.action.twitch.language.hungarian="Magyar" +AdvSceneSwitcher.action.twitch.language.dutch="Nederlands" +AdvSceneSwitcher.action.twitch.language.norwegian="Norsk" +AdvSceneSwitcher.action.twitch.language.polish="Polski" +AdvSceneSwitcher.action.twitch.language.portuguese="Português" +AdvSceneSwitcher.action.twitch.language.romanian="Română" +AdvSceneSwitcher.action.twitch.language.slovak="Slovenčina" +AdvSceneSwitcher.action.twitch.language.finnish="Suomi" +AdvSceneSwitcher.action.twitch.language.swedish="Svenska" +AdvSceneSwitcher.action.twitch.language.tagalog="Tagalog" +AdvSceneSwitcher.action.twitch.language.vietnamese="Tiếng Việt" +AdvSceneSwitcher.action.twitch.language.turkish="Türkçe" +AdvSceneSwitcher.action.twitch.language.czech="Čeština" +AdvSceneSwitcher.action.twitch.language.greek="Ελληνικά" +AdvSceneSwitcher.action.twitch.language.bulgarian="Български" +AdvSceneSwitcher.action.twitch.language.russian="Русский" +AdvSceneSwitcher.action.twitch.language.ukrainian="Українська" +AdvSceneSwitcher.action.twitch.language.arabic="العربية" +AdvSceneSwitcher.action.twitch.language.malay="بهاس ملايو" +AdvSceneSwitcher.action.twitch.language.hindi="मानक हिन्दी" +AdvSceneSwitcher.action.twitch.language.thai="ภาษาไทย" +AdvSceneSwitcher.action.twitch.language.chinese="中文" +AdvSceneSwitcher.action.twitch.language.japanese="日本語" +AdvSceneSwitcher.action.twitch.language.cantonese="粵語" +AdvSceneSwitcher.action.twitch.language.korean="한국어" +AdvSceneSwitcher.action.twitch.language.asl="American Sign Language" +AdvSceneSwitcher.action.twitch.language.other="Other" +AdvSceneSwitcher.action.twitch.language.getCurrent="Get current language" AdvSceneSwitcher.action.clipboard="Clipboard" AdvSceneSwitcher.action.clipboard.type.copy.text="Copy text" AdvSceneSwitcher.action.clipboard.type.copy.image="Copy image" diff --git a/plugins/twitch/CMakeLists.txt b/plugins/twitch/CMakeLists.txt index 49cfd328..e52a9452 100644 --- a/plugins/twitch/CMakeLists.txt +++ b/plugins/twitch/CMakeLists.txt @@ -90,6 +90,8 @@ target_sources( content-classification.hpp event-sub.cpp event-sub.hpp + language-selection.cpp + language-selection.hpp macro-action-twitch.cpp macro-action-twitch.hpp macro-condition-twitch.cpp diff --git a/plugins/twitch/language-selection.cpp b/plugins/twitch/language-selection.cpp new file mode 100644 index 00000000..4c9de28a --- /dev/null +++ b/plugins/twitch/language-selection.cpp @@ -0,0 +1,314 @@ +#include "language-selection.hpp" +#include "channel-selection.hpp" +#include "log-helper.hpp" +#include "obs-module-helper.hpp" +#include "twitch-helpers.hpp" +#include "ui-helpers.hpp" + +#include +#include + +namespace advss { + +namespace { + +struct LocaleMapping { + const QString languageCode; + const QString name; +}; + +const std::vector &getLocales() +{ + static std::vector locales; + static const char *lastLocaleCode = ""; + const char *curLocale = obs_get_locale(); + if (strcmp(lastLocaleCode, curLocale) != 0) { + locales.clear(); + locales.emplace_back(LocaleMapping{ + "en", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.english")}); + locales.emplace_back(LocaleMapping{ + "id", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.indonesian")}); + locales.emplace_back(LocaleMapping{ + "ca", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.catalan")}); + locales.emplace_back(LocaleMapping{ + "da", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.danish")}); + locales.emplace_back(LocaleMapping{ + "de", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.german")}); + locales.emplace_back(LocaleMapping{ + "es", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.spanish")}); + locales.emplace_back(LocaleMapping{ + "fr", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.french")}); + locales.emplace_back(LocaleMapping{ + "it", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.italian")}); + locales.emplace_back(LocaleMapping{ + "hu", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.hungarian")}); + locales.emplace_back(LocaleMapping{ + "nl", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.dutch")}); + locales.emplace_back(LocaleMapping{ + "no", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.norwegian")}); + locales.emplace_back(LocaleMapping{ + "pl", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.polish")}); + locales.emplace_back(LocaleMapping{ + "pt", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.portuguese")}); + locales.emplace_back(LocaleMapping{ + "ro", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.romanian")}); + locales.emplace_back(LocaleMapping{ + "sk", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.slovak")}); + locales.emplace_back(LocaleMapping{ + "fi", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.finnish")}); + locales.emplace_back(LocaleMapping{ + "sv", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.swedish")}); + locales.emplace_back(LocaleMapping{ + "tl", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.tagalog")}); + locales.emplace_back(LocaleMapping{ + "vi", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.vietnamese")}); + locales.emplace_back(LocaleMapping{ + "tr", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.turkish")}); + locales.emplace_back(LocaleMapping{ + "cs", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.czech")}); + locales.emplace_back(LocaleMapping{ + "el", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.greek")}); + locales.emplace_back(LocaleMapping{ + "bg", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.bulgarian")}); + locales.emplace_back(LocaleMapping{ + "ru", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.russian")}); + locales.emplace_back(LocaleMapping{ + "uk", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.ukrainian")}); + locales.emplace_back(LocaleMapping{ + "ar", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.arabic")}); + locales.emplace_back(LocaleMapping{ + "ms", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.malay")}); + locales.emplace_back(LocaleMapping{ + "hi", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.hindi")}); + locales.emplace_back(LocaleMapping{ + "th", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.thai")}); + locales.emplace_back(LocaleMapping{ + "zh", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.chinese")}); + locales.emplace_back(LocaleMapping{ + "ja", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.japanese")}); + locales.emplace_back(LocaleMapping{ + "zh-hk", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.cantonese")}); + locales.emplace_back(LocaleMapping{ + "ko", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.korean")}); + locales.emplace_back(LocaleMapping{ + "asl", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.asl")}); + locales.emplace_back(LocaleMapping{ + "other", + obs_module_text( + "AdvSceneSwitcher.action.twitch.language.other")}); + lastLocaleCode = curLocale; + } + + return locales; +} + +} // namespace + +void LanguageSelection::Load(obs_data_t *obj) +{ + OBSDataAutoRelease data = obs_data_get_obj(obj, "languageSelection"); + _language.Load(data, "language"); + _useManualInput = obs_data_get_bool(data, "useManualInput"); +} + +void LanguageSelection::Save(obs_data_t *obj) const +{ + OBSDataAutoRelease data = obs_data_create(); + _language.Save(data, "language"); + obs_data_set_bool(data, "useManualInput", _useManualInput); + obs_data_set_obj(obj, "languageSelection", data); +} + +void LanguageSelection::SetStreamLanguage(const TwitchToken &token) const +{ + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "broadcaster_language", _language.c_str()); + auto result = SendPatchRequest(token, "https://api.twitch.tv", + "/helix/channels", + {{"broadcaster_id", token.GetUserID()}}, + data.Get()); + + if (result.status != 204) { + blog(LOG_INFO, "Failed to set stream language! (%d)", + result.status); + } +} + +static void populateLocaleSelection(QComboBox *list) +{ + for (const auto &[languageCode, name] : getLocales()) { + list->addItem(name, languageCode); + } +} + +LanguageSelectionWidget::LanguageSelectionWidget(QWidget *parent) + : QWidget(parent), + _languageText(new VariableLineEdit(this)), + _languageCombo(new QComboBox(this)), + _toggleInput(new QPushButton(this)) +{ + populateLocaleSelection(_languageCombo); + + auto getCurrent = new QPushButton(obs_module_text( + "AdvSceneSwitcher.action.twitch.language.getCurrent")); + connect(getCurrent, &QPushButton::clicked, this, + &LanguageSelectionWidget::GetCurrentClicked); + + _toggleInput->setCheckable(true); + _toggleInput->setMaximumWidth(11); + SetButtonIcon(_toggleInput, GetThemeTypeName() == "Light" + ? ":/res/images/dots-vert.svg" + : "theme:Dark/dots-vert.svg"); + + connect(_toggleInput, &QPushButton::toggled, this, [this]() { + _languageText->setVisible(!_languageText->isVisible()); + _languageCombo->setVisible(!_languageCombo->isVisible()); + }); + + connect(_languageText, &VariableLineEdit::editingFinished, this, + &LanguageSelectionWidget::TextChanged); + connect(_languageCombo, &QComboBox::currentIndexChanged, this, + &LanguageSelectionWidget::ComboSelectionChanged); + + auto inputLayout = new QHBoxLayout; + inputLayout->addWidget(_languageCombo); + inputLayout->addWidget(_languageText); + inputLayout->addWidget(_toggleInput); + + auto layout = new QVBoxLayout; + layout->addLayout(inputLayout); + layout->addWidget(getCurrent); + setLayout(layout); +} + +void LanguageSelectionWidget::SetLanguageSelection( + const LanguageSelection &language) +{ + const QSignalBlocker b1(_languageText); + const QSignalBlocker b2(_languageCombo); + + _languageText->setText(language._language); + _languageCombo->setCurrentIndex( + _languageCombo->findData(QString(language._language.c_str()))); + _toggleInput->setChecked(language._useManualInput); + _languageText->setVisible(language._useManualInput); + _languageCombo->setVisible(!language._useManualInput); +} + +void LanguageSelectionWidget::TextChanged() +{ + LanguageSelection language; + language._useManualInput = _toggleInput->isChecked(); + language._language = _languageText->text().toStdString(); + emit LanguageChanged(language); + + const QSignalBlocker b(this); + SetLanguageSelection(language); +} + +void LanguageSelectionWidget::ComboSelectionChanged() +{ + LanguageSelection language; + language._useManualInput = _toggleInput->isChecked(); + language._language = + _languageCombo->currentData().toString().toStdString(); + emit LanguageChanged(language); + + const QSignalBlocker b(this); + SetLanguageSelection(language); +} + +void LanguageSelectionWidget::SetToken(const std::weak_ptr &t) +{ + _token = t; +} + +void LanguageSelectionWidget::GetCurrentClicked() +{ + auto token = _token.lock(); + if (!token) { + return; + } + + TwitchChannel channel; + channel.SetName(token->Name()); + const auto channelInfo = channel.GetInfo(*token); + if (!channelInfo) { + return; + } + + LanguageSelection language; + language._language = channelInfo->broadcaster_language; + + SetLanguageSelection(language); +} + +} // namespace advss diff --git a/plugins/twitch/language-selection.hpp b/plugins/twitch/language-selection.hpp new file mode 100644 index 00000000..af33a4a0 --- /dev/null +++ b/plugins/twitch/language-selection.hpp @@ -0,0 +1,41 @@ +#pragma once +#include "variable-line-edit.hpp" +#include "token.hpp" + +namespace advss { + +struct LanguageSelection { + void Load(obs_data_t *obj); + void Save(obs_data_t *obj) const; + void SetStreamLanguage(const TwitchToken &) const; + +private: + StringVariable _language = "en"; + bool _useManualInput = false; + friend class LanguageSelectionWidget; +}; + +class LanguageSelectionWidget final : public QWidget { + Q_OBJECT + +public: + explicit LanguageSelectionWidget(QWidget *parent = nullptr); + void SetLanguageSelection(const LanguageSelection &); + void SetToken(const std::weak_ptr &); + +private slots: + void GetCurrentClicked(); + void TextChanged(); + void ComboSelectionChanged(); +signals: + void LanguageChanged(const LanguageSelection &); + +private: + VariableLineEdit *_languageText; + QComboBox *_languageCombo; + QPushButton *_toggleInput; + + std::weak_ptr _token; +}; + +} // namespace advss diff --git a/plugins/twitch/macro-action-twitch.cpp b/plugins/twitch/macro-action-twitch.cpp index f394df68..50751189 100644 --- a/plugins/twitch/macro-action-twitch.cpp +++ b/plugins/twitch/macro-action-twitch.cpp @@ -59,6 +59,8 @@ const static std::map actionTypes = { "AdvSceneSwitcher.action.twitch.type.channel.info.category.set"}, {MacroActionTwitch::Action::CHANNEL_INFO_TAGS_SET, "AdvSceneSwitcher.action.twitch.type.channel.info.tags.set"}, + {MacroActionTwitch::Action::CHANNEL_INFO_LANGUAGE_SET, + "AdvSceneSwitcher.action.twitch.type.channel.info.language.set"}, {MacroActionTwitch::Action::CHANNEL_INFO_CONTENT_LABELS_SET, "AdvSceneSwitcher.action.twitch.type.channel.info.contentClassification.set"}, {MacroActionTwitch::Action::RAID_START, @@ -525,6 +527,9 @@ bool MacroActionTwitch::PerformAction() case Action::CHANNEL_INFO_TAGS_SET: _tags.SetStreamTags(*token); break; + case Action::CHANNEL_INFO_LANGUAGE_SET: + _language.SetStreamLanguage(*token); + break; case Action::CHANNEL_INFO_CONTENT_LABELS_SET: _contentClassification.SetContentClassification(*token); break; @@ -588,6 +593,7 @@ bool MacroActionTwitch::Save(obs_data_t *obj) const _streamTitle.Save(obj, "streamTitle"); _category.Save(obj); _tags.Save(obj); + _language.Save(obj); _contentClassification.Save(obj); _markerDescription.Save(obj, "markerDescription"); obs_data_set_bool(obj, "clipHasDelay", _clipHasDelay); @@ -618,6 +624,7 @@ bool MacroActionTwitch::Load(obs_data_t *obj) _streamTitle.Load(obj, "streamTitle"); _category.Load(obj); _tags.Load(obj); + _language.Load(obj); _contentClassification.Load(obj); _markerDescription.Load(obj, "markerDescription"); _clipHasDelay = obs_data_get_bool(obj, "clipHasDelay"); @@ -835,6 +842,7 @@ MacroActionTwitchEdit::MacroActionTwitchEdit( _streamTitle(new VariableLineEdit(this)), _category(new TwitchCategoryWidget(this)), _tags(new TagListWidget(this)), + _language(new LanguageSelectionWidget(this)), _contentClassification(new ContentClassificationEdit(this)), _markerDescription(new VariableLineEdit(this)), _clipHasDelay(new QCheckBox(obs_module_text( @@ -862,6 +870,7 @@ MacroActionTwitchEdit::MacroActionTwitchEdit( mainLayout->addWidget(_announcementMessage); mainLayout->addWidget(_chatMessage); mainLayout->addWidget(_tags); + mainLayout->addWidget(_language); mainLayout->addWidget(_contentClassification); mainLayout->addWidget(_tokenWarning); setLayout(mainLayout); @@ -900,6 +909,7 @@ void MacroActionTwitchEdit::TwitchTokenChanged(const QString &token) _channel->SetToken(_entryData->_token); _pointsReward->SetToken(_entryData->_token); _tags->SetToken(_entryData->_token); + _language->SetToken(_entryData->_token); _contentClassification->SetToken(_entryData->_token); _entryData->ResetChatConnection(); @@ -961,6 +971,12 @@ void MacroActionTwitchEdit::TagsChanged(const TwitchTagList &tags) _entryData->_tags = tags; } +void MacroActionTwitchEdit::LanguageChanged(const LanguageSelection &language) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_language = language; +} + void MacroActionTwitchEdit::ContentClassificationChanged( const ContentClassification &ccl) { @@ -1050,6 +1066,10 @@ void MacroActionTwitchEdit::SetWidgetSignalConnections() SLOT(CategoryChanged(const TwitchCategory &))); QWidget::connect(_tags, SIGNAL(TagListChanged(const TwitchTagList &)), this, SLOT(TagsChanged(const TwitchTagList &))); + QWidget::connect(_language, + SIGNAL(LanguageChanged(const LanguageSelection &)), + this, + SLOT(LanguageChanged(const LanguageSelection &))); QWidget::connect(_contentClassification, SIGNAL(ContentClassificationChanged( const ContentClassification &)), @@ -1100,6 +1120,9 @@ void MacroActionTwitchEdit::SetWidgetVisibility() MacroActionTwitch::Action::CHANNEL_INFO_CATEGORY_SET); _tags->setVisible(_entryData->GetAction() == MacroActionTwitch::Action::CHANNEL_INFO_TAGS_SET); + _language->setVisible( + _entryData->GetAction() == + MacroActionTwitch::Action::CHANNEL_INFO_LANGUAGE_SET); _contentClassification->setVisible( _entryData->GetAction() == MacroActionTwitch::Action::CHANNEL_INFO_CONTENT_LABELS_SET); @@ -1300,6 +1323,8 @@ void MacroActionTwitchEdit::UpdateEntryData() _category->SetCategory(_entryData->_category); _tags->SetTags(_entryData->_tags); _tags->SetToken(_entryData->_token); + _language->SetLanguageSelection(_entryData->_language); + _language->SetToken(_entryData->_token); _contentClassification->SetContentClassification( _entryData->_contentClassification); _contentClassification->SetToken(_entryData->_token); diff --git a/plugins/twitch/macro-action-twitch.hpp b/plugins/twitch/macro-action-twitch.hpp index 46e3fb41..7d5b4b0f 100644 --- a/plugins/twitch/macro-action-twitch.hpp +++ b/plugins/twitch/macro-action-twitch.hpp @@ -5,6 +5,7 @@ #include "channel-selection.hpp" #include "chat-connection.hpp" #include "content-classification.hpp" +#include "language-selection.hpp" #include "points-reward-selection.hpp" #include "tag-selection.hpp" @@ -171,6 +172,7 @@ public: obs_module_text("AdvSceneSwitcher.action.twitch.title.title"); TwitchCategory _category; TwitchTagList _tags; + LanguageSelection _language; ContentClassification _contentClassification; StringVariable _markerDescription = obs_module_text( "AdvSceneSwitcher.action.twitch.marker.description"); @@ -240,6 +242,7 @@ private slots: void StreamTitleChanged(); void CategoryChanged(const TwitchCategory &); void TagsChanged(const TwitchTagList &tags); + void LanguageChanged(const LanguageSelection &); void ContentClassificationChanged(const ContentClassification &ccl); void MarkerDescriptionChanged(); void ClipHasDelayChanged(int state); @@ -276,6 +279,7 @@ private: VariableLineEdit *_streamTitle; TwitchCategoryWidget *_category; TagListWidget *_tags; + LanguageSelectionWidget *_language; ContentClassificationEdit *_contentClassification; VariableLineEdit *_markerDescription; QCheckBox *_clipHasDelay;