deepl-rb 3.9.0 → 3.10.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.
Files changed (237) hide show
  1. checksums.yaml +4 -4
  2. data/.gitlab-ci.yml +70 -52
  3. data/.rubocop.yml +26 -0
  4. data/CHANGELOG.md +429 -18
  5. data/Gemfile +4 -0
  6. data/README.md +981 -223
  7. data/Rakefile +18 -5
  8. data/VERSION +1 -1
  9. data/deepl-rb.gemspec +6 -6
  10. data/lib/deepl/admin_client.rb +183 -0
  11. data/lib/deepl/api.rb +30 -6
  12. data/lib/deepl/app_info.rb +20 -0
  13. data/lib/deepl/client.rb +969 -0
  14. data/lib/deepl/configuration.rb +56 -10
  15. data/lib/deepl/constants/admin_usage_group_by.rb +13 -0
  16. data/lib/deepl/constants/custom_tag_usage_aggregate_by.rb +13 -0
  17. data/lib/deepl/constants/document_status_code.rb +17 -0
  18. data/lib/deepl/constants/language_include.rb +17 -0
  19. data/lib/deepl/constants/language_resource_name.rb +21 -0
  20. data/lib/deepl/constants/tag_handling_version.rb +14 -0
  21. data/lib/deepl/deprecation.rb +42 -0
  22. data/lib/deepl/document_api.rb +41 -22
  23. data/lib/deepl/exceptions/bad_request.rb +1 -3
  24. data/lib/deepl/exceptions/connection_error.rb +37 -0
  25. data/lib/deepl/exceptions/document_not_ready_error.rb +17 -0
  26. data/lib/deepl/exceptions/document_translation_error.rb +17 -1
  27. data/lib/deepl/exceptions/error.rb +13 -1
  28. data/lib/deepl/exceptions/not_found.rb +8 -3
  29. data/lib/deepl/exceptions/not_supported.rb +6 -0
  30. data/lib/deepl/exceptions/request_error.rb +19 -2
  31. data/lib/deepl/exceptions/server_error.rb +7 -1
  32. data/lib/deepl/exceptions/validation_error.rb +14 -0
  33. data/lib/deepl/requests/admin/base.rb +94 -0
  34. data/lib/deepl/requests/admin/create_developer_key.rb +38 -0
  35. data/lib/deepl/requests/admin/custom_tag_usage_analytics.rb +38 -0
  36. data/lib/deepl/requests/admin/list_developer_keys.rb +28 -0
  37. data/lib/deepl/requests/admin/update_developer_key.rb +35 -0
  38. data/lib/deepl/requests/admin/usage_analytics.rb +36 -0
  39. data/lib/deepl/requests/base.rb +229 -59
  40. data/lib/deepl/requests/correct.rb +57 -0
  41. data/lib/deepl/requests/document/download.rb +11 -3
  42. data/lib/deepl/requests/document/get_status.rb +2 -2
  43. data/lib/deepl/requests/document/upload.rb +43 -10
  44. data/lib/deepl/requests/glossary/create.rb +6 -2
  45. data/lib/deepl/requests/glossary/language_pairs.rb +20 -4
  46. data/lib/deepl/requests/language_resources.rb +41 -0
  47. data/lib/deepl/requests/languages.rb +28 -4
  48. data/lib/deepl/requests/languages_for_resource.rb +54 -0
  49. data/lib/deepl/requests/multilingual_glossary/create.rb +37 -0
  50. data/lib/deepl/requests/multilingual_glossary/destroy.rb +36 -0
  51. data/lib/deepl/requests/multilingual_glossary/destroy_dictionary.rb +51 -0
  52. data/lib/deepl/requests/multilingual_glossary/entries.rb +66 -0
  53. data/lib/deepl/requests/multilingual_glossary/find.rb +35 -0
  54. data/lib/deepl/requests/multilingual_glossary/helpers.rb +63 -0
  55. data/lib/deepl/requests/multilingual_glossary/list.rb +39 -0
  56. data/lib/deepl/requests/multilingual_glossary/replace_dictionary.rb +41 -0
  57. data/lib/deepl/requests/multilingual_glossary/update.rb +40 -0
  58. data/lib/deepl/requests/rephrase.rb +5 -9
  59. data/lib/deepl/requests/spoken_terms/create.rb +37 -0
  60. data/lib/deepl/requests/spoken_terms/destroy.rb +36 -0
  61. data/lib/deepl/requests/spoken_terms/destroy_term_list.rb +41 -0
  62. data/lib/deepl/requests/spoken_terms/entries.rb +46 -0
  63. data/lib/deepl/requests/spoken_terms/find.rb +35 -0
  64. data/lib/deepl/requests/spoken_terms/helpers.rb +67 -0
  65. data/lib/deepl/requests/spoken_terms/list.rb +35 -0
  66. data/lib/deepl/requests/spoken_terms/replace_term_list.rb +41 -0
  67. data/lib/deepl/requests/spoken_terms/update.rb +40 -0
  68. data/lib/deepl/requests/style_profile/base.rb +83 -0
  69. data/lib/deepl/requests/style_profile/create.rb +35 -0
  70. data/lib/deepl/requests/style_profile/destroy.rb +34 -0
  71. data/lib/deepl/requests/style_profile/find.rb +33 -0
  72. data/lib/deepl/requests/style_profile/list.rb +43 -0
  73. data/lib/deepl/requests/style_profile/update.rb +35 -0
  74. data/lib/deepl/requests/style_rule/create.rb +1 -1
  75. data/lib/deepl/requests/style_rule/create_custom_instruction.rb +4 -2
  76. data/lib/deepl/requests/style_rule/destroy.rb +4 -2
  77. data/lib/deepl/requests/style_rule/destroy_custom_instruction.rb +5 -3
  78. data/lib/deepl/requests/style_rule/find.rb +4 -2
  79. data/lib/deepl/requests/style_rule/find_custom_instruction.rb +5 -3
  80. data/lib/deepl/requests/style_rule/helpers.rb +35 -0
  81. data/lib/deepl/requests/style_rule/list.rb +1 -1
  82. data/lib/deepl/requests/style_rule/update.rb +4 -2
  83. data/lib/deepl/requests/style_rule/update_configured_rules.rb +4 -2
  84. data/lib/deepl/requests/style_rule/update_custom_instruction.rb +5 -3
  85. data/lib/deepl/requests/translate.rb +52 -20
  86. data/lib/deepl/requests/translation_memory/base.rb +1 -4
  87. data/lib/deepl/requests/translation_memory/create_export.rb +1 -1
  88. data/lib/deepl/requests/translation_memory/create_import.rb +1 -1
  89. data/lib/deepl/requests/translation_memory/destroy.rb +1 -1
  90. data/lib/deepl/requests/translation_memory/download_export.rb +4 -0
  91. data/lib/deepl/requests/translation_memory/find.rb +1 -1
  92. data/lib/deepl/requests/translation_memory/find_job.rb +1 -1
  93. data/lib/deepl/requests/translation_memory/list.rb +2 -1
  94. data/lib/deepl/requests/translation_memory/segments.rb +3 -2
  95. data/lib/deepl/requests/translation_memory/storage_base.rb +40 -8
  96. data/lib/deepl/requests/translation_memory/upload_file.rb +1 -1
  97. data/lib/deepl/requests/usage.rb +28 -1
  98. data/lib/deepl/requests/write_improvements.rb +35 -0
  99. data/lib/deepl/resources/admin_usage_report.rb +73 -0
  100. data/lib/deepl/resources/custom_tag_usage_report.rb +59 -0
  101. data/lib/deepl/resources/developer_key.rb +57 -0
  102. data/lib/deepl/resources/document_handle.rb +24 -25
  103. data/lib/deepl/resources/document_translation_status.rb +23 -4
  104. data/lib/deepl/resources/language.rb +10 -2
  105. data/lib/deepl/resources/language_resource.rb +57 -0
  106. data/lib/deepl/resources/language_support.rb +56 -0
  107. data/lib/deepl/resources/multilingual_glossary.rb +112 -0
  108. data/lib/deepl/resources/spoken_terms.rb +61 -0
  109. data/lib/deepl/resources/style_profile.rb +44 -0
  110. data/lib/deepl/resources/style_rule.rb +3 -0
  111. data/lib/deepl/resources/text.rb +18 -2
  112. data/lib/deepl/resources/translation_memory.rb +8 -2
  113. data/lib/deepl/resources/translation_memory_import.rb +1 -1
  114. data/lib/deepl/resources/translation_memory_job.rb +23 -3
  115. data/lib/deepl/resources/translation_memory_segments.rb +8 -6
  116. data/lib/deepl/resources/usage.rb +93 -4
  117. data/lib/deepl/style_rule_api.rb +11 -0
  118. data/lib/deepl/translation_memory_api.rb +104 -24
  119. data/lib/deepl/utils/client_builder.rb +54 -0
  120. data/lib/deepl/utils/document_poller.rb +70 -0
  121. data/lib/deepl/utils/exception_builder.rb +3 -2
  122. data/lib/deepl/utils/http_client_builder.rb +79 -0
  123. data/lib/deepl/utils/number_parser.rb +34 -0
  124. data/lib/deepl/utils/term_validator.rb +33 -0
  125. data/lib/deepl/utils/time_parser.rb +14 -0
  126. data/lib/deepl.rb +166 -41
  127. data/lib/http_client_options.rb +21 -1
  128. data/lib/version.rb +1 -1
  129. data/spec/fixtures.rb +33 -0
  130. data/spec/offline/admin_client_spec.rb +551 -0
  131. data/spec/offline/client_spec.rb +273 -0
  132. data/spec/{api → offline}/configuration_spec.rb +45 -3
  133. data/spec/{constants → offline}/constants_spec.rb +23 -1
  134. data/spec/offline/correct_text_spec.rb +50 -0
  135. data/spec/offline/deprecation_spec.rb +32 -0
  136. data/spec/offline/errors_spec.rb +92 -0
  137. data/spec/offline/glossaries_spec.rb +98 -0
  138. data/spec/offline/http_client_spec.rb +319 -0
  139. data/spec/offline/languages_spec.rb +155 -0
  140. data/spec/offline/module_methods_spec.rb +13 -0
  141. data/spec/offline/multilingual_glossaries_spec.rb +358 -0
  142. data/spec/offline/offline_helper.rb +71 -0
  143. data/spec/offline/rephrase_text_spec.rb +73 -0
  144. data/spec/offline/spoken_terms_spec.rb +215 -0
  145. data/spec/offline/style_profiles_spec.rb +266 -0
  146. data/spec/offline/style_rules_spec.rb +318 -0
  147. data/spec/offline/translate_document_spec.rb +606 -0
  148. data/spec/offline/translate_text_spec.rb +286 -0
  149. data/spec/offline/translation_memories_client_spec.rb +256 -0
  150. data/spec/offline/translation_memories_spec.rb +463 -0
  151. data/spec/offline/usage_spec.rb +151 -0
  152. data/spec/real_api/admin_client_spec.rb +77 -0
  153. data/spec/real_api/correct_text_spec.rb +20 -0
  154. data/spec/real_api/glossaries_spec.rb +28 -0
  155. data/spec/real_api/languages_spec.rb +27 -0
  156. data/spec/real_api/multilingual_glossaries_spec.rb +53 -0
  157. data/spec/real_api/proxy_spec.rb +41 -0
  158. data/spec/real_api/real_api_helper.rb +65 -0
  159. data/spec/real_api/rephrase_text_spec.rb +16 -0
  160. data/spec/real_api/spoken_terms_spec.rb +56 -0
  161. data/spec/real_api/style_profiles_spec.rb +46 -0
  162. data/spec/real_api/style_rules_spec.rb +46 -0
  163. data/spec/real_api/translate_document_spec.rb +18 -0
  164. data/spec/real_api/translate_text_spec.rb +16 -0
  165. data/spec/real_api/translation_memories_spec.rb +56 -0
  166. data/spec/real_api/usage_spec.rb +20 -0
  167. data/spec/spec_helper.rb +1 -50
  168. metadata +116 -76
  169. data/.circleci/config.yml +0 -27
  170. data/.github/workflows/add_issues_to_kanban.yml +0 -16
  171. data/spec/api/api_spec.rb +0 -20
  172. data/spec/api/deepl_spec.rb +0 -262
  173. data/spec/integration_tests/document_api_spec.rb +0 -211
  174. data/spec/integration_tests/document_error_paths_spec.rb +0 -33
  175. data/spec/integration_tests/glossary_api_spec.rb +0 -114
  176. data/spec/integration_tests/glossary_error_paths_spec.rb +0 -107
  177. data/spec/integration_tests/integration_test_utils.rb +0 -178
  178. data/spec/integration_tests/languages_api_spec.rb +0 -54
  179. data/spec/integration_tests/languages_error_paths_spec.rb +0 -25
  180. data/spec/integration_tests/rephrase_api_spec.rb +0 -90
  181. data/spec/integration_tests/rephrase_error_paths_spec.rb +0 -53
  182. data/spec/integration_tests/smoke_test_spec.rb +0 -24
  183. data/spec/integration_tests/style_rule_api_spec.rb +0 -117
  184. data/spec/integration_tests/style_rule_error_paths_spec.rb +0 -45
  185. data/spec/integration_tests/translate_api_spec.rb +0 -117
  186. data/spec/integration_tests/translate_error_paths_spec.rb +0 -48
  187. data/spec/integration_tests/translation_memory_api_spec.rb +0 -244
  188. data/spec/integration_tests/translation_memory_error_paths_spec.rb +0 -87
  189. data/spec/integration_tests/usage_api_spec.rb +0 -29
  190. data/spec/integration_tests/usage_error_paths_spec.rb +0 -18
  191. data/spec/requests/document/upload_spec.rb +0 -130
  192. data/spec/requests/extra_body_parameters_types_spec.rb +0 -82
  193. data/spec/requests/glossary/create_spec.rb +0 -44
  194. data/spec/requests/glossary/destroy_spec.rb +0 -27
  195. data/spec/requests/glossary/entries_spec.rb +0 -27
  196. data/spec/requests/glossary/find_spec.rb +0 -28
  197. data/spec/requests/glossary/language_pairs_spec.rb +0 -27
  198. data/spec/requests/glossary/list_spec.rb +0 -27
  199. data/spec/requests/languages_spec.rb +0 -27
  200. data/spec/requests/rephrase_spec.rb +0 -46
  201. data/spec/requests/style_rule/create_custom_instruction_spec.rb +0 -30
  202. data/spec/requests/style_rule/create_spec.rb +0 -29
  203. data/spec/requests/style_rule/destroy_custom_instruction_spec.rb +0 -28
  204. data/spec/requests/style_rule/destroy_spec.rb +0 -27
  205. data/spec/requests/style_rule/find_custom_instruction_spec.rb +0 -29
  206. data/spec/requests/style_rule/find_spec.rb +0 -28
  207. data/spec/requests/style_rule/list_spec.rb +0 -27
  208. data/spec/requests/style_rule/update_configured_rules_spec.rb +0 -31
  209. data/spec/requests/style_rule/update_custom_instruction_spec.rb +0 -32
  210. data/spec/requests/style_rule/update_spec.rb +0 -29
  211. data/spec/requests/translate_spec.rb +0 -338
  212. data/spec/requests/translation_memory/create_export_spec.rb +0 -37
  213. data/spec/requests/translation_memory/create_import_spec.rb +0 -61
  214. data/spec/requests/translation_memory/destroy_spec.rb +0 -36
  215. data/spec/requests/translation_memory/download_export_spec.rb +0 -54
  216. data/spec/requests/translation_memory/find_job_spec.rb +0 -37
  217. data/spec/requests/translation_memory/find_spec.rb +0 -36
  218. data/spec/requests/translation_memory/list_spec.rb +0 -27
  219. data/spec/requests/translation_memory/segments_spec.rb +0 -58
  220. data/spec/requests/translation_memory/upload_file_spec.rb +0 -55
  221. data/spec/requests/usage_spec.rb +0 -27
  222. data/spec/resources/custom_instruction_spec.rb +0 -32
  223. data/spec/resources/glossary_spec.rb +0 -38
  224. data/spec/resources/language_pair_spec.rb +0 -23
  225. data/spec/resources/language_spec.rb +0 -45
  226. data/spec/resources/style_rule_spec.rb +0 -68
  227. data/spec/resources/text_spec.rb +0 -23
  228. data/spec/resources/translation_memory_export_spec.rb +0 -38
  229. data/spec/resources/translation_memory_import_spec.rb +0 -30
  230. data/spec/resources/translation_memory_job_spec.rb +0 -109
  231. data/spec/resources/translation_memory_segments_spec.rb +0 -78
  232. data/spec/resources/translation_memory_spec.rb +0 -52
  233. data/spec/resources/usage_spec.rb +0 -35
  234. data/spec/support/live_mock_server.rb +0 -12
  235. data/spec/support/managed_glossary.rb +0 -65
  236. data/spec/support/managed_style_rule.rb +0 -17
  237. data/spec/support/managed_translation_memory.rb +0 -55
@@ -9,8 +9,11 @@ module DeepL
9
9
  ##
10
10
  # Common behaviour of the requests to a pre-signed storage URL handed out by the API, such
11
11
  # as a translation memory upload or download URL. Those URLs point outside of the DeepL
12
- # API, so they are requested over their own connection and the DeepL `Authorization` header
13
- # is deliberately not sent.
12
+ # API, so they are requested over their own connection and send only the User-Agent: neither
13
+ # the DeepL `Authorization` header nor other caller headers. That connection uses the same
14
+ # proxy, SSL and timeout options as the API connection. A non-2xx response raises the API
15
+ # error class for its status (e.g. DeepL::Exceptions::AuthorizationFailed for 401/403) in the
16
+ # module-level API and a plain DeepL::Exceptions::Error in DeepL::Client.
14
17
 
15
18
  class StorageBase < DeepL::Requests::Base
16
19
  attr_reader :storage_url
@@ -27,19 +30,48 @@ module DeepL
27
30
  private
28
31
 
29
32
  def http_client
30
- @http_client ||= begin
31
- client = Net::HTTP.new(uri.host, uri.port)
32
- client.use_ssl = uri.scheme == 'https'
33
- client
34
- end
33
+ @http_client ||= if api.http_client_options
34
+ Utils::HTTPClientBuilder.build(storage_url, api.http_client_options)
35
+ else
36
+ default_http_client
37
+ end
38
+ end
39
+
40
+ def http_client_options_of(_client)
41
+ api.http_client_options
42
+ end
43
+
44
+ def default_http_client
45
+ client = Net::HTTP.new(uri.host, uri.port)
46
+ client.use_ssl = uri.scheme == 'https'
47
+ Utils::HTTPClientBuilder.disable_lenient_defaults(client)
35
48
  end
36
49
 
37
50
  def uri
38
51
  @uri ||= URI(storage_url)
39
52
  end
40
53
 
54
+ # Storage responses are not DeepL API responses, so their statuses carry none of the API's
55
+ # meanings: an expired URL replies 403, which is no authorization failure. DeepL::Client
56
+ # raises a plain error; until the next major version, the module-level API raises the API
57
+ # error classes it has released.
58
+ def validate_response!(response, request = nil)
59
+ return if response.is_a?(Net::HTTPSuccess)
60
+ raise Utils::ExceptionBuilder.new(response, request).build unless api.client?
61
+
62
+ status = response.code.to_i
63
+ raise Exceptions::Error.new(
64
+ "Request to the storage URL failed with HTTP status #{status}",
65
+ http_status_code: status, should_retry: status == 429 || (500..599).cover?(status)
66
+ )
67
+ end
68
+
69
+ # Only the User-Agent: the caller's from the configured +headers+, else the library's.
41
70
  def headers
42
- { 'User-Agent' => api.configuration.user_agent }.merge(@additional_headers)
71
+ caller_agent = (api.configuration.headers || {}).find do |name, _|
72
+ name.to_s.casecmp?('user-agent')
73
+ end
74
+ { 'User-Agent' => caller_agent ? caller_agent.last : api.configuration.user_agent }
43
75
  end
44
76
  end
45
77
  end
@@ -22,7 +22,7 @@ module DeepL
22
22
  end
23
23
 
24
24
  def details
25
- "HTTP Headers: #{headers}\nPayload #{file_content.bytesize} byte(s)"
25
+ "HTTP Headers: #{loggable_headers}\nPayload #{file_content.bytesize} byte(s)"
26
26
  end
27
27
 
28
28
  def to_s
@@ -22,7 +22,34 @@ module DeepL
22
22
 
23
23
  def build_usage(request, response)
24
24
  data = JSON.parse(response.body)
25
- Resources::Usage.new(data['character_count'], data['character_limit'], request, response)
25
+ Resources::Usage.new(data['character_count'], data['character_limit'], request, response,
26
+ **usage_fields(data))
27
+ end
28
+
29
+ def usage_fields(data)
30
+ details = Resources::Usage::DETAIL_TYPES.to_h { |type| [type, detail(data, type)] }
31
+ details.merge(products: products(data['products']),
32
+ start_time: optional_time(data['start_time']),
33
+ end_time: optional_time(data['end_time']))
34
+ end
35
+
36
+ def detail(data, type)
37
+ count = data["#{type}_count"]
38
+ limit = data["#{type}_limit"]
39
+ Resources::Usage::Detail.new(count, limit) unless count.nil? || limit.nil?
40
+ end
41
+
42
+ def products(entries)
43
+ return nil unless entries.is_a?(Array)
44
+
45
+ entries.grep(Hash).map { |product| Resources::UsageProduct.new(product) }
46
+ end
47
+
48
+ # An unexpected billing-period format must not fail the whole usage response.
49
+ def optional_time(value)
50
+ Utils::TimeParser.parse_optional_time(value) if value.is_a?(String)
51
+ rescue ArgumentError
52
+ nil
26
53
  end
27
54
 
28
55
  def path
@@ -0,0 +1,35 @@
1
+ # Copyright 2026 DeepL SE (https://www.deepl.com)
2
+ # Use of this source code is governed by an MIT
3
+ # license that can be found in the LICENSE.md file.
4
+ # frozen_string_literal: true
5
+
6
+ module DeepL
7
+ module Requests
8
+ # @api private
9
+ # Builds the results of the write endpoints (rephrase, correct) from their response.
10
+ module WriteImprovements
11
+ private
12
+
13
+ # Returns one DeepL::Resources::Text per improvement. Raises DeepL::Exceptions::Error if
14
+ # the response has fewer improvements than +input_count+ input texts.
15
+ def build_improvements(input_count, request, response)
16
+ improvements = JSON.parse(response.body)['improvements']
17
+ validate_improvement_count!(improvements, input_count)
18
+ improvements.map do |improvement|
19
+ target_language = improvement['target_language']
20
+ Resources::Text.new(improvement['text'], improvement['detected_source_language'], nil,
21
+ request, response, nil, nil,
22
+ target_language.is_a?(String) ? target_language : nil)
23
+ end
24
+ end
25
+
26
+ def validate_improvement_count!(improvements, input_count)
27
+ return if improvements.is_a?(Array) && improvements.size >= input_count
28
+
29
+ count = improvements.is_a?(Array) ? improvements.size : 'no'
30
+ raise Exceptions::Error, 'Unexpected response from the DeepL API: expected ' \
31
+ "#{input_count} improvements, got #{count}"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,73 @@
1
+ # Copyright 2026 DeepL SE (https://www.deepl.com)
2
+ # Use of this source code is governed by an MIT
3
+ # license that can be found in the LICENSE.md file.
4
+ # frozen_string_literal: true
5
+
6
+ module DeepL
7
+ module Resources
8
+ ##
9
+ # Usage breakdown of an admin usage report. +speech_to_speech_minutes+ is +nil+ when the
10
+ # response does not contain it.
11
+ class AdminUsageBreakdown
12
+ attr_reader :total_characters, :text_translation_characters,
13
+ :document_translation_characters, :text_improvement_characters,
14
+ :speech_to_text_minutes, :speech_to_speech_minutes
15
+
16
+ def initialize(usage)
17
+ usage = {} unless usage.is_a?(Hash)
18
+ @total_characters = Utils::NumberParser.count(usage['total_characters'])
19
+ @text_translation_characters = Utils::NumberParser.count(usage['text_translation_characters'])
20
+ @document_translation_characters =
21
+ Utils::NumberParser.count(usage['document_translation_characters'])
22
+ @text_improvement_characters = Utils::NumberParser.count(usage['text_improvement_characters'])
23
+ @speech_to_text_minutes = Utils::NumberParser.minutes(usage['speech_to_text_minutes'])
24
+ @speech_to_speech_minutes =
25
+ Utils::NumberParser.optional_minutes(usage['speech_to_speech_minutes'])
26
+ end
27
+ end
28
+
29
+ ##
30
+ # Usage of a single API key. +usage_date+ is only set when grouped by key and day;
31
+ # +api_key_label+ is +nil+ when the response does not contain it.
32
+ class KeyUsage
33
+ attr_reader :api_key, :api_key_label, :usage, :usage_date
34
+
35
+ def initialize(key_usage)
36
+ @api_key = key_usage['api_key'].is_a?(String) ? key_usage['api_key'] : ''
37
+ @api_key_label = key_usage['api_key_label'].is_a?(String) ? key_usage['api_key_label'] : nil
38
+ @usage = AdminUsageBreakdown.new(key_usage['usage'])
39
+ @usage_date = Utils::TimeParser.parse_optional_utc_time(key_usage['usage_date'])
40
+ end
41
+ end
42
+
43
+ ##
44
+ # Admin usage analytics report. +key_usages+ is only filled when grouped by key and
45
+ # +key_and_day_usages+ only when grouped by key and day; otherwise they are empty.
46
+ # +total_usage+, +start_date+ and +end_date+ are +nil+ when the response does not contain them.
47
+ class AdminUsageReport < Base
48
+ attr_reader :total_usage, :start_date, :end_date, :group_by, :key_usages,
49
+ :key_and_day_usages
50
+
51
+ def initialize(data, *args) # rubocop:disable Metrics/AbcSize
52
+ super(*args)
53
+ report = data.is_a?(Hash) ? data['usage_report'] : nil
54
+ report = {} unless report.is_a?(Hash)
55
+ total_usage = report['total_usage']
56
+ @total_usage = total_usage.is_a?(Hash) ? AdminUsageBreakdown.new(total_usage) : nil
57
+ @start_date = Utils::TimeParser.parse_optional_utc_time(report['start_date'])
58
+ @end_date = Utils::TimeParser.parse_optional_utc_time(report['end_date'])
59
+ @group_by = report['group_by'].is_a?(String) ? report['group_by'] : nil
60
+ @key_usages = parse_key_usages(report['key_usages'])
61
+ @key_and_day_usages = parse_key_usages(report['key_and_day_usages'])
62
+ end
63
+
64
+ private
65
+
66
+ def parse_key_usages(key_usages)
67
+ return [] unless key_usages.is_a?(Array)
68
+
69
+ key_usages.grep(Hash).map { |usage| KeyUsage.new(usage) }
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,59 @@
1
+ # Copyright 2026 DeepL SE (https://www.deepl.com)
2
+ # Use of this source code is governed by an MIT
3
+ # license that can be found in the LICENSE.md file.
4
+ # frozen_string_literal: true
5
+
6
+ module DeepL
7
+ module Resources
8
+ ##
9
+ # Usage breakdown of a custom tag. +speech_to_speech_minutes+ is +nil+ when the response
10
+ # does not contain it.
11
+ class CustomTagUsageBreakdown
12
+ attr_reader :total_characters, :text_translation_characters,
13
+ :text_improvement_characters, :speech_to_text_minutes,
14
+ :speech_to_speech_minutes
15
+
16
+ def initialize(breakdown)
17
+ breakdown = {} unless breakdown.is_a?(Hash)
18
+ @total_characters = Utils::NumberParser.count(breakdown['total_characters'])
19
+ @text_translation_characters =
20
+ Utils::NumberParser.count(breakdown['text_translation_characters'])
21
+ @text_improvement_characters =
22
+ Utils::NumberParser.count(breakdown['text_improvement_characters'])
23
+ @speech_to_text_minutes = Utils::NumberParser.minutes(breakdown['speech_to_text_minutes'])
24
+ @speech_to_speech_minutes =
25
+ Utils::NumberParser.optional_minutes(breakdown['speech_to_speech_minutes'])
26
+ end
27
+ end
28
+
29
+ ##
30
+ # Usage of a single custom tag. +usage_date+ is only set when aggregated by day.
31
+ class CustomTagUsage
32
+ attr_reader :custom_tag, :breakdown, :usage_date
33
+
34
+ def initialize(usage)
35
+ @custom_tag = usage['custom_tag'].is_a?(String) ? usage['custom_tag'] : ''
36
+ @breakdown = CustomTagUsageBreakdown.new(usage['breakdown'])
37
+ @usage_date = Utils::TimeParser.parse_optional_utc_time(usage['usage_date'])
38
+ end
39
+ end
40
+
41
+ ##
42
+ # Custom tag usage analytics report. +next_page+ is +nil+ on the last page.
43
+ class CustomTagUsageReport < Base
44
+ attr_reader :aggregate_by, :start_date, :end_date, :next_page, :usage
45
+
46
+ def initialize(data, *args) # rubocop:disable Metrics/AbcSize
47
+ super(*args)
48
+ report = data.is_a?(Hash) ? data['custom_tag_usage_report'] : nil
49
+ report = {} unless report.is_a?(Hash)
50
+ @aggregate_by = report['aggregate_by'].is_a?(String) ? report['aggregate_by'] : nil
51
+ @start_date = Utils::TimeParser.parse_optional_utc_time(report['start_date'])
52
+ @end_date = Utils::TimeParser.parse_optional_utc_time(report['end_date'])
53
+ @next_page = report['next_page'].is_a?(Integer) ? report['next_page'] : nil
54
+ usages = report['usage'].is_a?(Array) ? report['usage'] : []
55
+ @usage = usages.grep(Hash).map { |item| CustomTagUsage.new(item) }
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,57 @@
1
+ # Copyright 2026 DeepL SE (https://www.deepl.com)
2
+ # Use of this source code is governed by an MIT
3
+ # license that can be found in the LICENSE.md file.
4
+ # frozen_string_literal: true
5
+
6
+ module DeepL
7
+ module Resources
8
+ ##
9
+ # Usage limits of a developer key. A +nil+ limit means unlimited.
10
+ class DeveloperKeyUsageLimits
11
+ attr_reader :characters, :speech_to_text_milliseconds, :speech_to_speech_milliseconds
12
+
13
+ def initialize(usage_limits)
14
+ usage_limits = {} unless usage_limits.is_a?(Hash)
15
+ @characters = Utils::NumberParser.optional_count(usage_limits['characters'])
16
+ @speech_to_text_milliseconds =
17
+ Utils::NumberParser.optional_count(usage_limits['speech_to_text_milliseconds'])
18
+ @speech_to_speech_milliseconds =
19
+ Utils::NumberParser.optional_count(usage_limits['speech_to_speech_milliseconds'])
20
+ end
21
+ end
22
+
23
+ ##
24
+ # A developer key. +label+ and +usage_limits+ are +nil+ when the response does not contain
25
+ # them.
26
+ class DeveloperKey < Base
27
+ attr_reader :key_id, :label, :creation_time, :deactivated_time, :is_deactivated,
28
+ :usage_limits
29
+
30
+ def initialize(developer_key, *args)
31
+ super(*args)
32
+ developer_key = {} unless developer_key.is_a?(Hash)
33
+ @key_id = developer_key['key_id'].is_a?(String) ? developer_key['key_id'] : ''
34
+ @label = developer_key['label'].is_a?(String) ? developer_key['label'] : nil
35
+ @creation_time = Utils::TimeParser.parse_optional_utc_time(developer_key['creation_time'])
36
+ @deactivated_time =
37
+ Utils::TimeParser.parse_optional_utc_time(developer_key['deactivated_time'])
38
+ @is_deactivated = developer_key['is_deactivated'] == true
39
+ @usage_limits = parse_usage_limits(developer_key['usage_limits'])
40
+ end
41
+
42
+ def deactivated?
43
+ is_deactivated
44
+ end
45
+
46
+ def to_s
47
+ label.nil? ? key_id : "#{label} (#{key_id})"
48
+ end
49
+
50
+ private
51
+
52
+ def parse_usage_limits(usage_limits)
53
+ usage_limits.is_a?(Hash) ? DeveloperKeyUsageLimits.new(usage_limits) : nil
54
+ end
55
+ end
56
+ end
57
+ end
@@ -8,49 +8,48 @@ module DeepL
8
8
  class DocumentHandle < Base
9
9
  attr_reader :document_id, :document_key
10
10
 
11
- def initialize(document_id, document_key, *args)
12
- super(*args)
11
+ # +api+ is the connection the handle was created with; status queries go through it.
12
+ # A handle for a stored ID and key needs only those two: +DocumentHandle.new(id, key)+.
13
+ def initialize(document_id, document_key, request = nil, response = nil, api: nil)
14
+ super(request, response)
13
15
 
14
16
  @document_id = document_id
15
17
  @document_key = document_key
18
+ @api = api
16
19
  end
17
20
 
18
21
  def to_s
19
22
  "DocumentHandle: ID: #{document_id} - Key: #{document_key}"
20
23
  end
21
24
 
25
+ # Leaves out the connection, which holds the auth key.
26
+ def inspect
27
+ "#<#{self.class.name} document_id=#{document_id.inspect}>"
28
+ end
29
+
22
30
  ##
23
- # For this DocumentHandle, waits until the document translation is finished and returns the
24
- # final status of the document.
31
+ # For this DocumentHandle, waits until the document translation is finished (done or
32
+ # failed) and returns the final status of the document. Stops early only if the
33
+ # deprecated +max_doc_status_queries+ is configured.
25
34
  #
35
+ # @deprecated Use DeepL::Client#translate_document_wait_until_done instead.
26
36
  # @return [DeepL::Resources::DocumentTranslationStatus] Final status of the document
27
37
  # translation.
28
-
29
38
  def wait_until_document_translation_finished
30
- doc_status = nil
31
- max_tries = max_doc_status_queries
32
- num_tries = 0
33
- loop do
34
- num_tries += 1
35
- sleep(calculate_waiting_time(doc_status)) unless doc_status.nil?
36
- doc_status = DeepL.document.get_status(self)
37
- break if doc_status.finished? || num_tries > max_tries
38
- end
39
- doc_status
39
+ Deprecation.warn_once('DocumentHandle#wait_until_document_translation_finished',
40
+ '[DEPRECATION] DeepL::Resources::DocumentHandle#' \
41
+ 'wait_until_document_translation_finished is deprecated, use ' \
42
+ 'DeepL::Client#translate_document_wait_until_done instead.')
43
+ Utils::DocumentPoller.wait_until_finished(
44
+ api, self, max_queries: api.configuration.max_doc_status_queries
45
+ )
40
46
  end
41
47
 
42
48
  private
43
49
 
44
- def calculate_waiting_time(_resp)
45
- # ignore _resp.seconds_remaining for now, while it is unreliable
46
- 5
47
- end
48
-
49
- def max_doc_status_queries
50
- configured_value = DeepL.configuration.max_doc_status_queries
51
- return configured_value unless configured_value.nil?
52
-
53
- 10
50
+ # Handles created without a connection fall back to the deprecated module configuration.
51
+ def api
52
+ @api || DeepL.send(:module_api)
54
53
  end
55
54
  end
56
55
  end
@@ -23,13 +23,24 @@ module DeepL
23
23
  "- Error message: #{error_message}"
24
24
  end
25
25
 
26
+ ##
27
+ # Checks if the translation is done, so the document can be downloaded.
28
+ #
29
+ # @return [Boolean] +true+ if the status is +done+.
30
+ def done?
31
+ status == Constants::DocumentStatusCode::DONE
32
+ end
33
+
26
34
  ##
27
35
  # Checks if the translation finished successfully
28
36
  #
37
+ # @deprecated Use #done? instead.
29
38
  # @return [true] if so
30
-
31
39
  def successful?
32
- status == 'done'
40
+ Deprecation.warn_once('DeepL::Resources::DocumentTranslationStatus#successful?',
41
+ '[DEPRECATION] DeepL::Resources::DocumentTranslationStatus#' \
42
+ 'successful? is deprecated, use #done? instead.')
43
+ done?
33
44
  end
34
45
 
35
46
  ##
@@ -38,7 +49,15 @@ module DeepL
38
49
  # @return [true] if so
39
50
 
40
51
  def error?
41
- status == 'error'
52
+ status == Constants::DocumentStatusCode::ERROR
53
+ end
54
+
55
+ ##
56
+ # Checks that the translation has not failed. Also true while it is queued or translating.
57
+ #
58
+ # @return [Boolean] +true+ unless the status is +error+.
59
+ def ok?
60
+ !error?
42
61
  end
43
62
 
44
63
  ##
@@ -47,7 +66,7 @@ module DeepL
47
66
  #
48
67
  # @return [true] if so
49
68
  def finished?
50
- successful? || error?
69
+ done? || error?
51
70
  end
52
71
  end
53
72
  end
@@ -8,20 +8,28 @@ module DeepL
8
8
  class Language < Base
9
9
  attr_reader :code, :name
10
10
 
11
- def initialize(code, name, supports_formality, *args)
11
+ # +client:+ is true for a language returned by DeepL::Client. @api private
12
+ def initialize(code, name, supports_formality, *args, client: false)
12
13
  super(*args)
13
14
 
14
15
  @code = code
15
16
  @name = name
16
17
  @supports_formality = supports_formality
18
+ @client = client
17
19
  end
18
20
 
19
21
  def to_s
20
22
  "#{code} - #{name}"
21
23
  end
22
24
 
25
+ # Whether the target language supports the +formality+ option.
26
+ #
27
+ # @return [Boolean, nil] +true+ or +false+ for a target language; for a source language
28
+ # +nil+ if it came from DeepL::Client.
29
+ # @raise [DeepL::Exceptions::NotSupported] For a source language from the module-level
30
+ # +DeepL.languages+, until the next major version.
23
31
  def supports_formality?
24
- return @supports_formality unless @supports_formality.nil?
32
+ return @supports_formality unless @supports_formality.nil? && !@client
25
33
 
26
34
  raise Exceptions::NotSupported, 'Support formality is only available on target languages'
27
35
  end
@@ -0,0 +1,57 @@
1
+ # Copyright 2026 DeepL SE (https://www.deepl.com)
2
+ # Use of this source code is governed by an MIT
3
+ # license that can be found in the LICENSE file.
4
+ # frozen_string_literal: true
5
+
6
+ module DeepL
7
+ module Resources
8
+ # A feature supported by a DeepL API resource, as returned by `DeepL::Client#get_language_resources`.
9
+ class ResourceFeature
10
+ attr_reader :name
11
+
12
+ def initialize(feature)
13
+ @name = feature['name']
14
+ @needs_source_support = feature['needs_source_support'] ? true : false
15
+ @needs_target_support = feature['needs_target_support'] ? true : false
16
+ end
17
+
18
+ def to_s
19
+ name
20
+ end
21
+
22
+ # True if the source language must support this feature for it to be available.
23
+ def needs_source_support?
24
+ @needs_source_support
25
+ end
26
+
27
+ # True if the target language must support this feature for it to be available.
28
+ def needs_target_support?
29
+ @needs_target_support
30
+ end
31
+ end
32
+
33
+ # A DeepL API resource and the features it supports, as returned by `DeepL::Client#get_language_resources`.
34
+ class LanguageResource < Base
35
+ attr_reader :name, :features
36
+
37
+ def initialize(resource, *args)
38
+ super(*args)
39
+
40
+ @name = resource['name']
41
+ @features = (resource['features'] || []).to_h do |feature|
42
+ resource_feature = ResourceFeature.new(feature)
43
+ [resource_feature.name, resource_feature]
44
+ end
45
+ end
46
+
47
+ def to_s
48
+ name
49
+ end
50
+
51
+ # Returns true if this resource exposes the given feature (e.g. 'glossary').
52
+ def supports?(feature)
53
+ features.key?(feature.to_s)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,56 @@
1
+ # Copyright 2026 DeepL SE (https://www.deepl.com)
2
+ # Use of this source code is governed by an MIT
3
+ # license that can be found in the LICENSE file.
4
+ # frozen_string_literal: true
5
+
6
+ module DeepL
7
+ module Resources
8
+ # Support level of a single feature of a language for a resource.
9
+ class LanguageFeature
10
+ attr_reader :status
11
+
12
+ def initialize(status)
13
+ @status = status
14
+ end
15
+
16
+ def to_s
17
+ status.to_s
18
+ end
19
+ end
20
+
21
+ # A language supported by a DeepL API resource, as returned by `DeepL::Client#get_languages_for_resource`.
22
+ class LanguageSupport < Base
23
+ attr_reader :code, :name, :status, :features
24
+
25
+ def initialize(language, *args)
26
+ super(*args)
27
+
28
+ @code = language['lang']
29
+ @name = language['name']
30
+ @status = language['status']
31
+ @usable_as_source = language['usable_as_source'] ? true : false
32
+ @usable_as_target = language['usable_as_target'] ? true : false
33
+ @features = (language['features'] || {}).to_h.transform_values do |feature|
34
+ LanguageFeature.new(feature.to_h['status'])
35
+ end
36
+ end
37
+
38
+ def to_s
39
+ "#{name} (#{code})"
40
+ end
41
+
42
+ def usable_as_source?
43
+ @usable_as_source
44
+ end
45
+
46
+ def usable_as_target?
47
+ @usable_as_target
48
+ end
49
+
50
+ # Returns true if this language supports the given feature (e.g. 'formality').
51
+ def supports?(feature)
52
+ features.key?(feature.to_s)
53
+ end
54
+ end
55
+ end
56
+ end