deepl-rb 3.8.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.gitlab-ci.yml +1 -1
  3. data/CHANGELOG.md +23 -1
  4. data/README.md +213 -4
  5. data/VERSION +1 -1
  6. data/deepl-rb.gemspec +34 -6
  7. data/lib/deepl/document_api.rb +11 -1
  8. data/lib/deepl/requests/base.rb +55 -3
  9. data/lib/deepl/requests/document/upload.rb +50 -1
  10. data/lib/deepl/requests/translate.rb +4 -1
  11. data/lib/deepl/requests/translation_memory/base.rb +47 -0
  12. data/lib/deepl/requests/translation_memory/create_export.rb +41 -0
  13. data/lib/deepl/requests/translation_memory/create_import.rb +53 -0
  14. data/lib/deepl/requests/translation_memory/destroy.rb +37 -0
  15. data/lib/deepl/requests/translation_memory/download_export.rb +35 -0
  16. data/lib/deepl/requests/translation_memory/find.rb +38 -0
  17. data/lib/deepl/requests/translation_memory/find_job.rb +38 -0
  18. data/lib/deepl/requests/translation_memory/list.rb +5 -18
  19. data/lib/deepl/requests/translation_memory/segments.rb +46 -0
  20. data/lib/deepl/requests/translation_memory/storage_base.rb +47 -0
  21. data/lib/deepl/requests/translation_memory/upload_file.rb +47 -0
  22. data/lib/deepl/resources/translation_memory.rb +4 -1
  23. data/lib/deepl/resources/translation_memory_export.rb +37 -0
  24. data/lib/deepl/resources/translation_memory_import.rb +29 -0
  25. data/lib/deepl/resources/translation_memory_job.rb +142 -0
  26. data/lib/deepl/resources/translation_memory_segments.rb +78 -0
  27. data/lib/deepl/translation_memory_api.rb +293 -1
  28. data/lib/deepl/utils/time_parser.rb +26 -0
  29. data/lib/deepl.rb +31 -0
  30. data/lib/version.rb +1 -1
  31. data/spec/integration_tests/document_api_spec.rb +47 -0
  32. data/spec/integration_tests/integration_test_utils.rb +8 -0
  33. data/spec/integration_tests/translate_api_spec.rb +19 -0
  34. data/spec/integration_tests/translation_memory_api_spec.rb +192 -2
  35. data/spec/integration_tests/translation_memory_error_paths_spec.rb +68 -0
  36. data/spec/requests/document/upload_spec.rb +130 -0
  37. data/spec/requests/translate_spec.rb +55 -0
  38. data/spec/requests/translation_memory/create_export_spec.rb +37 -0
  39. data/spec/requests/translation_memory/create_import_spec.rb +61 -0
  40. data/spec/requests/translation_memory/destroy_spec.rb +36 -0
  41. data/spec/requests/translation_memory/download_export_spec.rb +54 -0
  42. data/spec/requests/translation_memory/find_job_spec.rb +37 -0
  43. data/spec/requests/translation_memory/find_spec.rb +36 -0
  44. data/spec/requests/translation_memory/segments_spec.rb +58 -0
  45. data/spec/requests/translation_memory/upload_file_spec.rb +55 -0
  46. data/spec/resources/translation_memory_export_spec.rb +38 -0
  47. data/spec/resources/translation_memory_import_spec.rb +30 -0
  48. data/spec/resources/translation_memory_job_spec.rb +109 -0
  49. data/spec/resources/translation_memory_segments_spec.rb +78 -0
  50. data/spec/resources/translation_memory_spec.rb +18 -1
  51. data/spec/support/managed_glossary.rb +48 -0
  52. data/spec/support/managed_translation_memory.rb +48 -0
  53. metadata +34 -3
@@ -6,15 +6,20 @@
6
6
  require 'spec_helper'
7
7
 
8
8
  describe DeepL::TranslationMemoryApi, :mock_server_only do
9
+ include_context 'with temp dir'
10
+
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 }
14
+
9
15
  describe '#translate_with_translation_memory' do
10
16
  it 'when performing a request with translation_memory_id' do
11
17
  source_lang = 'DE'
12
18
  target_lang = 'EN'
13
19
  text = 'Protonenstrahl'
14
- translation_memory_id = 'a74d88fb-ed2a-4943-a664-a4512398b994'
15
20
 
16
21
  result = DeepL.translate(text, source_lang, target_lang,
17
- { translation_memory: translation_memory_id })
22
+ { translation_memory: default_tm_id })
18
23
  expect(result).to be_a(DeepL::Resources::Text)
19
24
  end
20
25
 
@@ -41,6 +46,191 @@ describe DeepL::TranslationMemoryApi, :mock_server_only do
41
46
  end
42
47
  end
43
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
+
44
234
  def build_test_translation_memory
45
235
  tm_data = {
46
236
  'translation_memory_id' => 'a74d88fb-ed2a-4943-a664-a4512398b994',
@@ -8,6 +8,9 @@ require 'spec_helper'
8
8
  describe DeepL::TranslationMemoryApi, :mock_server_only do # rubocop:disable RSpec/SpecFilePathFormat
9
9
  include_context 'with a live mock server'
10
10
 
11
+ let(:default_tm_id) { 'a74d88fb-ed2a-4943-a664-a4512398b994' }
12
+ let(:missing_uuid) { '00000000-0000-0000-0000-000000000000' }
13
+
11
14
  describe 'authorization failures' do
12
15
  let(:unauthorized_translation_memories) { described_class.new(unauthorized_api) }
13
16
 
@@ -15,5 +18,70 @@ describe DeepL::TranslationMemoryApi, :mock_server_only do # rubocop:disable RSp
15
18
  expect { unauthorized_translation_memories.list }
16
19
  .to raise_error(DeepL::Exceptions::AuthorizationFailed)
17
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
18
86
  end
19
87
  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
@@ -190,6 +190,61 @@ describe DeepL::Requests::Translate do
190
190
  end
191
191
  end
192
192
 
193
+ context 'when using `glossary_ids` options' do
194
+ it 'works with a nil value' do
195
+ request = described_class.new(api, nil, nil, nil, glossary_ids: nil)
196
+ expect(request.options[:glossary_ids]).to be_nil
197
+ end
198
+
199
+ it 'works with an array of strings' do
200
+ request = described_class.new(api, nil, nil, nil, glossary_ids: %w[id1 id2])
201
+ expect(request.options[:glossary_ids]).to eq(%w[id1 id2])
202
+ end
203
+ end
204
+
205
+ context 'when building the `glossary_ids` request parameter' do
206
+ def glossary_ids_param(source_lang, options)
207
+ described_class.new(api, text, source_lang, target_lang, options)
208
+ .send(:build_glossary_ids_param, source_lang)
209
+ end
210
+
211
+ it 'returns nil when `glossary_ids` is not provided' do
212
+ expect(glossary_ids_param('EN', {})).to be_nil
213
+ end
214
+
215
+ it 'returns nil when `glossary_ids` is nil' do
216
+ expect(glossary_ids_param('EN', { glossary_ids: nil })).to be_nil
217
+ end
218
+
219
+ it 'returns nil when `glossary_ids` is an empty array' do
220
+ expect(glossary_ids_param('EN', { glossary_ids: [] })).to be_nil
221
+ end
222
+
223
+ it 'resolves an array of IDs to an array of strings' do
224
+ expect(glossary_ids_param('EN', { glossary_ids: %w[id1 id2 id3] })).to eq(%w[id1 id2 id3])
225
+ end
226
+
227
+ it 'resolves glossary objects to their IDs' do
228
+ glossary = DeepL::Resources::Glossary.new({ 'glossary_id' => 'obj_id' }, nil, nil)
229
+ expect(glossary_ids_param('EN', { glossary_ids: [glossary, 'id2'] })).to eq(%w[obj_id id2])
230
+ end
231
+
232
+ it 'raises when `source_lang` is not set' do
233
+ expect { glossary_ids_param(nil, { glossary_ids: %w[id1] }) }
234
+ .to raise_error(ArgumentError, /requires `source_lang`/)
235
+ end
236
+
237
+ it 'raises when combined with the singular `glossary_id` option' do
238
+ expect { glossary_ids_param('EN', { glossary_id: 'single', glossary_ids: %w[id1] }) }
239
+ .to raise_error(ArgumentError, /cannot be used together with the `glossary_id`/)
240
+ end
241
+
242
+ it 'raises when more than 5 IDs are provided' do
243
+ expect { glossary_ids_param('EN', { glossary_ids: %w[id1 id2 id3 id4 id5 id6] }) }
244
+ .to raise_error(ArgumentError, /maximum of 5 glossary IDs/)
245
+ end
246
+ end
247
+
193
248
  context 'when using `formality` options' do
194
249
  it 'works with a nil values' do
195
250
  request = described_class.new(api, nil, nil, nil, formality: nil)
@@ -0,0 +1,37 @@
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::TranslationMemory::CreateExport do
9
+ subject(:translation_memory_create_export) { described_class.new(api, id, options) }
10
+
11
+ around do |tests|
12
+ tmp_env = replace_env_preserving_deepl_vars_except_mock_server
13
+ tests.call
14
+ ENV.replace(tmp_env)
15
+ end
16
+
17
+ let(:api) { build_deepl_api }
18
+ let(:id) { 'a74d88fb-ed2a-4943-a664-a4512398b994' }
19
+ let(:options) { {} }
20
+
21
+ describe '#initialize' do
22
+ context 'when building a request' do
23
+ it 'creates a request object' do
24
+ expect(translation_memory_create_export).to be_a(described_class)
25
+ end
26
+ end
27
+ end
28
+
29
+ describe '#to_s' do
30
+ context 'when building a request' do
31
+ it 'posts to the export endpoint below the v3 prefix' do
32
+ expect(translation_memory_create_export.to_s)
33
+ .to eq("POST /v3/translation_memories/#{id}/export")
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,61 @@
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::TranslationMemory::CreateImport do
9
+ subject(:translation_memory_create_import) do
10
+ described_class.new(api, file_name, content_length, 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(:file_name) { 'legal.tmx' }
21
+ let(:content_length) { 128 }
22
+ let(:options) { {} }
23
+
24
+ describe '#initialize' do
25
+ context 'when building a request' do
26
+ it 'creates a request object' do
27
+ expect(translation_memory_create_import).to be_a(described_class)
28
+ end
29
+ end
30
+ end
31
+
32
+ describe '#to_s' do
33
+ context 'when building a request' do
34
+ it 'posts to the import endpoint below the v3 prefix' do
35
+ expect(translation_memory_create_import.to_s)
36
+ .to eq('POST /v3/translation_memories/import')
37
+ end
38
+ end
39
+ end
40
+
41
+ describe '#details' do
42
+ context 'when only the required parameters are given' do
43
+ it 'declares the source file without the optional fields' do
44
+ expect(translation_memory_create_import.details).to include('legal.tmx', '128')
45
+ expect(translation_memory_create_import.details).not_to include('content_type')
46
+ expect(translation_memory_create_import.details).not_to include('parameters')
47
+ end
48
+ end
49
+
50
+ context 'when the optional parameters are given' do
51
+ let(:options) { { content_type: 'text/xml', display_name: 'Legal' } }
52
+
53
+ it 'declares the content type and the display name' do
54
+ expect(translation_memory_create_import.details)
55
+ .to include('content_type', 'text/xml')
56
+ expect(translation_memory_create_import.details)
57
+ .to include('parameters', 'display_name', 'Legal')
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,36 @@
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::TranslationMemory::Destroy do
9
+ subject(:translation_memory_destroy) { described_class.new(api, id, options) }
10
+
11
+ around do |tests|
12
+ tmp_env = replace_env_preserving_deepl_vars_except_mock_server
13
+ tests.call
14
+ ENV.replace(tmp_env)
15
+ end
16
+
17
+ let(:api) { build_deepl_api }
18
+ let(:id) { 'a74d88fb-ed2a-4943-a664-a4512398b994' }
19
+ let(:options) { {} }
20
+
21
+ describe '#initialize' do
22
+ context 'when building a request' do
23
+ it 'creates a request object' do
24
+ expect(translation_memory_destroy).to be_a(described_class)
25
+ end
26
+ end
27
+ end
28
+
29
+ describe '#to_s' do
30
+ context 'when building a request' do
31
+ it 'deletes the translation memory below the v3 prefix' do
32
+ expect(translation_memory_destroy.to_s).to eq("DELETE /v3/translation_memories/#{id}")
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,54 @@
1
+ # Copyright 2026 DeepL SE (https://www.deepl.com)
2
+ # Use of this source code is governed by an MIT
3
+ # license that can be found in the LICENSE.md file.
4
+ # frozen_string_literal: true
5
+
6
+ require 'spec_helper'
7
+
8
+ describe DeepL::Requests::TranslationMemory::DownloadExport do
9
+ subject(:download_export) { described_class.new(api, download_url, output_path) }
10
+
11
+ around do |tests|
12
+ tmp_env = replace_env_preserving_deepl_vars_except_mock_server
13
+ tests.call
14
+ ENV.replace(tmp_env)
15
+ end
16
+
17
+ let(:api) { build_deepl_api }
18
+ let(:download_url) { 'https://storage.example.com/download/tm?signature=abc' }
19
+ let(:output_path) { '/tmp/export.tmx' }
20
+
21
+ describe '#initialize' do
22
+ context 'when building a request' do
23
+ it 'creates a request object' do
24
+ expect(download_export).to be_a(described_class)
25
+ end
26
+ end
27
+
28
+ context 'when no download URL is given' do
29
+ let(:download_url) { nil }
30
+
31
+ it 'raises an error' do
32
+ expect { download_export }.to raise_error(DeepL::Exceptions::Error, /must not be empty/)
33
+ end
34
+ end
35
+ end
36
+
37
+ describe '#to_s' do
38
+ context 'when building a request' do
39
+ it 'gets from the pre-signed storage URL' do
40
+ expect(download_export.to_s).to eq('GET /download/tm?signature=abc')
41
+ end
42
+ end
43
+ end
44
+
45
+ describe '#details' do
46
+ context 'when building a request' do
47
+ # The download URL points outside of the DeepL API, so the auth key must not be sent there.
48
+ it 'does not send the DeepL authorization header' do
49
+ expect(download_export.details).not_to include('Authorization')
50
+ expect(download_export.details).to include('User-Agent')
51
+ end
52
+ end
53
+ end
54
+ end