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.
- checksums.yaml +4 -4
- data/.gitlab-ci.yml +1 -1
- data/CHANGELOG.md +23 -1
- data/README.md +213 -4
- data/VERSION +1 -1
- data/deepl-rb.gemspec +34 -6
- data/lib/deepl/document_api.rb +11 -1
- data/lib/deepl/requests/base.rb +55 -3
- data/lib/deepl/requests/document/upload.rb +50 -1
- data/lib/deepl/requests/translate.rb +4 -1
- data/lib/deepl/requests/translation_memory/base.rb +47 -0
- data/lib/deepl/requests/translation_memory/create_export.rb +41 -0
- data/lib/deepl/requests/translation_memory/create_import.rb +53 -0
- data/lib/deepl/requests/translation_memory/destroy.rb +37 -0
- data/lib/deepl/requests/translation_memory/download_export.rb +35 -0
- data/lib/deepl/requests/translation_memory/find.rb +38 -0
- data/lib/deepl/requests/translation_memory/find_job.rb +38 -0
- data/lib/deepl/requests/translation_memory/list.rb +5 -18
- data/lib/deepl/requests/translation_memory/segments.rb +46 -0
- data/lib/deepl/requests/translation_memory/storage_base.rb +47 -0
- data/lib/deepl/requests/translation_memory/upload_file.rb +47 -0
- data/lib/deepl/resources/translation_memory.rb +4 -1
- data/lib/deepl/resources/translation_memory_export.rb +37 -0
- data/lib/deepl/resources/translation_memory_import.rb +29 -0
- data/lib/deepl/resources/translation_memory_job.rb +142 -0
- data/lib/deepl/resources/translation_memory_segments.rb +78 -0
- data/lib/deepl/translation_memory_api.rb +293 -1
- data/lib/deepl/utils/time_parser.rb +26 -0
- data/lib/deepl.rb +31 -0
- data/lib/version.rb +1 -1
- data/spec/integration_tests/document_api_spec.rb +47 -0
- data/spec/integration_tests/integration_test_utils.rb +8 -0
- data/spec/integration_tests/translate_api_spec.rb +19 -0
- data/spec/integration_tests/translation_memory_api_spec.rb +192 -2
- data/spec/integration_tests/translation_memory_error_paths_spec.rb +68 -0
- data/spec/requests/document/upload_spec.rb +130 -0
- data/spec/requests/translate_spec.rb +55 -0
- data/spec/requests/translation_memory/create_export_spec.rb +37 -0
- data/spec/requests/translation_memory/create_import_spec.rb +61 -0
- data/spec/requests/translation_memory/destroy_spec.rb +36 -0
- data/spec/requests/translation_memory/download_export_spec.rb +54 -0
- data/spec/requests/translation_memory/find_job_spec.rb +37 -0
- data/spec/requests/translation_memory/find_spec.rb +36 -0
- data/spec/requests/translation_memory/segments_spec.rb +58 -0
- data/spec/requests/translation_memory/upload_file_spec.rb +55 -0
- data/spec/resources/translation_memory_export_spec.rb +38 -0
- data/spec/resources/translation_memory_import_spec.rb +30 -0
- data/spec/resources/translation_memory_job_spec.rb +109 -0
- data/spec/resources/translation_memory_segments_spec.rb +78 -0
- data/spec/resources/translation_memory_spec.rb +18 -1
- data/spec/support/managed_glossary.rb +48 -0
- data/spec/support/managed_translation_memory.rb +48 -0
- metadata +34 -3
|
@@ -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::FindJob do
|
|
9
|
+
subject(:translation_memory_find_job) { described_class.new(api, job_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(:job_id) { '5b1a0a52-1f3b-4b7f-9f6b-9d4d3f5b6c7d' }
|
|
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_find_job).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 'requests the job below the v3 prefix' do
|
|
32
|
+
expect(translation_memory_find_job.to_s)
|
|
33
|
+
.to eq("GET /v3/translation_memories/jobs/#{job_id}")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
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::Find do
|
|
9
|
+
subject(:translation_memory_find) { 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_find).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 'requests the translation memory below the v3 prefix' do
|
|
32
|
+
expect(translation_memory_find.to_s).to eq("GET /v3/translation_memories/#{id}")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
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::Segments do
|
|
9
|
+
subject(:translation_memory_segments) { 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_segments).to be_a(described_class)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
describe '#to_s' do
|
|
30
|
+
context 'when no options are given' do
|
|
31
|
+
it 'requests the segments below the v3 prefix without a query string' do
|
|
32
|
+
expect(translation_memory_segments.to_s)
|
|
33
|
+
.to eq("GET /v3/translation_memories/#{id}/segments")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
context 'when the supported options are given' do
|
|
38
|
+
let(:options) { { page_size: 10, page_cursor: 'NQ', filter_case_sensitive: true } }
|
|
39
|
+
|
|
40
|
+
it 'adds the options to the query string' do
|
|
41
|
+
expect(translation_memory_segments.to_s)
|
|
42
|
+
.to eq("GET /v3/translation_memories/#{id}/segments" \
|
|
43
|
+
'?page_size=10&page_cursor=NQ&filter_case_sensitive=true')
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
context 'when the text filter contains characters that must be escaped' do
|
|
48
|
+
let(:options) { { filter_text: 'a b+c' } }
|
|
49
|
+
|
|
50
|
+
# A space must be percent-encoded in a URI query string; `+` only means a space in a form
|
|
51
|
+
# body, and a literal `+` has to survive as %2B.
|
|
52
|
+
it 'percent-encodes the spaces instead of writing them as a plus' do
|
|
53
|
+
expect(translation_memory_segments.to_s)
|
|
54
|
+
.to eq("GET /v3/translation_memories/#{id}/segments?filter_text=a%20b%2Bc")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
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::UploadFile do
|
|
9
|
+
subject(:upload_file) { described_class.new(api, upload_url, file_content) }
|
|
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(:upload_url) { 'https://storage.example.com/upload/tm?signature=abc' }
|
|
19
|
+
let(:file_content) { '<tmx version="1.4"/>' }
|
|
20
|
+
|
|
21
|
+
describe '#initialize' do
|
|
22
|
+
context 'when building a request' do
|
|
23
|
+
it 'creates a request object' do
|
|
24
|
+
expect(upload_file).to be_a(described_class)
|
|
25
|
+
expect(upload_file.content_type).to eq(described_class::DEFAULT_CONTENT_TYPE)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
context 'when no upload URL is given' do
|
|
30
|
+
let(:upload_url) { '' }
|
|
31
|
+
|
|
32
|
+
it 'raises an error' do
|
|
33
|
+
expect { upload_file }.to raise_error(DeepL::Exceptions::Error, /must not be empty/)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe '#to_s' do
|
|
39
|
+
context 'when building a request' do
|
|
40
|
+
it 'puts to the pre-signed storage URL' do
|
|
41
|
+
expect(upload_file.to_s).to eq('PUT /upload/tm?signature=abc')
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe '#details' do
|
|
47
|
+
context 'when building a request' do
|
|
48
|
+
# The upload URL points outside of the DeepL API, so the auth key must not be sent there.
|
|
49
|
+
it 'does not send the DeepL authorization header' do
|
|
50
|
+
expect(upload_file.details).not_to include('Authorization')
|
|
51
|
+
expect(upload_file.details).to include('User-Agent')
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
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::Resources::TranslationMemoryExport do
|
|
9
|
+
subject(:translation_memory_export) { described_class.new(response, false, nil, nil) }
|
|
10
|
+
|
|
11
|
+
let(:response) do
|
|
12
|
+
{
|
|
13
|
+
'job_id' => '5b1a0a52-1f3b-4b7f-9f6b-9d4d3f5b6c7d',
|
|
14
|
+
'parameters' => { 'translation_memory_id' => 'a74d88fb-ed2a-4943-a664-a4512398b994' }
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe '#initialize' do
|
|
19
|
+
it 'creates a resource' do
|
|
20
|
+
expect(translation_memory_export).to be_a(described_class)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'assigns the attributes of a newly created export' do
|
|
24
|
+
expect(translation_memory_export.job_id).to eq('5b1a0a52-1f3b-4b7f-9f6b-9d4d3f5b6c7d')
|
|
25
|
+
expect(translation_memory_export.translation_memory_id)
|
|
26
|
+
.to eq('a74d88fb-ed2a-4943-a664-a4512398b994')
|
|
27
|
+
expect(translation_memory_export.reused_existing?).to be(false)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
context 'when the API reused a previously completed export' do
|
|
31
|
+
subject(:translation_memory_export) { described_class.new(response, true, nil, nil) }
|
|
32
|
+
|
|
33
|
+
it 'reports the export as reused' do
|
|
34
|
+
expect(translation_memory_export.reused_existing?).to be(true)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
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::Resources::TranslationMemoryImport do
|
|
9
|
+
subject(:translation_memory_import) do
|
|
10
|
+
described_class.new({
|
|
11
|
+
'job_id' => '5b1a0a52-1f3b-4b7f-9f6b-9d4d3f5b6c7d',
|
|
12
|
+
'upload_url' => 'https://storage.example.com/upload',
|
|
13
|
+
'expires_at' => '2026-01-01T00:00:00Z'
|
|
14
|
+
}, nil, nil)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe '#initialize' do
|
|
18
|
+
it 'creates a resource' do
|
|
19
|
+
expect(translation_memory_import).to be_a(described_class)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'assigns the attributes' do
|
|
23
|
+
expect(translation_memory_import.job_id).to eq('5b1a0a52-1f3b-4b7f-9f6b-9d4d3f5b6c7d')
|
|
24
|
+
expect(translation_memory_import.upload_url).to eq('https://storage.example.com/upload')
|
|
25
|
+
expect(translation_memory_import.expires_at).to eq(Time.parse('2026-01-01T00:00:00Z'))
|
|
26
|
+
expect(translation_memory_import.to_s)
|
|
27
|
+
.to eq('TranslationMemoryImport: ID: 5b1a0a52-1f3b-4b7f-9f6b-9d4d3f5b6c7d')
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
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::Resources::TranslationMemoryJob do
|
|
9
|
+
subject(:job) { described_class.new(response, nil, nil) }
|
|
10
|
+
|
|
11
|
+
let(:response) do
|
|
12
|
+
{
|
|
13
|
+
'job_id' => '5b1a0a52-1f3b-4b7f-9f6b-9d4d3f5b6c7d',
|
|
14
|
+
'product' => 'translation_memory',
|
|
15
|
+
'operation' => 'import',
|
|
16
|
+
'creation_time' => '2026-01-01T00:00:00Z',
|
|
17
|
+
'updated_time' => '2026-01-02T00:00:00Z',
|
|
18
|
+
'source_file' => { 'content_type' => 'application/xml', 'content_length' => 128 },
|
|
19
|
+
'parameters' => { 'display_name' => 'Legal' },
|
|
20
|
+
'results' => [
|
|
21
|
+
{ 'status' => 'completed', 'translation_memory_id' => 'tm-1',
|
|
22
|
+
'skipped_segment_count' => 2 }
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe '#initialize' do
|
|
28
|
+
it 'creates a resource' do
|
|
29
|
+
expect(job).to be_a(described_class)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'assigns the job level attributes' do
|
|
33
|
+
expect(job.job_id).to eq('5b1a0a52-1f3b-4b7f-9f6b-9d4d3f5b6c7d')
|
|
34
|
+
expect(job.product).to eq('translation_memory')
|
|
35
|
+
expect(job.operation).to eq('import')
|
|
36
|
+
expect(job.creation_time).to eq(Time.parse('2026-01-01T00:00:00Z'))
|
|
37
|
+
expect(job.updated_time).to eq(Time.parse('2026-01-02T00:00:00Z'))
|
|
38
|
+
expect(job.display_name).to eq('Legal')
|
|
39
|
+
expect(job.source_content_type).to eq('application/xml')
|
|
40
|
+
expect(job.source_content_length).to eq(128)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'maps the single result and delegates the status helpers to it' do
|
|
44
|
+
expect(job.result).to be_a(DeepL::Resources::TranslationMemoryJobResult)
|
|
45
|
+
expect(job.status).to eq('completed')
|
|
46
|
+
expect(job.finished?).to be(true)
|
|
47
|
+
expect(job.awaiting_input?).to be(false)
|
|
48
|
+
expect(job.error?).to be(false)
|
|
49
|
+
expect(job.result.translation_memory_id).to eq('tm-1')
|
|
50
|
+
expect(job.result.skipped_segment_count).to eq(2)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
context 'when the job is awaiting its file upload' do
|
|
55
|
+
let(:response) do
|
|
56
|
+
{ 'job_id' => 'job-1', 'operation' => 'import',
|
|
57
|
+
'results' => [{ 'status' => 'awaiting_input',
|
|
58
|
+
'status_metadata' => { 'required_action' => 'Waiting for upload' } }] }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'exposes the required action and does not report the job as finished' do
|
|
62
|
+
expect(job.awaiting_input?).to be(true)
|
|
63
|
+
expect(job.finished?).to be(false)
|
|
64
|
+
expect(job.result.required_action).to eq('Waiting for upload')
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
context 'when the export job completed' do
|
|
69
|
+
let(:response) do
|
|
70
|
+
{ 'job_id' => 'job-2', 'operation' => 'export',
|
|
71
|
+
'parameters' => { 'translation_memory_id' => 'tm-1' },
|
|
72
|
+
'results' => [{ 'status' => 'completed', 'download_url' => 'https://example.com/tmx',
|
|
73
|
+
'expires_at' => '2026-01-03T00:00:00Z' }] }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'exposes the download URL and its expiry' do
|
|
77
|
+
expect(job.translation_memory_id).to eq('tm-1')
|
|
78
|
+
expect(job.result.download_url).to eq('https://example.com/tmx')
|
|
79
|
+
expect(job.result.expires_at).to eq(Time.parse('2026-01-03T00:00:00Z'))
|
|
80
|
+
expect(job.to_s).to eq('TranslationMemoryJob: export - ID: job-2 - Status: completed')
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
context 'when the job failed' do
|
|
85
|
+
let(:response) do
|
|
86
|
+
{ 'job_id' => 'job-3', 'operation' => 'import',
|
|
87
|
+
'results' => [{ 'status' => 'failed', 'error' => { 'message' => 'Invalid TMX' } }] }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'reports the error and treats the job as finished' do
|
|
91
|
+
expect(job.error?).to be(true)
|
|
92
|
+
expect(job.finished?).to be(true)
|
|
93
|
+
expect(job.error_message).to eq('Invalid TMX')
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
context 'when the API returned no result' do
|
|
98
|
+
let(:response) { { 'job_id' => 'job-4', 'operation' => 'import' } }
|
|
99
|
+
|
|
100
|
+
it 'reports neither a status nor an error' do
|
|
101
|
+
expect(job.result).to be_nil
|
|
102
|
+
expect(job.status).to be_nil
|
|
103
|
+
expect(job.finished?).to be(false)
|
|
104
|
+
expect(job.awaiting_input?).to be(false)
|
|
105
|
+
expect(job.error?).to be(false)
|
|
106
|
+
expect(job.error_message).to be_nil
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
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::Resources::TranslationMemorySegments do
|
|
9
|
+
subject(:segments_page) { described_class.new(response, nil, nil) }
|
|
10
|
+
|
|
11
|
+
let(:response) do
|
|
12
|
+
{
|
|
13
|
+
'segments' => [
|
|
14
|
+
{
|
|
15
|
+
'source_segment_id' => 'tm-1-source-0',
|
|
16
|
+
'source_text' => 'Quelltext Nummer 0',
|
|
17
|
+
'creation_time' => '2026-01-01T00:00:00Z',
|
|
18
|
+
'updated_time' => '2026-01-02T00:00:00Z',
|
|
19
|
+
'last_used_time' => '2026-01-03T00:00:00Z',
|
|
20
|
+
'targets' => [
|
|
21
|
+
{
|
|
22
|
+
'target_segment_id' => 'tm-1-target-en-0',
|
|
23
|
+
'target_language' => 'en',
|
|
24
|
+
'target_text' => 'Source text number 0',
|
|
25
|
+
'creation_time' => '2026-01-01T00:00:00Z',
|
|
26
|
+
'updated_time' => '2026-01-02T00:00:00Z',
|
|
27
|
+
'last_used_time' => '2026-01-03T00:00:00Z'
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
'segment_count' => 12,
|
|
33
|
+
'next_page_cursor' => 'NQ'
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe '#initialize' do
|
|
38
|
+
it 'creates a resource' do
|
|
39
|
+
expect(segments_page).to be_a(described_class)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'assigns the page level attributes' do
|
|
43
|
+
expect(segments_page.segment_count).to eq(12)
|
|
44
|
+
expect(segments_page.next_page_cursor).to eq('NQ')
|
|
45
|
+
expect(segments_page.next_page?).to be(true)
|
|
46
|
+
expect(segments_page.to_s).to eq('1 of 12 segment(s)')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'maps the segments into resources' do
|
|
50
|
+
segment = segments_page.segments.first
|
|
51
|
+
expect(segment).to be_a(DeepL::Resources::TranslationMemorySegment)
|
|
52
|
+
expect(segment.source_segment_id).to eq('tm-1-source-0')
|
|
53
|
+
expect(segment.source_text).to eq('Quelltext Nummer 0')
|
|
54
|
+
expect(segment.creation_time).to eq(Time.parse('2026-01-01T00:00:00Z'))
|
|
55
|
+
expect(segment.updated_time).to eq(Time.parse('2026-01-02T00:00:00Z'))
|
|
56
|
+
expect(segment.last_used_time).to eq(Time.parse('2026-01-03T00:00:00Z'))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'maps the target segments into resources' do
|
|
60
|
+
target = segments_page.segments.first.targets.first
|
|
61
|
+
expect(target).to be_a(DeepL::Resources::TranslationMemoryTargetSegment)
|
|
62
|
+
expect(target.target_segment_id).to eq('tm-1-target-en-0')
|
|
63
|
+
expect(target.target_language).to eq('en')
|
|
64
|
+
expect(target.target_text).to eq('Source text number 0')
|
|
65
|
+
expect(target.to_s).to eq('en: Source text number 0')
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
context 'when the response is the last page' do
|
|
70
|
+
let(:response) { { 'segments' => [], 'segment_count' => 12 } }
|
|
71
|
+
|
|
72
|
+
it 'reports no further page' do
|
|
73
|
+
expect(segments_page.segments).to eq([])
|
|
74
|
+
expect(segments_page.next_page_cursor).to be_nil
|
|
75
|
+
expect(segments_page.next_page?).to be(false)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -12,7 +12,9 @@ describe DeepL::Resources::TranslationMemory do
|
|
|
12
12
|
'name' => 'Legal',
|
|
13
13
|
'source_language' => 'en',
|
|
14
14
|
'target_languages' => %w[es de],
|
|
15
|
-
'segment_count' => 3542
|
|
15
|
+
'segment_count' => 3542,
|
|
16
|
+
'creation_time' => '2026-01-01T00:00:00Z',
|
|
17
|
+
'updated_time' => '2026-01-02T00:00:00Z'
|
|
16
18
|
}, nil, nil)
|
|
17
19
|
end
|
|
18
20
|
|
|
@@ -29,6 +31,21 @@ describe DeepL::Resources::TranslationMemory do
|
|
|
29
31
|
expect(translation_memory.source_language).to eq('en')
|
|
30
32
|
expect(translation_memory.target_languages).to eq(%w[es de])
|
|
31
33
|
expect(translation_memory.segment_count).to eq(3542)
|
|
34
|
+
expect(translation_memory.creation_time).to eq(Time.parse('2026-01-01T00:00:00Z'))
|
|
35
|
+
expect(translation_memory.updated_time).to eq(Time.parse('2026-01-02T00:00:00Z'))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
context 'when the optional timestamps are missing' do
|
|
40
|
+
subject(:translation_memory) do
|
|
41
|
+
described_class.new({ 'translation_memory_id' => 'tm-1', 'name' => 'Legal' }, nil, nil)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'leaves the timestamps nil and defaults the collections' do
|
|
45
|
+
expect(translation_memory.creation_time).to be_nil
|
|
46
|
+
expect(translation_memory.updated_time).to be_nil
|
|
47
|
+
expect(translation_memory.target_languages).to eq([])
|
|
48
|
+
expect(translation_memory.segment_count).to eq(0)
|
|
32
49
|
end
|
|
33
50
|
end
|
|
34
51
|
end
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
# frozen_string_literal: true
|
|
5
5
|
|
|
6
6
|
module ManagedGlossary
|
|
7
|
+
# Bounded retry budget for tolerating a freshly-created glossary that is not
|
|
8
|
+
# yet resolvable on the real API (eventual consistency): up to ~10 seconds.
|
|
9
|
+
GLOSSARY_RETRY_ATTEMPTS = 20
|
|
10
|
+
GLOSSARY_RETRY_DELAY = 0.5
|
|
11
|
+
|
|
7
12
|
def with_managed_glossary(name:, source_lang:, target_lang:, entries:, options: {})
|
|
8
13
|
glossary = DeepL.glossaries.create(name, source_lang, target_lang, entries, options)
|
|
9
14
|
yield glossary
|
|
@@ -14,4 +19,47 @@ module ManagedGlossary
|
|
|
14
19
|
nil
|
|
15
20
|
end
|
|
16
21
|
end
|
|
22
|
+
|
|
23
|
+
# A just-created glossary is not always immediately resolvable by id on the
|
|
24
|
+
# real API (eventual consistency). Poll #find until each glossary is
|
|
25
|
+
# retrievable so subsequent calls (e.g. translate with glossary_ids) do not
|
|
26
|
+
# fail with a transient "Glossary <uuid> not found" error.
|
|
27
|
+
#
|
|
28
|
+
# Note: no keyword arguments here — Ruby 2.7 would fold a trailing options
|
|
29
|
+
# hash from callers into keywords, breaking positional-hash APIs.
|
|
30
|
+
def wait_until_glossaries_ready(*glossaries)
|
|
31
|
+
glossaries.each do |glossary|
|
|
32
|
+
remaining = GLOSSARY_RETRY_ATTEMPTS
|
|
33
|
+
begin
|
|
34
|
+
DeepL.glossaries.find(glossary.id)
|
|
35
|
+
rescue DeepL::Exceptions::RequestError
|
|
36
|
+
raise if (remaining -= 1) <= 0
|
|
37
|
+
|
|
38
|
+
sleep(GLOSSARY_RETRY_DELAY)
|
|
39
|
+
retry
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Translate with a bounded retry that tolerates a transient
|
|
45
|
+
# "Glossary <uuid> not found" error caused by eventual consistency on a
|
|
46
|
+
# freshly created glossary. Depending on timing the real API returns this as
|
|
47
|
+
# either a 400 or a 404, so match on the message rather than the status.
|
|
48
|
+
# Any other error is re-raised immediately.
|
|
49
|
+
#
|
|
50
|
+
# `options` is an explicit positional hash (matching DeepL.translate) rather
|
|
51
|
+
# than keyword arguments, so passing `{ glossary_ids: [...] }` works on Ruby
|
|
52
|
+
# 2.7 as well as 3.x.
|
|
53
|
+
def translate_retrying_missing_glossary(text, source_lang, target_lang, options = {})
|
|
54
|
+
remaining = GLOSSARY_RETRY_ATTEMPTS
|
|
55
|
+
begin
|
|
56
|
+
DeepL.translate(text, source_lang, target_lang, options)
|
|
57
|
+
rescue DeepL::Exceptions::RequestError => e
|
|
58
|
+
raise unless e.to_s.match?(/glossary .* not found/i)
|
|
59
|
+
raise if (remaining -= 1) <= 0
|
|
60
|
+
|
|
61
|
+
sleep(GLOSSARY_RETRY_DELAY)
|
|
62
|
+
retry
|
|
63
|
+
end
|
|
64
|
+
end
|
|
17
65
|
end
|
|
@@ -4,4 +4,52 @@
|
|
|
4
4
|
# frozen_string_literal: true
|
|
5
5
|
|
|
6
6
|
module ManagedTranslationMemory
|
|
7
|
+
# A minimal, valid TMX document to import in the translation memory specs.
|
|
8
|
+
EXAMPLE_TMX = <<~TMX
|
|
9
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
10
|
+
<tmx version="1.4">
|
|
11
|
+
<header creationtool="deepl-rb-spec" srclang="de" segtype="sentence"
|
|
12
|
+
adminlang="en" datatype="plaintext" o-tmf="TMX"/>
|
|
13
|
+
<body>
|
|
14
|
+
<tu>
|
|
15
|
+
<tuv xml:lang="de"><seg>Quelltext Nummer 0</seg></tuv>
|
|
16
|
+
<tuv xml:lang="en"><seg>Source text number 0</seg></tuv>
|
|
17
|
+
</tu>
|
|
18
|
+
</body>
|
|
19
|
+
</tmx>
|
|
20
|
+
TMX
|
|
21
|
+
|
|
22
|
+
# Imports the TMX file at +input_file_path+ and yields the finished import job, deleting the
|
|
23
|
+
# resulting translation memory afterwards.
|
|
24
|
+
def with_managed_translation_memory(input_file_path, display_name: nil)
|
|
25
|
+
job = DeepL.translation_memories.import_from_filepath(input_file_path,
|
|
26
|
+
display_name: display_name)
|
|
27
|
+
yield job
|
|
28
|
+
ensure
|
|
29
|
+
begin
|
|
30
|
+
DeepL.translation_memories.destroy(job.result.translation_memory_id) if job
|
|
31
|
+
rescue StandardError
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Creates an import job for the TMX file at +input_file_path+ through +translation_memories+
|
|
37
|
+
# and uploads the file, then returns the created import. The mock server keeps the job in the
|
|
38
|
+
# `awaiting_input` status for +processing_polls+ status queries before completing it, standing
|
|
39
|
+
# in for the live API detecting the upload asynchronously.
|
|
40
|
+
def create_uploaded_import(translation_memories, input_file_path, processing_polls:)
|
|
41
|
+
file_content = File.binread(input_file_path)
|
|
42
|
+
created = translation_memories.create_import(
|
|
43
|
+
File.basename(input_file_path), file_content.bytesize,
|
|
44
|
+
additional_headers: tm_job_processing_polls_header(processing_polls)
|
|
45
|
+
)
|
|
46
|
+
translation_memories.upload_file(created, file_content)
|
|
47
|
+
created
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Writes the example TMX document to +path+ and returns the path.
|
|
51
|
+
def write_example_tmx(path)
|
|
52
|
+
File.write(path, EXAMPLE_TMX)
|
|
53
|
+
path
|
|
54
|
+
end
|
|
7
55
|
end
|