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
@@ -4,6 +4,7 @@
4
4
  # frozen_string_literal: true
5
5
 
6
6
  module DeepL
7
+ # @deprecated Use the translation memory methods of DeepL::Client instead.
7
8
  class TranslationMemoryApi # rubocop:disable Metrics/ClassLength
8
9
  # Time to wait between two status queries of an import or export job.
9
10
  JOB_POLLING_INTERVAL_SECONDS = 5
@@ -19,6 +20,8 @@ module DeepL
19
20
  # @param [Hash] options Additional options for the request. Supports `page` (page number for
20
21
  # pagination, 0-indexed) and `page_size` (number of items per page).
21
22
  # @return [Array<DeepL::Resources::TranslationMemory>] The translation memories of the page.
23
+ #
24
+ # @deprecated Use DeepL::Client#list_translation_memories instead.
22
25
 
23
26
  def list(options = {})
24
27
  DeepL::Requests::TranslationMemory::List.new(@api, options).request
@@ -31,6 +34,8 @@ module DeepL
31
34
  # ID or object.
32
35
  # @param [Hash] options Additional options for the request.
33
36
  # @return [DeepL::Resources::TranslationMemory] The requested translation memory.
37
+ #
38
+ # @deprecated Use DeepL::Client#get_translation_memory instead.
34
39
 
35
40
  def find(translation_memory, options = {})
36
41
  DeepL::Requests::TranslationMemory::Find.new(
@@ -52,6 +57,8 @@ module DeepL
52
57
  # `filter_case_sensitive` (whether the filter is case-sensitive,
53
58
  # defaults to false).
54
59
  # @return [DeepL::Resources::TranslationMemorySegments] The requested page of segments.
60
+ #
61
+ # @deprecated Use DeepL::Client#list_translation_memory_segments instead.
55
62
 
56
63
  def segments(translation_memory, options = {})
57
64
  DeepL::Requests::TranslationMemory::Segments.new(
@@ -66,6 +73,8 @@ module DeepL
66
73
  # ID or object.
67
74
  # @param [Hash] options Additional options for the request.
68
75
  # @return [String] The ID of the deleted translation memory.
76
+ #
77
+ # @deprecated Use DeepL::Client#delete_translation_memory instead.
69
78
 
70
79
  def destroy(translation_memory, options = {})
71
80
  DeepL::Requests::TranslationMemory::Destroy.new(
@@ -83,14 +92,19 @@ module DeepL
83
92
  # @param [String, nil] content_type MIME type of the file, defaults to "application/xml".
84
93
  # @param [String, nil] display_name Name of the resulting translation memory, defaults to the
85
94
  # file name.
95
+ # @param [Hash, nil] extra_body_parameters Fields added to the request body last; they
96
+ # replace fields of the same name.
86
97
  # @param [Hash] additional_headers Additional HTTP headers for the request.
87
98
  # @return [DeepL::Resources::TranslationMemoryImport] The job ID and the upload URL.
99
+ #
100
+ # @deprecated Use DeepL::Client#create_translation_memory_import instead.
88
101
 
89
102
  def create_import(file_name, content_length, content_type: nil, display_name: nil,
90
- additional_headers: {})
103
+ extra_body_parameters: nil, additional_headers: {})
104
+ options = { content_type: content_type, display_name: display_name,
105
+ extra_body_parameters: extra_body_parameters }.compact
91
106
  DeepL::Requests::TranslationMemory::CreateImport.new(
92
- @api, file_name, content_length,
93
- { content_type: content_type, display_name: display_name }.compact, additional_headers
107
+ @api, file_name, content_length, options, additional_headers
94
108
  ).request
95
109
  end
96
110
 
@@ -99,12 +113,18 @@ module DeepL
99
113
  # upload URL is a pre-signed storage URL outside of the DeepL API, so no authorization header
100
114
  # is sent with this request.
101
115
  #
116
+ # @raise [DeepL::Exceptions::RequestError] If the storage URL replies with a non-2xx status:
117
+ # the API error class for that status, e.g.
118
+ # `AuthorizationFailed` for 401 and 403.
119
+ #
102
120
  # @param [String, DeepL::Resources::TranslationMemoryImport] translation_memory_import Import
103
121
  # returned by `create_import`, or its upload URL.
104
122
  # @param [String] file_content Content of the TMX file.
105
123
  # @param [String] content_type MIME type of the file. Must match the `content_type` declared
106
124
  # when the import job was created.
107
125
  # @return [nil]
126
+ #
127
+ # @deprecated Use DeepL::Client#upload_translation_memory_file instead.
108
128
 
109
129
  def upload_file(translation_memory_import, file_content,
110
130
  content_type: Requests::TranslationMemory::UploadFile::DEFAULT_CONTENT_TYPE)
@@ -122,6 +142,8 @@ module DeepL
122
142
  # @param [Hash] options Additional options for the request.
123
143
  # @return [DeepL::Resources::TranslationMemoryExport] The job ID, and whether the API reused a
124
144
  # previously completed export.
145
+ #
146
+ # @deprecated Use DeepL::Client#create_translation_memory_export instead.
125
147
 
126
148
  def create_export(translation_memory, options = {})
127
149
  DeepL::Requests::TranslationMemory::CreateExport.new(
@@ -135,6 +157,8 @@ module DeepL
135
157
  # @param [String, DeepL::Resources::TranslationMemoryJob] job Job ID or object.
136
158
  # @param [Hash] options Additional options for the request.
137
159
  # @return [DeepL::Resources::TranslationMemoryJob] The current status of the job.
160
+ #
161
+ # @deprecated Use DeepL::Client#get_translation_memory_job instead.
138
162
 
139
163
  def find_job(job, options = {})
140
164
  DeepL::Requests::TranslationMemory::FindJob.new(@api, extract_job_id(job), options).request
@@ -154,19 +178,20 @@ module DeepL
154
178
  #
155
179
  # @param [String, DeepL::Resources::TranslationMemoryJob] job Job ID or object.
156
180
  # @param [Hash] options Additional options for the status queries.
157
- # @param [Numeric, nil] timeout_s Maximum time in seconds to wait for the job to finish. Note
158
- # that this is not accurate to the second, the status is only
159
- # queried every five seconds.
181
+ # @param [Numeric, nil] timeout_s Maximum time in seconds to wait for the job to finish. The
182
+ # status is queried every five seconds; the last wait is cut
183
+ # so that the call never waits past `timeout_s`.
160
184
  # @return [DeepL::Resources::TranslationMemoryJob] The finished job.
185
+ #
186
+ # @deprecated Use DeepL::Client#wait_until_translation_memory_job_done instead.
161
187
 
162
188
  def wait_until_job_done(job, options = {}, timeout_s: nil)
189
+ deadline = timeout_s.nil? ? nil : monotonic_time + timeout_s
163
190
  job_status = find_job(job, options)
164
- started_at = monotonic_time
165
191
  until job_status.finished?
166
- raise_timeout_error(timeout_s) if timeout_exceeded?(started_at, timeout_s)
167
-
168
- log_job_polling
169
- sleep(JOB_POLLING_INTERVAL_SECONDS)
192
+ interval = polling_interval_until(deadline, timeout_s)
193
+ log_job_polling(interval)
194
+ sleep(interval)
170
195
  job_status = find_job(job, options)
171
196
  end
172
197
  raise_job_error(job_status) if job_status.error?
@@ -180,11 +205,17 @@ module DeepL
180
205
  #
181
206
  # @raise [DeepL::Exceptions::Error] If the job carries no download URL, for example because
182
207
  # it has not completed yet.
208
+ # @raise [DeepL::Exceptions::RequestError] If the storage URL replies with a non-2xx status:
209
+ # the API error class for that status, e.g.
210
+ # `AuthorizationFailed` for 401 and 403.
211
+ # @raise [SystemCallError, IOError] If the file cannot be written.
183
212
  #
184
213
  # @param [DeepL::Resources::TranslationMemoryJob, String] job Completed export job carrying
185
214
  # the download URL, or the download URL itself.
186
215
  # @param [String] output_path Path to the file to write to. Will be overwritten if the file
187
216
  # already exists.
217
+ #
218
+ # @deprecated Use DeepL::Client#download_translation_memory_export instead.
188
219
 
189
220
  def download_export(job, output_path)
190
221
  DeepL::Requests::TranslationMemory::DownloadExport.new(@api, extract_download_url(job),
@@ -195,7 +226,9 @@ module DeepL
195
226
  # Imports a TMX file as a new translation memory: creates the import job, uploads the file
196
227
  # and waits for the processing to finish.
197
228
  #
198
- # @raise [DeepL::Exceptions::Error] If the import fails.
229
+ # @raise [DeepL::Exceptions::Error] If no file exists at `input_file_path` (before any request
230
+ # is sent) or the import fails.
231
+ # @raise [SystemCallError, IOError] If the file cannot be read.
199
232
  #
200
233
  # @param [String] input_file_path Path to the TMX file to import.
201
234
  # @param [String, nil] display_name Name of the resulting translation memory, defaults to the
@@ -203,18 +236,23 @@ module DeepL
203
236
  # @param [Numeric, nil] timeout_s Maximum time in seconds to wait for the import to finish.
204
237
  # Note that the API keeps reporting `awaiting_input` for a
205
238
  # while after the upload, so allow for a generous timeout.
239
+ # @param [String, nil] content_type MIME type of the file, declared in the import job and sent
240
+ # with the upload. Defaults to "application/xml".
241
+ # @param [Hash, nil] extra_body_parameters Fields added to the body of the import job request
242
+ # last; they replace fields of the same name.
206
243
  # @return [DeepL::Resources::TranslationMemoryJob] The finished import job, its result carries
207
244
  # the ID of the new translation memory.
245
+ #
246
+ # @deprecated Use DeepL::Client#import_translation_memory_from_filepath instead.
208
247
 
209
- def import_from_filepath(input_file_path, display_name: nil, timeout_s: nil)
210
- unless File.exist?(input_file_path)
211
- raise Exceptions::Error, "No file found at #{input_file_path}"
212
- end
213
-
214
- file_content = File.binread(input_file_path)
248
+ def import_from_filepath(input_file_path, display_name: nil, timeout_s: nil,
249
+ content_type: nil, extra_body_parameters: nil)
250
+ check_input_file(input_file_path)
251
+ file_content = read_input_file(input_file_path)
215
252
  created = create_import(File.basename(input_file_path), file_content.bytesize,
216
- display_name: display_name)
217
- upload_file(created, file_content)
253
+ content_type: content_type, display_name: display_name,
254
+ extra_body_parameters: extra_body_parameters)
255
+ upload_file(created, file_content, content_type: content_type)
218
256
  wait_until_job_done(created.job_id, timeout_s: timeout_s)
219
257
  end
220
258
 
@@ -222,7 +260,10 @@ module DeepL
222
260
  # Exports a translation memory to a TMX file: creates the export job, waits for it to finish
223
261
  # and writes the result to +output_path+.
224
262
  #
263
+ # @raise [Errno::ENOENT] If the directory of `output_path` does not exist, checked before the
264
+ # export job is created.
225
265
  # @raise [DeepL::Exceptions::Error] If the export fails.
266
+ # @raise [SystemCallError, IOError] If the file cannot be written.
226
267
  #
227
268
  # @param [String, DeepL::Resources::TranslationMemory] translation_memory Translation memory
228
269
  # ID or object.
@@ -230,8 +271,11 @@ module DeepL
230
271
  # already exists.
231
272
  # @param [Numeric, nil] timeout_s Maximum time in seconds to wait for the export to finish.
232
273
  # @return [DeepL::Resources::TranslationMemoryJob] The finished export job.
274
+ #
275
+ # @deprecated Use DeepL::Client#export_translation_memory_to_filepath instead.
233
276
 
234
277
  def export_to_filepath(translation_memory, output_path, timeout_s: nil)
278
+ check_output_directory(output_path)
235
279
  created = create_export(translation_memory)
236
280
  job = wait_until_job_done(created.job_id, timeout_s: timeout_s)
237
281
  download_export(job, output_path)
@@ -282,17 +326,53 @@ module DeepL
282
326
  value.nil? || value.empty?
283
327
  end
284
328
 
285
- def log_job_polling
329
+ # Until the next major version, the module-level API raises the error classes of v3.9.0 for
330
+ # local file failures; DeepL::Client raises ArgumentError and wrapped errors.
331
+ def check_input_file(input_file_path)
332
+ return if File.exist?(input_file_path)
333
+
334
+ message = "No file found at #{input_file_path}"
335
+ raise ArgumentError, message if @api.client?
336
+
337
+ raise Exceptions::Error, message
338
+ end
339
+
340
+ def read_input_file(input_file_path)
341
+ File.binread(input_file_path)
342
+ rescue SystemCallError, IOError => e
343
+ raise e unless @api.client?
344
+
345
+ raise Exceptions::Error, "Could not read #{input_file_path}: #{e.message}"
346
+ end
347
+
348
+ def check_output_directory(output_path)
349
+ directory = File.dirname(output_path)
350
+ return if File.directory?(directory)
351
+
352
+ message = "Output directory #{directory} does not exist"
353
+ raise Exceptions::Error, message if @api.client?
354
+
355
+ raise Errno::ENOENT, message
356
+ end
357
+
358
+ def log_job_polling(interval)
286
359
  @api.configuration.logger&.info('Rechecking translation memory job status after sleeping ' \
287
- "for #{JOB_POLLING_INTERVAL_SECONDS} seconds.")
360
+ "for #{format('%.2f', interval)} seconds.")
288
361
  end
289
362
 
290
363
  def monotonic_time
291
364
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
292
365
  end
293
366
 
294
- def timeout_exceeded?(started_at, timeout_s)
295
- !timeout_s.nil? && (monotonic_time - started_at) > timeout_s
367
+ # The polling interval, cut to the time left until +deadline+ so that a wait never runs
368
+ # past +timeout_s+.
369
+ def polling_interval_until(deadline, timeout_s)
370
+ return JOB_POLLING_INTERVAL_SECONDS if deadline.nil?
371
+
372
+ remaining = deadline - monotonic_time
373
+ raise_timeout_error(timeout_s) if remaining <= 0
374
+
375
+ [JOB_POLLING_INTERVAL_SECONDS, remaining].min
296
376
  end
297
377
 
298
378
  def raise_timeout_error(timeout_s)
@@ -0,0 +1,54 @@
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 Utils
8
+ # Builds the API connection of a DeepL::Client or DeepL::AdminClient from its constructor
9
+ # options. Nothing is read from the environment or from module state.
10
+ # @api private
11
+ module ClientBuilder
12
+ extend self
13
+
14
+ def build_api(key, key_name:, server_url:, default_server_url:, headers:, max_retries:,
15
+ timeout:, proxy:, send_platform_info:, app_info:, http_client_options:, logger:) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/ParameterLists
16
+ key = key.to_s.strip
17
+ raise ArgumentError, "#{key_name} must not be empty" if key.empty?
18
+
19
+ headers = (headers || {}).to_h
20
+ app_info = AppInfo.from(app_info)
21
+ configuration = Configuration.new(
22
+ { auth_key: key, host: server_url || default_server_url,
23
+ max_network_retries: max_retries, timeout: timeout, logger: logger,
24
+ headers: headers.reject { |name, _| user_agent_header?(name) },
25
+ user_agent: headers.find { |name, _| user_agent_header?(name) }&.last },
26
+ app_info&.app_name, app_info&.app_version, send_platform_info
27
+ )
28
+ API.new(configuration, merge_proxy(http_client_options, proxy), client: true)
29
+ end
30
+
31
+ private
32
+
33
+ def user_agent_header?(name)
34
+ name.to_s.casecmp?('user-agent')
35
+ end
36
+
37
+ def merge_proxy(options, proxy) # rubocop:disable Metrics/MethodLength
38
+ return options if proxy.nil?
39
+ if options && !(options.proxy.nil? || options.proxy.empty?)
40
+ raise ArgumentError,
41
+ 'Set the proxy either with proxy: or in http_client_options:, not both'
42
+ end
43
+ return HTTPClientOptions.new(proxy) if options.nil?
44
+
45
+ HTTPClientOptions.new(proxy, options.cert_path,
46
+ enable_ssl_verification: options.enable_ssl_verification,
47
+ open_timeout: options.open_timeout,
48
+ read_timeout: options.read_timeout,
49
+ write_timeout: options.write_timeout,
50
+ ssl_timeout: options.ssl_timeout)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,70 @@
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 Utils
8
+ # @api private
9
+ # Polls the status of a document translation every 5 seconds.
10
+ module DocumentPoller
11
+ INTERVAL_S = 5
12
+
13
+ extend self
14
+
15
+ # Polls until the translation is done and returns that status. Raises
16
+ # DeepL::Exceptions::Error with the API's message when the translation fails, and when
17
+ # +timeout_s+ is set and expires. An unknown status keeps polling.
18
+ def wait_until_done(api, handle, timeout_s: nil)
19
+ deadline = timeout_s.nil? ? nil : monotonic_now + timeout_s
20
+ loop do
21
+ status = get_status(api, handle)
22
+ return status if status.done?
23
+
24
+ raise_if_failed(status)
25
+
26
+ sleep(interval_until(deadline, timeout_s))
27
+ end
28
+ end
29
+
30
+ # Polls until the translation is done or failed and returns that status, or the last status
31
+ # after +max_queries+ + 1 queries if +max_queries+ is set.
32
+ def wait_until_finished(api, handle, max_queries: nil)
33
+ num_queries = 0
34
+ loop do
35
+ sleep(INTERVAL_S) if num_queries.positive?
36
+ num_queries += 1
37
+ status = get_status(api, handle)
38
+ return status if status.finished? || (!max_queries.nil? && num_queries > max_queries)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def raise_if_failed(status)
45
+ return unless status.error?
46
+
47
+ raise Exceptions::Error, "Document translation failed: #{status.error_message}"
48
+ end
49
+
50
+ def get_status(api, handle)
51
+ Requests::Document::GetStatus.new(api, handle.document_id, handle.document_key).request
52
+ end
53
+
54
+ def interval_until(deadline, timeout_s)
55
+ return INTERVAL_S if deadline.nil?
56
+
57
+ remaining = deadline - monotonic_now
58
+ if remaining <= 0
59
+ raise Exceptions::Error, "Document translation did not finish within #{timeout_s} seconds"
60
+ end
61
+
62
+ [INTERVAL_S, remaining].min
63
+ end
64
+
65
+ def monotonic_now
66
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -21,13 +21,14 @@ module DeepL
21
21
  end
22
22
  end
23
23
 
24
- def initialize(response)
24
+ def initialize(response, request = nil)
25
25
  @response = response
26
+ @request = request
26
27
  end
27
28
 
28
29
  def build
29
30
  error_class = self.class.error_class_from_response_code(response.code.to_i)
30
- error_class.new(response)
31
+ error_class.new(response, request)
31
32
  end
32
33
  end
33
34
  end
@@ -0,0 +1,79 @@
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 Utils
8
+ module HTTPClientBuilder
9
+ extend self
10
+
11
+ ##
12
+ # Builds an unstarted Net::HTTP client for +host+ configured with +options+. Net::HTTP does
13
+ # not retry on its own (+max_retries+ 0): only the library's retries apply.
14
+ #
15
+ # @param [String] host Base URL of the API.
16
+ # @param [DeepL::HTTPClientOptions] options Proxy, SSL and timeout options.
17
+ # @return [Net::HTTP]
18
+ def build(host, options) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
19
+ uri = URI(host)
20
+ http = Net::HTTP.new(uri.host, uri.port, *proxy_arguments(options.proxy))
21
+ http.use_ssl = uri.scheme == 'https'
22
+ disable_lenient_defaults(http)
23
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE unless options.enable_ssl_verification
24
+ http.ca_file = options.cert_path if options.cert_path
25
+ http.open_timeout = options.open_timeout unless options.open_timeout.nil?
26
+ http.read_timeout = options.read_timeout unless options.read_timeout.nil?
27
+ http.write_timeout = options.write_timeout unless options.write_timeout.nil?
28
+ http.ssl_timeout = options.ssl_timeout unless options.ssl_timeout.nil?
29
+ http
30
+ end
31
+
32
+ ##
33
+ # Net::HTTP would re-send idempotent requests once on its own, outside max_retries, and would
34
+ # return a body shorter than its Content-Length as complete (+ignore_eof+, net-http 0.3+).
35
+ def disable_lenient_defaults(http)
36
+ http.max_retries = 0
37
+ http.ignore_eof = false if http.respond_to?(:ignore_eof=)
38
+ http
39
+ end
40
+
41
+ private
42
+
43
+ # The proxy is either a URL string, e.g. +http://user:pass@proxy.example:8080+, or a hash
44
+ # with +proxy_addr+ and +proxy_port+. Returns the proxy arguments of +Net::HTTP.new+.
45
+ def proxy_arguments(proxy)
46
+ if proxy.is_a?(String) && !proxy.empty?
47
+ proxy_uri = parse_proxy_url(proxy)
48
+
49
+ return [proxy_uri.host, proxy_uri.port, decode(proxy_uri.user),
50
+ decode(proxy_uri.password)]
51
+ end
52
+ unless proxy.is_a?(Hash) && proxy.key?('proxy_addr') && proxy.key?('proxy_port')
53
+ return [nil,
54
+ nil]
55
+ end
56
+
57
+ [proxy['proxy_addr'], proxy['proxy_port']]
58
+ end
59
+
60
+ # Error messages show the URL without its user info; the URI error, which repeats the URL,
61
+ # is not kept as cause.
62
+ def parse_proxy_url(proxy)
63
+ proxy_uri = begin
64
+ URI(proxy)
65
+ rescue URI::InvalidURIError
66
+ nil
67
+ end
68
+ return proxy_uri unless proxy_uri&.host.nil?
69
+
70
+ raise ArgumentError, "Invalid proxy URL: #{HTTPClientOptions.redact_proxy(proxy)}",
71
+ cause: nil
72
+ end
73
+
74
+ def decode(value)
75
+ value && URI::DEFAULT_PARSER.unescape(value)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,34 @@
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 Utils
8
+ ##
9
+ # Reads numbers from API responses without failing on unexpected values.
10
+ module NumberParser
11
+ extend self
12
+
13
+ # @return [Integer] The value if it is an Integer, otherwise 0.
14
+ def count(value)
15
+ value.is_a?(Integer) ? value : 0
16
+ end
17
+
18
+ # @return [Float] The value as a Float if it is numeric, otherwise 0.0.
19
+ def minutes(value)
20
+ value.is_a?(Numeric) ? value.to_f : 0.0
21
+ end
22
+
23
+ # @return [Float, nil] The value as a Float if it is numeric, otherwise nil.
24
+ def optional_minutes(value)
25
+ value.is_a?(Numeric) ? value.to_f : nil
26
+ end
27
+
28
+ # @return [Integer, nil] The value if it is an Integer, otherwise nil.
29
+ def optional_count(value)
30
+ value.is_a?(Integer) ? value : nil
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,33 @@
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 Utils
8
+ module TermValidator
9
+ extend self
10
+
11
+ # C0 and C1 control characters plus the Unicode line and paragraph separators.
12
+ INVALID_CHARACTERS = /[\u0000-\u001F\u0080-\u009F

]/.freeze
13
+
14
+ ##
15
+ # Strips surrounding whitespace from a single glossary or spoken term, then checks it
16
+ # before it is serialized into a request.
17
+ #
18
+ # @param [String] term The term to check.
19
+ # @return [String] The stripped term, which is what the request sends.
20
+ # @raise [DeepL::Exceptions::ValidationError] If the stripped term is empty or contains a
21
+ # control character.
22
+ def validate!(term)
23
+ term = term.to_s.strip
24
+ raise Exceptions::ValidationError, 'Term must not be empty' if term.empty?
25
+ if INVALID_CHARACTERS.match?(term)
26
+ raise Exceptions::ValidationError, "Term #{term.inspect} contains an invalid character"
27
+ end
28
+
29
+ term
30
+ end
31
+ end
32
+ end
33
+ end
@@ -21,6 +21,20 @@ module DeepL
21
21
 
22
22
  Time.parse(time_string)
23
23
  end
24
+
25
+ ##
26
+ # Like +parse_optional_time+, but treats timestamps without a zone offset as UTC.
27
+ #
28
+ # @param [Object] time_string Timestamp in ISO 8601 format, or nil.
29
+ # @return [Time, nil] The parsed time, or nil if no valid timestamp was given.
30
+ def parse_optional_utc_time(time_string)
31
+ return nil unless time_string.is_a?(String) && !time_string.empty?
32
+ return Time.parse(time_string) if Date._parse(time_string).key?(:offset)
33
+
34
+ Time.parse("#{time_string}Z")
35
+ rescue ArgumentError
36
+ nil
37
+ end
24
38
  end
25
39
  end
26
40
  end