4 # :title: Translator plugin for rbot
6 # Author:: Yaohan Chen <yaohan.chen@gmail.com>
7 # Copyright:: (C) 2007 Yaohan Chen
10 # This plugin allows using rbot to translate text on a few translation services
14 # * Configuration for whether to show translation engine
15 # * Optionally sync default translators with karma.rb ranking
20 # base class for implementing a translation service
22 # direction:: supported translation directions, a hash where each key is a source
23 # language name, and each value is Set of target language names. The
24 # methods in the Direction module are convenient for initializing this
27 INFO = 'Some translation service'
29 class UnsupportedDirectionError < ArgumentError
32 class NoTranslationError < RuntimeError
35 attr_reader :directions, :cache
37 def initialize(directions, cache={})
38 @directions = directions
43 # whether the translator supports this direction
44 def support?(from, to)
45 from != to && @directions[from].include?(to)
48 # this implements argument checking and caching. subclasses should define the
49 # do_translate method to implement actual translation
50 def translate(text, from, to)
51 raise UnsupportedDirectionError unless support?(from, to)
52 raise ArgumentError, _("Cannot translate empty string") if text.empty?
53 request = [text, from, to]
54 unless @cache.has_key? request
55 translation = do_translate(text, from, to)
56 raise NoTranslationError if translation.empty?
57 @cache[request] = translation
64 # given the set of supported languages, return a hash suitable for the directions
65 # attribute which includes any language to any other language
66 def self.all_to_all(languages)
67 directions = all_to_none(languages)
68 languages.each {|l| directions[l] = languages.to_set}
72 # a hash suitable for the directions attribute which includes any language from/to
73 # the given set of languages (center_languages)
74 def self.all_from_to(languages, center_languages)
75 directions = all_to_none(languages)
76 center_languages.each {|l| directions[l] = languages - [l]}
77 (languages - center_languages).each {|l| directions[l] = center_languages.to_set}
81 # get a hash from a list of pairs
82 def self.pairs(list_of_pairs)
83 languages = list_of_pairs.flatten.to_set
84 directions = all_to_none(languages)
85 list_of_pairs.each do |(from, to)|
86 directions[from] << to
91 # an empty hash with empty sets as default values
92 def self.all_to_none(languages)
94 # always return empty set when the key is non-existent, but put empty set in the
95 # hash only if the key is one of the languages
96 if languages.include? k
107 class NiftyTranslator < Translator
108 INFO = '@nifty Translation <http://nifty.amikai.com/amitext/indexUTF8.jsp>'
110 def initialize(cache={})
112 super(Translator::Direction.all_from_to(%w[ja en zh_CN ko], %w[ja]), cache)
115 def do_translate(text, from, to)
116 @form ||= WWW::Mechanize.new.
117 get('http://nifty.amikai.com/amitext/indexUTF8.jsp').
118 forms_with(:name => 'translateForm').last
119 @radio = @form.radiobuttons_with(:name => 'langpair').first
120 @radio.value = "#{from},#{to}".upcase
122 @form.fields_with(:name => 'sourceText').last.value = text
124 @form.submit(@form.buttons_with(:name => 'translate').last).
125 forms_with(:name => 'translateForm').last.fields_with(:name => 'translatedText').last.value
130 class ExciteTranslator < Translator
131 INFO = 'Excite.jp Translation <http://www.excite.co.jp/world/>'
133 def initialize(cache={})
137 super(Translator::Direction.all_from_to(%w[ja en zh_CN zh_TW ko], %w[ja]), cache)
139 @forms = Hash.new do |h, k|
142 h[k] = open_form('english')
143 when 'zh_CN', 'zh_TW'
144 # this way we don't need to fetch the same page twice
145 h['zh_CN'] = h['zh_TW'] = open_form('chinese')
147 h[k] = open_form('korean')
153 WWW::Mechanize.new.get("http://www.excite.co.jp/world/#{name}").
154 forms_with(:name => 'world').first
157 def do_translate(text, from, to)
158 non_ja_language = from != 'ja' ? from : to
159 form = @forms[non_ja_language]
161 if non_ja_language =~ /zh_(CN|TW)/
162 form_with_fields(:name => 'wb_lp').first.value = "#{from}#{to}".sub(/_(?:CN|TW)/, '').upcase
163 form_with_fields(:name => 'big5').first.value = ($1 == 'TW' ? 'yes' : 'no')
165 # the en<->ja page is in Shift_JIS while other pages are UTF-8
166 text = Iconv.iconv('Shift_JIS', 'UTF-8', text) if non_ja_language == 'en'
167 form.fields_with(:name => 'wb_lp').first.value = "#{from}#{to}".upcase
169 form.fields_with(:name => 'before').first.value = text
170 result = form.submit.forms_with(:name => 'world').first.fields_with(:name => 'after').first.value
171 # the en<->ja page is in Shift_JIS while other pages are UTF-8
172 if non_ja_language == 'en'
173 Iconv.iconv('UTF-8', 'Shift_JIS', result)
182 class GoogleTranslator < Translator
183 INFO = 'Google Translate <http://www.google.com/translate_t>'
186 %w[af sq am ar hy az eu be bn bh bg my ca chr zh zh_CN zh_TW hr
187 cs da dv en eo et tl fi fr gl ka de el gn gu iw hi hu is id iu
188 ga it ja kn kk km ko lv lt mk ms ml mt mr mn ne no or ps fa pl
189 pt_PT pa ro ru sa sr sd si sk sl es sw sv tg ta tl te th bo tr
190 uk ur uz ug vi cy yi auto]
191 def initialize(cache={})
194 super(Translator::Direction.all_to_all(LANGUAGES), cache)
197 def do_translate(text, from, to)
198 langpair = [from == 'auto' ? '' : from, to].map { |e| e.tr('_', '-') }.join("|")
199 raw_json = Irc::Utils.bot.httputil.get_response(URI.escape(
200 "http://ajax.googleapis.com/ajax/services/language/translate?v=1.0&q=#{text}&langpair=#{langpair}")).body
201 response = JSON.parse(raw_json)
203 if response["responseStatus"] != 200
204 raise Translator::NoTranslationError, response["responseDetails"]
206 translation = response["responseData"]["translatedText"]
207 return Utils.decode_html_entities(translation)
213 class BabelfishTranslator < Translator
214 INFO = 'AltaVista Babel Fish Translation <http://babelfish.altavista.com/babelfish/>'
216 def initialize(cache)
218 form = WWW::Mechanize.new.get('http://babelfish.altavista.com/babelfish/').
219 forms_with(:name => 'frmTrText').first
220 lang_list = form.fields_with(:name => 'lp').first
221 language_pairs = lang_list.options.map {|o| o.value.split('_')}.
222 reject {|p| p.empty?}
223 super(Translator::Direction.pairs(language_pairs), cache)
226 def do_translate(text, from, to)
227 @form ||= WWW::Mechanize.new.get('http://babelfish.altavista.com/babelfish/').
228 forms_with(:name => 'frmTrText').first
230 if @form.fields_with(:name => 'trtext').empty?
231 @form.add_field!('trtext', text)
233 @form.fields_with(:name => 'trtext').first.value = text
235 @lang_list.value = "#{from}_#{to}"
236 @form.submit.parser.search("div[@id='result']/div[@style]").inner_html
240 class WorldlingoTranslator < Translator
241 INFO = 'WorldLingo Free Online Translator <http://www.worldlingo.com/en/products_services/worldlingo_translator.html>'
243 LANGUAGES = %w[en fr de it pt es ru nl el sv ar ja ko zh_CN zh_TW]
244 def initialize(cache)
246 super(Translator::Direction.all_to_all(LANGUAGES), cache)
249 def translate(text, from, to)
250 response = Irc::Utils.bot.httputil.get_response(URI.escape(
251 "http://www.worldlingo.com/SEfpX0LV2xIxsIIELJ,2E5nOlz5RArCY,/texttranslate?wl_srcenc=utf-8&wl_trgenc=utf-8&wl_text=#{text}&wl_srclang=#{from.upcase}&wl_trglang=#{to.upcase}"))
252 # WorldLingo seems to respond an XML when error occurs
253 case response['Content-Type']
257 raise Translator::NoTranslationError
262 class TranslatorPlugin < Plugin
263 Config.register Config::IntegerValue.new('translator.timeout',
264 :default => 30, :validate => Proc.new{|v| v > 0},
265 :desc => _("Number of seconds to wait for the translation service before timeout"))
266 Config.register Config::StringValue.new('translator.destination',
268 :desc => _("Default destination language to be used with translate command"))
271 'nifty' => NiftyTranslator,
272 'excite' => ExciteTranslator,
273 'google_translate' => GoogleTranslator,
274 'babelfish' => BabelfishTranslator,
275 'worldlingo' => WorldlingoTranslator,
280 @failed_translators = []
282 TRANSLATORS.each_pair do |name, c|
283 watch_for_fail(name) do
284 @translators[name] = c.new(@registry.sub_registry(name))
285 map "#{name} :from :to *phrase",
286 :action => :cmd_translate, :thread => true
290 Config.register Config::ArrayValue.new('translator.default_list',
291 :default => TRANSLATORS.keys,
292 :validate => Proc.new {|l| l.all? {|t| TRANSLATORS.has_key?(t)}},
293 :desc => _("List of translators to try in order when translator name not specified"),
294 :on_change => Proc.new {|bot, v| update_default})
298 def watch_for_fail(name, &block)
302 @failed_translators << { :name => name, :reason => $!.to_s }
304 warning _("Translator %{name} cannot be used: %{reason}") %
305 {:name => name, :reason => $!}
306 map "#{name} [*args]", :action => :failed_translator,
307 :defaults => {:name => name, :reason => $!}
311 def failed_translator(m, params)
312 m.reply _("Translator %{name} cannot be used: %{reason}") %
313 {:name => params[:name], :reason => params[:reason]}
316 def help(plugin, topic=nil)
317 case (topic.intern rescue nil)
319 unless @failed_translators.empty?
320 failed_list = @failed_translators.map { |t| _("%{bold}%{translator}%{bold}: %{reason}") % {
321 :translator => t[:name],
322 :reason => t[:reason],
326 _("Failed translators: %{list}") % { :list => failed_list.join(", ") }
328 _("None of the translators failed")
331 if @translators.has_key?(plugin)
332 translator = @translators[plugin]
333 _('%{translator} <from> <to> <phrase> => Look up phrase using %{info}, supported from -> to languages: %{directions}') % {
334 :translator => plugin,
335 :info => translator.class::INFO,
336 :directions => translator.directions.map do |source, targets|
337 _('%{source} -> %{targets}') %
338 {:source => source, :targets => targets.to_a.join(', ')}
342 help_str = _('Command: <translator> <from> <to> <phrase>, where <translator> is one of: %{translators}. If "translator" is used in place of the translator name, the first translator in translator.default_list which supports the specified direction will be picked automatically. Use "help <translator>" to look up supported from and to languages') %
343 {:translators => @translators.keys.join(', ')}
345 help_str << "\n" + _("%{bold}Note%{bold}: %{failed_amt} translators failed, see %{reverse}%{prefix}help translate failed%{reverse} for details") % {
346 :failed_amt => @failed_translators.size,
349 :prefix => @bot.config['core.address_prefix'].first
358 @languages ||= @translators.map { |t| t.last.directions.keys }.flatten.uniq
362 @default_translators = bot.config['translator.default_list'] & @translators.keys
365 def cmd_translator(m, params)
366 params[:to] = @bot.config['translator.destination'] if params[:to].nil?
367 params[:from] ||= 'auto'
368 translator = @default_translators.find {|t| @translators[t].support?(params[:from], params[:to])}
371 cmd_translate m, params.merge({:translator => translator, :show_provider => true})
373 # When translate command is used without source language, "auto" as source
374 # language is assumed. It means that google translator is used and we let google
375 # figure out what the source language is.
377 # Problem is that the google translator will fail if the system that the bot is
378 # running on does not have the json gem installed.
379 if params[:from] == 'auto'
380 m.reply _("Unable to auto-detect source language due to broken google translator, see %{reverse}%{prefix}help translate failed%{reverse} for details") % {
382 :prefix => @bot.config['core.address_prefix'].first
385 m.reply _('None of the default translators (translator.default_list) supports translating from %{source} to %{target}') % {:source => params[:from], :target => params[:to]}
390 def cmd_translate(m, params)
391 # get the first word of the command
392 tname = params[:translator] || m.message[/\A(\w+)\s/, 1]
393 translator = @translators[tname]
394 from, to, phrase = params[:from], params[:to], params[:phrase].to_s
396 watch_for_fail(tname) do
398 translation = Timeout.timeout(@bot.config['translator.timeout']) do
399 translator.translate(phrase, from, to)
401 m.reply(if params[:show_provider]
402 _('%{translation} (provided by %{translator})') %
403 {:translation => translation, :translator => tname.gsub("_", " ")}
408 rescue Translator::UnsupportedDirectionError
409 m.reply _("%{translator} doesn't support translating from %{source} to %{target}") %
410 {:translator => tname, :source => from, :target => to}
411 rescue Translator::NoTranslationError
412 m.reply _('%{translator} failed to provide a translation') %
413 {:translator => tname}
414 rescue Timeout::Error
415 m.reply _('The translator timed out')
419 m.reply _('No translator called %{name}') % {:name => tname}
424 plugin = TranslatorPlugin.new
425 req = Hash[*%w(from to).map { |e| [e.to_sym, /#{plugin.languages.join("|")}/] }.flatten]
427 plugin.map 'translate [:from] [:to] *phrase',
428 :action => :cmd_translator, :thread => true, :requirements => req
429 plugin.map 'translator [:from] [:to] *phrase',
430 :action => :cmd_translator, :thread => true, :requirements => req