#i used google translate for the arabic sorry for any errors
Explore tagged Tumblr posts
Note
Hey! I saw that you're taking Moon Knight requests! Could you please write Jake Lockley x reader where both of them speaking broken English and most of their conversations are just bickering in their native languages?
Just imagine the fluff where they get together and teach each other the words of love in their languages through the trouble of language barrier 😭🥺
Language ☽ Jake Lockley × Arabian!GN!Reader
i only speak english and spanish, so im very sorry if the arabic is inaccurate. i wanted to try something different that wasn't french or italian. also, because im half mexican and half puerto rican, my spanish is different. words are different compared to those who are from South America or Central America
Warnings: roughly translated spanish to english, translated arabic (NOT GOOGLE TRANSLATE), mentions of marc & steven
*if you speak Arabic, PLEASE correct me if there's any errors! im open for corrections!*
Anyone who didn't understand your relationship would think you and your boyfriend were speaking gibberish.
However, you both knew bickering in your native tongues was very much normal, but it was mostly to yourselves since you didn't know Spanish and he didn't know Arabic. Today was different as you both were bickering about the damn English language. As easy as people made it seem, it was damn well difficult. How the heck was there words that sounded the same, but were both spelled differently and meant different things?
"Ine fakt la moana leh." (It just makes no sense.)
"No sé cómo la gente puede hablar en esa maldita idioma sin confundirse, de verdad." (I don't know how people can speak in that damn language without confusing themselves, honestly.)
"Lediham al-jaraa lekhbary baltehadth ballga al-inglizia?" (They have the audacity to tell me to speak English?)
Now, there stood a confused Jake, wondering if you both were arguing about the same thing or was it something completely different? "What you say?" You looked over at him, confused. "What?"
Honestly, you both were confused as to how you managed to be in a relationship for a few months without picking up some of each others language. Well, Jake only understood very little thanks to Steven and even Marc, thanks to his time as a mercenary. However, it still wasn't enough, but if he was being honest, he wanted to learn more about your language. You only ever picked up on what he meant when he called you "amor" since that's his go-to nickname for you. "Teach me?" You raised an eyebrow at him. "Teach you what?" He smiled. "Árabe."
"You sure?" He nodded and you smiled. "I can do it I think. You just have to be good listener." He grinned. "Well, I call you kalabi which is 'my heart.'"
"So like 'mi corazón' for me?" You tilted your head. "That was what you mean?" He chuckled. "Yes, amor." You felt your cheeks heat up. It always got to you when he called you that. You assumed it was because it was in his native tongue and not in English as you were normally used to. "We say habibi." He held your hand. "Can you teach me to say te amo?" You raised an eyebrow, until you realized what he said. "Anna asada ahbak." (I love you too.)
~~~~
like i said, please feel free to correct me for any incorrect translations in Arabic! i appreciate it. :)
my requests are still open for the moon boys! :D
#jake lockley x reader#jake lockley#jake lockley x you#jake lockley x y/n#moon knight x reader#marvel#marvel universe#mcu#jake lockely x reader#asks#ask#request
81 notes
·
View notes
Text
Fic, You Don't Know Everything
@ts-sideblog requested a fic about Logic getting mad when the others know things that he doesn’t. Let’s see where this goes.
Forgive me for using google translate, by the way. English is the only language I’m native level in and I’m only at ILR level 1 with high German.
Tip Jar
Fic under the cut. 2,093 words. I can’t think of any warnings. Let me know if I should add some. I’m bad with warnings but really feel I should get better because I myself need them for certain things. Please let me know if I should add a warning.
Abstract: Being basically one dimensional aspects of a three dimensional personality, the sides tend to be slightly territorial. Especially Logan, who can’t seem to comprehend anyone stepping out of their assigned role, even though he does so at times.
Logic was checking through his flashcards.
“German? Der Prinz ist dumm. Yes,”
He flipped to the next one. “Arabic. Al'amir ghabi. Yes,”
“Hey Logan! What’re you up to?”
Logic did not look up from his cards. “Not now, Morality. It is none of your concern,”
Patton watched him go down the hall. Logic didn’t notice the stupid Prince approaching.
“Dutch. Die prins is dom,”
“That would be Afrikaans, actually. There is a difference,”
Logic looked up to see if it was who he thought it was. Unfortunately, it was.
“Oh. Hello Roman,”
The silence between them became thick enough to cut with a knife. Logic’s eyes looked everywhere but the regal figure that was blocking his way. Roman’s arms were crossed and he looked at Logic with a cold stare that could drill through rock. After a solid half minute of this uncomfortable arrangement, Logan cleared his throat.
“So, you speak Afrikaans as well?”
“I am the imagination, Logan. I can speak any language I want, even if Thomas does not speak it,”
Roman pushed Logic against the wall and walked past him. Suddenly Logic understood how Thomas had played so many villains in plays. He then willed a trash can into existence and dumped the flash cards into it.
Any language? He could not believe this. He decided to go to a library within the mind space. That was where all the facts and practical ideas were kept. That was his space.
He decided to take refuge in the fiction library. Perhaps he could revisit the plot of an old epic or mystery novel. When he got there however, he found he was not alone.
“Hey,” said Anxiety, hanging upside down off a table reading from a book of works by Edgar Allan Poe.
Logic was slightly taken aback. “Anxiety, why are you here? This is my space. And… I have no memory of having ever read that,”
“You and Princey aren’t the only ones around here that like poetry, pocket protector. Besides, I like this guy. There’s no hope in anything. Everything is creepy. He knows the truth,”
“We have read the works of Edgar Allan Poe?” Logic asked, still not believing it.
“You don’t have all the information, Logan.”
Logan started talking quickly. “Anxiety, that is complete and utter nonsense. I am the mind. I am where the information is stored. How could any of you possibly have any information that I do not?”
Anxiety turned himself around until he sat upright. and then swayed slightly as the blood rushed out of his head.
“Oh, wow. Okay,” he finally righted himself.
“How long were you hanging upside down?” Logan asked, slightly concerned.
“Too long. Here, you want answers? Read this page. Even a literal idiot like you can figure it out. I’ve got somewhere to be,”
Anxiety shoved the open book into Logic’s chest, waited for him to get a grip on on it, and then sunk out of the library.
Logic looked down at the page Anxiety had left open. It had parts of two long poems on it and one short one. Sonnet-To Science.
Roman was walking angrily through a forest he had imagined, hacking at bushes and trees angrily with his katana. Today had not been a good day. He was experiencing a horrid case of writer’s block, he was generally feeling distracted, and then he had heard wind of Logic’s little project from Thomas and it had turned out to be true. On an ordinary day he might not be so bothered by it, but right now he was fuming.
Prince was just about to attempt to cut off a branch with one hack, when Anxiety popped out in front of him.
“Hey, Princey,” Anxiety said, an evil smirk on his face.
Roman screamed a surprisingly high pitched scream and fell over. The forest blinked away and they were standing in his room. A clean bright space with a large double bed and rich decorations that could convince you that you were in pre revolutionary France.
“What in the name of Hades’ helmet are you doing? I nearly killed you!” Roman cried out, clutching at his heart.
“Ah, cut it with the dramatics, Hercules. I’m here because you’re pissed. I thrive under these conditions. So much inner turmoil,”
Roman stood up. “So you are here to make it worse. This is why I do not like you. Well, it is among many reasons why I do not like you,”
“Oh, really? Well if you can spend two minutes of your stupid, ‘happy ever after’ existence being serious, I’ve got an idea,”
Roman looked confused. “What do you mean?”
“I mean I’m bored, you’re pissed, let’s mess with Logan. If we can disguise it properly, I’ll bet we can get dad in on it too. He’s pretty gullible,”
Roman sheathed his sword.
“That would not be a noble thing to do,” He said simply.
Anxiety buttered his words with sweet venom. “C’mon, man. I know you fantasize about being the villain too. It can’t always be me. When Thomas plays the bad guy we’re both right up on stage with him. You know it’s true,”
Roman narrowed his eyes and gave Anxiety a side glance. “What exactly did you have in mind?”
Logic’s mind was racing. He jumped from one book to the next, picking up facts here and there. How could any of them know something he didn’t? It made no sense. Nothing around here made sense. If Thomas knew something, then Logan must know it too, right?
He thought back to when Morality had corrected him a few months before. Was he making mistakes like that all the time? Did he just say wrong words left and right? He decided to take a breather. It was almost dinner time anyways.
Anxiety smirked and sunk down away from the library and appeared back in Roman’s room.
“So?” Roman asked, “did you find anything?”
“Oh yeah. He’s worried that he’s using malapropisms. He’s also upset that we know things that he doesn’t. Now if we can…”
“I’m sorry, what?”
“Huh?”
“That word. What was that word?”
Anxiety looked confused. “Malapropism?”
“Yes, that one,”
“Oh man. Maybe this was a bad idea. Yeah, this was a bad idea. A malapropism is the misuse of big complicated words, you dingus,”
The prince looked offended. “Did… Did you just make fun of the size of my vocabulary and then call me a dingus?”
“I’ve said before that creativity is not my department. Now are you ready to hit the books or what?”
The next day Logic found the prince sitting on an armchair reading a book of myths.
“Ah, Prince. I was just looking for you. We have… What is that book?”
“Oh, this? These are the Norse myths we have read. Fascinating tales. Ah, stories of death and blood and giants. True poetry,”
“I do not believe we have read that many,” Logic said
“That is where you are wrong. We have read all of them. Every surviving story we could get our hands on. Did you think I only read fairy tales, Logan?”
Logic had forgotten what he was going to ask the prince. He angrily walked away without a word.
Later, he found himself in the kitchen in search of a snack. Patton was there baking a cake.
“Hey there, Logan! What are you up to?” Patton asked, happily clapping his hands together to remove some of the flour.
Logic saw that some flour had gotten on his black shirt and began to hit it in an attempt to get it off. “Would you refrain from getting flour all over the kitchen? And why are you baking anyways? Wheat products are incredibly unhealthy,”
Morality smiled and started mixing the batter with a spoon. “Oh, I doubt it’ll kill ya teach. People have been using flour for over eight thousand years and we’re still all here, right?”
Logic took a double take. “Eight thousand years? Are you certain? I do not remember learning that,”
“Well sure. I’ve got some fun facts up my sleeve too ya know,”
He gave Logic a playful punch on the shoulder with his flour covered hand, leaving a white smear behind. Logic suddenly didn’t feel hungry anymore.
Later, Logic was writing down some of his ideas for the newest video when Anxiety popped up.
“You’re worried about something,” Anxiety said.
“And what, pray tell, am I so worried about?” Logic asked as he jotted down some more things on his notepad.
“Circumlocution and malapropisms,” Anxiety said before disappearing without any explaination.
Logic looked up just after Anxiety teleported away.
“What does that mean?” He said, a little too loudly.
Logan stormed into the reference library and opened a dictionary. Roman and Patton had been bringing up stories and facts he had never heard of all day, and now Anxiety had used two words that he did not know the definitions of.
“Circumlocution. The use of many words where fewer would do. When did we learn this?”
He flipped to another page. “Malaprop. The mistaken use of a word in place of a similar-sounding one. Well, I suppose Anxiety was right, but where did he learn those words?”
Roman was listening at a vent with a recording device he had imagined. Morality was bouncing on the balls of his feet. Anxiety was leaning against the wall, hood up, cleaning his nails.
Soon, morality couldn’t hold his excitement any longer. “What’s happening in there? Can you tell?”
Roman listened carefully. “He is ranting to himself. Anxiety, just how confused is he?”
Anxiety started peeling off a bit of nail that had gotten too long. “Imagine rehearsing for ten weeks thinking you’re going to be playing an Antipholus in the comedy of errors only to find you’ve actually been cast as Troy in a stage play of high school musical the day before final dress rehearsal,”
“Very confused would have done,” Said Roman. “How did we pull that off so fast?”
“Do you know what I am?” Asked Anxiety. “I am literally fear. Seriously, do I have to tell you how to add two plus three? I know what I’m doing, Princey,”
“And he is doing a great job!” Patton exclaimed.
“He is coming this way!” Roman announced.
They all vanished before Logan turned the corner.
“Something is happening. Think. Deductive reasoning. This is what you were made for,” Logic said to himself.
Anxiety heard a knock on his door. Yeah, he knew this was a bad idea. He decided to see what would happen if he just didn’t answer.
Logic threw open the door anyways and stepped inside where he immediately tripped and fell over a pile of black skinny jeans and t-shirts.
“Do you have any kind of organization system?” Logan said, angrily getting up.
“What? Not an emotionless robot today, Spock?” Anxiety asked.
Logan gritted his teeth. An angry teacher with messy hair and a look that deadly in his eyes would probably have at least slightly disturbed someone else. However, given how many problems Anxiety caused, he had seen almost every kind of reaction from the others so this did not surprise him one bit.
“Why are you all spitting information at me? That is my job!”
Anxiety leaned against his headboard and put his hands behind his head.
“It’s quite simple, Sherlock. You have been unbearable lately, and you refuse to accept that there are facts, words, and stories that don’t have to do with you. So, with a bit of reading, and a little help from me, Princey and dad gave you one of the most frustrating days of your life,”
Logic took a breath and straightened out his hair.
“I still think you are all trying to do my job. You are all inconsequential. You will see! I know more than all of you combined,”
Logic went off to one of the libraries. Anxiety smiled and murmured to himself after he left. “Never said you didn’t, Mr. Know-it-all,”
In case you were curious about that poem Anxiety shoved at Logic:
Sonnet-To Science, By Edgar Allan Poe
Science! true daughter of Old Time thou art! Who alterest all things with thy peering eyes. Why preyest thou thus upon the poet’s heart, Vulture, whose wings are dull realities? How should he love thee? or how deem thee wise, Who wouldst not leave him in his wandering To seek for treasure in the jewelled skies, Albeit he soared with an undaunted wing? Hast thou not dragged Diana from her car, And driven the Hamadryad from the wood To seek a shelter in some happier star? Hast thou not torn the Naiad from her flood, The Elfin from the green grass, and from me The summer dream beneath the tamarind tree?
#roman wrote a thing#I hung upside down for a few minutes#so I could remember what happens when you get up again#i was so dizzy#i also know some things about flour now.#my search history is so weird#sanders sides#thomas sanders#thatsthat24#anxiety sanders#logic sanders#logan sanders#Morality Sanders#Patton Sanders#roman sanders#prince sanders#platonic prinxiety#fanfiction#fan fiction#fanfic#fan fic#mircheckthisout
143 notes
·
View notes
Text
Using the Web Speech API for Multilingual Translations
Since the early days of science fiction, we have fantasized about machines that talk to us. Today it is commonplace. Even so, the technology for making websites talk is still pretty new.
We can make our pages on the web talk using the SpeechSynthesis part of the Web Speech API. This is still considered an experimental technology but it has great support in the latest versions of Chrome, Safari, and Firefox.
The fun part for me is using this technology with foreign languages. For that, Mac OSX and most Windows installations have great support on all browsers. Chrome loads a set of voices remotely, so if your operating system does not have international voices installed, just use Chrome. We’re going to walk through a three-step process to create a page that speaks the same text in multiple languages. Some of the basic code is derived from documentation found here but the final product adds some fun features and can be viewed at my Polyglot CodePen here.
Screen shot of the completed Polyglot app with a menu of languages.
Step 1: Start Simple
Let’s create a basic page with a <textarea> for the text we want the page to speak and include a button to click to trigger the speech.
<div id="wrapper"> <h1>Simple Text To Speech</h1> <p id="warning">Sorry, your browser does not support the Web Speech API.</p> <textarea id="txtFld">I love the sound of my computer-generated voice.</textarea> <label for="txtFld">Type text above. Then click the Speak button.</label> <div> <button type="button" id="speakBtn">Speak</button> <br> <p>Note: For best results on a Mac, use the latest version of Chrome, Safari, or FireFox. On Windows, use Chrome.</p> </div> </div>
The paragraph with ID warning will be shown only if the JavaScript detects no support for the Web Speech API. Also, note the ID values for the textarea and the button as we will use those in our JavaScript.
Feel free to style the HTML any way you’d like. You’re also free to work off the demo I created:
See the Pen Text-To-Speech Part 1 by Steven Estrella (@sgestrella) on CodePen.
Adding a style rule for the disabled state of the button is a good idea to avoid confusion for the few people who still use incompatible browsers, like the now-quaint Internet Explorer. Also, let’s use a style rule to hide the warning by default so we can control when it’s actually needed.
button:disabled { cursor: not-allowed; opacity: 0.3; } #warning { color: red; display: none; font-size: 1.4rem; }
Now on to the JavaScript! First, we add two variables to serve as references to the "Speak" button that triggers the speech and to the <textarea> element. An event listener at the bottom of the code tells the document to wait until the DOM elements load before calling the init() function. I used a handy utility function I call "qs" that is defined at the bottom of the code. It is a shortcut alternative to document.querySelector and it selects whatever selector value I pass to it and returns an object reference. Then we’ll add an event listener to the speakBtn object to make the button call the talk() function.
The talk() function creates a new instance of the SpeechSynthesisUtterance object that is part of the Web Speech API. It adds the text from the <textarea>(using ID txtFld) to the text property. Then the utterance is passed to the speechSynthesis method of the window object and we hear the spoken text. The specific voice you hear will vary by browser and operating system. On my Mac, for example, my default language is set to American English and the default voice for English is Alex. In Step 2, we will add code to create a menu to help the user choose voices for all available languages.
let speakBtn, txtFld; function init() { speakBtn = qs("#speakBtn"); txtFld = qs("#txtFld"); speakBtn.addEventListener("click", talk, false); if (!window.speechSynthesis) { speakBtn.disabled = true; qs("#warning").style.display = "block"; } } function talk() { let u = new SpeechSynthesisUtterance(); u.text = txtFld.value; speechSynthesis.speak(u); } // Reusable utility functions function qs(selectorText) { // Saves lots of typing for those who eschew jQuery return document.querySelector(selectorText); } document.addEventListener('DOMContentLoaded', function (e) { try {init();} catch (error) { console.log("Data didn't load", error); } });
Step 2: A Menu of International Voices
If we want to use anything other than the default language and speaking voice, we will have to add a bit more code. So that’s what we’re going tackle next.
We’re going to add a select element to hold the menu of voice options:
<h1>Multilingual Text To Speech</h1> <div class="uiunit"> <label for="speakerMenu">Voice: </label> <select id="speakerMenu"></select> speaks <span id="language">English.</span> <!-- etc. --> </div>
Before we create the code to populate the menu options, we should take care of the code that will help us connect language codes to their corresponding names. Each language is identified by a two-letter code such as "en" for English or "es" for Español (Spanish). We will take a simple list of these codes and their corresponding languages and make an array of objects of the form: {"code": "pt", "name": "Portuguese"}. Then we’ll need a utility function to help us search an array of objects for the value of a given property. We will use it in a few minutes to quickly find the language name that matches the language code of the selected voice. Copy the code below so that the two functions are just above and just below the // Generic Utility Functions comment.
function getLanguageTags() { let langs = ["ar-Arabic","cs-Czech","da-Danish","de-German","el-Greek","en-English","eo-Esperanto","es-Spanish","et-Estonian","fi-Finnish","fr-French","he-Hebrew","hi-Hindi","hu-Hungarian","id-Indonesian","it-Italian","ja-Japanese","ko-Korean","la-Latin","lt-Lithuanian","lv-Latvian","nb-Norwegian Bokmal","nl-Dutch","nn-Norwegian Nynorsk","no-Norwegian","pl-Polish","pt-Portuguese","ro-Romanian","ru-Russian","sk-Slovak","sl-Slovenian","sq-Albanian","sr-Serbian","sv-Swedish","th-Thai","tr-Turkish","zh-Chinese"]; let langobjects = []; for (let i=0;i<langs.length;i++) { let langparts = langs[i].split("-"); langobjects.push({"code":langparts[0],"name":langparts[1]}); } return langobjects; } // Generic Utility Functions function searchObjects(array, prop, term, casesensitive = false) { // Searches an array of objects for a given term in a given property // Returns an array of only those objects that test positive let regex = new RegExp(term, casesensitive ? "" : "i"); let newArrayOfObjects = array.filter(obj => regex.test(obj[prop])); return newArrayOfObjects; }
Now we can build out the options for the select element using JavaScript. We need to declare variables at the top of our JavaScript to hold references to the #speakerMenu select element, the #language span element, the array of synthesized voices (allVoices), an array of codes to identify the languages (langtags), and a place to keep track of the currently selected voice (voiceIndex). Add those just after the two variable declarations we created in Step 1.
let speakBtn, txtFld, speakerMenu, language, allVoices, langtags; let voiceIndex = 0;
The updated init() function sets some additional references to the #speakerMenu and the #language span and places all the language codes into an array of objects called langtags. The feature detection part of the code changes here, too. If the Web Speech API is supported, the setUpVoices() function is called. Also, for Chrome, we have to listen for changes to the loaded voices and repeat the setup when needed. Chrome polls the available voices every time you switch between one of its remote voices (the ones listed with the Google prefix while you are in Chrome) and all the other voices which are stored locally in the user’s operating system.
function init() { speakBtn = qs("#speakBtn"); txtFld = qs("#txtFld"); speakerMenu = qs("#speakerMenu"); language = qs("#language"); langtags = getLanguageTags(); speakBtn.addEventListener("click", talk, false); speakerMenu.addEventListener("change", selectSpeaker, false); if (window.speechSynthesis) { if (speechSynthesis.onvoiceschanged !== undefined) { // Chrome gets the voices asynchronously so this is needed speechSynthesis.onvoiceschanged = setUpVoices; } setUpVoices(); // For all the other browsers } else{ speakBtn.disabled = true; speakerMenu.disabled = true; qs("#warning").style.display = "block"; } }
The setUpVoices() function gets an array of what are called SpeechSynthesisVoice objects by calling the getVoices() method of the speechSynthesis object. This is done in our code using the getAllVoices() function. Unfortunately, I have found that the speechSynthesis.getVoices() method sometimes returns duplicates in the list, so I devoted nine lines of code to eliminate the those. Finally, at the end of getAllVoices(), I added a unique identifier number to each of the SpeechSynthesisVoice objects. That will help us in Step 3 when we need to filter the list of voices to only show voices for a given language. When complete, the allVoices array will contain objects that look like the ones below. Each object has id, voiceURI, name, and lang attributes. The localService attribute indicates whether the code for the voice is stored on the user’s computer or remotely on Google’s servers. Notice the lang attribute. The value consists of a two-letter language code (e.g. "es" for Spanish) followed by a dash and a region code (e.g. "MX" for Mexico). This identifies the language and regional accent of each voice.
{id:48, voiceURI:"Paulina", name:"Paulina", lang: "es-MX", localService:true}, {id:52, voiceURI:"Samantha", name:"Samantha", lang: "en-US", localService:true}, {id:72, voiceURI:"Google Deutsch", name:"Google Deutsch", lang: "de-DE", localService:false}
The last line of setUpVoices() calls a function to create the list of options that will appear in the #speakerMenu select element. The value of the id attribute for each voice is placed in the value attribute for the option. The name and lang attributes are the visible text items that appear in each option along with "(premium)" for those voices that are marked that way on some operating systems and browsers.
function setUpVoices() { allVoices = getAllVoices(); createSpeakerMenu(allVoices); } function getAllVoices() { let voicesall = speechSynthesis.getVoices(); let vuris = []; let voices = []; voicesall.forEach(function(obj,index) { let uri = obj.voiceURI; if (!vuris.includes(uri)) { vuris.push(uri); voices.push(obj); } }); voices.forEach(function(obj,index) {obj.id = index;}); return voices; } function createSpeakerMenu(voices) { let code = ; voices.forEach(function(vobj,i) { code += `<option value=${vobj.id}>`; code += `${vobj.name} (${vobj.lang})`; code += vobj.voiceURI.includes(".premium") ? ' (premium)' : ; code += `</option>`; }); speakerMenu.innerHTML = code; speakerMenu.selectedIndex = voiceIndex; }
You might recall that in the init() function, we had set up an event listener to call selectSpeaker() whenever the speakerMenu changes. The selectSpeaker() function stores the selectedIndex of the #speakerMenu select element. Next, it gets the value of the selected item which will be an integer that corresponds to the index of that voice in the allVoices() array. So, now we have retrieved the SpeechSynthesisVoice we want. We then grab the first two letters of the lang attribute (e.g. "en," "es," "ru," "de," "fr") and use that code to search the langtags array of language objects to find the appropriate language name. The searchObjects() function returns an array that will likely have only one entry. Regardless, the first entry (langcodeobj[0]) is all we need. Finally, we assign that name to the innerHTML attribute of the language span and it shows on the screen as expected.
// Code for when the user selects a speaker function selectSpeaker() { voiceIndex = speakerMenu.selectedIndex; let sval = Number(speakerMenu.value); let voice = allVoices[sval]; let langcode = voice.lang.substring(0,2); let langcodeobj = searchObjects(langtags, "code", langcode); language.innerHTML = langcodeobj[0].name; }
The only thing left for Step 2 to be complete is to make sure the talk() function works when we click the "Speak" button. Modify the talk() function to add attributes to the utterance to control which voice and language are used and how fast to speak the text. In my testing, a rate range of 0.5 to 2 works reliably well. I found that a rate below 0.5 has no effect. I think 0.8 works as a nice default for many languages, but as we’ll see in Step 3, there’s an easy way to let the user decide.
function talk() { let sval = Number(speakerMenu.value); let u = new SpeechSynthesisUtterance(); u.voice = allVoices[sval]; u.lang = u.voice.lang; u.text = txtFld.value; u.rate = 0.8; speechSynthesis.speak(u); }
That’s it for Step 2! Here’s the result of what we’ve done so far:
See the Pen Text-To-Speech Part 2 by Steven Estrella (@sgestrella) on CodePen.
Play around with it a bit. Sometimes it is fun to type an English phrase and then assign a French or German speaker to say it. Conversely, if you want to hear your worst first-year Spanish student, type a Spanish phrase and assign it to be spoken by an English voice.
Step 3: The Complete Polyglot
We’re in the final stretch! Some of the things we do in this step will be bits of polish to the UI but there are some functional things we need to do as well to button everything up. specifically, we’re going to:
Create a menu of available language options
Allow users to define the speed of the speech
Define a default phrase in the textarea that translates on language selection
Here’s what we’re looking at:
We’re adding a dropdown menu, speech rate setting, and a default phrase.
In the HTML, we’re going to add a new <select> element for the language menu and a number input (which will be used later to set the rate of speech). Notice we have deleted the #language span as it is no longer relevant once the language menu is working.
<div class="uiunit"> <label for="languageMenu">Language: </label> <select id="languageMenu"> <option selected value="all">Show All</option> </select> </div> <div class="uiunit"> <label for="speakerMenu">Voice: </label><select id="speakerMenu"></select> </div> <div class="uiunit"> <label for="rateFld">Speed: </label> <input type="number" id="rateFld" min="0.5" max="2" step="0.1" value="0.8" /> </div>
In the JavaScript, we will need to modify the variable declarations. We will keep track of all dialects in the allLanguages array and just the main languages in the primaryLanguages array. The langhash and langcodehash arrays will serve as hash tables so we can quickly get a language name when all we know is the two-letter language code and vice versa. We should only need to setup the languages menu once so a Boolean flag for initialSetup will come in handy.
let speakBtn, txtFld, speakerMenu, allVoices, langtags; let voiceIndex = 0; let allLanguages, primaryLanguages, langhash, langcodehash; let rateFld, languageMenu, blurbs; let initialSetup = true; let defaultBlurb = "I enjoy the traditional music of my native country.";
In the new init() function, let’s remove the line language = qs("#language"); then add the new code as seen here to create the blurbs, reference the rateFld number input and languageMenu select, and create hash tables for looking up language names and tags.
function init() { // ...keep existing content but delete language = qs("#language"); createBlurbs(); rateFld = qs("#rateFld"); languageMenu = qs("#languageMenu"); languageMenu.addEventListener("change", selectLanguage, false); langhash = getLookupTable(langtags, "name"); langcodehash = getLookupTable(langtags, "code"); if (window.speechSynthesis) { // ...keep existing content } else{ // ...keep existing content languageMenu.disabled = true; } }
The setUpVoices() function needs some work to accommodate the new languages menu and to trigger the filterVoices() function which we will use now to populate the #speakerMenu element. Also, we’re going to add the new functions: getAllLanguages() and getPrimaryLanguages(). The first one assembles an array of the unique values for the lang attribute found in the allVoices array of objects. Notice the return statement uses the spread operator combined with a new Set object to ensure that the returned array has no duplicates. The getPrimaryLanguages() function returns an array of the two-letter country codes. That makes a smaller list of just the main languages without reference to regional dialects.
function setUpVoices() { allVoices = getAllVoices(); allLanguages = getAllLanguages(allVoices); primaryLanguages = getPrimaryLanguages(allLanguages); filterVoices(); if (initialSetup && allVoices.length) { initialSetup = false; createLanguageMenu(); } } function getAllLanguages(voices) { let langs = []; voices.forEach(vobj => { langs.push(vobj.lang.trim()); }); return [...new Set(langs)]; } function getPrimaryLanguages(langlist) { let langs = []; langlist.forEach(vobj => { langs.push(vobj.substring(0,2)); }); return [...new Set(langs)]; }
The setUpVoices() function calls two additional functions. The filterVoices() function gets the two-letter language code from the current value of the #languageMenu select menu and uses it to filter the allVoices array and return only the available voice options for the chosen language. It then passes that array to the createSpeakerMenu() function (unchanged from Step 2) which populates the #speakerMenu with options. Then filterVoices() gets the blurb associated with the chosen language and places it in the textarea where it can be edited or replaced.
And, in case Chrome rebuilds this menu, the stored voiceIndex is used to restore the current selection. Next the createLanguageMenu() function uses our hash tables to create the needed menu options for the languageMenu select element. The selectLanguage() function is triggered whenever the user chooses a language. It then triggers filterVoices() and sets the #speakerMenu to display the first available option.
function filterVoices() { let langcode = languageMenu.value; voices = allVoices.filter(function (voice) { return langcode === "all" ? true : voice.lang.indexOf(langcode + "-") >= 0; }); createSpeakerMenu(voices); let t = blurbs[languageMenu.options[languageMenu.selectedIndex].text]; txtFld.value = t ? t : defaultBlurb; speakerMenu.selectedIndex = voiceIndex; } function createLanguageMenu() { let code = `<option selected value="all">Show All</option>`; let langnames = []; primaryLanguages.forEach(function(lobj,i) { langnames.push(langcodehash[lobj.substring(0,2)].name); }); langnames.sort(); langnames.forEach(function(lname,i) { let lcode = langhash[lname].code; code += `<option value=${lcode}>${lname}</option>`; }); languageMenu.innerHTML = code; } function selectLanguage() { filterVoices(); speakerMenu.selectedIndex = 0; }
In the utility functions section of the code toward the bottom, add the following code. This generic little utility will help you the next time you need to create a lookup table for an array of objects. In our case, we will use this to allow us to easily match a language code with its corresponding language name and vice versa.
function getLookupTable(objectsArray, propname) { return objectsArray.reduce((accumulator, currentValue) => (accumulator[currentValue[propname]] = currentValue, accumulator),{}); }
I added an array of text phrases, each of which is a translation of the English phrase, "I enjoy the traditional music of my native country." The language it’s displayed in will correspond to what’s selected in the language men.
Here we see the beauty of UTF-8 on full display. Above the getLanguagesTags() function, let’s add the code that generates all those translated blurbs. I only read Spanish, English, some Portuguese, and very little German, so I have to take on faith that Google Translate is providing accurate translations for the rest. If any of these is your native language, feel free to leave corrections in the comments.
function createBlurbs() { blurbs = { "Arabic" : "أنا أستمتع بالموسيقى التقليدية لبلدي الأم.", "Chinese" : "我喜歡我祖國的傳統音樂。", "Czech" : "Mám rád tradiční hudbu mé rodné země.", "Danish" : "Jeg nyder den traditionelle musik i mit hjemland.", "Dutch" : "Ik geniet van de traditionele muziek van mijn geboorteland.", "English" : "I enjoy the traditional music of my native country.", "Finnish" : "Nautin kotimaassani perinteistä musiikkia.", "French" : "J'apprécie la musique traditionnelle de mon pays d'origine.", "German" : "Ich genieße die traditionelle Musik meiner Heimat.", "Greek" : "Απολαμβάνω την παραδοσιακή μουσική της πατρίδας μου.", "Hebrew" : "אני נהנה מהמוסיקה המסורתית של מולדתי.", "Hindi" : "मैं अपने मूल देश के पारंपरिक संगीत का आनंद लेता हूं।", "Hungarian" : "Élvezem az én hazám hagyományos zenéjét.", "Indonesian" : "Saya menikmati musik tradisional negara asal saya.", "Italian" : "Mi piace la musica tradizionale del mio paese natale.", "Japanese" : "私は母国の伝統音楽を楽しんでいます。", "Korean" : "나는 내 조국의 전통 음악을 즐긴다.", "Norwegian Bokmal" : "Jeg liker den tradisjonelle musikken i mitt hjemland.", "Polish" : "Lubię tradycyjną muzykę mojego kraju.", "Portuguese" : "Eu gosto da música tradicional do meu país natal.", "Romanian" : "Îmi place muzica tradițională din țara mea natală.", "Russian" : "Мне нравится традиционная музыка моей родной страны.", "Slovak" : "Mám rád tradičnú hudbu svojej rodnej krajiny.", "Spanish" : "Disfruto de la música tradicional de mi país natal.", "Swedish" : "Jag njuter av traditionell musik i mitt hemland.", "Thai" : "ฉันเพลิดเพลินกับดนตรีดั้งเดิมของประเทศบ้านเกิดของฉัน", "Turkish" : "Ülkemdeki geleneksel müzikten zevk alıyorum." }; }
There’s one last thing: the numeric input for controlling the playback speed of the speech. Modify the talk() function to get the speech rate from the number input and we’re good to go!
Here’s the final product:
function talk() { ...// no changes except for the rateFld.value reference u.rate = Number(rateFld.value); speechSynthesis.speak(u); }
See the Pen Polyglot: Text-To-Speech in Multiple Languages by Steven Estrella (@sgestrella) on CodePen.
A Real World Application
My interest in this technology started many years ago in 1990 when I created a 26-lesson curriculum as part of my dissertation. It was delivered using my first programming language, HyperCard, on a Macintosh Plus which had a primitive text-to-speech feature. I used that feature to provide some feedback to the user while they progressed through the material. More recently, in 2018, I created a free progressive web app called Buenos Verbos that helps Spanish language students search and filter a database of 766 verbs. The chosen verb is then fully conjugated and the user can click the forms to hear them spoken. So perhaps web pages might like to talk and with some imagination you may find reasons to encourage them. The question is: what will you make your website say next?
The post Using the Web Speech API for Multilingual Translations appeared first on CSS-Tricks.
Using the Web Speech API for Multilingual Translations published first on https://deskbysnafu.tumblr.com/
0 notes
Text
Using the Web Speech API for Multilingual Translations
Since the early days of science fiction, we have fantasized about machines that talk to us. Today it is commonplace. Even so, the technology for making websites talk is still pretty new.
We can make our pages on the web talk using the SpeechSynthesis part of the Web Speech API. This is still considered an experimental technology but it has great support in the latest versions of Chrome, Safari, and Firefox.
The fun part for me is using this technology with foreign languages. For that, Mac OSX has great support for this on all browsers. On Windows, you have to use Chrome. We’re going to walk through a three-step process to create a page that speaks the same text in multiple languages. Some of the basic code is derived from documentation found here but the final product adds some fun features and can be viewed at my Polyglot CodePen here.
Screen shot of the completed Polyglot app with a menu of languages.
Step 1: Start Simple
Let’s create a basic page with a <textarea> for the text we want the page to speak and include a button to click to trigger the speech.
<div id="wrapper"> <h1>Simple Text To Speech</h1> <p id="warning">Sorry, your browser does not support the Web Speech API.</p> <textarea id="txtFld">I love the sound of my computer-generated voice.</textarea> <label for="txtFld">Type text above. Then click the Speak button.</label> <div> <button type="button" id="speakBtn">Speak</button> <br> <p>Note: For best results on a Mac, use the latest version of Chrome, Safari, or FireFox. On Windows, use Chrome.</p> </div> </div>
The paragraph with ID warning will be shown only if the JavaScript detects no support for the Web Speech API. Also, note the ID values for the textarea and the button as we will use those in our JavaScript.
Feel free to style the HTML any way you’d like. You’re also free to work off the demo I created:
See the Pen Text-To-Speech Part 1 by Steven Estrella (@sgestrella) on CodePen.
Adding a style rule for the disabled state of the button is a good idea to avoid confusion for the few people who still use incompatible browsers, like the now-quaint Internet Explorer. Also, let’s use a style rule to hide the warning by default so we can control when it’s actually needed.
button:disabled { cursor: not-allowed; opacity: 0.3; } #warning { color: red; display: none; font-size: 1.4rem; }
Now on to the JavaScript! First, we add two variables to serve as references to the "Speak" button that triggers the speech and to the <textarea> element. An event listener at the bottom of the code tells the document to wait until the DOM elements load before calling the init() function. I used a handy utility function I call "qs" that is defined at the bottom of the code. It is a shortcut alternative to document.querySelector and it selects whatever selector value I pass to it and returns an object reference. Then we’ll add an event listener to the speakBtn object to make the button call the talk() function.
The talk() function creates a new instance of the SpeechSynthesisUtterance object that is part of the Web Speech API. It adds the text from the <textarea>(using ID txtFld) to the text property. Then the utterance is passed to the speechSynthesis method of the window object and we hear the spoken text. The specific voice you hear will vary by browser and operating system. On my Mac, for example, my default language is set to American English and the default voice for English is Alex. In Step 2, we will add code to create a menu to help the user choose voices for all available languages.
let speakBtn, txtFld; function init() { speakBtn = qs("#speakBtn"); txtFld = qs("#txtFld"); speakBtn.addEventListener("click", talk, false); if (!window.speechSynthesis) { speakBtn.disabled = true; qs("#warning").style.display = "block"; } } function talk() { let u = new SpeechSynthesisUtterance(); u.text = txtFld.value; speechSynthesis.speak(u); } // Reusable utility functions function qs(selectorText) { // Saves lots of typing for those who eschew jQuery return document.querySelector(selectorText); } document.addEventListener('DOMContentLoaded', function (e) { try {init();} catch (error) { console.log("Data didn't load", error); } });
Step 2: A Menu of International Voices
If we want to use anything other than the default language and speaking voice, we will have to add a bit more code. So that’s what we’re going tackle next.
We’re going to add a select element to hold the menu of voice options:
<h1>Multilingual Text To Speech</h1> <div class="uiunit"> <label for="speakerMenu">Voice: </label> <select id="speakerMenu"></select> speaks <span id="language">English.</span> <!-- etc. --> </div>
Before we create the code to populate the menu options, we should take care of the code that will help us connect language codes to their corresponding names. Each language is identified by a two-letter code such as "en" for English or "es" for Español (Spanish). We will take a simple list of these codes and their corresponding languages and make an array of objects of the form: {"code": "pt", "name": "Portuguese"}. Then we’ll need a utility function to help us search an array of objects for the value of a given property. We will use it in a few minutes to quickly find the language name that matches the language code of the selected voice. Copy the code below so that the two functions are just above and just below the // Generic Utility Functions comment.
function getLanguageTags() { let langs = ["ar-Arabic","cs-Czech","da-Danish","de-German","el-Greek","en-English","eo-Esperanto","es-Spanish","et-Estonian","fi-Finnish","fr-French","he-Hebrew","hi-Hindi","hu-Hungarian","id-Indonesian","it-Italian","ja-Japanese","ko-Korean","la-Latin","lt-Lithuanian","lv-Latvian","nb-Norwegian Bokmal","nl-Dutch","nn-Norwegian Nynorsk","no-Norwegian","pl-Polish","pt-Portuguese","ro-Romanian","ru-Russian","sk-Slovak","sl-Slovenian","sq-Albanian","sr-Serbian","sv-Swedish","th-Thai","tr-Turkish","zh-Chinese"]; let langobjects = []; for (let i=0;i<langs.length;i++) { let langparts = langs[i].split("-"); langobjects.push({"code":langparts[0],"name":langparts[1]}); } return langobjects; } // Generic Utility Functions function searchObjects(array, prop, term, casesensitive = false) { // Searches an array of objects for a given term in a given property // Returns an array of only those objects that test positive let regex = new RegExp(term, casesensitive ? "" : "i"); let newArrayOfObjects = array.filter(obj => regex.test(obj[prop])); return newArrayOfObjects; }
Now we can build out the options for the select element using JavaScript. We need to declare variables at the top of our JavaScript to hold references to the #speakerMenu select element, the #language span element, the array of synthesized voices (allVoices), an array of codes to identify the languages (langtags), and a place to keep track of the currently selected voice (voiceIndex). Add those just after the two variable declarations we created in Step 1.
let speakBtn, txtFld, speakerMenu, language, allVoices, langtags; let voiceIndex = 0;
The updated init() function sets some additional references to the #speakerMenu and the #language span and places all the language codes into an array of objects called langtags. The feature detection part of the code changes here, too. If the Web Speech API is supported, the setUpVoices() function is called. Also, for Chrome, we have to listen for changes to the loaded voices and repeat the setup when needed. Chrome polls the available voices every time you switch between one of its remote voices (the ones listed with the Google prefix while you are in Chrome) and all the other voices which are stored locally in the user’s operating system.
function init() { speakBtn = qs("#speakBtn"); txtFld = qs("#txtFld"); speakerMenu = qs("#speakerMenu"); language = qs("#language"); langtags = getLanguageTags(); speakBtn.addEventListener("click", talk, false); speakerMenu.addEventListener("change", selectSpeaker, false); if (window.speechSynthesis) { if (speechSynthesis.onvoiceschanged !== undefined) { // Chrome gets the voices asynchronously so this is needed speechSynthesis.onvoiceschanged = setUpVoices; } setUpVoices(); // For all the other browsers } else{ speakBtn.disabled = true; speakerMenu.disabled = true; qs("#warning").style.display = "block"; } }
The setUpVoices() function gets an array of what are called SpeechSynthesisVoice objects by calling the getVoices() method of the speechSynthesis object. This is done in our code using the getAllVoices() function. Unfortunately, I have found that the speechSynthesis.getVoices() method sometimes returns duplicates in the list, so I devoted nine lines of code to eliminate the those. Finally, at the end of getAllVoices(), I added a unique identifier number to each of the SpeechSynthesisVoice objects. That will help us in Step 3 when we need to filter the list of voices to only show voices for a given language. When complete, the allVoices array will contain objects that look like the ones below. Each object has id, voiceURI, name, and lang attributes. The localService attribute indicates whether the code for the voice is stored on the user’s computer or remotely on Google’s servers. Notice the lang attribute. The value consists of a two-letter language code (e.g. "es" for Spanish) followed by a dash and a region code (e.g. "MX" for Mexico). This identifies the language and regional accent of each voice.
{id:48, voiceURI:"Paulina", name:"Paulina", lang: "es-MX", localService:true}, {id:52, voiceURI:"Samantha", name:"Samantha", lang: "en-US", localService:true}, {id:72, voiceURI:"Google Deutsch", name:"Google Deutsch", lang: "de-DE", localService:false}
The last line of setUpVoices() calls a function to create the list of options that will appear in the #speakerMenu select element. The value of the id attribute for each voice is placed in the value attribute for the option. The name and lang attributes are the visible text items that appear in each option along with "(premium)" for those voices that are marked that way on some operating systems and browsers.
function setUpVoices() { allVoices = getAllVoices(); createSpeakerMenu(allVoices); } function getAllVoices() { let voicesall = speechSynthesis.getVoices(); let vuris = []; let voices = []; voicesall.forEach(function(obj,index) { let uri = obj.voiceURI; if (!vuris.includes(uri)) { vuris.push(uri); voices.push(obj); } }); voices.forEach(function(obj,index) {obj.id = index;}); return voices; } function createSpeakerMenu(voices) { let code = ; voices.forEach(function(vobj,i) { code += `<option value=${vobj.id}>`; code += `${vobj.name} (${vobj.lang})`; code += vobj.voiceURI.includes(".premium") ? ' (premium)' : ; code += `</option>`; }); speakerMenu.innerHTML = code; speakerMenu.selectedIndex = voiceIndex; }
You might recall that in the init() function, we had set up an event listener to call selectSpeaker() whenever the speakerMenu changes. The selectSpeaker() function stores the selectedIndex of the #speakerMenu select element. Next, it gets the value of the selected item which will be an integer that corresponds to the index of that voice in the allVoices() array. So, now we have retrieved the SpeechSynthesisVoice we want. We then grab the first two letters of the lang attribute (e.g. "en," "es," "ru," "de," "fr") and use that code to search the langtags array of language objects to find the appropriate language name. The searchObjects() function returns an array that will likely have only one entry. Regardless, the first entry (langcodeobj[0]) is all we need. Finally, we assign that name to the innerHTML attribute of the language span and it shows on the screen as expected.
// Code for when the user selects a speaker function selectSpeaker() { voiceIndex = speakerMenu.selectedIndex; let sval = Number(speakerMenu.value); let voice = allVoices[sval]; let langcode = voice.lang.substring(0,2); let langcodeobj = searchObjects(langtags, "code", langcode); language.innerHTML = langcodeobj[0].name; }
The only thing left for Step 2 to be complete is to make sure the talk() function works when we click the "Speak" button. Modify the talk() function to add attributes to the utterance to control which voice and language are used and how fast to speak the text. In my testing, a rate range of 0.5 to 2 works reliably well. I found that a rate below 0.5 has no effect. I think 0.8 works as a nice default for many languages, but as we’ll see in Step 3, there’s an easy way to let the user decide.
function talk() { let sval = Number(speakerMenu.value); let u = new SpeechSynthesisUtterance(); u.voice = allVoices[sval]; u.lang = u.voice.lang; u.text = txtFld.value; u.rate = 0.8; speechSynthesis.speak(u); }
That’s it for Step 2! Here’s the result of what we’ve done so far:
See the Pen Text-To-Speech Part 2 by Steven Estrella (@sgestrella) on CodePen.
Play around with it a bit. Sometimes it is fun to type an English phrase and then assign a French or German speaker to say it. Conversely, if you want to hear your worst first-year Spanish student, type a Spanish phrase and assign it to be spoken by an English voice.
Step 3: The Complete Polyglot
We’re in the final stretch! Some of the things we do in this step will be bits of polish to the UI but there are some functional things we need to do as well to button everything up. specifically, we’re going to:
Create a menu of available language options
Allow users to define the speed of the speech
Define a default phrase in the textarea that translates on language selection
Here’s what we’re looking at:
We’re adding a dropdown menu, speech rate setting, and a default phrase.
In the HTML, we’re going to add a new <select> element for the language menu and a number input (which will be used later to set the rate of speech). Notice we have deleted the #language span as it is no longer relevant once the language menu is working.
<div class="uiunit"> <label for="languageMenu">Language: </label> <select id="languageMenu"> <option selected value="all">Show All</option> </select> </div> <div class="uiunit"> <label for="speakerMenu">Voice: </label><select id="speakerMenu"></select> </div> <div class="uiunit"> <label for="rateFld">Speed: </label> <input type="number" id="rateFld" min="0.5" max="2" step="0.1" value="0.8" /> </div>
In the JavaScript, we will need to modify the variable declarations. We will keep track of all dialects in the allLanguages array and just the main languages in the primaryLanguages array. The langhash and langcodehash arrays will serve as hash tables so we can quickly get a language name when all we know is the two-letter language code and vice versa. We should only need to setup the languages menu once so a Boolean flag for initialSetup will come in handy.
let speakBtn, txtFld, speakerMenu, allVoices, langtags; let voiceIndex = 0; let allLanguages, primaryLanguages, langhash, langcodehash; let rateFld, languageMenu, blurbs; let initialSetup = true; let defaultBlurb = "I enjoy the traditional music of my native country.";
In the new init() function, let’s remove the line language = qs("#language"); then add the new code as seen here to create the blurbs, reference the rateFld number input and languageMenu select, and create hash tables for looking up language names and tags.
function init() { // ...keep existing content but delete language = qs("#language"); createBlurbs(); rateFld = qs("#rateFld"); languageMenu = qs("#languageMenu"); languageMenu.addEventListener("change", selectLanguage, false); langhash = getLookupTable(langtags, "name"); langcodehash = getLookupTable(langtags, "code"); if (window.speechSynthesis) { // ...keep existing content } else{ // ...keep existing content languageMenu.disabled = true; } }
The setUpVoices() function needs some work to accommodate the new languages menu and to trigger the filterVoices() function which we will use now to populate the #speakerMenu element. Also, we’re going to add the new functions: getAllLanguages() and getPrimaryLanguages(). The first one assembles an array of the unique values for the lang attribute found in the allVoices array of objects. Notice the return statement uses the spread operator combined with a new Set object to ensure that the returned array has no duplicates. The getPrimaryLanguages() function returns an array of the two-letter country codes. That makes a smaller list of just the main languages without reference to regional dialects.
function setUpVoices() { allVoices = getAllVoices(); allLanguages = getAllLanguages(allVoices); primaryLanguages = getPrimaryLanguages(allLanguages); filterVoices(); if (initialSetup && allVoices.length) { initialSetup = false; createLanguageMenu(); } } function getAllLanguages(voices) { let langs = []; voices.forEach(vobj => { langs.push(vobj.lang.trim()); }); return [...new Set(langs)]; } function getPrimaryLanguages(langlist) { let langs = []; langlist.forEach(vobj => { langs.push(vobj.substring(0,2)); }); return [...new Set(langs)]; }
The setUpVoices() function calls two additional functions. The filterVoices() function gets the two-letter language code from the current value of the #languageMenu select menu and uses it to filter the allVoices array and return only the available voice options for the chosen language. It then passes that array to the createSpeakerMenu() function (unchanged from Step 2) which populates the #speakerMenu with options. Then filterVoices() gets the blurb associated with the chosen language and places it in the textarea where it can be edited or replaced.
And, in case Chrome rebuilds this menu, the stored voiceIndex is used to restore the current selection. Next the createLanguageMenu() function uses our hash tables to create the needed menu options for the languageMenu select element. The selectLanguage() function is triggered whenever the user chooses a language. It then triggers filterVoices() and sets the #speakerMenu to display the first available option.
function filterVoices() { let langcode = languageMenu.value; voices = allVoices.filter(function (voice) { return langcode === "all" ? true : voice.lang.indexOf(langcode + "-") >= 0; }); createSpeakerMenu(voices); let t = blurbs[languageMenu.options[languageMenu.selectedIndex].text]; txtFld.value = t ? t : defaultBlurb; speakerMenu.selectedIndex = voiceIndex; } function createLanguageMenu() { let code = `<option selected value="all">Show All</option>`; let langnames = []; primaryLanguages.forEach(function(lobj,i) { langnames.push(langcodehash[lobj.substring(0,2)].name); }); langnames.sort(); langnames.forEach(function(lname,i) { let lcode = langhash[lname].code; code += `<option value=${lcode}>${lname}</option>`; }); languageMenu.innerHTML = code; } function selectLanguage() { filterVoices(); speakerMenu.selectedIndex = 0; }
In the utility functions section of the code toward the bottom, add the following code. This generic little utility will help you the next time you need to create a lookup table for an array of objects. In our case, we will use this to allow us to easily match a language code with its corresponding language name and vice versa.
function getLookupTable(objectsArray, propname) { return objectsArray.reduce((accumulator, currentValue) => (accumulator[currentValue[propname]] = currentValue, accumulator),{}); }
I added an array of text phrases, each of which is a translation of the English phrase, "I enjoy the traditional music of my native country." The language it’s displayed in will correspond to what’s selected in the language men.
Here we see the beauty of UTF-8 on full display. Above the getLanguagesTags() function, let’s add the code that generates all those translated blurbs. I only read Spanish, English, some Portuguese, and very little German, so I have to take on faith that Google Translate is providing accurate translations for the rest. If any of these is your native language, feel free to leave corrections in the comments.
function createBlurbs() { blurbs = { "Arabic" : "أنا أستمتع بالموسيقى التقليدية لبلدي الأم.", "Chinese" : "我喜歡我祖國的傳統音樂。", "Czech" : "Mám rád tradiční hudbu mé rodné země.", "Danish" : "Jeg nyder den traditionelle musik i mit hjemland.", "Dutch" : "Ik geniet van de traditionele muziek van mijn geboorteland.", "English" : "I enjoy the traditional music of my native country.", "Finnish" : "Nautin kotimaassani perinteistä musiikkia.", "French" : "J'apprécie la musique traditionnelle de mon pays d'origine.", "German" : "Ich genieße die traditionelle Musik meiner Heimat.", "Greek" : "Απολαμβάνω την παραδοσιακή μουσική της πατρίδας μου.", "Hebrew" : "אני נהנה מהמוסיקה המסורתית של מולדתי.", "Hindi" : "मैं अपने मूल देश के पारंपरिक संगीत का आनंद लेता हूं।", "Hungarian" : "Élvezem az én hazám hagyományos zenéjét.", "Indonesian" : "Saya menikmati musik tradisional negara asal saya.", "Italian" : "Mi piace la musica tradizionale del mio paese natale.", "Japanese" : "私は母国の伝統音楽を楽しんでいます。", "Korean" : "나는 내 조국의 전통 음악을 즐긴다.", "Norwegian Bokmal" : "Jeg liker den tradisjonelle musikken i mitt hjemland.", "Polish" : "Lubię tradycyjną muzykę mojego kraju.", "Portuguese" : "Eu gosto da música tradicional do meu país natal.", "Romanian" : "Îmi place muzica tradițională din țara mea natală.", "Russian" : "Мне нравится традиционная музыка моей родной страны.", "Slovak" : "Mám rád tradičnú hudbu svojej rodnej krajiny.", "Spanish" : "Disfruto de la música tradicional de mi país natal.", "Swedish" : "Jag njuter av traditionell musik i mitt hemland.", "Thai" : "ฉันเพลิดเพลินกับดนตรีดั้งเดิมของประเทศบ้านเกิดของฉัน", "Turkish" : "Ülkemdeki geleneksel müzikten zevk alıyorum." }; }
There’s one last thing: the numeric input for controlling the playback speed of the speech. Modify the talk() function to get the speech rate from the number input and we’re good to go!
Here’s the final product:
function talk() { ...// no changes except for the rateFld.value reference u.rate = Number(rateFld.value); speechSynthesis.speak(u); }
See the Pen Polyglot: Text-To-Speech in Multiple Languages by Steven Estrella (@sgestrella) on CodePen.
A Real World Application
My interest in this technology started many years ago in 1990 when I created a 26-lesson curriculum as part of my dissertation. It was delivered using my first programming language, HyperCard, on a Macintosh Plus which had a primitive text-to-speech feature. I used that feature to provide some feedback to the user while they progressed through the material. More recently, in 2018, I created a free progressive web app called Buenos Verbos that helps Spanish language students search and filter a database of 766 verbs. The chosen verb is then fully conjugated and the user can click the forms to hear them spoken. So perhaps web pages might like to talk and with some imagination you may find reasons to encourage them. The question is: what will you make your website say next?
The post Using the Web Speech API for Multilingual Translations appeared first on CSS-Tricks.
😉SiliconWebX | 🌐CSS-Tricks
0 notes
Text
Using the Web Speech API for Multilingual Translations
Since the early days of science fiction, we have fantasized about machines that talk to us. Today it is commonplace. Even so, the technology for making websites talk is still pretty new.
We can make our pages on the web talk using the SpeechSynthesis part of the Web Speech API. This is still considered an experimental technology but it has great support in the latest versions of Chrome, Safari, and Firefox.
The fun part for me is using this technology with foreign languages. For that, Mac OSX has great support for this on all browsers. On Windows, you have to use Chrome. We’re going to walk through a three-step process to create a page that speaks the same text in multiple languages. Some of the basic code is derived from documentation found here but the final product adds some fun features and can be viewed at my Polyglot CodePen here.
Screen shot of the completed Polyglot app with a menu of languages.
Step 1: Start Simple
Let’s create a basic page with a <textarea> for the text we want the page to speak and include a button to click to trigger the speech.
<div id="wrapper"> <h1>Simple Text To Speech</h1> <p id="warning">Sorry, your browser does not support the Web Speech API.</p> <textarea id="txtFld">I love the sound of my computer-generated voice.</textarea> <label for="txtFld">Type text above. Then click the Speak button.</label> <div> <button type="button" id="speakBtn">Speak</button> <br> <p>Note: For best results on a Mac, use the latest version of Chrome, Safari, or FireFox. On Windows, use Chrome.</p> </div> </div>
The paragraph with ID warning will be shown only if the JavaScript detects no support for the Web Speech API. Also, note the ID values for the textarea and the button as we will use those in our JavaScript.
Feel free to style the HTML any way you’d like. You’re also free to work off the demo I created:
See the Pen Text-To-Speech Part 1 by Steven Estrella (@sgestrella) on CodePen.
Adding a style rule for the disabled state of the button is a good idea to avoid confusion for the few people who still use incompatible browsers, like the now-quaint Internet Explorer. Also, let’s use a style rule to hide the warning by default so we can control when it’s actually needed.
button:disabled { cursor: not-allowed; opacity: 0.3; } #warning { color: red; display: none; font-size: 1.4rem; }
Now on to the JavaScript! First, we add two variables to serve as references to the "Speak" button that triggers the speech and to the <textarea> element. An event listener at the bottom of the code tells the document to wait until the DOM elements load before calling the init() function. I used a handy utility function I call "qs" that is defined at the bottom of the code. It is a shortcut alternative to document.querySelector and it selects whatever selector value I pass to it and returns an object reference. Then we’ll add an event listener to the speakBtn object to make the button call the talk() function.
The talk() function creates a new instance of the SpeechSynthesisUtterance object that is part of the Web Speech API. It adds the text from the <textarea>(using ID txtFld) to the text property. Then the utterance is passed to the speechSynthesis method of the window object and we hear the spoken text. The specific voice you hear will vary by browser and operating system. On my Mac, for example, my default language is set to American English and the default voice for English is Alex. In Step 2, we will add code to create a menu to help the user choose voices for all available languages.
let speakBtn, txtFld; function init() { speakBtn = qs("#speakBtn"); txtFld = qs("#txtFld"); speakBtn.addEventListener("click", talk, false); if (!window.speechSynthesis) { speakBtn.disabled = true; qs("#warning").style.display = "block"; } } function talk() { let u = new SpeechSynthesisUtterance(); u.text = txtFld.value; speechSynthesis.speak(u); } // Reusable utility functions function qs(selectorText) { // Saves lots of typing for those who eschew jQuery return document.querySelector(selectorText); } document.addEventListener('DOMContentLoaded', function (e) { try {init();} catch (error) { console.log("Data didn't load", error); } });
Step 2: A Menu of International Voices
If we want to use anything other than the default language and speaking voice, we will have to add a bit more code. So that’s what we’re going tackle next.
We’re going to add a select element to hold the menu of voice options:
<h1>Multilingual Text To Speech</h1> <div class="uiunit"> <label for="speakerMenu">Voice: </label> <select id="speakerMenu"></select> speaks <span id="language">English.</span> <!-- etc. --> </div>
Before we create the code to populate the menu options, we should take care of the code that will help us connect language codes to their corresponding names. Each language is identified by a two-letter code such as "en" for English or "es" for Español (Spanish). We will take a simple list of these codes and their corresponding languages and make an array of objects of the form: {"code": "pt", "name": "Portuguese"}. Then we’ll need a utility function to help us search an array of objects for the value of a given property. We will use it in a few minutes to quickly find the language name that matches the language code of the selected voice. Copy the code below so that the two functions are just above and just below the // Generic Utility Functions comment.
function getLanguageTags() { let langs = ["ar-Arabic","cs-Czech","da-Danish","de-German","el-Greek","en-English","eo-Esperanto","es-Spanish","et-Estonian","fi-Finnish","fr-French","he-Hebrew","hi-Hindi","hu-Hungarian","id-Indonesian","it-Italian","ja-Japanese","ko-Korean","la-Latin","lt-Lithuanian","lv-Latvian","nb-Norwegian Bokmal","nl-Dutch","nn-Norwegian Nynorsk","no-Norwegian","pl-Polish","pt-Portuguese","ro-Romanian","ru-Russian","sk-Slovak","sl-Slovenian","sq-Albanian","sr-Serbian","sv-Swedish","th-Thai","tr-Turkish","zh-Chinese"]; let langobjects = []; for (let i=0;i<langs.length;i++) { let langparts = langs[i].split("-"); langobjects.push({"code":langparts[0],"name":langparts[1]}); } return langobjects; } // Generic Utility Functions function searchObjects(array, prop, term, casesensitive = false) { // Searches an array of objects for a given term in a given property // Returns an array of only those objects that test positive let regex = new RegExp(term, casesensitive ? "" : "i"); let newArrayOfObjects = array.filter(obj => regex.test(obj[prop])); return newArrayOfObjects; }
Now we can build out the options for the select element using JavaScript. We need to declare variables at the top of our JavaScript to hold references to the #speakerMenu select element, the #language span element, the array of synthesized voices (allVoices), an array of codes to identify the languages (langtags), and a place to keep track of the currently selected voice (voiceIndex). Add those just after the two variable declarations we created in Step 1.
let speakBtn, txtFld, speakerMenu, language, allVoices, langtags; let voiceIndex = 0;
The updated init() function sets some additional references to the #speakerMenu and the #language span and places all the language codes into an array of objects called langtags. The feature detection part of the code changes here, too. If the Web Speech API is supported, the setUpVoices() function is called. Also, for Chrome, we have to listen for changes to the loaded voices and repeat the setup when needed. Chrome polls the available voices every time you switch between one of its remote voices (the ones listed with the Google prefix while you are in Chrome) and all the other voices which are stored locally in the user’s operating system.
function init() { speakBtn = qs("#speakBtn"); txtFld = qs("#txtFld"); speakerMenu = qs("#speakerMenu"); language = qs("#language"); langtags = getLanguageTags(); speakBtn.addEventListener("click", talk, false); speakerMenu.addEventListener("change", selectSpeaker, false); if (window.speechSynthesis) { if (speechSynthesis.onvoiceschanged !== undefined) { // Chrome gets the voices asynchronously so this is needed speechSynthesis.onvoiceschanged = setUpVoices; } setUpVoices(); // For all the other browsers } else{ speakBtn.disabled = true; speakerMenu.disabled = true; qs("#warning").style.display = "block"; } }
The setUpVoices() function gets an array of what are called SpeechSynthesisVoice objects by calling the getVoices() method of the speechSynthesis object. This is done in our code using the getAllVoices() function. Unfortunately, I have found that the speechSynthesis.getVoices() method sometimes returns duplicates in the list, so I devoted nine lines of code to eliminate the those. Finally, at the end of getAllVoices(), I added a unique identifier number to each of the SpeechSynthesisVoice objects. That will help us in Step 3 when we need to filter the list of voices to only show voices for a given language. When complete, the allVoices array will contain objects that look like the ones below. Each object has id, voiceURI, name, and lang attributes. The localService attribute indicates whether the code for the voice is stored on the user’s computer or remotely on Google’s servers. Notice the lang attribute. The value consists of a two-letter language code (e.g. "es" for Spanish) followed by a dash and a region code (e.g. "MX" for Mexico). This identifies the language and regional accent of each voice.
{id:48, voiceURI:"Paulina", name:"Paulina", lang: "es-MX", localService:true}, {id:52, voiceURI:"Samantha", name:"Samantha", lang: "en-US", localService:true}, {id:72, voiceURI:"Google Deutsch", name:"Google Deutsch", lang: "de-DE", localService:false}
The last line of setUpVoices() calls a function to create the list of options that will appear in the #speakerMenu select element. The value of the id attribute for each voice is placed in the value attribute for the option. The name and lang attributes are the visible text items that appear in each option along with "(premium)" for those voices that are marked that way on some operating systems and browsers.
function setUpVoices() { allVoices = getAllVoices(); createSpeakerMenu(allVoices); } function getAllVoices() { let voicesall = speechSynthesis.getVoices(); let vuris = []; let voices = []; voicesall.forEach(function(obj,index) { let uri = obj.voiceURI; if (!vuris.includes(uri)) { vuris.push(uri); voices.push(obj); } }); voices.forEach(function(obj,index) {obj.id = index;}); return voices; } function createSpeakerMenu(voices) { let code = ; voices.forEach(function(vobj,i) { code += `<option value=${vobj.id}>`; code += `${vobj.name} (${vobj.lang})`; code += vobj.voiceURI.includes(".premium") ? ' (premium)' : ; code += `</option>`; }); speakerMenu.innerHTML = code; speakerMenu.selectedIndex = voiceIndex; }
You might recall that in the init() function, we had set up an event listener to call selectSpeaker() whenever the speakerMenu changes. The selectSpeaker() function stores the selectedIndex of the #speakerMenu select element. Next, it gets the value of the selected item which will be an integer that corresponds to the index of that voice in the allVoices() array. So, now we have retrieved the SpeechSynthesisVoice we want. We then grab the first two letters of the lang attribute (e.g. "en," "es," "ru," "de," "fr") and use that code to search the langtags array of language objects to find the appropriate language name. The searchObjects() function returns an array that will likely have only one entry. Regardless, the first entry (langcodeobj[0]) is all we need. Finally, we assign that name to the innerHTML attribute of the language span and it shows on the screen as expected.
// Code for when the user selects a speaker function selectSpeaker() { voiceIndex = speakerMenu.selectedIndex; let sval = Number(speakerMenu.value); let voice = allVoices[sval]; let langcode = voice.lang.substring(0,2); let langcodeobj = searchObjects(langtags, "code", langcode); language.innerHTML = langcodeobj[0].name; }
The only thing left for Step 2 to be complete is to make sure the talk() function works when we click the "Speak" button. Modify the talk() function to add attributes to the utterance to control which voice and language are used and how fast to speak the text. In my testing, a rate range of 0.5 to 2 works reliably well. I found that a rate below 0.5 has no effect. I think 0.8 works as a nice default for many languages, but as we’ll see in Step 3, there’s an easy way to let the user decide.
function talk() { let sval = Number(speakerMenu.value); let u = new SpeechSynthesisUtterance(); u.voice = allVoices[sval]; u.lang = u.voice.lang; u.text = txtFld.value; u.rate = 0.8; speechSynthesis.speak(u); }
That’s it for Step 2! Here’s the result of what we’ve done so far:
See the Pen Text-To-Speech Part 2 by Steven Estrella (@sgestrella) on CodePen.
Play around with it a bit. Sometimes it is fun to type an English phrase and then assign a French or German speaker to say it. Conversely, if you want to hear your worst first-year Spanish student, type a Spanish phrase and assign it to be spoken by an English voice.
Step 3: The Complete Polyglot
We’re in the final stretch! Some of the things we do in this step will be bits of polish to the UI but there are some functional things we need to do as well to button everything up. specifically, we’re going to:
Create a menu of available language options
Allow users to define the speed of the speech
Define a default phrase in the textarea that translates on language selection
Here’s what we’re looking at:
We’re adding a dropdown menu, speech rate setting, and a default phrase.
In the HTML, we’re going to add a new <select> element for the language menu and a number input (which will be used later to set the rate of speech). Notice we have deleted the #language span as it is no longer relevant once the language menu is working.
<div class="uiunit"> <label for="languageMenu">Language: </label> <select id="languageMenu"> <option selected value="all">Show All</option> </select> </div> <div class="uiunit"> <label for="speakerMenu">Voice: </label><select id="speakerMenu"></select> </div> <div class="uiunit"> <label for="rateFld">Speed: </label> <input type="number" id="rateFld" min="0.5" max="2" step="0.1" value="0.8" /> </div>
In the JavaScript, we will need to modify the variable declarations. We will keep track of all dialects in the allLanguages array and just the main languages in the primaryLanguages array. The langhash and langcodehash arrays will serve as hash tables so we can quickly get a language name when all we know is the two-letter language code and vice versa. We should only need to setup the languages menu once so a Boolean flag for initialSetup will come in handy.
let speakBtn, txtFld, speakerMenu, allVoices, langtags; let voiceIndex = 0; let allLanguages, primaryLanguages, langhash, langcodehash; let rateFld, languageMenu, blurbs; let initialSetup = true; let defaultBlurb = "I enjoy the traditional music of my native country.";
In the new init() function, let’s remove the line language = qs("#language"); then add the new code as seen here to create the blurbs, reference the rateFld number input and languageMenu select, and create hash tables for looking up language names and tags.
function init() { // ...keep existing content but delete language = qs("#language"); createBlurbs(); rateFld = qs("#rateFld"); languageMenu = qs("#languageMenu"); languageMenu.addEventListener("change", selectLanguage, false); langhash = getLookupTable(langtags, "name"); langcodehash = getLookupTable(langtags, "code"); if (window.speechSynthesis) { // ...keep existing content } else{ // ...keep existing content languageMenu.disabled = true; } }
The setUpVoices() function needs some work to accommodate the new languages menu and to trigger the filterVoices() function which we will use now to populate the #speakerMenu element. Also, we’re going to add the new functions: getAllLanguages() and getPrimaryLanguages(). The first one assembles an array of the unique values for the lang attribute found in the allVoices array of objects. Notice the return statement uses the spread operator combined with a new Set object to ensure that the returned array has no duplicates. The getPrimaryLanguages() function returns an array of the two-letter country codes. That makes a smaller list of just the main languages without reference to regional dialects.
function setUpVoices() { allVoices = getAllVoices(); allLanguages = getAllLanguages(allVoices); primaryLanguages = getPrimaryLanguages(allLanguages); filterVoices(); if (initialSetup && allVoices.length) { initialSetup = false; createLanguageMenu(); } } function getAllLanguages(voices) { let langs = []; voices.forEach(vobj => { langs.push(vobj.lang.trim()); }); return [...new Set(langs)]; } function getPrimaryLanguages(langlist) { let langs = []; langlist.forEach(vobj => { langs.push(vobj.substring(0,2)); }); return [...new Set(langs)]; }
The setUpVoices() function calls two additional functions. The filterVoices() function gets the two-letter language code from the current value of the #languageMenu select menu and uses it to filter the allVoices array and return only the available voice options for the chosen language. It then passes that array to the createSpeakerMenu() function (unchanged from Step 2) which populates the #speakerMenu with options. Then filterVoices() gets the blurb associated with the chosen language and places it in the textarea where it can be edited or replaced.
And, in case Chrome rebuilds this menu, the stored voiceIndex is used to restore the current selection. Next the createLanguageMenu() function uses our hash tables to create the needed menu options for the languageMenu select element. The selectLanguage() function is triggered whenever the user chooses a language. It then triggers filterVoices() and sets the #speakerMenu to display the first available option.
function filterVoices() { let langcode = languageMenu.value; voices = allVoices.filter(function (voice) { return langcode === "all" ? true : voice.lang.indexOf(langcode + "-") >= 0; }); createSpeakerMenu(voices); let t = blurbs[languageMenu.options[languageMenu.selectedIndex].text]; txtFld.value = t ? t : defaultBlurb; speakerMenu.selectedIndex = voiceIndex; } function createLanguageMenu() { let code = `<option selected value="all">Show All</option>`; let langnames = []; primaryLanguages.forEach(function(lobj,i) { langnames.push(langcodehash[lobj.substring(0,2)].name); }); langnames.sort(); langnames.forEach(function(lname,i) { let lcode = langhash[lname].code; code += `<option value=${lcode}>${lname}</option>`; }); languageMenu.innerHTML = code; } function selectLanguage() { filterVoices(); speakerMenu.selectedIndex = 0; }
In the utility functions section of the code toward the bottom, add the following code. This generic little utility will help you the next time you need to create a lookup table for an array of objects. In our case, we will use this to allow us to easily match a language code with its corresponding language name and vice versa.
function getLookupTable(objectsArray, propname) { return objectsArray.reduce((accumulator, currentValue) => (accumulator[currentValue[propname]] = currentValue, accumulator),{}); }
I added an array of text phrases, each of which is a translation of the English phrase, "I enjoy the traditional music of my native country." The language it’s displayed in will correspond to what’s selected in the language men.
Here we see the beauty of UTF-8 on full display. Above the getLanguagesTags() function, let’s add the code that generates all those translated blurbs. I only read Spanish, English, some Portuguese, and very little German, so I have to take on faith that Google Translate is providing accurate translations for the rest. If any of these is your native language, feel free to leave corrections in the comments.
function createBlurbs() { blurbs = { "Arabic" : "أنا أستمتع بالموسيقى التقليدية لبلدي الأم.", "Chinese" : "我喜歡我祖國的傳統音樂。", "Czech" : "Mám rád tradiční hudbu mé rodné země.", "Danish" : "Jeg nyder den traditionelle musik i mit hjemland.", "Dutch" : "Ik geniet van de traditionele muziek van mijn geboorteland.", "English" : "I enjoy the traditional music of my native country.", "Finnish" : "Nautin kotimaassani perinteistä musiikkia.", "French" : "J'apprécie la musique traditionnelle de mon pays d'origine.", "German" : "Ich genieße die traditionelle Musik meiner Heimat.", "Greek" : "Απολαμβάνω την παραδοσιακή μουσική της πατρίδας μου.", "Hebrew" : "אני נהנה מהמוסיקה המסורתית של מולדתי.", "Hindi" : "मैं अपने मूल देश के पारंपरिक संगीत का आनंद लेता हूं।", "Hungarian" : "Élvezem az én hazám hagyományos zenéjét.", "Indonesian" : "Saya menikmati musik tradisional negara asal saya.", "Italian" : "Mi piace la musica tradizionale del mio paese natale.", "Japanese" : "私は母国の伝統音楽を楽しんでいます。", "Korean" : "나는 내 조국의 전통 음악을 즐긴다.", "Norwegian Bokmal" : "Jeg liker den tradisjonelle musikken i mitt hjemland.", "Polish" : "Lubię tradycyjną muzykę mojego kraju.", "Portuguese" : "Eu gosto da música tradicional do meu país natal.", "Romanian" : "Îmi place muzica tradițională din țara mea natală.", "Russian" : "Мне нравится традиционная музыка моей родной страны.", "Slovak" : "Mám rád tradičnú hudbu svojej rodnej krajiny.", "Spanish" : "Disfruto de la música tradicional de mi país natal.", "Swedish" : "Jag njuter av traditionell musik i mitt hemland.", "Thai" : "ฉันเพลิดเพลินกับดนตรีดั้งเดิมของประเทศบ้านเกิดของฉัน", "Turkish" : "Ülkemdeki geleneksel müzikten zevk alıyorum." }; }
There’s one last thing: the numeric input for controlling the playback speed of the speech. Modify the talk() function to get the speech rate from the number input and we’re good to go!
Here’s the final product:
function talk() { ...// no changes except for the rateFld.value reference u.rate = Number(rateFld.value); speechSynthesis.speak(u); }
See the Pen Polyglot: Text-To-Speech in Multiple Languages by Steven Estrella (@sgestrella) on CodePen.
A Real World Application
My interest in this technology started many years ago in 1990 when I created a 26-lesson curriculum as part of my dissertation. It was delivered using my first programming language, HyperCard, on a Macintosh Plus which had a primitive text-to-speech feature. I used that feature to provide some feedback to the user while they progressed through the material. More recently, in 2018, I created a free progressive web app called Buenos Verbos that helps Spanish language students search and filter a database of 766 verbs. The chosen verb is then fully conjugated and the user can click the forms to hear them spoken. So perhaps web pages might like to talk and with some imagination you may find reasons to encourage them. The question is: what will you make your website say next?
The post Using the Web Speech API for Multilingual Translations appeared first on CSS-Tricks.
Using the Web Speech API for Multilingual Translations published first on https://deskbysnafu.tumblr.com/
0 notes