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
data/README.md CHANGED
@@ -27,6 +27,46 @@ gem 'deepl-rb', require: 'deepl'
27
27
 
28
28
  ## Usage
29
29
 
30
+ ### Creating a client
31
+
32
+ Create a `DeepL::Client` with your authentication key. Each client owns its configuration and
33
+ HTTP connection, so several clients with different keys or options can be used side by side.
34
+ The client reads no environment variables:
35
+
36
+ ```rb
37
+ client = DeepL::Client.new(ENV.fetch('DEEPL_AUTH_KEY'))
38
+ ```
39
+
40
+ `DeepL::Client.new` accepts these keyword arguments:
41
+
42
+ | Option | Default | Description |
43
+ | ------ | ------- | ----------- |
44
+ | `server_url:` | `https://api-free.deepl.com` for Free keys (ending in `:fx`), else `https://api.deepl.com` | Base URL of the DeepL API. A path prefix is kept. |
45
+ | `headers:` | `{}` | HTTP headers sent with every DeepL API request. A header of the same name, in any casing, replaces the library's, `Authorization` and `User-Agent` included, except `Content-Type`, `Content-Length`, `Transfer-Encoding` and `Host`. Pre-signed storage URLs get only the `User-Agent`. |
46
+ | `max_retries:` | `5` | Retries after the first attempt, for HTTP 429, any 5xx and transient network failures. |
47
+ | `timeout:` | `10` | Timeout of each attempt in seconds. A longer backoff interval takes precedence. |
48
+ | `proxy:` | none | Proxy URL, e.g. `http://user:password@proxy.example:8080`. |
49
+ | `send_platform_info:` | `true` | Adds the OS and Ruby versions to the `User-Agent`. |
50
+ | `app_info:` | none | A `DeepL::AppInfo` or a hash with `app_name` and `app_version`, added to the `User-Agent`. |
51
+ | `http_client_options:` | none | A `DeepL::HTTPClientOptions` with proxy, certificate, SSL verification and per-phase timeouts. Its timeouts take precedence over `timeout:`. |
52
+ | `logger:` | none | Logger for requests and retries, see [Logging](#logging). |
53
+
54
+ The authentication key is trimmed; an empty key raises `ArgumentError`.
55
+ `DeepL::Client.free_account_auth_key?(key)` returns whether a key is a DeepL API Free key.
56
+
57
+ `DeepL::Client` offers text translation (`translate_text`, see [Translate](#translate)),
58
+ document translation (`translate_document` and its steps, see
59
+ [Translate documents](#translate-documents)), rephrasing and correction (`rephrase_text` and
60
+ `correct_text`, see [Rephrase Text](#rephrase-text) and [Correct Text](#correct-text)),
61
+ multilingual glossaries (see [Multilingual glossaries](#multilingual-glossaries)),
62
+ spoken terms (see
63
+ [Spoken terms](#spoken-terms)) and usage (`get_usage`, see [Monitor usage](#monitor-usage)); for the other operations, use the module-level API shown in the following sections. The module-level API (`DeepL.configure`, `DeepL.translate`, `DeepL.glossaries`, ...)
64
+ is deprecated and will be removed in the next major version: each method prints a deprecation
65
+ warning once per process when Ruby's deprecation warnings are enabled (`ruby -W:deprecated` or
66
+ `Warning[:deprecated] = true`). See [Migrating to DeepL::Client](#migrating-to-deeplclient).
67
+
68
+ ### Module-level configuration (deprecated)
69
+
30
70
  Setup an environment variable named `DEEPL_AUTH_KEY` with your authentication key:
31
71
 
32
72
  ```sh
@@ -47,68 +87,144 @@ You can also configure the API host and the API version:
47
87
  DeepL.configure do |config|
48
88
  config.auth_key = 'your-api-token'
49
89
  config.host = 'https://api-free.deepl.com' # Default value is 'https://api.deepl.com'
50
- config.version = 'v1' # Default value is 'v2'
51
90
  end
52
91
  ```
53
92
 
93
+ Without a host, keys ending in `:fx` use `https://api-free.deepl.com`. The `version` setting
94
+ is deprecated.
95
+
96
+ ### Migrating to DeepL::Client
97
+
98
+ The module-level API keeps working until the next major version. Its deprecation warnings
99
+ appear only with Ruby's deprecation warnings on (`ruby -W:deprecated` or
100
+ `Warning[:deprecated] = true`). The `DeepL::Client` methods replacing it:
101
+
102
+ | Module-level API | `DeepL::Client` |
103
+ | ---------------- | --------------- |
104
+ | `DeepL.configure` | `DeepL::Client.new(ENV.fetch('DEEPL_AUTH_KEY'), ...)` |
105
+ | `DeepL.translate` | `translate_text` |
106
+ | `DeepL.rephrase` | `rephrase_text` |
107
+ | `DeepL.usage` | `get_usage` |
108
+ | `DeepL.languages(type:)` | `get_source_languages` / `get_target_languages` |
109
+ | `DeepL.document.*` | `translate_document`, `translate_document_upload`, `translate_document_get_status`, `translate_document_wait_until_done`, `translate_document_download` |
110
+ | `DeepL.glossaries` | the `*_multilingual_glossary*` methods |
111
+ | `DeepL.style_rules` | the style rule methods (`get_all_style_rules`, `create_style_rule`, ...) |
112
+ | `DeepL.translation_memories` | the translation memory methods (`list_translation_memories`, ...) |
113
+ | `DeepL.with_session` / `DeepL.http_client` | the `http_client_options:` and `proxy:` options |
114
+ | per-call `additional_headers` | the `headers:` option |
115
+
116
+ `DeepL::Client` behaves differently from the module-level API in these points:
117
+
118
+ - Network failures raise `DeepL::Exceptions::ConnectionError` (the module-level API raises the
119
+ original network error).
120
+ - The timeout of each attempt defaults to 10 seconds (60 for the module-level API).
121
+ - `translate_document` returns the final `DeepL::Resources::DocumentTranslationStatus` (the
122
+ module-level `DeepL.document.translate_document` returns the number of bytes written).
123
+ - The translation memory file helpers raise `ArgumentError` for a missing input file and
124
+ `DeepL::Exceptions::Error` for other local file failures (the module-level API raises
125
+ `DeepL::Exceptions::Error` and the raw `Errno::*`).
126
+
54
127
  ### Available languages
55
128
 
56
- Available languages can be retrieved via API:
129
+ `get_source_languages` and `get_target_languages` of `DeepL::Client` return the languages that
130
+ can be translated from and into, as `DeepL::Resources::Language` objects with upper-case codes:
57
131
 
58
132
  ```rb
59
- languages = DeepL.languages
133
+ languages = client.get_source_languages
60
134
 
61
- puts languages.class
62
- # => Array
63
135
  puts languages.first.class
64
136
  # => DeepL::Resources::Language
65
137
  puts "#{languages.first.code} -> #{languages.first.name}"
66
138
  # => "ES -> Spanish"
139
+
140
+ german = client.get_target_languages.find { |language| language.code == 'DE' }
141
+ puts german.supports_formality?
142
+ # => true
67
143
  ```
68
144
 
69
- Note that source and target languages may be different, which can be retrieved by using the `type`
70
- option:
145
+ `supports_formality?` is `true` or `false` for target languages and `nil` for source languages.
146
+ For a source language of the module-level `DeepL.languages`, it raises
147
+ `DeepL::Exceptions::NotSupported`, as in version 3.9.0.
148
+
149
+ All languages are also defined on the
150
+ [official API documentation](https://developers.deepl.com/docs/api-reference/translate).
151
+
152
+ The module-level `DeepL.languages(type: :source)` and `DeepL.languages(type: :target)` are
153
+ deprecated.
154
+
155
+ #### Languages per resource
156
+
157
+ `get_languages_for_resource` returns the languages supported by a given API resource, with
158
+ source/target usability and per-feature support. The language codes are returned as the API sends
159
+ them (`de`, `en-GB`). The resources are defined in `DeepL::Constants::LanguageResourceName`
160
+ (`TRANSLATE_TEXT`, `TRANSLATE_DOCUMENT`, `GLOSSARY`, `VOICE`, `WRITE`, `STYLE_RULES`,
161
+ `TRANSLATION_MEMORY`). Pass `include:` with one value or an array of values from
162
+ `DeepL::Constants::LanguageInclude` (`BETA`, `EXTERNAL`) to also list beta languages and features
163
+ or features that rely on third-party providers. Other strings are sent unchanged, for both the
164
+ resource and `include:`.
71
165
 
72
166
  ```rb
73
- puts DeepL.languages(type: :source).count
74
- # => 24
75
- puts DeepL.languages(type: :target).count
76
- # => 26
167
+ languages = client.get_languages_for_resource(
168
+ DeepL::Constants::LanguageResourceName::TRANSLATE_TEXT,
169
+ include: [DeepL::Constants::LanguageInclude::BETA]
170
+ )
171
+
172
+ german = languages.find { |language| language.code == 'de' }
173
+ puts german.usable_as_target?
174
+ # => true
175
+ puts german.supports?(:formality)
176
+ # => true
177
+ puts german.features['formality'].status
178
+ # => "stable"
77
179
  ```
78
180
 
79
- All languages are also defined on the
80
- [official API documentation](https://developers.deepl.com/docs/api-reference/translate).
181
+ `get_language_resources` lists every resource with its `name` and features, and whether a
182
+ feature must be supported by the source language, the target language, or both:
81
183
 
82
- Note that target languages may include the `supports_formality` flag, which may be checked
83
- using the `DeepL::Resources::Language#supports_formality?`.
184
+ ```rb
185
+ translate_text = client.get_language_resources.find { |resource| resource.name == 'translate_text' }
186
+ formality = translate_text.features['formality']
187
+ puts formality.needs_source_support?
188
+ # => false
189
+ puts formality.needs_target_support?
190
+ # => true
191
+ ```
84
192
 
85
193
  ### Translate
86
194
 
87
- To translate a simple text, use the `translate` method:
195
+ To translate a simple text, use the `translate_text` method of `DeepL::Client`:
88
196
 
89
197
  ```rb
90
- translation = DeepL.translate 'This is my text', 'EN', 'ES'
198
+ translation = client.translate_text 'This is my text', 'EN', 'ES'
91
199
 
92
200
  puts translation.class
93
201
  # => DeepL::Resources::Text
94
202
  puts translation.text
95
203
  # => 'Este es mi texto'
204
+ puts translation.billed_characters
205
+ # => 15
96
206
  ```
97
207
 
208
+ Each result has `text`, `detected_source_language`, `billed_characters` (the characters
209
+ billed for the text; the library always asks the API for it), `model_type_used` and
210
+ `tag_handling_version` (the tag handling version the API used, when tag handling was
211
+ requested). A field the response does not contain is `nil`.
212
+
98
213
  Enable auto-detect source language by skipping the source language with `nil`:
99
214
 
100
215
  ```rb
101
- translation = DeepL.translate 'This is my text', nil, 'ES'
216
+ translation = client.translate_text 'This is my text', nil, 'ES'
102
217
 
103
218
  puts translation.detected_source_language
104
219
  # => 'EN'
105
220
  ```
106
221
 
107
- Translate a list of texts by passing an array as an argument:
222
+ Translate a list of texts by passing an array as an argument. An array, also one with a
223
+ single text, returns an array of results:
108
224
 
109
225
  ```rb
110
226
  texts = ['Sample text', 'Another text']
111
- translations = DeepL.translate texts, 'EN', 'ES'
227
+ translations = client.translate_text texts, 'EN', 'ES'
112
228
 
113
229
  puts translations.class
114
230
  # => Array
@@ -116,12 +232,13 @@ puts translations.first.class
116
232
  # => DeepL::Resources::Text
117
233
  ```
118
234
 
119
- You can also use custom query parameters, like `tag_handling`, `split_sentences`, `non_splitting_tags` or `ignore_tags`:
235
+ Options are passed as keyword arguments; an option set to `nil` is not sent. You can use
236
+ options like `tag_handling`, `split_sentences`, `non_splitting_tags` or `ignore_tags`:
120
237
 
121
238
  ```rb
122
- translation = DeepL.translate '<p>A sample</p>', 'EN', 'ES',
123
- tag_handling: 'xml', split_sentences: false,
124
- non_splitting_tags: 'h1', ignore_tags: %w[code pre]
239
+ translation = client.translate_text '<p>A sample</p>', 'EN', 'ES',
240
+ tag_handling: 'xml', split_sentences: false,
241
+ non_splitting_tags: 'h1', ignore_tags: %w[code pre]
125
242
 
126
243
  puts translation.text
127
244
  # => "<p>Una muestra</p>"
@@ -130,20 +247,24 @@ puts translation.text
130
247
  To specify which version of the tag handling algorithm to use, you can use the `tag_handling_version` parameter:
131
248
 
132
249
  ```rb
133
- translation = DeepL.translate '<p>A sample</p>', 'EN', 'ES',
134
- tag_handling: 'xml', tag_handling_version: 'v2'
250
+ translation = client.translate_text '<p>A sample</p>', 'EN', 'ES',
251
+ tag_handling: 'xml',
252
+ tag_handling_version: DeepL::Constants::TagHandlingVersion::V2
135
253
 
136
254
  puts translation.text
137
255
  # => "<p>Una muestra</p>"
256
+ puts translation.tag_handling_version
257
+ # => "v2"
138
258
  ```
139
259
 
140
- The available values are `'v1'` and `'v2'`.
260
+ The available values are `'v1'` and `'v2'` (`DeepL::Constants::TagHandlingVersion::V1` and
261
+ `V2`); other strings are sent as given.
141
262
 
142
263
  To translate with context, simply supply the `context` parameter:
143
264
 
144
265
  ```rb
145
- translation = DeepL.translate 'That is hot!', 'EN', 'ES',
146
- context: 'He did not like the jalapenos in his meal.'
266
+ translation = client.translate_text 'That is hot!', 'EN', 'ES',
267
+ context: 'He did not like the jalapenos in his meal.'
147
268
 
148
269
  puts translation.text
149
270
  # => "¡Eso es picante!"
@@ -152,8 +273,8 @@ puts translation.text
152
273
  To specify a type of translation model to use, you can use the `model_type` option:
153
274
 
154
275
  ```rb
155
- translation = DeepL.translate 'That is hot!', 'EN', 'DE',
156
- model_type: 'quality_optimized'
276
+ translation = client.translate_text 'That is hot!', 'EN', 'DE',
277
+ model_type: 'quality_optimized'
157
278
  ```
158
279
 
159
280
  This would use next-gen translation models for the translation. The available values are
@@ -165,11 +286,12 @@ This would use next-gen translation models for the translation. The available va
165
286
  - `'latency_optimized'`: use a translation model that minimizes response time, at the cost
166
287
  of translation quality.
167
288
 
168
- To translate with custom instructions, supply the `custom_instructions` parameter:
289
+ To translate with custom instructions, supply the `custom_instructions` parameter, an array
290
+ of instructions. A single string is sent as one instruction, commas included:
169
291
 
170
292
  ```rb
171
- translation = DeepL.translate 'Hello, world!', 'EN', 'DE',
172
- custom_instructions: ['Use informal language', 'Be concise']
293
+ translation = client.translate_text 'Hello, world!', 'EN', 'DE',
294
+ custom_instructions: ['Use informal language', 'Be concise']
173
295
 
174
296
  puts translation.text
175
297
  ```
@@ -190,46 +312,89 @@ The following parameters will be automatically converted:
190
312
  | `non_splitting_tags` | Converts arrays to strings joining by commas
191
313
  | `ignore_tags` | Converts arrays to strings joining by commas
192
314
  | `formality` | No conversion applied
193
- | `glossary_id` | No conversion applied
315
+ | `glossary` | A glossary ID string, a `Glossary` or a `MultilingualGlossary` object, sent as `glossary_id` (see [Glossaries](#glossaries))
316
+ | `glossary_id` | Deprecated, use `glossary`. No conversion applied
194
317
  | `style_rule` | No conversion applied (can be a string ID or a StyleRule object)
318
+ | `style_profile` | No conversion applied (can be a string ID or a StyleProfile object, see [Style profiles](#style-profiles))
195
319
  | `translation_memory` | No conversion applied (can be a string ID or a TranslationMemory object)
196
320
  | `translation_memory_threshold` | No conversion applied (integer 0-100, recommended minimum 75)
197
321
  | `context` | No conversion applied
198
- | `custom_instructions` | No conversion applied
199
- | `tag_handling_version` | No conversion applied
322
+ | `custom_instructions` | An array of strings; a string is sent as a one-element array
323
+ | `tag_handling_version` | No conversion applied (`DeepL::Constants::TagHandlingVersion`)
200
324
  | `extra_body_parameters` | Hash of extra parameters to pass in the body of the HTTP request. Can be used to access beta features, or to override built-in parameters for testing purposes. Extra parameters can override keys explicitly set by the client.
201
325
 
326
+ `show_billed_characters: true` is always sent.
327
+
328
+ The deprecated module-level `DeepL.translate(text, source_lang, target_lang, options = {},
329
+ additional_headers = {})` takes the same options. Unlike `translate_text`, it returns a single
330
+ result for a one-element array.
331
+
202
332
  ### Rephrase Text
203
333
 
204
- To rephrase or improve text, including changing the writing style or tone of the text, use the `rephrase` method:
334
+ To rephrase or improve text, including changing the writing style or tone of the text, use
335
+ the `rephrase_text` method of `DeepL::Client`:
205
336
 
206
337
  ```rb
207
- rephrased_text = DeepL.rephrase 'you will acquire new rephrased text', 'EN'
338
+ rephrased_text = client.rephrase_text 'you will acquire new rephrased text', 'EN'
208
339
 
209
340
  puts rephrased_text.class
210
341
  # => DeepL::Resources::Text
211
342
  puts rephrased_text.text
212
343
  # => 'You get new rephrased text.'
344
+ puts rephrased_text.detected_source_language
345
+ # => 'en'
346
+ puts rephrased_text.target_language
347
+ # => 'en-US'
213
348
  ```
214
349
 
215
- As with translate, the text input can be a single string or an array of strings.
350
+ As with `translate_text`, the text can be a string or an array of strings: a string returns a
351
+ `DeepL::Resources::Text`, an array returns an array of them (also for an array with one
352
+ element). The target language is optional.
216
353
 
217
- You can use the additional arguments to specify the writing style or tone you want for the rephrased text:
354
+ Use the `writing_style:` or `tone:` keyword to choose the writing style or tone of the
355
+ rephrased text (`DeepL::Constants::WritingStyle` and `DeepL::Constants::Tone` list the values;
356
+ any string is sent as given). `extra_body_parameters:` is a hash added to the request body
357
+ last; it replaces fields of the same name.
218
358
 
219
359
  ```rb
220
- rephrased_text = DeepL.rephrase 'you will acquire new rephrased text', 'EN', 'casual'
360
+ rephrased_text = client.rephrase_text 'you will acquire new rephrased text', 'EN',
361
+ writing_style: 'casual'
221
362
 
222
363
  puts rephrased_text.text
223
364
  # => 'You'll get new, rephrased text.'
224
365
  ```
225
366
 
226
367
  ```rb
227
- rephrased_text = DeepL.rephrase 'you will acquire new rephrased text', 'EN', nil, 'friendly'
368
+ rephrased_text = client.rephrase_text 'you will acquire new rephrased text', 'EN',
369
+ tone: DeepL::Constants::Tone::FRIENDLY
228
370
 
229
371
  puts rephrased_text.text
230
372
  # => 'You'll get to enjoy new, rephrased text!'
231
373
  ```
232
374
 
375
+ The deprecated module-level `DeepL.rephrase(text, target_lang = nil, writing_style = nil,
376
+ tone = nil, options = {}, additional_headers = {})` takes the writing style and tone as
377
+ positional arguments. Unlike `rephrase_text`, it returns a single result for a one-element
378
+ array.
379
+
380
+ ### Correct Text
381
+
382
+ To fix grammar, spelling and punctuation, use the `correct_text` method of `DeepL::Client`.
383
+ It accepts a string or an array of strings, plus an optional target language and
384
+ `extra_body_parameters:`:
385
+
386
+ ```ruby
387
+ corrected = client.correct_text 'I has a apple.', 'en-US'
388
+ puts corrected.text
389
+ # => 'It has an apple.'
390
+ puts corrected.target_language
391
+ # => 'en-US'
392
+ ```
393
+
394
+ A string returns a `DeepL::Resources::Text`; an array returns an array of them (also for an
395
+ array with one element). Empty text raises `ArgumentError`. It raises
396
+ `DeepL::Exceptions::Error` if the response contains fewer improvements than texts were sent.
397
+
233
398
  ### Glossaries
234
399
 
235
400
  To create a glossary, use the `glossaries.create` method. The glossary `entries` argument should be an array of text pairs. Each pair includes the source and the target translations.
@@ -249,17 +414,19 @@ puts glossary.entry_count
249
414
  # => 2
250
415
  ```
251
416
 
252
- Created glossaries can be used in the `translate` method by specifying the `glossary_id` option:
417
+ Created glossaries can be used in text and document translation by specifying the
418
+ `glossary` option, a glossary ID or a `Glossary` / `MultilingualGlossary` object. It is sent
419
+ as `glossary_id` and replaces the deprecated `glossary_id` option:
253
420
 
254
421
  ```rb
255
- translation = DeepL.translate 'Hello World', 'EN', 'ES', glossary_id: 'aa48c7f0-0d02-413e-8a06-d5bbf0ca7a6e'
422
+ translation = client.translate_text 'Hello World', 'EN', 'ES', glossary: 'aa48c7f0-0d02-413e-8a06-d5bbf0ca7a6e'
256
423
 
257
424
  puts translation.class
258
425
  # => DeepL::Resources::Text
259
426
  puts translation.text
260
427
  # => 'Hola Tierra'
261
428
 
262
- translation = DeepL.translate "I wish we had a car.", 'EN', 'ES', glossary_id: 'aa48c7f0-0d02-413e-8a06-d5bbf0ca7a6e'
429
+ translation = client.translate_text "I wish we had a car.", 'EN', 'ES', glossary: glossary
263
430
 
264
431
  puts translation.class
265
432
  # => DeepL::Resources::Text
@@ -269,16 +436,19 @@ puts translation.text
269
436
 
270
437
  To use more than one glossary at once, specify the `glossary_ids` option with an
271
438
  array of up to 5 glossary IDs (as strings or `DeepL::Resources::Glossary`
272
- objects) instead of `glossary_id`. This works for both text and document
273
- translation. `glossary_ids` requires `source_lang` to be set, cannot be combined
274
- with `glossary_id`, and raises `ArgumentError` if these rules are violated or
275
- more than 5 IDs are provided:
439
+ objects) instead of `glossary`. This works for both text and document
440
+ translation. `glossary_ids` cannot be combined with `glossary` or `glossary_id`,
441
+ and raises `ArgumentError` if this rule is violated or more than 5 IDs are
442
+ provided. For text translation, a glossary (`glossary` or `glossary_ids`) can be
443
+ used without `source_lang`: the API detects the source language and uses the
444
+ glossary's dictionary for the detected language pair. Document translation with
445
+ `glossary_ids` requires `source_lang` and raises `ArgumentError` without it:
276
446
 
277
447
  ```rb
278
448
  # Text translation with multiple glossaries
279
- translation = DeepL.translate 'Hello World', 'EN', 'ES',
280
- glossary_ids: ['aa48c7f0-0d02-413e-8a06-d5bbf0ca7a6e',
281
- 'bb59d8g1-1e13-524f-9b17-e6ccg1db8b7f']
449
+ translation = client.translate_text 'Hello World', 'EN', 'ES',
450
+ glossary_ids: ['aa48c7f0-0d02-413e-8a06-d5bbf0ca7a6e',
451
+ 'bb59d8g1-1e13-524f-9b17-e6ccg1db8b7f']
282
452
 
283
453
  # Document translation with multiple glossaries
284
454
  handle = DeepL.document.upload 'my_document.docx', 'EN', 'ES', 'my_document.docx',
@@ -343,60 +513,248 @@ puts language_pairs.first.target_lang
343
513
  # => de
344
514
  ```
345
515
 
516
+ ### Multilingual glossaries
517
+
518
+ Multilingual glossaries (v3) group several language-pair dictionaries under one
519
+ glossary and are managed through `DeepL::Client`. Every method that takes a
520
+ glossary accepts its ID or a `DeepL::Resources::MultilingualGlossary` object; the
521
+ ID is URL-encoded in the request path. A dictionary's `entries` is a hash of
522
+ source term to target term.
523
+
524
+ To create a multilingual glossary, pass a name and one
525
+ `DeepL::Resources::MultilingualGlossaryDictionaryEntries` (or a hash with
526
+ `:source_lang`, `:target_lang` and `:entries`) per language pair. Entries are given
527
+ as a hash (`{ 'Hello' => 'Hallo' }`), as source/target pairs or as a TSV string (one
528
+ source/target pair per line, separated by a tab); a hash dictionary may also give
529
+ them as a CSV string with `entries_format: 'csv'`. Entries given as a hash or as pairs are
530
+ always sent as TSV, whatever `entries_format` says. Terms not given as CSV are checked like v2
531
+ glossary terms: each term is stripped of surrounding whitespace, and an empty term,
532
+ one with a control character, a pair or TSV line that is not exactly two terms, or a
533
+ source term that appears twice raises
534
+ `DeepL::Exceptions::ValidationError` before anything is sent. CSV content is sent as
535
+ given. Further options, such as
536
+ `extra_body_parameters`, are given as additional keywords:
537
+
538
+ ```rb
539
+ glossary = client.create_multilingual_glossary(
540
+ 'My Glossary',
541
+ [
542
+ DeepL::Resources::MultilingualGlossaryDictionaryEntries.new(
543
+ 'EN', 'DE', { 'Hello' => 'Hallo', 'World' => 'Welt' }
544
+ ),
545
+ { source_lang: 'EN', target_lang: 'FR', entries: [['Hello', 'Bonjour']] }
546
+ ]
547
+ )
548
+
549
+ puts glossary.class
550
+ # => DeepL::Resources::MultilingualGlossary
551
+ puts glossary.id
552
+ # => 'aa48c7f0-0d02-413e-8a06-d5bbf0ca7a6e'
553
+ puts glossary.creation_time.class
554
+ # => Time
555
+ puts glossary.dictionaries.first.entry_count
556
+ # => 2
557
+ ```
558
+
559
+ To create a multilingual glossary with one dictionary from CSV content (a string
560
+ or an IO), use `create_multilingual_glossary_from_csv`:
561
+
562
+ ```rb
563
+ glossary = client.create_multilingual_glossary_from_csv(
564
+ 'My Glossary', 'EN', 'DE', File.read('entries.csv')
565
+ )
566
+ ```
567
+
568
+ List or fetch multilingual glossaries with `list_multilingual_glossaries` and
569
+ `get_multilingual_glossary`. The glossary does not include the entries; fetch the
570
+ entries of one language pair with `get_multilingual_glossary_dictionary_entries`,
571
+ which raises `DeepL::Exceptions::NotFound` if the glossary has no dictionary for
572
+ that pair. The returned object can be passed to the create, update and replace
573
+ methods:
574
+
575
+ ```rb
576
+ glossaries = client.list_multilingual_glossaries
577
+ glossary = client.get_multilingual_glossary 'aa48c7f0-0d02-413e-8a06-d5bbf0ca7a6e'
578
+
579
+ dictionary = client.get_multilingual_glossary_dictionary_entries(glossary, 'EN', 'DE')
580
+ puts dictionary.entries.inspect
581
+ # => {"Hello"=>"Hallo", "World"=>"Welt"}
582
+ ```
583
+
584
+ `update_multilingual_glossary` renames the glossary and/or merges entries into
585
+ its dictionaries (a new language pair adds a dictionary). Keywords left `nil` are
586
+ not sent. `update_multilingual_glossary_name`,
587
+ `update_multilingual_glossary_dictionary` and
588
+ `update_multilingual_glossary_dictionary_from_csv` do one of these at a time. To
589
+ replace the entries of a dictionary instead, use
590
+ `replace_multilingual_glossary_dictionary` (or
591
+ `replace_multilingual_glossary_dictionary_from_csv`):
592
+
593
+ ```rb
594
+ client.update_multilingual_glossary(
595
+ glossary, name: 'Renamed Glossary',
596
+ dictionaries: [{ source_lang: 'EN', target_lang: 'FR', entries: [['World', 'Monde']] }]
597
+ )
598
+ client.update_multilingual_glossary_name(glossary, 'Renamed Glossary')
599
+ client.update_multilingual_glossary_dictionary(
600
+ glossary, { source_lang: 'EN', target_lang: 'FR', entries: [['World', 'Monde']] }
601
+ )
602
+ client.replace_multilingual_glossary_dictionary(
603
+ glossary, { source_lang: 'EN', target_lang: 'DE', entries: [['Hello', 'Hallo'], ['car', 'Auto']] }
604
+ )
605
+ ```
606
+
607
+ To delete one dictionary or the entire glossary (both return `nil`):
608
+
609
+ ```rb
610
+ client.delete_multilingual_glossary_dictionary(glossary, 'EN', 'FR')
611
+ client.delete_multilingual_glossary(glossary)
612
+ ```
613
+
614
+ ### Spoken terms
615
+
616
+ Spoken terms collections improve the transcription of terms such as product
617
+ names. A collection holds one list of terms per language and is managed through
618
+ `DeepL::Client`. Every method that takes a collection accepts its ID or a
619
+ `DeepL::Resources::SpokenTerms` object. Terms are passed and returned as arrays
620
+ of strings.
621
+
622
+ To create a collection, pass a name and one `DeepL::Resources::SpokenTermsEntries`
623
+ (or a hash with `:lang` and `:entries`) per language. Terms are checked like
624
+ glossary terms: each term is stripped of surrounding whitespace and sent stripped,
625
+ and `create_spoken_terms`, `update_spoken_terms` and `replace_spoken_terms_list`
626
+ raise `DeepL::Exceptions::ValidationError` before sending anything if a term is then empty or contains
627
+ a C0 or C1 control character (such as a tab or newline), U+2028 or U+2029.
628
+ Further options, such as `extra_body_parameters`, are given as additional
629
+ keywords:
630
+
631
+ ```rb
632
+ spoken_terms = client.create_spoken_terms(
633
+ 'Product names',
634
+ [DeepL::Resources::SpokenTermsEntries.new('en', ['DeepL', 'webhook'])]
635
+ )
636
+
637
+ puts spoken_terms.class
638
+ # => DeepL::Resources::SpokenTerms
639
+ puts spoken_terms.spoken_terms_id
640
+ # => 'a74d88fb-ed2a-4943-a664-a4512398b994'
641
+ puts spoken_terms.creation_time.class
642
+ # => Time
643
+ puts spoken_terms.term_lists.first.entry_count
644
+ # => 2
645
+ ```
646
+
647
+ `creation_time` is `nil` if the API returns a malformed timestamp. List all
648
+ collections or fetch one with `list_spoken_terms` and `get_spoken_terms`:
649
+
650
+ ```rb
651
+ collections = client.list_spoken_terms
652
+ spoken_terms = client.get_spoken_terms 'a74d88fb-ed2a-4943-a664-a4512398b994'
653
+ ```
654
+
655
+ `update_spoken_terms` renames a collection and/or merges terms into the list of
656
+ each given language. Arguments that are not given are not sent:
657
+
658
+ ```rb
659
+ client.update_spoken_terms(spoken_terms, name: 'Renamed')
660
+ client.update_spoken_terms(spoken_terms, name: 'Renamed',
661
+ extra_body_parameters: { example_param: 'value' })
662
+ updated = client.update_spoken_terms(
663
+ spoken_terms, term_lists: [{ lang: 'en', entries: ['authentication'] }]
664
+ )
665
+ puts updated.term_lists.first.entry_count
666
+ # => 3
667
+ ```
668
+
669
+ To read the terms of one language, use `get_spoken_terms_entries`:
670
+
671
+ ```rb
672
+ entries = client.get_spoken_terms_entries(spoken_terms, 'en')
673
+ puts entries.entries.inspect
674
+ # => ["DeepL", "authentication", "webhook"]
675
+ ```
676
+
677
+ `replace_spoken_terms_list` replaces the terms of one language, or adds the list
678
+ if the language is new. It takes one `DeepL::Resources::SpokenTermsEntries` or a
679
+ hash with `:lang` and `:entries`:
680
+
681
+ ```rb
682
+ term_list = client.replace_spoken_terms_list(
683
+ spoken_terms, DeepL::Resources::SpokenTermsEntries.new('en', ['API'])
684
+ )
685
+ puts term_list.class
686
+ # => DeepL::Resources::SpokenTermsList
687
+ puts term_list.entry_count
688
+ # => 1
689
+ ```
690
+
691
+ To delete the list of one language or the entire collection (both return `nil`):
692
+
693
+ ```rb
694
+ client.delete_spoken_terms_list(spoken_terms, 'de')
695
+ client.delete_spoken_terms(spoken_terms)
696
+ ```
697
+
346
698
  ### Style Rules
347
699
 
348
700
  Style rules allow you to customize your translations using a managed, shared list
349
- of rules for style, formatting, and more. Multiple style rules can be stored with
350
- your account, each with a user-specified name and a uniquely-assigned ID.
701
+ of rules for style, formatting, and more. Multiple style rule lists can be stored
702
+ with your account, each with a user-specified name and a uniquely-assigned ID
703
+ (`style_id`). `DeepL::Client` manages them with `get_all_style_rules`,
704
+ `create_style_rule`, `get_style_rule`, `update_style_rule_name`,
705
+ `update_style_rule_configured_rules` and `delete_style_rule`. Every method that
706
+ takes a style rule list accepts its ID string or a `DeepL::Resources::StyleRule`
707
+ object.
351
708
 
352
709
  #### Creating a style rule
353
710
 
354
- Use `create` to create a new style rule with a name and language code. You can
355
- optionally provide `configured_rules` and `custom_instructions`.
711
+ Use `create_style_rule` with a name and language code. The optional keywords
712
+ `configured_rules` (category -> rule -> value) and `custom_instructions` (hashes
713
+ with `label`, `prompt` and optionally `source_language`) are sent as given;
714
+ keywords left `nil` are not sent. `extra_body_parameters` adds fields to the
715
+ request body.
356
716
 
357
717
  ```rb
718
+ client = DeepL::Client.new(ENV.fetch('DEEPL_AUTH_KEY'))
719
+
358
720
  # Simple creation with just a name and language
359
- style_rule = DeepL.style_rules.create('My Style Rule', 'en')
721
+ style_rule = client.create_style_rule('My Style Rule', 'en')
360
722
  puts "Created: #{style_rule.name} (#{style_rule.style_id})"
361
723
 
362
724
  # Creation with configured rules and custom instructions
363
- style_rule = DeepL.style_rules.create(
725
+ style_rule = client.create_style_rule(
364
726
  'Formal English',
365
727
  'en',
366
- configured_rules: { style_and_tone: { formality: 'formal' } },
728
+ configured_rules: { dates_and_times: { calendar_era: 'use_bc_and_ad' } },
367
729
  custom_instructions: [{ label: 'Tone', prompt: 'Always use formal language' }]
368
730
  )
369
731
  ```
370
732
 
371
733
  #### Retrieving and listing style rules
372
734
 
373
- Use `find` to retrieve a single style rule by ID, or `list` to list all style
374
- rules.
375
-
376
- `list` returns a list of `StyleRule` objects corresponding to all of your stored
377
- style rules. The method accepts optional parameters: `page` (page number for
378
- pagination, 0-indexed), `page_size` (number of items per page), and `detailed`.
379
- When `true`, the response includes `configured_rules` and `custom_instructions`
380
- for each style rule. When `false` (default), these fields are omitted for faster
381
- responses.
735
+ Use `get_style_rule` to retrieve a single style rule list, or `get_all_style_rules`
736
+ to list them one page at a time. `get_all_style_rules` returns a list of
737
+ `StyleRule` objects and accepts the optional keywords `page` (page number,
738
+ 0-indexed), `page_size` (number of items per page) and `detailed`. When `true`,
739
+ the response includes `configured_rules` and `custom_instructions` for each style
740
+ rule list; when `false` (default), these fields are omitted for faster responses.
741
+ Keywords left `nil` are not sent.
382
742
 
383
743
  ```rb
384
- # Get a single style rule by ID
385
- style_rule = DeepL.style_rules.find('YOUR_STYLE_ID')
744
+ # Get a single style rule list by ID
745
+ style_rule = client.get_style_rule('YOUR_STYLE_ID')
386
746
  puts "#{style_rule.name} (#{style_rule.language})"
387
747
 
388
- # List all style rules
389
- style_rules = DeepL.style_rules.list
390
- style_rules.each do |rule|
748
+ # List the first page of style rule lists
749
+ client.get_all_style_rules.each do |rule|
391
750
  puts "#{rule.name} (#{rule.style_id})"
392
751
  end
393
752
 
394
753
  # List with pagination
395
- style_rules = DeepL.style_rules.list(page: 0, page_size: 10)
754
+ style_rules = client.get_all_style_rules(page: 0, page_size: 10)
396
755
 
397
756
  # List with detailed configuration
398
- style_rules = DeepL.style_rules.list(detailed: true)
399
- style_rules.each do |rule|
757
+ client.get_all_style_rules(detailed: true).each do |rule|
400
758
  if rule.configured_rules
401
759
  puts " Number formatting: #{rule.configured_rules.numbers.keys.join(', ')}"
402
760
  end
@@ -405,17 +763,18 @@ end
405
763
 
406
764
  #### Updating a style rule
407
765
 
408
- Use `update_name` to rename a style rule, and `update_configured_rules` to
409
- update its configured rules.
766
+ Use `update_style_rule_name` to rename a style rule list, and
767
+ `update_style_rule_configured_rules` to replace its configured rules. Both accept
768
+ `extra_body_parameters`.
410
769
 
411
770
  ```rb
412
771
  # Update the name
413
- updated = DeepL.style_rules.update_name('YOUR_STYLE_ID', 'New Name')
772
+ updated = client.update_style_rule_name(style_rule, 'New Name')
414
773
 
415
774
  # Update configured rules
416
- updated = DeepL.style_rules.update_configured_rules(
417
- 'YOUR_STYLE_ID',
418
- { style_and_tone: { formality: 'formal' } }
775
+ updated = client.update_style_rule_configured_rules(
776
+ style_rule,
777
+ { dates_and_times: { calendar_era: 'use_bc_and_ad' } }
419
778
  )
420
779
  ```
421
780
 
@@ -425,66 +784,183 @@ and `vocabulary`.
425
784
 
426
785
  #### Managing custom instructions
427
786
 
428
- Custom instructions allow you to add free-text prompts to a style rule. Each
787
+ Custom instructions allow you to add free-text prompts to a style rule list. Each
429
788
  instruction has an `id`, `label`, `prompt`, and `source_language`. Use
430
- `create_custom_instruction`, `find_custom_instruction`,
431
- `update_custom_instruction`, and `destroy_custom_instruction` to manage them.
789
+ `create_style_rule_custom_instruction`, `get_style_rule_custom_instruction`,
790
+ `update_style_rule_custom_instruction`, and `delete_style_rule_custom_instruction`
791
+ to manage them. An instruction is given as its ID or a
792
+ `DeepL::Resources::CustomInstruction` object; `source_language` is optional and
793
+ not sent if `nil`.
432
794
 
433
795
  ```rb
434
796
  # Create a custom instruction
435
- instruction = DeepL.style_rules.create_custom_instruction(
436
- 'YOUR_STYLE_ID', 'Formal tone', 'Always use formal language'
797
+ instruction = client.create_style_rule_custom_instruction(
798
+ style_rule, 'Formal tone', 'Always use formal language'
437
799
  )
438
800
  puts "Created instruction: #{instruction.id}"
439
801
 
440
802
  # Create with an optional source language
441
- instruction = DeepL.style_rules.create_custom_instruction(
442
- 'YOUR_STYLE_ID', 'Formal tone', 'Always use formal language', 'en'
803
+ instruction = client.create_style_rule_custom_instruction(
804
+ style_rule, 'Formal tone', 'Always use formal language', source_language: 'en'
443
805
  )
444
806
 
445
807
  # Get a custom instruction
446
- instruction = DeepL.style_rules.find_custom_instruction('YOUR_STYLE_ID', instruction.id)
808
+ instruction = client.get_style_rule_custom_instruction(style_rule, instruction)
447
809
 
448
810
  # Update a custom instruction
449
- updated = DeepL.style_rules.update_custom_instruction(
450
- 'YOUR_STYLE_ID', instruction.id, 'Updated label', 'Use very formal language'
811
+ updated = client.update_style_rule_custom_instruction(
812
+ style_rule, instruction, 'Updated label', 'Use very formal language'
451
813
  )
452
814
 
453
- # Delete a custom instruction
454
- DeepL.style_rules.destroy_custom_instruction('YOUR_STYLE_ID', instruction.id)
815
+ # Delete a custom instruction (returns nil)
816
+ client.delete_style_rule_custom_instruction(style_rule, instruction)
455
817
  ```
456
818
 
457
819
  #### Deleting a style rule
458
820
 
459
- Use `destroy` to delete a style rule by ID.
821
+ Use `delete_style_rule` to delete a style rule list. It returns `nil`.
460
822
 
461
823
  ```rb
462
- DeepL.style_rules.destroy('YOUR_STYLE_ID')
824
+ client.delete_style_rule('YOUR_STYLE_ID')
463
825
  ```
464
826
 
827
+ The deprecated module-level `DeepL.style_rules` (`list`, `create`, `find`,
828
+ `update_name`, `update_configured_rules`, `destroy`, `create_custom_instruction`,
829
+ `find_custom_instruction`, `update_custom_instruction`,
830
+ `destroy_custom_instruction`) keeps working unchanged.
831
+
465
832
  #### Using style rules in translations
466
833
 
467
- Style rules can be used in the `translate` method by specifying the `style_rule` option
834
+ Style rules can be used in `translate_text` by specifying the `style_rule` option
468
835
  with either a style rule ID string or a `StyleRule` object:
469
836
 
470
837
  ```rb
471
838
  # Using a style rule ID
472
- translation = DeepL.translate 'Hello World', 'EN', 'ES', style_rule: 'dca2e053-8ae5-45e6-a0d2-881156e7f4e4'
839
+ translation = client.translate_text('Hello World', 'EN', 'ES',
840
+ style_rule: 'dca2e053-8ae5-45e6-a0d2-881156e7f4e4')
473
841
 
474
842
  # Or using a StyleRule object
475
- style_rules = DeepL.style_rules.list
476
- translation = DeepL.translate 'Hello World', 'EN', 'ES', style_rule: style_rules.first
843
+ style_rule = client.get_all_style_rules.first
844
+ translation = client.translate_text('Hello World', 'EN', 'ES', style_rule: style_rule)
477
845
  ```
478
846
 
479
- The same `style_rule` option can be passed to document translation via
480
- `DeepL.document.upload` (or `DeepL.document.translate_document`), accepting either
481
- a style rule ID string or a `StyleRule` object:
847
+ The same `style_rule` option can be passed to document translation
848
+ (`translate_document` or `translate_document_upload`), accepting either a style
849
+ rule ID string or a `StyleRule` object:
482
850
 
483
851
  ```rb
484
- handle = DeepL.document.upload 'my_document.docx', 'EN', 'ES', 'my_document.docx',
485
- style_rule: 'dca2e053-8ae5-45e6-a0d2-881156e7f4e4'
852
+ client.translate_document('my_document.docx', 'my_document_es.docx', 'EN', 'ES',
853
+ style_rule: 'dca2e053-8ae5-45e6-a0d2-881156e7f4e4')
486
854
  ```
487
855
 
856
+ ### Style profiles
857
+
858
+ A style profile bundles glossaries, style rule lists and translation memories under one
859
+ name. `DeepL::Client` manages them with `create_style_profile`, `get_style_profile`,
860
+ `list_style_profiles`, `update_style_profile` and `delete_style_profile`. The last three
861
+ take a style profile ID string or a `DeepL::Resources::StyleProfile` object; the ID of a
862
+ profile is `style_profile_id`.
863
+
864
+ #### Creating a style profile
865
+
866
+ Only the name is required. The optional keywords are `description`, `industry`,
867
+ `glossary_ids`, `style_rule_ids`, `translation_memory_ids` and
868
+ `glossary_priority_order_enabled` (when `true`, the order of `glossary_ids` sets priority).
869
+ The component lists take IDs or the resource objects (`Glossary`, `MultilingualGlossary`,
870
+ `StyleRule`, `TranslationMemory`), which are sent as their IDs. If any component cannot be
871
+ used, the request fails and no style profile is created. `extra_body_parameters` adds
872
+ fields to the request body.
873
+
874
+ ```rb
875
+ client = DeepL::Client.new(ENV.fetch('DEEPL_AUTH_KEY'))
876
+
877
+ # Only a name
878
+ profile = client.create_style_profile('Legal, German')
879
+
880
+ # With components
881
+ profile = client.create_style_profile(
882
+ 'Legal, German',
883
+ description: 'Contract wording for the German market.',
884
+ industry: 'Legal',
885
+ glossary_ids: ['YOUR_GLOSSARY_ID'],
886
+ style_rule_ids: [style_rule], # a DeepL::Resources::StyleRule
887
+ translation_memory_ids: ['YOUR_TM_ID'],
888
+ glossary_priority_order_enabled: true
889
+ )
890
+ profile.class
891
+ # => DeepL::Resources::StyleProfile
892
+ profile.style_profile_id
893
+ # => "3f1c9d0e-7a24-4b58-9c31-8f0e5d2a1b47"
894
+ profile.style_rule_ids
895
+ # => ["YOUR_STYLE_ID"]
896
+ profile.creation_time.class
897
+ # => Time
898
+ ```
899
+
900
+ `description` and `industry` are `nil` when the style profile has none.
901
+
902
+ #### Retrieving and listing style profiles
903
+
904
+ `list_style_profiles` returns an array of style profiles, one page at a time. It accepts
905
+ `page:` (0-indexed, default 0) and `page_size:` (default 10; values above 100 are reduced to
906
+ 100).
907
+
908
+ ```rb
909
+ profile = client.get_style_profile('YOUR_STYLE_PROFILE_ID')
910
+
911
+ profiles = client.list_style_profiles
912
+ profiles = client.list_style_profiles(page: 1, page_size: 100)
913
+ ```
914
+
915
+ #### Updating a style profile
916
+
917
+ `update_style_profile` takes the same keywords as `create_style_profile`, plus `name`.
918
+ Keywords you leave out (or set to `nil`) are not sent and leave the field unchanged. An
919
+ empty string clears `description` or `industry`. A component list you send replaces the
920
+ linked list in full; an empty array unlinks all components of that kind.
921
+
922
+ ```rb
923
+ updated = client.update_style_profile(
924
+ 'YOUR_STYLE_PROFILE_ID',
925
+ name: 'Legal, German (2026)',
926
+ industry: 'Legal',
927
+ description: '', # clears the description
928
+ style_rule_ids: [] # unlinks all style rule lists
929
+ )
930
+ updated.description
931
+ # => nil
932
+ ```
933
+
934
+ #### Deleting a style profile
935
+
936
+ `delete_style_profile` returns `nil`.
937
+
938
+ ```rb
939
+ client.delete_style_profile('YOUR_STYLE_PROFILE_ID')
940
+ ```
941
+
942
+ #### Using style profiles in translations
943
+
944
+ Pass the `style_profile` option, a style profile ID string or a `StyleProfile` object, to
945
+ `translate_text`, `translate_document` or `translate_document_upload` to apply the
946
+ profile's glossaries, style rule lists and translation memories. A component that does not
947
+ fit the language pair is left out. The option cannot be combined with `glossary`,
948
+ `glossary_id`, `glossary_ids`, `style_rule`, `translation_memory`,
949
+ `translation_memory_threshold` or `custom_instructions`; the API rejects such requests.
950
+
951
+ ```rb
952
+ translation = client.translate_text('The artist is here.', nil, 'DE', style_profile: profile)
953
+ puts translation.text
954
+ # => Der Maler ist hier.
955
+
956
+ client.translate_document('/path/to/document.docx', '/path/to/translated.docx', 'EN', 'DE',
957
+ style_profile: 'YOUR_STYLE_PROFILE_ID')
958
+ ```
959
+
960
+ With documents, an unknown style profile is not rejected at upload: the translation fails
961
+ with a `DeepL::Exceptions::DocumentTranslationError` whose message contains
962
+ `Style profile not found.`
963
+
488
964
  ### Translation Memories
489
965
 
490
966
  Translation memories allow you to store and reuse previously created translations.
@@ -495,37 +971,44 @@ account, each with a source language and one or more target languages.
495
971
  Translation memories can also be managed in the DeepL UI via
496
972
  https://www.deepl.com/translation-memory.
497
973
 
498
- Every method that takes a translation memory accepts either a string containing
499
- the translation memory ID or a `TranslationMemory` object.
974
+ `DeepL::Client` manages them with `list_translation_memories`,
975
+ `get_translation_memory`, `list_translation_memory_segments`,
976
+ `import_translation_memory_from_filepath`, `export_translation_memory_to_filepath`
977
+ and `delete_translation_memory`, plus the single steps of an import or export
978
+ described below. Every method that takes a translation memory accepts either a
979
+ string containing the translation memory ID or a `TranslationMemory` object, and
980
+ every method that takes a job accepts its ID or a `TranslationMemoryJob`.
500
981
 
501
982
  #### Listing translation memories
502
983
 
503
- `translation_memories.list` returns a list of `TranslationMemory` objects
504
- for your stored translation memories. The method accepts optional parameters:
505
- `page` (page number for pagination, 0-indexed) and `page_size` (number of items
506
- per page, max 25).
984
+ `list_translation_memories` returns a list of `TranslationMemory` objects for
985
+ your stored translation memories, one page at a time. It accepts the optional
986
+ keywords `page` (page number for pagination, 0-indexed) and `page_size` (number
987
+ of items per page, max 25). Keywords left `nil` are not sent.
507
988
 
508
989
  ```rb
990
+ client = DeepL::Client.new(ENV.fetch('DEEPL_AUTH_KEY'))
991
+
509
992
  # List translation memories
510
- translation_memories = DeepL.translation_memories.list
511
- translation_memories.each do |tm|
993
+ client.list_translation_memories.each do |tm|
512
994
  puts "#{tm.name} (#{tm.translation_memory_id})"
513
995
  puts " Source: #{tm.source_language}, Targets: #{tm.target_languages.join(', ')}"
514
996
  puts " Segments: #{tm.segment_count}"
515
997
  end
516
998
 
517
999
  # List with pagination
518
- translation_memories = DeepL.translation_memories.list(page: 0, page_size: 10)
1000
+ translation_memories = client.list_translation_memories(page: 0, page_size: 10)
519
1001
  ```
520
1002
 
521
1003
  #### Retrieving a single translation memory
522
1004
 
523
- `translation_memories.find` retrieves one translation memory by ID. In addition
524
- to the fields returned by `list`, the resource carries the `creation_time` and
525
- `updated_time` timestamps.
1005
+ `get_translation_memory` retrieves one translation memory. In addition to the
1006
+ fields returned by `list_translation_memories`, the resource carries the
1007
+ `creation_time` and `updated_time` timestamps (`nil` if the API sends none or a
1008
+ malformed one).
526
1009
 
527
1010
  ```rb
528
- tm = DeepL.translation_memories.find 'YOUR_TM_ID'
1011
+ tm = client.get_translation_memory('YOUR_TM_ID')
529
1012
 
530
1013
  puts tm.class
531
1014
  # => DeepL::Resources::TranslationMemory
@@ -539,7 +1022,7 @@ puts tm.creation_time.class
539
1022
 
540
1023
  #### Listing the segments of a translation memory
541
1024
 
542
- `translation_memories.segments` returns one page of the segments of a
1025
+ `list_translation_memory_segments` returns one page of the segments of a
543
1026
  translation memory as a `TranslationMemorySegments` object. Each segment holds
544
1027
  the source text and one target per target language of the translation memory.
545
1028
 
@@ -547,13 +1030,14 @@ Pagination is cursor-based: omit `page_cursor` on the first call, then pass the
547
1030
  `next_page_cursor` of the previous response until `next_page?` is `false`. The
548
1031
  method also accepts `page_size` (1-100, defaults to 50), `filter_text` (a
549
1032
  substring matched against the source and target texts, at least 2 characters)
550
- and `filter_case_sensitive` (defaults to `false`).
1033
+ and `filter_case_sensitive` (defaults to `false`). Keywords left `nil` are not
1034
+ sent.
551
1035
 
552
1036
  Note that `segment_count` is the number of segments stored in the translation
553
1037
  memory; a text filter does not reduce it.
554
1038
 
555
1039
  ```rb
556
- page = DeepL.translation_memories.segments 'YOUR_TM_ID', page_size: 50
1040
+ page = client.list_translation_memory_segments('YOUR_TM_ID', page_size: 50)
557
1041
 
558
1042
  puts page.class
559
1043
  # => DeepL::Resources::TranslationMemorySegments
@@ -566,28 +1050,35 @@ puts page.segments.first.targets.first.target_text
566
1050
 
567
1051
  # Walk through every page of segments
568
1052
  while page.next_page?
569
- page = DeepL.translation_memories.segments 'YOUR_TM_ID',
570
- page_size: 50,
571
- page_cursor: page.next_page_cursor
1053
+ page = client.list_translation_memory_segments('YOUR_TM_ID',
1054
+ page_size: 50,
1055
+ page_cursor: page.next_page_cursor)
572
1056
  end
573
1057
 
574
1058
  # Only the segments matching a text
575
- page = DeepL.translation_memories.segments 'YOUR_TM_ID',
576
- filter_text: 'Nummer 1',
577
- filter_case_sensitive: true
1059
+ page = client.list_translation_memory_segments('YOUR_TM_ID',
1060
+ filter_text: 'Nummer 1',
1061
+ filter_case_sensitive: true)
578
1062
  ```
579
1063
 
580
1064
  #### Importing a translation memory
581
1065
 
582
- `translation_memories.import_from_filepath` creates a new translation memory
583
- from a TMX file. It creates the import job, uploads the file and waits for the
584
- processing to finish, and returns the finished `TranslationMemoryJob`. Its
585
- result carries the ID of the newly created translation memory.
1066
+ `import_translation_memory_from_filepath` creates a new translation memory from
1067
+ a TMX file. It creates the import job, uploads the file and waits until the job
1068
+ is completed, and returns the `TranslationMemoryJob`. Its result carries the ID
1069
+ of the newly created translation memory. The optional keywords are
1070
+ `display_name` (the name of the translation memory, defaults to the file name),
1071
+ `content_type` (the MIME type of the file, declared in the job and sent with the
1072
+ upload, defaults to `application/xml`), `timeout_s` and `extra_body_parameters`
1073
+ (fields added to the body of the import job request). It raises an
1074
+ `ArgumentError` before any request if the file does not exist, and a
1075
+ `DeepL::Exceptions::Error` if the file cannot be read, with the API's error
1076
+ message if the job fails or expires, and when `timeout_s` expires.
586
1077
 
587
1078
  ```rb
588
- job = DeepL.translation_memories.import_from_filepath 'legal.tmx',
589
- display_name: 'Legal',
590
- timeout_s: 300
1079
+ job = client.import_translation_memory_from_filepath('legal.tmx',
1080
+ display_name: 'Legal',
1081
+ timeout_s: 300)
591
1082
 
592
1083
  puts job.class
593
1084
  # => DeepL::Resources::TranslationMemoryJob
@@ -599,81 +1090,100 @@ puts job.result.skipped_segment_count
599
1090
  # => 0
600
1091
  ```
601
1092
 
602
- The three steps can also be performed separately, for example to upload a file
603
- that is not available on the local file system. The upload URL is a pre-signed
604
- storage URL outside of the DeepL API, so no authorization header is sent with
605
- the upload.
1093
+ The three steps can also be performed separately with
1094
+ `create_translation_memory_import`, `upload_translation_memory_file` and
1095
+ `wait_until_translation_memory_job_done`, for example to upload a file that is
1096
+ not available on the local file system. `create_translation_memory_import`
1097
+ accepts `content_type`, `display_name` and `extra_body_parameters`; keywords left
1098
+ `nil` are not sent. The upload URL is a pre-signed storage URL outside of the
1099
+ DeepL API, so no authorization header and, of the configured `headers`, only a
1100
+ `User-Agent` is sent with the upload; the proxy, SSL and timeout settings of the client apply. A non-2xx
1101
+ reply of a storage URL (for example 403 for an expired URL) raises a plain
1102
+ `DeepL::Exceptions::Error` carrying `http_status_code`; its `should_retry?` is
1103
+ `true` for 429 and 5xx.
606
1104
 
607
1105
  ```rb
608
- content = File.binread 'legal.tmx'
609
- created = DeepL.translation_memories.create_import 'legal.tmx', content.bytesize,
610
- content_type: 'application/xml',
611
- display_name: 'Legal'
1106
+ content = File.binread('legal.tmx')
1107
+ created = client.create_translation_memory_import('legal.tmx', content.bytesize,
1108
+ content_type: 'application/xml',
1109
+ display_name: 'Legal')
612
1110
 
613
1111
  puts created.upload_url
614
1112
  # => 'https://...'
615
1113
 
616
- DeepL.translation_memories.upload_file created, content
617
- job = DeepL.translation_memories.wait_until_job_done created.job_id
1114
+ client.upload_translation_memory_file(created, content, content_type: 'application/xml')
1115
+ job = client.wait_until_translation_memory_job_done(created.job_id)
618
1116
  ```
619
1117
 
620
1118
  Until the file is uploaded the job stays in the `awaiting_input` status and
621
1119
  `result.required_action` describes what is missing. The API detects the upload
622
1120
  asynchronously, so the job keeps reporting `awaiting_input` for a while
623
1121
  afterwards, typically around half a minute, before it completes.
624
- `wait_until_job_done` therefore polls through that status like any other
625
- non-terminal one. A job whose file is never uploaded does not finish on its own,
626
- so pass `timeout_s` when that is a possibility.
1122
+ `wait_until_translation_memory_job_done` therefore polls through that status
1123
+ like any other non-terminal one. A job whose file is never uploaded does not
1124
+ finish on its own, and a status the library does not know keeps it polling too, so pass
1125
+ `timeout_s` to bound the wait.
627
1126
 
628
1127
  #### Exporting a translation memory
629
1128
 
630
- `translation_memories.export_to_filepath` writes a translation memory to a TMX
631
- file. It creates the export job, waits for it to finish and downloads the
632
- result, overwriting the output file if it already exists.
1129
+ `export_translation_memory_to_filepath` writes a translation memory to a TMX
1130
+ file. It creates the export job, waits until it is completed and downloads the
1131
+ result, overwriting the output file if it already exists. It accepts
1132
+ `timeout_s` and raises a `DeepL::Exceptions::Error` if the job fails or expires,
1133
+ if the output directory does not exist (checked before the job is created), and
1134
+ if the file cannot be written.
633
1135
 
634
1136
  ```rb
635
- job = DeepL.translation_memories.export_to_filepath 'YOUR_TM_ID', 'export.tmx'
1137
+ job = client.export_translation_memory_to_filepath('YOUR_TM_ID', 'export.tmx')
636
1138
 
637
1139
  puts job.status
638
1140
  # => 'completed'
639
1141
  ```
640
1142
 
641
- The steps can be performed separately as well. Repeating the export of an
642
- unchanged translation memory reuses the previously completed job instead of
643
- starting a new one, which `reused_existing?` reports. Just like the upload URL,
644
- the download URL is a pre-signed storage URL and is requested without an
645
- authorization header.
1143
+ The steps can be performed separately as well, with
1144
+ `create_translation_memory_export`, `wait_until_translation_memory_job_done` and
1145
+ `download_translation_memory_export`. Repeating the export of an unchanged
1146
+ translation memory reuses the previously completed job instead of starting a new
1147
+ one, which `reused_existing?` reports. Just like the upload URL, the download URL
1148
+ is a pre-signed storage URL and is requested without an authorization header.
646
1149
 
647
1150
  ```rb
648
- created = DeepL.translation_memories.create_export 'YOUR_TM_ID'
1151
+ created = client.create_translation_memory_export('YOUR_TM_ID')
649
1152
 
650
1153
  puts created.reused_existing?
651
1154
  # => false
652
1155
 
653
- job = DeepL.translation_memories.wait_until_job_done created.job_id
1156
+ job = client.wait_until_translation_memory_job_done(created.job_id)
654
1157
 
655
1158
  puts job.result.download_url
656
1159
  # => 'https://...'
657
1160
 
658
- DeepL.translation_memories.download_export job, 'export.tmx'
1161
+ client.download_translation_memory_export(job, 'export.tmx')
659
1162
  ```
660
1163
 
661
1164
  #### Tracking import and export jobs
662
1165
 
663
- `translation_memories.find_job` returns the current status of an import or
664
- export job, and `translation_memories.wait_until_job_done` polls it every five
665
- seconds until it finished, raising if the job failed or expired. Pass
666
- `timeout_s` to give up after a number of seconds instead of waiting forever.
1166
+ `get_translation_memory_job` returns the current status of an import or export
1167
+ job, and `wait_until_translation_memory_job_done` polls it every five seconds
1168
+ until it is completed, raising a `DeepL::Exceptions::Error` with the API's error
1169
+ message if the job failed or expired. A status the library does not know keeps it
1170
+ polling. Pass `timeout_s` to give up after a number of seconds instead of
1171
+ waiting forever; the last wait is cut so that the call never waits longer than
1172
+ `timeout_s`.
1173
+
1174
+ `done?` is `true` once the job is completed, `ok?` is `false` once it failed or
1175
+ expired (and `true` while it runs), and `finished?` is `true` for any terminal
1176
+ status.
667
1177
 
668
1178
  ```rb
669
- job = DeepL.translation_memories.find_job 'YOUR_JOB_ID'
1179
+ job = client.get_translation_memory_job('YOUR_JOB_ID')
670
1180
 
671
1181
  puts job.operation
672
1182
  # => 'import'
673
1183
  puts job.status
674
1184
  # => 'processing'
675
- puts job.finished?
676
- # => false
1185
+ puts [job.done?, job.ok?, job.finished?].inspect
1186
+ # => [false, true, false]
677
1187
  ```
678
1188
 
679
1189
  The status is one of `awaiting_input`, `processing`, `completed`, `downloaded`,
@@ -681,16 +1191,25 @@ The status is one of `awaiting_input`, `processing`, `completed`, `downloaded`,
681
1191
 
682
1192
  #### Deleting a translation memory
683
1193
 
684
- `translation_memories.destroy` deletes a translation memory and returns its ID.
1194
+ `delete_translation_memory` deletes a translation memory. It returns `nil`.
685
1195
 
686
1196
  ```rb
687
- DeepL.translation_memories.destroy 'YOUR_TM_ID'
688
- # => 'YOUR_TM_ID'
1197
+ client.delete_translation_memory('YOUR_TM_ID')
689
1198
  ```
690
1199
 
1200
+ The deprecated module-level `DeepL.translation_memories` (`list`, `find`,
1201
+ `segments`, `destroy`, `create_import`, `upload_file`, `import_from_filepath`,
1202
+ `create_export`, `download_export`, `export_to_filepath`, `find_job`,
1203
+ `wait_until_job_done`) keeps working unchanged. Its `import_from_filepath` also
1204
+ accepts `content_type` and `extra_body_parameters`, and `create_import` accepts
1205
+ `extra_body_parameters`. Its file helpers raise `DeepL::Exceptions::Error` for a
1206
+ missing input file and the raw `Errno::*` error for other local file failures,
1207
+ such as an unreadable input file or a missing output directory (checked before
1208
+ the export job is created).
1209
+
691
1210
  #### Using a translation memory in translations
692
1211
 
693
- Pass the `translation_memory` parameter to `translate` to use a translation
1212
+ Pass the `translation_memory` option to `translate_text` to use a translation
694
1213
  memory. You can pass either a string containing the translation memory ID, or a
695
1214
  `TranslationMemory` object. Use `translation_memory_threshold` to control the
696
1215
  minimum matching percentage for fuzzy matches (0-100, recommended minimum of
@@ -698,71 +1217,248 @@ minimum matching percentage for fuzzy matches (0-100, recommended minimum of
698
1217
 
699
1218
  ```rb
700
1219
  # Using a translation memory ID
701
- translation = DeepL.translate 'Hello, world!', 'EN', 'DE',
702
- translation_memory: 'YOUR_TM_ID',
703
- translation_memory_threshold: 80
1220
+ translation = client.translate_text('Hello, world!', 'EN', 'DE',
1221
+ translation_memory: 'YOUR_TM_ID',
1222
+ translation_memory_threshold: 80)
704
1223
 
705
1224
  # Or using a TranslationMemory object
706
- translation_memories = DeepL.translation_memories.list
707
- translation = DeepL.translate 'Hello, world!', 'EN', 'DE',
708
- translation_memory: translation_memories.first
1225
+ translation_memories = client.list_translation_memories
1226
+ translation = client.translate_text('Hello, world!', 'EN', 'DE',
1227
+ translation_memory: translation_memories.first)
709
1228
  ```
710
1229
 
711
1230
  The same `translation_memory` and `translation_memory_threshold` options can be
712
- passed to document translation via `DeepL.document.upload` (or
713
- `DeepL.document.translate_document`). The `translation_memory` option accepts
714
- either a translation memory ID string or a `TranslationMemory` object:
1231
+ passed to document translation via `translate_document_upload` (or
1232
+ `translate_document`). The `translation_memory` option accepts either a
1233
+ translation memory ID string or a `TranslationMemory` object:
715
1234
 
716
1235
  ```rb
717
- handle = DeepL.document.upload 'my_document.docx', 'EN', 'DE', 'my_document.docx',
718
- translation_memory: 'YOUR_TM_ID',
719
- translation_memory_threshold: 80
1236
+ handle = client.translate_document_upload('my_document.docx', 'EN', 'DE',
1237
+ translation_memory: 'YOUR_TM_ID',
1238
+ translation_memory_threshold: 80)
720
1239
  ```
721
1240
 
722
1241
  ### Monitor usage
723
1242
 
724
- To check current API usage, use:
1243
+ To check current API usage, call `get_usage`. It returns a `DeepL::Resources::Usage`. Each
1244
+ usage type is a `DeepL::Resources::Usage::Detail` with `count`, `limit` and `limit_reached?`:
1245
+ `character`, plus `document` and `team_document` for accounts with a document limit.
1246
+ `any_limit_reached?` checks `character`, `document` and `team_document`:
725
1247
 
726
1248
  ```rb
727
- usage = DeepL.usage
1249
+ usage = client.get_usage
728
1250
 
729
- puts usage.character_count
1251
+ puts usage.character.count
730
1252
  # => 180118
731
- puts usage.character_limit
1253
+ puts usage.character.limit
732
1254
  # => 1250000
1255
+ puts 'Translation limit reached.' if usage.any_limit_reached?
1256
+ puts "Document usage: #{usage.document}" if usage.document
1257
+ # => Document usage: 5 of 10
1258
+ ```
1259
+
1260
+ `DeepL.usage` and the `Usage` readers `character_count`, `character_limit` and
1261
+ `quota_exceeded?` are deprecated. Use `client.get_usage`, `character.count`,
1262
+ `character.limit` and `character.limit_reached?` (or `any_limit_reached?`) instead.
1263
+
1264
+ For API Pro users, the response also contains this API key's usage, speech usage, the billing
1265
+ period and a per-product breakdown. These fields are `nil` when the response does not contain
1266
+ them (e.g. for API Free users). `any_limit_reached?` does not consider them; call
1267
+ `limit_reached?` on them directly, e.g. `usage.api_key_character.limit_reached?`:
1268
+
1269
+ ```rb
1270
+ usage = client.get_usage
1271
+
1272
+ # Characters used by the whole account vs. by this API key
1273
+ puts "account: #{usage.character}"
1274
+ # => account: 5947223 of 1000000000000
1275
+ puts "this key: #{usage.api_key_character}"
1276
+ # => this key: 636 of 1000000000000
1277
+
1278
+ puts usage.speech_to_text_minutes # => 30 of 600
1279
+ puts usage.speech_to_speech_minutes # => 12 of 600
1280
+ puts usage.start_time # => 2025-05-13 09:18:42 UTC (Time)
1281
+ puts usage.end_time # => 2025-06-13 09:18:42 UTC (Time)
1282
+
1283
+ usage.products&.each do |product|
1284
+ # product_type and billing_unit are plain strings, e.g. 'translate' and 'characters'
1285
+ puts "#{product.product_type}: key #{product.api_key_unit_count}, " \
1286
+ "account #{product.account_unit_count} #{product.billing_unit}"
1287
+ end
1288
+ # => translate: key 636, account 5941580 characters
733
1289
  ```
734
1290
 
735
1291
  ### Translate documents
736
1292
 
737
- To translate a document, use the `document.translate_document` method. Example:
1293
+ To translate a document, use the `translate_document` method of `DeepL::Client`. It uploads
1294
+ the document, waits until the translation is done and writes the translated document to the
1295
+ output path, which must not exist yet. It returns the final
1296
+ `DeepL::Resources::DocumentTranslationStatus`:
738
1297
 
739
1298
  ```rb
740
- DeepL.document.translate_document('/path/to/spanish_document.pdf', '/path/to/translated_document.pdf', 'ES', 'EN')
1299
+ status = client.translate_document('/path/to/spanish_document.pdf',
1300
+ '/path/to/translated_document.pdf', 'ES', 'EN')
1301
+ puts status.billed_characters
1302
+ # => 2045
741
1303
  ```
742
1304
 
743
- The lower level `upload`, `get_status` and `download` methods are also exposed, as well as the convenience method `wait_until_document_translation_finished` on the `DocumentHandle` object, which would replace `get_status`:
1305
+ A missing input file raises `ArgumentError` before anything is sent. Every other failure (also
1306
+ an existing output file, which is kept, and a failed translation) raises
1307
+ `DeepL::Exceptions::DocumentTranslationError`. Once the upload succeeded, its `document_handle`
1308
+ is set, so you can still query the status and download the document. If a DeepL error caused
1309
+ it, its `http_status_code` and `should_retry?` are copied from that error. `timeout_s:` limits the wait after the upload in seconds; by default
1310
+ `translate_document` waits until the translation is done or failed:
1311
+
1312
+ ```rb
1313
+ begin
1314
+ client.translate_document('/path/to/document.docx', '/path/to/translated.docx', 'EN', 'DE',
1315
+ timeout_s: 600, formality: 'more')
1316
+ rescue DeepL::Exceptions::DocumentTranslationError => e
1317
+ puts "Translation failed: #{e.message}, document: #{e.document_handle&.document_id}"
1318
+ end
1319
+ ```
1320
+
1321
+ The steps are also exposed. `translate_document_upload` returns a
1322
+ `DeepL::Resources::DocumentHandle` (`document_id`, `document_key`). To resume from a stored ID
1323
+ and key, create the handle with `DeepL::Resources::DocumentHandle.new(document_id, document_key)`.
1324
+ `translate_document_wait_until_done(handle, timeout_s: nil)` queries the status every 5 seconds
1325
+ until the translation is done and returns that status; it raises `DeepL::Exceptions::Error`
1326
+ with the API's error message if the translation fails, and when `timeout_s` expires. A status
1327
+ the library does not know keeps it polling, so pass `timeout_s` to bound the wait.
1328
+ `translate_document_get_status` queries the status once, and `translate_document_download`
1329
+ writes the translated document, overwriting an existing file; it raises
1330
+ `DeepL::Exceptions::DocumentNotReadyError` if the translation is not done yet.
1331
+
1332
+ ```rb
1333
+ handle = client.translate_document_upload('/path/to/spanish_document.pdf', 'ES', 'EN',
1334
+ filename: 'report.pdf')
1335
+ status = client.translate_document_get_status(handle)
1336
+ puts "#{status.status}, ok: #{status.ok?}, done: #{status.done?}"
1337
+ # => translating, ok: true, done: false
1338
+ client.translate_document_wait_until_done(handle)
1339
+ client.translate_document_download(handle, '/path/to/translated_document.pdf')
1340
+ ```
1341
+
1342
+ A `DeepL::Resources::DocumentTranslationStatus` has `document_id`, `status` (a string;
1343
+ `DeepL::Constants::DocumentStatusCode` has the values `QUEUED`, `TRANSLATING`, `DONE` and
1344
+ `ERROR`), `seconds_remaining`, `billed_characters` and `error_message`, and the predicates
1345
+ `done?` (done), `error?` (failed), `ok?` (not failed, also while queued or translating)
1346
+ and `finished?` (done or failed). `successful?` is a deprecated alias of `done?`.
1347
+
1348
+ With the deprecated module-level API, the same methods are `DeepL.document.translate_document`,
1349
+ `upload`, `get_status` and `download`, which take the filename as a positional parameter and
1350
+ optional additional headers. `DeepL.document.translate_document` waits until the translation is
1351
+ done or failed and returns the number of bytes written; the deprecated configuration setting
1352
+ `max_doc_status_queries` limits the number of status requests. The deprecated
1353
+ `DocumentHandle#wait_until_document_translation_finished` returns the final status also when
1354
+ the translation failed. `DeepL.document.translate_document` raises
1355
+ `DeepL::Exceptions::DocumentTranslationError` if the output file already exists.
1356
+
744
1357
  ```rb
745
1358
  doc_handle = DeepL.document.upload('/path/to/spanish_document.pdf', 'ES', 'EN')
746
- doc_status = doc_handle.wait_until_document_translation_finished # alternatively poll `DeepL.document.get_status`
747
- # until the `doc_status.successful?`
1359
+ doc_status = doc_handle.wait_until_document_translation_finished
748
1360
  DeepL.document.download(doc_handle, '/path/to/translated_document.pdf') unless doc_status.error?
749
1361
  ```
750
1362
 
751
1363
  You can also pass additional options to document translation methods, including `extra_body_parameters`:
752
1364
 
753
1365
  ```rb
754
- options = {
755
- formality: 'more',
756
- extra_body_parameters: { example_param: 'true' }
757
- }
758
- doc_handle = DeepL.document.upload('/path/to/spanish_document.pdf', 'ES', 'EN', nil, options)
1366
+ doc_handle = client.translate_document_upload('/path/to/spanish_document.pdf', 'ES', 'DE',
1367
+ formality: 'more',
1368
+ extra_body_parameters: { example_param: 'true' })
1369
+ ```
1370
+
1371
+ Document translation options include:
1372
+
1373
+ - `filename` (`DeepL::Client` only): the file name sent with the upload, including its
1374
+ extension. Defaults to the base name of the input path.
1375
+ - `formality`: same as for text translation.
1376
+ - `glossary` / `glossary_ids`: see the glossary sections above. `glossary_id` is deprecated,
1377
+ use `glossary`.
1378
+ - `output_format`: file extension of the desired output format, e.g. `'pdf'` to translate
1379
+ a `docx` into a PDF.
1380
+ - `enable_watermark`: `true` adds a "Translated by DeepL" watermark to the translated
1381
+ document. The API supports it only for `docx` and `pdf` output and ignores it for other
1382
+ formats. When not set, the parameter is not sent.
1383
+ - `style_rule`, `style_profile`, `translation_memory`, `translation_memory_threshold`:
1384
+ same as in text translation; see the style rules, style profiles and translation memory
1385
+ sections above.
1386
+
1387
+ ```rb
1388
+ client.translate_document('/path/to/document.docx', '/path/to/translated.pdf', 'EN', 'DE',
1389
+ output_format: 'pdf', enable_watermark: true)
759
1390
  ```
760
1391
 
761
1392
  The `extra_body_parameters` option allows you to pass arbitrary parameters in the request body. This can be used to access beta features by adding new parameters, or to override built-in parameters (such as `target_lang`, `source_lang`, etc.) for testing purposes.
762
1393
 
1394
+ ### Admin API
1395
+
1396
+ The Admin API lets you manage the developer keys of your organization and retrieve usage analytics. It requires an admin API key, which you can create in the "Admin API Keys" tab of your DeepL account. Admin keys cannot be used for translation, and developer keys cannot access the Admin API: the API rejects a wrong key with `DeepL::Exceptions::AuthorizationFailed`.
1397
+
1398
+ Create a `DeepL::AdminClient` with your admin key. It is independent of `DeepL.configure`, does not read the `DEEPL_AUTH_KEY` and `DEEPL_SERVER_URL` environment variables, and uses the `https://api.deepl.com` server by default. The admin key is trimmed. It also accepts the `server_url:`, `headers:`, `max_retries:`, `timeout:`, `proxy:`, `send_platform_info:`, `app_info:`, `http_client_options:` and `logger:` keyword arguments of [`DeepL::Client`](#creating-a-client); without `server_url:` it uses `https://api.deepl.com`, also for keys ending in `:fx`.
1399
+
1400
+ ```rb
1401
+ admin_client = DeepL::AdminClient.new('YOUR_ADMIN_KEY')
1402
+ ```
1403
+
1404
+ #### Managing developer keys
1405
+
1406
+ ```rb
1407
+ key = admin_client.create_developer_key(label: 'Staging Team Key')
1408
+ puts "Created #{key.label} (#{key.key_id})"
1409
+
1410
+ admin_client.list_developer_keys.each do |developer_key|
1411
+ puts "#{developer_key.label}: deactivated=#{developer_key.deactivated?}"
1412
+ end
1413
+
1414
+ key = admin_client.rename_developer_key(key, 'Production Team Key')
1415
+
1416
+ # Changes only the given limits; the others keep their current value.
1417
+ # nil removes a limit (unlimited) and 0 blocks that usage.
1418
+ key = admin_client.set_developer_key_usage_limits(key, characters: 500_000,
1419
+ speech_to_text_milliseconds: 60_000)
1420
+ key = admin_client.set_developer_key_usage_limits(key, speech_to_speech_milliseconds: nil)
1421
+
1422
+ # Deactivation is permanent.
1423
+ admin_client.deactivate_developer_key(key)
1424
+ ```
1425
+
1426
+ `rename_developer_key`, `set_developer_key_usage_limits` and `deactivate_developer_key` accept either a key ID or a `DeepL::Resources::DeveloperKey` object and return the updated `DeepL::Resources::DeveloperKey`. `set_developer_key_usage_limits` accepts `characters:`, `speech_to_text_milliseconds:` and `speech_to_speech_milliseconds:`. `create_developer_key` takes the label as the `label:` keyword argument; without it the API labels the key "DeepL API Key". Labels and limits are sent as given and validated by the API. `create_developer_key`, `deactivate_developer_key`, `rename_developer_key` and `set_developer_key_usage_limits` also accept `extra_body_parameters:`, a hash added to the request body last that replaces fields of the same name. A response without a `key_id` raises `DeepL::Exceptions::Error`. `label` and `usage_limits` are `nil` when the response does not contain them.
1427
+
1428
+ A failed `create_developer_key` request is retried only after HTTP 429, a DNS resolution error or a connection timeout, when the API cannot have created the key. Other requests are retried like all other requests of the library.
1429
+
1430
+ #### Retrieving usage analytics
1431
+
1432
+ `get_usage_analytics` returns a `DeepL::Resources::AdminUsageReport` for a date range. Dates are UTC calendar days, and both the start day and the end day are included. They can be `Date`, `Time` or `DateTime` objects or `YYYY-MM-DD` strings. For `Time` and `DateTime` values the SDK sends the UTC date of that moment, so `Time.new(2026, 10, 20, 0, 0, 0, '+02:00')` is sent as `2026-10-19`. An invalid date raises `ArgumentError` without sending a request. The API requires the end date to be after the start date, so a report covers at least two days, and at most 366 days after it; otherwise both analytics methods raise `DeepL::Exceptions::BadRequest`. Use `group_by:` to break the usage down by key (`DeepL::Constants::AdminUsageGroupBy::KEY`, see `key_usages`) or by key and day (`DeepL::Constants::AdminUsageGroupBy::KEY_AND_DAY`, see `key_and_day_usages`). The `group_by` of the report is the raw string the API returns. The usage breakdowns include `speech_to_text_minutes` and `speech_to_speech_minutes`. `speech_to_speech_minutes` of a usage breakdown, and `total_usage`, `start_date` and `end_date` of a report, are `nil` when the response does not contain them.
1433
+
1434
+ ```rb
1435
+ report = admin_client.get_usage_analytics('2026-06-01', '2026-07-01',
1436
+ group_by: DeepL::Constants::AdminUsageGroupBy::KEY)
1437
+ puts "Total characters: #{report.total_usage&.total_characters}"
1438
+ report.key_usages.each do |key_usage|
1439
+ puts "#{key_usage.api_key_label}: #{key_usage.usage.total_characters}"
1440
+ end
1441
+ ```
1442
+
1443
+ `get_custom_tag_usage_analytics` returns a `DeepL::Resources::CustomTagUsageReport` with the usage per custom tag, for a date range with the same limits. Use `aggregate_by:` to choose between the whole period (`DeepL::Constants::CustomTagUsageAggregateBy::PERIOD`, the default) and per-day usage (`DeepL::Constants::CustomTagUsageAggregateBy::DAY`). The results are paginated: pass the `next_page` value of a report as `page:` to get the next page. The `aggregate_by` of a custom tag report is the raw string the API returns. `speech_to_speech_minutes` of a custom tag breakdown is `nil` when the response does not contain it.
1444
+
1445
+ ```rb
1446
+ page = nil
1447
+ loop do
1448
+ report = admin_client.get_custom_tag_usage_analytics('2026-06-01', '2026-07-01', page: page)
1449
+ report.usage.each do |tag_usage|
1450
+ puts "#{tag_usage.custom_tag}: #{tag_usage.breakdown.total_characters}"
1451
+ end
1452
+ break if report.next_page.nil?
1453
+
1454
+ page = report.next_page
1455
+ end
1456
+ ```
1457
+
763
1458
  ### Sending additional HTTP headers
764
1459
 
765
- You can pass additional HTTP headers to `translate`, `rephrase`, and the
1460
+ `DeepL::Client.new(key, headers: { 'X-DeepL-Reporting-Tag' => 'my-tag' })` sends the headers
1461
+ with every DeepL API request of that client. With the module-level API, you can pass additional HTTP headers to `translate`, `rephrase` and the
766
1462
  `document` methods. For example, to send the `X-DeepL-Reporting-Tag` header
767
1463
  for usage reporting (see the [cookbook entry](https://developers.deepl.com/docs/learning-how-tos/cookbook/sending-custom-reporting-tags-from-client-libraries)):
768
1464
 
@@ -772,6 +1468,12 @@ translation = DeepL.translate 'Hello, world!', 'EN', 'DE', {}, additional_header
772
1468
  rephrased = DeepL.rephrase 'Hello, world!', 'EN', nil, nil, {}, additional_headers
773
1469
  ```
774
1470
 
1471
+ A header in `headers:` (or `config.headers`) or in the additional headers replaces a library
1472
+ header of the same name, in any casing, `Authorization` and `User-Agent` included, except
1473
+ `Content-Type`, `Content-Length`, `Transfer-Encoding` and `Host`, which the library always sets.
1474
+ Pre-signed translation memory storage URLs get only the `User-Agent`, the caller's if one is
1475
+ set.
1476
+
775
1477
  ### Handle exceptions
776
1478
 
777
1479
  You can capture and process exceptions that may be raised during API calls. These are all the possible exceptions:
@@ -780,13 +1482,20 @@ You can capture and process exceptions that may be raised during API calls. Thes
780
1482
  | --------------- | ----------- |
781
1483
  | `DeepL::Exceptions::AuthorizationFailed` | The authorization process has failed. Check your `auth_key` value. |
782
1484
  | `DeepL::Exceptions::BadRequest` | Something is wrong in your request. Check `exception.message` for more information. |
783
- | `DeepL::Exceptions::DocumentTranslationError` | An error occured during document translation. Check `exception.message` for more information. |
1485
+ | `DeepL::Exceptions::ConnectionError` | Raised by `DeepL::Client` and `DeepL::AdminClient` (the module-level API raises the original network error). No complete HTTP response from the API: a DNS, connection or timeout failure, or a response body cut off before its `Content-Length`, after all retries (`should_retry?` is true), or a TLS failure, a proxy failure (the proxy refuses `CONNECT`) or a malformed response, raised without retrying (`should_retry?` is false). The original exception is `exception.cause`. |
1486
+ | `DeepL::Exceptions::DocumentNotReadyError` | A subclass of `ServerError`: HTTP 503 on a document download, the translated document is not ready yet. |
1487
+ | `DeepL::Exceptions::DocumentTranslationError` | An error occured during document translation. Check `exception.message` for more information; `exception.document_handle` is the handle of the uploaded document, `nil` if the upload failed; `http_status_code` and `should_retry?` are those of the DeepL error that caused it, if any. |
784
1488
  | `DeepL::Exceptions::LimitExceeded` | You've reached the API's call limit. |
785
1489
  | `DeepL::Exceptions::QuotaExceeded` | You've reached the API's character limit. |
786
- | `DeepL::Exceptions::RequestError` | An unkown request error. Check `exception.response` and `exception.request` for more information. |
787
- | `DeepL::Exceptions::NotSupported` | The requested method or API endpoint is not supported. |
1490
+ | `DeepL::Exceptions::RequestError` | Any other HTTP error status. Its message contains the status and the API's message; check `exception.response` and `exception.request` for more information. |
1491
+ | `DeepL::Exceptions::NotSupported` | Deprecated. Raised only by the module-level API: `supports_formality?` of a source language from `DeepL.languages`. |
788
1492
  | `DeepL::Exceptions::RequestEntityTooLarge` | Your request is too large, reduce the amount of data you are sending. The API has a request size limit of 128 KiB. |
789
- | `DeepL::Exceptions::ServerError` | An error occured in the DeepL API, wait a short amount of time and retry. |
1493
+ | `DeepL::Exceptions::ServerError` | An error occured in the DeepL API (HTTP 5xx), raised after all retries. |
1494
+ | `DeepL::Exceptions::ValidationError` | An invalid argument detected before any request: an invalid glossary or spoken terms term, or an unsupported `DeepL.languages` type. |
1495
+
1496
+ All of them are subclasses of `DeepL::Exceptions::Error`, which is also raised for a response
1497
+ that cannot be parsed. Errors of an HTTP response carry the status as an `Integer` in
1498
+ `exception.http_status_code` (`nil` otherwise).
790
1499
 
791
1500
  An exampling of handling a generic exception:
792
1501
 
@@ -811,37 +1520,61 @@ require 'logger'
811
1520
  logger = Logger.new(STDOUT)
812
1521
  logger.level = Logger::INFO
813
1522
 
814
- deepl.configure do |config|
815
- config.auth_key = configuration.auth_key
1523
+ DeepL.configure do |config|
1524
+ config.auth_key = ENV.fetch('DEEPL_AUTH_KEY')
816
1525
  config.logger = logger
817
1526
  end
818
1527
  ```
819
1528
 
1529
+ ### Retries and timeouts
1530
+
1531
+ Requests that fail with HTTP 429, any 5xx status or a transient network failure (DNS, refused
1532
+ or reset connection, broken pipe, unreachable host, timeout, response body cut off before its
1533
+ `Content-Length`) are retried up to `max_retries:`
1534
+ times (default 5) with exponential backoff; a request is sent at most `max_retries:` + 1 times.
1535
+ Each attempt has a timeout of `timeout:` seconds (default 10; the module-level
1536
+ `config.timeout` defaults to 60). After the last retry, network failures raise
1537
+ `DeepL::Exceptions::ConnectionError` in `DeepL::Client` and `DeepL::AdminClient`; the
1538
+ module-level API raises the original network error. TLS failures (including certificate
1539
+ verification), proxy failures (the proxy refuses `CONNECT`, e.g. with 407) and malformed
1540
+ responses are not retried and are raised the same way at once.
1541
+ `AdminClient#create_developer_key` is only retried when the request was not sent (HTTP 429,
1542
+ DNS failure, refused connection or connection timeout). With the module-level API, set
1543
+ `config.max_network_retries` and `config.timeout`.
1544
+
820
1545
  ### Proxy configuration
821
1546
 
822
- To use HTTP proxies, a session needs to be used. The proxy can then be configured as part of the HTTP client options:
1547
+ Pass a proxy URL to the client:
1548
+
1549
+ ```rb
1550
+ client = DeepL::Client.new(auth_key, proxy: 'http://user:password@localhost:3128')
1551
+ ```
1552
+
1553
+ `DeepL::HTTPClientOptions` also takes the proxy as a URL or as a hash with `proxy_addr` and
1554
+ `proxy_port`, together with certificate, SSL verification and timeout options. With the
1555
+ module-level API, a session needs to be used; the proxy is configured as part of the HTTP client options:
823
1556
 
824
1557
  ```rb
825
- client_options = HTTPClientOptions.new({ 'proxy_addr' => 'http://localhost', 'proxy_port' => 80 })
826
- deepl.with_session(client_options) do |session|
1558
+ client_options = DeepL::HTTPClientOptions.new({ 'proxy_addr' => 'http://localhost', 'proxy_port' => 80 })
1559
+ DeepL.with_session(client_options) do |session|
827
1560
  # ...
828
1561
  end
829
1562
  ```
830
1563
 
831
1564
  ### Anonymous platform information
832
1565
 
833
- By default, we send some basic information about the platform the client library is running on with each request, see [here for an explanation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent). This data is completely anonymous and only used to improve our product, not track any individual users. If you do not wish to send this data, you can opt-out by setting the `send_platform_info` flag in the configuration to `false` like so:
1566
+ By default, we send some basic information about the platform the client library is running on with each request, see [here for an explanation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent). This data is completely anonymous and only used to improve our product, not track any individual users. If you do not wish to send this data, create the client with `send_platform_info: false`, or with the module-level API set the `User-Agent` to the library name and version only:
834
1567
 
835
1568
  ```rb
836
- deepl.configure({}, nil, nil, false) do |config|
837
- # ...
1569
+ DeepL.configure do |config|
1570
+ config.user_agent = "deepl-ruby/#{DeepL::VERSION}"
838
1571
  end
839
1572
  ```
840
1573
 
841
- You can also complete customize the `User-Agent` header like so:
1574
+ You can also completely replace the `User-Agent` header with `headers: { 'User-Agent' => 'myCustomUserAgent' }`, or with the module-level API like so:
842
1575
 
843
1576
  ```rb
844
- deepl.configure do |config|
1577
+ DeepL.configure do |config|
845
1578
  config.user_agent = 'myCustomUserAgent'
846
1579
  end
847
1580
  ```
@@ -851,10 +1584,10 @@ end
851
1584
  When writing an application that send multiple requests, using a HTTP session will give better performance through [HTTP Keep-Alive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive). You can use it by simply wrapping your requests in a `with_session` block:
852
1585
 
853
1586
  ```rb
854
- deepl.with_session do |session|
855
- deepl.translate(sentence1, 'DE', 'EN-GB')
856
- deepl.translate(sentence2, 'DE', 'EN-GB')
857
- deepl.translate(sentence3, 'DE', 'EN-GB')
1587
+ DeepL.with_session do |session|
1588
+ DeepL.translate(sentence1, 'DE', 'EN-GB')
1589
+ DeepL.translate(sentence2, 'DE', 'EN-GB')
1590
+ DeepL.translate(sentence3, 'DE', 'EN-GB')
858
1591
  end
859
1592
  ```
860
1593
 
@@ -863,12 +1596,19 @@ end
863
1596
  If you use this library in an application, please identify the application by setting the name and version of the plugin:
864
1597
 
865
1598
  ```rb
866
- deepl.configure({}, 'MyTranslationPlugin', '1.0.1') do |config|
867
- # ...
1599
+ client = DeepL::Client.new(auth_key, app_info: { app_name: 'MyTranslationPlugin',
1600
+ app_version: '1.0.1' })
1601
+ ```
1602
+
1603
+ With the module-level API, append them to the `User-Agent`:
1604
+
1605
+ ```rb
1606
+ DeepL.configure do |config|
1607
+ config.user_agent = "#{config.user_agent} MyTranslationPlugin/1.0.1"
868
1608
  end
869
1609
  ```
870
1610
 
871
- This information is passed along when the library makes calls to the DeepL API. Both name and version are required. Please note that setting the `User-Agent` header via `deepl.configure` will override this setting, if you need to use this, please manually identify your Application in the `User-Agent` header.
1611
+ This information is passed along when the library makes calls to the DeepL API. Both name and version are required. Please note that setting the `User-Agent` header via `DeepL.configure` will override this setting, if you need to use this, please manually identify your Application in the `User-Agent` header.
872
1612
 
873
1613
  ## Options Constants
874
1614
 
@@ -884,14 +1624,14 @@ The available values for various possible options are provided under the `DeepL:
884
1624
  To view all the possible options for a given constant, call `options`:
885
1625
 
886
1626
  ```rb
887
- all_available_tones = DeepL::Constants::Tones.options
1627
+ all_available_tones = DeepL::Constants::Tone.options
888
1628
  ```
889
1629
 
890
1630
  To check if a given string is a possible option for a given constant, call `valid?`:
891
1631
 
892
1632
  ```rb
893
- DeepL::Constants::Tones.valid?('friendly') # true
894
- DeepL::Constants::Tones.valid?('rude') # false
1633
+ DeepL::Constants::Tone.valid?('friendly') # true
1634
+ DeepL::Constants::Tone.valid?('rude') # false
895
1635
  ```
896
1636
 
897
1637
  ## Integrations
@@ -927,7 +1667,25 @@ cd deepl-rb
927
1667
  bundle install
928
1668
  ```
929
1669
 
930
- To run tests (rspec and rubocop), use
1670
+ There are two suites of specs, which must run in separate processes:
1671
+
1672
+ - `spec/offline` tests options, validation, errors, retries and polling against intercepted
1673
+ requests (WebMock). It needs no key and no network:
1674
+
1675
+ ```
1676
+ bundle exec rspec spec/offline
1677
+ ```
1678
+
1679
+ - `spec/real_api` sends every endpoint once to the real API. Set `DEEPL_AUTH_KEY` to a test key,
1680
+ `DEEPL_ADMIN_KEY` to an admin key for the Admin API specs, and `DEEPL_SERVER_URL` if the keys
1681
+ are not for the default server. The specs create resources named `sdktest-ruby-...` and delete
1682
+ them again; developer keys cannot be deleted and are deactivated instead. The Admin API spec creates a key, renames it, sets its limits and deactivates it:
1683
+
1684
+ ```
1685
+ DEEPL_AUTH_KEY=... DEEPL_ADMIN_KEY=... bundle exec rspec spec/real_api
1686
+ ```
1687
+
1688
+ To run the offline specs and rubocop, use
931
1689
 
932
1690
  ```
933
1691
  bundle exec rake test