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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d01e00c6188bc51a7520d8a712f988f14125e606fd27a8cd7a8d1576075a433d
4
- data.tar.gz: 5c1e552315ce4837b6505f2af0e1f911f4a625bdfcc3011f432502766e8c21b3
3
+ metadata.gz: 5e4d18f41ea8a29e79e6d789db02d52b1eb92d88504050755a7564872f3b5e59
4
+ data.tar.gz: d615308a0f20887d376b2f2b1556d066463334fe436da6b7aa893704b1b899e8
5
5
  SHA512:
6
- metadata.gz: 6a13defd7a2f234d187d496a4da810b56abd06a98a07458e9110ca81be81aa93388c5d65f7804a5ed7fcacea0d2ddc617d2bc1be66c6baf57d98fa545710bf4f
7
- data.tar.gz: 888436899b2d2f0e00c734f9bbb20a1692f9b4a3d1aa1cb10ce100215bc3b5d362d19176f2c5855baa70ce312adf5ee5415c489a6ee71ba4ebb0a292266120d4
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::PROJECT_PATH}/#{resource_path}"
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::PROJECT_PATH}/#{resource_path}/#{id}")
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::PROJECT_PATH}/#{self.class.resource_path}/#{id}"))
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::PROJECT_PATH}/#{self.class.resource_path}/#{id}/locales/#{locale}/translations"))
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::PROJECT_PATH}/#{self.class.resource_path}/#{id}", body: to_json))
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::PROJECT_PATH}/#{self.class.resource_path}", body: to_json(with_translations: true))
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 Project API Key from #{ENV_API_KEY}."
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(' Project API Key:'), Prompt.ask(' Path to configuration file:', '.wti')]
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 Project API Key and is not ignored by git.")
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 Project API Token from WebTranslateIt.com
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::PROJECT_PATH
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(parse_erb_in_configuration)
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::PROJECT_PATH))
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::PROJECT_PATH}/stats" : "#{Connection::PROJECT_PATH}/stats?file=#{file_id}"
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::PROJECT_PATH}/zip_file"
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::PROJECT_PATH}/locales") { |req| req.set_form_data({'id' => locale_code}, ';') }
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::PROJECT_PATH}/locales/#{locale_code}"))
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::PROJECT_PATH}/terms/#{id}/locales/#{locale}/translations/#{id}", body: to_json))
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::PROJECT_PATH}/#{self.class.parent_resource_path}/#{parent_id}/locales/#{locale}/translations"
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::PROJECT_PATH}/files/#{id}/locales/#{locale}"
191
+ "#{Connection.project_path}/files/#{id}/locales/#{locale}"
192
192
  end
193
193
 
194
194
  def api_url_for_create
195
- "#{Connection::PROJECT_PATH}/files"
195
+ "#{Connection.project_path}/files"
196
196
  end
197
197
 
198
198
  def api_url_for_delete
199
- "#{Connection::PROJECT_PATH}/files/#{id}"
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)['error']
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
@@ -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
- 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 project API token and where to save the configuration file (by default it will create a `.wti` in your project root directory).
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.0
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