champollion 0.3.4 → 0.4.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.
- package/README.md +41 -26
- package/bin/cli.js +53 -5
- package/index.js +63 -2
- package/lib/api-key.js +17 -4
- package/lib/autofix.js +83 -36
- package/lib/bridge/method_bridge.py +15 -3
- package/lib/cards/reader.js +34 -0
- package/lib/cards/remote.js +15 -0
- package/lib/cards/search-names.js +178 -0
- package/lib/command-help.js +286 -85
- package/lib/commands/audit.js +10 -3
- package/lib/commands/card.js +583 -226
- package/lib/commands/doctor.js +54 -18
- package/lib/commands/help.js +37 -32
- package/lib/commands/init.js +1689 -87
- package/lib/commands/integrity.js +127 -40
- package/lib/commands/leaderboard.js +187 -67
- package/lib/commands/models.js +9 -2
- package/lib/commands/provenance.js +7 -2
- package/lib/commands/recommend.js +43 -14
- package/lib/commands/register-corpus.js +632 -125
- package/lib/commands/seal-corpus.js +1 -1
- package/lib/commands/status.js +564 -27
- package/lib/commands/submit.js +17 -12
- package/lib/commands/sync.js +31 -7
- package/lib/commands/tm.js +15 -9
- package/lib/commands/verify.js +27 -3
- package/lib/commands/wrap.js +63 -5
- package/lib/commands/xliff.js +135 -64
- package/lib/commercial-eligibility.js +1 -1
- package/lib/config.js +196 -14
- package/lib/content-estimate.js +96 -0
- package/lib/content-refusals.js +270 -0
- package/lib/content-review.js +372 -0
- package/lib/content-sync.js +1127 -344
- package/lib/content.js +94 -7
- package/lib/corpus-registration.mjs +194 -35
- package/lib/cost-label.js +29 -0
- package/lib/cost-report.js +726 -78
- package/lib/diff.js +38 -4
- package/lib/docusaurus-sync.js +965 -253
- package/lib/edit-distance.js +31 -0
- package/lib/fallback.js +964 -0
- package/lib/file-scope.js +106 -0
- package/lib/flatten.js +80 -3
- package/lib/flutter-locales.js +124 -0
- package/lib/format.js +266 -12
- package/lib/hash.js +146 -21
- package/lib/icu-structure.js +929 -0
- package/lib/integrity.js +223 -75
- package/lib/language-pair.js +157 -0
- package/lib/lint.js +78 -16
- package/lib/local-only-marks.js +106 -0
- package/lib/locale-layout.js +1103 -0
- package/lib/locale-state.js +571 -0
- package/lib/methods/anthropic.js +5 -0
- package/lib/methods/apertium.js +6 -3
- package/lib/methods/api.js +138 -25
- package/lib/methods/base.js +17 -0
- package/lib/methods/coaching-data.js +153 -0
- package/lib/methods/content-separator.js +43 -0
- package/lib/methods/deepl.js +1 -1
- package/lib/methods/direct-llm.js +252 -103
- package/lib/methods/external.js +146 -63
- package/lib/methods/gemini.js +1 -0
- package/lib/methods/google-translate.js +1 -0
- package/lib/methods/http-utils.js +41 -0
- package/lib/methods/libretranslate.js +7 -2
- package/lib/methods/llm-coached.js +68 -128
- package/lib/methods/llm.js +80 -31
- package/lib/methods/local.js +93 -10
- package/lib/methods/microsoft-translator.js +1 -2
- package/lib/methods/openai.js +4 -2
- package/lib/methods/openrouter-client.js +20 -19
- package/lib/methods/openrouter-pricing.js +150 -13
- package/lib/methods/prompt-methods.js +20 -0
- package/lib/methods/provider-pricing.js +42 -1
- package/lib/methods/request-capture.js +104 -0
- package/lib/methods/tilde.js +1 -1
- package/lib/methods/translated.js +1 -2
- package/lib/missing-key.js +93 -0
- package/lib/models.js +11 -0
- package/lib/name-rules.js +32 -0
- package/lib/named-keys.js +172 -0
- package/lib/no-translate.js +4 -3
- package/lib/output.js +160 -19
- package/lib/pairs.js +586 -30
- package/lib/placeholders.js +394 -0
- package/lib/plugins.js +8 -0
- package/lib/plural-gap-redo.js +109 -0
- package/lib/plurals.js +323 -0
- package/lib/po.js +1187 -0
- package/lib/public-catalogue.js +74 -0
- package/lib/recommend.js +527 -32
- package/lib/redo.js +95 -0
- package/lib/refusal-category.js +44 -0
- package/lib/registers.js +255 -11
- package/lib/repair-script.js +20 -13
- package/lib/scripts.js +6 -1
- package/lib/seal.mjs +4 -3
- package/lib/sealed-qualifier.mjs +1 -1
- package/lib/segment.js +2 -1
- package/lib/seo.js +19 -9
- package/lib/serve.js +43 -6
- package/lib/shared-output-seed.js +164 -0
- package/lib/source-contexts.js +39 -0
- package/lib/submit.mjs +57 -5
- package/lib/sync.js +2923 -474
- package/lib/terminology.js +13 -4
- package/lib/tm-evict.js +179 -0
- package/lib/tm-seed.js +5 -2
- package/lib/tm.js +818 -36
- package/lib/translate-pair.js +639 -34
- package/lib/translate.js +78 -5
- package/lib/types.js +22 -3
- package/lib/validate.js +880 -17
- package/lib/verify.js +1296 -104
- package/lib/watch.js +32 -13
- package/lib/xliff.js +44 -3
- package/package.json +1 -1
- package/shared/CORPORA-CARDS.md +2 -0
- package/shared/cards-fallback.json +1 -1
- package/shared/curated-orthography-conventions.json +26 -8
- package/shared/gettext-plural-forms.json +45 -0
- package/shared/method-registry.json +2 -0
- package/shared/metric-registry.json +96 -18
- package/shared/schemas/champollion-plugin.schema.json +4 -0
- package/shared/schemas/corpora-card.schema.json +8 -2
- package/shared/schemas/method-index-record.schema.json +67 -0
- package/shared/schemas/method-registry.schema.json +4 -0
- package/shared/schemas/metric-registry.schema.json +55 -1
- package/shared/docent/corpus.json +0 -11739
package/lib/terminology.js
CHANGED
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
* }
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
+
import { output } from './output.js';
|
|
32
|
+
|
|
31
33
|
/**
|
|
32
34
|
* Check whether required dictionary terms were used in translations.
|
|
33
35
|
*
|
|
@@ -99,13 +101,20 @@ function verifyTerminology(translations, sourceFlat, dictionary) {
|
|
|
99
101
|
*/
|
|
100
102
|
function logTermViolations(violations, pairKey) {
|
|
101
103
|
if (violations.length === 0) return;
|
|
104
|
+
if (output.getMode() === 'json') {
|
|
105
|
+
output.warn(`${pairKey}: ${violations.length} dictionary term(s) may not have been applied`, {
|
|
106
|
+
pairKey, termViolations: violations,
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
102
110
|
|
|
103
|
-
|
|
111
|
+
const lines = ['', ` [TERM] ${pairKey}: ${violations.length} dictionary term(s) may not have been applied:`];
|
|
104
112
|
for (const { key, term, expected, got } of violations) {
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
lines.push(` ⚠ "${key}": expected "${expected}" for term "${term}"`);
|
|
114
|
+
lines.push(` → got "${got}"`);
|
|
107
115
|
}
|
|
108
|
-
|
|
116
|
+
lines.push('');
|
|
117
|
+
output.block(lines);
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
export { verifyTerminology, logTermViolations };
|
package/lib/tm-evict.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation Memory eviction for DAMAGED values, and the TM text of a key.
|
|
3
|
+
*
|
|
4
|
+
* THE FINDING (next-intl synthetic user, 2026-10): an ICU plural came back
|
|
5
|
+
* with mangled keywords, passed the gate of its day, was written AND stored
|
|
6
|
+
* in the TM. `verify` caught it — and `sync --force-keys Home.items` then
|
|
7
|
+
* re-served the same broken text from the TM for $0. Only --no-tm fixed it.
|
|
8
|
+
*
|
|
9
|
+
* WHICH ENTRY. Since model carry-over (lib/tm.js), a lookup that misses the
|
|
10
|
+
* current model falls back to the same text under ANOTHER model. Evicting
|
|
11
|
+
* the current method key alone therefore leaves the entry that actually
|
|
12
|
+
* served; the next lookup finds it again. So eviction removes every entry
|
|
13
|
+
* for (source text, locale) — under any method key — whose cached text IS
|
|
14
|
+
* the damaged value.
|
|
15
|
+
*
|
|
16
|
+
* NEVER A HAND-WRITTEN VALUE. Equality with a cached translation is the
|
|
17
|
+
* proof that the pipeline produced the value on disk (the same proof the
|
|
18
|
+
* i18next plural cleanup uses). A value someone wrote by hand has no TM
|
|
19
|
+
* entry equal to it, so nothing is evicted for it — and the file itself is
|
|
20
|
+
* never touched here.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { cacheKey, evictTM, storeTM } from './tm.js';
|
|
24
|
+
|
|
25
|
+
/** gettext's msgctxt/msgid separator (lib/po.js). */
|
|
26
|
+
const CONTEXT_SEPARATOR = '\u0004';
|
|
27
|
+
|
|
28
|
+
/** Separates a borrowed plural form from the text it is cached with (never in UI text). */
|
|
29
|
+
const PLURAL_FORM_MARK = '\u0005';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The text a key's translation is cached under. Normally the source text;
|
|
33
|
+
* for a gettext entry with a context (key `msgctxt\u0004msgid`) the context
|
|
34
|
+
* is folded in — "Open" the verb and "Open" the adjective must not share a
|
|
35
|
+
* cache entry, or the second is served the first's translation.
|
|
36
|
+
*
|
|
37
|
+
* A BORROWED plural form (an i18next key translated from another
|
|
38
|
+
* category's source text — French `count_many` from English `count_other`,
|
|
39
|
+
* lib/plurals.js `borrowed`) folds its form in the same way: the two keys
|
|
40
|
+
* send the same source text, but the model is asked for different forms
|
|
41
|
+
* ("2 recettes" / "1 000 000 de recettes"). Sharing one entry, the second
|
|
42
|
+
* answer replaced the first, and a redo wrote one form into both keys
|
|
43
|
+
* (Round 6, i18next persona). The form it borrows FROM keeps the plain
|
|
44
|
+
* entry, so caches written before this stay valid for it. gettext
|
|
45
|
+
* (msgid_plural) and ARB/ICU plurals are ONE message per key — nothing is
|
|
46
|
+
* borrowed there.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} key
|
|
49
|
+
* @param {string} sourceValue
|
|
50
|
+
* @param {string|null} [pluralForm] - The borrowed form ("many"), or null
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
function tmSourceText(key, sourceValue, pluralForm = null) {
|
|
54
|
+
if (typeof key !== 'string' || typeof sourceValue !== 'string') return sourceValue;
|
|
55
|
+
const i = key.indexOf(CONTEXT_SEPARATOR);
|
|
56
|
+
const base = i < 0 ? sourceValue : `${key.slice(0, i)}${CONTEXT_SEPARATOR}${sourceValue}`;
|
|
57
|
+
return pluralForm ? `${PLURAL_FORM_MARK}${pluralForm}${PLURAL_FORM_MARK}${base}` : base;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* tmSourceText for a TARGET key of an expanded file (lib/plurals.js
|
|
62
|
+
* expandPluralsForLocale): a borrowed plural form gets its own identity.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} key - Target key
|
|
65
|
+
* @param {string} sourceValue - The source text it is translated from
|
|
66
|
+
* @param {object|null} expansion - expectedForTarget(...).expansion
|
|
67
|
+
* @returns {string}
|
|
68
|
+
*/
|
|
69
|
+
function tmTextFor(key, sourceValue, expansion) {
|
|
70
|
+
return tmSourceText(key, sourceValue, expansion?.borrowed?.[key] || null);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The texts whose cache entries PROVE the pipeline produced a value for this
|
|
75
|
+
* key: tmTextFor, and — for a borrowed plural form — the plain text too,
|
|
76
|
+
* which is where every version before this one cached it (shared with the
|
|
77
|
+
* form it borrows from). For proofs only ("is this value Champollion's?"),
|
|
78
|
+
* never for serving: the plain entry is the other form's translation.
|
|
79
|
+
*
|
|
80
|
+
* @returns {string[]}
|
|
81
|
+
*/
|
|
82
|
+
function tmProofTextsFor(key, sourceValue, expansion) {
|
|
83
|
+
const own = tmTextFor(key, sourceValue, expansion);
|
|
84
|
+
const plain = tmSourceText(key, sourceValue);
|
|
85
|
+
return own === plain ? [own] : [own, plain];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* An evictor over one TM object. The per-locale method-key index is built
|
|
90
|
+
* once (a TM can hold 70k entries; a verify run may evict many values).
|
|
91
|
+
*
|
|
92
|
+
* @param {object} tm - TM object (mutated)
|
|
93
|
+
* @returns {{ evictProducing: (sourceText: string, locale: string, value: string,
|
|
94
|
+
* extraMethodKeys?: string[]) => number }}
|
|
95
|
+
*/
|
|
96
|
+
function createTMEvictor(tm) {
|
|
97
|
+
let index = null;
|
|
98
|
+
const methodKeysFor = (locale) => {
|
|
99
|
+
if (!index) {
|
|
100
|
+
index = new Map();
|
|
101
|
+
for (const [k, entry] of Object.entries(tm)) {
|
|
102
|
+
if (k === '_meta' || !entry || typeof entry.l !== 'string' || typeof entry.m !== 'string') continue;
|
|
103
|
+
if (!index.has(entry.l)) index.set(entry.l, new Set());
|
|
104
|
+
index.get(entry.l).add(entry.m);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return index.get(locale) || new Set();
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
/**
|
|
112
|
+
* Evict every entry for (sourceText, locale) whose cached text is `value`.
|
|
113
|
+
*
|
|
114
|
+
* @returns {number} Entries removed
|
|
115
|
+
*/
|
|
116
|
+
evictProducing(sourceText, locale, value, extraMethodKeys = []) {
|
|
117
|
+
if (!tm || typeof sourceText !== 'string' || typeof value !== 'string') return 0;
|
|
118
|
+
let removed = 0;
|
|
119
|
+
for (const m of new Set([...methodKeysFor(locale), ...extraMethodKeys])) {
|
|
120
|
+
const entry = tm[cacheKey(sourceText, locale, m)];
|
|
121
|
+
if (entry && entry.t === value && evictTM(tm, sourceText, locale, m)) removed++;
|
|
122
|
+
}
|
|
123
|
+
return removed;
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Repair a cache written before borrowed plural forms had their own entry.
|
|
130
|
+
*
|
|
131
|
+
* Then, French `count_many` and `count_other` (both translated from the
|
|
132
|
+
* English `count_other` text) shared ONE entry, holding whichever answer was
|
|
133
|
+
* stored last — the `_many` text when the model returned the keys in that
|
|
134
|
+
* order. Served to `count_other` by a redo, it wrote "2 de recettes". The
|
|
135
|
+
* files tell which it holds: when the entry's text is what the borrowed key
|
|
136
|
+
* holds on disk, and the other key holds something else, the entry is the
|
|
137
|
+
* borrowed form's translation. It moves to the borrowed form's own entry
|
|
138
|
+
* (that answer was paid for), and the shared entry is removed, so the form
|
|
139
|
+
* it borrows from is translated again the next time it is queued instead of
|
|
140
|
+
* being served the wrong form. An entry that matches the other key's value
|
|
141
|
+
* (or neither) is left as it is: it stays valid for that key, and the
|
|
142
|
+
* borrowed form has no entry until it is translated once more.
|
|
143
|
+
*
|
|
144
|
+
* @param {object} tm - TM object (mutated)
|
|
145
|
+
* @param {{ expansion: object|null, targetFlat: object, locale: string }} p
|
|
146
|
+
* @returns {string[]} Borrowed keys whose translation was moved
|
|
147
|
+
*/
|
|
148
|
+
function splitSharedPluralEntries(tm, { expansion, targetFlat, locale }) {
|
|
149
|
+
const moved = new Set();
|
|
150
|
+
if (!tm || !expansion?.borrowed || !targetFlat) return [];
|
|
151
|
+
let methodKeys = null;
|
|
152
|
+
for (const [b, form] of Object.entries(expansion.borrowed)) {
|
|
153
|
+
const s = expansion.origin?.[b];
|
|
154
|
+
// The target must have the form it borrows from, un-borrowed.
|
|
155
|
+
if (!s || expansion.origin[s] !== s || expansion.borrowed[s]) continue;
|
|
156
|
+
const src = expansion.flat?.[b];
|
|
157
|
+
const vb = targetFlat[b];
|
|
158
|
+
const vs = targetFlat[s];
|
|
159
|
+
if (typeof src !== 'string' || typeof vb !== 'string' || typeof vs !== 'string' || vb === vs) continue;
|
|
160
|
+
if (!methodKeys) {
|
|
161
|
+
methodKeys = new Set();
|
|
162
|
+
for (const [k, entry] of Object.entries(tm)) {
|
|
163
|
+
if (k !== '_meta' && entry && entry.l === locale && typeof entry.m === 'string') methodKeys.add(entry.m);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const plain = tmSourceText(b, src);
|
|
167
|
+
const own = tmSourceText(b, src, form);
|
|
168
|
+
for (const mk of methodKeys) {
|
|
169
|
+
const entry = tm[cacheKey(plain, locale, mk)];
|
|
170
|
+
if (!entry || entry.t !== vb) continue;
|
|
171
|
+
if (!tm[cacheKey(own, locale, mk)]) storeTM(tm, own, locale, mk, entry.t);
|
|
172
|
+
evictTM(tm, plain, locale, mk);
|
|
173
|
+
moved.add(b);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return [...moved];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export { tmSourceText, tmTextFor, tmProofTextsFor, splitSharedPluralEntries, createTMEvictor, CONTEXT_SEPARATOR, PLURAL_FORM_MARK };
|
package/lib/tm-seed.js
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
import fs from 'node:fs';
|
|
40
40
|
import path from 'node:path';
|
|
41
41
|
import crypto from 'node:crypto';
|
|
42
|
-
import { loadTM, saveTM, lookupTM, storeTM, isTMDirty, tmSize, tmMethodKey } from './tm.js';
|
|
42
|
+
import { loadTM, saveTM, lookupTM, storeTM, isTMDirty, tmSize, tmMethodKey, setModelCarryover } from './tm.js';
|
|
43
43
|
import { readContentManifest } from './content-sync.js';
|
|
44
44
|
import { splitBlocks } from './segment.js';
|
|
45
45
|
import { isPathContained } from './security.js';
|
|
@@ -85,7 +85,7 @@ function translatableBlockTexts(body) {
|
|
|
85
85
|
* @param {string[]|null} [options.translatableFields] - Hugo-lane front matter
|
|
86
86
|
* fields (null → DEFAULT_TRANSLATABLE_FIELDS). The Docusaurus lane always
|
|
87
87
|
* uses the defaults, mirroring runDocusaurusSync.
|
|
88
|
-
* @param {string|null} [options.contentDir] -
|
|
88
|
+
* @param {string|null} [options.contentDir] - Content directory, any folder of Markdown (null = lane off)
|
|
89
89
|
* @param {{ localesDir: string, docsDir: string, blogDir: string }|null} [options.docusaurus]
|
|
90
90
|
* Docusaurus lane directories (null = lane off)
|
|
91
91
|
* @param {boolean} [options.dryRun] - Report what would be seeded, write nothing
|
|
@@ -114,6 +114,9 @@ function seedTMFromExisting(options) {
|
|
|
114
114
|
|
|
115
115
|
const manifest = readContentManifest(cwd);
|
|
116
116
|
const tm = loadTM(cwd);
|
|
117
|
+
// Seeding RE-KEYS translations under the current method key; a carried
|
|
118
|
+
// (other-model) hit must not count as 'already cached' or nothing is re-keyed.
|
|
119
|
+
setModelCarryover(tm, false);
|
|
117
120
|
const tmSizeBefore = tmSize(tm);
|
|
118
121
|
|
|
119
122
|
const pairEntries = [...pairs.entries()]
|