您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Automatically translates messages in Twitch chat to other languages.
当前为
// ==UserScript== // @name Twitch Chat Translator // @namespace MrSelenix // @version 0.4.2 // @description Automatically translates messages in Twitch chat to other languages. // @author MrSelenix // @match https://www.twitch.tv/* // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function () { 'use strict'; // Store the original messages in a map (key: message element, value: original text) const originalMessages = new Map(); let translationTextColor = GM_getValue('translationTextColor', '#808080'); let textColor, iconColor, menuColor, hoverColor, smallText; let savedWords = GM_getValue('customWords', []); let blockEmotes = GM_getValue('blockEmotes', false); let emoteRegex = new RegExp('\\n.*', 'gs'); let repeatedCharacterLimit = 4; // Darkmode Colours const darkMode_Text = '#ffffffff'; const darkMode_Icon = '#d3d3d3ff'; const darkMode_Menu = '#252530ff'; const darkMode_Hover = '#53535f7a'; const darkMode_smallText = '#b0b0b0ff'; // Lightmode Colours const lightMode_Text = '#000000ff'; const lightMode_Icon = '#595959ff'; const lightMode_Menu = '#d4d4d9ff'; const lightMode_Hover = '#d2d2d2ba'; const lightMode_smallText = '#727272'; // Function to check if the current theme is light function isLightTheme() { return document.documentElement.classList.contains('tw-root--theme-light'); } // Function to set colors based on the current theme function setColors() { if (isLightTheme()) { textColor = lightMode_Text; iconColor = lightMode_Icon; menuColor = lightMode_Menu; hoverColor = lightMode_Hover; smallText = lightMode_smallText; } else { textColor = darkMode_Text; iconColor = darkMode_Icon; menuColor = darkMode_Menu; hoverColor = darkMode_Hover; smallText = darkMode_smallText; } // Apply the colors to your UI elements if necessary console.log('Theme updated:', { textColor, iconColor, menuColor, hoverColor, smallText }); } // Initial setup of colors setColors(); function refreshButton() { const existingButton = document.getElementById('toggle-settings'); if (existingButton) { console.log("toggle-settings located... Refreshing"); existingButton.remove(); setTimeout(addButton, 150); } else { console.log("toggle-settings not located"); addButton(); } } // Function to observe changes in the <html> class attribute function observeThemeChanges() { const targetNode = document.documentElement; // Ensure this is the <html> element const config = { attributes: true, attributeFilter: ['class'] }; // Monitor 'class' changes const callback = function(mutationsList) { for (let mutation of mutationsList) { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { console.log('Detected class change on <html>: ', mutation); // Debugging log setColors(); // Call setColors whenever the class changes refreshButton(); } } }; const themeObserver = new MutationObserver(callback); // Start observing the <html> element for class attribute changes themeObserver.observe(targetNode, config); console.log('themeObserver has been set up and is observing the <html> element.'); } // Start observing theme changes observeThemeChanges(); // Function to translate text using the Google Translate API function translateText(text, destinationLanguage) { return new Promise((resolve, reject) => { const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${destinationLanguage}&dt=t&q=${encodeURIComponent(text)}`; fetch(url) .then(response => response.json()) .then(data => { if (data && data[0] && data[0][0]) { const translation = data[0].map(item => item[0]).join(''); // Collect all translation parts const detectedLanguage = data[2]; // Detected language is at index 2 // Sanitize both texts for comparison const sanitizedText = text.trim().toLowerCase(); const sanitizedTranslation = translation.trim().toLowerCase(); const translationFull = translation.replace(/(\w)\1{3,}/gi, (match, p1) => p1.repeat(`${repeatedCharacterLimit}`)); //Restrict repeated characters to a maximimum of the set limit after translating (Limit=4, "Woooooooooo" => "Woooo") // console.log(savedWords); //console.log(`Text=${text},Translation=${translationFull}`); // console.log(savedWords.includes(text)); // Only resolve if the translation is different from the original text if (text = translationFull || levenshteinDistance(`${sanitizedText}`, `${sanitizedTranslation}`) <= 3) { resolve(null); } else { resolve({ translationFull, detectedLanguage }); } } else { reject('Translation error: Invalid response format'); } }) .catch(error => reject(error)); }); } // Function to translate a message function translateMessage(messageElement, fullMessage) { // Get the selected language from GM_getValue const targetLanguage = GM_getValue('selectedLanguage', 'en'); return translateText(fullMessage, targetLanguage) .then(result => { if (result === null) { return; // No translation needed } const { translation, detectedLanguage } = result; // Check if the detected language is supported in our languageMap if (detectedLanguage in languageMap && detectedLanguage !== targetLanguage) { // Convert detected language code to full language name const fullLanguageName = languageMap[detectedLanguage]; const translationHTML = `<span style="color: ${translationTextColor}; font-weight: bold;"> (Translated from "${fullLanguageName}": ${translation})</span>`; const messageBody = messageElement.querySelector('[data-a-target="chat-line-message-body"]'); // console.log(`Message Body: ${messageBody}`); if (messageBody) { let lastTextFragment = messageBody.querySelector('span.text-fragment:last-child'); if (!lastTextFragment) { // If no span.text-fragment exists at the end, create one lastTextFragment = document.createElement('span'); lastTextFragment.className = 'text-fragment'; lastTextFragment.textContent = ''; // Empty content messageBody.appendChild(lastTextFragment); } // Append the translation lastTextFragment.insertAdjacentHTML('beforeend', translationHTML); // console.log(`Translation appended to: ${lastTextFragment}`); } } }) .catch(error => { console.error("Error translating message:", error); }); } function addButton() { // Check if the button has already been added if (document.getElementById('toggle-settings')) return; // Create a container div with a class similar to Twitch's buttons const settingsButton = document.createElement('div'); settingsButton.className = 'Layout-sc-1xcs6mc-0 WmSnIX'; // Create the button const newButton = document.createElement('button'); newButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="${iconColor}" class="bi bi-translate" viewBox="-4 -6 22 22"><path d="M4.545 6.714 4.11 8H3l1.862-5h1.284L8 8H6.833l-.435-1.286H4.545zm1.634-.736L5.5 3.956h-.049l-.679 2.022H6.18z"></path><path d="M0 2a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v3h3a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-3H2a2 2 0 0 1-2-2V2zm2-1a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H2zm7.138 9.995c.193.301.402.583.63.846-.748.575-1.673 1.001-2.768 1.292.178.217.451.635.555.867 1.125-.359 2.08-.844 2.886-1.494.777.665 1.739 1.165 2.93 1.472.133-.254.414-.673.629-.89-1.125-.253-2.057-.694-2.82-1.284.681-.747 1.222-1.651 1.621-2.757H14V8h-3v1.047h.765c-.318.844-.74 1.546-1.272 2.13a6.066 6.066 0 0 1-.415-.492 1.988 1.988 0 0 1-.94.31z"></path></svg>`; newButton.classList.add('btn', 'btn-primary'); newButton.id = 'toggle-settings'; newButton.title = "Translation Settings"; newButton.style.cssText = ` margin-right: 5px; background-color: transparent; border-style: none; padding-left: 3px; padding-right: 5px; border-radius: 4px; transition: background-color 0.3s ease, border-radius 0.3s ease; `; // Define hover effect newButton.onmouseover = function() { this.style.backgroundColor = `${hoverColor}`; }; newButton.onmouseout = function() { this.style.backgroundColor = 'transparent'; }; // Declare closeMenuOnOutsideClick at the function level let closeMenuOnOutsideClick; newButton.addEventListener('click', () => { let settingsMenu = document.getElementById('settings-menu'); if (!settingsMenu) { settingsMenu = document.createElement('div'); settingsMenu.id = 'settings-menu'; settingsMenu.style.cssText = ` position: absolute; width: 280px; height: 350px; background-color: ${menuColor}; display: none; z-index: 1000; bottom: 40px; right: 70px; border-radius: 6px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); padding: 10px; color: ${textColor}; font-size: 14px; cursor: default; `; // Add the title to the settings menu const title = document.createElement('h2'); title.textContent = "Translation Settings"; title.style.cssText = ` margin: 0 0 10px 0; font-size: 20px; text-align: center; color: ${textColor}; `; settingsMenu.appendChild(title); // Add a colored line for contrast const divider = document.createElement('hr'); divider.style.cssText = ` width: 100%; height: 2px; background-color: #55556e; border: none; margin: 5px 0; `; settingsMenu.appendChild(divider); // Add checkbox for enabling translations const checkboxContainer = document.createElement('div'); checkboxContainer.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin: 5px 0; `; const checkboxLabel = document.createElement('label'); checkboxLabel.innerHTML = ` <span style="font-size: 18px; font-family: 'Helvetica', sans-serif;">Enable Translations</span><br> <span style="font-size: 12px; font-family: 'Helvetica', sans-serif; color: ${smallText};">(New Messages Only)</span> `; checkboxLabel.style.marginRight = '10px'; // Space between label and checkbox const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; // Retrieve the saved state from GM_getValue or default to true checkbox.checked = GM_getValue('translationsEnabled', true); checkbox.style.cursor = 'pointer'; // Save the state when it changes checkbox.addEventListener('change', function() { GM_setValue('translationsEnabled', this.checked); let translationsEnabled = GM_getValue('translationsEnabled'); //console.log(`TranslationsEnabled=${translationsEnabled}`); //console.log("---"); }); //3rd Party Emote Blocker const emoteCheckboxContainer = document.createElement('div'); emoteCheckboxContainer.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin: 5px 0; `; const emoteCheckboxLabel = document.createElement('label'); emoteCheckboxLabel.innerHTML = ` <span style="font-size: 14px; font-family: 'Helvetica', sans-serif;">Hide 3rd-party Emotes from translations</span><br> `; emoteCheckboxLabel.style.marginRight = '10px'; // Space between label and checkbox const emoteCheckbox = document.createElement('input'); emoteCheckbox.type = 'checkbox'; // Retrieve the saved state from GM_getValue or default to false emoteCheckbox.checked = GM_getValue('blockEmotes', false); emoteCheckbox.style.cursor = 'pointer'; // Save the state when it changes emoteCheckbox.addEventListener('change', function() { GM_setValue('blockEmotes', this.checked); let blockEmotes = GM_getValue('blockEmotes'); console.log(`blockEmotes=${blockEmotes}`); console.log("---"); }); checkboxContainer.appendChild(checkboxLabel); checkboxContainer.appendChild(checkbox); settingsMenu.appendChild(checkboxContainer); emoteCheckboxContainer.appendChild(emoteCheckboxLabel); emoteCheckboxContainer.appendChild(emoteCheckbox); settingsMenu.appendChild(emoteCheckboxContainer); // Add "Translate to..." label and dropdown const translationTargetContainer = document.createElement('div'); translationTargetContainer.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin: 10px 0; `; const translationTargetLabel = document.createElement('label'); translationTargetLabel.textContent = "Translate to..."; translationTargetLabel.style.marginRight = '10px'; // Space between label and dropdown const languageSelect = document.createElement('select'); languageSelect.style.cssText = ` width: 150px; cursor: pointer; background-color: #ffffff; color: black; border: 1px solid #55556e; padding: 2px; border-radius: 3px; `; // Populate dropdown with languages Object.entries(languageMap).forEach(([code, name]) => { const option = document.createElement('option'); option.value = code; option.textContent = name; option.style.cursor = 'pointer'; languageSelect.appendChild(option); }); // Set the selected language based on saved value or default to English languageSelect.value = GM_getValue('selectedLanguage', 'en'); // Save the selected language when it changes languageSelect.addEventListener('change', function() { GM_setValue('selectedLanguage', this.value); }); translationTargetContainer.appendChild(translationTargetLabel); translationTargetContainer.appendChild(languageSelect); settingsMenu.appendChild(translationTargetContainer); // Add "Text Colour" label and input const textColorContainer = document.createElement('div'); textColorContainer.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin: 10px 0; `; const textColorLabel = document.createElement('label'); textColorLabel.innerHTML = ` <span style="font-size: 14px; font-family: 'Helvetica', sans-serif;">Text Color</span><br> <span style="font-size: 11px; font-family: 'Helvetica', sans-serif; color: ${smallText};">Default #808080</span> `; textColorLabel.style.marginRight = '10px'; // Space between label and input const textColorInput = document.createElement('input'); textColorInput.type = 'text'; textColorInput.placeholder = 'HEX color'; textColorInput.style.cssText = ` width: 80px; // Adjust width as needed color: black; background-color: #ffffff; border: 1px solid #55556e; border-radius: 4px; padding: 5px; `; // Create the color picker input, now directly in place of colorPreview const colorPicker = document.createElement('input'); colorPicker.type = 'color'; colorPicker.style.cssText = ` width: 22px; height: 20px; margin-right: 5px; cursor: pointer; `; colorPicker.value = GM_getValue('translationTextColor', '#808080'); // Set initial value from saved state or default to #808080 (gray) const initialColor = GM_getValue('translationTextColor', '#808080'); textColorInput.value = initialColor; colorPicker.value = initialColor; // Set color picker to match text input // Function to update color when changed function updateColor(color) { textColorInput.value = color; translationTextColor = color; GM_setValue('translationTextColor', color); } // Event listener for the text input textColorInput.addEventListener('input', function() { let hexColor = this.value; hexColor = hexColor.replace('#', ''); if (/^[0-9A-F]{6}$/i.test(hexColor)) { const fullHex = '#' + hexColor; this.style.borderColor = '#55556e'; updateColor(fullHex); colorPicker.value = fullHex; // Sync with color picker } else { this.style.borderColor = 'red'; } }); // Event listener for "Enter" key to lose focus on the text box textColorInput.addEventListener('keydown', function(event) { if (event.key === 'Enter') { this.blur(); // Remove focus from the text input } }); // Event listener for color picker change colorPicker.addEventListener('change', function() { updateColor(this.value); }); // Container for both input and color picker const inputWithPreview = document.createElement('div'); inputWithPreview.style.cssText = ` display: flex; align-items: center; `; // Add new container for text entry field and label const textEntryContainer = document.createElement('div'); textEntryContainer.style.cssText = ` display: flex; flex-direction: column; margin: 10px 0; `; const textEntryLabel = document.createElement('label'); textEntryLabel.innerHTML = ` <span style="font-size: 14px; font-family: 'Helvetica', sans-serif;">Blocked Words/Phrases</span><br> <span style="font-size: 12px; font-family: 'Helvetica', sans-serif; color: ${smallText};">Remove words/emotes from messages before translating. Not case-Sensitive</span> `; textEntryLabel.style.marginBottom = '5px'; // Space between label and input // Create the text entry input field const textEntryInput = document.createElement('input'); textEntryInput.type = 'text'; textEntryInput.placeholder = 'OMEGALUL, Sadge, NOPERS'; textEntryInput.style.cssText = ` width: 100%; color: black; background-color: #ffffff; border: 1px solid #55556e; border-radius: 4px; padding: 5px; `; // Retrieve and set the saved value for this text field (if any), ensure it's an array const savedWords = GM_getValue('customWords', []); textEntryInput.value = savedWords.join(', '); // Convert the array to a string if there are saved words // Event listener to update the saved array in GM_setValue whenever the contents change textEntryInput.addEventListener('input', function() { // Split the input into an array by commas and remove any extra spaces const wordsArray = this.value.split(',').map(word => word.trim()).filter(word => word.length > 0); // Save the array in GM_setValue GM_setValue('customWords', wordsArray); }); textEntryInput.addEventListener('keydown', function(event) { if (event.keyCode === 32) { // Prevent the event from bubbling up but still allow the space to be typed event.stopPropagation(); // We need to manually insert the space since stopPropagation might interfere const start = this.selectionStart; const end = this.selectionEnd; this.value = this.value.substring(0, start) + ' ' + this.value.substring(end); this.selectionStart = this.selectionEnd = start + 1; // Prevent default only to stop any potential navigation or other default behavior event.preventDefault(); } }); textEntryInput.addEventListener('keydown', function(event) { if (event.key === 'Enter') { this.blur(); // Remove focus from the text input } }); textEntryContainer.appendChild(textEntryLabel); textEntryContainer.appendChild(textEntryInput); inputWithPreview.appendChild(colorPicker); // Color picker now in place of the preview inputWithPreview.appendChild(textColorInput); textColorContainer.appendChild(textColorLabel); textColorContainer.appendChild(inputWithPreview); settingsMenu.appendChild(textColorContainer); settingsMenu.appendChild(textEntryContainer); newButton.appendChild(settingsMenu); // Define closeMenuOnOutsideClick here with access to settingsMenu closeMenuOnOutsideClick = function(event) { if (!newButton.contains(event.target) && !settingsMenu.contains(event.target)) { settingsMenu.style.display = 'none'; document.removeEventListener('click', closeMenuOnOutsideClick); } }; settingsMenu.addEventListener('click', function(e) { e.stopPropagation(); // Prevent click events from reaching document }); } // Toggle the display of the settings menu settingsMenu.style.display = settingsMenu.style.display === 'none' ? 'block' : 'none'; // Manage the event listener based on visibility if (settingsMenu.style.display === 'block') { document.addEventListener('click', closeMenuOnOutsideClick); } else { document.removeEventListener('click', closeMenuOnOutsideClick); } }); // Append the button to the new container settingsButton.appendChild(newButton); // Find the specific container div const twitchButtonContainer = document.querySelector('.Layout-sc-1xcs6mc-0.kEPLoI'); //Find general button container below chat if (twitchButtonContainer) { //If it exists... const chatButton = twitchButtonContainer.querySelector('.Layout-sc-1xcs6mc-0.jOVwMQ'); //Find sendChat button container if (chatButton) { //If it exists... twitchButtonContainer.insertBefore(settingsButton, chatButton); console.log("Located the chat button. Appending Translation Settings before it"); } else { twitchButtonContainer.appendChild(settingsButton); console.log("Did not locate chat button, appended Translation Settings to main button container instead"); } } } // Variable to store the MutationObserver reference let observer = null; let modViewObserver = null; function updateWords() { savedWords = GM_getValue('customWords', []); } function updateBlockedEmotes() { blockEmotes = GM_getValue('blockEmotes'); } function startTranslation() { const chatContainer = document.querySelector('[data-test-selector="chat-scrollable-area__message-container"]'); if (chatContainer) { // Translate the initial set of chat messages, but now it checks if translations are enabled observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(newNode => { if (newNode.nodeType === Node.ELEMENT_NODE) { const newMessageElement = newNode.querySelector('span.text-fragment'); if (newMessageElement) { // Collect all text fragments for the message const messageContainer = newNode.closest('[data-a-target="chat-line-message-body"]') || newNode; if (messageContainer && !originalMessages.has(messageContainer)) { const textFragments = Array.from(messageContainer.querySelectorAll('span.text-fragment')); if (textFragments.length > 0) { const sanitizedArray = textFragments.map(fragment => { updateBlockedEmotes(); if (blockEmotes){ let sanitizedText = fragment.textContent.replace(/[a-zA-Z]+\n.*/gs, '').trim(); // Removes all emote data from the end of the text-fragment return sanitizedText; } else { let sanitizedText = fragment.textContent.replace(/\n.*/gs, '').trim(); // Removes all emote data from the end of the text-fragment return sanitizedText; } }); let halfMessage = sanitizedArray.join(' '); // Store the original message text originalMessages.set(messageContainer, halfMessage); updateWords(); //remove savedWords from final message before translations savedWords.forEach(str => { if (str.trim() !== '') { let regex = new RegExp(str, 'gi'); halfMessage = halfMessage.replace(regex, ''); } }); // Check if translations are enabled before translating new messages if (GM_getValue('translationsEnabled', true)) { updateWords(); let fullMessage = halfMessage.replace(/(\w)\1{3,}/gi, (match, p1) => p1.repeat(4)); //Restrict repeated characters to a maximimum of the set limit before translating (Limit=4, "Woooooooooo" => "Woooo") //console.log(`Full Message: ${fullMessage}`); translateMessage(messageContainer, fullMessage); } } } } } }); }); }); const observerConfig = { childList: true, subtree: true }; observer.observe(chatContainer, observerConfig); } else { setTimeout(startTranslation, 500); // Retry if chat container isn't found yet } } function levenshteinDistance(str1, str2) { const len1 = str1.length; const len2 = str2.length; // Create a 2D array (matrix) with dimensions (len1+1) x (len2+1) const dp = Array(len1 + 1).fill(null).map(() => Array(len2 + 1).fill(0)); // Initialize the matrix for (let i = 0; i <= len1; i++) { dp[i][0] = i; // Distance of any first string to an empty second string } for (let j = 0; j <= len2; j++) { dp[0][j] = j; // Distance of any second string to an empty first string } // Compute the Levenshtein distance for (let i = 1; i <= len1; i++) { for (let j = 1; j <= len2; j++) { const cost = str1[i - 1] === str2[j - 1] ? 0 : 1; // No cost if characters are the same dp[i][j] = Math.min( dp[i - 1][j] + 1, // Deletion dp[i][j - 1] + 1, // Insertion dp[i - 1][j - 1] + cost // Substitution ); } } // The value in the bottom-right corner is the Levenshtein distance return dp[len1][len2]; } function setupModViewObserver() { modViewObserver = new MutationObserver(() => { const chatContainer = document.querySelector('[data-test-selector="chat-scrollable-area__message-container"]'); if (chatContainer && !observer) { startTranslation(); } // Also add the button when Mod View changes setTimeout(addButton, 150); }); modViewObserver.observe(document.body, { childList: true, subtree: true }); } window.addEventListener('load', () => { setupModViewObserver(); startTranslation(); setTimeout(addButton, 150); // Add button on initial load }); })(); //Language Map for all supported languages const languageMap = { "af": "Afrikaans", "sq": "Albanian", "ar": "Arabic", "hy": "Armenian", "az": "Azerbaijani", "be": "Belarusian", "bn": "Bengali", "bs": "Bosnian", "bg": "Bulgarian", "ca": "Catalan", "zh-cn": "Chinese (Simplified)", "zh-tw": "Chinese (Traditional)", "hr": "Croatian", "cs": "Czech", "da": "Danish", "nl": "Dutch", "en": "English", "et": "Estonian", "tl": "Filipino", "fi": "Finnish", "fr": "French", "ka": "Georgian", "de": "German", "el": "Greek", "ht": "Haitian Creole", "haw": "Hawaiian", "iw": "Hebrew", "hi": "Hindi", "hu": "Hungarian", "is": "Icelandic", "id": "Indonesian", "ga": "Irish", "it": "Italian", "ja": "Japanese", "jw": "Javanese", "ko": "Korean", "la": "Latin", "lb": "Luxembourgish", "lv": "Latvian", "lt": "Lithuanian", "mk": "Macedonian", "mt": "Maltese", "mn": "Mongolian", "ne": "Nepali", "no": "Norwegian", "fa": "Persian", "pl": "Polish", "pt": "Portuguese", "pa": "Punjabi", "ro": "Romanian", "ru": "Russian", "sm": "Samoan", "sr": "Serbian", "sk": "Slovak", "sl": "Slovenian", "es": "Spanish", "sv": "Swedish", "th": "Thai", "tr": "Turkish", "uk": "Ukrainian", "ur": "Urdu", "uz": "Uzbek", "vi": "Vietnamese", "cy": "Welsh", "yi": "Yiddish", "zu": "Zulu" // This list does not include all languages supported by GoogleAPIs and might include languages that are no longer supported. };