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,142 @@
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
+ module DeepL
7
+ module Resources
8
+ ##
9
+ # The outcome of a translation memory import or export job.
10
+
11
+ class TranslationMemoryJobResult
12
+ STATUS_AWAITING_INPUT = 'awaiting_input'
13
+ STATUS_PROCESSING = 'processing'
14
+ STATUS_COMPLETED = 'completed'
15
+ STATUS_DOWNLOADED = 'downloaded'
16
+ STATUS_FAILED = 'failed'
17
+ STATUS_EXPIRED = 'expired'
18
+
19
+ attr_reader :status, :required_action, :download_url, :expires_at, :error_message,
20
+ :translation_memory_id, :skipped_segment_count
21
+
22
+ def initialize(result)
23
+ status_metadata = result['status_metadata'] || {}
24
+ error = result['error'] || {}
25
+
26
+ @status = result['status']
27
+ @required_action = status_metadata['required_action']
28
+ @download_url = result['download_url']
29
+ @expires_at = Utils::TimeParser.parse_optional_time(result['expires_at'])
30
+ @error_message = error['message']
31
+ @translation_memory_id = result['translation_memory_id']
32
+ @skipped_segment_count = result['skipped_segment_count']
33
+ end
34
+
35
+ ##
36
+ # Checks if the job terminated. Note that this could be due to an error as well, but means
37
+ # no further waiting is necessary.
38
+ #
39
+ # @return [true] if so
40
+
41
+ def finished?
42
+ [STATUS_COMPLETED, STATUS_DOWNLOADED, STATUS_FAILED, STATUS_EXPIRED].include?(status)
43
+ end
44
+
45
+ ##
46
+ # Checks if the job is still waiting for the TMX file to be uploaded. Note that the API
47
+ # detects an upload asynchronously, so a job keeps reporting this status for a while after
48
+ # its file has been uploaded.
49
+ #
50
+ # @return [true] if so
51
+
52
+ def awaiting_input?
53
+ status == STATUS_AWAITING_INPUT
54
+ end
55
+
56
+ ##
57
+ # Checks if there was an error during the job, including it having expired.
58
+ #
59
+ # @return [true] if so
60
+
61
+ def error?
62
+ [STATUS_FAILED, STATUS_EXPIRED].include?(status)
63
+ end
64
+
65
+ def to_s
66
+ "TranslationMemoryJobResult: Status: #{status} - Error message: #{error_message}"
67
+ end
68
+ end
69
+
70
+ ##
71
+ # Status of a translation memory import or export job. The API returns exactly one result.
72
+
73
+ class TranslationMemoryJob < Base
74
+ OPERATION_IMPORT = 'import'
75
+ OPERATION_EXPORT = 'export'
76
+
77
+ attr_reader :job_id, :product, :operation, :creation_time, :updated_time, :results,
78
+ :translation_memory_id, :display_name, :source_content_type,
79
+ :source_content_length
80
+
81
+ def initialize(job, *args)
82
+ super(*args)
83
+ extract_basic_fields(job)
84
+ extract_parameters(job)
85
+ @results = (job['results'] || []).map { |result| TranslationMemoryJobResult.new(result) }
86
+ end
87
+
88
+ ##
89
+ # The single result of the job, or nil if the API returned none.
90
+ #
91
+ # @return [DeepL::Resources::TranslationMemoryJobResult, nil]
92
+
93
+ def result
94
+ results.first
95
+ end
96
+
97
+ def status
98
+ result&.status
99
+ end
100
+
101
+ def finished?
102
+ result ? result.finished? : false
103
+ end
104
+
105
+ def awaiting_input?
106
+ result ? result.awaiting_input? : false
107
+ end
108
+
109
+ def error?
110
+ result ? result.error? : false
111
+ end
112
+
113
+ def error_message
114
+ result&.error_message
115
+ end
116
+
117
+ def to_s
118
+ "TranslationMemoryJob: #{operation} - ID: #{job_id} - Status: #{status}"
119
+ end
120
+
121
+ private
122
+
123
+ def extract_basic_fields(job)
124
+ @job_id = job['job_id']
125
+ @product = job['product']
126
+ @operation = job['operation']
127
+ @creation_time = Utils::TimeParser.parse_optional_time(job['creation_time'])
128
+ @updated_time = Utils::TimeParser.parse_optional_time(job['updated_time'])
129
+ end
130
+
131
+ def extract_parameters(job)
132
+ parameters = job['parameters'] || {}
133
+ source_file = job['source_file'] || {}
134
+
135
+ @translation_memory_id = parameters['translation_memory_id']
136
+ @display_name = parameters['display_name']
137
+ @source_content_type = source_file['content_type']
138
+ @source_content_length = source_file['content_length']
139
+ end
140
+ end
141
+ end
142
+ 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
+ module DeepL
7
+ module Resources
8
+ class TranslationMemoryTargetSegment
9
+ attr_reader :target_segment_id, :target_language, :target_text, :creation_time,
10
+ :updated_time, :last_used_time
11
+
12
+ def initialize(target_segment)
13
+ @target_segment_id = target_segment['target_segment_id']
14
+ @target_language = target_segment['target_language']
15
+ @target_text = target_segment['target_text']
16
+ @creation_time = Utils::TimeParser.parse_optional_time(target_segment['creation_time'])
17
+ @updated_time = Utils::TimeParser.parse_optional_time(target_segment['updated_time'])
18
+ @last_used_time = Utils::TimeParser.parse_optional_time(target_segment['last_used_time'])
19
+ end
20
+
21
+ def to_s
22
+ "#{target_language}: #{target_text}"
23
+ end
24
+ end
25
+
26
+ class TranslationMemorySegment
27
+ attr_reader :source_segment_id, :source_text, :targets, :creation_time, :updated_time,
28
+ :last_used_time
29
+
30
+ def initialize(segment)
31
+ @source_segment_id = segment['source_segment_id']
32
+ @source_text = segment['source_text']
33
+ @targets = (segment['targets'] || []).map do |target|
34
+ TranslationMemoryTargetSegment.new(target)
35
+ end
36
+ @creation_time = Utils::TimeParser.parse_optional_time(segment['creation_time'])
37
+ @updated_time = Utils::TimeParser.parse_optional_time(segment['updated_time'])
38
+ @last_used_time = Utils::TimeParser.parse_optional_time(segment['last_used_time'])
39
+ end
40
+
41
+ def to_s
42
+ "#{source_segment_id} - #{source_text}"
43
+ end
44
+ end
45
+
46
+ ##
47
+ # One page of the segments of a translation memory. Pagination is cursor-based: pass
48
+ # `next_page_cursor` as the `page_cursor` option of the next request until it is nil.
49
+
50
+ class TranslationMemorySegments < Base
51
+ attr_reader :segments, :segment_count, :next_page_cursor
52
+
53
+ def initialize(segments_response, *args)
54
+ super(*args)
55
+ @segments = (segments_response['segments'] || []).map do |segment|
56
+ TranslationMemorySegment.new(segment)
57
+ end
58
+ # Note that this is the number of segments stored in the translation memory, it is not
59
+ # reduced by a text filter.
60
+ @segment_count = segments_response['segment_count'] || 0
61
+ @next_page_cursor = segments_response['next_page_cursor']
62
+ end
63
+
64
+ ##
65
+ # Checks whether another page of segments can be requested.
66
+ #
67
+ # @return [true] if so
68
+
69
+ def next_page?
70
+ !next_page_cursor.nil?
71
+ end
72
+
73
+ def to_s
74
+ "#{segments.size} of #{segment_count} segment(s)"
75
+ end
76
+ end
77
+ end
78
+ end
@@ -4,14 +4,306 @@
4
4
  # frozen_string_literal: true
5
5
 
6
6
  module DeepL
7
- class TranslationMemoryApi
7
+ class TranslationMemoryApi # rubocop:disable Metrics/ClassLength
8
+ # Time to wait between two status queries of an import or export job.
9
+ JOB_POLLING_INTERVAL_SECONDS = 5
10
+
8
11
  def initialize(api, options = {})
9
12
  @api = api
10
13
  @options = options
11
14
  end
12
15
 
16
+ ##
17
+ # Lists the translation memories of the account.
18
+ #
19
+ # @param [Hash] options Additional options for the request. Supports `page` (page number for
20
+ # pagination, 0-indexed) and `page_size` (number of items per page).
21
+ # @return [Array<DeepL::Resources::TranslationMemory>] The translation memories of the page.
22
+
13
23
  def list(options = {})
14
24
  DeepL::Requests::TranslationMemory::List.new(@api, options).request
15
25
  end
26
+
27
+ ##
28
+ # Retrieves a single translation memory.
29
+ #
30
+ # @param [String, DeepL::Resources::TranslationMemory] translation_memory Translation memory
31
+ # ID or object.
32
+ # @param [Hash] options Additional options for the request.
33
+ # @return [DeepL::Resources::TranslationMemory] The requested translation memory.
34
+
35
+ def find(translation_memory, options = {})
36
+ DeepL::Requests::TranslationMemory::Find.new(
37
+ @api, extract_translation_memory_id(translation_memory), options
38
+ ).request
39
+ end
40
+
41
+ ##
42
+ # Retrieves one page of the segments of a translation memory. Pagination is cursor-based:
43
+ # omit `page_cursor` on the first call, then pass the `next_page_cursor` of the previous
44
+ # response until it is nil.
45
+ #
46
+ # @param [String, DeepL::Resources::TranslationMemory] translation_memory Translation memory
47
+ # ID or object.
48
+ # @param [Hash] options Additional options for the request. Supports `page_size` (maximum
49
+ # number of segments per page, 1-100, defaults to 50), `page_cursor`
50
+ # (cursor of a previous response), `filter_text` (substring filter
51
+ # across source and target text, at least 2 characters) and
52
+ # `filter_case_sensitive` (whether the filter is case-sensitive,
53
+ # defaults to false).
54
+ # @return [DeepL::Resources::TranslationMemorySegments] The requested page of segments.
55
+
56
+ def segments(translation_memory, options = {})
57
+ DeepL::Requests::TranslationMemory::Segments.new(
58
+ @api, extract_translation_memory_id(translation_memory), options
59
+ ).request
60
+ end
61
+
62
+ ##
63
+ # Deletes a translation memory.
64
+ #
65
+ # @param [String, DeepL::Resources::TranslationMemory] translation_memory Translation memory
66
+ # ID or object.
67
+ # @param [Hash] options Additional options for the request.
68
+ # @return [String] The ID of the deleted translation memory.
69
+
70
+ def destroy(translation_memory, options = {})
71
+ DeepL::Requests::TranslationMemory::Destroy.new(
72
+ @api, extract_translation_memory_id(translation_memory), options
73
+ ).request
74
+ end
75
+
76
+ ##
77
+ # Creates an import job for a new translation memory. The job only declares the file, upload
78
+ # the TMX file itself to the returned upload URL with `upload_file`, then poll `find_job` for
79
+ # the outcome. Use `import_from_filepath` to do all three steps at once.
80
+ #
81
+ # @param [String] file_name Name of the TMX file to import, for example "legal.tmx".
82
+ # @param [Integer] content_length Size of the TMX file in bytes.
83
+ # @param [String, nil] content_type MIME type of the file, defaults to "application/xml".
84
+ # @param [String, nil] display_name Name of the resulting translation memory, defaults to the
85
+ # file name.
86
+ # @param [Hash] additional_headers Additional HTTP headers for the request.
87
+ # @return [DeepL::Resources::TranslationMemoryImport] The job ID and the upload URL.
88
+
89
+ def create_import(file_name, content_length, content_type: nil, display_name: nil,
90
+ additional_headers: {})
91
+ DeepL::Requests::TranslationMemory::CreateImport.new(
92
+ @api, file_name, content_length,
93
+ { content_type: content_type, display_name: display_name }.compact, additional_headers
94
+ ).request
95
+ end
96
+
97
+ ##
98
+ # Uploads a TMX file to the upload URL of an import job, which starts the processing. The
99
+ # upload URL is a pre-signed storage URL outside of the DeepL API, so no authorization header
100
+ # is sent with this request.
101
+ #
102
+ # @param [String, DeepL::Resources::TranslationMemoryImport] translation_memory_import Import
103
+ # returned by `create_import`, or its upload URL.
104
+ # @param [String] file_content Content of the TMX file.
105
+ # @param [String] content_type MIME type of the file. Must match the `content_type` declared
106
+ # when the import job was created.
107
+ # @return [nil]
108
+
109
+ def upload_file(translation_memory_import, file_content,
110
+ content_type: Requests::TranslationMemory::UploadFile::DEFAULT_CONTENT_TYPE)
111
+ DeepL::Requests::TranslationMemory::UploadFile.new(
112
+ @api, extract_upload_url(translation_memory_import), file_content, content_type
113
+ ).request
114
+ end
115
+
116
+ ##
117
+ # Creates an export job for a translation memory. Poll `find_job` for the download URL of the
118
+ # exported TMX file. Use `export_to_filepath` to do both steps and write the file at once.
119
+ #
120
+ # @param [String, DeepL::Resources::TranslationMemory] translation_memory Translation memory
121
+ # ID or object.
122
+ # @param [Hash] options Additional options for the request.
123
+ # @return [DeepL::Resources::TranslationMemoryExport] The job ID, and whether the API reused a
124
+ # previously completed export.
125
+
126
+ def create_export(translation_memory, options = {})
127
+ DeepL::Requests::TranslationMemory::CreateExport.new(
128
+ @api, extract_translation_memory_id(translation_memory), options
129
+ ).request
130
+ end
131
+
132
+ ##
133
+ # Retrieves the status of a translation memory import or export job.
134
+ #
135
+ # @param [String, DeepL::Resources::TranslationMemoryJob] job Job ID or object.
136
+ # @param [Hash] options Additional options for the request.
137
+ # @return [DeepL::Resources::TranslationMemoryJob] The current status of the job.
138
+
139
+ def find_job(job, options = {})
140
+ DeepL::Requests::TranslationMemory::FindJob.new(@api, extract_job_id(job), options).request
141
+ end
142
+
143
+ ##
144
+ # Polls a translation memory import or export job until it is finished, `sleep`ing between
145
+ # the status queries, and returns the final status.
146
+ #
147
+ # Note that an import job keeps reporting `awaiting_input` for a while after its file has
148
+ # been uploaded, because the API detects the upload asynchronously. That status is therefore
149
+ # polled through like any other non-terminal one. A job whose file is never uploaded does not
150
+ # finish on its own, so pass `timeout_s` when that is a possibility.
151
+ #
152
+ # @raise [DeepL::Exceptions::Error] If the job failed or expired, or if `timeout_s` elapsed
153
+ # before the job finished.
154
+ #
155
+ # @param [String, DeepL::Resources::TranslationMemoryJob] job Job ID or object.
156
+ # @param [Hash] options Additional options for the status queries.
157
+ # @param [Numeric, nil] timeout_s Maximum time in seconds to wait for the job to finish. Note
158
+ # that this is not accurate to the second, the status is only
159
+ # queried every five seconds.
160
+ # @return [DeepL::Resources::TranslationMemoryJob] The finished job.
161
+
162
+ def wait_until_job_done(job, options = {}, timeout_s: nil)
163
+ job_status = find_job(job, options)
164
+ started_at = monotonic_time
165
+ until job_status.finished?
166
+ raise_timeout_error(timeout_s) if timeout_exceeded?(started_at, timeout_s)
167
+
168
+ log_job_polling
169
+ sleep(JOB_POLLING_INTERVAL_SECONDS)
170
+ job_status = find_job(job, options)
171
+ end
172
+ raise_job_error(job_status) if job_status.error?
173
+
174
+ job_status
175
+ end
176
+
177
+ ##
178
+ # Downloads the TMX file of a completed export job. The download URL is a pre-signed storage
179
+ # URL outside of the DeepL API, so no authorization header is sent with this request.
180
+ #
181
+ # @raise [DeepL::Exceptions::Error] If the job carries no download URL, for example because
182
+ # it has not completed yet.
183
+ #
184
+ # @param [DeepL::Resources::TranslationMemoryJob, String] job Completed export job carrying
185
+ # the download URL, or the download URL itself.
186
+ # @param [String] output_path Path to the file to write to. Will be overwritten if the file
187
+ # already exists.
188
+
189
+ def download_export(job, output_path)
190
+ DeepL::Requests::TranslationMemory::DownloadExport.new(@api, extract_download_url(job),
191
+ output_path).request
192
+ end
193
+
194
+ ##
195
+ # Imports a TMX file as a new translation memory: creates the import job, uploads the file
196
+ # and waits for the processing to finish.
197
+ #
198
+ # @raise [DeepL::Exceptions::Error] If the import fails.
199
+ #
200
+ # @param [String] input_file_path Path to the TMX file to import.
201
+ # @param [String, nil] display_name Name of the resulting translation memory, defaults to the
202
+ # file name.
203
+ # @param [Numeric, nil] timeout_s Maximum time in seconds to wait for the import to finish.
204
+ # Note that the API keeps reporting `awaiting_input` for a
205
+ # while after the upload, so allow for a generous timeout.
206
+ # @return [DeepL::Resources::TranslationMemoryJob] The finished import job, its result carries
207
+ # the ID of the new translation memory.
208
+
209
+ def import_from_filepath(input_file_path, display_name: nil, timeout_s: nil)
210
+ unless File.exist?(input_file_path)
211
+ raise Exceptions::Error, "No file found at #{input_file_path}"
212
+ end
213
+
214
+ file_content = File.binread(input_file_path)
215
+ created = create_import(File.basename(input_file_path), file_content.bytesize,
216
+ display_name: display_name)
217
+ upload_file(created, file_content)
218
+ wait_until_job_done(created.job_id, timeout_s: timeout_s)
219
+ end
220
+
221
+ ##
222
+ # Exports a translation memory to a TMX file: creates the export job, waits for it to finish
223
+ # and writes the result to +output_path+.
224
+ #
225
+ # @raise [DeepL::Exceptions::Error] If the export fails.
226
+ #
227
+ # @param [String, DeepL::Resources::TranslationMemory] translation_memory Translation memory
228
+ # ID or object.
229
+ # @param [String] output_path Path to the file to write to. Will be overwritten if the file
230
+ # already exists.
231
+ # @param [Numeric, nil] timeout_s Maximum time in seconds to wait for the export to finish.
232
+ # @return [DeepL::Resources::TranslationMemoryJob] The finished export job.
233
+
234
+ def export_to_filepath(translation_memory, output_path, timeout_s: nil)
235
+ created = create_export(translation_memory)
236
+ job = wait_until_job_done(created.job_id, timeout_s: timeout_s)
237
+ download_export(job, output_path)
238
+ job
239
+ end
240
+
241
+ private
242
+
243
+ def extract_translation_memory_id(translation_memory)
244
+ id = if translation_memory.is_a?(Resources::TranslationMemory)
245
+ translation_memory.translation_memory_id
246
+ else
247
+ translation_memory
248
+ end
249
+ raise Exceptions::Error, 'Translation memory ID must not be empty' if blank?(id)
250
+
251
+ id
252
+ end
253
+
254
+ def extract_job_id(job)
255
+ id = job.is_a?(Resources::TranslationMemoryJob) ? job.job_id : job
256
+ raise Exceptions::Error, 'Job ID must not be empty' if blank?(id)
257
+
258
+ id
259
+ end
260
+
261
+ def extract_upload_url(translation_memory_import)
262
+ if translation_memory_import.is_a?(Resources::TranslationMemoryImport)
263
+ translation_memory_import.upload_url
264
+ else
265
+ translation_memory_import
266
+ end
267
+ end
268
+
269
+ def extract_download_url(job)
270
+ return job unless job.is_a?(Resources::TranslationMemoryJob)
271
+
272
+ download_url = job.result&.download_url
273
+ if blank?(download_url)
274
+ raise Exceptions::Error, 'Translation memory export job has no download URL, ' \
275
+ 'it may not have completed yet'
276
+ end
277
+
278
+ download_url
279
+ end
280
+
281
+ def blank?(value)
282
+ value.nil? || value.empty?
283
+ end
284
+
285
+ def log_job_polling
286
+ @api.configuration.logger&.info('Rechecking translation memory job status after sleeping ' \
287
+ "for #{JOB_POLLING_INTERVAL_SECONDS} seconds.")
288
+ end
289
+
290
+ def monotonic_time
291
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
292
+ end
293
+
294
+ def timeout_exceeded?(started_at, timeout_s)
295
+ !timeout_s.nil? && (monotonic_time - started_at) > timeout_s
296
+ end
297
+
298
+ def raise_timeout_error(timeout_s)
299
+ raise Exceptions::Error,
300
+ "Manual timeout of #{timeout_s}s exceeded for the translation memory job"
301
+ end
302
+
303
+ def raise_job_error(job_status)
304
+ raise Exceptions::Error,
305
+ "Error occurred during the translation memory #{job_status.operation}: " \
306
+ "#{job_status.error_message || job_status.status}"
307
+ end
16
308
  end
17
309
  end
@@ -0,0 +1,26 @@
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 'time'
7
+
8
+ module DeepL
9
+ module Utils
10
+ module TimeParser
11
+ extend self
12
+
13
+ ##
14
+ # Parses an optional timestamp returned by the API.
15
+ #
16
+ # @param [String, nil] time_string Timestamp in ISO 8601 format, or nil.
17
+ # @return [Time, nil] The parsed time, or nil if no timestamp was given.
18
+
19
+ def parse_optional_time(time_string)
20
+ return nil if time_string.nil? || time_string.empty?
21
+
22
+ Time.parse(time_string)
23
+ end
24
+ end
25
+ end
26
+ end
data/lib/deepl.rb CHANGED
@@ -41,7 +41,17 @@ require_relative 'deepl/requests/style_rule/create_custom_instruction'
41
41
  require_relative 'deepl/requests/style_rule/find_custom_instruction'
42
42
  require_relative 'deepl/requests/style_rule/update_custom_instruction'
43
43
  require_relative 'deepl/requests/style_rule/destroy_custom_instruction'
44
+ require_relative 'deepl/requests/translation_memory/base'
45
+ require_relative 'deepl/requests/translation_memory/storage_base'
44
46
  require_relative 'deepl/requests/translation_memory/list'
47
+ require_relative 'deepl/requests/translation_memory/find'
48
+ require_relative 'deepl/requests/translation_memory/segments'
49
+ require_relative 'deepl/requests/translation_memory/destroy'
50
+ require_relative 'deepl/requests/translation_memory/create_import'
51
+ require_relative 'deepl/requests/translation_memory/upload_file'
52
+ require_relative 'deepl/requests/translation_memory/create_export'
53
+ require_relative 'deepl/requests/translation_memory/find_job'
54
+ require_relative 'deepl/requests/translation_memory/download_export'
45
55
  require_relative 'deepl/requests/languages'
46
56
  require_relative 'deepl/requests/translate'
47
57
  require_relative 'deepl/requests/usage'
@@ -54,6 +64,10 @@ require_relative 'deepl/resources/document_translation_status'
54
64
  require_relative 'deepl/resources/glossary'
55
65
  require_relative 'deepl/resources/style_rule'
56
66
  require_relative 'deepl/resources/translation_memory'
67
+ require_relative 'deepl/resources/translation_memory_segments'
68
+ require_relative 'deepl/resources/translation_memory_import'
69
+ require_relative 'deepl/resources/translation_memory_export'
70
+ require_relative 'deepl/resources/translation_memory_job'
57
71
  require_relative 'deepl/resources/language'
58
72
  require_relative 'deepl/resources/language_pair'
59
73
  require_relative 'deepl/resources/text'
@@ -62,6 +76,7 @@ require_relative 'deepl/resources/usage'
62
76
  # -- Utils
63
77
  require_relative 'deepl/utils/exception_builder'
64
78
  require_relative 'deepl/utils/backoff_timer'
79
+ require_relative 'deepl/utils/time_parser'
65
80
 
66
81
  # -- Constants
67
82
  require_relative 'deepl/constants/base_constant'
@@ -100,9 +115,26 @@ module DeepL
100
115
  Requests::Languages.new(api, options).request
101
116
  end
102
117
 
103
- def translate(text, source_lang, target_lang, options = {})
118
+ ##
119
+ # Translates +text+ from +source_lang+ into +target_lang+.
120
+ #
121
+ # @param [String, Array<String>] text Text(s) to translate.
122
+ # @param [String, nil] source_lang Source language. `nil` enables automatic detection.
123
+ # @param [String] target_lang Target language.
124
+ # @param [Hash] options Additional (body) options for the translation. Notable options:
125
+ # * +:glossary_id+ - A single glossary ID (string) to use for the translation. Requires
126
+ # +source_lang+ to be set. Cannot be combined with +:glossary_ids+.
127
+ # * +:glossary_ids+ - An array of up to 5 glossary IDs (strings or
128
+ # `DeepL::Resources::Glossary` objects) to use for the translation. Glossaries are applied
129
+ # in order (first match wins). Requires +source_lang+ to be set. Cannot be combined with
130
+ # +:glossary_id+. Raises `ArgumentError` if these rules are violated or more than 5 IDs are
131
+ # provided.
132
+ # @param [Hash] additional_headers Additional HTTP headers for the translation.
133
+ # @return [DeepL::Resources::Text, Array<DeepL::Resources::Text>] Translated text resource(s).
134
+ def translate(text, source_lang, target_lang, options = {}, additional_headers = {})
104
135
  configure if @configuration.nil?
105
- Requests::Translate.new(api, text, source_lang, target_lang, options).request
136
+ Requests::Translate.new(api, text, source_lang, target_lang, options,
137
+ additional_headers).request
106
138
  end
107
139
 
108
140
  def document(options = {})
@@ -125,9 +157,11 @@ module DeepL
125
157
  TranslationMemoryApi.new(api, options)
126
158
  end
127
159
 
128
- def rephrase(text, target_lang = nil, writing_style = nil, tone = nil, options = {}) # rubocop:disable Metrics/ParameterLists
160
+ def rephrase(text, target_lang = nil, writing_style = nil, tone = nil, options = {}, # rubocop:disable Metrics/ParameterLists
161
+ additional_headers = {})
129
162
  configure if @configuration.nil?
130
- Requests::Rephrase.new(api, text, target_lang, writing_style, tone, options).request
163
+ Requests::Rephrase.new(api, text, target_lang, writing_style, tone, options,
164
+ additional_headers).request
131
165
  end
132
166
 
133
167
  def usage(options = {})
data/lib/version.rb CHANGED
@@ -4,5 +4,5 @@
4
4
  # frozen_string_literal: true
5
5
 
6
6
  module DeepL
7
- VERSION = '3.7.0'
7
+ VERSION = '3.9.0'
8
8
  end