Using Javascript to change website language - javascript
I'm working on a GUI website that can use several languages. The original HTML-files I got to work with were totally static. So if translation was needed I had to parse through alle files, note where some words or terms were, collect them all hand them to the translation department and enter those translations in the new language files.
Since those files were totally static it meant having to translate whole sections several times. Not very effictient.
So now I am working on some kind of dictionary in Javascript, to just exchange the terms in those websites. Mostly it works this way:
var dicEnglish = {
term 1: "This is the English text"
Ref: "Another English text"
}
var dicFrench = {
term 1: "This is the French text"
Ref: "Another French text"
}
Which contains all the possible content that needs to be changed. Every candidate in the HTML-code gets a class="dicRef" id="l_dicTag_#"as identifier, which I slice down to the dictionary tag and exchange with the following code:
var imgSrc = "en";
var ActiveDic;
var langSel;
if(window.name){
langSel=window.name;
}
else{langSel="English";
}
function LangChange(){
langClass = document.getElementsByClassName("dicRef");
var i = langClass.length;
var Start, Stop, idSrc, idDic;
var navText;
switch(langSel){
case "French":
langSel="French";
imgSrc = "en";
navText="Anglais";
break;
case "English":
case "Anglais":
default:
langSel="English";
imgSrc = "fr";
navText="French";
break;
}
ActiveDic="dic"+langSel;
window.name=langSel;
while(i--){
idSrc = langClass[i].id;
Start=idSrc.indexOf("_")+1;
Stop=idSrc.lastIndexOf("_");
idDic=idSrc.slice(Start,Stop);
if(window[ActiveDic][idDic]){
document.getElementById(idSrc).innerHTML=window[ActiveDic][idDic];}
else{
document.getElementById(idSrc).innerHTML="N/A";
}
}
if(document.getElementById("imgSel")){
document.getElementById("imgSel").src="../../img/"+imgSrc+".gif";
}
if (document.getElementById("l_SelLang1_1")){
document.getElementById("l_SelLang1_1").innerHTML=navText;
}
}
The problem lies in the uniqueness of the id-tag. Since some terms can occur more than once and some are generated the counter is needed. I'd prefer to ommit the counter, but can't find any other identifier to sort out all target terms and change their content.
Since I want to be safe for the future I'd prefer a solution that makes it possible to handle a possible third language. Working with the inner HTML would need to tag the same term several times, once for each language.
So is there any way to target all terms to be exchanged more efficently and easily, or a better way to do it? I can only work with client-side solutions, so no PHP and so on.
No offense to the other answerers but storing the text in JavaScript or in data attributes is not good for search engines or disabled site visitors and offers no benefits while added unnecessarily complicated code. The best and most simple solution in my opinion is to make use of HTML lang attribute and use JavaScript to show and hide the desired language. This solution also gracefully degrades so if a site visitor has their JavaScript disabled it will still display the content. Here is my solution:
HTML
<button id="switch-lang">Switch Language</button>
<h1><span lang="en">Hello</span> <span lang="es">Hola</span></h1>
<p lang="en">I really enjoy coding.</p>
<p lang="es">Me gusta mucho la codificación.</p>
jQuery
$('[lang="es"]').hide();
$('#switch-lang').click(function() {
$('[lang="es"]').toggle();
$('[lang="en"]').toggle();
});
Then I would recommend adding HTML5 Geolocation to determine which language to show initially based on the users location in the world. I would also use Fontawesome language icon to show users they can switch languages in a way that is understandable by anyone: http://fontawesome.io/icon/language/
Here is the working code example at CodePen: https://codepen.io/codepajamas/pen/ZejaQz?editors=1010
Here is an additional example on code pen using a select menu to change between 3
(or more) languages: https://codepen.io/codepajamas/pen/NjGOMV
Updated Full Example with Geolocation and Cookies
I kept working on this and created an updated example switching between two languages Chinese and English (if you need more than two languages you would have to hide all languages and show only the one selected instead of using toggle the way I am). This code also detects if an existing cookie is already set for the language using jQuery Cookie. It also checks their geolocation if their browser supports it automatically setting the language to Chinese if they are in either Taiwan or China and defaults to English in all other countries. The code below is commented so you can see what each step is doing and hopefully be able to modify it to suit your needs. Here it is:
HTML
<button id="switch-lang">Switch Language Icon Here</button>
<h1><span lang="en">Hello</span> <span lang="zh">你好</span></h1>
<p lang="en">I really enjoy coding.</p>
<p lang="zh">我真的很喜歡編碼。</p>
jQuery
Note: this requires linking to not only jQuery but also jQuery Cookie
$(function () {
///// Language Switching (2 languages: English and Chinese). /////
// Initially disable language switching button.
$('#switch-lang').css({'pointer-events':'none',
'cursor':'default'}).attr('disabled','disabled');
function langButtonListen() {
$('#switch-lang').click(function (event) {
event.preventDefault();
$('[lang="zh"]').toggle();
$('[lang="en"]').toggle();
// Switch cookie stored language.
if ($.cookie('lang') === 'en') {
$.cookie('lang', 'zh', { expires: 7 });
} else {
$.cookie('lang', 'en', { expires: 7 });
}
});
// Enable lang switching button.
$('#switch-lang').css({'pointer-events':'auto',
'cursor':'pointer'}).removeAttr('disabled');
}
// Check if language cookie already exists.
if ($.cookie('lang')) {
var lang = $.cookie('lang');
if (lang === 'en') {
$('[lang="zh"]').hide();
langButtonListen();
} else {
$('[lang="en"]').hide();
langButtonListen();
}
} else {
// no cookie set, so detect language based on location.
if ("geolocation" in navigator) {
// geolocation is available
navigator.geolocation.getCurrentPosition(function (position) {
// accepted geolocation so figure out which country
var lat = position.coords.latitude,
lng = position.coords.longitude;
$.getJSON('http://maps.googleapis.com/maps/api/geocode/json?latlng='+lat+','+lng+'&sensor=true', null, function (response) {
var country = response.results[response.results.length-1].formatted_address;
if (country === 'Taiwan' || country === 'China') {
$('[lang="en"]').hide();
$.cookie('lang', 'zh', { expires: 7 });
langButtonListen();
} else {
$('[lang="zh"]').hide();
$.cookie('lang', 'en', { expires: 7 });
langButtonListen();
}
}).fail(function (err) {
console.log('error: '+err);
$('[lang="zh"]').hide();
$.cookie('lang', 'en', { expires: 7 });
langButtonListen();
});
},
function (error) {
if (error.code == error.PERMISSION_DENIED) {
// denied geolocation
$('[lang="zh"]').hide();
$.cookie('lang', 'en', { expires: 7 });
langButtonListen();
} else {
console.log('Unknown error. Defaulting to English!');
$('[lang="zh"]').hide();
$.cookie('lang', 'en', { expires: 7 });
langButtonListen();
}
});
} else {
// geolocation IS NOT available
$('[lang="zh"]').hide();
$.cookie('lang', 'en', { expires: 7 });
langButtonListen());
}
}
});
You can use data attributes: the fact that "HTML5 attributes are not supported in IE6 and IE7" means that you don't get the getAttribute() method or the dataset property for retrieving/accessing them. But you can still retrieve them as explained in this post.
<div id="geoff" data-geoff="geoff">
var geoff = document.getElementById("geoff");
alert(geoff.getAttribute("data-geoff"));
Even better, you can use jQuery .data() to support previous versions of IE.
Something along these lines should work:
<div data-translate="translation_key"></div>
$("[data-translate]").each(function(){
var key = $(this).data('translate');
$(this).html(dictionary[key][current_lang] || "N/A");
});
Working example: https://jsfiddle.net/x93oLad8/4/
One of the ways around this might be to use some sort of client-side templating system for your interface. That way you don't need to unnecessarily load your HTML with a bunch of data attributes detailing the language requirements, but just describe it once in the JavaScript and use a couple of functions to assist with the translation. I've coded up quick example below to show you what I mean.
Here's the dictionary object. It contains all the translations by country code. This means you don't need separate dictionaries for each country. This is important because it means we can use this single object structure very easily in out translation function as you'll see in a moment. It also means you can add as many languages and translations as you like.
var dict = {
en: {
'Hallo': 'Hallo',
'Goodbye': 'Goodbye',
'castle': 'castle'
},
fr: {
'Hallo': 'Bonjour',
'Goodbye': 'Au revoir',
'castle': 'chateau'
},
de: {
'Hallo': 'Hallo',
'Goodbye': 'Auf Wiedersehen',
'castle': 'schloss'
}
}
This is our country code and it relates directly to the country code key in our dictionary object:
var lang = 'fr';
The first of our two functions. This takes a template and a language and performs the translation, returning whatever's left (usually some sort of HTML as in our example).
function applyTemplate(tmpl, lang) {
// find all words within {{word}} a double set of curly braces
// (this format is similar to the handlebars templating engine)
var regex = /\{\{([a-zA-Z])\w+\}\}/g
// for each found word perform the translation and
// remove the curly braces
return tmpl.replace(regex, function (word) {
return translate(dict, lang, word.replace(/[\{\}]/g, ''));
});
}
The translate function takes the dictionary, the language, and a word and returns the translated word. Note that this is much easier with one object containing all the country translations.
function translate(dict, lang, word) {
return dict[lang][word];
}
Some HTML. Here is our template (display: none) and the output element. Note the words in the curly braces are the ones to be translated.
<div class="template"><div>{{Goodbye}}, {{castle}}</div></div>
<div id="translation"></div>
Finally, putting it all together:
// grab the template
var tmpl = document.querySelector('.template').textContent;
var translation = document.querySelector('#translation');
// grab our translated html and add it to the output element
var html = applyTemplate(tmpl, lang);
translation.insertAdjacentHTML('afterbegin', html);
DEMO
Now, obviously you don't have to use this method (there are dozens of JS templating engines out there), but templating is particularly useful for sites that need to use multiple languages. Many do this on the back end but, as you can see, it can be easily done client-side too.
Hope this was useful and given you a couple of different ideas on how you might approach your solution.
<script type="text/javascript">
// Load the Google Transliteration API
google.load("elements", "1", {
packages: "transliteration"
});
var transliterationControl;
function onLoad() {
var options = {
sourceLanguage: 'en',
destinationLanguage: ['hi','or','bn','ta','te'],
transliterationEnabled: true,
shortcutKey: 'ctrl+g'
};
// Create an instance on TransliterationControl with the required
// options.
transliterationControl =
new google.elements.transliteration.TransliterationControl(options);
// Enable transliteration in the textfields with the given ids.
var ids = [ "transl1", "transl2" ];
transliterationControl.makeTransliteratable(ids);
// Add the STATE_CHANGED event handler to correcly maintain the state
// of the checkbox.
transliterationControl.addEventListener(
google.elements.transliteration.TransliterationControl.EventType.STATE_CHANGED,
transliterateStateChangeHandler);
// Add the SERVER_UNREACHABLE event handler to display an error message
// if unable to reach the server.
transliterationControl.addEventListener(
google.elements.transliteration.TransliterationControl.EventType.SERVER_UNREACHABLE,
serverUnreachableHandler);
// Add the SERVER_REACHABLE event handler to remove the error message
// once the server becomes reachable.
transliterationControl.addEventListener(
google.elements.transliteration.TransliterationControl.EventType.SERVER_REACHABLE,
serverReachableHandler);
// Set the checkbox to the correct state.
document.getElementById('checkboxId').checked =
transliterationControl.isTransliterationEnabled();
// Populate the language dropdown
var destinationLanguage =
transliterationControl.getLanguagePair().destinationLanguage;
var languageSelect = document.getElementById('languageDropDown');
var supportedDestinationLanguages =
google.elements.transliteration.getDestinationLanguages(
google.elements.transliteration.LanguageCode.ENGLISH);
for (var lang in supportedDestinationLanguages) {
var opt = document.createElement('option');
opt.text = lang;
if (lang=="TAMIL" || lang=="TELUGU" || lang=="HINDI" || lang=="ORIYA" || lang=="BENGALI"){
opt.value = supportedDestinationLanguages[lang];
if (destinationLanguage == opt.value) {
opt.selected = true;
}
try {
languageSelect.add(opt, null);
} catch (ex) {
languageSelect.add(opt);
}
}//End of if
}
}
// Handler for STATE_CHANGED event which makes sure checkbox status
// reflects the transliteration enabled or disabled status.
function transliterateStateChangeHandler(e) {
document.getElementById('checkboxId').checked = e.transliterationEnabled;
}
// Handler for checkbox's click event. Calls toggleTransliteration to toggle
// the transliteration state.
function checkboxClickHandler() {
transliterationControl.toggleTransliteration();
}
// Handler for dropdown option change event. Calls setLanguagePair to
// set the new language.
function languageChangeHandler() {
var dropdown = document.getElementById('languageDropDown');
transliterationControl.setLanguagePair(
google.elements.transliteration.LanguageCode.ENGLISH,
dropdown.options[dropdown.selectedIndex].value);
}
// SERVER_UNREACHABLE event handler which displays the error message.
function serverUnreachableHandler(e) {
document.getElementById("errorDiv").innerHTML =
"Transliteration Server unreachable";
}
// SERVER_UNREACHABLE event handler which clears the error message.
function serverReachableHandler(e) {
document.getElementById("errorDiv").innerHTML = "";
}
google.setOnLoadCallback(onLoad);
Related
Change Country for Loqate Address Verification Control
I'm successfully using the Loqate Address Verfication control, and filter its address results down to a single country, chosen in a select control. However, when the country code in the select is changed, I've not been able to change the country filter in the Loqate address service to use the updated value. var addressControl; var avOptions = { suppressAutocomplete: false, setCountryByIP: false }; $(function () { addressControl = initialiseAddressValidation(); }); $("#CountryCode").change(function () { if (addressControl) { addressControl.destroy(); addressControl = null; } addressControl = initialiseAddressValidation(); }); function initialiseAddressValidation() { avOptions.key = $("#avKey").val(); avOptions.countries = { codesList: $("#CountryCode").val() }; //other config truncated } Loqate's docs suggest that the .destroy() method I'm calling on change of the select should remove the control, and I tried setting it to null, before recreating it with the new country code value, but the address results it returns are still for the original country is was initialised with on load. Does anyone know how to set a new country filter to the Loqate address verifier without completely reloading the page? My working solution, based on #SlapdashFox 's invaluable assistance. Done this way to avoid having to completely re-create the control on every change of country $("#CountryCode").change(function () { if (addressControl) { addressControl.options.search.countries = $("#CountryCode").val(); } else { addressControl = initialiseAddressValidation(); } });
Looks like you're changing the wrong value here, you want to be changing the search.countries attribute within your options object. In your above example you could do this as follows: function initialiseAddressValidation() { avOptions.key = $("#avKey").val(); avOptions.search = { countries: $("#CountryCode").val() }; }
Determine the "lang" attribute of <html> .on('change') and display items accordingly
I have a site that uses a language translator plugin on Wordpress. On this site, I have an image of a "certification badge" that has English on the image, but I also have a version that is in Spanish instead. I am trying to force the English version to hide, and the Spanish version to show upon change of the HTML attribute 'lang' and have gotten most of the work completed and working, only it is working backwards. It all starts out fine, but when I change the site language to Spanish, the badges don't change. Then when I change back to English, the badges change to the Spanish version... What am I missing? The first block of code is for when the page initially loads, and the second is looking for the change event from the plugin. // Change PPE Badges based on language if (jQuery("html").attr("lang").toLowerCase() === 'es' || jQuery("html").attr("lang").toLowerCase() === 'es-mx') { jQuery('.badgeSpanish').show(); jQuery('.badgeEnglish').hide(); console.log('Language: Spanish'); } else { jQuery('.badgeSpanish').hide(); console.log('Language: English'); } jQuery(document).on('change', 'html', function() { if (jQuery("html").attr("lang").toLowerCase() === 'es' || jQuery("html").attr("lang").toLowerCase() === 'es-mx') { console.log('Language: Spanish'); jQuery('.badgeSpanish').show(); jQuery('.badgeEnglish').hide(); } else if (jQuery("html").attr("lang").toLowerCase() === 'en' || jQuery("html").attr("lang").toLowerCase() === 'en-us' || jQuery("html").attr("lang").toLowerCase() === 'auto') { console.log('Language: English'); jQuery('.badgeEnglish').show(); jQuery('.badgeSpanish').hide(); } }); Here is the example site to demonstrate: http://harsini.staging.wpengine.com/
Give this a shot instead: function updateBadge(){ // gets rid of the need to constantly retrieve and mutilate the language var language = jQuery("html").attr("lang").toLowerCase(); // a switch becomes useful because you can check against multiple values // and set a default if need be switch(language){ case "es": case "es-mx": jQuery(".badgeEnglish").hide(); jQuery(".badgeSpanish").show(); break; default: jQuery(".badgeSpanish").hide(); jQuery(".badgeEnglish").show(); } } jQuery(document).ready(function(){ // update the badge as soon as the document is ready updateBadge(); // and update it every time the language is changed jQuery(this).on("change", "html", updateBadge); });
Sharepoint 2013 list - how to conditionally colour java script calculated fields in sharepoint list?
I have some script that I've been working on in a Sharepoint 2013 list, it fundamentally checks some criteria against a list and returns "Pending", "Complete", "Overdue", or "Missing Due Date". In order to check against the current date, I've used some Java script found online and modified it to work for my list. The only problem is, I'm also trying to color the text: Red if the value is 'Overdue' Green if the value is 'Complete' Black for all other cases. The code returns the values correctly and colors 'Complete' values green, but I cannot get the 'Overdue' values to the colour red. Here is my code: ="<div style='text-align:left; color:" &IF([Completed Date]="","" &"<img src='/_layouts/images/blank.gif' onload=""{" &"var SPday=new Date();" &"SPday.setFullYear("&YEAR(DueDate)&","&MONTH(DueDate)-1&","&DAY(DueDate)&");" &"var Days=Math.round((SPday.getTime()-new Date().getTime())/86400000);" &"this.parentNode.innerHTML=((Days<0)?'red':'black');" &"}""" &"","green") &"'>" &IF([Completed Date]="",IF(ISBLANK(DueDate),"Missing Due date","<img src='/_layouts/images/blank.gif' onload=""{" &"var SPday=new Date();"&"SPday.setFullYear("&YEAR(DueDate)&","&MONTH(DueDate)-1&","&DAY(DueDate)&");" &"var Days=Math.round((SPday.getTime()-new Date().getTime())/86400000);" &"this.parentNode.innerHTML=((Days<0)?'Overdue':'Pending');" &"}"">"),"Complete") &"</div>" Can this be done this way or should I be trying another way?
For such complex logic, you'd better use CSR. (function () { 'use strict'; var CustomCtx = {}; /** * Initialization */ function init() { CustomCtx.Templates = {}; CustomCtx.Templates.Fields = { // update ChangeCompleted as your filed static name 'ChangeCompleted': { 'View': customDisplay } }; // Register the custom template SPClientTemplates.TemplateManager.RegisterTemplateOverrides(CustomCtx); } /** * Rendering template */ function customDisplay(ctx) { //your logic here } // Run our intiialization init(); })(); Links for your reference. https://social.msdn.microsoft.com/Forums/office/en-US/154c3ed9-9e22-4fda-aba2-b7b12cc4a509/changing-text-color-in-sharepoint-based-on-column-value?forum=sharepointdevelopment https://www.codeproject.com/Articles/620110/SharePoint-Client-Side-Rendering-List-Views
How Can I Insert My Javascript Into My Drupal Site/Node
I'm trying to insert a cookie that is provided by a video host that will resume a video where the user left off. They have an example that obviously works. When trying to insert this into my Drupal site, the cookie won't work. The video just starts back at the beginning. I have enabled "PHP input filter", as I read that I needed to do that for drupal to insert the script. Please see the code that is in my node below. Can anyone help me figure out why this isn't working, how to get it to work, or a better way of doing this with Drupal? Thank you, <script type="text/javascript"> wistiaEmbed.ready( function() { var all_cookies = document.cookie.split(';'), // gets the value of the cookies on the page cookie_str = "resume_video=", resume_cookie = does_resume_cookie_exist(all_cookies); function does_resume_cookie_exist(cookie_arr) { var i, curr_string, found; for (i = 0; i < cookie_arr.length; i++) { curr_string = cookie_arr[i]; if (curr_string.substring(0,5) === cookie_str.substring(0,5)) { // we've found it! found = curr_string; break; } } return found; } function set_cookie_time(t) { document.cookie = cookie_str + t.toString(); // this takes the time (t) and sets the cookie with that time } if (resume_cookie) { num = resume_cookie.split('=')[1]; start_time = parseInt(num); wistiaEmbed.time(start_time).play(); // plays the video at the specific time defined in the cookie upon return to the page } else { set_cookie_time(0); // places a cookie on the visitor wistiaEmbed.play(); // starts the video from the beginning } wistiaEmbed.bind("timechange", function(t) { // on timechange, reset cookie set_cookie_time(t); }); wistiaEmbed.bind("end", function() { // if person has watched the entire video, sets the video to beginning upon retun set_cookie_time(0); }); }); </script> <div id="wistia_npcc5k96s9" class="wistia_embed" style="width:640px;height:508px;"> </div> <script charset="ISO-8859-1" src="http://fast.wistia.com/assets/external/E-v1.js"> </script> <script> wistiaEmbed = Wistia.embed("npcc5k96s9"); </script>**strong text**
What version of drupal are you using? Does the code that you gave actually output in your request response? There are several solutions to this (IMO). If the code is showing up in the response, it could be some other javascript error that preventing your code from executing. If that snippet of code is applicable to all nodes of that type you can use the node_view hook in order to inject your code on that node type, for example (I am assuming D7): function mymodule_node_view($node, $view_mode) { if($node->type =='video_page') { drupal_add_js('resume_video.js'); // this js holds your code snippet } } Here's a reference that could help you out https://api.drupal.org/api/drupal/modules%21node%21node.api.php/function/hook_node_view/7 You can similarly inject that snippet of code at the theme layer using a theme hook, probably hook_preprocess https://api.drupal.org/api/drupal/modules!system!theme.api.php/function/hook_preprocess/7 Hope that helps.
CodeMirror with spell checker
I would like to use the functionality of CodeMirror (such as linenumbering, wrapping, search, etc.) for plain text, without particular need of code highlightening but instead with Google Chrome spell checker or some other natural language (especially English) spell checking activated (I do not need to have it work on other browsers). How can I do this? Is it possible to write a plain text mode add-on that enables spell checking?
I actually integrated typo.js with CodeMirror while coding for NoTex.ch; you can have a look at it here CodeMirror.rest.js; I needed a way to get the reStructuredText markup spell checked, and since I use CodeMirror's excellent syntax highlighting capabilities, it was quite straight forward to do. You can check the code at the provided link, but I'll summarize, what I've done: Initialize the typo.js library; see also the author's blog/documentation: var typo = new Typo ("en_US", AFF_DATA, DIC_DATA, { platform: 'any' }); Define a regular expression for your word separators: var rx_word = "!\"#$%&()*+,-./:;<=>?#[\\\\\\]^_`{|}~"; Define an overlay mode for CodeMirror: CodeMirror.defineMode ("myoverlay", function (config, parserConfig) { var overlay = { token: function (stream, state) { if (stream.match (rx_word) && typo && !typo.check (stream.current ())) return "spell-error"; //CSS class: cm-spell-error while (stream.next () != null) { if (stream.match (rx_word, false)) return null; } return null; } }; var mode = CodeMirror.getMode ( config, parserConfig.backdrop || "text/x-myoverlay" ); return CodeMirror.overlayMode (mode, overlay); }); Use the overlay with CodeMirror; see the user manual to figure out how exactly you do this. I've done it in my code so you could check it out there too, but I recommend the user manual. Define CSS class: .CodeMirror .cm-spell-error { background: url(images/red-wavy-underline.gif) bottom repeat-x; } This approach works great for German, English and Spanish. With the French dictionary typo.js seems to have some (accent) problems, and languages like Hebrew, Hungarian, and Italian - where the number of affixes is long or the dictionary is quite extensive - it does not work really, since typo.js at its current implementation uses too much memory and is too slow. With German (and Spanish) typo.js can block the JavaScript VM for a few hundred milliseconds (but only during initialization!), so you might want to consider background threads with HTML5 web workers (see CodeMirror.typo.worker.js for an example). Further typo.js does not seem to support Unicode (due to JavaScript restrictions): At least, I did not manage to get it to work with non-Latin languages like Russian, Greek, Hindi etc. I've not refactored the described solution into a nice separate project apart from (now quite big) NoTex.ch, but I might do it quite soon; till then you've to patch your own solution based on the above description or hinted code. I hope this helps.
In CodeMirror 5.18.0 and above, you can set inputStyle: 'contenteditable' with spellcheck: true to be able to use your web browser's spellcheck features. For example: var myTextArea = document.getElementById('my-text-area'); var editor = CodeMirror.fromTextArea(myTextArea, { inputStyle: 'contenteditable', spellcheck: true, }); The relevant commits that made this solution possible are: Make inputStyle an option, move relevant logic into TextareaInput (CodeMirror 5.0.0, February 2015) Add experimental spellcheck option (CodeMirror 5.18.0, August 2016)
This is a working version of hsk81's answer. It uses CodeMirror's overlay mode, and looks for any word inside quotes, html tags, etc. It has a sample typo.check that should be replaced with something like Typo.js. It underlines unknown words with a red squiggly line. This was tested using an IPython's %%html cell. <style> .CodeMirror .cm-spell-error { background: url("https://raw.githubusercontent.com/jwulf/typojs-project/master/public/images/red-wavy-underline.gif") bottom repeat-x; } </style> <h2>Overlay Parser Demo</h2> <form><textarea id="code" name="code"> </textarea></form> <script> var typo = { check: function(current) { var dictionary = {"apple": 1, "banana":1, "can't":1, "this":1, "that":1, "the":1}; return current.toLowerCase() in dictionary; } } CodeMirror.defineMode("spell-check", function(config, parserConfig) { var rx_word = new RegExp("[^\!\"\#\$\%\&\(\)\*\+\,\-\.\/\:\;\<\=\>\?\#\[\\\]\^\_\`\{\|\}\~\ ]"); var spellOverlay = { token: function (stream, state) { var ch; if (stream.match(rx_word)) { while ((ch = stream.peek()) != null) { if (!ch.match(rx_word)) { break; } stream.next(); } if (!typo.check(stream.current())) return "spell-error"; return null; } while (stream.next() != null && !stream.match(rx_word, false)) {} return null; } }; return CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || "text/html"), spellOverlay); }); var editor = CodeMirror.fromTextArea(document.getElementById("code"), {mode: "spell-check"}); </script>
CodeMirror is not based on an HTML textarea, so you can't use the built-in spell check You could implement your own spell check for CodeMirror with something like typo.js I don't believe anyone has done this yet.
I wrote a squiggly underline type spell checker a while ago. It needs a rewrite to be honest I was very new to JavaScript then. But the principles are all there. https://github.com/jameswestgate/SpellAsYouType
I created a spellchecker with typos suggestions/corrections: https://gist.github.com/kofifus/4b2f79cadc871a29439d919692099406 demo: https://plnkr.co/edit/0y1wCHXx3k3mZaHFOpHT Below are relevant parts of the code: First I make a promise to get load the dictionaries. I use typo.js for the dictionary, loading can take a while if they are not hosted locally, so it is better to start the loading as soon as your starts before login/CM initializing etc: function loadTypo() { // hosting the dicts on your local domain will give much faster results const affDict='https://rawgit.com/ropensci/hunspell/master/inst/dict/en_US.aff'; const dicDict='https://rawgit.com/ropensci/hunspell/master/inst/dict/en_US.dic'; return new Promise(function(resolve, reject) { var xhr_aff = new XMLHttpRequest(); xhr_aff.open('GET', affDict, true); xhr_aff.onload = function() { if (xhr_aff.readyState === 4 && xhr_aff.status === 200) { //console.log('aff loaded'); var xhr_dic = new XMLHttpRequest(); xhr_dic.open('GET', dicDict, true); xhr_dic.onload = function() { if (xhr_dic.readyState === 4 && xhr_dic.status === 200) { //console.log('dic loaded'); resolve(new Typo('en_US', xhr_aff.responseText, xhr_dic.responseText, { platform: 'any' })); } else { console.log('failed loading aff'); reject(); } }; //console.log('loading dic'); xhr_dic.send(null); } else { console.log('failed loading aff'); reject(); } }; //console.log('loading aff'); xhr_aff.send(null); }); } Second I add an overlay to detect and mark typos like this: cm.spellcheckOverlay={ token: function(stream) { var ch = stream.peek(); var word = ""; if (rx_word.includes(ch) || ch==='\uE000' || ch==='\uE001') { stream.next(); return null; } while ((ch = stream.peek()) && !rx_word.includes(ch)) { word += ch; stream.next(); } if (! /[a-z]/i.test(word)) return null; // no letters if (startSpellCheck.ignoreDict[word]) return null; if (!typo.check(word)) return "spell-error"; // CSS class: cm-spell-error } } cm.addOverlay(cm.spellcheckOverlay); Third I use a list box to show suggestions and fix typos: function getSuggestionBox(typo) { function sboxShow(cm, sbox, items, x, y) { let selwidget=sbox.children[0]; let options=''; if (items==='hourglass') { options='<option>⌛</option>'; // hourglass } else { items.forEach(s => options += '<option value="' + s + '">' + s + '</option>'); options+='<option value="##ignoreall##">ignore all</option>'; } selwidget.innerHTML=options; selwidget.disabled=(items==='hourglass'); selwidget.size = selwidget.length; selwidget.value=-1; // position widget inside cm let cmrect=cm.getWrapperElement().getBoundingClientRect(); sbox.style.left=x+'px'; sbox.style.top=(y-sbox.offsetHeight/2)+'px'; let widgetRect = sbox.getBoundingClientRect(); if (widgetRect.top<cmrect.top) sbox.style.top=(cmrect.top+2)+'px'; if (widgetRect.right>cmrect.right) sbox.style.left=(cmrect.right-widgetRect.width-2)+'px'; if (widgetRect.bottom>cmrect.bottom) sbox.style.top=(cmrect.bottom-widgetRect.height-2)+'px'; } function sboxHide(sbox) { sbox.style.top=sbox.style.left='-1000px'; } // create suggestions widget let sbox=document.getElementById('suggestBox'); if (!sbox) { sbox=document.createElement('div'); sbox.style.zIndex=100000; sbox.id='suggestBox'; sbox.style.position='fixed'; sboxHide(sbox); let selwidget=document.createElement('select'); selwidget.multiple='yes'; sbox.appendChild(selwidget); sbox.suggest=((cm, e) => { // e is the event from cm contextmenu event if (!e.target.classList.contains('cm-spell-error')) return false; // not on typo let token=e.target.innerText; if (!token) return false; // sanity // save cm instance, token, token coordinates in sbox sbox.codeMirror=cm; sbox.token=token; let tokenRect = e.target.getBoundingClientRect(); let start=cm.coordsChar({left: tokenRect.left+1, top: tokenRect.top+1}); let end=cm.coordsChar({left: tokenRect.right-1, top: tokenRect.top+1}); sbox.cmpos={ line: start.line, start: start.ch, end: end.ch}; // show hourglass sboxShow(cm, sbox, 'hourglass', e.pageX, e.pageY); // let the ui refresh with the hourglass & show suggestions setTimeout(() => { sboxShow(cm, sbox, typo.suggest(token), e.pageX, e.pageY); // typo.suggest takes a while }, 100); e.preventDefault(); return false; }); sbox.onmouseleave=(e => { sboxHide(sbox) }); selwidget.onchange=(e => { sboxHide(sbox) let cm=sbox.codeMirror, correction=e.target.value; if (correction=='##ignoreall##') { startSpellCheck.ignoreDict[sbox.token]=true; cm.setOption('maxHighlightLength', (--cm.options.maxHighlightLength) +1); // ugly hack to rerun overlays } else { cm.replaceRange(correction, { line: sbox.cmpos.line, ch: sbox.cmpos.start}, { line: sbox.cmpos.line, ch: sbox.cmpos.end}); cm.focus(); cm.setCursor({line: sbox.cmpos.line, ch: sbox.cmpos.start+correction.length}); } }); document.body.appendChild(sbox); } return sbox; } Hope this helps !