language detection, twitch funcitonality updates
This commit is contained in:
parent
f7e3248b90
commit
f1df6b24ed
14 changed files with 944 additions and 648 deletions
|
|
@ -21,12 +21,14 @@
|
||||||
"electron-squirrel-startup": "^1.0.0",
|
"electron-squirrel-startup": "^1.0.0",
|
||||||
"emoji-picker-element": "^1.21.0",
|
"emoji-picker-element": "^1.21.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"flag-icons": "^7.1.0",
|
||||||
"ini": "^2.0.0",
|
"ini": "^2.0.0",
|
||||||
"kill-process-by-name": "^1.0.5",
|
"kill-process-by-name": "^1.0.5",
|
||||||
"node-google-tts-api": "^1.1.1",
|
"node-google-tts-api": "^1.1.1",
|
||||||
"querystring": "^0.2.1",
|
"querystring": "^0.2.1",
|
||||||
"socket.io": "^4.7.1",
|
"socket.io": "^4.7.1",
|
||||||
"socket.io-client": "^4.7.1",
|
"socket.io-client": "^4.7.1",
|
||||||
|
"sockette": "^2.0.6",
|
||||||
"tmi.js": "^1.8.5",
|
"tmi.js": "^1.8.5",
|
||||||
"url": "^0.11.1",
|
"url": "^0.11.1",
|
||||||
"winston": "^3.10.0",
|
"winston": "^3.10.0",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
# from wsgiref.simple_server import WSGIServer
|
|
||||||
from flask import Flask, Response, jsonify, request
|
from flask import Flask, Response, jsonify, request
|
||||||
import gevent
|
import gevent
|
||||||
|
|
||||||
|
|
@ -201,14 +200,6 @@ def stop_recording():
|
||||||
return Response("Speech recognition stopped", status=200)
|
return Response("Speech recognition stopped", status=200)
|
||||||
|
|
||||||
|
|
||||||
# @app.before_request
|
|
||||||
# def custom_warning():
|
|
||||||
# if environment == "dev":
|
|
||||||
# print(
|
|
||||||
# # "Running in internal development environment. This server is not for production use."
|
|
||||||
# )
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/terminate", methods=["GET"])
|
@app.route("/terminate", methods=["GET"])
|
||||||
def terminate_processes():
|
def terminate_processes():
|
||||||
shutdown_server()
|
shutdown_server()
|
||||||
|
|
@ -222,25 +213,37 @@ def shutdown_server():
|
||||||
func()
|
func()
|
||||||
|
|
||||||
|
|
||||||
# @app.route("/detect", methods=["POST"])
|
|
||||||
# def server_status():
|
|
||||||
# try:
|
|
||||||
# request_data = request.json
|
|
||||||
# message = request_data.get("message", "")
|
|
||||||
# confidence_values = detector.compute_language_confidence_values(message)
|
|
||||||
# for language, value in confidence_values:
|
|
||||||
# print(f"{language.name}: {value:.2f}")
|
|
||||||
# message = request_data.get("message", "")
|
|
||||||
# except Exception as e:
|
|
||||||
# return jsonify({"error": "An error occurred"}), 500
|
|
||||||
# return jsonify({"message": "Audio triggered"}), 200
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/status", methods=["GET"])
|
@app.route("/status", methods=["GET"])
|
||||||
def server_status():
|
def server_status():
|
||||||
return jsonify({"status": "server is running"})
|
return jsonify({"status": "server is running"})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/detect", methods=["POST"])
|
||||||
|
def get_language():
|
||||||
|
try:
|
||||||
|
request_data = request.json
|
||||||
|
message = request_data.get("message", "")
|
||||||
|
lang = LanguageDetection().predict_lang(message)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": "An error occurred"}), 500
|
||||||
|
return jsonify({"languages": lang}), 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/translate", methods=["POST"])
|
||||||
|
def get_translation():
|
||||||
|
try:
|
||||||
|
settings.read(settingsPath)
|
||||||
|
request_data = request.json
|
||||||
|
message = request_data.get("message", "")
|
||||||
|
detectedLanguage = request_data.get("language", "")
|
||||||
|
translated = MyMemoryTranslator(
|
||||||
|
source=detectedLanguage, target=settings["LANGUAGE"]["TRANSLATE_TO"]
|
||||||
|
).translate(message)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": e}), 500
|
||||||
|
return jsonify({"translation": translated}), 200
|
||||||
|
|
||||||
|
|
||||||
@app.route("/audio", methods=["POST"])
|
@app.route("/audio", methods=["POST"])
|
||||||
def trigger_backend_event():
|
def trigger_backend_event():
|
||||||
try:
|
try:
|
||||||
|
|
@ -255,7 +258,7 @@ def trigger_backend_event():
|
||||||
count = request_data.get("count")
|
count = request_data.get("count")
|
||||||
text_to_speech_service.say(filteredMessage, voice, count)
|
text_to_speech_service.say(filteredMessage, voice, count)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": "An error occurred"}), 500
|
return jsonify({"error": e}), 500
|
||||||
return jsonify({"message": "Audio triggered"}), 200
|
return jsonify({"message": "Audio triggered"}), 200
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -265,18 +268,10 @@ def get_voices():
|
||||||
voices = text_to_speech_service.voices()
|
voices = text_to_speech_service.voices()
|
||||||
return jsonify({"voices": voices}), 200
|
return jsonify({"voices": voices}), 200
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": "An error occurred"}), 500
|
return jsonify({"error": e}), 500
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# LANGUAGE = LanguageDetection()
|
|
||||||
# lang = LANGUAGE.predict_lang("hola cómo estás")
|
|
||||||
# print(lang)
|
|
||||||
# text = "Keep it up. You are awesome"
|
|
||||||
# translated = MyMemoryTranslator(
|
|
||||||
# source="english", target="spanish latin america"
|
|
||||||
# ).translate(text)
|
|
||||||
# print(translated)
|
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
settings.read(settingsPath)
|
settings.read(settingsPath)
|
||||||
port = int(settings["GENERAL"]["PORT"])
|
port = int(settings["GENERAL"]["PORT"])
|
||||||
|
|
|
||||||
|
|
@ -513,3 +513,32 @@ h1 {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fi {
|
||||||
|
position: relative;
|
||||||
|
z-index: 5;
|
||||||
|
top: 45px;
|
||||||
|
left: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-header {
|
||||||
|
background-color: var(--main-color4);
|
||||||
|
border-radius: 5px;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 5px;
|
||||||
|
margin: 10px 0px 5px -5px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-message {
|
||||||
|
position: relative;
|
||||||
|
margin: 20px 0px 0px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-icon {
|
||||||
|
position: relative;
|
||||||
|
padding: 0px 0px 0px 0px;
|
||||||
|
margin: -45px 0px 0px -40px;
|
||||||
|
top: -15px;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -395,27 +395,6 @@ input[type='lol'] {
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .tooltip .tooltiptext {
|
|
||||||
width: 120px;
|
|
||||||
background-color: black;
|
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip .tooltiptext::after {
|
|
||||||
content: "";
|
|
||||||
margin-left: -5px;
|
|
||||||
border-width: 5px;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: black transparent transparent transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip:hover .tooltiptext {
|
|
||||||
visibility: visible;
|
|
||||||
} */
|
|
||||||
|
|
||||||
div[type='text']:disabled {
|
div[type='text']:disabled {
|
||||||
background: #4b4b4b;
|
background: #4b4b4b;
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="./css/logger.css" />
|
<link rel="stylesheet" href="./css/logger.css" />
|
||||||
<script type="module" src="https://cdn.jsdelivr.net/npm/emoji-picker-element@^1/index.js"></script>
|
<script type="module" src="https://cdn.jsdelivr.net/npm/emoji-picker-element@^1/index.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.0.0/css/flag-icons.min.css" />
|
||||||
<!--#endregion -->
|
<!--#endregion -->
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
@ -151,6 +152,15 @@
|
||||||
tip="Port to use to host additional services"
|
tip="Port to use to host additional services"
|
||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="AdvancedMenuRow">
|
||||||
|
<div class="AdvancedMenuLabel">Open Settings file</div>
|
||||||
|
<button type="text" class="AdvancedMenuButton" id="OPEN_SETTINGS_FILE"><i class="fa-solid fa-file-lines"></i></button>
|
||||||
|
<i
|
||||||
|
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
|
||||||
|
id="Info_OPEN_SETTINGS_FILE"
|
||||||
|
tip="Open the settings file"
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset id="TTSMenu" class="AdvancedMenu">
|
<fieldset id="TTSMenu" class="AdvancedMenu">
|
||||||
|
|
@ -160,22 +170,14 @@
|
||||||
<label for="USE_TTS" class="toggle-small"></label>
|
<label for="USE_TTS" class="toggle-small"></label>
|
||||||
<div class="AdvancedMenuLabel3">Enable TTS</div>
|
<div class="AdvancedMenuLabel3">Enable TTS</div>
|
||||||
</legend>
|
</legend>
|
||||||
<div class="AdvancedMenuRow inputTTS">
|
<div class="AdvancedMenuRow inputTTS" style="height: 0; visibility: hidden">
|
||||||
<div class="AdvancedMenuLabel">Default TTS Service</div>
|
<div class="AdvancedMenuLabel">Default TTS Service</div>
|
||||||
<select class="menu-select" name="primaryTTSService" id="primaryTTSService"></select>
|
<select class="menu-select" name="primaryTTSService" id="primaryTTSService"></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="AdvancedMenuRow languageDetectionInput">
|
<div class="AdvancedMenuRow languageDetectionInput" style="height: 0; visibility: hidden">
|
||||||
<div class="AdvancedMenuLabel">Default TTS language</div>
|
|
||||||
<select class="menu-select" name="defaultLanguage" id="defaultLanguage" tip="Language Service to use"></select>
|
|
||||||
</div>
|
|
||||||
<div class="AdvancedMenuRow languageDetectionInput">
|
|
||||||
<div class="AdvancedMenuLabel">2<sup>nd</sup> TTS Service</div>
|
<div class="AdvancedMenuLabel">2<sup>nd</sup> TTS Service</div>
|
||||||
<select class="menu-select" name="secondaryTTSService" id="secondaryTTSService"></select>
|
<select class="menu-select" name="secondaryTTSService" id="secondaryTTSService"></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="AdvancedMenuRow languageDetectionInput">
|
|
||||||
<div class="AdvancedMenuLabel">2<sup>nd</sup> TTS language</div>
|
|
||||||
<select class="menu-select" name="secondaryLanguage" id="secondaryLanguage" tip="Language Service to use"></select>
|
|
||||||
</div>
|
|
||||||
<div class="AdvancedMenuRow inputTTS">
|
<div class="AdvancedMenuRow inputTTS">
|
||||||
<div class="AdvancedMenuLabel">TTS Output Device</div>
|
<div class="AdvancedMenuLabel">TTS Output Device</div>
|
||||||
<select class="menu-select" name="ttsAudioDevice" id="ttsAudioDevice"></select>
|
<select class="menu-select" name="ttsAudioDevice" id="ttsAudioDevice"></select>
|
||||||
|
|
@ -247,16 +249,25 @@
|
||||||
</legend>
|
</legend>
|
||||||
<div class="AdvancedMenuRow languageDetectionInput">
|
<div class="AdvancedMenuRow languageDetectionInput">
|
||||||
<div class="AdvancedMenuLabel">Translate chat messages to</div>
|
<div class="AdvancedMenuLabel">Translate chat messages to</div>
|
||||||
<select class="menu-select" name="language" id="translateChatMessageLanguage" tip="Language Service to use"></select>
|
<select class="menu-select" name="language" id="TRANSLATE_TO" tip="Language Service to use"></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="AdvancedMenuRow languageDetectionInput">
|
<div class="AdvancedMenuRow languageDetectionInput">
|
||||||
<div class="AdvancedMenuLabel">Broadcast translation</div>
|
<div class="AdvancedMenuLabel">Broadcast translation to chat</div>
|
||||||
<label for="USE_CHAT_LANGUAGE_DETECTION" class="toggle-small"></label>
|
<input type="checkbox" id="BROADCAST_TRANSLATION" class="checkbox" />
|
||||||
<input type="checkbox" id="USE_CHAT_LANGUAGE_DETECTION" class="checkbox" />
|
<label for="BROADCAST_TRANSLATION" class="toggle-small"></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="AdvancedMenuRow languageDetectionInput">
|
<div class="AdvancedMenuRow languageDetectionInput">
|
||||||
<div class="AdvancedMenuLabel">Output to TTS service</div>
|
<div class="AdvancedMenuLabel">Output translation to TTS</div>
|
||||||
<select class="menu-select" name="language" id="translateChatMessageLanguage" tip="Language Service to use"></select>
|
<input type="checkbox" id="OUTPUT_TO_TTS" class="checkbox" />
|
||||||
|
<label for="OUTPUT_TO_TTS" class="toggle-small"></label>
|
||||||
|
</div>
|
||||||
|
<div class="AdvancedMenuRow outputToTtsInput">
|
||||||
|
<div class="AdvancedMenuLabel">Default TTS service language</div>
|
||||||
|
<select class="menu-select" name="defaultLanguage" id="defaultLanguage" tip="Language Service to use"></select>
|
||||||
|
</div>
|
||||||
|
<div class="AdvancedMenuRow outputToTtsInput">
|
||||||
|
<div class="AdvancedMenuLabel">2<sup>nd</sup> TTS service language</div>
|
||||||
|
<select class="menu-select" name="secondaryLanguage" id="secondaryLanguage" tip="Language Service to use"></select>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
|
@ -267,7 +278,7 @@
|
||||||
<label for="USE_NOTIFICATION_SOUNDS" class="toggle-small"></label>
|
<label for="USE_NOTIFICATION_SOUNDS" class="toggle-small"></label>
|
||||||
<div class="AdvancedMenuLabel3">Enable notification sounds</div>
|
<div class="AdvancedMenuLabel3">Enable notification sounds</div>
|
||||||
</legend>
|
</legend>
|
||||||
<div class="AdvancedMenuRow inputTTS">
|
<div class="AdvancedMenuRow inputNotificationSound">
|
||||||
<div class="AdvancedMenuLabel">Notification sounds Output Device</div>
|
<div class="AdvancedMenuLabel">Notification sounds Output Device</div>
|
||||||
<select class="menu-select" name="notificationSoundAudioDevice" id="notificationSoundAudioDevice"></select>
|
<select class="menu-select" name="notificationSoundAudioDevice" id="notificationSoundAudioDevice"></select>
|
||||||
<i
|
<i
|
||||||
|
|
@ -276,7 +287,6 @@
|
||||||
tip="Outputting to specific device will NOT work with voicemeter"
|
tip="Outputting to specific device will NOT work with voicemeter"
|
||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="AdvancedMenuRow inputNotificationSound">
|
<div class="AdvancedMenuRow inputNotificationSound">
|
||||||
<div class="AdvancedMenuLabel">Notification Volume</div>
|
<div class="AdvancedMenuLabel">Notification Volume</div>
|
||||||
<div class="slider-container">
|
<div class="slider-container">
|
||||||
|
|
@ -304,6 +314,20 @@
|
||||||
<label for="USE_TWITCH" class="toggle-small"></label>
|
<label for="USE_TWITCH" class="toggle-small"></label>
|
||||||
<div class="AdvancedMenuLabel3">Enable Twitch</div>
|
<div class="AdvancedMenuLabel3">Enable Twitch</div>
|
||||||
</legend>
|
</legend>
|
||||||
|
<div class="AdvancedMenuRow inputTwitch">
|
||||||
|
<div class="AdvancedMenuLabel">Oauth Token</div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
type2="text"
|
||||||
|
class="fname inputTwitch"
|
||||||
|
id="TWITCH_OAUTH_TOKEN"
|
||||||
|
placeholder="click the key icon to get the OAuth token"
|
||||||
|
/>
|
||||||
|
<button class="password-toggle-btn password-toggle-btn1">
|
||||||
|
<span class="password-toggle-icon"><i class="fa-regular fa-eye-slash"></i></span>
|
||||||
|
</button>
|
||||||
|
<i class="fa-solid fa-key fa-2x SmallButton option-icon-container" id="Info_USERNAME" tip="Get OAuth Token"></i>
|
||||||
|
</div>
|
||||||
<div class="AdvancedMenuRow inputTwitch">
|
<div class="AdvancedMenuRow inputTwitch">
|
||||||
<div class="AdvancedMenuLabel">Channel Name</div>
|
<div class="AdvancedMenuLabel">Channel Name</div>
|
||||||
<input type="text" class="fname inputTwitch" id="TWITCH_CHANNEL_NAME" />
|
<input type="text" class="fname inputTwitch" id="TWITCH_CHANNEL_NAME" />
|
||||||
|
|
@ -313,20 +337,6 @@
|
||||||
tip="The channel you want to connect to"
|
tip="The channel you want to connect to"
|
||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="AdvancedMenuRow inputTwitch">
|
|
||||||
<div class="AdvancedMenuLabel">Oauth Token</div>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
type2="text"
|
|
||||||
class="fname inputTwitch"
|
|
||||||
id="TWITCH_OAUTH_TOKEN"
|
|
||||||
placeholder="click the ? icon to get the OAuth token"
|
|
||||||
/>
|
|
||||||
<button class="password-toggle-btn password-toggle-btn1">
|
|
||||||
<span class="password-toggle-icon"><i class="fa-regular fa-eye-slash"></i></span>
|
|
||||||
</button>
|
|
||||||
<i class="fa fa-question-circle fa-2x SmallButton option-icon-container" id="Info_USERNAME" tip="Get OAuth Token"></i>
|
|
||||||
</div>
|
|
||||||
<div class="AdvancedMenuRow inputTwitch">
|
<div class="AdvancedMenuRow inputTwitch">
|
||||||
<div class="AdvancedMenuLabel">Test credentials</div>
|
<div class="AdvancedMenuLabel">Test credentials</div>
|
||||||
<button type="text" class="AdvancedMenuButton" id="TestTwitchCredentials">Test</button>
|
<button type="text" class="AdvancedMenuButton" id="TestTwitchCredentials">Test</button>
|
||||||
|
|
@ -336,17 +346,6 @@
|
||||||
tip="Test Twitch credentials"
|
tip="Test Twitch credentials"
|
||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="AdvancedMenuRow inputTwitch">
|
|
||||||
<div class="AdvancedMenuLabel">Get Twitch emotes</div>
|
|
||||||
<button type="text" class="AdvancedMenuButton" id="GetTwitchEmotes">
|
|
||||||
<img src="https://static-cdn.jtvnw.net/emoticons/v2/25/static/light/1.0" />
|
|
||||||
</button>
|
|
||||||
<i
|
|
||||||
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
|
|
||||||
id="Info_TWITCH_GET_EMOTES"
|
|
||||||
tip="Get Twitch emotes available to you"
|
|
||||||
></i>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset id="AdvancedMenuServer" class="AdvancedMenu">
|
<fieldset id="AdvancedMenuServer" class="AdvancedMenu">
|
||||||
|
|
@ -356,7 +355,7 @@
|
||||||
<label for="USE_MODULES" class="toggle-small"></label>
|
<label for="USE_MODULES" class="toggle-small"></label>
|
||||||
<div class="AdvancedMenuLabel3">Enable Modules</div>
|
<div class="AdvancedMenuLabel3">Enable Modules</div>
|
||||||
</legend>
|
</legend>
|
||||||
<div class="AdvancedMenuRow inputServer">
|
<div class="AdvancedMenuRow inputServer" style="height: 0; visibility: hidden">
|
||||||
<div class="AdvancedMenuLabel">Use PNGtuber</div>
|
<div class="AdvancedMenuLabel">Use PNGtuber</div>
|
||||||
<input type="checkbox" id="USE_PNGTUBER" class="checkbox" />
|
<input type="checkbox" id="USE_PNGTUBER" class="checkbox" />
|
||||||
<label for="USE_PNGTUBER" class="toggle-small"></label>
|
<label for="USE_PNGTUBER" class="toggle-small"></label>
|
||||||
|
|
@ -389,7 +388,7 @@
|
||||||
tip="You can use it as a browsersource on http://localhost:PORT/chat"
|
tip="You can use it as a browsersource on http://localhost:PORT/chat"
|
||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="AdvancedMenuRow inputServer">
|
<div class="AdvancedMenuRow inputServer" style="height: 0; visibility: hidden">
|
||||||
<div class="AdvancedMenuLabel">Use Finger</div>
|
<div class="AdvancedMenuLabel">Use Finger</div>
|
||||||
<input type="checkbox" id="USE_CHATBUBBLE" class="checkbox" />
|
<input type="checkbox" id="USE_CHATBUBBLE" class="checkbox" />
|
||||||
<label for="USE_CHATBUBBLE" class="toggle-small"></label>
|
<label for="USE_CHATBUBBLE" class="toggle-small"></label>
|
||||||
|
|
@ -402,7 +401,7 @@
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset id="AdvancedMenuAmazon" class="AdvancedMenu">
|
<fieldset id="AdvancedMenuAmazon" class="AdvancedMenu" style="height: 0; visibility: hidden">
|
||||||
<legend class="legendStyle" tip="Enable/Disable Amazon">
|
<legend class="legendStyle" tip="Enable/Disable Amazon">
|
||||||
<img class="AdvancedMenuIcon" src="./images/amazon.png" alt />
|
<img class="AdvancedMenuIcon" src="./images/amazon.png" alt />
|
||||||
<input type="checkbox" id="USE_AMAZON" class="checkbox" />
|
<input type="checkbox" id="USE_AMAZON" class="checkbox" />
|
||||||
|
|
@ -479,7 +478,7 @@
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset id="AdvancedMenuGoogle" class="AdvancedMenu">
|
<fieldset id="AdvancedMenuGoogle" class="AdvancedMenu" style="height: 0; visibility: hidden">
|
||||||
<legend class="legendStyle" tip="Enable/Disable Google service">
|
<legend class="legendStyle" tip="Enable/Disable Google service">
|
||||||
<img class="AdvancedMenuIcon" src="./images/google.png" alt />
|
<img class="AdvancedMenuIcon" src="./images/google.png" alt />
|
||||||
<input type="checkbox" id="USE_GOOGLE" class="checkbox" />
|
<input type="checkbox" id="USE_GOOGLE" class="checkbox" />
|
||||||
|
|
@ -556,8 +555,8 @@
|
||||||
<div id="emoji-picker" class="emoji-picker">
|
<div id="emoji-picker" class="emoji-picker">
|
||||||
<emoji-picker class="dark"></emoji-picker>
|
<emoji-picker class="dark"></emoji-picker>
|
||||||
</div>
|
</div>
|
||||||
<button class="SmallButton" id="emojis">
|
<button class="SmallButton">
|
||||||
<i class="fa-regular fa-grin fa-2x" aria-hidden="true"></i>
|
<i class="fa-regular fa-grin fa-2x" id="emojis" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<!-- User text input-->
|
<!-- User text input-->
|
||||||
<input id="textInput" class="input-box" type="text" name="msg" placeholder="Tap 'Enter' to send a message" />
|
<input id="textInput" class="input-box" type="text" name="msg" placeholder="Tap 'Enter' to send a message" />
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* global settings, fs, settingsPath, ini, shell, options, axios */
|
/* global settings,twitch, fs, settingsPath, ini, shell, options, axios */
|
||||||
|
|
||||||
const twitchAuthentication = () =>
|
const twitchAuthentication = () =>
|
||||||
new Promise(resolve => {
|
new Promise(resolve => {
|
||||||
|
|
@ -70,34 +70,10 @@ const twitchAuthentication = () =>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function getTwitchUserId() {
|
|
||||||
// Get user Logo with access token
|
|
||||||
options = {
|
|
||||||
method: 'GET',
|
|
||||||
url: 'https://api.twitch.tv/helix/users',
|
|
||||||
headers: {
|
|
||||||
'Client-ID': settings.TWITCH.CLIENT_ID,
|
|
||||||
Authorization: `Bearer ${settings.TWITCH.OAUTH_TOKEN}`
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
axios
|
|
||||||
.request(options)
|
|
||||||
.then(responseLogoUrl => {
|
|
||||||
console.log(responseLogoUrl.data.data[0]);
|
|
||||||
settings.TWITCH.USERNAME = responseLogoUrl.data.data[0].display_name;
|
|
||||||
settings.TWITCH.USER_LOGO_URL = responseLogoUrl.data.data[0].profile_image_url;
|
|
||||||
settings.TWITCH.USER_ID = responseLogoUrl.data.data[0].id;
|
|
||||||
fs.writeFileSync(settingsPath, ini.stringify(settings));
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTwitchOauthToken() {
|
function getTwitchOauthToken() {
|
||||||
return twitchAuthentication().then(res => {
|
return twitchAuthentication().then(res => {
|
||||||
getTwitchUserId();
|
twitch.getTwitchUserId();
|
||||||
|
// twitch.getTwitchChannelId();
|
||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* global settings, addVoiceService, internalVoices, ttsRequestCount, main, path, pythonPath, settingsPath, ipcRenderer */
|
/* global settings, sound, twitch, getLanguageProperties, addSingleTooltip, showChatMessage, languageObject, addVoiceService, internalVoices, ttsRequestCount, main, path, pythonPath, settingsPath, ipcRenderer */
|
||||||
|
|
||||||
const spawn = require('child_process').spawn;
|
const spawn = require('child_process').spawn;
|
||||||
const kill = require('kill-process-by-name');
|
const kill = require('kill-process-by-name');
|
||||||
|
|
@ -14,7 +14,7 @@ async function getInstalledVoices() {
|
||||||
const response = await fetch(`http://127.0.0.1:${settings.GENERAL.PORT}/voices`, { method: 'GET' });
|
const response = await fetch(`http://127.0.0.1:${settings.GENERAL.PORT}/voices`, { method: 'GET' });
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const responseData = await response.json();
|
const responseData = await response.json();
|
||||||
console.log('Response:', responseData);
|
console.log('Voices:', responseData);
|
||||||
internalVoices = responseData;
|
internalVoices = responseData;
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to send termination signal to Flask server.');
|
console.error('Failed to send termination signal to Flask server.');
|
||||||
|
|
@ -44,13 +44,137 @@ async function getInstalledVoices() {
|
||||||
secondaryVoice.value = settings.TTS.SECONDARY_VOICE;
|
secondaryVoice.value = settings.TTS.SECONDARY_VOICE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setTranslatedMessage(message) {
|
||||||
|
const messageBox = document.getElementById(message.messageId).getElementsByClassName('msg-box')[0];
|
||||||
|
|
||||||
|
const translationHeader = document.createElement('div');
|
||||||
|
translationHeader.className = 'translation-header';
|
||||||
|
translationHeader.innerText = 'Translation';
|
||||||
|
messageBox.appendChild(translationHeader);
|
||||||
|
|
||||||
|
const translationIcon = document.createElement('div');
|
||||||
|
translationIcon.className = 'translation-icon';
|
||||||
|
const languageElement = document.createElement('span');
|
||||||
|
const language = getLanguageProperties(settings.LANGUAGE.TRANSLATE_TO);
|
||||||
|
languageElement.classList = `fi fi-${language.ISO3166} fis`;
|
||||||
|
languageElement.setAttribute('tip', language.name);
|
||||||
|
addSingleTooltip(languageElement);
|
||||||
|
translationIcon.appendChild(languageElement);
|
||||||
|
messageBox.appendChild(translationIcon);
|
||||||
|
|
||||||
|
const translationMessage = document.createElement('div');
|
||||||
|
translationMessage.className = 'translation-message';
|
||||||
|
translationMessage.innerText = message.translation;
|
||||||
|
messageBox.appendChild(translationMessage);
|
||||||
|
showChatMessage();
|
||||||
|
if (settings.LANGUAGE.OUTPUT_TO_TTS) {
|
||||||
|
sound.playVoice({
|
||||||
|
filteredMessage: message.translation,
|
||||||
|
logoUrl: message.logoUrl,
|
||||||
|
username: message.username,
|
||||||
|
formattedMessage: message.formattedMessage,
|
||||||
|
language
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTranslatedMessage(message) {
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'POST', // HTTP method
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json; charset="utf-8"' // Specify the content type
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: message.message,
|
||||||
|
language: message.language
|
||||||
|
}) // Convert the data to JSON and include it in the request body
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://127.0.0.1:${settings.GENERAL.PORT}/translate`, requestOptions);
|
||||||
|
if (response.ok) {
|
||||||
|
const responseData = await response.json();
|
||||||
|
|
||||||
|
console.log('Translated message:', responseData);
|
||||||
|
setTranslatedMessage({
|
||||||
|
translation: responseData.translation,
|
||||||
|
messageId: message.messageId,
|
||||||
|
ISO3166: message.ISO3166,
|
||||||
|
formattedMessage: message.formattedMessage,
|
||||||
|
username: message.username,
|
||||||
|
logoUrl: message.logoUrl
|
||||||
|
});
|
||||||
|
if (settings.LANGUAGE.BROADCAST_TRANSLATION) {
|
||||||
|
twitch.sendMessage(`[${message.language} > ${settings.LANGUAGE.TRANSLATE_TO}] @${message.username}: ${responseData.translation}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Failed to send termination signal to Flask server.');
|
||||||
|
message.message = 'Error,could not translate message';
|
||||||
|
message.languaga = 'en-GB';
|
||||||
|
getTranslatedMessage(message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending termination signal:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterLanguage(message) {
|
||||||
|
const language = getLanguageProperties(message.language);
|
||||||
|
|
||||||
|
if (settings.LANGUAGE.TRANSLATE_TO !== 'none') {
|
||||||
|
getTranslatedMessage({
|
||||||
|
message: message.message,
|
||||||
|
messageId: message.messageId,
|
||||||
|
language: language.IETF,
|
||||||
|
ISO3166: language.ISO3166,
|
||||||
|
username: message.username,
|
||||||
|
formattedMessage: message.formattedMessage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return language;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDetectedLanguage(message) {
|
||||||
|
if (!settings.LANGUAGE.USE_DETECTION) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'POST', // HTTP method
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json' // Specify the content type
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ message: message.message }) // Convert the data to JSON and include it in the request body
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://127.0.0.1:${settings.GENERAL.PORT}/detect`, requestOptions);
|
||||||
|
if (response.ok) {
|
||||||
|
const responseData = await response.json();
|
||||||
|
|
||||||
|
console.log('Detected Languages:', responseData);
|
||||||
|
return filterLanguage({
|
||||||
|
language: responseData.languages[0],
|
||||||
|
message: message.message,
|
||||||
|
messageId: message.messageId,
|
||||||
|
username: message.username,
|
||||||
|
formattedMessage: message.formattedMessage
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Failed to send termination signal to Flask server.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending termination signal:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function getBackendServerStatus() {
|
async function getBackendServerStatus() {
|
||||||
console.log('getting status');
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://127.0.0.1:${settings.GENERAL.PORT}/status`, { method: 'GET' });
|
const response = await fetch(`http://127.0.0.1:${settings.GENERAL.PORT}/status`, { method: 'GET' });
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const responseData = await response.json();
|
const responseData = await response.json();
|
||||||
console.log('Response:', responseData);
|
console.log('Status:', responseData);
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to send termination signal to Flask server.');
|
console.error('Failed to send termination signal to Flask server.');
|
||||||
}
|
}
|
||||||
|
|
@ -93,7 +217,7 @@ async function getInternalTTSAudio(requestData) {
|
||||||
const response = await fetch(`http://127.0.0.1:${settings.GENERAL.PORT}/audio`, requestOptions);
|
const response = await fetch(`http://127.0.0.1:${settings.GENERAL.PORT}/audio`, requestOptions);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const responseData = await response.json();
|
const responseData = await response.json();
|
||||||
console.log('Response:', responseData);
|
console.log('Audio:', responseData);
|
||||||
return ttsRequestCount;
|
return ttsRequestCount;
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to send termination signal to Flask server.');
|
console.error('Failed to send termination signal to Flask server.');
|
||||||
|
|
@ -113,10 +237,6 @@ const createBackendServer = () =>
|
||||||
// Capture the stdout of the Python process
|
// Capture the stdout of the Python process
|
||||||
python.stdout.on('data', data => {
|
python.stdout.on('data', data => {
|
||||||
console.info(`${data}`);
|
console.info(`${data}`);
|
||||||
if (data.toString().startsWith('kees')) {
|
|
||||||
console.log('yess');
|
|
||||||
// getBackendServerStatus();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Capture the stderr of the Python process
|
// Capture the stderr of the Python process
|
||||||
|
|
@ -175,4 +295,4 @@ ipcRenderer.on('quit-event', async () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = { getInternalTTSAudio };
|
module.exports = { getInternalTTSAudio, getDetectedLanguage };
|
||||||
|
|
|
||||||
|
|
@ -4,329 +4,329 @@
|
||||||
// *info page with credits, version and more info
|
// *info page with credits, version and more info
|
||||||
|
|
||||||
const languages = {
|
const languages = {
|
||||||
none: { IETF: 'none', 'ISO-639': 'none' },
|
none: { IETF: 'none', ISO639: 'none', ISO3166: 'xx' },
|
||||||
acehnese: { IETF: 'ace-ID', 'ISO-639': 'ace' },
|
english: { IETF: 'en-GB', ISO639: 'en', ISO3166: 'gb' },
|
||||||
afrikaans: { IETF: 'af-ZA', 'ISO-639': 'af' },
|
spanish: { IETF: 'es-ES', ISO639: 'es', ISO3166: 'es' },
|
||||||
akan: { IETF: 'ak-GH', 'ISO-639': 'ak' },
|
dutch: { IETF: 'nl-NL', ISO639: 'nl', ISO3166: 'nl' },
|
||||||
albanian: { IETF: 'sq-AL', 'ISO-639': 'sq' },
|
'chinese simplified': { IETF: 'zh-CN', ISO639: 'zh', ISO3166: 'cn' },
|
||||||
amharic: { IETF: 'am-ET', 'ISO-639': 'am' },
|
russian: { IETF: 'ru-RU', ISO639: 'ru', ISO3166: 'ru' },
|
||||||
'antigua and barbuda creole english': { IETF: 'aig-AG', 'ISO-639': 'aig' },
|
indonesian: { IETF: 'id-ID', ISO639: 'id', ISO3166: 'id' },
|
||||||
arabic: { IETF: 'ar-SA', 'ISO-639': 'ar' },
|
hindi: { IETF: 'hi-IN', ISO639: 'hi', ISO3166: 'in' },
|
||||||
'arabic egyptian': { IETF: 'ar-EG', 'ISO-639': 'ar' },
|
filipino: { IETF: 'fil-PH', ISO639: 'fil', ISO3166: 'ph' },
|
||||||
aragonese: { IETF: 'an-ES', 'ISO-639': 'an' },
|
turkish: { IETF: 'tr-TR', ISO639: 'tr', ISO3166: 'tr' },
|
||||||
armenian: { IETF: 'hy-AM', 'ISO-639': 'hy' },
|
acehnese: { IETF: 'ace-ID', ISO639: 'ace', ISO3166: 'id' },
|
||||||
assamese: { IETF: 'as-IN', 'ISO-639': 'as' },
|
afrikaans: { IETF: 'af-ZA', ISO639: 'af', ISO3166: 'za' },
|
||||||
asturian: { IETF: 'ast-ES', 'ISO-639': 'ast' },
|
akan: { IETF: 'ak-GH', ISO639: 'ak', ISO3166: 'gh' },
|
||||||
'austrian german': { IETF: 'de-AT', 'ISO-639': 'de' },
|
albanian: { IETF: 'sq-AL', ISO639: 'sq', ISO3166: 'al' },
|
||||||
awadhi: { IETF: 'awa-IN', 'ISO-639': 'awa' },
|
amharic: { IETF: 'am-ET', ISO639: 'am', ISO3166: 'et' },
|
||||||
'ayacucho quechua': { IETF: 'quy-PE', 'ISO-639': 'quy' },
|
'antigua and barbuda creole english': { IETF: 'aig-AG', ISO639: 'aig', ISO3166: 'ag' },
|
||||||
azerbaijani: { IETF: 'az-AZ', 'ISO-639': 'az' },
|
arabic: { IETF: 'ar-SA', ISO639: 'ar', ISO3166: 'sa' },
|
||||||
'bahamas creole english': { IETF: 'bah-BS', 'ISO-639': 'bah' },
|
'arabic egyptian': { IETF: 'ar-EG', ISO639: 'ar', ISO3166: 'eg' },
|
||||||
bajan: { IETF: 'bjs-BB', 'ISO-639': 'bjs' },
|
// aragonese: { IETF: 'an-ES', ISO639: 'an', ISO3166: 'es' },
|
||||||
balinese: { IETF: 'ban-ID', 'ISO-639': 'ban' },
|
armenian: { IETF: 'hy-AM', ISO639: 'hy', ISO3166: 'am' },
|
||||||
'balkan gipsy': { IETF: 'rm-RO', 'ISO-639': 'rm' },
|
assamese: { IETF: 'as-IN', ISO639: 'as', ISO3166: 'in' },
|
||||||
bambara: { IETF: 'bm-ML', 'ISO-639': 'bm' },
|
asturian: { IETF: 'ast-ES', ISO639: 'ast', ISO3166: 'es' },
|
||||||
banjar: { IETF: 'bjn-ID', 'ISO-639': 'bjn' },
|
'austrian german': { IETF: 'de-AT', ISO639: 'de', ISO3166: 'at' },
|
||||||
bashkir: { IETF: 'ba-RU', 'ISO-639': 'ba' },
|
awadhi: { IETF: 'awa-IN', ISO639: 'awa', ISO3166: 'in' },
|
||||||
basque: { IETF: 'eu-ES', 'ISO-639': 'eu' },
|
'ayacucho quechua': { IETF: 'quy-PE', ISO639: 'quy', ISO3166: 'pe' },
|
||||||
belarusian: { IETF: 'be-BY', 'ISO-639': 'be' },
|
azerbaijani: { IETF: 'az-AZ', ISO639: 'az', ISO3166: 'az' },
|
||||||
'belgian french': { IETF: 'fr-BE', 'ISO-639': 'fr' },
|
'bahamas creole english': { IETF: 'bah-BS', ISO639: 'bah', ISO3166: 'bs' },
|
||||||
bemba: { IETF: 'bem-ZM', 'ISO-639': 'bem' },
|
bajan: { IETF: 'bjs-BB', ISO639: 'bjs', ISO3166: 'bb' },
|
||||||
bengali: { IETF: 'bn-IN', 'ISO-639': 'bn' },
|
balinese: { IETF: 'ban-ID', ISO639: 'ban', ISO3166: 'id' },
|
||||||
bhojpuri: { IETF: 'bho-IN', 'ISO-639': 'bho' },
|
'balkan gipsy': { IETF: 'rm-RO', ISO639: 'rm', ISO3166: 'ro' },
|
||||||
bihari: { IETF: 'bh-IN', 'ISO-639': 'bh' },
|
bambara: { IETF: 'bm-ML', ISO639: 'bm', ISO3166: 'ml' },
|
||||||
bislama: { IETF: 'bi-VU', 'ISO-639': 'bi' },
|
banjar: { IETF: 'bjn-ID', ISO639: 'bjn', ISO3166: 'id' },
|
||||||
borana: { IETF: 'gax-KE', 'ISO-639': 'gax' },
|
bashkir: { IETF: 'ba-RU', ISO639: 'ba', ISO3166: 'ru' },
|
||||||
bosnian: { IETF: 'bs-BA', 'ISO-639': 'bs' },
|
basque: { IETF: 'eu-ES', ISO639: 'eu', ISO3166: 'es-pv' },
|
||||||
'bosnian (cyrillic)': { IETF: 'bs-Cyrl-BA', 'ISO-639': 'bs' },
|
belarusian: { IETF: 'be-BY', ISO639: 'be', ISO3166: 'by' },
|
||||||
breton: { IETF: 'br-FR', 'ISO-639': 'br' },
|
'belgian french': { IETF: 'fr-BE', ISO639: 'fr', ISO3166: 'be' },
|
||||||
buginese: { IETF: 'bug-ID', 'ISO-639': 'bug' },
|
bemba: { IETF: 'bem-ZM', ISO639: 'bem', ISO3166: 'zm' },
|
||||||
bulgarian: { IETF: 'bg-BG', 'ISO-639': 'bg' },
|
bengali: { IETF: 'bn-IN', ISO639: 'bn', ISO3166: 'bd' },
|
||||||
burmese: { IETF: 'my-MM', 'ISO-639': 'my' },
|
bhojpuri: { IETF: 'bho-IN', ISO639: 'bho', ISO3166: 'in' },
|
||||||
catalan: { IETF: 'ca-ES', 'ISO-639': 'ca' },
|
bihari: { IETF: 'bh-IN', ISO639: 'bh', ISO3166: 'in' },
|
||||||
'catalan valencian': { IETF: 'cav-ES', 'ISO-639': 'cav' },
|
bislama: { IETF: 'bi-VU', ISO639: 'bi', ISO3166: 'vu' },
|
||||||
cebuano: { IETF: 'ceb-PH', 'ISO-639': 'ceb' },
|
borana: { IETF: 'gax-KE', ISO639: 'gax', ISO3166: 'ke' },
|
||||||
'central atlas tamazight': { IETF: 'tzm-MA', 'ISO-639': 'tzm' },
|
bosnian: { IETF: 'bs-BA', ISO639: 'bs', ISO3166: 'ba' },
|
||||||
'central aymara': { IETF: 'ayr-BO', 'ISO-639': 'ayr' },
|
'bosnian (cyrillic)': { IETF: 'bs-Cyrl-BA', ISO639: 'bs', ISO3166: 'ba' },
|
||||||
'central kanuri (latin script)': { IETF: 'knc-NG', 'ISO-639': 'knc' },
|
breton: { IETF: 'br-FR', ISO639: 'br', ISO3166: 'fr' },
|
||||||
'chadian arabic': { IETF: 'shu-TD', 'ISO-639': 'shu' },
|
buginese: { IETF: 'bug-ID', ISO639: 'bug', ISO3166: 'id' },
|
||||||
chamorro: { IETF: 'ch-GU', 'ISO-639': 'ch' },
|
bulgarian: { IETF: 'bg-BG', ISO639: 'bg', ISO3166: 'bg' },
|
||||||
cherokee: { IETF: 'chr-US', 'ISO-639': 'chr' },
|
burmese: { IETF: 'my-MM', ISO639: 'my', ISO3166: 'mm' },
|
||||||
chhattisgarhi: { IETF: 'hne-IN', 'ISO-639': 'hne' },
|
catalan: { IETF: 'ca-ES', ISO639: 'ca', ISO3166: 'es' },
|
||||||
'chinese simplified': { IETF: 'zh-CN', 'ISO-639': 'zh' },
|
'catalan valencian': { IETF: 'cav-ES', ISO639: 'cav', ISO3166: 'es' },
|
||||||
'chinese trad. (hong kong)': { IETF: 'zh-HK', 'ISO-639': 'zh' },
|
cebuano: { IETF: 'ceb-PH', ISO639: 'ceb', ISO3166: 'ph' },
|
||||||
'chinese traditional': { IETF: 'zh-TW', 'ISO-639': 'zh' },
|
'central atlas tamazight': { IETF: 'tzm-MA', ISO639: 'tzm', ISO3166: 'ma' },
|
||||||
'chinese traditional macau': { IETF: 'zh-MO', 'ISO-639': 'zh' },
|
'central aymara': { IETF: 'ayr-BO', ISO639: 'ayr', ISO3166: 'bo' },
|
||||||
chittagonian: { IETF: 'ctg-BD', 'ISO-639': 'ctg' },
|
'central kanuri (latin script)': { IETF: 'knc-NG', ISO639: 'knc', ISO3166: 'ng' },
|
||||||
chokwe: { IETF: 'cjk-AO', 'ISO-639': 'cjk' },
|
'chadian arabic': { IETF: 'shu-TD', ISO639: 'shu', ISO3166: 'td' },
|
||||||
'classical greek': { IETF: 'grc-GR', 'ISO-639': 'grc' },
|
chamorro: { IETF: 'ch-GU', ISO639: 'ch', ISO3166: 'gu' },
|
||||||
'comorian ngazidja': { IETF: 'zdj-KM', 'ISO-639': 'zdj' },
|
cherokee: { IETF: 'chr-US', ISO639: 'chr', ISO3166: 'us' },
|
||||||
coptic: { IETF: 'cop-EG', 'ISO-639': 'cop' },
|
chhattisgarhi: { IETF: 'hne-IN', ISO639: 'hne', ISO3166: 'in' },
|
||||||
'crimean tatar': { IETF: 'crh-RU', 'ISO-639': 'crh' },
|
'chinese trad. (hong kong)': { IETF: 'zh-HK', ISO639: 'zh', ISO3166: 'hk' },
|
||||||
'crioulo upper guinea': { IETF: 'pov-GW', 'ISO-639': 'pov' },
|
'chinese traditional': { IETF: 'zh-TW', ISO639: 'zh', ISO3166: 'tw' },
|
||||||
croatian: { IETF: 'hr-HR', 'ISO-639': 'hr' },
|
'chinese traditional macau': { IETF: 'zh-MO', ISO639: 'zh', ISO3166: 'mo' },
|
||||||
czech: { IETF: 'cs-CZ', 'ISO-639': 'cs' },
|
chittagonian: { IETF: 'ctg-BD', ISO639: 'ctg', ISO3166: 'bd' },
|
||||||
danish: { IETF: 'da-DK', 'ISO-639': 'da' },
|
chokwe: { IETF: 'cjk-AO', ISO639: 'cjk', ISO3166: 'ao' },
|
||||||
dari: { IETF: 'prs-AF', 'ISO-639': 'prs' },
|
'classical greek': { IETF: 'grc-GR', ISO639: 'grc', ISO3166: 'gr' },
|
||||||
dimli: { IETF: 'diq-TR', 'ISO-639': 'diq' },
|
'comorian ngazidja': { IETF: 'zdj-KM', ISO639: 'zdj', ISO3166: 'km' },
|
||||||
dutch: { IETF: 'nl-NL', 'ISO-639': 'nl' },
|
coptic: { IETF: 'cop-EG', ISO639: 'cop', ISO3166: 'eg' },
|
||||||
dyula: { IETF: 'dyu-CI', 'ISO-639': 'dyu' },
|
'crimean tatar': { IETF: 'crh-RU', ISO639: 'crh', ISO3166: 'tr' },
|
||||||
dzongkha: { IETF: 'dz-BT', 'ISO-639': 'dz' },
|
'crioulo upper guinea': { IETF: 'pov-GW', ISO639: 'pov', ISO3166: 'gw' },
|
||||||
'eastern yiddish': { IETF: 'ydd-US', 'ISO-639': 'ydd' },
|
croatian: { IETF: 'hr-HR', ISO639: 'hr', ISO3166: 'hr' },
|
||||||
emakhuwa: { IETF: 'vmw-MZ', 'ISO-639': 'vmw' },
|
czech: { IETF: 'cs-CZ', ISO639: 'cs', ISO3166: 'cz' },
|
||||||
english: { IETF: 'en-GB', 'ISO-639': 'en' },
|
danish: { IETF: 'da-DK', ISO639: 'da', ISO3166: 'dk' },
|
||||||
'english australia': { IETF: 'en-AU', 'ISO-639': 'en' },
|
dari: { IETF: 'prs-AF', ISO639: 'prs', ISO3166: 'af' },
|
||||||
'english canada': { IETF: 'en-CA', 'ISO-639': 'en' },
|
dimli: { IETF: 'diq-TR', ISO639: 'diq', ISO3166: 'tr' },
|
||||||
'english india': { IETF: 'en-IN', 'ISO-639': 'en' },
|
dyula: { IETF: 'dyu-CI', ISO639: 'dyu', ISO3166: 'ci' },
|
||||||
'english ireland': { IETF: 'en-IE', 'ISO-639': 'en' },
|
dzongkha: { IETF: 'dz-BT', ISO639: 'dz', ISO3166: 'bt' },
|
||||||
'english new zealand': { IETF: 'en-NZ', 'ISO-639': 'en' },
|
'eastern yiddish': { IETF: 'ydd-US', ISO639: 'ydd', ISO3166: 'il' },
|
||||||
'english singapore': { IETF: 'en-SG', 'ISO-639': 'en' },
|
emakhuwa: { IETF: 'vmw-MZ', ISO639: 'vmw', ISO3166: 'mz' },
|
||||||
'english south africa': { IETF: 'en-ZA', 'ISO-639': 'en' },
|
'english australia': { IETF: 'en-AU', ISO639: 'en', ISO3166: 'au' },
|
||||||
'english us': { IETF: 'en-US', 'ISO-639': 'en' },
|
'english canada': { IETF: 'en-CA', ISO639: 'en', ISO3166: 'ca' },
|
||||||
esperanto: { IETF: 'eo-EU', 'ISO-639': 'eo' },
|
'english india': { IETF: 'en-IN', ISO639: 'en', ISO3166: 'in' },
|
||||||
estonian: { IETF: 'et-EE', 'ISO-639': 'et' },
|
'english ireland': { IETF: 'en-IE', ISO639: 'en', ISO3166: 'ie' },
|
||||||
ewe: { IETF: 'ee-GH', 'ISO-639': 'ee' },
|
'english new zealand': { IETF: 'en-NZ', ISO639: 'en', ISO3166: 'nz' },
|
||||||
fanagalo: { IETF: 'fn-FNG', 'ISO-639': 'fn' },
|
'english singapore': { IETF: 'en-SG', ISO639: 'en', ISO3166: 'sg' },
|
||||||
faroese: { IETF: 'fo-FO', 'ISO-639': 'fo' },
|
'english south africa': { IETF: 'en-ZA', ISO639: 'en', ISO3166: 'za' },
|
||||||
fijian: { IETF: 'fj-FJ', 'ISO-639': 'fj' },
|
'english us': { IETF: 'en-US', ISO639: 'en', ISO3166: 'us' },
|
||||||
filipino: { IETF: 'fil-PH', 'ISO-639': 'fil' },
|
esperanto: { IETF: 'eo-EU', ISO639: 'eo', ISO3166: 'eu' },
|
||||||
finnish: { IETF: 'fi-FI', 'ISO-639': 'fi' },
|
estonian: { IETF: 'et-EE', ISO639: 'et', ISO3166: 'ee' },
|
||||||
flemish: { IETF: 'nl-BE', 'ISO-639': 'nl' },
|
ewe: { IETF: 'ee-GH', ISO639: 'ee', ISO3166: 'gh' },
|
||||||
fon: { IETF: 'fon-BJ', 'ISO-639': 'fon' },
|
fanagalo: { IETF: 'fn-FNG', ISO639: 'fn', ISO3166: 'za' },
|
||||||
french: { IETF: 'fr-FR', 'ISO-639': 'fr' },
|
faroese: { IETF: 'fo-FO', ISO639: 'fo', ISO3166: 'fo' },
|
||||||
'french canada': { IETF: 'fr-CA', 'ISO-639': 'fr' },
|
fijian: { IETF: 'fj-FJ', ISO639: 'fj', ISO3166: 'fj' },
|
||||||
'french swiss': { IETF: 'fr-CH', 'ISO-639': 'fr' },
|
finnish: { IETF: 'fi-FI', ISO639: 'fi', ISO3166: 'fi' },
|
||||||
friulian: { IETF: 'fur-IT', 'ISO-639': 'fur' },
|
flemish: { IETF: 'nl-BE', ISO639: 'nl', ISO3166: 'be' },
|
||||||
fula: { IETF: 'ff-FUL', 'ISO-639': 'ff' },
|
fon: { IETF: 'fon-BJ', ISO639: 'fon', ISO3166: 'bj' },
|
||||||
galician: { IETF: 'gl-ES', 'ISO-639': 'gl' },
|
french: { IETF: 'fr-FR', ISO639: 'fr', ISO3166: 'fr' },
|
||||||
gamargu: { IETF: 'mfi-NG', 'ISO-639': 'mfi' },
|
'french canada': { IETF: 'fr-CA', ISO639: 'fr', ISO3166: 'ca' },
|
||||||
garo: { IETF: 'grt-IN', 'ISO-639': 'grt' },
|
'french swiss': { IETF: 'fr-CH', ISO639: 'fr', ISO3166: 'ch' },
|
||||||
georgian: { IETF: 'ka-GE', 'ISO-639': 'ka' },
|
friulian: { IETF: 'fur-IT', ISO639: 'fur', ISO3166: 'it' },
|
||||||
german: { IETF: 'de-DE', 'ISO-639': 'de' },
|
fula: { IETF: 'ff-FUL', ISO639: 'ff', ISO3166: 'cm' },
|
||||||
gilbertese: { IETF: 'gil-KI', 'ISO-639': 'gil' },
|
galician: { IETF: 'gl-ES', ISO639: 'gl', ISO3166: 'es-ga' },
|
||||||
glavda: { IETF: 'glw-NG', 'ISO-639': 'glw' },
|
gamargu: { IETF: 'mfi-NG', ISO639: 'mfi', ISO3166: 'ng' },
|
||||||
greek: { IETF: 'el-GR', 'ISO-639': 'el' },
|
garo: { IETF: 'grt-IN', ISO639: 'grt', ISO3166: 'in' },
|
||||||
'grenadian creole english': { IETF: 'gcl-GD', 'ISO-639': 'gcl' },
|
georgian: { IETF: 'ka-GE', ISO639: 'ka', ISO3166: 'ge' },
|
||||||
guarani: { IETF: 'gn-PY', 'ISO-639': 'gn' },
|
german: { IETF: 'de-DE', ISO639: 'de', ISO3166: 'de' },
|
||||||
gujarati: { IETF: 'gu-IN', 'ISO-639': 'gu' },
|
gilbertese: { IETF: 'gil-KI', ISO639: 'gil', ISO3166: 'ki' },
|
||||||
'guyanese creole english': { IETF: 'gyn-GY', 'ISO-639': 'gyn' },
|
glavda: { IETF: 'glw-NG', ISO639: 'glw', ISO3166: 'ng' },
|
||||||
'haitian creole french': { IETF: 'ht-HT', 'ISO-639': 'ht' },
|
greek: { IETF: 'el-GR', ISO639: 'el', ISO3166: 'gr' },
|
||||||
'halh mongolian': { IETF: 'khk-MN', 'ISO-639': 'khk' },
|
'grenadian creole english': { IETF: 'gcl-GD', ISO639: 'gcl', ISO3166: 'gd' },
|
||||||
hausa: { IETF: 'ha-NE', 'ISO-639': 'ha' },
|
guarani: { IETF: 'gn-PY', ISO639: 'gn', ISO3166: 'py' },
|
||||||
hawaiian: { IETF: 'haw-US', 'ISO-639': 'haw' },
|
gujarati: { IETF: 'gu-IN', ISO639: 'gu', ISO3166: 'in' },
|
||||||
hebrew: { IETF: 'he-IL', 'ISO-639': 'he' },
|
'guyanese creole english': { IETF: 'gyn-GY', ISO639: 'gyn', ISO3166: 'gy' },
|
||||||
higi: { IETF: 'hig-NG', 'ISO-639': 'hig' },
|
'haitian creole french': { IETF: 'ht-HT', ISO639: 'ht', ISO3166: 'ht' },
|
||||||
hiligaynon: { IETF: 'hil-PH', 'ISO-639': 'hil' },
|
'halh mongolian': { IETF: 'khk-MN', ISO639: 'khk', ISO3166: 'mn' },
|
||||||
'hill mari': { IETF: 'mrj-RU', 'ISO-639': 'mrj' },
|
hausa: { IETF: 'ha-NE', ISO639: 'ha', ISO3166: 'ne' },
|
||||||
hindi: { IETF: 'hi-IN', 'ISO-639': 'hi' },
|
hawaiian: { IETF: 'haw-US', ISO639: 'haw', ISO3166: 'xx' },
|
||||||
hmong: { IETF: 'hmn-CN', 'ISO-639': 'hmn' },
|
hebrew: { IETF: 'he-IL', ISO639: 'he', ISO3166: 'il' },
|
||||||
hungarian: { IETF: 'hu-HU', 'ISO-639': 'hu' },
|
higi: { IETF: 'hig-NG', ISO639: 'hig', ISO3166: 'ng' },
|
||||||
icelandic: { IETF: 'is-IS', 'ISO-639': 'is' },
|
hiligaynon: { IETF: 'hil-PH', ISO639: 'hil', ISO3166: 'ph' },
|
||||||
'igbo ibo': { IETF: 'ibo-NG', 'ISO-639': 'ibo' },
|
'hill mari': { IETF: 'mrj-RU', ISO639: 'mrj', ISO3166: 'xx' },
|
||||||
'igbo ig': { IETF: 'ig-NG', 'ISO-639': 'ig' },
|
hmong: { IETF: 'hmn-CN', ISO639: 'hmn', ISO3166: 'cn' },
|
||||||
ilocano: { IETF: 'ilo-PH', 'ISO-639': 'ilo' },
|
hungarian: { IETF: 'hu-HU', ISO639: 'hu', ISO3166: 'hu' },
|
||||||
indonesian: { IETF: 'id-ID', 'ISO-639': 'id' },
|
icelandic: { IETF: 'is-IS', ISO639: 'is', ISO3166: 'is' },
|
||||||
'inuktitut greenlandic': { IETF: 'kl-GL', 'ISO-639': 'kl' },
|
'igbo ibo': { IETF: 'ibo-NG', ISO639: 'ibo', ISO3166: 'ng' },
|
||||||
'irish gaelic': { IETF: 'ga-IE', 'ISO-639': 'ga' },
|
'igbo ig': { IETF: 'ig-NG', ISO639: 'ig', ISO3166: 'ng' },
|
||||||
italian: { IETF: 'it-IT', 'ISO-639': 'it' },
|
ilocano: { IETF: 'ilo-PH', ISO639: 'ilo', ISO3166: 'ph' },
|
||||||
'italian swiss': { IETF: 'it-CH', 'ISO-639': 'it' },
|
'inuktitut greenlandic': { IETF: 'kl-GL', ISO639: 'kl', ISO3166: 'gl' },
|
||||||
'jamaican creole english': { IETF: 'jam-JM', 'ISO-639': 'jam' },
|
'irish gaelic': { IETF: 'ga-IE', ISO639: 'ga', ISO3166: 'ie' },
|
||||||
japanese: { IETF: 'ja-JP', 'ISO-639': 'ja' },
|
italian: { IETF: 'it-IT', ISO639: 'it', ISO3166: 'it' },
|
||||||
javanese: { IETF: 'jv-ID', 'ISO-639': 'jv' },
|
'italian swiss': { IETF: 'it-CH', ISO639: 'it', ISO3166: 'ch' },
|
||||||
jingpho: { IETF: 'kac-MM', 'ISO-639': 'kac' },
|
'jamaican creole english': { IETF: 'jam-JM', ISO639: 'jam', ISO3166: 'jm' },
|
||||||
"k'iche'": { IETF: 'quc-GT', 'ISO-639': 'quc' },
|
japanese: { IETF: 'ja-JP', ISO639: 'ja', ISO3166: 'jp' },
|
||||||
'kabiy<69>': { IETF: 'kbp-TG', 'ISO-639': 'kbp' },
|
javanese: { IETF: 'jv-ID', ISO639: 'jv', ISO3166: 'id' },
|
||||||
kabuverdianu: { IETF: 'kea-CV', 'ISO-639': 'kea' },
|
jingpho: { IETF: 'kac-MM', ISO639: 'kac', ISO3166: 'mm' },
|
||||||
kabylian: { IETF: 'kab-DZ', 'ISO-639': 'kab' },
|
"k'iche'": { IETF: 'quc-GT', ISO639: 'quc', ISO3166: 'gt' },
|
||||||
kalenjin: { IETF: 'kln-KE', 'ISO-639': 'kln' },
|
kabiye: { IETF: 'kbp-TG', ISO639: 'kbp', ISO3166: 'tg' },
|
||||||
kamba: { IETF: 'kam-KE', 'ISO-639': 'kam' },
|
kabuverdianu: { IETF: 'kea-CV', ISO639: 'kea', ISO3166: 'cv' },
|
||||||
kannada: { IETF: 'kn-IN', 'ISO-639': 'kn' },
|
kabylian: { IETF: 'kab-DZ', ISO639: 'kab', ISO3166: 'dz' },
|
||||||
kanuri: { IETF: 'kr-KAU', 'ISO-639': 'kr' },
|
kalenjin: { IETF: 'kln-KE', ISO639: 'kln', ISO3166: 'ke' },
|
||||||
karen: { IETF: 'kar-MM', 'ISO-639': 'kar' },
|
kamba: { IETF: 'kam-KE', ISO639: 'kam', ISO3166: 'ke' },
|
||||||
'kashmiri (devanagari script)': { IETF: 'ks-IN', 'ISO-639': 'ks' },
|
kannada: { IETF: 'kn-IN', ISO639: 'kn', ISO3166: 'in' },
|
||||||
'kashmiri (arabic script)': { IETF: 'kas-IN', 'ISO-639': 'kas' },
|
kanuri: { IETF: 'kr-KAU', ISO639: 'kr', ISO3166: 'xx' },
|
||||||
kazakh: { IETF: 'kk-KZ', 'ISO-639': 'kk' },
|
karen: { IETF: 'kar-MM', ISO639: 'kar', ISO3166: 'mm' },
|
||||||
khasi: { IETF: 'kha-IN', 'ISO-639': 'kha' },
|
'kashmiri (devanagari script)': { IETF: 'ks-IN', ISO639: 'ks', ISO3166: 'in' },
|
||||||
khmer: { IETF: 'km-KH', 'ISO-639': 'km' },
|
'kashmiri (arabic script)': { IETF: 'kas-IN', ISO639: 'kas', ISO3166: 'in' },
|
||||||
'kikuyu kik': { IETF: 'kik-KE', 'ISO-639': 'kik' },
|
kazakh: { IETF: 'kk-KZ', ISO639: 'kk', ISO3166: 'kz' },
|
||||||
'kikuyu ki': { IETF: 'ki-KE', 'ISO-639': 'ki' },
|
khasi: { IETF: 'kha-IN', ISO639: 'kha', ISO3166: 'in' },
|
||||||
kimbundu: { IETF: 'kmb-AO', 'ISO-639': 'kmb' },
|
khmer: { IETF: 'km-KH', ISO639: 'km', ISO3166: 'kh' },
|
||||||
kinyarwanda: { IETF: 'rw-RW', 'ISO-639': 'rw' },
|
'kikuyu kik': { IETF: 'kik-KE', ISO639: 'kik', ISO3166: 'ke' },
|
||||||
kirundi: { IETF: 'rn-BI', 'ISO-639': 'rn' },
|
'kikuyu ki': { IETF: 'ki-KE', ISO639: 'ki', ISO3166: 'ke' },
|
||||||
kisii: { IETF: 'guz-KE', 'ISO-639': 'guz' },
|
kimbundu: { IETF: 'kmb-AO', ISO639: 'kmb', ISO3166: 'ao' },
|
||||||
kongo: { IETF: 'kg-CG', 'ISO-639': 'kg' },
|
kinyarwanda: { IETF: 'rw-RW', ISO639: 'rw', ISO3166: 'rw' },
|
||||||
konkani: { IETF: 'kok-IN', 'ISO-639': 'kok' },
|
kirundi: { IETF: 'rn-BI', ISO639: 'rn', ISO3166: 'bi' },
|
||||||
korean: { IETF: 'ko-KR', 'ISO-639': 'ko' },
|
kisii: { IETF: 'guz-KE', ISO639: 'guz', ISO3166: 'ke' },
|
||||||
'northern kurdish': { IETF: 'kmr-TR', 'ISO-639': 'kmr' },
|
kongo: { IETF: 'kg-CG', ISO639: 'kg', ISO3166: 'cg' },
|
||||||
'kurdish sorani': { IETF: 'ckb-IQ', 'ISO-639': 'ckb' },
|
konkani: { IETF: 'kok-IN', ISO639: 'kok', ISO3166: 'in' },
|
||||||
kyrgyz: { IETF: 'ky-KG', 'ISO-639': 'ky' },
|
korean: { IETF: 'ko-KR', ISO639: 'ko', ISO3166: 'ko' },
|
||||||
lao: { IETF: 'lo-LA', 'ISO-639': 'lo' },
|
'northern kurdish': { IETF: 'kmr-TR', ISO639: 'kmr', ISO3166: 'tr' },
|
||||||
latgalian: { IETF: 'ltg-LV', 'ISO-639': 'ltg' },
|
'kurdish sorani': { IETF: 'ckb-IQ', ISO639: 'ckb', ISO3166: 'iq' },
|
||||||
latin: { IETF: 'la-XN', 'ISO-639': 'la' },
|
kyrgyz: { IETF: 'ky-KG', ISO639: 'ky', ISO3166: 'kg' },
|
||||||
latvian: { IETF: 'lv-LV', 'ISO-639': 'lv' },
|
lao: { IETF: 'lo-LA', ISO639: 'lo', ISO3166: 'la' },
|
||||||
ligurian: { IETF: 'lij-IT', 'ISO-639': 'lij' },
|
latgalian: { IETF: 'ltg-LV', ISO639: 'ltg', ISO3166: 'lv' },
|
||||||
limburgish: { IETF: 'li-NL', 'ISO-639': 'li' },
|
latin: { IETF: 'la-XN', ISO639: 'la', ISO3166: 'xx' },
|
||||||
lingala: { IETF: 'ln-LIN', 'ISO-639': 'ln' },
|
latvian: { IETF: 'lv-LV', ISO639: 'lv', ISO3166: 'lg' },
|
||||||
lithuanian: { IETF: 'lt-LT', 'ISO-639': 'lt' },
|
ligurian: { IETF: 'lij-IT', ISO639: 'lij', ISO3166: 'it' },
|
||||||
lombard: { IETF: 'lmo-IT', 'ISO-639': 'lmo' },
|
limburgish: { IETF: 'li-NL', ISO639: 'li', ISO3166: 'nl' },
|
||||||
'luba-kasai': { IETF: 'lua-CD', 'ISO-639': 'lua' },
|
lingala: { IETF: 'ln-LIN', ISO639: 'ln', ISO3166: 'cd' },
|
||||||
luganda: { IETF: 'lg-UG', 'ISO-639': 'lg' },
|
lithuanian: { IETF: 'lt-LT', ISO639: 'lt', ISO3166: 'lt' },
|
||||||
luhya: { IETF: 'luy-KE', 'ISO-639': 'luy' },
|
lombard: { IETF: 'lmo-IT', ISO639: 'lmo', ISO3166: 'it' },
|
||||||
luo: { IETF: 'luo-KE', 'ISO-639': 'luo' },
|
'luba-kasai': { IETF: 'lua-CD', ISO639: 'lua', ISO3166: 'cd' },
|
||||||
luxembourgish: { IETF: 'lb-LU', 'ISO-639': 'lb' },
|
luganda: { IETF: 'lg-UG', ISO639: 'lg', ISO3166: 'ug' },
|
||||||
maa: { IETF: 'mas-KE', 'ISO-639': 'mas' },
|
luhya: { IETF: 'luy-KE', ISO639: 'luy', ISO3166: 'ke' },
|
||||||
macedonian: { IETF: 'mk-MK', 'ISO-639': 'mk' },
|
luo: { IETF: 'luo-KE', ISO639: 'luo', ISO3166: 'ke' },
|
||||||
magahi: { IETF: 'mag-IN', 'ISO-639': 'mag' },
|
luxembourgish: { IETF: 'lb-LU', ISO639: 'lb', ISO3166: 'lu' },
|
||||||
maithili: { IETF: 'mai-IN', 'ISO-639': 'mai' },
|
maa: { IETF: 'mas-KE', ISO639: 'mas', ISO3166: 'ke' },
|
||||||
malagasy: { IETF: 'mg-MG', 'ISO-639': 'mg' },
|
macedonian: { IETF: 'mk-MK', ISO639: 'mk', ISO3166: 'mk' },
|
||||||
malay: { IETF: 'ms-MY', 'ISO-639': 'ms' },
|
magahi: { IETF: 'mag-IN', ISO639: 'mag', ISO3166: 'in' },
|
||||||
malayalam: { IETF: 'ml-IN', 'ISO-639': 'ml' },
|
maithili: { IETF: 'mai-IN', ISO639: 'mai', ISO3166: 'in' },
|
||||||
maldivian: { IETF: 'dv-MV', 'ISO-639': 'dv' },
|
malagasy: { IETF: 'mg-MG', ISO639: 'mg', ISO3166: 'mg' },
|
||||||
maltese: { IETF: 'mt-MT', 'ISO-639': 'mt' },
|
malay: { IETF: 'ms-MY', ISO639: 'ms', ISO3166: 'my' },
|
||||||
mandara: { IETF: 'mfi-CM', 'ISO-639': 'mfi' },
|
malayalam: { IETF: 'ml-IN', ISO639: 'ml', ISO3166: 'in' },
|
||||||
manipuri: { IETF: 'mni-IN', 'ISO-639': 'mni' },
|
maldivian: { IETF: 'dv-MV', ISO639: 'dv', ISO3166: 'mv' },
|
||||||
'manx gaelic': { IETF: 'gv-IM', 'ISO-639': 'gv' },
|
maltese: { IETF: 'mt-MT', ISO639: 'mt', ISO3166: 'mt' },
|
||||||
maori: { IETF: 'mi-NZ', 'ISO-639': 'mi' },
|
mandara: { IETF: 'mfi-CM', ISO639: 'mfi', ISO3166: 'cm' },
|
||||||
marathi: { IETF: 'mr-IN', 'ISO-639': 'mr' },
|
manipuri: { IETF: 'mni-IN', ISO639: 'mni', ISO3166: 'in' },
|
||||||
margi: { IETF: 'mrt-NG', 'ISO-639': 'mrt' },
|
'manx gaelic': { IETF: 'gv-IM', ISO639: 'gv', ISO3166: 'im' },
|
||||||
mari: { IETF: 'mhr-RU', 'ISO-639': 'mhr' },
|
maori: { IETF: 'mi-NZ', ISO639: 'mi', ISO3166: 'nz' },
|
||||||
marshallese: { IETF: 'mh-MH', 'ISO-639': 'mh' },
|
marathi: { IETF: 'mr-IN', ISO639: 'mr', ISO3166: 'in' },
|
||||||
mende: { IETF: 'men-SL', 'ISO-639': 'men' },
|
margi: { IETF: 'mrt-NG', ISO639: 'mrt', ISO3166: 'ng' },
|
||||||
meru: { IETF: 'mer-KE', 'ISO-639': 'mer' },
|
mari: { IETF: 'mhr-RU', ISO639: 'mhr', ISO3166: 'xx' },
|
||||||
mijikenda: { IETF: 'nyf-KE', 'ISO-639': 'nyf' },
|
marshallese: { IETF: 'mh-MH', ISO639: 'mh', ISO3166: 'mh' },
|
||||||
minangkabau: { IETF: 'min-ID', 'ISO-639': 'min' },
|
mende: { IETF: 'men-SL', ISO639: 'men', ISO3166: 'sl' },
|
||||||
mizo: { IETF: 'lus-IN', 'ISO-639': 'lus' },
|
meru: { IETF: 'mer-KE', ISO639: 'mer', ISO3166: 'ke' },
|
||||||
mongolian: { IETF: 'mn-MN', 'ISO-639': 'mn' },
|
mijikenda: { IETF: 'nyf-KE', ISO639: 'nyf', ISO3166: 'ke' },
|
||||||
montenegrin: { IETF: 'sr-ME', 'ISO-639': 'sr' },
|
minangkabau: { IETF: 'min-ID', ISO639: 'min', ISO3166: 'id' },
|
||||||
morisyen: { IETF: 'mfe-MU', 'ISO-639': 'mfe' },
|
mizo: { IETF: 'lus-IN', ISO639: 'lus', ISO3166: 'in' },
|
||||||
'moroccan arabic': { IETF: 'ar-MA', 'ISO-639': 'ar' },
|
mongolian: { IETF: 'mn-MN', ISO639: 'mn', ISO3166: 'mn' },
|
||||||
mossi: { IETF: 'mos-BF', 'ISO-639': 'mos' },
|
montenegrin: { IETF: 'sr-ME', ISO639: 'sr', ISO3166: 'me' },
|
||||||
ndau: { IETF: 'ndc-MZ', 'ISO-639': 'ndc' },
|
morisyen: { IETF: 'mfe-MU', ISO639: 'mfe', ISO3166: 'mu' },
|
||||||
ndebele: { IETF: 'nr-ZA', 'ISO-639': 'nr' },
|
'moroccan arabic': { IETF: 'ar-MA', ISO639: 'ar', ISO3166: 'ma' },
|
||||||
nepali: { IETF: 'ne-NP', 'ISO-639': 'ne' },
|
mossi: { IETF: 'mos-BF', ISO639: 'mos', ISO3166: 'bf' },
|
||||||
'nigerian fulfulde': { IETF: 'fuv-NG', 'ISO-639': 'fuv' },
|
ndau: { IETF: 'ndc-MZ', ISO639: 'ndc', ISO3166: 'mz' },
|
||||||
niuean: { IETF: 'niu-NU', 'ISO-639': 'niu' },
|
ndebele: { IETF: 'nr-ZA', ISO639: 'nr', ISO3166: 'za' },
|
||||||
'north azerbaijani': { IETF: 'azj-AZ', 'ISO-639': 'azj' },
|
nepali: { IETF: 'ne-NP', ISO639: 'ne', ISO3166: 'np' },
|
||||||
sesotho: { IETF: 'nso-ZA', 'ISO-639': 'nso' },
|
'nigerian fulfulde': { IETF: 'fuv-NG', ISO639: 'fuv', ISO3166: 'ng' },
|
||||||
'northern uzbek': { IETF: 'uzn-UZ', 'ISO-639': 'uzn' },
|
niuean: { IETF: 'niu-NU', ISO639: 'niu', ISO3166: 'nu' },
|
||||||
'norwegian bokm<6B>l': { IETF: 'nb-NO', 'ISO-639': 'nb' },
|
'north azerbaijani': { IETF: 'azj-AZ', ISO639: 'azj', ISO3166: 'az' },
|
||||||
'norwegian nynorsk': { IETF: 'nn-NO', 'ISO-639': 'nn' },
|
sesotho: { IETF: 'nso-ZA', ISO639: 'nso', ISO3166: 'za' },
|
||||||
nuer: { IETF: 'nus-SS', 'ISO-639': 'nus' },
|
'northern uzbek': { IETF: 'uzn-UZ', ISO639: 'uzn', ISO3166: 'uz' },
|
||||||
nyanja: { IETF: 'ny-MW', 'ISO-639': 'ny' },
|
'norwegian bokm<6B>l': { IETF: 'nb-NO', ISO639: 'nb', ISO3166: 'no' },
|
||||||
occitan: { IETF: 'oc-FR', 'ISO-639': 'oc' },
|
'norwegian nynorsk': { IETF: 'nn-NO', ISO639: 'nn', ISO3166: 'no' },
|
||||||
'occitan aran': { IETF: 'oc-ES', 'ISO-639': 'oc' },
|
nuer: { IETF: 'nus-SS', ISO639: 'nus', ISO3166: 'ss' },
|
||||||
odia: { IETF: 'or-IN', 'ISO-639': 'or' },
|
nyanja: { IETF: 'ny-MW', ISO639: 'ny', ISO3166: 'mw' },
|
||||||
oriya: { IETF: 'ory-IN', 'ISO-639': 'ory' },
|
occitan: { IETF: 'oc-FR', ISO639: 'oc', ISO3166: 'fr' },
|
||||||
urdu: { IETF: 'ur-PK', 'ISO-639': 'ur' },
|
'occitan aran': { IETF: 'oc-ES', ISO639: 'oc', ISO3166: 'es-ct' },
|
||||||
palauan: { IETF: 'pau-PW', 'ISO-639': 'pau' },
|
odia: { IETF: 'or-IN', ISO639: 'or', ISO3166: 'in' },
|
||||||
pali: { IETF: 'pi-IN', 'ISO-639': 'pi' },
|
oriya: { IETF: 'ory-IN', ISO639: 'ory', ISO3166: 'in' },
|
||||||
pangasinan: { IETF: 'pag-PH', 'ISO-639': 'pag' },
|
urdu: { IETF: 'ur-PK', ISO639: 'ur', ISO3166: 'pk' },
|
||||||
papiamentu: { IETF: 'pap-CW', 'ISO-639': 'pap' },
|
palauan: { IETF: 'pau-PW', ISO639: 'pau', ISO3166: 'pw' },
|
||||||
pashto: { IETF: 'ps-PK', 'ISO-639': 'ps' },
|
pali: { IETF: 'pi-IN', ISO639: 'pi', ISO3166: 'in' },
|
||||||
persian: { IETF: 'fa-IR', 'ISO-639': 'fa' },
|
pangasinan: { IETF: 'pag-PH', ISO639: 'pag', ISO3166: 'ph' },
|
||||||
pijin: { IETF: 'pis-SB', 'ISO-639': 'pis' },
|
papiamentu: { IETF: 'pap-CW', ISO639: 'pap', ISO3166: 'cw' },
|
||||||
'plateau malagasy': { IETF: 'plt-MG', 'ISO-639': 'plt' },
|
pashto: { IETF: 'ps-PK', ISO639: 'ps', ISO3166: 'pk' },
|
||||||
polish: { IETF: 'pl-PL', 'ISO-639': 'pl' },
|
persian: { IETF: 'fa-IR', ISO639: 'fa', ISO3166: 'ir' },
|
||||||
portuguese: { IETF: 'pt-PT', 'ISO-639': 'pt' },
|
pijin: { IETF: 'pis-SB', ISO639: 'pis', ISO3166: 'sb' },
|
||||||
'portuguese brazil': { IETF: 'pt-BR', 'ISO-639': 'pt' },
|
'plateau malagasy': { IETF: 'plt-MG', ISO639: 'plt', ISO3166: 'mg' },
|
||||||
potawatomi: { IETF: 'pot-US', 'ISO-639': 'pot' },
|
polish: { IETF: 'pl-PL', ISO639: 'pl', ISO3166: 'pl' },
|
||||||
punjabi: { IETF: 'pa-IN', 'ISO-639': 'pa' },
|
portuguese: { IETF: 'pt-PT', ISO639: 'pt', ISO3166: 'pt' },
|
||||||
'punjabi (pakistan)': { IETF: 'pnb-PK', 'ISO-639': 'pnb' },
|
'portuguese brazil': { IETF: 'pt-BR', ISO639: 'pt', ISO3166: 'br' },
|
||||||
quechua: { IETF: 'qu-PE', 'ISO-639': 'qu' },
|
potawatomi: { IETF: 'pot-US', ISO639: 'pot', ISO3166: 'us' },
|
||||||
rohingya: { IETF: 'rhg-MM', 'ISO-639': 'rhg' },
|
punjabi: { IETF: 'pa-IN', ISO639: 'pa', ISO3166: 'in' },
|
||||||
rohingyalish: { IETF: 'rhl-MM', 'ISO-639': 'rhl' },
|
'punjabi (pakistan)': { IETF: 'pnb-PK', ISO639: 'pnb', ISO3166: 'pk' },
|
||||||
romanian: { IETF: 'ro-RO', 'ISO-639': 'ro' },
|
quechua: { IETF: 'qu-PE', ISO639: 'qu', ISO3166: 'pe' },
|
||||||
romansh: { IETF: 'roh-CH', 'ISO-639': 'roh' },
|
rohingya: { IETF: 'rhg-MM', ISO639: 'rhg', ISO3166: 'mm' },
|
||||||
rundi: { IETF: 'run-BI', 'ISO-639': 'run' },
|
rohingyalish: { IETF: 'rhl-MM', ISO639: 'rhl', ISO3166: 'mm' },
|
||||||
russian: { IETF: 'ru-RU', 'ISO-639': 'ru' },
|
romanian: { IETF: 'ro-RO', ISO639: 'ro', ISO3166: 'ro' },
|
||||||
'saint lucian creole french': { IETF: 'acf-LC', 'ISO-639': 'acf' },
|
romansh: { IETF: 'roh-CH', ISO639: 'roh', ISO3166: 'ch' },
|
||||||
samoan: { IETF: 'sm-WS', 'ISO-639': 'sm' },
|
rundi: { IETF: 'run-BI', ISO639: 'run', ISO3166: 'bi' },
|
||||||
sango: { IETF: 'sg-CF', 'ISO-639': 'sg' },
|
'saint lucian creole french': { IETF: 'acf-LC', ISO639: 'acf', ISO3166: 'lc' },
|
||||||
sanskrit: { IETF: 'sa-IN', 'ISO-639': 'sa' },
|
samoan: { IETF: 'sm-WS', ISO639: 'sm', ISO3166: 'ws' },
|
||||||
santali: { IETF: 'sat-IN', 'ISO-639': 'sat' },
|
sango: { IETF: 'sg-CF', ISO639: 'sg', ISO3166: 'cf' },
|
||||||
sardinian: { IETF: 'sc-IT', 'ISO-639': 'sc' },
|
sanskrit: { IETF: 'sa-IN', ISO639: 'sa', ISO3166: 'in' },
|
||||||
'scots gaelic': { IETF: 'gd-GB', 'ISO-639': 'gd' },
|
santali: { IETF: 'sat-IN', ISO639: 'sat', ISO3166: 'in' },
|
||||||
sena: { IETF: 'seh-ZW', 'ISO-639': 'seh' },
|
sardinian: { IETF: 'sc-IT', ISO639: 'sc', ISO3166: 'it' },
|
||||||
'serbian cyrillic': { IETF: 'sr-Cyrl-RS', 'ISO-639': 'sr' },
|
'scots gaelic': { IETF: 'gd-GB', ISO639: 'gd', ISO3166: 'gb-sct' },
|
||||||
'serbian latin': { IETF: 'sr-Latn-RS', 'ISO-639': 'sr' },
|
sena: { IETF: 'seh-ZW', ISO639: 'seh', ISO3166: 'zw' },
|
||||||
'seselwa creole french': { IETF: 'crs-SC', 'ISO-639': 'crs' },
|
'serbian cyrillic': { IETF: 'sr-Cyrl-RS', ISO639: 'sr', ISO3166: 'rs' },
|
||||||
'setswana (south africa)': { IETF: 'tn-ZA', 'ISO-639': 'tn' },
|
'serbian latin': { IETF: 'sr-Latn-RS', ISO639: 'sr', ISO3166: 'rs' },
|
||||||
shan: { IETF: 'shn-MM', 'ISO-639': 'shn' },
|
'seselwa creole french': { IETF: 'crs-SC', ISO639: 'crs', ISO3166: 'sc' },
|
||||||
shona: { IETF: 'sn-ZW', 'ISO-639': 'sn' },
|
'setswana (south africa)': { IETF: 'tn-ZA', ISO639: 'tn', ISO3166: 'za' },
|
||||||
sicilian: { IETF: 'scn-IT', 'ISO-639': 'scn' },
|
shan: { IETF: 'shn-MM', ISO639: 'shn', ISO3166: 'mm' },
|
||||||
silesian: { IETF: 'szl-PL', 'ISO-639': 'szl' },
|
shona: { IETF: 'sn-ZW', ISO639: 'sn', ISO3166: 'zw' },
|
||||||
'sindhi snd': { IETF: 'snd-PK', 'ISO-639': 'snd' },
|
sicilian: { IETF: 'scn-IT', ISO639: 'scn', ISO3166: 'it' },
|
||||||
'sindhi sd': { IETF: 'sd-PK', 'ISO-639': 'sd' },
|
silesian: { IETF: 'szl-PL', ISO639: 'szl', ISO3166: 'pl' },
|
||||||
sinhala: { IETF: 'si-LK', 'ISO-639': 'si' },
|
'sindhi snd': { IETF: 'snd-PK', ISO639: 'snd', ISO3166: 'pk' },
|
||||||
slovak: { IETF: 'sk-SK', 'ISO-639': 'sk' },
|
'sindhi sd': { IETF: 'sd-PK', ISO639: 'sd', ISO3166: 'pk' },
|
||||||
slovenian: { IETF: 'sl-SI', 'ISO-639': 'sl' },
|
sinhala: { IETF: 'si-LK', ISO639: 'si', ISO3166: 'lk' },
|
||||||
somali: { IETF: 'so-SO', 'ISO-639': 'so' },
|
slovak: { IETF: 'sk-SK', ISO639: 'sk', ISO3166: 'sk' },
|
||||||
'sotho southern': { IETF: 'st-LS', 'ISO-639': 'st' },
|
slovenian: { IETF: 'sl-SI', ISO639: 'sl', ISO3166: 'si' },
|
||||||
'south azerbaijani': { IETF: 'azb-AZ', 'ISO-639': 'azb' },
|
somali: { IETF: 'so-SO', ISO639: 'so', ISO3166: 'so' },
|
||||||
'southern pashto': { IETF: 'pbt-PK', 'ISO-639': 'pbt' },
|
'sotho southern': { IETF: 'st-LS', ISO639: 'st', ISO3166: 'ls' },
|
||||||
'southwestern dinka': { IETF: 'dik-SS', 'ISO-639': 'dik' },
|
'south azerbaijani': { IETF: 'azb-AZ', ISO639: 'azb', ISO3166: 'az' },
|
||||||
spanish: { IETF: 'es-ES', 'ISO-639': 'es' },
|
'southern pashto': { IETF: 'pbt-PK', ISO639: 'pbt', ISO3166: 'pk' },
|
||||||
'spanish argentina': { IETF: 'es-AR', 'ISO-639': 'es' },
|
'southwestern dinka': { IETF: 'dik-SS', ISO639: 'dik', ISO3166: 'ss' },
|
||||||
'spanish colombia': { IETF: 'es-CO', 'ISO-639': 'es' },
|
'spanish argentina': { IETF: 'es-AR', ISO639: 'es', ISO3166: 'ar' },
|
||||||
'spanish latin america': { IETF: 'es-419', 'ISO-639': 'es' },
|
'spanish colombia': { IETF: 'es-CO', ISO639: 'es', ISO3166: 'co' },
|
||||||
'spanish mexico': { IETF: 'es-MX', 'ISO-639': 'es' },
|
'spanish latin america': { IETF: 'es-419', ISO639: 'es', ISO3166: 'do' },
|
||||||
'spanish united states': { IETF: 'es-US', 'ISO-639': 'es' },
|
'spanish mexico': { IETF: 'es-MX', ISO639: 'es', ISO3166: 'mx' },
|
||||||
'sranan tongo': { IETF: 'srn-SR', 'ISO-639': 'srn' },
|
'spanish united states': { IETF: 'es-US', ISO639: 'es', ISO3166: 'es' },
|
||||||
'standard latvian': { IETF: 'lvs-LV', 'ISO-639': 'lvs' },
|
'sranan tongo': { IETF: 'srn-SR', ISO639: 'srn', ISO3166: 'sr' },
|
||||||
'standard malay': { IETF: 'zsm-MY', 'ISO-639': 'zsm' },
|
'standard latvian': { IETF: 'lvs-LV', ISO639: 'lvs', ISO3166: 'lv' },
|
||||||
sundanese: { IETF: 'su-ID', 'ISO-639': 'su' },
|
'standard malay': { IETF: 'zsm-MY', ISO639: 'zsm', ISO3166: 'my' },
|
||||||
swahili: { IETF: 'sw-KE', 'ISO-639': 'sw' },
|
sundanese: { IETF: 'su-ID', ISO639: 'su', ISO3166: 'id' },
|
||||||
swati: { IETF: 'ss-SZ', 'ISO-639': 'ss' },
|
swahili: { IETF: 'sw-KE', ISO639: 'sw', ISO3166: 'ke' },
|
||||||
swedish: { IETF: 'sv-SE', 'ISO-639': 'sv' },
|
swati: { IETF: 'ss-SZ', ISO639: 'ss', ISO3166: 'sz' },
|
||||||
'swiss german': { IETF: 'de-CH', 'ISO-639': 'de' },
|
swedish: { IETF: 'sv-SE', ISO639: 'sv', ISO3166: 'se' },
|
||||||
'syriac (aramaic)': { IETF: 'syc-TR', 'ISO-639': 'syc' },
|
'swiss german': { IETF: 'de-CH', ISO639: 'de', ISO3166: 'ch' },
|
||||||
tagalog: { IETF: 'tl-PH', 'ISO-639': 'tl' },
|
'syriac (aramaic)': { IETF: 'syc-TR', ISO639: 'syc', ISO3166: 'tr' },
|
||||||
tahitian: { IETF: 'ty-PF', 'ISO-639': 'ty' },
|
tagalog: { IETF: 'tl-PH', ISO639: 'tl', ISO3166: 'ph' },
|
||||||
tajik: { IETF: 'tg-TJ', 'ISO-639': 'tg' },
|
tahitian: { IETF: 'ty-PF', ISO639: 'ty', ISO3166: 'pf' },
|
||||||
'tamashek (tuareg)': { IETF: 'tmh-DZ', 'ISO-639': 'tmh' },
|
tajik: { IETF: 'tg-TJ', ISO639: 'tg', ISO3166: 'tj' },
|
||||||
tamasheq: { IETF: 'taq-ML', 'ISO-639': 'taq' },
|
'tamashek (tuareg)': { IETF: 'tmh-DZ', ISO639: 'tmh', ISO3166: 'dz' },
|
||||||
'tamil india': { IETF: 'ta-IN', 'ISO-639': 'ta' },
|
tamasheq: { IETF: 'taq-ML', ISO639: 'taq', ISO3166: 'ml' },
|
||||||
'tamil sri lanka': { IETF: 'ta-LK', 'ISO-639': 'ta' },
|
'tamil india': { IETF: 'ta-IN', ISO639: 'ta', ISO3166: 'in' },
|
||||||
taroko: { IETF: 'trv-TW', 'ISO-639': 'trv' },
|
'tamil sri lanka': { IETF: 'ta-LK', ISO639: 'ta', ISO3166: 'lk' },
|
||||||
tatar: { IETF: 'tt-RU', 'ISO-639': 'tt' },
|
taroko: { IETF: 'trv-TW', ISO639: 'trv', ISO3166: 'tw' },
|
||||||
telugu: { IETF: 'te-IN', 'ISO-639': 'te' },
|
tatar: { IETF: 'tt-RU', ISO639: 'tt', ISO3166: 'ru' },
|
||||||
tetum: { IETF: 'tet-TL', 'ISO-639': 'tet' },
|
telugu: { IETF: 'te-IN', ISO639: 'te', ISO3166: 'in' },
|
||||||
thai: { IETF: 'th-TH', 'ISO-639': 'th' },
|
tetum: { IETF: 'tet-TL', ISO639: 'tet', ISO3166: 'tl' },
|
||||||
tibetan: { IETF: 'bo-CN', 'ISO-639': 'bo' },
|
thai: { IETF: 'th-TH', ISO639: 'th', ISO3166: 'th' },
|
||||||
tigrinya: { IETF: 'ti-ET', 'ISO-639': 'ti' },
|
tibetan: { IETF: 'bo-CN', ISO639: 'bo', ISO3166: 'cn' },
|
||||||
'tok pisin': { IETF: 'tpi-PG', 'ISO-639': 'tpi' },
|
tigrinya: { IETF: 'ti-ET', ISO639: 'ti', ISO3166: 'et' },
|
||||||
tokelauan: { IETF: 'tkl-TK', 'ISO-639': 'tkl' },
|
'tok pisin': { IETF: 'tpi-PG', ISO639: 'tpi', ISO3166: 'pg' },
|
||||||
tongan: { IETF: 'to-TO', 'ISO-639': 'to' },
|
tokelauan: { IETF: 'tkl-TK', ISO639: 'tkl', ISO3166: 'tk' },
|
||||||
'tosk albanian': { IETF: 'als-AL', 'ISO-639': 'als' },
|
tongan: { IETF: 'to-TO', ISO639: 'to', ISO3166: 'to' },
|
||||||
tsonga: { IETF: 'ts-ZA', 'ISO-639': 'ts' },
|
'tosk albanian': { IETF: 'als-AL', ISO639: 'als', ISO3166: 'al' },
|
||||||
tswa: { IETF: 'tsc-MZ', 'ISO-639': 'tsc' },
|
tsonga: { IETF: 'ts-ZA', ISO639: 'ts', ISO3166: 'za' },
|
||||||
tswana: { IETF: 'tn-BW', 'ISO-639': 'tn' },
|
tswa: { IETF: 'tsc-MZ', ISO639: 'tsc', ISO3166: 'mz' },
|
||||||
tumbuka: { IETF: 'tum-MW', 'ISO-639': 'tum' },
|
tswana: { IETF: 'tn-BW', ISO639: 'tn', ISO3166: 'bw' },
|
||||||
turkish: { IETF: 'tr-TR', 'ISO-639': 'tr' },
|
tumbuka: { IETF: 'tum-MW', ISO639: 'tum', ISO3166: 'mw' },
|
||||||
turkmen: { IETF: 'tk-TM', 'ISO-639': 'tk' },
|
turkmen: { IETF: 'tk-TM', ISO639: 'tk', ISO3166: 'tm' },
|
||||||
tuvaluan: { IETF: 'tvl-TV', 'ISO-639': 'tvl' },
|
tuvaluan: { IETF: 'tvl-TV', ISO639: 'tvl', ISO3166: 'tv' },
|
||||||
twi: { IETF: 'tw-GH', 'ISO-639': 'tw' },
|
twi: { IETF: 'tw-GH', ISO639: 'tw', ISO3166: 'gh' },
|
||||||
udmurt: { IETF: 'udm-RU', 'ISO-639': 'udm' },
|
udmurt: { IETF: 'udm-RU', ISO639: 'udm', ISO3166: 'xx' },
|
||||||
ukrainian: { IETF: 'uk-UA', 'ISO-639': 'uk' },
|
ukrainian: { IETF: 'uk-UA', ISO639: 'uk', ISO3166: 'ua' },
|
||||||
uma: { IETF: 'ppk-ID', 'ISO-639': 'ppk' },
|
uma: { IETF: 'ppk-ID', ISO639: 'ppk', ISO3166: 'id' },
|
||||||
umbundu: { IETF: 'umb-AO', 'ISO-639': 'umb' },
|
umbundu: { IETF: 'umb-AO', ISO639: 'umb', ISO3166: 'ao' },
|
||||||
'uyghur uig': { IETF: 'uig-CN', 'ISO-639': 'uig' },
|
'uyghur uig': { IETF: 'uig-CN', ISO639: 'uig', ISO3166: 'cn' },
|
||||||
'uyghur ug': { IETF: 'ug-CN', 'ISO-639': 'ug' },
|
'uyghur ug': { IETF: 'ug-CN', ISO639: 'ug', ISO3166: 'cn' },
|
||||||
uzbek: { IETF: 'uz-UZ', 'ISO-639': 'uz' },
|
uzbek: { IETF: 'uz-UZ', ISO639: 'uz', ISO3166: 'uz' },
|
||||||
venetian: { IETF: 'vec-IT', 'ISO-639': 'vec' },
|
venetian: { IETF: 'vec-IT', ISO639: 'vec', ISO3166: 'it' },
|
||||||
vietnamese: { IETF: 'vi-VN', 'ISO-639': 'vi' },
|
vietnamese: { IETF: 'vi-VN', ISO639: 'vi', ISO3166: 'vn' },
|
||||||
'vincentian creole english': { IETF: 'svc-VC', 'ISO-639': 'svc' },
|
'vincentian creole english': { IETF: 'svc-VC', ISO639: 'svc', ISO3166: 'vc' },
|
||||||
'virgin islands creole english': { IETF: 'vic-US', 'ISO-639': 'vic' },
|
'virgin islands creole english': { IETF: 'vic-US', ISO639: 'vic', ISO3166: 'vi' },
|
||||||
wallisian: { IETF: 'wls-WF', 'ISO-639': 'wls' },
|
wallisian: { IETF: 'wls-WF', ISO639: 'wls', ISO3166: 'wf' },
|
||||||
'waray (philippines)': { IETF: 'war-PH', 'ISO-639': 'war' },
|
'waray (philippines)': { IETF: 'war-PH', ISO639: 'war', ISO3166: 'ph' },
|
||||||
welsh: { IETF: 'cy-GB', 'ISO-639': 'cy' },
|
welsh: { IETF: 'cy-GB', ISO639: 'cy', ISO3166: 'gb-wls' },
|
||||||
'west central oromo': { IETF: 'gaz-ET', 'ISO-639': 'gaz' },
|
'west central oromo': { IETF: 'gaz-ET', ISO639: 'gaz', ISO3166: 'et' },
|
||||||
'western persian': { IETF: 'pes-IR', 'ISO-639': 'pes' },
|
'western persian': { IETF: 'pes-IR', ISO639: 'pes', ISO3166: 'ir' },
|
||||||
wolof: { IETF: 'wo-SN', 'ISO-639': 'wo' },
|
wolof: { IETF: 'wo-SN', ISO639: 'wo', ISO3166: 'sn' },
|
||||||
xhosa: { IETF: 'xh-ZA', 'ISO-639': 'xh' },
|
xhosa: { IETF: 'xh-ZA', ISO639: 'xh', ISO3166: 'za' },
|
||||||
yiddish: { IETF: 'yi-YD', 'ISO-639': 'yi' },
|
yiddish: { IETF: 'yi-YD', ISO639: 'yi', ISO3166: 'il' },
|
||||||
yoruba: { IETF: 'yo-NG', 'ISO-639': 'yo' },
|
yoruba: { IETF: 'yo-NG', ISO639: 'yo', ISO3166: 'ng' },
|
||||||
zulu: { IETF: 'zu-ZA', 'ISO-639': 'zu' }
|
zulu: { IETF: 'zu-ZA', ISO639: 'zu', ISO3166: 'za' }
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { languages };
|
module.exports = { languages };
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const twitchTemplate = `
|
const twitchTemplate = `
|
||||||
<img class="user-img" src="" />
|
<img class="user-img" src="" />
|
||||||
<img class="status-circle sender" src="./images/twitch-icon.png" />
|
<img class="status-circle sender" src="./images/twitch-icon.png" tip="twitch" />
|
||||||
<span class="post-time sender"></span>
|
<span class="post-time sender"></span>
|
||||||
<span class="username sender"></span>
|
<span class="username sender"></span>
|
||||||
<div class="msg-box sender"></div>
|
<div class="msg-box sender"></div>
|
||||||
|
|
@ -15,7 +15,7 @@ const userTemplate = `
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
const messageTemplate = `
|
const messageTemplate = `
|
||||||
<article class="msg-container user" id="msg-0">
|
<article class=" user">
|
||||||
<img class="user-img" src="https://gravatar.com/avatar/56234674574535734573000000000001?d=retro" />
|
<img class="user-img" src="https://gravatar.com/avatar/56234674574535734573000000000001?d=retro" />
|
||||||
<img class="status-circle user" src="./images/twitch-icon.png" />
|
<img class="status-circle user" src="./images/twitch-icon.png" />
|
||||||
<span class="post-time user"> 12:00 PM</span>
|
<span class="post-time user"> 12:00 PM</span>
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,6 @@ const axios = require('axios');
|
||||||
const { webFrame, ipcRenderer, shell } = require('electron'); // necessary electron libraries to send data to the app
|
const { webFrame, ipcRenderer, shell } = require('electron'); // necessary electron libraries to send data to the app
|
||||||
const io = require('socket.io-client');
|
const io = require('socket.io-client');
|
||||||
|
|
||||||
const util = require('util');
|
|
||||||
const exec = util.promisify(require('child_process').exec);
|
|
||||||
|
|
||||||
const GoogleTTS = require('node-google-tts-api');
|
const GoogleTTS = require('node-google-tts-api');
|
||||||
|
|
||||||
const tts = new GoogleTTS();
|
const tts = new GoogleTTS();
|
||||||
|
|
@ -39,6 +36,7 @@ const sttModel = document.querySelector('#sttModel'); // obtain the html referen
|
||||||
const ttsAudioDevices = document.querySelector('#ttsAudioDevice'); // obtain the html reference of the installedTTS comboBox
|
const ttsAudioDevices = document.querySelector('#ttsAudioDevice'); // obtain the html reference of the installedTTS comboBox
|
||||||
const notificationSoundAudioDevices = document.querySelector('#notificationSoundAudioDevice'); // obtain the html reference of the installedTTS comboBox
|
const notificationSoundAudioDevices = document.querySelector('#notificationSoundAudioDevice'); // obtain the html reference of the installedTTS comboBox
|
||||||
const emojiPicker = document.body.querySelector('emoji-picker');
|
const emojiPicker = document.body.querySelector('emoji-picker');
|
||||||
|
const lol = document.body.querySelector('country-flag-emoji-polyfill');
|
||||||
|
|
||||||
// laod local javascript files
|
// laod local javascript files
|
||||||
const chat = require(path.join(__dirname, './js/chat'));
|
const chat = require(path.join(__dirname, './js/chat'));
|
||||||
|
|
@ -62,7 +60,8 @@ const server = require(path.join(__dirname, './js/server'));
|
||||||
const backend = require(path.join(__dirname, './js/backend'));
|
const backend = require(path.join(__dirname, './js/backend'));
|
||||||
const socket = io(`http://localhost:${settings.GENERAL.PORT}`); // Connect to your Socket.IO server
|
const socket = io(`http://localhost:${settings.GENERAL.PORT}`); // Connect to your Socket.IO server
|
||||||
|
|
||||||
const twitch = settings.TWITCH.USE_TWITCH ? require(path.join(__dirname, './js/twitch')) : '';
|
let twitch = null;
|
||||||
|
twitch = settings.TWITCH.USE_TWITCH ? require(path.join(__dirname, './js/twitch')) : '';
|
||||||
const Polly = settings.AMAZON.USE_AMAZON ? require(path.join(__dirname, './js/amazon')) : '';
|
const Polly = settings.AMAZON.USE_AMAZON ? require(path.join(__dirname, './js/amazon')) : '';
|
||||||
const google = settings.GOOGLE.USE_GOOGLE ? require(path.join(__dirname, './js/google')) : '';
|
const google = settings.GOOGLE.USE_GOOGLE ? require(path.join(__dirname, './js/google')) : '';
|
||||||
|
|
||||||
|
|
@ -73,6 +72,8 @@ let ttsRequestCount = 0;
|
||||||
ttsRequestCount = 0;
|
ttsRequestCount = 0;
|
||||||
let customEmojis = [];
|
let customEmojis = [];
|
||||||
customEmojis = [];
|
customEmojis = [];
|
||||||
|
let messageId = 0;
|
||||||
|
messageId = 0;
|
||||||
|
|
||||||
// initialize values
|
// initialize values
|
||||||
config.getGeneralSettings();
|
config.getGeneralSettings();
|
||||||
|
|
@ -135,6 +136,7 @@ fs.readdir(sttModels, (err, files) => {
|
||||||
sttModel.value = settings.STT.LANGUAGE;
|
sttModel.value = settings.STT.LANGUAGE;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: refactor obtaining audio devices.
|
||||||
async function getAudioDevices() {
|
async function getAudioDevices() {
|
||||||
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
|
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -144,11 +146,14 @@ async function getAudioDevices() {
|
||||||
const audioOutputDevices = devices.filter(device => device.kind === 'audiooutput');
|
const audioOutputDevices = devices.filter(device => device.kind === 'audiooutput');
|
||||||
|
|
||||||
audioOutputDevices.forEach(device => {
|
audioOutputDevices.forEach(device => {
|
||||||
const option = document.createElement('option');
|
const option1 = document.createElement('option');
|
||||||
option.text = device.label || `Output ${device.deviceId}`;
|
const option2 = document.createElement('option');
|
||||||
option.value = device.deviceId;
|
option1.text = device.label || `Output ${device.deviceId}`;
|
||||||
ttsAudioDevices.appendChild(option);
|
option2.text = device.label || `Output ${device.deviceId}`;
|
||||||
notificationSoundAudioDevices.appendChild(option);
|
option1.value = device.deviceId;
|
||||||
|
option2.value = device.deviceId;
|
||||||
|
ttsAudioDevices.appendChild(option1);
|
||||||
|
notificationSoundAudioDevices.appendChild(option2);
|
||||||
});
|
});
|
||||||
|
|
||||||
ttsAudioDevices.selectedIndex = settings.AUDIO.SELECTED_TTS_AUDIO_DEVICE;
|
ttsAudioDevices.selectedIndex = settings.AUDIO.SELECTED_TTS_AUDIO_DEVICE;
|
||||||
|
|
@ -162,10 +167,12 @@ function setLanguagesinSelect(languageSelector, setting) {
|
||||||
|
|
||||||
for (const language in languageObject.languages) {
|
for (const language in languageObject.languages) {
|
||||||
if (Object.prototype.hasOwnProperty.call(languageObject.languages, language)) {
|
if (Object.prototype.hasOwnProperty.call(languageObject.languages, language)) {
|
||||||
const iso639 = languageObject.languages[language]['ISO-639'];
|
const IETF = languageObject.languages[language].IETF;
|
||||||
|
const ISO639 = languageObject.languages[language].ISO639;
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = iso639;
|
|
||||||
option.innerHTML = `${iso639} - ${language}`;
|
option.value = IETF;
|
||||||
|
option.innerHTML = `${ISO639} : ${language}`;
|
||||||
languageSelect.appendChild(option);
|
languageSelect.appendChild(option);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -173,9 +180,10 @@ function setLanguagesinSelect(languageSelector, setting) {
|
||||||
languageSelect.selectedIndex = setting;
|
languageSelect.selectedIndex = setting;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLanguagesinSelect('#language', settings.GENERAL.LANGUAGE);
|
setLanguagesinSelect('#language', settings.GENERAL.LANGUAGE_INDEX);
|
||||||
setLanguagesinSelect('#defaultLanguage', settings.TTS.PRIMARY_TTS_LANGUAGE_INDEX);
|
setLanguagesinSelect('#defaultLanguage', settings.TTS.PRIMARY_TTS_LANGUAGE_INDEX);
|
||||||
setLanguagesinSelect('#secondaryLanguage', settings.TTS.SECONDARY_TTS_LANGUAGE_INDEX);
|
setLanguagesinSelect('#secondaryLanguage', settings.TTS.SECONDARY_TTS_LANGUAGE_INDEX);
|
||||||
|
setLanguagesinSelect('#TRANSLATE_TO', settings.LANGUAGE.TRANSLATE_TO_INDEX);
|
||||||
|
|
||||||
function addVoiceService(name) {
|
function addVoiceService(name) {
|
||||||
function addToselect(select) {
|
function addToselect(select) {
|
||||||
|
|
@ -191,13 +199,17 @@ function addVoiceService(name) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Small tooltip
|
// Small tooltip
|
||||||
Array.from(document.body.querySelectorAll('[tip]')).forEach(el => {
|
function addSingleTooltip(el) {
|
||||||
const tip = document.createElement('div');
|
const tip = document.createElement('div');
|
||||||
const body = document.querySelector('.container');
|
const body = document.querySelector('.container');
|
||||||
const element = el;
|
const element = el;
|
||||||
tip.classList.add('tooltip');
|
tip.classList.add('tooltip');
|
||||||
tip.classList.add('tooltiptext');
|
|
||||||
tip.innerText = el.getAttribute('tip');
|
tip.innerText = el.getAttribute('tip');
|
||||||
|
if (el.src) {
|
||||||
|
const image = document.createElement('img');
|
||||||
|
image.src = el.src;
|
||||||
|
tip.appendChild(image);
|
||||||
|
}
|
||||||
tip.style.transform = `translate(${el.hasAttribute('tip-left') ? 'calc(-100% - 5px)' : '15px'}, ${
|
tip.style.transform = `translate(${el.hasAttribute('tip-left') ? 'calc(-100% - 5px)' : '15px'}, ${
|
||||||
el.hasAttribute('tip-top') ? '-100%' : '15px'
|
el.hasAttribute('tip-top') ? '-100%' : '15px'
|
||||||
})`;
|
})`;
|
||||||
|
|
@ -205,16 +217,21 @@ Array.from(document.body.querySelectorAll('[tip]')).forEach(el => {
|
||||||
element.onmousemove = e => {
|
element.onmousemove = e => {
|
||||||
tip.style.left = `${e.x}px`;
|
tip.style.left = `${e.x}px`;
|
||||||
tip.style.top = `${e.y}px`;
|
tip.style.top = `${e.y}px`;
|
||||||
tip.style.zIndex = 1;
|
|
||||||
tip.style.visibility = 'visible';
|
tip.style.visibility = 'visible';
|
||||||
};
|
};
|
||||||
element.onmouseleave = e => {
|
element.onmouseleave = e => {
|
||||||
tip.style.visibility = 'hidden';
|
tip.style.visibility = 'hidden';
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.from(document.body.querySelectorAll('[tip]')).forEach(el => {
|
||||||
|
addSingleTooltip(el);
|
||||||
});
|
});
|
||||||
|
|
||||||
function showChatMessage(article) {
|
function showChatMessage(article) {
|
||||||
|
if (article !== undefined) {
|
||||||
document.querySelector('#chatBox').appendChild(article);
|
document.querySelector('#chatBox').appendChild(article);
|
||||||
|
}
|
||||||
|
|
||||||
const messages = Array.from(document.body.querySelectorAll('.msg-container'));
|
const messages = Array.from(document.body.querySelectorAll('.msg-container'));
|
||||||
|
|
||||||
|
|
@ -285,3 +302,35 @@ if (fs.existsSync(emoteListSavePath)) {
|
||||||
emojiPicker.customEmoji = emotes;
|
emojiPicker.customEmoji = emotes;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLanguageProperties(languageToDetect) {
|
||||||
|
const filteredLanguage = Object.keys(languageObject.languages).reduce(function (accumulator, currentValue) {
|
||||||
|
if (
|
||||||
|
languageObject.languages[currentValue].IETF === languageToDetect ||
|
||||||
|
languageObject.languages[currentValue].ISO639 === languageToDetect ||
|
||||||
|
languageObject.languages[currentValue].ISO3166 === languageToDetect
|
||||||
|
) {
|
||||||
|
accumulator[currentValue] = languageObject.languages[currentValue];
|
||||||
|
}
|
||||||
|
return accumulator;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const language = {
|
||||||
|
name: Object.getOwnPropertyNames(filteredLanguage)[0],
|
||||||
|
ISO3166: filteredLanguage[Object.keys(filteredLanguage)[0]].ISO3166,
|
||||||
|
ISO639: filteredLanguage[Object.keys(filteredLanguage)[0]].ISO639,
|
||||||
|
IETF: filteredLanguage[Object.keys(filteredLanguage)[0]].IETF
|
||||||
|
};
|
||||||
|
|
||||||
|
return language;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.querySelector('#emojis').addEventListener('click', () => {
|
||||||
|
emojiPicker.style.visibility === 'visible' ? (emojiPicker.style.visibility = 'hidden') : (emojiPicker.style.visibility = 'visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.addEventListener('click', e => {
|
||||||
|
if (e.target.id !== 'emojis' && e.target.nodeName !== 'EMOJI-PICKER' && emojiPicker.style.visibility === 'visible') {
|
||||||
|
emojiPicker.style.visibility = 'hidden';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ function getGeneralSettings() {
|
||||||
|
|
||||||
// Language detection
|
// Language detection
|
||||||
document.body.querySelector('#USE_DETECTION').checked = settings.LANGUAGE.USE_DETECTION;
|
document.body.querySelector('#USE_DETECTION').checked = settings.LANGUAGE.USE_DETECTION;
|
||||||
|
document.body.querySelector('#OUTPUT_TO_TTS').checked = settings.LANGUAGE.OUTPUT_TO_TTS;
|
||||||
|
document.body.querySelector('#BROADCAST_TRANSLATION').checked = settings.LANGUAGE.BROADCAST_TRANSLATION;
|
||||||
|
|
||||||
// TTS
|
// TTS
|
||||||
document.body.querySelector('#USE_TTS').checked = settings.TTS.USE_TTS;
|
document.body.querySelector('#USE_TTS').checked = settings.TTS.USE_TTS;
|
||||||
|
|
@ -100,7 +102,7 @@ document.body.querySelector('#sttModel').addEventListener('change', () => {
|
||||||
document.body.querySelector('#defaultLanguage').addEventListener('change', () => {
|
document.body.querySelector('#defaultLanguage').addEventListener('change', () => {
|
||||||
const select = document.querySelector('#defaultLanguage');
|
const select = document.querySelector('#defaultLanguage');
|
||||||
settings.TTS.PRIMARY_TTS_LANGUAGE_INDEX = select.selectedIndex;
|
settings.TTS.PRIMARY_TTS_LANGUAGE_INDEX = select.selectedIndex;
|
||||||
settings.TTS.PRIMARY_TTS_LANGUAGE = select.options[select.selectedIndex].text;
|
settings.TTS.PRIMARY_TTS_LANGUAGE = select.options[select.selectedIndex].value;
|
||||||
fs.writeFileSync(settingsPath, ini.stringify(settings));
|
fs.writeFileSync(settingsPath, ini.stringify(settings));
|
||||||
createNotification('Saved default language!', 'success');
|
createNotification('Saved default language!', 'success');
|
||||||
});
|
});
|
||||||
|
|
@ -115,11 +117,27 @@ document.body.querySelector('#secondaryVoice').addEventListener('change', () =>
|
||||||
document.body.querySelector('#secondaryLanguage').addEventListener('change', () => {
|
document.body.querySelector('#secondaryLanguage').addEventListener('change', () => {
|
||||||
const select = document.querySelector('#secondaryLanguage');
|
const select = document.querySelector('#secondaryLanguage');
|
||||||
settings.TTS.SECONDARY_TTS_LANGUAGE_INDEX = select.selectedIndex;
|
settings.TTS.SECONDARY_TTS_LANGUAGE_INDEX = select.selectedIndex;
|
||||||
settings.TTS.SECONDARY_TTS_LANGUAGE = select.options[select.selectedIndex].text;
|
settings.TTS.SECONDARY_TTS_LANGUAGE = select.options[select.selectedIndex].value;
|
||||||
fs.writeFileSync(settingsPath, ini.stringify(settings));
|
fs.writeFileSync(settingsPath, ini.stringify(settings));
|
||||||
createNotification('Saved secondary language!', 'success');
|
createNotification('Saved secondary language!', 'success');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.body.querySelector('#language').addEventListener('change', () => {
|
||||||
|
const select = document.querySelector('#language');
|
||||||
|
settings.GENERAL.LANGUAGE_INDEX = select.selectedIndex;
|
||||||
|
settings.GENERAL.LANGUAGE = select.options[select.selectedIndex].value;
|
||||||
|
fs.writeFileSync(settingsPath, ini.stringify(settings));
|
||||||
|
createNotification('Saved language!', 'success');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.querySelector('#TRANSLATE_TO').addEventListener('change', () => {
|
||||||
|
const select = document.querySelector('#TRANSLATE_TO');
|
||||||
|
settings.LANGUAGE.TRANSLATE_TO_INDEX = select.selectedIndex;
|
||||||
|
settings.LANGUAGE.TRANSLATE_TO = select.options[select.selectedIndex].value;
|
||||||
|
fs.writeFileSync(settingsPath, ini.stringify(settings));
|
||||||
|
createNotification('Saved primary voice!', 'success');
|
||||||
|
});
|
||||||
|
|
||||||
document.body.querySelector('#ttsAudioDevice').addEventListener('change', () => {
|
document.body.querySelector('#ttsAudioDevice').addEventListener('change', () => {
|
||||||
settings.AUDIO.TTS_AUDIO_DEVICE = ttsAudioDevices.value;
|
settings.AUDIO.TTS_AUDIO_DEVICE = ttsAudioDevices.value;
|
||||||
settings.AUDIO.SELECTED_TTS_AUDIO_DEVICE = ttsAudioDevices.selectedIndex;
|
settings.AUDIO.SELECTED_TTS_AUDIO_DEVICE = ttsAudioDevices.selectedIndex;
|
||||||
|
|
@ -138,6 +156,7 @@ document.body.querySelector('#TWITCH_CHANNEL_NAME').addEventListener('change', (
|
||||||
settings.TWITCH.CHANNEL_NAME = document.body.querySelector('#TWITCH_CHANNEL_NAME').value;
|
settings.TWITCH.CHANNEL_NAME = document.body.querySelector('#TWITCH_CHANNEL_NAME').value;
|
||||||
fs.writeFileSync(settingsPath, ini.stringify(settings));
|
fs.writeFileSync(settingsPath, ini.stringify(settings));
|
||||||
createNotification('Saved Channel name, please restart the application to reset twitch service', 'warning');
|
createNotification('Saved Channel name, please restart the application to reset twitch service', 'warning');
|
||||||
|
twitch.getTwitchChannelId();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.querySelector('#TWITCH_OAUTH_TOKEN').addEventListener('change', () => {
|
document.body.querySelector('#TWITCH_OAUTH_TOKEN').addEventListener('change', () => {
|
||||||
|
|
@ -207,10 +226,13 @@ function createNotification(message = null, type = null) {
|
||||||
alertSound = 'error.mp3';
|
alertSound = 'error.mp3';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settings.AUDIO.USE_NOTIFICATION_SOUNDS) {
|
||||||
const notfication = new Audio(path.join(resourcesPath, `./sounds/notifications/${alertSound}`));
|
const notfication = new Audio(path.join(resourcesPath, `./sounds/notifications/${alertSound}`));
|
||||||
notfication.volume = settings.AUDIO.NOTIFICATION_VOLUME / 100;
|
notfication.volume = settings.AUDIO.NOTIFICATION_VOLUME / 100;
|
||||||
notfication.play();
|
notfication.play();
|
||||||
setTimeout(() => notification.remove(), 10000);
|
}
|
||||||
|
|
||||||
|
setTimeout(() => notification.remove(), 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for configs
|
// Check for configs
|
||||||
|
|
@ -242,6 +264,10 @@ function toggleRadio(toggle, inputs) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.body.querySelector('#OPEN_SETTINGS_FILE').addEventListener('click', () => {
|
||||||
|
shell.openExternal(settingsPath);
|
||||||
|
});
|
||||||
|
|
||||||
// #region Use Custom theme toggle logic
|
// #region Use Custom theme toggle logic
|
||||||
document.body.querySelector('#USE_CUSTOM_THEME').addEventListener('click', () => {
|
document.body.querySelector('#USE_CUSTOM_THEME').addEventListener('click', () => {
|
||||||
const toggle = document.getElementById('USE_CUSTOM_THEME').checked;
|
const toggle = document.getElementById('USE_CUSTOM_THEME').checked;
|
||||||
|
|
@ -263,7 +289,7 @@ document.body.querySelector('#min-button').addEventListener('click', () => {
|
||||||
document.body.querySelector('#Info_USERNAME').addEventListener('click', async () => {
|
document.body.querySelector('#Info_USERNAME').addEventListener('click', async () => {
|
||||||
const element = document.body.querySelector('#TWITCH_OAUTH_TOKEN');
|
const element = document.body.querySelector('#TWITCH_OAUTH_TOKEN');
|
||||||
element.value = await auth.getTwitchOauthToken();
|
element.value = await auth.getTwitchOauthToken();
|
||||||
|
twitch.checkIfTokenIsValid();
|
||||||
createNotification('Saved OAuth token!', 'success');
|
createNotification('Saved OAuth token!', 'success');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -333,11 +359,6 @@ document.body.querySelector('#TestTwitchCredentials').addEventListener('click',
|
||||||
// resetTwitch(;
|
// resetTwitch(;
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.querySelector('#GetTwitchEmotes').addEventListener('click', () => {
|
|
||||||
twitch.getUserAvailableTwitchEmotes('#GetTwitchEmotes');
|
|
||||||
// resetTwitch(;
|
|
||||||
});
|
|
||||||
|
|
||||||
function toggleTwitch() {
|
function toggleTwitch() {
|
||||||
const toggle = settings.TWITCH.USE_TWITCH;
|
const toggle = settings.TWITCH.USE_TWITCH;
|
||||||
const inputs = document.getElementsByClassName('inputTwitch');
|
const inputs = document.getElementsByClassName('inputTwitch');
|
||||||
|
|
@ -467,6 +488,36 @@ document.body.querySelector('#USE_STT').addEventListener('change', () => {
|
||||||
createNotification(`${toggle ? 'Enabled' : 'Disabled'} speech to text!`, 'success');
|
createNotification(`${toggle ? 'Enabled' : 'Disabled'} speech to text!`, 'success');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function toggleOutputToTts() {
|
||||||
|
const toggle = settings.LANGUAGE.OUTPUT_TO_TTS;
|
||||||
|
const inputs = document.getElementsByClassName('outputToTtsInput');
|
||||||
|
toggleRadio(toggle, inputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleOutputToTts();
|
||||||
|
|
||||||
|
document.body.querySelector('#OUTPUT_TO_TTS').addEventListener('change', () => {
|
||||||
|
let toggle = document.getElementById('OUTPUT_TO_TTS').checked;
|
||||||
|
if (!settings.TTS.USE_TTS) {
|
||||||
|
toggle = false;
|
||||||
|
createNotification('Enable TTS first', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.LANGUAGE.OUTPUT_TO_TTS = toggle;
|
||||||
|
const inputs = document.getElementsByClassName('outputToTtsInput');
|
||||||
|
toggleRadio(toggle, inputs);
|
||||||
|
fs.writeFileSync(settingsPath, ini.stringify(settings));
|
||||||
|
createNotification(`${toggle ? 'Enabled' : 'Disabled'} Outputting translations to TTS!`, 'success');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.querySelector('#BROADCAST_TRANSLATION').addEventListener('change', () => {
|
||||||
|
const toggle = document.getElementById('BROADCAST_TRANSLATION').checked;
|
||||||
|
settings.LANGUAGE.BROADCAST_TRANSLATION = toggle;
|
||||||
|
fs.writeFileSync(settingsPath, ini.stringify(settings));
|
||||||
|
createNotification(`${toggle ? 'Enabled' : 'Disabled'} Language detection!`, 'success');
|
||||||
|
});
|
||||||
|
|
||||||
function toggleLanguageDetection() {
|
function toggleLanguageDetection() {
|
||||||
const toggle = settings.LANGUAGE.USE_DETECTION;
|
const toggle = settings.LANGUAGE.USE_DETECTION;
|
||||||
const inputs = document.getElementsByClassName('languageDetectionInput');
|
const inputs = document.getElementsByClassName('languageDetectionInput');
|
||||||
|
|
@ -478,6 +529,14 @@ toggleLanguageDetection();
|
||||||
document.body.querySelector('#USE_DETECTION').addEventListener('change', () => {
|
document.body.querySelector('#USE_DETECTION').addEventListener('change', () => {
|
||||||
const toggle = document.getElementById('USE_DETECTION').checked;
|
const toggle = document.getElementById('USE_DETECTION').checked;
|
||||||
settings.LANGUAGE.USE_DETECTION = toggle;
|
settings.LANGUAGE.USE_DETECTION = toggle;
|
||||||
|
|
||||||
|
if (!toggle) {
|
||||||
|
settings.LANGUAGE.BROADCAST_TRANSLATION = false;
|
||||||
|
document.body.querySelector('#BROADCAST_TRANSLATION').checked = false;
|
||||||
|
settings.LANGUAGE.OUTPUT_TO_TTS = false;
|
||||||
|
document.body.querySelector('#OUTPUT_TO_TTS').checked = false;
|
||||||
|
}
|
||||||
|
|
||||||
fs.writeFileSync(settingsPath, ini.stringify(settings));
|
fs.writeFileSync(settingsPath, ini.stringify(settings));
|
||||||
const inputs = document.getElementsByClassName('languageDetectionInput');
|
const inputs = document.getElementsByClassName('languageDetectionInput');
|
||||||
toggleRadio(toggle, inputs);
|
toggleRadio(toggle, inputs);
|
||||||
|
|
@ -588,6 +647,9 @@ document.body.querySelector('#ttsVolume').addEventListener('change', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.querySelector('#TestDefaultTTSButton').addEventListener('click', async () => {
|
document.body.querySelector('#TestDefaultTTSButton').addEventListener('click', async () => {
|
||||||
|
if (!settings.TTS.PRIMARY_VOICE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const text = document.getElementById('testPrimaryTTS').value;
|
const text = document.getElementById('testPrimaryTTS').value;
|
||||||
const requestData = {
|
const requestData = {
|
||||||
message: `user: ${text}`,
|
message: `user: ${text}`,
|
||||||
|
|
@ -599,6 +661,9 @@ document.body.querySelector('#TestDefaultTTSButton').addEventListener('click', a
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.querySelector('#TestSecondaryTTSButton').addEventListener('click', async () => {
|
document.body.querySelector('#TestSecondaryTTSButton').addEventListener('click', async () => {
|
||||||
|
if (!settings.TTS.SECONDARY_VOICE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const text = document.getElementById('testSecondaryTTS').value;
|
const text = document.getElementById('testSecondaryTTS').value;
|
||||||
const requestData = {
|
const requestData = {
|
||||||
message: `user: ${text}`,
|
message: `user: ${text}`,
|
||||||
|
|
@ -678,13 +743,7 @@ document.body.querySelector('emoji-picker').addEventListener('emoji-click', e =>
|
||||||
div.focus();
|
div.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
// #region Use Custom theme toggle logic
|
|
||||||
document.body.querySelector('#emojis').addEventListener('click', () => {
|
|
||||||
const emojiPicker = document.body.querySelector('#emoji-picker');
|
|
||||||
// console.log(emojiPicker);
|
|
||||||
emojiPicker.style.visibility === 'visible' ? (emojiPicker.style.visibility = 'hidden') : (emojiPicker.style.visibility = 'visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getGeneralSettings
|
getGeneralSettings,
|
||||||
|
createNotification
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
/* global ttsAudioFile, path, resourcesPath, settings, fs, notificationSound, backend, socket, requestData */
|
/* global ttsAudioFile, path, getLanguageProperties, resourcesPath, settings, fs, notificationSound, backend, socket, requestData */
|
||||||
|
|
||||||
let trueMessage = '';
|
|
||||||
let currentLogoUrl = '';
|
|
||||||
let currentUsername = '';
|
|
||||||
const voiceSoundArray = [];
|
const voiceSoundArray = [];
|
||||||
let status = 0;
|
let status = 0;
|
||||||
const counter = 0;
|
const counter = 0;
|
||||||
|
|
@ -11,7 +8,6 @@ const playTTS = data =>
|
||||||
new Promise(resolve => {
|
new Promise(resolve => {
|
||||||
ttsAudioFile = path.join(resourcesPath, `./sounds/tts/${data.service}_${data.count}.mp3`);
|
ttsAudioFile = path.join(resourcesPath, `./sounds/tts/${data.service}_${data.count}.mp3`);
|
||||||
const tts = new Audio(ttsAudioFile);
|
const tts = new Audio(ttsAudioFile);
|
||||||
// console.log(settings.AUDIO.TTS_AUDIO_DEVICE);
|
|
||||||
tts.setSinkId(settings.AUDIO.TTS_AUDIO_DEVICE);
|
tts.setSinkId(settings.AUDIO.TTS_AUDIO_DEVICE);
|
||||||
|
|
||||||
tts.addEventListener('ended', () => {
|
tts.addEventListener('ended', () => {
|
||||||
|
|
@ -89,24 +85,20 @@ function playAudio(data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function playVoice(filteredMessage, logoUrl, username, message) {
|
async function playVoice(message) {
|
||||||
trueMessage = filteredMessage;
|
if (!settings.TTS.PRIMARY_VOICE) {
|
||||||
currentLogoUrl = logoUrl;
|
return;
|
||||||
currentUsername = username;
|
}
|
||||||
const textObject = { filtered: filteredMessage, formatted: message };
|
const textObject = { filtered: message.filteredMessage, formatted: message.formattedMessage };
|
||||||
let voice;
|
let voice = settings.TTS.PRIMARY_VOICE;
|
||||||
textObject.filtered = `${username}: ${filteredMessage}`;
|
textObject.filtered = `${message.username}: ${message.filteredMessage}`;
|
||||||
|
|
||||||
// if (
|
if (settings.LANGUAGE.USE_DETECTION && settings.TTS.SECONDARY_VOICE && settings.LANGUAGE.OUTPUT_TO_TTS) {
|
||||||
// settings.TTS.PRIMARY_TTS_LANGUAGE.toLowerCase() !== settings.TTS.SECONDARY_TTS_LANGUAGE.toLowerCase() &&
|
const secondaryTTSLanguage = getLanguageProperties(settings.TTS.SECONDARY_TTS_LANGUAGE);
|
||||||
// language[0].lang === settings.TTS.SECONDARY_TTS_LANGUAGE.toLowerCase()
|
if (message.language.ISO639 === secondaryTTSLanguage.ISO639) {
|
||||||
// ) {
|
voice = settings.TTS.SECONDARY_VOICE;
|
||||||
// voice = settings.TTS.SECONDARY_TTS_NAME;
|
}
|
||||||
// textObject.filtered = `${username}: ${filteredMessage}`;
|
}
|
||||||
// } else {
|
|
||||||
// voice = settings.TTS.PRIMARY_TTS_NAME;
|
|
||||||
// textObject.filtered = `${username}: ${filteredMessage}`;
|
|
||||||
// }
|
|
||||||
|
|
||||||
const service = document.getElementById('primaryTTSService').value;
|
const service = document.getElementById('primaryTTSService').value;
|
||||||
|
|
||||||
|
|
@ -114,7 +106,7 @@ async function playVoice(filteredMessage, logoUrl, username, message) {
|
||||||
case 'Internal': {
|
case 'Internal': {
|
||||||
const requestData = {
|
const requestData = {
|
||||||
message: textObject.filtered,
|
message: textObject.filtered,
|
||||||
voice: settings.TTS.PRIMARY_VOICE
|
voice: voice
|
||||||
};
|
};
|
||||||
|
|
||||||
const count = await backend.getInternalTTSAudio(requestData);
|
const count = await backend.getInternalTTSAudio(requestData);
|
||||||
|
|
@ -130,10 +122,8 @@ async function playVoice(filteredMessage, logoUrl, username, message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.MODULES.USE_CHATBUBBLE) {
|
if (settings.MODULES.USE_CHATBUBBLE) {
|
||||||
socket.emit('xxx', currentLogoUrl, currentUsername, textObject);
|
socket.emit('xxx', message);
|
||||||
}
|
}
|
||||||
|
|
||||||
playNotificationSound();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { playAudio, playVoice, playNotificationSound };
|
module.exports = { playAudio, playVoice, playNotificationSound };
|
||||||
|
|
|
||||||
288
src/js/twitch.js
288
src/js/twitch.js
|
|
@ -1,4 +1,4 @@
|
||||||
/* global client, fs, main, path, resourcesPath, customEmojis, emojiPicker, settings, options, sound, showChatMessage, messageTemplates, getPostTime */
|
/* global client, playNotificationSound, messageId, addSingleTooltip, settingsPath, fs, ini, backend, main, path, resourcesPath, customEmojis, emojiPicker,config, settings, options, sound, showChatMessage, messageTemplates, getPostTime */
|
||||||
|
|
||||||
const tmi = require('tmi.js');
|
const tmi = require('tmi.js');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
|
@ -11,7 +11,8 @@ function sendMessage(message) {
|
||||||
client.say(settings.TWITCH.CHANNEL_NAME, message).catch(console.error);
|
client.say(settings.TWITCH.CHANNEL_NAME, message).catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
client = new tmi.Client({
|
if (settings.TWITCH.USERNAME && settings.TWITCH.OAUTH_TOKEN) {
|
||||||
|
client = new tmi.Client({
|
||||||
options: {
|
options: {
|
||||||
skipUpdatingEmotesets: true
|
skipUpdatingEmotesets: true
|
||||||
},
|
},
|
||||||
|
|
@ -20,13 +21,66 @@ client = new tmi.Client({
|
||||||
password: settings.TWITCH.OAUTH_TOKEN
|
password: settings.TWITCH.OAUTH_TOKEN
|
||||||
},
|
},
|
||||||
channels: [settings.TWITCH.CHANNEL_NAME]
|
channels: [settings.TWITCH.CHANNEL_NAME]
|
||||||
});
|
});
|
||||||
|
|
||||||
client
|
client
|
||||||
.connect()
|
.connect()
|
||||||
.then(data => {})
|
.then(data => {})
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
|
|
||||||
|
client.on('message', (channel, tags, message, self) => {
|
||||||
|
if (self) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const emotes = tags.emotes || {};
|
||||||
|
const emoteValues = Object.entries(emotes);
|
||||||
|
let filteredMessage = message;
|
||||||
|
let emoteMessage = message;
|
||||||
|
|
||||||
|
emoteValues.forEach(entry => {
|
||||||
|
entry[1].forEach(lol => {
|
||||||
|
const [start, end] = lol.split('-');
|
||||||
|
const emote = `<img src="https://static-cdn.jtvnw.net/emoticons/v2/${entry[0]}/default/dark/1.0"/>`;
|
||||||
|
emoteMessage = emoteMessage.replaceAll(message.slice(parseInt(start), parseInt(end) + 1), emote);
|
||||||
|
filteredMessage = filteredMessage.replaceAll(message.slice(parseInt(start), parseInt(end) + 1), '');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageObject = parseString(emoteMessage);
|
||||||
|
|
||||||
|
getProfileImage(tags['user-id'], tags['display-name'], messageObject, filteredMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkIfTokenIsValid() {
|
||||||
|
options = {
|
||||||
|
method: 'GET',
|
||||||
|
url: 'https://id.twitch.tv/oauth2/validate',
|
||||||
|
headers: {
|
||||||
|
Authorization: `OAuth ${settings.TWITCH.OAUTH_TOKEN}`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
axios
|
||||||
|
.request(options)
|
||||||
|
.then(response => {
|
||||||
|
document.getElementById('TWITCH_CHANNEL_NAME').disabled = false;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
document.getElementById('TWITCH_CHANNEL_NAME').disabled = true;
|
||||||
|
config.createNotification('Oauth Token is invalid, please obtain a new one', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(checkIfTokenIsValid, 600000);
|
||||||
|
|
||||||
|
if (settings.TWITCH.OAUTH_TOKEN) {
|
||||||
|
checkIfTokenIsValid();
|
||||||
|
} else {
|
||||||
|
document.getElementById('TWITCH_CHANNEL_NAME').disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
function ping(element) {
|
function ping(element) {
|
||||||
const value = document.body.querySelector(element);
|
const value = document.body.querySelector(element);
|
||||||
|
|
||||||
|
|
@ -42,16 +96,18 @@ function ping(element) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayTwitchMessage(logoUrl, username, messageObject, fileteredMessage) {
|
async function displayTwitchMessage(logoUrl, username, messageObject, filteredMessage) {
|
||||||
const article = document.createElement('article');
|
const article = document.createElement('article');
|
||||||
article.className = 'msg-container sender';
|
article.className = 'msg-container sender';
|
||||||
|
article.setAttribute('id', messageId);
|
||||||
|
|
||||||
article.innerHTML = messageTemplates.twitchTemplate;
|
article.innerHTML = messageTemplates.twitchTemplate;
|
||||||
|
|
||||||
const userImg = article.querySelector('.user-img');
|
const userImg = article.querySelector('.user-img');
|
||||||
if (userImg) {
|
if (userImg) {
|
||||||
userImg.src = logoUrl;
|
userImg.src = logoUrl;
|
||||||
|
userImg.setAttribute('tip', '');
|
||||||
}
|
}
|
||||||
|
addSingleTooltip(userImg);
|
||||||
|
|
||||||
const usernameHtml = article.querySelector('.username');
|
const usernameHtml = article.querySelector('.username');
|
||||||
if (usernameHtml) {
|
if (usernameHtml) {
|
||||||
|
|
@ -66,28 +122,48 @@ function displayTwitchMessage(logoUrl, username, messageObject, fileteredMessage
|
||||||
|
|
||||||
article.appendChild(postTime);
|
article.appendChild(postTime);
|
||||||
|
|
||||||
const msg = article.querySelector('.msg-box');
|
const formattedMessage = article.querySelector('.msg-box');
|
||||||
if (msg) {
|
if (formattedMessage) {
|
||||||
messageObject.forEach(entry => {
|
messageObject.forEach(entry => {
|
||||||
if (entry.text) {
|
if (entry.text) {
|
||||||
msg.innerHTML += entry.text;
|
formattedMessage.innerHTML += entry.text;
|
||||||
} else {
|
} else {
|
||||||
msg.innerHTML += entry.html;
|
formattedMessage.innerHTML += entry.html;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (settings.LANGUAGE.USE_DETECTION) {
|
||||||
|
await backend.getDetectedLanguage({ message: filteredMessage, messageId, username, logoUrl, formattedMessage }).then(language => {
|
||||||
|
const languageElement = document.createElement('span');
|
||||||
|
languageElement.classList = `fi fi-${language.ISO3166} fis`;
|
||||||
|
languageElement.setAttribute('tip', language.name);
|
||||||
|
article.appendChild(languageElement);
|
||||||
|
addSingleTooltip(languageElement);
|
||||||
|
|
||||||
// Appends the message to the main chat box (shows the message)
|
// Appends the message to the main chat box (shows the message)
|
||||||
showChatMessage(article);
|
showChatMessage(article);
|
||||||
|
|
||||||
if (fileteredMessage) {
|
if (filteredMessage && !settings.LANGUAGE.OUTPUT_TO_TTS) {
|
||||||
sound.playVoice(fileteredMessage, logoUrl, username, msg);
|
sound.playVoice({ filteredMessage, logoUrl, username, formattedMessage, language });
|
||||||
}
|
}
|
||||||
|
|
||||||
window.article = article;
|
window.article = article;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
showChatMessage(article);
|
||||||
|
|
||||||
|
if (filteredMessage) {
|
||||||
|
sound.playVoice({ filteredMessage, logoUrl, username, formattedMessage });
|
||||||
|
}
|
||||||
|
|
||||||
|
window.article = article;
|
||||||
|
}
|
||||||
|
|
||||||
|
messageId++;
|
||||||
|
sound.playNotificationSound();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProfileImage(userid, username, message, fileteredMessage) {
|
function getProfileImage(userid, username, message, filteredMessage) {
|
||||||
// Get user Logo with access token
|
// Get user Logo with access token
|
||||||
options = {
|
options = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
|
@ -99,7 +175,7 @@ function getProfileImage(userid, username, message, fileteredMessage) {
|
||||||
.request(options)
|
.request(options)
|
||||||
.then(responseLogoUrl => {
|
.then(responseLogoUrl => {
|
||||||
logoUrl = responseLogoUrl.data.data[0].profile_image_url;
|
logoUrl = responseLogoUrl.data.data[0].profile_image_url;
|
||||||
displayTwitchMessage(logoUrl, username, message, fileteredMessage);
|
displayTwitchMessage(logoUrl, username, message, filteredMessage);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
@ -123,28 +199,6 @@ function parseString(inputString) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
client.on('message', (channel, tags, message, self) => {
|
|
||||||
if (self) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const emotes = tags.emotes || {};
|
|
||||||
const emoteValues = Object.entries(emotes);
|
|
||||||
let filteredMessage = message;
|
|
||||||
let emoteMessage = message;
|
|
||||||
|
|
||||||
emoteValues.forEach(entry => {
|
|
||||||
entry[1].forEach(lol => {
|
|
||||||
const [start, end] = lol.split('-');
|
|
||||||
const emote = `<img src="https://static-cdn.jtvnw.net/emoticons/v2/${entry[0]}/default/dark/1.0"/>`;
|
|
||||||
emoteMessage = emoteMessage.replaceAll(message.slice(parseInt(start), parseInt(end) + 1), emote);
|
|
||||||
filteredMessage = filteredMessage.replaceAll(message.slice(parseInt(start), parseInt(end) + 1), '');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const messageObject = parseString(emoteMessage);
|
|
||||||
getProfileImage(tags['user-id'], tags['display-name'], messageObject, filteredMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
function saveTwitchEmotesToFile(TwitchEmotes) {
|
function saveTwitchEmotesToFile(TwitchEmotes) {
|
||||||
const data = JSON.stringify(TwitchEmotes);
|
const data = JSON.stringify(TwitchEmotes);
|
||||||
const savePath =
|
const savePath =
|
||||||
|
|
@ -164,33 +218,19 @@ function saveTwitchEmotesToFile(TwitchEmotes) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function formatGlobalTwitchEmotes(emotes, name) {
|
function formatTwitchEmotes(channel) {
|
||||||
for (const emote of emotes) {
|
|
||||||
const emojiToBeAdded = {
|
|
||||||
name: emote.name,
|
|
||||||
shortcodes: [emote.name],
|
|
||||||
url: emote.images.url_1x,
|
|
||||||
category: name
|
|
||||||
};
|
|
||||||
await customEmojis.push(emojiToBeAdded);
|
|
||||||
}
|
|
||||||
emojiPicker.customEmoji = customEmojis;
|
|
||||||
saveTwitchEmotesToFile(customEmojis);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatFollowedChannelsTwitchEmotes(channel) {
|
|
||||||
if (channel.emotes.length === 0) {
|
if (channel.emotes.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
channel.emotes.forEach(emote => {
|
channel.emotes.forEach(emote => {
|
||||||
if (emote.emote_type === 'bitstier') {
|
if (channel.name !== 'Twitch Global' && emote.emote_type === 'bitstier') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (emote.emote_type === 'subscriptions' && parseInt(channel.tier) < parseInt(emote.tier)) {
|
if (channel.name !== 'Twitch Global' && emote.emote_type === 'subscriptions' && parseInt(channel.tier) < parseInt(emote.tier)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (emote.emote_type === 'follower ' && parseInt(channel.tier) === '0') {
|
if (channel.name !== 'Twitch Global' && emote.emote_type === 'follower ' && parseInt(channel.tier) === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const emojiToBeAdded = {
|
const emojiToBeAdded = {
|
||||||
|
|
@ -205,7 +245,7 @@ function formatFollowedChannelsTwitchEmotes(channel) {
|
||||||
saveTwitchEmotesToFile(customEmojis);
|
saveTwitchEmotesToFile(customEmojis);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTwitchChannelFollows(paginationToken) {
|
function getTwitchUserFollows(paginationToken) {
|
||||||
let url = '';
|
let url = '';
|
||||||
if (!paginationToken) {
|
if (!paginationToken) {
|
||||||
url = `https://api.twitch.tv/helix/channels/followed?user_id=${settings.TWITCH.USER_ID}&first=100`;
|
url = `https://api.twitch.tv/helix/channels/followed?user_id=${settings.TWITCH.USER_ID}&first=100`;
|
||||||
|
|
@ -227,11 +267,15 @@ function getTwitchChannelFollows(paginationToken) {
|
||||||
// console.log(responseLogoUrl);
|
// console.log(responseLogoUrl);
|
||||||
|
|
||||||
responseLogoUrl.data.data.forEach(channel => {
|
responseLogoUrl.data.data.forEach(channel => {
|
||||||
twitchChannels.push({ broadcaster_id: channel.broadcaster_id, broadcaster_name: channel.broadcaster_name, tier: '0' });
|
twitchChannels.push({
|
||||||
|
broadcaster_id: channel.broadcaster_id,
|
||||||
|
broadcaster_name: channel.broadcaster_name,
|
||||||
|
tier: '0'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Object.keys(responseLogoUrl.data.pagination).length !== 0) {
|
if (Object.keys(responseLogoUrl.data.pagination).length !== 0) {
|
||||||
getTwitchChannelFollows(responseLogoUrl.data.pagination.cursor);
|
getTwitchUserFollows(responseLogoUrl.data.pagination.cursor);
|
||||||
} else {
|
} else {
|
||||||
getTwitchChannelSubscriptions(twitchChannels);
|
getTwitchChannelSubscriptions(twitchChannels);
|
||||||
}
|
}
|
||||||
|
|
@ -255,7 +299,6 @@ function getTwitchChannelSubscriptions(channels) {
|
||||||
axios
|
axios
|
||||||
.request(options)
|
.request(options)
|
||||||
.then(responseLogoUrl => {
|
.then(responseLogoUrl => {
|
||||||
// console.log(responseLogoUrl);
|
|
||||||
const objIndex = twitchChannels.findIndex(obj => obj.broadcaster_id === channel.broadcaster_id);
|
const objIndex = twitchChannels.findIndex(obj => obj.broadcaster_id === channel.broadcaster_id);
|
||||||
twitchChannels[objIndex].tier = responseLogoUrl.data.data[0].tier;
|
twitchChannels[objIndex].tier = responseLogoUrl.data.data[0].tier;
|
||||||
getTwitchChannelEmotes(channel);
|
getTwitchChannelEmotes(channel);
|
||||||
|
|
@ -282,14 +325,9 @@ function getTwitchChannelEmotes(channel) {
|
||||||
axios
|
axios
|
||||||
.request(options)
|
.request(options)
|
||||||
.then(responseLogoUrl => {
|
.then(responseLogoUrl => {
|
||||||
// console.log(channel.broadcaster_id);
|
|
||||||
if (channel.broadcaster_id === '98553286') {
|
|
||||||
// console.log(responseLogoUrl);
|
|
||||||
// console.log(channel);
|
|
||||||
}
|
|
||||||
if (responseLogoUrl.data.data.length !== 0) {
|
if (responseLogoUrl.data.data.length !== 0) {
|
||||||
channel.emotes = responseLogoUrl.data.data;
|
channel.emotes = responseLogoUrl.data.data;
|
||||||
formatFollowedChannelsTwitchEmotes(channel);
|
formatTwitchEmotes(channel);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
|
@ -297,7 +335,7 @@ function getTwitchChannelEmotes(channel) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTwitchGLobalEmotes() {
|
function getTwitchGlobalEmotes() {
|
||||||
// Get user Logo with access token
|
// Get user Logo with access token
|
||||||
options = {
|
options = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
|
@ -311,37 +349,7 @@ function getTwitchGLobalEmotes() {
|
||||||
axios
|
axios
|
||||||
.request(options)
|
.request(options)
|
||||||
.then(responseLogoUrl => {
|
.then(responseLogoUrl => {
|
||||||
formatGlobalTwitchEmotes(responseLogoUrl.data.data, 'Twitch Global');
|
formatTwitchEmotes({ broadcaster_name: 'Twitch Global', emotes: responseLogoUrl.data.data });
|
||||||
// console.log(responseLogoUrl);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentTwitchChannelId(channelId) {
|
|
||||||
let channel = {};
|
|
||||||
options = {
|
|
||||||
method: 'GET',
|
|
||||||
url: `https://api.twitch.tv/helix/users?${channelId !== undefined ? 'id=' + channelId : 'login=' + settings.TWITCH.CHANNEL_NAME}`,
|
|
||||||
headers: {
|
|
||||||
'Client-ID': settings.TWITCH.CLIENT_ID,
|
|
||||||
Authorization: `Bearer ${settings.TWITCH.OAUTH_TOKEN}`
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
axios
|
|
||||||
.request(options)
|
|
||||||
.then(responseData => {
|
|
||||||
// console.log(responseData);
|
|
||||||
channel = { broadcaster_id: responseData.data.data[0].id, broadcaster_name: responseData.data.data[0].display_name, tier: '0' };
|
|
||||||
if (responseData.data.data[0].id !== settings.TWITCH.USER_ID) {
|
|
||||||
getCurrentTwitchChannelId(settings.TWITCH.USER_ID);
|
|
||||||
channel.tier = '0';
|
|
||||||
} else {
|
|
||||||
channel.tier = '3000';
|
|
||||||
}
|
|
||||||
getTwitchChannelEmotes(channel);
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
@ -350,10 +358,90 @@ function getCurrentTwitchChannelId(channelId) {
|
||||||
|
|
||||||
async function getUserAvailableTwitchEmotes() {
|
async function getUserAvailableTwitchEmotes() {
|
||||||
if (settings.TWITCH.OAUTH_TOKEN) {
|
if (settings.TWITCH.OAUTH_TOKEN) {
|
||||||
await getTwitchGLobalEmotes();
|
await getTwitchGlobalEmotes();
|
||||||
await getTwitchChannelFollows();
|
await getTwitchUserFollows();
|
||||||
await getCurrentTwitchChannelId();
|
await getTwitchChannelEmotes({
|
||||||
|
broadcaster_id: settings.TWITCH.USER_ID,
|
||||||
|
broadcaster_name: settings.TWITCH.USERNAME,
|
||||||
|
tier: '3000'
|
||||||
|
});
|
||||||
|
await getTwitchChannelEmotes({
|
||||||
|
broadcaster_id: settings.TWITCH.CHANNEL_USER_ID,
|
||||||
|
broadcaster_name: settings.TWITCH.CHANNEL_NAME,
|
||||||
|
tier: '1'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { sendMessage, ping, client, getUserAvailableTwitchEmotes };
|
function getTwitchChannelId() {
|
||||||
|
options = {
|
||||||
|
method: 'GET',
|
||||||
|
url: `https://api.twitch.tv/helix/users?login=${settings.TWITCH.CHANNEL_NAME}`,
|
||||||
|
headers: {
|
||||||
|
'Client-ID': settings.TWITCH.CLIENT_ID,
|
||||||
|
Authorization: `Bearer ${settings.TWITCH.OAUTH_TOKEN}`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
axios
|
||||||
|
.request(options)
|
||||||
|
.then(responseLogoUrl => {
|
||||||
|
settings.TWITCH.CHANNEL_USER_ID = responseLogoUrl.data.data[0].id;
|
||||||
|
fs.writeFileSync(settingsPath, ini.stringify(settings));
|
||||||
|
config.createNotification('Obtained channel info succesfully', 'success');
|
||||||
|
getUserAvailableTwitchEmotes();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
config.createNotification('could not obtain channel info, please try again', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTwitchUserId() {
|
||||||
|
// Get user Logo with access token
|
||||||
|
options = {
|
||||||
|
method: 'GET',
|
||||||
|
url: 'https://api.twitch.tv/helix/users',
|
||||||
|
headers: {
|
||||||
|
'Client-ID': settings.TWITCH.CLIENT_ID,
|
||||||
|
Authorization: `Bearer ${settings.TWITCH.OAUTH_TOKEN}`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
axios
|
||||||
|
.request(options)
|
||||||
|
.then(responseLogoUrl => {
|
||||||
|
// console.log(responseLogoUrl.data.data[0]);
|
||||||
|
settings.TWITCH.USERNAME = responseLogoUrl.data.data[0].display_name;
|
||||||
|
settings.TWITCH.USER_LOGO_URL = responseLogoUrl.data.data[0].profile_image_url;
|
||||||
|
settings.TWITCH.USER_ID = responseLogoUrl.data.data[0].id;
|
||||||
|
fs.writeFileSync(settingsPath, ini.stringify(settings));
|
||||||
|
config.createNotification('Obtained user info succesfully', 'success');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
config.createNotification('could not obtain user info, please try again', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// const Sockette = require('sockette');
|
||||||
|
|
||||||
|
// const ws = new Sockette('wss://eventsub.wss.twitch.tv/ws', {
|
||||||
|
// timeout: 5e3,
|
||||||
|
// maxAttempts: 10,
|
||||||
|
// onopen: e => console.log('Connected!', e),
|
||||||
|
// onmessage: e => console.log('Received:', e),
|
||||||
|
// onreconnect: e => console.log('Reconnecting...', e),
|
||||||
|
// onmaximum: e => console.log('Stop Attempting!', e),
|
||||||
|
// onclose: e => console.log('Closed!', e),
|
||||||
|
// onerror: e => console.log('Error:', e)
|
||||||
|
// });
|
||||||
|
|
||||||
|
// ws.send('Hello, world!');
|
||||||
|
// ws.json({ type: 'ping' });
|
||||||
|
// ws.close(); // graceful shutdown
|
||||||
|
|
||||||
|
// Reconnect 10s later
|
||||||
|
// setTimeout(ws.reconnect, 10e3);
|
||||||
|
|
||||||
|
module.exports = { sendMessage, ping, client, getUserAvailableTwitchEmotes, getTwitchChannelId, getTwitchUserId, checkIfTokenIsValid };
|
||||||
|
|
|
||||||
22
src/main.js
22
src/main.js
|
|
@ -76,6 +76,7 @@ app.whenReady().then(() => {
|
||||||
|
|
||||||
app.on('window-all-closed', event => {
|
app.on('window-all-closed', event => {
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
|
kill('loquendoBot_backend');
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -88,6 +89,7 @@ app.on('activate', () => {
|
||||||
|
|
||||||
app.on('before-quit', () => {
|
app.on('before-quit', () => {
|
||||||
window.webContents.send('quit-event');
|
window.webContents.send('quit-event');
|
||||||
|
kill('loquendoBot_backend');
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('resize-window', (event, width, height) => {
|
ipcMain.on('resize-window', (event, width, height) => {
|
||||||
|
|
@ -134,21 +136,28 @@ async function createIniFile() {
|
||||||
POSITION_Y: 0,
|
POSITION_Y: 0,
|
||||||
WIDTH: 1024,
|
WIDTH: 1024,
|
||||||
HEIGHT: 768,
|
HEIGHT: 768,
|
||||||
LANGUAGE: 'EN',
|
LANGUAGE: 'none',
|
||||||
|
LANGUAGE_INDEX: '0',
|
||||||
PORT: 9000,
|
PORT: 9000,
|
||||||
VIEWERS_PANEL: false,
|
VIEWERS_PANEL: false,
|
||||||
LOCATION: pythonPath,
|
LOCATION: pythonPath,
|
||||||
ZOOMLEVEL: 1
|
ZOOMLEVEL: 1
|
||||||
},
|
},
|
||||||
LANGUAGE: {
|
LANGUAGE: {
|
||||||
USE_DETECTION: false
|
USE_DETECTION: false,
|
||||||
|
TRANSLATE_TO: 'none',
|
||||||
|
LANGUAGE_INDEX: '0',
|
||||||
|
BROADCAST_TRANSLATION: false,
|
||||||
|
OUTPUT_TO: false
|
||||||
},
|
},
|
||||||
TTS: {
|
TTS: {
|
||||||
USE_TTS: true,
|
USE_TTS: false,
|
||||||
PRIMARY_VOICE: '',
|
PRIMARY_VOICE: '',
|
||||||
PRIMARY_TTS_LANGUAGE: 'EN',
|
PRIMARY_TTS_LANGUAGE: 'none',
|
||||||
SECONDARY_VOICE: '',
|
SECONDARY_VOICE: '',
|
||||||
SECONDARY_TTS_LANGUAGE: 'EN'
|
SECONDARY_TTS_LANGUAGE: 'none',
|
||||||
|
PRIMARY_TTS_LANGUAGE_INDEX: 0,
|
||||||
|
SECONDARY_TTS_LANGUAGE_INDEX: 0
|
||||||
},
|
},
|
||||||
STT: {
|
STT: {
|
||||||
USE_STT: false,
|
USE_STT: false,
|
||||||
|
|
@ -158,7 +167,7 @@ async function createIniFile() {
|
||||||
LANGUAGE: 'vosk-model-small-es-0.42'
|
LANGUAGE: 'vosk-model-small-es-0.42'
|
||||||
},
|
},
|
||||||
AUDIO: {
|
AUDIO: {
|
||||||
USE_NOTIFICATION_SOUNDS: true,
|
USE_NOTIFICATION_SOUNDS: false,
|
||||||
SELECTED_NOTIFICATION_AUDIO_DEVICE: 'default',
|
SELECTED_NOTIFICATION_AUDIO_DEVICE: 'default',
|
||||||
NOTIFICATION_AUDIO_DEVICE: 0,
|
NOTIFICATION_AUDIO_DEVICE: 0,
|
||||||
NOTIFICATION_SOUND: 0,
|
NOTIFICATION_SOUND: 0,
|
||||||
|
|
@ -182,6 +191,7 @@ async function createIniFile() {
|
||||||
TWITCH: {
|
TWITCH: {
|
||||||
USE_TWITCH: false,
|
USE_TWITCH: false,
|
||||||
CHANNEL_NAME: '',
|
CHANNEL_NAME: '',
|
||||||
|
CHANNEL_USER_ID: '',
|
||||||
USERNAME: '',
|
USERNAME: '',
|
||||||
USER_ID: '',
|
USER_ID: '',
|
||||||
USER_LOGO_URL: '',
|
USER_LOGO_URL: '',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue