translatomatic 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.gitattributes +20 -20
- data/.gitignore +19 -15
- data/.rspec +3 -3
- data/.rubocop.yml +28 -0
- data/.translatomatic/config.yml +4 -0
- data/.travis.yml +4 -6
- data/.yardopts +9 -9
- data/Gemfile +8 -4
- data/Guardfile +4 -5
- data/README.de.md +55 -50
- data/README.en.md +177 -0
- data/README.es.md +53 -48
- data/README.fr.md +53 -48
- data/README.it.md +54 -49
- data/README.ja.md +63 -58
- data/README.ko.md +59 -54
- data/README.md +17 -13
- data/README.ms.md +50 -45
- data/README.pt.md +54 -49
- data/README.ru.md +57 -52
- data/README.sv.md +51 -46
- data/README.zh.md +60 -55
- data/Rakefile +3 -3
- data/TODO.txt +6 -0
- data/bin/console +3 -3
- data/bin/translatomatic +4 -2
- data/config/i18n-tasks.yml +130 -0
- data/config/locales/translatomatic/de.yml +141 -99
- data/config/locales/translatomatic/en.yml +129 -89
- data/config/locales/translatomatic/es.yml +136 -99
- data/config/locales/translatomatic/fr.yml +139 -100
- data/config/locales/translatomatic/it.yml +135 -97
- data/config/locales/translatomatic/ja.yml +137 -98
- data/config/locales/translatomatic/ko.yml +138 -98
- data/config/locales/translatomatic/ms.yml +138 -100
- data/config/locales/translatomatic/pt.yml +137 -101
- data/config/locales/translatomatic/ru.yml +136 -98
- data/config/locales/translatomatic/sv.yml +134 -96
- data/config/locales/translatomatic/zh.yml +136 -97
- data/db/migrate/201712170000_initial.rb +2 -3
- data/lib/translatomatic.rb +40 -25
- data/lib/translatomatic/cli.rb +5 -1
- data/lib/translatomatic/cli/base.rb +61 -58
- data/lib/translatomatic/cli/common_options.rb +14 -11
- data/lib/translatomatic/cli/config.rb +96 -91
- data/lib/translatomatic/cli/database.rb +85 -23
- data/lib/translatomatic/cli/main.rb +158 -104
- data/lib/translatomatic/cli/thor.rb +29 -0
- data/lib/translatomatic/cli/translate.rb +134 -157
- data/lib/translatomatic/config.rb +10 -301
- data/lib/translatomatic/config/display.rb +78 -0
- data/lib/translatomatic/config/files.rb +60 -0
- data/lib/translatomatic/config/location_settings.rb +133 -0
- data/lib/translatomatic/config/options.rb +68 -0
- data/lib/translatomatic/config/selector.rb +127 -0
- data/lib/translatomatic/config/settings.rb +148 -0
- data/lib/translatomatic/converter.rb +40 -28
- data/lib/translatomatic/database.rb +127 -110
- data/lib/translatomatic/define_options.rb +4 -5
- data/lib/translatomatic/escaped_unicode.rb +86 -76
- data/lib/translatomatic/extractor.rb +5 -2
- data/lib/translatomatic/extractor/base.rb +12 -12
- data/lib/translatomatic/extractor/ruby.rb +7 -6
- data/lib/translatomatic/file_translator.rb +101 -244
- data/lib/translatomatic/flattenation.rb +39 -0
- data/lib/translatomatic/http.rb +13 -0
- data/lib/translatomatic/http/client.rb +144 -0
- data/lib/translatomatic/http/exception.rb +43 -0
- data/lib/translatomatic/http/file_param.rb +27 -0
- data/lib/translatomatic/http/param.rb +37 -0
- data/lib/translatomatic/http/request.rb +91 -0
- data/lib/translatomatic/i18n.rb +43 -0
- data/lib/translatomatic/locale.rb +71 -59
- data/lib/translatomatic/logger.rb +43 -28
- data/lib/translatomatic/metadata.rb +58 -0
- data/lib/translatomatic/model.rb +4 -2
- data/lib/translatomatic/model/locale.rb +5 -5
- data/lib/translatomatic/model/text.rb +5 -5
- data/lib/translatomatic/option.rb +57 -34
- data/lib/translatomatic/path_utils.rb +126 -0
- data/lib/translatomatic/progress_updater.rb +13 -16
- data/lib/translatomatic/provider.rb +101 -0
- data/lib/translatomatic/provider/base.rb +136 -0
- data/lib/translatomatic/provider/frengly.rb +55 -0
- data/lib/translatomatic/provider/google.rb +78 -0
- data/lib/translatomatic/provider/google_web.rb +50 -0
- data/lib/translatomatic/provider/microsoft.rb +144 -0
- data/lib/translatomatic/provider/my_memory.rb +75 -0
- data/lib/translatomatic/provider/yandex.rb +61 -0
- data/lib/translatomatic/resource_file.rb +59 -53
- data/lib/translatomatic/resource_file/base.rb +171 -237
- data/lib/translatomatic/resource_file/csv.rb +176 -24
- data/lib/translatomatic/resource_file/html.rb +21 -42
- data/lib/translatomatic/resource_file/key_value_support.rb +117 -0
- data/lib/translatomatic/resource_file/markdown.rb +36 -38
- data/lib/translatomatic/resource_file/plist.rb +121 -126
- data/lib/translatomatic/resource_file/po.rb +104 -82
- data/lib/translatomatic/resource_file/properties.rb +48 -77
- data/lib/translatomatic/resource_file/properties.treetop +87 -0
- data/lib/translatomatic/resource_file/resw.rb +56 -41
- data/lib/translatomatic/resource_file/subtitle.rb +86 -54
- data/lib/translatomatic/resource_file/text.rb +18 -18
- data/lib/translatomatic/resource_file/xcode_strings.rb +32 -63
- data/lib/translatomatic/resource_file/xcode_strings.treetop +85 -0
- data/lib/translatomatic/resource_file/xml.rb +94 -81
- data/lib/translatomatic/resource_file/yaml.rb +54 -68
- data/lib/translatomatic/retry_executor.rb +37 -0
- data/lib/translatomatic/slurp.rb +32 -0
- data/lib/translatomatic/string_batcher.rb +50 -0
- data/lib/translatomatic/string_escaping.rb +61 -0
- data/lib/translatomatic/text.rb +263 -0
- data/lib/translatomatic/text_collection.rb +66 -0
- data/lib/translatomatic/tmx.rb +5 -3
- data/lib/translatomatic/tmx/document.rb +107 -82
- data/lib/translatomatic/tmx/translation_unit.rb +19 -18
- data/lib/translatomatic/translation.rb +8 -28
- data/lib/translatomatic/translation/collection.rb +199 -0
- data/lib/translatomatic/translation/fetcher.rb +123 -0
- data/lib/translatomatic/translation/munging.rb +112 -0
- data/lib/translatomatic/translation/result.rb +50 -0
- data/lib/translatomatic/translation/sharer.rb +32 -0
- data/lib/translatomatic/translation/stats.rb +44 -0
- data/lib/translatomatic/translator.rb +91 -88
- data/lib/translatomatic/type_cast.rb +63 -0
- data/lib/translatomatic/util.rb +37 -33
- data/lib/translatomatic/version.rb +2 -2
- data/translatomatic.gemspec +57 -46
- metadata +136 -59
- data/lib/translatomatic/http_request.rb +0 -162
- data/lib/translatomatic/string.rb +0 -188
- data/lib/translatomatic/translation_result.rb +0 -86
- data/lib/translatomatic/translation_stats.rb +0 -31
- data/lib/translatomatic/translator/base.rb +0 -128
- data/lib/translatomatic/translator/frengly.rb +0 -62
- data/lib/translatomatic/translator/google.rb +0 -37
- data/lib/translatomatic/translator/microsoft.rb +0 -41
- data/lib/translatomatic/translator/my_memory.rb +0 -68
- data/lib/translatomatic/translator/yandex.rb +0 -56
@@ -0,0 +1,101 @@
|
|
1
|
+
module Translatomatic
|
2
|
+
# Provides methods to access and create instances of
|
3
|
+
# interfaces to translation APIs.
|
4
|
+
module Provider
|
5
|
+
class << self
|
6
|
+
include Translatomatic::Util
|
7
|
+
|
8
|
+
# @return [Class] The provider class corresponding to the given name
|
9
|
+
def find(name)
|
10
|
+
load_providers
|
11
|
+
name && !name.empty? ? const_get(name) : nil
|
12
|
+
end
|
13
|
+
|
14
|
+
# Resolve the given list of provider names to a list of providers.
|
15
|
+
# If the list is empty, return all providers that are configured.
|
16
|
+
# @param list [Array<String>] Provider names or providers
|
17
|
+
# @param options [Hash<String,String>] Provider options
|
18
|
+
# @return [Array<Translatomatic::Provider::Base>] Providers
|
19
|
+
def resolve(list, options = {})
|
20
|
+
list = [list].flatten.compact.collect do |provider|
|
21
|
+
if provider.respond_to?(:translate)
|
22
|
+
provider
|
23
|
+
else
|
24
|
+
klass = find(provider)
|
25
|
+
provider = create_provider(klass, options)
|
26
|
+
end
|
27
|
+
provider
|
28
|
+
end
|
29
|
+
|
30
|
+
# if we didn't resolve to any providers, find all available
|
31
|
+
# providers that work with the given options.
|
32
|
+
list.empty? ? available(options) : list
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [List<Class>] A list of all provider classes
|
36
|
+
def types
|
37
|
+
load_providers
|
38
|
+
constants.collect { |c| const_get(c) }.select do |klass|
|
39
|
+
klass.is_a?(Class) && klass != Translatomatic::Provider::Base
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [List<String>] A list of all providers
|
44
|
+
def names
|
45
|
+
types.collect { |i| i.name.demodulize }
|
46
|
+
end
|
47
|
+
|
48
|
+
# Find all configured providers
|
49
|
+
# @param options [Hash<String,String>] Provider options
|
50
|
+
# @return [Array<#translate>] A list of provider instances
|
51
|
+
def available(options = {})
|
52
|
+
available = types.collect { |klass| create_provider(klass, options) }
|
53
|
+
available.compact
|
54
|
+
end
|
55
|
+
|
56
|
+
# Get errors for the specified provider
|
57
|
+
# @param name [String] Provider name
|
58
|
+
def get_error(name)
|
59
|
+
@provider_errors[name]
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def test_provider(provider)
|
65
|
+
provider.languages
|
66
|
+
end
|
67
|
+
|
68
|
+
def create_provider(klass, options = {})
|
69
|
+
return nil unless klass
|
70
|
+
klass.new(options)
|
71
|
+
rescue StandardError => e
|
72
|
+
name = klass.name.demodulize
|
73
|
+
log.debug(t('provider.unavailable', name: name))
|
74
|
+
provider_error(name, e)
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
|
78
|
+
def loaded_providers?
|
79
|
+
@loaded_providers
|
80
|
+
end
|
81
|
+
|
82
|
+
def provider_error(name, e)
|
83
|
+
@provider_errors ||= {}
|
84
|
+
@provider_errors[name] = e
|
85
|
+
end
|
86
|
+
|
87
|
+
def load_providers
|
88
|
+
return if loaded_providers?
|
89
|
+
Dir[File.join(__dir__, 'provider/*.rb')].sort.each do |file|
|
90
|
+
begin
|
91
|
+
require file
|
92
|
+
rescue StandardError => e
|
93
|
+
name = File.basename(file, '.rb').classify
|
94
|
+
provider_error(name, e)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
@loaded_providers = true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module Translatomatic
|
2
|
+
module Provider
|
3
|
+
# Base class for interfaces to translation APIs
|
4
|
+
# @abstract
|
5
|
+
class Base
|
6
|
+
include Translatomatic::DefineOptions
|
7
|
+
|
8
|
+
# Listener for translation events
|
9
|
+
attr_accessor :listener
|
10
|
+
|
11
|
+
# @return [boolean] True if a string can have alternative translations
|
12
|
+
def self.supports_alternative_translations?
|
13
|
+
false
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [boolean] true if this provider supports html5
|
17
|
+
# <span translate="no"></span> tags.
|
18
|
+
def self.supports_no_translate_html?
|
19
|
+
false
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(options = {})
|
23
|
+
@listener = options[:listener]
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [String] The name of this provider.
|
27
|
+
def name
|
28
|
+
self.class.name.demodulize
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [String] The name of this provider
|
32
|
+
def to_s
|
33
|
+
name
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Array<String>] A list of languages
|
37
|
+
# supported by this provider.
|
38
|
+
def languages
|
39
|
+
[]
|
40
|
+
end
|
41
|
+
|
42
|
+
# Translate strings from one locale to another
|
43
|
+
# @param strings [Array<String,Text>] A list of text/strings to translate.
|
44
|
+
# @param from [String, Translatomatic::Locale] The locale of the
|
45
|
+
# given strings.
|
46
|
+
# @param to [String, Translatomatic::Locale] The locale to translate to.
|
47
|
+
# @return [Array<Translatomatic::Translation::Result>] Translations
|
48
|
+
def translate(strings, from, to)
|
49
|
+
@updated_listener = false
|
50
|
+
@translations = []
|
51
|
+
@from = from
|
52
|
+
@to = to
|
53
|
+
strings = [strings] unless strings.is_a?(Array)
|
54
|
+
from = build_locale(from)
|
55
|
+
to = build_locale(to)
|
56
|
+
if from.language == to.language
|
57
|
+
strings.each { |i| add_translations(i, i) }
|
58
|
+
else
|
59
|
+
perform_translate(strings, from, to)
|
60
|
+
end
|
61
|
+
@translations
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
include Translatomatic::Util
|
67
|
+
|
68
|
+
TRANSLATION_RETRIES = 3
|
69
|
+
|
70
|
+
# all subclasses must implement this
|
71
|
+
def perform_translate(_strings, _from, _to)
|
72
|
+
raise 'subclass must implement perform_translate'
|
73
|
+
end
|
74
|
+
|
75
|
+
# subclasses that call perform_fetch_translations must implement this
|
76
|
+
def fetch_translations(_string, _from, _to)
|
77
|
+
raise 'subclass must implement fetch_translations'
|
78
|
+
end
|
79
|
+
|
80
|
+
def http_client(*args)
|
81
|
+
@http_client ||= Translatomatic::HTTP::Client.new(*args)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Fetch translations for the given strings, one at a time, by
|
85
|
+
# opening a http connection to the given url and calling
|
86
|
+
# fetch_translation() on each string. Error handling and recovery
|
87
|
+
# is performed by this method.
|
88
|
+
# (subclass must implement fetch_translation if this method is used)
|
89
|
+
def perform_fetch_translations(url, strings, from, to)
|
90
|
+
untranslated = strings.dup
|
91
|
+
|
92
|
+
http_client.start(url) do |_http|
|
93
|
+
until untranslated.empty?
|
94
|
+
# get next string to translate
|
95
|
+
string = untranslated[0]
|
96
|
+
# fetch translation
|
97
|
+
fetch_translations(string, from, to)
|
98
|
+
untranslated.shift
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def add_translations(original, result)
|
104
|
+
# successful translation
|
105
|
+
result = [result] unless result.is_a?(Array)
|
106
|
+
result = convert_to_translations(original, result)
|
107
|
+
@listener.update_progress(1) if @listener
|
108
|
+
@translations += result
|
109
|
+
end
|
110
|
+
|
111
|
+
def convert_to_translations(original, result)
|
112
|
+
result.collect { |i| translation(original, i) }.compact
|
113
|
+
end
|
114
|
+
|
115
|
+
def translation(original, translated)
|
116
|
+
return nil if translated.blank?
|
117
|
+
string1 = Translatomatic::Text[original, @from]
|
118
|
+
string2 = Translatomatic::Text[translated, @to]
|
119
|
+
Translatomatic::Translation::Result.new(string1, string2, name)
|
120
|
+
end
|
121
|
+
|
122
|
+
def batcher(strings, max_count:, max_length:)
|
123
|
+
StringBatcher.new(strings, max_count: max_count, max_length: max_length)
|
124
|
+
end
|
125
|
+
|
126
|
+
def try_hash(hash, *keys)
|
127
|
+
result = hash
|
128
|
+
keys.each do |key|
|
129
|
+
result ||= {}
|
130
|
+
result = result.is_a?(Hash) ? result[key] : nil
|
131
|
+
end
|
132
|
+
result
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module Translatomatic
|
4
|
+
module Provider
|
5
|
+
# Interface to the Frengly translation API
|
6
|
+
# @see http://www.frengly.com/api
|
7
|
+
class Frengly < Base
|
8
|
+
define_option :frengly_api_key, use_env: true,
|
9
|
+
desc: t('provider.frengly.api_key')
|
10
|
+
define_option :frengly_email, use_env: true,
|
11
|
+
desc: t('provider.email_address')
|
12
|
+
define_option :frengly_password, use_env: true,
|
13
|
+
desc: t('provider.password')
|
14
|
+
|
15
|
+
# Create a new Frengly provider instance
|
16
|
+
def initialize(options = {})
|
17
|
+
super(options)
|
18
|
+
@key = options[:frengly_api_key] || ENV['FRENGLY_API_KEY'] # optional
|
19
|
+
@email = options[:frengly_email]
|
20
|
+
@password = options[:frengly_password]
|
21
|
+
raise t('provider.email_required') unless @email
|
22
|
+
raise t('provider.password_required') unless @password
|
23
|
+
end
|
24
|
+
|
25
|
+
# (see Base#languages)
|
26
|
+
def languages
|
27
|
+
%w[en fr de es pt it nl tl fi el iw pl ru sv]
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
URL = 'http://frengly.com/frengly/data/translateREST'.freeze
|
33
|
+
|
34
|
+
def perform_translate(strings, from, to)
|
35
|
+
perform_fetch_translations(URL, strings, from, to)
|
36
|
+
end
|
37
|
+
|
38
|
+
def fetch_translations(string, from, to)
|
39
|
+
body = {
|
40
|
+
src: from.language,
|
41
|
+
dest: to.language,
|
42
|
+
text: string,
|
43
|
+
email: @email,
|
44
|
+
password: @password,
|
45
|
+
premiumkey: @key
|
46
|
+
}.to_json
|
47
|
+
|
48
|
+
# TODO: work out what the response looks like
|
49
|
+
response = http_client.post(URL, body, content_type: 'application/json')
|
50
|
+
data = JSON.parse(response.body)
|
51
|
+
add_translations(string, data['text'])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
|
2
|
+
module Translatomatic
|
3
|
+
module Provider
|
4
|
+
# Interface to the Google translation API
|
5
|
+
# @see https://cloud.google.com/translate/
|
6
|
+
class Google < Base
|
7
|
+
define_option :google_api_key,
|
8
|
+
desc: t('provider.google.api_key'), use_env: true
|
9
|
+
define_option :google_model, enum: %i[base nmt],
|
10
|
+
desc: t('provider.google.model'), use_env: true
|
11
|
+
|
12
|
+
# (see Base.supports_no_translate_html?)
|
13
|
+
def self.supports_no_translate_html?
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
# Create a new Google provider instance
|
18
|
+
def initialize(options = {})
|
19
|
+
super(options)
|
20
|
+
@key = options[:google_api_key] || ENV['GOOGLE_API_KEY']
|
21
|
+
raise t('provider.google.key_required') if @key.nil?
|
22
|
+
@model = options[:google_model]
|
23
|
+
end
|
24
|
+
|
25
|
+
# (see Base#languages)
|
26
|
+
def languages
|
27
|
+
@languages ||= fetch_languages
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
BASE_URL = 'https://translation.googleapis.com'.freeze
|
33
|
+
TRANSLATE_URL = (BASE_URL + '/language/translate/v2').freeze
|
34
|
+
LANGUAGES_URL = (BASE_URL + '/language/translate/v2/languages').freeze
|
35
|
+
# https://cloud.google.com/translate/faq
|
36
|
+
# TODO: limit requested characters per second?
|
37
|
+
LIMIT = [128, 5000].freeze # strings, characters per request
|
38
|
+
|
39
|
+
def perform_translate(strings, from, to)
|
40
|
+
batcher(strings, max_count: LIMIT[0], max_length: LIMIT[1])
|
41
|
+
.each_batch do |texts|
|
42
|
+
perform_translate_texts(texts, from, to)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def perform_translate_texts(texts, from, to)
|
47
|
+
request_body = request_body(texts, from, to)
|
48
|
+
response = http_client.post(TRANSLATE_URL, request_body)
|
49
|
+
body = JSON.parse(response.body)
|
50
|
+
data = body['data'] || {}
|
51
|
+
translations = data['translations'] || []
|
52
|
+
translations = translations.collect { |i| i['translatedText'] }
|
53
|
+
texts.zip(translations).each do |original, translated|
|
54
|
+
add_translations(original, translated)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def request_body(strings, from, to)
|
59
|
+
body = {
|
60
|
+
q: strings,
|
61
|
+
source: from.language,
|
62
|
+
target: to.language,
|
63
|
+
format: 'html', # required for <span translate="no"></span>
|
64
|
+
key: @key
|
65
|
+
}
|
66
|
+
body[:model] = @model if @model
|
67
|
+
body
|
68
|
+
end
|
69
|
+
|
70
|
+
def fetch_languages
|
71
|
+
response = http_client.get(LANGUAGES_URL, key: @key)
|
72
|
+
body = JSON.parse(response.body)
|
73
|
+
languages = try_hash(body, 'data', 'languages') || []
|
74
|
+
languages.collect { |i| i['language'] }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Translatomatic
|
2
|
+
module Provider
|
3
|
+
# Google translation web interface.
|
4
|
+
# supports multiple translations
|
5
|
+
# @see https://translate.google.com.au
|
6
|
+
class GoogleWeb < Base
|
7
|
+
attr_accessor :dt
|
8
|
+
|
9
|
+
# (see Base.supports_alternative_translations?)
|
10
|
+
def self.supports_alternative_translations?
|
11
|
+
true
|
12
|
+
end
|
13
|
+
|
14
|
+
# Create a new GoogleWeb provider instance
|
15
|
+
def initialize(options = {})
|
16
|
+
super(options)
|
17
|
+
require 'google_web_translate'
|
18
|
+
@dt = %w[t at]
|
19
|
+
@debug = options[:debug]
|
20
|
+
end
|
21
|
+
|
22
|
+
# (see Base#languages)
|
23
|
+
def languages
|
24
|
+
api.respond_to?(:languages) ? api.languages : []
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def api
|
30
|
+
options = { debug: @debug, dt: @dt, http_client: http_client }
|
31
|
+
@api ||= GoogleWebTranslate::API.new(options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def perform_translate(strings, from, to)
|
35
|
+
strings.each do |string|
|
36
|
+
result = api.translate(string, from, to)
|
37
|
+
add_translations(string, translations_from_result(result))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def translations_from_result(result)
|
42
|
+
if result.alternatives.present?
|
43
|
+
result.alternatives
|
44
|
+
else
|
45
|
+
result.translation
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'builder'
|
2
|
+
|
3
|
+
module Translatomatic
|
4
|
+
module Provider
|
5
|
+
# Interface to the Microsoft translation API
|
6
|
+
# @see https://www.microsoft.com/en-us/translator/translatorapi.aspx
|
7
|
+
# @see http://docs.microsofttranslator.com/text-translate.html
|
8
|
+
class Microsoft < Base
|
9
|
+
define_option :microsoft_api_key,
|
10
|
+
desc: t('provider.microsoft.api_key'), use_env: true
|
11
|
+
|
12
|
+
# (see Base.supports_alternative_translations?)
|
13
|
+
def self.supports_alternative_translations?
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
# (see Base.supports_no_translate_html?)
|
18
|
+
def self.supports_no_translate_html?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
# Create a new Microsoft provider instance
|
23
|
+
def initialize(options = {})
|
24
|
+
super(options)
|
25
|
+
@key = options[:microsoft_api_key] || ENV['MICROSOFT_API_KEY']
|
26
|
+
raise t('provider.microsoft.key_required') if @key.nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
# (see Base#languages)
|
30
|
+
def languages
|
31
|
+
@languages ||= fetch_languages
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
BASE_URL = 'https://api.microsofttranslator.com/V2/Http.svc'.freeze
|
37
|
+
# this endpoint returns one translation per source text
|
38
|
+
TRANSLATE_URL1 = "#{BASE_URL}/TranslateArray".freeze
|
39
|
+
LIMITS_URL1 = [2000, 10_000].freeze # strings, characters per request
|
40
|
+
# this url returns multiple translations
|
41
|
+
TRANSLATE_URL2 = "#{BASE_URL}/GetTranslationsArray".freeze
|
42
|
+
LIMITS_URL2 = [10, 10_000].freeze # strings, characters per request
|
43
|
+
MAX_TRANSLATIONS = 10 # for URL2
|
44
|
+
LANGUAGES_URL = "#{BASE_URL}/GetLanguagesForTranslate".freeze
|
45
|
+
ARRAYS_NS = 'http://schemas.microsoft.com/2003/10/Serialization/Arrays'.freeze
|
46
|
+
WEB_SERVICE_NS = 'http://schemas.datacontract.org/2004/07/Microsoft.MT.Web.Service.V2'.freeze
|
47
|
+
|
48
|
+
def perform_translate(strings, from, to)
|
49
|
+
# get multiple translations for strings with context, so we
|
50
|
+
# can choose the best translation.
|
51
|
+
strings_with_context = strings.select { |i| context?(i) }
|
52
|
+
strings_without_context = strings.reject { |i| context?(i) }
|
53
|
+
|
54
|
+
fetch_translation_array(strings_with_context, from, to, true)
|
55
|
+
fetch_translation_array(strings_without_context, from, to, false)
|
56
|
+
end
|
57
|
+
|
58
|
+
# fetch translations for given strings
|
59
|
+
def fetch_translation_array(strings, from, to, multiple)
|
60
|
+
limit = multiple ? LIMITS_URL2 : LIMITS_URL1
|
61
|
+
batcher(strings, max_count: limit[0], max_length: limit[1])
|
62
|
+
.each_batch do |texts|
|
63
|
+
translate_texts(texts, from, to, multiple)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def translate_texts(texts, from, to, multiple)
|
68
|
+
url = multiple ? TRANSLATE_URL2 : TRANSLATE_URL1
|
69
|
+
headers = { 'Ocp-Apim-Subscription-Key' => @key }
|
70
|
+
body = build_body_xml(texts, from, to, multiple)
|
71
|
+
response = http_client.post(url, body,
|
72
|
+
headers: headers,
|
73
|
+
content_type: 'application/xml')
|
74
|
+
add_translations_from_response(response, texts, multiple)
|
75
|
+
end
|
76
|
+
|
77
|
+
def add_translations_from_response(response, texts, multiple)
|
78
|
+
doc = Nokogiri::XML(response.body)
|
79
|
+
if multiple
|
80
|
+
add_translations_from_response_multiple(doc, texts)
|
81
|
+
else
|
82
|
+
results = doc.search('//xmlns:TranslatedText').collect(&:content)
|
83
|
+
texts.zip(results).each do |original, tr|
|
84
|
+
add_translations(original, tr)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def add_translations_from_response_multiple(doc, texts)
|
90
|
+
# there should be one GetTranslationsResponse for each string
|
91
|
+
responses = doc.search('//xmlns:GetTranslationsResponse')
|
92
|
+
texts.zip(responses).each do |original, tr|
|
93
|
+
results = tr.search('TranslatedText').collect(&:content)
|
94
|
+
add_translations(original, results)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def fetch_languages
|
99
|
+
# this request redirects to a html page
|
100
|
+
headers = { 'Ocp-Apim-Subscription-Key' => @key }
|
101
|
+
query = { 'appid' => '' }
|
102
|
+
response = http_client.get(LANGUAGES_URL, query, headers: headers)
|
103
|
+
log.debug("#{name} response: #{response.body}")
|
104
|
+
doc = Nokogiri::XML(response.body)
|
105
|
+
doc.search('//xmlns:string').collect(&:content)
|
106
|
+
end
|
107
|
+
|
108
|
+
def xml_root(multiple)
|
109
|
+
multiple ? 'GetTranslationsArray' : 'TranslateArray'
|
110
|
+
end
|
111
|
+
|
112
|
+
def build_body_xml(strings, from, to, multiple)
|
113
|
+
root = xml_root(multiple) + 'Request'
|
114
|
+
xml = Builder::XmlMarkup.new
|
115
|
+
xml.tag!(root, 'xmlns:a' => ARRAYS_NS) do
|
116
|
+
xml.AppId
|
117
|
+
xml.From(from)
|
118
|
+
build_options_xml(xml) if multiple
|
119
|
+
build_texts_xml(xml, strings)
|
120
|
+
xml.To(to)
|
121
|
+
xml.MaxTranslations MAX_TRANSLATIONS if multiple
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def build_texts_xml(xml, strings)
|
126
|
+
xml.Texts do
|
127
|
+
strings.each do |string|
|
128
|
+
xml.tag!('a:string', string)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def build_options_xml(xml)
|
134
|
+
xml.tag!('Options', 'xmlns:o' => WEB_SERVICE_NS) do
|
135
|
+
xml.tag!('o:IncludeMultipleMTAlternatives', 'true')
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def context?(text)
|
140
|
+
text.is_a?(Translatomatic::Text) && text.context.present?
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|