Recent Weblogs

Links I like

Google Translate Meets Prototype

Great frameworks come together to make fantastic implementations.

Google Translate

Google has released a fantastic service to translate language, offering over 30 various languages to translate to. As well as detecting what language a given piece of text is written in. With these features combined the possibilities of implementation are quite vast.

Walk the DOM - Prototype

I have been working Prototype.js for a few years and it is certainly my preferred Javascript framework. It assists in particular with this project for walking the DOM. A limitation of the translation API is that the service will only accept a string of a given length. When including HTML markup, white space etc this limit isn't hard to reach. My approach was to walk the DOM, collect text nodes and send them off indivually.

Translator

My implementation as of now named "Translator" accepts 3 parameters in the constructor, one being an ID or element reference, the second being a CSS selector which will be invoked on the primary element, such that you can collect all paragraphs, headers etc to be translated. A benefactor of this approach is that any elements with event handlers or unique properties are not disturbed as only the text nodes of these elements are collected and swapped during translation. Translator also recognizes a node that has a node value that is beyond Google's maximum length limit, and will automatically splice these large nodes into more bite size payloads for translation. It is very careful not to disturb the order of the DOM and reconstructs the series of nodes just as the original large node was. Translator saves a reference to the original node such that when a user translates from French to German to Dutch it always translating based off of the original content, this is to avoid multiple translation which could lead to corrupted translations.

EventDispatcher - Superclass

Translator extends from EventDispatcher, a proto class that I had written a while back. Translator dispatches two event types "begin" and "complete". Begin dispatches an object that has the source element's innerHTML, source language and destination language as well as the collection of text nodes that Translator has collected for translation preperation. This is handy for caching as well as providing the user a notification that it is currently processing the translation.

The Code

/*
Copyright (c) 2008 Matthew Foster

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/var 
Translator = Class.create(EventDispatcher, {
	initialize : function(element, selector, config){
		this.element = $(element);
		this.preservedHTML = this.element.innerHTML;
		this.config = Object.extend(this.getDefaultConfig(), config);			
		this.textNodeCollection = this.collectChildren(selector);
		
		this.entityWasher = new Element("div");
		this.textNodeCollection = this.textNodeCollection.findAll(this.purifyTextNode);
	},
	purifyTextNode : function(node){
		try{
			if(!node)
				return false;
			return node.nodeType == 3;
		}
		catch(e){
			return false;
		}		
	},
	getDefaultConfig : function(){
		return { maxLength : 1000, srcLang : "en", recursive : true, type : "text" };	
	},
	collectChildren : function(selector){
		return this.element.select(selector).collect(this.collectTextNodes.bind(this)).flatten();
	},
	collectTextNodes : function(element){
		var self = this;
		var stack = $A(element.childNodes).collect(function(child){
			if(child.nodeType == 3  && child.nodeValue.length < self.config.maxLength && child.nodeValue.search(/^\s+$/g) == -1)
				return child;
			else if(child.nodeType == 3  && child.nodeValue.length > self.config.maxLength)
				return self.splitTextNode(child);
			else if(child.nodeType == 1 && self.config.recursive)
				return self.collectTextNodes(child);
		});			
		return stack;
	},
	splitStringByMax : function(text){
		var offset = 0;
		var textArr = [];
		while(text.length > this.config.maxLength){				
			var tmp = text.substr(0, this.config.maxLength);
			offset = tmp.lastIndexOf(" ");
			var subText = text.substr(0, offset);				
			text = text.substr(offset);
			textArr.push(subText);				
		}
		textArr.push(text);
		return textArr;
	},
	splitTextNode : function(node){
		var nodeStack = [];
		var textArr = this.splitStringByMax(node.nodeValue);
		
		var prevNode = false;
		textArr.each(function(text, itr){
			var newNode = document.createTextNode(text);
			nodeStack.push(newNode);		
			if(node.nextSibling != null && !prevNode)
				node.parentNode.insertBefore(newNode, node.nextSibling);
			else if(prevNode && prevNode.nextSibling != null)
				node.parentNode.insertBefore(newNode, prevNode.nextSibling);
			else 
				node.parentNode.appendChild(newNode);
			prevNode = newNode;
		});
		node.parentNode.removeChild(node);
		
		return nodeStack;
	},
	getEventBeginObject : function(destLang){
		return { 
			destLang : destLang,
			srcLang : this.config.srcLang,
			srcLangNodes : this.textNodeCollection,
			srcHTML : this.preservedHTML
		}	
	},
	getEventCompleteObject : function(result){
		return {
			srcLangNodes : this.textNodeCollection,
			destLangNodes : this.translationStack,
			destLangHTML : this.element.innerHTML,
			srcLangHTML : this.preservedHTML,
			result : result,
			resultStack : this.resultStack			
		}		
	},
	finishTranslation : function(result){
		this.translating = false;
		this.dispatchEvent("complete", this.getEventCompleteObject(result));
	
	},
	translate : function(destLang){
		if(this.translating)
			return false;
		var self = this;
		this.dispatchEvent("begin", this.getEventBeginObject(destLang));
		this.textNodeCount = this.textNodeCollection.length;
		this.translationStack = [];
		this.resultStack = [];
		this.textNodeCollection.each(function(node){
			self.translating = true;
			google.language.translate(node.nodeValue, self.config.srcLang, destLang, self.handleTranslation.bind(self, node));
		});
		return true;	
	},
	handleTranslation : function(node, obj){
		try{
			var parent = node.preservedParent || node.parentNode;
			this.entityWasher.innerHTML = obj.translation;
			var translatedText = this.entityWasher.innerHTML;
			if(node.nodeValue.search(/^\s/) > -1)
				translatedText = " " + translatedText;
			if(node.nodeValue.search(/\s$/) > -1)
				translatedText = translatedText + " ";
				
			var newText = document.createTextNode(translatedText);
						
			if(node.placeHolder)
				parent.replaceChild(newText, node.placeHolder);
			else
				parent.replaceChild(newText, node);
				
			node.placeHolder = newText;
			node.preservedParent = parent;
			this.translationStack.push(newText);
			this.resultStack.push(obj);
			this.textNodeCount--;
			if(this.textNodeCount <= 0)
				this.finishTranslation(obj);
		}
		catch(e){
			console.log("Error has occured with handling translation error = %o arguments = %o", e, arguments);
		}
	}

});
	

References

  • Google Translate API learn more about how Google translate can improve your site's accessability to foreign audiences
  • Prototype.js a fantastic Javascript library that can augment your approach to writing clean and efficient JS code.
  • My Demo take a look at my demo page which has a much more slimmed down code base and presents just what you need to do, to get Translator to work on a page.

Comments are Disabled.