phraseapp_updater 0.1.2 → 0.1.3
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 +4 -4
- data/README.markdown +63 -14
- data/bin/phraseapp_updater +93 -84
- data/lib/phraseapp_updater/locale_file.rb +4 -0
- data/lib/phraseapp_updater/phraseapp_api.rb +25 -3
- data/lib/phraseapp_updater/version.rb +1 -1
- metadata +3 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8fda2274698391255a06511a13c2ee32f163986e
|
4
|
+
data.tar.gz: 7d8e53efb3379ca73721762d61ae3382d9c4179d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2813702a946d84e7fcb926b9ddf8e4accd6c9fd2c18ca678d389b8e953759cd041c7b09248da8d51f7032b113aa7208ae15d580f23223afb4dc518d78b6cce09
|
7
|
+
data.tar.gz: 67d9a874fc97e09018e464ade886851a4c32b63d05ebc8c90da46be5b4179d4368130c1bd0c23bec91c1b7cebb9f0d2699ffe44bd359dccdae1e14a51fc2e7c8
|
data/README.markdown
CHANGED
@@ -2,9 +2,15 @@
|
|
2
2
|
|
3
3
|
[](https://travis-ci.org/iknow/phraseapp_updater)
|
4
4
|
|
5
|
-
**Version** 0.1.
|
5
|
+
**Version** 0.1.3
|
6
6
|
|
7
|
-
This is a tool for
|
7
|
+
This is a tool for merging PhraseApp locale data with locale data
|
8
|
+
committed in your project.
|
9
|
+
|
10
|
+
It can perform three-way merges of [PhraseApp](https://phraseapp.com) locale data with locale data commited to your application.
|
11
|
+
It can also pull from PhraseApp, ignoring missing keys (this is very
|
12
|
+
useful for using "unverified" status for marking a translation as a
|
13
|
+
draft).
|
8
14
|
|
9
15
|
Our current workflow has localizers working on a `master` project on
|
10
16
|
PhraseApp. This regularly gets pulled into the `master` branch of our
|
@@ -19,7 +25,7 @@ them, but the API only allows either a) completely overwriting
|
|
19
25
|
PhraseApp's data or b) reapplying PhraseApp's data on top of the
|
20
26
|
uploaded data.
|
21
27
|
|
22
|
-
What we want instead is a three way merge where the
|
28
|
+
What we want instead is a three way merge where the committed data wins
|
23
29
|
on conflict. Non-conflicting changes on PhraseApp are preserved, while
|
24
30
|
changes on both sides take the committed data. The result of the merge
|
25
31
|
is then sent to PhraseApp, keeping it up-to-date with the newest commit
|
@@ -74,15 +80,16 @@ Or install it yourself as:
|
|
74
80
|
CLI
|
75
81
|
---
|
76
82
|
|
77
|
-
|
78
|
-
data. The two directories should contain the previous revision of your
|
79
|
-
locale files and the latest revision of the same files. These will be
|
80
|
-
used in the merge with the files on PhraseApp.
|
83
|
+
**Push**
|
81
84
|
|
82
|
-
|
85
|
+
`phraseapp_updater push` operates on two directories and your PhraseApp API
|
86
|
+
data. The two directories should contain the previous revision of your
|
87
|
+
locale files from PhraseApp and the latest revision of the same files
|
88
|
+
committed to your application's respository. These will be used in the
|
89
|
+
merge with the files on PhraseApp.
|
83
90
|
|
84
91
|
```
|
85
|
-
phraseapp_updater
|
92
|
+
phraseapp_updater push --new_locales_path="/data/previous", --previous_locales_path="/data/new" --phraseapp_api_key="yourkey" --phraseapp_project_id="projectid"
|
86
93
|
```
|
87
94
|
|
88
95
|
The arguments provided to the command can also be specified as shell
|
@@ -95,23 +102,65 @@ PA_API_KEY
|
|
95
102
|
PA_PROJECT_ID
|
96
103
|
```
|
97
104
|
|
105
|
+
Additionally, PhraseApp credentials can be loaded from a
|
106
|
+
`.phraseapp.yml` file, specified with `--config-file-path`
|
107
|
+
|
108
|
+
**Pull**
|
109
|
+
|
110
|
+
`phraseapp_updater pull` pulls data down from your PhraseApp project.
|
111
|
+
However, when keys are missing from the PhraseApp data, it restores them
|
112
|
+
(if present) from the files at fallback path provided. This allows you
|
113
|
+
to mark keys as "unverified" on PhraseApp, meaning you don't pull in
|
114
|
+
draft translations, while allowing you to keep the current version of
|
115
|
+
that translation.
|
116
|
+
|
117
|
+
If you want to pull without this fallback behavior, PhraseApp's [client](https://phraseapp.com/docs/developers/cli/)
|
118
|
+
is the best tool to use.
|
119
|
+
|
120
|
+
```
|
121
|
+
phraseapp_updater pull --fallback_path="/data/app/locales" --phraseapp_api_key="yourkey" --phraseapp_project_id="projectid"
|
122
|
+
```
|
123
|
+
|
124
|
+
The PhraseApp data passed to the command can also be specified as shell
|
125
|
+
variables:
|
126
|
+
|
127
|
+
```
|
128
|
+
PA_API_KEY
|
129
|
+
PA_PROJECT_ID
|
130
|
+
```
|
131
|
+
|
132
|
+
Additionally, PhraseApp credentials can be loaded from a
|
133
|
+
`.phraseapp.yml` file, specified with `--config-file-path`
|
134
|
+
|
98
135
|
Ruby
|
99
136
|
---
|
100
137
|
|
101
|
-
`PhraseAppUpdater.push`
|
138
|
+
`PhraseAppUpdater.push` and `PhraseAppUpdater.pull` are analogous to the command line versions:
|
102
139
|
|
103
140
|
```ruby
|
104
141
|
PhraseAppUpdater.push("api_key", "project_id", "previous/path", "current/path")
|
142
|
+
PhraseAppUpdater.pull("api_key", "project_id", "fallback/path")
|
105
143
|
```
|
106
144
|
|
145
|
+
|
146
|
+
## git-based Driver
|
147
|
+
|
148
|
+
We use a small bash script for driving this library to push and pull
|
149
|
+
from PhraseApp. While there are many ways to merge data in your
|
150
|
+
application with PhraseApp, this works for us:
|
151
|
+
|
152
|
+
https://gist.github.com/kevingriffin/d59821446ce424a56c7da2686d4ae082
|
153
|
+
|
107
154
|
## Future Improvements
|
108
155
|
|
109
156
|
If you'd like to contribute, these would be very helpful!
|
110
157
|
|
111
|
-
*
|
112
|
-
|
113
|
-
*
|
114
|
-
*
|
158
|
+
* Separating downloading and resolving data from PhraseApp from pushing
|
159
|
+
back up to it, to enable different kinds of workflows.
|
160
|
+
* Expose the changed files on the command line.
|
161
|
+
* Implement other `LocaleFile`s with `parse` for non-JSON types.
|
162
|
+
* Checking if PhraseApp files changed during execution before upload, to reduce the race condition window.
|
163
|
+
* More specs for the API and shell.
|
115
164
|
|
116
165
|
## Development
|
117
166
|
|
data/bin/phraseapp_updater
CHANGED
@@ -3,128 +3,137 @@ require 'thor'
|
|
3
3
|
require 'phraseapp_updater'
|
4
4
|
|
5
5
|
class PhraseAppUpdaterCLI < Thor
|
6
|
-
desc "push", "Update PhraseApp project
|
7
|
-
option :new_locales_path, type: :string
|
8
|
-
option :previous_locales_path, type: :string
|
9
|
-
option :phraseapp_api_key, type: :string
|
10
|
-
option :phraseapp_project_id, type: :string
|
11
|
-
option :config_file_path, type: :string
|
12
|
-
option :store_results_path, type: :string
|
13
|
-
option :store_phraseapp_originals_path, type: :string
|
6
|
+
desc "push", "Update PhraseApp project via a 3-way merge with local locale files and PhraseApp data."
|
7
|
+
option :new_locales_path, type: :string, desc: "Path to newest revision of local files. Shell variable: PA_NEW_LOCALES_PATH"
|
8
|
+
option :previous_locales_path, type: :string, desc: "Path to previous revision of local files. Shell variable: PA_PREVIOUS_LOCALES_PATH"
|
9
|
+
option :phraseapp_api_key, type: :string, desc: "PhraseApp API key. Shell variable: PA_API_KEY"
|
10
|
+
option :phraseapp_project_id, type: :string, desc: "PhraseApp project ID. Shell variable: PA_PROJECT_ID"
|
11
|
+
option :config_file_path, type: :string, desc: "Path to .phraseapp.yml config file to read API key and project ID."
|
12
|
+
option :store_results_path, type: :string, desc: "Path to write the resolved files. Shell variable: PA_STORE_RESULTS_PATH"
|
13
|
+
option :store_phraseapp_originals_path, type: :string, desc: "Path to write the files downloaded from PhraseApp before the merge. Shell variable: PA_STORE_PHRASEAPP_ORIGINALS_PATH"
|
14
14
|
|
15
15
|
def push
|
16
|
-
|
17
|
-
|
18
|
-
if options[:phraseapp_api_key] || options[:phraseapp_project_id]
|
19
|
-
raise RuntimeError.new("Provided both a path to PhraseApp config file and command line arguments. Specify only one. #{options}")
|
20
|
-
end
|
21
|
-
|
22
|
-
config = PhraseAppUpdater.load_config(options[:config_file_path])
|
23
|
-
|
24
|
-
phraseapp_api_key = config.api_key
|
25
|
-
phraseapp_project_id = config.project_id
|
26
|
-
else
|
27
|
-
phraseapp_api_key = options.fetch(:phraseapp_api_key, ENV["PA_API_KEY"])
|
28
|
-
phraseapp_project_id = options.fetch(:phraseapp_project_id, ENV["PA_PROJECT_ID"])
|
29
|
-
end
|
30
|
-
|
31
|
-
if phraseapp_api_key.to_s.empty?
|
32
|
-
raise RuntimeError.new("Must provide Phraseapp API key. --phraseapp_api_key or PA_API_KEY")
|
33
|
-
end
|
16
|
+
phraseapp_api_key, phraseapp_project_id = load_phraseapp_credentials(options)
|
34
17
|
|
35
|
-
|
36
|
-
|
37
|
-
end
|
38
|
-
|
39
|
-
new_locales_path = options.fetch(:new_locales_path, ENV["PA_NEW_LOCALES_PATH"])
|
40
|
-
|
41
|
-
if new_locales_path.to_s.empty?
|
42
|
-
raise RuntimeError.new("Must provide a path to the locale files to upload. --new_locales_path or PA_NEW_LOCALES_PATH")
|
43
|
-
end
|
18
|
+
new_locales_path = options.fetch(:new_locales_path, ENV["PA_NEW_LOCALES_PATH"]).to_s
|
19
|
+
validate_readable_path!('new_locales_path', new_locales_path)
|
44
20
|
|
45
|
-
|
46
|
-
|
47
|
-
end
|
21
|
+
previous_locales_path = options.fetch(:previous_locales_path, ENV["PA_PREVIOUS_LOCALES_PATH"]).to_s
|
22
|
+
validate_readable_path!('previous_locales_path', previous_locales_path)
|
48
23
|
|
49
|
-
|
50
|
-
|
51
|
-
raise RuntimeError.new("Must provide a path to the locale files to upload. --previous_locales_path or PA_PREVIOUS_LOCALES_PATH")
|
52
|
-
end
|
24
|
+
store_results_path = options.fetch(:store_results_path, ENV["PA_STORE_RESULTS_PATH"]).to_s
|
25
|
+
validate_writable_path!('store_results_path', store_results_path)
|
53
26
|
|
54
|
-
|
55
|
-
|
56
|
-
end
|
57
|
-
|
58
|
-
store_results_path = options.fetch(:store_results_path, ENV["PA_store_results_path"]).to_s
|
59
|
-
|
60
|
-
if !store_results_path.empty? && (!File.writable?(store_results_path) || !File.directory?(store_results_path))
|
61
|
-
raise RuntimeError.new("Path to store results is not a writable directory: #{store_results_path}")
|
62
|
-
end
|
63
|
-
|
64
|
-
store_phraseapp_originals_path = options.fetch(:store_phraseapp_originals_path, ENV["PA_store_phraseapp_originals_path"]).to_s
|
65
|
-
|
66
|
-
if !store_phraseapp_originals_path.empty? && (!File.writable?(store_phraseapp_originals_path) || !File.directory?(store_phraseapp_originals_path))
|
67
|
-
raise RuntimeError.new("Path to store PhraseApp originals is not a writable directory: #{store_phraseapp_originals_path}")
|
68
|
-
end
|
27
|
+
store_phraseapp_originals_path = options.fetch(:store_phraseapp_originals_path, ENV["PA_STORE_PHRASEAPP_ORIGINALS_PATH"]).to_s
|
28
|
+
validate_writable_path!('store_phraseapp_originals_path', store_phraseapp_originals_path)
|
69
29
|
|
70
30
|
begin
|
71
31
|
result = PhraseAppUpdater.push(phraseapp_api_key, phraseapp_project_id, previous_locales_path, new_locales_path)
|
72
32
|
|
73
33
|
unless store_results_path.empty?
|
74
|
-
result.resolved_files
|
75
|
-
path = "#{store_results_path.chomp("/")}/#{file.name}.json"
|
76
|
-
File.write(path, file.content)
|
77
|
-
end
|
34
|
+
write_locale_files(store_results_path, result.resolved_files)
|
78
35
|
end
|
79
36
|
|
80
37
|
unless store_phraseapp_originals_path.empty?
|
81
|
-
result.original_phraseapp_files
|
82
|
-
path = "#{store_phraseapp_originals_path.chomp("/")}/#{file.name}.json"
|
83
|
-
File.write(path, file.content)
|
84
|
-
end
|
38
|
+
write_locale_files(store_phraseapp_originals_path, result.original_phraseapp_files)
|
85
39
|
end
|
40
|
+
rescue PhraseAppUpdater::PhraseAppAPI::BadAPIKeyError => e
|
41
|
+
puts "Bad PhraseApp API key."
|
42
|
+
rescue PhraseAppUpdater::PhraseAppAPI::BadProjectIDError => e
|
43
|
+
puts "Bad PhraseApp project ID: #{phraseapp_project_id}"
|
86
44
|
rescue StandardError => e
|
87
|
-
|
88
|
-
# Raise more specific errors and handle
|
45
|
+
puts "Unknown error when pushing files"
|
89
46
|
raise e
|
90
47
|
end
|
91
48
|
end
|
92
49
|
|
93
50
|
|
94
|
-
desc "pull", "Pulls data from PhraseApp for deployment."
|
95
|
-
option :fallback_path, type: :string, required: true
|
96
|
-
option :destination_path, type: :string
|
97
|
-
option :phraseapp_api_key, type: :string
|
98
|
-
option :phraseapp_project_id, type: :string
|
51
|
+
desc "pull", "Pulls data from PhraseApp for deployment, replacing missing keys from fallback."
|
52
|
+
option :fallback_path, type: :string, required: true, desc: "Path to the files to restore missing keys from."
|
53
|
+
option :destination_path, type: :string, desc: "Path to write the resolved files to. If not provided, --fallback_path is used."
|
54
|
+
option :phraseapp_api_key, type: :string, desc: "PhraseApp API key. Shell variable: PA_API_KEY"
|
55
|
+
option :phraseapp_project_id, type: :string, desc: "PhraseApp project ID. Shell variable: PA_PROJECT_ID"
|
56
|
+
option :config_file_path, type: :string, desc: "Path to .phraseapp.yml config file to read API key and project ID."
|
99
57
|
|
100
58
|
def pull
|
59
|
+
phraseapp_api_key, phraseapp_project_id = load_phraseapp_credentials(options)
|
60
|
+
|
101
61
|
fallback_path = options[:fallback_path]
|
102
|
-
|
103
|
-
|
62
|
+
validate_readable_path!('fallback_path', fallback_path)
|
63
|
+
|
64
|
+
destination_path = options.fetch(:destination_path, fallback_path).to_s
|
65
|
+
validate_writable_path!('destination_path', destination_path)
|
66
|
+
|
67
|
+
begin
|
68
|
+
files = PhraseAppUpdater.pull(phraseapp_api_key, phraseapp_project_id, fallback_path)
|
69
|
+
write_locale_files(destination_path, files)
|
70
|
+
rescue PhraseAppUpdater::PhraseAppAPI::BadAPIKeyError => e
|
71
|
+
puts "Bad PhraseApp API key."
|
72
|
+
rescue PhraseAppUpdater::PhraseAppAPI::BadProjectIDError => e
|
73
|
+
puts "Bad PhraseApp project ID: #{phraseapp_project_id}"
|
74
|
+
rescue StandardError => e
|
75
|
+
puts "Unknown error when pushing files"
|
76
|
+
raise e
|
104
77
|
end
|
78
|
+
end
|
105
79
|
|
106
|
-
|
80
|
+
private
|
81
|
+
|
82
|
+
def load_phraseapp_credentials(options)
|
83
|
+
if options[:config_file_path]
|
107
84
|
|
108
|
-
|
109
|
-
|
85
|
+
if options[:phraseapp_api_key] || options[:phraseapp_project_id]
|
86
|
+
raise RuntimeError.new("Provided both a path to PhraseApp config file and command line arguments. Specify only one. #{options}")
|
87
|
+
end
|
88
|
+
|
89
|
+
config = PhraseAppUpdater.load_config(options[:config_file_path])
|
90
|
+
|
91
|
+
phraseapp_api_key = config.api_key
|
92
|
+
phraseapp_project_id = config.project_id
|
93
|
+
else
|
94
|
+
phraseapp_api_key = options.fetch(:phraseapp_api_key, ENV["PA_API_KEY"]).to_s
|
95
|
+
phraseapp_project_id = options.fetch(:phraseapp_project_id, ENV["PA_PROJECT_ID"]).to_s
|
110
96
|
end
|
111
97
|
|
112
|
-
phraseapp_api_key
|
113
|
-
if phraseapp_api_key.to_s.empty?
|
98
|
+
if phraseapp_api_key.empty?
|
114
99
|
raise RuntimeError.new("Must provide Phraseapp API key. --phraseapp_api_key or PA_API_KEY")
|
115
100
|
end
|
116
101
|
|
117
|
-
phraseapp_project_id
|
118
|
-
if phraseapp_project_id.to_s.empty?
|
102
|
+
if phraseapp_project_id.empty?
|
119
103
|
raise RuntimeError.new("Must provide Phraseapp project ID. --phraseapp_project_id or PA_PROJECT_ID")
|
120
104
|
end
|
121
105
|
|
122
|
-
|
106
|
+
return [phraseapp_api_key, phraseapp_project_id]
|
107
|
+
end
|
108
|
+
|
109
|
+
def validate_path!(name, path)
|
110
|
+
if path.empty?
|
111
|
+
raise RuntimeError.new("#{name} was empty.")
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def validate_readable_path!(name, path)
|
116
|
+
validate_path!(name, path)
|
117
|
+
|
118
|
+
unless File.readable?(path) && File.directory?(path)
|
119
|
+
raise RuntimeError.new("#{name} path is not a readable directory: #{path}")
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def validate_writable_path!(name, path)
|
124
|
+
validate_path!(name, path)
|
125
|
+
|
126
|
+
unless File.writable?(path) && File.directory?(path)
|
127
|
+
raise RuntimeError.new("#{name} path is not a writable directory: #{path}")
|
128
|
+
end
|
129
|
+
end
|
123
130
|
|
131
|
+
def write_locale_files(path, files)
|
124
132
|
files.each do |file|
|
125
|
-
|
126
|
-
File.write(
|
133
|
+
full_path = "#{path.chomp('/')}/#{file.name_with_extension}"
|
134
|
+
File.write(full_path, file.content)
|
127
135
|
end
|
136
|
+
puts "Wrote #{files.count} files to #{path}: #{files.map(&:name_with_extension)}"
|
128
137
|
end
|
129
138
|
end
|
130
139
|
|
@@ -1,5 +1,5 @@
|
|
1
|
-
require 'phraseapp-ruby'
|
2
1
|
require 'phraseapp_updater/locale_file'
|
2
|
+
require 'phraseapp-ruby'
|
3
3
|
require 'thread'
|
4
4
|
|
5
5
|
class PhraseAppUpdater
|
@@ -83,7 +83,17 @@ class PhraseAppUpdater
|
|
83
83
|
private
|
84
84
|
|
85
85
|
def phraseapp_request(&block)
|
86
|
-
|
86
|
+
begin
|
87
|
+
res, err = block.call
|
88
|
+
rescue RuntimeError => e
|
89
|
+
if e.message[/\(401\)/]
|
90
|
+
raise BadAPIKeyError.new(e)
|
91
|
+
elsif e.message[/not found/]
|
92
|
+
raise BadProjectIDError.new(e)
|
93
|
+
else
|
94
|
+
raise e
|
95
|
+
end
|
96
|
+
end
|
87
97
|
|
88
98
|
unless err.nil?
|
89
99
|
if err.respond_to?(:error)
|
@@ -139,7 +149,7 @@ class PhraseAppUpdater
|
|
139
149
|
end
|
140
150
|
|
141
151
|
def generate_upload_tag
|
142
|
-
"
|
152
|
+
"phraseapp_updater_upload_#{Time.now.strftime('%Y%m%d%H%M%S')}"
|
143
153
|
end
|
144
154
|
|
145
155
|
class Locale
|
@@ -158,6 +168,18 @@ class PhraseAppUpdater
|
|
158
168
|
"#{name} : #{id}"
|
159
169
|
end
|
160
170
|
end
|
171
|
+
|
172
|
+
class BadAPIKeyError < RuntimeError
|
173
|
+
def initialize(original_error)
|
174
|
+
super(original_error.message)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
class BadProjectIDError < RuntimeError
|
179
|
+
def initialize(original_error)
|
180
|
+
super(original_error.message)
|
181
|
+
end
|
182
|
+
end
|
161
183
|
end
|
162
184
|
end
|
163
185
|
|
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.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kevin Griffin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-02-
|
11
|
+
date: 2017-02-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -199,9 +199,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
199
199
|
version: '0'
|
200
200
|
requirements: []
|
201
201
|
rubyforge_project:
|
202
|
-
rubygems_version: 2.
|
202
|
+
rubygems_version: 2.5.1
|
203
203
|
signing_key:
|
204
204
|
specification_version: 4
|
205
205
|
summary: A three-way differ for PhraseApp projects.
|
206
206
|
test_files: []
|
207
|
-
has_rdoc:
|