deepl_diff 1.1.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e41201f9e6bf36b1fa7b9d34b58d3aa6822550d293c5ee4dd2a20a0edd75a887
4
- data.tar.gz: 7d7dece832fa86e2d1e1f8b9c2c7083bd0f3a17cbd7101671cd44854cd6ac6d4
3
+ metadata.gz: 2c71f2ab36a0f279252d4a42bdd0fc915b51668c87ab05279474598487d19e74
4
+ data.tar.gz: bf24b7de677ac350e1fbe174409442626ff45101a57a04909494e46f36c0a5d5
5
5
  SHA512:
6
- metadata.gz: 68defc006c6803311d571a96b0f45f5eed120c55ecf0ad854fd4303c8a780f447b190c20bd48cc3484fd44ddecf18ae1b4b22691b4a14cc054a6154d410523eb
7
- data.tar.gz: a455a6e767ae6c6afe390db2720fbbe9adebe096f3bbf6f0fe735ad1a54408e54ee4cbbed5640bcdad3dcf92705fed4187c8ddb4cabed834c3d0434294093cce
6
+ metadata.gz: 14e9475cad58ca2114b1849a442b2f01cbb958a2413bd86603c29fddc07cb5d815e8bbb342608b77dc353b1a7899f15b6d447a36a30cd0ef9c591aa726c6dd3d
7
+ data.tar.gz: 3fb543ab7c6cd3111bcd0ddf4843cc1b8d8ec6e4f0f35b18c8c85fda2be590641e2bd2ff3679a3150ca8dd3f36bcd5db47536a1bc1675ec5c6fcc6f7ac84010a
data/.rubocop.yml CHANGED
@@ -24,5 +24,6 @@ Gemspec/DevelopmentDependencies:
24
24
  Metrics/ClassLength:
25
25
  Exclude:
26
26
  - lib/deepl_diff/tokenizer.rb
27
+ - lib/deepl_diff/request.rb
27
28
  # Test classes are mostly tables of cases.
28
29
  - test/**/*
data/README.md CHANGED
@@ -17,6 +17,16 @@ If your user changes a single word within the long description, you will be char
17
17
 
18
18
  Much better approach is to try to translate every repeated structural element (sentence) in your texts array just once to save money. This gem helps to make it done.
19
19
 
20
+ ## Dependencies
21
+
22
+ This gem loads two: [`ox`](https://github.com/ohler55/ox) to walk the HTML, and
23
+ [`punkt-segmenter`](https://github.com/lfcipriani/punkt-segmenter) to split text
24
+ into sentences.
25
+
26
+ Everything else is duck typed and supplied by you: `DeepLDiff.api` is anything
27
+ answering to `#translate`, and both `RedisCacheStore` and `RedisRateLimiter`
28
+ take anything answering to `#with`. Bring your own client, pool and store.
29
+
20
30
  ## Installation
21
31
 
22
32
  Add this line to your application's Gemfile:
@@ -38,11 +48,14 @@ Or install it yourself as:
38
48
  ```ruby
39
49
  require "deepl_diff"
40
50
 
41
- # This dependencies are not included, as you might need to roll your own cache based on different store
51
+ # None of these are dependencies of this gem. It loads only `ox` and
52
+ # `punkt-segmenter`; the API client, the connection pool, and whatever backs
53
+ # the cache and the rate limiter are yours to choose and to require.
54
+ require "deepl"
42
55
  require "redis"
43
56
  require "connection_pool"
44
57
  require "redis-namespace"
45
- require "ratelimit" # Optional, if you will use
58
+ require "ratelimit" # Optional, only if you use the rate limiter
46
59
 
47
60
  # Setup https://github.com/wikiti/deepl-rb
48
61
  DeepL.configure do |config|
@@ -108,10 +121,10 @@ DeepL API has a limitation: query can not be longer than approximately 128 KB. I
108
121
 
109
122
  ## Development
110
123
 
111
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
124
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
112
125
 
113
126
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
114
127
 
115
128
  ## Contributing
116
129
 
117
- Bug reports and pull requests are welcome on GitHub at https://github.com/wikiti/deepl-rb.
130
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Halvanhelv/deepl_diff.
data/deepl_diff.gemspec CHANGED
@@ -45,12 +45,9 @@ between revisions of long texts.
45
45
  spec.add_development_dependency "rubocop", "~> 1.90"
46
46
  spec.add_development_dependency "simplecov", "~> 1.2"
47
47
 
48
- spec.add_dependency "connection_pool", ">= 2.4", "< 4.0"
49
- spec.add_dependency "deepl-rb", "~> 3.9"
50
- spec.add_dependency "dry-initializer", "~> 3.2"
48
+ # The only two gems this one loads. Everything else -- the DeepL client, the
49
+ # connection pool, and whatever backs the cache and the rate limiter -- is
50
+ # supplied by the application and duck typed, so it stays out of the gemspec.
51
51
  spec.add_dependency "ox", "~> 2.14"
52
52
  spec.add_dependency "punkt-segmenter", "~> 0.9"
53
- spec.add_dependency "ratelimit", "~> 1.1"
54
- spec.add_dependency "redis", ">= 5.0", "< 7.0"
55
- spec.add_dependency "redis-namespace", "~> 1.11"
56
53
  end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class DeepLDiff::Cache
4
- extend Dry::Initializer
5
-
6
- param :from
7
- param :to
4
+ def initialize(from, to)
5
+ @from = from
6
+ @to = to
7
+ end
8
8
 
9
9
  def cached_and_missing(values)
10
10
  keys = values.map { |v| key(v) }
@@ -22,6 +22,8 @@ class DeepLDiff::Cache
22
22
 
23
23
  private
24
24
 
25
+ attr_reader :from, :to
26
+
25
27
  def store_value(value, translation)
26
28
  cache_store.write(key(value), translation)
27
29
  translation
@@ -1,18 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class DeepLDiff::Chunker
4
- extend ::Dry::Initializer
5
-
6
4
  class Error < StandardError; end
7
5
 
8
- Chunk = Struct.new(:texts, :bytesize)
6
+ Chunk = Struct.new(:texts, :escaped_size)
9
7
 
10
8
  MAX_CHUNK_SIZE = 1700
11
9
  COUNT_LIMIT = 300
12
10
 
13
- param :values
14
- option :limit, default: proc { MAX_CHUNK_SIZE }
15
- option :count_limit, default: proc { COUNT_LIMIT }
11
+ def initialize(values, limit: MAX_CHUNK_SIZE, count_limit: COUNT_LIMIT)
12
+ @values = values
13
+ @limit = limit
14
+ @count_limit = count_limit
15
+ end
16
16
 
17
17
  def call
18
18
  chunks.map(&:texts)
@@ -35,22 +35,29 @@ class DeepLDiff::Chunker
35
35
 
36
36
  private
37
37
 
38
+ attr_reader :values, :limit, :count_limit
39
+
38
40
  def next_chunk?(tail, value)
39
41
  tail.nil? ||
40
- (size(value) + tail.bytesize > limit) ||
41
- tail.texts.size > count_limit
42
+ (escaped_size(value) + tail.escaped_size > limit) ||
43
+ tail.texts.size >= count_limit
42
44
  end
43
45
 
44
- def size(text)
46
+ # What the limit is about is the size of the request that goes over the
47
+ # wire, so every measurement here is of the escaped form. Mixing it with
48
+ # String#size lets a chunk of non-ASCII text run several times over.
49
+ def escaped_size(text)
45
50
  CGI.escape(text).size
46
51
  end
47
52
 
48
53
  def update_chunk(chunk, value)
49
54
  chunk.texts << value
50
- chunk.bytesize += value.size
55
+ chunk.escaped_size += escaped_size(value)
51
56
  end
52
57
 
53
58
  def validate_value_size(value)
54
- raise Error, "Too long part #{value.size} > #{limit}" if value.size > limit
59
+ size = escaped_size(value)
60
+
61
+ raise Error, "Too long part #{size} > #{limit}" if size > limit
55
62
  end
56
63
  end
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class DeepLDiff::RedisCacheStore
4
- extend Dry::Initializer
4
+ ONE_WEEK = 60 * 60 * 24 * 7
5
5
 
6
- param :connection_pool
7
-
8
- option :timeout, default: proc { 60 * 60 * 24 * 7 }
9
- option :namespace, default: proc { DeepLDiff::CACHE_NAMESPACE }
6
+ # `connection_pool` is anything answering to #with, and what it yields is
7
+ # anything Redis::Namespace accepts. Neither gem is a dependency of this one.
8
+ def initialize(connection_pool, timeout: ONE_WEEK, namespace: DeepLDiff::CACHE_NAMESPACE)
9
+ @connection_pool = connection_pool
10
+ @timeout = timeout
11
+ @namespace = namespace
12
+ end
10
13
 
11
14
  def read_multi(keys)
12
15
  redis { |redis| redis.mget(*keys) }
@@ -18,6 +21,8 @@ class DeepLDiff::RedisCacheStore
18
21
 
19
22
  private
20
23
 
24
+ attr_reader :connection_pool, :timeout, :namespace
25
+
21
26
  def redis
22
27
  connection_pool.with do |redis|
23
28
  yield Redis::Namespace.new(namespace, redis: redis)
@@ -1,15 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class DeepLDiff::RedisRateLimiter
4
- extend Dry::Initializer
5
-
6
4
  class RateLimitExceeded < StandardError; end
7
5
 
8
- param :connection_pool
9
- param :threshold, default: proc { 8000 }
10
- param :interval, default: proc { 60 }
6
+ DEFAULT_THRESHOLD = 8000
7
+ DEFAULT_INTERVAL = 60
11
8
 
12
- option :namespace, default: proc { DeepLDiff::CACHE_NAMESPACE }
9
+ # `connection_pool` is anything answering to #with, and what it yields is
10
+ # anything Ratelimit accepts. Neither gem is a dependency of this one.
11
+ def initialize(connection_pool,
12
+ threshold: DEFAULT_THRESHOLD,
13
+ interval: DEFAULT_INTERVAL,
14
+ namespace: DeepLDiff::CACHE_NAMESPACE)
15
+ @connection_pool = connection_pool
16
+ @threshold = threshold
17
+ @interval = interval
18
+ @namespace = namespace
19
+ end
13
20
 
14
21
  def check(size)
15
22
  connection_pool.with do |redis|
@@ -19,4 +26,8 @@ class DeepLDiff::RedisRateLimiter
19
26
  rate_limit.add size
20
27
  end
21
28
  end
29
+
30
+ private
31
+
32
+ attr_reader :connection_pool, :threshold, :interval, :namespace
22
33
  end
@@ -1,25 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class DeepLDiff::Request
4
- extend Dry::Initializer
5
4
  extend Forwardable
6
5
 
7
- param :values
8
- param :options
6
+ class Error < StandardError; end
9
7
 
10
8
  def_delegators :DeepLDiff, :api, :cache_store, :rate_limiter
11
9
  def_delegators :"DeepLDiff::Linearizer", :linearize, :restore
12
10
 
11
+ def initialize(values, options)
12
+ @values = values
13
+ # #from and #to consume their keys so the rest can go to the API as-is.
14
+ # Copy first: the caller's hash is theirs, and it is often frozen.
15
+ @options = options.dup
16
+ end
17
+
13
18
  def call
14
19
  validate_globals
15
20
 
16
- return values if from == to || values.empty?
21
+ return values if same_language? || nothing_to_translate?
17
22
 
18
23
  translation
19
24
  end
20
25
 
21
26
  private
22
27
 
28
+ attr_reader :values, :options
29
+
23
30
  def from
24
31
  @from ||= options.delete(:from) || detect_language
25
32
  end
@@ -28,6 +35,19 @@ class DeepLDiff::Request
28
35
  @to ||= options.delete(:to) { nil }
29
36
  end
30
37
 
38
+ # A detected language arrives as a String while :to is usually a Symbol, so
39
+ # the two have to be compared on equal footing or the short circuit never
40
+ # fires and the text gets translated into its own language.
41
+ def same_language?
42
+ !to.nil? && from.to_s.casecmp?(to.to_s)
43
+ end
44
+
45
+ # Covers values holding no translatable text at all: "", nil, an empty
46
+ # collection, or a scalar the tokenizer has nothing to say about.
47
+ def nothing_to_translate?
48
+ text_tokens_texts.all?(&:empty?)
49
+ end
50
+
31
51
  def detect_language
32
52
  api.translate(text_tokens_texts.join(" ")[0..100], nil, to)
33
53
  .detected_source_language.downcase
@@ -123,7 +143,12 @@ class DeepLDiff::Request
123
143
  # Restores texts from tokens
124
144
  # [..., "<b>Horoshiy</b> Malchik", ...]
125
145
  def texts_translated
126
- @texts_translated ||= tokens_translated.map do |group|
146
+ @texts_translated ||= tokens_translated.map.with_index do |group, index|
147
+ source = texts[index]
148
+ # Only strings are rebuilt from tokens. Anything else has no tokens to
149
+ # rebuild from; nil keeps collapsing to "" the way it always has.
150
+ next source unless source.nil? || source.is_a?(String)
151
+
127
152
  group.map { |value, type| type == :text ? value : fix_ascii(value) }.join
128
153
  end
129
154
  end
@@ -135,7 +160,13 @@ class DeepLDiff::Request
135
160
 
136
161
  def call_api(values)
137
162
  check_rate_limit(values)
138
- [api.translate(values, from, to, options)].flatten.map(&:text)
163
+ translations = [api.translate(values, from, to, options)].flatten.map(&:text)
164
+ return translations if translations.size == values.size
165
+
166
+ # Letting a short response through means shifting nils into the results,
167
+ # which surfaces much later as a NoMethodError far from the cause.
168
+ raise Error,
169
+ "API returned #{translations.size} translations for #{values.size} values"
139
170
  end
140
171
 
141
172
  def cache
@@ -149,7 +149,8 @@ class DeepLDiff::Tokenizer < Ox::Sax
149
149
 
150
150
  class << self
151
151
  def tokenize(value)
152
- return [] if value.nil?
152
+ # Anything that is not a string has no markup and no sentences in it.
153
+ return [] unless value.is_a?(String)
153
154
 
154
155
  tokenizer = new(value).tap do |h|
155
156
  Ox.sax_parse(h, StringIO.new(value), HTML_OPTIONS)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeepLDiff
4
- VERSION = "1.1.1"
4
+ VERSION = "2.1.0"
5
5
  end
data/lib/deepl_diff.rb CHANGED
@@ -7,8 +7,6 @@ require "stringio"
7
7
 
8
8
  require "ox"
9
9
  require "punkt-segmenter"
10
- require "dry/initializer"
11
- require "deepl"
12
10
 
13
11
  require "deepl_diff/version"
14
12
  require "deepl_diff/tokenizer"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deepl_diff
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Islam Gagiev
@@ -65,54 +65,6 @@ dependencies:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
67
  version: '1.2'
68
- - !ruby/object:Gem::Dependency
69
- name: connection_pool
70
- requirement: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - ">="
73
- - !ruby/object:Gem::Version
74
- version: '2.4'
75
- - - "<"
76
- - !ruby/object:Gem::Version
77
- version: '4.0'
78
- type: :runtime
79
- prerelease: false
80
- version_requirements: !ruby/object:Gem::Requirement
81
- requirements:
82
- - - ">="
83
- - !ruby/object:Gem::Version
84
- version: '2.4'
85
- - - "<"
86
- - !ruby/object:Gem::Version
87
- version: '4.0'
88
- - !ruby/object:Gem::Dependency
89
- name: deepl-rb
90
- requirement: !ruby/object:Gem::Requirement
91
- requirements:
92
- - - "~>"
93
- - !ruby/object:Gem::Version
94
- version: '3.9'
95
- type: :runtime
96
- prerelease: false
97
- version_requirements: !ruby/object:Gem::Requirement
98
- requirements:
99
- - - "~>"
100
- - !ruby/object:Gem::Version
101
- version: '3.9'
102
- - !ruby/object:Gem::Dependency
103
- name: dry-initializer
104
- requirement: !ruby/object:Gem::Requirement
105
- requirements:
106
- - - "~>"
107
- - !ruby/object:Gem::Version
108
- version: '3.2'
109
- type: :runtime
110
- prerelease: false
111
- version_requirements: !ruby/object:Gem::Requirement
112
- requirements:
113
- - - "~>"
114
- - !ruby/object:Gem::Version
115
- version: '3.2'
116
68
  - !ruby/object:Gem::Dependency
117
69
  name: ox
118
70
  requirement: !ruby/object:Gem::Requirement
@@ -141,54 +93,6 @@ dependencies:
141
93
  - - "~>"
142
94
  - !ruby/object:Gem::Version
143
95
  version: '0.9'
144
- - !ruby/object:Gem::Dependency
145
- name: ratelimit
146
- requirement: !ruby/object:Gem::Requirement
147
- requirements:
148
- - - "~>"
149
- - !ruby/object:Gem::Version
150
- version: '1.1'
151
- type: :runtime
152
- prerelease: false
153
- version_requirements: !ruby/object:Gem::Requirement
154
- requirements:
155
- - - "~>"
156
- - !ruby/object:Gem::Version
157
- version: '1.1'
158
- - !ruby/object:Gem::Dependency
159
- name: redis
160
- requirement: !ruby/object:Gem::Requirement
161
- requirements:
162
- - - ">="
163
- - !ruby/object:Gem::Version
164
- version: '5.0'
165
- - - "<"
166
- - !ruby/object:Gem::Version
167
- version: '7.0'
168
- type: :runtime
169
- prerelease: false
170
- version_requirements: !ruby/object:Gem::Requirement
171
- requirements:
172
- - - ">="
173
- - !ruby/object:Gem::Version
174
- version: '5.0'
175
- - - "<"
176
- - !ruby/object:Gem::Version
177
- version: '7.0'
178
- - !ruby/object:Gem::Dependency
179
- name: redis-namespace
180
- requirement: !ruby/object:Gem::Requirement
181
- requirements:
182
- - - "~>"
183
- - !ruby/object:Gem::Version
184
- version: '1.11'
185
- type: :runtime
186
- prerelease: false
187
- version_requirements: !ruby/object:Gem::Requirement
188
- requirements:
189
- - - "~>"
190
- - !ruby/object:Gem::Version
191
- version: '1.11'
192
96
  description: "\nDeepL API wrapper for Ruby which helps to translate only changes\nbetween
193
97
  revisions of long texts.\n "
194
98
  email: