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
15 # base class for implementing a translation service
17 # direction:: supported translation directions, a hash where each key is a source
18 # language name, and each value is Set of target language names. The
19 # methods in the Direction module are convenient for initializing this
23 class UnsupportedDirectionError < ArgumentError
26 attr_reader :directions, :cache
28 def initialize(directions, cache={})
29 @directions = directions
34 # whether the translator supports this direction
35 def support?(from, to)
36 from != to && @directions[from].include?(to)
39 # this implements checking of languages and caching. subclasses should define the
40 # do_translate method to implement actual translation
41 def translate(text, from, to)
42 raise UnsupportedDirectionError unless support?(from, to)
43 @cache[[text, from, to]] ||= do_translate(text, from, to)
47 # given the set of supported languages, return a hash suitable for the directions
48 # attribute which includes any language to any other language
49 def self.all_to_all(languages)
50 directions = all_to_all(languages)
51 languages.each {|l| directions[l] = languages.to_set}
55 # a hash suitable for the directions attribute which includes any language from/to
56 # the given set of languages (center_languages)
57 def self.all_from_to(languages, center_languages)
58 directions = all_to_none(languages)
59 center_languages.each {|l| directions[l] = languages - [l]}
60 (languages - center_languages).each {|l| directions[l] = center_languages.to_set}
64 # get a hash from a list of pairs
65 def self.pairs(list_of_pairs)
66 languages = list_of_pairs.flatten.to_set
67 directions = all_to_none(languages)
68 list_of_pairs.each do |(from, to)|
69 directions[from] << to
74 # an empty hash with empty sets as default values
75 def self.all_to_none(languages)
77 # always return empty set when the key is non-existent, but put empty set in the
78 # hash only if the key is one of the languages
79 if languages.include? k
90 class NiftyTranslator < Translator
91 def initialize(cache={})
93 super(Translator::Direction.all_from_to(%w[ja en zh_CN ko], %w[ja]), cache)
94 @form = WWW::Mechanize.new.
95 get('http://nifty.amikai.com/amitext/indexUTF8.jsp').
96 forms.name('translateForm').first
99 def do_translate(text, from, to)
100 @form.radiobuttons.name('langpair').value = "#{from},#{to}".upcase
101 @form.fields.name('sourceText').value = text
103 @form.submit(@form.buttons.name('translate')).
104 forms.name('translateForm').fields.name('translatedText').value
109 class ExciteTranslator < Translator
111 def initialize(cache={})
115 super(Translator::Direction.all_from_to(%w[ja en zh_CN zh_TW ko], %w[ja]), cache)
117 @forms = Hash.new do |h, k|
120 h[k] = open_form('english')
121 when 'zh_CN', 'zh_TW'
122 # this way we don't need to fetch the same page twice
123 h['zh_CN'] = h['zh_TW'] = open_form('chinese')
125 h[k] = open_form('korean')
131 WWW::Mechanize.new.get("http://www.excite.co.jp/world/#{name}").
132 forms.name('world').first
135 def do_translate(text, from, to)
136 non_ja_language = from != 'ja' ? from : to
137 form = @forms[non_ja_language]
139 if non_ja_language =~ /zh_(CN|TW)/
140 form.fields.name('wb_lp').value = "#{from}#{to}".sub(/_(?:CN|TW)/, '').upcase
141 form.fields.name('big5').value = ($1 == 'TW' ? 'yes' : 'no')
143 # the en<->ja page is in Shift_JIS while other pages are UTF-8
144 text = Iconv.iconv('Shift_JIS', 'UTF-8', text) if non_ja_language == 'en'
145 form.fields.name('wb_lp').value = "#{from}#{to}".upcase
147 form.fields.name('before').value = text
148 result = form.submit.forms.name('world').fields.name('after').value
149 # the en<->ja page is in Shift_JIS while other pages are UTF-8
150 if non_ja_language == 'en'
151 Iconv.iconv('UTF-8', 'Shift_JIS', result)
160 class GoogleTranslator < Translator
161 def initialize(cache={})
164 language_pairs = @lang_list.options.map do |o|
165 # these options have values like "en|zh-CN"; map to things like ['en', 'zh_CN'].
166 o.value.split('|').map {|l| l.sub('-', '_')}
168 super(Translator::Direction.pairs(language_pairs), cache)
172 agent = WWW::Mechanize.new
173 # without faking the user agent, Google Translate will serve non-UTF-8 text
174 agent.user_agent_alias = 'Linux Konqueror'
175 @form = agent.get('http://www.google.com/translate_t').
176 forms.action('/translate_t').first
177 @lang_list = @form.fields.name('langpair')
180 def do_translate(text, from, to)
183 @lang_list.value = "#{from}|#{to}".sub('_', '-')
184 @form.fields.name('text').value = text
185 @form.submit.parser.search('div#result_box').inner_html
190 class BabelfishTranslator < Translator
191 def initialize(cache)
194 @form = WWW::Mechanize.new.get('http://babelfish.altavista.com/babelfish/').
195 forms.name('frmTrText').first
196 @lang_list = @form.fields.name('lp')
197 language_pairs = @lang_list.options.map {|o| o.value.split('_')}.
198 reject {|p| p.empty?}
199 super(Translator::Direction.pairs(language_pairs), cache)
202 def translate(text, from, to)
204 if @form.fields.name('trtext').empty?
205 @form.add_field!('trtext', text)
207 @form.fields.name('trtext').value = text
209 @lang_list.value = "#{from}_#{to}"
210 @form.submit.parser.search("td.s/div[@style]").inner_html
214 class TranslatorPlugin < Plugin
215 BotConfig.register BotConfigIntegerValue.new('translate.timeout',
216 :default => 30, :validate => Proc.new{|v| v > 0},
217 :desc => _("Number of seconds to wait for the translation service before timeout"))
221 translator_classes = {
222 'nifty' => NiftyTranslator,
223 'excite' => ExciteTranslator,
224 'google_translate' => GoogleTranslator,
225 'babelfish' => BabelfishTranslator
230 translator_classes.each_pair do |name, c|
232 @translators[name] = c.new(@registry.sub_registry(name))
233 map "#{name} :from :to *phrase", :action => :cmd_translate
235 warning _("Translator %{name} cannot be used: %{reason}") %
236 {:name => name, :reason => $!}
241 def help(plugin, topic=nil)
242 if @translators.has_key?(topic)
243 _('Supported directions of translation for %{translator}: %{directions}') % {
244 :translator => topic,
245 :directions => @translators[topic].directions.map do |source, targets|
246 _('%{source} -> %{targets}') %
247 {:source => source, :targets => targets.to_a.join(', ')}
251 _('Command: <translator> <from> <to> <phrase>, where <translator> is one of: %{translators}. Use help <translator> to look up supported from and to languages') %
252 {:translators => @translators.keys.join(', ')}
256 def cmd_translate(m, params)
257 # get the first word of the command
258 translator = @translators[m.message[/\A(\w+)\s/, 1]]
259 from, to, phrase = params[:from], params[:to], params[:phrase].to_s
262 if translator.support?(from, to)
263 translation = Timeout.timeout(@bot.config['translate.timeout']) do
264 translator.translate(phrase, from, to)
266 if translation.empty?
267 m.reply _('No translation returned')
272 m.reply _("%{translator} doesn't support translating from %{source} to %{target}") %
273 {:source => from, :target => to}
275 rescue Timeout::Error
276 m.reply _('The translator timed out')
279 m.reply _('No translator called %{name}') % {:name => translator}
284 plugin = TranslatorPlugin.new