phraseapp_updater 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b9bfceed600065602f978bbafed9f6e6a729636c
4
- data.tar.gz: 581f527db36b94cb44ab4fdf9eeaf621ecdbbf4e
3
+ metadata.gz: 8fda2274698391255a06511a13c2ee32f163986e
4
+ data.tar.gz: 7d8e53efb3379ca73721762d61ae3382d9c4179d
5
5
  SHA512:
6
- metadata.gz: baedaa18f216752c02e01b79c12b7236c38b624089fe54d904e75abc1f0c41e26fefdb95451eaa74b6054d9dd6d7aae2a79a4bd1175d620b3d6c195d80340d33
7
- data.tar.gz: 443a35fde4d991f45e4f540539aae8ed0ee1f926efce325279a5978129dbcab8d35ec7c70e1a84cff99603cdadd0d176dba3ed9c1993ca2d9f4207bb455e73aa
6
+ metadata.gz: 2813702a946d84e7fcb926b9ddf8e4accd6c9fd2c18ca678d389b8e953759cd041c7b09248da8d51f7032b113aa7208ae15d580f23223afb4dc518d78b6cce09
7
+ data.tar.gz: 67d9a874fc97e09018e464ade886851a4c32b63d05ebc8c90da46be5b4179d4368130c1bd0c23bec91c1b7cebb9f0d2699ffe44bd359dccdae1e14a51fc2e7c8
data/README.markdown CHANGED
@@ -2,9 +2,15 @@
2
2
 
3
3
  [![Build Status](https://travis-ci.org/iknow/phraseapp_updater.svg?branch=master)](https://travis-ci.org/iknow/phraseapp_updater)
4
4
 
5
- **Version** 0.1.0
5
+ **Version** 0.1.3
6
6
 
7
- This is a tool for performing three-way merges of [PhraseApp](https://phraseapp.com) locale data with locale data commited to your application.
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 uploaded data wins
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
- `phraseapp_updater` operates on two directories and your PhraseApp API
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
- The main command is the the `push_changes` command:
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 push_changes --new_locales_path="/data/previous", --previous_locales_path="/data/new" --phraseapp_api_key="yourkey" --phraseapp_project_id="projectid"
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` is analogous to the command line version:
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
- * Expose the changed files on the command line
112
- * Implement other `LocaleFile`s with `parse` for non-JSON types
113
- * Checking if PhraseApp files changed during execution before upload, to reduce the race condition window
114
- * More specs for the API and shell
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
 
@@ -3,128 +3,137 @@ require 'thor'
3
3
  require 'phraseapp_updater'
4
4
 
5
5
  class PhraseAppUpdaterCLI < Thor
6
- desc "push", "Update PhraseApp project by merging changes from locale file and PhraseApp"
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
- if options[:config_file_path]
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
- if phraseapp_project_id.to_s.empty?
36
- raise RuntimeError.new("Must provide Phraseapp project ID. --phraseapp_project_id or PA_PROJECT_ID")
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
- unless File.readable?(new_locales_path) && File.directory?(new_locales_path)
46
- raise RuntimeError.new("Path to locales is not a readable directory: #{new_locales_path}")
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
- previous_locales_path = options.fetch(:previous_locales_path, ENV["PA_PREVIOUS_LOCALES_PATH"])
50
- if previous_locales_path.to_s.empty?
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
- unless File.readable?(previous_locales_path) && File.directory?(previous_locales_path)
55
- raise RuntimeError.new("Path to locales is not a readable directory: #{previous_locales_path}")
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.each do |file|
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.each do |file|
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
- # Like a bad API key
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
- unless File.readable?(fallback_path) && File.directory?(fallback_path)
103
- raise RuntimeError.new("fallback_path directory is not a readable directory: #{fallback_path}")
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
- destination_path = options.fetch(:destination_path, fallback_path)
80
+ private
81
+
82
+ def load_phraseapp_credentials(options)
83
+ if options[:config_file_path]
107
84
 
108
- unless File.writable?(fallback_path) && File.directory?(fallback_path)
109
- raise RuntimeError.new("destination directory is not a writable directory: #{fallback_path}")
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 = options.fetch(:phraseapp_api_key, ENV["PA_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 = options.fetch(:phraseapp_project_id, ENV["PA_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
- files = PhraseAppUpdater.pull(phraseapp_api_key, phraseapp_project_id, fallback_path)
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
- path = "#{destination_path.chomp("/")}/#{file.name}.json"
126
- File.write(path, file.content)
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
 
@@ -24,6 +24,10 @@ class PhraseAppUpdater
24
24
  "#{name}, #{content[0,20]}..."
25
25
  end
26
26
 
27
+ def name_with_extension
28
+ "#{name}.json"
29
+ end
30
+
27
31
  private
28
32
 
29
33
  def parse(content)
@@ -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
- res, err = block.call
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
- "file_merge_upload_#{Time.now.strftime('%Y%m%d%H%M%S')}"
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
 
@@ -1,3 +1,3 @@
1
1
  class PhraseAppUpdater
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.3"
3
3
  end
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.2
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-16 00:00:00.000000000 Z
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.4.8
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: