translatomatic 0.1.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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +51 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +6 -0
  7. data/Gemfile.lock +137 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +74 -0
  10. data/Rakefile +6 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/bin/translatomatic +6 -0
  14. data/db/database.yml +9 -0
  15. data/db/migrate/201712170000_initial.rb +23 -0
  16. data/lib/translatomatic/cli.rb +92 -0
  17. data/lib/translatomatic/config.rb +26 -0
  18. data/lib/translatomatic/converter.rb +157 -0
  19. data/lib/translatomatic/converter_stats.rb +27 -0
  20. data/lib/translatomatic/database.rb +105 -0
  21. data/lib/translatomatic/escaped_unicode.rb +90 -0
  22. data/lib/translatomatic/model/locale.rb +22 -0
  23. data/lib/translatomatic/model/text.rb +13 -0
  24. data/lib/translatomatic/model.rb +4 -0
  25. data/lib/translatomatic/option.rb +24 -0
  26. data/lib/translatomatic/resource_file/base.rb +137 -0
  27. data/lib/translatomatic/resource_file/html.rb +33 -0
  28. data/lib/translatomatic/resource_file/plist.rb +29 -0
  29. data/lib/translatomatic/resource_file/properties.rb +60 -0
  30. data/lib/translatomatic/resource_file/text.rb +28 -0
  31. data/lib/translatomatic/resource_file/xcode_strings.rb +65 -0
  32. data/lib/translatomatic/resource_file/xml.rb +64 -0
  33. data/lib/translatomatic/resource_file/yaml.rb +80 -0
  34. data/lib/translatomatic/resource_file.rb +74 -0
  35. data/lib/translatomatic/translation_result.rb +68 -0
  36. data/lib/translatomatic/translator/base.rb +47 -0
  37. data/lib/translatomatic/translator/frengly.rb +64 -0
  38. data/lib/translatomatic/translator/google.rb +30 -0
  39. data/lib/translatomatic/translator/microsoft.rb +32 -0
  40. data/lib/translatomatic/translator/my_memory.rb +55 -0
  41. data/lib/translatomatic/translator/yandex.rb +37 -0
  42. data/lib/translatomatic/translator.rb +63 -0
  43. data/lib/translatomatic/util.rb +24 -0
  44. data/lib/translatomatic/version.rb +3 -0
  45. data/lib/translatomatic.rb +27 -0
  46. data/translatomatic.gemspec +46 -0
  47. metadata +329 -0
@@ -0,0 +1,60 @@
1
+ module Translatomatic::ResourceFile
2
+ class Properties < Base
3
+
4
+ def self.extensions
5
+ %w{properties}
6
+ end
7
+
8
+ # (see Translatomatic::ResourceFile::Base#initialize)
9
+ def initialize(path, locale = nil)
10
+ super(path, locale)
11
+ @valid = true
12
+ @properties = @path.exist? ? read(@path) : {}
13
+ end
14
+
15
+ # (see Translatomatic::ResourceFile::Base#save(target))
16
+ def save(target = path)
17
+ out = ""
18
+ properties.each do |key, value|
19
+ # TODO: maintain original line ending format?
20
+ value = value.gsub("\n", "\\n") # convert newlines to \n
21
+ out += "#{key} = #{value}\n"
22
+ end
23
+ # escape unicode characters
24
+ out = Translatomatic::EscapedUnicode.escape(out)
25
+ target.write(out)
26
+ end
27
+
28
+ private
29
+
30
+ # parse key = value property file
31
+ def read(path)
32
+ contents = path.read
33
+ # convert escaped unicode characters into unicode
34
+ contents = Translatomatic::EscapedUnicode.unescape(contents)
35
+ result = {}
36
+ contents.gsub!(/\\\s*\n\s*/m, '') # put multi line strings on one line
37
+ lines = contents.split("\n")
38
+
39
+ lines.each do |line|
40
+ line.strip!
41
+ next if line.length == 0
42
+ equal_idx = line.index("=")
43
+
44
+ if line[0] == ?! || line[0] == ?#
45
+ # comment
46
+ # TODO: translate comments or keep originals?
47
+ next
48
+ elsif equal_idx.nil?
49
+ @valid = false
50
+ return {}
51
+ end
52
+ name, value = line.split(/\s*=\s*/, 2)
53
+ value = value.gsub("\\n", "\n") # convert \n to newlines
54
+ result[name] = value
55
+ end
56
+ result
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,28 @@
1
+ module Translatomatic::ResourceFile
2
+ class Text < Base
3
+
4
+ def self.extensions
5
+ %w{txt md text}
6
+ end
7
+
8
+ # (see Translatomatic::ResourceFile::Base#initialize)
9
+ def initialize(path, locale = nil)
10
+ super(path, locale)
11
+ @valid = true
12
+ @properties = @path.exist? ? read(@path) : {}
13
+ end
14
+
15
+ # (see Translatomatic::ResourceFile::Base#save(target))
16
+ def save(target = path)
17
+ values = @properties.values.collect { |i| i.strip + "\n" }
18
+ target.write(values.join)
19
+ end
20
+
21
+ private
22
+
23
+ def read(path)
24
+ text = path.read
25
+ { "text" => text }
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,65 @@
1
+ module Translatomatic::ResourceFile
2
+
3
+ # XCode strings file
4
+ # @see https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/LoadingResources/Strings/Strings.html
5
+ class XCodeStrings < Base
6
+
7
+ def self.extensions
8
+ %w{strings}
9
+ end
10
+
11
+ # (see Translatomatic::ResourceFile::Base#initialize)
12
+ def initialize(path, locale = nil)
13
+ super(path, locale)
14
+ @valid = true
15
+ @properties = @path.exist? ? read(@path) : {}
16
+ end
17
+
18
+ # (see Translatomatic::ResourceFile::Base#locale_path)
19
+ # @note localization files in XCode use the following file name
20
+ # convention: Project/locale.lproj/filename
21
+ def locale_path(locale)
22
+ if path.to_s.match(/\/([-\w]+).lproj\/.+.strings$/)
23
+ # xcode style
24
+ filename = path.basename
25
+ path.parent.parent + (locale.to_s + ".lproj") + filename
26
+ else
27
+ super(locale)
28
+ end
29
+ end
30
+
31
+ # (see Translatomatic::ResourceFile::Base#save(target))
32
+ def save(target = path)
33
+ out = ""
34
+ properties.each do |key, value|
35
+ key = escape(key)
36
+ value = escape(value)
37
+ out += %Q{"#{key}" = "#{value}";\n}
38
+ end
39
+ target.write(out)
40
+ end
41
+
42
+ private
43
+
44
+ def read(path)
45
+ result = {}
46
+ content = path.read
47
+ uncommented = content.gsub(/\/\*.*?\*\//, '')
48
+ key_values = uncommented.scan(/"(.*?[^\\])"\s*=\s*"(.*?[^\\])"\s*;/m)
49
+ key_values.each do |entry|
50
+ key, value = entry
51
+ result[unescape(key)] = unescape(value)
52
+ end
53
+ result
54
+ end
55
+
56
+ def unescape(string)
57
+ string ? string.gsub(/\\(["'])/) { |i| i } : ''
58
+ end
59
+
60
+ def escape(string)
61
+ string ? string.gsub(/["']/) { |i| "\\#{i}" } : ''
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,64 @@
1
+ module Translatomatic::ResourceFile
2
+ class XML < Base
3
+
4
+ def self.extensions
5
+ %w{xml}
6
+ end
7
+
8
+ # (see Translatomatic::ResourceFile::Base#initialize)
9
+ def initialize(path, locale = nil)
10
+ super(path, locale)
11
+ @valid = true
12
+ @properties = @path.exist? ? read(@path) : {}
13
+ end
14
+
15
+ # (see Translatomatic::ResourceFile::Base#set)
16
+ def set(key, value)
17
+ super(key, value)
18
+ @nodemap[key].content = value if @nodemap.include?(key)
19
+ end
20
+
21
+ # (see Translatomatic::ResourceFile::Base#save(target))
22
+ def save(target = path)
23
+ target.write(@doc.to_xml) if @doc
24
+ end
25
+
26
+ private
27
+
28
+ # initialize nodemap from nokogiri document
29
+ # returns property hash
30
+ def init_nodemap(doc)
31
+ # map of key1 => node, key2 => node, ...
32
+ @nodemap = flatten_xml(doc)
33
+ # map of key => node content
34
+ @nodemap.transform_values { |v| v.content }
35
+ end
36
+
37
+ # parse key = value property file
38
+ def read(path)
39
+ begin
40
+ # parse xml with nokogiri
41
+ @doc = Nokogiri::XML(path.open) do |config|
42
+ config.noblanks
43
+ end
44
+ init_nodemap(@doc)
45
+ rescue Exception
46
+ @valid = false
47
+ {}
48
+ end
49
+ end
50
+
51
+ def flatten_xml(doc)
52
+ result = {}
53
+ text_nodes = doc.search(text_nodes_xpath)
54
+ text_nodes.each_with_index do |node, i|
55
+ result["key#{i + 1}"] = node
56
+ end
57
+ result
58
+ end
59
+
60
+ def text_nodes_xpath
61
+ '//text()'
62
+ end
63
+ end # class
64
+ end # module
@@ -0,0 +1,80 @@
1
+ require 'yaml'
2
+
3
+ module Translatomatic::ResourceFile
4
+ class YAML < Base
5
+
6
+ def self.extensions
7
+ %w{yml yaml}
8
+ end
9
+
10
+ # (see Translatomatic::ResourceFile::Base#initialize)
11
+ def initialize(path, locale = nil)
12
+ super(path, locale)
13
+ @valid = true
14
+ @data = {}
15
+ @properties = @path.exist? ? read : {}
16
+ end
17
+
18
+ # (see Translatomatic::ResourceFile::Base#locale_path)
19
+ # @note localization files in rails use the following file name
20
+ # convention: config/locales/en.yml.
21
+ def locale_path(locale)
22
+ if path.to_s.match(/config\/locales\/[-\w]+.yml$/)
23
+ # rails style
24
+ filename = locale.to_s + path.extname
25
+ path.dirname + filename
26
+ else
27
+ super(locale)
28
+ end
29
+ end
30
+
31
+ # (see Translatomatic::ResourceFile::Base#set)
32
+ def set(key, value)
33
+ super(key, value)
34
+
35
+ hash = @data
36
+ path = key.split(/\./)
37
+ last_key = path.pop
38
+ path.each { |i| hash = (hash[i] ||= {}) }
39
+ hash[last_key] = value
40
+ end
41
+
42
+ # (see Translatomatic::ResourceFile::Base#save(target))
43
+ def save(target = path)
44
+ out = @data.to_yaml
45
+ out.sub!(/^---\n/m, '')
46
+ target.write(out)
47
+ end
48
+
49
+ private
50
+
51
+ def read
52
+ begin
53
+ @data = ::YAML.load_file(@path) || {}
54
+ flatten_data(@data)
55
+ rescue Exception
56
+ @valid = false
57
+ {}
58
+ end
59
+ end
60
+
61
+ def flatten_data(data)
62
+ result = {}
63
+ unless data.kind_of?(Hash)
64
+ @valid = false
65
+ return {}
66
+ end
67
+ data.each do |key, value|
68
+ if value.kind_of?(Hash)
69
+ children = flatten_data(value)
70
+ children.each do |ck, cv|
71
+ result[key + "." + ck] = cv
72
+ end
73
+ else
74
+ result[key] = value
75
+ end
76
+ end
77
+ result
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,74 @@
1
+
2
+ module Translatomatic
3
+ module ResourceFile
4
+ class << self
5
+ include Translatomatic::Util
6
+ end
7
+
8
+ # Load a resource file. If locale is not specified, the locale of the
9
+ # file will be determined from the filename, or else the current default
10
+ # locale will be used.
11
+ # @param [String] path Path to the resource file
12
+ # @param [String] locale Locale of the resource file
13
+ # @return [Translatomatic::ResourceFile::Base] The resource file, or nil
14
+ # if the file type is unsupported.
15
+ def self.load(path, locale = nil)
16
+ path = path.kind_of?(Pathname) ? path : Pathname.new(path)
17
+ modules.each do |mod|
18
+ # match on entire filename to support extensions containing locales
19
+ if extension_match(mod, path)
20
+ log.debug("attempting to load #{path.to_s} using #{mod.name.demodulize}")
21
+ file = mod.new(path, locale)
22
+ return file if file.valid?
23
+ end
24
+ end
25
+ nil
26
+ end
27
+
28
+ # Find all resource files under the given directory. Follows symlinks.
29
+ # @param [String, Pathname] path The path to search from
30
+ # @return [Array<Translatomatic::ResourceFile>] Resource files found
31
+ def self.find(path, options = {})
32
+ files = []
33
+ include_dot_directories = options[:include_dot_directories]
34
+ path = Pathname.new(path) unless path.kind_of?(Pathname)
35
+ path.find do |file|
36
+ if !include_dot_directories && file.basename.to_s[0] == ?.
37
+ Find.prune
38
+ else
39
+ resource = load(file)
40
+ files << resource if resource
41
+ end
42
+ end
43
+ files
44
+ end
45
+
46
+ # Find all configured resource file classes
47
+ # @return [Array<Class>] Available resource file classes
48
+ def self.modules
49
+ self.constants.map { |c| self.const_get(c) }.select do |klass|
50
+ klass.is_a?(Class) && klass != Base
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def self.extension_match(mod, path)
57
+ filename = path.basename.to_s.downcase
58
+ mod.extensions.each do |extension|
59
+ # don't match end of line in case file has locale extension
60
+ return true if filename.match(/\.#{extension}/)
61
+ end
62
+ false
63
+ end
64
+ end
65
+ end
66
+
67
+ require 'translatomatic/resource_file/base'
68
+ require 'translatomatic/resource_file/yaml'
69
+ require 'translatomatic/resource_file/properties'
70
+ require 'translatomatic/resource_file/text'
71
+ require 'translatomatic/resource_file/xml'
72
+ require 'translatomatic/resource_file/html'
73
+ require 'translatomatic/resource_file/plist'
74
+ require 'translatomatic/resource_file/xcode_strings'
@@ -0,0 +1,68 @@
1
+ require 'set'
2
+
3
+ module Translatomatic
4
+ class TranslationResult
5
+
6
+ # Translation results
7
+ # @return [Hash<String,String>] Translation results
8
+ attr_reader :properties
9
+
10
+ # @return [Locale] The locale of the original strings
11
+ attr_reader :from_locale
12
+
13
+ # @return [Locale] The target locale
14
+ attr_reader :to_locale
15
+
16
+ # @return [Set<String>] Untranslated strings
17
+ attr_reader :untranslated
18
+
19
+ # Create a translation result
20
+ # @param [Hash<String,String>] properties Untranslated properties
21
+ # @param [Locale] from_locale The locale of the untranslated strings
22
+ # @param [Locale] to_locale The target locale
23
+ def initialize(properties, from_locale, to_locale)
24
+ @properties = properties.dup
25
+ @value_to_keys = {}
26
+ @untranslated = Set.new
27
+ properties.each do |key, value|
28
+ @untranslated << value
29
+ keylist = (@value_to_keys[value] ||= [])
30
+ keylist << key
31
+ end
32
+ @from_locale = from_locale
33
+ @to_locale = to_locale
34
+ end
35
+
36
+ # Update result with a list of translated strings.
37
+ # @param [Array<String>] original Original strings
38
+ # @param [Array<String>] translated Translated strings
39
+ # @return [void]
40
+ def update_strings(original, translated)
41
+ raise "strings length mismatch" unless original.length == translated.length
42
+ original.zip(translated).each do |text1, text2|
43
+ update(text1, text2)
44
+ end
45
+ end
46
+
47
+ # Update result with texts from the database.
48
+ # @param [Array<Translatomatic::Model::Text>] list Texts from database
49
+ # @return [void]
50
+ def update_db_strings(list)
51
+ list.each do |t|
52
+ original = t.from_text.value
53
+ translated = t.value
54
+ update(original, translated)
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def update(original, translated)
61
+ keys = @value_to_keys[original]
62
+ raise "no key mapping for text '#{original}'" unless keys
63
+ keys.each { |key| @properties[key] = translated }
64
+
65
+ @untranslated.delete(original)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,47 @@
1
+ require 'bing_translator'
2
+
3
+ module Translatomatic
4
+ module Translator
5
+ @abstract
6
+ class Base
7
+
8
+ class << self
9
+ attr_reader :options
10
+ private
11
+ include Translatomatic::DefineOptions
12
+ end
13
+
14
+ # @return [String] The name of this translator.
15
+ def name
16
+ self.class.name.demodulize
17
+ end
18
+
19
+ # @return [Array<String>] A list of languages supported by this translator.
20
+ def languages
21
+ []
22
+ end
23
+
24
+ # Translate strings from one locale to another
25
+ # @param [Array<String>] strings A list of strings to translate.
26
+ # @param [String, Locale] from The locale of the given strings.
27
+ # @param [String, Locale] to The locale to translate to.
28
+ # @return [Array<String>] Translated strings
29
+ def translate(strings, from, to)
30
+ strings = [strings] unless strings.kind_of?(Array)
31
+ from = parse_locale(from) if from.kind_of?(String)
32
+ to = parse_locale(to) if to.kind_of?(String)
33
+ return strings if from.language == to.language
34
+ perform_translate(strings, from, to)
35
+ end
36
+
37
+ private
38
+
39
+ include Translatomatic::Util
40
+
41
+ def perform_translate(strings, from, to)
42
+ raise "subclasses must implement perform_translate"
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,64 @@
1
+ require 'net/http'
2
+
3
+ module Translatomatic
4
+ module Translator
5
+
6
+ class Frengly < Base
7
+
8
+ define_options({
9
+ name: :frengly_api_key, desc: "Frengly API key", use_env: true
10
+ },
11
+ { name: :frengly_email, desc: "Email address", use_env: true
12
+ },
13
+ { name: :frengly_password, desc: "Password", use_env: true
14
+ })
15
+
16
+ # Create a new Frengly translator instance
17
+ def initialize(options = {})
18
+ @key = options[:frengly_api_key] || ENV["FRENGLY_API_KEY"] # optional
19
+ @email = options[:frengly_email]
20
+ @password = options[:frengly_password]
21
+ raise "email address required" unless @email
22
+ raise "password required" unless @password
23
+ end
24
+
25
+ # (see Translatomatic::Translator::Base#languages)
26
+ def languages
27
+ ['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'
33
+
34
+ def perform_translate(strings, from, to)
35
+ translated = []
36
+ uri = URI.parse(URL)
37
+
38
+ Net::HTTP.start(uri.host, uri.port) do |http|
39
+ strings.each do |string|
40
+ body = {
41
+ src: from.language,
42
+ dest: to.language,
43
+ text: string,
44
+ email: @email,
45
+ password: @password,
46
+ premiumkey: @key
47
+ }.to_json
48
+
49
+ # TODO: work out what the response looks like
50
+ req = Net::HTTP::Post.new(uri)
51
+ req.body = body
52
+ req.content_type = 'application/json'
53
+ response = http.request(req)
54
+ raise response.body unless response.kind_of? Net::HTTPSuccess
55
+ data = JSON.parse(response.body)
56
+ translated << data['text']
57
+ end
58
+ translated
59
+ end
60
+ end
61
+
62
+ end # class
63
+ end # module
64
+ end
@@ -0,0 +1,30 @@
1
+ module Translatomatic
2
+ module Translator
3
+
4
+ class Google < Base
5
+
6
+ define_options({ name: :google_api_key, desc: "Google API key",
7
+ use_env: true
8
+ })
9
+
10
+ # Create a new Google translator instance
11
+ def initialize(options = {})
12
+ key = options[:google_api_key] || ENV["GOOGLE_API_KEY"]
13
+ raise "google api key required" if key.nil?
14
+ EasyTranslate.api_key = key
15
+ end
16
+
17
+ # (see Translatomatic::Translator::Base#languages)
18
+ def languages
19
+ EasyTranslate::LANGUAGES.keys
20
+ end
21
+
22
+ private
23
+
24
+ def perform_translate(strings, from, to)
25
+ EasyTranslate.translate(strings, from: from.language, to: to.language)
26
+ end
27
+
28
+ end # class
29
+ end # module
30
+ end
@@ -0,0 +1,32 @@
1
+ require 'bing_translator'
2
+
3
+ module Translatomatic
4
+ module Translator
5
+
6
+ class Microsoft < Base
7
+
8
+ define_options({
9
+ name: :microsoft_api_key, desc: "Microsoft API key", use_env: true
10
+ })
11
+
12
+ # Create a new Microsoft translator instance
13
+ def initialize(options = {})
14
+ key = options[:microsoft_api_key] || ENV["MICROSOFT_API_KEY"]
15
+ raise "microsoft api key required" if key.nil?
16
+ @impl = BingTranslator.new(key)
17
+ end
18
+
19
+ # TODO: implement language list
20
+ # (see Translatomatic::Translator::Base#languages)
21
+ #def languages
22
+ #end
23
+
24
+ private
25
+
26
+ def perform_translate(strings, from, to)
27
+ @impl.translate_array(strings, from: from.language, to: to.language)
28
+ end
29
+
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,55 @@
1
+ require 'bing_translator'
2
+
3
+ module Translatomatic
4
+ module Translator
5
+
6
+ class MyMemory < Base
7
+
8
+ define_options(
9
+ { name: :mymemory_api_key, desc: "MyMemory API key", use_env: true },
10
+ { name: :mymemory_email, desc: "Email address", use_env: true }
11
+ )
12
+
13
+ # Create a new MyMemory translator instance
14
+ def initialize(options = {})
15
+ @key = options[:mymemory_api_key] || ENV["MYMEMORY_API_KEY"]
16
+ @email = options[:mymemory_email] || ENV["MYMEMORY_EMAIL"]
17
+ end
18
+
19
+ # TODO: implement language list
20
+ # (see Translatomatic::Translator::Base#languages)
21
+ #def languages
22
+ #end
23
+
24
+ private
25
+
26
+ URL = 'https://api.mymemory.translated.net/get'
27
+
28
+ def perform_translate(strings, from, to)
29
+ translated = []
30
+ uri = URI.parse(URL)
31
+
32
+ http_options = { use_ssl: uri.scheme == "https" }
33
+ Net::HTTP.start(uri.host, uri.port, http_options) do |http|
34
+ strings.each do |string|
35
+ query = {
36
+ langpair: from.to_s + "|" + to.to_s,
37
+ q: string
38
+ }
39
+ query.merge!(de: @email) if @email
40
+ query.merge!(key: @key) if @key
41
+ uri.query = URI.encode_www_form(query)
42
+
43
+ req = Net::HTTP::Get.new(uri)
44
+ response = http.request(req)
45
+ raise response.body unless response.kind_of? Net::HTTPSuccess
46
+ data = JSON.parse(response.body)
47
+ translated << data['responseData']['translatedText']
48
+ end
49
+ translated
50
+ end
51
+ end
52
+
53
+ end
54
+ end
55
+ end