deepl-rb 3.7.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/.gitlab-ci.yml +4 -2
  3. data/CHANGELOG.md +39 -1
  4. data/Gemfile +0 -2
  5. data/README.md +225 -16
  6. data/VERSION +1 -1
  7. data/deepl-rb.gemspec +52 -19
  8. data/lib/deepl/document_api.rb +11 -1
  9. data/lib/deepl/requests/base.rb +55 -3
  10. data/lib/deepl/requests/document/upload.rb +50 -1
  11. data/lib/deepl/requests/rephrase.rb +3 -2
  12. data/lib/deepl/requests/translate.rb +6 -3
  13. data/lib/deepl/requests/translation_memory/base.rb +47 -0
  14. data/lib/deepl/requests/translation_memory/create_export.rb +41 -0
  15. data/lib/deepl/requests/translation_memory/create_import.rb +53 -0
  16. data/lib/deepl/requests/translation_memory/destroy.rb +37 -0
  17. data/lib/deepl/requests/translation_memory/download_export.rb +35 -0
  18. data/lib/deepl/requests/translation_memory/find.rb +38 -0
  19. data/lib/deepl/requests/translation_memory/find_job.rb +38 -0
  20. data/lib/deepl/requests/translation_memory/list.rb +5 -18
  21. data/lib/deepl/requests/translation_memory/segments.rb +46 -0
  22. data/lib/deepl/requests/translation_memory/storage_base.rb +47 -0
  23. data/lib/deepl/requests/translation_memory/upload_file.rb +47 -0
  24. data/lib/deepl/resources/translation_memory.rb +4 -1
  25. data/lib/deepl/resources/translation_memory_export.rb +37 -0
  26. data/lib/deepl/resources/translation_memory_import.rb +29 -0
  27. data/lib/deepl/resources/translation_memory_job.rb +142 -0
  28. data/lib/deepl/resources/translation_memory_segments.rb +78 -0
  29. data/lib/deepl/translation_memory_api.rb +293 -1
  30. data/lib/deepl/utils/time_parser.rb +26 -0
  31. data/lib/deepl.rb +38 -4
  32. data/lib/version.rb +1 -1
  33. data/spec/api/deepl_spec.rb +134 -332
  34. data/spec/integration_tests/document_api_spec.rb +51 -18
  35. data/spec/integration_tests/document_error_paths_spec.rb +33 -0
  36. data/spec/integration_tests/glossary_api_spec.rb +114 -0
  37. data/spec/integration_tests/glossary_error_paths_spec.rb +107 -0
  38. data/spec/integration_tests/integration_test_utils.rb +8 -0
  39. data/spec/integration_tests/languages_api_spec.rb +54 -0
  40. data/spec/integration_tests/languages_error_paths_spec.rb +25 -0
  41. data/spec/integration_tests/rephrase_api_spec.rb +90 -0
  42. data/spec/integration_tests/rephrase_error_paths_spec.rb +53 -0
  43. data/spec/integration_tests/smoke_test_spec.rb +24 -0
  44. data/spec/integration_tests/style_rule_api_spec.rb +1 -19
  45. data/spec/integration_tests/style_rule_error_paths_spec.rb +45 -0
  46. data/spec/integration_tests/translate_api_spec.rb +117 -0
  47. data/spec/integration_tests/translate_error_paths_spec.rb +48 -0
  48. data/spec/integration_tests/translation_memory_api_spec.rb +191 -17
  49. data/spec/integration_tests/translation_memory_error_paths_spec.rb +87 -0
  50. data/spec/integration_tests/usage_api_spec.rb +29 -0
  51. data/spec/integration_tests/usage_error_paths_spec.rb +18 -0
  52. data/spec/requests/document/upload_spec.rb +130 -0
  53. data/spec/requests/glossary/create_spec.rb +0 -21
  54. data/spec/requests/glossary/destroy_spec.rb +0 -39
  55. data/spec/requests/glossary/entries_spec.rb +0 -35
  56. data/spec/requests/glossary/find_spec.rb +0 -40
  57. data/spec/requests/glossary/language_pairs_spec.rb +0 -13
  58. data/spec/requests/glossary/list_spec.rb +0 -27
  59. data/spec/requests/languages_spec.rb +0 -41
  60. data/spec/requests/rephrase_spec.rb +13 -139
  61. data/spec/requests/style_rule/create_custom_instruction_spec.rb +0 -24
  62. data/spec/requests/style_rule/create_spec.rb +0 -16
  63. data/spec/requests/style_rule/destroy_custom_instruction_spec.rb +0 -26
  64. data/spec/requests/style_rule/destroy_spec.rb +0 -27
  65. data/spec/requests/style_rule/find_custom_instruction_spec.rb +0 -27
  66. data/spec/requests/style_rule/find_spec.rb +0 -28
  67. data/spec/requests/style_rule/list_spec.rb +0 -31
  68. data/spec/requests/style_rule/update_configured_rules_spec.rb +0 -21
  69. data/spec/requests/style_rule/update_custom_instruction_spec.rb +0 -26
  70. data/spec/requests/style_rule/update_spec.rb +0 -19
  71. data/spec/requests/translate_spec.rb +63 -217
  72. data/spec/requests/translation_memory/create_export_spec.rb +37 -0
  73. data/spec/requests/translation_memory/create_import_spec.rb +61 -0
  74. data/spec/requests/translation_memory/destroy_spec.rb +36 -0
  75. data/spec/requests/translation_memory/download_export_spec.rb +54 -0
  76. data/spec/requests/translation_memory/find_job_spec.rb +37 -0
  77. data/spec/requests/translation_memory/find_spec.rb +36 -0
  78. data/spec/requests/translation_memory/list_spec.rb +0 -34
  79. data/spec/requests/translation_memory/segments_spec.rb +58 -0
  80. data/spec/requests/translation_memory/upload_file_spec.rb +55 -0
  81. data/spec/requests/usage_spec.rb +0 -16
  82. data/spec/resources/custom_instruction_spec.rb +32 -0
  83. data/spec/resources/style_rule_spec.rb +68 -0
  84. data/spec/resources/translation_memory_export_spec.rb +38 -0
  85. data/spec/resources/translation_memory_import_spec.rb +30 -0
  86. data/spec/resources/translation_memory_job_spec.rb +109 -0
  87. data/spec/resources/translation_memory_segments_spec.rb +78 -0
  88. data/spec/resources/translation_memory_spec.rb +18 -1
  89. data/spec/spec_helper.rb +15 -45
  90. data/spec/support/live_mock_server.rb +12 -0
  91. data/spec/support/managed_glossary.rb +65 -0
  92. data/spec/support/managed_style_rule.rb +17 -0
  93. data/spec/support/managed_translation_memory.rb +55 -0
  94. metadata +50 -17
  95. data/spec/fixtures/vcr_cassettes/deepl_document.yml +0 -95
  96. data/spec/fixtures/vcr_cassettes/deepl_document_download.yml +0 -1214
  97. data/spec/fixtures/vcr_cassettes/deepl_glossaries.yml +0 -1163
  98. data/spec/fixtures/vcr_cassettes/deepl_languages.yml +0 -54
  99. data/spec/fixtures/vcr_cassettes/deepl_rephrase.yml +0 -87
  100. data/spec/fixtures/vcr_cassettes/deepl_translate.yml +0 -358
  101. data/spec/fixtures/vcr_cassettes/deepl_usage.yml +0 -129
  102. data/spec/fixtures/vcr_cassettes/glossaries.yml +0 -1702
  103. data/spec/fixtures/vcr_cassettes/languages.yml +0 -229
  104. data/spec/fixtures/vcr_cassettes/rephrase_texts.yml +0 -401
  105. data/spec/fixtures/vcr_cassettes/style_rules.yml +0 -92
  106. data/spec/fixtures/vcr_cassettes/style_rules_crud.yml +0 -926
  107. data/spec/fixtures/vcr_cassettes/translate_texts.yml +0 -10630
  108. data/spec/fixtures/vcr_cassettes/translation_memories.yml +0 -74
  109. data/spec/fixtures/vcr_cassettes/usage.yml +0 -171
@@ -0,0 +1,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
@@ -24,20 +24,4 @@ describe DeepL::Requests::Usage do
24
24
  end
25
25
  end
26
26
  end
27
-
28
- describe '#request' do
29
- around do |example|
30
- VCR.use_cassette('usage') { example.call }
31
- end
32
-
33
- context 'when performing a valid request' do
34
- it 'returns an usage object' do
35
- usage = usage_req.request
36
-
37
- expect(usage).to be_a(DeepL::Resources::Usage)
38
- expect(usage.character_count).to be_a(Numeric)
39
- expect(usage.character_limit).to be_a(Numeric)
40
- end
41
- end
42
- end
43
27
  end
@@ -0,0 +1,32 @@
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::CustomInstruction do
9
+ subject(:custom_instruction) do
10
+ described_class.new({
11
+ 'id' => 'ci-1',
12
+ 'label' => 'Formal',
13
+ 'prompt' => 'Always use formal language',
14
+ 'source_language' => 'en'
15
+ })
16
+ end
17
+
18
+ describe '#initialize' do
19
+ context 'when building a basic object' do
20
+ it 'creates a resource' do
21
+ expect(custom_instruction).to be_a(described_class)
22
+ end
23
+
24
+ it 'assigns the attributes' do
25
+ expect(custom_instruction.id).to eq('ci-1')
26
+ expect(custom_instruction.label).to eq('Formal')
27
+ expect(custom_instruction.prompt).to eq('Always use formal language')
28
+ expect(custom_instruction.source_language).to eq('en')
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,68 @@
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::StyleRule do
9
+ subject(:style_rule) do
10
+ described_class.new({
11
+ 'style_id' => 'dca2e053-8ae5-45e6-a0d2-881156e7f4e4',
12
+ 'name' => 'Default Style Rule',
13
+ 'creation_time' => '2025-01-01T00:00:00Z',
14
+ 'updated_time' => '2025-01-02T00:00:00Z',
15
+ 'language' => 'en',
16
+ 'version' => 1
17
+ }, nil, nil)
18
+ end
19
+
20
+ describe '#initialize' do
21
+ context 'when building a basic object' do
22
+ it 'creates a resource' do
23
+ expect(style_rule).to be_a(described_class)
24
+ end
25
+
26
+ it 'assigns the attributes' do
27
+ expect(style_rule.style_id).to eq('dca2e053-8ae5-45e6-a0d2-881156e7f4e4')
28
+ expect(style_rule.name).to eq('Default Style Rule')
29
+ expect(style_rule.language).to eq('en')
30
+ expect(style_rule.version).to eq(1)
31
+ expect(style_rule.creation_time).to eq(Time.parse('2025-01-01T00:00:00Z'))
32
+ expect(style_rule.updated_time).to eq(Time.parse('2025-01-02T00:00:00Z'))
33
+ expect(style_rule.configured_rules).to be_nil
34
+ expect(style_rule.custom_instructions).to be_nil
35
+ end
36
+ end
37
+
38
+ context 'when building an object with configured rules and custom instructions' do
39
+ subject(:style_rule) do
40
+ described_class.new({
41
+ 'style_id' => 'dca2e053-8ae5-45e6-a0d2-881156e7f4e4',
42
+ 'name' => 'Detailed Style Rule',
43
+ 'language' => 'en',
44
+ 'version' => 1,
45
+ 'configured_rules' => {
46
+ 'dates_and_times' => { 'calendar_era' => 'use_bc_and_ad' }
47
+ },
48
+ 'custom_instructions' => [
49
+ { 'id' => 'ci-1', 'label' => 'Test', 'prompt' => 'Be formal' }
50
+ ]
51
+ }, nil, nil)
52
+ end
53
+
54
+ it 'maps configured_rules into a ConfiguredRules resource' do
55
+ expect(style_rule.configured_rules).to be_a(DeepL::Resources::ConfiguredRules)
56
+ expect(style_rule.configured_rules.dates_and_times)
57
+ .to eq({ 'calendar_era' => 'use_bc_and_ad' })
58
+ end
59
+
60
+ it 'maps custom_instructions into an array of CustomInstruction resources' do
61
+ expect(style_rule.custom_instructions).to all(be_a(DeepL::Resources::CustomInstruction))
62
+ expect(style_rule.custom_instructions.first.id).to eq('ci-1')
63
+ expect(style_rule.custom_instructions.first.label).to eq('Test')
64
+ expect(style_rule.custom_instructions.first.prompt).to eq('Be formal')
65
+ end
66
+ end
67
+ end
68
+ 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
data/spec/spec_helper.rb CHANGED
@@ -14,59 +14,29 @@ SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter
14
14
  require_relative '../lib/deepl'
15
15
  require_relative 'integration_tests/integration_test_utils'
16
16
 
17
+ # Auto-load test helpers under spec/support
18
+ Dir[File.expand_path('support/**/*.rb', __dir__)].sort.each { |f| require f }
19
+
17
20
  # Lib config
18
21
  ENV['DEEPL_AUTH_KEY'] ||= 'TEST-TOKEN'
19
22
 
20
- # General helpers
21
- def build_deepl_api
22
- DeepL::API.new(DeepL::Configuration.new)
23
- end
23
+ RSpec.configure do |config|
24
+ config.include ManagedGlossary
25
+ config.include ManagedStyleRule
26
+ config.include ManagedTranslationMemory
24
27
 
25
- uri_ignoring_deepl_api_subdomain = lambda do |request1, request2|
26
- deepl_api_regexp = %r{https?://api.*\.deepl\.com/}
27
- uri1 = request1.uri
28
- uri2 = request2.uri
29
- uri1_match = uri1.match(deepl_api_regexp)
30
- uri2_match = uri2.match(deepl_api_regexp)
31
- if uri1_match && uri2_match
32
- uri1_without_deepl_domain = uri1.gsub(uri1_match[0], '')
33
- uri2_without_deepl_domain = uri2.gsub(uri2_match[0], '')
34
- uri1_without_deepl_domain == uri2_without_deepl_domain
35
- else
36
- uri1 == uri2
28
+ config.before(:each, :mock_server_only) do
29
+ skip 'Only runs on mock server' unless ENV.key?('DEEPL_MOCK_SERVER_PORT')
37
30
  end
38
- end
39
-
40
- headers_ignoring_user_agent = lambda do |request1, request2|
41
- user_agent_key = 'User-Agent'
42
- # Pass by reference, so we need to use a copy of the headers
43
- headers1 = request1.headers.dup
44
- headers2 = request2.headers.dup
45
- headers1_has_user_agent = headers1.key?(user_agent_key)
46
- headers2_has_user_agent = headers2.key?(user_agent_key)
47
- return false if headers1_has_user_agent != headers2_has_user_agent
48
31
 
49
- headers1.delete(user_agent_key)
50
- headers2.delete(user_agent_key)
51
- headers1 == headers2
32
+ config.before(:each, :real_server_only) do
33
+ skip 'Only runs on real server' if ENV.key?('DEEPL_MOCK_SERVER_PORT')
34
+ end
52
35
  end
53
36
 
54
- # VCR tapes configuration
55
- require 'vcr'
56
- VCR.configure do |config|
57
- config.cassette_library_dir = 'spec/fixtures/vcr_cassettes'
58
- config.hook_into :webmock
59
- config.filter_sensitive_data('VALID_TOKEN') { ENV.fetch('DEEPL_AUTH_KEY', nil) }
60
-
61
- # to record new or missing VCR cassettes, call rspec like this:
62
- # $ VCR_RECORD_MODE=new_episodes bundle exec rspec
63
-
64
- record_mode = ENV['VCR_RECORD_MODE'] ? ENV['VCR_RECORD_MODE'].to_sym : :none
65
- config.default_cassette_options = {
66
- record: record_mode,
67
- match_requests_on: [:method, uri_ignoring_deepl_api_subdomain, :body,
68
- headers_ignoring_user_agent]
69
- }
37
+ # General helpers
38
+ def build_deepl_api
39
+ DeepL::API.new(DeepL::Configuration.new)
70
40
  end
71
41
 
72
42
  # Test helpers
@@ -0,0 +1,12 @@
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
+ # Shared setup for integration specs that hit the live mock server: expose an
7
+ # API the server rejects as unauthorized without mutating global DeepL state.
8
+ RSpec.shared_context 'with a live mock server' do
9
+ let(:unauthorized_api) do
10
+ DeepL::API.new(DeepL::Configuration.new(auth_key: 'invalid'))
11
+ end
12
+ end