Behavior, Content, Money – 3 Things you should never give away for free!!!

BCmoney MobileTV

WordReference API – AJAX SDK & Widget

Posted by bcmoney on June 1, 2011 in JavaScript, JSON, Semantic Web, Web Services with 22 Comments


No Gravatar
Languages

Yesterday I wrote about the Google API shutdown. It seems that I was wrong in that post about Wordreference not having an API, just a few days earlier founder Michael Kellogg announced the introduction of the brand-spanking new Wordreference API.

Like a dunce I contacted Michael by email to learn about this new revelation without double-checking the site itself (I knew in the past they didn’t have one, as I had checked just a month or so before out of curiosity). It seems that the API was in-development for quite some time, but was rushed out the door on the news of the Google Translate API shutdown. I can easily say that nothing was lost in the final rush, the API is excellent! There are both HTML or JSON versions of the API currently supported and the RESTful URIs make the whole thing quite intuitive to work with.

Awesome! So, feeling a bit dejected earlier this week, yet today having newfound hope for the future of online Translations, I’ve decided to build an SDK and example Widget for the newly released WordReference Dictionary API. I wanted to show multiple ways of interacting with the Translation API (including JSONp, jQuery, YQL, Server Proxy, Cache, etc) all directly via JavaScript. Also, with the completed Widget example, I wanted to show how you can use the API to simulate a full-text translation in real-time, without there being a full-text machine translation feature supported by WordReference (which works on words or phrase lookup only, since it is primarily a dictionary, not a translation service). The only downside of this approach, is that it may result in ALOT of requests to the API.

The CSS presentation styling is extremely minimal, but in the near future I’d consider adding more CSS3 hip features like text-shadows, gradients, transitions/fades and rounded corners. In the meantime, we get this basic CSS:

#wordreference.widget { font-family:'American Typewriter',Verdana,Arial,serif; }
#wordreference.widget legend { background:url('../images/logo_s.jpg') no-repeat; height:32px; font-size:1.4em; padding-left:38px; }
#wordreference.widget a { text-decoration:none; }
#wordreference.widget img { border:0; }
#wordreference.widget optgroup { font-size:8pt }
#wordreference.widget #from { float:left; padding:10px }
#wordreference.widget #swap { float:left; padding:10px; vertical-align:middle; font-size:0.7em; }
#wordreference.widget #to { float:left; padding:10px }
#wordreference.widget #text, #wordreference.widget #translation { color:#000; }
#wordreference.widget .unclicked { color:#ccc; }
#wordreference.widget .clear { clear:both; }
#wordreference.widget .acknowledgement { font-size:0.8em; color:#ccc; }
#wordreference.widget .acknowledgement a { color:#cccccc; border-bottom:1px dotted #ccc; }

Most of the behavior and thus real action happens in the following JavaScript:

/**
 * wordreference
 *   Implements the WordReference API (version 0.8 - 2011-06-10)
 *
 * @license http://www.gnu.org/licenses/lgpl.html
 * @author bcmoney
 * @credit http://bcmoney.tv/blog  *unless otherwise noted*
 */
JSONP_METHOD = false; //set this to true when using JSONp
/*****************************************************************************/
/** utilities for domain and path retrieval, plus data validation checks    **/

/*
 * getDomain
 *   Pulls just the domain out of the specified URL
 * @param _url String  OPTIONAL Full URL to check the domain of (defaults to current URL)
 * @return String  split section of the URL representing the domain without protocols, paths, params or hashes (i.e. example.com)
 */
function getDomain(_url) {
    var url = (!empty(_url)) ? _url : window.location.href;
    return url.split(//+/g)[1];
}

/*
 * getPath
 *   Lists the path of the specified URL
 * @param _url String  OPTIONAL Full URL to check the domain of (defaults to current URL)
 * @return String  pathname after the URL's domain
 */
function getPath(_url) {
    var url = (!empty(_url)) ? _url : window.location.href;
    a = document.createElement('a');
    a.href = url;
    path = a.pathname;
    document.removeChild(this.a);
    return path;
}

/* GUP
 *   Get Url Paramaters
 * USAGE:
 *  var param1Val = gup('param1');
 * @param name String  name of a query parameter to look for a value of in URL
 * @param _url String  OPTIONAL, full URL to check for parameters in (defaults to the current URL)
 * @author Justin Barlow
 * @license http://creativecommons.org/publicdomain/mark/1.0/
 * @credit http://www.netlobo.com/url_query_string_javascript.html
 */
function gup(name, _url) {
    var url = (!empty(_url)) ? _url : window.location.href;
    name = name.replace(/[[]/,"\[").replace(/[]]/,"\]");
    var regexS = "[\?&]"+name+"=([^&#]*)";
    var regex = new RegExp(regexS);
    var results = regex.exec(url);
    if(empty(results)) {
        return "";
    }
    else {
        return results[1];
    }
}

/*
 * empty
 *   Simulate PHP emptiness check
 * @param mixed_var  Any combination of Objects (i.e. Array, String, Int, Double etc)
 * @author Philippe Baumann, Kevin van Zonneveld, Marc Jansen, et. al
 * @license http://www.opensource.org/licenses/mit-license.php
 * @credit http://phpjs.org/functions/empty:392
 */
function empty (mixed_var) {
    var key;
    if (mixed_var === "" || mixed_var === 0 || mixed_var === "0" || mixed_var === null || mixed_var === false || typeof mixed_var === 'undefined') {
        return true;
    }
    if (typeof mixed_var == 'object') {
        for (key in mixed_var) {
            return false;
        }
        return true;
    }
    return false;
}

/*
 * nodeExists
 * @author p00ya
 * @credit http://stackoverflow.com/questions/1129209/check-if-json-keys-nodes-exist
 */
function nodeExists(p, a) {
    for (i in a) {
        var key = a[i];
        if (p[key] == null) {
            return '';
        }
        p = p[key];
    }
    return p;
}

/*
 * console
 *   Replace missing console (log, info, error, warn, etc) functions
 * @license http://developer.yahoo.com/yui/license.html
 * @author Patrick Donelan
 * @credit http://blog.patspam.com/2009/the-curse-of-consolelog
 */
if (!("console" in window) || !("firebug" in console)) {
    var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", "profile", "profileEnd"];
    window.console = {};
    for (var i = 0, len = names.length; i < len; ++i) {
        window.console[names[i]] = function(){ };
    }
}

/************************************************************/
/* AJAX cross-browser helper utilities */
/************************************************************/
/*
 * loadDoc
 *   Loads a Document (i.e. JSON or HTML) into a Text string
 * @param docURL String  a link to the Document to load
 * @return responseText
 */
function loadDoc(docURL) {
    if (window.XMLHttpRequest) {		// FireFox, Opera, Chrome, Safari
        xhttp = new XMLHttpRequest();
    }
    else {								// Internet Explorer
        xhttp = new ActiveXObject("Microsoft.XMLHTTP");
    }
    xhttp.open("GET",docURL,false);
        xhttp.setRequestHeader("Content-type", "text/plain; charset=utf-8");//try to force response type
    xhttp.send();
    return xhttp.responseText;
}

/*
 * loadXMLDoc
 *   Loads an XML Document into an XML (DOM) Object
 * @param docURL String  a link to the XML Document to load
 * @return responseXML
 */
function loadXMLDoc(docURL) {
    if (window.XMLHttpRequest) {		// FireFox, Opera, Chrome, Safari
        xhttp = new XMLHttpRequest();
    }
    else {								// Internet Explorer
        xhttp = new ActiveXObject("Microsoft.XMLHTTP");
    }
    xhttp.open("GET",docURL,false);
    xhttp.send();
    return xhttp.responseXML;
}

/*
 * loadXMLString
 *   Loads an XML String into an XML (DOM) Object
 * @param txt String  text to the Document to load
 * @return responseText
 */
function loadXMLString(txt) {
    if (window.DOMParser) {		// FireFox, Opera, Chrome, Safari
        parser=new DOMParser();
        xmlDoc=parser.parseFromString(txt,"text/xml");
    }
    else { 						// Internet Explorer
        xmlDoc=new ActiveXObject("Microsoft.XMLDOM");
        xmlDoc.async = "false";
        xmlDoc.loadXML(txt);
    }
    return xmlDoc;
}

/************************************************************/	
    API = 'http://api.wordreference.com';  //base URL for the API request
    API_VERSION = '0.8';                   //leave blank '' for latest version
    API_KEY = '82972';                     //NOW REQUIRED... back off, get your own API Key or userID!
    API_FORMAT = 'json';                   //'json' for JSON response type, or, blank '' for HTML 
    CALLBACK = '?callback=displayTranslation';  //name of your callback function
var API_URL = API + '/' + API_VERSION + '/' + API_KEY + '/' + API_FORMAT + '/';
/************************************************************/	

/**
 * wordreferenceAPI
 *   call request to WordReference API with our input data
 * @param language String representing the four character translation code (i.e. "enfr" or "enja")
 * @param word String representing the word (term) to lookup a translation for
 */
function wordreferenceAPI(language, word) {
    var api_endpoint = API_URL + language + '/' + encodeURIComponent(word) + API_URL_CREDENTIALS; //DEBUG: console.log(api_enpoint);
    /******************************PROXY*******************************/
    json = loadDoc("proxy.php?url="+api_endpoint); /* using a server-side PROXY, 1 of 5 alternatives that may be used (JSONp, jQuery, YQL, Server-side PROXY, or Local CACHE), see "tests/basic.html" */
    displayTranslation(json);
}

/**
 * displayTranslation
 *   process translation results and output the response
 * @param json String  takes in a String of JSON and parses it into an accsesible Object (NOTE: may need to pass your JSON through JSON.stringify)
 */
function displayTranslation(json) {
    var wordreference = JSON.parse(json); //use default "json2.js" parser (just in case JSON object not available)
    var term = ''; var sense = '';     

    /* parse JSON response */
    try {
        if (nodeExists(wordreference, ["term0", "Entries"])) {
        alert(wordreference.length);
            term = wordreference.term0.Entries[0].FirstTranslation.term;
            sense = wordreference.term0.Entries[0].FirstTranslation.sense;
            console.info(term + ' ' + sense);
        }
        else if (nodeExists(wordreference, ["term0", "PrincipalTranslations"])) {
            term = wordreference.term0.PrincipalTranslations[0].FirstTranslation.term;
            sense = wordreference.term0.PrincipalTranslations[0].FirstTranslation.sense;
            console.info(term + ' ' + sense);
        }
        else if (wordreference.Note.indexOf("No translation was found") !== -1) {
            term = lookahead;
            console.warn(wordreference.Note);
        }
        else {
            console.log('ERROR: Invalid input characters or looked up a word with no entry');
        }
    }
    catch (err) {
        console.error('ERROR: ' + err.description);
    }
    document.getElementById('translation').value += term + ' ';
        if (JSONP_METHOD) { jsonp.removeScriptTag(); } //JSONP only... remove the added SCRIPT tag to ensure limited lifetime of injected JS code
}

/*****************************************************************************/
/**                                WIDGET                                    **/
/*****************************************************************************/
//Somewhat regrettable but necssary (for now) GLOBALs
var unclicked = true;   //boolean to register the first click on the textareas
var lookahead = '';     //placeholder for the letters typed so far (in current word)
var caret_position = 0; //current caret position

/**
 * swapTranslationLanguage
 *   Exchange the selected indexes and values of two HTML Form Select inputs
 * @param ele1 String  name of the first HTML Form DOM Element to swap
 * @param ele2 String  name of the second HTML Form DOM Element to swap
 * @return boolean  true if the value is identical to the placeholder text, false otherwise
 */
function swapTranslationLanguage(ele1,ele2) {
    var element1 = (!empty(document.getElementById(ele1))) ? document.getElementById(ele1) : document.getElementById("language_to");
    var element2 = (!empty(document.getElementById(ele2))) ? document.getElementById(ele2) : document.getElementById("language_from");
    var swapMem = element1.value;
    setFormSelectValue(ele1,element2.value);
    setFormSelectValue(ele2,swapMem);
}

/**
 * setFormSelectValue
 *   Set the value of an HTML Form DOM input element
 * @param ele Element  representing the HTML Form DOM Element to clear text of
 * @param txt String  placeholder text to check against Form's current text
 * @return boolean  true if the value is identical to the placeholder text, false otherwise
 */
function setFormSelectValue(ele,txt) {
    if(document.getElementsByName(ele)!=null && document.getElementsByName(ele)[0]!=null) {
        var swapMem = document.getElementsByName(ele)[0];
        for(index=0; index < swapMem.length; index++) {
            if(swapMem[index].value == txt) {
                swapMem.selectedIndex = index;
            }
        }
    }
}

/**
 * clearDefaultText
 *   Clear any placeholder text that was stuck into a form input by default
 * @param ele Element  representing the HTML Form DOM Element to clear text of
 * @param txt String  placeholder text to check against Form's current text
 * @return boolean  true if the value is identical to the placeholder text, false otherwise
 */
function clearDefaultText(ele, txt) {
    if (!empty(ele) && unclicked) {
        if (ele.value == txt) {
            ele.value='';
            return true;
        }
    }
}

/**
 * lookupTranslation
 *   Perform a real-time interactive Dictionary lookup on the typed word
 * @param from String
 * @param to String
 * @param text String
 * @return translated  String Translation for performing an update on the current Translation
 */
function lookupTranslation(from, to, text) {
    var txt = (!empty(text) && !empty(document.getElementById(text).value)) ? document.getElementById(text).value : 'default ';
//DEBUG:  console.log(caret_position + "n" + lookahead + "n" + from + "|" + to + "n" + text);
        word = txt.substring(caret_position+1, txt.lastIndexOf(' ')+1);
            fromLanguage = (!empty(from) && !empty(document.getElementById(from).value)) ? document.getElementById(from).value : "en";
            toLanguage = (!empty(to) && !empty(document.getElementById(to).value)) ? document.getElementById(to).value : "fr";
            lang = fromLanguage + toLanguage;
        translated = wordreferenceAPI(lang, word);
        lookahead = '';
    return translated;
}  

IE5 = (document.all) ? 1 : 0; // Must capture the event if it is NS6.0 and NS4.0+
if(!IE5) { document.captureEvents(Event.KEYPRESS); } 

/**
 * checkKeyPress
 *   Check whether or not a key that was pressed was SPACEBAR or ENTER
 * @param evt Event  runtime event giving access to each typed key
 * @return boolean  true if SPACEBAR or ENTER keys were pressed, false otherwise
 */
function checkKeyPress(evt) {
    evt = (evt) ? evt : ((event) ? event : null);
    pressedKey = !(IE5) ? (evt.charCode || evt.keyCode) : evt.keyCode; // Grab the ascii code of the key that was pressed
    if (String.fromCharCode(pressedKey) == ' ' || pressedKey == 32 || String.fromCharCode(pressedKey) == 'n' || pressedKey == 13) {
      self.focus(); // Display the key and its ASCII code
      lookupTranslation('language_from', 'language_to', 'text');
      caret_position++;
    }
    else if (pressedKey == 8 && (lookahead.substring(caret_position-1,caret_position) !== ' ')) {
       lookahead = lookahead.substring(0,caret_position-1);
       caret_position--;
       return false;
    }
    else {
      lookahead += String.fromCharCode(pressedKey);
      caret_position++;
      return false;
    }
}

The structure for the HTML is a plain and simple HTML5 skeleton (without any exciting new features) as follows:

<!DOCTYPE html>
<html>
<head>
  <title>WordReference AJAX SDK / Widget</title>
  <link rel="shortcut icon" href="favicon.ico" />
  <link rel="stylesheet" type="text/css" media="all" href="style/widget.css" />
</head>
<body>

Translate
<>
Translations © WordReference.com
</body> </html>

As you can see in the following demo, there are still a few bugs to be worked out on both the SDK end and the API end, but the premise is there so I’m releasing it anyway!


-or-

There were some small gotchas, as with the first-run of any API. Here are a few of the (very minor nitpicky things) I noticed:

  • Spanish-English dictionary (esen) – Not supported yet, but supposedly coming soon!
  • First Word – Queries like “Hello World” don’t seem to work properly as the API just grabs the first matched word (hello), thus each word should be called separately and its hard to tell when to do that, since other queries for popular sayings like “the cat’s meow” work fine … knowing me I could be doing something wrong like not encoding properly though, if there was a way to specify that you are looking for a full-term match not just partial that might quickly solve this one without needing too many server-side coding changes, something like &match=phrase as an optional additional URL parameter on each request?
  • Error/Status Codes – It would be helpful to see a brief error code and/or short status code explanation (i.e. “no definition available for that term”, “no match”, “not a known word”, “unable to decode”, etc… so we could double check encoding, punctuation, spelling/typos, but I can see how its hard to make that work, some kind of generic error message would work for now too).
  • RESTful APIs – nice and simple URLs, they did a great job here (might also make sense to add some mod_redirect magic to support a dash between dictionaries, i.e. en-fr)? Something like the following, but I suck at regular expressions so definitely not perfect:
    RewriteRule ^([A-Za-z]2)-([A-Za-z]2)/([A-Za-z]*)/?u=([^/.]+)$ /$1$2/$3?u=$4 [R]
  • Full text translations – probably not realistic, given the nature of the site being primarily a Dictionary for words and short phrases… not full paragraphs, documents or websites… ok, that’s a given, but one resource I have found very useful in my personal use of the site (dating back to 2003-2004 in my University days struggling to get through Spanish classes and brush-up on my French in University) is the automatic link to the forums you get when typing in full phrases on the WordReference site directly. (i.e. ) I wonder if there are any plans to provide at least some of those same links in the API (maybe via a totally separate API call like http://wordreference.com/suggestion/json/ but that might be already available via the forum software’s API if one exists?? Even better yet and less realistic, would be to be able to have users rate the responses and automatically return the highest rated response in the API. That would take lots of key-mashing to make happen, and probably not happening anytime in the near future

Last but not least, for those who prefer to not dig into any API code at all, there was already an official WordReference MINI translation widget/tool built by the WordReference team themselves, which can be embedded via a simple iFrame, here’s what it looks like when in use:

That would definitely be the easiest possible way to quickly include translations on your site or blog, and the really nice thing about it is that it already includes links to the user discussion forums for the incredibly helpful community suggestions and assistance threads around specific terms in a given language.

Summary

In closing, Mkellog has done an excellent job on version 0.8 of the WordReference API, so a tip of the hat for him! I hope he continues to develop it and considers a tiered models to support an even higher quality of service, such that Google Translate API won’t even be missed!

UPDATE (2011-06-10):
JSONp requests where a callback is specified don’t seem to be working right now, though I did see them working in the first week the API was released have been fixed. Also, I have noticed the widget is not working well in IE7 *seems to work fine now*, will take a look at that later (but maybe not because I hate IE in general). Seems to be working well in IE8+, Safari 4+, Opera 10+, Chrome 8+, FF 3.5+ and even on iPhone for me so far… please feel free to report bugs or inconsistencies in these or other browser versions though!