Skip to content

A Ruby spanish translation DCOP server

Monday, 1 May 2006  |  richard dale

I've been here in Gran Canaria for nearly six months now, and my spanish hasn't progressed as fast as I hoped it would. Learning another language is really hard! It makes me realise how tough it must be for non-native english speakers to contribute to english based Free Software projects such as KDE.

I hadn't learned a language since I was at school over 30 years ago, and I just didn't know what it would be like, or how long it would take. But I thought by now I would actually be able to understand what people were saying even if I couldn't speak back fluently. But actually I'm still having trouble following the most basic conversations :(. It seems like it will take a year or so to get into the swing of it. Although, I should really be spending my evenings learning vocabulary and grammar, but of course I tend to either end up hacking or drinking instead.

In the meantime, while I attempt to master spanish, I need to be able to translate emails, program specs etc in a quick and dirty way without needing to look everything up in a paper based dictionary. I often use the google translation service, which is better than nothing, although often the translations are so poor they're really funny.. Also you have to get to the google translation page, select the spanish to english option, and then paste you text in. So I've hacked up a quick and dirty DCOP server in Korundum to access google translate - I can just send it a spanish string, and it then translates it via google and sends the translation back. The advantage of wrapping a web service in DCOP is that once you have the server, and app which can talk DCOP can easily use the server. Here is the code for the DCOP server, along with a GUI based client which can translate the contents of a KDE::TextEdit field, and another client that takes a text file and translates each field in the text file in turn.

Here is the top level of the DCOP server, I just created a Ruby DCOP project in KDevelop:

require 'Korundum' require 'spanishserver.rb'

aboutdata = KDE::AboutData.new("spanishserver", "KDE", "0.1", "A Spanish to English translation service", KDE::AboutData::License_GPL, "(C) 2006, Richard Dale") KDE::CmdLineArgs.init(ARGV, aboutdata)

if !KDE::UniqueApplication.start puts "spanishserver is already running!" exit(0) end

app = KDE::UniqueApplication.new service = SpanishTranslator.new app.exec

This is the code for the server itself. It subclasses KDE::HTMLPart and uses it for sending out the url request, and getting the reply back as HTML. The first thing you need to do is to have a look at the page's HTML with konqueror's 'View Document Source' option. The google page has a pop-up menu to select with pair of languages to translate, and it adds a string such as '&langpair=es|en' to the uri the page sends. The text to be translated is in a textfield called 'text', and it gets added to the uri as '?text=some text'.

The server hooks up to the browser extension's 'loadingProgress(int)' signal, and when the progress reaches 100 percent, it means the page is complete, and the reply can be sent back to the DCOP client. The html text can be extracted from the KDE::HTMLPart with a 'src = htmlDocument.toString.string' statement, and then I use a simple regular expression to pull out the translated text. Note that the messages to and from the DCOP server are asynchronous; the initial message requesting the translation includes a DCOP reference of where to send the reply. This allows the DCOP server to process the request in the background without keeping the client waiting. Then the statement '@client.translation($1, @direction)' sends the translation back to the client, which must have a DCOP slot of type 'void translation(QString,QString)':

class SpanishTranslator < KDE::HTMLPart k_dcop 'void translate(QString, QString, DCOPRef)' slots 'progress(int)'
def initialize
    super
    connect(browserExtension(), SIGNAL('loadingProgress(int)'), 
            self, SLOT('progress(int)'))
end

def translate(text, direction, client)
    @direction = direction
    @client = client
    @url = KDE::URL.new("http://translate.google.com/translate_t?text=%s&langpair=%s" %
       [text, direction])
    openURL(@url)
end

def progress(percent)
    if percent == 100 && !@url.nil?
        src = htmlDocument.toString.string
        if src =~ %r{<TEXTAREA.*>([^>]*)</TEXTAREA>.*<TEXTAREA.*>([^>]*)</TEXTAREA>}
            @client.translation($1, @direction)
            @url = nil
        end
    end
end

end

Here is a simple client with two text fields, one spanish and one english, which allows you to translate in both directions. It also has a couple of buttons, which access the KDE clipboard via klipper and translate it. Note that is has a DCOP slot called 'void translation(QString,QString)' which is used as a callback from the DCOP server with the completed translation. require 'Korundum'

class TranslatorClient < KDE::MainWindow k_dcop 'void translation(QString,QString)' slots :to_english, :to_spanish, :translate_clipboard

def initialize(parent = nil)
    super
    @spanish_clipboard = KDE::PushButton.new("Clipboard", self)
    @spanish_text = KDE::TextEdit.new(self)
    @english_clipboard = KDE::PushButton.new("Clipboard", self)
    @english_text = KDE::TextEdit.new(self)

    labels = Qt::VBoxLayout.new do |l|
        l.addWidget(Qt::Label.new("Spanish", self))
        l.addWidget(Qt::Label.new("English", self))
    end

    buttons = Qt::VBoxLayout.new do |b|
        b.addWidget(@spanish_clipboard)
        b.addWidget(@english_clipboard)
    end

    texts = Qt::VBoxLayout.new do |t|
        t.addWidget(@spanish_text)
        t.addWidget(@english_text)
    end

    setUpLayout()
    layout.direction = Qt::BoxLayout::LeftToRight
    layout.addLayout(labels)
    layout.addLayout(buttons)
    layout.addLayout(texts)

    connect(@spanish_text, SIGNAL(:returnPressed), self, SLOT(:to_english))
    connect(@spanish_clipboard, SIGNAL(:clicked), self, SLOT(:translate_clipboard))
    connect(@english_text, SIGNAL(:returnPressed), self, SLOT(:to_spanish))
    connect(@english_clipboard, SIGNAL(:clicked), self, SLOT(:translate_clipboard))
    
    @translation_server = KDE::DCOPRef.new("spanishserver", "SpanishTranslator")
    @dcop_self = KDE::DCOPRef.new("translatorclient-%d" % $$, "TranslatorClient")
    @klipper = KDE::DCOPRef.new("klipper", "klipper")
end

def to_english
    @translation_server.translate(@spanish_text.text, "es|en", @dcop_self)
end

def to_spanish
    @translation_server.translate(@english_text.text, "en|es", @dcop_self)
end

def translate_clipboard
    if sender() == @english_clipboard
        @english_text.text = @klipper.getClipboardContents
        to_spanish
    elsif sender() == @spanish_clipboard
        @spanish_text.text = @klipper.getClipboardContents
        to_english
    end
end

def translation(text, direction)
    if direction == "es|en"
        @english_text.text = text
    elsif direction == "en|es"
        @spanish_text.text = text
    end
end

end

about = KDE::AboutData.new("translatorclient", "TranslatorClient", "0.1") KDE::CmdLineArgs.init(ARGV, about) app = KDE::Application.new translator = TranslatorClient.new app.mainWidget = translator translator.caption = app.makeStdCaption("Spanish Translation Client") translator.show app.exec

Finally, here is the code for doing a 'batch' translation of each field within a wiki page; it sends each field in turn to the DCOP server, and just writes the translated text to standard output: require 'Korundum'

class TranslateWiki < KDE::TextEdit k_dcop 'void translation(QString,QString)' slots :translate, :next_line

def initialize
    super
    @server = KDE::DCOPRef.new("spanishserver", "SpanishTranslator")
    @dcop_self = KDE::DCOPRef.new("twikitranslator-%d" % $$, "TranslateWiki")
    connect(self, SIGNAL(:returnPressed), SLOT(:translate))
end

def translate
    @line_number = 0
    @file = File.open(self.text.chop).readlines
    Qt::Timer.singleShot(10, self, SLOT(:next_line))
end

def next_line
    line = ""
    loop do
        if @line_number >= @file.length
            return $kapp.quit
        end

        line = @file[@line_number]
        line.chop!
        @line_start = ""
        if line =~ /^(\s*\*\s*(\[\w+:\s*)?)(.*)/
            @line_start = $1 + " "
            line = $3
        end
        @line_number += 1

        if line =~ /^\s*$/
            puts @line_start
        elsif line =~ /^(---)|(%TOC%)|(\*\s*2006)/
            puts line
        else
            break
        end
    end

    @server.translate(line, "es|en", @dcop_self)
end

def translation(translation, direction)
    puts @line_start + translation
    Qt::Timer.singleShot(10, self, SLOT(:next_line))
end

end

aboutdata = KDE::AboutData.new("twikitranslator", "KDE", "0.1", "A Spanish to English twiki translator", KDE::AboutData::License_GPL, "(C) 2006, Richard Dale") KDE::CmdLineArgs.init(ARGV, aboutdata) app = KDE::Application.new t = TranslateWiki.new app.mainWidget = t t.show app.exec

Although the code here translates text, the same general idea can be used to access any website which doesn't have any sort of SOAP or REST based service that you can hook up to. For example, there are many sites such as kde apps org, which have an internal search option and using the same technique you can easily build a DCOP server to access those.

Regards -- Senor Dale Cerveza