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.
- checksums.yaml +4 -4
- data/.gitlab-ci.yml +4 -2
- data/CHANGELOG.md +39 -1
- data/Gemfile +0 -2
- data/README.md +225 -16
- data/VERSION +1 -1
- data/deepl-rb.gemspec +52 -19
- 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/rephrase.rb +3 -2
- data/lib/deepl/requests/translate.rb +6 -3
- 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 +38 -4
- data/lib/version.rb +1 -1
- data/spec/api/deepl_spec.rb +134 -332
- data/spec/integration_tests/document_api_spec.rb +51 -18
- data/spec/integration_tests/document_error_paths_spec.rb +33 -0
- data/spec/integration_tests/glossary_api_spec.rb +114 -0
- data/spec/integration_tests/glossary_error_paths_spec.rb +107 -0
- data/spec/integration_tests/integration_test_utils.rb +8 -0
- data/spec/integration_tests/languages_api_spec.rb +54 -0
- data/spec/integration_tests/languages_error_paths_spec.rb +25 -0
- data/spec/integration_tests/rephrase_api_spec.rb +90 -0
- data/spec/integration_tests/rephrase_error_paths_spec.rb +53 -0
- data/spec/integration_tests/smoke_test_spec.rb +24 -0
- data/spec/integration_tests/style_rule_api_spec.rb +1 -19
- data/spec/integration_tests/style_rule_error_paths_spec.rb +45 -0
- data/spec/integration_tests/translate_api_spec.rb +117 -0
- data/spec/integration_tests/translate_error_paths_spec.rb +48 -0
- data/spec/integration_tests/translation_memory_api_spec.rb +191 -17
- data/spec/integration_tests/translation_memory_error_paths_spec.rb +87 -0
- data/spec/integration_tests/usage_api_spec.rb +29 -0
- data/spec/integration_tests/usage_error_paths_spec.rb +18 -0
- data/spec/requests/document/upload_spec.rb +130 -0
- data/spec/requests/glossary/create_spec.rb +0 -21
- data/spec/requests/glossary/destroy_spec.rb +0 -39
- data/spec/requests/glossary/entries_spec.rb +0 -35
- data/spec/requests/glossary/find_spec.rb +0 -40
- data/spec/requests/glossary/language_pairs_spec.rb +0 -13
- data/spec/requests/glossary/list_spec.rb +0 -27
- data/spec/requests/languages_spec.rb +0 -41
- data/spec/requests/rephrase_spec.rb +13 -139
- data/spec/requests/style_rule/create_custom_instruction_spec.rb +0 -24
- data/spec/requests/style_rule/create_spec.rb +0 -16
- data/spec/requests/style_rule/destroy_custom_instruction_spec.rb +0 -26
- data/spec/requests/style_rule/destroy_spec.rb +0 -27
- data/spec/requests/style_rule/find_custom_instruction_spec.rb +0 -27
- data/spec/requests/style_rule/find_spec.rb +0 -28
- data/spec/requests/style_rule/list_spec.rb +0 -31
- data/spec/requests/style_rule/update_configured_rules_spec.rb +0 -21
- data/spec/requests/style_rule/update_custom_instruction_spec.rb +0 -26
- data/spec/requests/style_rule/update_spec.rb +0 -19
- data/spec/requests/translate_spec.rb +63 -217
- 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/list_spec.rb +0 -34
- data/spec/requests/translation_memory/segments_spec.rb +58 -0
- data/spec/requests/translation_memory/upload_file_spec.rb +55 -0
- data/spec/requests/usage_spec.rb +0 -16
- data/spec/resources/custom_instruction_spec.rb +32 -0
- data/spec/resources/style_rule_spec.rb +68 -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/spec_helper.rb +15 -45
- data/spec/support/live_mock_server.rb +12 -0
- data/spec/support/managed_glossary.rb +65 -0
- data/spec/support/managed_style_rule.rb +17 -0
- data/spec/support/managed_translation_memory.rb +55 -0
- metadata +50 -17
- data/spec/fixtures/vcr_cassettes/deepl_document.yml +0 -95
- data/spec/fixtures/vcr_cassettes/deepl_document_download.yml +0 -1214
- data/spec/fixtures/vcr_cassettes/deepl_glossaries.yml +0 -1163
- data/spec/fixtures/vcr_cassettes/deepl_languages.yml +0 -54
- data/spec/fixtures/vcr_cassettes/deepl_rephrase.yml +0 -87
- data/spec/fixtures/vcr_cassettes/deepl_translate.yml +0 -358
- data/spec/fixtures/vcr_cassettes/deepl_usage.yml +0 -129
- data/spec/fixtures/vcr_cassettes/glossaries.yml +0 -1702
- data/spec/fixtures/vcr_cassettes/languages.yml +0 -229
- data/spec/fixtures/vcr_cassettes/rephrase_texts.yml +0 -401
- data/spec/fixtures/vcr_cassettes/style_rules.yml +0 -92
- data/spec/fixtures/vcr_cassettes/style_rules_crud.yml +0 -926
- data/spec/fixtures/vcr_cassettes/translate_texts.yml +0 -10630
- data/spec/fixtures/vcr_cassettes/translation_memories.yml +0 -74
- 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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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:
|
|
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
|