deepl-rb 3.8.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.gitlab-ci.yml +1 -1
  3. data/CHANGELOG.md +23 -1
  4. data/README.md +213 -4
  5. data/VERSION +1 -1
  6. data/deepl-rb.gemspec +34 -6
  7. data/lib/deepl/document_api.rb +11 -1
  8. data/lib/deepl/requests/base.rb +55 -3
  9. data/lib/deepl/requests/document/upload.rb +50 -1
  10. data/lib/deepl/requests/translate.rb +4 -1
  11. data/lib/deepl/requests/translation_memory/base.rb +47 -0
  12. data/lib/deepl/requests/translation_memory/create_export.rb +41 -0
  13. data/lib/deepl/requests/translation_memory/create_import.rb +53 -0
  14. data/lib/deepl/requests/translation_memory/destroy.rb +37 -0
  15. data/lib/deepl/requests/translation_memory/download_export.rb +35 -0
  16. data/lib/deepl/requests/translation_memory/find.rb +38 -0
  17. data/lib/deepl/requests/translation_memory/find_job.rb +38 -0
  18. data/lib/deepl/requests/translation_memory/list.rb +5 -18
  19. data/lib/deepl/requests/translation_memory/segments.rb +46 -0
  20. data/lib/deepl/requests/translation_memory/storage_base.rb +47 -0
  21. data/lib/deepl/requests/translation_memory/upload_file.rb +47 -0
  22. data/lib/deepl/resources/translation_memory.rb +4 -1
  23. data/lib/deepl/resources/translation_memory_export.rb +37 -0
  24. data/lib/deepl/resources/translation_memory_import.rb +29 -0
  25. data/lib/deepl/resources/translation_memory_job.rb +142 -0
  26. data/lib/deepl/resources/translation_memory_segments.rb +78 -0
  27. data/lib/deepl/translation_memory_api.rb +293 -1
  28. data/lib/deepl/utils/time_parser.rb +26 -0
  29. data/lib/deepl.rb +31 -0
  30. data/lib/version.rb +1 -1
  31. data/spec/integration_tests/document_api_spec.rb +47 -0
  32. data/spec/integration_tests/integration_test_utils.rb +8 -0
  33. data/spec/integration_tests/translate_api_spec.rb +19 -0
  34. data/spec/integration_tests/translation_memory_api_spec.rb +192 -2
  35. data/spec/integration_tests/translation_memory_error_paths_spec.rb +68 -0
  36. data/spec/requests/document/upload_spec.rb +130 -0
  37. data/spec/requests/translate_spec.rb +55 -0
  38. data/spec/requests/translation_memory/create_export_spec.rb +37 -0
  39. data/spec/requests/translation_memory/create_import_spec.rb +61 -0
  40. data/spec/requests/translation_memory/destroy_spec.rb +36 -0
  41. data/spec/requests/translation_memory/download_export_spec.rb +54 -0
  42. data/spec/requests/translation_memory/find_job_spec.rb +37 -0
  43. data/spec/requests/translation_memory/find_spec.rb +36 -0
  44. data/spec/requests/translation_memory/segments_spec.rb +58 -0
  45. data/spec/requests/translation_memory/upload_file_spec.rb +55 -0
  46. data/spec/resources/translation_memory_export_spec.rb +38 -0
  47. data/spec/resources/translation_memory_import_spec.rb +30 -0
  48. data/spec/resources/translation_memory_job_spec.rb +109 -0
  49. data/spec/resources/translation_memory_segments_spec.rb +78 -0
  50. data/spec/resources/translation_memory_spec.rb +18 -1
  51. data/spec/support/managed_glossary.rb +48 -0
  52. data/spec/support/managed_translation_memory.rb +48 -0
  53. metadata +34 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e06f20b76b38eff20ef4f3f7c7863f374afe2e314438b0bc021df6c4535f991
4
- data.tar.gz: 30ac12887bd80d6e81cf5c815e5bb8bc5725ea50c66edfd7f4c34c1881e5ad5d
3
+ metadata.gz: 35f6c08b68a4b9aa8907b543ebbfbab38e9f9f35f7e2dc60decaa4bac57340f5
4
+ data.tar.gz: 975d1d68cd43fec41b93252b609bc67a619d08d4fd2e6d6bc9b822ca01fd78ac
5
5
  SHA512:
6
- metadata.gz: 0c5ded1350056aa468d7255ff93eb537dfcf141608a1761651530d5a1d01851bd242a1f205b14626ff8136ec103f38911731242f86e0127e00e75a6fc5c7da08
7
- data.tar.gz: 570ce96a8751e4de0116b93cac78d08453df5eaf65bef669f9151b361481ab85bea83f72e8d07ffaac01c7e010d8537fd003b0ef3f762d2cc3b2c29c601bfec0
6
+ metadata.gz: 749cd28143798f5e08ba848bc4eaa476254ef24656e690fcb3e5ca84314eb54f68ffc7f3675ae74a0cc24c01d194d9dfebcd2d819e0982fad44aba5869e37344
7
+ data.tar.gz: 4a3b7ca19832725c49b0708f544f9842de7733cc8777b41f66835e4dbead815994ca6dab8b8b3052d172417240dcf5fff5934fc3959ef226001e8ad4e0769735
data/.gitlab-ci.yml CHANGED
@@ -4,7 +4,7 @@ include:
4
4
  - project: '${CI_PROJECT_NAMESPACE}/ci-libs-for-client-libraries'
5
5
  file:
6
6
  - '/${CI_PROJECT_NAME}/.gitlab-ci.yml'
7
- - project: 'deepl/ops/ci-cd-infrastructure/gitlab-ci-lib'
7
+ - project: 'deepl-org/deepl/devex/gitlab-ci-lib'
8
8
  file:
9
9
  - '/templates/.buildkit.yml'
10
10
  - '/templates/.secret-detection.yml'
data/CHANGELOG.md CHANGED
@@ -5,6 +5,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
+
9
+ ## [3.9.0] - 2026-08-11
10
+ ### Added
11
+ - Added support for using multiple glossaries in text and document translation
12
+ via the `glossary_ids` option (up to 5 glossary IDs) in `translate` and
13
+ document upload.
14
+ - Added support for using a style rule in document translation via the
15
+ `style_id` option (the `style_rule` option) in document upload.
16
+ - Added support for using a translation memory in document translation via the
17
+ `translation_memory_id` option (the `translation_memory` and
18
+ `translation_memory_threshold` options) in document upload.
19
+ - Added support for translation memory management via `DeepL.translation_memories`:
20
+ `find()`, `segments()`, `destroy()`, `create_import()`, `upload_file()`,
21
+ `create_export()`, `find_job()`, `wait_until_job_done()`, `download_export()`,
22
+ and the convenience methods `import_from_filepath()` and `export_to_filepath()`.
23
+ Every method taking a translation memory accepts a string ID or a
24
+ `TranslationMemory` object. `wait_until_job_done()`, `import_from_filepath()`
25
+ and `export_to_filepath()` accept an optional `timeout_s` to stop waiting for
26
+ a job that does not finish. Please refer to the README for usage instructions.
27
+ - Added the `creation_time` and `updated_time` attributes to `TranslationMemory`.
28
+
8
29
  ### Changed
9
30
  - Reworked the test suite to run against the `deepl-mock` server with OpenAPI
10
31
  request and response validation enabled in CI, replacing recorded VCR
@@ -134,7 +155,8 @@ The change in major version is only due to the change in maintainership, there i
134
155
  ### Fixed
135
156
  - Make RequestEntityTooLarge error message more clear
136
157
 
137
- [Unreleased]: https://github.com/DeepLcom/deepl-rb/compare/v3.8.0...HEAD
158
+ [Unreleased]: https://github.com/DeepLcom/deepl-rb/compare/v3.9.0...HEAD
159
+ [3.9.0]: https://github.com/DeepLcom/deepl-rb/compare/v3.8.0...v3.9.0
138
160
  [3.8.0]: https://github.com/DeepLcom/deepl-rb/compare/v3.7.0...v3.8.0
139
161
  [3.7.0]: https://github.com/DeepLcom/deepl-rb/compare/v3.6.1...v3.7.0
140
162
  [3.6.1]: https://github.com/DeepLcom/deepl-rb/compare/v3.6.0...v3.6.1
data/README.md CHANGED
@@ -267,6 +267,25 @@ puts translation.text
267
267
  # => Ojalá tuviéramos un auto.
268
268
  ```
269
269
 
270
+ To use more than one glossary at once, specify the `glossary_ids` option with an
271
+ array of up to 5 glossary IDs (as strings or `DeepL::Resources::Glossary`
272
+ objects) instead of `glossary_id`. This works for both text and document
273
+ translation. `glossary_ids` requires `source_lang` to be set, cannot be combined
274
+ with `glossary_id`, and raises `ArgumentError` if these rules are violated or
275
+ more than 5 IDs are provided:
276
+
277
+ ```rb
278
+ # Text translation with multiple glossaries
279
+ translation = DeepL.translate 'Hello World', 'EN', 'ES',
280
+ glossary_ids: ['aa48c7f0-0d02-413e-8a06-d5bbf0ca7a6e',
281
+ 'bb59d8g1-1e13-524f-9b17-e6ccg1db8b7f']
282
+
283
+ # Document translation with multiple glossaries
284
+ handle = DeepL.document.upload 'my_document.docx', 'EN', 'ES', 'my_document.docx',
285
+ glossary_ids: ['aa48c7f0-0d02-413e-8a06-d5bbf0ca7a6e',
286
+ 'bb59d8g1-1e13-524f-9b17-e6ccg1db8b7f']
287
+ ```
288
+
270
289
  To list all the glossaries available, use the `glossaries.list` method:
271
290
 
272
291
  ```rb
@@ -457,6 +476,15 @@ style_rules = DeepL.style_rules.list
457
476
  translation = DeepL.translate 'Hello World', 'EN', 'ES', style_rule: style_rules.first
458
477
  ```
459
478
 
479
+ The same `style_rule` option can be passed to document translation via
480
+ `DeepL.document.upload` (or `DeepL.document.translate_document`), accepting either
481
+ a style rule ID string or a `StyleRule` object:
482
+
483
+ ```rb
484
+ handle = DeepL.document.upload 'my_document.docx', 'EN', 'ES', 'my_document.docx',
485
+ style_rule: 'dca2e053-8ae5-45e6-a0d2-881156e7f4e4'
486
+ ```
487
+
460
488
  ### Translation Memories
461
489
 
462
490
  Translation memories allow you to store and reuse previously created translations.
@@ -464,11 +492,11 @@ They can be used in text translation requests to improve consistency by matching
464
492
  against stored segments. Multiple translation memories can be stored with your
465
493
  account, each with a source language and one or more target languages.
466
494
 
467
- #### Uploading and managing translation memories
495
+ Translation memories can also be managed in the DeepL UI via
496
+ https://www.deepl.com/translation-memory.
468
497
 
469
- Currently translation memories must be uploaded and managed in the DeepL UI via
470
- https://www.deepl.com/translation-memory. Full translation memory management via
471
- the API will come shortly.
498
+ Every method that takes a translation memory accepts either a string containing
499
+ the translation memory ID or a `TranslationMemory` object.
472
500
 
473
501
  #### Listing translation memories
474
502
 
@@ -490,6 +518,176 @@ end
490
518
  translation_memories = DeepL.translation_memories.list(page: 0, page_size: 10)
491
519
  ```
492
520
 
521
+ #### Retrieving a single translation memory
522
+
523
+ `translation_memories.find` retrieves one translation memory by ID. In addition
524
+ to the fields returned by `list`, the resource carries the `creation_time` and
525
+ `updated_time` timestamps.
526
+
527
+ ```rb
528
+ tm = DeepL.translation_memories.find 'YOUR_TM_ID'
529
+
530
+ puts tm.class
531
+ # => DeepL::Resources::TranslationMemory
532
+ puts tm.name
533
+ # => 'Legal'
534
+ puts tm.segment_count
535
+ # => 12
536
+ puts tm.creation_time.class
537
+ # => Time
538
+ ```
539
+
540
+ #### Listing the segments of a translation memory
541
+
542
+ `translation_memories.segments` returns one page of the segments of a
543
+ translation memory as a `TranslationMemorySegments` object. Each segment holds
544
+ the source text and one target per target language of the translation memory.
545
+
546
+ Pagination is cursor-based: omit `page_cursor` on the first call, then pass the
547
+ `next_page_cursor` of the previous response until `next_page?` is `false`. The
548
+ method also accepts `page_size` (1-100, defaults to 50), `filter_text` (a
549
+ substring matched against the source and target texts, at least 2 characters)
550
+ and `filter_case_sensitive` (defaults to `false`).
551
+
552
+ Note that `segment_count` is the number of segments stored in the translation
553
+ memory; a text filter does not reduce it.
554
+
555
+ ```rb
556
+ page = DeepL.translation_memories.segments 'YOUR_TM_ID', page_size: 50
557
+
558
+ puts page.class
559
+ # => DeepL::Resources::TranslationMemorySegments
560
+ puts page.segment_count
561
+ # => 12
562
+ puts page.segments.first.source_text
563
+ # => 'Quelltext Nummer 0'
564
+ puts page.segments.first.targets.first.target_text
565
+ # => 'Source text number 0'
566
+
567
+ # Walk through every page of segments
568
+ while page.next_page?
569
+ page = DeepL.translation_memories.segments 'YOUR_TM_ID',
570
+ page_size: 50,
571
+ page_cursor: page.next_page_cursor
572
+ end
573
+
574
+ # Only the segments matching a text
575
+ page = DeepL.translation_memories.segments 'YOUR_TM_ID',
576
+ filter_text: 'Nummer 1',
577
+ filter_case_sensitive: true
578
+ ```
579
+
580
+ #### Importing a translation memory
581
+
582
+ `translation_memories.import_from_filepath` creates a new translation memory
583
+ from a TMX file. It creates the import job, uploads the file and waits for the
584
+ processing to finish, and returns the finished `TranslationMemoryJob`. Its
585
+ result carries the ID of the newly created translation memory.
586
+
587
+ ```rb
588
+ job = DeepL.translation_memories.import_from_filepath 'legal.tmx',
589
+ display_name: 'Legal',
590
+ timeout_s: 300
591
+
592
+ puts job.class
593
+ # => DeepL::Resources::TranslationMemoryJob
594
+ puts job.status
595
+ # => 'completed'
596
+ puts job.result.translation_memory_id
597
+ # => 'a74d88fb-ed2a-4943-a664-a4512398b994'
598
+ puts job.result.skipped_segment_count
599
+ # => 0
600
+ ```
601
+
602
+ The three steps can also be performed separately, for example to upload a file
603
+ that is not available on the local file system. The upload URL is a pre-signed
604
+ storage URL outside of the DeepL API, so no authorization header is sent with
605
+ the upload.
606
+
607
+ ```rb
608
+ content = File.binread 'legal.tmx'
609
+ created = DeepL.translation_memories.create_import 'legal.tmx', content.bytesize,
610
+ content_type: 'application/xml',
611
+ display_name: 'Legal'
612
+
613
+ puts created.upload_url
614
+ # => 'https://...'
615
+
616
+ DeepL.translation_memories.upload_file created, content
617
+ job = DeepL.translation_memories.wait_until_job_done created.job_id
618
+ ```
619
+
620
+ Until the file is uploaded the job stays in the `awaiting_input` status and
621
+ `result.required_action` describes what is missing. The API detects the upload
622
+ asynchronously, so the job keeps reporting `awaiting_input` for a while
623
+ afterwards, typically around half a minute, before it completes.
624
+ `wait_until_job_done` therefore polls through that status like any other
625
+ non-terminal one. A job whose file is never uploaded does not finish on its own,
626
+ so pass `timeout_s` when that is a possibility.
627
+
628
+ #### Exporting a translation memory
629
+
630
+ `translation_memories.export_to_filepath` writes a translation memory to a TMX
631
+ file. It creates the export job, waits for it to finish and downloads the
632
+ result, overwriting the output file if it already exists.
633
+
634
+ ```rb
635
+ job = DeepL.translation_memories.export_to_filepath 'YOUR_TM_ID', 'export.tmx'
636
+
637
+ puts job.status
638
+ # => 'completed'
639
+ ```
640
+
641
+ The steps can be performed separately as well. Repeating the export of an
642
+ unchanged translation memory reuses the previously completed job instead of
643
+ starting a new one, which `reused_existing?` reports. Just like the upload URL,
644
+ the download URL is a pre-signed storage URL and is requested without an
645
+ authorization header.
646
+
647
+ ```rb
648
+ created = DeepL.translation_memories.create_export 'YOUR_TM_ID'
649
+
650
+ puts created.reused_existing?
651
+ # => false
652
+
653
+ job = DeepL.translation_memories.wait_until_job_done created.job_id
654
+
655
+ puts job.result.download_url
656
+ # => 'https://...'
657
+
658
+ DeepL.translation_memories.download_export job, 'export.tmx'
659
+ ```
660
+
661
+ #### Tracking import and export jobs
662
+
663
+ `translation_memories.find_job` returns the current status of an import or
664
+ export job, and `translation_memories.wait_until_job_done` polls it every five
665
+ seconds until it finished, raising if the job failed or expired. Pass
666
+ `timeout_s` to give up after a number of seconds instead of waiting forever.
667
+
668
+ ```rb
669
+ job = DeepL.translation_memories.find_job 'YOUR_JOB_ID'
670
+
671
+ puts job.operation
672
+ # => 'import'
673
+ puts job.status
674
+ # => 'processing'
675
+ puts job.finished?
676
+ # => false
677
+ ```
678
+
679
+ The status is one of `awaiting_input`, `processing`, `completed`, `downloaded`,
680
+ `failed` or `expired`.
681
+
682
+ #### Deleting a translation memory
683
+
684
+ `translation_memories.destroy` deletes a translation memory and returns its ID.
685
+
686
+ ```rb
687
+ DeepL.translation_memories.destroy 'YOUR_TM_ID'
688
+ # => 'YOUR_TM_ID'
689
+ ```
690
+
493
691
  #### Using a translation memory in translations
494
692
 
495
693
  Pass the `translation_memory` parameter to `translate` to use a translation
@@ -510,6 +708,17 @@ translation = DeepL.translate 'Hello, world!', 'EN', 'DE',
510
708
  translation_memory: translation_memories.first
511
709
  ```
512
710
 
711
+ The same `translation_memory` and `translation_memory_threshold` options can be
712
+ passed to document translation via `DeepL.document.upload` (or
713
+ `DeepL.document.translate_document`). The `translation_memory` option accepts
714
+ either a translation memory ID string or a `TranslationMemory` object:
715
+
716
+ ```rb
717
+ handle = DeepL.document.upload 'my_document.docx', 'EN', 'DE', 'my_document.docx',
718
+ translation_memory: 'YOUR_TM_ID',
719
+ translation_memory_threshold: 80
720
+ ```
721
+
513
722
  ### Monitor usage
514
723
 
515
724
  To check current API usage, use:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.8.0
1
+ 3.9.0
data/deepl-rb.gemspec CHANGED
@@ -2,17 +2,17 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: deepl-rb 3.8.0 ruby lib
5
+ # stub: deepl-rb 3.9.0 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "deepl-rb".freeze
9
- s.version = "3.8.0".freeze
9
+ s.version = "3.9.0"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.metadata = { "bug_tracker_uri" => "https://github.com/DeepLcom/deepl-rb/issues", "changelog_uri" => "https://github.com/DeepLcom/deepl-rb/blob/main/CHANGELOG.md", "documentation_uri" => "https://github.com/DeepLcom/deepl-rb/blob/main/README.md", "homepage_uri" => "https://github.com/DeepLcom/deepl-rb" } if s.respond_to? :metadata=
13
13
  s.require_paths = ["lib".freeze]
14
14
  s.authors = ["DeepL SE".freeze]
15
- s.date = "2026-06-18"
15
+ s.date = "2026-08-12"
16
16
  s.description = "Official Ruby library for the DeepL language translation API (v2). For more information, check this: https://www.deepl.com/docs/api-reference.html".freeze
17
17
  s.email = "open-source@deepl.com".freeze
18
18
  s.extra_rdoc_files = [
@@ -82,7 +82,17 @@ Gem::Specification.new do |s|
82
82
  "lib/deepl/requests/style_rule/update_configured_rules.rb",
83
83
  "lib/deepl/requests/style_rule/update_custom_instruction.rb",
84
84
  "lib/deepl/requests/translate.rb",
85
+ "lib/deepl/requests/translation_memory/base.rb",
86
+ "lib/deepl/requests/translation_memory/create_export.rb",
87
+ "lib/deepl/requests/translation_memory/create_import.rb",
88
+ "lib/deepl/requests/translation_memory/destroy.rb",
89
+ "lib/deepl/requests/translation_memory/download_export.rb",
90
+ "lib/deepl/requests/translation_memory/find.rb",
91
+ "lib/deepl/requests/translation_memory/find_job.rb",
85
92
  "lib/deepl/requests/translation_memory/list.rb",
93
+ "lib/deepl/requests/translation_memory/segments.rb",
94
+ "lib/deepl/requests/translation_memory/storage_base.rb",
95
+ "lib/deepl/requests/translation_memory/upload_file.rb",
86
96
  "lib/deepl/requests/usage.rb",
87
97
  "lib/deepl/resources/base.rb",
88
98
  "lib/deepl/resources/document_handle.rb",
@@ -93,11 +103,16 @@ Gem::Specification.new do |s|
93
103
  "lib/deepl/resources/style_rule.rb",
94
104
  "lib/deepl/resources/text.rb",
95
105
  "lib/deepl/resources/translation_memory.rb",
106
+ "lib/deepl/resources/translation_memory_export.rb",
107
+ "lib/deepl/resources/translation_memory_import.rb",
108
+ "lib/deepl/resources/translation_memory_job.rb",
109
+ "lib/deepl/resources/translation_memory_segments.rb",
96
110
  "lib/deepl/resources/usage.rb",
97
111
  "lib/deepl/style_rule_api.rb",
98
112
  "lib/deepl/translation_memory_api.rb",
99
113
  "lib/deepl/utils/backoff_timer.rb",
100
114
  "lib/deepl/utils/exception_builder.rb",
115
+ "lib/deepl/utils/time_parser.rb",
101
116
  "lib/http_client_options.rb",
102
117
  "lib/version.rb",
103
118
  "license_checker.sh",
@@ -123,6 +138,7 @@ Gem::Specification.new do |s|
123
138
  "spec/integration_tests/translation_memory_error_paths_spec.rb",
124
139
  "spec/integration_tests/usage_api_spec.rb",
125
140
  "spec/integration_tests/usage_error_paths_spec.rb",
141
+ "spec/requests/document/upload_spec.rb",
126
142
  "spec/requests/extra_body_parameters_types_spec.rb",
127
143
  "spec/requests/glossary/create_spec.rb",
128
144
  "spec/requests/glossary/destroy_spec.rb",
@@ -143,7 +159,15 @@ Gem::Specification.new do |s|
143
159
  "spec/requests/style_rule/update_custom_instruction_spec.rb",
144
160
  "spec/requests/style_rule/update_spec.rb",
145
161
  "spec/requests/translate_spec.rb",
162
+ "spec/requests/translation_memory/create_export_spec.rb",
163
+ "spec/requests/translation_memory/create_import_spec.rb",
164
+ "spec/requests/translation_memory/destroy_spec.rb",
165
+ "spec/requests/translation_memory/download_export_spec.rb",
166
+ "spec/requests/translation_memory/find_job_spec.rb",
167
+ "spec/requests/translation_memory/find_spec.rb",
146
168
  "spec/requests/translation_memory/list_spec.rb",
169
+ "spec/requests/translation_memory/segments_spec.rb",
170
+ "spec/requests/translation_memory/upload_file_spec.rb",
147
171
  "spec/requests/usage_spec.rb",
148
172
  "spec/resources/custom_instruction_spec.rb",
149
173
  "spec/resources/glossary_spec.rb",
@@ -151,6 +175,10 @@ Gem::Specification.new do |s|
151
175
  "spec/resources/language_spec.rb",
152
176
  "spec/resources/style_rule_spec.rb",
153
177
  "spec/resources/text_spec.rb",
178
+ "spec/resources/translation_memory_export_spec.rb",
179
+ "spec/resources/translation_memory_import_spec.rb",
180
+ "spec/resources/translation_memory_job_spec.rb",
181
+ "spec/resources/translation_memory_segments_spec.rb",
154
182
  "spec/resources/translation_memory_spec.rb",
155
183
  "spec/resources/usage_spec.rb",
156
184
  "spec/spec_helper.rb",
@@ -161,12 +189,12 @@ Gem::Specification.new do |s|
161
189
  ]
162
190
  s.homepage = "https://github.com/DeepLcom/deepl-rb".freeze
163
191
  s.licenses = ["MIT".freeze]
164
- s.rubygems_version = "3.6.2".freeze
192
+ s.rubygems_version = "3.4.10".freeze
165
193
  s.summary = "Official Ruby library for the DeepL language translation API.".freeze
166
194
 
167
195
  s.specification_version = 4
168
196
 
169
- s.add_development_dependency(%q<juwelier>.freeze, [">= 0".freeze])
170
- s.add_development_dependency(%q<byebug>.freeze, [">= 0".freeze])
197
+ s.add_development_dependency(%q<juwelier>.freeze, [">= 0"])
198
+ s.add_development_dependency(%q<byebug>.freeze, [">= 0"])
171
199
  end
172
200
 
@@ -25,7 +25,17 @@ module DeepL
25
25
  # @param [String, nil] filename The filename of the file, including its extension. Used to open
26
26
  # the different kinds of documents (PDFs, etc). If nil, will use
27
27
  # the filename of +input_file_path+.
28
- # @param [Hash] options Additional (body) options for the upload.
28
+ # @param [Hash] options Additional (body) options for the upload. Notable glossary options:
29
+ # +:glossary_id+ (a single glossary ID) or +:glossary_ids+ (an array of
30
+ # up to 5 glossary IDs, as strings or `DeepL::Resources::Glossary`
31
+ # objects). +:glossary_ids+ requires +source_lang+ to be set, cannot be
32
+ # combined with +:glossary_id+, and raises `ArgumentError` if these rules
33
+ # are violated or more than 5 IDs are provided. Notable style/translation
34
+ # memory options (mirroring text translation): +:style_rule+ (a style rule
35
+ # ID string or a `DeepL::Resources::StyleRule` object),
36
+ # +:translation_memory+ (a translation memory ID string or a
37
+ # `DeepL::Resources::TranslationMemory` object), and
38
+ # +:translation_memory_threshold+ (integer 0-100, recommended minimum 75).
29
39
  # @param [Hash] additional_headers Additional HTTP headers for the upload.
30
40
  # @return [DeepL::Resources::DocumentHandle] Document handle for the uploaded document.
31
41
 
@@ -6,6 +6,9 @@
6
6
  module DeepL
7
7
  module Requests
8
8
  class Base # rubocop:disable Metrics/ClassLength
9
+ # Maximum number of glossaries that may be provided via the +glossary_ids+ option.
10
+ MAX_GLOSSARY_IDS = 5
11
+
9
12
  attr_reader :api, :response, :options
10
13
 
11
14
  def initialize(api, options = {}, additional_headers = {})
@@ -85,7 +88,7 @@ module DeepL
85
88
  api.configuration.logger&.info("Request to the DeepL API: #{self}")
86
89
  api.configuration.logger&.debug("Request details: #{details}")
87
90
  loop do
88
- resp = api.http_client.request(req)
91
+ resp = http_client.request(req)
89
92
  validate_response!(resp)
90
93
  return [req, resp]
91
94
  rescue DeepL::Exceptions::Error => e
@@ -95,7 +98,7 @@ module DeepL
95
98
  api.configuration.logger&.info("Encountered a retryable exception: #{e.message}")
96
99
  end
97
100
  api.configuration.logger&.info("Starting retry #{@backoff_timer.num_retries + 1} for " \
98
- "request #{request} after sleeping for " \
101
+ "request #{self} after sleeping for " \
99
102
  "#{format('%.2f', @backoff_timer.time_until_deadline)}")
100
103
  files_to_reset.each(&:rewind)
101
104
  @backoff_timer.sleep_until_deadline
@@ -106,7 +109,7 @@ module DeepL
106
109
  api.configuration.logger&.info("Encountered a retryable exception: #{e.message}")
107
110
  end
108
111
  api.configuration.logger&.info("Starting retry #{@backoff_timer.num_retries + 1} for " \
109
- "request #{request} after sleeping for " \
112
+ "request #{self} after sleeping for " \
110
113
  "#{format('%.2f', @backoff_timer.time_until_deadline)}")
111
114
  files_to_reset.each(&:rewind)
112
115
  @backoff_timer.sleep_until_deadline
@@ -172,6 +175,49 @@ module DeepL
172
175
  raise Utils::ExceptionBuilder.new(response).build
173
176
  end
174
177
 
178
+ # Validates the +glossary_ids+ option and resolves it to an array of glossary ID strings.
179
+ #
180
+ # The +glossary_ids+ option accepts an array of up to `MAX_GLOSSARY_IDS` glossary
181
+ # identifiers. Each entry may be a glossary ID string or a
182
+ # `DeepL::Resources::Glossary` object. It:
183
+ # * requires +source_lang+ to be set,
184
+ # * cannot be combined with the singular +glossary_id+ option,
185
+ # * allows at most `MAX_GLOSSARY_IDS` identifiers.
186
+ #
187
+ # The API expects +glossary_ids+ as an array (a JSON array in the JSON translate body).
188
+ # Callers sending multipart/form-data (e.g. document upload) should join the returned
189
+ # array into a comma-separated string.
190
+ #
191
+ # @param [String, nil] source_lang The source language for the request.
192
+ # @return [Array<String>, nil] The list of glossary IDs, or nil if the +glossary_ids+
193
+ # option was not provided.
194
+ # @raise [ArgumentError] If any of the validation rules above are violated.
195
+ def build_glossary_ids_param(source_lang)
196
+ return nil unless option?(:glossary_ids)
197
+
198
+ ids = Array(delete_option(:glossary_ids))
199
+ return nil if ids.empty?
200
+
201
+ validate_glossary_ids!(ids, source_lang)
202
+ ids.map do |glossary|
203
+ glossary.is_a?(DeepL::Resources::Glossary) ? glossary.id : glossary.to_s
204
+ end
205
+ end
206
+
207
+ def validate_glossary_ids!(ids, source_lang)
208
+ if option?(:glossary_id)
209
+ raise ArgumentError,
210
+ 'The `glossary_ids` option cannot be used together with the `glossary_id` option'
211
+ end
212
+ if source_lang.nil?
213
+ raise ArgumentError, 'The `glossary_ids` option requires `source_lang` to be set'
214
+ end
215
+ return unless ids.size > MAX_GLOSSARY_IDS
216
+
217
+ raise ArgumentError,
218
+ "A maximum of #{MAX_GLOSSARY_IDS} glossary IDs may be provided via `glossary_ids`"
219
+ end
220
+
175
221
  def path
176
222
  raise NotImplementedError
177
223
  end
@@ -189,6 +235,12 @@ module DeepL
189
235
  api.configuration.host
190
236
  end
191
237
 
238
+ # Subclasses that talk to a host other than the DeepL API, such as a pre-signed storage
239
+ # URL, override this to request through their own connection.
240
+ def http_client
241
+ api.http_client
242
+ end
243
+
192
244
  def query_params
193
245
  options
194
246
  end
@@ -49,10 +49,59 @@ module DeepL
49
49
  ]
50
50
  filename_param = filename || File.basename(input_file_path)
51
51
  form_data.push(['filename', filename_param]) unless filename_param.nil?
52
- add_supported_options_to_form(form_data)
52
+ add_extra_options_to_form(form_data)
53
53
  form_data
54
54
  end
55
55
 
56
+ def add_extra_options_to_form(form_data)
57
+ add_supported_options_to_form(form_data)
58
+ add_glossary_ids_to_form(form_data)
59
+ add_style_rule_to_form(form_data)
60
+ add_translation_memory_to_form(form_data)
61
+ add_translation_memory_threshold_to_form(form_data)
62
+ end
63
+
64
+ # Validates and serializes the `glossary_ids` option (see
65
+ # `DeepL::Requests::Base#build_glossary_ids_param`) and appends it to the form data as a
66
+ # comma-separated `glossary_ids` field. Requires `source_lang`, cannot be combined with
67
+ # the singular `glossary_id` option, and allows at most 5 IDs.
68
+ def add_glossary_ids_to_form(form_data)
69
+ glossary_ids = build_glossary_ids_param(source_lang)
70
+ form_data.push(['glossary_ids', glossary_ids.join(',')]) unless glossary_ids.nil?
71
+ end
72
+
73
+ # Serializes the `style_rule` option and appends it to the form data as a `style_id`
74
+ # field. Mirrors text translation: accepts either a style rule ID string or a
75
+ # `DeepL::Resources::StyleRule` object.
76
+ def add_style_rule_to_form(form_data)
77
+ return unless option?(:style_rule)
78
+
79
+ rule = delete_option(:style_rule)
80
+ style_id = rule.is_a?(DeepL::Resources::StyleRule) ? rule.style_id : rule
81
+ form_data.push(['style_id', style_id.to_s])
82
+ end
83
+
84
+ # Serializes the `translation_memory` option and appends it to the form data as a
85
+ # `translation_memory_id` field. Mirrors text translation: the `translation_memory`
86
+ # option accepts either a translation memory ID string or a
87
+ # `DeepL::Resources::TranslationMemory` object.
88
+ def add_translation_memory_to_form(form_data)
89
+ return unless option?(:translation_memory)
90
+
91
+ tm = delete_option(:translation_memory)
92
+ tm_id = tm.is_a?(DeepL::Resources::TranslationMemory) ? tm.translation_memory_id : tm
93
+ form_data.push(['translation_memory_id', tm_id.to_s])
94
+ end
95
+
96
+ # Serializes the `translation_memory_threshold` option (integer 0-100) and appends it to
97
+ # the form data as a `translation_memory_threshold` field, mirroring text translation.
98
+ def add_translation_memory_threshold_to_form(form_data)
99
+ return unless option?(:translation_memory_threshold)
100
+
101
+ threshold = delete_option(:translation_memory_threshold)
102
+ form_data.push(['translation_memory_threshold', threshold.to_s])
103
+ end
104
+
56
105
  def add_supported_options_to_form(form_data)
57
106
  SUPPORTED_OPTIONS.each do |option_name|
58
107
  option_value = option(option_name)
@@ -39,7 +39,7 @@ module DeepL
39
39
  tweak_parameters!
40
40
  end
41
41
 
42
- def request # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
42
+ def request # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
43
43
  text_arrayified = text.is_a?(Array) ? text : [text]
44
44
  payload = { text: text_arrayified, source_lang: source_lang, target_lang: target_lang }
45
45
 
@@ -65,6 +65,9 @@ module DeepL
65
65
  payload[:translation_memory_threshold] = delete_option(:translation_memory_threshold)
66
66
  end
67
67
 
68
+ glossary_ids = build_glossary_ids_param(source_lang)
69
+ payload[:glossary_ids] = glossary_ids unless glossary_ids.nil?
70
+
68
71
  build_texts(*execute_request_with_retries(post_request(payload)))
69
72
  end
70
73
 
@@ -0,0 +1,47 @@
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 Requests
8
+ module TranslationMemory
9
+ ##
10
+ # Common behaviour of the translation memory endpoints: they are served under the `v3`
11
+ # prefix instead of the configured API version, and take their parameters in the query
12
+ # string rather than in the request body.
13
+
14
+ class Base < DeepL::Requests::Base
15
+ private
16
+
17
+ def uri
18
+ @uri ||= begin
19
+ base_uri = URI("#{host}/v3/#{path}")
20
+ query_string = build_query_string
21
+ base_uri.query = query_string unless query_string.empty?
22
+ base_uri
23
+ end
24
+ end
25
+
26
+ def build_query_string
27
+ # encode_www_form encodes a space as `+`, which is correct for a form body but not for
28
+ # a URI query string. A literal `+` is already escaped as %2B, so every remaining `+`
29
+ # is a space.
30
+ URI.encode_www_form(query_params).gsub('+', '%20')
31
+ end
32
+
33
+ def query_params
34
+ {}
35
+ end
36
+
37
+ def get_request # rubocop:disable Naming/AccessorMethodName
38
+ Net::HTTP::Get.new(uri.request_uri, add_json_content_type(headers))
39
+ end
40
+
41
+ def delete_request
42
+ Net::HTTP::Delete.new(uri.request_uri, add_json_content_type(headers))
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end