How To Add Translation Feature To Twitter

I don’t know about you but, I follow few peple that sometimes tweet in different language then English. And English is the only foreign language I know. So if I want to see what they tweet about sometimes I use google translate to read the content.

I always thought that it will be nice feature for Twitter to have translate button, so I created one, using WebSockets and Ruby.

So how I did this, first I downloaded websockets ruby library from github web-socket-ruby, I already had translation script written in Ruby that use google translate, so I added websocket server to it.

#!/usr/bin/ruby

require 'net/http'
require 'uri'
require 'optparse'
require 'json'
require 'socket'
require 'web_socket'

class NotConnectedException < Exception
end

def server(port, domains)
  server = WebSocketServer.new(
    :accepted_domains => domains,
    :port => port)
  puts("Server is running at port %d" % server.port)
  server.run() do |ws|
    puts("Connection accepted")
    puts("Path: #{ws.path}, Origin: #{ws.origin}")
    if ws.path == "/translate"
      ws.handshake()
      while data = ws.receive()
        printf("Received: %p\n", data)
        data = JSON.parse(data)
        response = translate(data['text'], nil, data['to_lang']).join("\n")
        ws.send(response)
        printf("Sent: %p\n", response)
      end
    else
      ws.handshake("404 Not Found")
    end
    puts("Connection closed")
  end
end

def escape(o)
  o.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
    '%' + $1.unpack('H2' * $1.size).join('%').upcase
  }.tr(' ', '+')
end

def translate(text, from=nil, to=nil, cookie=nil)
  url = URI.parse("http://translate.google.com/translate_a/t")
  query = "?hl=en&client=t&text=#{escape(text)}&multires=1&otf=1&pc=0&sc=1&ie=UTF-8&oe=UTF-8"
  query += "&sl=" + (from ? from : 'auto')
  if to
    query += "&tl=#{to}"
  end
  begin
    http = Net::HTTP.new(url.host)
    res = http.get(url.path + query)
    res.response['content-type'] =~ /charset=(.*)/
    charset = $1
    JSON.parse(res.body.gsub(/,{2,}/, ',').gsub(/,\]/, ']'))[0].map {|i|
      # google sometimes put spaces around numbers
      i[0].gsub(/\{ *([0-9]) *\}/, '{\1}')
    }
  rescue NoMethodError
    raise NotConnectedException
  rescue SocketError
    raise NotConnectedException
  rescue 
  end  
 
end

def msg(str, type='info')
  system("zenity --#{type} --title='translation' --text='#{str}'")
end

params = ARGV.getopts('i:o:gcsp:')

def error(msg, gui=false)
  if gui
    msg(msg, 'error')
  else
    puts msg
  end
end

def usage()
  puts "usage:"
  puts "translate [-i <INPUT LANG>] -o <OUTPUT LANG> [MORE OPTIONS]"
  puts "-g - show zenity dialog"
  puts "-c - get input from clipboard"
  puts "-s - run as server"
  puts "-p - server port"
end

begin
  if params['s']
     begin
       server(params['p'] ? params['p'].to_i() : 8080, 'twitter.com')
     rescue Interrupt
       puts "Server Exit"
     end
  else
    if params['c']
      input = `xclip -o -sel clip`
    else
      input = ARGF.read()
    end
    translation = translate(input, params['i'], params['o'])
    if params['g']
      msg(translation.join(". "))
    else
      translation.each{|sentence|
        if sentence != ''
          puts sentence
        end
      }
    end
  end
rescue JSON::ParserError => e
  error("Response Error: " + e.message, params['g'])
rescue NotConnectedException
  error("sorry but it seems that your internet connection is down", params['g'])
end

Then I created this bookmarklet. (I notice that profile page use jQuery.noConflict() so I can’t access it from bookmarklet. So here is updated code that insert jQuery script again – in use continuation to block execution of the script until jQuery is loaded). To use bookmarklet just copy code below and insert into url address bar when twitter tab is active

javascript:(function(continuation) {
    function attr(elem, key, value) {
        elem.setAttribute(document.createAttribute(key, value));
    }
    var script = (function() {
        var head = document.getElementsByTagName('head')[0];
        return function(src) {
            var script = document.createElement('script');
            script.setAttribute('src', src);
            script.setAttribute('type', 'text/javascript');
            script.setAttribute('async', 'false');
            head.appendChild(script);
            return script;
        };
    })();
    script('https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js');
    var interval = 100;
    setTimeout(function() {
        if (jQuery) {
            continuation(jQuery.noConflict());
        } else {
            setTimeout(arguments.callee, interval);
        }
    }, interval);
})(function($) {
    function translator(fun, lang) {
        var socket = new WebSocket("ws://localhost:8080/translate");
        socket.onopen = function() {
            console.log("Socket has been opened!");
        };
        socket.onclose = function() {
            console.log("closed");
        };
        return fun(function(text, respond) {
            socket.onmessage = function(msg) {
                respond(msg.data);
            };
            var data = JSON.stringify({to_lang: lang, text: text});
            socket.send(data);
        });
    }
    var translate_tweet = translator(function(translate) {
        return function(content) {
            var tweet_container = content.find('.js-tweet-text');
            var tweet = tweet_container.html();
            content.data('original', tweet);
            var links = [];
            var i = 0;
            tweet = tweet.replace(/<a[^>]+>[^<]+<\/a>|<a[^>]+><s>(#|@)<\/s><b>[^<]+<\/b><\/a>/g, function(link) {
                links.push(link);
                return '{' + i++ + '}';
            });
            translate(tweet, function(result) {
                for (var i=links.length; i--;) {
                    result = result.replace('{' + i + '}', links[i]);
                }
                tweet_container.html(result);
                var actions = content.find('.tweet-actions');
                if (actions.find('.action-orig-container').length == 0) {
                    actions.append('<li class="action-orig-container"><a href="#">Original</a></li>');
                }
            });
        };
    }, prompt('Select Language: af - afrikaans, sk - albánskej, ar - عربي, be - Беларускі, bg - Български, zh - 荃湾, zh - 太阳, hr - Hrvatski, cs - Český, da - Danske, et - Eesti, tl - filipiński, fi - Suomi, fr - Français, gl - galijski, el - Ελληνικά, iw - עברית, hi - हिन्दी, es - Español, nl - Nederlands, id - indonezyjski, ga - Gaeilge, is - Íslenska, ja - 日本語, yi - ייִדיש, ca - Català, ko - 한국의, lt - Lietuvos, lv - Latvijas, mk - Македонски, ms - Melayu, mt - Malti, de - Deutsch, no - Norsk, fa - فارسی, pl - polski, ru - Русский, ro - Română, sr - Српски, sk - Slovenský, sl - Slovenski, sw - Swahili, sv - Svenska, th - ภาษาไทย, tr - Türk, uk - Український, cy - walijski, hu - Magyar, vi - Việt, it - Italiano'));
    $('.content').unbind('mouseover').live('mouseover', function() {
        var $this = $(this);
        if ($this.find('.action-trans-container').length == 0) {
            $this.find('.action-fav-container').
                after('<li class="action-trans-container"><a href="#">Translate</a></li>');
        }
    });
    $('.action-orig-container').unbind('onclick').live('click', function() {
        var content = $(this).parents('.content');
        content.find('.js-tweet-text').html(content.data('original'));
        content.find('.action-orig-container').remove();
        return false;
    });
    $('.action-trans-container').unbind('onclick').live('click', function() {
        translate_tweet($(this).parents('.content'));
        return false;
    });
});

CODE LICENSE: you can use the code for whatever purpose you like it’s realeas on Sharing Agreement.