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.
Files changed (139) hide show
  1. checksums.yaml +5 -5
  2. data/.gitattributes +20 -20
  3. data/.gitignore +19 -15
  4. data/.rspec +3 -3
  5. data/.rubocop.yml +28 -0
  6. data/.translatomatic/config.yml +4 -0
  7. data/.travis.yml +4 -6
  8. data/.yardopts +9 -9
  9. data/Gemfile +8 -4
  10. data/Guardfile +4 -5
  11. data/README.de.md +55 -50
  12. data/README.en.md +177 -0
  13. data/README.es.md +53 -48
  14. data/README.fr.md +53 -48
  15. data/README.it.md +54 -49
  16. data/README.ja.md +63 -58
  17. data/README.ko.md +59 -54
  18. data/README.md +17 -13
  19. data/README.ms.md +50 -45
  20. data/README.pt.md +54 -49
  21. data/README.ru.md +57 -52
  22. data/README.sv.md +51 -46
  23. data/README.zh.md +60 -55
  24. data/Rakefile +3 -3
  25. data/TODO.txt +6 -0
  26. data/bin/console +3 -3
  27. data/bin/translatomatic +4 -2
  28. data/config/i18n-tasks.yml +130 -0
  29. data/config/locales/translatomatic/de.yml +141 -99
  30. data/config/locales/translatomatic/en.yml +129 -89
  31. data/config/locales/translatomatic/es.yml +136 -99
  32. data/config/locales/translatomatic/fr.yml +139 -100
  33. data/config/locales/translatomatic/it.yml +135 -97
  34. data/config/locales/translatomatic/ja.yml +137 -98
  35. data/config/locales/translatomatic/ko.yml +138 -98
  36. data/config/locales/translatomatic/ms.yml +138 -100
  37. data/config/locales/translatomatic/pt.yml +137 -101
  38. data/config/locales/translatomatic/ru.yml +136 -98
  39. data/config/locales/translatomatic/sv.yml +134 -96
  40. data/config/locales/translatomatic/zh.yml +136 -97
  41. data/db/migrate/201712170000_initial.rb +2 -3
  42. data/lib/translatomatic.rb +40 -25
  43. data/lib/translatomatic/cli.rb +5 -1
  44. data/lib/translatomatic/cli/base.rb +61 -58
  45. data/lib/translatomatic/cli/common_options.rb +14 -11
  46. data/lib/translatomatic/cli/config.rb +96 -91
  47. data/lib/translatomatic/cli/database.rb +85 -23
  48. data/lib/translatomatic/cli/main.rb +158 -104
  49. data/lib/translatomatic/cli/thor.rb +29 -0
  50. data/lib/translatomatic/cli/translate.rb +134 -157
  51. data/lib/translatomatic/config.rb +10 -301
  52. data/lib/translatomatic/config/display.rb +78 -0
  53. data/lib/translatomatic/config/files.rb +60 -0
  54. data/lib/translatomatic/config/location_settings.rb +133 -0
  55. data/lib/translatomatic/config/options.rb +68 -0
  56. data/lib/translatomatic/config/selector.rb +127 -0
  57. data/lib/translatomatic/config/settings.rb +148 -0
  58. data/lib/translatomatic/converter.rb +40 -28
  59. data/lib/translatomatic/database.rb +127 -110
  60. data/lib/translatomatic/define_options.rb +4 -5
  61. data/lib/translatomatic/escaped_unicode.rb +86 -76
  62. data/lib/translatomatic/extractor.rb +5 -2
  63. data/lib/translatomatic/extractor/base.rb +12 -12
  64. data/lib/translatomatic/extractor/ruby.rb +7 -6
  65. data/lib/translatomatic/file_translator.rb +101 -244
  66. data/lib/translatomatic/flattenation.rb +39 -0
  67. data/lib/translatomatic/http.rb +13 -0
  68. data/lib/translatomatic/http/client.rb +144 -0
  69. data/lib/translatomatic/http/exception.rb +43 -0
  70. data/lib/translatomatic/http/file_param.rb +27 -0
  71. data/lib/translatomatic/http/param.rb +37 -0
  72. data/lib/translatomatic/http/request.rb +91 -0
  73. data/lib/translatomatic/i18n.rb +43 -0
  74. data/lib/translatomatic/locale.rb +71 -59
  75. data/lib/translatomatic/logger.rb +43 -28
  76. data/lib/translatomatic/metadata.rb +58 -0
  77. data/lib/translatomatic/model.rb +4 -2
  78. data/lib/translatomatic/model/locale.rb +5 -5
  79. data/lib/translatomatic/model/text.rb +5 -5
  80. data/lib/translatomatic/option.rb +57 -34
  81. data/lib/translatomatic/path_utils.rb +126 -0
  82. data/lib/translatomatic/progress_updater.rb +13 -16
  83. data/lib/translatomatic/provider.rb +101 -0
  84. data/lib/translatomatic/provider/base.rb +136 -0
  85. data/lib/translatomatic/provider/frengly.rb +55 -0
  86. data/lib/translatomatic/provider/google.rb +78 -0
  87. data/lib/translatomatic/provider/google_web.rb +50 -0
  88. data/lib/translatomatic/provider/microsoft.rb +144 -0
  89. data/lib/translatomatic/provider/my_memory.rb +75 -0
  90. data/lib/translatomatic/provider/yandex.rb +61 -0
  91. data/lib/translatomatic/resource_file.rb +59 -53
  92. data/lib/translatomatic/resource_file/base.rb +171 -237
  93. data/lib/translatomatic/resource_file/csv.rb +176 -24
  94. data/lib/translatomatic/resource_file/html.rb +21 -42
  95. data/lib/translatomatic/resource_file/key_value_support.rb +117 -0
  96. data/lib/translatomatic/resource_file/markdown.rb +36 -38
  97. data/lib/translatomatic/resource_file/plist.rb +121 -126
  98. data/lib/translatomatic/resource_file/po.rb +104 -82
  99. data/lib/translatomatic/resource_file/properties.rb +48 -77
  100. data/lib/translatomatic/resource_file/properties.treetop +87 -0
  101. data/lib/translatomatic/resource_file/resw.rb +56 -41
  102. data/lib/translatomatic/resource_file/subtitle.rb +86 -54
  103. data/lib/translatomatic/resource_file/text.rb +18 -18
  104. data/lib/translatomatic/resource_file/xcode_strings.rb +32 -63
  105. data/lib/translatomatic/resource_file/xcode_strings.treetop +85 -0
  106. data/lib/translatomatic/resource_file/xml.rb +94 -81
  107. data/lib/translatomatic/resource_file/yaml.rb +54 -68
  108. data/lib/translatomatic/retry_executor.rb +37 -0
  109. data/lib/translatomatic/slurp.rb +32 -0
  110. data/lib/translatomatic/string_batcher.rb +50 -0
  111. data/lib/translatomatic/string_escaping.rb +61 -0
  112. data/lib/translatomatic/text.rb +263 -0
  113. data/lib/translatomatic/text_collection.rb +66 -0
  114. data/lib/translatomatic/tmx.rb +5 -3
  115. data/lib/translatomatic/tmx/document.rb +107 -82
  116. data/lib/translatomatic/tmx/translation_unit.rb +19 -18
  117. data/lib/translatomatic/translation.rb +8 -28
  118. data/lib/translatomatic/translation/collection.rb +199 -0
  119. data/lib/translatomatic/translation/fetcher.rb +123 -0
  120. data/lib/translatomatic/translation/munging.rb +112 -0
  121. data/lib/translatomatic/translation/result.rb +50 -0
  122. data/lib/translatomatic/translation/sharer.rb +32 -0
  123. data/lib/translatomatic/translation/stats.rb +44 -0
  124. data/lib/translatomatic/translator.rb +91 -88
  125. data/lib/translatomatic/type_cast.rb +63 -0
  126. data/lib/translatomatic/util.rb +37 -33
  127. data/lib/translatomatic/version.rb +2 -2
  128. data/translatomatic.gemspec +57 -46
  129. metadata +136 -59
  130. data/lib/translatomatic/http_request.rb +0 -162
  131. data/lib/translatomatic/string.rb +0 -188
  132. data/lib/translatomatic/translation_result.rb +0 -86
  133. data/lib/translatomatic/translation_stats.rb +0 -31
  134. data/lib/translatomatic/translator/base.rb +0 -128
  135. data/lib/translatomatic/translator/frengly.rb +0 -62
  136. data/lib/translatomatic/translator/google.rb +0 -37
  137. data/lib/translatomatic/translator/microsoft.rb +0 -41
  138. data/lib/translatomatic/translator/my_memory.rb +0 -68
  139. data/lib/translatomatic/translator/yandex.rb +0 -56
@@ -0,0 +1,75 @@
1
+ module Translatomatic
2
+ module Provider
3
+ # Interface to the MyMemory translation API
4
+ # @see https://mymemory.translated.net/doc/
5
+ class MyMemory < Base
6
+ define_option :mymemory_api_key, use_env: true,
7
+ desc: t('provider.mymemory.api_key')
8
+ define_option :mymemory_email, use_env: true,
9
+ desc: t('provider.email_address')
10
+
11
+ # Create a new MyMemory provider instance
12
+ def initialize(options = {})
13
+ super(options)
14
+ @key = options[:mymemory_api_key] || ENV['MYMEMORY_API_KEY']
15
+ @email = options[:mymemory_email] || ENV['MYMEMORY_EMAIL']
16
+ @query_options = {}
17
+ @query_options[:de] = @email if @email
18
+ @query_options.merge!(key: @key) if @key
19
+ end
20
+
21
+ # (see Base#languages)
22
+ def languages
23
+ Locale.language_codes
24
+ end
25
+
26
+ # Upload a set of translations to MyMemory
27
+ # @param tmx [Translatomatic::TMX::Document] TMX document
28
+ # @return [void]
29
+ def upload(tmx)
30
+ body = [
31
+ { key: :tmx, filename: 'import.tmx',
32
+ content: tmx.to_xml, mime_type: 'application/xml' },
33
+ { key: :private, value: 0 }
34
+ ]
35
+ response = http_client.post(UPLOAD_URL, body)
36
+ log.debug(t('provider.share_response',
37
+ response: response.body.inspect))
38
+ end
39
+
40
+ private
41
+
42
+ GET_URL = 'https://api.mymemory.translated.net/get'.freeze
43
+ UPLOAD_URL = 'https://api.mymemory.translated.net/tmx/import'.freeze
44
+ MAIN_URL = 'https://mymemory.api.net'.freeze
45
+
46
+ def perform_translate(strings, from, to)
47
+ perform_fetch_translations(GET_URL, strings, from, to)
48
+ end
49
+
50
+ def fetch_translations(string, from, to)
51
+ response = http_client.get(GET_URL, {
52
+ langpair: from.to_s + '|' + to.to_s,
53
+ q: string # multiple q strings not supported (tested 20180101)
54
+ }.merge(@query_options))
55
+
56
+ log.debug("#{name} response: #{response.body}")
57
+ data = JSON.parse(response.body)
58
+ # matches = data['matches'] # all translations
59
+ # matches.collect { |i| match_data(i) }
60
+ result = try_hash(data, 'responseData', 'translatedText')
61
+ add_translations(string, result)
62
+ end
63
+
64
+ # https://mymemory.translated.net/doc/features.php
65
+ def match_data(match)
66
+ {
67
+ translation: match['translation'],
68
+ quality: match['quality'],
69
+ usage_count: match['usage-count'],
70
+ match: match['match'], # partial matches, see features.php above
71
+ }
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,61 @@
1
+ module Translatomatic
2
+ module Provider
3
+ # Interface to the Yandex translation API
4
+ # @see https://tech.yandex.com/translate/
5
+ class Yandex < Base
6
+ define_option :yandex_api_key, use_env: true,
7
+ desc: t('provider.yandex.api_key')
8
+
9
+ # Create a new Yandex provider instance
10
+ def initialize(options = {})
11
+ super(options)
12
+ @api_key = options[:yandex_api_key] || ENV['YANDEX_API_KEY']
13
+ raise t('provider.yandex.key_required') if @api_key.nil?
14
+ end
15
+
16
+ # (see Base#languages)
17
+ def languages
18
+ @languages ||= begin
19
+ response = http_client.post(LANGUAGES_URL, key: @api_key, ui: 'en')
20
+ data = JSON.parse(response.body) || {}
21
+ langs = data['langs'] || {}
22
+ langs.keys.flatten.uniq
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ BASE_URL = 'https://translate.yandex.net/api/v1.5/tr.json'.freeze
29
+ TRANSLATE_URL = (BASE_URL + '/translate').freeze
30
+ LANGUAGES_URL = (BASE_URL + '/getLangs').freeze
31
+ LIMIT = [nil, 10_000].freeze # strings, characters per request
32
+
33
+ def perform_translate(strings, from, to)
34
+ batcher(strings, max_count: LIMIT[0], max_length: LIMIT[1])
35
+ .each_batch do |texts|
36
+ fetch_translations(texts, from, to)
37
+ end
38
+ end
39
+
40
+ def fetch_translations(strings, from, to)
41
+ body = request_body(strings, from, to)
42
+ response = http_client.post(TRANSLATE_URL, body)
43
+ log.debug("#{name} response: #{response.body}")
44
+ data = JSON.parse(response.body)
45
+ result = data['text'] || []
46
+ strings.zip(result).each do |original, translated|
47
+ add_translations(original, translated)
48
+ end
49
+ end
50
+
51
+ def request_body(strings, from, to)
52
+ {
53
+ key: @api_key,
54
+ text: strings,
55
+ lang: from.language + '-' + to.language,
56
+ format: 'plain' # 'html'
57
+ }
58
+ end
59
+ end
60
+ end
61
+ end
@@ -4,76 +4,81 @@ module Translatomatic
4
4
  module ResourceFile
5
5
  class << self
6
6
  include Translatomatic::Util
7
- end
8
7
 
9
- # Load a resource file. If locale is not specified, the locale of the
10
- # file will be determined from the filename, or else the current default
11
- # locale will be used.
12
- # @param path [String] Path to the resource file
13
- # @param locale [String] Locale of the resource file
14
- # @return [Translatomatic::ResourceFile::Base] The resource file, or nil
15
- # if the file type is unsupported.
16
- def self.load(path, locale = nil)
17
- path = path.kind_of?(Pathname) ? path : Pathname.new(path)
18
- types_for_path(path).each do |klass|
19
- log.debug(t("file.loading", file: path, name: klass.name.demodulize))
20
- return klass.new(path, locale: locale)
8
+ # Load a resource file. If options[:locale] is not specified,
9
+ # the locale of the file will be determined from the filename,
10
+ # or else the current default locale will be used.
11
+ # @param path [String] Path to the resource file
12
+ # @return [Translatomatic::ResourceFile::Base] The resource file, or nil
13
+ # if the file type is unsupported.
14
+ def load(path, options = {})
15
+ path = path.is_a?(Pathname) ? path : Pathname.new(path)
16
+ types_for_path(path).each do |klass|
17
+ log.debug(t('file.loading', file: path, name: klass.name.demodulize))
18
+ return klass.new(path, options)
19
+ end
20
+ nil
21
21
  end
22
- nil
23
- end
24
22
 
25
- # Create a new resource file
26
- def self.create(path, locale = nil)
27
- klass = const_get(klass_name)
28
- klass.new
29
- end
23
+ # Create a new resource file
24
+ def create(path, options = {})
25
+ klass = types_for_path(path).first
26
+ return nil unless klass
27
+ file = klass.new
28
+ file.path = path
29
+ file.locale = build_locale(options[:locale])
30
+ file
31
+ end
30
32
 
31
- # Find all resource files under the given directory. Follows symlinks.
32
- # @param path [String, Pathname] The path to search from
33
- # @return [Array<Translatomatic::ResourceFile>] Resource files found
34
- def self.find(path, options = {})
35
- files = []
36
- include_dot_directories = options[:include_dot_directories]
37
- path = Pathname.new(path) unless path.kind_of?(Pathname)
38
- path.find do |file|
39
- if !include_dot_directories && file.basename.to_s[0] == ?.
40
- Find.prune
41
- else
42
- resource = load(file)
43
- files << resource if resource
33
+ # Find all resource files under the given directory. Follows symlinks.
34
+ # @param path [String, Pathname] The path to search from
35
+ # @return [Array<Translatomatic::ResourceFile>] Resource files found
36
+ def find(path, options = {})
37
+ files = []
38
+ include_dot_directories = options[:include_dot_directories]
39
+ path = Pathname.new(path) unless path.is_a?(Pathname)
40
+ path.find do |file|
41
+ puts "loading #{file}"
42
+ if !include_dot_directories && file.basename.to_s[0] == '.'
43
+ Find.prune
44
+ else
45
+ resource = load(file)
46
+ files << resource if resource
47
+ end
44
48
  end
49
+ files
45
50
  end
46
- files
47
- end
48
51
 
49
- # Find all configured resource file classes
50
- # @return [Array<Class>] Available resource file classes
51
- def self.types
52
- @types ||= self.constants.map { |c| self.const_get(c) }.select do |klass|
53
- klass.is_a?(Class) && klass != Base
52
+ # Find all configured resource file classes
53
+ # @return [Array<Class>] Available resource file classes
54
+ def types
55
+ @types ||= constants.map { |c| const_get(c) }.select do |klass|
56
+ klass.is_a?(Class) && klass != Base && klass < Base
57
+ end
54
58
  end
55
- end
56
59
 
57
- private
60
+ private
58
61
 
59
- # find classes that can load the given path by file extension
60
- def self.types_for_path(path)
61
- path = path.kind_of?(Pathname) ? path : Pathname.new(path)
62
- types.select { |klass| klass.enabled? && extension_match(klass, path) }
63
- end
62
+ # find classes that can load the given path by file extension
63
+ def types_for_path(path)
64
+ path = path.is_a?(Pathname) ? path : Pathname.new(path)
65
+ types.select { |klass| klass.enabled? && extension_match(klass, path) }
66
+ end
64
67
 
65
- def self.extension_match(klass, path)
66
- filename = path.basename.to_s.downcase
67
- klass.extensions.each do |extension|
68
- # don't match end of line in case file has locale extension
69
- return true if filename.match(/\.#{extension}/)
68
+ def extension_match(klass, path)
69
+ filename = path.basename.to_s.downcase
70
+ klass.extensions.each do |extension|
71
+ # don't match end of line in case file has locale extension
72
+ return true if filename =~ /\.#{extension}/
73
+ end
74
+ false
70
75
  end
71
- false
72
76
  end
73
77
  end
74
78
  end
75
79
 
76
80
  require 'translatomatic/resource_file/base'
81
+ require 'translatomatic/resource_file/key_value_support'
77
82
  require 'translatomatic/resource_file/yaml'
78
83
  require 'translatomatic/resource_file/properties'
79
84
  require 'translatomatic/resource_file/text'
@@ -85,3 +90,4 @@ require 'translatomatic/resource_file/plist'
85
90
  require 'translatomatic/resource_file/resw'
86
91
  require 'translatomatic/resource_file/subtitle'
87
92
  require 'translatomatic/resource_file/csv'
93
+ require 'translatomatic/resource_file/po'
@@ -1,269 +1,203 @@
1
- # Base class for resource file implementations
2
- # @abstract Subclasses implement different types of resource files
3
- class Translatomatic::ResourceFile::Base
4
-
5
- attr_accessor :locale
6
- attr_accessor :path
7
-
8
- # @return [Hash<String,String>] key -> value properties
9
- attr_reader :properties
10
-
11
- # @return [boolean] True if this resource format is enabled
12
- def self.enabled?
13
- true
14
- end
15
-
16
- # @return [Array<String>] File extensions supported by this resource file
17
- def self.extensions
18
- raise "extensions must be implemented by subclass"
19
- end
20
-
21
- # @return [boolean] True if the file format consists of keys and values
22
- def self.is_key_value?
23
- false
24
- end
25
-
26
- # @return [boolean] true if this resource file supports variable interpolation
27
- def self.supports_variable_interpolation?
28
- false
29
- end
30
-
31
- # Create a new resource file or load an existing file.
32
- # If options[:locale] is unspecified, attempts to determine
33
- # the locale of the file automatically, and if that fails,
34
- # uses the default locale.
35
- # Raises an exception if errors were encountered loading the file.
36
- # @param path [String] Path to the file
37
- # @param options [Hash<Symbol,String>] Options
38
- # @return [Translatomatic::ResourceFile::Base] the resource file.
39
- def initialize(path = nil, options = {})
40
- raise "expected options hash" if options && !options.kind_of?(Hash)
41
- raise t("file.unsupported", file: path) unless self.class.enabled?
42
- @options = options || {}
43
- @properties = {}
44
- @path = path.nil? || path.kind_of?(Pathname) ? path : Pathname.new(path)
45
- update_locale
46
- init
47
- load if @path && @path.exist?
48
- end
49
-
50
- # Save the resource file.
51
- # @param target [Pathname] The destination path
52
- # @param options [Hash<Symbol, Object>] Output format options
53
- # @return [void]
54
- def save(target = path, options = {})
55
- raise "save(path) must be implemented by subclass"
56
- end
57
-
58
- # @return [String] The format of this resource file, e.g. "Properties"
59
- def format
60
- self.class.name.demodulize.downcase.to_sym
61
- end
62
-
63
- # Create a path for the current resource file with a given locale
64
- # @param locale [String] The target locale
65
- # @return [Pathname] The path of this resource file modified for the given locale
66
- def locale_path(locale)
67
- basename = path.sub_ext('').basename.to_s
68
-
69
- extlist = extension_list
70
- if extlist.length >= 2 && loc_idx = find_locale(extlist)
71
- # extension(s) contains locale, replace it
72
- extlist[loc_idx] = locale.to_s
73
- elsif valid_locale?(basename)
74
- # basename is a locale name, replace it
75
- path.dirname + (locale.to_s + path.extname)
76
- else
77
- # remove any underscore and trailing text from basename
78
- deunderscored = basename.sub(/_.*?$/, '')
79
- # add _locale.ext
80
- filename = deunderscored + "_" + locale.to_s + path.extname
81
- path.dirname + filename
82
- end
83
- end
84
-
85
- # Set all properties
86
- # @param properties [Hash<String,String>] New properties
87
- def properties=(properties)
88
- # use set rather that set @properties directly as subclasses override set()
89
- properties.each do |key, value|
90
- set(key, value)
91
- end
92
- end
1
+ module Translatomatic
2
+ module ResourceFile
3
+ # Base class for resource file implementations
4
+ # @abstract Subclasses implement different types of resource files
5
+ class Base
6
+ # @return [Hash<Symbol,Object] Options used in the constructor
7
+ attr_reader :options
8
+
9
+ # @return [Locale] The locale of the contents of this resource file
10
+ attr_accessor :locale
11
+
12
+ # @return [Pathname] The path to this resource file
13
+ attr_accessor :path
14
+
15
+ # @return [boolean] True if this resource format is enabled
16
+ def self.enabled?
17
+ true
18
+ end
93
19
 
94
- # Get the value of a property
95
- # @param key [String] The name of the property
96
- # @return [String] The value of the property
97
- def get(key)
98
- @properties[key]
99
- end
20
+ # @return [String] Preferred character to use to separate the locale
21
+ # from the file basename
22
+ def self.preferred_locale_separator
23
+ '.'
24
+ end
100
25
 
101
- # Set a property
102
- # @param key [String] The name of the property
103
- # @param value [String] The new value of the property
104
- # @return [String] The new value of the property
105
- def set(key, value)
106
- @properties[key] = value
107
- end
26
+ # @return [Array<String>] File extensions supported by this resource file
27
+ def self.extensions
28
+ raise 'extensions must be implemented by subclass'
29
+ end
108
30
 
109
- # @return [String] String representation of this file
110
- def to_s
111
- path.basename.to_s
112
- end
31
+ # @return [boolean] True if the file format consists of keys and values
32
+ def self.key_value?
33
+ false
34
+ end
113
35
 
114
- def type
115
- self.class.name.demodulize
116
- end
36
+ # @return [boolean] true if this resource file supports
37
+ # variable interpolation
38
+ def self.supports_variable_interpolation?
39
+ false
40
+ end
117
41
 
118
- # @return [Array<String>] All property values split into sentences
119
- def sentences
120
- sentences = []
121
- properties.values.each do |value|
122
- string = Translatomatic::String.new(value, locale)
123
- sentences += string.sentences
124
- end
125
- sentences
126
- end
42
+ # Create a new resource file or load an existing file.
43
+ # If options[:locale] is unspecified, attempts to determine
44
+ # the locale of the file automatically, and if that fails,
45
+ # uses the default locale.
46
+ # Raises an exception if errors were encountered loading the file.
47
+ # @param path [String] Path to the file
48
+ # @param options [Hash<Symbol,String>] Options
49
+ # @return [Translatomatic::ResourceFile::Base] the resource file.
50
+ def initialize(path = nil, options = {})
51
+ raise 'expected options hash' if options && !options.is_a?(Hash)
52
+ raise t('file.unsupported', file: path) unless self.class.enabled?
53
+ initialize_attributes(path, options)
54
+ update_locale
55
+ init
56
+ try_load
57
+ end
127
58
 
128
- # Create an interpolated variable string.
129
- # @return [String] A string representing the interpolated variable, or
130
- # nil if this resource file doesn't support variable interpolation.
131
- def create_variable(name)
132
- return nil unless supports_variable_interpolation?
133
- raise "create_variable(name) must be implemented by subclass"
134
- end
59
+ # Save the resource file.
60
+ # @param target [Pathname] The destination path
61
+ # @param options [Hash<Symbol, Object>] Output format options
62
+ # @return [void]
63
+ def save(target = path, options = {})
64
+ raise 'save(path) must be implemented by subclass'
65
+ end
135
66
 
136
- # @return [Regexp] A regexp used to match interpolated variables
137
- def variable_regex
138
- return nil unless supports_variable_interpolation?
139
- raise "variable_regex must be implemented by subclass"
140
- end
67
+ # @return [Symbol] The type of this resource file, e.g. ":properties"
68
+ def type
69
+ self.class.name.demodulize.downcase.to_sym
70
+ end
141
71
 
142
- private
72
+ # Create a path for the current resource file with a given locale
73
+ # @param target_locale [String] The target locale
74
+ # @return [Pathname] The path of this resource file modified
75
+ # for the given locale
76
+ def locale_path(target_locale)
77
+ modify_path_locale(path, target_locale,
78
+ self.class.preferred_locale_separator)
79
+ end
143
80
 
144
- include Translatomatic::Util
81
+ # Set all properties
82
+ # @param properties [Hash<String,String>] New properties
83
+ def properties=(properties)
84
+ # use set rather that set @properties directly as subclasses
85
+ # override set()
86
+ properties.each do |key, value|
87
+ set(key, value)
88
+ end
89
+ end
145
90
 
146
- # called by constructor before load
147
- def init
148
- end
91
+ # @return [Hash<String,String>] key -> value properties
92
+ def properties
93
+ @properties.dup
94
+ end
149
95
 
150
- # load contents from @path
151
- def load
152
- raise "load must be implemented by subclass"
153
- end
96
+ # Get the value of a property
97
+ # @param key [String] The name of the property
98
+ # @return [String] The value of the property
99
+ def get(key)
100
+ @properties[key]
101
+ end
154
102
 
155
- def update_locale
156
- locale = @options[:locale] || detect_locale || Translatomatic::Locale.default
157
- @locale = Translatomatic::Locale.parse(locale)
158
- raise t("file.unknown_locale") unless @locale && @locale.language
159
- end
103
+ # Get context of a property
104
+ # @param key [String] The name of the property
105
+ # @return [Array<String>] The property context, may be nil
106
+ def get_context(key)
107
+ @metadata.get_context(key)
108
+ end
160
109
 
161
- def created_by
162
- options = {
163
- app: "Translatomatic",
164
- version: Translatomatic::VERSION,
165
- date: I18n.l(DateTime.now),
166
- }
167
- # use created by string in current file's locale, fall back to
168
- # english locale if translation is missing.
169
- t("file.created_by", options.merge({
170
- locale: locale.language,
171
- default: t("file.created_by", options.merge(locale: "en"))
172
- })
173
- )
174
- end
110
+ # Set a property
111
+ # @param key [String] The name of the property
112
+ # @param value [String] The new value of the property
113
+ # @return [String] The new value of the property
114
+ def set(key, value)
115
+ @properties[key] = value
116
+ end
175
117
 
176
- def read_contents(path)
177
- File.read(path.to_s, mode: "r:bom|utf-8")
178
- end
118
+ # @return [String] String representation of this file
119
+ def to_s
120
+ relative_path(path).to_s
121
+ end
179
122
 
180
- def parsing_error(error)
181
- raise Exception.new(error)
182
- end
123
+ # @return [Array<Text>] All property values split into sentences
124
+ def sentences
125
+ sentences = []
126
+ properties.each_value do |value|
127
+ string = build_text(value, locale)
128
+ sentences += string.sentences
129
+ end
130
+ sentences
131
+ end
183
132
 
184
- # detect locale from filename
185
- def detect_locale
186
- return nil unless path
187
- tag = nil
188
- basename = path.sub_ext('').basename.to_s
189
- directory = path.dirname.basename.to_s
190
- extlist = extension_list
133
+ # Create an interpolated variable string.
134
+ # @param name [String] The variable name
135
+ # @return [String] A string representing the interpolated variable, or
136
+ # nil if this resource file doesn't support variable interpolation.
137
+ def create_variable(name)
138
+ return nil unless self.class.supports_variable_interpolation?
139
+ raise 'create_variable(name) must be implemented by subclass'
140
+ end
191
141
 
192
- if basename.match(/_([-\w]{2,})$/i)
193
- # locale after underscore in filename
194
- tag = $1
195
- elsif directory.match(/^([-\w]+)\.lproj$/)
196
- # xcode localized strings
197
- tag = $1
198
- elsif extlist.length >= 2 && loc_idx = find_locale(extlist)
199
- # multiple parts to extension, e.g. index.html.en
200
- tag = extlist[loc_idx]
201
- elsif valid_locale?(basename)
202
- # try to match on entire basename
203
- # (support for rails en.yml)
204
- tag = basename
205
- elsif valid_locale?(path.parent.basename)
206
- # try to match on parent directory, e.g. strings/en-US/text.resw
207
- tag = path.parent.basename
208
- end
142
+ # @return [Regexp] A regexp used to match interpolated variables
143
+ def variable_regex
144
+ return nil unless self.class.supports_variable_interpolation?
145
+ raise 'variable_regex must be implemented by subclass'
146
+ end
209
147
 
210
- tag ? Translatomatic::Locale.parse(tag, true) : nil
211
- end
148
+ private
212
149
 
213
- def valid_locale?(tag)
214
- Translatomatic::Locale.new(tag).valid?
215
- end
150
+ include Translatomatic::Util
151
+ include Translatomatic::Flattenation
152
+ include Translatomatic::PathUtils
153
+ include Translatomatic::DefineOptions
216
154
 
217
- # test if the list of strings contains a valid locale
218
- # return the index to the locale, or nil if no locales found
219
- def find_locale(list)
220
- list.find_index { |i| valid_locale?(i) }
221
- end
155
+ # called by constructor before load
156
+ def init; end
222
157
 
223
- # ext_sub() only removes the last extension
224
- def strip_extensions
225
- filename = path.basename.to_s
226
- filename.sub!(/\..*$/, '')
227
- path.parent + filename
228
- end
158
+ # load contents from @path
159
+ def load
160
+ raise 'load must be implemented by subclass'
161
+ end
229
162
 
230
- # for index.html.de, returns ['html', 'de']
231
- def extension_list
232
- filename = path.basename.to_s
233
- idx = filename.index('.')
234
- idx && idx < filename.length - 1 ? filename[idx + 1..-1].split('.') : []
235
- end
163
+ def try_load
164
+ return unless @path
165
+ raise t('file.not_found', file: path) unless @path.exist?
166
+ load
167
+ end
236
168
 
237
- # flatten hash or array of hashes to a hash of key => value pairs
238
- def flatten(data)
239
- result = {}
169
+ def initialize_attributes(path, options)
170
+ @options = options || {}
171
+ @properties = {}
172
+ @metadata = Metadata.new
173
+ @path = path.nil? || path.is_a?(Pathname) ? path : Pathname.new(path)
174
+ end
240
175
 
241
- if data.kind_of?(Hash)
242
- data.each do |key, value|
243
- flatten_add(result, key, value)
176
+ def update_locale
177
+ default = Translatomatic::Locale.default
178
+ locale = @options[:locale] || detect_path_locale(path) || default
179
+ @locale = Translatomatic::Locale.parse(locale)
180
+ raise t('file.unknown_locale') unless @locale && @locale.language
244
181
  end
245
- elsif data.kind_of?(Array)
246
- data.each_with_index do |value, i|
247
- key = "key" + i.to_s
248
- flatten_add(result, key, value)
182
+
183
+ def created_by
184
+ options = {
185
+ app: 'Translatomatic',
186
+ version: Translatomatic::VERSION,
187
+ date: I18n.l(Time.now),
188
+ locale: locale.language,
189
+ url: Translatomatic::URL
190
+ }
191
+ t('file.created_by', options)
249
192
  end
250
- end
251
193
 
252
- result
253
- end
194
+ def created_by?
195
+ @created_by
196
+ end
254
197
 
255
- def flatten_add(result, key, value)
256
- if needs_flatten?(value)
257
- children = flatten(value)
258
- children.each do |ck, cv|
259
- result[key + "." + ck] = cv
198
+ def parsing_error(error)
199
+ raise StandardError, error
260
200
  end
261
- else
262
- result[key] = value
263
201
  end
264
202
  end
265
-
266
- def needs_flatten?(value)
267
- value.kind_of?(Array) || value.kind_of?(Hash)
268
- end
269
203
  end