web_translate_it 3.4.0 → 3.5.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 +4 -4
- data/history.md +7 -0
- data/lib/web_translate_it/api_resource.rb +6 -6
- data/lib/web_translate_it/commands/init.rb +22 -6
- data/lib/web_translate_it/configuration.rb +16 -2
- data/lib/web_translate_it/connection.rb +64 -1
- data/lib/web_translate_it/project.rb +5 -5
- data/lib/web_translate_it/term_translation.rb +1 -1
- data/lib/web_translate_it/translation_base.rb +1 -1
- data/lib/web_translate_it/translation_file.rb +3 -3
- data/lib/web_translate_it/util/http_response.rb +12 -1
- data/lib/web_translate_it/util/project_id_text.rb +36 -0
- data/lib/web_translate_it.rb +1 -0
- data/readme.md +27 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5e4d18f41ea8a29e79e6d789db02d52b1eb92d88504050755a7564872f3b5e59
|
|
4
|
+
data.tar.gz: d615308a0f20887d376b2f2b1556d066463334fe436da6b7aa893704b1b899e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8fa287ad61eb72fcb01c39e006bbdaaf864cdd55f76c814781077fc0f8ca1a77c93df066f7e66a07ee7d93f4c1496d0b421eaa458c2dcdbd1e0331550078dbb5
|
|
7
|
+
data.tar.gz: fe40d7dea985eff437449bae98799574b50c2757bbb77480d50cfe261bf06ba0d1f61b306a420c0efb3d56253fca3f2117d4a8700b7a0154ddf52f9294df70cb
|
data/history.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## Version 3.5.0 / 2026-11-05
|
|
2
|
+
|
|
3
|
+
* Support the new API tokens (project, personal, integration) besides the project API keys. The environment variable `WTI_PROJECT_API_KEY` and the `api_key` setting in `.wti` keep their names.
|
|
4
|
+
* Support organization tokens: set `project_id` in `.wti`, or `WTI_PROJECT_ID`, to the project's numeric id and `wti` addresses `/api/projects/<id>` instead of `/api/projects/current`. A non-numeric id is rejected. When the API finds no project for a `wti_` token at `/api/projects/current`, `wti` prints a hint, once. `wti init` writes `project_id` into `.wti` when `WTI_PROJECT_ID` is set.
|
|
5
|
+
* Friendlier errors: a project accepting API tokens only (`legacy_token_disabled`), a revoked or expired token (`invalid_token`) and a read-only token used for writing (`insufficient_scope`) now say what to do.
|
|
6
|
+
* User-facing text says "API token" rather than "Project API Key".
|
|
7
|
+
|
|
1
8
|
## Version 3.4.0 / 2026-11-04
|
|
2
9
|
|
|
3
10
|
* **The project API key is now sent in an `Authorization: Bearer` header instead of in the URL.** Requests go to `/api/projects/current/...` rather than `/api/projects/<api key>/...`, so the key no longer ends up in proxy, load balancer and server logs. webtranslateit.com accepts the header since 2026-11-04. Earlier `wti` versions keep working: the API still accepts the key in the URL.
|
|
@@ -19,7 +19,7 @@ module WebTranslateIt
|
|
|
19
19
|
|
|
20
20
|
def self.find_all(connection, params = {}) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
|
|
21
21
|
params = params.transform_keys(&:to_s)
|
|
22
|
-
url = "#{Connection
|
|
22
|
+
url = "#{Connection.project_path}/#{resource_path}"
|
|
23
23
|
url += "?#{HashUtil.to_params(filter_params(params))}" unless params.empty?
|
|
24
24
|
|
|
25
25
|
Concurrency.with_retries do
|
|
@@ -43,7 +43,7 @@ module WebTranslateIt
|
|
|
43
43
|
|
|
44
44
|
def self.find(connection, id)
|
|
45
45
|
Concurrency.with_retries do
|
|
46
|
-
response = connection.get("#{Connection
|
|
46
|
+
response = connection.get("#{Connection.project_path}/#{resource_path}/#{id}")
|
|
47
47
|
return nil if response.code.to_i == 404
|
|
48
48
|
|
|
49
49
|
record = new(JSON.parse(response.body), connection: connection)
|
|
@@ -66,7 +66,7 @@ module WebTranslateIt
|
|
|
66
66
|
|
|
67
67
|
def delete
|
|
68
68
|
Concurrency.with_retries do
|
|
69
|
-
HttpResponse.handle_response(connection.delete("#{Connection
|
|
69
|
+
HttpResponse.handle_response(connection.delete("#{Connection.project_path}/#{self.class.resource_path}/#{id}"))
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
72
|
|
|
@@ -76,7 +76,7 @@ module WebTranslateIt
|
|
|
76
76
|
return nil if new_record
|
|
77
77
|
|
|
78
78
|
Concurrency.with_retries do
|
|
79
|
-
response = HttpResponse.handle_response(connection.get("#{Connection
|
|
79
|
+
response = HttpResponse.handle_response(connection.get("#{Connection.project_path}/#{self.class.resource_path}/#{id}/locales/#{locale}/translations"))
|
|
80
80
|
json = JSON.parse(response)
|
|
81
81
|
return nil if json.empty?
|
|
82
82
|
|
|
@@ -106,13 +106,13 @@ module WebTranslateIt
|
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
Concurrency.with_retries do
|
|
109
|
-
HttpResponse.handle_response(connection.put("#{Connection
|
|
109
|
+
HttpResponse.handle_response(connection.put("#{Connection.project_path}/#{self.class.resource_path}/#{id}", body: to_json))
|
|
110
110
|
end
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
def create
|
|
114
114
|
Concurrency.with_retries do
|
|
115
|
-
raw = connection.post("#{Connection
|
|
115
|
+
raw = connection.post("#{Connection.project_path}/#{self.class.resource_path}", body: to_json(with_translations: true))
|
|
116
116
|
response = JSON.parse(HttpResponse.handle_response(raw))
|
|
117
117
|
self.id = response['id']
|
|
118
118
|
self.new_record = false
|
|
@@ -4,12 +4,14 @@ module WebTranslateIt
|
|
|
4
4
|
|
|
5
5
|
module Commands
|
|
6
6
|
|
|
7
|
-
class Init < Base
|
|
7
|
+
class Init < Base # rubocop:todo Metrics/ClassLength
|
|
8
8
|
|
|
9
9
|
ENV_API_KEY = 'WTI_PROJECT_API_KEY'
|
|
10
10
|
|
|
11
11
|
def call
|
|
12
12
|
puts '# Initializing project'
|
|
13
|
+
Connection.hint_without_config_file = true
|
|
14
|
+
validate_project_id
|
|
13
15
|
api_key, path = read_parameters
|
|
14
16
|
ensure_directory_exists(path)
|
|
15
17
|
project_info = JSON.parse(WebTranslateIt::Project.fetch_info(api_key))['project']
|
|
@@ -21,15 +23,21 @@ module WebTranslateIt
|
|
|
21
23
|
|
|
22
24
|
private
|
|
23
25
|
|
|
26
|
+
def validate_project_id
|
|
27
|
+
Connection.project_id
|
|
28
|
+
rescue ArgumentError => e
|
|
29
|
+
abort StringUtil.failure(e.message)
|
|
30
|
+
end
|
|
31
|
+
|
|
24
32
|
def read_parameters
|
|
25
33
|
if parameters.any?
|
|
26
34
|
warn_env_takes_precedence if env_api_key && parameters[0] != env_api_key
|
|
27
35
|
[parameters[0], '.wti']
|
|
28
36
|
elsif env_api_key
|
|
29
|
-
puts " Using the
|
|
37
|
+
puts " Using the API token from #{ENV_API_KEY}."
|
|
30
38
|
[env_api_key, Prompt.ask(' Path to configuration file:', '.wti')]
|
|
31
39
|
else
|
|
32
|
-
[Prompt.ask('
|
|
40
|
+
[Prompt.ask(' API token:'), Prompt.ask(' Path to configuration file:', '.wti')]
|
|
33
41
|
end
|
|
34
42
|
end
|
|
35
43
|
|
|
@@ -54,7 +62,7 @@ module WebTranslateIt
|
|
|
54
62
|
return if git?('check-ignore', '-q', path)
|
|
55
63
|
|
|
56
64
|
puts ''
|
|
57
|
-
puts StringUtil.important(" Warning: `#{path}` contains your
|
|
65
|
+
puts StringUtil.important(" Warning: `#{path}` contains your API token and is not ignored by git.")
|
|
58
66
|
puts " Add `#{path}` to .gitignore, or keep the key out of the file: set #{ENV_API_KEY}"
|
|
59
67
|
puts " and use `api_key: <%= ENV['#{ENV_API_KEY}'] %>` in `#{path}`."
|
|
60
68
|
end
|
|
@@ -112,14 +120,22 @@ module WebTranslateIt
|
|
|
112
120
|
key_from_env?(api_key) ? "<%= ENV['#{ENV_API_KEY}'] %>" : api_key
|
|
113
121
|
end
|
|
114
122
|
|
|
123
|
+
# Organization tokens reach a project by its numeric id only. Written when
|
|
124
|
+
# WTI_PROJECT_ID is set, so the generated file is unchanged otherwise.
|
|
125
|
+
def project_id_block
|
|
126
|
+
return '' if ENV.fetch('WTI_PROJECT_ID', '').strip.empty?
|
|
127
|
+
|
|
128
|
+
"\n# Project id, required for organization tokens. WTI_PROJECT_ID takes precedence over this value.\nproject_id: #{Connection.project_id}\n"
|
|
129
|
+
end
|
|
130
|
+
|
|
115
131
|
def generate_configuration(api_key, project_info)
|
|
116
132
|
<<~FILE
|
|
117
|
-
# Required - The
|
|
133
|
+
# Required - The API token from WebTranslateIt.com (project, personal, integration or organization token)
|
|
118
134
|
# More information: https://github.com/webtranslateit/webtranslateit/wiki#configuration-file
|
|
119
135
|
# The #{ENV_API_KEY} environment variable, when set, takes precedence over this value.
|
|
120
136
|
|
|
121
137
|
api_key: #{api_key_line(api_key)}
|
|
122
|
-
|
|
138
|
+
#{project_id_block}
|
|
123
139
|
# Optional - Locales not to sync with WebTranslateIt.
|
|
124
140
|
# Takes a string, a symbol, or an array of string or symbol.
|
|
125
141
|
|
|
@@ -18,6 +18,7 @@ module WebTranslateIt
|
|
|
18
18
|
self.path = root_path
|
|
19
19
|
self.logger = logger
|
|
20
20
|
if File.exist?(File.expand_path(path_to_config_file, path))
|
|
21
|
+
load_project_id
|
|
21
22
|
self.api_key = ENV.fetch('WTI_PROJECT_API_KEY') { configuration['api_key'] }
|
|
22
23
|
self.before_pull = configuration['before_pull']
|
|
23
24
|
self.after_pull = configuration['after_pull']
|
|
@@ -95,7 +96,7 @@ module WebTranslateIt
|
|
|
95
96
|
|
|
96
97
|
# Convenience method which returns the endpoint for fetching a list of locales for a project.
|
|
97
98
|
def api_url
|
|
98
|
-
Connection
|
|
99
|
+
Connection.project_path
|
|
99
100
|
end
|
|
100
101
|
|
|
101
102
|
# Returns a logger. If RAILS_DEFAULT_LOGGER is defined, use it, else, define a new logger.
|
|
@@ -108,11 +109,24 @@ module WebTranslateIt
|
|
|
108
109
|
end
|
|
109
110
|
|
|
110
111
|
def configuration
|
|
111
|
-
@configuration ||= YAML.safe_load(
|
|
112
|
+
@configuration ||= YAML.safe_load(rendered_configuration)
|
|
112
113
|
end
|
|
113
114
|
|
|
114
115
|
private
|
|
115
116
|
|
|
117
|
+
# Fails early, with a clear message, on a project id that is not numeric.
|
|
118
|
+
def load_project_id
|
|
119
|
+
ProjectIdText.validate!(rendered_configuration)
|
|
120
|
+
Connection.project_id = configuration['project_id']
|
|
121
|
+
Connection.project_id
|
|
122
|
+
rescue ArgumentError => e
|
|
123
|
+
abort StringUtil.failure("\n#{e.message}")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def rendered_configuration
|
|
127
|
+
@rendered_configuration ||= parse_erb_in_configuration
|
|
128
|
+
end
|
|
129
|
+
|
|
116
130
|
def load_project_data # rubocop:todo Metrics/AbcSize
|
|
117
131
|
project = JSON.parse(WebTranslateIt::Project.fetch_info(api_key))['project']
|
|
118
132
|
self.ignore_locales = locales_to_ignore(configuration)
|
|
@@ -7,16 +7,58 @@ module WebTranslateIt
|
|
|
7
7
|
# The API resolves `current` to the project the bearer token belongs to.
|
|
8
8
|
PROJECT_PATH = '/api/projects/current'
|
|
9
9
|
|
|
10
|
+
ORGANIZATION_TOKEN_HINT = 'No project matched this API token at /api/projects/current. Either the project doesn\'t accept API tokens yet ' \
|
|
11
|
+
'(a manager can switch it in Project settings → API), or this is an organization token: ' \
|
|
12
|
+
"set %<setting>s to the project's numeric id."
|
|
13
|
+
# The error the API answers when authentication finds no record for the token.
|
|
14
|
+
AUTHENTICATION_NOT_FOUND = 'No project found for this API token'
|
|
15
|
+
|
|
10
16
|
@debug = false
|
|
11
17
|
|
|
12
18
|
class << self
|
|
13
19
|
|
|
14
20
|
attr_reader :debug
|
|
15
21
|
|
|
22
|
+
# The hint is printed at most once per process.
|
|
23
|
+
attr_accessor :organization_token_hint_printed
|
|
24
|
+
|
|
25
|
+
# `wti init` rewrites .wti, so its hint mentions WTI_PROJECT_ID only.
|
|
26
|
+
attr_accessor :hint_without_config_file
|
|
27
|
+
|
|
28
|
+
# Numeric project id from the configuration file. Organization tokens can only
|
|
29
|
+
# reach a project by its id. WTI_PROJECT_ID takes precedence over it.
|
|
30
|
+
def project_id=(value)
|
|
31
|
+
@project_id = normalize_project_id(value)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def project_id
|
|
35
|
+
normalize_project_id(ENV.fetch('WTI_PROJECT_ID', nil), 'WTI_PROJECT_ID') || @project_id
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# `/api/projects/<id>` when a project id is set, `/api/projects/current` otherwise.
|
|
39
|
+
def project_path
|
|
40
|
+
id = project_id
|
|
41
|
+
id ? "/api/projects/#{id}" : PROJECT_PATH
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def normalize_project_id(value, source = 'project_id')
|
|
47
|
+
id = value.to_s.strip
|
|
48
|
+
return nil if id.empty?
|
|
49
|
+
raise ArgumentError, "Error: #{source} must be the numeric id of your project (digits only, no leading zero), got `#{id}`." unless id.match?(/\A[1-9]\d*\z/)
|
|
50
|
+
|
|
51
|
+
id
|
|
52
|
+
end
|
|
53
|
+
|
|
16
54
|
end
|
|
17
55
|
|
|
18
56
|
attr_reader :api_key, :http_connection
|
|
19
57
|
|
|
58
|
+
def project_path
|
|
59
|
+
self.class.project_path
|
|
60
|
+
end
|
|
61
|
+
|
|
20
62
|
#
|
|
21
63
|
# Initialize and yield a HTTPS Keep-Alive connection to WebTranslateIt.com
|
|
22
64
|
#
|
|
@@ -75,7 +117,28 @@ module WebTranslateIt
|
|
|
75
117
|
request['Authorization'] = "Bearer #{api_key}"
|
|
76
118
|
request.body = body if body
|
|
77
119
|
yield request if block_given?
|
|
78
|
-
http_connection.request(request)
|
|
120
|
+
response = http_connection.request(request)
|
|
121
|
+
print_organization_token_hint(path, response) if response.code.to_i == 404
|
|
122
|
+
response
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# `current` answers 404 to organization tokens, which can only address a project by id.
|
|
126
|
+
# Only the authentication error counts: an ordinary resource 404 says nothing about the token.
|
|
127
|
+
def print_organization_token_hint(path, response)
|
|
128
|
+
return if self.class.organization_token_hint_printed
|
|
129
|
+
return unless path.start_with?(PROJECT_PATH) && api_key.to_s.start_with?('wti_')
|
|
130
|
+
return unless authentication_not_found?(response)
|
|
131
|
+
|
|
132
|
+
self.class.organization_token_hint_printed = true
|
|
133
|
+
setting = self.class.hint_without_config_file ? '`WTI_PROJECT_ID`' : '`project_id` in .wti (or `WTI_PROJECT_ID`)'
|
|
134
|
+
warn format(ORGANIZATION_TOKEN_HINT, setting: setting)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def authentication_not_found?(response)
|
|
138
|
+
body = JSON.parse(response.body.to_s)
|
|
139
|
+
body.is_a?(Hash) && body['error'].to_s.start_with?(AUTHENTICATION_NOT_FOUND)
|
|
140
|
+
rescue JSON::ParserError
|
|
141
|
+
false
|
|
79
142
|
end
|
|
80
143
|
|
|
81
144
|
end
|
|
@@ -7,13 +7,13 @@ module WebTranslateIt
|
|
|
7
7
|
def self.fetch_info(api_key)
|
|
8
8
|
Concurrency.with_retries do
|
|
9
9
|
WebTranslateIt::Connection.new(api_key) do |conn|
|
|
10
|
-
return HttpResponse.handle_response(conn.get(Connection
|
|
10
|
+
return HttpResponse.handle_response(conn.get(Connection.project_path))
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def self.fetch_stats(api_key, file_id = nil)
|
|
16
|
-
url = file_id.nil? ? "#{Connection
|
|
16
|
+
url = file_id.nil? ? "#{Connection.project_path}/stats" : "#{Connection.project_path}/stats?file=#{file_id}"
|
|
17
17
|
Concurrency.with_retries do
|
|
18
18
|
WebTranslateIt::Connection.new(api_key) do |conn|
|
|
19
19
|
return HttpResponse.handle_response(conn.get(url))
|
|
@@ -24,7 +24,7 @@ module WebTranslateIt
|
|
|
24
24
|
# Fetch every language file of a project in a single request, as a zip
|
|
25
25
|
# archive. Pass a locale to narrow the archive down to that locale.
|
|
26
26
|
def self.fetch_zip(connection, locale: nil)
|
|
27
|
-
url = "#{Connection
|
|
27
|
+
url = "#{Connection.project_path}/zip_file"
|
|
28
28
|
url += "?locale=#{URI.encode_www_form_component(locale)}" if locale
|
|
29
29
|
Concurrency.with_retries do
|
|
30
30
|
HttpResponse.handle_response(connection.get(url))
|
|
@@ -33,14 +33,14 @@ module WebTranslateIt
|
|
|
33
33
|
|
|
34
34
|
def self.create_locale(connection, locale_code)
|
|
35
35
|
Concurrency.with_retries do
|
|
36
|
-
response = connection.post("#{Connection
|
|
36
|
+
response = connection.post("#{Connection.project_path}/locales") { |req| req.set_form_data({'id' => locale_code}, ';') }
|
|
37
37
|
HttpResponse.handle_response(response)
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def self.delete_locale(connection, locale_code)
|
|
42
42
|
Concurrency.with_retries do
|
|
43
|
-
HttpResponse.handle_response(connection.delete("#{Connection
|
|
43
|
+
HttpResponse.handle_response(connection.delete("#{Connection.project_path}/locales/#{locale_code}"))
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
|
|
@@ -43,7 +43,7 @@ module WebTranslateIt
|
|
|
43
43
|
|
|
44
44
|
def update
|
|
45
45
|
Concurrency.with_retries do
|
|
46
|
-
HttpResponse.handle_response(connection.put("#{Connection
|
|
46
|
+
HttpResponse.handle_response(connection.put("#{Connection.project_path}/terms/#{id}/locales/#{locale}/translations/#{id}", body: to_json))
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
|
|
@@ -30,7 +30,7 @@ module WebTranslateIt
|
|
|
30
30
|
def assign_attributes(_params); end
|
|
31
31
|
|
|
32
32
|
def translation_path
|
|
33
|
-
"#{Connection
|
|
33
|
+
"#{Connection.project_path}/#{self.class.parent_resource_path}/#{parent_id}/locales/#{locale}/translations"
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
end
|
|
@@ -188,15 +188,15 @@ module WebTranslateIt
|
|
|
188
188
|
|
|
189
189
|
# Convenience method which returns the URL of the API endpoint for a locale.
|
|
190
190
|
def api_url
|
|
191
|
-
"#{Connection
|
|
191
|
+
"#{Connection.project_path}/files/#{id}/locales/#{locale}"
|
|
192
192
|
end
|
|
193
193
|
|
|
194
194
|
def api_url_for_create
|
|
195
|
-
"#{Connection
|
|
195
|
+
"#{Connection.project_path}/files"
|
|
196
196
|
end
|
|
197
197
|
|
|
198
198
|
def api_url_for_delete
|
|
199
|
-
"#{Connection
|
|
199
|
+
"#{Connection.project_path}/files/#{id}"
|
|
200
200
|
end
|
|
201
201
|
|
|
202
202
|
def local_checksum
|
|
@@ -40,6 +40,14 @@ module WebTranslateIt
|
|
|
40
40
|
request.add_field('Content-Type', 'application/json')
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
FRIENDLY_MESSAGES = {
|
|
44
|
+
'legacy_token_disabled' => 'This project only accepts API tokens now. Create a token in Project settings → API ' \
|
|
45
|
+
'(or Account settings → API tokens) and put it in WTI_PROJECT_API_KEY / `api_key` in .wti.',
|
|
46
|
+
'invalid_token' => 'This API token was revoked or has expired. Create a new token in Project settings → API ' \
|
|
47
|
+
'(or Account settings → API tokens) and put it in WTI_PROJECT_API_KEY / `api_key` in .wti.',
|
|
48
|
+
'insufficient_scope' => 'This is a read-only token; this command needs a read & write token.'
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
43
51
|
def self.raise_on_error!(response)
|
|
44
52
|
code = response.code.to_i
|
|
45
53
|
raise_on_rate_limit!(response) if code == 429
|
|
@@ -54,7 +62,10 @@ module WebTranslateIt
|
|
|
54
62
|
end
|
|
55
63
|
|
|
56
64
|
def self.error_message(response)
|
|
57
|
-
JSON.parse(response.body)
|
|
65
|
+
body = JSON.parse(response.body)
|
|
66
|
+
return response.body.to_s unless body.is_a?(Hash)
|
|
67
|
+
|
|
68
|
+
FRIENDLY_MESSAGES.fetch(body['code'], nil) || body['error'] || response.body.to_s
|
|
58
69
|
rescue StandardError
|
|
59
70
|
response.body.to_s
|
|
60
71
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebTranslateIt
|
|
4
|
+
|
|
5
|
+
# Checks the `project_id` of a configuration file as written. YAML turns `012` or `0x1F`
|
|
6
|
+
# into a different Integer, which can no longer be told from a plain id, so the value is
|
|
7
|
+
# read from the parse tree instead: every spelling of the key (`project_id :`,
|
|
8
|
+
# `"project_id":`) is seen, and the last one wins when the key is repeated, as it does
|
|
9
|
+
# for YAML.safe_load.
|
|
10
|
+
module ProjectIdText
|
|
11
|
+
|
|
12
|
+
NULLS = %w[~ null Null NULL].freeze
|
|
13
|
+
|
|
14
|
+
def self.validate!(yaml_text)
|
|
15
|
+
node = node_for(yaml_text)
|
|
16
|
+
return if node.nil? || null?(node)
|
|
17
|
+
return if node.is_a?(Psych::Nodes::Scalar) && node.value.strip.match?(/\A[1-9]\d*\z/)
|
|
18
|
+
|
|
19
|
+
raw = node.is_a?(Psych::Nodes::Scalar) ? node.value.strip : node.class.name.split('::').last.downcase
|
|
20
|
+
raise ArgumentError, "Error: project_id must be the numeric id of your project (digits only, no leading zero), got `#{raw}`."
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.node_for(yaml_text)
|
|
24
|
+
root = YAML.parse(yaml_text)&.root
|
|
25
|
+
return unless root.is_a?(Psych::Nodes::Mapping)
|
|
26
|
+
|
|
27
|
+
root.children.each_slice(2).reverse_each.find { |key, _| key.is_a?(Psych::Nodes::Scalar) && key.value == 'project_id' }&.last
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.null?(node)
|
|
31
|
+
node.is_a?(Psych::Nodes::Scalar) && !node.quoted && (node.value.strip.empty? || NULLS.include?(node.value.strip))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
end
|
data/lib/web_translate_it.rb
CHANGED
|
@@ -21,6 +21,7 @@ require 'web_translate_it/util/spinner'
|
|
|
21
21
|
require 'web_translate_it/util/http_response'
|
|
22
22
|
require 'web_translate_it/util/concurrency'
|
|
23
23
|
require 'web_translate_it/util/prompt'
|
|
24
|
+
require 'web_translate_it/util/project_id_text'
|
|
24
25
|
require 'web_translate_it/configuration'
|
|
25
26
|
require 'web_translate_it/translation_file'
|
|
26
27
|
require 'web_translate_it/api_resource'
|
data/readme.md
CHANGED
|
@@ -59,7 +59,18 @@ Check `wti --help` for help.
|
|
|
59
59
|
|
|
60
60
|
`proj_pvt_V8skdjsdDDA4` is the API token, which you can find in your project settings.
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
### API tokens
|
|
63
|
+
|
|
64
|
+
`wti` accepts these credentials, passed to `wti init`, in `WTI_PROJECT_API_KEY` or as `api_key` in `.wti`. Legacy project API keys keep working. API tokens (`wti_…`) have two independent properties:
|
|
65
|
+
|
|
66
|
+
* **Kind**: a **personal** token acts as you, within the roles you hold; an **integration** token is not tied to a person, so it keeps working when someone leaves.
|
|
67
|
+
* **Owner**: a **project** token belongs to one project, and `wti` finds the project from the token. An **organization** token belongs to an organization, and can't tell which project you mean, so you also have to give the project's numeric id (see below).
|
|
68
|
+
|
|
69
|
+
A token has the format `wti_<kind><scope>_…`: the first letter after `wti_` is the kind (`p` personal, `i` integration) and the second is the scope (`r` read-only, `w` read & write). So `wti_pr_…` is a personal read-only token and `wti_iw_…` an integration read & write token. A read-only token can pull but not push.
|
|
70
|
+
|
|
71
|
+
`wti_` tokens only work once the project is set to "Both" or "API tokens only" in Project settings → API; for organization tokens, the organization must be set the same way too. Until then the API answers 404. A project set to "API tokens only" refuses the legacy project API keys, and `wti` tells you to create a token in Project settings → API (or Account settings → API tokens).
|
|
72
|
+
|
|
73
|
+
If you’d like to specify another path for your configuration file, you can use `wti init`. This command will ask you to enter your API token and where to save the configuration file (by default it will create a `.wti` in your project root directory).
|
|
63
74
|
|
|
64
75
|
### Keeping the API token out of `.wti`
|
|
65
76
|
|
|
@@ -73,6 +84,21 @@ api_key: <%= ENV['WTI_PROJECT_API_KEY'] %>
|
|
|
73
84
|
|
|
74
85
|
`.wti` is read through ERB, so you can point `api_key` at any environment variable this way. In CI, store the token as a secret and expose it as `WTI_PROJECT_API_KEY`.
|
|
75
86
|
|
|
87
|
+
### Organization tokens: `project_id`
|
|
88
|
+
|
|
89
|
+
With an organization token, add the numeric id of the project to `.wti`:
|
|
90
|
+
|
|
91
|
+
``` yaml
|
|
92
|
+
api_key: <%= ENV['WTI_PROJECT_API_KEY'] %>
|
|
93
|
+
project_id: 12345
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
or set the `WTI_PROJECT_ID` environment variable. Like `WTI_PROJECT_API_KEY`, `WTI_PROJECT_ID` overrides every configuration file in that shell. If `WTI_PROJECT_ID` is set when you run `wti init`, the generated `.wti` includes `project_id`.
|
|
97
|
+
|
|
98
|
+
Write the id as plain digits, without a leading zero: `wti` rejects anything else. If you get a "not found" error with a `wti_` token, either the project doesn't accept API tokens yet or you need `project_id`.
|
|
99
|
+
|
|
100
|
+
Library users: `project_id` is process-wide. The last `WebTranslateIt::Configuration` loaded, or the `WTI_PROJECT_ID` environment variable, wins, so don't load configurations for different projects with organization tokens in the same process.
|
|
101
|
+
|
|
76
102
|
Now you’re all set and you can use the `wti` commands on your project.
|
|
77
103
|
|
|
78
104
|
## Using on multiple projects
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: web_translate_it
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Edouard Briere
|
|
@@ -104,6 +104,7 @@ files:
|
|
|
104
104
|
- lib/web_translate_it/util/concurrency.rb
|
|
105
105
|
- lib/web_translate_it/util/hash_util.rb
|
|
106
106
|
- lib/web_translate_it/util/http_response.rb
|
|
107
|
+
- lib/web_translate_it/util/project_id_text.rb
|
|
107
108
|
- lib/web_translate_it/util/prompt.rb
|
|
108
109
|
- lib/web_translate_it/util/spinner.rb
|
|
109
110
|
- lib/web_translate_it/util/string_util.rb
|