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
@@ -0,0 +1,77 @@
1
+ # Copyright 2026 DeepL SE (https://www.deepl.com)
2
+ # Use of this source code is governed by an MIT
3
+ # license that can be found in the LICENSE.md file.
4
+ # frozen_string_literal: true
5
+
6
+ require_relative 'real_api_helper'
7
+
8
+ describe DeepL::AdminClient do
9
+ subject(:client) do
10
+ described_class.new(ENV.fetch('DEEPL_ADMIN_KEY'),
11
+ server_url: ENV.fetch('DEEPL_SERVER_URL', nil))
12
+ end
13
+
14
+ before do
15
+ next unless ENV.fetch('DEEPL_ADMIN_KEY', '').empty?
16
+
17
+ raise 'DEEPL_ADMIN_KEY must be set to run the Admin API specs'
18
+ end
19
+
20
+ let(:end_date) { Time.now.utc.to_date }
21
+ let(:start_date) { end_date - 7 }
22
+
23
+ def key_listed?(client, key_id)
24
+ 5.times do |attempt|
25
+ return true if client.list_developer_keys.map(&:key_id).include?(key_id)
26
+
27
+ sleep 1 unless attempt == 4
28
+ end
29
+ false
30
+ end
31
+
32
+ it 'sends GET /v2/admin/analytics and parses the report' do
33
+ report = client.get_usage_analytics(start_date, end_date,
34
+ group_by: DeepL::Constants::AdminUsageGroupBy::KEY_AND_DAY)
35
+
36
+ expect(report.start_date).not_to be_nil
37
+ expect(report.end_date).not_to be_nil
38
+ expect(report.group_by).to eq('keyAndDay')
39
+ end
40
+
41
+ it 'sends GET /v2/admin/analytics/custom-tags and parses the report' do
42
+ report = client.get_custom_tag_usage_analytics(
43
+ start_date, end_date, aggregate_by: DeepL::Constants::CustomTagUsageAggregateBy::DAY
44
+ )
45
+
46
+ expect(report.start_date).not_to be_nil
47
+ expect(report.end_date).not_to be_nil
48
+ expect(report.aggregate_by).to eq('day')
49
+ end
50
+
51
+ it 'sends the developer key requests: create, list, rename, set limits and deactivate' do
52
+ key = client.create_developer_key(label: unique_name('developer-key'))
53
+ failure = nil
54
+ begin
55
+ expect(key.deactivated?).to be(false)
56
+ expect(key_listed?(client, key.key_id)).to be(true), 'created key not listed after 5 attempts'
57
+ renamed = client.rename_developer_key(key, "#{key.label}-renamed")
58
+ expect(renamed.label).to eq("#{key.label}-renamed")
59
+ limited = client.set_developer_key_usage_limits(key, characters: 5000,
60
+ speech_to_text_milliseconds: nil)
61
+ expect(limited.usage_limits).to have_attributes(characters: 5000,
62
+ speech_to_text_milliseconds: nil)
63
+ rescue StandardError, RSpec::Expectations::ExpectationNotMetError => e
64
+ failure = e
65
+ raise
66
+ ensure
67
+ begin
68
+ deactivated = client.deactivate_developer_key(key)
69
+ rescue DeepL::Exceptions::NotFound
70
+ deactivated = nil
71
+ rescue StandardError
72
+ raise if failure.nil?
73
+ end
74
+ end
75
+ expect(deactivated&.deactivated?).to be(true)
76
+ end
77
+ end
@@ -0,0 +1,20 @@
1
+ # Copyright 2026 DeepL SE (https://www.deepl.com)
2
+ # Use of this source code is governed by an MIT
3
+ # license that can be found in the LICENSE file.
4
+ # frozen_string_literal: true
5
+
6
+ require_relative 'real_api_helper'
7
+
8
+ describe 'DeepL::Client#correct_text' do
9
+ let(:client) do
10
+ DeepL::Client.new(ENV.fetch('DEEPL_AUTH_KEY'), server_url: ENV.fetch('DEEPL_SERVER_URL', nil))
11
+ end
12
+
13
+ it 'sends POST /v2/write/correct and parses the result' do
14
+ result = client.correct_text('Protonenstrahl', 'de')
15
+
16
+ expect(result).to be_a(DeepL::Resources::Text)
17
+ expect(result.text).to be_a(String)
18
+ expect(result.detected_source_language).not_to be_empty
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ # Copyright 2026 DeepL SE (https://www.deepl.com)
2
+ # Use of this source code is governed by an MIT
3
+ # license that can be found in the LICENSE file.
4
+ # frozen_string_literal: true
5
+
6
+ require_relative 'real_api_helper'
7
+
8
+ describe 'DeepL.glossaries' do
9
+ it 'sends the v2 glossary requests: create, get, list, entries and delete' do
10
+ glossaries = DeepL.glossaries
11
+ name = unique_name('glossary')
12
+ entries = [%w[Apple Apfel], %w[Banana Banane]]
13
+
14
+ glossary = glossaries.create(name, 'EN', 'DE', entries)
15
+ begin
16
+ expect([glossary.name, glossary.source_lang, glossary.target_lang, glossary.entry_count])
17
+ .to eq([name, 'en', 'de', 2])
18
+
19
+ fetched = retry_while_not_found { glossaries.find(glossary.id) }
20
+ expect([fetched.id, fetched.name]).to eq([glossary.id, name])
21
+ expect(glossaries.list.map(&:id)).to include(glossary.id)
22
+ expect(glossaries.entries(glossary.id)).to match_array(entries)
23
+ ensure
24
+ delete_quietly { glossaries.destroy(glossary.id) }
25
+ end
26
+ expect { glossaries.find(glossary.id) }.to raise_error(DeepL::Exceptions::NotFound)
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ # Copyright 2026 DeepL SE (https://www.deepl.com)
2
+ # Use of this source code is governed by an MIT
3
+ # license that can be found in the LICENSE file.
4
+ # frozen_string_literal: true
5
+
6
+ require_relative 'real_api_helper'
7
+
8
+ describe 'languages' do
9
+ it 'sends GET /v3/languages and parses the source languages' do
10
+ languages = DeepL.languages
11
+ codes = languages.map(&:code)
12
+
13
+ expect(codes).to include('EN')
14
+ expect(codes).to eq(codes.map(&:upcase))
15
+ expect(languages.map(&:name)).to all(be_a(String))
16
+ end
17
+
18
+ it 'sends GET /v3/languages/resources and parses the resources and their features' do
19
+ client = DeepL::Client.new(ENV.fetch('DEEPL_AUTH_KEY'),
20
+ server_url: ENV.fetch('DEEPL_SERVER_URL', nil))
21
+ resources = client.get_language_resources
22
+
23
+ translate_text = resources.find { |resource| resource.name == 'translate_text' }
24
+ expect(translate_text.features['glossary'].needs_source_support?).to be(true)
25
+ expect(translate_text.features['glossary'].needs_target_support?).to be(true)
26
+ end
27
+ end
@@ -0,0 +1,53 @@
1
+ # Copyright 2026 DeepL SE (https://www.deepl.com)
2
+ # Use of this source code is governed by an MIT
3
+ # license that can be found in the LICENSE file.
4
+ # frozen_string_literal: true
5
+
6
+ require_relative 'real_api_helper'
7
+
8
+ describe 'DeepL::Client multilingual glossaries' do
9
+ let(:client) do
10
+ DeepL::Client.new(ENV.fetch('DEEPL_AUTH_KEY'), server_url: ENV.fetch('DEEPL_SERVER_URL', nil))
11
+ end
12
+
13
+ def pairs_of(glossary)
14
+ glossary.dictionaries.map { |d| "#{d.source_lang}-#{d.target_lang}" }
15
+ end
16
+
17
+ it 'creates, reads, updates, replaces and deletes a multilingual glossary' do
18
+ name = unique_name('glossary-v3')
19
+ dictionaries = [
20
+ DeepL::Resources::MultilingualGlossaryDictionaryEntries.new('EN', 'DE', [%w[Apple Apfel]]),
21
+ { source_lang: 'EN', target_lang: 'FR', entries: [%w[Apple Pomme]] }
22
+ ]
23
+
24
+ glossary = client.create_multilingual_glossary(name, dictionaries)
25
+ begin
26
+ expect(glossary.name).to eq(name)
27
+ expect(glossary.creation_time).to be_a(Time)
28
+ expect(pairs_of(glossary)).to contain_exactly('en-de', 'en-fr')
29
+
30
+ fetched = retry_while_not_found { client.get_multilingual_glossary(glossary) }
31
+ expect([fetched.id, fetched.name]).to eq([glossary.id, name])
32
+ expect(client.list_multilingual_glossaries.map(&:id)).to include(glossary.id)
33
+
34
+ entries = client.get_multilingual_glossary_dictionary_entries(glossary, 'EN', 'DE')
35
+ expect(entries.entries).to eq('Apple' => 'Apfel')
36
+
37
+ pear = { source_lang: 'EN', target_lang: 'DE', entries: [%w[Pear Birne]] }
38
+ updated = client.update_multilingual_glossary(glossary, name: "#{name}-renamed",
39
+ dictionaries: [pear])
40
+ expect(updated.name).to eq("#{name}-renamed")
41
+
42
+ replaced = client.replace_multilingual_glossary_dictionary(
43
+ glossary, { source_lang: 'EN', target_lang: 'DE', entries: [%w[Cherry Kirsche]] }
44
+ )
45
+ expect([replaced.source_lang, replaced.target_lang, replaced.entry_count])
46
+ .to eq(['en', 'de', 1])
47
+
48
+ expect(client.delete_multilingual_glossary_dictionary(glossary, 'EN', 'FR')).to be_nil
49
+ ensure
50
+ delete_quietly { client.delete_multilingual_glossary(glossary) }
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,41 @@
1
+ # Copyright 2026 DeepL SE (https://www.deepl.com)
2
+ # Use of this source code is governed by an MIT
3
+ # license that can be found in the LICENSE file.
4
+ # frozen_string_literal: true
5
+
6
+ require 'webrick'
7
+ require 'webrick/httpproxy'
8
+ require_relative 'real_api_helper'
9
+
10
+ # A forward proxy that records the target of each CONNECT.
11
+ class RecordingProxy < WEBrick::HTTPProxyServer
12
+ def connect_targets
13
+ @connect_targets ||= []
14
+ end
15
+
16
+ def do_CONNECT(req, res) # rubocop:disable Naming/MethodName
17
+ connect_targets << req.unparsed_uri
18
+ super
19
+ end
20
+ end
21
+
22
+ describe 'DeepL.with_session' do
23
+ it 'sends requests through the configured proxy' do
24
+ proxy = RecordingProxy.new(Port: 0, BindAddress: '127.0.0.1', AccessLog: [],
25
+ Logger: WEBrick::Log.new(File::NULL))
26
+ thread = Thread.new { proxy.start }
27
+ begin
28
+ port = proxy.listeners.first.addr[1]
29
+ options = DeepL::HTTPClientOptions.new({ 'proxy_addr' => '127.0.0.1', 'proxy_port' => port })
30
+
31
+ usage = DeepL.with_session(options) { DeepL.usage }
32
+
33
+ host = URI(DeepL.configuration.host)
34
+ expect(usage.character_limit).to be_an(Integer)
35
+ expect(proxy.connect_targets).to eq(["#{host.host}:#{host.port}"])
36
+ ensure
37
+ proxy.shutdown
38
+ thread.join
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,65 @@
1
+ # Copyright 2026 DeepL SE (https://www.deepl.com)
2
+ # Use of this source code is governed by an MIT
3
+ # license that can be found in the LICENSE file.
4
+ # frozen_string_literal: true
5
+
6
+ # The real-API suite sends one real request per SDK operation to the API at DEEPL_SERVER_URL.
7
+ # Run it on its own: bundle exec rspec spec/real_api
8
+
9
+ raise 'DEEPL_AUTH_KEY must be set to run spec/real_api' if ENV.fetch('DEEPL_AUTH_KEY', '').empty?
10
+ if defined?(WebMock)
11
+ raise 'spec/real_api and spec/offline cannot run in one process: WebMock blocks the network'
12
+ end
13
+
14
+ require 'securerandom'
15
+ require_relative '../spec_helper'
16
+ require_relative '../fixtures'
17
+
18
+ module RealApiHelper
19
+ RUN_ID = "#{Time.now.utc.strftime('%Y%m%d%H%M')}-#{SecureRandom.alphanumeric(6).downcase}"
20
+
21
+ # Name of a resource created by this run, for example sdktest-ruby-202610151530-k3f9qz-glossary.
22
+ def unique_name(label)
23
+ "sdktest-ruby-#{RUN_ID}-#{label}"
24
+ end
25
+
26
+ # A new glossary can be briefly invisible: retry NotFound for a few seconds.
27
+ def retry_while_not_found(attempts: 10)
28
+ tries = 0
29
+ begin
30
+ yield
31
+ rescue DeepL::Exceptions::NotFound
32
+ tries += 1
33
+ raise if tries >= attempts
34
+
35
+ sleep(1)
36
+ retry
37
+ end
38
+ end
39
+
40
+ # Cleanup that must not hide the failure of the example it runs after.
41
+ def delete_quietly
42
+ yield
43
+ rescue DeepL::Exceptions::Error
44
+ nil
45
+ end
46
+
47
+ # Fetches pages of 0-based index until +id+ is in one of them. The block returns the IDs of a
48
+ # page; an empty page ends the list.
49
+ def found_on_any_page?(id, max_pages: 20)
50
+ max_pages.times do |page|
51
+ ids = yield(page)
52
+ return false if ids.empty?
53
+ return true if ids.include?(id)
54
+ end
55
+ false
56
+ end
57
+ end
58
+
59
+ # The host comes from DEEPL_SERVER_URL, which Configuration reads itself.
60
+ DeepL.configure { |config| config.auth_key = ENV.fetch('DEEPL_AUTH_KEY') }
61
+
62
+ RSpec.configure do |config|
63
+ config.include RealApiHelper
64
+ config.include Fixtures
65
+ end
@@ -0,0 +1,16 @@
1
+ # Copyright 2026 DeepL SE (https://www.deepl.com)
2
+ # Use of this source code is governed by an MIT
3
+ # license that can be found in the LICENSE file.
4
+ # frozen_string_literal: true
5
+
6
+ require_relative 'real_api_helper'
7
+
8
+ describe 'DeepL.rephrase' do
9
+ it 'sends POST /v2/write/rephrase and parses the result' do
10
+ result = DeepL.rephrase('Protonenstrahl', 'de')
11
+
12
+ expect(result).to be_a(DeepL::Resources::Text)
13
+ expect(result.text).to be_a(String)
14
+ expect(result.detected_source_language).not_to be_empty
15
+ end
16
+ end
@@ -0,0 +1,56 @@
1
+ # Copyright 2026 DeepL SE (https://www.deepl.com)
2
+ # Use of this source code is governed by an MIT
3
+ # license that can be found in the LICENSE file.
4
+ # frozen_string_literal: true
5
+
6
+ require_relative 'real_api_helper'
7
+
8
+ describe 'DeepL::Client spoken terms' do
9
+ let(:client) do
10
+ DeepL::Client.new(ENV.fetch('DEEPL_AUTH_KEY'), server_url: ENV.fetch('DEEPL_SERVER_URL', nil))
11
+ end
12
+
13
+ def term_lists_of(collection)
14
+ collection.term_lists.map { |list| [list.lang, list.entry_count] }
15
+ end
16
+
17
+ it 'creates, reads, updates, replaces and deletes a spoken terms collection' do
18
+ name = unique_name('spokenTerms')
19
+
20
+ term_list = DeepL::Resources::SpokenTermsEntries.new('en', %w[DeepL webhook])
21
+ created = client.create_spoken_terms(name, [term_list])
22
+ id = created.spoken_terms_id
23
+ begin
24
+ expect(created.name).to eq(name)
25
+ expect(created.creation_time).to be_a(Time)
26
+ expect(term_lists_of(created)).to eq([['en', 2]])
27
+
28
+ fetched = retry_while_not_found { client.get_spoken_terms(id) }
29
+ expect([fetched.spoken_terms_id, fetched.name]).to eq([id, name])
30
+ expect(client.list_spoken_terms.map(&:spoken_terms_id)).to include(id)
31
+
32
+ more = [{ lang: 'en', entries: ['authentication'] }]
33
+ updated = client.update_spoken_terms(created, term_lists: more)
34
+ expect(term_lists_of(updated)).to eq([['en', 3]])
35
+ entries = client.get_spoken_terms_entries(id, 'en')
36
+ expect(entries.lang).to eq('en')
37
+ expect(entries.entries).to contain_exactly('DeepL', 'authentication', 'webhook')
38
+
39
+ replaced = client.replace_spoken_terms_list(id, { lang: 'en', entries: ['API'] })
40
+ expect([replaced.lang, replaced.entry_count]).to eq(['en', 1])
41
+ expect(client.get_spoken_terms_entries(id, 'en').entries).to eq(['API'])
42
+
43
+ added = client.replace_spoken_terms_list(
44
+ created, DeepL::Resources::SpokenTermsEntries.new('de', ['Schnittstelle'])
45
+ )
46
+ expect([added.lang, added.entry_count]).to eq(['de', 1])
47
+ expect(client.delete_spoken_terms_list(id, 'de')).to be_nil
48
+ expect(term_lists_of(client.get_spoken_terms(id))).to eq([['en', 1]])
49
+
50
+ expect(client.delete_spoken_terms(id)).to be_nil
51
+ expect { client.get_spoken_terms(id) }.to raise_error(DeepL::Exceptions::NotFound)
52
+ ensure
53
+ delete_quietly { client.delete_spoken_terms(id) }
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,46 @@
1
+ # Copyright 2026 DeepL SE (https://www.deepl.com)
2
+ # Use of this source code is governed by an MIT
3
+ # license that can be found in the LICENSE file.
4
+ # frozen_string_literal: true
5
+
6
+ require_relative 'real_api_helper'
7
+
8
+ describe 'DeepL::Client style profiles' do
9
+ let(:client) do
10
+ DeepL::Client.new(ENV.fetch('DEEPL_AUTH_KEY'), server_url: ENV.fetch('DEEPL_SERVER_URL', nil))
11
+ end
12
+
13
+ it 'creates, reads, lists, updates and deletes a style profile' do
14
+ rule = DeepL.style_rules.create(unique_name('styleProfileRule'), 'de')
15
+ rule_id = rule.style_id
16
+ id = nil
17
+ begin
18
+ name = unique_name('styleProfile')
19
+ profile = client.create_style_profile(name, description: 'SDK test', style_rule_ids: [rule])
20
+ id = profile.style_profile_id
21
+ expect([profile.name, profile.description, profile.style_rule_ids])
22
+ .to eq([name, 'SDK test', [rule_id]])
23
+ expect([profile.glossary_ids, profile.translation_memory_ids]).to eq([[], []])
24
+
25
+ fetched = retry_while_not_found { client.get_style_profile(id) }
26
+ expect([fetched.style_profile_id, fetched.name, fetched.description, fetched.style_rule_ids])
27
+ .to eq([id, name, 'SDK test', [rule_id]])
28
+ expect(fetched.creation_time).to be_a(Time)
29
+
30
+ expect(found_on_any_page?(id) do |page|
31
+ client.list_style_profiles(page: page, page_size: 100).map(&:style_profile_id)
32
+ end).to be(true)
33
+
34
+ updated = client.update_style_profile(profile, name: "#{name}-renamed", industry: 'Legal',
35
+ description: '', style_rule_ids: [])
36
+ expect([updated.name, updated.industry, updated.description, updated.style_rule_ids])
37
+ .to eq(["#{name}-renamed", 'Legal', nil, []])
38
+
39
+ expect(client.delete_style_profile(id)).to be_nil
40
+ expect { client.get_style_profile(id) }.to raise_error(DeepL::Exceptions::NotFound)
41
+ ensure
42
+ delete_quietly { client.delete_style_profile(id) } if id
43
+ delete_quietly { DeepL.style_rules.destroy(rule_id) }
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,46 @@
1
+ # Copyright 2026 DeepL SE (https://www.deepl.com)
2
+ # Use of this source code is governed by an MIT
3
+ # license that can be found in the LICENSE file.
4
+ # frozen_string_literal: true
5
+
6
+ require_relative 'real_api_helper'
7
+
8
+ describe 'DeepL::Client style rules' do
9
+ let(:client) do
10
+ DeepL::Client.new(ENV.fetch('DEEPL_AUTH_KEY'), server_url: ENV.fetch('DEEPL_SERVER_URL', nil))
11
+ end
12
+
13
+ it 'sends the style rule requests: create, get, list, rename, configure, instructions, delete' do
14
+ name = unique_name('style-rule')
15
+ rules = { dates_and_times: { calendar_era: 'use_bc_and_ad' } }
16
+
17
+ rule = client.create_style_rule(name, 'en', configured_rules: rules,
18
+ custom_instructions: [{ label: 'Init',
19
+ prompt: 'Be formal',
20
+ source_language: 'de' }])
21
+ begin
22
+ expect([rule.name, rule.language, rule.creation_time.class]).to eq([name, 'en', Time])
23
+
24
+ fetched = client.get_style_rule(rule.style_id)
25
+ expect([fetched.style_id, fetched.name]).to eq([rule.style_id, name])
26
+ expect(found_on_any_page?(rule.style_id) do |page|
27
+ client.get_all_style_rules(page: page, page_size: 25, detailed: true).map(&:style_id)
28
+ end).to be(true)
29
+
30
+ expect(client.update_style_rule_name(rule, "#{name}-renamed").name).to eq("#{name}-renamed")
31
+ expect(client.update_style_rule_configured_rules(rule, rules).style_id).to eq(rule.style_id)
32
+
33
+ instruction = client.create_style_rule_custom_instruction(rule, 'Label', 'Prompt',
34
+ source_language: 'de')
35
+ expect([instruction.id.class, instruction.label]).to eq([String, 'Label'])
36
+ found = client.get_style_rule_custom_instruction(rule, instruction)
37
+ expect(found.prompt).to eq('Prompt')
38
+ updated = client.update_style_rule_custom_instruction(rule, instruction.id, 'New Label',
39
+ 'New Prompt', source_language: 'en')
40
+ expect(updated.label).to eq('New Label')
41
+ expect(client.delete_style_rule_custom_instruction(rule, instruction)).to be_nil
42
+ ensure
43
+ delete_quietly { client.delete_style_rule(rule) }
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,18 @@
1
+ # Copyright 2026 DeepL SE (https://www.deepl.com)
2
+ # Use of this source code is governed by an MIT
3
+ # license that can be found in the LICENSE file.
4
+ # frozen_string_literal: true
5
+
6
+ require_relative 'real_api_helper'
7
+
8
+ describe 'DeepL.document' do
9
+ include_context 'with a temporary directory'
10
+
11
+ it 'uploads, polls and downloads a .txt document' do
12
+ input, output = create_text_document(temp_dir)
13
+
14
+ DeepL.document.translate_document(input, output, 'EN', 'DE')
15
+
16
+ expect(File.size(output)).to be_positive
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ # Copyright 2026 DeepL SE (https://www.deepl.com)
2
+ # Use of this source code is governed by an MIT
3
+ # license that can be found in the LICENSE file.
4
+ # frozen_string_literal: true
5
+
6
+ require_relative 'real_api_helper'
7
+
8
+ describe 'DeepL.translate' do
9
+ it 'sends POST /v2/translate and parses the result' do
10
+ result = DeepL.translate('proton beam', nil, 'DE')
11
+
12
+ expect(result).to be_a(DeepL::Resources::Text)
13
+ expect(result.text).to be_a(String)
14
+ expect(result.detected_source_language).not_to be_empty
15
+ end
16
+ end
@@ -0,0 +1,56 @@
1
+ # Copyright 2026 DeepL SE (https://www.deepl.com)
2
+ # Use of this source code is governed by an MIT
3
+ # license that can be found in the LICENSE file.
4
+ # frozen_string_literal: true
5
+
6
+ require_relative 'real_api_helper'
7
+
8
+ # The import appears as a translation memory only after about a minute, and a run killed in that
9
+ # window leaves one memory behind.
10
+ describe 'DeepL::Client translation memories' do
11
+ include_context 'with a temporary directory'
12
+
13
+ let(:client) do
14
+ DeepL::Client.new(ENV.fetch('DEEPL_AUTH_KEY'), server_url: ENV.fetch('DEEPL_SERVER_URL', nil))
15
+ end
16
+
17
+ let(:job_timeout_s) { 120 }
18
+
19
+ it 'sends the translation memory requests: import, upload, get, list, segments, export, delete' do
20
+ name = unique_name('translation-memory')
21
+ memory_id = nil
22
+ begin
23
+ created = client.create_translation_memory_import('example.tmx',
24
+ Fixtures::EXAMPLE_TMX.bytesize,
25
+ display_name: name)
26
+ expect([created.job_id.class, created.upload_url.class]).to eq([String, String])
27
+
28
+ client.upload_translation_memory_file(created, Fixtures::EXAMPLE_TMX)
29
+ import_job = client.wait_until_translation_memory_job_done(created.job_id,
30
+ timeout_s: job_timeout_s)
31
+ memory_id = import_job.result.translation_memory_id
32
+ expect([import_job.operation, import_job.status, memory_id.class])
33
+ .to eq(['import', 'completed', String])
34
+
35
+ fetched = client.get_translation_memory(memory_id)
36
+ expect([fetched.translation_memory_id, fetched.name]).to eq([memory_id, name])
37
+ expect(found_on_any_page?(memory_id) do |page|
38
+ client.list_translation_memories(page: page, page_size: 100).map(&:translation_memory_id)
39
+ end).to be(true)
40
+
41
+ segments = client.list_translation_memory_segments(memory_id)
42
+ expect(segments.segments).not_to be_empty
43
+ expect(segments.segments.first.source_segment_id).to be_a(String)
44
+
45
+ exported = client.create_translation_memory_export(memory_id)
46
+ export_job = client.wait_until_translation_memory_job_done(
47
+ client.get_translation_memory_job(exported.job_id), timeout_s: job_timeout_s
48
+ )
49
+ output = File.join(temp_dir, 'export.tmx')
50
+ client.download_translation_memory_export(export_job, output)
51
+ expect(File.read(output)).to include('<tmx')
52
+ ensure
53
+ delete_quietly { client.delete_translation_memory(memory_id) } unless memory_id.nil?
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,20 @@
1
+ # Copyright 2026 DeepL SE (https://www.deepl.com)
2
+ # Use of this source code is governed by an MIT
3
+ # license that can be found in the LICENSE file.
4
+ # frozen_string_literal: true
5
+
6
+ require_relative 'real_api_helper'
7
+
8
+ describe 'DeepL::Client#get_usage' do
9
+ let(:client) do
10
+ DeepL::Client.new(ENV.fetch('DEEPL_AUTH_KEY'), server_url: ENV.fetch('DEEPL_SERVER_URL', nil))
11
+ end
12
+
13
+ it 'sends GET /v2/usage and parses the usage' do
14
+ usage = client.get_usage
15
+
16
+ expect(usage.character.count).to be_an(Integer)
17
+ expect(usage.character.limit).to be_an(Integer)
18
+ expect(usage.any_limit_reached?).to be(true).or be(false)
19
+ end
20
+ end
data/spec/spec_helper.rb CHANGED
@@ -3,60 +3,11 @@
3
3
  # license that can be found in the LICENSE.md file.
4
4
  # frozen_string_literal: true
5
5
 
6
- # Coverage
6
+ # Coverage. It must start before the library is loaded.
7
7
  require 'simplecov'
8
8
  SimpleCov.start
9
9
 
10
10
  require 'simplecov-cobertura'
11
11
  SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter
12
12
 
13
- # Load lib
14
13
  require_relative '../lib/deepl'
15
- require_relative 'integration_tests/integration_test_utils'
16
-
17
- # Auto-load test helpers under spec/support
18
- Dir[File.expand_path('support/**/*.rb', __dir__)].sort.each { |f| require f }
19
-
20
- # Lib config
21
- ENV['DEEPL_AUTH_KEY'] ||= 'TEST-TOKEN'
22
-
23
- RSpec.configure do |config|
24
- config.include ManagedGlossary
25
- config.include ManagedStyleRule
26
- config.include ManagedTranslationMemory
27
-
28
- config.before(:each, :mock_server_only) do
29
- skip 'Only runs on mock server' unless ENV.key?('DEEPL_MOCK_SERVER_PORT')
30
- end
31
-
32
- config.before(:each, :real_server_only) do
33
- skip 'Only runs on real server' if ENV.key?('DEEPL_MOCK_SERVER_PORT')
34
- end
35
- end
36
-
37
- # General helpers
38
- def build_deepl_api
39
- DeepL::API.new(DeepL::Configuration.new)
40
- end
41
-
42
- # Test helpers
43
-
44
- def replace_env_preserving_deepl_vars
45
- env_auth_key = ENV.fetch('DEEPL_AUTH_KEY', false)
46
- env_server_url = ENV.fetch('DEEPL_SERVER_URL', false)
47
- env_mock_server_port = ENV.fetch('DEEPL_MOCK_SERVER_PORT', false)
48
- tmp_env = ENV.to_hash
49
- ENV.clear
50
- ENV['DEEPL_AUTH_KEY'] = (env_auth_key || 'VALID')
51
- ENV['DEEPL_SERVER_URL'] = (env_server_url || '')
52
- ENV['DEEPL_MOCK_SERVER_PORT'] = (env_mock_server_port || '')
53
- tmp_env
54
- end
55
-
56
- def replace_env_preserving_deepl_vars_except_mock_server
57
- env_auth_key = ENV.fetch('DEEPL_AUTH_KEY', false)
58
- tmp_env = ENV.to_hash
59
- ENV.clear
60
- ENV['DEEPL_AUTH_KEY'] = (env_auth_key || 'VALID')
61
- tmp_env
62
- end