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
42 # Many translators use Mechanize, which changed namespace around version 1.0
43 # To support both pre-1.0 and post-1.0 namespaces, we use these auxiliary
44 # method. The translator still needs to require 'mechanize' on initialization
47 return Mechanize if defined? Mechanize
51 # whether the translator supports this direction
52 def support?(from, to)
53 from != to && @directions[from].include?(to)
56 # this implements argument checking and caching. subclasses should define the
57 # do_translate method to implement actual translation
58 def translate(text, from, to)
59 raise UnsupportedDirectionError unless support?(from, to)
60 raise ArgumentError, _("Cannot translate empty string") if text.empty?
61 request = [text, from, to]
62 unless @cache.has_key? request
63 translation = do_translate(text, from, to)
64 raise NoTranslationError if translation.empty?
65 @cache[request] = translation
72 # given the set of supported languages, return a hash suitable for the directions
73 # attribute which includes any language to any other language
74 def self.all_to_all(languages)
75 directions = all_to_none(languages)
76 languages.each {|l| directions[l] = languages.to_set}
80 # a hash suitable for the directions attribute which includes any language from/to
81 # the given set of languages (center_languages)
82 def self.all_from_to(languages, center_languages)
83 directions = all_to_none(languages)
84 center_languages.each {|l| directions[l] = languages - [l]}
85 (languages - center_languages).each {|l| directions[l] = center_languages.to_set}
89 # get a hash from a list of pairs
90 def self.pairs(list_of_pairs)
91 languages = list_of_pairs.flatten.to_set
92 directions = all_to_none(languages)
93 list_of_pairs.each do |(from, to)|
94 directions[from] << to
99 # an empty hash with empty sets as default values
100 def self.all_to_none(languages)
102 # always return empty set when the key is non-existent, but put empty set in the
103 # hash only if the key is one of the languages
104 if languages.include? k
114 class GoogleTranslator < Translator
115 INFO = 'Google Translate <http://www.google.com/translate_t>'
116 URL = 'https://translate.google.com/'
119 %w[af sq am ar hy az eu be bn bh bg my ca chr zh zh_CN zh_TW hr
120 cs da dv en eo et tl fi fr gl ka de el gn gu iw hi hu is id iu
121 ga it ja kn kk km ko lv lt mk ms ml mt mr mn ne no or ps fa pl
122 pt_PT pa ro ru sa sr sd si sk sl es sw sv tg ta tl te th bo tr
123 uk ur uz ug vi cy yi auto]
124 def initialize(cache={})
126 super(Translator::Direction.all_to_all(LANGUAGES), cache)
129 def do_translate(text, from, to)
130 agent = Mechanize.new
131 agent.user_agent_alias = 'Linux Mozilla'
133 form = page.form_with(:id => 'gt-form')
138 return page.search('#result_box span').first.content
142 class YandexTranslator < Translator
143 INFO = 'Yandex Translator <http://translate.yandex.com/>'
144 LANGUAGES = %w{ar az be bg ca cs da de el en es et fi fr he hr hu hy it ka lt lv mk nl no pl pt ro ru sk sl sq sr sv tr uk}
146 URL = 'https://translate.yandex.net/api/v1.5/tr.json/translate?key=%s&lang=%s-%s&text=%s'
147 KEY = 'trnsl.1.1.20140326T031210Z.1e298c8adb4058ed.d93278fea8d79e0a0ba76b6ab4bfbf6ac43ada72'
148 def initialize(cache)
151 super(Translator::Direction.all_to_all(LANGUAGES), cache)
154 def translate(text, from, to)
155 res = Irc::Utils.bot.httputil.get_response(URL % [KEY, from, to, URI.escape(text)])
156 res = JSON.parse(res.body)
158 if res['code'] != 200
159 raise Translator::NoTranslationError
161 res['text'].join(' ')
167 class TranslatorPlugin < Plugin
168 Config.register Config::IntegerValue.new('translator.timeout',
169 :default => 30, :validate => Proc.new{|v| v > 0},
170 :desc => _("Number of seconds to wait for the translation service before timeout"))
171 Config.register Config::StringValue.new('translator.destination',
173 :desc => _("Default destination language to be used with translate command"))
176 'google_translate' => GoogleTranslator,
177 'yandex' => YandexTranslator,
182 @failed_translators = []
184 TRANSLATORS.each_pair do |name, c|
185 watch_for_fail(name) do
186 @translators[name] = c.new(@registry.sub_registry(name))
187 map "#{name} :from :to *phrase",
188 :action => :cmd_translate, :thread => true
192 Config.register Config::ArrayValue.new('translator.default_list',
193 :default => TRANSLATORS.keys,
194 :validate => Proc.new {|l| l.all? {|t| TRANSLATORS.has_key?(t)}},
195 :desc => _("List of translators to try in order when translator name not specified"),
196 :on_change => Proc.new {|bot, v| update_default})
200 def watch_for_fail(name, &block)
204 debug 'Translator error: '+$!.to_s
206 @failed_translators << { :name => name, :reason => $!.to_s }
208 warning _("Translator %{name} cannot be used: %{reason}") %
209 {:name => name, :reason => $!}
210 map "#{name} [*args]", :action => :failed_translator,
211 :defaults => {:name => name, :reason => $!}
215 def failed_translator(m, params)
216 m.reply _("Translator %{name} cannot be used: %{reason}") %
217 {:name => params[:name], :reason => params[:reason]}
220 def help(plugin, topic=nil)
221 case (topic.intern rescue nil)
223 unless @failed_translators.empty?
224 failed_list = @failed_translators.map { |t| _("%{bold}%{translator}%{bold}: %{reason}") % {
225 :translator => t[:name],
226 :reason => t[:reason],
230 _("Failed translators: %{list}") % { :list => failed_list.join(", ") }
232 _("None of the translators failed")
235 if @translators.has_key?(plugin)
236 translator = @translators[plugin]
237 _('%{translator} <from> <to> <phrase> => Look up phrase using %{info}, supported from -> to languages: %{directions}') % {
238 :translator => plugin,
239 :info => translator.class::INFO,
240 :directions => translator.directions.map do |source, targets|
241 _('%{source} -> %{targets}') %
242 {:source => source, :targets => targets.to_a.join(', ')}
246 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') %
247 {:translators => @translators.keys.join(', ')}
249 help_str << "\n" + _("%{bold}Note%{bold}: %{failed_amt} translators failed, see %{reverse}%{prefix}help translate failed%{reverse} for details") % {
250 :failed_amt => @failed_translators.size,
253 :prefix => @bot.config['core.address_prefix'].first
262 @languages ||= @translators.map { |t| t.last.directions.keys }.flatten.uniq
266 @default_translators = bot.config['translator.default_list'] & @translators.keys
269 def cmd_translator(m, params)
270 params[:to] = @bot.config['translator.destination'] if params[:to].nil?
271 params[:from] ||= 'auto'
272 translator = @default_translators.find {|t| @translators[t].support?(params[:from], params[:to])}
275 cmd_translate m, params.merge({:translator => translator, :show_provider => false})
277 # When translate command is used without source language, "auto" as source
278 # language is assumed. It means that google translator is used and we let google
279 # figure out what the source language is.
281 # Problem is that the google translator will fail if the system that the bot is
282 # running on does not have the json gem installed.
283 if params[:from] == 'auto'
284 m.reply _("Unable to auto-detect source language due to broken google translator, see %{reverse}%{prefix}help translate failed%{reverse} for details") % {
286 :prefix => @bot.config['core.address_prefix'].first
289 m.reply _('None of the default translators (translator.default_list) supports translating from %{source} to %{target}') % {:source => params[:from], :target => params[:to]}
294 def cmd_translate(m, params)
295 # get the first word of the command
296 tname = params[:translator] || m.message[/\A(\w+)\s/, 1]
297 translator = @translators[tname]
298 from, to, phrase = params[:from], params[:to], params[:phrase].to_s
300 watch_for_fail(tname) do
302 translation = Timeout.timeout(@bot.config['translator.timeout']) do
303 translator.translate(phrase, from, to)
305 m.reply(if params[:show_provider]
306 _('%{translation} (provided by %{translator})') %
307 {:translation => translation, :translator => tname.gsub("_", " ")}
312 rescue Translator::UnsupportedDirectionError
313 m.reply _("%{translator} doesn't support translating from %{source} to %{target}") %
314 {:translator => tname, :source => from, :target => to}
315 rescue Translator::NoTranslationError
316 m.reply _('%{translator} failed to provide a translation') %
317 {:translator => tname}
318 rescue Timeout::Error
319 m.reply _('The translator timed out')
323 m.reply _('No translator called %{name}') % {:name => tname}
327 # URL translation has nothing to do with Translators so let's make it
328 # separate, and Google exclusive for now
329 def cmd_translate_url(m, params)
330 params[:to] = @bot.config['translator.destination'] if params[:to].nil?
331 params[:from] ||= 'auto'
333 translate_url = "http://translate.google.com/translate?sl=%{from}&tl=%{to}&u=%{url}" % {
334 :from => params[:from],
336 :url => CGI.escape(params[:url].to_s)
339 m.reply(translate_url)
343 plugin = TranslatorPlugin.new
344 req = Hash[*%w(from to).map { |e| [e.to_sym, /#{plugin.languages.join("|")}/] }.flatten]
346 plugin.map 'translate [:from] [:to] :url',
347 :action => :cmd_translate_url, :requirements => req.merge(:url => %r{^https?://[^\s]*})
348 plugin.map 'translator [:from] [:to] :url',
349 :action => :cmd_translate_url, :requirements => req.merge(:url => %r{^https?://[^\s]*})
350 plugin.map 'translate [:from] [:to] *phrase',
351 :action => :cmd_translator, :thread => true, :requirements => req
352 plugin.map 'translator [:from] [:to] *phrase',
353 :action => :cmd_translator, :thread => true, :requirements => req