translation_diff 1.0.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 (85) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +84 -0
  3. data/.github/workflows/release.yml +28 -0
  4. data/.gitignore +11 -0
  5. data/.rubocop.yml +39 -0
  6. data/.ruby-version +1 -0
  7. data/CHANGELOG.md +723 -0
  8. data/Gemfile +61 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +158 -0
  11. data/Rakefile +41 -0
  12. data/data/languages/azure.json +285 -0
  13. data/data/languages/deepl.json +220 -0
  14. data/data/languages/google.json +399 -0
  15. data/data/languages/modernmt.json +413 -0
  16. data/docs/caching.md +185 -0
  17. data/docs/configuration.md +205 -0
  18. data/docs/contracts.md +187 -0
  19. data/docs/development.md +34 -0
  20. data/docs/errors.md +92 -0
  21. data/docs/how-it-works.md +145 -0
  22. data/docs/instrumentation.md +96 -0
  23. data/docs/languages.md +94 -0
  24. data/docs/providers.md +379 -0
  25. data/docs/sql-cache.md +366 -0
  26. data/lib/generators/translation_diff/install_generator.rb +15 -0
  27. data/lib/generators/translation_diff/templates/create_translation_diff_tables.rb.erb +24 -0
  28. data/lib/translation_diff/active_record/support.rb +34 -0
  29. data/lib/translation_diff/active_record.rb +3 -0
  30. data/lib/translation_diff/batch.rb +98 -0
  31. data/lib/translation_diff/call_preparation.rb +47 -0
  32. data/lib/translation_diff/capabilities.rb +9 -0
  33. data/lib/translation_diff/configuration/cache_guard_options.rb +39 -0
  34. data/lib/translation_diff/configuration/cache_ttl_option.rb +30 -0
  35. data/lib/translation_diff/configuration/option_table.rb +38 -0
  36. data/lib/translation_diff/configuration/provider_option_owners.rb +40 -0
  37. data/lib/translation_diff/configuration.rb +135 -0
  38. data/lib/translation_diff/context.rb +23 -0
  39. data/lib/translation_diff/dispatcher.rb +57 -0
  40. data/lib/translation_diff/document.rb +23 -0
  41. data/lib/translation_diff/errors.rb +44 -0
  42. data/lib/translation_diff/fragment.rb +33 -0
  43. data/lib/translation_diff/http_provider.rb +128 -0
  44. data/lib/translation_diff/instrumentation.rb +25 -0
  45. data/lib/translation_diff/languages/refresh.rb +70 -0
  46. data/lib/translation_diff/languages/set.rb +53 -0
  47. data/lib/translation_diff/languages.rb +30 -0
  48. data/lib/translation_diff/leaves.rb +22 -0
  49. data/lib/translation_diff/markup.rb +85 -0
  50. data/lib/translation_diff/passage.rb +149 -0
  51. data/lib/translation_diff/preview.rb +3 -0
  52. data/lib/translation_diff/previewer.rb +78 -0
  53. data/lib/translation_diff/provider.rb +91 -0
  54. data/lib/translation_diff/providers/amazon.rb +126 -0
  55. data/lib/translation_diff/providers/azure.rb +78 -0
  56. data/lib/translation_diff/providers/deepl.rb +88 -0
  57. data/lib/translation_diff/providers/google.rb +64 -0
  58. data/lib/translation_diff/providers/libretranslate.rb +65 -0
  59. data/lib/translation_diff/providers/modernmt.rb +74 -0
  60. data/lib/translation_diff/providers/null.rb +20 -0
  61. data/lib/translation_diff/providers.rb +69 -0
  62. data/lib/translation_diff/railtie.rb +12 -0
  63. data/lib/translation_diff/rate_limiters/active_record.rb +92 -0
  64. data/lib/translation_diff/rate_limiters/redis.rb +59 -0
  65. data/lib/translation_diff/rate_limiters.rb +9 -0
  66. data/lib/translation_diff/redaction.rb +45 -0
  67. data/lib/translation_diff/registry.rb +31 -0
  68. data/lib/translation_diff/segment.rb +32 -0
  69. data/lib/translation_diff/segmenters/pragmatic.rb +102 -0
  70. data/lib/translation_diff/segmenters/simple.rb +122 -0
  71. data/lib/translation_diff/segmenters.rb +4 -0
  72. data/lib/translation_diff/sentence_cache.rb +76 -0
  73. data/lib/translation_diff/stores/active_record.rb +106 -0
  74. data/lib/translation_diff/stores/memory.rb +34 -0
  75. data/lib/translation_diff/stores/redis.rb +49 -0
  76. data/lib/translation_diff/stores.rb +9 -0
  77. data/lib/translation_diff/tasks/translation_diff.rake +21 -0
  78. data/lib/translation_diff/translation/request.rb +8 -0
  79. data/lib/translation_diff/translation/response.rb +35 -0
  80. data/lib/translation_diff/translation/usage.rb +8 -0
  81. data/lib/translation_diff/translator.rb +103 -0
  82. data/lib/translation_diff/version.rb +3 -0
  83. data/lib/translation_diff.rb +93 -0
  84. data/translation_diff.gemspec +56 -0
  85. metadata +243 -0
@@ -0,0 +1,103 @@
1
+ # One translation, coordinated: each step hands its value to the next, and a translation rides home on its own segment.
2
+ class TranslationDiff::Translator
3
+ # Its own class, so rescuing a mis-driven translator cannot also swallow a provider or a cache failure.
4
+ class Error < TranslationDiff::Error; end
5
+
6
+ include TranslationDiff::Instrumentation
7
+ include TranslationDiff::CallPreparation
8
+
9
+ attr_reader :config
10
+
11
+ # `provider:`, `config:` and `assume_supported:` are reserved; every other keyword reaches the provider untouched.
12
+ def initialize(values, from: nil, to: nil, provider: nil, config: nil, assume_supported: false, **options)
13
+ raise ArgumentError, "a translation needs a target language: pass `to:` a language code." if to.nil?
14
+
15
+ @values = values
16
+ @from = from
17
+ @to = to
18
+ @options = options
19
+ @config = config || TranslationDiff.config
20
+ @requested_provider = provider
21
+ @assume_supported = assume_supported
22
+ end
23
+
24
+ # Hands back the caller's value untouched unless something in it was actually translated.
25
+ def call
26
+ document = TranslationDiff::Document.new(TranslationDiff::Leaves.collapse_nils(@values))
27
+ passages = document.strings.map { |string| passage(string) }
28
+ segments = passages.flat_map(&:segments).reject(&:empty?)
29
+ return @values if segments.empty?
30
+ return @values if same_language?(@from)
31
+
32
+ provider = resolve_provider
33
+ from = resolve_source_language(provider, segments)
34
+ return @values if same_language?(from)
35
+
36
+ translated(document, passages, segments, provider, from)
37
+ end
38
+
39
+ private
40
+
41
+ # `from:` given means the whole pair is already known, so it is validated once, up front. `from:` nil means
42
+ # the target alone is checked before a possibly billed #detect runs, and the full pair only once it answers.
43
+ def resolve_source_language(provider, segments)
44
+ return @from.tap { |from| ensure_supported!(provider, from) } unless @from.nil?
45
+
46
+ ensure_supported!(provider, nil)
47
+ source_language(provider, segments).tap { |from| ensure_supported!(provider, from) }
48
+ end
49
+
50
+ # The `translate` event wraps everything a call that reaches a provider does, and nothing an early return does.
51
+ def translated(document, passages, segments, provider, from)
52
+ values = TranslationDiff::Leaves.count(@values)
53
+ characters = segments.sum { |segment| segment.core.size }
54
+ payload = { call_id: call_id, from: from.to_s, to: @to.to_s, provider: provider.cache_key,
55
+ values: values, characters: characters }
56
+ instrument("translate", payload) do
57
+ fill(provider, segments, from)
58
+ rebuild(document, passages)
59
+ end
60
+ end
61
+
62
+ # Opaque and short: a correlation key for this call's own events, generated once, never derived from the text.
63
+ def call_id = @call_id ||= SecureRandom.hex(6)
64
+
65
+ # Resolved at first use, never in the constructor: a value with nothing to translate needs no provider at all.
66
+ def resolve_provider
67
+ TranslationDiff::Providers.resolve(@requested_provider, config).tap { |provider| log("provider #{provider.class}") }
68
+ end
69
+
70
+ # The strings walk and the map walk visit the same leaves in the same order, and the value itself was never touched.
71
+ def rebuild(document, passages)
72
+ rendered = passages.map(&:render)
73
+ document.map { rendered.shift }
74
+ end
75
+
76
+ # Detection is attempted only where it is declared: every provider inherits a #detect that raises.
77
+ def source_language(provider, segments)
78
+ return @from unless @from.nil?
79
+
80
+ ensure_detects_language!(provider, Error, "translating")
81
+ provider.detect(segments.first.core)
82
+ end
83
+
84
+ # The cache answers for what it has, the provider for the rest, and only what came back is written home.
85
+ def fill(provider, segments, from)
86
+ cache = cache_for(provider, from)
87
+ misses = cache.fill(segments)
88
+ id = call_id
89
+ instrument("cache", call_id: id, provider: provider.cache_key,
90
+ hits: segments.size - misses.size, misses: misses.size)
91
+ TranslationDiff::Dispatcher.new(provider: provider, from: from, to: @to, options: @options,
92
+ config: config, call_id: id).dispatch(misses)
93
+ store(cache, misses, provider)
94
+ end
95
+
96
+ # A translation already paid for at the provider must reach the caller even if writing it back never does.
97
+ def store(cache, misses, provider)
98
+ cache.store(misses)
99
+ rescue StandardError => e
100
+ warn_log("cache write failed (#{e.class}), the translation is returned uncached")
101
+ instrument("cache_error", call_id: call_id, provider: provider.cache_key, error: e.class.to_s)
102
+ end
103
+ end
@@ -0,0 +1,3 @@
1
+ module TranslationDiff
2
+ VERSION = "1.0.0".freeze
3
+ end
@@ -0,0 +1,93 @@
1
+ require "cgi/escape"
2
+ require "digest/md5"
3
+ require "digest/sha2"
4
+ require "forwardable"
5
+ require "securerandom"
6
+ require "stringio"
7
+
8
+ require "ox"
9
+
10
+ require "translation_diff/version"
11
+ require "translation_diff/errors"
12
+ require "translation_diff/redaction"
13
+ require "translation_diff/capabilities"
14
+ require "json"
15
+ require "translation_diff/languages"
16
+ require "translation_diff/languages/set"
17
+ require "translation_diff/languages/refresh"
18
+ require "translation_diff/translation/usage"
19
+ require "translation_diff/translation/request"
20
+ require "translation_diff/translation/response"
21
+ require "translation_diff/registry"
22
+ require "translation_diff/document"
23
+ require "translation_diff/leaves"
24
+ require "translation_diff/markup"
25
+ require "translation_diff/segment"
26
+ require "translation_diff/batch"
27
+ require "translation_diff/fragment"
28
+ require "translation_diff/passage"
29
+ require "translation_diff/sentence_cache"
30
+ require "translation_diff/configuration"
31
+ require "translation_diff/configuration/provider_option_owners"
32
+
33
+ require "translation_diff/provider"
34
+ require "translation_diff/http_provider"
35
+ require "translation_diff/providers"
36
+ require "translation_diff/providers/null"
37
+
38
+ require "translation_diff/providers/deepl"
39
+ require "translation_diff/providers/google"
40
+ require "translation_diff/providers/azure"
41
+ require "translation_diff/providers/modernmt"
42
+ require "translation_diff/providers/libretranslate"
43
+ require "translation_diff/providers/amazon"
44
+
45
+ require "translation_diff/segmenters"
46
+ require "translation_diff/segmenters/simple"
47
+ require "translation_diff/segmenters/pragmatic"
48
+ require "translation_diff/stores"
49
+ require "translation_diff/stores/memory"
50
+ require "translation_diff/stores/redis"
51
+ require "translation_diff/active_record"
52
+ require "translation_diff/active_record/support"
53
+ require "translation_diff/stores/active_record"
54
+ require "translation_diff/rate_limiters"
55
+ require "translation_diff/rate_limiters/redis"
56
+ require "translation_diff/rate_limiters/active_record"
57
+ require "translation_diff/instrumentation"
58
+ require "translation_diff/call_preparation"
59
+ require "translation_diff/dispatcher"
60
+ require "translation_diff/translator"
61
+ require "translation_diff/preview"
62
+ require "translation_diff/previewer"
63
+ require "translation_diff/context"
64
+
65
+ # Only when a host application has already loaded Rails -- never required unconditionally, so a non-Rails
66
+ # application never pays for it, and the gem's own suite exercises this same guarded require, not a shortcut.
67
+ require "translation_diff/railtie" if defined?(Rails::Railtie)
68
+
69
+ module TranslationDiff
70
+ class << self
71
+ def config = @config ||= Configuration.new
72
+
73
+ def configure = yield(config)
74
+
75
+ # Without this, one test's configuration leaks into every test that runs after it.
76
+ def reset! = @config = nil
77
+
78
+ # An isolated copy of the configuration with the same entry point, for per-tenant settings.
79
+ def context(&) = Context.new(config.copy.tap(&))
80
+
81
+ # `provider:`, `config:` and `assume_supported:` are reserved; every other keyword is forwarded to the provider.
82
+ def translate(values, from: nil, to: nil, provider: nil, assume_supported: false, **)
83
+ Translator.new(values, from: from, to: to, provider: provider, config: config,
84
+ assume_supported: assume_supported, **).call
85
+ end
86
+
87
+ # Answers what `translate` would do to `values`, without calling a provider or writing anything.
88
+ def preview(values, from: nil, to: nil, provider: nil, assume_supported: false, **)
89
+ Previewer.new(values, from: from, to: to, provider: provider, config: config,
90
+ assume_supported: assume_supported, **).call
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,56 @@
1
+ lib = File.expand_path("lib", __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "translation_diff/version"
4
+
5
+ # rubocop:disable-next Metrics/BlockLength
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "translation_diff"
8
+ spec.version = TranslationDiff::VERSION
9
+ spec.authors = ["Islam Gagiev"]
10
+ spec.email = ["omniacinis@gmail.com"]
11
+
12
+ spec.summary = "A translation cache that sends only new or changed sentences to your provider."
13
+ spec.description = %(
14
+ TranslationDiff extracts translatable text from HTML, splits it into
15
+ sentences, and caches each sentence by content hash. Only the sentences
16
+ missing from the cache are sent to whichever translation provider you plug
17
+ in through a small provider contract, so re-translating a long text after a
18
+ small edit costs the price of the edit, not the whole text.
19
+ )
20
+ spec.homepage = "https://github.com/Halvanhelv/translation_diff"
21
+ spec.license = "MIT"
22
+ spec.required_ruby_version = ">= 3.4"
23
+
24
+ if spec.respond_to?(:metadata)
25
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
26
+ spec.metadata["homepage_uri"] = spec.homepage
27
+ spec.metadata["source_code_uri"] = spec.homepage
28
+ spec.metadata["rubygems_mfa_required"] = "true"
29
+ else
30
+ raise "RubyGems 2.0 or newer is required to protect against " \
31
+ "public gem pushes."
32
+ end
33
+
34
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
35
+ f.match(%r{^(test|spec|features)/})
36
+ end
37
+ spec.bindir = "exe"
38
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
39
+ spec.require_paths = ["lib"]
40
+
41
+ spec.add_development_dependency "minitest", "~> 6.0"
42
+ spec.add_development_dependency "rake", "~> 13.0"
43
+ spec.add_development_dependency "rubocop", "~> 1.90"
44
+ spec.add_development_dependency "simplecov", "~> 1.2"
45
+
46
+ # `ox` walks HTML; `pragmatic_segmenter` backs the default sentence
47
+ # segmenter and has zero dependencies of its own; `faraday` and
48
+ # `faraday-retry` are the HTTP transport every REST provider inherits.
49
+ # Everything else -- the connection pool, and whatever backs the cache and
50
+ # the rate limiter -- is supplied by the application and duck typed, so it
51
+ # stays out of the gemspec.
52
+ spec.add_dependency "faraday", "~> 2.9"
53
+ spec.add_dependency "faraday-retry", "~> 2.2"
54
+ spec.add_dependency "ox", "~> 2.14"
55
+ spec.add_dependency "pragmatic_segmenter", "~> 0.3"
56
+ end
metadata ADDED
@@ -0,0 +1,243 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: translation_diff
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Islam Gagiev
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: minitest
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '6.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '6.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rubocop
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.90'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.90'
54
+ - !ruby/object:Gem::Dependency
55
+ name: simplecov
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.2'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.2'
68
+ - !ruby/object:Gem::Dependency
69
+ name: faraday
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '2.9'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '2.9'
82
+ - !ruby/object:Gem::Dependency
83
+ name: faraday-retry
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.2'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '2.2'
96
+ - !ruby/object:Gem::Dependency
97
+ name: ox
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '2.14'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '2.14'
110
+ - !ruby/object:Gem::Dependency
111
+ name: pragmatic_segmenter
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.3'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '0.3'
124
+ description: "\nTranslationDiff extracts translatable text from HTML, splits it into\nsentences,
125
+ and caches each sentence by content hash. Only the sentences\nmissing from the cache
126
+ are sent to whichever translation provider you plug\nin through a small provider
127
+ contract, so re-translating a long text after a\nsmall edit costs the price of the
128
+ edit, not the whole text.\n "
129
+ email:
130
+ - omniacinis@gmail.com
131
+ executables: []
132
+ extensions: []
133
+ extra_rdoc_files: []
134
+ files:
135
+ - ".github/workflows/ci.yml"
136
+ - ".github/workflows/release.yml"
137
+ - ".gitignore"
138
+ - ".rubocop.yml"
139
+ - ".ruby-version"
140
+ - CHANGELOG.md
141
+ - Gemfile
142
+ - LICENSE.txt
143
+ - README.md
144
+ - Rakefile
145
+ - data/languages/azure.json
146
+ - data/languages/deepl.json
147
+ - data/languages/google.json
148
+ - data/languages/modernmt.json
149
+ - docs/caching.md
150
+ - docs/configuration.md
151
+ - docs/contracts.md
152
+ - docs/development.md
153
+ - docs/errors.md
154
+ - docs/how-it-works.md
155
+ - docs/instrumentation.md
156
+ - docs/languages.md
157
+ - docs/providers.md
158
+ - docs/sql-cache.md
159
+ - lib/generators/translation_diff/install_generator.rb
160
+ - lib/generators/translation_diff/templates/create_translation_diff_tables.rb.erb
161
+ - lib/translation_diff.rb
162
+ - lib/translation_diff/active_record.rb
163
+ - lib/translation_diff/active_record/support.rb
164
+ - lib/translation_diff/batch.rb
165
+ - lib/translation_diff/call_preparation.rb
166
+ - lib/translation_diff/capabilities.rb
167
+ - lib/translation_diff/configuration.rb
168
+ - lib/translation_diff/configuration/cache_guard_options.rb
169
+ - lib/translation_diff/configuration/cache_ttl_option.rb
170
+ - lib/translation_diff/configuration/option_table.rb
171
+ - lib/translation_diff/configuration/provider_option_owners.rb
172
+ - lib/translation_diff/context.rb
173
+ - lib/translation_diff/dispatcher.rb
174
+ - lib/translation_diff/document.rb
175
+ - lib/translation_diff/errors.rb
176
+ - lib/translation_diff/fragment.rb
177
+ - lib/translation_diff/http_provider.rb
178
+ - lib/translation_diff/instrumentation.rb
179
+ - lib/translation_diff/languages.rb
180
+ - lib/translation_diff/languages/refresh.rb
181
+ - lib/translation_diff/languages/set.rb
182
+ - lib/translation_diff/leaves.rb
183
+ - lib/translation_diff/markup.rb
184
+ - lib/translation_diff/passage.rb
185
+ - lib/translation_diff/preview.rb
186
+ - lib/translation_diff/previewer.rb
187
+ - lib/translation_diff/provider.rb
188
+ - lib/translation_diff/providers.rb
189
+ - lib/translation_diff/providers/amazon.rb
190
+ - lib/translation_diff/providers/azure.rb
191
+ - lib/translation_diff/providers/deepl.rb
192
+ - lib/translation_diff/providers/google.rb
193
+ - lib/translation_diff/providers/libretranslate.rb
194
+ - lib/translation_diff/providers/modernmt.rb
195
+ - lib/translation_diff/providers/null.rb
196
+ - lib/translation_diff/railtie.rb
197
+ - lib/translation_diff/rate_limiters.rb
198
+ - lib/translation_diff/rate_limiters/active_record.rb
199
+ - lib/translation_diff/rate_limiters/redis.rb
200
+ - lib/translation_diff/redaction.rb
201
+ - lib/translation_diff/registry.rb
202
+ - lib/translation_diff/segment.rb
203
+ - lib/translation_diff/segmenters.rb
204
+ - lib/translation_diff/segmenters/pragmatic.rb
205
+ - lib/translation_diff/segmenters/simple.rb
206
+ - lib/translation_diff/sentence_cache.rb
207
+ - lib/translation_diff/stores.rb
208
+ - lib/translation_diff/stores/active_record.rb
209
+ - lib/translation_diff/stores/memory.rb
210
+ - lib/translation_diff/stores/redis.rb
211
+ - lib/translation_diff/tasks/translation_diff.rake
212
+ - lib/translation_diff/translation/request.rb
213
+ - lib/translation_diff/translation/response.rb
214
+ - lib/translation_diff/translation/usage.rb
215
+ - lib/translation_diff/translator.rb
216
+ - lib/translation_diff/version.rb
217
+ - translation_diff.gemspec
218
+ homepage: https://github.com/Halvanhelv/translation_diff
219
+ licenses:
220
+ - MIT
221
+ metadata:
222
+ allowed_push_host: https://rubygems.org
223
+ homepage_uri: https://github.com/Halvanhelv/translation_diff
224
+ source_code_uri: https://github.com/Halvanhelv/translation_diff
225
+ rubygems_mfa_required: 'true'
226
+ rdoc_options: []
227
+ require_paths:
228
+ - lib
229
+ required_ruby_version: !ruby/object:Gem::Requirement
230
+ requirements:
231
+ - - ">="
232
+ - !ruby/object:Gem::Version
233
+ version: '3.4'
234
+ required_rubygems_version: !ruby/object:Gem::Requirement
235
+ requirements:
236
+ - - ">="
237
+ - !ruby/object:Gem::Version
238
+ version: '0'
239
+ requirements: []
240
+ rubygems_version: 3.6.9
241
+ specification_version: 4
242
+ summary: A translation cache that sends only new or changed sentences to your provider.
243
+ test_files: []