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