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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +84 -0
- data/.github/workflows/release.yml +28 -0
- data/.gitignore +11 -0
- data/.rubocop.yml +39 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +723 -0
- data/Gemfile +61 -0
- data/LICENSE.txt +21 -0
- data/README.md +158 -0
- data/Rakefile +41 -0
- data/data/languages/azure.json +285 -0
- data/data/languages/deepl.json +220 -0
- data/data/languages/google.json +399 -0
- data/data/languages/modernmt.json +413 -0
- data/docs/caching.md +185 -0
- data/docs/configuration.md +205 -0
- data/docs/contracts.md +187 -0
- data/docs/development.md +34 -0
- data/docs/errors.md +92 -0
- data/docs/how-it-works.md +145 -0
- data/docs/instrumentation.md +96 -0
- data/docs/languages.md +94 -0
- data/docs/providers.md +379 -0
- data/docs/sql-cache.md +366 -0
- data/lib/generators/translation_diff/install_generator.rb +15 -0
- data/lib/generators/translation_diff/templates/create_translation_diff_tables.rb.erb +24 -0
- data/lib/translation_diff/active_record/support.rb +34 -0
- data/lib/translation_diff/active_record.rb +3 -0
- data/lib/translation_diff/batch.rb +98 -0
- data/lib/translation_diff/call_preparation.rb +47 -0
- data/lib/translation_diff/capabilities.rb +9 -0
- data/lib/translation_diff/configuration/cache_guard_options.rb +39 -0
- data/lib/translation_diff/configuration/cache_ttl_option.rb +30 -0
- data/lib/translation_diff/configuration/option_table.rb +38 -0
- data/lib/translation_diff/configuration/provider_option_owners.rb +40 -0
- data/lib/translation_diff/configuration.rb +135 -0
- data/lib/translation_diff/context.rb +23 -0
- data/lib/translation_diff/dispatcher.rb +57 -0
- data/lib/translation_diff/document.rb +23 -0
- data/lib/translation_diff/errors.rb +44 -0
- data/lib/translation_diff/fragment.rb +33 -0
- data/lib/translation_diff/http_provider.rb +128 -0
- data/lib/translation_diff/instrumentation.rb +25 -0
- data/lib/translation_diff/languages/refresh.rb +70 -0
- data/lib/translation_diff/languages/set.rb +53 -0
- data/lib/translation_diff/languages.rb +30 -0
- data/lib/translation_diff/leaves.rb +22 -0
- data/lib/translation_diff/markup.rb +85 -0
- data/lib/translation_diff/passage.rb +149 -0
- data/lib/translation_diff/preview.rb +3 -0
- data/lib/translation_diff/previewer.rb +78 -0
- data/lib/translation_diff/provider.rb +91 -0
- data/lib/translation_diff/providers/amazon.rb +126 -0
- data/lib/translation_diff/providers/azure.rb +78 -0
- data/lib/translation_diff/providers/deepl.rb +88 -0
- data/lib/translation_diff/providers/google.rb +64 -0
- data/lib/translation_diff/providers/libretranslate.rb +65 -0
- data/lib/translation_diff/providers/modernmt.rb +74 -0
- data/lib/translation_diff/providers/null.rb +20 -0
- data/lib/translation_diff/providers.rb +69 -0
- data/lib/translation_diff/railtie.rb +12 -0
- data/lib/translation_diff/rate_limiters/active_record.rb +92 -0
- data/lib/translation_diff/rate_limiters/redis.rb +59 -0
- data/lib/translation_diff/rate_limiters.rb +9 -0
- data/lib/translation_diff/redaction.rb +45 -0
- data/lib/translation_diff/registry.rb +31 -0
- data/lib/translation_diff/segment.rb +32 -0
- data/lib/translation_diff/segmenters/pragmatic.rb +102 -0
- data/lib/translation_diff/segmenters/simple.rb +122 -0
- data/lib/translation_diff/segmenters.rb +4 -0
- data/lib/translation_diff/sentence_cache.rb +76 -0
- data/lib/translation_diff/stores/active_record.rb +106 -0
- data/lib/translation_diff/stores/memory.rb +34 -0
- data/lib/translation_diff/stores/redis.rb +49 -0
- data/lib/translation_diff/stores.rb +9 -0
- data/lib/translation_diff/tasks/translation_diff.rake +21 -0
- data/lib/translation_diff/translation/request.rb +8 -0
- data/lib/translation_diff/translation/response.rb +35 -0
- data/lib/translation_diff/translation/usage.rb +8 -0
- data/lib/translation_diff/translator.rb +103 -0
- data/lib/translation_diff/version.rb +3 -0
- data/lib/translation_diff.rb +93 -0
- data/translation_diff.gemspec +56 -0
- metadata +243 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
{
|
|
2
|
+
"provider": "modernmt",
|
|
3
|
+
"captured_at": "2026-09-10",
|
|
4
|
+
"endpoint": "https://api.modernmt.com/translate/languages",
|
|
5
|
+
"source": [
|
|
6
|
+
"ace",
|
|
7
|
+
"af",
|
|
8
|
+
"ak",
|
|
9
|
+
"als",
|
|
10
|
+
"am",
|
|
11
|
+
"ar",
|
|
12
|
+
"as",
|
|
13
|
+
"ast",
|
|
14
|
+
"awa",
|
|
15
|
+
"ayr",
|
|
16
|
+
"az",
|
|
17
|
+
"azb",
|
|
18
|
+
"azj",
|
|
19
|
+
"ba",
|
|
20
|
+
"ban",
|
|
21
|
+
"be",
|
|
22
|
+
"bem",
|
|
23
|
+
"bg",
|
|
24
|
+
"bho",
|
|
25
|
+
"bjn",
|
|
26
|
+
"bm",
|
|
27
|
+
"bn",
|
|
28
|
+
"bo",
|
|
29
|
+
"bs",
|
|
30
|
+
"bug",
|
|
31
|
+
"ca",
|
|
32
|
+
"ceb",
|
|
33
|
+
"cjk",
|
|
34
|
+
"ckb",
|
|
35
|
+
"crh",
|
|
36
|
+
"cs",
|
|
37
|
+
"cy",
|
|
38
|
+
"da",
|
|
39
|
+
"de",
|
|
40
|
+
"dik",
|
|
41
|
+
"diq",
|
|
42
|
+
"dyu",
|
|
43
|
+
"dz",
|
|
44
|
+
"ee",
|
|
45
|
+
"el",
|
|
46
|
+
"en",
|
|
47
|
+
"eo",
|
|
48
|
+
"es",
|
|
49
|
+
"es-419",
|
|
50
|
+
"es-es",
|
|
51
|
+
"et",
|
|
52
|
+
"fi",
|
|
53
|
+
"fj",
|
|
54
|
+
"fo",
|
|
55
|
+
"fon",
|
|
56
|
+
"fr",
|
|
57
|
+
"fur",
|
|
58
|
+
"fuv",
|
|
59
|
+
"ga",
|
|
60
|
+
"gaz",
|
|
61
|
+
"gd",
|
|
62
|
+
"gl",
|
|
63
|
+
"gn",
|
|
64
|
+
"gu",
|
|
65
|
+
"ha",
|
|
66
|
+
"he",
|
|
67
|
+
"hi",
|
|
68
|
+
"hne",
|
|
69
|
+
"hr",
|
|
70
|
+
"ht",
|
|
71
|
+
"hu",
|
|
72
|
+
"hy",
|
|
73
|
+
"id",
|
|
74
|
+
"ig",
|
|
75
|
+
"ilo",
|
|
76
|
+
"is",
|
|
77
|
+
"it",
|
|
78
|
+
"ja",
|
|
79
|
+
"jv",
|
|
80
|
+
"ka",
|
|
81
|
+
"kab",
|
|
82
|
+
"kac",
|
|
83
|
+
"kam",
|
|
84
|
+
"kas",
|
|
85
|
+
"kbp",
|
|
86
|
+
"kea",
|
|
87
|
+
"kg",
|
|
88
|
+
"khk",
|
|
89
|
+
"ki",
|
|
90
|
+
"kk",
|
|
91
|
+
"km",
|
|
92
|
+
"kmb",
|
|
93
|
+
"kmr",
|
|
94
|
+
"kn",
|
|
95
|
+
"knc",
|
|
96
|
+
"ko",
|
|
97
|
+
"ks",
|
|
98
|
+
"ky",
|
|
99
|
+
"la",
|
|
100
|
+
"lb",
|
|
101
|
+
"lg",
|
|
102
|
+
"li",
|
|
103
|
+
"lij",
|
|
104
|
+
"lmo",
|
|
105
|
+
"ln",
|
|
106
|
+
"lo",
|
|
107
|
+
"lt",
|
|
108
|
+
"ltg",
|
|
109
|
+
"lua",
|
|
110
|
+
"luo",
|
|
111
|
+
"lus",
|
|
112
|
+
"lv",
|
|
113
|
+
"lvs",
|
|
114
|
+
"mag",
|
|
115
|
+
"mai",
|
|
116
|
+
"mg",
|
|
117
|
+
"mi",
|
|
118
|
+
"min",
|
|
119
|
+
"mk",
|
|
120
|
+
"ml",
|
|
121
|
+
"mn",
|
|
122
|
+
"mni",
|
|
123
|
+
"mos",
|
|
124
|
+
"mr",
|
|
125
|
+
"ms",
|
|
126
|
+
"mt",
|
|
127
|
+
"my",
|
|
128
|
+
"nb",
|
|
129
|
+
"ne",
|
|
130
|
+
"nl",
|
|
131
|
+
"nn",
|
|
132
|
+
"nso",
|
|
133
|
+
"nus",
|
|
134
|
+
"ny",
|
|
135
|
+
"oc",
|
|
136
|
+
"or",
|
|
137
|
+
"pa",
|
|
138
|
+
"pag",
|
|
139
|
+
"pap",
|
|
140
|
+
"pbt",
|
|
141
|
+
"pes",
|
|
142
|
+
"pl",
|
|
143
|
+
"plt",
|
|
144
|
+
"prs",
|
|
145
|
+
"ps",
|
|
146
|
+
"pt",
|
|
147
|
+
"pt-br",
|
|
148
|
+
"pt-pt",
|
|
149
|
+
"quy",
|
|
150
|
+
"rn",
|
|
151
|
+
"ro",
|
|
152
|
+
"ru",
|
|
153
|
+
"rw",
|
|
154
|
+
"sa",
|
|
155
|
+
"sat",
|
|
156
|
+
"sc",
|
|
157
|
+
"scn",
|
|
158
|
+
"sd",
|
|
159
|
+
"sg",
|
|
160
|
+
"shn",
|
|
161
|
+
"si",
|
|
162
|
+
"sk",
|
|
163
|
+
"sl",
|
|
164
|
+
"sm",
|
|
165
|
+
"sn",
|
|
166
|
+
"so",
|
|
167
|
+
"sq",
|
|
168
|
+
"sr",
|
|
169
|
+
"ss",
|
|
170
|
+
"st",
|
|
171
|
+
"su",
|
|
172
|
+
"sv",
|
|
173
|
+
"sw",
|
|
174
|
+
"szl",
|
|
175
|
+
"ta",
|
|
176
|
+
"taq",
|
|
177
|
+
"te",
|
|
178
|
+
"tg",
|
|
179
|
+
"th",
|
|
180
|
+
"ti",
|
|
181
|
+
"tk",
|
|
182
|
+
"tl",
|
|
183
|
+
"tn",
|
|
184
|
+
"tpi",
|
|
185
|
+
"tr",
|
|
186
|
+
"ts",
|
|
187
|
+
"tt",
|
|
188
|
+
"tum",
|
|
189
|
+
"tw",
|
|
190
|
+
"tzm",
|
|
191
|
+
"ug",
|
|
192
|
+
"uk",
|
|
193
|
+
"umb",
|
|
194
|
+
"ur",
|
|
195
|
+
"uzn",
|
|
196
|
+
"vec",
|
|
197
|
+
"vi",
|
|
198
|
+
"war",
|
|
199
|
+
"wo",
|
|
200
|
+
"xh",
|
|
201
|
+
"ydd",
|
|
202
|
+
"yo",
|
|
203
|
+
"zh",
|
|
204
|
+
"zh-cn",
|
|
205
|
+
"zh-tw",
|
|
206
|
+
"zsm",
|
|
207
|
+
"zu"
|
|
208
|
+
],
|
|
209
|
+
"target": [
|
|
210
|
+
"ace",
|
|
211
|
+
"af",
|
|
212
|
+
"ak",
|
|
213
|
+
"als",
|
|
214
|
+
"am",
|
|
215
|
+
"ar",
|
|
216
|
+
"as",
|
|
217
|
+
"ast",
|
|
218
|
+
"awa",
|
|
219
|
+
"ayr",
|
|
220
|
+
"az",
|
|
221
|
+
"azb",
|
|
222
|
+
"azj",
|
|
223
|
+
"ba",
|
|
224
|
+
"ban",
|
|
225
|
+
"be",
|
|
226
|
+
"bem",
|
|
227
|
+
"bg",
|
|
228
|
+
"bho",
|
|
229
|
+
"bjn",
|
|
230
|
+
"bm",
|
|
231
|
+
"bn",
|
|
232
|
+
"bo",
|
|
233
|
+
"bs",
|
|
234
|
+
"bug",
|
|
235
|
+
"ca",
|
|
236
|
+
"ceb",
|
|
237
|
+
"cjk",
|
|
238
|
+
"ckb",
|
|
239
|
+
"crh",
|
|
240
|
+
"cs",
|
|
241
|
+
"cy",
|
|
242
|
+
"da",
|
|
243
|
+
"de",
|
|
244
|
+
"dik",
|
|
245
|
+
"diq",
|
|
246
|
+
"dyu",
|
|
247
|
+
"dz",
|
|
248
|
+
"ee",
|
|
249
|
+
"el",
|
|
250
|
+
"en",
|
|
251
|
+
"eo",
|
|
252
|
+
"es",
|
|
253
|
+
"es-419",
|
|
254
|
+
"es-es",
|
|
255
|
+
"et",
|
|
256
|
+
"fi",
|
|
257
|
+
"fj",
|
|
258
|
+
"fo",
|
|
259
|
+
"fon",
|
|
260
|
+
"fr",
|
|
261
|
+
"fur",
|
|
262
|
+
"fuv",
|
|
263
|
+
"ga",
|
|
264
|
+
"gaz",
|
|
265
|
+
"gd",
|
|
266
|
+
"gl",
|
|
267
|
+
"gn",
|
|
268
|
+
"gu",
|
|
269
|
+
"ha",
|
|
270
|
+
"he",
|
|
271
|
+
"hi",
|
|
272
|
+
"hne",
|
|
273
|
+
"hr",
|
|
274
|
+
"ht",
|
|
275
|
+
"hu",
|
|
276
|
+
"hy",
|
|
277
|
+
"id",
|
|
278
|
+
"ig",
|
|
279
|
+
"ilo",
|
|
280
|
+
"is",
|
|
281
|
+
"it",
|
|
282
|
+
"ja",
|
|
283
|
+
"jv",
|
|
284
|
+
"ka",
|
|
285
|
+
"kab",
|
|
286
|
+
"kac",
|
|
287
|
+
"kam",
|
|
288
|
+
"kas",
|
|
289
|
+
"kbp",
|
|
290
|
+
"kea",
|
|
291
|
+
"kg",
|
|
292
|
+
"khk",
|
|
293
|
+
"ki",
|
|
294
|
+
"kk",
|
|
295
|
+
"km",
|
|
296
|
+
"kmb",
|
|
297
|
+
"kmr",
|
|
298
|
+
"kn",
|
|
299
|
+
"knc",
|
|
300
|
+
"ko",
|
|
301
|
+
"ks",
|
|
302
|
+
"ky",
|
|
303
|
+
"la",
|
|
304
|
+
"lb",
|
|
305
|
+
"lg",
|
|
306
|
+
"li",
|
|
307
|
+
"lij",
|
|
308
|
+
"lmo",
|
|
309
|
+
"ln",
|
|
310
|
+
"lo",
|
|
311
|
+
"lt",
|
|
312
|
+
"ltg",
|
|
313
|
+
"lua",
|
|
314
|
+
"luo",
|
|
315
|
+
"lus",
|
|
316
|
+
"lv",
|
|
317
|
+
"lvs",
|
|
318
|
+
"mag",
|
|
319
|
+
"mai",
|
|
320
|
+
"mg",
|
|
321
|
+
"mi",
|
|
322
|
+
"min",
|
|
323
|
+
"mk",
|
|
324
|
+
"ml",
|
|
325
|
+
"mn",
|
|
326
|
+
"mni",
|
|
327
|
+
"mos",
|
|
328
|
+
"mr",
|
|
329
|
+
"ms",
|
|
330
|
+
"mt",
|
|
331
|
+
"my",
|
|
332
|
+
"nb",
|
|
333
|
+
"ne",
|
|
334
|
+
"nl",
|
|
335
|
+
"nn",
|
|
336
|
+
"nso",
|
|
337
|
+
"nus",
|
|
338
|
+
"ny",
|
|
339
|
+
"oc",
|
|
340
|
+
"or",
|
|
341
|
+
"pa",
|
|
342
|
+
"pag",
|
|
343
|
+
"pap",
|
|
344
|
+
"pbt",
|
|
345
|
+
"pes",
|
|
346
|
+
"pl",
|
|
347
|
+
"plt",
|
|
348
|
+
"prs",
|
|
349
|
+
"ps",
|
|
350
|
+
"pt",
|
|
351
|
+
"pt-br",
|
|
352
|
+
"pt-pt",
|
|
353
|
+
"quy",
|
|
354
|
+
"rn",
|
|
355
|
+
"ro",
|
|
356
|
+
"ru",
|
|
357
|
+
"rw",
|
|
358
|
+
"sa",
|
|
359
|
+
"sat",
|
|
360
|
+
"sc",
|
|
361
|
+
"scn",
|
|
362
|
+
"sd",
|
|
363
|
+
"sg",
|
|
364
|
+
"shn",
|
|
365
|
+
"si",
|
|
366
|
+
"sk",
|
|
367
|
+
"sl",
|
|
368
|
+
"sm",
|
|
369
|
+
"sn",
|
|
370
|
+
"so",
|
|
371
|
+
"sq",
|
|
372
|
+
"sr",
|
|
373
|
+
"ss",
|
|
374
|
+
"st",
|
|
375
|
+
"su",
|
|
376
|
+
"sv",
|
|
377
|
+
"sw",
|
|
378
|
+
"szl",
|
|
379
|
+
"ta",
|
|
380
|
+
"taq",
|
|
381
|
+
"te",
|
|
382
|
+
"tg",
|
|
383
|
+
"th",
|
|
384
|
+
"ti",
|
|
385
|
+
"tk",
|
|
386
|
+
"tl",
|
|
387
|
+
"tn",
|
|
388
|
+
"tpi",
|
|
389
|
+
"tr",
|
|
390
|
+
"ts",
|
|
391
|
+
"tt",
|
|
392
|
+
"tum",
|
|
393
|
+
"tw",
|
|
394
|
+
"tzm",
|
|
395
|
+
"ug",
|
|
396
|
+
"uk",
|
|
397
|
+
"umb",
|
|
398
|
+
"ur",
|
|
399
|
+
"uzn",
|
|
400
|
+
"vec",
|
|
401
|
+
"vi",
|
|
402
|
+
"war",
|
|
403
|
+
"wo",
|
|
404
|
+
"xh",
|
|
405
|
+
"ydd",
|
|
406
|
+
"yo",
|
|
407
|
+
"zh",
|
|
408
|
+
"zh-cn",
|
|
409
|
+
"zh-tw",
|
|
410
|
+
"zsm",
|
|
411
|
+
"zu"
|
|
412
|
+
]
|
|
413
|
+
}
|
data/docs/caching.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Caching
|
|
2
|
+
|
|
3
|
+
## What a cache key is made of
|
|
4
|
+
|
|
5
|
+
One entry per sentence, keyed by the provider's `cache_key`, the lowercased
|
|
6
|
+
source and target language codes, a digest of the provider options that call
|
|
7
|
+
passed (`formality:`, a glossary id, ...), and a digest of the sentence
|
|
8
|
+
itself. `Stores::Redis` prefixes all of that with `cache_namespace`.
|
|
9
|
+
|
|
10
|
+
**No provider's `*_api_base` option is part of the key.** Two configurations
|
|
11
|
+
pointing `deepl_api_base` (or any other provider's `_api_base`) at different
|
|
12
|
+
endpoints share cache entries. For DeepL's own free and paid hosts that is
|
|
13
|
+
correct -- they return the same translations -- but a self-hosted or proxied
|
|
14
|
+
endpoint may not, and it would be served, and would serve, the real
|
|
15
|
+
service's entries. Give such a configuration its own `cache_namespace` (or
|
|
16
|
+
its own Redis database). The key format is left alone here on purpose:
|
|
17
|
+
changing its shape invalidates every entry already cached, everywhere, at
|
|
18
|
+
once.
|
|
19
|
+
|
|
20
|
+
**A cache entry written before a bug fix keeps serving what the bug
|
|
21
|
+
produced.** The key above is built from the source sentence, never from the
|
|
22
|
+
value stored under it, so fixing what a provider's reply decodes to does not
|
|
23
|
+
invalidate what is already cached -- an entry written under the HTML-entity
|
|
24
|
+
double-escaping fixed in the Unreleased CHANGELOG entry is served exactly as
|
|
25
|
+
it was written until it expires. Give the configuration a new
|
|
26
|
+
`cache_namespace`, or let `cache_ttl` lapse, to force every sentence to be
|
|
27
|
+
retranslated under the fix.
|
|
28
|
+
|
|
29
|
+
Both read and write the same cache, keyed per provider, so switching one
|
|
30
|
+
never serves you the other's translations.
|
|
31
|
+
|
|
32
|
+
`TranslationDiff::SentenceCache` is the class that builds the key and does
|
|
33
|
+
both the read and the write.
|
|
34
|
+
|
|
35
|
+
## The options digest is lossy, on purpose
|
|
36
|
+
|
|
37
|
+
The per-call options are canonicalised to a string before they are digested,
|
|
38
|
+
and that canonical form flattens more than it distinguishes. Nesting is not
|
|
39
|
+
recorded, so `["x", ["y", "z"]]` and `["x", "y", "z"]` canonicalise
|
|
40
|
+
identically; neither is emptiness typed, so `tags: []` and `tags: {}` do
|
|
41
|
+
too. Two calls whose options differ only in one of those ways share a cache
|
|
42
|
+
entry.
|
|
43
|
+
|
|
44
|
+
This is a known property, not an oversight. The pipeline this replaced
|
|
45
|
+
collides on exactly the same inputs -- that was checked, not assumed -- so
|
|
46
|
+
reproducing it was the choice that left every warm cache warm. Fixing it
|
|
47
|
+
would give those calls new keys and re-translate everything already cached
|
|
48
|
+
under the old ones, for a distinction no provider option this gem ships
|
|
49
|
+
actually makes. If you pass an option where that distinction matters, give
|
|
50
|
+
the configuration its own `cache_namespace`.
|
|
51
|
+
|
|
52
|
+
A value the canonical form cannot render at all -- anything that is not a
|
|
53
|
+
String, Symbol, Numeric, `true`, `false`, `nil`, or an Array or Hash of
|
|
54
|
+
those -- raises `TranslationDiff::SentenceCache::Error` rather than being
|
|
55
|
+
guessed at. A key that is silently wrong costs you the whole cache and tells
|
|
56
|
+
you nothing.
|
|
57
|
+
|
|
58
|
+
No options at all contributes no field to the key, which is the four-field
|
|
59
|
+
key every already-warm cache is keyed on.
|
|
60
|
+
|
|
61
|
+
## Asking what a call would do, without doing it
|
|
62
|
+
|
|
63
|
+
`TranslationDiff.preview` answers what a `translate` call would send and
|
|
64
|
+
find cached, without calling a provider and without writing anything: how
|
|
65
|
+
many sentences it would send, how many the cache already has, and how many
|
|
66
|
+
characters that is. It reads the same store, through the same
|
|
67
|
+
`SentenceCache`, keyed the same way -- see [What a cache key is made
|
|
68
|
+
of](#what-a-cache-key-is-made-of) above -- so a preview and the call it
|
|
69
|
+
predicts always agree.
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
preview = TranslationDiff.preview(article_body, from: "en", to: "es")
|
|
73
|
+
preview.sendable_sentences # => 1, not yet cached
|
|
74
|
+
preview.cached_sentences # => 4, already cached
|
|
75
|
+
preview.sendable_characters # => 23
|
|
76
|
+
preview.characters # => 412, the total this call would consider
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`sendable_sentences` and `cached_sentences` are the same two counts the
|
|
80
|
+
`cache` event reports as `misses` and `hits`; `characters` is the same total
|
|
81
|
+
the `translate` event reports. A preview and the call it predicts are
|
|
82
|
+
answering the same question through the same numbers, so "this edit will
|
|
83
|
+
send 23 of 412 characters" and what the events for that call later report
|
|
84
|
+
should agree.
|
|
85
|
+
|
|
86
|
+
**`from:` is required wherever there is anything to preview.** `translate`
|
|
87
|
+
can leave `from:` unset and pay for one `#detect` request to find it; a
|
|
88
|
+
preview never calls the provider, so it cannot pay for that request either.
|
|
89
|
+
Passing `to:` alone raises `TranslationDiff::Previewer::Error`, naming the
|
|
90
|
+
provider and telling you to pass `from:` explicitly -- unless the document
|
|
91
|
+
holds nothing translatable, or the source and target already match, in
|
|
92
|
+
which case there is nothing to preview and an empty result comes back
|
|
93
|
+
regardless of `from:`.
|
|
94
|
+
|
|
95
|
+
This is the supported way to ask an editor's question before it becomes a
|
|
96
|
+
bill -- show "this edit will send 1 sentence" before the author saves:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
preview = TranslationDiff.preview(edited_body, from: "en", to: "es")
|
|
100
|
+
"This edit will send #{preview.sendable_sentences} sentence#{'s' unless preview.sendable_sentences == 1}."
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## The cache store contract
|
|
104
|
+
|
|
105
|
+
`config.cache` accepts either a registered name (`:redis`, `:memory`,
|
|
106
|
+
`:active_record`) or an object satisfying this contract directly:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
# Reads several keys at once, returning an array the same length as keys,
|
|
110
|
+
# with nil in a missing key's position.
|
|
111
|
+
def read_multi(keys); end
|
|
112
|
+
|
|
113
|
+
# Writes one key. The second write of the same key replaces the first.
|
|
114
|
+
def write(key, value); end
|
|
115
|
+
|
|
116
|
+
# Writes several pairs at once. Optional -- see "write_multi is optional" below.
|
|
117
|
+
def write_multi(pairs); end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`test/support/cache_store_contract.rb` is the executable form of this
|
|
121
|
+
contract: include `CacheStoreContract` in a test class that defines
|
|
122
|
+
`#store`. It only exercises `read_multi` and `write` -- the two required
|
|
123
|
+
methods -- so a store that implements only those two still passes it.
|
|
124
|
+
`test/support/batching_cache_store_contract.rb` holds the optional half:
|
|
125
|
+
include `BatchingCacheStoreContract` too, alongside `CacheStoreContract`,
|
|
126
|
+
once `#store` also implements `write_multi`.
|
|
127
|
+
|
|
128
|
+
Three stores ship with this gem: `TranslationDiff::Stores::Memory`, the
|
|
129
|
+
default -- a bounded, in-process LRU, not thread-safe by design, evicting by
|
|
130
|
+
`cache_max_size` rather than by time; `TranslationDiff::Stores::Redis`,
|
|
131
|
+
built from `redis_url` when that is set, expiring entries after `cache_ttl`
|
|
132
|
+
and namespacing every key under `cache_namespace`; and
|
|
133
|
+
`TranslationDiff::Stores::ActiveRecord`, opt-in, caching in the
|
|
134
|
+
application's own database -- see [SQL cache](sql-cache.md). Neither `redis`
|
|
135
|
+
nor `connection_pool` nor `redis-namespace` is a dependency of this gem --
|
|
136
|
+
`Stores::Redis` takes anything answering to `#with` the way
|
|
137
|
+
`ConnectionPool` does, and yields anything `Redis::Namespace` accepts.
|
|
138
|
+
|
|
139
|
+
## `write_multi` is optional
|
|
140
|
+
|
|
141
|
+
A store need not implement `write_multi`. `SentenceCache#store` checks: a
|
|
142
|
+
store that answers to it gets one call carrying every translated sentence
|
|
143
|
+
from the batch; a store that does not is called once per sentence through
|
|
144
|
+
`write` instead, exactly as it always was. A custom cache store written
|
|
145
|
+
against the contract before `write_multi` existed keeps working unchanged
|
|
146
|
+
-- that is what "optional" means here.
|
|
147
|
+
|
|
148
|
+
All three shipped stores implement it: `Stores::Memory` loops over the
|
|
149
|
+
pairs (there is no round trip to save in-process); `Stores::Redis`
|
|
150
|
+
pipelines the writes; `Stores::ActiveRecord` upserts the whole batch in
|
|
151
|
+
one statement.
|
|
152
|
+
|
|
153
|
+
### The three write paths fail differently
|
|
154
|
+
|
|
155
|
+
Nobody had written this down before: what a partial failure leaves cached
|
|
156
|
+
depends on which of these shapes wrote it.
|
|
157
|
+
|
|
158
|
+
- **No `write_multi` (the per-key path), and `Stores::Memory`'s loop.**
|
|
159
|
+
Sentences are written one at a time, in order. A failure at sentence N
|
|
160
|
+
leaves 1..N-1 written, N failed, and N+1.. never attempted.
|
|
161
|
+
- **`Stores::Redis#write_multi`.** A Redis pipeline is not a
|
|
162
|
+
transaction: each `SETEX` in it runs independently of the others, so a
|
|
163
|
+
failure in one does not stop its siblings from landing. Which of the
|
|
164
|
+
batch actually landed does not follow the sentence order the way the
|
|
165
|
+
per-key path's does.
|
|
166
|
+
- **`Stores::ActiveRecord#write_multi`.** One `upsert_all` statement for
|
|
167
|
+
the whole batch. It either lands as a whole or it does not -- there is no
|
|
168
|
+
partial batch to reason about.
|
|
169
|
+
|
|
170
|
+
A caller that needs to know which sentences got cached after a failure
|
|
171
|
+
needs to know which of these three shapes wrote them; the answer is not the
|
|
172
|
+
same for all three.
|
|
173
|
+
|
|
174
|
+
None of the three ever reaches the caller as an exception, though. The
|
|
175
|
+
cache is an optimisation on top of a translation that has already been
|
|
176
|
+
paid for at the provider: `Translator#fill` rescues whatever error surfaces
|
|
177
|
+
here, logs it, fires a `cache_error` event (provider and error class only,
|
|
178
|
+
never the text -- see [Instrumentation](instrumentation.md)), and returns
|
|
179
|
+
the translation regardless. This holds for all three shapes and every
|
|
180
|
+
store, not only `Stores::ActiveRecord` -- a `Stores::Memory` bug, a
|
|
181
|
+
dropped Redis connection, a SQL write blocked by a read-only replica (see
|
|
182
|
+
[Rails replica routing](sql-cache.md#rails-replica-routing)) all behave the
|
|
183
|
+
same way from the caller's side. What differs between the three shapes
|
|
184
|
+
above is only what ends up cached, never whether the translation comes
|
|
185
|
+
back.
|