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 # 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
106 class YandexTranslator < Translator
107 INFO = 'Yandex Translator <http://translate.yandex.com/>'
108 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}
110 URL = 'https://translate.yandex.net/api/v1.5/tr.json/translate?key=%s&lang=%s-%s&text=%s'
111 KEY = 'trnsl.1.1.20140326T031210Z.1e298c8adb4058ed.d93278fea8d79e0a0ba76b6ab4bfbf6ac43ada72'
112 def initialize(cache, bot)
115 super(Translator::Direction.all_to_all(LANGUAGES), cache, bot)
118 def translate(text, from, to)
119 res = @bot.httputil.get_response(URL % [KEY, from, to, URI.escape(text)])
120 res = JSON.parse(res.body)
122 if res['code'] != 200
123 raise Translator::NoTranslationError
125 res['text'].join(' ')
131 class TranslatorPlugin < Plugin
132 Config.register Config::IntegerValue.new('translator.timeout',
133 :default => 30, :validate => Proc.new{|v| v > 0},
134 :desc => _("Number of seconds to wait for the translation service before timeout"))
135 Config.register Config::StringValue.new('translator.destination',
137 :desc => _("Default destination language to be used with translate command"))
140 'yandex' => YandexTranslator,
145 @failed_translators = []
147 TRANSLATORS.each_pair do |name, c|
148 watch_for_fail(name) do
149 @translators[name] = c.new(@registry.sub_registry(name), @bot)
150 map "#{name} :from :to *phrase",
151 :action => :cmd_translate, :thread => true
155 Config.register Config::ArrayValue.new('translator.default_list',
156 :default => TRANSLATORS.keys,
157 :validate => Proc.new {|l| l.all? {|t| TRANSLATORS.has_key?(t)}},
158 :desc => _("List of translators to try in order when translator name not specified"),
159 :on_change => Proc.new {|bot, v| update_default})
163 def watch_for_fail(name, &block)
167 debug 'Translator error: '+$!.to_s
169 @failed_translators << { :name => name, :reason => $!.to_s }
171 warning _("Translator %{name} cannot be used: %{reason}") %
172 {:name => name, :reason => $!}
173 map "#{name} [*args]", :action => :failed_translator,
174 :defaults => {:name => name, :reason => $!}
178 def failed_translator(m, params)
179 m.reply _("Translator %{name} cannot be used: %{reason}") %
180 {:name => params[:name], :reason => params[:reason]}
183 def help(plugin, topic=nil)
184 case (topic.intern rescue nil)
186 unless @failed_translators.empty?
187 failed_list = @failed_translators.map { |t| _("%{bold}%{translator}%{bold}: %{reason}") % {
188 :translator => t[:name],
189 :reason => t[:reason],
193 _("Failed translators: %{list}") % { :list => failed_list.join(", ") }
195 _("None of the translators failed")
198 if @translators.has_key?(plugin)
199 translator = @translators[plugin]
200 _('%{translator} <from> <to> <phrase> => Look up phrase using %{info}, supported from -> to languages: %{directions}') % {
201 :translator => plugin,
202 :info => translator.class::INFO,
203 :directions => translator.directions.map do |source, targets|
204 _('%{source} -> %{targets}') %
205 {:source => source, :targets => targets.to_a.join(', ')}
209 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') %
210 {:translators => @translators.keys.join(', ')}
212 help_str << "\n" + _("%{bold}Note%{bold}: %{failed_amt} translators failed, see %{reverse}%{prefix}help translate failed%{reverse} for details") % {
213 :failed_amt => @failed_translators.size,
216 :prefix => @bot.config['core.address_prefix'].first
225 @languages ||= @translators.map { |t| t.last.directions.keys }.flatten.uniq
229 @default_translators = bot.config['translator.default_list'] & @translators.keys
232 def cmd_translator(m, params)
233 params[:to] = @bot.config['translator.destination'] if params[:to].nil?
234 params[:from] ||= 'auto'
235 translator = @default_translators.find {|t| @translators[t].support?(params[:from], params[:to])}
238 cmd_translate m, params.merge({:translator => translator, :show_provider => false})
240 m.reply _('None of the default translators (translator.default_list) supports translating from %{source} to %{target}') % {:source => params[:from], :target => params[:to]}
244 def cmd_translate(m, params)
245 # get the first word of the command
246 tname = params[:translator] || m.message[/\A(\w+)\s/, 1]
247 translator = @translators[tname]
248 from, to, phrase = params[:from], params[:to], params[:phrase].to_s
250 watch_for_fail(tname) do
252 translation = Timeout.timeout(@bot.config['translator.timeout']) do
253 translator.translate(phrase, from, to)
255 m.reply(if params[:show_provider]
256 _('%{translation} (provided by %{translator})') %
257 {:translation => translation, :translator => tname.gsub("_", " ")}
262 rescue Translator::UnsupportedDirectionError
263 m.reply _("%{translator} doesn't support translating from %{source} to %{target}") %
264 {:translator => tname, :source => from, :target => to}
265 rescue Translator::NoTranslationError
266 m.reply _('%{translator} failed to provide a translation') %
267 {:translator => tname}
268 rescue Timeout::Error
269 m.reply _('The translator timed out')
273 m.reply _('No translator called %{name}') % {:name => tname}
277 # URL translation has nothing to do with Translators so let's make it
278 # separate, and Google exclusive for now
279 def cmd_translate_url(m, params)
280 params[:to] = @bot.config['translator.destination'] if params[:to].nil?
281 params[:from] ||= 'auto'
283 translate_url = "http://translate.google.com/translate?sl=%{from}&tl=%{to}&u=%{url}" % {
284 :from => params[:from],
286 :url => CGI.escape(params[:url].to_s)
289 m.reply(translate_url)
293 plugin = TranslatorPlugin.new
294 req = Hash[*%w(from to).map { |e| [e.to_sym, /#{plugin.languages.join("|")}/] }.flatten]
296 plugin.map 'translate [:from] [:to] :url',
297 :action => :cmd_translate_url, :requirements => req.merge(:url => %r{^https?://[^\s]*})
298 plugin.map 'translator [:from] [:to] :url',
299 :action => :cmd_translate_url, :requirements => req.merge(:url => %r{^https?://[^\s]*})
300 plugin.map 'translate [:from] [:to] *phrase',
301 :action => :cmd_translator, :thread => true, :requirements => req
302 plugin.map 'translator [:from] [:to] *phrase',
303 :action => :cmd_translator, :thread => true, :requirements => req