deepl-rb 3.7.0 → 3.9.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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/.gitlab-ci.yml +4 -2
  3. data/CHANGELOG.md +39 -1
  4. data/Gemfile +0 -2
  5. data/README.md +225 -16
  6. data/VERSION +1 -1
  7. data/deepl-rb.gemspec +52 -19
  8. data/lib/deepl/document_api.rb +11 -1
  9. data/lib/deepl/requests/base.rb +55 -3
  10. data/lib/deepl/requests/document/upload.rb +50 -1
  11. data/lib/deepl/requests/rephrase.rb +3 -2
  12. data/lib/deepl/requests/translate.rb +6 -3
  13. data/lib/deepl/requests/translation_memory/base.rb +47 -0
  14. data/lib/deepl/requests/translation_memory/create_export.rb +41 -0
  15. data/lib/deepl/requests/translation_memory/create_import.rb +53 -0
  16. data/lib/deepl/requests/translation_memory/destroy.rb +37 -0
  17. data/lib/deepl/requests/translation_memory/download_export.rb +35 -0
  18. data/lib/deepl/requests/translation_memory/find.rb +38 -0
  19. data/lib/deepl/requests/translation_memory/find_job.rb +38 -0
  20. data/lib/deepl/requests/translation_memory/list.rb +5 -18
  21. data/lib/deepl/requests/translation_memory/segments.rb +46 -0
  22. data/lib/deepl/requests/translation_memory/storage_base.rb +47 -0
  23. data/lib/deepl/requests/translation_memory/upload_file.rb +47 -0
  24. data/lib/deepl/resources/translation_memory.rb +4 -1
  25. data/lib/deepl/resources/translation_memory_export.rb +37 -0
  26. data/lib/deepl/resources/translation_memory_import.rb +29 -0
  27. data/lib/deepl/resources/translation_memory_job.rb +142 -0
  28. data/lib/deepl/resources/translation_memory_segments.rb +78 -0
  29. data/lib/deepl/translation_memory_api.rb +293 -1
  30. data/lib/deepl/utils/time_parser.rb +26 -0
  31. data/lib/deepl.rb +38 -4
  32. data/lib/version.rb +1 -1
  33. data/spec/api/deepl_spec.rb +134 -332
  34. data/spec/integration_tests/document_api_spec.rb +51 -18
  35. data/spec/integration_tests/document_error_paths_spec.rb +33 -0
  36. data/spec/integration_tests/glossary_api_spec.rb +114 -0
  37. data/spec/integration_tests/glossary_error_paths_spec.rb +107 -0
  38. data/spec/integration_tests/integration_test_utils.rb +8 -0
  39. data/spec/integration_tests/languages_api_spec.rb +54 -0
  40. data/spec/integration_tests/languages_error_paths_spec.rb +25 -0
  41. data/spec/integration_tests/rephrase_api_spec.rb +90 -0
  42. data/spec/integration_tests/rephrase_error_paths_spec.rb +53 -0
  43. data/spec/integration_tests/smoke_test_spec.rb +24 -0
  44. data/spec/integration_tests/style_rule_api_spec.rb +1 -19
  45. data/spec/integration_tests/style_rule_error_paths_spec.rb +45 -0
  46. data/spec/integration_tests/translate_api_spec.rb +117 -0
  47. data/spec/integration_tests/translate_error_paths_spec.rb +48 -0
  48. data/spec/integration_tests/translation_memory_api_spec.rb +191 -17
  49. data/spec/integration_tests/translation_memory_error_paths_spec.rb +87 -0
  50. data/spec/integration_tests/usage_api_spec.rb +29 -0
  51. data/spec/integration_tests/usage_error_paths_spec.rb +18 -0
  52. data/spec/requests/document/upload_spec.rb +130 -0
  53. data/spec/requests/glossary/create_spec.rb +0 -21
  54. data/spec/requests/glossary/destroy_spec.rb +0 -39
  55. data/spec/requests/glossary/entries_spec.rb +0 -35
  56. data/spec/requests/glossary/find_spec.rb +0 -40
  57. data/spec/requests/glossary/language_pairs_spec.rb +0 -13
  58. data/spec/requests/glossary/list_spec.rb +0 -27
  59. data/spec/requests/languages_spec.rb +0 -41
  60. data/spec/requests/rephrase_spec.rb +13 -139
  61. data/spec/requests/style_rule/create_custom_instruction_spec.rb +0 -24
  62. data/spec/requests/style_rule/create_spec.rb +0 -16
  63. data/spec/requests/style_rule/destroy_custom_instruction_spec.rb +0 -26
  64. data/spec/requests/style_rule/destroy_spec.rb +0 -27
  65. data/spec/requests/style_rule/find_custom_instruction_spec.rb +0 -27
  66. data/spec/requests/style_rule/find_spec.rb +0 -28
  67. data/spec/requests/style_rule/list_spec.rb +0 -31
  68. data/spec/requests/style_rule/update_configured_rules_spec.rb +0 -21
  69. data/spec/requests/style_rule/update_custom_instruction_spec.rb +0 -26
  70. data/spec/requests/style_rule/update_spec.rb +0 -19
  71. data/spec/requests/translate_spec.rb +63 -217
  72. data/spec/requests/translation_memory/create_export_spec.rb +37 -0
  73. data/spec/requests/translation_memory/create_import_spec.rb +61 -0
  74. data/spec/requests/translation_memory/destroy_spec.rb +36 -0
  75. data/spec/requests/translation_memory/download_export_spec.rb +54 -0
  76. data/spec/requests/translation_memory/find_job_spec.rb +37 -0
  77. data/spec/requests/translation_memory/find_spec.rb +36 -0
  78. data/spec/requests/translation_memory/list_spec.rb +0 -34
  79. data/spec/requests/translation_memory/segments_spec.rb +58 -0
  80. data/spec/requests/translation_memory/upload_file_spec.rb +55 -0
  81. data/spec/requests/usage_spec.rb +0 -16
  82. data/spec/resources/custom_instruction_spec.rb +32 -0
  83. data/spec/resources/style_rule_spec.rb +68 -0
  84. data/spec/resources/translation_memory_export_spec.rb +38 -0
  85. data/spec/resources/translation_memory_import_spec.rb +30 -0
  86. data/spec/resources/translation_memory_job_spec.rb +109 -0
  87. data/spec/resources/translation_memory_segments_spec.rb +78 -0
  88. data/spec/resources/translation_memory_spec.rb +18 -1
  89. data/spec/spec_helper.rb +15 -45
  90. data/spec/support/live_mock_server.rb +12 -0
  91. data/spec/support/managed_glossary.rb +65 -0
  92. data/spec/support/managed_style_rule.rb +17 -0
  93. data/spec/support/managed_translation_memory.rb +55 -0
  94. metadata +50 -17
  95. data/spec/fixtures/vcr_cassettes/deepl_document.yml +0 -95
  96. data/spec/fixtures/vcr_cassettes/deepl_document_download.yml +0 -1214
  97. data/spec/fixtures/vcr_cassettes/deepl_glossaries.yml +0 -1163
  98. data/spec/fixtures/vcr_cassettes/deepl_languages.yml +0 -54
  99. data/spec/fixtures/vcr_cassettes/deepl_rephrase.yml +0 -87
  100. data/spec/fixtures/vcr_cassettes/deepl_translate.yml +0 -358
  101. data/spec/fixtures/vcr_cassettes/deepl_usage.yml +0 -129
  102. data/spec/fixtures/vcr_cassettes/glossaries.yml +0 -1702
  103. data/spec/fixtures/vcr_cassettes/languages.yml +0 -229
  104. data/spec/fixtures/vcr_cassettes/rephrase_texts.yml +0 -401
  105. data/spec/fixtures/vcr_cassettes/style_rules.yml +0 -92
  106. data/spec/fixtures/vcr_cassettes/style_rules_crud.yml +0 -926
  107. data/spec/fixtures/vcr_cassettes/translate_texts.yml +0 -10630
  108. data/spec/fixtures/vcr_cassettes/translation_memories.yml +0 -74
  109. data/spec/fixtures/vcr_cassettes/usage.yml +0 -171
@@ -0,0 +1,117 @@
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 'spec_helper'
7
+
8
+ describe 'DeepL.translate' do # rubocop:disable RSpec/DescribeClass
9
+ include_context 'with a live mock server'
10
+
11
+ let(:text) { 'proton beam' }
12
+
13
+ describe 'happy path' do
14
+ it 'translates a single string into a Text resource' do
15
+ result = DeepL.translate(text, 'EN', 'DE')
16
+
17
+ expect(result).to be_a(DeepL::Resources::Text)
18
+ expect(result.text).to be_a(String)
19
+ expect(result.text).not_to be_empty
20
+ end
21
+
22
+ it 'translates an array of strings into an array of Text resources' do
23
+ results = DeepL.translate(%w[Sample Word], 'EN', 'ES')
24
+
25
+ expect(results).to be_an(Array)
26
+ expect(results.size).to eq(2)
27
+ expect(results).to all(be_a(DeepL::Resources::Text))
28
+ end
29
+ end
30
+
31
+ describe 'options' do
32
+ it 'accepts a formality setting' do
33
+ result = DeepL.translate('How are you?', 'EN', 'DE', { formality: 'more' })
34
+
35
+ expect(result).to be_a(DeepL::Resources::Text)
36
+ end
37
+
38
+ it 'translates with XML tag handling and ignored tags' do
39
+ result = DeepL.translate('<p>Sample <code>x</code></p>', 'EN', 'ES',
40
+ { tag_handling: 'xml', ignore_tags: %w[code] })
41
+
42
+ expect(result.text).to be_a(String)
43
+ expect(result.text).not_to be_empty
44
+ end
45
+
46
+ it 'accepts a context option' do
47
+ result = DeepL.translate('That is hot!', 'EN', 'ES', { context: 'about the weather' })
48
+
49
+ expect(result).to be_a(DeepL::Resources::Text)
50
+ end
51
+
52
+ it 'accepts an empty context option' do
53
+ result = DeepL.translate('That is hot!', 'EN', 'ES', { context: '' })
54
+
55
+ expect(result).to be_a(DeepL::Resources::Text)
56
+ end
57
+
58
+ it 'translates an HTML document with split_sentences, outline_detection and splitting_tags' do
59
+ document = '<document><meta><title>Title</title></meta>' \
60
+ '<content><par>First sentence. Second one.</par></content></document>'
61
+ result = DeepL.translate(document, 'EN', 'ES',
62
+ { tag_handling: 'xml', split_sentences: 'nonewlines',
63
+ outline_detection: false, splitting_tags: %w[title par] })
64
+
65
+ expect(result).to be_a(DeepL::Resources::Text)
66
+ expect(result.text).not_to be_empty
67
+ end
68
+
69
+ it 'translates using a glossary' do
70
+ with_managed_glossary(name: 'Translate Glossary', source_lang: 'en', target_lang: 'de',
71
+ entries: [%w[Hello Hallo]]) do |glossary|
72
+ result = DeepL.translate('Hello', 'EN', 'DE', { glossary_id: glossary.id })
73
+
74
+ expect(result).to be_a(DeepL::Resources::Text)
75
+ expect(result.text).not_to be_empty
76
+ end
77
+ end
78
+
79
+ it 'translates using multiple glossaries via glossary_ids' do # rubocop:disable RSpec/ExampleLength
80
+ with_managed_glossary(name: 'Translate Glossary IDs 1', source_lang: 'en', target_lang: 'de',
81
+ entries: [%w[Hello Hallo]]) do |glossary1|
82
+ with_managed_glossary(name: 'Translate Glossary IDs 2', source_lang: 'en',
83
+ target_lang: 'de', entries: [%w[World Welt]]) do |glossary2|
84
+ # Newly created glossaries can lag behind (eventual consistency), so
85
+ # ensure they are resolvable before translating and tolerate a
86
+ # transient "not found" on the translate call itself.
87
+ wait_until_glossaries_ready(glossary1, glossary2)
88
+ result = translate_retrying_missing_glossary(
89
+ 'Hello', 'EN', 'DE', { glossary_ids: [glossary1.id, glossary2.id] }
90
+ )
91
+
92
+ expect(result).to be_a(DeepL::Resources::Text)
93
+ expect(result.text).not_to be_empty
94
+ end
95
+ end
96
+ end
97
+
98
+ %w[quality_optimized latency_optimized prefer_quality_optimized].each do |model_type|
99
+ it "returns model_type_used for #{model_type}" do
100
+ result = DeepL.translate(text, 'EN', 'DE', { model_type: model_type })
101
+
102
+ expect(result).to be_a(DeepL::Resources::Text)
103
+ expect(result.model_type_used).not_to be_nil
104
+ end
105
+ end
106
+
107
+ %w[v1 v2].each do |version|
108
+ it "translates with tag_handling_version #{version}" do
109
+ result = DeepL.translate('<p>Hello world</p>', 'EN', 'DE',
110
+ { tag_handling: 'html', tag_handling_version: version })
111
+
112
+ expect(result).to be_a(DeepL::Resources::Text)
113
+ expect(result.text).not_to be_empty
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,48 @@
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 'spec_helper'
7
+
8
+ describe 'DeepL.translate error paths' do # rubocop:disable RSpec/DescribeClass
9
+ include_context 'with a live mock server'
10
+
11
+ let(:text) { 'proton beam' }
12
+
13
+ describe 'authorization failures' do
14
+ it 'raises AuthorizationFailed when the auth key is invalid' do
15
+ request = DeepL::Requests::Translate.new(unauthorized_api, text, 'EN', 'DE')
16
+
17
+ expect { request.request }.to raise_error(DeepL::Exceptions::AuthorizationFailed)
18
+ end
19
+ end
20
+
21
+ describe 'bad request errors' do
22
+ it 'raises BadRequest for an unsupported target language' do
23
+ expect { DeepL.translate(text, 'EN', 'ZZ') }
24
+ .to raise_error(DeepL::Exceptions::BadRequest)
25
+ end
26
+
27
+ it 'raises BadRequest when no target language is given' do
28
+ expect { DeepL.translate(text, 'EN', nil) }
29
+ .to raise_error(DeepL::Exceptions::BadRequest)
30
+ end
31
+
32
+ # Real-server only: the mock coerces text:[null] to text:[""] and accepts it,
33
+ # so it cannot reproduce this; prod rejects [null] with 400.
34
+ it 'raises BadRequest when the text is nil', :real_server_only do
35
+ expect { DeepL.translate(nil, 'EN', 'DE') }
36
+ .to raise_error(DeepL::Exceptions::BadRequest)
37
+ end
38
+ end
39
+
40
+ describe 'request entity too large errors' do
41
+ it 'raises RequestEntityTooLarge when too many texts are sent in a single request' do
42
+ texts = Array.new(10_000) { |i| "This is the sentence number #{i}" }
43
+
44
+ expect { DeepL.translate(texts, 'EN', 'DE') }
45
+ .to raise_error(DeepL::Exceptions::RequestEntityTooLarge)
46
+ end
47
+ end
48
+ end
@@ -5,34 +5,25 @@
5
5
 
6
6
  require 'spec_helper'
7
7
 
8
- describe DeepL::TranslationMemoryApi do
9
- before do
10
- VCR.turn_off!
11
- WebMock.allow_net_connect!
12
- end
8
+ describe DeepL::TranslationMemoryApi, :mock_server_only do
9
+ include_context 'with temp dir'
13
10
 
14
- after do
15
- VCR.turn_on!
16
- WebMock.disable_net_connect!
17
- end
11
+ let(:default_tm_id) { 'a74d88fb-ed2a-4943-a664-a4512398b994' }
12
+ let(:tmx_file_path) { write_example_tmx(File.join(temp_dir, 'example.tmx')) }
13
+ let(:translation_memories) { DeepL.translation_memories }
18
14
 
19
15
  describe '#translate_with_translation_memory' do
20
16
  it 'when performing a request with translation_memory_id' do
21
- skip 'Only runs on mock server' if real_server?
22
-
23
17
  source_lang = 'DE'
24
18
  target_lang = 'EN'
25
19
  text = 'Protonenstrahl'
26
- translation_memory_id = 'a74d88fb-ed2a-4943-a664-a4512398b994'
27
20
 
28
21
  result = DeepL.translate(text, source_lang, target_lang,
29
- { translation_memory: translation_memory_id })
22
+ { translation_memory: default_tm_id })
30
23
  expect(result).to be_a(DeepL::Resources::Text)
31
24
  end
32
25
 
33
26
  it 'when performing a request with translation_memory resource object' do
34
- skip 'Only runs on mock server' if real_server?
35
-
36
27
  source_lang = 'DE'
37
28
  target_lang = 'EN'
38
29
  text = 'Protonenstrahl'
@@ -46,8 +37,6 @@ describe DeepL::TranslationMemoryApi do
46
37
 
47
38
  describe '#list_translation_memories' do
48
39
  it 'when requesting a list of all translation memories' do
49
- skip 'Only runs on mock server' if real_server?
50
-
51
40
  translation_memories = DeepL.translation_memories.list
52
41
  expect(translation_memories).to be_an(Array)
53
42
  expect(translation_memories).not_to be_empty
@@ -57,6 +46,191 @@ describe DeepL::TranslationMemoryApi do
57
46
  end
58
47
  end
59
48
 
49
+ describe '#find' do
50
+ it 'when requesting a single translation memory by ID' do
51
+ translation_memory = DeepL.translation_memories.find(default_tm_id)
52
+ expect(translation_memory).to be_a(DeepL::Resources::TranslationMemory)
53
+ expect(translation_memory.translation_memory_id).to eq(default_tm_id)
54
+ expect(translation_memory.name).to eq('Default Translation Memory')
55
+ expect(translation_memory.source_language).to eq('de')
56
+ expect(translation_memory.target_languages).to eq(%w[en es fr])
57
+ expect(translation_memory.segment_count).to eq(12)
58
+ end
59
+
60
+ it 'when requesting a single translation memory by resource object' do
61
+ translation_memory = DeepL.translation_memories.find(build_test_translation_memory)
62
+ expect(translation_memory.translation_memory_id).to eq(default_tm_id)
63
+ expect(translation_memory.creation_time).to be_a(Time)
64
+ expect(translation_memory.updated_time).to be_a(Time)
65
+ end
66
+ end
67
+
68
+ describe '#segments' do
69
+ it 'when requesting the segments of a translation memory' do
70
+ page = DeepL.translation_memories.segments(default_tm_id)
71
+ expect(page).to be_a(DeepL::Resources::TranslationMemorySegments)
72
+ expect(page.segments.length).to eq(12)
73
+ expect(page.segment_count).to eq(12)
74
+ expect(page.next_page?).to be(false)
75
+ expect(page.segments.first.source_text).to eq('Quelltext Nummer 0')
76
+ end
77
+
78
+ it 'when reading segment timestamps' do
79
+ # The API does not return creation_time/updated_time on segments at all, and returns
80
+ # last_used_time only for segments that have been used.
81
+ page = DeepL.translation_memories.segments(default_tm_id)
82
+ expect(page.segments.first.creation_time).to be_nil
83
+ expect(page.segments.first.updated_time).to be_nil
84
+ expect(page.segments.first.last_used_time).to be_a(Time)
85
+ expect(page.segments[1].last_used_time).to be_nil
86
+ end
87
+
88
+ it 'when reading the target segments of a source segment' do
89
+ page = DeepL.translation_memories.segments(default_tm_id, page_size: 1)
90
+ segment = page.segments.first
91
+ expect(segment.targets.map(&:target_language)).to eq(%w[en es fr])
92
+ expect(segment.targets.first.target_text).to eq('Target text number 0 (de->en)')
93
+ expect(segment.targets.first.target_segment_id).not_to be_nil
94
+ expect(segment.targets.first.last_used_time).to be_a(Time)
95
+ end
96
+
97
+ it 'when paginating through the segments with the page cursor' do
98
+ first_page = DeepL.translation_memories.segments(default_tm_id, page_size: 5)
99
+ second_page = next_segments_page(first_page)
100
+ last_page = next_segments_page(second_page)
101
+
102
+ expect(first_page.segments.length).to eq(5)
103
+ expect(first_page.next_page?).to be(true)
104
+ expect(second_page.segments.first.source_text).to eq('Quelltext Nummer 5')
105
+ expect(last_page.segments.length).to eq(2)
106
+ expect(last_page.next_page?).to be(false)
107
+ end
108
+
109
+ it 'when filtering the segments by a text containing a space' do
110
+ page = DeepL.translation_memories.segments(default_tm_id, filter_text: 'Nummer 1')
111
+ expect(page.segments.map(&:source_text))
112
+ .to eq(['Quelltext Nummer 1', 'Quelltext Nummer 10', 'Quelltext Nummer 11'])
113
+ # segment_count is the number of segments stored in the translation memory, a text filter
114
+ # does not reduce it.
115
+ expect(page.segment_count).to eq(12)
116
+ end
117
+
118
+ it 'when filtering the segments case-sensitively' do
119
+ page = DeepL.translation_memories.segments(default_tm_id, filter_text: 'quelltext',
120
+ filter_case_sensitive: true)
121
+ expect(page.segments).to be_empty
122
+ expect(page.segment_count).to eq(12)
123
+ end
124
+ end
125
+
126
+ describe '#import_from_filepath' do
127
+ it 'when importing a translation memory from a TMX file' do
128
+ with_managed_translation_memory(tmx_file_path, display_name: 'Imported TM') do |job|
129
+ expect(job).to be_a(DeepL::Resources::TranslationMemoryJob)
130
+ expect(job.operation).to eq('import')
131
+ expect(job.display_name).to eq('Imported TM')
132
+ expect(job.status).to eq('completed')
133
+ expect(job.finished?).to be(true)
134
+ expect(job.result.translation_memory_id).not_to be_nil
135
+ expect(job.result.skipped_segment_count).to eq(0)
136
+ end
137
+ end
138
+
139
+ it 'when the imported translation memory can be retrieved afterwards' do
140
+ with_managed_translation_memory(tmx_file_path) do |job|
141
+ translation_memory = DeepL.translation_memories.find(job.result.translation_memory_id)
142
+ expect(translation_memory.name).to eq('example.tmx')
143
+ expect(translation_memory.source_language).to eq('de')
144
+ expect(job.source_content_type).to eq('application/xml')
145
+ expect(job.source_content_length).to eq(File.size(tmx_file_path))
146
+ end
147
+ end
148
+ end
149
+
150
+ describe '#create_import' do
151
+ it 'when the import job is still awaiting the file upload' do
152
+ created = DeepL.translation_memories.create_import('awaiting.tmx', 128,
153
+ display_name: 'Awaiting Upload')
154
+ expect(created).to be_a(DeepL::Resources::TranslationMemoryImport)
155
+ expect(created.upload_url).to start_with('http')
156
+ expect(created.expires_at).to be_a(Time)
157
+
158
+ job = DeepL.translation_memories.find_job(created.job_id)
159
+ expect(job.awaiting_input?).to be(true)
160
+ expect(job.status).to eq('awaiting_input')
161
+ expect(job.result.required_action).not_to be_nil
162
+ end
163
+
164
+ # The API detects the upload asynchronously, so an uploaded job keeps reporting
165
+ # `awaiting_input` for a while. The wait loop must poll through that status.
166
+ it 'when waiting on an uploaded job it polls through the awaiting_input status' do
167
+ created = create_uploaded_import(translation_memories, tmx_file_path, processing_polls: 1)
168
+ polled_statuses = []
169
+ allow(translation_memories).to receive(:find_job).and_wrap_original do |original, *args|
170
+ original.call(*args).tap { |status| polled_statuses << status.status }
171
+ end
172
+ expect(translation_memories).to receive(:sleep).once
173
+
174
+ job = translation_memories.wait_until_job_done(created.job_id)
175
+
176
+ expect(polled_statuses).to eq(%w[awaiting_input completed])
177
+ expect(job.status).to eq('completed')
178
+ DeepL.translation_memories.destroy(job.result.translation_memory_id)
179
+ end
180
+
181
+ it 'when the file is never uploaded it waits until the timeout of the caller' do
182
+ created = translation_memories.create_import('never-uploaded.tmx', 128)
183
+ # Such a job does not finish on its own, only the timeout ends the wait.
184
+ expect(translation_memories).not_to receive(:sleep)
185
+
186
+ expect { translation_memories.wait_until_job_done(created.job_id, timeout_s: 0) }
187
+ .to raise_error(DeepL::Exceptions::Error, /Manual timeout of 0s exceeded/)
188
+ end
189
+ end
190
+
191
+ describe '#export_to_filepath' do
192
+ let(:output_path) { File.join(temp_dir, 'export.tmx') }
193
+
194
+ it 'when exporting a translation memory to a TMX file' do
195
+ job = DeepL.translation_memories.export_to_filepath(default_tm_id, output_path)
196
+ expect(job).to be_a(DeepL::Resources::TranslationMemoryJob)
197
+ expect(job.operation).to eq('export')
198
+ expect(job.status).to eq('completed')
199
+ expect(job.result.download_url).to start_with('http')
200
+
201
+ contents = File.read(output_path)
202
+ expect(contents).to include('<tmx version="1.4">')
203
+ expect(contents).to include('<seg>Quelltext Nummer 0</seg>')
204
+ expect(contents).to include('<tuv xml:lang="en"><seg>Target text number 0 (de->en)</seg>')
205
+ end
206
+
207
+ it 'when re-exporting an unchanged translation memory it reuses the completed job' do
208
+ job = DeepL.translation_memories.export_to_filepath(default_tm_id, output_path)
209
+ reused = DeepL.translation_memories.create_export(default_tm_id)
210
+ expect(reused).to be_a(DeepL::Resources::TranslationMemoryExport)
211
+ expect(reused.reused_existing?).to be(true)
212
+ expect(reused.job_id).to eq(job.job_id)
213
+ expect(reused.translation_memory_id).to eq(default_tm_id)
214
+ end
215
+ end
216
+
217
+ describe '#destroy' do
218
+ it 'when deleting a translation memory it can no longer be found' do
219
+ job = DeepL.translation_memories.import_from_filepath(tmx_file_path)
220
+ translation_memory_id = job.result.translation_memory_id
221
+
222
+ expect(DeepL.translation_memories.destroy(translation_memory_id))
223
+ .to eq(translation_memory_id)
224
+ expect { DeepL.translation_memories.find(translation_memory_id) }
225
+ .to raise_error(DeepL::Exceptions::NotFound)
226
+ end
227
+ end
228
+
229
+ def next_segments_page(page)
230
+ DeepL.translation_memories.segments(default_tm_id, page_size: 5,
231
+ page_cursor: page.next_page_cursor)
232
+ end
233
+
60
234
  def build_test_translation_memory
61
235
  tm_data = {
62
236
  'translation_memory_id' => 'a74d88fb-ed2a-4943-a664-a4512398b994',
@@ -0,0 +1,87 @@
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 'spec_helper'
7
+
8
+ describe DeepL::TranslationMemoryApi, :mock_server_only do # rubocop:disable RSpec/SpecFilePathFormat
9
+ include_context 'with a live mock server'
10
+
11
+ let(:default_tm_id) { 'a74d88fb-ed2a-4943-a664-a4512398b994' }
12
+ let(:missing_uuid) { '00000000-0000-0000-0000-000000000000' }
13
+
14
+ describe 'authorization failures' do
15
+ let(:unauthorized_translation_memories) { described_class.new(unauthorized_api) }
16
+
17
+ it 'raises AuthorizationFailed when listing with an invalid auth key' do
18
+ expect { unauthorized_translation_memories.list }
19
+ .to raise_error(DeepL::Exceptions::AuthorizationFailed)
20
+ end
21
+
22
+ it 'raises AuthorizationFailed on a read operation (find)' do
23
+ expect { unauthorized_translation_memories.find(default_tm_id) }
24
+ .to raise_error(DeepL::Exceptions::AuthorizationFailed)
25
+ end
26
+
27
+ it 'raises AuthorizationFailed on a write operation (create_export)' do
28
+ expect { unauthorized_translation_memories.create_export(default_tm_id) }
29
+ .to raise_error(DeepL::Exceptions::AuthorizationFailed)
30
+ end
31
+ end
32
+
33
+ describe 'not-found failures' do
34
+ it 'raises NotFound when #find is called with a missing UUID' do
35
+ expect { DeepL.translation_memories.find(missing_uuid) }
36
+ .to raise_error(DeepL::Exceptions::NotFound)
37
+ end
38
+
39
+ it 'raises NotFound when #segments is called with a missing UUID' do
40
+ expect { DeepL.translation_memories.segments(missing_uuid) }
41
+ .to raise_error(DeepL::Exceptions::NotFound)
42
+ end
43
+
44
+ it 'raises NotFound when #find_job is called with a missing UUID' do
45
+ expect { DeepL.translation_memories.find_job(missing_uuid) }
46
+ .to raise_error(DeepL::Exceptions::NotFound)
47
+ end
48
+ end
49
+
50
+ describe 'bad-request failures' do
51
+ it 'raises BadRequest when #segments is called with a too short text filter' do
52
+ expect { DeepL.translation_memories.segments(default_tm_id, filter_text: 'a') }
53
+ .to raise_error(DeepL::Exceptions::BadRequest)
54
+ end
55
+
56
+ it 'raises an error when #destroy is called with a malformed UUID' do
57
+ expect { DeepL.translation_memories.destroy('invalid-uuid') }
58
+ .to raise_error(DeepL::Exceptions::Error)
59
+ end
60
+ end
61
+
62
+ describe 'client-side validation failures' do
63
+ it 'raises an error when #find is called with an empty ID' do
64
+ expect { DeepL.translation_memories.find('') }
65
+ .to raise_error(DeepL::Exceptions::Error, /must not be empty/)
66
+ end
67
+
68
+ it 'raises an error when #find_job is called with an empty job ID' do
69
+ expect { DeepL.translation_memories.find_job(nil) }
70
+ .to raise_error(DeepL::Exceptions::Error, /must not be empty/)
71
+ end
72
+
73
+ it 'raises an error when #import_from_filepath is given a missing file' do
74
+ expect { DeepL.translation_memories.import_from_filepath('/no/such/file.tmx') }
75
+ .to raise_error(DeepL::Exceptions::Error, /No file found/)
76
+ end
77
+
78
+ it 'raises an error when #download_export is given an unfinished job' do
79
+ created = DeepL.translation_memories.create_export(default_tm_id)
80
+ job = DeepL::Resources::TranslationMemoryJob.new(
81
+ { 'job_id' => created.job_id, 'results' => [{ 'status' => 'processing' }] }, nil, nil
82
+ )
83
+ expect { DeepL.translation_memories.download_export(job, '/tmp/unused.tmx') }
84
+ .to raise_error(DeepL::Exceptions::Error, /no download URL/)
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,29 @@
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 'spec_helper'
7
+
8
+ describe 'DeepL.usage' do # rubocop:disable RSpec/DescribeClass
9
+ describe 'happy path' do
10
+ it 'returns a Usage resource with integer counters where count does not exceed limit' do
11
+ usage = DeepL.usage
12
+
13
+ expect(usage).to be_a(DeepL::Resources::Usage)
14
+ expect(usage).to respond_to(:character_count, :character_limit)
15
+ expect(usage.character_count).to be_an(Integer)
16
+ expect(usage.character_limit).to be_an(Integer)
17
+ expect(usage.character_count).to be <= usage.character_limit
18
+ end
19
+ end
20
+
21
+ describe 'resource shape' do
22
+ it 'exposes quota_exceeded? consistent with character_count vs character_limit' do
23
+ usage = DeepL.usage
24
+
25
+ expect(usage).to respond_to(:quota_exceeded?)
26
+ expect(usage.quota_exceeded?).to eq(usage.character_count >= usage.character_limit)
27
+ end
28
+ end
29
+ 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 'spec_helper'
7
+
8
+ describe 'DeepL.usage error paths' do # rubocop:disable RSpec/DescribeClass
9
+ include_context 'with a live mock server'
10
+
11
+ describe 'authorization failures' do
12
+ it 'raises AuthorizationFailed when the auth key is invalid' do
13
+ request = DeepL::Requests::Usage.new(unauthorized_api)
14
+
15
+ expect { request.request }.to raise_error(DeepL::Exceptions::AuthorizationFailed)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,130 @@
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 'spec_helper'
7
+
8
+ describe DeepL::Requests::Document::Upload do
9
+ subject(:upload) do
10
+ described_class.new(api, input_file_path, source_lang, target_lang, filename, options)
11
+ end
12
+
13
+ around do |tests|
14
+ tmp_env = replace_env_preserving_deepl_vars_except_mock_server
15
+ tests.call
16
+ ENV.replace(tmp_env)
17
+ end
18
+
19
+ let(:api) { build_deepl_api }
20
+ let(:input_file_path) { '/tmp/example.txt' }
21
+ let(:source_lang) { 'EN' }
22
+ let(:target_lang) { 'DE' }
23
+ let(:filename) { 'example.txt' }
24
+ let(:options) { {} }
25
+ let(:input_file) { instance_double(File) }
26
+
27
+ # Builds the multipart form data without performing any network or filesystem IO.
28
+ def form_data
29
+ upload.send(:build_base_form_data, input_file)
30
+ end
31
+
32
+ def form_value(form, key)
33
+ field = form.find { |name, _| name == key }
34
+ field&.last
35
+ end
36
+
37
+ describe '#initialize' do
38
+ it 'creates a request object' do
39
+ expect(upload).to be_a(described_class)
40
+ end
41
+ end
42
+
43
+ describe 'building the upload form data' do
44
+ context 'when using the `style_rule` option' do
45
+ it 'does not append a `style_id` field when not provided' do
46
+ expect(form_value(form_data, 'style_id')).to be_nil
47
+ end
48
+
49
+ it 'appends a `style_id` field from a style rule ID string' do
50
+ options[:style_rule] = 'style-123'
51
+ expect(form_value(form_data, 'style_id')).to eq('style-123')
52
+ end
53
+
54
+ it 'appends a `style_id` field from a StyleRule object' do
55
+ options[:style_rule] =
56
+ DeepL::Resources::StyleRule.new({ 'style_id' => 'style-obj' }, nil, nil)
57
+ expect(form_value(form_data, 'style_id')).to eq('style-obj')
58
+ end
59
+ end
60
+
61
+ context 'when using the `translation_memory` option' do
62
+ it 'does not append a `translation_memory_id` field when not provided' do
63
+ expect(form_value(form_data, 'translation_memory_id')).to be_nil
64
+ end
65
+
66
+ it 'appends a `translation_memory_id` field from a translation memory ID string' do
67
+ options[:translation_memory] = 'tm-123'
68
+ expect(form_value(form_data, 'translation_memory_id')).to eq('tm-123')
69
+ end
70
+
71
+ it 'appends a `translation_memory_id` field from a TranslationMemory object' do
72
+ options[:translation_memory] =
73
+ DeepL::Resources::TranslationMemory.new({ 'translation_memory_id' => 'tm-obj' }, nil, nil)
74
+ expect(form_value(form_data, 'translation_memory_id')).to eq('tm-obj')
75
+ end
76
+
77
+ it 'appends a `translation_memory_threshold` field serialized to a string' do
78
+ options[:translation_memory] = 'tm-123'
79
+ options[:translation_memory_threshold] = 80
80
+ form = form_data
81
+ expect(form_value(form, 'translation_memory_id')).to eq('tm-123')
82
+ expect(form_value(form, 'translation_memory_threshold')).to eq('80')
83
+ end
84
+
85
+ it 'does not append a `translation_memory_threshold` field when not provided' do
86
+ options[:translation_memory] = 'tm-123'
87
+ expect(form_value(form_data, 'translation_memory_threshold')).to be_nil
88
+ end
89
+ end
90
+
91
+ context 'when using the `glossary_ids` option' do
92
+ it 'does not append a `glossary_ids` field when not provided' do
93
+ expect(form_value(form_data, 'glossary_ids')).to be_nil
94
+ end
95
+
96
+ it 'appends a comma-separated `glossary_ids` field from an array of IDs' do
97
+ options[:glossary_ids] = %w[gid1 gid2 gid3]
98
+ expect(form_value(form_data, 'glossary_ids')).to eq('gid1,gid2,gid3')
99
+ end
100
+
101
+ it 'appends `glossary_ids` from Glossary objects using their IDs' do
102
+ options[:glossary_ids] = [
103
+ DeepL::Resources::Glossary.new({ 'glossary_id' => 'gid-obj' }, nil, nil),
104
+ 'gid-str'
105
+ ]
106
+ expect(form_value(form_data, 'glossary_ids')).to eq('gid-obj,gid-str')
107
+ end
108
+
109
+ it 'raises when combined with the singular `glossary_id` option' do
110
+ options[:glossary_id] = 'single'
111
+ options[:glossary_ids] = %w[gid1]
112
+ expect { form_data }.to raise_error(ArgumentError, /cannot be used together/)
113
+ end
114
+
115
+ it 'raises when more than 5 glossary IDs are provided' do
116
+ options[:glossary_ids] = %w[a b c d e f]
117
+ expect { form_data }.to raise_error(ArgumentError, /maximum of 5 glossary IDs/)
118
+ end
119
+
120
+ context 'when `source_lang` is not set' do
121
+ let(:source_lang) { nil }
122
+
123
+ it 'raises because `glossary_ids` requires `source_lang`' do
124
+ options[:glossary_ids] = %w[gid1]
125
+ expect { form_data }.to raise_error(ArgumentError, /requires `source_lang`/)
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end