Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | 24x 24x 24x 24x 24x 24x 24x 2x 2x 2x 94x 70x 70x 1x 38x 35x 35x 35x 74x 74x 20x 20x 20x 20x 20x 20x 20x 1x 24x | /**
* Two-layer disk translation cache (SHY-0072).
*
* - SEED (committed, read-only at runtime): src/data/translation-cache-seed.json
* — written only by Claude backfill PRs draining the miss queue.
* - RUNTIME (gitignored, created on first write): data/translation-cache.json
* — populated by translate-on-first-view; wins over seed on collision.
*
* Keys: `sha256(text):target`. Disk, NOT Firestore — free-tier quota is a
* real constraint and this cache is hot. Runtime writes are atomic
* (write `.tmp`, rename). A corrupt layer never crashes boot: that layer
* loads empty with an ERROR log (fail-open to re-translation, fail-silent
* to users).
*/
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const log = require('./log');
const os = require('node:os');
const DEFAULT_SEED = path.join(__dirname, '..', 'data', 'translation-cache-seed.json');
// Under Jest, an un-overridden runtime path falls back to a per-process
// tmpdir so test runs never pollute the repo's data/ dir or each other.
let testDefaultDir = null;
function defaultRuntimePath() {
Eif (process.env.NODE_ENV === 'test') {
Eif (!testDefaultDir) testDefaultDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tcache-default-'));
return path.join(testDefaultDir, 'translation-cache.json');
}
return path.join(__dirname, '..', '..', 'data', 'translation-cache.json');
}
function keyOf(text, target) {
return `${crypto.createHash('sha256').update(text).digest('hex')}:${target}`;
}
function loadLayer(p, label) {
try {
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'));
} catch (err) {
log.error('translate', `translation cache ${label} unreadable — starting that layer empty`, {
path: p,
error: err.message,
});
}
return {};
}
function createTranslationCache({
seedPath = DEFAULT_SEED,
runtimePath = defaultRuntimePath(),
} = {}) {
const runtime = loadLayer(runtimePath, 'runtime');
// Merged view: seed under runtime (runtime wins).
const mem = { ...loadLayer(seedPath, 'seed'), ...runtime };
return {
/** @returns {string|null} cached translation or null on miss */
get(text, target) {
const v = mem[keyOf(text, target)];
return v === undefined ? null : v;
},
/** Persist a translation: memory + atomic runtime-file write. */
set(text, target, translated) {
const k = keyOf(text, target);
mem[k] = translated;
runtime[k] = translated;
fs.mkdirSync(path.dirname(runtimePath), { recursive: true });
const tmp = `${runtimePath}.tmp`;
fs.writeFileSync(tmp, JSON.stringify(runtime));
fs.renameSync(tmp, runtimePath);
},
/** Entry count across both layers (future 50K-entries WARN hook). */
size() {
return Object.keys(mem).length;
},
};
}
module.exports = { createTranslationCache };
|