phraseapp_updater 0.1.7 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
|