phraseapp_updater 0.1.7 → 2.0.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/Gemfile +1 -0
- data/README.markdown +79 -72
- data/bin/phraseapp_common.sh +82 -0
- data/bin/phraseapp_updater +220 -93
- data/bin/synchronize_phraseapp.sh +141 -0
- data/lib/phraseapp_updater.rb +105 -116
- data/lib/phraseapp_updater/locale_file.rb +69 -28
- data/lib/phraseapp_updater/locale_file/json_file.rb +20 -13
- data/lib/phraseapp_updater/locale_file/yaml_file.rb +16 -9
- data/lib/phraseapp_updater/phraseapp_api.rb +116 -60
- data/lib/phraseapp_updater/version.rb +3 -1
- data/phraseapp_updater.gemspec +1 -0
- metadata +19 -4
- data/lib/phraseapp_updater/locale_file/loader.rb +0 -24
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'multi_json'
|
2
4
|
require 'oj'
|
3
5
|
|
4
6
|
# We're working with pure JSON, not
|
5
7
|
# serialized Ruby objects
|
6
|
-
Oj.default_options = {mode: :strict}
|
8
|
+
Oj.default_options = { mode: :strict }
|
7
9
|
|
8
10
|
class PhraseAppUpdater
|
9
11
|
class LocaleFile
|
@@ -11,21 +13,26 @@ class PhraseAppUpdater
|
|
11
13
|
EXTENSION = "json"
|
12
14
|
PHRASEAPP_TYPE = "nested_json"
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
-
|
16
|
+
class << self
|
17
|
+
def load(content)
|
18
|
+
Oj.load(content)
|
19
|
+
rescue Oj::ParseError => e
|
20
|
+
raise ArgumentError.new("Provided content was not valid JSON: #{e}")
|
21
|
+
end
|
17
22
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
23
|
+
def dump(hash)
|
24
|
+
# Add indentation for better diffs
|
25
|
+
Oj.dump(hash, indent: 2, mode: :strict)
|
26
|
+
end
|
23
27
|
|
24
|
-
|
25
|
-
|
26
|
-
|
28
|
+
def extension
|
29
|
+
EXTENSION
|
30
|
+
end
|
31
|
+
|
32
|
+
def phraseapp_type
|
33
|
+
PHRASEAPP_TYPE
|
34
|
+
end
|
27
35
|
end
|
28
36
|
end
|
29
37
|
end
|
30
38
|
end
|
31
|
-
|
@@ -5,17 +5,24 @@ class PhraseAppUpdater
|
|
5
5
|
EXTENSION = "yml"
|
6
6
|
PHRASEAPP_TYPE = "yml"
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
class << self
|
9
|
+
def load(content)
|
10
|
+
Psych.load(content)
|
11
|
+
rescue Psych::SyntaxError => e
|
12
|
+
raise ArgumentError.new("Provided content was not valid YAML")
|
13
|
+
end
|
11
14
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
def dump(hash)
|
16
|
+
Psych.dump(hash)
|
17
|
+
end
|
18
|
+
|
19
|
+
def extension
|
20
|
+
EXTENSION
|
21
|
+
end
|
17
22
|
|
18
|
-
|
23
|
+
def phraseapp_type
|
24
|
+
PHRASEAPP_TYPE
|
25
|
+
end
|
19
26
|
end
|
20
27
|
end
|
21
28
|
end
|
@@ -1,44 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'phraseapp_updater/locale_file'
|
4
|
+
require 'phraseapp_updater/index_by'
|
2
5
|
require 'phraseapp-ruby'
|
3
|
-
require '
|
6
|
+
require 'parallel'
|
4
7
|
require 'tempfile'
|
5
8
|
|
6
9
|
class PhraseAppUpdater
|
10
|
+
using IndexBy
|
7
11
|
class PhraseAppAPI
|
12
|
+
GIT_TAG_PREFIX = 'gitancestor_'
|
13
|
+
|
8
14
|
def initialize(api_key, project_id, locale_file_class)
|
9
15
|
@client = PhraseApp::Client.new(PhraseApp::Auth::Credentials.new(token: api_key))
|
10
16
|
@project_id = project_id
|
11
17
|
@locale_file_class = locale_file_class
|
12
18
|
end
|
13
19
|
|
14
|
-
def
|
15
|
-
|
16
|
-
|
17
|
-
|
20
|
+
def create_project(name, parent_commit)
|
21
|
+
params = PhraseApp::RequestParams::ProjectParams.new(
|
22
|
+
name: name,
|
23
|
+
main_format: @locale_file_class.phraseapp_type)
|
24
|
+
project = phraseapp_request { @client.project_create(params) }
|
25
|
+
STDERR.puts "Created project #{name} for #{parent_commit}"
|
26
|
+
|
27
|
+
@project_id = project.id
|
28
|
+
store_parent_commit(parent_commit)
|
29
|
+
|
30
|
+
project.id
|
31
|
+
end
|
32
|
+
|
33
|
+
# We mark projects with their parent git commit using a tag with a
|
34
|
+
# well-known prefix. We only allow one tag with this prefix at once.
|
35
|
+
def read_parent_commit
|
36
|
+
tags = phraseapp_request { @client.tags_list(@project_id, 1, 100) }
|
37
|
+
git_tag = tags.detect { |t| t.name.start_with?(GIT_TAG_PREFIX) }
|
38
|
+
raise MissingGitParentError.new if git_tag.nil?
|
39
|
+
|
40
|
+
git_tag.name.delete_prefix(GIT_TAG_PREFIX)
|
41
|
+
end
|
42
|
+
|
43
|
+
def update_parent_commit(commit_hash)
|
44
|
+
previous_parent = read_parent_commit
|
45
|
+
phraseapp_request do
|
46
|
+
@client.tag_delete(@project_id, GIT_TAG_PREFIX + previous_parent)
|
47
|
+
end
|
48
|
+
store_parent_commit(commit_hash)
|
49
|
+
end
|
50
|
+
|
51
|
+
def fetch_locales
|
52
|
+
# This is a paginated API, however the maximum page size of 100 is well
|
53
|
+
# above our expected locale size, so we take the first page only for now
|
18
54
|
phraseapp_request { @client.locales_list(@project_id, 1, 100) }.map do |pa_locale|
|
19
55
|
Locale.new(pa_locale)
|
20
56
|
end
|
21
57
|
end
|
22
58
|
|
23
|
-
def
|
24
|
-
|
25
|
-
|
59
|
+
def create_locale(name, default: false)
|
60
|
+
phraseapp_request do
|
61
|
+
params = PhraseApp::RequestParams::LocaleParams.new(
|
62
|
+
name: name,
|
63
|
+
code: name,
|
64
|
+
default: default,
|
65
|
+
)
|
66
|
+
@client.locale_create(@project_id, params)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def download_files(locales, skip_unverified:)
|
71
|
+
results = threaded_request(locales) do |locale|
|
72
|
+
STDERR.puts "Downloading file for #{locale}"
|
26
73
|
download_file(locale, skip_unverified)
|
27
|
-
end
|
28
|
-
|
74
|
+
end
|
75
|
+
|
76
|
+
locales.zip(results).map do |locale, file_contents|
|
77
|
+
@locale_file_class.from_file_content(locale.name, file_contents)
|
29
78
|
end
|
30
79
|
end
|
31
80
|
|
32
|
-
def upload_files(locale_files)
|
81
|
+
def upload_files(locale_files, default_locale:)
|
82
|
+
known_locales = fetch_locales.index_by(&:name)
|
83
|
+
|
33
84
|
threaded_request(locale_files) do |locale_file|
|
34
|
-
|
85
|
+
unless known_locales.has_key?(locale_file.locale_name)
|
86
|
+
create_locale(locale_file.locale_name,
|
87
|
+
default: (locale_file.locale_name == default_locale))
|
88
|
+
end
|
89
|
+
|
90
|
+
STDERR.puts "Uploading #{locale_file}"
|
35
91
|
upload_file(locale_file)
|
36
|
-
end
|
92
|
+
end
|
37
93
|
end
|
38
94
|
|
39
95
|
def remove_keys_not_in_uploads(upload_ids)
|
40
96
|
threaded_request(upload_ids) do |upload_id|
|
41
|
-
puts "Removing keys not in upload #{upload_id}"
|
97
|
+
STDERR.puts "Removing keys not in upload #{upload_id}"
|
42
98
|
remove_keys_not_in_upload(upload_id)
|
43
99
|
end
|
44
100
|
end
|
@@ -46,18 +102,18 @@ class PhraseAppUpdater
|
|
46
102
|
def download_file(locale, skip_unverified)
|
47
103
|
download_params = PhraseApp::RequestParams::LocaleDownloadParams.new
|
48
104
|
|
49
|
-
download_params.file_format = @locale_file_class.
|
105
|
+
download_params.file_format = @locale_file_class.phraseapp_type
|
50
106
|
download_params.skip_unverified_translations = skip_unverified
|
51
107
|
|
52
108
|
phraseapp_request { @client.locale_download(@project_id, locale.id, download_params) }
|
53
109
|
end
|
54
110
|
|
55
111
|
def upload_file(locale_file)
|
56
|
-
upload_params = create_upload_params(locale_file.
|
112
|
+
upload_params = create_upload_params(locale_file.locale_name)
|
57
113
|
|
58
114
|
# The PhraseApp gem only accepts a filename to upload,
|
59
115
|
# so we need to write the file out and pass it the path
|
60
|
-
Tempfile.create([
|
116
|
+
Tempfile.create([locale_file.locale_name, ".json"]) do |f|
|
61
117
|
f.write(locale_file.content)
|
62
118
|
f.close
|
63
119
|
|
@@ -72,7 +128,7 @@ class PhraseAppUpdater
|
|
72
128
|
|
73
129
|
begin
|
74
130
|
phraseapp_request { @client.keys_delete(@project_id, delete_params) }
|
75
|
-
rescue RuntimeError =>
|
131
|
+
rescue RuntimeError => _e
|
76
132
|
# PhraseApp will accept but mark invalid uploads, however the gem
|
77
133
|
# returns the same response in both cases. If we call this API
|
78
134
|
# with the ID of an upload of a bad file, it will fail.
|
@@ -84,65 +140,51 @@ class PhraseAppUpdater
|
|
84
140
|
|
85
141
|
private
|
86
142
|
|
143
|
+
def store_parent_commit(commit_hash)
|
144
|
+
params = PhraseApp::RequestParams::TagParams.new(
|
145
|
+
name: GIT_TAG_PREFIX + commit_hash)
|
146
|
+
phraseapp_request { @client.tag_create(@project_id, params) }
|
147
|
+
end
|
148
|
+
|
87
149
|
def phraseapp_request(&block)
|
88
|
-
|
89
|
-
res, err = block.call
|
90
|
-
rescue RuntimeError => e
|
91
|
-
if e.message[/\(401\)/]
|
92
|
-
raise BadAPIKeyError.new(e)
|
93
|
-
elsif e.message[/not found/]
|
94
|
-
raise BadProjectIDError.new(e)
|
95
|
-
else
|
96
|
-
raise e
|
97
|
-
end
|
98
|
-
end
|
150
|
+
res, err = block.call
|
99
151
|
|
100
152
|
unless err.nil?
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
153
|
+
error =
|
154
|
+
if err.respond_to?(:error)
|
155
|
+
err.error
|
156
|
+
else
|
157
|
+
err.errors.join('|')
|
158
|
+
end
|
106
159
|
|
107
160
|
raise RuntimeError.new(error)
|
108
161
|
end
|
109
162
|
|
110
163
|
res
|
164
|
+
|
165
|
+
rescue RuntimeError => e
|
166
|
+
if e.message.match?(/\(401\)/)
|
167
|
+
raise BadAPIKeyError.new(e)
|
168
|
+
elsif e.message.match?(/not found/)
|
169
|
+
raise BadProjectIDError.new(e, @project_id)
|
170
|
+
elsif e.message.match?(/has already been taken/)
|
171
|
+
raise ProjectNameTakenError.new(e)
|
172
|
+
else
|
173
|
+
raise
|
174
|
+
end
|
111
175
|
end
|
112
176
|
|
113
177
|
# PhraseApp allows two concurrent connections at a time.
|
114
178
|
THREAD_COUNT = 2
|
115
179
|
|
116
180
|
def threaded_request(worklist, &block)
|
117
|
-
|
118
|
-
threads = []
|
119
|
-
|
120
|
-
THREAD_COUNT.times do
|
121
|
-
threads << Thread.new do
|
122
|
-
Thread.current[:result] = {}
|
123
|
-
|
124
|
-
begin
|
125
|
-
while work = queue.pop(true) do
|
126
|
-
Thread.current[:result][work] = block.call(work)
|
127
|
-
end
|
128
|
-
rescue ThreadError => e
|
129
|
-
Thread.exit
|
130
|
-
end
|
131
|
-
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
threads.each(&:join)
|
136
|
-
|
137
|
-
threads.each_with_object({}) do |thread, results|
|
138
|
-
results.merge!(thread[:result])
|
139
|
-
end
|
181
|
+
Parallel.map(worklist, in_threads: THREAD_COUNT, &block)
|
140
182
|
end
|
141
183
|
|
142
184
|
def create_upload_params(locale_name)
|
143
185
|
upload_params = PhraseApp::RequestParams::UploadParams.new
|
144
|
-
upload_params.file_encoding =
|
145
|
-
upload_params.file_format = @locale_file_class.
|
186
|
+
upload_params.file_encoding = 'UTF-8'
|
187
|
+
upload_params.file_format = @locale_file_class.phraseapp_type
|
146
188
|
upload_params.locale_id = locale_name
|
147
189
|
upload_params.skip_unverification = false
|
148
190
|
upload_params.update_translations = true
|
@@ -171,6 +213,12 @@ class PhraseAppUpdater
|
|
171
213
|
end
|
172
214
|
end
|
173
215
|
|
216
|
+
class MissingGitParentError < RuntimeError
|
217
|
+
def initialize
|
218
|
+
super('Could not locate tag representing git ancestor commit')
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
174
222
|
class BadAPIKeyError < RuntimeError
|
175
223
|
def initialize(original_error)
|
176
224
|
super(original_error.message)
|
@@ -178,10 +226,18 @@ class PhraseAppUpdater
|
|
178
226
|
end
|
179
227
|
|
180
228
|
class BadProjectIDError < RuntimeError
|
229
|
+
attr_reader :project_id
|
230
|
+
|
231
|
+
def initialize(original_error, id)
|
232
|
+
@project_id = id
|
233
|
+
super(original_error.message)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
class ProjectNameTakenError < RuntimeError
|
181
238
|
def initialize(original_error)
|
182
239
|
super(original_error.message)
|
183
240
|
end
|
184
241
|
end
|
185
242
|
end
|
186
243
|
end
|
187
|
-
|
data/phraseapp_updater.gemspec
CHANGED
@@ -25,6 +25,7 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.add_dependency "multi_json", "~> 1.12"
|
26
26
|
spec.add_dependency "oj", "~> 2.18"
|
27
27
|
spec.add_dependency "deep_merge", "~> 1.1"
|
28
|
+
spec.add_dependency "parallel", "~> 1.12"
|
28
29
|
|
29
30
|
spec.add_development_dependency "bundler", "~> 1.12"
|
30
31
|
spec.add_development_dependency "rake", "~> 10.0"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: phraseapp_updater
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kevin Griffin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-10-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -94,6 +94,20 @@ dependencies:
|
|
94
94
|
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '1.1'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: parallel
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '1.12'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '1.12'
|
97
111
|
- !ruby/object:Gem::Dependency
|
98
112
|
name: bundler
|
99
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -168,14 +182,15 @@ files:
|
|
168
182
|
- README.markdown
|
169
183
|
- Rakefile
|
170
184
|
- bin/console
|
185
|
+
- bin/phraseapp_common.sh
|
171
186
|
- bin/phraseapp_updater
|
172
187
|
- bin/setup
|
188
|
+
- bin/synchronize_phraseapp.sh
|
173
189
|
- lib/phraseapp_updater.rb
|
174
190
|
- lib/phraseapp_updater/differ.rb
|
175
191
|
- lib/phraseapp_updater/index_by.rb
|
176
192
|
- lib/phraseapp_updater/locale_file.rb
|
177
193
|
- lib/phraseapp_updater/locale_file/json_file.rb
|
178
|
-
- lib/phraseapp_updater/locale_file/loader.rb
|
179
194
|
- lib/phraseapp_updater/locale_file/yaml_file.rb
|
180
195
|
- lib/phraseapp_updater/phraseapp_api.rb
|
181
196
|
- lib/phraseapp_updater/version.rb
|
@@ -201,7 +216,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
201
216
|
version: '0'
|
202
217
|
requirements: []
|
203
218
|
rubyforge_project:
|
204
|
-
rubygems_version: 2.
|
219
|
+
rubygems_version: 2.7.7
|
205
220
|
signing_key:
|
206
221
|
specification_version: 4
|
207
222
|
summary: A three-way differ for PhraseApp projects.
|
@@ -1,24 +0,0 @@
|
|
1
|
-
require 'phraseapp_updater/locale_file'
|
2
|
-
|
3
|
-
class PhraseAppUpdater
|
4
|
-
class LocaleFile
|
5
|
-
class Loader
|
6
|
-
def initialize(extension)
|
7
|
-
@extension = extension
|
8
|
-
end
|
9
|
-
|
10
|
-
def load(filename)
|
11
|
-
unless File.readable?(filename) && File.file?(filename)
|
12
|
-
raise RuntimeError.new("Couldn't read localization file at #{filename}")
|
13
|
-
end
|
14
|
-
|
15
|
-
LocaleFile.class_for_file_format(@extension).new(File.basename(filename).chomp(".#{@extension}"), File.read(filename))
|
16
|
-
end
|
17
|
-
|
18
|
-
def filenames(locale_directory)
|
19
|
-
Dir["#{locale_directory}/*.#{@extension}"]
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|